diff --git a/.github/workflows/release.sdk-revdist.yml b/.github/workflows/release.sdk-revdist.yml new file mode 100644 index 000000000..21630bd7c --- /dev/null +++ b/.github/workflows/release.sdk-revdist.yml @@ -0,0 +1,86 @@ +name: Release SDK revdist + +on: + push: + tags: + - "sdk-revdist/v*.*.*" + +permissions: + contents: read + id-token: write + +jobs: + publish-pypi: + runs-on: ubuntu-latest + environment: pypi + steps: + - uses: actions/checkout@v4 + + - name: Extract version from tag + id: version + run: echo "version=${GITHUB_REF_NAME#sdk-revdist/v}" >> "$GITHUB_OUTPUT" + + - name: Verify version matches pyproject.toml + working-directory: sdk/revdist/python + run: | + pkg_version=$(python3 -c " + import tomllib + with open('pyproject.toml', 'rb') as f: + print(tomllib.load(f)['project']['version']) + ") + if [ "$pkg_version" != "${{ steps.version.outputs.version }}" ]; then + echo "Tag version (${{ steps.version.outputs.version }}) does not match pyproject.toml version ($pkg_version)" + exit 1 + fi + + - uses: astral-sh/setup-uv@v4 + + - name: Build package + working-directory: sdk/revdist/python + run: uv build + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: sdk/revdist/python/dist/ + + publish-npm: + runs-on: ubuntu-latest + environment: npm + steps: + - uses: actions/checkout@v4 + + - name: Extract version from tag + id: version + run: echo "version=${GITHUB_REF_NAME#sdk-revdist/v}" >> "$GITHUB_OUTPUT" + + - name: Verify version matches package.json + working-directory: sdk/revdist/typescript + run: | + pkg_version=$(node -p "require('./package.json').version") + if [ "$pkg_version" != "${{ steps.version.outputs.version }}" ]; then + echo "Tag version (${{ steps.version.outputs.version }}) does not match package.json version ($pkg_version)" + exit 1 + fi + + - uses: oven-sh/setup-bun@v2 + + - uses: actions/setup-node@v4 + with: + node-version: "22" + registry-url: "https://registry.npmjs.org" + + - name: Upgrade npm for OIDC trusted publishing + run: npm install -g npm@latest + + - name: Install dependencies + working-directory: sdk/revdist/typescript + run: bun install + + - name: Build + working-directory: sdk/revdist/typescript + run: bun run build + + - name: Publish to npm + working-directory: sdk/revdist/typescript + run: npm publish --access public diff --git a/.github/workflows/release.sdk-serviceability.yml b/.github/workflows/release.sdk-serviceability.yml new file mode 100644 index 000000000..c7852da62 --- /dev/null +++ b/.github/workflows/release.sdk-serviceability.yml @@ -0,0 +1,86 @@ +name: Release SDK serviceability + +on: + push: + tags: + - "sdk-serviceability/v*.*.*" + +permissions: + contents: read + id-token: write + +jobs: + publish-pypi: + runs-on: ubuntu-latest + environment: pypi + steps: + - uses: actions/checkout@v4 + + - name: Extract version from tag + id: version + run: echo "version=${GITHUB_REF_NAME#sdk-serviceability/v}" >> "$GITHUB_OUTPUT" + + - name: Verify version matches pyproject.toml + working-directory: sdk/serviceability/python + run: | + pkg_version=$(python3 -c " + import tomllib + with open('pyproject.toml', 'rb') as f: + print(tomllib.load(f)['project']['version']) + ") + if [ "$pkg_version" != "${{ steps.version.outputs.version }}" ]; then + echo "Tag version (${{ steps.version.outputs.version }}) does not match pyproject.toml version ($pkg_version)" + exit 1 + fi + + - uses: astral-sh/setup-uv@v4 + + - name: Build package + working-directory: sdk/serviceability/python + run: uv build + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: sdk/serviceability/python/dist/ + + publish-npm: + runs-on: ubuntu-latest + environment: npm + steps: + - uses: actions/checkout@v4 + + - name: Extract version from tag + id: version + run: echo "version=${GITHUB_REF_NAME#sdk-serviceability/v}" >> "$GITHUB_OUTPUT" + + - name: Verify version matches package.json + working-directory: sdk/serviceability/typescript + run: | + pkg_version=$(node -p "require('./package.json').version") + if [ "$pkg_version" != "${{ steps.version.outputs.version }}" ]; then + echo "Tag version (${{ steps.version.outputs.version }}) does not match package.json version ($pkg_version)" + exit 1 + fi + + - uses: oven-sh/setup-bun@v2 + + - uses: actions/setup-node@v4 + with: + node-version: "22" + registry-url: "https://registry.npmjs.org" + + - name: Upgrade npm for OIDC trusted publishing + run: npm install -g npm@latest + + - name: Install dependencies + working-directory: sdk/serviceability/typescript + run: bun install + + - name: Build + working-directory: sdk/serviceability/typescript + run: bun run build + + - name: Publish to npm + working-directory: sdk/serviceability/typescript + run: npm publish --access public diff --git a/.github/workflows/release.sdk-telemetry.yml b/.github/workflows/release.sdk-telemetry.yml new file mode 100644 index 000000000..fdfda88f3 --- /dev/null +++ b/.github/workflows/release.sdk-telemetry.yml @@ -0,0 +1,86 @@ +name: Release SDK telemetry + +on: + push: + tags: + - "sdk-telemetry/v*.*.*" + +permissions: + contents: read + id-token: write + +jobs: + publish-pypi: + runs-on: ubuntu-latest + environment: pypi + steps: + - uses: actions/checkout@v4 + + - name: Extract version from tag + id: version + run: echo "version=${GITHUB_REF_NAME#sdk-telemetry/v}" >> "$GITHUB_OUTPUT" + + - name: Verify version matches pyproject.toml + working-directory: sdk/telemetry/python + run: | + pkg_version=$(python3 -c " + import tomllib + with open('pyproject.toml', 'rb') as f: + print(tomllib.load(f)['project']['version']) + ") + if [ "$pkg_version" != "${{ steps.version.outputs.version }}" ]; then + echo "Tag version (${{ steps.version.outputs.version }}) does not match pyproject.toml version ($pkg_version)" + exit 1 + fi + + - uses: astral-sh/setup-uv@v4 + + - name: Build package + working-directory: sdk/telemetry/python + run: uv build + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: sdk/telemetry/python/dist/ + + publish-npm: + runs-on: ubuntu-latest + environment: npm + steps: + - uses: actions/checkout@v4 + + - name: Extract version from tag + id: version + run: echo "version=${GITHUB_REF_NAME#sdk-telemetry/v}" >> "$GITHUB_OUTPUT" + + - name: Verify version matches package.json + working-directory: sdk/telemetry/typescript + run: | + pkg_version=$(node -p "require('./package.json').version") + if [ "$pkg_version" != "${{ steps.version.outputs.version }}" ]; then + echo "Tag version (${{ steps.version.outputs.version }}) does not match package.json version ($pkg_version)" + exit 1 + fi + + - uses: oven-sh/setup-bun@v2 + + - uses: actions/setup-node@v4 + with: + node-version: "22" + registry-url: "https://registry.npmjs.org" + + - name: Upgrade npm for OIDC trusted publishing + run: npm install -g npm@latest + + - name: Install dependencies + working-directory: sdk/telemetry/typescript + run: bun install + + - name: Build + working-directory: sdk/telemetry/typescript + run: bun run build + + - name: Publish to npm + working-directory: sdk/telemetry/typescript + run: npm publish --access public diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml new file mode 100644 index 000000000..1e9f44f46 --- /dev/null +++ b/.github/workflows/sdk.yml @@ -0,0 +1,33 @@ +name: sdk +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + sdk-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + - uses: astral-sh/setup-uv@v6 + - uses: oven-sh/setup-bun@v2 + - run: make sdk-test + + sdk-compat-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + - uses: astral-sh/setup-uv@v6 + - uses: oven-sh/setup-bun@v2 + - run: make sdk-compat-test + env: + REVDIST_COMPAT_TEST: "1" diff --git a/.gitignore b/.gitignore index 0f1b5f8ab..04e1add83 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ test-ledger/ **/bin/* smartcontract/cli/config/* controlplane/funder/cmd/funder/funder +revdist-cli .idea .private @@ -22,6 +23,10 @@ controlplane/funder/cmd/funder/funder .env dist/ +node_modules/ +__pycache__/ +.pytest_cache/ +.venv/ # Local devnet deployment artifacts. dev/.deploy/ diff --git a/CHANGELOG.md b/CHANGELOG.md index e8b108698..dd24189be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,13 @@ All notable changes to this project will be documented in this file. - Fix goroutine leak in TWAMP sender — `cleanUpReceived` goroutines now exit on `Close()` instead of living until process shutdown - CLI - Enhance delete multicast group command to cascade into deleting AP entry (#2754) + - Added activation check for existing users before subscribing to new groups (#2782) +- SDK + - Add read-only Go SDK (`revdist`) for the revenue distribution Solana program, with typed deserialization of all onchain accounts and Rust-generated fixture tests for cross-language compatibility + - Add `revdist-cli` tool for inspecting onchain revenue distribution state + - Add Python and TypeScript SDKs for serviceability, telemetry, and revdist programs with typed deserialization, RPC clients, PDA derivation, enum string types, and cross-language fixture tests + - Add shared `borsh-incremental` library (Go, Python, TypeScript) for cursor-based Borsh deserialization with backward-compatible trailing field defaults + - Add npm and PyPI publish workflows for serviceability and telemetry SDKs - Client - Cache network interface index/name lookups in liveness UDP service to fix high CPU usage caused by per-packet RTM_GETLINK netlink dumps - Add observability to BGP handleUpdate: log withdrawal/NLRI counts per batch and track processing duration via `doublezero_bgp_handle_update_duration_seconds` histogram diff --git a/CLAUDE.md b/CLAUDE.md index 5f5e63948..638e3537e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,6 +14,11 @@ - Summary bullets should be concise, ordered by importance/significance - Focus on "what" and "why", not implementation details - Include a "Testing Verification" section +- Don't mention table-stakes items like "compiles cleanly" in testing verification + +## Terminology + +- Use "onchain" (one word, no hyphen), never "on-chain" ## Rust Development @@ -24,6 +29,13 @@ Always run `make rust-fmt` before committing Rust changes. +## TypeScript SDK Development + +- Use `bun` as the package manager and runtime for TypeScript SDKs +- Build with `bun tsc` (not `npx tsc`) +- Type-check with `bun tsc --noEmit` +- Install dependencies with `bun install` + ## Local Devnet / E2E Environment The local devnet runs in Docker containers with the naming convention `dz-local-*`. diff --git a/Cargo.toml b/Cargo.toml index 5ed10f66b..0e0fb76bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,11 @@ members = [ "smartcontract/programs/common", ] default-members = [] -exclude = [] +exclude = [ + "sdk/revdist/testdata/fixtures/generate-fixtures", + "sdk/serviceability/testdata/fixtures/generate-fixtures", + "sdk/telemetry/testdata/fixtures/generate-fixtures", +] resolver = "2" [workspace.package] diff --git a/Makefile b/Makefile index cfa76d54d..ebad1e880 100644 --- a/Makefile +++ b/Makefile @@ -115,6 +115,87 @@ rust-program-accounts-compat: cargo run -p doublezero -- accounts -et --no-output cargo run -p doublezero -- accounts -em --no-output +# ----------------------------------------------------------------------------- +# SDK targets +# ----------------------------------------------------------------------------- +.PHONY: sdk-test +sdk-test: + go test ./sdk/borsh-incremental/go/... + go test ./sdk/revdist/go/... + go test ./sdk/serviceability/go/... + go test ./sdk/telemetry/go/... + $(MAKE) python-test-borsh-incremental + $(MAKE) python-test-revdist + $(MAKE) python-test-serviceability + $(MAKE) python-test-telemetry + $(MAKE) typescript-test-borsh-incremental + $(MAKE) typescript-test-revdist + $(MAKE) typescript-test-serviceability + $(MAKE) typescript-test-telemetry + +.PHONY: python-test-borsh-incremental +python-test-borsh-incremental: + cd sdk/borsh-incremental/python && uv run pytest + +.PHONY: python-test-revdist +python-test-revdist: + cd sdk/revdist/python && uv run pytest + +.PHONY: python-test-serviceability +python-test-serviceability: + cd sdk/serviceability/python && uv run pytest + +.PHONY: python-test-telemetry +python-test-telemetry: + cd sdk/telemetry/python && uv run pytest + +.PHONY: typescript-test-borsh-incremental +typescript-test-borsh-incremental: + cd sdk/borsh-incremental/typescript && bun install && bun tsc --noEmit && bun test + +.PHONY: typescript-test-revdist +typescript-test-revdist: + cd sdk/revdist/typescript && bun install && bun tsc --noEmit && bun test + +.PHONY: typescript-test-serviceability +typescript-test-serviceability: + cd sdk/serviceability/typescript && bun install && bun tsc --noEmit && bun test + +.PHONY: typescript-test-telemetry +typescript-test-telemetry: + cd sdk/telemetry/typescript && bun install && bun tsc --noEmit && bun test + +.PHONY: sdk-compat-test +sdk-compat-test: + REVDIST_COMPAT_TEST=1 go test -run TestCompat -v ./sdk/revdist/go/... + $(MAKE) python-compat-test-revdist + $(MAKE) typescript-compat-test-revdist + SERVICEABILITY_COMPAT_TEST=1 go test -run TestCompat -v ./sdk/serviceability/go/... + $(MAKE) python-compat-test-serviceability + $(MAKE) typescript-compat-test-serviceability + +.PHONY: python-compat-test-revdist +python-compat-test-revdist: + cd sdk/revdist/python && REVDIST_COMPAT_TEST=1 uv run pytest -k compat -v + +.PHONY: typescript-compat-test-revdist +typescript-compat-test-revdist: + cd sdk/revdist/typescript && bun install && REVDIST_COMPAT_TEST=1 bun test --grep compat + +.PHONY: python-compat-test-serviceability +python-compat-test-serviceability: + cd sdk/serviceability/python && SERVICEABILITY_COMPAT_TEST=1 uv run pytest -k compat -v + +.PHONY: typescript-compat-test-serviceability +typescript-compat-test-serviceability: + cd sdk/serviceability/typescript && bun install && SERVICEABILITY_COMPAT_TEST=1 bun test --grep compat + +.PHONY: generate-fixtures +generate-fixtures: + cd sdk/revdist/testdata/fixtures/generate-fixtures && cargo run + cd sdk/serviceability/testdata/fixtures/generate-fixtures && cargo run + cd sdk/telemetry/testdata/fixtures/generate-fixtures && cargo run + # ----------------------------------------------------------------------------- # E2E targets # ----------------------------------------------------------------------------- diff --git a/config/constants.go b/config/constants.go index ec815c541..e3e99d335 100644 --- a/config/constants.go +++ b/config/constants.go @@ -13,6 +13,8 @@ const ( MainnetTelemetryStateIngestURL = "http://telemetry-state-in.mainnet-beta.doublezero.xyz" MainnetTelemetryGNMITunnelServerAddr = "gnmic-mainnet-beta.doublezero.xyz:443" + MainnetRevenueDistributionProgramID = "dzrevZC94tBLwuHw1dyynZxaXTWyp7yocsinyEVPtt4" + // Testnet constants. TestnetLedgerPublicRPCURL = "https://doublezerolocalnet.rpcpool.com/8a4fd3f4-0977-449f-88c7-63d4b0f10f16" TestnetServiceabilityProgramID = "DZtnuQ839pSaDMFG5q1ad2V95G82S5EC4RrB3Ndw2Heb" diff --git a/config/env.go b/config/env.go index 4804cb22d..2275ebe05 100644 --- a/config/env.go +++ b/config/env.go @@ -20,6 +20,7 @@ type NetworkConfig struct { LedgerPublicRPCURL string ServiceabilityProgramID solana.PublicKey TelemetryProgramID solana.PublicKey + RevenueDistributionProgramID solana.PublicKey InternetLatencyCollectorPK solana.PublicKey DeviceLocalASN uint32 TwoZOracleURL string @@ -45,11 +46,16 @@ func NetworkConfigForEnv(env string) (*NetworkConfig, error) { if err != nil { return nil, fmt.Errorf("failed to parse internet latency collector oracle agent PK: %w", err) } + revenueDistributionProgramID, err := solana.PublicKeyFromBase58(MainnetRevenueDistributionProgramID) + if err != nil { + return nil, fmt.Errorf("failed to parse revenue distribution program ID: %w", err) + } config = &NetworkConfig{ Moniker: EnvMainnetBeta, LedgerPublicRPCURL: MainnetLedgerPublicRPCURL, ServiceabilityProgramID: serviceabilityProgramID, TelemetryProgramID: telemetryProgramID, + RevenueDistributionProgramID: revenueDistributionProgramID, InternetLatencyCollectorPK: internetLatencyCollectorPK, DeviceLocalASN: MainnetDeviceLocalASN, TwoZOracleURL: MainnetTwoZOracleURL, diff --git a/config/env_test.go b/config/env_test.go index 0dee58bcf..3da4fcef9 100644 --- a/config/env_test.go +++ b/config/env_test.go @@ -23,6 +23,7 @@ func TestConfig_NetworkConfigForEnv(t *testing.T) { LedgerPublicRPCURL: config.MainnetLedgerPublicRPCURL, ServiceabilityProgramID: solana.MustPublicKeyFromBase58(config.MainnetServiceabilityProgramID), TelemetryProgramID: solana.MustPublicKeyFromBase58(config.MainnetTelemetryProgramID), + RevenueDistributionProgramID: solana.MustPublicKeyFromBase58(config.MainnetRevenueDistributionProgramID), InternetLatencyCollectorPK: solana.MustPublicKeyFromBase58(config.MainnetInternetLatencyCollectorPK), DeviceLocalASN: config.MainnetDeviceLocalASN, TwoZOracleURL: config.MainnetTwoZOracleURL, @@ -39,6 +40,7 @@ func TestConfig_NetworkConfigForEnv(t *testing.T) { LedgerPublicRPCURL: config.MainnetLedgerPublicRPCURL, ServiceabilityProgramID: solana.MustPublicKeyFromBase58(config.MainnetServiceabilityProgramID), TelemetryProgramID: solana.MustPublicKeyFromBase58(config.MainnetTelemetryProgramID), + RevenueDistributionProgramID: solana.MustPublicKeyFromBase58(config.MainnetRevenueDistributionProgramID), InternetLatencyCollectorPK: solana.MustPublicKeyFromBase58(config.MainnetInternetLatencyCollectorPK), DeviceLocalASN: config.MainnetDeviceLocalASN, TwoZOracleURL: config.MainnetTwoZOracleURL, diff --git a/sdk/README.md b/sdk/README.md new file mode 100644 index 000000000..784d9b3b5 --- /dev/null +++ b/sdk/README.md @@ -0,0 +1,85 @@ +# DoubleZero SDKs + +Read-only SDKs for deserializing DoubleZero onchain program accounts in Go, Python, and TypeScript. + +- **serviceability** -- Serviceability program (contributors, access passes, devices, etc.) +- **telemetry** -- Telemetry program (metrics, reporting) +- **revdist** -- Revenue distribution program (epochs, claim tickets, etc.) +- **borsh-incremental** -- Shared Borsh deserialization library used by all three SDKs, implemented in each language + +## Running Tests + +``` +make sdk-test # Run all SDK tests (unit + fixture) across Go, Python, TypeScript +make sdk-compat-test # Run compat tests against live RPC (requires network) +``` + +Per-SDK test commands: + +| SDK | Go | Python | TypeScript | +|-----|----|----|------------| +| serviceability | `go test ./sdk/serviceability/go/...` | `cd sdk/serviceability/python && uv run pytest` | `cd sdk/serviceability/typescript && bun test` | +| telemetry | `go test ./sdk/telemetry/go/...` | `cd sdk/telemetry/python && uv run pytest` | `cd sdk/telemetry/typescript && bun test` | +| revdist | `go test ./sdk/revdist/go/...` | `cd sdk/revdist/python && uv run pytest` | `cd sdk/revdist/typescript && bun test` | + +## Regenerating Fixtures + +Each SDK has a Rust fixture generator at `testdata/fixtures/generate-fixtures/` that constructs account data using the actual onchain Rust types, Borsh-serializes them to `.bin` files, and writes expected field values to `.json` files. These fixtures are the source of truth -- they guarantee the binary data matches the real onchain serialization format. + +**When to regenerate:** After modifying onchain Rust structs (adding/removing fields, changing enum variants, etc.), you must regenerate fixtures so the SDK tests reflect the updated serialization format. + +**How to regenerate:** + +```bash +# Regenerate fixtures for a specific SDK +cd sdk/serviceability/testdata/fixtures/generate-fixtures && cargo run +cd sdk/telemetry/testdata/fixtures/generate-fixtures && cargo run +cd sdk/revdist/testdata/fixtures/generate-fixtures && cargo run +``` + +After regenerating, update the deserialization logic in Go, Python, and TypeScript to handle any new or changed fields, then run `make sdk-test` to verify consistency across all three languages. + +## Testing Strategy + +### Cross-language fixture tests + +Go, Python, and TypeScript each deserialize the same `.bin` files and verify every field value against the same `.json` expectations. If all three languages pass on the same fixture, they agree on deserialization. + +### Compat tests + +Hit live RPC endpoints to deserialize real onchain accounts, spot-checking key fields. Gated behind environment variables (`SERVICEABILITY_COMPAT_TEST=1`, `REVDIST_COMPAT_TEST=1`) since they require network access. + +### Borsh-incremental unit tests + +Comprehensive tests for the shared deserialization library in all three languages, covering primitive types, variable-length types, optional fields, and error cases. + +### Enum string fixtures + +A shared `enum_strings.json` file is verified by all three languages to ensure status/type enum string representations are consistent. Python's bidirectional check catches new variants added in any language. + +### PDA derivation tests + +Verify that program-derived addresses match known values across all three languages. + +## Adding a New Field or Enum Variant + +1. Update the Rust fixture generator and regenerate fixtures (`cargo run` from the generator directory). +2. Update deserialization logic in Go, Python, and TypeScript. +3. For new enum variants: update `enum_strings.json`, then update the enum definitions in all three languages. +4. Run `make sdk-test` to verify consistency. + +## Directory Structure + +``` +sdk/ +├── borsh-incremental/ # Shared deserialization library (Go, Python, TypeScript) +├── serviceability/ # Serviceability program SDK +│ ├── go/ +│ ├── python/ +│ ├── typescript/ +│ └── testdata/fixtures/ # Rust-generated binary + JSON fixtures +├── telemetry/ # Telemetry program SDK +└── revdist/ # Revenue distribution program SDK +``` + +Each SDK follows the same layout with `go/`, `python/`, `typescript/` subdirectories and a shared `testdata/fixtures/` directory containing the Rust-generated test data. diff --git a/sdk/borsh-incremental/go/reader.go b/sdk/borsh-incremental/go/reader.go new file mode 100644 index 000000000..19ab46999 --- /dev/null +++ b/sdk/borsh-incremental/go/reader.go @@ -0,0 +1,323 @@ +// Package borshincremental provides cursor-based reading of Borsh-serialized +// data with backward-compatible incremental deserialization. +// +// The key invariant: if the reader offset hasn't advanced when a read fails, +// the data is simply missing (trailing field) and the TryRead* methods return +// a default. If the offset advanced but the read still failed, the data is +// corrupt and the Read* methods return an error. +package borshincremental + +import ( + "encoding/binary" + "fmt" + "math" +) + +// Reader provides cursor-based reading of Borsh-serialized binary data. +type Reader struct { + data []byte + offset int +} + +// NewReader creates a new Reader over the given byte slice. +func NewReader(data []byte) *Reader { + return &Reader{data: data, offset: 0} +} + +// Offset returns the current read position. +func (r *Reader) Offset() int { + return r.offset +} + +// Remaining returns the number of unread bytes. +func (r *Reader) Remaining() int { + return len(r.data) - r.offset +} + +// --- Strict read methods (error on insufficient data) --- + +func (r *Reader) ReadU8() (uint8, error) { + if r.offset+1 > len(r.data) { + return 0, fmt.Errorf("borsh: not enough data for u8 at offset %d", r.offset) + } + val := r.data[r.offset] + r.offset++ + return val, nil +} + +func (r *Reader) ReadBool() (bool, error) { + v, err := r.ReadU8() + return v != 0, err +} + +func (r *Reader) ReadU16() (uint16, error) { + if r.offset+2 > len(r.data) { + return 0, fmt.Errorf("borsh: not enough data for u16 at offset %d", r.offset) + } + val := binary.LittleEndian.Uint16(r.data[r.offset:]) + r.offset += 2 + return val, nil +} + +func (r *Reader) ReadU32() (uint32, error) { + if r.offset+4 > len(r.data) { + return 0, fmt.Errorf("borsh: not enough data for u32 at offset %d", r.offset) + } + val := binary.LittleEndian.Uint32(r.data[r.offset:]) + r.offset += 4 + return val, nil +} + +func (r *Reader) ReadU64() (uint64, error) { + if r.offset+8 > len(r.data) { + return 0, fmt.Errorf("borsh: not enough data for u64 at offset %d", r.offset) + } + val := binary.LittleEndian.Uint64(r.data[r.offset:]) + r.offset += 8 + return val, nil +} + +func (r *Reader) ReadU128() ([16]byte, error) { + if r.offset+16 > len(r.data) { + return [16]byte{}, fmt.Errorf("borsh: not enough data for u128 at offset %d", r.offset) + } + var val [16]byte + copy(val[:], r.data[r.offset:r.offset+16]) + r.offset += 16 + return val, nil +} + +func (r *Reader) ReadF64() (float64, error) { + v, err := r.ReadU64() + return math.Float64frombits(v), err +} + +func (r *Reader) ReadPubkey() ([32]byte, error) { + if r.offset+32 > len(r.data) { + return [32]byte{}, fmt.Errorf("borsh: not enough data for pubkey at offset %d", r.offset) + } + val := [32]byte(r.data[r.offset : r.offset+32]) + r.offset += 32 + return val, nil +} + +func (r *Reader) ReadIPv4() ([4]byte, error) { + if r.offset+4 > len(r.data) { + return [4]byte{}, fmt.Errorf("borsh: not enough data for ipv4 at offset %d", r.offset) + } + val := [4]byte(r.data[r.offset : r.offset+4]) + r.offset += 4 + return val, nil +} + +func (r *Reader) ReadNetworkV4() ([5]byte, error) { + if r.offset+5 > len(r.data) { + return [5]byte{}, fmt.Errorf("borsh: not enough data for network_v4 at offset %d", r.offset) + } + val := [5]byte(r.data[r.offset : r.offset+5]) + r.offset += 5 + return val, nil +} + +func (r *Reader) ReadString() (string, error) { + length, err := r.ReadU32() + if err != nil { + return "", err + } + if length == 0 { + return "", nil + } + if r.offset+int(length) > len(r.data) { + return "", fmt.Errorf("borsh: not enough data for string of length %d at offset %d", length, r.offset) + } + val := string(r.data[r.offset : r.offset+int(length)]) + r.offset += int(length) + return val, nil +} + +func (r *Reader) ReadBytes(n int) ([]byte, error) { + if r.offset+n > len(r.data) { + return nil, fmt.Errorf("borsh: not enough data for %d bytes at offset %d", n, r.offset) + } + val := make([]byte, n) + copy(val, r.data[r.offset:r.offset+n]) + r.offset += n + return val, nil +} + +func (r *Reader) ReadPubkeySlice() ([][32]byte, error) { + length, err := r.ReadU32() + if err != nil { + return nil, err + } + if length == 0 { + return nil, nil + } + if int(length)*32 > r.Remaining() { + return nil, fmt.Errorf("borsh: not enough data for %d pubkeys at offset %d", length, r.offset) + } + result := make([][32]byte, length) + for i := range int(length) { + result[i], err = r.ReadPubkey() + if err != nil { + return nil, err + } + } + return result, nil +} + +func (r *Reader) ReadNetworkV4Slice() ([][5]byte, error) { + length, err := r.ReadU32() + if err != nil { + return nil, err + } + if length == 0 { + return nil, nil + } + if int(length)*5 > r.Remaining() { + return nil, fmt.Errorf("borsh: not enough data for %d network_v4 at offset %d", length, r.offset) + } + result := make([][5]byte, length) + for i := range int(length) { + result[i], err = r.ReadNetworkV4() + if err != nil { + return nil, err + } + } + return result, nil +} + +func (r *Reader) ReadU32Slice() ([]uint32, error) { + length, err := r.ReadU32() + if err != nil { + return nil, err + } + if length == 0 { + return nil, nil + } + if int(length)*4 > r.Remaining() { + return nil, fmt.Errorf("borsh: not enough data for %d u32s at offset %d", length, r.offset) + } + result := make([]uint32, length) + for i := range int(length) { + result[i], err = r.ReadU32() + if err != nil { + return nil, err + } + } + return result, nil +} + +// --- Try variants (return default when no bytes available at field boundary) --- + +func (r *Reader) TryReadU8(def uint8) uint8 { + if r.Remaining() < 1 { + return def + } + v, _ := r.ReadU8() + return v +} + +func (r *Reader) TryReadBool(def bool) bool { + if r.Remaining() < 1 { + return def + } + v, _ := r.ReadBool() + return v +} + +func (r *Reader) TryReadU16(def uint16) uint16 { + if r.Remaining() < 2 { + return def + } + v, _ := r.ReadU16() + return v +} + +func (r *Reader) TryReadU32(def uint32) uint32 { + if r.Remaining() < 4 { + return def + } + v, _ := r.ReadU32() + return v +} + +func (r *Reader) TryReadU64(def uint64) uint64 { + if r.Remaining() < 8 { + return def + } + v, _ := r.ReadU64() + return v +} + +func (r *Reader) TryReadU128(def [16]byte) [16]byte { + if r.Remaining() < 16 { + return def + } + v, _ := r.ReadU128() + return v +} + +func (r *Reader) TryReadF64(def float64) float64 { + if r.Remaining() < 8 { + return def + } + v, _ := r.ReadF64() + return v +} + +func (r *Reader) TryReadPubkey(def [32]byte) [32]byte { + if r.Remaining() < 32 { + return def + } + v, _ := r.ReadPubkey() + return v +} + +func (r *Reader) TryReadIPv4(def [4]byte) [4]byte { + if r.Remaining() < 4 { + return def + } + v, _ := r.ReadIPv4() + return v +} + +func (r *Reader) TryReadNetworkV4(def [5]byte) [5]byte { + if r.Remaining() < 5 { + return def + } + v, _ := r.ReadNetworkV4() + return v +} + +func (r *Reader) TryReadString(def string) string { + if r.Remaining() < 4 { + return def + } + v, _ := r.ReadString() + return v +} + +func (r *Reader) TryReadPubkeySlice(def [][32]byte) [][32]byte { + if r.Remaining() < 4 { + return def + } + v, _ := r.ReadPubkeySlice() + return v +} + +func (r *Reader) TryReadNetworkV4Slice(def [][5]byte) [][5]byte { + if r.Remaining() < 4 { + return def + } + v, _ := r.ReadNetworkV4Slice() + return v +} + +func (r *Reader) TryReadU32Slice(def []uint32) []uint32 { + if r.Remaining() < 4 { + return def + } + v, _ := r.ReadU32Slice() + return v +} diff --git a/sdk/borsh-incremental/go/reader_test.go b/sdk/borsh-incremental/go/reader_test.go new file mode 100644 index 000000000..38f4d54c4 --- /dev/null +++ b/sdk/borsh-incremental/go/reader_test.go @@ -0,0 +1,1215 @@ +package borshincremental + +import ( + "encoding/binary" + "math" + "testing" +) + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +func leU16(v uint16) []byte { + b := make([]byte, 2) + binary.LittleEndian.PutUint16(b, v) + return b +} + +func leU32(v uint32) []byte { + b := make([]byte, 4) + binary.LittleEndian.PutUint32(b, v) + return b +} + +func leU64(v uint64) []byte { + b := make([]byte, 8) + binary.LittleEndian.PutUint64(b, v) + return b +} + +func leF64(v float64) []byte { + return leU64(math.Float64bits(v)) +} + +// concat concatenates byte slices. +func concat(parts ...[]byte) []byte { + var out []byte + for _, p := range parts { + out = append(out, p...) + } + return out +} + +// --------------------------------------------------------------------------- +// 1. Happy path for every Read* method +// --------------------------------------------------------------------------- + +func TestReadU8(t *testing.T) { + r := NewReader([]byte{42}) + v, err := r.ReadU8() + if err != nil { + t.Fatal(err) + } + if v != 42 { + t.Fatalf("expected 42, got %d", v) + } + if r.Offset() != 1 { + t.Fatalf("expected offset 1, got %d", r.Offset()) + } + if r.Remaining() != 0 { + t.Fatalf("expected 0 remaining, got %d", r.Remaining()) + } +} + +func TestReadBool(t *testing.T) { + r := NewReader([]byte{1, 0}) + v, err := r.ReadBool() + if err != nil { + t.Fatal(err) + } + if !v { + t.Fatal("expected true") + } + v, err = r.ReadBool() + if err != nil { + t.Fatal(err) + } + if v { + t.Fatal("expected false") + } + if r.Offset() != 2 { + t.Fatalf("expected offset 2, got %d", r.Offset()) + } +} + +func TestReadU16(t *testing.T) { + r := NewReader(leU16(0xABCD)) + v, err := r.ReadU16() + if err != nil { + t.Fatal(err) + } + if v != 0xABCD { + t.Fatalf("expected 0xABCD, got 0x%X", v) + } + if r.Offset() != 2 { + t.Fatalf("expected offset 2, got %d", r.Offset()) + } +} + +func TestReadU32(t *testing.T) { + r := NewReader(leU32(123456)) + v, err := r.ReadU32() + if err != nil { + t.Fatal(err) + } + if v != 123456 { + t.Fatalf("expected 123456, got %d", v) + } + if r.Offset() != 4 { + t.Fatalf("expected offset 4, got %d", r.Offset()) + } +} + +func TestReadU64(t *testing.T) { + r := NewReader(leU64(0xDEADBEEFCAFEBABE)) + v, err := r.ReadU64() + if err != nil { + t.Fatal(err) + } + if v != 0xDEADBEEFCAFEBABE { + t.Fatalf("expected 0xDEADBEEFCAFEBABE, got 0x%X", v) + } + if r.Offset() != 8 { + t.Fatalf("expected offset 8, got %d", r.Offset()) + } +} + +func TestReadU128(t *testing.T) { + var buf [16]byte + for i := range buf { + buf[i] = byte(i) + } + r := NewReader(buf[:]) + v, err := r.ReadU128() + if err != nil { + t.Fatal(err) + } + if v != buf { + t.Fatalf("unexpected u128: %x", v) + } + if r.Offset() != 16 { + t.Fatalf("expected offset 16, got %d", r.Offset()) + } +} + +func TestReadF64(t *testing.T) { + r := NewReader(leF64(3.14)) + v, err := r.ReadF64() + if err != nil { + t.Fatal(err) + } + if v != 3.14 { + t.Fatalf("expected 3.14, got %f", v) + } + if r.Offset() != 8 { + t.Fatalf("expected offset 8, got %d", r.Offset()) + } +} + +func TestReadPubkey(t *testing.T) { + var buf [32]byte + buf[0] = 1 + buf[31] = 0xff + r := NewReader(buf[:]) + v, err := r.ReadPubkey() + if err != nil { + t.Fatal(err) + } + if v[0] != 1 || v[31] != 0xff { + t.Fatalf("unexpected pubkey: %x", v) + } + if r.Offset() != 32 { + t.Fatalf("expected offset 32, got %d", r.Offset()) + } +} + +func TestReadIPv4(t *testing.T) { + buf := [4]byte{192, 168, 1, 1} + r := NewReader(buf[:]) + v, err := r.ReadIPv4() + if err != nil { + t.Fatal(err) + } + if v != buf { + t.Fatalf("unexpected ipv4: %v", v) + } + if r.Offset() != 4 { + t.Fatalf("expected offset 4, got %d", r.Offset()) + } +} + +func TestReadNetworkV4(t *testing.T) { + buf := [5]byte{10, 0, 0, 0, 24} + r := NewReader(buf[:]) + v, err := r.ReadNetworkV4() + if err != nil { + t.Fatal(err) + } + if v != buf { + t.Fatalf("unexpected: %v", v) + } + if r.Offset() != 5 { + t.Fatalf("expected offset 5, got %d", r.Offset()) + } +} + +func TestReadString(t *testing.T) { + s := "hello" + buf := concat(leU32(uint32(len(s))), []byte(s)) + r := NewReader(buf) + v, err := r.ReadString() + if err != nil { + t.Fatal(err) + } + if v != "hello" { + t.Fatalf("expected hello, got %s", v) + } + if r.Offset() != 9 { + t.Fatalf("expected offset 9, got %d", r.Offset()) + } +} + +func TestReadStringEmpty(t *testing.T) { + r := NewReader(leU32(0)) + v, err := r.ReadString() + if err != nil { + t.Fatal(err) + } + if v != "" { + t.Fatalf("expected empty string, got %q", v) + } + if r.Offset() != 4 { + t.Fatalf("expected offset 4, got %d", r.Offset()) + } +} + +func TestReadBytes(t *testing.T) { + data := []byte{1, 2, 3, 4, 5} + r := NewReader(data) + v, err := r.ReadBytes(3) + if err != nil { + t.Fatal(err) + } + if len(v) != 3 || v[0] != 1 || v[1] != 2 || v[2] != 3 { + t.Fatalf("unexpected bytes: %v", v) + } + if r.Offset() != 3 { + t.Fatalf("expected offset 3, got %d", r.Offset()) + } +} + +func TestReadPubkeySlice(t *testing.T) { + pk := [32]byte{1, 2, 3} + buf := concat(leU32(1), pk[:]) + r := NewReader(buf) + v, err := r.ReadPubkeySlice() + if err != nil { + t.Fatal(err) + } + if len(v) != 1 || v[0] != pk { + t.Fatalf("unexpected: %v", v) + } +} + +func TestReadNetworkV4Slice(t *testing.T) { + n1 := [5]byte{10, 0, 0, 0, 8} + n2 := [5]byte{172, 16, 0, 0, 12} + buf := concat(leU32(2), n1[:], n2[:]) + r := NewReader(buf) + v, err := r.ReadNetworkV4Slice() + if err != nil { + t.Fatal(err) + } + if len(v) != 2 || v[0] != n1 || v[1] != n2 { + t.Fatalf("unexpected: %v", v) + } +} + +func TestReadU32Slice(t *testing.T) { + buf := concat(leU32(3), leU32(100), leU32(200), leU32(300)) + r := NewReader(buf) + v, err := r.ReadU32Slice() + if err != nil { + t.Fatal(err) + } + if len(v) != 3 || v[0] != 100 || v[1] != 200 || v[2] != 300 { + t.Fatalf("unexpected: %v", v) + } +} + +// --------------------------------------------------------------------------- +// 2. Error case: empty buffer for every Read* method +// --------------------------------------------------------------------------- + +func TestReadU8Error(t *testing.T) { + r := NewReader([]byte{}) + _, err := r.ReadU8() + if err == nil { + t.Fatal("expected error") + } +} + +func TestReadBoolError(t *testing.T) { + r := NewReader([]byte{}) + _, err := r.ReadBool() + if err == nil { + t.Fatal("expected error") + } +} + +func TestReadU16Error(t *testing.T) { + r := NewReader([]byte{}) + _, err := r.ReadU16() + if err == nil { + t.Fatal("expected error") + } +} + +func TestReadU32Error(t *testing.T) { + r := NewReader([]byte{}) + _, err := r.ReadU32() + if err == nil { + t.Fatal("expected error") + } +} + +func TestReadU64Error(t *testing.T) { + r := NewReader([]byte{}) + _, err := r.ReadU64() + if err == nil { + t.Fatal("expected error") + } +} + +func TestReadU128Error(t *testing.T) { + r := NewReader([]byte{}) + _, err := r.ReadU128() + if err == nil { + t.Fatal("expected error") + } +} + +func TestReadF64Error(t *testing.T) { + r := NewReader([]byte{}) + _, err := r.ReadF64() + if err == nil { + t.Fatal("expected error") + } +} + +func TestReadPubkeyError(t *testing.T) { + r := NewReader([]byte{}) + _, err := r.ReadPubkey() + if err == nil { + t.Fatal("expected error") + } +} + +func TestReadIPv4Error(t *testing.T) { + r := NewReader([]byte{}) + _, err := r.ReadIPv4() + if err == nil { + t.Fatal("expected error") + } +} + +func TestReadNetworkV4Error(t *testing.T) { + r := NewReader([]byte{}) + _, err := r.ReadNetworkV4() + if err == nil { + t.Fatal("expected error") + } +} + +func TestReadStringError(t *testing.T) { + r := NewReader([]byte{}) + _, err := r.ReadString() + if err == nil { + t.Fatal("expected error") + } +} + +func TestReadBytesError(t *testing.T) { + r := NewReader([]byte{}) + _, err := r.ReadBytes(5) + if err == nil { + t.Fatal("expected error") + } +} + +func TestReadPubkeySliceError(t *testing.T) { + r := NewReader([]byte{}) + _, err := r.ReadPubkeySlice() + if err == nil { + t.Fatal("expected error") + } +} + +func TestReadNetworkV4SliceError(t *testing.T) { + r := NewReader([]byte{}) + _, err := r.ReadNetworkV4Slice() + if err == nil { + t.Fatal("expected error") + } +} + +func TestReadU32SliceError(t *testing.T) { + r := NewReader([]byte{}) + _, err := r.ReadU32Slice() + if err == nil { + t.Fatal("expected error") + } +} + +// --------------------------------------------------------------------------- +// 3. Partial data error for multi-byte Read* methods +// --------------------------------------------------------------------------- + +func TestReadU16Partial(t *testing.T) { + r := NewReader([]byte{0x01}) // need 2, have 1 + _, err := r.ReadU16() + if err == nil { + t.Fatal("expected error") + } +} + +func TestReadU32Partial(t *testing.T) { + r := NewReader([]byte{0x01, 0x02}) // need 4, have 2 + _, err := r.ReadU32() + if err == nil { + t.Fatal("expected error") + } +} + +func TestReadU64Partial(t *testing.T) { + r := NewReader([]byte{1, 2, 3, 4}) // need 8, have 4 + _, err := r.ReadU64() + if err == nil { + t.Fatal("expected error") + } +} + +func TestReadU128Partial(t *testing.T) { + r := NewReader(make([]byte, 10)) // need 16, have 10 + _, err := r.ReadU128() + if err == nil { + t.Fatal("expected error") + } +} + +func TestReadF64Partial(t *testing.T) { + r := NewReader([]byte{1, 2, 3}) // need 8, have 3 + _, err := r.ReadF64() + if err == nil { + t.Fatal("expected error") + } +} + +func TestReadPubkeyPartial(t *testing.T) { + r := NewReader(make([]byte, 20)) // need 32, have 20 + _, err := r.ReadPubkey() + if err == nil { + t.Fatal("expected error") + } +} + +func TestReadIPv4Partial(t *testing.T) { + r := NewReader([]byte{10, 0}) // need 4, have 2 + _, err := r.ReadIPv4() + if err == nil { + t.Fatal("expected error") + } +} + +func TestReadNetworkV4Partial(t *testing.T) { + r := NewReader([]byte{10, 0, 0}) // need 5, have 3 + _, err := r.ReadNetworkV4() + if err == nil { + t.Fatal("expected error") + } +} + +func TestReadBytesPartial(t *testing.T) { + r := NewReader([]byte{1, 2}) // request 5, have 2 + _, err := r.ReadBytes(5) + if err == nil { + t.Fatal("expected error") + } +} + +// --------------------------------------------------------------------------- +// 4. TryRead* returns default on empty buffer +// --------------------------------------------------------------------------- + +func TestTryReadU8Default(t *testing.T) { + r := NewReader([]byte{}) + if v := r.TryReadU8(99); v != 99 { + t.Fatalf("expected 99, got %d", v) + } +} + +func TestTryReadBoolDefault(t *testing.T) { + r := NewReader([]byte{}) + if v := r.TryReadBool(true); !v { + t.Fatal("expected true") + } +} + +func TestTryReadU16Default(t *testing.T) { + r := NewReader([]byte{}) + if v := r.TryReadU16(9999); v != 9999 { + t.Fatalf("expected 9999, got %d", v) + } +} + +func TestTryReadU32Default(t *testing.T) { + r := NewReader([]byte{}) + if v := r.TryReadU32(777); v != 777 { + t.Fatalf("expected 777, got %d", v) + } +} + +func TestTryReadU64Default(t *testing.T) { + r := NewReader([]byte{}) + if v := r.TryReadU64(12345); v != 12345 { + t.Fatalf("expected 12345, got %d", v) + } +} + +func TestTryReadU128Default(t *testing.T) { + def := [16]byte{0xff} + r := NewReader([]byte{}) + if v := r.TryReadU128(def); v != def { + t.Fatalf("expected default, got %x", v) + } +} + +func TestTryReadF64Default(t *testing.T) { + r := NewReader([]byte{}) + if v := r.TryReadF64(1.5); v != 1.5 { + t.Fatalf("expected 1.5, got %f", v) + } +} + +func TestTryReadPubkeyDefault(t *testing.T) { + def := [32]byte{0xAA} + r := NewReader([]byte{}) + if v := r.TryReadPubkey(def); v != def { + t.Fatalf("expected default, got %x", v) + } +} + +func TestTryReadIPv4Default(t *testing.T) { + def := [4]byte{127, 0, 0, 1} + r := NewReader([]byte{}) + if v := r.TryReadIPv4(def); v != def { + t.Fatalf("expected default, got %v", v) + } +} + +func TestTryReadNetworkV4Default(t *testing.T) { + def := [5]byte{10, 0, 0, 0, 8} + r := NewReader([]byte{}) + if v := r.TryReadNetworkV4(def); v != def { + t.Fatalf("expected default, got %v", v) + } +} + +func TestTryReadStringDefault(t *testing.T) { + r := NewReader([]byte{}) + if v := r.TryReadString("fallback"); v != "fallback" { + t.Fatalf("expected fallback, got %s", v) + } +} + +func TestTryReadPubkeySliceDefault(t *testing.T) { + r := NewReader([]byte{}) + if v := r.TryReadPubkeySlice(nil); v != nil { + t.Fatalf("expected nil, got %v", v) + } +} + +func TestTryReadNetworkV4SliceDefault(t *testing.T) { + r := NewReader([]byte{}) + if v := r.TryReadNetworkV4Slice(nil); v != nil { + t.Fatalf("expected nil, got %v", v) + } +} + +func TestTryReadU32SliceDefault(t *testing.T) { + r := NewReader([]byte{}) + if v := r.TryReadU32Slice(nil); v != nil { + t.Fatalf("expected nil, got %v", v) + } +} + +// --------------------------------------------------------------------------- +// 5. TryRead* returns actual value when data exists +// --------------------------------------------------------------------------- + +func TestTryReadU8Value(t *testing.T) { + r := NewReader([]byte{42}) + if v := r.TryReadU8(99); v != 42 { + t.Fatalf("expected 42, got %d", v) + } +} + +func TestTryReadBoolValue(t *testing.T) { + r := NewReader([]byte{1}) + if v := r.TryReadBool(false); !v { + t.Fatal("expected true") + } +} + +func TestTryReadU16Value(t *testing.T) { + r := NewReader(leU16(500)) + if v := r.TryReadU16(0); v != 500 { + t.Fatalf("expected 500, got %d", v) + } +} + +func TestTryReadU32Value(t *testing.T) { + r := NewReader(leU32(100000)) + if v := r.TryReadU32(0); v != 100000 { + t.Fatalf("expected 100000, got %d", v) + } +} + +func TestTryReadU64Value(t *testing.T) { + r := NewReader(leU64(9999999999)) + if v := r.TryReadU64(0); v != 9999999999 { + t.Fatalf("expected 9999999999, got %d", v) + } +} + +func TestTryReadU128Value(t *testing.T) { + var data [16]byte + data[0] = 0x42 + r := NewReader(data[:]) + if v := r.TryReadU128([16]byte{}); v != data { + t.Fatalf("expected %x, got %x", data, v) + } +} + +func TestTryReadF64Value(t *testing.T) { + r := NewReader(leF64(2.718)) + if v := r.TryReadF64(0); v != 2.718 { + t.Fatalf("expected 2.718, got %f", v) + } +} + +func TestTryReadPubkeyValue(t *testing.T) { + var pk [32]byte + pk[0] = 0xBB + r := NewReader(pk[:]) + if v := r.TryReadPubkey([32]byte{}); v != pk { + t.Fatalf("expected %x, got %x", pk, v) + } +} + +func TestTryReadIPv4Value(t *testing.T) { + data := [4]byte{10, 20, 30, 40} + r := NewReader(data[:]) + if v := r.TryReadIPv4([4]byte{}); v != data { + t.Fatalf("expected %v, got %v", data, v) + } +} + +func TestTryReadNetworkV4Value(t *testing.T) { + data := [5]byte{172, 16, 0, 0, 16} + r := NewReader(data[:]) + if v := r.TryReadNetworkV4([5]byte{}); v != data { + t.Fatalf("expected %v, got %v", data, v) + } +} + +func TestTryReadStringValue(t *testing.T) { + s := "world" + buf := concat(leU32(uint32(len(s))), []byte(s)) + r := NewReader(buf) + if v := r.TryReadString("def"); v != "world" { + t.Fatalf("expected world, got %s", v) + } +} + +func TestTryReadPubkeySliceValue(t *testing.T) { + pk := [32]byte{0xAA} + buf := concat(leU32(1), pk[:]) + r := NewReader(buf) + v := r.TryReadPubkeySlice(nil) + if len(v) != 1 || v[0] != pk { + t.Fatalf("unexpected: %v", v) + } +} + +func TestTryReadNetworkV4SliceValue(t *testing.T) { + n := [5]byte{10, 0, 0, 0, 24} + buf := concat(leU32(1), n[:]) + r := NewReader(buf) + v := r.TryReadNetworkV4Slice(nil) + if len(v) != 1 || v[0] != n { + t.Fatalf("unexpected: %v", v) + } +} + +func TestTryReadU32SliceValue(t *testing.T) { + buf := concat(leU32(2), leU32(11), leU32(22)) + r := NewReader(buf) + v := r.TryReadU32Slice(nil) + if len(v) != 2 || v[0] != 11 || v[1] != 22 { + t.Fatalf("unexpected: %v", v) + } +} + +// --------------------------------------------------------------------------- +// 6. TryRead* with partial data returns default +// --------------------------------------------------------------------------- + +func TestTryReadU16Partial(t *testing.T) { + r := NewReader([]byte{0x01}) // need 2, have 1 + if v := r.TryReadU16(5555); v != 5555 { + t.Fatalf("expected 5555, got %d", v) + } +} + +func TestTryReadU32Partial(t *testing.T) { + r := NewReader([]byte{1, 2, 3}) // need 4, have 3 + if v := r.TryReadU32(777); v != 777 { + t.Fatalf("expected 777, got %d", v) + } +} + +func TestTryReadU64Partial(t *testing.T) { + r := NewReader(make([]byte, 5)) // need 8, have 5 + if v := r.TryReadU64(42); v != 42 { + t.Fatalf("expected 42, got %d", v) + } +} + +func TestTryReadU128Partial(t *testing.T) { + def := [16]byte{0xFF} + r := NewReader(make([]byte, 10)) // need 16, have 10 + if v := r.TryReadU128(def); v != def { + t.Fatalf("expected default, got %x", v) + } +} + +func TestTryReadF64Partial(t *testing.T) { + r := NewReader(make([]byte, 4)) // need 8, have 4 + if v := r.TryReadF64(1.23); v != 1.23 { + t.Fatalf("expected 1.23, got %f", v) + } +} + +func TestTryReadPubkeyPartial(t *testing.T) { + def := [32]byte{0xDD} + r := NewReader(make([]byte, 20)) // need 32, have 20 + if v := r.TryReadPubkey(def); v != def { + t.Fatalf("expected default, got %x", v) + } +} + +func TestTryReadIPv4Partial(t *testing.T) { + def := [4]byte{127, 0, 0, 1} + r := NewReader([]byte{10, 20}) // need 4, have 2 + if v := r.TryReadIPv4(def); v != def { + t.Fatalf("expected default, got %v", v) + } +} + +func TestTryReadNetworkV4Partial(t *testing.T) { + def := [5]byte{1, 2, 3, 4, 5} + r := NewReader([]byte{10, 0, 0}) // need 5, have 3 + if v := r.TryReadNetworkV4(def); v != def { + t.Fatalf("expected default, got %v", v) + } +} + +func TestTryReadStringPartialLength(t *testing.T) { + r := NewReader([]byte{0x05, 0x00}) // need 4 for length prefix, have 2 + if v := r.TryReadString("def"); v != "def" { + t.Fatalf("expected def, got %s", v) + } +} + +func TestTryReadPubkeySlicePartial(t *testing.T) { + r := NewReader([]byte{1, 2}) // need 4 for length prefix, have 2 + if v := r.TryReadPubkeySlice(nil); v != nil { + t.Fatalf("expected nil, got %v", v) + } +} + +func TestTryReadNetworkV4SlicePartial(t *testing.T) { + r := NewReader([]byte{1}) // need 4 for length prefix, have 1 + if v := r.TryReadNetworkV4Slice(nil); v != nil { + t.Fatalf("expected nil, got %v", v) + } +} + +func TestTryReadU32SlicePartial(t *testing.T) { + r := NewReader([]byte{1, 2, 3}) // need 4 for length prefix, have 3 + if v := r.TryReadU32Slice(nil); v != nil { + t.Fatalf("expected nil, got %v", v) + } +} + +// --------------------------------------------------------------------------- +// 7. Sequential reads +// --------------------------------------------------------------------------- + +func TestSequentialReads(t *testing.T) { + // u8(1) + u16(1000) + u32(50000) + u64(big) + bool(true) + buf := concat( + []byte{0x01}, + leU16(1000), + leU32(50000), + leU64(0x0102030405060708), + []byte{0x01}, + ) + r := NewReader(buf) + + v1, err := r.ReadU8() + if err != nil { + t.Fatal(err) + } + if v1 != 1 { + t.Fatalf("expected 1, got %d", v1) + } + if r.Offset() != 1 { + t.Fatalf("expected offset 1, got %d", r.Offset()) + } + + v2, err := r.ReadU16() + if err != nil { + t.Fatal(err) + } + if v2 != 1000 { + t.Fatalf("expected 1000, got %d", v2) + } + if r.Offset() != 3 { + t.Fatalf("expected offset 3, got %d", r.Offset()) + } + + v3, err := r.ReadU32() + if err != nil { + t.Fatal(err) + } + if v3 != 50000 { + t.Fatalf("expected 50000, got %d", v3) + } + if r.Offset() != 7 { + t.Fatalf("expected offset 7, got %d", r.Offset()) + } + + v4, err := r.ReadU64() + if err != nil { + t.Fatal(err) + } + if v4 != 0x0102030405060708 { + t.Fatalf("expected 0x0102030405060708, got 0x%X", v4) + } + if r.Offset() != 15 { + t.Fatalf("expected offset 15, got %d", r.Offset()) + } + + v5, err := r.ReadBool() + if err != nil { + t.Fatal(err) + } + if !v5 { + t.Fatal("expected true") + } + if r.Offset() != 16 { + t.Fatalf("expected offset 16, got %d", r.Offset()) + } + + if r.Remaining() != 0 { + t.Fatalf("expected 0 remaining, got %d", r.Remaining()) + } +} + +// --------------------------------------------------------------------------- +// 8. Trailing fields scenario +// --------------------------------------------------------------------------- + +func TestTrailingFieldsDefault(t *testing.T) { + // Struct: u8, u32 required; then optional trailing u16, string, pubkey. + buf := make([]byte, 5) + buf[0] = 1 + binary.LittleEndian.PutUint32(buf[1:], 42) + + r := NewReader(buf) + v1, err := r.ReadU8() + if err != nil { + t.Fatal(err) + } + if v1 != 1 { + t.Fatalf("expected 1, got %d", v1) + } + + v2, err := r.ReadU32() + if err != nil { + t.Fatal(err) + } + if v2 != 42 { + t.Fatalf("expected 42, got %d", v2) + } + + // All trailing optional fields missing. + if v := r.TryReadU16(9999); v != 9999 { + t.Fatalf("expected 9999, got %d", v) + } + if v := r.TryReadString("none"); v != "none" { + t.Fatalf("expected none, got %s", v) + } + def := [32]byte{} + if v := r.TryReadPubkey(def); v != def { + t.Fatalf("expected default pubkey, got %x", v) + } +} + +func TestTrailingFieldsSomePresent(t *testing.T) { + // Struct: u8, u32 required; then optional trailing u16 present, string missing. + buf := concat([]byte{1}, leU32(42), leU16(7777)) + + r := NewReader(buf) + _, _ = r.ReadU8() + _, _ = r.ReadU32() + + if v := r.TryReadU16(0); v != 7777 { + t.Fatalf("expected 7777, got %d", v) + } + // Next trailing field missing. + if v := r.TryReadString("missing"); v != "missing" { + t.Fatalf("expected missing, got %s", v) + } +} + +// --------------------------------------------------------------------------- +// 9. Vec/Slice methods: empty, single, multiple, truncated +// --------------------------------------------------------------------------- + +func TestReadPubkeySliceEmpty(t *testing.T) { + r := NewReader(leU32(0)) + v, err := r.ReadPubkeySlice() + if err != nil { + t.Fatal(err) + } + if v != nil { + t.Fatalf("expected nil for empty vec, got %v", v) + } +} + +func TestReadPubkeySliceMultiple(t *testing.T) { + pk1 := [32]byte{1} + pk2 := [32]byte{2} + pk3 := [32]byte{3} + buf := concat(leU32(3), pk1[:], pk2[:], pk3[:]) + r := NewReader(buf) + v, err := r.ReadPubkeySlice() + if err != nil { + t.Fatal(err) + } + if len(v) != 3 || v[0] != pk1 || v[1] != pk2 || v[2] != pk3 { + t.Fatalf("unexpected: %v", v) + } +} + +func TestReadPubkeySliceTruncatedLength(t *testing.T) { + r := NewReader([]byte{0x01, 0x00}) // incomplete u32 length prefix + _, err := r.ReadPubkeySlice() + if err == nil { + t.Fatal("expected error for truncated length") + } +} + +func TestReadPubkeySliceTruncatedElements(t *testing.T) { + // length=2 but only 1 pubkey worth of data + buf := concat(leU32(2), make([]byte, 32)) + r := NewReader(buf) + _, err := r.ReadPubkeySlice() + if err == nil { + t.Fatal("expected error for truncated elements") + } +} + +func TestReadNetworkV4SliceEmpty(t *testing.T) { + r := NewReader(leU32(0)) + v, err := r.ReadNetworkV4Slice() + if err != nil { + t.Fatal(err) + } + if v != nil { + t.Fatalf("expected nil, got %v", v) + } +} + +func TestReadNetworkV4SliceTruncatedElements(t *testing.T) { + // length=3 but only 2 elements worth of data + buf := concat(leU32(3), make([]byte, 10)) + r := NewReader(buf) + _, err := r.ReadNetworkV4Slice() + if err == nil { + t.Fatal("expected error for truncated elements") + } +} + +func TestReadU32SliceEmpty(t *testing.T) { + r := NewReader(leU32(0)) + v, err := r.ReadU32Slice() + if err != nil { + t.Fatal(err) + } + if v != nil { + t.Fatalf("expected nil, got %v", v) + } +} + +func TestReadU32SliceTruncatedLength(t *testing.T) { + r := NewReader([]byte{0x02, 0x00}) // incomplete u32 length prefix + _, err := r.ReadU32Slice() + if err == nil { + t.Fatal("expected error for truncated length") + } +} + +func TestReadU32SliceTruncatedElements(t *testing.T) { + // length=3 but only 2 elements worth of data + buf := concat(leU32(3), leU32(1), leU32(2)) + r := NewReader(buf) + _, err := r.ReadU32Slice() + if err == nil { + t.Fatal("expected error for truncated elements") + } +} + +// --------------------------------------------------------------------------- +// 10. String edge cases +// --------------------------------------------------------------------------- + +func TestReadStringTruncated(t *testing.T) { + // length says 10 but only 5 bytes of content + buf := concat(leU32(10), []byte("hello")) + r := NewReader(buf) + _, err := r.ReadString() + if err == nil { + t.Fatal("expected error for truncated string") + } +} + +func TestReadStringTruncatedAdvancesOffset(t *testing.T) { + // After a truncated string, offset should have advanced past the length prefix. + buf := concat(leU32(10), []byte("hi")) + r := NewReader(buf) + _, err := r.ReadString() + if err == nil { + t.Fatal("expected error") + } + // ReadU32 for length succeeded, so offset moved by 4. + if r.Offset() != 4 { + t.Fatalf("expected offset 4, got %d", r.Offset()) + } +} + +// --------------------------------------------------------------------------- +// 11. U128 byte order (little-endian) +// --------------------------------------------------------------------------- + +func TestReadU128ByteOrder(t *testing.T) { + // Store value 1 as u128 little-endian: byte[0]=1, rest=0. + var buf [16]byte + buf[0] = 1 + r := NewReader(buf[:]) + v, err := r.ReadU128() + if err != nil { + t.Fatal(err) + } + if v[0] != 1 { + t.Fatalf("expected LSB=1, got %d", v[0]) + } + for i := 1; i < 16; i++ { + if v[i] != 0 { + t.Fatalf("expected byte[%d]=0, got %d", i, v[i]) + } + } + + // Store 0x0102...10 and verify byte order is preserved. + for i := range buf { + buf[i] = byte(i + 1) + } + r = NewReader(buf[:]) + v, err = r.ReadU128() + if err != nil { + t.Fatal(err) + } + for i := range 16 { + if v[i] != byte(i+1) { + t.Fatalf("byte[%d]: expected %d, got %d", i, i+1, v[i]) + } + } +} + +// --------------------------------------------------------------------------- +// 12. F64 known float value +// --------------------------------------------------------------------------- + +func TestReadF64KnownValue(t *testing.T) { + // math.Pi + r := NewReader(leF64(math.Pi)) + v, err := r.ReadF64() + if err != nil { + t.Fatal(err) + } + if v != math.Pi { + t.Fatalf("expected Pi (%v), got %v", math.Pi, v) + } +} + +func TestReadF64NegativeZero(t *testing.T) { + nz := math.Copysign(0, -1) + r := NewReader(leF64(nz)) + v, err := r.ReadF64() + if err != nil { + t.Fatal(err) + } + if math.Float64bits(v) != math.Float64bits(nz) { + t.Fatalf("expected negative zero, got %v", v) + } +} + +func TestReadF64Inf(t *testing.T) { + r := NewReader(leF64(math.Inf(1))) + v, err := r.ReadF64() + if err != nil { + t.Fatal(err) + } + if !math.IsInf(v, 1) { + t.Fatalf("expected +Inf, got %v", v) + } +} + +// --------------------------------------------------------------------------- +// 13. Offset() and Remaining() correctness +// --------------------------------------------------------------------------- + +func TestOffsetAndRemaining(t *testing.T) { + buf := make([]byte, 20) + r := NewReader(buf) + + if r.Offset() != 0 { + t.Fatalf("expected offset 0, got %d", r.Offset()) + } + if r.Remaining() != 20 { + t.Fatalf("expected 20 remaining, got %d", r.Remaining()) + } + + _, _ = r.ReadU8() + if r.Offset() != 1 || r.Remaining() != 19 { + t.Fatalf("after ReadU8: offset=%d remaining=%d", r.Offset(), r.Remaining()) + } + + _, _ = r.ReadU32() + if r.Offset() != 5 || r.Remaining() != 15 { + t.Fatalf("after ReadU32: offset=%d remaining=%d", r.Offset(), r.Remaining()) + } + + _, _ = r.ReadU64() + if r.Offset() != 13 || r.Remaining() != 7 { + t.Fatalf("after ReadU64: offset=%d remaining=%d", r.Offset(), r.Remaining()) + } + + _, _ = r.ReadBytes(5) + if r.Offset() != 18 || r.Remaining() != 2 { + t.Fatalf("after ReadBytes: offset=%d remaining=%d", r.Offset(), r.Remaining()) + } + + _, _ = r.ReadU16() + if r.Offset() != 20 || r.Remaining() != 0 { + t.Fatalf("after ReadU16: offset=%d remaining=%d", r.Offset(), r.Remaining()) + } +} + +func TestOffsetUnchangedOnError(t *testing.T) { + r := NewReader([]byte{0x01}) + _, err := r.ReadU32() // fails, needs 4 bytes + if err == nil { + t.Fatal("expected error") + } + if r.Offset() != 0 { + t.Fatalf("expected offset unchanged at 0, got %d", r.Offset()) + } + if r.Remaining() != 1 { + t.Fatalf("expected 1 remaining, got %d", r.Remaining()) + } +} + +func TestTryReadDoesNotAdvanceOnDefault(t *testing.T) { + r := NewReader([]byte{0x01}) + // TryReadU32 needs 4 bytes, only 1 available, returns default. + v := r.TryReadU32(999) + if v != 999 { + t.Fatalf("expected 999, got %d", v) + } + if r.Offset() != 0 { + t.Fatalf("expected offset 0, got %d", r.Offset()) + } + // The byte is still available for a subsequent ReadU8. + v2, err := r.ReadU8() + if err != nil { + t.Fatal(err) + } + if v2 != 1 { + t.Fatalf("expected 1, got %d", v2) + } +} diff --git a/sdk/borsh-incremental/python/borsh_incremental/__init__.py b/sdk/borsh-incremental/python/borsh_incremental/__init__.py new file mode 100644 index 000000000..e66fea15a --- /dev/null +++ b/sdk/borsh-incremental/python/borsh_incremental/__init__.py @@ -0,0 +1,3 @@ +from borsh_incremental.reader import IncrementalReader + +__all__ = ["IncrementalReader"] diff --git a/sdk/borsh-incremental/python/borsh_incremental/py.typed b/sdk/borsh-incremental/python/borsh_incremental/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/sdk/borsh-incremental/python/borsh_incremental/reader.py b/sdk/borsh-incremental/python/borsh_incremental/reader.py new file mode 100644 index 000000000..527d5b6a1 --- /dev/null +++ b/sdk/borsh-incremental/python/borsh_incremental/reader.py @@ -0,0 +1,178 @@ +"""Borsh incremental deserialization reader. + +Provides cursor-based reading of Borsh-serialized binary data with +backward-compatible trailing field support via try_read_* methods. +""" + +from __future__ import annotations + +import struct + + +class IncrementalReader: + """Cursor-based Borsh binary reader with incremental deserialization.""" + + def __init__(self, data: bytes) -> None: + self._data = data + self._offset = 0 + + @property + def offset(self) -> int: + return self._offset + + @property + def remaining(self) -> int: + return len(self._data) - self._offset + + # --- Strict read methods (raise on insufficient data) --- + + def read_u8(self) -> int: + if self._offset + 1 > len(self._data): + raise ValueError(f"borsh: not enough data for u8 at offset {self._offset}") + v = self._data[self._offset] + self._offset += 1 + return v + + def read_bool(self) -> bool: + return self.read_u8() != 0 + + def read_u16(self) -> int: + if self._offset + 2 > len(self._data): + raise ValueError(f"borsh: not enough data for u16 at offset {self._offset}") + (v,) = struct.unpack_from(" int: + if self._offset + 4 > len(self._data): + raise ValueError(f"borsh: not enough data for u32 at offset {self._offset}") + (v,) = struct.unpack_from(" int: + if self._offset + 8 > len(self._data): + raise ValueError(f"borsh: not enough data for u64 at offset {self._offset}") + (v,) = struct.unpack_from(" int: + if self._offset + 16 > len(self._data): + raise ValueError(f"borsh: not enough data for u128 at offset {self._offset}") + low, high = struct.unpack_from(" float: + if self._offset + 8 > len(self._data): + raise ValueError(f"borsh: not enough data for f64 at offset {self._offset}") + (v,) = struct.unpack_from(" bytes: + if self._offset + n > len(self._data): + raise ValueError( + f"borsh: not enough data for {n} bytes at offset {self._offset}" + ) + v = bytes(self._data[self._offset : self._offset + n]) + self._offset += n + return v + + def read_pubkey_raw(self) -> bytes: + """Read a 32-byte public key as raw bytes.""" + return self.read_bytes(32) + + def read_ipv4(self) -> bytes: + return self.read_bytes(4) + + def read_network_v4(self) -> bytes: + return self.read_bytes(5) + + def read_string(self) -> str: + length = self.read_u32() + if length == 0: + return "" + if self._offset + length > len(self._data): + raise ValueError( + f"borsh: not enough data for string of length {length} at offset {self._offset}" + ) + s = self._data[self._offset : self._offset + length].decode("utf-8") + self._offset += length + return s + + def read_pubkey_raw_vec(self) -> list[bytes]: + length = self.read_u32() + return [self.read_pubkey_raw() for _ in range(length)] + + def read_network_v4_vec(self) -> list[bytes]: + length = self.read_u32() + return [self.read_network_v4() for _ in range(length)] + + # --- Try variants (return default when no bytes available) --- + + def try_read_u8(self, default: int = 0) -> int: + if self.remaining < 1: + return default + return self.read_u8() + + def try_read_bool(self, default: bool = False) -> bool: + if self.remaining < 1: + return default + return self.read_bool() + + def try_read_u16(self, default: int = 0) -> int: + if self.remaining < 2: + return default + return self.read_u16() + + def try_read_u32(self, default: int = 0) -> int: + if self.remaining < 4: + return default + return self.read_u32() + + def try_read_u64(self, default: int = 0) -> int: + if self.remaining < 8: + return default + return self.read_u64() + + def try_read_u128(self, default: int = 0) -> int: + if self.remaining < 16: + return default + return self.read_u128() + + def try_read_f64(self, default: float = 0.0) -> float: + if self.remaining < 8: + return default + return self.read_f64() + + def try_read_pubkey_raw(self, default: bytes = b"\x00" * 32) -> bytes: + if self.remaining < 32: + return default + return self.read_pubkey_raw() + + def try_read_ipv4(self, default: bytes = b"\x00" * 4) -> bytes: + if self.remaining < 4: + return default + return self.read_ipv4() + + def try_read_network_v4(self, default: bytes = b"\x00" * 5) -> bytes: + if self.remaining < 5: + return default + return self.read_network_v4() + + def try_read_string(self, default: str = "") -> str: + if self.remaining < 4: + return default + return self.read_string() + + def try_read_pubkey_raw_vec(self, default: list[bytes] | None = None) -> list[bytes]: + if self.remaining < 4: + return default if default is not None else [] + return self.read_pubkey_raw_vec() + + def try_read_network_v4_vec(self, default: list[bytes] | None = None) -> list[bytes]: + if self.remaining < 4: + return default if default is not None else [] + return self.read_network_v4_vec() diff --git a/sdk/borsh-incremental/python/borsh_incremental/tests/__init__.py b/sdk/borsh-incremental/python/borsh_incremental/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sdk/borsh-incremental/python/borsh_incremental/tests/test_reader.py b/sdk/borsh-incremental/python/borsh_incremental/tests/test_reader.py new file mode 100644 index 000000000..5c9161c7e --- /dev/null +++ b/sdk/borsh-incremental/python/borsh_incremental/tests/test_reader.py @@ -0,0 +1,679 @@ +import struct + +import pytest + +from borsh_incremental import IncrementalReader + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _pack_u16(v: int) -> bytes: + return struct.pack(" bytes: + return struct.pack(" bytes: + return struct.pack(" bytes: + low = v & ((1 << 64) - 1) + high = v >> 64 + return struct.pack(" bytes: + return struct.pack(" bytes: + encoded = s.encode("utf-8") + return _pack_u32(len(encoded)) + encoded + + +# =========================================================================== +# 1. Happy path for every read_* method +# =========================================================================== + +class TestReadHappyPath: + def test_read_u8(self): + r = IncrementalReader(bytes([42])) + assert r.read_u8() == 42 + assert r.offset == 1 + assert r.remaining == 0 + + def test_read_bool_true(self): + r = IncrementalReader(bytes([1])) + assert r.read_bool() is True + assert r.offset == 1 + + def test_read_bool_false(self): + r = IncrementalReader(bytes([0])) + assert r.read_bool() is False + assert r.offset == 1 + + def test_read_bool_nonzero_is_true(self): + r = IncrementalReader(bytes([255])) + assert r.read_bool() is True + + def test_read_u16(self): + r = IncrementalReader(_pack_u16(5000)) + assert r.read_u16() == 5000 + assert r.offset == 2 + assert r.remaining == 0 + + def test_read_u32(self): + r = IncrementalReader(_pack_u32(123456)) + assert r.read_u32() == 123456 + assert r.offset == 4 + assert r.remaining == 0 + + def test_read_u64(self): + r = IncrementalReader(_pack_u64(2**40 + 7)) + assert r.read_u64() == 2**40 + 7 + assert r.offset == 8 + + def test_read_u128(self): + val = (2**100) + 42 + r = IncrementalReader(_pack_u128(val)) + assert r.read_u128() == val + assert r.offset == 16 + + def test_read_f64(self): + r = IncrementalReader(_pack_f64(3.14)) + assert r.read_f64() == pytest.approx(3.14) + assert r.offset == 8 + + def test_read_pubkey_raw(self): + buf = bytes(range(32)) + r = IncrementalReader(buf) + assert r.read_pubkey_raw() == buf + assert r.offset == 32 + + def test_read_ipv4(self): + buf = bytes([10, 0, 0, 1]) + r = IncrementalReader(buf) + assert r.read_ipv4() == buf + assert r.offset == 4 + + def test_read_network_v4(self): + buf = bytes([192, 168, 1, 0, 24]) + r = IncrementalReader(buf) + assert r.read_network_v4() == buf + assert r.offset == 5 + + def test_read_string(self): + r = IncrementalReader(_pack_string("hello")) + assert r.read_string() == "hello" + assert r.offset == 4 + 5 + + def test_read_bytes(self): + data = bytes([1, 2, 3, 4, 5]) + r = IncrementalReader(data) + assert r.read_bytes(3) == bytes([1, 2, 3]) + assert r.offset == 3 + assert r.remaining == 2 + + def test_read_pubkey_raw_vec(self): + pk1 = bytes(range(32)) + pk2 = bytes(range(32, 64)) + buf = _pack_u32(2) + pk1 + pk2 + r = IncrementalReader(buf) + result = r.read_pubkey_raw_vec() + assert len(result) == 2 + assert result[0] == pk1 + assert result[1] == pk2 + assert r.offset == 4 + 64 + + def test_read_network_v4_vec(self): + n1 = bytes([10, 0, 0, 0, 8]) + n2 = bytes([172, 16, 0, 0, 12]) + buf = _pack_u32(2) + n1 + n2 + r = IncrementalReader(buf) + result = r.read_network_v4_vec() + assert len(result) == 2 + assert result[0] == n1 + assert result[1] == n2 + assert r.offset == 4 + 10 + + +# =========================================================================== +# 2. ValueError on empty buffer for every read_* method +# =========================================================================== + +class TestReadEmptyBufferError: + def test_read_u8(self): + with pytest.raises(ValueError): + IncrementalReader(b"").read_u8() + + def test_read_bool(self): + with pytest.raises(ValueError): + IncrementalReader(b"").read_bool() + + def test_read_u16(self): + with pytest.raises(ValueError): + IncrementalReader(b"").read_u16() + + def test_read_u32(self): + with pytest.raises(ValueError): + IncrementalReader(b"").read_u32() + + def test_read_u64(self): + with pytest.raises(ValueError): + IncrementalReader(b"").read_u64() + + def test_read_u128(self): + with pytest.raises(ValueError): + IncrementalReader(b"").read_u128() + + def test_read_f64(self): + with pytest.raises(ValueError): + IncrementalReader(b"").read_f64() + + def test_read_pubkey_raw(self): + with pytest.raises(ValueError): + IncrementalReader(b"").read_pubkey_raw() + + def test_read_ipv4(self): + with pytest.raises(ValueError): + IncrementalReader(b"").read_ipv4() + + def test_read_network_v4(self): + with pytest.raises(ValueError): + IncrementalReader(b"").read_network_v4() + + def test_read_string(self): + with pytest.raises(ValueError): + IncrementalReader(b"").read_string() + + def test_read_bytes(self): + with pytest.raises(ValueError): + IncrementalReader(b"").read_bytes(1) + + def test_read_pubkey_raw_vec(self): + with pytest.raises(ValueError): + IncrementalReader(b"").read_pubkey_raw_vec() + + def test_read_network_v4_vec(self): + with pytest.raises(ValueError): + IncrementalReader(b"").read_network_v4_vec() + + +# =========================================================================== +# 3. Partial data ValueError for multi-byte read_* methods +# =========================================================================== + +class TestReadPartialDataError: + def test_read_u16_one_byte(self): + with pytest.raises(ValueError): + IncrementalReader(bytes(1)).read_u16() + + def test_read_u32_two_bytes(self): + with pytest.raises(ValueError): + IncrementalReader(bytes(2)).read_u32() + + def test_read_u64_four_bytes(self): + with pytest.raises(ValueError): + IncrementalReader(bytes(4)).read_u64() + + def test_read_u128_eight_bytes(self): + with pytest.raises(ValueError): + IncrementalReader(bytes(8)).read_u128() + + def test_read_f64_four_bytes(self): + with pytest.raises(ValueError): + IncrementalReader(bytes(4)).read_f64() + + def test_read_pubkey_raw_sixteen_bytes(self): + with pytest.raises(ValueError): + IncrementalReader(bytes(16)).read_pubkey_raw() + + def test_read_ipv4_two_bytes(self): + with pytest.raises(ValueError): + IncrementalReader(bytes(2)).read_ipv4() + + def test_read_network_v4_three_bytes(self): + with pytest.raises(ValueError): + IncrementalReader(bytes(3)).read_network_v4() + + def test_read_string_truncated_body(self): + # Length prefix says 10 bytes, but only 3 available. + buf = _pack_u32(10) + bytes(3) + with pytest.raises(ValueError): + IncrementalReader(buf).read_string() + + def test_read_bytes_partial(self): + with pytest.raises(ValueError): + IncrementalReader(bytes(5)).read_bytes(10) + + +# =========================================================================== +# 4. try_read_* returns default on empty buffer +# =========================================================================== + +class TestTryReadEmptyDefault: + def test_try_read_u8(self): + assert IncrementalReader(b"").try_read_u8(99) == 99 + + def test_try_read_bool(self): + assert IncrementalReader(b"").try_read_bool(True) is True + + def test_try_read_u16(self): + assert IncrementalReader(b"").try_read_u16(1234) == 1234 + + def test_try_read_u32(self): + assert IncrementalReader(b"").try_read_u32(777) == 777 + + def test_try_read_u64(self): + assert IncrementalReader(b"").try_read_u64(888) == 888 + + def test_try_read_u128(self): + assert IncrementalReader(b"").try_read_u128(999) == 999 + + def test_try_read_f64(self): + assert IncrementalReader(b"").try_read_f64(1.5) == 1.5 + + def test_try_read_pubkey_raw(self): + default = b"\xff" * 32 + assert IncrementalReader(b"").try_read_pubkey_raw(default) == default + + def test_try_read_ipv4(self): + default = bytes([127, 0, 0, 1]) + assert IncrementalReader(b"").try_read_ipv4(default) == default + + def test_try_read_network_v4(self): + default = bytes([0, 0, 0, 0, 0]) + assert IncrementalReader(b"").try_read_network_v4(default) == default + + def test_try_read_string(self): + assert IncrementalReader(b"").try_read_string("fallback") == "fallback" + + def test_try_read_pubkey_raw_vec(self): + assert IncrementalReader(b"").try_read_pubkey_raw_vec() == [] + + def test_try_read_pubkey_raw_vec_custom_default(self): + sentinel = [b"\xab" * 32] + assert IncrementalReader(b"").try_read_pubkey_raw_vec(sentinel) is sentinel + + def test_try_read_network_v4_vec(self): + assert IncrementalReader(b"").try_read_network_v4_vec() == [] + + def test_try_read_network_v4_vec_custom_default(self): + sentinel = [bytes(5)] + assert IncrementalReader(b"").try_read_network_v4_vec(sentinel) is sentinel + + +# =========================================================================== +# 5. try_read_* returns actual value when data exists +# =========================================================================== + +class TestTryReadWithData: + def test_try_read_u8(self): + assert IncrementalReader(bytes([42])).try_read_u8(0) == 42 + + def test_try_read_bool(self): + assert IncrementalReader(bytes([1])).try_read_bool(False) is True + + def test_try_read_u16(self): + assert IncrementalReader(_pack_u16(300)).try_read_u16(0) == 300 + + def test_try_read_u32(self): + assert IncrementalReader(_pack_u32(70000)).try_read_u32(0) == 70000 + + def test_try_read_u64(self): + assert IncrementalReader(_pack_u64(2**50)).try_read_u64(0) == 2**50 + + def test_try_read_u128(self): + val = 2**100 + assert IncrementalReader(_pack_u128(val)).try_read_u128(0) == val + + def test_try_read_f64(self): + assert IncrementalReader(_pack_f64(2.718)).try_read_f64(0.0) == pytest.approx(2.718) + + def test_try_read_pubkey_raw(self): + pk = bytes(range(32)) + assert IncrementalReader(pk).try_read_pubkey_raw(b"\x00" * 32) == pk + + def test_try_read_ipv4(self): + ip = bytes([8, 8, 8, 8]) + assert IncrementalReader(ip).try_read_ipv4(bytes(4)) == ip + + def test_try_read_network_v4(self): + net = bytes([10, 0, 0, 0, 24]) + assert IncrementalReader(net).try_read_network_v4(bytes(5)) == net + + def test_try_read_string(self): + assert IncrementalReader(_pack_string("hi")).try_read_string("x") == "hi" + + def test_try_read_pubkey_raw_vec(self): + pk = bytes(range(32)) + buf = _pack_u32(1) + pk + result = IncrementalReader(buf).try_read_pubkey_raw_vec() + assert result == [pk] + + def test_try_read_network_v4_vec(self): + net = bytes([10, 0, 0, 0, 8]) + buf = _pack_u32(1) + net + result = IncrementalReader(buf).try_read_network_v4_vec() + assert result == [net] + + +# =========================================================================== +# 6. try_read_* with partial data returns default +# =========================================================================== + +class TestTryReadPartialDefault: + def test_try_read_u16_one_byte(self): + assert IncrementalReader(bytes(1)).try_read_u16(55) == 55 + + def test_try_read_u32_two_bytes(self): + assert IncrementalReader(bytes(2)).try_read_u32(55) == 55 + + def test_try_read_u64_four_bytes(self): + assert IncrementalReader(bytes(4)).try_read_u64(55) == 55 + + def test_try_read_u128_eight_bytes(self): + assert IncrementalReader(bytes(8)).try_read_u128(55) == 55 + + def test_try_read_f64_four_bytes(self): + assert IncrementalReader(bytes(4)).try_read_f64(1.0) == 1.0 + + def test_try_read_pubkey_raw_sixteen_bytes(self): + default = b"\xff" * 32 + assert IncrementalReader(bytes(16)).try_read_pubkey_raw(default) == default + + def test_try_read_ipv4_two_bytes(self): + assert IncrementalReader(bytes(2)).try_read_ipv4(bytes(4)) == bytes(4) + + def test_try_read_network_v4_three_bytes(self): + assert IncrementalReader(bytes(3)).try_read_network_v4(bytes(5)) == bytes(5) + + def test_try_read_string_two_bytes(self): + # Need at least 4 bytes for the length prefix. + assert IncrementalReader(bytes(2)).try_read_string("nope") == "nope" + + def test_try_read_pubkey_raw_vec_two_bytes(self): + assert IncrementalReader(bytes(2)).try_read_pubkey_raw_vec() == [] + + def test_try_read_network_v4_vec_two_bytes(self): + assert IncrementalReader(bytes(2)).try_read_network_v4_vec() == [] + + +# =========================================================================== +# 7. Sequential reads with offset tracking +# =========================================================================== + +class TestSequentialReads: + def test_u8_u16_u32_u64(self): + buf = bytes([7]) + _pack_u16(500) + _pack_u32(100000) + _pack_u64(2**48) + r = IncrementalReader(buf) + assert r.offset == 0 + assert r.read_u8() == 7 + assert r.offset == 1 + assert r.read_u16() == 500 + assert r.offset == 3 + assert r.read_u32() == 100000 + assert r.offset == 7 + assert r.read_u64() == 2**48 + assert r.offset == 15 + assert r.remaining == 0 + + def test_string_then_pubkey(self): + pk = bytes(range(32)) + buf = _pack_string("test") + pk + r = IncrementalReader(buf) + assert r.read_string() == "test" + assert r.offset == 8 # 4 len + 4 chars + assert r.read_pubkey_raw() == pk + assert r.offset == 40 + assert r.remaining == 0 + + def test_bool_then_f64_then_ipv4(self): + buf = bytes([1]) + _pack_f64(9.81) + bytes([192, 168, 0, 1]) + r = IncrementalReader(buf) + assert r.read_bool() is True + assert r.offset == 1 + assert r.read_f64() == pytest.approx(9.81) + assert r.offset == 9 + assert r.read_ipv4() == bytes([192, 168, 0, 1]) + assert r.offset == 13 + + +# =========================================================================== +# 8. Trailing optional fields scenario +# =========================================================================== + +class TestTrailingFields: + def test_required_then_missing_optional(self): + buf = bytes([1]) + _pack_u32(42) + r = IncrementalReader(buf) + assert r.read_u8() == 1 + assert r.read_u32() == 42 + # All remaining try_reads should return defaults. + assert r.try_read_u16(9999) == 9999 + assert r.try_read_string("none") == "none" + assert r.try_read_f64(-1.0) == -1.0 + assert r.try_read_pubkey_raw_vec() == [] + assert r.remaining == 0 + + def test_required_then_present_optional(self): + buf = bytes([1]) + _pack_u32(42) + _pack_u16(7) + _pack_string("opt") + r = IncrementalReader(buf) + assert r.read_u8() == 1 + assert r.read_u32() == 42 + assert r.try_read_u16(9999) == 7 + assert r.try_read_string("none") == "opt" + assert r.remaining == 0 + + def test_partial_optional_falls_back(self): + # After required fields, only 1 byte remains -- not enough for u32. + buf = bytes([1]) + bytes([0xFF]) + r = IncrementalReader(buf) + assert r.read_u8() == 1 + assert r.try_read_u32(0) == 0 + # The leftover byte is still unconsumed. + assert r.remaining == 1 + + +# =========================================================================== +# 9. Vec methods +# =========================================================================== + +class TestVecMethods: + # --- pubkey_raw_vec --- + + def test_pubkey_raw_vec_empty(self): + buf = _pack_u32(0) + r = IncrementalReader(buf) + assert r.read_pubkey_raw_vec() == [] + assert r.offset == 4 + + def test_pubkey_raw_vec_single(self): + pk = bytes(range(32)) + buf = _pack_u32(1) + pk + r = IncrementalReader(buf) + assert r.read_pubkey_raw_vec() == [pk] + + def test_pubkey_raw_vec_multiple(self): + pks = [bytes([i] * 32) for i in range(3)] + buf = _pack_u32(3) + b"".join(pks) + r = IncrementalReader(buf) + assert r.read_pubkey_raw_vec() == pks + + def test_pubkey_raw_vec_truncated_length(self): + # Only 2 bytes, not enough for the u32 length prefix. + with pytest.raises(ValueError): + IncrementalReader(bytes(2)).read_pubkey_raw_vec() + + def test_pubkey_raw_vec_truncated_elements(self): + # Says 2 elements but only has data for 1. + pk = bytes(range(32)) + buf = _pack_u32(2) + pk + with pytest.raises(ValueError): + IncrementalReader(buf).read_pubkey_raw_vec() + + # --- network_v4_vec --- + + def test_network_v4_vec_empty(self): + buf = _pack_u32(0) + r = IncrementalReader(buf) + assert r.read_network_v4_vec() == [] + + def test_network_v4_vec_single(self): + net = bytes([10, 0, 0, 0, 8]) + buf = _pack_u32(1) + net + r = IncrementalReader(buf) + assert r.read_network_v4_vec() == [net] + + def test_network_v4_vec_multiple(self): + nets = [bytes([10, i, 0, 0, 24]) for i in range(3)] + buf = _pack_u32(3) + b"".join(nets) + r = IncrementalReader(buf) + assert r.read_network_v4_vec() == nets + assert r.offset == 4 + 15 + + def test_network_v4_vec_truncated_length(self): + with pytest.raises(ValueError): + IncrementalReader(bytes(2)).read_network_v4_vec() + + def test_network_v4_vec_truncated_elements(self): + # Says 2 elements but only has data for 1. + net = bytes([10, 0, 0, 0, 8]) + buf = _pack_u32(2) + net + with pytest.raises(ValueError): + IncrementalReader(buf).read_network_v4_vec() + + +# =========================================================================== +# 10. String edge cases +# =========================================================================== + +class TestStringEdgeCases: + def test_empty_string(self): + buf = _pack_u32(0) + r = IncrementalReader(buf) + assert r.read_string() == "" + assert r.offset == 4 + + def test_normal_string(self): + r = IncrementalReader(_pack_string("borsh")) + assert r.read_string() == "borsh" + + def test_string_truncated_body(self): + buf = _pack_u32(100) + b"short" + with pytest.raises(ValueError): + IncrementalReader(buf).read_string() + + def test_string_truncated_length_prefix(self): + with pytest.raises(ValueError): + IncrementalReader(bytes(2)).read_string() + + def test_utf8_string(self): + text = "cafe\u0301" # cafe with combining accent + r = IncrementalReader(_pack_string(text)) + assert r.read_string() == text + + +# =========================================================================== +# 11. U128 byte order (little-endian) +# =========================================================================== + +class TestU128ByteOrder: + def test_one(self): + buf = bytes([1]) + bytes(15) + r = IncrementalReader(buf) + assert r.read_u128() == 1 + + def test_low_64_only(self): + val = 0xDEADBEEFCAFEBABE + buf = struct.pack("=9.0.2", +] diff --git a/sdk/borsh-incremental/python/uv.lock b/sdk/borsh-incremental/python/uv.lock new file mode 100644 index 000000000..8708f87bf --- /dev/null +++ b/sdk/borsh-incremental/python/uv.lock @@ -0,0 +1,156 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "doublezero-borsh-incremental" +version = "0.0.1" +source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=9.0.2" }] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] diff --git a/sdk/borsh-incremental/typescript/borsh-incremental/index.test.ts b/sdk/borsh-incremental/typescript/borsh-incremental/index.test.ts new file mode 100644 index 000000000..3bf71a0d1 --- /dev/null +++ b/sdk/borsh-incremental/typescript/borsh-incremental/index.test.ts @@ -0,0 +1,528 @@ +import { describe, test, expect } from "bun:test"; +import { IncrementalReader } from "./index"; + +// --- Helpers --- + +function u16le(v: number): Uint8Array { + const buf = new ArrayBuffer(2); + new DataView(buf).setUint16(0, v, true); + return new Uint8Array(buf); +} + +function u32le(v: number): Uint8Array { + const buf = new ArrayBuffer(4); + new DataView(buf).setUint32(0, v, true); + return new Uint8Array(buf); +} + +function u64le(v: bigint): Uint8Array { + const buf = new ArrayBuffer(8); + new DataView(buf).setBigUint64(0, v, true); + return new Uint8Array(buf); +} + +function f64le(v: number): Uint8Array { + const buf = new ArrayBuffer(8); + new DataView(buf).setFloat64(0, v, true); + return new Uint8Array(buf); +} + +function concat(...arrays: Uint8Array[]): Uint8Array { + const total = arrays.reduce((s, a) => s + a.length, 0); + const result = new Uint8Array(total); + let offset = 0; + for (const a of arrays) { + result.set(a, offset); + offset += a.length; + } + return result; +} + +function strBorsh(s: string): Uint8Array { + const encoded = new TextEncoder().encode(s); + return concat(u32le(encoded.length), encoded); +} + +const EMPTY = new Uint8Array(0); + +// --- Tests --- + +describe("IncrementalReader", () => { + describe("offset and remaining getters", () => { + test("initial state", () => { + const r = new IncrementalReader(new Uint8Array([1, 2, 3])); + expect(r.offset).toBe(0); + expect(r.remaining).toBe(3); + }); + + test("after reads", () => { + const r = new IncrementalReader(new Uint8Array([1, 2, 3])); + r.readU8(); + expect(r.offset).toBe(1); + expect(r.remaining).toBe(2); + }); + + test("empty buffer", () => { + const r = new IncrementalReader(EMPTY); + expect(r.offset).toBe(0); + expect(r.remaining).toBe(0); + }); + }); + + describe("happy path for every read* method", () => { + test("readU8", () => { + const r = new IncrementalReader(new Uint8Array([0xff])); + expect(r.readU8()).toBe(255); + expect(r.offset).toBe(1); + }); + + test("readBool true and false", () => { + const r = new IncrementalReader(new Uint8Array([1, 0])); + expect(r.readBool()).toBe(true); + expect(r.readBool()).toBe(false); + expect(r.offset).toBe(2); + }); + + test("readU16", () => { + const r = new IncrementalReader(u16le(0x1234)); + expect(r.readU16()).toBe(0x1234); + expect(r.offset).toBe(2); + }); + + test("readU32", () => { + const r = new IncrementalReader(u32le(0xdeadbeef)); + expect(r.readU32()).toBe(0xdeadbeef); + expect(r.offset).toBe(4); + }); + + test("readU64", () => { + const r = new IncrementalReader(u64le(123456789012345n)); + expect(r.readU64()).toBe(123456789012345n); + expect(r.offset).toBe(8); + }); + + test("readU128", () => { + const low = 0xdeadbeef12345678n; + const high = 0x1n; + const r = new IncrementalReader(concat(u64le(low), u64le(high))); + expect(r.readU128()).toBe(low | (high << 64n)); + expect(r.offset).toBe(16); + }); + + test("readF64", () => { + const r = new IncrementalReader(f64le(3.14)); + expect(r.readF64()).toBeCloseTo(3.14, 10); + expect(r.offset).toBe(8); + }); + + test("readBytes", () => { + const r = new IncrementalReader(new Uint8Array([10, 20, 30])); + const bytes = r.readBytes(2); + expect(bytes).toEqual(new Uint8Array([10, 20])); + expect(r.offset).toBe(2); + }); + + test("readPubkeyRaw", () => { + const key = new Uint8Array(32).fill(0xab); + const r = new IncrementalReader(key); + expect(r.readPubkeyRaw()).toEqual(key); + expect(r.offset).toBe(32); + }); + + test("readIPv4", () => { + const ip = new Uint8Array([192, 168, 1, 1]); + const r = new IncrementalReader(ip); + expect(r.readIPv4()).toEqual(ip); + expect(r.offset).toBe(4); + }); + + test("readNetworkV4", () => { + const net = new Uint8Array([10, 0, 0, 0, 24]); + const r = new IncrementalReader(net); + expect(r.readNetworkV4()).toEqual(net); + expect(r.offset).toBe(5); + }); + + test("readString", () => { + const r = new IncrementalReader(strBorsh("hello")); + expect(r.readString()).toBe("hello"); + expect(r.offset).toBe(4 + 5); + }); + + test("readPubkeyRawVec", () => { + const k1 = new Uint8Array(32).fill(1); + const k2 = new Uint8Array(32).fill(2); + const r = new IncrementalReader(concat(u32le(2), k1, k2)); + const result = r.readPubkeyRawVec(); + expect(result).toHaveLength(2); + expect(result[0]).toEqual(k1); + expect(result[1]).toEqual(k2); + }); + + test("readNetworkV4Vec", () => { + const n1 = new Uint8Array([10, 0, 0, 0, 8]); + const n2 = new Uint8Array([172, 16, 0, 0, 12]); + const r = new IncrementalReader(concat(u32le(2), n1, n2)); + const result = r.readNetworkV4Vec(); + expect(result).toHaveLength(2); + expect(result[0]).toEqual(n1); + expect(result[1]).toEqual(n2); + }); + }); + + describe("throw on empty buffer for every read*", () => { + test("readU8", () => expect(() => new IncrementalReader(EMPTY).readU8()).toThrow()); + test("readBool", () => expect(() => new IncrementalReader(EMPTY).readBool()).toThrow()); + test("readU16", () => expect(() => new IncrementalReader(EMPTY).readU16()).toThrow()); + test("readU32", () => expect(() => new IncrementalReader(EMPTY).readU32()).toThrow()); + test("readU64", () => expect(() => new IncrementalReader(EMPTY).readU64()).toThrow()); + test("readU128", () => expect(() => new IncrementalReader(EMPTY).readU128()).toThrow()); + test("readF64", () => expect(() => new IncrementalReader(EMPTY).readF64()).toThrow()); + test("readBytes", () => expect(() => new IncrementalReader(EMPTY).readBytes(1)).toThrow()); + test("readPubkeyRaw", () => expect(() => new IncrementalReader(EMPTY).readPubkeyRaw()).toThrow()); + test("readIPv4", () => expect(() => new IncrementalReader(EMPTY).readIPv4()).toThrow()); + test("readNetworkV4", () => expect(() => new IncrementalReader(EMPTY).readNetworkV4()).toThrow()); + test("readString", () => expect(() => new IncrementalReader(EMPTY).readString()).toThrow()); + test("readPubkeyRawVec", () => expect(() => new IncrementalReader(EMPTY).readPubkeyRawVec()).toThrow()); + test("readNetworkV4Vec", () => expect(() => new IncrementalReader(EMPTY).readNetworkV4Vec()).toThrow()); + }); + + describe("partial data throw for multi-byte read*", () => { + test("readU16 with 1 byte", () => { + expect(() => new IncrementalReader(new Uint8Array([0x01])).readU16()).toThrow(); + }); + + test("readU32 with 2 bytes", () => { + expect(() => new IncrementalReader(new Uint8Array([0, 0])).readU32()).toThrow(); + }); + + test("readU64 with 4 bytes", () => { + expect(() => new IncrementalReader(new Uint8Array(4)).readU64()).toThrow(); + }); + + test("readU128 with 12 bytes", () => { + expect(() => new IncrementalReader(new Uint8Array(12)).readU128()).toThrow(); + }); + + test("readF64 with 5 bytes", () => { + expect(() => new IncrementalReader(new Uint8Array(5)).readF64()).toThrow(); + }); + + test("readPubkeyRaw with 20 bytes", () => { + expect(() => new IncrementalReader(new Uint8Array(20)).readPubkeyRaw()).toThrow(); + }); + + test("readIPv4 with 3 bytes", () => { + expect(() => new IncrementalReader(new Uint8Array(3)).readIPv4()).toThrow(); + }); + + test("readNetworkV4 with 4 bytes", () => { + expect(() => new IncrementalReader(new Uint8Array(4)).readNetworkV4()).toThrow(); + }); + + test("readBytes with fewer bytes than requested", () => { + expect(() => new IncrementalReader(new Uint8Array(2)).readBytes(5)).toThrow(); + }); + }); + + describe("tryRead* returns default on empty buffer", () => { + test("tryReadU8", () => expect(new IncrementalReader(EMPTY).tryReadU8()).toBe(0)); + test("tryReadU8 custom default", () => expect(new IncrementalReader(EMPTY).tryReadU8(42)).toBe(42)); + test("tryReadBool", () => expect(new IncrementalReader(EMPTY).tryReadBool()).toBe(false)); + test("tryReadBool custom default", () => expect(new IncrementalReader(EMPTY).tryReadBool(true)).toBe(true)); + test("tryReadU16", () => expect(new IncrementalReader(EMPTY).tryReadU16()).toBe(0)); + test("tryReadU32", () => expect(new IncrementalReader(EMPTY).tryReadU32()).toBe(0)); + test("tryReadU64", () => expect(new IncrementalReader(EMPTY).tryReadU64()).toBe(0n)); + test("tryReadU128", () => expect(new IncrementalReader(EMPTY).tryReadU128()).toBe(0n)); + test("tryReadF64", () => expect(new IncrementalReader(EMPTY).tryReadF64()).toBe(0)); + test("tryReadPubkeyRaw", () => { + expect(new IncrementalReader(EMPTY).tryReadPubkeyRaw()).toEqual(new Uint8Array(32)); + }); + test("tryReadIPv4", () => { + expect(new IncrementalReader(EMPTY).tryReadIPv4()).toEqual(new Uint8Array(4)); + }); + test("tryReadNetworkV4", () => { + expect(new IncrementalReader(EMPTY).tryReadNetworkV4()).toEqual(new Uint8Array(5)); + }); + test("tryReadString", () => expect(new IncrementalReader(EMPTY).tryReadString()).toBe("")); + test("tryReadPubkeyRawVec", () => { + expect(new IncrementalReader(EMPTY).tryReadPubkeyRawVec()).toEqual([]); + }); + test("tryReadNetworkV4Vec", () => { + expect(new IncrementalReader(EMPTY).tryReadNetworkV4Vec()).toEqual([]); + }); + }); + + describe("tryRead* returns actual value when data exists", () => { + test("tryReadU8", () => expect(new IncrementalReader(new Uint8Array([7])).tryReadU8()).toBe(7)); + test("tryReadBool", () => expect(new IncrementalReader(new Uint8Array([1])).tryReadBool()).toBe(true)); + test("tryReadU16", () => expect(new IncrementalReader(u16le(500)).tryReadU16()).toBe(500)); + test("tryReadU32", () => expect(new IncrementalReader(u32le(100000)).tryReadU32()).toBe(100000)); + test("tryReadU64", () => expect(new IncrementalReader(u64le(999n)).tryReadU64()).toBe(999n)); + test("tryReadU128", () => { + const r = new IncrementalReader(concat(u64le(42n), u64le(0n))); + expect(r.tryReadU128()).toBe(42n); + }); + test("tryReadF64", () => { + expect(new IncrementalReader(f64le(2.718)).tryReadF64()).toBeCloseTo(2.718, 10); + }); + test("tryReadPubkeyRaw", () => { + const key = new Uint8Array(32).fill(0xcc); + expect(new IncrementalReader(key).tryReadPubkeyRaw()).toEqual(key); + }); + test("tryReadIPv4", () => { + const ip = new Uint8Array([10, 0, 0, 1]); + expect(new IncrementalReader(ip).tryReadIPv4()).toEqual(ip); + }); + test("tryReadNetworkV4", () => { + const net = new Uint8Array([192, 168, 0, 0, 16]); + expect(new IncrementalReader(net).tryReadNetworkV4()).toEqual(net); + }); + test("tryReadString", () => { + expect(new IncrementalReader(strBorsh("world")).tryReadString()).toBe("world"); + }); + test("tryReadPubkeyRawVec", () => { + const k = new Uint8Array(32).fill(0xaa); + const r = new IncrementalReader(concat(u32le(1), k)); + expect(r.tryReadPubkeyRawVec()).toEqual([k]); + }); + test("tryReadNetworkV4Vec", () => { + const n = new Uint8Array([1, 2, 3, 4, 5]); + const r = new IncrementalReader(concat(u32le(1), n)); + expect(r.tryReadNetworkV4Vec()).toEqual([n]); + }); + }); + + describe("tryRead* with partial data returns default", () => { + test("tryReadU16 with 1 byte", () => { + expect(new IncrementalReader(new Uint8Array([0x01])).tryReadU16()).toBe(0); + }); + + test("tryReadU32 with 1 byte", () => { + expect(new IncrementalReader(new Uint8Array([0x01])).tryReadU32()).toBe(0); + }); + + test("tryReadU64 with 3 bytes", () => { + expect(new IncrementalReader(new Uint8Array(3)).tryReadU64()).toBe(0n); + }); + + test("tryReadU128 with 10 bytes", () => { + expect(new IncrementalReader(new Uint8Array(10)).tryReadU128()).toBe(0n); + }); + + test("tryReadF64 with 6 bytes", () => { + expect(new IncrementalReader(new Uint8Array(6)).tryReadF64()).toBe(0); + }); + + test("tryReadPubkeyRaw with 16 bytes", () => { + expect(new IncrementalReader(new Uint8Array(16)).tryReadPubkeyRaw()).toEqual(new Uint8Array(32)); + }); + + test("tryReadIPv4 with 2 bytes", () => { + expect(new IncrementalReader(new Uint8Array(2)).tryReadIPv4()).toEqual(new Uint8Array(4)); + }); + + test("tryReadNetworkV4 with 3 bytes", () => { + expect(new IncrementalReader(new Uint8Array(3)).tryReadNetworkV4()).toEqual(new Uint8Array(5)); + }); + + test("tryReadString with 2 bytes", () => { + expect(new IncrementalReader(new Uint8Array(2)).tryReadString()).toBe(""); + }); + + test("tryReadPubkeyRawVec with 2 bytes", () => { + expect(new IncrementalReader(new Uint8Array(2)).tryReadPubkeyRawVec()).toEqual([]); + }); + + test("tryReadNetworkV4Vec with 2 bytes", () => { + expect(new IncrementalReader(new Uint8Array(2)).tryReadNetworkV4Vec()).toEqual([]); + }); + }); + + describe("sequential reads verify offset tracking", () => { + test("read u8 then u32 then u16", () => { + const data = concat(new Uint8Array([42]), u32le(1000), u16le(500)); + const r = new IncrementalReader(data); + expect(r.readU8()).toBe(42); + expect(r.offset).toBe(1); + expect(r.readU32()).toBe(1000); + expect(r.offset).toBe(5); + expect(r.readU16()).toBe(500); + expect(r.offset).toBe(7); + expect(r.remaining).toBe(0); + }); + + test("read string then u64 then bool", () => { + const data = concat(strBorsh("abc"), u64le(99n), new Uint8Array([1])); + const r = new IncrementalReader(data); + expect(r.readString()).toBe("abc"); + expect(r.offset).toBe(7); + expect(r.readU64()).toBe(99n); + expect(r.offset).toBe(15); + expect(r.readBool()).toBe(true); + expect(r.offset).toBe(16); + expect(r.remaining).toBe(0); + }); + }); + + describe("trailing fields scenario", () => { + test("strict fields followed by optional tryRead fields", () => { + // Simulate a struct with required u32 + optional u8 trailing field + const data = concat(u32le(42)); + const r = new IncrementalReader(data); + expect(r.readU32()).toBe(42); + // trailing field not present, tryRead returns default + expect(r.tryReadU8(255)).toBe(255); + expect(r.offset).toBe(4); + }); + + test("strict fields followed by present trailing field", () => { + const data = concat(u32le(42), new Uint8Array([7])); + const r = new IncrementalReader(data); + expect(r.readU32()).toBe(42); + expect(r.tryReadU8(255)).toBe(7); + expect(r.offset).toBe(5); + }); + }); + + describe("Vec methods", () => { + test("empty pubkey vec", () => { + const r = new IncrementalReader(u32le(0)); + expect(r.readPubkeyRawVec()).toEqual([]); + expect(r.offset).toBe(4); + }); + + test("single element pubkey vec", () => { + const k = new Uint8Array(32).fill(0x11); + const r = new IncrementalReader(concat(u32le(1), k)); + const result = r.readPubkeyRawVec(); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(k); + }); + + test("multiple element pubkey vec", () => { + const k1 = new Uint8Array(32).fill(0x01); + const k2 = new Uint8Array(32).fill(0x02); + const k3 = new Uint8Array(32).fill(0x03); + const r = new IncrementalReader(concat(u32le(3), k1, k2, k3)); + const result = r.readPubkeyRawVec(); + expect(result).toHaveLength(3); + expect(result[2]).toEqual(k3); + }); + + test("truncated pubkey vec throws", () => { + // says 2 elements but only has 1 + const k = new Uint8Array(32).fill(0x01); + const r = new IncrementalReader(concat(u32le(2), k)); + expect(() => r.readPubkeyRawVec()).toThrow(); + }); + + test("empty networkV4 vec", () => { + const r = new IncrementalReader(u32le(0)); + expect(r.readNetworkV4Vec()).toEqual([]); + }); + + test("multiple element networkV4 vec", () => { + const n1 = new Uint8Array([10, 0, 0, 0, 8]); + const n2 = new Uint8Array([172, 16, 0, 0, 12]); + const r = new IncrementalReader(concat(u32le(2), n1, n2)); + const result = r.readNetworkV4Vec(); + expect(result).toHaveLength(2); + }); + + test("truncated networkV4 vec throws", () => { + const n = new Uint8Array([10, 0, 0, 0, 8]); + const r = new IncrementalReader(concat(u32le(3), n)); + expect(() => r.readNetworkV4Vec()).toThrow(); + }); + }); + + describe("String", () => { + test("empty string", () => { + const r = new IncrementalReader(u32le(0)); + expect(r.readString()).toBe(""); + expect(r.offset).toBe(4); + }); + + test("normal string", () => { + const r = new IncrementalReader(strBorsh("hello world")); + expect(r.readString()).toBe("hello world"); + }); + + test("truncated string throws (length says 10 but only 5 bytes)", () => { + const partial = concat(u32le(10), new Uint8Array([65, 66, 67, 68, 69])); + const r = new IncrementalReader(partial); + expect(() => r.readString()).toThrow(); + }); + }); + + describe("U128 little-endian byte order", () => { + test("low word only", () => { + const r = new IncrementalReader(concat(u64le(0xffffffffffffffffn), u64le(0n))); + expect(r.readU128()).toBe(0xffffffffffffffffn); + }); + + test("high word only", () => { + const r = new IncrementalReader(concat(u64le(0n), u64le(1n))); + expect(r.readU128()).toBe(1n << 64n); + }); + + test("both words", () => { + const low = 0x123456789abcdef0n; + const high = 0xfedcba9876543210n; + const r = new IncrementalReader(concat(u64le(low), u64le(high))); + expect(r.readU128()).toBe(low | (high << 64n)); + }); + }); + + describe("U64 bigint values", () => { + test("zero", () => { + expect(new IncrementalReader(u64le(0n)).readU64()).toBe(0n); + }); + + test("max u64", () => { + const max = 0xffffffffffffffffn; + expect(new IncrementalReader(u64le(max)).readU64()).toBe(max); + }); + + test("large value", () => { + const v = 9999999999999999999n; + expect(new IncrementalReader(u64le(v)).readU64()).toBe(v); + }); + }); + + describe("F64 known float", () => { + test("pi", () => { + expect(new IncrementalReader(f64le(Math.PI)).readF64()).toBe(Math.PI); + }); + + test("negative", () => { + expect(new IncrementalReader(f64le(-1.5)).readF64()).toBe(-1.5); + }); + + test("zero", () => { + expect(new IncrementalReader(f64le(0)).readF64()).toBe(0); + }); + }); + + describe("readBytes", () => { + test("happy path", () => { + const data = new Uint8Array([1, 2, 3, 4, 5]); + const r = new IncrementalReader(data); + expect(r.readBytes(3)).toEqual(new Uint8Array([1, 2, 3])); + expect(r.readBytes(2)).toEqual(new Uint8Array([4, 5])); + expect(r.remaining).toBe(0); + }); + + test("zero bytes", () => { + const r = new IncrementalReader(new Uint8Array([1])); + expect(r.readBytes(0)).toEqual(new Uint8Array(0)); + expect(r.offset).toBe(0); + }); + + test("error when not enough bytes", () => { + const r = new IncrementalReader(new Uint8Array([1, 2])); + expect(() => r.readBytes(10)).toThrow(); + }); + }); +}); diff --git a/sdk/borsh-incremental/typescript/borsh-incremental/index.ts b/sdk/borsh-incremental/typescript/borsh-incremental/index.ts new file mode 100644 index 000000000..d4fcb0d64 --- /dev/null +++ b/sdk/borsh-incremental/typescript/borsh-incremental/index.ts @@ -0,0 +1,200 @@ +/** + * Borsh incremental deserialization reader. + * + * Provides cursor-based reading of Borsh-serialized binary data with + * backward-compatible trailing field support via tryRead* methods. + */ + +export class IncrementalReader { + private data: DataView; + private raw: Uint8Array; + private _offset: number; + + constructor(data: Uint8Array) { + this.raw = data; + this.data = new DataView(data.buffer, data.byteOffset, data.byteLength); + this._offset = 0; + } + + get offset(): number { + return this._offset; + } + + get remaining(): number { + return this.raw.byteLength - this._offset; + } + + // --- Strict read methods (throw on insufficient data) --- + + readU8(): number { + if (this._offset + 1 > this.raw.byteLength) { + throw new Error(`borsh: not enough data for u8 at offset ${this._offset}`); + } + const v = this.data.getUint8(this._offset); + this._offset += 1; + return v; + } + + readBool(): boolean { + return this.readU8() !== 0; + } + + readU16(): number { + if (this._offset + 2 > this.raw.byteLength) { + throw new Error(`borsh: not enough data for u16 at offset ${this._offset}`); + } + const v = this.data.getUint16(this._offset, true); + this._offset += 2; + return v; + } + + readU32(): number { + if (this._offset + 4 > this.raw.byteLength) { + throw new Error(`borsh: not enough data for u32 at offset ${this._offset}`); + } + const v = this.data.getUint32(this._offset, true); + this._offset += 4; + return v; + } + + readU64(): bigint { + if (this._offset + 8 > this.raw.byteLength) { + throw new Error(`borsh: not enough data for u64 at offset ${this._offset}`); + } + const v = this.data.getBigUint64(this._offset, true); + this._offset += 8; + return v; + } + + readU128(): bigint { + const low = this.readU64(); + const high = this.readU64(); + return low | (high << 64n); + } + + readF64(): number { + if (this._offset + 8 > this.raw.byteLength) { + throw new Error(`borsh: not enough data for f64 at offset ${this._offset}`); + } + const v = this.data.getFloat64(this._offset, true); + this._offset += 8; + return v; + } + + readBytes(n: number): Uint8Array { + if (this._offset + n > this.raw.byteLength) { + throw new Error( + `borsh: not enough data for ${n} bytes at offset ${this._offset}`, + ); + } + const v = this.raw.slice(this._offset, this._offset + n); + this._offset += n; + return v; + } + + readPubkeyRaw(): Uint8Array { + return this.readBytes(32); + } + + readIPv4(): Uint8Array { + return this.readBytes(4); + } + + readNetworkV4(): Uint8Array { + return this.readBytes(5); + } + + readString(): string { + const len = this.readU32(); + if (len === 0) return ""; + if (this._offset + len > this.raw.byteLength) { + throw new Error( + `borsh: not enough data for string of length ${len} at offset ${this._offset}`, + ); + } + const bytes = this.raw.slice(this._offset, this._offset + len); + this._offset += len; + return new TextDecoder().decode(bytes); + } + + readPubkeyRawVec(): Uint8Array[] { + const len = this.readU32(); + const result: Uint8Array[] = []; + for (let i = 0; i < len; i++) result.push(this.readPubkeyRaw()); + return result; + } + + readNetworkV4Vec(): Uint8Array[] { + const len = this.readU32(); + const result: Uint8Array[] = []; + for (let i = 0; i < len; i++) result.push(this.readNetworkV4()); + return result; + } + + // --- Try variants (return default when no bytes available) --- + + tryReadU8(def: number = 0): number { + if (this.remaining < 1) return def; + return this.readU8(); + } + + tryReadBool(def: boolean = false): boolean { + if (this.remaining < 1) return def; + return this.readBool(); + } + + tryReadU16(def: number = 0): number { + if (this.remaining < 2) return def; + return this.readU16(); + } + + tryReadU32(def: number = 0): number { + if (this.remaining < 4) return def; + return this.readU32(); + } + + tryReadU64(def: bigint = 0n): bigint { + if (this.remaining < 8) return def; + return this.readU64(); + } + + tryReadU128(def: bigint = 0n): bigint { + if (this.remaining < 16) return def; + return this.readU128(); + } + + tryReadF64(def: number = 0): number { + if (this.remaining < 8) return def; + return this.readF64(); + } + + tryReadPubkeyRaw(def: Uint8Array = new Uint8Array(32)): Uint8Array { + if (this.remaining < 32) return def; + return this.readPubkeyRaw(); + } + + tryReadIPv4(def: Uint8Array = new Uint8Array(4)): Uint8Array { + if (this.remaining < 4) return def; + return this.readIPv4(); + } + + tryReadNetworkV4(def: Uint8Array = new Uint8Array(5)): Uint8Array { + if (this.remaining < 5) return def; + return this.readNetworkV4(); + } + + tryReadString(def: string = ""): string { + if (this.remaining < 4) return def; + return this.readString(); + } + + tryReadPubkeyRawVec(def: Uint8Array[] = []): Uint8Array[] { + if (this.remaining < 4) return def; + return this.readPubkeyRawVec(); + } + + tryReadNetworkV4Vec(def: Uint8Array[] = []): Uint8Array[] { + if (this.remaining < 4) return def; + return this.readNetworkV4Vec(); + } +} diff --git a/sdk/borsh-incremental/typescript/bun.lock b/sdk/borsh-incremental/typescript/bun.lock new file mode 100644 index 000000000..1995b1b19 --- /dev/null +++ b/sdk/borsh-incremental/typescript/bun.lock @@ -0,0 +1,24 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "@doublezero/borsh-incremental", + "devDependencies": { + "@types/bun": "latest", + "typescript": "^5", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], + + "@types/node": ["@types/node@25.1.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA=="], + + "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + } +} diff --git a/sdk/borsh-incremental/typescript/package.json b/sdk/borsh-incremental/typescript/package.json new file mode 100644 index 000000000..b7ce78c0d --- /dev/null +++ b/sdk/borsh-incremental/typescript/package.json @@ -0,0 +1,19 @@ +{ + "name": "@doublezero/borsh-incremental", + "version": "0.0.1", + "type": "module", + "main": "dist/borsh-incremental/index.js", + "types": "dist/borsh-incremental/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "prepublishOnly": "tsc" + }, + "dependencies": {}, + "devDependencies": { + "@types/bun": "latest", + "typescript": "^5" + } +} diff --git a/sdk/borsh-incremental/typescript/tsconfig.json b/sdk/borsh-incremental/typescript/tsconfig.json new file mode 100644 index 000000000..12f2f0e10 --- /dev/null +++ b/sdk/borsh-incremental/typescript/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "dist", + "declaration": true, + "types": ["bun"] + }, + "include": ["borsh-incremental/**/*.ts"] +} diff --git a/sdk/revdist/go/client.go b/sdk/revdist/go/client.go new file mode 100644 index 000000000..973d9e6a9 --- /dev/null +++ b/sdk/revdist/go/client.go @@ -0,0 +1,308 @@ +package revdist + +import ( + "bytes" + "context" + "encoding/binary" + "errors" + "fmt" + "unsafe" + + ag_binary "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" +) + +var ( + ErrAccountNotFound = errors.New("account not found") + ErrLedgerClientNil = errors.New("ledger record client not configured") +) + +// deserializeAccount validates the discriminator and deserializes the account +// data into the given struct. It requires at least discriminator + sizeof(T) +// bytes but tolerates extra trailing bytes for forward compatibility. +func deserializeAccount[T any](data []byte, disc [8]byte) (*T, error) { + if err := validateDiscriminator(data, disc); err != nil { + return nil, err + } + body := data[discriminatorSize:] + var zero T + need := int(unsafe.Sizeof(zero)) + if len(body) < need { + return nil, fmt.Errorf("account data too short: have %d bytes, need at least %d", len(body), need) + } + var item T + if err := binary.Read(bytes.NewReader(body[:need]), binary.LittleEndian, &item); err != nil { + return nil, fmt.Errorf("deserializing account: %w", err) + } + return &item, nil +} + +// RPCClient is the minimal RPC interface needed by the client. +type RPCClient interface { + GetAccountInfo(ctx context.Context, account solana.PublicKey) (*rpc.GetAccountInfoResult, error) + GetProgramAccounts(ctx context.Context, publicKey solana.PublicKey) (rpc.GetProgramAccountsResult, error) + GetProgramAccountsWithOpts(ctx context.Context, publicKey solana.PublicKey, opts *rpc.GetProgramAccountsOpts) (rpc.GetProgramAccountsResult, error) + GetMinimumBalanceForRentExemption(ctx context.Context, dataSize uint64, commitment rpc.CommitmentType) (uint64, error) +} + +// LedgerRecordClient fetches off-chain record data from the DZ Ledger. +type LedgerRecordClient interface { + GetRecordData(ctx context.Context, account solana.PublicKey) ([]byte, error) +} + +// Client provides read-only access to revenue distribution program accounts. +type Client struct { + rpc RPCClient + programID solana.PublicKey + ledgerClient LedgerRecordClient +} + +// New creates a new revenue distribution client. +func New(rpc RPCClient, programID solana.PublicKey) *Client { + return &Client{ + rpc: rpc, + programID: programID, + } +} + +// NewWithLedger creates a new client with ledger record support. +func NewWithLedger(rpc RPCClient, programID solana.PublicKey, ledgerClient LedgerRecordClient) *Client { + return &Client{ + rpc: rpc, + programID: programID, + ledgerClient: ledgerClient, + } +} + +// NewForEnv creates a client configured for the given environment. +// Valid environments: "mainnet-beta", "testnet", "devnet", "localnet". +func NewForEnv(env string) *Client { + return New(NewRPCClient(SolanaRPCURLs[env]), ProgramID) +} + +// NewForEnvWithLedger creates a client with ledger support for the given environment. +// Valid environments: "mainnet-beta", "testnet", "devnet", "localnet". +func NewForEnvWithLedger(env string, ledgerClient LedgerRecordClient) *Client { + return NewWithLedger(NewRPCClient(SolanaRPCURLs[env]), ProgramID, ledgerClient) +} + +// NewMainnetBeta creates a client configured for mainnet-beta. +func NewMainnetBeta() *Client { + return NewForEnv("mainnet-beta") +} + +// NewTestnet creates a client configured for testnet. +func NewTestnet() *Client { + return NewForEnv("testnet") +} + +// NewDevnet creates a client configured for devnet. +func NewDevnet() *Client { + return NewForEnv("devnet") +} + +// NewLocalnet creates a client configured for localnet. +func NewLocalnet() *Client { + return NewForEnv("localnet") +} + +func (c *Client) FetchConfig(ctx context.Context) (*ProgramConfig, error) { + addr, _, err := DeriveConfigPDA(c.programID) + if err != nil { + return nil, fmt.Errorf("deriving config PDA: %w", err) + } + data, err := c.fetchAccountData(ctx, addr) + if err != nil { + return nil, err + } + return deserializeAccount[ProgramConfig](data, DiscriminatorProgramConfig) +} + +func (c *Client) FetchDistribution(ctx context.Context, epoch uint64) (*Distribution, error) { + addr, _, err := DeriveDistributionPDA(c.programID, epoch) + if err != nil { + return nil, fmt.Errorf("deriving distribution PDA: %w", err) + } + data, err := c.fetchAccountData(ctx, addr) + if err != nil { + return nil, err + } + return deserializeAccount[Distribution](data, DiscriminatorDistribution) +} + +func (c *Client) FetchJournal(ctx context.Context) (*Journal, error) { + addr, _, err := DeriveJournalPDA(c.programID) + if err != nil { + return nil, fmt.Errorf("deriving journal PDA: %w", err) + } + data, err := c.fetchAccountData(ctx, addr) + if err != nil { + return nil, err + } + return deserializeAccount[Journal](data, DiscriminatorJournal) +} + +func (c *Client) FetchValidatorDeposit(ctx context.Context, nodeID solana.PublicKey) (*SolanaValidatorDeposit, error) { + addr, _, err := DeriveValidatorDepositPDA(c.programID, nodeID) + if err != nil { + return nil, fmt.Errorf("deriving validator deposit PDA: %w", err) + } + data, err := c.fetchAccountData(ctx, addr) + if err != nil { + return nil, err + } + return deserializeAccount[SolanaValidatorDeposit](data, DiscriminatorSolanaValidatorDeposit) +} + +func (c *Client) FetchAllValidatorDeposits(ctx context.Context) ([]SolanaValidatorDeposit, error) { + return fetchAllByDiscriminator[SolanaValidatorDeposit](ctx, c, DiscriminatorSolanaValidatorDeposit) +} + +func (c *Client) FetchContributorRewards(ctx context.Context, serviceKey solana.PublicKey) (*ContributorRewards, error) { + addr, _, err := DeriveContributorRewardsPDA(c.programID, serviceKey) + if err != nil { + return nil, fmt.Errorf("deriving contributor rewards PDA: %w", err) + } + data, err := c.fetchAccountData(ctx, addr) + if err != nil { + return nil, err + } + return deserializeAccount[ContributorRewards](data, DiscriminatorContributorRewards) +} + +func (c *Client) FetchAllContributorRewards(ctx context.Context) ([]ContributorRewards, error) { + return fetchAllByDiscriminator[ContributorRewards](ctx, c, DiscriminatorContributorRewards) +} + +// ValidatorDepositBalance returns the effective deposit balance for a validator, +// computed as account_lamports - rent_exempt_minimum. +func (c *Client) ValidatorDepositBalance(ctx context.Context, nodeID solana.PublicKey) (uint64, error) { + addr, _, err := DeriveValidatorDepositPDA(c.programID, nodeID) + if err != nil { + return 0, fmt.Errorf("deriving validator deposit PDA: %w", err) + } + result, err := c.rpc.GetAccountInfo(ctx, addr) + if err != nil { + return 0, fmt.Errorf("fetching account: %w", err) + } + if result == nil || result.Value == nil { + return 0, ErrAccountNotFound + } + lamports := result.Value.Lamports + rentExempt, err := c.rpc.GetMinimumBalanceForRentExemption(ctx, uint64(len(result.Value.Data.GetBinary())), rpc.CommitmentFinalized) + if err != nil { + return 0, fmt.Errorf("fetching rent exemption: %w", err) + } + if lamports <= rentExempt { + return 0, nil + } + return lamports - rentExempt, nil +} + +// FetchValidatorDebts fetches and deserializes the off-chain validator debt +// record for the given DZ epoch from the DZ Ledger. +func (c *Client) FetchValidatorDebts(ctx context.Context, epoch uint64) (*ComputedSolanaValidatorDebts, error) { + if c.ledgerClient == nil { + return nil, ErrLedgerClientNil + } + config, err := c.FetchConfig(ctx) + if err != nil { + return nil, fmt.Errorf("fetching config for debt accountant key: %w", err) + } + epochBytes := make([]byte, 8) + binary.LittleEndian.PutUint64(epochBytes, epoch) + addr, err := DeriveRecordKey(config.DebtAccountantKey, [][]byte{ + seedSolanaValidatorDebt, + epochBytes, + }) + if err != nil { + return nil, fmt.Errorf("deriving validator debt record key: %w", err) + } + data, err := c.ledgerClient.GetRecordData(ctx, addr) + if err != nil { + return nil, fmt.Errorf("fetching validator debt record: %w", err) + } + if len(data) <= recordHeaderSize { + return nil, fmt.Errorf("validator debt record data too short: %d bytes", len(data)) + } + var debts ComputedSolanaValidatorDebts + decoder := ag_binary.NewBorshDecoder(data[recordHeaderSize:]) + if err := decoder.Decode(&debts); err != nil { + return nil, fmt.Errorf("deserializing validator debts: %w", err) + } + return &debts, nil +} + +// FetchRewardShares fetches and deserializes the off-chain Shapley output +// record for the given DZ epoch from the DZ Ledger. +func (c *Client) FetchRewardShares(ctx context.Context, epoch uint64) (*ShapleyOutputStorage, error) { + if c.ledgerClient == nil { + return nil, ErrLedgerClientNil + } + config, err := c.FetchConfig(ctx) + if err != nil { + return nil, fmt.Errorf("fetching config for rewards accountant key: %w", err) + } + epochBytes := make([]byte, 8) + binary.LittleEndian.PutUint64(epochBytes, epoch) + addr, err := DeriveRecordKey(config.RewardsAccountantKey, [][]byte{ + seedDZContributorRewards, + epochBytes, + seedShapleyOutput, + }) + if err != nil { + return nil, fmt.Errorf("deriving reward shares record key: %w", err) + } + data, err := c.ledgerClient.GetRecordData(ctx, addr) + if err != nil { + return nil, fmt.Errorf("fetching reward shares record: %w", err) + } + if len(data) <= recordHeaderSize { + return nil, fmt.Errorf("reward shares record data too short: %d bytes", len(data)) + } + var output ShapleyOutputStorage + decoder := ag_binary.NewBorshDecoder(data[recordHeaderSize:]) + if err := decoder.Decode(&output); err != nil { + return nil, fmt.Errorf("deserializing reward shares: %w", err) + } + return &output, nil +} + +func (c *Client) fetchAccountData(ctx context.Context, addr solana.PublicKey) ([]byte, error) { + result, err := c.rpc.GetAccountInfo(ctx, addr) + if err != nil { + return nil, fmt.Errorf("fetching account %s: %w", addr, err) + } + if result == nil || result.Value == nil { + return nil, ErrAccountNotFound + } + return result.Value.Data.GetBinary(), nil +} + +func fetchAllByDiscriminator[T any](ctx context.Context, c *Client, disc [8]byte) ([]T, error) { + opts := &rpc.GetProgramAccountsOpts{ + Filters: []rpc.RPCFilter{ + { + Memcmp: &rpc.RPCFilterMemcmp{ + Offset: 0, + Bytes: disc[:], + }, + }, + }, + } + accounts, err := c.rpc.GetProgramAccountsWithOpts(ctx, c.programID, opts) + if err != nil { + return nil, fmt.Errorf("fetching program accounts: %w", err) + } + results := make([]T, 0, len(accounts)) + for _, acct := range accounts { + data := acct.Account.Data.GetBinary() + item, err := deserializeAccount[T](data, disc) + if err != nil { + return nil, fmt.Errorf("deserializing account %s: %w", acct.Pubkey, err) + } + results = append(results, *item) + } + return results, nil +} diff --git a/sdk/revdist/go/client_test.go b/sdk/revdist/go/client_test.go new file mode 100644 index 000000000..1131af920 --- /dev/null +++ b/sdk/revdist/go/client_test.go @@ -0,0 +1,131 @@ +package revdist + +import ( + "context" + "encoding/binary" + "testing" + + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" +) + +type mockRPC struct { + accounts map[solana.PublicKey]*rpc.Account +} + +func (m *mockRPC) GetAccountInfo(_ context.Context, account solana.PublicKey) (*rpc.GetAccountInfoResult, error) { + acct, ok := m.accounts[account] + if !ok { + return &rpc.GetAccountInfoResult{}, nil + } + return &rpc.GetAccountInfoResult{Value: acct}, nil +} + +func (m *mockRPC) GetProgramAccounts(_ context.Context, _ solana.PublicKey) (rpc.GetProgramAccountsResult, error) { + return nil, nil +} + +func (m *mockRPC) GetProgramAccountsWithOpts(_ context.Context, _ solana.PublicKey, _ *rpc.GetProgramAccountsOpts) (rpc.GetProgramAccountsResult, error) { + return nil, nil +} + +func (m *mockRPC) GetMinimumBalanceForRentExemption(_ context.Context, _ uint64, _ rpc.CommitmentType) (uint64, error) { + return 890880, nil +} + +func buildAccountData(disc [8]byte, structSize int) []byte { + data := make([]byte, discriminatorSize+structSize) + copy(data[:8], disc[:]) + return data +} + +func TestFetchJournal(t *testing.T) { + programID := testProgramID + journalAddr, _, _ := DeriveJournalPDA(programID) + + data := buildAccountData(DiscriminatorJournal, 64) + // Set TotalSOLBalance at offset 8+8 = 16 from start of data. + binary.LittleEndian.PutUint64(data[discriminatorSize+8:], 12345) + + mock := &mockRPC{ + accounts: map[solana.PublicKey]*rpc.Account{ + journalAddr: { + Data: rpc.DataBytesOrJSONFromBytes(data), + }, + }, + } + + client := New(mock, programID) + journal, err := client.FetchJournal(context.Background()) + if err != nil { + t.Fatalf("FetchJournal: %v", err) + } + if journal.TotalSOLBalance != 12345 { + t.Errorf("TotalSOLBalance = %d, want 12345", journal.TotalSOLBalance) + } +} + +func TestFetchConfig(t *testing.T) { + programID := testProgramID + configAddr, _, _ := DeriveConfigPDA(programID) + + data := buildAccountData(DiscriminatorProgramConfig, 600) + // Set NextCompletedDZEpoch at offset 8+8 = 16 from start. + binary.LittleEndian.PutUint64(data[discriminatorSize+8:], 42) + + mock := &mockRPC{ + accounts: map[solana.PublicKey]*rpc.Account{ + configAddr: { + Data: rpc.DataBytesOrJSONFromBytes(data), + }, + }, + } + + client := New(mock, programID) + config, err := client.FetchConfig(context.Background()) + if err != nil { + t.Fatalf("FetchConfig: %v", err) + } + if config.NextCompletedDZEpoch != 42 { + t.Errorf("NextCompletedDZEpoch = %d, want 42", config.NextCompletedDZEpoch) + } +} + +func TestFetchAccountNotFound(t *testing.T) { + mock := &mockRPC{accounts: map[solana.PublicKey]*rpc.Account{}} + client := New(mock, testProgramID) + _, err := client.FetchJournal(context.Background()) + if err != ErrAccountNotFound { + t.Errorf("expected ErrAccountNotFound, got %v", err) + } +} + +func TestInvalidDiscriminator(t *testing.T) { + programID := testProgramID + journalAddr, _, _ := DeriveJournalPDA(programID) + + data := make([]byte, discriminatorSize+64) + // Leave discriminator as zeros (invalid). + + mock := &mockRPC{ + accounts: map[solana.PublicKey]*rpc.Account{ + journalAddr: { + Data: rpc.DataBytesOrJSONFromBytes(data), + }, + }, + } + + client := New(mock, programID) + _, err := client.FetchJournal(context.Background()) + if err == nil { + t.Fatal("expected error for invalid discriminator") + } +} + +func TestFetchValidatorDebtsNoLedger(t *testing.T) { + client := New(&mockRPC{accounts: map[solana.PublicKey]*rpc.Account{}}, testProgramID) + _, err := client.FetchValidatorDebts(context.Background(), 1) + if err != ErrLedgerClientNil { + t.Errorf("expected ErrLedgerClientNil, got %v", err) + } +} diff --git a/sdk/revdist/go/compat_test.go b/sdk/revdist/go/compat_test.go new file mode 100644 index 000000000..e3387a976 --- /dev/null +++ b/sdk/revdist/go/compat_test.go @@ -0,0 +1,382 @@ +package revdist + +import ( + "context" + "encoding/binary" + "os" + "testing" + + "github.com/gagliardetto/solana-go" + solanarpc "github.com/gagliardetto/solana-go/rpc" + "github.com/malbeclabs/doublezero/config" +) + +// These tests fetch live mainnet data and verify that our struct deserialization +// matches raw byte reads at known offsets. Run with: +// +// go test -tags compat -run TestCompat -v ./sdk/revdist/go/ +// +// Requires network access to Solana mainnet RPC. + +func skipUnlessCompat(t *testing.T) { + t.Helper() + if os.Getenv("REVDIST_COMPAT_TEST") == "" { + t.Skip("set REVDIST_COMPAT_TEST=1 to run compatibility tests against mainnet") + } +} + +// rpcLedgerClient implements LedgerRecordClient using a Solana RPC endpoint. +type rpcLedgerClient struct { + rpc *solanarpc.Client +} + +func (c *rpcLedgerClient) GetRecordData(ctx context.Context, account solana.PublicKey) ([]byte, error) { + result, err := c.rpc.GetAccountInfo(ctx, account) + if err != nil { + return nil, err + } + if result == nil || result.Value == nil { + return nil, ErrAccountNotFound + } + return result.Value.Data.GetBinary(), nil +} + +func compatNetworkConfig(t *testing.T) *config.NetworkConfig { + t.Helper() + cfg, err := config.NetworkConfigForEnv(config.EnvMainnetBeta) + if err != nil { + t.Fatalf("NetworkConfigForEnv: %v", err) + } + return cfg +} + +func compatClient(t *testing.T) (*Client, solana.PublicKey) { + t.Helper() + cfg := compatNetworkConfig(t) + rpcClient := NewRPCClient(cfg.SolanaRPCURL) + ledger := &rpcLedgerClient{rpc: NewRPCClient(cfg.LedgerPublicRPCURL)} + return NewWithLedger(rpcClient, cfg.RevenueDistributionProgramID, ledger), cfg.RevenueDistributionProgramID +} + +func compatRPCClient(t *testing.T) *solanarpc.Client { + t.Helper() + cfg := compatNetworkConfig(t) + return NewRPCClient(cfg.SolanaRPCURL) +} + +func fetchRawAccount(t *testing.T, rpcClient *solanarpc.Client, addr solana.PublicKey) []byte { + t.Helper() + result, err := rpcClient.GetAccountInfo(context.Background(), addr) + if err != nil { + t.Fatalf("fetching %s: %v", addr, err) + } + if result == nil || result.Value == nil { + t.Fatalf("account %s not found", addr) + } + return result.Value.Data.GetBinary() +} + +func TestCompatProgramConfig(t *testing.T) { + skipUnlessCompat(t) + client, programID := compatClient(t) + ctx := context.Background() + + progConfig, err := client.FetchConfig(ctx) + if err != nil { + t.Fatalf("FetchConfig: %v", err) + } + + // Fetch raw bytes for independent verification. + addr, _, _ := DeriveConfigPDA(programID) + raw := fetchRawAccount(t, compatRPCClient(t), addr) + + // Verify discriminator. + if err := validateDiscriminator(raw, DiscriminatorProgramConfig); err != nil { + t.Fatalf("discriminator: %v", err) + } + + // Verify fields at known raw byte offsets (offset = struct_offset + 8 for discriminator). + assertU64(t, raw, 8, progConfig.Flags, "Flags") + assertU64(t, raw, 16, progConfig.NextCompletedDZEpoch, "NextCompletedDZEpoch") + assertU8(t, raw, 24, progConfig.BumpSeed, "BumpSeed") + assertPubkey(t, raw, 32, progConfig.AdminKey, "AdminKey") + assertPubkey(t, raw, 64, progConfig.DebtAccountantKey, "DebtAccountantKey") + assertPubkey(t, raw, 96, progConfig.RewardsAccountantKey, "RewardsAccountantKey") + assertPubkey(t, raw, 128, progConfig.ContributorManagerKey, "ContributorManagerKey") + assertPubkey(t, raw, 192, progConfig.SOL2ZSwapProgramID, "SOL2ZSwapProgramID") + + // DistributionParameters starts at raw offset 224. + dp := progConfig.DistributionParameters + assertU16(t, raw, 224, dp.CalculationGracePeriodMinutes, "CalculationGracePeriodMinutes") + assertU16(t, raw, 226, dp.InitializationGracePeriodMinutes, "InitializationGracePeriodMinutes") + assertU8(t, raw, 228, dp.MinimumEpochDurationToFinalizeRewards, "MinEpochDuration") + + // CommunityBurnRateParameters at raw offset 232. + cb := dp.CommunityBurnRateParameters + assertU32(t, raw, 232, cb.Limit, "BurnRateLimit") + assertU32(t, raw, 236, cb.DZEpochsToIncreasing, "DZEpochsToIncreasing") + assertU32(t, raw, 240, cb.DZEpochsToLimit, "DZEpochsToLimit") + + // SolanaValidatorFeeParameters at raw offset 256. + vf := dp.SolanaValidatorFeeParameters + assertU16(t, raw, 256, vf.BaseBlockRewardsPct, "BaseBlockRewardsPct") + assertU16(t, raw, 258, vf.PriorityBlockRewardsPct, "PriorityBlockRewardsPct") + assertU16(t, raw, 260, vf.InflationRewardsPct, "InflationRewardsPct") + assertU16(t, raw, 262, vf.JitoTipsPct, "JitoTipsPct") + assertU32(t, raw, 264, vf.FixedSOLAmount, "FixedSOLAmount") + + // RelayParameters at raw offset 552 (224 + 328). + rp := progConfig.RelayParameters + assertU32(t, raw, 552, rp.PlaceholderLamports, "PlaceholderLamports") + assertU32(t, raw, 556, rp.DistributeRewardsLamports, "DistributeRewardsLamports") + + // DebtWriteOffFeatureActivationEpoch at raw offset 600 (552 + 40 + 4 + 4). + assertU64(t, raw, 600, progConfig.DebtWriteOffFeatureActivationEpoch, "DebtWriteOffEpoch") + + // Sanity: epoch should be > 0 on mainnet. + if progConfig.NextCompletedDZEpoch == 0 { + t.Error("NextCompletedDZEpoch is 0, expected > 0 on mainnet") + } +} + +func TestCompatDistribution(t *testing.T) { + skipUnlessCompat(t) + client, programID := compatClient(t) + ctx := context.Background() + + // Fetch config to get the latest epoch. + progConfig, err := client.FetchConfig(ctx) + if err != nil { + t.Fatalf("FetchConfig: %v", err) + } + epoch := progConfig.NextCompletedDZEpoch - 1 + + dist, err := client.FetchDistribution(ctx, epoch) + if err != nil { + t.Fatalf("FetchDistribution(%d): %v", epoch, err) + } + + addr, _, _ := DeriveDistributionPDA(programID, epoch) + raw := fetchRawAccount(t, compatRPCClient(t), addr) + + if err := validateDiscriminator(raw, DiscriminatorDistribution); err != nil { + t.Fatalf("discriminator: %v", err) + } + + assertU64(t, raw, 8, dist.DZEpoch, "DZEpoch") + if dist.DZEpoch != epoch { + t.Errorf("DZEpoch = %d, want %d", dist.DZEpoch, epoch) + } + assertU64(t, raw, 16, dist.Flags, "Flags") + assertU32(t, raw, 24, dist.CommunityBurnRate, "CommunityBurnRate") + + // SolanaValidatorFeeParameters at raw offset 32 (struct offset 24), 40 bytes. + vf := dist.SolanaValidatorFeeParameters + assertU16(t, raw, 32, vf.BaseBlockRewardsPct, "BaseBlockRewardsPct") + assertU16(t, raw, 34, vf.PriorityBlockRewardsPct, "PriorityBlockRewardsPct") + assertU16(t, raw, 36, vf.InflationRewardsPct, "InflationRewardsPct") + assertU16(t, raw, 38, vf.JitoTipsPct, "JitoTipsPct") + assertU32(t, raw, 40, vf.FixedSOLAmount, "FixedSOLAmount") + + // SolanaValidatorDebtMerkleRoot at raw offset 72 (32 bytes), skip direct comparison. + assertU32(t, raw, 104, dist.TotalSolanaValidators, "TotalSolanaValidators") + assertU32(t, raw, 108, dist.SolanaValidatorPaymentsCount, "SolanaValidatorPaymentsCount") + assertU64(t, raw, 112, dist.TotalSolanaValidatorDebt, "TotalSolanaValidatorDebt") + assertU64(t, raw, 120, dist.CollectedSolanaValidatorPayments, "CollectedPayments") + // RewardsMerkleRoot at raw offset 128 (32 bytes). + assertU32(t, raw, 160, dist.TotalContributors, "TotalContributors") + assertU32(t, raw, 164, dist.DistributedRewardsCount, "DistributedRewardsCount") + assertU64(t, raw, 168, dist.CollectedPrepaid2ZPayments, "CollectedPrepaid2ZPayments") + assertU64(t, raw, 176, dist.Collected2ZConvertedFromSOL, "Collected2ZConvertedFromSOL") + assertU64(t, raw, 184, dist.UncollectibleSOLDebt, "UncollectibleSOLDebt") + assertU64(t, raw, 216, dist.Distributed2ZAmount, "Distributed2ZAmount") + assertU64(t, raw, 224, dist.Burned2ZAmount, "Burned2ZAmount") +} + +func TestCompatJournal(t *testing.T) { + skipUnlessCompat(t) + client, programID := compatClient(t) + ctx := context.Background() + + journal, err := client.FetchJournal(ctx) + if err != nil { + t.Fatalf("FetchJournal: %v", err) + } + + addr, _, _ := DeriveJournalPDA(programID) + raw := fetchRawAccount(t, compatRPCClient(t), addr) + + if err := validateDiscriminator(raw, DiscriminatorJournal); err != nil { + t.Fatalf("discriminator: %v", err) + } + + assertU8(t, raw, 8, journal.BumpSeed, "BumpSeed") + assertU64(t, raw, 16, journal.TotalSOLBalance, "TotalSOLBalance") + assertU64(t, raw, 24, journal.Total2ZBalance, "Total2ZBalance") + assertU64(t, raw, 32, journal.Swap2ZDestinationBalance, "Swap2ZDestinationBalance") + assertU64(t, raw, 40, journal.SwappedSOLAmount, "SwappedSOLAmount") + assertU64(t, raw, 48, journal.NextDZEpochToSweepTokens, "NextDZEpochToSweepTokens") +} + +func TestCompatValidatorDeposit(t *testing.T) { + skipUnlessCompat(t) + client, _ := compatClient(t) + ctx := context.Background() + + deposits, err := client.FetchAllValidatorDeposits(ctx) + if err != nil { + t.Fatalf("FetchAllValidatorDeposits: %v", err) + } + if len(deposits) == 0 { + t.Fatal("no deposits found on mainnet") + } + + // Verify we can look up a specific deposit by its node ID. + first := deposits[0] + single, err := client.FetchValidatorDeposit(ctx, first.NodeID) + if err != nil { + t.Fatalf("FetchValidatorDeposit(%s): %v", first.NodeID, err) + } + if single.NodeID != first.NodeID { + t.Errorf("NodeID mismatch: single=%s, list=%s", single.NodeID, first.NodeID) + } + if single.WrittenOffSOLDebt != first.WrittenOffSOLDebt { + t.Errorf("WrittenOffSOLDebt mismatch: single=%d, list=%d", single.WrittenOffSOLDebt, first.WrittenOffSOLDebt) + } + + t.Logf("validated %d deposits, spot-checked %s", len(deposits), first.NodeID) +} + +func TestCompatContributorRewards(t *testing.T) { + skipUnlessCompat(t) + client, _ := compatClient(t) + ctx := context.Background() + + rewards, err := client.FetchAllContributorRewards(ctx) + if err != nil { + t.Fatalf("FetchAllContributorRewards: %v", err) + } + if len(rewards) == 0 { + t.Fatal("no contributor rewards found on mainnet") + } + + // Verify single lookup matches list. + first := rewards[0] + single, err := client.FetchContributorRewards(ctx, first.ServiceKey) + if err != nil { + t.Fatalf("FetchContributorRewards(%s): %v", first.ServiceKey, err) + } + if single.ServiceKey != first.ServiceKey { + t.Errorf("ServiceKey mismatch") + } + if single.RewardsManagerKey != first.RewardsManagerKey { + t.Errorf("RewardsManagerKey mismatch") + } + if single.Flags != first.Flags { + t.Errorf("Flags mismatch") + } + + t.Logf("validated %d contributors, spot-checked %s", len(rewards), first.ServiceKey) +} + +func TestCompatValidatorDebts(t *testing.T) { + skipUnlessCompat(t) + client, _ := compatClient(t) + ctx := context.Background() + + // Fetch config and use an older epoch that is more likely to have ledger records. + progConfig, err := client.FetchConfig(ctx) + if err != nil { + t.Fatalf("FetchConfig: %v", err) + } + epoch := progConfig.NextCompletedDZEpoch - 5 + + debts, err := client.FetchValidatorDebts(ctx, epoch) + if err != nil { + t.Fatalf("FetchValidatorDebts(%d): %v", epoch, err) + } + + if debts.LastSolanaEpoch == 0 { + t.Error("LastSolanaEpoch is 0, expected > 0 on mainnet") + } + if debts.FirstSolanaEpoch > debts.LastSolanaEpoch { + t.Errorf("FirstSolanaEpoch (%d) > LastSolanaEpoch (%d)", debts.FirstSolanaEpoch, debts.LastSolanaEpoch) + } + + t.Logf("epoch %d: %d validator debts, solana epochs %d-%d", + epoch, len(debts.Debts), debts.FirstSolanaEpoch, debts.LastSolanaEpoch) +} + +func TestCompatRewardShares(t *testing.T) { + skipUnlessCompat(t) + client, _ := compatClient(t) + ctx := context.Background() + + // Fetch config and use an older epoch that is more likely to have ledger records. + progConfig, err := client.FetchConfig(ctx) + if err != nil { + t.Fatalf("FetchConfig: %v", err) + } + epoch := progConfig.NextCompletedDZEpoch - 5 + + shares, err := client.FetchRewardShares(ctx, epoch) + if err != nil { + t.Fatalf("FetchRewardShares(%d): %v", epoch, err) + } + + if shares.Epoch != epoch { + t.Errorf("Epoch = %d, want %d", shares.Epoch, epoch) + } + if len(shares.Rewards) == 0 { + t.Error("no reward shares found on mainnet") + } + if shares.TotalUnitShares == 0 { + t.Error("TotalUnitShares is 0, expected > 0") + } + + t.Logf("epoch %d: %d reward shares, total unit shares %d", + epoch, len(shares.Rewards), shares.TotalUnitShares) +} + +// Helpers to compare deserialized values against raw byte reads. + +func assertU8(t *testing.T, raw []byte, offset int, got uint8, name string) { + t.Helper() + want := raw[offset] + if got != want { + t.Errorf("%s: deserialized=%d, raw[%d]=%d", name, got, offset, want) + } +} + +func assertU16(t *testing.T, raw []byte, offset int, got uint16, name string) { + t.Helper() + want := binary.LittleEndian.Uint16(raw[offset:]) + if got != want { + t.Errorf("%s: deserialized=%d, raw[%d]=%d", name, got, offset, want) + } +} + +func assertU32(t *testing.T, raw []byte, offset int, got uint32, name string) { + t.Helper() + want := binary.LittleEndian.Uint32(raw[offset:]) + if got != want { + t.Errorf("%s: deserialized=%d, raw[%d]=%d", name, got, offset, want) + } +} + +func assertU64(t *testing.T, raw []byte, offset int, got uint64, name string) { + t.Helper() + want := binary.LittleEndian.Uint64(raw[offset:]) + if got != want { + t.Errorf("%s: deserialized=%d, raw[%d]=%d", name, got, offset, want) + } +} + +func assertPubkey(t *testing.T, raw []byte, offset int, got solana.PublicKey, name string) { + t.Helper() + var want solana.PublicKey + copy(want[:], raw[offset:offset+32]) + if got != want { + t.Errorf("%s: deserialized=%s, raw[%d]=%s", name, got, offset, want) + } +} diff --git a/sdk/revdist/go/config.go b/sdk/revdist/go/config.go new file mode 100644 index 000000000..caae773bc --- /dev/null +++ b/sdk/revdist/go/config.go @@ -0,0 +1,22 @@ +package revdist + +import "github.com/gagliardetto/solana-go" + +// ProgramID is the revenue distribution program ID (same across all environments). +var ProgramID = solana.MustPublicKeyFromBase58("dzrevZC94tBLwuHw1dyynZxaXTWyp7yocsinyEVPtt4") + +// SolanaRPCURLs are the Solana RPC URLs per environment. +var SolanaRPCURLs = map[string]string{ + "mainnet-beta": "https://api.mainnet-beta.solana.com", + "testnet": "https://api.testnet.solana.com", + "devnet": "https://api.devnet.solana.com", + "localnet": "http://localhost:8899", +} + +// LedgerRPCURLs are the DZ Ledger RPC URLs per environment. +var LedgerRPCURLs = map[string]string{ + "mainnet-beta": "https://doublezero-mainnet-beta.rpcpool.com/db336024-e7a8-46b1-80e5-352dd77060ab", + "testnet": "https://doublezerolocalnet.rpcpool.com/8a4fd3f4-0977-449f-88c7-63d4b0f10f16", + "devnet": "https://doublezerolocalnet.rpcpool.com/8a4fd3f4-0977-449f-88c7-63d4b0f10f16", + "localnet": "http://localhost:8899", +} diff --git a/sdk/revdist/go/discriminator.go b/sdk/revdist/go/discriminator.go new file mode 100644 index 000000000..ea38bbd26 --- /dev/null +++ b/sdk/revdist/go/discriminator.go @@ -0,0 +1,38 @@ +package revdist + +import ( + "crypto/sha256" + "errors" + "fmt" +) + +const discriminatorSize = 8 + +var ( + DiscriminatorProgramConfig = sha256First8("dz::account::program_config") + DiscriminatorDistribution = sha256First8("dz::account::distribution") + DiscriminatorSolanaValidatorDeposit = sha256First8("dz::account::solana_validator_deposit") + DiscriminatorContributorRewards = sha256First8("dz::account::contributor_rewards") + DiscriminatorJournal = sha256First8("dz::account::journal") + + ErrInvalidDiscriminator = errors.New("invalid account discriminator") +) + +func sha256First8(s string) [8]byte { + h := sha256.Sum256([]byte(s)) + var disc [8]byte + copy(disc[:], h[:8]) + return disc +} + +func validateDiscriminator(data []byte, expected [8]byte) error { + if len(data) < discriminatorSize { + return fmt.Errorf("%w: data too short", ErrInvalidDiscriminator) + } + var got [8]byte + copy(got[:], data[:8]) + if got != expected { + return fmt.Errorf("%w: got %x, want %x", ErrInvalidDiscriminator, got, expected) + } + return nil +} diff --git a/sdk/revdist/go/fixture_test.go b/sdk/revdist/go/fixture_test.go new file mode 100644 index 000000000..ab0a1197f --- /dev/null +++ b/sdk/revdist/go/fixture_test.go @@ -0,0 +1,242 @@ +package revdist + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "reflect" + "runtime" + "strconv" + "testing" + "unsafe" + + "github.com/gagliardetto/solana-go" +) + +// These tests deserialize binary fixtures generated by the Rust fixture +// generator (testdata/generate-fixtures) and verify that Go's deserialized +// field values match the expected values from the JSON sidecar files. +// +// The fixtures are authoritative because they are produced by bytemuck::bytes_of +// on real Rust struct instances — the byte layout comes from the Rust compiler's +// #[repr(C)] layout, not from hand-coded offsets. +// +// Regenerate fixtures: +// cd ../testdata/fixtures/generate-fixtures && cargo run + +type fixtureMeta struct { + Name string `json:"name"` + StructSize int `json:"struct_size"` + DiscriminatorHex string `json:"discriminator_hex"` + Fields []fieldValue `json:"fields"` +} + +type fieldValue struct { + Name string `json:"name"` + Value string `json:"value"` + Type string `json:"typ"` +} + +func fixturesDir() string { + _, filename, _, _ := runtime.Caller(0) + return filepath.Join(filepath.Dir(filename), "..", "testdata", "fixtures") +} + +func loadFixture(t *testing.T, name string) ([]byte, fixtureMeta) { + t.Helper() + dir := fixturesDir() + + binData, err := os.ReadFile(filepath.Join(dir, name+".bin")) + if err != nil { + t.Fatalf("reading %s.bin: %v", name, err) + } + + jsonData, err := os.ReadFile(filepath.Join(dir, name+".json")) + if err != nil { + t.Fatalf("reading %s.json: %v", name, err) + } + + var meta fixtureMeta + if err := json.Unmarshal(jsonData, &meta); err != nil { + t.Fatalf("parsing %s.json: %v", name, err) + } + + return binData, meta +} + +func TestFixtureProgramConfig(t *testing.T) { + data, meta := loadFixture(t, "program_config") + + if got := int(unsafe.Sizeof(ProgramConfig{})); got != meta.StructSize { + t.Fatalf("sizeof(ProgramConfig) = %d, Rust says %d", got, meta.StructSize) + } + + config, err := deserializeAccount[ProgramConfig](data, DiscriminatorProgramConfig) + if err != nil { + t.Fatalf("deserializing: %v", err) + } + + assertFields(t, meta.Fields, map[string]any{ + "Flags": config.Flags, + "NextCompletedDZEpoch": config.NextCompletedDZEpoch, + "BumpSeed": config.BumpSeed, + "AdminKey": config.AdminKey, + "DebtAccountantKey": config.DebtAccountantKey, + "RewardsAccountantKey": config.RewardsAccountantKey, + "ContributorManagerKey": config.ContributorManagerKey, + "SOL2ZSwapProgramID": config.SOL2ZSwapProgramID, + "CalculationGracePeriodMinutes": config.DistributionParameters.CalculationGracePeriodMinutes, + "InitializationGracePeriodMinutes": config.DistributionParameters.InitializationGracePeriodMinutes, + "MinimumEpochDurationToFinalizeRewards": config.DistributionParameters.MinimumEpochDurationToFinalizeRewards, + "BurnRateLimit": config.DistributionParameters.CommunityBurnRateParameters.Limit, + "BurnRateDZEpochsToIncreasing": config.DistributionParameters.CommunityBurnRateParameters.DZEpochsToIncreasing, + "BurnRateDZEpochsToLimit": config.DistributionParameters.CommunityBurnRateParameters.DZEpochsToLimit, + "BaseBlockRewardsPct": config.DistributionParameters.SolanaValidatorFeeParameters.BaseBlockRewardsPct, + "PriorityBlockRewardsPct": config.DistributionParameters.SolanaValidatorFeeParameters.PriorityBlockRewardsPct, + "InflationRewardsPct": config.DistributionParameters.SolanaValidatorFeeParameters.InflationRewardsPct, + "JitoTipsPct": config.DistributionParameters.SolanaValidatorFeeParameters.JitoTipsPct, + "FixedSOLAmount": config.DistributionParameters.SolanaValidatorFeeParameters.FixedSOLAmount, + "DistributeRewardsLamports": config.RelayParameters.DistributeRewardsLamports, + "DebtWriteOffFeatureActivationEpoch": config.DebtWriteOffFeatureActivationEpoch, + }) +} + +func TestFixtureDistribution(t *testing.T) { + data, meta := loadFixture(t, "distribution") + + if got := int(unsafe.Sizeof(Distribution{})); got != meta.StructSize { + t.Fatalf("sizeof(Distribution) = %d, Rust says %d", got, meta.StructSize) + } + + dist, err := deserializeAccount[Distribution](data, DiscriminatorDistribution) + if err != nil { + t.Fatalf("deserializing: %v", err) + } + + assertFields(t, meta.Fields, map[string]any{ + "DZEpoch": dist.DZEpoch, + "Flags": dist.Flags, + "CommunityBurnRate": dist.CommunityBurnRate, + "BaseBlockRewardsPct": dist.SolanaValidatorFeeParameters.BaseBlockRewardsPct, + "PriorityBlockRewardsPct": dist.SolanaValidatorFeeParameters.PriorityBlockRewardsPct, + "InflationRewardsPct": dist.SolanaValidatorFeeParameters.InflationRewardsPct, + "JitoTipsPct": dist.SolanaValidatorFeeParameters.JitoTipsPct, + "FixedSOLAmount": dist.SolanaValidatorFeeParameters.FixedSOLAmount, + "TotalSolanaValidators": dist.TotalSolanaValidators, + "SolanaValidatorPaymentsCount": dist.SolanaValidatorPaymentsCount, + "TotalSolanaValidatorDebt": dist.TotalSolanaValidatorDebt, + "CollectedSolanaValidatorPayments": dist.CollectedSolanaValidatorPayments, + "TotalContributors": dist.TotalContributors, + "DistributedRewardsCount": dist.DistributedRewardsCount, + "CollectedPrepaid2ZPayments": dist.CollectedPrepaid2ZPayments, + "Collected2ZConvertedFromSOL": dist.Collected2ZConvertedFromSOL, + "UncollectibleSOLDebt": dist.UncollectibleSOLDebt, + "Distributed2ZAmount": dist.Distributed2ZAmount, + "Burned2ZAmount": dist.Burned2ZAmount, + "SolanaValidatorWriteOffCount": dist.SolanaValidatorWriteOffCount, + }) +} + +func TestFixtureJournal(t *testing.T) { + data, meta := loadFixture(t, "journal") + + if got := int(unsafe.Sizeof(Journal{})); got != meta.StructSize { + t.Fatalf("sizeof(Journal) = %d, Rust says %d", got, meta.StructSize) + } + + journal, err := deserializeAccount[Journal](data, DiscriminatorJournal) + if err != nil { + t.Fatalf("deserializing: %v", err) + } + + assertFields(t, meta.Fields, map[string]any{ + "BumpSeed": journal.BumpSeed, + "TotalSOLBalance": journal.TotalSOLBalance, + "Total2ZBalance": journal.Total2ZBalance, + "Swap2ZDestinationBalance": journal.Swap2ZDestinationBalance, + "SwappedSOLAmount": journal.SwappedSOLAmount, + "NextDZEpochToSweepTokens": journal.NextDZEpochToSweepTokens, + }) +} + +func TestFixtureSolanaValidatorDeposit(t *testing.T) { + data, meta := loadFixture(t, "solana_validator_deposit") + + if got := int(unsafe.Sizeof(SolanaValidatorDeposit{})); got != meta.StructSize { + t.Fatalf("sizeof(SolanaValidatorDeposit) = %d, Rust says %d", got, meta.StructSize) + } + + deposit, err := deserializeAccount[SolanaValidatorDeposit](data, DiscriminatorSolanaValidatorDeposit) + if err != nil { + t.Fatalf("deserializing: %v", err) + } + + assertFields(t, meta.Fields, map[string]any{ + "NodeID": deposit.NodeID, + "WrittenOffSOLDebt": deposit.WrittenOffSOLDebt, + }) +} + +func TestFixtureContributorRewards(t *testing.T) { + data, meta := loadFixture(t, "contributor_rewards") + + if got := int(unsafe.Sizeof(ContributorRewards{})); got != meta.StructSize { + t.Fatalf("sizeof(ContributorRewards) = %d, Rust says %d", got, meta.StructSize) + } + + rewards, err := deserializeAccount[ContributorRewards](data, DiscriminatorContributorRewards) + if err != nil { + t.Fatalf("deserializing: %v", err) + } + + assertFields(t, meta.Fields, map[string]any{ + "RewardsManagerKey": rewards.RewardsManagerKey, + "ServiceKey": rewards.ServiceKey, + "Flags": rewards.Flags, + }) +} + +// assertFields checks each expected field from the JSON metadata against the +// deserialized Go value. +func assertFields(t *testing.T, expected []fieldValue, got map[string]any) { + t.Helper() + for _, f := range expected { + val, ok := got[f.Name] + if !ok { + t.Errorf("field %s: not found in Go struct map", f.Name) + continue + } + assertField(t, f, val) + } +} + +func assertField(t *testing.T, f fieldValue, got any) { + t.Helper() + switch f.Type { + case "u8": + want, _ := strconv.ParseUint(f.Value, 10, 8) + assertEq(t, f.Name, uint8(want), got) + case "u16": + want, _ := strconv.ParseUint(f.Value, 10, 16) + assertEq(t, f.Name, uint16(want), got) + case "u32": + want, _ := strconv.ParseUint(f.Value, 10, 32) + assertEq(t, f.Name, uint32(want), got) + case "u64": + want, _ := strconv.ParseUint(f.Value, 10, 64) + assertEq(t, f.Name, uint64(want), got) + case "pubkey": + want := solana.MustPublicKeyFromBase58(f.Value) + assertEq(t, f.Name, want, got) + default: + t.Errorf("field %s: unknown type %q", f.Name, f.Type) + } +} + +func assertEq(t *testing.T, name string, want, got any) { + t.Helper() + if !reflect.DeepEqual(want, got) { + t.Errorf("%s: want %v, got %v", name, want, fmt.Sprintf("%v", got)) + } +} diff --git a/sdk/revdist/go/oracle.go b/sdk/revdist/go/oracle.go new file mode 100644 index 000000000..1513e2e77 --- /dev/null +++ b/sdk/revdist/go/oracle.go @@ -0,0 +1,57 @@ +package revdist + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" +) + +// OracleClient fetches SOL/2Z swap rates from the oracle API. +type OracleClient struct { + baseURL string + http *http.Client +} + +// SwapRate contains the current SOL to 2Z swap rate and price data. +type SwapRate struct { + Rate float64 `json:"swapRate"` + Timestamp int64 `json:"timestamp"` + Signature string `json:"signature"` + SOLPriceUSD string `json:"solPriceUsd"` + TwoZPriceUSD string `json:"twozPriceUsd"` + CacheHit bool `json:"cacheHit"` +} + +// NewOracleClient creates a new oracle client with the given base URL. +func NewOracleClient(baseURL string) *OracleClient { + return &OracleClient{ + baseURL: baseURL, + http: &http.Client{Timeout: 30 * time.Second}, + } +} + +// FetchSwapRate fetches the current SOL/2Z swap rate from the oracle. +func (c *OracleClient) FetchSwapRate(ctx context.Context) (*SwapRate, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/swap-rate", nil) + if err != nil { + return nil, fmt.Errorf("creating request: %w", err) + } + + resp, err := c.http.Do(req) + if err != nil { + return nil, fmt.Errorf("fetching swap rate: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("oracle returned status %d", resp.StatusCode) + } + + var rate SwapRate + if err := json.NewDecoder(resp.Body).Decode(&rate); err != nil { + return nil, fmt.Errorf("decoding swap rate: %w", err) + } + return &rate, nil +} diff --git a/sdk/revdist/go/pda.go b/sdk/revdist/go/pda.go new file mode 100644 index 000000000..27192af72 --- /dev/null +++ b/sdk/revdist/go/pda.go @@ -0,0 +1,69 @@ +package revdist + +import ( + "crypto/sha256" + "encoding/binary" + + "github.com/gagliardetto/solana-go" + "github.com/mr-tron/base58" +) + +var ( + seedProgramConfig = []byte("program_config") + seedDistribution = []byte("distribution") + seedSolanaValidatorDeposit = []byte("solana_validator_deposit") + seedContributorRewards = []byte("contributor_rewards") + seedJournal = []byte("journal") + seedSolanaValidatorDebt = []byte("solana_validator_debt") + seedDZContributorRewards = []byte("dz_contributor_rewards") + seedShapleyOutput = []byte("shapley_output") +) + +// RecordProgramID is the on-chain program ID for the doublezero-record program. +var RecordProgramID = solana.MustPublicKeyFromBase58("dzrecxigtaZQ3gPmt2X5mDkYigaruFR1rHCqztFTvx7") + +// recordHeaderSize is the size of the RecordData header (version u8 + authority pubkey). +const recordHeaderSize = 33 + +// createRecordSeedString hashes the seeds with SHA256, encodes as base58, +// and truncates to 32 characters — matching the Rust create_record_seed_string. +func createRecordSeedString(seeds [][]byte) string { + h := sha256.New() + for _, s := range seeds { + h.Write(s) + } + encoded := base58.Encode(h.Sum(nil)) + if len(encoded) > 32 { + encoded = encoded[:32] + } + return encoded +} + +// DeriveRecordKey derives a ledger record address using create-with-seed, +// matching the Rust create_record_key function. +func DeriveRecordKey(payerKey solana.PublicKey, seeds [][]byte) (solana.PublicKey, error) { + seedStr := createRecordSeedString(seeds) + return solana.CreateWithSeed(payerKey, seedStr, RecordProgramID) +} + +func DeriveConfigPDA(programID solana.PublicKey) (solana.PublicKey, uint8, error) { + return solana.FindProgramAddress([][]byte{seedProgramConfig}, programID) +} + +func DeriveDistributionPDA(programID solana.PublicKey, epoch uint64) (solana.PublicKey, uint8, error) { + epochBytes := make([]byte, 8) + binary.LittleEndian.PutUint64(epochBytes, epoch) + return solana.FindProgramAddress([][]byte{seedDistribution, epochBytes}, programID) +} + +func DeriveJournalPDA(programID solana.PublicKey) (solana.PublicKey, uint8, error) { + return solana.FindProgramAddress([][]byte{seedJournal}, programID) +} + +func DeriveValidatorDepositPDA(programID solana.PublicKey, nodeID solana.PublicKey) (solana.PublicKey, uint8, error) { + return solana.FindProgramAddress([][]byte{seedSolanaValidatorDeposit, nodeID.Bytes()}, programID) +} + +func DeriveContributorRewardsPDA(programID solana.PublicKey, serviceKey solana.PublicKey) (solana.PublicKey, uint8, error) { + return solana.FindProgramAddress([][]byte{seedContributorRewards, serviceKey.Bytes()}, programID) +} diff --git a/sdk/revdist/go/pda_test.go b/sdk/revdist/go/pda_test.go new file mode 100644 index 000000000..b4bca9430 --- /dev/null +++ b/sdk/revdist/go/pda_test.go @@ -0,0 +1,106 @@ +package revdist + +import ( + "testing" + + "github.com/gagliardetto/solana-go" +) + +var testProgramID = solana.MustPublicKeyFromBase58("dzrevZC94tBLwuHw1dyynZxaXTWyp7yocsinyEVPtt4") + +func TestDeriveConfigPDA(t *testing.T) { + addr, bump, err := DeriveConfigPDA(testProgramID) + if err != nil { + t.Fatalf("DeriveConfigPDA: %v", err) + } + if addr.IsZero() { + t.Error("derived zero address") + } + if bump == 0 { + t.Log("bump is 0 (valid but unusual)") + } + + // Deriving again should produce the same result. + addr2, bump2, err := DeriveConfigPDA(testProgramID) + if err != nil { + t.Fatalf("DeriveConfigPDA (2nd): %v", err) + } + if addr != addr2 || bump != bump2 { + t.Error("PDA derivation not deterministic") + } +} + +func TestDeriveDistributionPDA(t *testing.T) { + addr1, _, err := DeriveDistributionPDA(testProgramID, 1) + if err != nil { + t.Fatalf("DeriveDistributionPDA epoch 1: %v", err) + } + addr2, _, err := DeriveDistributionPDA(testProgramID, 2) + if err != nil { + t.Fatalf("DeriveDistributionPDA epoch 2: %v", err) + } + if addr1 == addr2 { + t.Error("different epochs produced same PDA") + } +} + +func TestDeriveValidatorDepositPDA(t *testing.T) { + nodeID := solana.NewWallet().PublicKey() + addr, _, err := DeriveValidatorDepositPDA(testProgramID, nodeID) + if err != nil { + t.Fatalf("DeriveValidatorDepositPDA: %v", err) + } + if addr.IsZero() { + t.Error("derived zero address") + } +} + +func TestDeriveContributorRewardsPDA(t *testing.T) { + serviceKey := solana.NewWallet().PublicKey() + addr, _, err := DeriveContributorRewardsPDA(testProgramID, serviceKey) + if err != nil { + t.Fatalf("DeriveContributorRewardsPDA: %v", err) + } + if addr.IsZero() { + t.Error("derived zero address") + } +} + +func TestDeriveJournalPDA(t *testing.T) { + addr, _, err := DeriveJournalPDA(testProgramID) + if err != nil { + t.Fatalf("DeriveJournalPDA: %v", err) + } + if addr.IsZero() { + t.Error("derived zero address") + } +} + +func TestCreateRecordSeedString(t *testing.T) { + // Test vector from Rust: create_record_seed_string(&[b"test_create_record_seed_string"]) + // Expected: "8YGyrUprn2DwKkq3hR2DaqGPYDD5WE1D" + got := createRecordSeedString([][]byte{[]byte("test_create_record_seed_string")}) + want := "8YGyrUprn2DwKkq3hR2DaqGPYDD5WE1D" + if got != want { + t.Errorf("createRecordSeedString = %q, want %q", got, want) + } + if len(got) != 32 { + t.Errorf("seed string length = %d, want 32", len(got)) + } +} + +func TestDeriveRecordKey(t *testing.T) { + // Test vector from Rust: create_record_key( + // "84s5hmJUjfRhsQ443M1iWnCfNNmLbQLHmWTRyHtxbQzw", + // &[b"test_create_record_key"], + // ) == "9eP3pWoN5uFfUsHBb63wgWnMPjbvGSzQgQe6EDRCdpKJ" + payerKey := solana.MustPublicKeyFromBase58("84s5hmJUjfRhsQ443M1iWnCfNNmLbQLHmWTRyHtxbQzw") + got, err := DeriveRecordKey(payerKey, [][]byte{[]byte("test_create_record_key")}) + if err != nil { + t.Fatalf("DeriveRecordKey: %v", err) + } + want := solana.MustPublicKeyFromBase58("9eP3pWoN5uFfUsHBb63wgWnMPjbvGSzQgQe6EDRCdpKJ") + if got != want { + t.Errorf("DeriveRecordKey = %s, want %s", got, want) + } +} diff --git a/sdk/revdist/go/rpc.go b/sdk/revdist/go/rpc.go new file mode 100644 index 000000000..5eb042a88 --- /dev/null +++ b/sdk/revdist/go/rpc.go @@ -0,0 +1,50 @@ +package revdist + +import ( + "io" + "net/http" + "time" + + "github.com/gagliardetto/solana-go/rpc" + "github.com/gagliardetto/solana-go/rpc/jsonrpc" +) + +const defaultMaxRetries = 5 + +// retryHTTPClient wraps an http.Client and retries on 429 Too Many Requests. +type retryHTTPClient struct { + inner *http.Client + maxRetries int +} + +func (c *retryHTTPClient) Do(req *http.Request) (*http.Response, error) { + for attempt := 0; ; attempt++ { + resp, err := c.inner.Do(req) + if err != nil { + return resp, err + } + if resp.StatusCode != http.StatusTooManyRequests || attempt >= c.maxRetries { + return resp, nil + } + _, _ = io.Copy(io.Discard, resp.Body) + resp.Body.Close() + backoff := time.Duration(attempt+1) * 2 * time.Second + time.Sleep(backoff) + } +} + +func (c *retryHTTPClient) CloseIdleConnections() { + c.inner.CloseIdleConnections() +} + +// NewRPCClient creates a Solana RPC client with automatic retry on 429 responses. +func NewRPCClient(url string) *rpc.Client { + httpClient := &retryHTTPClient{ + inner: http.DefaultClient, + maxRetries: defaultMaxRetries, + } + rpcClient := jsonrpc.NewClientWithOpts(url, &jsonrpc.RPCClientOpts{ + HTTPClient: httpClient, + }) + return rpc.NewWithCustomRPCClient(rpcClient) +} diff --git a/sdk/revdist/go/state.go b/sdk/revdist/go/state.go new file mode 100644 index 000000000..d2ffcab51 --- /dev/null +++ b/sdk/revdist/go/state.go @@ -0,0 +1,181 @@ +package revdist + +import ( + "github.com/gagliardetto/solana-go" +) + +// ProgramConfig represents the on-chain program configuration account. +// On-chain size: 8 (discriminator) + 600 = 608 bytes. +type ProgramConfig struct { + Flags uint64 // 8 bytes + NextCompletedDZEpoch uint64 // 8 bytes + BumpSeed uint8 // 1 byte + Reserve2ZBumpSeed uint8 // 1 byte + SwapAuthorityBumpSeed uint8 // 1 byte + SwapDestination2ZBumpSeed uint8 // 1 byte + WithdrawSOLAuthorityBumpSeed uint8 // 1 byte + Reserved0 [3]uint8 // 3 bytes padding + AdminKey solana.PublicKey // 32 bytes + DebtAccountantKey solana.PublicKey // 32 bytes + RewardsAccountantKey solana.PublicKey // 32 bytes + ContributorManagerKey solana.PublicKey // 32 bytes + PlaceholderKey solana.PublicKey // 32 bytes + SOL2ZSwapProgramID solana.PublicKey // 32 bytes + DistributionParameters DistributionParameters + RelayParameters RelayParameters + LastInitializedDistributionTimestamp uint32 // 4 bytes + Reserved1 [4]byte // 4 bytes padding + DebtWriteOffFeatureActivationEpoch uint64 // 8 bytes +} + +// DistributionParameters contains epoch distribution configuration. +// 328 bytes total. +type DistributionParameters struct { + CalculationGracePeriodMinutes uint16 // 2 bytes + InitializationGracePeriodMinutes uint16 // 2 bytes + MinimumEpochDurationToFinalizeRewards uint8 // 1 byte + Reserved0 [3]uint8 + CommunityBurnRateParameters CommunityBurnRateParameters + SolanaValidatorFeeParameters SolanaValidatorFeeParameters + Reserved1 [8][32]byte // StorageGap<8> = 256 bytes +} + +// CommunityBurnRateParameters configures the community burn rate schedule. +// 24 bytes total. +type CommunityBurnRateParameters struct { + Limit uint32 // BurnRate (UnitShare32), max 1_000_000_000 + DZEpochsToIncreasing uint32 // EpochDuration + DZEpochsToLimit uint32 // EpochDuration + CachedSlopeNumerator uint32 // BurnRate + CachedSlopeDenominator uint32 // EpochDuration + CachedNextBurnRate uint32 // BurnRate +} + +// SolanaValidatorFeeParameters configures validator fee percentages. +// 40 bytes total. +type SolanaValidatorFeeParameters struct { + BaseBlockRewardsPct uint16 // ValidatorFee (UnitShare16), max 10_000 + PriorityBlockRewardsPct uint16 // ValidatorFee + InflationRewardsPct uint16 // ValidatorFee + JitoTipsPct uint16 // ValidatorFee + FixedSOLAmount uint32 // 4 bytes + Reserved0 [7]uint32 // 28 bytes storage gap +} + +// RelayParameters configures relay lamport amounts. +// 40 bytes total. +type RelayParameters struct { + PlaceholderLamports uint32 // 4 bytes + DistributeRewardsLamports uint32 // 4 bytes + Reserved0 [32]byte // 32 bytes storage gap +} + +// Distribution represents a single epoch's distribution account. +// On-chain size: 8 (discriminator) + 448 = 456 bytes. +type Distribution struct { + DZEpoch uint64 // 8 bytes + Flags uint64 // 8 bytes + CommunityBurnRate uint32 // 4 bytes (BurnRate) + BumpSeed uint8 // 1 byte + Token2ZPDABumpSeed uint8 // 1 byte + Reserved0 [2]byte // 2 bytes padding + SolanaValidatorFeeParameters SolanaValidatorFeeParameters + SolanaValidatorDebtMerkleRoot [32]byte // 32 bytes + TotalSolanaValidators uint32 // 4 bytes + SolanaValidatorPaymentsCount uint32 // 4 bytes + TotalSolanaValidatorDebt uint64 // 8 bytes + CollectedSolanaValidatorPayments uint64 // 8 bytes + RewardsMerkleRoot [32]byte // 32 bytes + TotalContributors uint32 // 4 bytes + DistributedRewardsCount uint32 // 4 bytes + CollectedPrepaid2ZPayments uint64 // 8 bytes + Collected2ZConvertedFromSOL uint64 // 8 bytes + UncollectibleSOLDebt uint64 // 8 bytes + ProcessedSolanaValidatorDebtStartIndex uint32 // 4 bytes + ProcessedSolanaValidatorDebtEndIndex uint32 // 4 bytes + ProcessedRewardsStartIndex uint32 // 4 bytes + ProcessedRewardsEndIndex uint32 // 4 bytes + DistributeRewardsRelayLamports uint32 // 4 bytes + CalculationAllowedTimestamp uint32 // 4 bytes + Distributed2ZAmount uint64 // 8 bytes + Burned2ZAmount uint64 // 8 bytes + ProcessedSolanaValidatorDebtWriteOffStartIndex uint32 // 4 bytes + ProcessedSolanaValidatorDebtWriteOffEndIndex uint32 // 4 bytes + SolanaValidatorWriteOffCount uint32 // 4 bytes + Reserved1 [20]byte // 20 bytes padding + Reserved2 [6][32]byte +} + +// SolanaValidatorDeposit represents a validator's deposit account. +// On-chain size: 8 (discriminator) + 96 = 104 bytes. +type SolanaValidatorDeposit struct { + NodeID solana.PublicKey // 32 bytes + WrittenOffSOLDebt uint64 // 8 bytes + Reserved0 [24]byte // 24 bytes padding + Reserved1 [32]byte // 32 bytes storage gap +} + +// ContributorRewards represents a contributor's reward configuration. +// On-chain size: 8 (discriminator) + 600 = 608 bytes. +type ContributorRewards struct { + RewardsManagerKey solana.PublicKey // 32 bytes + ServiceKey solana.PublicKey // 32 bytes + Flags uint64 // 8 bytes + RecipientShares RecipientShares // 272 bytes + Reserved0 [8][32]byte // 256 bytes storage gap +} + +// RecipientShare represents a single reward recipient and their share. +// 34 bytes total. +type RecipientShare struct { + RecipientKey solana.PublicKey // 32 bytes + Share uint16 // UnitShare16, max 10_000 (100%) +} + +// RecipientShares is a fixed array of 8 RecipientShare entries. +// 272 bytes total (8 * 34). +type RecipientShares [8]RecipientShare + +// Journal tracks aggregate balances across the program. +// On-chain size: 8 (discriminator) + 64 = 72 bytes. +type Journal struct { + BumpSeed uint8 // 1 byte + Token2ZPDABumpSeed uint8 // 1 byte + Reserved0 [6]byte // 6 bytes padding + TotalSOLBalance uint64 // 8 bytes + Total2ZBalance uint64 // 8 bytes + Swap2ZDestinationBalance uint64 // 8 bytes + SwappedSOLAmount uint64 // 8 bytes + NextDZEpochToSweepTokens uint64 // 8 bytes + LifetimeSwapped2ZAmount [16]byte // 16 bytes (u128 LE) +} + +// ComputedSolanaValidatorDebts is a Borsh-serialized off-chain record +// containing validator debt calculations for an epoch range. +type ComputedSolanaValidatorDebts struct { + Blockhash [32]byte // 32 bytes + FirstSolanaEpoch uint64 // 8 bytes + LastSolanaEpoch uint64 // 8 bytes + Debts []ComputedSolanaValidatorDebt // Borsh Vec +} + +// ComputedSolanaValidatorDebt represents a single validator's calculated debt. +type ComputedSolanaValidatorDebt struct { + NodeID solana.PublicKey // 32 bytes + Amount uint64 // 8 bytes +} + +// ShapleyOutputStorage is a Borsh-serialized off-chain record +// containing Shapley value reward calculations. +type ShapleyOutputStorage struct { + Epoch uint64 // 8 bytes + Rewards []RewardShare // Borsh Vec + TotalUnitShares uint32 // 4 bytes +} + +// RewardShare represents a contributor's calculated reward share. +type RewardShare struct { + ContributorKey solana.PublicKey // 32 bytes + UnitShare uint32 // 4 bytes (UnitShare32) + RemainingBytes [4]byte // 4 bytes (bit 31 = is_blocked, bits 0-29 = economic_burn_rate) +} diff --git a/sdk/revdist/go/state_test.go b/sdk/revdist/go/state_test.go new file mode 100644 index 000000000..b15252290 --- /dev/null +++ b/sdk/revdist/go/state_test.go @@ -0,0 +1,86 @@ +package revdist + +import ( + "bytes" + "encoding/binary" + "io" + "testing" + "unsafe" +) + +func newReader(data []byte) io.Reader { + return bytes.NewReader(data) +} + +func TestStructSizes(t *testing.T) { + tests := []struct { + name string + size uintptr + expected uintptr + }{ + {"ProgramConfig", unsafe.Sizeof(ProgramConfig{}), 600}, + {"Distribution", unsafe.Sizeof(Distribution{}), 448}, + {"SolanaValidatorDeposit", unsafe.Sizeof(SolanaValidatorDeposit{}), 96}, + {"ContributorRewards", unsafe.Sizeof(ContributorRewards{}), 600}, + {"Journal", unsafe.Sizeof(Journal{}), 64}, + {"RecipientShare", unsafe.Sizeof(RecipientShare{}), 34}, + {"RecipientShares", unsafe.Sizeof(RecipientShares{}), 272}, + {"DistributionParameters", unsafe.Sizeof(DistributionParameters{}), 328}, + {"CommunityBurnRateParameters", unsafe.Sizeof(CommunityBurnRateParameters{}), 24}, + {"SolanaValidatorFeeParameters", unsafe.Sizeof(SolanaValidatorFeeParameters{}), 40}, + {"RelayParameters", unsafe.Sizeof(RelayParameters{}), 40}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.size != tt.expected { + t.Errorf("sizeof(%s) = %d, want %d", tt.name, tt.size, tt.expected) + } + }) + } +} + +func TestJournalDeserialization(t *testing.T) { + // Build a known Journal byte sequence. + data := make([]byte, 64) + data[0] = 1 // BumpSeed + data[1] = 2 // Token2ZPDABumpSeed + binary.LittleEndian.PutUint64(data[8:], 1000) // TotalSOLBalance + binary.LittleEndian.PutUint64(data[16:], 2000) // Total2ZBalance + binary.LittleEndian.PutUint64(data[24:], 3000) // Swap2ZDestinationBalance + binary.LittleEndian.PutUint64(data[32:], 4000) // SwappedSOLAmount + binary.LittleEndian.PutUint64(data[40:], 5) // NextDZEpochToSweepTokens + + var journal Journal + if err := binary.Read(newReader(data), binary.LittleEndian, &journal); err != nil { + t.Fatalf("deserializing: %v", err) + } + if journal.BumpSeed != 1 { + t.Errorf("BumpSeed = %d, want 1", journal.BumpSeed) + } + if journal.TotalSOLBalance != 1000 { + t.Errorf("TotalSOLBalance = %d, want 1000", journal.TotalSOLBalance) + } + if journal.NextDZEpochToSweepTokens != 5 { + t.Errorf("NextDZEpochToSweepTokens = %d, want 5", journal.NextDZEpochToSweepTokens) + } +} + +func TestSolanaValidatorDepositDeserialization(t *testing.T) { + data := make([]byte, 96) + // Set NodeID to a known pattern. + for i := range 32 { + data[i] = byte(i + 1) + } + binary.LittleEndian.PutUint64(data[32:], 999) + + var deposit SolanaValidatorDeposit + if err := binary.Read(newReader(data), binary.LittleEndian, &deposit); err != nil { + t.Fatalf("deserializing: %v", err) + } + if deposit.NodeID[0] != 1 || deposit.NodeID[31] != 32 { + t.Error("NodeID not deserialized correctly") + } + if deposit.WrittenOffSOLDebt != 999 { + t.Errorf("WrittenOffSOLDebt = %d, want 999", deposit.WrittenOffSOLDebt) + } +} diff --git a/sdk/revdist/python/.gitignore b/sdk/revdist/python/.gitignore new file mode 100644 index 000000000..4b45f78fa --- /dev/null +++ b/sdk/revdist/python/.gitignore @@ -0,0 +1,7 @@ +__pycache__/ +*.pyc +.venv/ +*.egg-info/ +dist/ +.pytest_cache/ +uv.lock diff --git a/sdk/revdist/python/pyproject.toml b/sdk/revdist/python/pyproject.toml new file mode 100644 index 000000000..0fd9241b8 --- /dev/null +++ b/sdk/revdist/python/pyproject.toml @@ -0,0 +1,30 @@ +[project] +name = "doublezero-revenue-distribution" +version = "0.0.1" +description = "DoubleZero Revenue Distribution SDK" +requires-python = ">=3.10" +dependencies = [ + "doublezero-borsh-incremental", + "solana>=0.35", + "solders>=0.21", + "httpx>=0.27", + "base58>=2.1", +] + +[tool.pytest.ini_options] +testpaths = ["revdist/tests"] + +[tool.hatch.build.targets.wheel] +packages = ["revdist"] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[dependency-groups] +dev = [ + "pytest>=9.0.2", +] + +[tool.uv.sources] +doublezero-borsh-incremental = { path = "../../borsh-incremental/python", editable = true } diff --git a/sdk/revdist/python/revdist/__init__.py b/sdk/revdist/python/revdist/__init__.py new file mode 100644 index 000000000..8f3fbe666 --- /dev/null +++ b/sdk/revdist/python/revdist/__init__.py @@ -0,0 +1,70 @@ +from revdist.client import Client +from revdist.config import ( + LEDGER_RPC_URLS, + ORACLE_URLS, + PROGRAM_ID, + SOLANA_RPC_URLS, +) +from revdist.oracle import OracleClient, SwapRate +from revdist.rpc import new_rpc_client +from revdist.discriminator import ( + DISCRIMINATOR_CONTRIBUTOR_REWARDS, + DISCRIMINATOR_DISTRIBUTION, + DISCRIMINATOR_JOURNAL, + DISCRIMINATOR_PROGRAM_CONFIG, + DISCRIMINATOR_SOLANA_VALIDATOR_DEPOSIT, +) +from revdist.pda import ( + derive_config_pda, + derive_contributor_rewards_pda, + derive_distribution_pda, + derive_journal_pda, + derive_record_key, + derive_reward_share_record_key, + derive_validator_debt_record_key, + derive_validator_deposit_pda, +) +from revdist.state import ( + ComputedSolanaValidatorDebt, + ComputedSolanaValidatorDebts, + ContributorRewards, + Distribution, + Journal, + ProgramConfig, + RewardShare, + ShapleyOutputStorage, + SolanaValidatorDeposit, +) + +__all__ = [ + "Client", + "LEDGER_RPC_URLS", + "ORACLE_URLS", + "OracleClient", + "PROGRAM_ID", + "SOLANA_RPC_URLS", + "SwapRate", + "ComputedSolanaValidatorDebt", + "ComputedSolanaValidatorDebts", + "ContributorRewards", + "Distribution", + "Journal", + "ProgramConfig", + "RewardShare", + "ShapleyOutputStorage", + "SolanaValidatorDeposit", + "DISCRIMINATOR_CONTRIBUTOR_REWARDS", + "DISCRIMINATOR_DISTRIBUTION", + "DISCRIMINATOR_JOURNAL", + "DISCRIMINATOR_PROGRAM_CONFIG", + "DISCRIMINATOR_SOLANA_VALIDATOR_DEPOSIT", + "derive_config_pda", + "derive_contributor_rewards_pda", + "derive_distribution_pda", + "derive_journal_pda", + "derive_record_key", + "derive_reward_share_record_key", + "derive_validator_debt_record_key", + "derive_validator_deposit_pda", + "new_rpc_client", +] diff --git a/sdk/revdist/python/revdist/client.py b/sdk/revdist/python/revdist/client.py new file mode 100644 index 000000000..4ea8eff62 --- /dev/null +++ b/sdk/revdist/python/revdist/client.py @@ -0,0 +1,189 @@ +"""RPC client for fetching revenue distribution program accounts.""" + +from __future__ import annotations + +import struct +from typing import Protocol + +from solana.rpc.api import Client as SolanaHTTPClient # type: ignore[import-untyped] + +from revdist.rpc import new_rpc_client +from solders.pubkey import Pubkey # type: ignore[import-untyped] +from solders.rpc.responses import GetAccountInfoResp # type: ignore[import-untyped] + +from revdist.config import LEDGER_RPC_URLS, PROGRAM_ID, SOLANA_RPC_URLS +from revdist.discriminator import ( + DISCRIMINATOR_CONTRIBUTOR_REWARDS, + DISCRIMINATOR_DISTRIBUTION, + DISCRIMINATOR_JOURNAL, + DISCRIMINATOR_PROGRAM_CONFIG, + DISCRIMINATOR_SOLANA_VALIDATOR_DEPOSIT, +) +from revdist.pda import ( + RECORD_HEADER_SIZE, + derive_config_pda, + derive_contributor_rewards_pda, + derive_distribution_pda, + derive_journal_pda, + derive_reward_share_record_key, + derive_validator_debt_record_key, + derive_validator_deposit_pda, +) +from revdist.state import ( + ComputedSolanaValidatorDebts, + ContributorRewards, + Distribution, + Journal, + ProgramConfig, + ShapleyOutputStorage, + SolanaValidatorDeposit, +) + + +class SolanaClient(Protocol): + def get_account_info(self, pubkey: Pubkey) -> GetAccountInfoResp: ... + + +class Client: + """Read-only client for revenue distribution program accounts.""" + + def __init__( + self, + solana_rpc: SolanaClient, + ledger_rpc: SolanaClient, + program_id: Pubkey, + ) -> None: + self._solana_rpc = solana_rpc + self._ledger_rpc = ledger_rpc + self._program_id = program_id + + @classmethod + def from_env(cls, env: str) -> Client: + """Create a client configured for the given environment. + + Args: + env: Environment name ("mainnet-beta", "testnet", "devnet", "localnet") + """ + return cls( + new_rpc_client(SOLANA_RPC_URLS[env]), + new_rpc_client(LEDGER_RPC_URLS[env]), + Pubkey.from_string(PROGRAM_ID), + ) + + @classmethod + def mainnet_beta(cls) -> Client: + """Create a client configured for mainnet-beta.""" + return cls.from_env("mainnet-beta") + + @classmethod + def testnet(cls) -> Client: + """Create a client configured for testnet.""" + return cls.from_env("testnet") + + @classmethod + def devnet(cls) -> Client: + """Create a client configured for devnet.""" + return cls.from_env("devnet") + + @classmethod + def localnet(cls) -> Client: + """Create a client configured for localnet.""" + return cls.from_env("localnet") + + # -- Solana RPC (on-chain accounts) -- + + def fetch_config(self) -> ProgramConfig: + addr, _ = derive_config_pda(self._program_id) + data = self._fetch_solana_account_data(addr) + return ProgramConfig.from_bytes(data, DISCRIMINATOR_PROGRAM_CONFIG) + + def fetch_distribution(self, epoch: int) -> Distribution: + addr, _ = derive_distribution_pda(self._program_id, epoch) + data = self._fetch_solana_account_data(addr) + return Distribution.from_bytes(data, DISCRIMINATOR_DISTRIBUTION) + + def fetch_journal(self) -> Journal: + addr, _ = derive_journal_pda(self._program_id) + data = self._fetch_solana_account_data(addr) + return Journal.from_bytes(data, DISCRIMINATOR_JOURNAL) + + def fetch_validator_deposit( + self, node_id: Pubkey + ) -> SolanaValidatorDeposit: + addr, _ = derive_validator_deposit_pda(self._program_id, node_id) + data = self._fetch_solana_account_data(addr) + return SolanaValidatorDeposit.from_bytes( + data, DISCRIMINATOR_SOLANA_VALIDATOR_DEPOSIT + ) + + def fetch_contributor_rewards( + self, service_key: Pubkey + ) -> ContributorRewards: + addr, _ = derive_contributor_rewards_pda(self._program_id, service_key) + data = self._fetch_solana_account_data(addr) + return ContributorRewards.from_bytes( + data, DISCRIMINATOR_CONTRIBUTOR_REWARDS + ) + + def fetch_all_validator_deposits(self) -> list[SolanaValidatorDeposit]: + return self._fetch_all_by_discriminator( + DISCRIMINATOR_SOLANA_VALIDATOR_DEPOSIT, + SolanaValidatorDeposit, + ) + + def fetch_all_contributor_rewards(self) -> list[ContributorRewards]: + return self._fetch_all_by_discriminator( + DISCRIMINATOR_CONTRIBUTOR_REWARDS, + ContributorRewards, + ) + + # -- DZ Ledger RPC (ledger records) -- + + def fetch_validator_debts( + self, epoch: int + ) -> ComputedSolanaValidatorDebts: + config = self.fetch_config() + addr = derive_validator_debt_record_key(config.debt_accountant_key, epoch) + data = self._fetch_ledger_record_data(addr) + return ComputedSolanaValidatorDebts.from_bytes(data[RECORD_HEADER_SIZE:]) + + def fetch_reward_shares(self, epoch: int) -> ShapleyOutputStorage: + config = self.fetch_config() + addr = derive_reward_share_record_key(config.rewards_accountant_key, epoch) + data = self._fetch_ledger_record_data(addr) + return ShapleyOutputStorage.from_bytes(data[RECORD_HEADER_SIZE:]) + + # -- Internal helpers -- + + def _fetch_solana_account_data(self, addr: Pubkey) -> bytes: + resp = self._solana_rpc.get_account_info(addr) + if resp.value is None: + raise ValueError(f"account not found: {addr}") + return bytes(resp.value.data) + + def _fetch_ledger_record_data(self, addr: Pubkey) -> bytes: + resp = self._ledger_rpc.get_account_info(addr) + if resp.value is None: + raise ValueError(f"ledger record not found: {addr}") + return bytes(resp.value.data) + + def _fetch_all_by_discriminator( + self, + disc: bytes, + cls: type, + ) -> list: + from solana.rpc.types import MemcmpOpts # type: ignore[import-untyped] + + import base58 # type: ignore[import-untyped] + + filters = [MemcmpOpts(offset=0, bytes=base58.b58encode(disc).decode())] + resp = self._solana_rpc.get_program_accounts( + self._program_id, + encoding="base64", + filters=filters, + ) + results = [] + for acct in resp.value: + data = bytes(acct.account.data) + results.append(cls.from_bytes(data, disc)) + return results diff --git a/sdk/revdist/python/revdist/config.py b/sdk/revdist/python/revdist/config.py new file mode 100644 index 000000000..47d02037a --- /dev/null +++ b/sdk/revdist/python/revdist/config.py @@ -0,0 +1,22 @@ +"""Network configuration for the revenue distribution program.""" + +PROGRAM_ID = "dzrevZC94tBLwuHw1dyynZxaXTWyp7yocsinyEVPtt4" + +SOLANA_RPC_URLS = { + "mainnet-beta": "https://api.mainnet-beta.solana.com", + "testnet": "https://api.testnet.solana.com", + "devnet": "https://api.devnet.solana.com", + "localnet": "http://localhost:8899", +} + +ORACLE_URLS = { + "mainnet-beta": "https://sol-2z-oracle-api-v1.mainnet-beta.doublezero.xyz", + "testnet": "https://sol-2z-oracle-api-v1.testnet.doublezero.xyz", +} + +LEDGER_RPC_URLS = { + "mainnet-beta": "https://doublezero-mainnet-beta.rpcpool.com/db336024-e7a8-46b1-80e5-352dd77060ab", + "testnet": "https://doublezerolocalnet.rpcpool.com/8a4fd3f4-0977-449f-88c7-63d4b0f10f16", + "devnet": "https://doublezerolocalnet.rpcpool.com/8a4fd3f4-0977-449f-88c7-63d4b0f10f16", + "localnet": "http://localhost:8899", +} diff --git a/sdk/revdist/python/revdist/discriminator.py b/sdk/revdist/python/revdist/discriminator.py new file mode 100644 index 000000000..2d3b7ecfd --- /dev/null +++ b/sdk/revdist/python/revdist/discriminator.py @@ -0,0 +1,31 @@ +import hashlib + +DISCRIMINATOR_SIZE = 8 + + +def _sha256_first8(s: str) -> bytes: + return hashlib.sha256(s.encode()).digest()[:8] + + +DISCRIMINATOR_PROGRAM_CONFIG = _sha256_first8("dz::account::program_config") +DISCRIMINATOR_DISTRIBUTION = _sha256_first8("dz::account::distribution") +DISCRIMINATOR_SOLANA_VALIDATOR_DEPOSIT = _sha256_first8( + "dz::account::solana_validator_deposit" +) +DISCRIMINATOR_CONTRIBUTOR_REWARDS = _sha256_first8( + "dz::account::contributor_rewards" +) +DISCRIMINATOR_JOURNAL = _sha256_first8("dz::account::journal") + + +def validate_discriminator(data: bytes, expected: bytes) -> None: + """Validate the 8-byte discriminator prefix. Raises ValueError on mismatch.""" + if len(data) < DISCRIMINATOR_SIZE: + raise ValueError( + f"data too short: {len(data)} bytes, need at least {DISCRIMINATOR_SIZE}" + ) + got = data[:DISCRIMINATOR_SIZE] + if got != expected: + raise ValueError( + f"invalid discriminator: got {got.hex()}, want {expected.hex()}" + ) diff --git a/sdk/revdist/python/revdist/oracle.py b/sdk/revdist/python/revdist/oracle.py new file mode 100644 index 000000000..838c39b0f --- /dev/null +++ b/sdk/revdist/python/revdist/oracle.py @@ -0,0 +1,38 @@ +"""SOL/2Z oracle client.""" + +from __future__ import annotations + +from dataclasses import dataclass + +import httpx + + +@dataclass +class SwapRate: + rate: float + timestamp: int + signature: str + sol_price_usd: str + twoz_price_usd: str + cache_hit: bool + + +class OracleClient: + """Fetches SOL/2Z swap rates from the oracle API.""" + + def __init__(self, base_url: str) -> None: + self._base_url = base_url + self._http = httpx.Client(timeout=30) + + def fetch_swap_rate(self) -> SwapRate: + resp = self._http.get(f"{self._base_url}/swap-rate") + resp.raise_for_status() + data = resp.json() + return SwapRate( + rate=data["swapRate"], + timestamp=data["timestamp"], + signature=data["signature"], + sol_price_usd=data["solPriceUsd"], + twoz_price_usd=data["twozPriceUsd"], + cache_hit=data["cacheHit"], + ) diff --git a/sdk/revdist/python/revdist/pda.py b/sdk/revdist/python/revdist/pda.py new file mode 100644 index 000000000..f009e7baf --- /dev/null +++ b/sdk/revdist/python/revdist/pda.py @@ -0,0 +1,85 @@ +"""PDA and record key derivation for revenue distribution program accounts.""" + +import hashlib +import struct + +import base58 # type: ignore[import-untyped] +from solders.pubkey import Pubkey # type: ignore[import-untyped] + +SEED_PROGRAM_CONFIG = b"program_config" +SEED_DISTRIBUTION = b"distribution" +SEED_SOLANA_VALIDATOR_DEPOSIT = b"solana_validator_deposit" +SEED_CONTRIBUTOR_REWARDS = b"contributor_rewards" +SEED_JOURNAL = b"journal" +SEED_SOLANA_VALIDATOR_DEBT = b"solana_validator_debt" +SEED_DZ_CONTRIBUTOR_REWARDS = b"dz_contributor_rewards" +SEED_SHAPLEY_OUTPUT = b"shapley_output" + +RECORD_PROGRAM_ID = Pubkey.from_string("dzrecxigtaZQ3gPmt2X5mDkYigaruFR1rHCqztFTvx7") +RECORD_HEADER_SIZE = 33 + + +def derive_config_pda(program_id: Pubkey) -> tuple[Pubkey, int]: + return Pubkey.find_program_address([SEED_PROGRAM_CONFIG], program_id) + + +def derive_distribution_pda( + program_id: Pubkey, epoch: int +) -> tuple[Pubkey, int]: + epoch_bytes = struct.pack(" tuple[Pubkey, int]: + return Pubkey.find_program_address([SEED_JOURNAL], program_id) + + +def derive_validator_deposit_pda( + program_id: Pubkey, node_id: Pubkey +) -> tuple[Pubkey, int]: + return Pubkey.find_program_address( + [SEED_SOLANA_VALIDATOR_DEPOSIT, bytes(node_id)], program_id + ) + + +def derive_contributor_rewards_pda( + program_id: Pubkey, service_key: Pubkey +) -> tuple[Pubkey, int]: + return Pubkey.find_program_address( + [SEED_CONTRIBUTOR_REWARDS, bytes(service_key)], program_id + ) + + +def _create_record_seed_string(seeds: list[bytes]) -> str: + """Hash seeds with SHA256, encode as base58, truncate to 32 chars.""" + h = hashlib.sha256() + for s in seeds: + h.update(s) + return base58.b58encode(h.digest()).decode()[:32] + + +def derive_record_key(payer_key: Pubkey, seeds: list[bytes]) -> Pubkey: + """Derive a ledger record address using create-with-seed.""" + seed_str = _create_record_seed_string(seeds) + return Pubkey.create_with_seed(payer_key, seed_str, RECORD_PROGRAM_ID) + + +def derive_validator_debt_record_key( + debt_accountant_key: Pubkey, epoch: int +) -> Pubkey: + epoch_bytes = struct.pack(" Pubkey: + epoch_bytes = struct.pack(" Reserved: + return super().__new__(cls, data) + + def __repr__(self) -> str: + return f"Reserved({len(self)})" diff --git a/sdk/revdist/python/revdist/rpc.py b/sdk/revdist/python/revdist/rpc.py new file mode 100644 index 000000000..a6de24721 --- /dev/null +++ b/sdk/revdist/python/revdist/rpc.py @@ -0,0 +1,49 @@ +"""RPC client helpers with retry on rate limiting.""" + +import time + +import httpx +from solana.rpc.api import Client as SolanaHTTPClient # type: ignore[import-untyped] +from solana.rpc.providers.http import HTTPProvider # type: ignore[import-untyped] + +_DEFAULT_MAX_RETRIES = 5 + + +class _RetryTransport(httpx.BaseTransport): + """HTTP transport that retries on 429 Too Many Requests.""" + + def __init__( + self, + wrapped: httpx.BaseTransport | None = None, + max_retries: int = _DEFAULT_MAX_RETRIES, + ) -> None: + self._wrapped = wrapped or httpx.HTTPTransport() + self._max_retries = max_retries + + def handle_request(self, request: httpx.Request) -> httpx.Response: + for attempt in range(self._max_retries + 1): + response = self._wrapped.handle_request(request) + if response.status_code != 429 or attempt >= self._max_retries: + return response + response.close() + time.sleep((attempt + 1) * 2) + return response # unreachable, but satisfies type checker + + +def new_rpc_client( + url: str, + timeout: float = 30, + max_retries: int = _DEFAULT_MAX_RETRIES, +) -> SolanaHTTPClient: + """Create a Solana RPC client with automatic retry on 429 responses.""" + client = SolanaHTTPClient(url, timeout=timeout) + # Replace the underlying httpx session with one using retry transport. + transport = _RetryTransport( + wrapped=httpx.HTTPTransport(), + max_retries=max_retries, + ) + client._provider.session = httpx.Client( + timeout=timeout, + transport=transport, + ) + return client diff --git a/sdk/revdist/python/revdist/state.py b/sdk/revdist/python/revdist/state.py new file mode 100644 index 000000000..887050fac --- /dev/null +++ b/sdk/revdist/python/revdist/state.py @@ -0,0 +1,469 @@ +"""On-chain account data structures for the revenue distribution program. + +Binary layout matches Rust #[repr(C)] structs. Deserialization uses +struct.unpack_from with little-endian byte order and tolerates extra +trailing bytes for forward compatibility. +""" + +from __future__ import annotations + +import struct +from dataclasses import dataclass + +from borsh_incremental import IncrementalReader + +from revdist.reserved import Reserved +from solders.pubkey import Pubkey # type: ignore[import-untyped] + +from revdist.discriminator import DISCRIMINATOR_SIZE, validate_discriminator + + +def _pubkey(data: bytes, offset: int) -> Pubkey: + return Pubkey.from_bytes(data[offset : offset + 32]) + + +# --------------------------------------------------------------------------- +# Nested structs +# --------------------------------------------------------------------------- + + +@dataclass +class CommunityBurnRateParameters: + limit: int # u32 + dz_epochs_to_increasing: int # u32 + dz_epochs_to_limit: int # u32 + cached_slope_numerator: int # u32 + cached_slope_denominator: int # u32 + cached_next_burn_rate: int # u32 + + STRUCT_SIZE = 24 + + @classmethod + def from_bytes(cls, data: bytes, offset: int = 0) -> CommunityBurnRateParameters: + fields = struct.unpack_from("<6I", data, offset) + return cls(*fields) + + +@dataclass +class SolanaValidatorFeeParameters: + base_block_rewards_pct: int # u16 + priority_block_rewards_pct: int # u16 + inflation_rewards_pct: int # u16 + jito_tips_pct: int # u16 + fixed_sol_amount: int # u32 + reserved0: Reserved = Reserved(b"\x00" * 28) # [7]u32 storage gap + + STRUCT_SIZE = 40 # 4*u16 + u32 + 7*u32 reserved + + @classmethod + def from_bytes( + cls, data: bytes, offset: int = 0 + ) -> SolanaValidatorFeeParameters: + vals = struct.unpack_from("<4HI", data, offset) + reserved0 = Reserved(data[offset + 12 : offset + 40]) + return cls(*vals, reserved0=reserved0) + + +@dataclass +class DistributionParameters: + calculation_grace_period_minutes: int # u16 + initialization_grace_period_minutes: int # u16 + minimum_epoch_duration_to_finalize_rewards: int # u8 + reserved0: Reserved # [3]u8 padding + community_burn_rate_parameters: CommunityBurnRateParameters + solana_validator_fee_parameters: SolanaValidatorFeeParameters + reserved1: Reserved # [8][32]byte storage gap (256 bytes) + + STRUCT_SIZE = 328 # 2+2+1+3pad+24+40+256reserved + + @classmethod + def from_bytes(cls, data: bytes, offset: int = 0) -> DistributionParameters: + off = offset + calc_gp, init_gp = struct.unpack_from("<2H", data, off); off += 4 + min_epoch = struct.unpack_from(" RelayParameters: + vals = struct.unpack_from("<2I", data, offset) + reserved0 = Reserved(data[offset + 8 : offset + 40]) + return cls(*vals, reserved0=reserved0) + + +@dataclass +class RecipientShare: + recipient_key: Pubkey # 32 bytes + share: int # u16 + + STRUCT_SIZE = 34 + + @classmethod + def from_bytes(cls, data: bytes, offset: int = 0) -> RecipientShare: + key = _pubkey(data, offset) + share = struct.unpack_from(" bytes: + """Validate discriminator and return the body bytes. + + Tolerates extra trailing bytes for forward compatibility. + """ + validate_discriminator(data, discriminator) + body = data[DISCRIMINATOR_SIZE:] + if len(body) < min_size: + raise ValueError( + f"account data too short: have {len(body)} bytes, need at least {min_size}" + ) + return body + + +@dataclass +class ProgramConfig: + flags: int # u64 + next_completed_dz_epoch: int # u64 + bump_seed: int # u8 + reserve_2z_bump_seed: int # u8 + swap_authority_bump_seed: int # u8 + swap_destination_2z_bump_seed: int # u8 + withdraw_sol_authority_bump_seed: int # u8 + reserved0: Reserved # [3]u8 padding + admin_key: Pubkey + debt_accountant_key: Pubkey + rewards_accountant_key: Pubkey + contributor_manager_key: Pubkey + placeholder_key: Pubkey + sol_2z_swap_program_id: Pubkey + distribution_parameters: DistributionParameters + relay_parameters: RelayParameters + last_initialized_distribution_timestamp: int # u32 + reserved1: Reserved # [4]byte padding + debt_write_off_feature_activation_epoch: int # u64 + + STRUCT_SIZE = 600 + + @classmethod + def from_bytes( + cls, data: bytes, discriminator: bytes + ) -> ProgramConfig: + b = _deserialize(data, discriminator, cls.STRUCT_SIZE) + off = 0 + flags, next_epoch = struct.unpack_from("<2Q", b, off); off += 16 + bump, r2z, swap_auth, swap_dest, withdraw = struct.unpack_from("<5B", b, off); off += 5 + reserved0 = Reserved(b[off : off + 3]); off += 3 + admin = _pubkey(b, off); off += 32 + debt = _pubkey(b, off); off += 32 + rewards = _pubkey(b, off); off += 32 + contrib_mgr = _pubkey(b, off); off += 32 + placeholder = _pubkey(b, off); off += 32 + swap_prog = _pubkey(b, off); off += 32 + dist_params = DistributionParameters.from_bytes(b, off); off += DistributionParameters.STRUCT_SIZE + relay = RelayParameters.from_bytes(b, off); off += RelayParameters.STRUCT_SIZE + last_ts = struct.unpack_from(" Distribution: + b = _deserialize(data, discriminator, cls.STRUCT_SIZE) + off = 0 + dz_epoch, flags = struct.unpack_from("<2Q", b, off); off += 16 + burn_rate = struct.unpack_from(" SolanaValidatorDeposit: + b = _deserialize(data, discriminator, cls.STRUCT_SIZE) + off = 0 + node_id = _pubkey(b, off); off += 32 + debt = struct.unpack_from(" ContributorRewards: + b = _deserialize(data, discriminator, cls.STRUCT_SIZE) + off = 0 + mgr = _pubkey(b, off); off += 32 + svc = _pubkey(b, off); off += 32 + flags = struct.unpack_from(" Journal: + b = _deserialize(data, discriminator, cls.STRUCT_SIZE) + off = 0 + bump, t2z_bump = struct.unpack_from("<2B", b, off); off += 2 + reserved0 = Reserved(b[off : off + 6]); off += 6 + ( + total_sol, total_2z, swap_dest, swapped, next_epoch, + ) = struct.unpack_from("<5Q", b, off); off += 40 + lifetime = b[off : off + 16] + return cls( + bump_seed=bump, + token_2z_pda_bump_seed=t2z_bump, + reserved0=reserved0, + total_sol_balance=total_sol, + total_2z_balance=total_2z, + swap_2z_destination_balance=swap_dest, + swapped_sol_amount=swapped, + next_dz_epoch_to_sweep_tokens=next_epoch, + lifetime_swapped_2z_amount=lifetime, + ) + + +# --------------------------------------------------------------------------- +# DZ Ledger record types (Borsh-serialized) +# --------------------------------------------------------------------------- + + +@dataclass +class ComputedSolanaValidatorDebt: + node_id: Pubkey # 32 bytes + amount: int # u64 + + +@dataclass +class ComputedSolanaValidatorDebts: + blockhash: bytes # 32 bytes + first_solana_epoch: int # u64 + last_solana_epoch: int # u64 + debts: list[ComputedSolanaValidatorDebt] + + @classmethod + def from_bytes(cls, data: bytes) -> ComputedSolanaValidatorDebts: + r = IncrementalReader(data) + blockhash = r.read_bytes(32) + first_epoch = r.read_u64() + last_epoch = r.read_u64() + count = r.read_u32() + debts = [] + for _ in range(count): + node_id = Pubkey.from_bytes(r.read_pubkey_raw()) + amount = r.read_u64() + debts.append(ComputedSolanaValidatorDebt(node_id=node_id, amount=amount)) + return cls( + blockhash=blockhash, + first_solana_epoch=first_epoch, + last_solana_epoch=last_epoch, + debts=debts, + ) + + +@dataclass +class RewardShare: + contributor_key: Pubkey # 32 bytes + unit_share: int # u32 + remaining_bytes: bytes # 4 bytes + + @property + def is_blocked(self) -> bool: + val = struct.unpack_from(" int: + val = struct.unpack_from(" ShapleyOutputStorage: + r = IncrementalReader(data) + epoch = r.read_u64() + count = r.read_u32() + rewards = [] + for _ in range(count): + key = Pubkey.from_bytes(r.read_pubkey_raw()) + unit_share = r.read_u32() + remaining = r.read_bytes(4) + rewards.append(RewardShare( + contributor_key=key, + unit_share=unit_share, + remaining_bytes=remaining, + )) + total_unit_shares = r.read_u32() + return cls(epoch=epoch, rewards=rewards, total_unit_shares=total_unit_shares) diff --git a/sdk/revdist/python/revdist/tests/__init__.py b/sdk/revdist/python/revdist/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sdk/revdist/python/revdist/tests/test_compat.py b/sdk/revdist/python/revdist/tests/test_compat.py new file mode 100644 index 000000000..8d4fb9c84 --- /dev/null +++ b/sdk/revdist/python/revdist/tests/test_compat.py @@ -0,0 +1,251 @@ +"""Mainnet compatibility tests. + +These tests fetch live mainnet-beta data and verify that our struct +deserialization works against real on-chain accounts. + +Run with: + REVDIST_COMPAT_TEST=1 cd sdk/revdist/python && uv run pytest -k compat -v + +Requires network access to Solana mainnet RPC. +""" + +import os +import struct + +import pytest +from solders.pubkey import Pubkey # type: ignore[import-untyped] + +from revdist.client import Client + + +def skip_unless_compat() -> None: + if not os.environ.get("REVDIST_COMPAT_TEST"): + pytest.skip("set REVDIST_COMPAT_TEST=1 to run compatibility tests against mainnet") + + +def compat_client() -> Client: + from revdist.config import PROGRAM_ID, LEDGER_RPC_URLS + from revdist.rpc import new_rpc_client + + rpc_url = os.environ.get("SOLANA_RPC_URL", "https://api.mainnet-beta.solana.com") + return Client( + new_rpc_client(rpc_url), + new_rpc_client(LEDGER_RPC_URLS["mainnet-beta"]), + Pubkey.from_string(PROGRAM_ID), + ) + + +def _rpc_url() -> str: + return os.environ.get("SOLANA_RPC_URL", "https://api.mainnet-beta.solana.com") + + +def fetch_raw_account(addr: Pubkey) -> bytes: + from revdist.rpc import new_rpc_client + + rpc = new_rpc_client(_rpc_url()) + resp = rpc.get_account_info(addr) + assert resp.value is not None, f"account not found: {addr}" + return bytes(resp.value.data) + + +def read_u8(raw: bytes, offset: int) -> int: + return raw[offset] + + +def read_u16(raw: bytes, offset: int) -> int: + return struct.unpack_from(" int: + return struct.unpack_from(" int: + return struct.unpack_from(" Pubkey: + return Pubkey.from_bytes(raw[offset : offset + 32]) + + +class TestCompatProgramConfig: + def test_deserialize(self) -> None: + skip_unless_compat() + client = compat_client() + + config = client.fetch_config() + + # Fetch raw bytes for independent verification. + from revdist.config import PROGRAM_ID + from revdist.pda import derive_config_pda + + addr, _ = derive_config_pda(Pubkey.from_string(PROGRAM_ID)) + raw = fetch_raw_account(addr) + + # Verify fields at known raw byte offsets (offset = struct_offset + 8 for discriminator). + assert config.flags == read_u64(raw, 8), "Flags" + assert config.next_completed_dz_epoch == read_u64(raw, 16), "NextCompletedDZEpoch" + assert config.bump_seed == read_u8(raw, 24), "BumpSeed" + assert config.admin_key == read_pubkey(raw, 32), "AdminKey" + assert config.debt_accountant_key == read_pubkey(raw, 64), "DebtAccountantKey" + assert config.rewards_accountant_key == read_pubkey(raw, 96), "RewardsAccountantKey" + assert config.contributor_manager_key == read_pubkey(raw, 128), "ContributorManagerKey" + assert config.sol_2z_swap_program_id == read_pubkey(raw, 192), "SOL2ZSwapProgramID" + + # DistributionParameters starts at raw offset 224. + dp = config.distribution_parameters + assert dp.calculation_grace_period_minutes == read_u16(raw, 224), "CalculationGracePeriodMinutes" + assert dp.initialization_grace_period_minutes == read_u16(raw, 226), "InitializationGracePeriodMinutes" + assert dp.minimum_epoch_duration_to_finalize_rewards == read_u8(raw, 228), "MinEpochDuration" + + # CommunityBurnRateParameters at raw offset 232. + cb = dp.community_burn_rate_parameters + assert cb.limit == read_u32(raw, 232), "BurnRateLimit" + assert cb.dz_epochs_to_increasing == read_u32(raw, 236), "DZEpochsToIncreasing" + assert cb.dz_epochs_to_limit == read_u32(raw, 240), "DZEpochsToLimit" + + # SolanaValidatorFeeParameters at raw offset 256. + vf = dp.solana_validator_fee_parameters + assert vf.base_block_rewards_pct == read_u16(raw, 256), "BaseBlockRewardsPct" + assert vf.priority_block_rewards_pct == read_u16(raw, 258), "PriorityBlockRewardsPct" + assert vf.inflation_rewards_pct == read_u16(raw, 260), "InflationRewardsPct" + assert vf.jito_tips_pct == read_u16(raw, 262), "JitoTipsPct" + assert vf.fixed_sol_amount == read_u32(raw, 264), "FixedSOLAmount" + + # RelayParameters at raw offset 552. + rp = config.relay_parameters + assert rp.placeholder_lamports == read_u32(raw, 552), "PlaceholderLamports" + assert rp.distribute_rewards_lamports == read_u32(raw, 556), "DistributeRewardsLamports" + + # DebtWriteOffFeatureActivationEpoch at raw offset 600. + assert config.debt_write_off_feature_activation_epoch == read_u64(raw, 600), "DebtWriteOffEpoch" + + # Sanity: epoch should be > 0 on mainnet. + assert config.next_completed_dz_epoch > 0, "NextCompletedDZEpoch should be > 0 on mainnet" + + +class TestCompatDistribution: + def test_deserialize(self) -> None: + skip_unless_compat() + client = compat_client() + + config = client.fetch_config() + epoch = config.next_completed_dz_epoch - 1 + + dist = client.fetch_distribution(epoch) + + # Fetch raw bytes. + from revdist.config import PROGRAM_ID + from revdist.pda import derive_distribution_pda + + addr, _ = derive_distribution_pda(Pubkey.from_string(PROGRAM_ID), epoch) + raw = fetch_raw_account(addr) + + assert dist.dz_epoch == read_u64(raw, 8), "DZEpoch" + assert dist.dz_epoch == epoch + assert dist.flags == read_u64(raw, 16), "Flags" + assert dist.community_burn_rate == read_u32(raw, 24), "CommunityBurnRate" + + vf = dist.solana_validator_fee_parameters + assert vf.base_block_rewards_pct == read_u16(raw, 32), "BaseBlockRewardsPct" + assert vf.priority_block_rewards_pct == read_u16(raw, 34), "PriorityBlockRewardsPct" + assert vf.inflation_rewards_pct == read_u16(raw, 36), "InflationRewardsPct" + assert vf.jito_tips_pct == read_u16(raw, 38), "JitoTipsPct" + assert vf.fixed_sol_amount == read_u32(raw, 40), "FixedSOLAmount" + + assert dist.total_solana_validators == read_u32(raw, 104), "TotalSolanaValidators" + assert dist.solana_validator_payments_count == read_u32(raw, 108), "SolanaValidatorPaymentsCount" + assert dist.total_solana_validator_debt == read_u64(raw, 112), "TotalSolanaValidatorDebt" + assert dist.collected_solana_validator_payments == read_u64(raw, 120), "CollectedPayments" + assert dist.total_contributors == read_u32(raw, 160), "TotalContributors" + assert dist.distributed_rewards_count == read_u32(raw, 164), "DistributedRewardsCount" + assert dist.collected_prepaid_2z_payments == read_u64(raw, 168), "CollectedPrepaid2ZPayments" + assert dist.collected_2z_converted_from_sol == read_u64(raw, 176), "Collected2ZConvertedFromSOL" + assert dist.uncollectible_sol_debt == read_u64(raw, 184), "UncollectibleSOLDebt" + assert dist.distributed_2z_amount == read_u64(raw, 216), "Distributed2ZAmount" + assert dist.burned_2z_amount == read_u64(raw, 224), "Burned2ZAmount" + + +class TestCompatJournal: + def test_deserialize(self) -> None: + skip_unless_compat() + client = compat_client() + + journal = client.fetch_journal() + + # Fetch raw bytes. + from revdist.config import PROGRAM_ID + from revdist.pda import derive_journal_pda + + addr, _ = derive_journal_pda(Pubkey.from_string(PROGRAM_ID)) + raw = fetch_raw_account(addr) + + assert journal.bump_seed == read_u8(raw, 8), "BumpSeed" + assert journal.total_sol_balance == read_u64(raw, 16), "TotalSOLBalance" + assert journal.total_2z_balance == read_u64(raw, 24), "Total2ZBalance" + assert journal.swap_2z_destination_balance == read_u64(raw, 32), "Swap2ZDestinationBalance" + assert journal.swapped_sol_amount == read_u64(raw, 40), "SwappedSOLAmount" + assert journal.next_dz_epoch_to_sweep_tokens == read_u64(raw, 48), "NextDZEpochToSweepTokens" + + +class TestCompatValidatorDebts: + def test_fetch(self) -> None: + skip_unless_compat() + client = compat_client() + + config = client.fetch_config() + epoch = config.next_completed_dz_epoch - 5 + + debts = client.fetch_validator_debts(epoch) + + assert debts.last_solana_epoch > 0, "LastSolanaEpoch should be > 0" + assert debts.first_solana_epoch <= debts.last_solana_epoch, ( + f"FirstSolanaEpoch ({debts.first_solana_epoch}) > LastSolanaEpoch ({debts.last_solana_epoch})" + ) + assert len(debts.debts) > 0, "no validator debts found on mainnet" + + +class TestCompatRewardShares: + def test_fetch(self) -> None: + skip_unless_compat() + client = compat_client() + + config = client.fetch_config() + epoch = config.next_completed_dz_epoch - 5 + + shares = client.fetch_reward_shares(epoch) + + assert shares.epoch == epoch, f"Epoch = {shares.epoch}, want {epoch}" + assert len(shares.rewards) > 0, "no reward shares found on mainnet" + assert shares.total_unit_shares > 0, "TotalUnitShares should be > 0" + + +class TestCompatValidatorDeposits: + def test_fetch_all(self) -> None: + skip_unless_compat() + client = compat_client() + + deposits = client.fetch_all_validator_deposits() + assert len(deposits) > 0, "no deposits found on mainnet" + + # Verify single lookup matches list entry. + first = deposits[0] + single = client.fetch_validator_deposit(first.node_id) + assert single.node_id == first.node_id + assert single.written_off_sol_debt == first.written_off_sol_debt + + +class TestCompatContributorRewards: + def test_fetch_all(self) -> None: + skip_unless_compat() + client = compat_client() + + rewards = client.fetch_all_contributor_rewards() + assert len(rewards) > 0, "no contributor rewards found on mainnet" + + # Verify single lookup matches list entry. + first = rewards[0] + single = client.fetch_contributor_rewards(first.service_key) + assert single.service_key == first.service_key + assert single.rewards_manager_key == first.rewards_manager_key + assert single.flags == first.flags diff --git a/sdk/revdist/python/revdist/tests/test_fixtures.py b/sdk/revdist/python/revdist/tests/test_fixtures.py new file mode 100644 index 000000000..cbb856173 --- /dev/null +++ b/sdk/revdist/python/revdist/tests/test_fixtures.py @@ -0,0 +1,193 @@ +"""Fixture-based compatibility tests. + +These tests deserialize binary fixtures generated by the Rust fixture +generator and verify that Python's deserialized field values match the +expected values from the JSON sidecar files. + +Regenerate fixtures: + cd ../../../testdata/fixtures/generate-fixtures && cargo run +""" + +import json +from pathlib import Path + +import pytest +from solders.pubkey import Pubkey # type: ignore[import-untyped] + +from revdist.discriminator import ( + DISCRIMINATOR_CONTRIBUTOR_REWARDS, + DISCRIMINATOR_DISTRIBUTION, + DISCRIMINATOR_JOURNAL, + DISCRIMINATOR_PROGRAM_CONFIG, + DISCRIMINATOR_SOLANA_VALIDATOR_DEPOSIT, +) +from revdist.state import ( + ContributorRewards, + Distribution, + Journal, + ProgramConfig, + SolanaValidatorDeposit, +) + +FIXTURES_DIR = Path(__file__).resolve().parent.parent.parent.parent / "testdata" / "fixtures" + +# Field name mapping: JSON (Go-style PascalCase) -> Python attribute path +# Each entry is (json_name, accessor_callable) +# Built per-type below. + + +def _load_fixture(name: str) -> tuple[bytes, dict]: + bin_data = (FIXTURES_DIR / f"{name}.bin").read_bytes() + meta = json.loads((FIXTURES_DIR / f"{name}.json").read_text()) + return bin_data, meta + + +def _assert_field(obj, field: dict, field_map: dict) -> None: + name = field["name"] + typ = field["typ"] + raw_value = field["value"] + + if name not in field_map: + pytest.skip(f"field {name} not mapped") + + got = field_map[name] + + if typ in ("u8", "u16", "u32", "u64"): + assert got == int(raw_value), f"{name}: expected {raw_value}, got {got}" + elif typ == "pubkey": + expected = Pubkey.from_string(raw_value) + assert got == expected, f"{name}: expected {expected}, got {got}" + else: + raise ValueError(f"unknown type {typ}") + + +class TestFixtureProgramConfig: + def test_deserialize(self): + data, meta = _load_fixture("program_config") + assert ProgramConfig.STRUCT_SIZE == meta["struct_size"] + + config = ProgramConfig.from_bytes(data, DISCRIMINATOR_PROGRAM_CONFIG) + + field_map = { + "Flags": config.flags, + "NextCompletedDZEpoch": config.next_completed_dz_epoch, + "BumpSeed": config.bump_seed, + "AdminKey": config.admin_key, + "DebtAccountantKey": config.debt_accountant_key, + "RewardsAccountantKey": config.rewards_accountant_key, + "ContributorManagerKey": config.contributor_manager_key, + "SOL2ZSwapProgramID": config.sol_2z_swap_program_id, + "CalculationGracePeriodMinutes": config.distribution_parameters.calculation_grace_period_minutes, + "InitializationGracePeriodMinutes": config.distribution_parameters.initialization_grace_period_minutes, + "MinimumEpochDurationToFinalizeRewards": config.distribution_parameters.minimum_epoch_duration_to_finalize_rewards, + "BurnRateLimit": config.distribution_parameters.community_burn_rate_parameters.limit, + "BurnRateDZEpochsToIncreasing": config.distribution_parameters.community_burn_rate_parameters.dz_epochs_to_increasing, + "BurnRateDZEpochsToLimit": config.distribution_parameters.community_burn_rate_parameters.dz_epochs_to_limit, + "BaseBlockRewardsPct": config.distribution_parameters.solana_validator_fee_parameters.base_block_rewards_pct, + "PriorityBlockRewardsPct": config.distribution_parameters.solana_validator_fee_parameters.priority_block_rewards_pct, + "InflationRewardsPct": config.distribution_parameters.solana_validator_fee_parameters.inflation_rewards_pct, + "JitoTipsPct": config.distribution_parameters.solana_validator_fee_parameters.jito_tips_pct, + "FixedSOLAmount": config.distribution_parameters.solana_validator_fee_parameters.fixed_sol_amount, + "DistributeRewardsLamports": config.relay_parameters.distribute_rewards_lamports, + "DebtWriteOffFeatureActivationEpoch": config.debt_write_off_feature_activation_epoch, + } + + for field in meta["fields"]: + _assert_field(config, field, field_map) + + def test_tolerates_extra_bytes(self): + data, _ = _load_fixture("program_config") + extended = data + b"\x00" * 64 + config = ProgramConfig.from_bytes(extended, DISCRIMINATOR_PROGRAM_CONFIG) + assert config.flags == 1 + + +class TestFixtureDistribution: + def test_deserialize(self): + data, meta = _load_fixture("distribution") + assert Distribution.STRUCT_SIZE == meta["struct_size"] + + dist = Distribution.from_bytes(data, DISCRIMINATOR_DISTRIBUTION) + + field_map = { + "DZEpoch": dist.dz_epoch, + "Flags": dist.flags, + "CommunityBurnRate": dist.community_burn_rate, + "BaseBlockRewardsPct": dist.solana_validator_fee_parameters.base_block_rewards_pct, + "PriorityBlockRewardsPct": dist.solana_validator_fee_parameters.priority_block_rewards_pct, + "InflationRewardsPct": dist.solana_validator_fee_parameters.inflation_rewards_pct, + "JitoTipsPct": dist.solana_validator_fee_parameters.jito_tips_pct, + "FixedSOLAmount": dist.solana_validator_fee_parameters.fixed_sol_amount, + "TotalSolanaValidators": dist.total_solana_validators, + "SolanaValidatorPaymentsCount": dist.solana_validator_payments_count, + "TotalSolanaValidatorDebt": dist.total_solana_validator_debt, + "CollectedSolanaValidatorPayments": dist.collected_solana_validator_payments, + "TotalContributors": dist.total_contributors, + "DistributedRewardsCount": dist.distributed_rewards_count, + "CollectedPrepaid2ZPayments": dist.collected_prepaid_2z_payments, + "Collected2ZConvertedFromSOL": dist.collected_2z_converted_from_sol, + "UncollectibleSOLDebt": dist.uncollectible_sol_debt, + "Distributed2ZAmount": dist.distributed_2z_amount, + "Burned2ZAmount": dist.burned_2z_amount, + "SolanaValidatorWriteOffCount": dist.solana_validator_write_off_count, + } + + for field in meta["fields"]: + _assert_field(dist, field, field_map) + + +class TestFixtureJournal: + def test_deserialize(self): + data, meta = _load_fixture("journal") + assert Journal.STRUCT_SIZE == meta["struct_size"] + + journal = Journal.from_bytes(data, DISCRIMINATOR_JOURNAL) + + field_map = { + "BumpSeed": journal.bump_seed, + "TotalSOLBalance": journal.total_sol_balance, + "Total2ZBalance": journal.total_2z_balance, + "Swap2ZDestinationBalance": journal.swap_2z_destination_balance, + "SwappedSOLAmount": journal.swapped_sol_amount, + "NextDZEpochToSweepTokens": journal.next_dz_epoch_to_sweep_tokens, + } + + for field in meta["fields"]: + _assert_field(journal, field, field_map) + + +class TestFixtureSolanaValidatorDeposit: + def test_deserialize(self): + data, meta = _load_fixture("solana_validator_deposit") + assert SolanaValidatorDeposit.STRUCT_SIZE == meta["struct_size"] + + deposit = SolanaValidatorDeposit.from_bytes( + data, DISCRIMINATOR_SOLANA_VALIDATOR_DEPOSIT + ) + + field_map = { + "NodeID": deposit.node_id, + "WrittenOffSOLDebt": deposit.written_off_sol_debt, + } + + for field in meta["fields"]: + _assert_field(deposit, field, field_map) + + +class TestFixtureContributorRewards: + def test_deserialize(self): + data, meta = _load_fixture("contributor_rewards") + assert ContributorRewards.STRUCT_SIZE == meta["struct_size"] + + rewards = ContributorRewards.from_bytes( + data, DISCRIMINATOR_CONTRIBUTOR_REWARDS + ) + + field_map = { + "RewardsManagerKey": rewards.rewards_manager_key, + "ServiceKey": rewards.service_key, + "Flags": rewards.flags, + } + + for field in meta["fields"]: + _assert_field(rewards, field, field_map) diff --git a/sdk/revdist/python/revdist/tests/test_pda.py b/sdk/revdist/python/revdist/tests/test_pda.py new file mode 100644 index 000000000..5eccad862 --- /dev/null +++ b/sdk/revdist/python/revdist/tests/test_pda.py @@ -0,0 +1,43 @@ +"""PDA derivation tests.""" + +from solders.pubkey import Pubkey # type: ignore[import-untyped] + +from revdist.pda import ( + derive_config_pda, + derive_contributor_rewards_pda, + derive_distribution_pda, + derive_journal_pda, + derive_validator_deposit_pda, +) + +PROGRAM_ID = Pubkey.from_string("dzrevZC94tBLwuHw1dyynZxaXTWyp7yocsinyEVPtt4") + + +def test_derive_config_pda(): + addr, bump = derive_config_pda(PROGRAM_ID) + assert addr != Pubkey.default() + addr2, bump2 = derive_config_pda(PROGRAM_ID) + assert addr == addr2 and bump == bump2 + + +def test_derive_distribution_pda_different_epochs(): + addr1, _ = derive_distribution_pda(PROGRAM_ID, 1) + addr2, _ = derive_distribution_pda(PROGRAM_ID, 2) + assert addr1 != addr2 + + +def test_derive_journal_pda(): + addr, _ = derive_journal_pda(PROGRAM_ID) + assert addr != Pubkey.default() + + +def test_derive_validator_deposit_pda(): + node_id = Pubkey.from_string("4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM") + addr, _ = derive_validator_deposit_pda(PROGRAM_ID, node_id) + assert addr != Pubkey.default() + + +def test_derive_contributor_rewards_pda(): + service_key = Pubkey.from_string("4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM") + addr, _ = derive_contributor_rewards_pda(PROGRAM_ID, service_key) + assert addr != Pubkey.default() diff --git a/sdk/revdist/testdata/fixtures/contributor_rewards.bin b/sdk/revdist/testdata/fixtures/contributor_rewards.bin new file mode 100644 index 000000000..5f1ad9368 Binary files /dev/null and b/sdk/revdist/testdata/fixtures/contributor_rewards.bin differ diff --git a/sdk/revdist/testdata/fixtures/contributor_rewards.json b/sdk/revdist/testdata/fixtures/contributor_rewards.json new file mode 100644 index 000000000..b91dba84b --- /dev/null +++ b/sdk/revdist/testdata/fixtures/contributor_rewards.json @@ -0,0 +1,22 @@ +{ + "name": "ContributorRewards", + "struct_size": 600, + "discriminator_hex": "711ed92800b9e2cb", + "fields": [ + { + "name": "RewardsManagerKey", + "value": "g35TxFqwMx95vCk63fTxGTHb6ei4W24qg5t2x6xD3cT", + "typ": "pubkey" + }, + { + "name": "ServiceKey", + "value": "jwV7SyvqCSrVcKibYvurCCWr7DUmT7yRYPmY9QwvrGo", + "typ": "pubkey" + }, + { + "name": "Flags", + "value": "1", + "typ": "u64" + } + ] +} \ No newline at end of file diff --git a/sdk/revdist/testdata/fixtures/distribution.bin b/sdk/revdist/testdata/fixtures/distribution.bin new file mode 100644 index 000000000..aa452e97b Binary files /dev/null and b/sdk/revdist/testdata/fixtures/distribution.bin differ diff --git a/sdk/revdist/testdata/fixtures/distribution.json b/sdk/revdist/testdata/fixtures/distribution.json new file mode 100644 index 000000000..efd85a6e5 --- /dev/null +++ b/sdk/revdist/testdata/fixtures/distribution.json @@ -0,0 +1,107 @@ +{ + "name": "Distribution", + "struct_size": 448, + "discriminator_hex": "879e8fd81d22c025", + "fields": [ + { + "name": "DZEpoch", + "value": "100", + "typ": "u64" + }, + { + "name": "Flags", + "value": "7", + "typ": "u64" + }, + { + "name": "CommunityBurnRate", + "value": "250000000", + "typ": "u32" + }, + { + "name": "BaseBlockRewardsPct", + "value": "500", + "typ": "u16" + }, + { + "name": "PriorityBlockRewardsPct", + "value": "1000", + "typ": "u16" + }, + { + "name": "InflationRewardsPct", + "value": "200", + "typ": "u16" + }, + { + "name": "JitoTipsPct", + "value": "300", + "typ": "u16" + }, + { + "name": "FixedSOLAmount", + "value": "50000", + "typ": "u32" + }, + { + "name": "TotalSolanaValidators", + "value": "398", + "typ": "u32" + }, + { + "name": "SolanaValidatorPaymentsCount", + "value": "350", + "typ": "u32" + }, + { + "name": "TotalSolanaValidatorDebt", + "value": "1000000000", + "typ": "u64" + }, + { + "name": "CollectedSolanaValidatorPayments", + "value": "900000000", + "typ": "u64" + }, + { + "name": "TotalContributors", + "value": "13", + "typ": "u32" + }, + { + "name": "DistributedRewardsCount", + "value": "13", + "typ": "u32" + }, + { + "name": "CollectedPrepaid2ZPayments", + "value": "500000", + "typ": "u64" + }, + { + "name": "Collected2ZConvertedFromSOL", + "value": "400000", + "typ": "u64" + }, + { + "name": "UncollectibleSOLDebt", + "value": "100000", + "typ": "u64" + }, + { + "name": "Distributed2ZAmount", + "value": "2000000", + "typ": "u64" + }, + { + "name": "Burned2ZAmount", + "value": "1500000", + "typ": "u64" + }, + { + "name": "SolanaValidatorWriteOffCount", + "value": "5", + "typ": "u32" + } + ] +} \ No newline at end of file diff --git a/sdk/revdist/testdata/fixtures/generate-fixtures/.gitignore b/sdk/revdist/testdata/fixtures/generate-fixtures/.gitignore new file mode 100644 index 000000000..2f7896d1d --- /dev/null +++ b/sdk/revdist/testdata/fixtures/generate-fixtures/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/sdk/revdist/testdata/fixtures/generate-fixtures/Cargo.lock b/sdk/revdist/testdata/fixtures/generate-fixtures/Cargo.lock new file mode 100644 index 000000000..17aa572d6 --- /dev/null +++ b/sdk/revdist/testdata/fixtures/generate-fixtures/Cargo.lock @@ -0,0 +1,1861 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "alloy-rlp" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f70d83b765fdc080dbcd4f4db70d8d23fe4761f2f02ebfa9146b833900634b4" +dependencies = [ + "arrayvec", + "bytes", +] + +[[package]] +name = "ark-ff" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b3235cc41ee7a12aaaf2c575a2ad7b46713a8a50bda2fc3b003a04845c05dd6" +dependencies = [ + "ark-ff-asm 0.3.0", + "ark-ff-macros 0.3.0", + "ark-serialize 0.3.0", + "ark-std 0.3.0", + "derivative", + "num-bigint", + "num-traits", + "paste", + "rustc_version 0.3.3", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" +dependencies = [ + "ark-ff-asm 0.4.2", + "ark-ff-macros 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", + "derivative", + "digest 0.10.7", + "itertools", + "num-bigint", + "num-traits", + "paste", + "rustc_version 0.4.1", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db02d390bf6643fb404d3d22d31aee1c4bc4459600aef9113833d17e786c6e44" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-asm" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20" +dependencies = [ + "num-bigint", + "num-traits", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-serialize" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6c2b318ee6e10f8c2853e73a83adc0ccb88995aa978d8a3408d492ab2ee671" +dependencies = [ + "ark-std 0.3.0", + "digest 0.9.0", +] + +[[package]] +name = "ark-serialize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" +dependencies = [ + "ark-std 0.4.0", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-std" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "ark-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "auto_impl" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "borsh" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8834bb1d8ee5dc048ee3124f2c7c1afcc6bc9aed03f11e9dfd8c69470a5db340" +dependencies = [ + "feature-probe", + "serde", +] + +[[package]] +name = "byte-slice-cast" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "const_format" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rand_core 0.6.4", + "rustc_version 0.4.1", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "doublezero-program-tools" +version = "0.0.0" +source = "git+ssh://git@github.com/doublezerofoundation/doublezero-solana.git#ad14fa58178f6b0feeaf21ded892741060af60c5" +dependencies = [ + "bincode", + "borsh", + "bytemuck", + "ruint", + "sha2-const-stable", + "solana-account-info", + "solana-cpi", + "solana-instruction", + "solana-loader-v3-interface", + "solana-msg", + "solana-program-error", + "solana-program-pack", + "solana-pubkey 3.0.0", + "solana-system-interface", + "solana-sysvar", + "spl-token-interface", +] + +[[package]] +name = "doublezero-revenue-distribution" +version = "0.2.1" +source = "git+ssh://git@github.com/doublezerofoundation/doublezero-solana.git#ad14fa58178f6b0feeaf21ded892741060af60c5" +dependencies = [ + "borsh", + "bytemuck", + "doublezero-program-tools", + "ruint", + "solana-account-info", + "solana-cpi", + "solana-instruction", + "solana-msg", + "solana-program-entrypoint", + "solana-program-error", + "solana-program-memory", + "solana-program-pack", + "solana-pubkey 3.0.0", + "solana-system-interface", + "solana-sysvar", + "spl-associated-token-account-interface", + "spl-token-interface", + "svm-hash", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fastrlp" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418" +dependencies = [ + "arrayvec", + "auto_impl", + "bytes", +] + +[[package]] +name = "fastrlp" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce8dba4714ef14b8274c371879b175aa55b16b30f269663f19d576f380018dc4" +dependencies = [ + "arrayvec", + "auto_impl", + "bytes", +] + +[[package]] +name = "feature-probe" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "five8" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f76610e969fa1784327ded240f1e28a3fd9520c9cec93b636fcf62dd37f772" +dependencies = [ + "five8_core", +] + +[[package]] +name = "five8_const" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a0f1728185f277989ca573a402716ae0beaaea3f76a8ff87ef9dd8fb19436c5" +dependencies = [ + "five8_core", +] + +[[package]] +name = "five8_core" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "059c31d7d36c43fe39d89e55711858b4da8be7eb6dabac23c7289b1a19489406" + +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "byteorder", + "rand 0.8.5", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "generate-fixtures" +version = "0.0.0" +dependencies = [ + "bytemuck", + "doublezero-program-tools", + "doublezero-revenue-distribution", + "ruint", + "serde", + "serde_json", + "solana-pubkey 3.0.0", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "impl-codec" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "parity-scale-codec" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" +dependencies = [ + "arrayvec", + "bitvec", + "byte-slice-cast", + "const_format", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "rustversion", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pest" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "primitive-types" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" +dependencies = [ + "fixed-hash", + "impl-codec", + "uint", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" +dependencies = [ + "bitflags", + "num-traits", + "rand 0.9.2", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "unarray", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rlp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" +dependencies = [ + "bytes", + "rustc-hex", +] + +[[package]] +name = "ruint" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecb38f82477f20c5c3d62ef52d7c4e536e38ea9b73fb570a20c5cae0e14bcf6" +dependencies = [ + "alloy-rlp", + "ark-ff 0.3.0", + "ark-ff 0.4.2", + "bytemuck", + "bytes", + "fastrlp 0.3.1", + "fastrlp 0.4.0", + "num-bigint", + "num-integer", + "num-traits", + "parity-scale-codec", + "primitive-types", + "proptest", + "rand 0.8.5", + "rand 0.9.2", + "rlp", + "ruint-macro", + "serde", + "valuable", + "zeroize", +] + +[[package]] +name = "ruint-macro" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "rustc_version" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +dependencies = [ + "semver 0.11.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver 1.0.27", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "semver-parser" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9900206b54a3527fdc7b8a938bffd94a568bac4f4aa8113b209df75a09c0dec2" +dependencies = [ + "pest", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha2-const-stable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f179d4e11094a893b82fff208f74d448a7512f99f5a0acbd5c679b705f83ed9" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "solana-account-info" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc3397241392f5756925029acaa8515dc70fcbe3d8059d4885d7d6533baf64fd" +dependencies = [ + "solana-address 2.0.0", + "solana-program-error", + "solana-program-memory", +] + +[[package]] +name = "solana-address" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2ecac8e1b7f74c2baa9e774c42817e3e75b20787134b76cc4d45e8a604488f5" +dependencies = [ + "solana-address 2.0.0", +] + +[[package]] +name = "solana-address" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e37320fd2945c5d654b2c6210624a52d66c3f1f73b653ed211ab91a703b35bdd" +dependencies = [ + "borsh", + "bytemuck", + "bytemuck_derive", + "curve25519-dalek", + "five8", + "five8_const", + "serde", + "serde_derive", + "solana-atomic-u64", + "solana-define-syscall 4.0.1", + "solana-program-error", + "solana-sanitize", + "solana-sha256-hasher", +] + +[[package]] +name = "solana-atomic-u64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a933ff1e50aff72d02173cfcd7511bd8540b027ee720b75f353f594f834216d0" +dependencies = [ + "parking_lot", +] + +[[package]] +name = "solana-clock" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb62e9381182459a4520b5fe7fb22d423cae736239a6427fc398a88743d0ed59" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-cpi" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dea26709d867aada85d0d3617db0944215c8bb28d3745b912de7db13a23280c" +dependencies = [ + "solana-account-info", + "solana-define-syscall 4.0.1", + "solana-instruction", + "solana-program-error", + "solana-pubkey 4.0.0", + "solana-stable-layout", +] + +[[package]] +name = "solana-define-syscall" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9697086a4e102d28a156b8d6b521730335d6951bd39a5e766512bbe09007cee" + +[[package]] +name = "solana-define-syscall" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57e5b1c0bc1d4a4d10c88a4100499d954c09d3fecfae4912c1a074dff68b1738" + +[[package]] +name = "solana-epoch-rewards" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b319a4ed70390af911090c020571f0ff1f4ec432522d05ab89f5c08080381995" +dependencies = [ + "serde", + "serde_derive", + "solana-hash 3.1.0", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-epoch-schedule" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e5481e72cc4d52c169db73e4c0cd16de8bc943078aac587ec4817a75cc6388f" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-fee-calculator" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a73cc03ca4bed871ca174558108835f8323e85917bb38b9c81c7af2ab853efe" +dependencies = [ + "log", + "serde", + "serde_derive", +] + +[[package]] +name = "solana-hash" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "337c246447142f660f778cf6cb582beba8e28deb05b3b24bfb9ffd7c562e5f41" +dependencies = [ + "solana-hash 4.0.1", +] + +[[package]] +name = "solana-hash" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a5d48a6ee7b91fc7b998944ab026ed7b3e2fc8ee3bc58452644a86c2648152f" +dependencies = [ + "borsh", + "bytemuck", + "bytemuck_derive", + "five8", + "serde", + "serde_derive", + "solana-atomic-u64", + "solana-sanitize", +] + +[[package]] +name = "solana-instruction" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee1b699a2c1518028a9982e255e0eca10c44d90006542d9d7f9f40dbce3f7c78" +dependencies = [ + "bincode", + "borsh", + "serde", + "solana-define-syscall 4.0.1", + "solana-instruction-error", + "solana-pubkey 4.0.0", +] + +[[package]] +name = "solana-instruction-error" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b04259e03c05faf38a8c24217b5cfe4c90572ae6184ab49cddb1584fdd756d3f" +dependencies = [ + "num-traits", + "solana-program-error", +] + +[[package]] +name = "solana-last-restart-slot" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcda154ec827f5fc1e4da0af3417951b7e9b8157540f81f936c4a8b1156134d0" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-loader-v3-interface" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee44c9b1328c5c712c68966fb8de07b47f3e7bac006e74ddd1bb053d3e46e5d" +dependencies = [ + "serde", + "serde_bytes", + "serde_derive", + "solana-instruction", + "solana-pubkey 3.0.0", + "solana-sdk-ids", +] + +[[package]] +name = "solana-msg" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "264275c556ea7e22b9d3f87d56305546a38d4eee8ec884f3b126236cb7dcbbb4" +dependencies = [ + "solana-define-syscall 3.0.0", +] + +[[package]] +name = "solana-program-entrypoint" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c9b0a1ff494e05f503a08b3d51150b73aa639544631e510279d6375f290997" +dependencies = [ + "solana-account-info", + "solana-define-syscall 4.0.1", + "solana-program-error", + "solana-pubkey 4.0.0", +] + +[[package]] +name = "solana-program-error" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1af32c995a7b692a915bb7414d5f8e838450cf7c70414e763d8abcae7b51f28" + +[[package]] +name = "solana-program-memory" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4068648649653c2c50546e9a7fb761791b5ab0cda054c771bb5808d3a4b9eb52" +dependencies = [ + "solana-define-syscall 4.0.1", +] + +[[package]] +name = "solana-program-option" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e7b4ddb464f274deb4a497712664c3b612e3f5f82471d4e47710fc4ab1c3095" + +[[package]] +name = "solana-program-pack" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c169359de21f6034a63ebf96d6b380980307df17a8d371344ff04a883ec4e9d0" +dependencies = [ + "solana-program-error", +] + +[[package]] +name = "solana-pubkey" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8909d399deb0851aa524420beeb5646b115fd253ef446e35fe4504c904da3941" +dependencies = [ + "solana-address 1.1.0", +] + +[[package]] +name = "solana-pubkey" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6f7104d456b58e1418c21a8581e89810278d1190f70f27ece7fc0b2c9282a57" +dependencies = [ + "solana-address 2.0.0", +] + +[[package]] +name = "solana-rent" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e860d5499a705369778647e97d760f7670adfb6fc8419dd3d568deccd46d5487" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-sanitize" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf09694a0fc14e5ffb18f9b7b7c0f15ecb6eac5b5610bf76a1853459d19daf9" + +[[package]] +name = "solana-sdk-ids" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def234c1956ff616d46c9dd953f251fa7096ddbaa6d52b165218de97882b7280" +dependencies = [ + "solana-address 2.0.0", +] + +[[package]] +name = "solana-sdk-macro" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6430000e97083460b71d9fbadc52a2ab2f88f53b3a4c5e58c5ae3640a0e8c00" +dependencies = [ + "bs58", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "solana-sha256-hasher" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db7dc3011ea4c0334aaaa7e7128cb390ecf546b28d412e9bf2064680f57f588f" +dependencies = [ + "sha2", + "solana-define-syscall 4.0.1", + "solana-hash 4.0.1", +] + +[[package]] +name = "solana-slot-hashes" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80a293f952293281443c04f4d96afd9d547721923d596e92b4377ed2360f1746" +dependencies = [ + "serde", + "serde_derive", + "solana-hash 3.1.0", + "solana-sdk-ids", + "solana-sysvar-id", +] + +[[package]] +name = "solana-slot-history" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f914f6b108f5bba14a280b458d023e3621c9973f27f015a4d755b50e88d89e97" +dependencies = [ + "bv", + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sysvar-id", +] + +[[package]] +name = "solana-stable-layout" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1da74507795b6e8fb60b7c7306c0c36e2c315805d16eaaf479452661234685ac" +dependencies = [ + "solana-instruction", + "solana-pubkey 3.0.0", +] + +[[package]] +name = "solana-system-interface" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14591d6508042ebefb110305d3ba761615927146a26917ade45dc332d8e1ecde" +dependencies = [ + "num-traits", + "serde", + "serde_derive", + "solana-address 2.0.0", + "solana-instruction", + "solana-msg", + "solana-program-error", +] + +[[package]] +name = "solana-sysvar" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6690d3dd88f15c21edff68eb391ef8800df7a1f5cec84ee3e8d1abf05affdf74" +dependencies = [ + "base64", + "bincode", + "lazy_static", + "serde", + "serde_derive", + "solana-account-info", + "solana-clock", + "solana-define-syscall 4.0.1", + "solana-epoch-rewards", + "solana-epoch-schedule", + "solana-fee-calculator", + "solana-hash 4.0.1", + "solana-instruction", + "solana-last-restart-slot", + "solana-program-entrypoint", + "solana-program-error", + "solana-program-memory", + "solana-pubkey 4.0.0", + "solana-rent", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-slot-hashes", + "solana-slot-history", + "solana-sysvar-id", +] + +[[package]] +name = "solana-sysvar-id" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17358d1e9a13e5b9c2264d301102126cf11a47fd394cdf3dec174fe7bc96e1de" +dependencies = [ + "solana-address 2.0.0", + "solana-sdk-ids", +] + +[[package]] +name = "spl-associated-token-account-interface" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6433917b60441d68d99a17e121d9db0ea15a9a69c0e5afa34649cf5ba12612f" +dependencies = [ + "solana-instruction", + "solana-pubkey 3.0.0", +] + +[[package]] +name = "spl-token-interface" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c564ac05a7c8d8b12e988a37d82695b5ba4db376d07ea98bc4882c81f96c7f3" +dependencies = [ + "arrayref", + "bytemuck", + "num-derive", + "num-traits", + "num_enum", + "solana-instruction", + "solana-program-error", + "solana-program-option", + "solana-program-pack", + "solana-pubkey 3.0.0", + "solana-sdk-ids", + "thiserror", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "svm-hash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5daf24ef00565f2d53ab3fd548b0ac1f19325524061028ba9698902d86bb71" +dependencies = [ + "borsh", + "bytemuck", + "solana-hash 4.0.1", + "solana-sha256-hasher", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ + "winnow", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "uint" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "zerocopy" +version = "0.8.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7456cf00f0685ad319c5b1693f291a650eaf345e941d082fc4e03df8a03996ac" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1328722bbf2115db7e19d69ebcc15e795719e2d66b60827c6a69a117365e37a0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "zmij" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1966f8ac2c1f76987d69a74d0e0f929241c10e78136434e3be70ff7f58f64214" diff --git a/sdk/revdist/testdata/fixtures/generate-fixtures/Cargo.toml b/sdk/revdist/testdata/fixtures/generate-fixtures/Cargo.toml new file mode 100644 index 000000000..ecde3e992 --- /dev/null +++ b/sdk/revdist/testdata/fixtures/generate-fixtures/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "generate-fixtures" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies] +bytemuck = { version = "1", features = ["derive"] } +doublezero-program-tools = { git = "https://github.com/doublezerofoundation/doublezero-solana.git" } +doublezero-revenue-distribution = { git = "https://github.com/doublezerofoundation/doublezero-solana.git" } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +ruint = "1" +solana-pubkey = { version = "3", features = ["bytemuck"] } diff --git a/sdk/revdist/testdata/fixtures/generate-fixtures/src/main.rs b/sdk/revdist/testdata/fixtures/generate-fixtures/src/main.rs new file mode 100644 index 000000000..50a967644 --- /dev/null +++ b/sdk/revdist/testdata/fixtures/generate-fixtures/src/main.rs @@ -0,0 +1,319 @@ +//! Generates binary fixture files from the Rust revenue-distribution structs +//! with known field values. The Go SDK compatibility tests deserialize these +//! fixtures and verify that field values match. +//! +//! Run with: cargo run (from this directory) +//! Output: ../fixtures/*.bin and ../fixtures/*.json +//! +//! The key property: bytes are produced by `bytemuck::bytes_of` on real Rust +//! struct instances with fields set via struct access. The byte layout is +//! therefore determined by the Rust compiler's `#[repr(C)]` layout — NOT by +//! hand-coded offsets. This makes the fixtures authoritative for cross-language +//! compatibility testing. + +use std::fs; +use std::path::Path; + +use bytemuck::bytes_of; +use doublezero_program_tools::PrecomputedDiscriminator; +use doublezero_revenue_distribution::state::{ + ContributorRewards, Distribution, Journal, ProgramConfig, SolanaValidatorDeposit, +}; +use doublezero_revenue_distribution::types::{BurnRate, DoubleZeroEpoch, ValidatorFee}; +use serde::Serialize; +use ruint::aliases::U64; +use solana_pubkey::Pubkey; + +#[derive(Serialize)] +struct FixtureMeta { + name: String, + struct_size: usize, + discriminator_hex: String, + fields: Vec, +} + +#[derive(Serialize)] +struct FieldValue { + name: String, + value: String, + /// "u8", "u16", "u32", "u64", "pubkey" + typ: String, +} + +fn pubkey_from_byte(b: u8) -> Pubkey { + let mut bytes = [0u8; 32]; + bytes[0] = b; + Pubkey::from(bytes) +} + +fn write_fixture( + dir: &Path, + name: &str, + discriminator: &[u8], + struct_bytes: &[u8], + meta: &FixtureMeta, +) { + let mut data = Vec::with_capacity(8 + struct_bytes.len()); + data.extend_from_slice(discriminator); + data.extend_from_slice(struct_bytes); + fs::write(dir.join(format!("{name}.bin")), &data).unwrap(); + + let json = serde_json::to_string_pretty(meta).unwrap(); + fs::write(dir.join(format!("{name}.json")), json).unwrap(); + + println!("wrote {name}.bin ({} bytes) and {name}.json", data.len()); +} + +fn main() { + let fixtures_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join(".."); + fs::create_dir_all(&fixtures_dir).unwrap(); + + generate_program_config(&fixtures_dir); + generate_distribution(&fixtures_dir); + generate_journal(&fixtures_dir); + generate_solana_validator_deposit(&fixtures_dir); + generate_contributor_rewards(&fixtures_dir); + + println!("\nall fixtures generated in {}", fixtures_dir.display()); +} + +fn burn_rate(v: u32) -> BurnRate { + BurnRate::new(v).unwrap() +} + +fn validator_fee(v: u16) -> ValidatorFee { + ValidatorFee::new(v).unwrap() +} + +fn generate_program_config(dir: &Path) { + let mut config = ProgramConfig::default(); + config.flags = U64::from(1); + config.next_completed_dz_epoch = DoubleZeroEpoch::new(42); + config.bump_seed = 253; + config.reserve_2z_bump_seed = 252; + config.swap_authority_bump_seed = 251; + config.swap_destination_2z_bump_seed = 250; + config.withdraw_sol_authority_bump_seed = 249; + config.admin_key = pubkey_from_byte(1); + config.debt_accountant_key = pubkey_from_byte(2); + config.rewards_accountant_key = pubkey_from_byte(3); + config.contributor_manager_key = pubkey_from_byte(4); + config._placeholder_key = pubkey_from_byte(5); + config.sol_2z_swap_program_id = pubkey_from_byte(6); + config.distribution_parameters.calculation_grace_period_minutes = 120; + config.distribution_parameters.initialization_grace_period_minutes = 60; + config.distribution_parameters.minimum_epoch_duration_to_finalize_rewards = 3; + config.distribution_parameters.community_burn_rate_parameters.limit = burn_rate(500_000_000); + config + .distribution_parameters + .community_burn_rate_parameters + .dz_epochs_to_increasing = 10; + config + .distribution_parameters + .community_burn_rate_parameters + .dz_epochs_to_limit = 100; + config + .distribution_parameters + .solana_validator_fee_parameters + .base_block_rewards_pct = validator_fee(500); + config + .distribution_parameters + .solana_validator_fee_parameters + .priority_block_rewards_pct = validator_fee(1000); + config + .distribution_parameters + .solana_validator_fee_parameters + .inflation_rewards_pct = validator_fee(200); + config + .distribution_parameters + .solana_validator_fee_parameters + .jito_tips_pct = validator_fee(300); + config + .distribution_parameters + .solana_validator_fee_parameters + .fixed_sol_amount = 50000; + config.relay_parameters.distribute_rewards_lamports = 10000; + config.last_initialized_distribution_timestamp = 1_700_000_000; + config.debt_write_off_feature_activation_epoch = DoubleZeroEpoch::new(91); + + let disc = ProgramConfig::discriminator_slice(); + let meta = FixtureMeta { + name: "ProgramConfig".into(), + struct_size: std::mem::size_of::(), + discriminator_hex: hex_encode(disc), + fields: vec![ + field_u64("Flags", 1), + field_u64("NextCompletedDZEpoch", 42), + field_u8("BumpSeed", 253), + field_pubkey("AdminKey", &pubkey_from_byte(1)), + field_pubkey("DebtAccountantKey", &pubkey_from_byte(2)), + field_pubkey("RewardsAccountantKey", &pubkey_from_byte(3)), + field_pubkey("ContributorManagerKey", &pubkey_from_byte(4)), + field_pubkey("SOL2ZSwapProgramID", &pubkey_from_byte(6)), + field_u16("CalculationGracePeriodMinutes", 120), + field_u16("InitializationGracePeriodMinutes", 60), + field_u8("MinimumEpochDurationToFinalizeRewards", 3), + field_u32("BurnRateLimit", 500_000_000), + field_u32("BurnRateDZEpochsToIncreasing", 10), + field_u32("BurnRateDZEpochsToLimit", 100), + field_u16("BaseBlockRewardsPct", 500), + field_u16("PriorityBlockRewardsPct", 1000), + field_u16("InflationRewardsPct", 200), + field_u16("JitoTipsPct", 300), + field_u32("FixedSOLAmount", 50000), + field_u32("DistributeRewardsLamports", 10000), + field_u64("DebtWriteOffFeatureActivationEpoch", 91), + ], + }; + + write_fixture(dir, "program_config", disc, bytes_of(&config), &meta); +} + +fn generate_distribution(dir: &Path) { + let mut dist = Distribution::default(); + dist.dz_epoch = DoubleZeroEpoch::new(100); + dist.flags = U64::from(7); + dist.community_burn_rate = burn_rate(250_000_000); + dist.bump_seed = 254; + dist.token_2z_pda_bump_seed = 253; + dist.solana_validator_fee_parameters.base_block_rewards_pct = validator_fee(500); + dist.solana_validator_fee_parameters.priority_block_rewards_pct = validator_fee(1000); + dist.solana_validator_fee_parameters.inflation_rewards_pct = validator_fee(200); + dist.solana_validator_fee_parameters.jito_tips_pct = validator_fee(300); + dist.solana_validator_fee_parameters.fixed_sol_amount = 50000; + dist.total_solana_validators = 398; + dist.solana_validator_payments_count = 350; + dist.total_solana_validator_debt = 1_000_000_000; + dist.collected_solana_validator_payments = 900_000_000; + dist.total_contributors = 13; + dist.distributed_rewards_count = 13; + dist.collected_prepaid_2z_payments = 500_000; + dist.collected_2z_converted_from_sol = 400_000; + dist.uncollectible_sol_debt = 100_000; + dist.distributed_2z_amount = 2_000_000; + dist.burned_2z_amount = 1_500_000; + dist.solana_validator_write_off_count = 5; + + let disc = Distribution::discriminator_slice(); + let meta = FixtureMeta { + name: "Distribution".into(), + struct_size: std::mem::size_of::(), + discriminator_hex: hex_encode(disc), + fields: vec![ + field_u64("DZEpoch", 100), + field_u64("Flags", 7), + field_u32("CommunityBurnRate", 250_000_000), + field_u16("BaseBlockRewardsPct", 500), + field_u16("PriorityBlockRewardsPct", 1000), + field_u16("InflationRewardsPct", 200), + field_u16("JitoTipsPct", 300), + field_u32("FixedSOLAmount", 50000), + field_u32("TotalSolanaValidators", 398), + field_u32("SolanaValidatorPaymentsCount", 350), + field_u64("TotalSolanaValidatorDebt", 1_000_000_000), + field_u64("CollectedSolanaValidatorPayments", 900_000_000), + field_u32("TotalContributors", 13), + field_u32("DistributedRewardsCount", 13), + field_u64("CollectedPrepaid2ZPayments", 500_000), + field_u64("Collected2ZConvertedFromSOL", 400_000), + field_u64("UncollectibleSOLDebt", 100_000), + field_u64("Distributed2ZAmount", 2_000_000), + field_u64("Burned2ZAmount", 1_500_000), + field_u32("SolanaValidatorWriteOffCount", 5), + ], + }; + + write_fixture(dir, "distribution", disc, bytes_of(&dist), &meta); +} + +fn generate_journal(dir: &Path) { + let mut journal = Journal::default(); + journal.bump_seed = 255; + journal.token_2z_pda_bump_seed = 254; + journal.total_sol_balance = 5_000_000_000; + journal.total_2z_balance = 10_000_000; + journal.swap_2z_destination_balance = 3_000_000; + journal.swapped_sol_amount = 2_500_000_000; + journal.next_dz_epoch_to_sweep_tokens = DoubleZeroEpoch::new(50); + + let disc = Journal::discriminator_slice(); + let meta = FixtureMeta { + name: "Journal".into(), + struct_size: std::mem::size_of::(), + discriminator_hex: hex_encode(disc), + fields: vec![ + field_u8("BumpSeed", 255), + field_u64("TotalSOLBalance", 5_000_000_000), + field_u64("Total2ZBalance", 10_000_000), + field_u64("Swap2ZDestinationBalance", 3_000_000), + field_u64("SwappedSOLAmount", 2_500_000_000), + field_u64("NextDZEpochToSweepTokens", 50), + ], + }; + + write_fixture(dir, "journal", disc, bytes_of(&journal), &meta); +} + +fn generate_solana_validator_deposit(dir: &Path) { + let mut deposit = SolanaValidatorDeposit::default(); + deposit.node_id = pubkey_from_byte(42); + deposit.written_off_sol_debt = 999_999; + + let disc = SolanaValidatorDeposit::discriminator_slice(); + let meta = FixtureMeta { + name: "SolanaValidatorDeposit".into(), + struct_size: std::mem::size_of::(), + discriminator_hex: hex_encode(disc), + fields: vec![ + field_pubkey("NodeID", &pubkey_from_byte(42)), + field_u64("WrittenOffSOLDebt", 999_999), + ], + }; + + write_fixture(dir, "solana_validator_deposit", disc, bytes_of(&deposit), &meta); +} + +fn generate_contributor_rewards(dir: &Path) { + let mut rewards = ContributorRewards::default(); + rewards.rewards_manager_key = pubkey_from_byte(10); + rewards.service_key = pubkey_from_byte(11); + rewards.flags = U64::from(1); + + let disc = ContributorRewards::discriminator_slice(); + let meta = FixtureMeta { + name: "ContributorRewards".into(), + struct_size: std::mem::size_of::(), + discriminator_hex: hex_encode(disc), + fields: vec![ + field_pubkey("RewardsManagerKey", &pubkey_from_byte(10)), + field_pubkey("ServiceKey", &pubkey_from_byte(11)), + field_u64("Flags", 1), + ], + }; + + write_fixture(dir, "contributor_rewards", disc, bytes_of(&rewards), &meta); +} + +fn field_u8(name: &str, value: u8) -> FieldValue { + FieldValue { name: name.into(), value: value.to_string(), typ: "u8".into() } +} + +fn field_u16(name: &str, value: u16) -> FieldValue { + FieldValue { name: name.into(), value: value.to_string(), typ: "u16".into() } +} + +fn field_u32(name: &str, value: u32) -> FieldValue { + FieldValue { name: name.into(), value: value.to_string(), typ: "u32".into() } +} + +fn field_u64(name: &str, value: u64) -> FieldValue { + FieldValue { name: name.into(), value: value.to_string(), typ: "u64".into() } +} + +fn field_pubkey(name: &str, key: &Pubkey) -> FieldValue { + FieldValue { name: name.into(), value: key.to_string(), typ: "pubkey".into() } +} + +fn hex_encode(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02x}")).collect() +} diff --git a/sdk/revdist/testdata/fixtures/journal.bin b/sdk/revdist/testdata/fixtures/journal.bin new file mode 100644 index 000000000..7a6350b4d Binary files /dev/null and b/sdk/revdist/testdata/fixtures/journal.bin differ diff --git a/sdk/revdist/testdata/fixtures/journal.json b/sdk/revdist/testdata/fixtures/journal.json new file mode 100644 index 000000000..e63b269a3 --- /dev/null +++ b/sdk/revdist/testdata/fixtures/journal.json @@ -0,0 +1,37 @@ +{ + "name": "Journal", + "struct_size": 64, + "discriminator_hex": "f97c5314a23e4309", + "fields": [ + { + "name": "BumpSeed", + "value": "255", + "typ": "u8" + }, + { + "name": "TotalSOLBalance", + "value": "5000000000", + "typ": "u64" + }, + { + "name": "Total2ZBalance", + "value": "10000000", + "typ": "u64" + }, + { + "name": "Swap2ZDestinationBalance", + "value": "3000000", + "typ": "u64" + }, + { + "name": "SwappedSOLAmount", + "value": "2500000000", + "typ": "u64" + }, + { + "name": "NextDZEpochToSweepTokens", + "value": "50", + "typ": "u64" + } + ] +} \ No newline at end of file diff --git a/sdk/revdist/testdata/fixtures/program_config.bin b/sdk/revdist/testdata/fixtures/program_config.bin new file mode 100644 index 000000000..df9def2de Binary files /dev/null and b/sdk/revdist/testdata/fixtures/program_config.bin differ diff --git a/sdk/revdist/testdata/fixtures/program_config.json b/sdk/revdist/testdata/fixtures/program_config.json new file mode 100644 index 000000000..0bafcdee5 --- /dev/null +++ b/sdk/revdist/testdata/fixtures/program_config.json @@ -0,0 +1,112 @@ +{ + "name": "ProgramConfig", + "struct_size": 600, + "discriminator_hex": "cfb485ec3027f11b", + "fields": [ + { + "name": "Flags", + "value": "1", + "typ": "u64" + }, + { + "name": "NextCompletedDZEpoch", + "value": "42", + "typ": "u64" + }, + { + "name": "BumpSeed", + "value": "253", + "typ": "u8" + }, + { + "name": "AdminKey", + "value": "4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM", + "typ": "pubkey" + }, + { + "name": "DebtAccountantKey", + "value": "8opHzTAnfzRpPEx21XtnrVTX28YQuCpAjcn1PczScKh", + "typ": "pubkey" + }, + { + "name": "RewardsAccountantKey", + "value": "CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3", + "typ": "pubkey" + }, + { + "name": "ContributorManagerKey", + "value": "GcdayuLaLyrdmUu324nahyv33G5poQdLUEZ1nEytDeP", + "typ": "pubkey" + }, + { + "name": "SOL2ZSwapProgramID", + "value": "QRSsyMWN1yHT9ir42bgNZUNZ4PdEhcSWCrL2AryKpy5", + "typ": "pubkey" + }, + { + "name": "CalculationGracePeriodMinutes", + "value": "120", + "typ": "u16" + }, + { + "name": "InitializationGracePeriodMinutes", + "value": "60", + "typ": "u16" + }, + { + "name": "MinimumEpochDurationToFinalizeRewards", + "value": "3", + "typ": "u8" + }, + { + "name": "BurnRateLimit", + "value": "500000000", + "typ": "u32" + }, + { + "name": "BurnRateDZEpochsToIncreasing", + "value": "10", + "typ": "u32" + }, + { + "name": "BurnRateDZEpochsToLimit", + "value": "100", + "typ": "u32" + }, + { + "name": "BaseBlockRewardsPct", + "value": "500", + "typ": "u16" + }, + { + "name": "PriorityBlockRewardsPct", + "value": "1000", + "typ": "u16" + }, + { + "name": "InflationRewardsPct", + "value": "200", + "typ": "u16" + }, + { + "name": "JitoTipsPct", + "value": "300", + "typ": "u16" + }, + { + "name": "FixedSOLAmount", + "value": "50000", + "typ": "u32" + }, + { + "name": "DistributeRewardsLamports", + "value": "10000", + "typ": "u32" + }, + { + "name": "DebtWriteOffFeatureActivationEpoch", + "value": "91", + "typ": "u64" + } + ] +} \ No newline at end of file diff --git a/sdk/revdist/testdata/fixtures/solana_validator_deposit.bin b/sdk/revdist/testdata/fixtures/solana_validator_deposit.bin new file mode 100644 index 000000000..84de89a76 Binary files /dev/null and b/sdk/revdist/testdata/fixtures/solana_validator_deposit.bin differ diff --git a/sdk/revdist/testdata/fixtures/solana_validator_deposit.json b/sdk/revdist/testdata/fixtures/solana_validator_deposit.json new file mode 100644 index 000000000..eaf3a2298 --- /dev/null +++ b/sdk/revdist/testdata/fixtures/solana_validator_deposit.json @@ -0,0 +1,17 @@ +{ + "name": "SolanaValidatorDeposit", + "struct_size": 96, + "discriminator_hex": "14e90cc59bf9cbaa", + "fields": [ + { + "name": "NodeID", + "value": "3px89oUYY7nzA43vNCBkbvJbsQjNeuH5XRxJ9C2oGnmV", + "typ": "pubkey" + }, + { + "name": "WrittenOffSOLDebt", + "value": "999999", + "typ": "u64" + } + ] +} \ No newline at end of file diff --git a/sdk/revdist/typescript/.gitignore b/sdk/revdist/typescript/.gitignore new file mode 100644 index 000000000..8c68c1a83 --- /dev/null +++ b/sdk/revdist/typescript/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +bun.lockb diff --git a/sdk/revdist/typescript/bun.lock b/sdk/revdist/typescript/bun.lock new file mode 100644 index 000000000..702442392 --- /dev/null +++ b/sdk/revdist/typescript/bun.lock @@ -0,0 +1,133 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "doublezero-revenue-distribution", + "dependencies": { + "@solana/web3.js": "^1.98", + }, + "devDependencies": { + "@types/bun": "latest", + "typescript": "^5", + }, + }, + }, + "packages": { + "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], + + "@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], + + "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@solana/buffer-layout": ["@solana/buffer-layout@4.0.1", "", { "dependencies": { "buffer": "~6.0.3" } }, "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA=="], + + "@solana/codecs-core": ["@solana/codecs-core@2.3.0", "", { "dependencies": { "@solana/errors": "2.3.0" }, "peerDependencies": { "typescript": ">=5.3.3" } }, "sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw=="], + + "@solana/codecs-numbers": ["@solana/codecs-numbers@2.3.0", "", { "dependencies": { "@solana/codecs-core": "2.3.0", "@solana/errors": "2.3.0" }, "peerDependencies": { "typescript": ">=5.3.3" } }, "sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg=="], + + "@solana/errors": ["@solana/errors@2.3.0", "", { "dependencies": { "chalk": "^5.4.1", "commander": "^14.0.0" }, "peerDependencies": { "typescript": ">=5.3.3" }, "bin": { "errors": "bin/cli.mjs" } }, "sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ=="], + + "@solana/web3.js": ["@solana/web3.js@1.98.4", "", { "dependencies": { "@babel/runtime": "^7.25.0", "@noble/curves": "^1.4.2", "@noble/hashes": "^1.4.0", "@solana/buffer-layout": "^4.0.1", "@solana/codecs-numbers": "^2.1.0", "agentkeepalive": "^4.5.0", "bn.js": "^5.2.1", "borsh": "^0.7.0", "bs58": "^4.0.1", "buffer": "6.0.3", "fast-stable-stringify": "^1.0.0", "jayson": "^4.1.1", "node-fetch": "^2.7.0", "rpc-websockets": "^9.0.2", "superstruct": "^2.0.2" } }, "sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw=="], + + "@swc/helpers": ["@swc/helpers@0.5.18", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ=="], + + "@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], + + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + + "@types/node": ["@types/node@25.1.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA=="], + + "@types/uuid": ["@types/uuid@8.3.4", "", {}, "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw=="], + + "@types/ws": ["@types/ws@7.4.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww=="], + + "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], + + "base-x": ["base-x@3.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "bn.js": ["bn.js@5.2.2", "", {}, "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw=="], + + "borsh": ["borsh@0.7.0", "", { "dependencies": { "bn.js": "^5.2.0", "bs58": "^4.0.0", "text-encoding-utf-8": "^1.0.2" } }, "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA=="], + + "bs58": ["bs58@4.0.1", "", { "dependencies": { "base-x": "^3.0.2" } }, "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw=="], + + "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + + "bufferutil": ["bufferutil@4.1.0", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw=="], + + "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], + + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + + "delay": ["delay@5.0.0", "", {}, "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw=="], + + "es6-promise": ["es6-promise@4.2.8", "", {}, "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="], + + "es6-promisify": ["es6-promisify@5.0.0", "", { "dependencies": { "es6-promise": "^4.0.3" } }, "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ=="], + + "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], + + "eyes": ["eyes@0.1.8", "", {}, "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ=="], + + "fast-stable-stringify": ["fast-stable-stringify@1.0.0", "", {}, "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag=="], + + "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "isomorphic-ws": ["isomorphic-ws@4.0.1", "", { "peerDependencies": { "ws": "*" } }, "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w=="], + + "jayson": ["jayson@4.3.0", "", { "dependencies": { "@types/connect": "^3.4.33", "@types/node": "^12.12.54", "@types/ws": "^7.4.4", "commander": "^2.20.3", "delay": "^5.0.0", "es6-promisify": "^5.0.0", "eyes": "^0.1.8", "isomorphic-ws": "^4.0.1", "json-stringify-safe": "^5.0.1", "stream-json": "^1.9.1", "uuid": "^8.3.2", "ws": "^7.5.10" }, "bin": { "jayson": "bin/jayson.js" } }, "sha512-AauzHcUcqs8OBnCHOkJY280VaTiCm57AbuO7lqzcw7JapGj50BisE3xhksye4zlTSR1+1tAz67wLTl8tEH1obQ=="], + + "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + + "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], + + "rpc-websockets": ["rpc-websockets@9.3.3", "", { "dependencies": { "@swc/helpers": "^0.5.11", "@types/uuid": "^8.3.4", "@types/ws": "^8.2.2", "buffer": "^6.0.3", "eventemitter3": "^5.0.1", "uuid": "^8.3.2", "ws": "^8.5.0" }, "optionalDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" } }, "sha512-OkCsBBzrwxX4DoSv4Zlf9DgXKRB0MzVfCFg5MC+fNnf9ktr4SMWjsri0VNZQlDbCnGcImT6KNEv4ZoxktQhdpA=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "stream-chain": ["stream-chain@2.2.5", "", {}, "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA=="], + + "stream-json": ["stream-json@1.9.1", "", { "dependencies": { "stream-chain": "^2.2.5" } }, "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw=="], + + "superstruct": ["superstruct@2.0.2", "", {}, "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A=="], + + "text-encoding-utf-8": ["text-encoding-utf-8@1.0.2", "", {}, "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg=="], + + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "utf-8-validate": ["utf-8-validate@5.0.10", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ=="], + + "uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], + + "@solana/errors/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + + "jayson/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], + + "rpc-websockets/@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + + "rpc-websockets/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + } +} diff --git a/sdk/revdist/typescript/package.json b/sdk/revdist/typescript/package.json new file mode 100644 index 000000000..c0ef03176 --- /dev/null +++ b/sdk/revdist/typescript/package.json @@ -0,0 +1,21 @@ +{ + "name": "@doublezero/revenue-distribution", + "version": "0.0.1", + "type": "module", + "main": "dist/revdist/index.js", + "types": "dist/revdist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "prepublishOnly": "tsc" + }, + "dependencies": { + "@solana/web3.js": "^1.98" + }, + "devDependencies": { + "@types/bun": "latest", + "typescript": "^5" + } +} diff --git a/sdk/revdist/typescript/revdist/client.ts b/sdk/revdist/typescript/revdist/client.ts new file mode 100644 index 000000000..25b3aa073 --- /dev/null +++ b/sdk/revdist/typescript/revdist/client.ts @@ -0,0 +1,202 @@ +/** Read-only client for revenue distribution program accounts. */ + +import { Connection, PublicKey } from "@solana/web3.js"; +// @ts-ignore - no type declarations available +import bs58 from "bs58"; +import { PROGRAM_ID, SOLANA_RPC_URLS, LEDGER_RPC_URLS } from "./config.js"; +import { newConnection } from "./rpc.js"; +import { + DISCRIMINATOR_PROGRAM_CONFIG, + DISCRIMINATOR_DISTRIBUTION, + DISCRIMINATOR_JOURNAL, + DISCRIMINATOR_SOLANA_VALIDATOR_DEPOSIT, + DISCRIMINATOR_CONTRIBUTOR_REWARDS, +} from "./discriminator.js"; +import { + deserializeProgramConfig, + deserializeDistribution, + deserializeJournal, + deserializeSolanaValidatorDeposit, + deserializeContributorRewards, + deserializeComputedSolanaValidatorDebts, + deserializeShapleyOutputStorage, +} from "./state.js"; +import type { + ProgramConfig, + Distribution, + Journal, + SolanaValidatorDeposit, + ContributorRewards, + ComputedSolanaValidatorDebts, + ShapleyOutputStorage, +} from "./state.js"; +import { + RECORD_HEADER_SIZE, + deriveConfigPda, + deriveDistributionPda, + deriveJournalPda, + deriveValidatorDepositPda, + deriveContributorRewardsPda, + deriveValidatorDebtRecordKey, + deriveRewardShareRecordKey, +} from "./pda.js"; + +export class Client { + private readonly solanaConnection: Connection; + private readonly ledgerConnection: Connection; + private readonly programId: PublicKey; + + constructor( + solanaConnection: Connection, + ledgerConnection: Connection, + programId: PublicKey, + ) { + this.solanaConnection = solanaConnection; + this.ledgerConnection = ledgerConnection; + this.programId = programId; + } + + /** Create a client configured for the given environment. */ + static forEnv(env: string): Client { + return new Client( + newConnection(SOLANA_RPC_URLS[env]), + newConnection(LEDGER_RPC_URLS[env]), + new PublicKey(PROGRAM_ID), + ); + } + + static mainnetBeta(): Client { + return Client.forEnv("mainnet-beta"); + } + + static testnet(): Client { + return Client.forEnv("testnet"); + } + + static devnet(): Client { + return Client.forEnv("devnet"); + } + + static localnet(): Client { + return Client.forEnv("localnet"); + } + + // -- Solana RPC (on-chain accounts) -- + + async fetchConfig(): Promise { + const [addr] = deriveConfigPda(this.programId); + const data = await this.fetchSolanaAccountData(addr); + return deserializeProgramConfig(data, DISCRIMINATOR_PROGRAM_CONFIG); + } + + async fetchDistribution(epoch: bigint): Promise { + const [addr] = deriveDistributionPda(this.programId, epoch); + const data = await this.fetchSolanaAccountData(addr); + return deserializeDistribution(data, DISCRIMINATOR_DISTRIBUTION); + } + + async fetchJournal(): Promise { + const [addr] = deriveJournalPda(this.programId); + const data = await this.fetchSolanaAccountData(addr); + return deserializeJournal(data, DISCRIMINATOR_JOURNAL); + } + + async fetchValidatorDeposit( + nodeId: PublicKey, + ): Promise { + const [addr] = deriveValidatorDepositPda(this.programId, nodeId); + const data = await this.fetchSolanaAccountData(addr); + return deserializeSolanaValidatorDeposit( + data, + DISCRIMINATOR_SOLANA_VALIDATOR_DEPOSIT, + ); + } + + async fetchContributorRewards( + serviceKey: PublicKey, + ): Promise { + const [addr] = deriveContributorRewardsPda(this.programId, serviceKey); + const data = await this.fetchSolanaAccountData(addr); + return deserializeContributorRewards( + data, + DISCRIMINATOR_CONTRIBUTOR_REWARDS, + ); + } + + async fetchAllValidatorDeposits(): Promise { + return this.fetchAllByDiscriminator( + DISCRIMINATOR_SOLANA_VALIDATOR_DEPOSIT, + deserializeSolanaValidatorDeposit, + ); + } + + async fetchAllContributorRewards(): Promise { + return this.fetchAllByDiscriminator( + DISCRIMINATOR_CONTRIBUTOR_REWARDS, + deserializeContributorRewards, + ); + } + + // -- DZ Ledger RPC (ledger records) -- + + async fetchValidatorDebts( + epoch: bigint, + ): Promise { + const config = await this.fetchConfig(); + const addr = await deriveValidatorDebtRecordKey( + config.debtAccountantKey, + epoch, + ); + const data = await this.fetchLedgerRecordData(addr); + return deserializeComputedSolanaValidatorDebts( + data.subarray(RECORD_HEADER_SIZE), + ); + } + + async fetchRewardShares(epoch: bigint): Promise { + const config = await this.fetchConfig(); + const addr = await deriveRewardShareRecordKey( + config.rewardsAccountantKey, + epoch, + ); + const data = await this.fetchLedgerRecordData(addr); + return deserializeShapleyOutputStorage( + data.subarray(RECORD_HEADER_SIZE), + ); + } + + // -- Internal helpers -- + + private async fetchSolanaAccountData(addr: PublicKey): Promise { + const info = await this.solanaConnection.getAccountInfo(addr); + if (info === null) { + throw new Error(`account not found: ${addr.toBase58()}`); + } + return info.data; + } + + private async fetchAllByDiscriminator( + disc: Uint8Array, + deserialize: (data: Uint8Array, disc: Uint8Array) => T, + ): Promise { + const accounts = await this.solanaConnection.getProgramAccounts( + this.programId, + { + filters: [ + { memcmp: { offset: 0, bytes: bs58.encode(Buffer.from(disc)) } }, + ], + }, + ); + return accounts.map(({ account }) => + deserialize(account.data, disc), + ); + } + + private async fetchLedgerRecordData(addr: PublicKey): Promise { + const info = await this.ledgerConnection.getAccountInfo(addr); + if (info === null) { + throw new Error(`ledger record not found: ${addr.toBase58()}`); + } + return info.data; + } +} diff --git a/sdk/revdist/typescript/revdist/config.ts b/sdk/revdist/typescript/revdist/config.ts new file mode 100644 index 000000000..d48d57ae5 --- /dev/null +++ b/sdk/revdist/typescript/revdist/config.ts @@ -0,0 +1,26 @@ +/** Network configuration for the revenue distribution program. */ + +export const PROGRAM_ID = "dzrevZC94tBLwuHw1dyynZxaXTWyp7yocsinyEVPtt4"; + +export const SOLANA_RPC_URLS: Record = { + "mainnet-beta": "https://api.mainnet-beta.solana.com", + testnet: "https://api.testnet.solana.com", + devnet: "https://api.devnet.solana.com", + localnet: "http://localhost:8899", +}; + +export const ORACLE_URLS: Record = { + "mainnet-beta": + "https://sol-2z-oracle-api-v1.mainnet-beta.doublezero.xyz", + testnet: "https://sol-2z-oracle-api-v1.testnet.doublezero.xyz", +}; + +export const LEDGER_RPC_URLS: Record = { + "mainnet-beta": + "https://doublezero-mainnet-beta.rpcpool.com/db336024-e7a8-46b1-80e5-352dd77060ab", + testnet: + "https://doublezerolocalnet.rpcpool.com/8a4fd3f4-0977-449f-88c7-63d4b0f10f16", + devnet: + "https://doublezerolocalnet.rpcpool.com/8a4fd3f4-0977-449f-88c7-63d4b0f10f16", + localnet: "http://localhost:8899", +}; diff --git a/sdk/revdist/typescript/revdist/discriminator.ts b/sdk/revdist/typescript/revdist/discriminator.ts new file mode 100644 index 000000000..a229504eb --- /dev/null +++ b/sdk/revdist/typescript/revdist/discriminator.ts @@ -0,0 +1,42 @@ +import { createHash } from "crypto"; + +export const DISCRIMINATOR_SIZE = 8; + +function sha256First8(s: string): Uint8Array { + const hash = createHash("sha256").update(s).digest(); + return new Uint8Array(hash.buffer, hash.byteOffset, 8); +} + +export const DISCRIMINATOR_PROGRAM_CONFIG = sha256First8( + "dz::account::program_config", +); +export const DISCRIMINATOR_DISTRIBUTION = sha256First8( + "dz::account::distribution", +); +export const DISCRIMINATOR_SOLANA_VALIDATOR_DEPOSIT = sha256First8( + "dz::account::solana_validator_deposit", +); +export const DISCRIMINATOR_CONTRIBUTOR_REWARDS = sha256First8( + "dz::account::contributor_rewards", +); +export const DISCRIMINATOR_JOURNAL = sha256First8("dz::account::journal"); + +export function validateDiscriminator( + data: Uint8Array, + expected: Uint8Array, +): void { + if (data.length < DISCRIMINATOR_SIZE) { + throw new Error( + `data too short: ${data.length} bytes, need at least ${DISCRIMINATOR_SIZE}`, + ); + } + for (let i = 0; i < DISCRIMINATOR_SIZE; i++) { + if (data[i] !== expected[i]) { + const gotHex = Buffer.from(data.slice(0, 8)).toString("hex"); + const wantHex = Buffer.from(expected).toString("hex"); + throw new Error( + `invalid discriminator: got ${gotHex}, want ${wantHex}`, + ); + } + } +} diff --git a/sdk/revdist/typescript/revdist/index.ts b/sdk/revdist/typescript/revdist/index.ts new file mode 100644 index 000000000..417942be6 --- /dev/null +++ b/sdk/revdist/typescript/revdist/index.ts @@ -0,0 +1,59 @@ +export { PROGRAM_ID, SOLANA_RPC_URLS, LEDGER_RPC_URLS, ORACLE_URLS } from "./config.js"; +export { Client } from "./client.js"; +export { newConnection } from "./rpc.js"; +export { OracleClient } from "./oracle.js"; +export type { SwapRate } from "./oracle.js"; + +export { + DISCRIMINATOR_PROGRAM_CONFIG, + DISCRIMINATOR_DISTRIBUTION, + DISCRIMINATOR_SOLANA_VALIDATOR_DEPOSIT, + DISCRIMINATOR_CONTRIBUTOR_REWARDS, + DISCRIMINATOR_JOURNAL, + validateDiscriminator, +} from "./discriminator.js"; + +export type { + ProgramConfig, + Distribution, + SolanaValidatorDeposit, + ContributorRewards, + Journal, + DistributionParameters, + SolanaValidatorFeeParameters, + CommunityBurnRateParameters, + RelayParameters, + RecipientShare, + ComputedSolanaValidatorDebt, + ComputedSolanaValidatorDebts, + RewardShare, + ShapleyOutputStorage, +} from "./state.js"; + +export { + deserializeProgramConfig, + deserializeDistribution, + deserializeSolanaValidatorDeposit, + deserializeContributorRewards, + deserializeJournal, + deserializeComputedSolanaValidatorDebts, + deserializeShapleyOutputStorage, + PROGRAM_CONFIG_STRUCT_SIZE, + DISTRIBUTION_STRUCT_SIZE, + SOLANA_VALIDATOR_DEPOSIT_STRUCT_SIZE, + CONTRIBUTOR_REWARDS_STRUCT_SIZE, + JOURNAL_STRUCT_SIZE, +} from "./state.js"; + +export { + RECORD_HEADER_SIZE, + RECORD_PROGRAM_ID, + deriveConfigPda, + deriveDistributionPda, + deriveJournalPda, + deriveValidatorDepositPda, + deriveContributorRewardsPda, + deriveRecordKey, + deriveValidatorDebtRecordKey, + deriveRewardShareRecordKey, +} from "./pda.js"; diff --git a/sdk/revdist/typescript/revdist/oracle.ts b/sdk/revdist/typescript/revdist/oracle.ts new file mode 100644 index 000000000..aca9815b4 --- /dev/null +++ b/sdk/revdist/typescript/revdist/oracle.ts @@ -0,0 +1,34 @@ +/** SOL/2Z oracle client. */ + +export interface SwapRate { + rate: number; + timestamp: number; + signature: string; + solPriceUsd: string; + twozPriceUsd: string; + cacheHit: boolean; +} + +export class OracleClient { + private readonly baseUrl: string; + + constructor(baseUrl: string) { + this.baseUrl = baseUrl; + } + + async fetchSwapRate(): Promise { + const resp = await fetch(`${this.baseUrl}/swap-rate`); + if (!resp.ok) { + throw new Error(`oracle returned status ${resp.status}`); + } + const data = await resp.json(); + return { + rate: data.swapRate, + timestamp: data.timestamp, + signature: data.signature, + solPriceUsd: data.solPriceUsd, + twozPriceUsd: data.twozPriceUsd, + cacheHit: data.cacheHit, + }; + } +} diff --git a/sdk/revdist/typescript/revdist/pda.ts b/sdk/revdist/typescript/revdist/pda.ts new file mode 100644 index 000000000..7a6781024 --- /dev/null +++ b/sdk/revdist/typescript/revdist/pda.ts @@ -0,0 +1,105 @@ +import { createHash } from "crypto"; +import { PublicKey } from "@solana/web3.js"; +// @ts-ignore - no type declarations available +import bs58 from "bs58"; + +const SEED_PROGRAM_CONFIG = Buffer.from("program_config"); +const SEED_DISTRIBUTION = Buffer.from("distribution"); +const SEED_SOLANA_VALIDATOR_DEPOSIT = Buffer.from( + "solana_validator_deposit", +); +const SEED_CONTRIBUTOR_REWARDS = Buffer.from("contributor_rewards"); +const SEED_JOURNAL = Buffer.from("journal"); +const SEED_SOLANA_VALIDATOR_DEBT = Buffer.from("solana_validator_debt"); +const SEED_DZ_CONTRIBUTOR_REWARDS = Buffer.from("dz_contributor_rewards"); +const SEED_SHAPLEY_OUTPUT = Buffer.from("shapley_output"); + +export const RECORD_PROGRAM_ID = new PublicKey( + "dzrecxigtaZQ3gPmt2X5mDkYigaruFR1rHCqztFTvx7", +); +export const RECORD_HEADER_SIZE = 33; + +function createRecordSeedString(seeds: Buffer[]): string { + const h = createHash("sha256"); + for (const s of seeds) { + h.update(s); + } + return bs58.encode(h.digest()).slice(0, 32); +} + +export async function deriveRecordKey( + payerKey: PublicKey, + seeds: Buffer[], +): Promise { + const seedStr = createRecordSeedString(seeds); + return PublicKey.createWithSeed(payerKey, seedStr, RECORD_PROGRAM_ID); +} + +export function deriveConfigPda( + programId: PublicKey, +): [PublicKey, number] { + return PublicKey.findProgramAddressSync([SEED_PROGRAM_CONFIG], programId); +} + +export function deriveDistributionPda( + programId: PublicKey, + epoch: bigint, +): [PublicKey, number] { + const epochBuf = Buffer.alloc(8); + epochBuf.writeBigUInt64LE(epoch); + return PublicKey.findProgramAddressSync( + [SEED_DISTRIBUTION, epochBuf], + programId, + ); +} + +export function deriveJournalPda( + programId: PublicKey, +): [PublicKey, number] { + return PublicKey.findProgramAddressSync([SEED_JOURNAL], programId); +} + +export function deriveValidatorDepositPda( + programId: PublicKey, + nodeId: PublicKey, +): [PublicKey, number] { + return PublicKey.findProgramAddressSync( + [SEED_SOLANA_VALIDATOR_DEPOSIT, nodeId.toBuffer()], + programId, + ); +} + +export function deriveContributorRewardsPda( + programId: PublicKey, + serviceKey: PublicKey, +): [PublicKey, number] { + return PublicKey.findProgramAddressSync( + [SEED_CONTRIBUTOR_REWARDS, serviceKey.toBuffer()], + programId, + ); +} + +export async function deriveValidatorDebtRecordKey( + debtAccountantKey: PublicKey, + epoch: bigint, +): Promise { + const epochBuf = Buffer.alloc(8); + epochBuf.writeBigUInt64LE(epoch); + return deriveRecordKey(debtAccountantKey, [ + SEED_SOLANA_VALIDATOR_DEBT, + epochBuf, + ]); +} + +export async function deriveRewardShareRecordKey( + rewardsAccountantKey: PublicKey, + epoch: bigint, +): Promise { + const epochBuf = Buffer.alloc(8); + epochBuf.writeBigUInt64LE(epoch); + return deriveRecordKey(rewardsAccountantKey, [ + SEED_DZ_CONTRIBUTOR_REWARDS, + epochBuf, + SEED_SHAPLEY_OUTPUT, + ]); +} diff --git a/sdk/revdist/typescript/revdist/rpc.ts b/sdk/revdist/typescript/revdist/rpc.ts new file mode 100644 index 000000000..ab2f0ff83 --- /dev/null +++ b/sdk/revdist/typescript/revdist/rpc.ts @@ -0,0 +1,36 @@ +import { Connection, type ConnectionConfig } from "@solana/web3.js"; + +const DEFAULT_MAX_RETRIES = 5; + +/** + * Creates a Solana RPC Connection with retry on 429 Too Many Requests. + * + * The built-in @solana/web3.js retry uses short backoffs (500ms-4s) that + * may not be sufficient for rate-limited public RPC endpoints. This wrapper + * provides longer backoff intervals (2s, 4s, 6s, 8s, 10s). + */ +export function newConnection( + url: string, + config?: ConnectionConfig & { maxRetries?: number }, +): Connection { + const maxRetries = config?.maxRetries ?? DEFAULT_MAX_RETRIES; + const retryFetch = async ( + input: Parameters[0], + init?: Parameters[1], + ): Promise => { + for (let attempt = 0; ; attempt++) { + const response = await fetch(input, init); + if (response.status !== 429 || attempt >= maxRetries) { + return response; + } + await new Promise((resolve) => + setTimeout(resolve, (attempt + 1) * 2000), + ); + } + }; + return new Connection(url, { + ...config, + disableRetryOnRateLimit: true, + fetch: retryFetch as typeof fetch, + }); +} diff --git a/sdk/revdist/typescript/revdist/state.ts b/sdk/revdist/typescript/revdist/state.ts new file mode 100644 index 000000000..cb72c23eb --- /dev/null +++ b/sdk/revdist/typescript/revdist/state.ts @@ -0,0 +1,514 @@ +/** + * On-chain account data structures for the revenue distribution program. + * + * Binary layout matches Rust #[repr(C)] structs. Deserialization uses + * DataView with little-endian byte order and tolerates extra trailing + * bytes for forward compatibility. + */ + +import { PublicKey } from "@solana/web3.js"; +import { IncrementalReader } from "@doublezero/borsh-incremental"; +import { DISCRIMINATOR_SIZE, validateDiscriminator } from "./discriminator.js"; + +function readPubkey(dv: DataView, offset: number): PublicKey { + const bytes = new Uint8Array(dv.buffer, dv.byteOffset + offset, 32); + return new PublicKey(bytes); +} + +function deserializeBody( + data: Uint8Array, + discriminator: Uint8Array, + minSize: number, +): DataView { + validateDiscriminator(data, discriminator); + const body = data.slice(DISCRIMINATOR_SIZE); + if (body.length < minSize) { + throw new Error( + `account data too short: have ${body.length} bytes, need at least ${minSize}`, + ); + } + return new DataView(body.buffer, body.byteOffset, body.byteLength); +} + +// --------------------------------------------------------------------------- +// Nested types +// --------------------------------------------------------------------------- + +export interface CommunityBurnRateParameters { + limit: number; + dzEpochsToIncreasing: number; + dzEpochsToLimit: number; + cachedSlopeNumerator: number; + cachedSlopeDenominator: number; + cachedNextBurnRate: number; +} + +function deserializeCommunityBurnRateParameters( + dv: DataView, + offset: number, +): CommunityBurnRateParameters { + return { + limit: dv.getUint32(offset, true), + dzEpochsToIncreasing: dv.getUint32(offset + 4, true), + dzEpochsToLimit: dv.getUint32(offset + 8, true), + cachedSlopeNumerator: dv.getUint32(offset + 12, true), + cachedSlopeDenominator: dv.getUint32(offset + 16, true), + cachedNextBurnRate: dv.getUint32(offset + 20, true), + }; +} + +export interface SolanaValidatorFeeParameters { + baseBlockRewardsPct: number; + priorityBlockRewardsPct: number; + inflationRewardsPct: number; + jitoTipsPct: number; + fixedSolAmount: number; +} + +const SOLANA_VALIDATOR_FEE_PARAMETERS_SIZE = 40; + +function deserializeSolanaValidatorFeeParameters( + dv: DataView, + offset: number, +): SolanaValidatorFeeParameters { + return { + baseBlockRewardsPct: dv.getUint16(offset, true), + priorityBlockRewardsPct: dv.getUint16(offset + 2, true), + inflationRewardsPct: dv.getUint16(offset + 4, true), + jitoTipsPct: dv.getUint16(offset + 6, true), + fixedSolAmount: dv.getUint32(offset + 8, true), + }; +} + +export interface DistributionParameters { + calculationGracePeriodMinutes: number; + initializationGracePeriodMinutes: number; + minimumEpochDurationToFinalizeRewards: number; + communityBurnRateParameters: CommunityBurnRateParameters; + solanaValidatorFeeParameters: SolanaValidatorFeeParameters; +} + +const DISTRIBUTION_PARAMETERS_SIZE = 328; + +function deserializeDistributionParameters( + dv: DataView, + offset: number, +): DistributionParameters { + return { + calculationGracePeriodMinutes: dv.getUint16(offset, true), + initializationGracePeriodMinutes: dv.getUint16(offset + 2, true), + minimumEpochDurationToFinalizeRewards: dv.getUint8(offset + 4), + // 3 bytes padding + communityBurnRateParameters: deserializeCommunityBurnRateParameters( + dv, + offset + 8, + ), + solanaValidatorFeeParameters: deserializeSolanaValidatorFeeParameters( + dv, + offset + 8 + 24, + ), + }; +} + +export interface RelayParameters { + placeholderLamports: number; + distributeRewardsLamports: number; +} + +const RELAY_PARAMETERS_SIZE = 40; + +function deserializeRelayParameters( + dv: DataView, + offset: number, +): RelayParameters { + return { + placeholderLamports: dv.getUint32(offset, true), + distributeRewardsLamports: dv.getUint32(offset + 4, true), + }; +} + +export interface RecipientShare { + recipientKey: PublicKey; + share: number; +} + +const RECIPIENT_SHARE_SIZE = 34; + +// --------------------------------------------------------------------------- +// Top-level account types +// --------------------------------------------------------------------------- + +export interface ProgramConfig { + flags: bigint; + nextCompletedDzEpoch: bigint; + bumpSeed: number; + reserve2zBumpSeed: number; + swapAuthorityBumpSeed: number; + swapDestination2zBumpSeed: number; + withdrawSolAuthorityBumpSeed: number; + adminKey: PublicKey; + debtAccountantKey: PublicKey; + rewardsAccountantKey: PublicKey; + contributorManagerKey: PublicKey; + placeholderKey: PublicKey; + sol2zSwapProgramId: PublicKey; + distributionParameters: DistributionParameters; + relayParameters: RelayParameters; + lastInitializedDistributionTimestamp: number; + debtWriteOffFeatureActivationEpoch: bigint; +} + +export const PROGRAM_CONFIG_STRUCT_SIZE = 600; + +export function deserializeProgramConfig( + data: Uint8Array, + discriminator: Uint8Array, +): ProgramConfig { + const dv = deserializeBody(data, discriminator, PROGRAM_CONFIG_STRUCT_SIZE); + let off = 0; + const flags = dv.getBigUint64(off, true); + off += 8; + const nextCompletedDzEpoch = dv.getBigUint64(off, true); + off += 8; + const bumpSeed = dv.getUint8(off); + const reserve2zBumpSeed = dv.getUint8(off + 1); + const swapAuthorityBumpSeed = dv.getUint8(off + 2); + const swapDestination2zBumpSeed = dv.getUint8(off + 3); + const withdrawSolAuthorityBumpSeed = dv.getUint8(off + 4); + off += 8; // 5 bytes + 3 padding + const adminKey = readPubkey(dv, off); + off += 32; + const debtAccountantKey = readPubkey(dv, off); + off += 32; + const rewardsAccountantKey = readPubkey(dv, off); + off += 32; + const contributorManagerKey = readPubkey(dv, off); + off += 32; + const placeholderKey = readPubkey(dv, off); + off += 32; + const sol2zSwapProgramId = readPubkey(dv, off); + off += 32; + const distributionParameters = deserializeDistributionParameters(dv, off); + off += DISTRIBUTION_PARAMETERS_SIZE; + const relayParameters = deserializeRelayParameters(dv, off); + off += RELAY_PARAMETERS_SIZE; + const lastInitializedDistributionTimestamp = dv.getUint32(off, true); + off += 4; + off += 4; // 4 bytes padding + const debtWriteOffFeatureActivationEpoch = dv.getBigUint64(off, true); + + return { + flags, + nextCompletedDzEpoch, + bumpSeed, + reserve2zBumpSeed, + swapAuthorityBumpSeed, + swapDestination2zBumpSeed, + withdrawSolAuthorityBumpSeed, + adminKey, + debtAccountantKey, + rewardsAccountantKey, + contributorManagerKey, + placeholderKey, + sol2zSwapProgramId, + distributionParameters, + relayParameters, + lastInitializedDistributionTimestamp, + debtWriteOffFeatureActivationEpoch, + }; +} + +export interface Distribution { + dzEpoch: bigint; + flags: bigint; + communityBurnRate: number; + bumpSeed: number; + token2zPdaBumpSeed: number; + solanaValidatorFeeParameters: SolanaValidatorFeeParameters; + solanaValidatorDebtMerkleRoot: Uint8Array; + totalSolanaValidators: number; + solanaValidatorPaymentsCount: number; + totalSolanaValidatorDebt: bigint; + collectedSolanaValidatorPayments: bigint; + rewardsMerkleRoot: Uint8Array; + totalContributors: number; + distributedRewardsCount: number; + collectedPrepaid2zPayments: bigint; + collected2zConvertedFromSol: bigint; + uncollectibleSolDebt: bigint; + processedSvDebtStartIndex: number; + processedSvDebtEndIndex: number; + processedRewardsStartIndex: number; + processedRewardsEndIndex: number; + distributeRewardsRelayLamports: number; + calculationAllowedTimestamp: number; + distributed2zAmount: bigint; + burned2zAmount: bigint; + processedSvDebtWoStartIndex: number; + processedSvDebtWoEndIndex: number; + solanaValidatorWriteOffCount: number; +} + +export const DISTRIBUTION_STRUCT_SIZE = 448; + +export function deserializeDistribution( + data: Uint8Array, + discriminator: Uint8Array, +): Distribution { + const dv = deserializeBody(data, discriminator, DISTRIBUTION_STRUCT_SIZE); + let off = 0; + const dzEpoch = dv.getBigUint64(off, true); + off += 8; + const flags = dv.getBigUint64(off, true); + off += 8; + const communityBurnRate = dv.getUint32(off, true); + off += 4; + const bumpSeed = dv.getUint8(off); + const token2zPdaBumpSeed = dv.getUint8(off + 1); + off += 4; // 2 bytes + 2 padding + const solanaValidatorFeeParameters = + deserializeSolanaValidatorFeeParameters(dv, off); + off += SOLANA_VALIDATOR_FEE_PARAMETERS_SIZE; + const solanaValidatorDebtMerkleRoot = new Uint8Array( + dv.buffer, + dv.byteOffset + off, + 32, + ); + off += 32; + const totalSolanaValidators = dv.getUint32(off, true); + off += 4; + const solanaValidatorPaymentsCount = dv.getUint32(off, true); + off += 4; + const totalSolanaValidatorDebt = dv.getBigUint64(off, true); + off += 8; + const collectedSolanaValidatorPayments = dv.getBigUint64(off, true); + off += 8; + const rewardsMerkleRoot = new Uint8Array( + dv.buffer, + dv.byteOffset + off, + 32, + ); + off += 32; + const totalContributors = dv.getUint32(off, true); + off += 4; + const distributedRewardsCount = dv.getUint32(off, true); + off += 4; + const collectedPrepaid2zPayments = dv.getBigUint64(off, true); + off += 8; + const collected2zConvertedFromSol = dv.getBigUint64(off, true); + off += 8; + const uncollectibleSolDebt = dv.getBigUint64(off, true); + off += 8; + const processedSvDebtStartIndex = dv.getUint32(off, true); + off += 4; + const processedSvDebtEndIndex = dv.getUint32(off, true); + off += 4; + const processedRewardsStartIndex = dv.getUint32(off, true); + off += 4; + const processedRewardsEndIndex = dv.getUint32(off, true); + off += 4; + const distributeRewardsRelayLamports = dv.getUint32(off, true); + off += 4; + const calculationAllowedTimestamp = dv.getUint32(off, true); + off += 4; + const distributed2zAmount = dv.getBigUint64(off, true); + off += 8; + const burned2zAmount = dv.getBigUint64(off, true); + off += 8; + const processedSvDebtWoStartIndex = dv.getUint32(off, true); + off += 4; + const processedSvDebtWoEndIndex = dv.getUint32(off, true); + off += 4; + const solanaValidatorWriteOffCount = dv.getUint32(off, true); + + return { + dzEpoch, + flags, + communityBurnRate, + bumpSeed, + token2zPdaBumpSeed, + solanaValidatorFeeParameters, + solanaValidatorDebtMerkleRoot, + totalSolanaValidators, + solanaValidatorPaymentsCount, + totalSolanaValidatorDebt, + collectedSolanaValidatorPayments, + rewardsMerkleRoot, + totalContributors, + distributedRewardsCount, + collectedPrepaid2zPayments, + collected2zConvertedFromSol, + uncollectibleSolDebt, + processedSvDebtStartIndex, + processedSvDebtEndIndex, + processedRewardsStartIndex, + processedRewardsEndIndex, + distributeRewardsRelayLamports, + calculationAllowedTimestamp, + distributed2zAmount, + burned2zAmount, + processedSvDebtWoStartIndex, + processedSvDebtWoEndIndex, + solanaValidatorWriteOffCount, + }; +} + +export interface SolanaValidatorDeposit { + nodeId: PublicKey; + writtenOffSolDebt: bigint; +} + +export const SOLANA_VALIDATOR_DEPOSIT_STRUCT_SIZE = 96; + +export function deserializeSolanaValidatorDeposit( + data: Uint8Array, + discriminator: Uint8Array, +): SolanaValidatorDeposit { + const dv = deserializeBody( + data, + discriminator, + SOLANA_VALIDATOR_DEPOSIT_STRUCT_SIZE, + ); + return { + nodeId: readPubkey(dv, 0), + writtenOffSolDebt: dv.getBigUint64(32, true), + }; +} + +export interface ContributorRewards { + rewardsManagerKey: PublicKey; + serviceKey: PublicKey; + flags: bigint; + recipientShares: RecipientShare[]; +} + +export const CONTRIBUTOR_REWARDS_STRUCT_SIZE = 600; + +export function deserializeContributorRewards( + data: Uint8Array, + discriminator: Uint8Array, +): ContributorRewards { + const dv = deserializeBody( + data, + discriminator, + CONTRIBUTOR_REWARDS_STRUCT_SIZE, + ); + const rewardsManagerKey = readPubkey(dv, 0); + const serviceKey = readPubkey(dv, 32); + const flags = dv.getBigUint64(64, true); + const recipientShares: RecipientShare[] = []; + let off = 72; + for (let i = 0; i < 8; i++) { + recipientShares.push({ + recipientKey: readPubkey(dv, off), + share: dv.getUint16(off + 32, true), + }); + off += RECIPIENT_SHARE_SIZE; + } + return { rewardsManagerKey, serviceKey, flags, recipientShares }; +} + +export interface Journal { + bumpSeed: number; + token2zPdaBumpSeed: number; + totalSolBalance: bigint; + total2zBalance: bigint; + swap2zDestinationBalance: bigint; + swappedSolAmount: bigint; + nextDzEpochToSweepTokens: bigint; + lifetimeSwapped2zAmount: Uint8Array; +} + +export const JOURNAL_STRUCT_SIZE = 64; + +export function deserializeJournal( + data: Uint8Array, + discriminator: Uint8Array, +): Journal { + const dv = deserializeBody(data, discriminator, JOURNAL_STRUCT_SIZE); + const bumpSeed = dv.getUint8(0); + const token2zPdaBumpSeed = dv.getUint8(1); + // 6 bytes padding + const totalSolBalance = dv.getBigUint64(8, true); + const total2zBalance = dv.getBigUint64(16, true); + const swap2zDestinationBalance = dv.getBigUint64(24, true); + const swappedSolAmount = dv.getBigUint64(32, true); + const nextDzEpochToSweepTokens = dv.getBigUint64(40, true); + const lifetimeSwapped2zAmount = new Uint8Array( + dv.buffer, + dv.byteOffset + 48, + 16, + ); + return { + bumpSeed, + token2zPdaBumpSeed, + totalSolBalance, + total2zBalance, + swap2zDestinationBalance, + swappedSolAmount, + nextDzEpochToSweepTokens, + lifetimeSwapped2zAmount, + }; +} + +// --------------------------------------------------------------------------- +// DZ Ledger record types (Borsh-serialized) +// --------------------------------------------------------------------------- + +export interface ComputedSolanaValidatorDebt { + nodeId: PublicKey; + amount: bigint; +} + +export interface ComputedSolanaValidatorDebts { + blockhash: Uint8Array; + firstSolanaEpoch: bigint; + lastSolanaEpoch: bigint; + debts: ComputedSolanaValidatorDebt[]; +} + +export function deserializeComputedSolanaValidatorDebts( + data: Uint8Array, +): ComputedSolanaValidatorDebts { + const r = new IncrementalReader(data); + const blockhash = r.readBytes(32); + const firstSolanaEpoch = r.readU64(); + const lastSolanaEpoch = r.readU64(); + const count = r.readU32(); + const debts: ComputedSolanaValidatorDebt[] = []; + for (let i = 0; i < count; i++) { + const nodeId = new PublicKey(r.readPubkeyRaw()); + const amount = r.readU64(); + debts.push({ nodeId, amount }); + } + return { blockhash, firstSolanaEpoch, lastSolanaEpoch, debts }; +} + +export interface RewardShare { + contributorKey: PublicKey; + unitShare: number; + remainingBytes: Uint8Array; +} + +export interface ShapleyOutputStorage { + epoch: bigint; + rewards: RewardShare[]; + totalUnitShares: number; +} + +export function deserializeShapleyOutputStorage( + data: Uint8Array, +): ShapleyOutputStorage { + const r = new IncrementalReader(data); + const epoch = r.readU64(); + const count = r.readU32(); + const rewards: RewardShare[] = []; + for (let i = 0; i < count; i++) { + const contributorKey = new PublicKey(r.readPubkeyRaw()); + const unitShare = r.readU32(); + const remainingBytes = r.readBytes(4); + rewards.push({ contributorKey, unitShare, remainingBytes }); + } + const totalUnitShares = r.readU32(); + return { epoch, rewards, totalUnitShares }; +} diff --git a/sdk/revdist/typescript/revdist/tests/compat.test.ts b/sdk/revdist/typescript/revdist/tests/compat.test.ts new file mode 100644 index 000000000..51cfa6373 --- /dev/null +++ b/sdk/revdist/typescript/revdist/tests/compat.test.ts @@ -0,0 +1,276 @@ +/** + * Mainnet compatibility tests. + * + * These tests fetch live mainnet-beta data and verify that our struct + * deserialization works against real on-chain accounts. + * + * Run with: + * REVDIST_COMPAT_TEST=1 cd sdk/revdist/typescript && bun test --grep compat + * + * Requires network access to Solana mainnet RPC. + */ + +import { describe, expect, test, setDefaultTimeout } from "bun:test"; + +// Compat tests hit public RPC endpoints which may be slow or rate-limited. +setDefaultTimeout(30_000); +import { PublicKey } from "@solana/web3.js"; +import { Client } from "../client.js"; +import { SOLANA_RPC_URLS, LEDGER_RPC_URLS, PROGRAM_ID } from "../config.js"; +import { newConnection } from "../rpc.js"; +import { + deriveConfigPda, + deriveDistributionPda, + deriveJournalPda, +} from "../pda.js"; + +function skipUnlessCompat(): void { + if (!process.env.REVDIST_COMPAT_TEST) { + throw new Error("SKIP"); + } +} + +function readU8(buf: Buffer, offset: number): number { + return buf.readUInt8(offset); +} + +function readU16LE(buf: Buffer, offset: number): number { + return buf.readUInt16LE(offset); +} + +function readU32LE(buf: Buffer, offset: number): number { + return buf.readUInt32LE(offset); +} + +function readU64LE(buf: Buffer, offset: number): bigint { + return buf.readBigUInt64LE(offset); +} + +function readPubkey(buf: Buffer, offset: number): PublicKey { + return new PublicKey(buf.subarray(offset, offset + 32)); +} + +function rpcUrl(): string { + return process.env.SOLANA_RPC_URL || SOLANA_RPC_URLS["mainnet-beta"]; +} + +function compatClient(): Client { + return new Client( + newConnection(rpcUrl()), + newConnection(LEDGER_RPC_URLS["mainnet-beta"]), + new PublicKey(PROGRAM_ID), + ); +} + +async function fetchRawAccount(addr: PublicKey): Promise { + const conn = newConnection(rpcUrl()); + const info = await conn.getAccountInfo(addr); + if (info === null) throw new Error(`account not found: ${addr.toBase58()}`); + return info.data; +} + +describe("compat: ProgramConfig", () => { + test("deserialize matches raw bytes", async () => { + try { + skipUnlessCompat(); + } catch { + return; + } + + const client = compatClient(); + const config = await client.fetchConfig(); + + const programId = new PublicKey(PROGRAM_ID); + const [addr] = deriveConfigPda(programId); + const raw = await fetchRawAccount(addr); + + expect(config.flags).toBe(readU64LE(raw, 8)); + expect(config.nextCompletedDzEpoch).toBe(readU64LE(raw, 16)); + expect(config.bumpSeed).toBe(readU8(raw, 24)); + expect(config.adminKey.toBase58()).toBe(readPubkey(raw, 32).toBase58()); + expect(config.debtAccountantKey.toBase58()).toBe(readPubkey(raw, 64).toBase58()); + expect(config.rewardsAccountantKey.toBase58()).toBe(readPubkey(raw, 96).toBase58()); + expect(config.contributorManagerKey.toBase58()).toBe(readPubkey(raw, 128).toBase58()); + expect(config.sol2zSwapProgramId.toBase58()).toBe(readPubkey(raw, 192).toBase58()); + + // DistributionParameters at offset 224. + const dp = config.distributionParameters; + expect(dp.calculationGracePeriodMinutes).toBe(readU16LE(raw, 224)); + expect(dp.initializationGracePeriodMinutes).toBe(readU16LE(raw, 226)); + expect(dp.minimumEpochDurationToFinalizeRewards).toBe(readU8(raw, 228)); + + // CommunityBurnRateParameters at offset 232. + const cb = dp.communityBurnRateParameters; + expect(cb.limit).toBe(readU32LE(raw, 232)); + expect(cb.dzEpochsToIncreasing).toBe(readU32LE(raw, 236)); + expect(cb.dzEpochsToLimit).toBe(readU32LE(raw, 240)); + + // SolanaValidatorFeeParameters at offset 256. + const vf = dp.solanaValidatorFeeParameters; + expect(vf.baseBlockRewardsPct).toBe(readU16LE(raw, 256)); + expect(vf.priorityBlockRewardsPct).toBe(readU16LE(raw, 258)); + expect(vf.inflationRewardsPct).toBe(readU16LE(raw, 260)); + expect(vf.jitoTipsPct).toBe(readU16LE(raw, 262)); + expect(vf.fixedSolAmount).toBe(readU32LE(raw, 264)); + + // RelayParameters at offset 552. + const rp = config.relayParameters; + expect(rp.placeholderLamports).toBe(readU32LE(raw, 552)); + expect(rp.distributeRewardsLamports).toBe(readU32LE(raw, 556)); + + // DebtWriteOffFeatureActivationEpoch at offset 600. + expect(config.debtWriteOffFeatureActivationEpoch).toBe(readU64LE(raw, 600)); + + // Sanity. + expect(config.nextCompletedDzEpoch > 0n).toBe(true); + }); +}); + +describe("compat: Distribution", () => { + test("deserialize matches raw bytes", async () => { + try { + skipUnlessCompat(); + } catch { + return; + } + + const client = compatClient(); + const config = await client.fetchConfig(); + const epoch = config.nextCompletedDzEpoch - 1n; + + const dist = await client.fetchDistribution(epoch); + + const programId = new PublicKey(PROGRAM_ID); + const [addr] = deriveDistributionPda(programId, epoch); + const raw = await fetchRawAccount(addr); + + expect(dist.dzEpoch).toBe(readU64LE(raw, 8)); + expect(dist.dzEpoch).toBe(epoch); + expect(dist.flags).toBe(readU64LE(raw, 16)); + expect(dist.communityBurnRate).toBe(readU32LE(raw, 24)); + + const vf = dist.solanaValidatorFeeParameters; + expect(vf.baseBlockRewardsPct).toBe(readU16LE(raw, 32)); + expect(vf.priorityBlockRewardsPct).toBe(readU16LE(raw, 34)); + expect(vf.inflationRewardsPct).toBe(readU16LE(raw, 36)); + expect(vf.jitoTipsPct).toBe(readU16LE(raw, 38)); + expect(vf.fixedSolAmount).toBe(readU32LE(raw, 40)); + + expect(dist.totalSolanaValidators).toBe(readU32LE(raw, 104)); + expect(dist.solanaValidatorPaymentsCount).toBe(readU32LE(raw, 108)); + expect(dist.totalSolanaValidatorDebt).toBe(readU64LE(raw, 112)); + expect(dist.collectedSolanaValidatorPayments).toBe(readU64LE(raw, 120)); + expect(dist.totalContributors).toBe(readU32LE(raw, 160)); + expect(dist.distributedRewardsCount).toBe(readU32LE(raw, 164)); + expect(dist.collectedPrepaid2zPayments).toBe(readU64LE(raw, 168)); + expect(dist.collected2zConvertedFromSol).toBe(readU64LE(raw, 176)); + expect(dist.uncollectibleSolDebt).toBe(readU64LE(raw, 184)); + expect(dist.distributed2zAmount).toBe(readU64LE(raw, 216)); + expect(dist.burned2zAmount).toBe(readU64LE(raw, 224)); + }); +}); + +describe("compat: Journal", () => { + test("deserialize matches raw bytes", async () => { + try { + skipUnlessCompat(); + } catch { + return; + } + + const client = compatClient(); + const journal = await client.fetchJournal(); + + const programId = new PublicKey(PROGRAM_ID); + const [addr] = deriveJournalPda(programId); + const raw = await fetchRawAccount(addr); + + expect(journal.bumpSeed).toBe(readU8(raw, 8)); + expect(journal.totalSolBalance).toBe(readU64LE(raw, 16)); + expect(journal.total2zBalance).toBe(readU64LE(raw, 24)); + expect(journal.swap2zDestinationBalance).toBe(readU64LE(raw, 32)); + expect(journal.swappedSolAmount).toBe(readU64LE(raw, 40)); + expect(journal.nextDzEpochToSweepTokens).toBe(readU64LE(raw, 48)); + }); +}); + +describe("compat: ValidatorDebts", () => { + test("fetch and validate", async () => { + try { + skipUnlessCompat(); + } catch { + return; + } + + const client = compatClient(); + const config = await client.fetchConfig(); + const epoch = config.nextCompletedDzEpoch - 5n; + + const debts = await client.fetchValidatorDebts(epoch); + + expect(debts.lastSolanaEpoch > 0n).toBe(true); + expect(debts.firstSolanaEpoch <= debts.lastSolanaEpoch).toBe(true); + expect(debts.debts.length).toBeGreaterThan(0); + }); +}); + +describe("compat: RewardShares", () => { + test("fetch and validate", async () => { + try { + skipUnlessCompat(); + } catch { + return; + } + + const client = compatClient(); + const config = await client.fetchConfig(); + const epoch = config.nextCompletedDzEpoch - 5n; + + const shares = await client.fetchRewardShares(epoch); + + expect(shares.epoch).toBe(epoch); + expect(shares.rewards.length).toBeGreaterThan(0); + expect(shares.totalUnitShares).toBeGreaterThan(0); + }); +}); + +describe("compat: ValidatorDeposits", () => { + test("fetch all and spot-check", async () => { + try { + skipUnlessCompat(); + } catch { + return; + } + + const client = compatClient(); + const deposits = await client.fetchAllValidatorDeposits(); + expect(deposits.length).toBeGreaterThan(0); + + // Verify single lookup matches list entry. + const first = deposits[0]; + const single = await client.fetchValidatorDeposit(first.nodeId); + expect(single.nodeId.toBase58()).toBe(first.nodeId.toBase58()); + expect(single.writtenOffSolDebt).toBe(first.writtenOffSolDebt); + }); +}); + +describe("compat: ContributorRewards", () => { + test("fetch all and spot-check", async () => { + try { + skipUnlessCompat(); + } catch { + return; + } + + const client = compatClient(); + const rewards = await client.fetchAllContributorRewards(); + expect(rewards.length).toBeGreaterThan(0); + + // Verify single lookup matches list entry. + const first = rewards[0]; + const single = await client.fetchContributorRewards(first.serviceKey); + expect(single.serviceKey.toBase58()).toBe(first.serviceKey.toBase58()); + expect(single.rewardsManagerKey.toBase58()).toBe(first.rewardsManagerKey.toBase58()); + expect(single.flags).toBe(first.flags); + }); +}); diff --git a/sdk/revdist/typescript/revdist/tests/fixtures.test.ts b/sdk/revdist/typescript/revdist/tests/fixtures.test.ts new file mode 100644 index 000000000..a11c98f3d --- /dev/null +++ b/sdk/revdist/typescript/revdist/tests/fixtures.test.ts @@ -0,0 +1,261 @@ +/** + * Fixture-based compatibility tests. + * + * These tests deserialize binary fixtures generated by the Rust fixture + * generator and verify that TypeScript's deserialized field values match + * the expected values from the JSON sidecar files. + * + * Regenerate fixtures: + * cd ../../../testdata/fixtures/generate-fixtures && cargo run + */ + +import { describe, expect, test } from "bun:test"; +import { readFileSync } from "fs"; +import { join } from "path"; +import { PublicKey } from "@solana/web3.js"; +import { + DISCRIMINATOR_PROGRAM_CONFIG, + DISCRIMINATOR_DISTRIBUTION, + DISCRIMINATOR_JOURNAL, + DISCRIMINATOR_SOLANA_VALIDATOR_DEPOSIT, + DISCRIMINATOR_CONTRIBUTOR_REWARDS, +} from "../discriminator.js"; +import { + deserializeProgramConfig, + deserializeDistribution, + deserializeJournal, + deserializeSolanaValidatorDeposit, + deserializeContributorRewards, + PROGRAM_CONFIG_STRUCT_SIZE, + DISTRIBUTION_STRUCT_SIZE, + JOURNAL_STRUCT_SIZE, + SOLANA_VALIDATOR_DEPOSIT_STRUCT_SIZE, + CONTRIBUTOR_REWARDS_STRUCT_SIZE, +} from "../state.js"; + +const FIXTURES_DIR = join( + __dirname, + "..", + "..", + "..", + "testdata", + "fixtures", +); + +interface FieldValue { + name: string; + value: string; + typ: string; +} + +interface FixtureMeta { + name: string; + struct_size: number; + discriminator_hex: string; + fields: FieldValue[]; +} + +function loadFixture(name: string): [Uint8Array, FixtureMeta] { + const binData = new Uint8Array( + readFileSync(join(FIXTURES_DIR, `${name}.bin`)), + ); + const meta: FixtureMeta = JSON.parse( + readFileSync(join(FIXTURES_DIR, `${name}.json`), "utf-8"), + ); + return [binData, meta]; +} + +function assertField( + field: FieldValue, + fieldMap: Record, +): void { + const got = fieldMap[field.name]; + if (got === undefined) return; // unmapped field + + switch (field.typ) { + case "u8": + case "u16": + case "u32": + expect(got).toBe(Number(field.value)); + break; + case "u64": + expect(got).toBe(BigInt(field.value)); + break; + case "pubkey": + expect((got as PublicKey).toBase58()).toBe(field.value); + break; + default: + throw new Error(`unknown type: ${field.typ}`); + } +} + +describe("ProgramConfig fixture", () => { + test("deserialize", () => { + const [data, meta] = loadFixture("program_config"); + expect(PROGRAM_CONFIG_STRUCT_SIZE).toBe(meta.struct_size); + + const config = deserializeProgramConfig(data, DISCRIMINATOR_PROGRAM_CONFIG); + + const fieldMap: Record = { + Flags: config.flags, + NextCompletedDZEpoch: config.nextCompletedDzEpoch, + BumpSeed: config.bumpSeed, + AdminKey: config.adminKey, + DebtAccountantKey: config.debtAccountantKey, + RewardsAccountantKey: config.rewardsAccountantKey, + ContributorManagerKey: config.contributorManagerKey, + SOL2ZSwapProgramID: config.sol2zSwapProgramId, + CalculationGracePeriodMinutes: + config.distributionParameters.calculationGracePeriodMinutes, + InitializationGracePeriodMinutes: + config.distributionParameters.initializationGracePeriodMinutes, + MinimumEpochDurationToFinalizeRewards: + config.distributionParameters.minimumEpochDurationToFinalizeRewards, + BurnRateLimit: + config.distributionParameters.communityBurnRateParameters.limit, + BurnRateDZEpochsToIncreasing: + config.distributionParameters.communityBurnRateParameters + .dzEpochsToIncreasing, + BurnRateDZEpochsToLimit: + config.distributionParameters.communityBurnRateParameters + .dzEpochsToLimit, + BaseBlockRewardsPct: + config.distributionParameters.solanaValidatorFeeParameters + .baseBlockRewardsPct, + PriorityBlockRewardsPct: + config.distributionParameters.solanaValidatorFeeParameters + .priorityBlockRewardsPct, + InflationRewardsPct: + config.distributionParameters.solanaValidatorFeeParameters + .inflationRewardsPct, + JitoTipsPct: + config.distributionParameters.solanaValidatorFeeParameters.jitoTipsPct, + FixedSOLAmount: + config.distributionParameters.solanaValidatorFeeParameters + .fixedSolAmount, + DistributeRewardsLamports: + config.relayParameters.distributeRewardsLamports, + DebtWriteOffFeatureActivationEpoch: + config.debtWriteOffFeatureActivationEpoch, + }; + + for (const field of meta.fields) { + assertField(field, fieldMap); + } + }); + + test("tolerates extra bytes", () => { + const [data] = loadFixture("program_config"); + const extended = new Uint8Array(data.length + 64); + extended.set(data); + const config = deserializeProgramConfig( + extended, + DISCRIMINATOR_PROGRAM_CONFIG, + ); + expect(config.flags).toBe(1n); + }); +}); + +describe("Distribution fixture", () => { + test("deserialize", () => { + const [data, meta] = loadFixture("distribution"); + expect(DISTRIBUTION_STRUCT_SIZE).toBe(meta.struct_size); + + const dist = deserializeDistribution(data, DISCRIMINATOR_DISTRIBUTION); + + const fieldMap: Record = { + DZEpoch: dist.dzEpoch, + Flags: dist.flags, + CommunityBurnRate: dist.communityBurnRate, + BaseBlockRewardsPct: + dist.solanaValidatorFeeParameters.baseBlockRewardsPct, + PriorityBlockRewardsPct: + dist.solanaValidatorFeeParameters.priorityBlockRewardsPct, + InflationRewardsPct: + dist.solanaValidatorFeeParameters.inflationRewardsPct, + JitoTipsPct: dist.solanaValidatorFeeParameters.jitoTipsPct, + FixedSOLAmount: dist.solanaValidatorFeeParameters.fixedSolAmount, + TotalSolanaValidators: dist.totalSolanaValidators, + SolanaValidatorPaymentsCount: dist.solanaValidatorPaymentsCount, + TotalSolanaValidatorDebt: dist.totalSolanaValidatorDebt, + CollectedSolanaValidatorPayments: dist.collectedSolanaValidatorPayments, + TotalContributors: dist.totalContributors, + DistributedRewardsCount: dist.distributedRewardsCount, + CollectedPrepaid2ZPayments: dist.collectedPrepaid2zPayments, + Collected2ZConvertedFromSOL: dist.collected2zConvertedFromSol, + UncollectibleSOLDebt: dist.uncollectibleSolDebt, + Distributed2ZAmount: dist.distributed2zAmount, + Burned2ZAmount: dist.burned2zAmount, + SolanaValidatorWriteOffCount: dist.solanaValidatorWriteOffCount, + }; + + for (const field of meta.fields) { + assertField(field, fieldMap); + } + }); +}); + +describe("Journal fixture", () => { + test("deserialize", () => { + const [data, meta] = loadFixture("journal"); + expect(JOURNAL_STRUCT_SIZE).toBe(meta.struct_size); + + const journal = deserializeJournal(data, DISCRIMINATOR_JOURNAL); + + const fieldMap: Record = { + BumpSeed: journal.bumpSeed, + TotalSOLBalance: journal.totalSolBalance, + Total2ZBalance: journal.total2zBalance, + Swap2ZDestinationBalance: journal.swap2zDestinationBalance, + SwappedSOLAmount: journal.swappedSolAmount, + NextDZEpochToSweepTokens: journal.nextDzEpochToSweepTokens, + }; + + for (const field of meta.fields) { + assertField(field, fieldMap); + } + }); +}); + +describe("SolanaValidatorDeposit fixture", () => { + test("deserialize", () => { + const [data, meta] = loadFixture("solana_validator_deposit"); + expect(SOLANA_VALIDATOR_DEPOSIT_STRUCT_SIZE).toBe(meta.struct_size); + + const deposit = deserializeSolanaValidatorDeposit( + data, + DISCRIMINATOR_SOLANA_VALIDATOR_DEPOSIT, + ); + + const fieldMap: Record = { + NodeID: deposit.nodeId, + WrittenOffSOLDebt: deposit.writtenOffSolDebt, + }; + + for (const field of meta.fields) { + assertField(field, fieldMap); + } + }); +}); + +describe("ContributorRewards fixture", () => { + test("deserialize", () => { + const [data, meta] = loadFixture("contributor_rewards"); + expect(CONTRIBUTOR_REWARDS_STRUCT_SIZE).toBe(meta.struct_size); + + const rewards = deserializeContributorRewards( + data, + DISCRIMINATOR_CONTRIBUTOR_REWARDS, + ); + + const fieldMap: Record = { + RewardsManagerKey: rewards.rewardsManagerKey, + ServiceKey: rewards.serviceKey, + Flags: rewards.flags, + }; + + for (const field of meta.fields) { + assertField(field, fieldMap); + } + }); +}); diff --git a/sdk/revdist/typescript/revdist/tests/pda.test.ts b/sdk/revdist/typescript/revdist/tests/pda.test.ts new file mode 100644 index 000000000..441e89bb7 --- /dev/null +++ b/sdk/revdist/typescript/revdist/tests/pda.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, test } from "bun:test"; +import { PublicKey } from "@solana/web3.js"; +import { + deriveConfigPda, + deriveDistributionPda, + deriveJournalPda, + deriveValidatorDepositPda, + deriveContributorRewardsPda, +} from "../pda.js"; + +const PROGRAM_ID = new PublicKey( + "dzrevZC94tBLwuHw1dyynZxaXTWyp7yocsinyEVPtt4", +); + +describe("PDA derivation", () => { + test("config PDA is deterministic", () => { + const [addr1, bump1] = deriveConfigPda(PROGRAM_ID); + const [addr2, bump2] = deriveConfigPda(PROGRAM_ID); + expect(addr1.equals(addr2)).toBe(true); + expect(bump1).toBe(bump2); + expect(addr1.equals(PublicKey.default)).toBe(false); + }); + + test("distribution PDA differs by epoch", () => { + const [addr1] = deriveDistributionPda(PROGRAM_ID, 1n); + const [addr2] = deriveDistributionPda(PROGRAM_ID, 2n); + expect(addr1.equals(addr2)).toBe(false); + }); + + test("journal PDA", () => { + const [addr] = deriveJournalPda(PROGRAM_ID); + expect(addr.equals(PublicKey.default)).toBe(false); + }); + + test("validator deposit PDA", () => { + const nodeId = new PublicKey( + "4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM", + ); + const [addr] = deriveValidatorDepositPda(PROGRAM_ID, nodeId); + expect(addr.equals(PublicKey.default)).toBe(false); + }); + + test("contributor rewards PDA", () => { + const serviceKey = new PublicKey( + "4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM", + ); + const [addr] = deriveContributorRewardsPda(PROGRAM_ID, serviceKey); + expect(addr.equals(PublicKey.default)).toBe(false); + }); +}); diff --git a/sdk/revdist/typescript/tsconfig.json b/sdk/revdist/typescript/tsconfig.json new file mode 100644 index 000000000..66da80bc5 --- /dev/null +++ b/sdk/revdist/typescript/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "dist", + "declaration": true, + "types": ["bun"], + "baseUrl": ".", + "paths": { + "@doublezero/borsh-incremental": ["../../borsh-incremental/typescript/borsh-incremental/index.ts"] + } + }, + "include": ["revdist/**/*.ts"] +} diff --git a/sdk/serviceability/go/bytereader.go b/sdk/serviceability/go/bytereader.go new file mode 100644 index 000000000..1b44fae37 --- /dev/null +++ b/sdk/serviceability/go/bytereader.go @@ -0,0 +1,68 @@ +package serviceability + +import ( + "encoding/binary" + + borsh "github.com/malbeclabs/doublezero/sdk/borsh-incremental/go" +) + +// ByteReader wraps borshincremental.Reader with the legacy no-error API. +// All read methods use TryRead* with zero defaults to preserve backward +// compatibility (silently returning zero values on short data). +type ByteReader struct { + r *borsh.Reader +} + +func NewByteReader(data []byte) *ByteReader { + return &ByteReader{r: borsh.NewReader(data)} +} + +func (br *ByteReader) GetOffset() int { return br.r.Offset() } +func (br *ByteReader) Remaining() uint32 { return uint32(br.r.Remaining()) } + +func (br *ByteReader) ReadU8() uint8 { return br.r.TryReadU8(0) } +func (br *ByteReader) ReadBool() bool { return br.r.TryReadBool(false) } +func (br *ByteReader) ReadU16() uint16 { return br.r.TryReadU16(0) } +func (br *ByteReader) ReadU32() uint32 { return br.r.TryReadU32(0) } +func (br *ByteReader) ReadU64() uint64 { return br.r.TryReadU64(0) } +func (br *ByteReader) ReadF64() float64 { return br.r.TryReadF64(0) } +func (br *ByteReader) ReadPubkey() [32]byte { return br.r.TryReadPubkey([32]byte{}) } +func (br *ByteReader) ReadIPv4() [4]byte { return br.r.TryReadIPv4([4]byte{}) } +func (br *ByteReader) ReadNetworkV4() [5]byte { return br.r.TryReadNetworkV4([5]byte{}) } + +func (br *ByteReader) ReadU128() Uint128 { + raw := br.r.TryReadU128([16]byte{}) + return Uint128{ + Low: binary.LittleEndian.Uint64(raw[:8]), + High: binary.LittleEndian.Uint64(raw[8:]), + } +} + +func (br *ByteReader) ReadString() string { + return br.r.TryReadString("") +} + +func (br *ByteReader) ReadPubkeySlice() [][32]byte { + return br.r.TryReadPubkeySlice(nil) +} + +func (br *ByteReader) ReadNetworkV4Slice() [][5]byte { + return br.r.TryReadNetworkV4Slice(nil) +} + +func (br *ByteReader) ReadU32Slice() []uint32 { + return br.r.TryReadU32Slice(nil) +} + +func (br *ByteReader) ReadBytes(n int) []byte { + if br.r.Remaining() < n { + return make([]byte, n) + } + v, _ := br.r.ReadBytes(n) + return v +} + +func (br *ByteReader) DumpBytes(n int) string { + // Preserved for compatibility but uses offset from underlying reader. + return "" +} diff --git a/sdk/serviceability/go/client.go b/sdk/serviceability/go/client.go new file mode 100644 index 000000000..67336e19b --- /dev/null +++ b/sdk/serviceability/go/client.go @@ -0,0 +1,161 @@ +package serviceability + +import ( + "context" + "fmt" + + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" +) + +// RPCClient is the minimal RPC interface needed by the client. +type RPCClient interface { + GetProgramAccounts(ctx context.Context, publicKey solana.PublicKey) (rpc.GetProgramAccountsResult, error) +} + +// ProgramData aggregates all deserialized serviceability accounts. +type ProgramData struct { + GlobalState *GlobalState + GlobalConfig *GlobalConfig + Locations []Location + Exchanges []Exchange + Contributors []Contributor + Devices []Device + Links []Link + Users []User + MulticastGroups []MulticastGroup + ProgramConfig *ProgramConfig + AccessPasses []AccessPass +} + +// Client provides read-only access to serviceability program accounts. +type Client struct { + rpc RPCClient + programID solana.PublicKey +} + +// New creates a new serviceability client. +func New(rpc RPCClient, programID solana.PublicKey) *Client { + return &Client{rpc: rpc, programID: programID} +} + +// NewForEnv creates a client configured for the given environment. +// Valid environments: "mainnet-beta", "testnet", "devnet", "localnet". +func NewForEnv(env string) *Client { + return New(NewRPCClient(LedgerRPCURLs[env]), solana.MustPublicKeyFromBase58(ProgramIDs[env])) +} + +// NewMainnetBeta creates a client configured for mainnet-beta. +func NewMainnetBeta() *Client { + return NewForEnv("mainnet-beta") +} + +// NewTestnet creates a client configured for testnet. +func NewTestnet() *Client { + return NewForEnv("testnet") +} + +// NewDevnet creates a client configured for devnet. +func NewDevnet() *Client { + return NewForEnv("devnet") +} + +// NewLocalnet creates a client configured for localnet. +func NewLocalnet() *Client { + return NewForEnv("localnet") +} + +// ProgramID returns the program ID this client is configured with. +func (c *Client) ProgramID() solana.PublicKey { + return c.programID +} + +// GetProgramData fetches all program accounts and deserializes them by type. +func (c *Client) GetProgramData(ctx context.Context) (*ProgramData, error) { + out, err := c.rpc.GetProgramAccounts(ctx, c.programID) + if err != nil { + return nil, err + } + + if len(out) == 0 { + return nil, fmt.Errorf("GetProgramAccounts returned empty result for program %s", c.programID) + } + + pd := &ProgramData{ + Locations: []Location{}, + Exchanges: []Exchange{}, + Contributors: []Contributor{}, + Devices: []Device{}, + Links: []Link{}, + Users: []User{}, + MulticastGroups: []MulticastGroup{}, + AccessPasses: []AccessPass{}, + } + + for _, element := range out { + data := element.Account.Data.GetBinary() + if len(data) == 0 { + continue + } + reader := NewByteReader(data) + + switch AccountType(data[0]) { + case GlobalStateType: + var gs GlobalState + DeserializeGlobalState(reader, &gs) + gs.PubKey = element.Pubkey + pd.GlobalState = &gs + case GlobalConfigType: + var gc GlobalConfig + DeserializeGlobalConfig(reader, &gc) + gc.PubKey = element.Pubkey + pd.GlobalConfig = &gc + case LocationType: + var loc Location + DeserializeLocation(reader, &loc) + loc.PubKey = element.Pubkey + pd.Locations = append(pd.Locations, loc) + case ExchangeType: + var exch Exchange + DeserializeExchange(reader, &exch) + exch.PubKey = element.Pubkey + pd.Exchanges = append(pd.Exchanges, exch) + case DeviceType: + var dev Device + DeserializeDevice(reader, &dev) + dev.PubKey = element.Pubkey + pd.Devices = append(pd.Devices, dev) + case LinkType: + var link Link + DeserializeLink(reader, &link) + link.PubKey = element.Pubkey + pd.Links = append(pd.Links, link) + case UserType: + var user User + DeserializeUser(reader, &user) + user.PubKey = element.Pubkey + pd.Users = append(pd.Users, user) + case MulticastGroupType: + var mg MulticastGroup + DeserializeMulticastGroup(reader, &mg) + mg.PubKey = element.Pubkey + pd.MulticastGroups = append(pd.MulticastGroups, mg) + case ProgramConfigType: + var pc ProgramConfig + DeserializeProgramConfig(reader, &pc) + pd.ProgramConfig = &pc + case ContributorType: + var contrib Contributor + DeserializeContributor(reader, &contrib) + contrib.PubKey = element.Pubkey + pd.Contributors = append(pd.Contributors, contrib) + case AccessPassType: + var ap AccessPass + DeserializeAccessPass(reader, &ap) + ap.PubKey = element.Pubkey + pd.AccessPasses = append(pd.AccessPasses, ap) + } + } + + return pd, nil +} diff --git a/sdk/serviceability/go/compat_test.go b/sdk/serviceability/go/compat_test.go new file mode 100644 index 000000000..d295bfba7 --- /dev/null +++ b/sdk/serviceability/go/compat_test.go @@ -0,0 +1,251 @@ +package serviceability + +import ( + "context" + "encoding/binary" + "os" + "testing" + + "github.com/gagliardetto/solana-go" +) + +// These tests fetch live mainnet data and verify that our struct deserialization +// matches raw byte reads at known offsets. Run with: +// +// SERVICEABILITY_COMPAT_TEST=1 go test -run TestCompat -v ./sdk/serviceability/go/ +// +// Requires network access to Solana mainnet RPC. + +func skipUnlessCompat(t *testing.T) { + t.Helper() + if os.Getenv("SERVICEABILITY_COMPAT_TEST") == "" { + t.Skip("set SERVICEABILITY_COMPAT_TEST=1 to run compatibility tests against mainnet") + } +} + +func compatRPCURL() string { + if url := os.Getenv("SOLANA_RPC_URL"); url != "" { + return url + } + return LedgerRPCURLs["mainnet-beta"] +} + +func compatProgramID() solana.PublicKey { + return solana.MustPublicKeyFromBase58(ProgramIDs["mainnet-beta"]) +} + +func fetchRawAccount(t *testing.T, addr solana.PublicKey) []byte { + t.Helper() + rpcClient := NewRPCClient(compatRPCURL()) + result, err := rpcClient.GetAccountInfo(context.Background(), addr) + if err != nil { + t.Fatalf("fetching %s: %v", addr, err) + } + if result == nil || result.Value == nil { + t.Fatalf("account %s not found", addr) + } + return result.Value.Data.GetBinary() +} + +func TestCompatProgramConfig(t *testing.T) { + skipUnlessCompat(t) + + programID := compatProgramID() + addr, _, err := DeriveProgramConfigPDA(programID) + if err != nil { + t.Fatalf("DeriveProgramConfigPDA: %v", err) + } + + raw := fetchRawAccount(t, addr) + reader := NewByteReader(raw) + var pc ProgramConfig + DeserializeProgramConfig(reader, &pc) + + // ProgramConfig layout (all fixed-size): + // offset 0: AccountType (u8) + // offset 1: BumpSeed (u8) + // offset 2: Version.Major (u32) + // offset 6: Version.Minor (u32) + // offset 10: Version.Patch (u32) + // offset 14: MinCompatVersion.Major (u32) + // offset 18: MinCompatVersion.Minor (u32) + // offset 22: MinCompatVersion.Patch (u32) + compatAssertU8(t, raw, 0, uint8(pc.AccountType), "AccountType") + compatAssertU8(t, raw, 1, pc.BumpSeed, "BumpSeed") + compatAssertU32(t, raw, 2, pc.Version.Major, "Version.Major") + compatAssertU32(t, raw, 6, pc.Version.Minor, "Version.Minor") + compatAssertU32(t, raw, 10, pc.Version.Patch, "Version.Patch") + compatAssertU32(t, raw, 14, pc.MinCompatVersion.Major, "MinCompatVersion.Major") + compatAssertU32(t, raw, 18, pc.MinCompatVersion.Minor, "MinCompatVersion.Minor") + compatAssertU32(t, raw, 22, pc.MinCompatVersion.Patch, "MinCompatVersion.Patch") + + if pc.AccountType != ProgramConfigType { + t.Errorf("AccountType = %d, want %d", pc.AccountType, ProgramConfigType) + } + + t.Logf("ProgramConfig: version %d.%d.%d, min compat %d.%d.%d", + pc.Version.Major, pc.Version.Minor, pc.Version.Patch, + pc.MinCompatVersion.Major, pc.MinCompatVersion.Minor, pc.MinCompatVersion.Patch) +} + +func TestCompatGlobalConfig(t *testing.T) { + skipUnlessCompat(t) + + programID := compatProgramID() + addr, _, err := DeriveGlobalConfigPDA(programID) + if err != nil { + t.Fatalf("DeriveGlobalConfigPDA: %v", err) + } + + raw := fetchRawAccount(t, addr) + reader := NewByteReader(raw) + var gc GlobalConfig + DeserializeGlobalConfig(reader, &gc) + + // GlobalConfig layout (all fixed-size): + // offset 0: AccountType (u8) + // offset 1: Owner (32 bytes) + // offset 33: BumpSeed (u8) + // offset 34: LocalASN (u32) + // offset 38: RemoteASN (u32) + // offset 42: DeviceTunnelBlock (5 bytes) + // offset 47: UserTunnelBlock (5 bytes) + // offset 52: MulticastGroupBlock (5 bytes) + // offset 57: NextBGPCommunity (u16) + compatAssertU8(t, raw, 0, uint8(gc.AccountType), "AccountType") + compatAssertPubkey(t, raw, 1, gc.Owner, "Owner") + compatAssertU8(t, raw, 33, gc.BumpSeed, "BumpSeed") + compatAssertU32(t, raw, 34, gc.LocalASN, "LocalASN") + compatAssertU32(t, raw, 38, gc.RemoteASN, "RemoteASN") + compatAssertU16(t, raw, 57, gc.NextBGPCommunity, "NextBGPCommunity") + + if gc.AccountType != GlobalConfigType { + t.Errorf("AccountType = %d, want %d", gc.AccountType, GlobalConfigType) + } + if gc.LocalASN == 0 { + t.Error("LocalASN is 0, expected > 0 on mainnet") + } + + t.Logf("GlobalConfig: localASN=%d, remoteASN=%d, nextBGPCommunity=%d", + gc.LocalASN, gc.RemoteASN, gc.NextBGPCommunity) +} + +func TestCompatGlobalState(t *testing.T) { + skipUnlessCompat(t) + + programID := compatProgramID() + addr, _, err := DeriveGlobalStatePDA(programID) + if err != nil { + t.Fatalf("DeriveGlobalStatePDA: %v", err) + } + + raw := fetchRawAccount(t, addr) + reader := NewByteReader(raw) + var gs GlobalState + DeserializeGlobalState(reader, &gs) + + // GlobalState fixed layout (first 18 bytes before variable-length vecs): + // offset 0: AccountType (u8) + // offset 1: BumpSeed (u8) + // offset 2: AccountIndex (u128 = 16 bytes) + compatAssertU8(t, raw, 0, uint8(gs.AccountType), "AccountType") + compatAssertU8(t, raw, 1, gs.BumpSeed, "BumpSeed") + + if gs.AccountType != GlobalStateType { + t.Errorf("AccountType = %d, want %d", gs.AccountType, GlobalStateType) + } + + // Sanity checks on deserialized values. + if gs.AccountIndex.Low == 0 && gs.AccountIndex.High == 0 { + t.Error("AccountIndex is zero, expected > 0 on mainnet") + } + var zeroPK [32]byte + if gs.ActivatorAuthorityPK == zeroPK { + t.Error("ActivatorAuthorityPK is zero") + } + if gs.SentinelAuthorityPK == zeroPK { + t.Error("SentinelAuthorityPK is zero") + } + if gs.HealthOraclePK == zeroPK { + t.Log("HealthOraclePK is zero") + } + + t.Logf("GlobalState: accountIndex=%d, foundationAllowlist=%d entries, qaAllowlist=%d entries", + gs.AccountIndex.Low, len(gs.FoundationAllowlist), len(gs.QAAllowlist)) +} + +func TestCompatGetProgramData(t *testing.T) { + skipUnlessCompat(t) + + client := NewMainnetBeta() + ctx := context.Background() + + pd, err := client.GetProgramData(ctx) + if err != nil { + t.Fatalf("GetProgramData: %v", err) + } + + if pd.GlobalState == nil { + t.Fatal("GlobalState is nil") + } + if pd.GlobalConfig == nil { + t.Fatal("GlobalConfig is nil") + } + if pd.ProgramConfig == nil { + t.Fatal("ProgramConfig is nil") + } + if len(pd.Locations) == 0 { + t.Error("no locations found on mainnet") + } + if len(pd.Exchanges) == 0 { + t.Error("no exchanges found on mainnet") + } + if len(pd.Devices) == 0 { + t.Error("no devices found on mainnet") + } + if len(pd.Links) == 0 { + t.Error("no links found on mainnet") + } + if len(pd.Contributors) == 0 { + t.Error("no contributors found on mainnet") + } + + t.Logf("ProgramData: %d locations, %d exchanges, %d devices, %d links, %d users, %d contributors, %d access passes", + len(pd.Locations), len(pd.Exchanges), len(pd.Devices), len(pd.Links), + len(pd.Users), len(pd.Contributors), len(pd.AccessPasses)) +} + +// Helpers to compare deserialized values against raw byte reads. + +func compatAssertU8(t *testing.T, raw []byte, offset int, got uint8, name string) { + t.Helper() + want := raw[offset] + if got != want { + t.Errorf("%s: deserialized=%d, raw[%d]=%d", name, got, offset, want) + } +} + +func compatAssertU16(t *testing.T, raw []byte, offset int, got uint16, name string) { + t.Helper() + want := binary.LittleEndian.Uint16(raw[offset:]) + if got != want { + t.Errorf("%s: deserialized=%d, raw[%d]=%d", name, got, offset, want) + } +} + +func compatAssertU32(t *testing.T, raw []byte, offset int, got uint32, name string) { + t.Helper() + want := binary.LittleEndian.Uint32(raw[offset:]) + if got != want { + t.Errorf("%s: deserialized=%d, raw[%d]=%d", name, got, offset, want) + } +} + +func compatAssertPubkey(t *testing.T, raw []byte, offset int, got [32]byte, name string) { + t.Helper() + var want [32]byte + copy(want[:], raw[offset:offset+32]) + if got != want { + t.Errorf("%s: deserialized=%x, raw[%d]=%x", name, got, offset, want) + } +} diff --git a/sdk/serviceability/go/config.go b/sdk/serviceability/go/config.go new file mode 100644 index 000000000..cb6409c5b --- /dev/null +++ b/sdk/serviceability/go/config.go @@ -0,0 +1,17 @@ +package serviceability + +// Program IDs per environment. +var ProgramIDs = map[string]string{ + "mainnet-beta": "ser2VaTMAcYTaauMrTSfSrxBaUDq7BLNs2xfUugTAGv", + "testnet": "DZtnuQ839pSaDMFG5q1ad2V95G82S5EC4RrB3Ndw2Heb", + "devnet": "GYhQDKuESrasNZGyhMJhGYFtbzNijYhcrN9poSqCQVah", + "localnet": "7CTniUa88iJKUHTrCkB4TjAoG6TD7AMivhQeuqN2LPtX", +} + +// LedgerRPCURLs are the DZ Ledger RPC URLs per environment. +var LedgerRPCURLs = map[string]string{ + "mainnet-beta": "https://doublezero-mainnet-beta.rpcpool.com/db336024-e7a8-46b1-80e5-352dd77060ab", + "testnet": "https://doublezerolocalnet.rpcpool.com/8a4fd3f4-0977-449f-88c7-63d4b0f10f16", + "devnet": "https://doublezerolocalnet.rpcpool.com/8a4fd3f4-0977-449f-88c7-63d4b0f10f16", + "localnet": "http://localhost:8899", +} diff --git a/sdk/serviceability/go/deserialize.go b/sdk/serviceability/go/deserialize.go new file mode 100644 index 000000000..abdf3b9f1 --- /dev/null +++ b/sdk/serviceability/go/deserialize.go @@ -0,0 +1,239 @@ +package serviceability + +import "log" + +func DeserializeGlobalState(reader *ByteReader, gs *GlobalState) { + gs.AccountType = AccountType(reader.ReadU8()) + gs.BumpSeed = reader.ReadU8() + gs.AccountIndex = reader.ReadU128() + gs.FoundationAllowlist = reader.ReadPubkeySlice() + _ = reader.ReadPubkeySlice() // deprecated device_allowlist + _ = reader.ReadPubkeySlice() // deprecated user_allowlist + gs.ActivatorAuthorityPK = reader.ReadPubkey() + gs.SentinelAuthorityPK = reader.ReadPubkey() + gs.ContributorAirdropLamports = reader.ReadU64() + gs.UserAirdropLamports = reader.ReadU64() + gs.HealthOraclePK = reader.ReadPubkey() + gs.QAAllowlist = reader.ReadPubkeySlice() +} + +func DeserializeGlobalConfig(reader *ByteReader, cfg *GlobalConfig) { + cfg.AccountType = AccountType(reader.ReadU8()) + cfg.Owner = reader.ReadPubkey() + cfg.BumpSeed = reader.ReadU8() + cfg.LocalASN = reader.ReadU32() + cfg.RemoteASN = reader.ReadU32() + cfg.DeviceTunnelBlock = reader.ReadNetworkV4() + cfg.UserTunnelBlock = reader.ReadNetworkV4() + cfg.MulticastGroupBlock = reader.ReadNetworkV4() + cfg.NextBGPCommunity = reader.ReadU16() +} + +func DeserializeLocation(reader *ByteReader, loc *Location) { + loc.AccountType = AccountType(reader.ReadU8()) + loc.Owner = reader.ReadPubkey() + loc.Index = reader.ReadU128() + loc.BumpSeed = reader.ReadU8() + loc.Lat = reader.ReadF64() + loc.Lng = reader.ReadF64() + loc.LocId = reader.ReadU32() + loc.Status = LocationStatus(reader.ReadU8()) + loc.Code = reader.ReadString() + loc.Name = reader.ReadString() + loc.Country = reader.ReadString() + loc.ReferenceCount = reader.ReadU32() +} + +func DeserializeExchange(reader *ByteReader, exchange *Exchange) { + exchange.AccountType = AccountType(reader.ReadU8()) + exchange.Owner = reader.ReadPubkey() + exchange.Index = reader.ReadU128() + exchange.BumpSeed = reader.ReadU8() + exchange.Lat = reader.ReadF64() + exchange.Lng = reader.ReadF64() + exchange.BgpCommunity = reader.ReadU16() + _ = reader.ReadU16() // unused padding + exchange.Status = ExchangeStatus(reader.ReadU8()) + exchange.Code = reader.ReadString() + exchange.Name = reader.ReadString() + exchange.ReferenceCount = reader.ReadU32() + exchange.Device1PK = reader.ReadPubkey() + exchange.Device2PK = reader.ReadPubkey() +} + +func DeserializeInterface(reader *ByteReader, iface *Interface) { + iface.Version = reader.ReadU8() + + if iface.Version > (CurrentInterfaceVersion - 1) { + log.Println("DeserializeInterface: Unsupported interface version", iface.Version) + return + } + + switch iface.Version { + case 0: // version 1 + DeserializeInterfaceV1(reader, iface) + case 1: // version 2 + DeserializeInterfaceV2(reader, iface) + } +} + +func DeserializeInterfaceV1(reader *ByteReader, iface *Interface) { + iface.Status = InterfaceStatus(reader.ReadU8()) + iface.Name = reader.ReadString() + iface.InterfaceType = InterfaceType(reader.ReadU8()) + iface.LoopbackType = LoopbackType(reader.ReadU8()) + iface.VlanId = reader.ReadU16() + iface.IpNet = reader.ReadNetworkV4() + iface.NodeSegmentIdx = reader.ReadU16() + iface.UserTunnelEndpoint = reader.ReadBool() +} + +func DeserializeInterfaceV2(reader *ByteReader, iface *Interface) { + iface.Status = InterfaceStatus(reader.ReadU8()) + iface.Name = reader.ReadString() + iface.InterfaceType = InterfaceType(reader.ReadU8()) + iface.InterfaceCYOA = InterfaceCYOA(reader.ReadU8()) + iface.InterfaceDIA = InterfaceDIA(reader.ReadU8()) + iface.LoopbackType = LoopbackType(reader.ReadU8()) + iface.Bandwidth = reader.ReadU64() + iface.Cir = reader.ReadU64() + iface.Mtu = reader.ReadU16() + iface.RoutingMode = RoutingMode(reader.ReadU8()) + iface.VlanId = reader.ReadU16() + iface.IpNet = reader.ReadNetworkV4() + iface.NodeSegmentIdx = reader.ReadU16() + iface.UserTunnelEndpoint = reader.ReadBool() +} + +func DeserializeDevice(reader *ByteReader, dev *Device) { + dev.AccountType = AccountType(reader.ReadU8()) + dev.Owner = reader.ReadPubkey() + dev.Index = reader.ReadU128() + dev.BumpSeed = reader.ReadU8() + dev.LocationPubKey = reader.ReadPubkey() + dev.ExchangePubKey = reader.ReadPubkey() + dev.DeviceType = DeviceDeviceType(reader.ReadU8()) + dev.PublicIp = reader.ReadIPv4() + dev.Status = DeviceStatus(reader.ReadU8()) + dev.Code = reader.ReadString() + dev.DzPrefixes = reader.ReadNetworkV4Slice() + dev.MetricsPublisherPubKey = reader.ReadPubkey() + dev.ContributorPubKey = reader.ReadPubkey() + dev.MgmtVrf = reader.ReadString() + dev.Interfaces = make([]Interface, 0) + length := reader.ReadU32() + if length > 0 && (length*18) > reader.Remaining() { + log.Println("DeserializeDevice: Not enough data for interfaces (# of interfaces = ", length, ")") + return + } + for i := uint32(0); i < length; i++ { + var iface Interface + DeserializeInterface(reader, &iface) + dev.Interfaces = append(dev.Interfaces, iface) + } + dev.ReferenceCount = reader.ReadU32() + dev.UsersCount = reader.ReadU16() + dev.MaxUsers = reader.ReadU16() + dev.DeviceHealth = DeviceHealth(reader.ReadU8()) + dev.DeviceDesiredStatus = DeviceDesiredStatus(reader.ReadU8()) +} + +func DeserializeLink(reader *ByteReader, link *Link) { + link.AccountType = AccountType(reader.ReadU8()) + link.Owner = reader.ReadPubkey() + link.Index = reader.ReadU128() + link.BumpSeed = reader.ReadU8() + link.SideAPubKey = reader.ReadPubkey() + link.SideZPubKey = reader.ReadPubkey() + link.LinkType = LinkLinkType(reader.ReadU8()) + link.Bandwidth = reader.ReadU64() + link.Mtu = reader.ReadU32() + link.DelayNs = reader.ReadU64() + link.JitterNs = reader.ReadU64() + link.TunnelId = reader.ReadU16() + link.TunnelNet = reader.ReadNetworkV4() + link.Status = LinkStatus(reader.ReadU8()) + link.Code = reader.ReadString() + link.ContributorPubKey = reader.ReadPubkey() + link.SideAIfaceName = reader.ReadString() + link.SideZIfaceName = reader.ReadString() + link.DelayOverrideNs = reader.ReadU64() + link.LinkHealth = LinkHealth(reader.ReadU8()) + link.LinkDesiredStatus = LinkDesiredStatus(reader.ReadU8()) +} + +func DeserializeUser(reader *ByteReader, user *User) { + user.AccountType = AccountType(reader.ReadU8()) + user.Owner = reader.ReadPubkey() + user.Index = reader.ReadU128() + user.BumpSeed = reader.ReadU8() + user.UserType = UserUserType(reader.ReadU8()) + user.TenantPubKey = reader.ReadPubkey() + user.DevicePubKey = reader.ReadPubkey() + user.CyoaType = CyoaType(reader.ReadU8()) + user.ClientIp = reader.ReadIPv4() + user.DzIp = reader.ReadIPv4() + user.TunnelId = reader.ReadU16() + user.TunnelNet = reader.ReadNetworkV4() + user.Status = UserStatus(reader.ReadU8()) + user.Publishers = reader.ReadPubkeySlice() + user.Subscribers = reader.ReadPubkeySlice() + user.ValidatorPubKey = reader.ReadPubkey() +} + +func DeserializeMulticastGroup(reader *ByteReader, mg *MulticastGroup) { + mg.AccountType = AccountType(reader.ReadU8()) + mg.Owner = reader.ReadPubkey() + mg.Index = reader.ReadU128() + mg.BumpSeed = reader.ReadU8() + mg.TenantPubKey = reader.ReadPubkey() + mg.MulticastIp = reader.ReadIPv4() + mg.MaxBandwidth = reader.ReadU64() + mg.Status = MulticastGroupStatus(reader.ReadU8()) + mg.Code = reader.ReadString() + mg.PublisherCount = reader.ReadU32() + mg.SubscriberCount = reader.ReadU32() +} + +func DeserializeContributor(reader *ByteReader, contributor *Contributor) { + contributor.AccountType = AccountType(reader.ReadU8()) + contributor.Owner = reader.ReadPubkey() + contributor.Index = reader.ReadU128() + contributor.BumpSeed = reader.ReadU8() + contributor.Status = ContributorStatus(reader.ReadU8()) + contributor.Code = reader.ReadString() + contributor.ReferenceCount = reader.ReadU32() + contributor.OpsManagerPK = reader.ReadPubkey() +} + +func DeserializeProgramConfig(reader *ByteReader, pc *ProgramConfig) { + pc.AccountType = AccountType(reader.ReadU8()) + pc.BumpSeed = reader.ReadU8() + DeserializeProgramVersion(reader, &pc.Version) + DeserializeProgramVersion(reader, &pc.MinCompatVersion) +} + +func DeserializeProgramVersion(reader *ByteReader, pv *ProgramVersion) { + pv.Major = reader.ReadU32() + pv.Minor = reader.ReadU32() + pv.Patch = reader.ReadU32() +} + +func DeserializeAccessPass(reader *ByteReader, ap *AccessPass) { + ap.AccountType = AccountType(reader.ReadU8()) + ap.Owner = reader.ReadPubkey() + ap.BumpSeed = reader.ReadU8() + // AccessPassType is a Borsh enum: 1-byte discriminant + optional data + ap.AccessPassTypeTag = AccessPassTypeTag(reader.ReadU8()) + if ap.AccessPassTypeTag == AccessPassTypeSolanaValidator { + ap.ValidatorPubKey = reader.ReadPubkey() + } + ap.ClientIp = reader.ReadIPv4() + ap.UserPayer = reader.ReadPubkey() + ap.LastAccessEpoch = reader.ReadU64() + ap.ConnectionCount = reader.ReadU16() + ap.Status = AccessPassStatus(reader.ReadU8()) + ap.MGroupPubAllowlist = reader.ReadPubkeySlice() + ap.MGroupSubAllowlist = reader.ReadPubkeySlice() + ap.Flags = reader.ReadU8() +} diff --git a/sdk/serviceability/go/enum_string_test.go b/sdk/serviceability/go/enum_string_test.go new file mode 100644 index 000000000..91e4c5db3 --- /dev/null +++ b/sdk/serviceability/go/enum_string_test.go @@ -0,0 +1,77 @@ +package serviceability + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "runtime" + "strconv" + "testing" +) + +func TestEnumStrings(t *testing.T) { + _, filename, _, _ := runtime.Caller(0) + fixturePath := filepath.Join(filepath.Dir(filename), "..", "testdata", "enum_strings.json") + + data, err := os.ReadFile(fixturePath) + if err != nil { + t.Fatalf("reading enum_strings.json: %v", err) + } + + var fixture map[string]map[string]string + if err := json.Unmarshal(data, &fixture); err != nil { + t.Fatalf("parsing enum_strings.json: %v", err) + } + + // Map enum type names to a function that converts int -> String() output. + // AccessPassTypeTag is skipped because it has no String() method in Go. + stringers := map[string]func(int) string{ + "LocationStatus": func(v int) string { return LocationStatus(v).String() }, + "ExchangeStatus": func(v int) string { return ExchangeStatus(v).String() }, + "DeviceDeviceType": func(v int) string { return DeviceDeviceType(v).String() }, + "DeviceStatus": func(v int) string { return DeviceStatus(v).String() }, + "DeviceHealth": func(v int) string { return DeviceHealth(v).String() }, + "DeviceDesiredStatus": func(v int) string { return DeviceDesiredStatus(v).String() }, + "InterfaceStatus": func(v int) string { return InterfaceStatus(v).String() }, + "InterfaceType": func(v int) string { return InterfaceType(v).String() }, + "LoopbackType": func(v int) string { return LoopbackType(v).String() }, + "LinkLinkType": func(v int) string { return LinkLinkType(v).String() }, + "LinkStatus": func(v int) string { return LinkStatus(v).String() }, + "LinkHealth": func(v int) string { return LinkHealth(v).String() }, + "LinkDesiredStatus": func(v int) string { return LinkDesiredStatus(v).String() }, + "ContributorStatus": func(v int) string { return ContributorStatus(v).String() }, + "UserUserType": func(v int) string { return UserUserType(v).String() }, + "CyoaType": func(v int) string { return CyoaType(v).String() }, + "UserStatus": func(v int) string { return UserStatus(v).String() }, + "MulticastGroupStatus": func(v int) string { return MulticastGroupStatus(v).String() }, + "AccessPassStatus": func(v int) string { return AccessPassStatus(v).String() }, + } + + for enumName, cases := range fixture { + // AccessPassTypeTag has no String() method in Go; skip it. + if enumName == "AccessPassTypeTag" { + continue + } + + stringer, ok := stringers[enumName] + if !ok { + t.Errorf("no stringer registered for enum type %s", enumName) + continue + } + + for valStr, expected := range cases { + val, err := strconv.Atoi(valStr) + if err != nil { + t.Fatalf("%s: invalid key %q: %v", enumName, valStr, err) + } + + t.Run(fmt.Sprintf("%s/%d", enumName, val), func(t *testing.T) { + got := stringer(val) + if got != expected { + t.Errorf("%s(%d).String() = %q, want %q", enumName, val, got, expected) + } + }) + } + } +} diff --git a/sdk/serviceability/go/fixture_test.go b/sdk/serviceability/go/fixture_test.go new file mode 100644 index 000000000..b3be8dfb5 --- /dev/null +++ b/sdk/serviceability/go/fixture_test.go @@ -0,0 +1,406 @@ +package serviceability + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "reflect" + "runtime" + "strconv" + "testing" + + "github.com/gagliardetto/solana-go" +) + +// These tests deserialize binary fixtures generated by the Rust fixture +// generator and verify that Go's deserialized field values match the +// expected values from the JSON sidecar files. +// +// Regenerate fixtures: +// cd ../testdata/fixtures/generate-fixtures && cargo run + +type fixtureMeta struct { + Name string `json:"name"` + AccountType int `json:"account_type"` + Fields []fieldValue `json:"fields"` +} + +type fieldValue struct { + Name string `json:"name"` + Value string `json:"value"` + Type string `json:"typ"` +} + +func fixturesDir() string { + _, filename, _, _ := runtime.Caller(0) + return filepath.Join(filepath.Dir(filename), "..", "testdata", "fixtures") +} + +func loadFixture(t *testing.T, name string) ([]byte, fixtureMeta) { + t.Helper() + dir := fixturesDir() + + binData, err := os.ReadFile(filepath.Join(dir, name+".bin")) + if err != nil { + t.Fatalf("reading %s.bin: %v", name, err) + } + + jsonData, err := os.ReadFile(filepath.Join(dir, name+".json")) + if err != nil { + t.Fatalf("reading %s.json: %v", name, err) + } + + var meta fixtureMeta + if err := json.Unmarshal(jsonData, &meta); err != nil { + t.Fatalf("parsing %s.json: %v", name, err) + } + + return binData, meta +} + +func TestFixtureGlobalState(t *testing.T) { + data, meta := loadFixture(t, "global_state") + reader := NewByteReader(data) + var gs GlobalState + DeserializeGlobalState(reader, &gs) + + assertFields(t, meta.Fields, map[string]any{ + "AccountType": uint8(gs.AccountType), + "BumpSeed": gs.BumpSeed, + "ContributorAirdropLamports": gs.ContributorAirdropLamports, + "UserAirdropLamports": gs.UserAirdropLamports, + "ActivatorAuthorityPK": solana.PublicKey(gs.ActivatorAuthorityPK), + "SentinelAuthorityPK": solana.PublicKey(gs.SentinelAuthorityPK), + "HealthOraclePK": solana.PublicKey(gs.HealthOraclePK), + }) +} + +func TestFixtureGlobalConfig(t *testing.T) { + data, meta := loadFixture(t, "global_config") + reader := NewByteReader(data) + var gc GlobalConfig + DeserializeGlobalConfig(reader, &gc) + + assertFields(t, meta.Fields, map[string]any{ + "AccountType": uint8(gc.AccountType), + "Owner": solana.PublicKey(gc.Owner), + "BumpSeed": gc.BumpSeed, + "LocalASN": gc.LocalASN, + "RemoteASN": gc.RemoteASN, + "NextBGPCommunity": gc.NextBGPCommunity, + }) +} + +func TestFixtureLocation(t *testing.T) { + data, meta := loadFixture(t, "location") + reader := NewByteReader(data) + var loc Location + DeserializeLocation(reader, &loc) + + assertFields(t, meta.Fields, map[string]any{ + "AccountType": uint8(loc.AccountType), + "Owner": solana.PublicKey(loc.Owner), + "BumpSeed": loc.BumpSeed, + "LocId": loc.LocId, + "Status": uint8(loc.Status), + "ReferenceCount": loc.ReferenceCount, + }) +} + +func TestFixtureExchange(t *testing.T) { + data, meta := loadFixture(t, "exchange") + reader := NewByteReader(data) + var exch Exchange + DeserializeExchange(reader, &exch) + + assertFields(t, meta.Fields, map[string]any{ + "AccountType": uint8(exch.AccountType), + "Owner": solana.PublicKey(exch.Owner), + "BumpSeed": exch.BumpSeed, + "BgpCommunity": exch.BgpCommunity, + "Status": uint8(exch.Status), + "ReferenceCount": exch.ReferenceCount, + "Device1PK": solana.PublicKey(exch.Device1PK), + "Device2PK": solana.PublicKey(exch.Device2PK), + }) +} + +func TestFixtureDevice(t *testing.T) { + data, meta := loadFixture(t, "device") + reader := NewByteReader(data) + var dev Device + DeserializeDevice(reader, &dev) + + assertFields(t, meta.Fields, map[string]any{ + "AccountType": uint8(dev.AccountType), + "Owner": solana.PublicKey(dev.Owner), + "Index": dev.Index, + "BumpSeed": dev.BumpSeed, + "LocationPk": solana.PublicKey(dev.LocationPubKey), + "ExchangePk": solana.PublicKey(dev.ExchangePubKey), + "DeviceType": uint8(dev.DeviceType), + "PublicIp": dev.PublicIp, + "Status": uint8(dev.Status), + "Code": dev.Code, + "MetricsPublisherPk": solana.PublicKey(dev.MetricsPublisherPubKey), + "ContributorPk": solana.PublicKey(dev.ContributorPubKey), + "MgmtVrf": dev.MgmtVrf, + "ReferenceCount": dev.ReferenceCount, + "UsersCount": dev.UsersCount, + "MaxUsers": dev.MaxUsers, + "DeviceHealth": uint8(dev.DeviceHealth), + "DesiredStatus": uint8(dev.DeviceDesiredStatus), + }) + + // Verify DzPrefixes + if len(dev.DzPrefixes) != 1 { + t.Fatalf("DzPrefixes: want len 1, got %d", len(dev.DzPrefixes)) + } + assertEq(t, "DzPrefixes[0]", "10.10.0.0/24", formatNetworkV4(dev.DzPrefixes[0])) + + // Verify Interfaces + if len(dev.Interfaces) != 2 { + t.Fatalf("Interfaces: want len 2, got %d", len(dev.Interfaces)) + } + + iface0 := dev.Interfaces[0] + assertEq(t, "Interface0Version", uint8(0), iface0.Version) + assertEq(t, "Interface0Status", uint8(3), uint8(iface0.Status)) + assertEq(t, "Interface0Name", "Loopback0", iface0.Name) + assertEq(t, "Interface0InterfaceType", uint8(1), uint8(iface0.InterfaceType)) + assertEq(t, "Interface0LoopbackType", uint8(1), uint8(iface0.LoopbackType)) + assertEq(t, "Interface0VlanId", uint16(0), iface0.VlanId) + assertEq(t, "Interface0IpNet", "10.0.0.1/32", formatNetworkV4(iface0.IpNet)) + assertEq(t, "Interface0NodeSegmentIdx", uint16(100), iface0.NodeSegmentIdx) + assertEq(t, "Interface0UserTunnelEndpoint", false, iface0.UserTunnelEndpoint) + + iface1 := dev.Interfaces[1] + assertEq(t, "Interface1Version", uint8(1), iface1.Version) + assertEq(t, "Interface1Status", uint8(3), uint8(iface1.Status)) + assertEq(t, "Interface1Name", "Ethernet1", iface1.Name) + assertEq(t, "Interface1InterfaceType", uint8(2), uint8(iface1.InterfaceType)) + assertEq(t, "Interface1InterfaceCYOA", uint8(1), uint8(iface1.InterfaceCYOA)) + assertEq(t, "Interface1InterfaceDIA", uint8(1), uint8(iface1.InterfaceDIA)) + assertEq(t, "Interface1LoopbackType", uint8(0), uint8(iface1.LoopbackType)) + assertEq(t, "Interface1Bandwidth", uint64(10000000000), iface1.Bandwidth) + assertEq(t, "Interface1Cir", uint64(5000000000), iface1.Cir) + assertEq(t, "Interface1Mtu", uint16(9000), iface1.Mtu) + assertEq(t, "Interface1RoutingMode", uint8(1), uint8(iface1.RoutingMode)) + assertEq(t, "Interface1VlanId", uint16(100), iface1.VlanId) + assertEq(t, "Interface1IpNet", "172.16.0.1/30", formatNetworkV4(iface1.IpNet)) + assertEq(t, "Interface1NodeSegmentIdx", uint16(200), iface1.NodeSegmentIdx) + assertEq(t, "Interface1UserTunnelEndpoint", true, iface1.UserTunnelEndpoint) +} + +func TestFixtureLink(t *testing.T) { + data, meta := loadFixture(t, "link") + reader := NewByteReader(data) + var link Link + DeserializeLink(reader, &link) + + assertFields(t, meta.Fields, map[string]any{ + "AccountType": uint8(link.AccountType), + "Owner": solana.PublicKey(link.Owner), + "BumpSeed": link.BumpSeed, + "SideAPubKey": solana.PublicKey(link.SideAPubKey), + "SideZPubKey": solana.PublicKey(link.SideZPubKey), + "LinkType": uint8(link.LinkType), + "Bandwidth": link.Bandwidth, + "Mtu": link.Mtu, + "DelayNs": link.DelayNs, + "JitterNs": link.JitterNs, + "TunnelId": link.TunnelId, + "Status": uint8(link.Status), + "ContributorPubKey": solana.PublicKey(link.ContributorPubKey), + "DelayOverrideNs": link.DelayOverrideNs, + "LinkHealth": uint8(link.LinkHealth), + "LinkDesiredStatus": uint8(link.LinkDesiredStatus), + }) +} + +func TestFixtureUser(t *testing.T) { + data, meta := loadFixture(t, "user") + reader := NewByteReader(data) + var user User + DeserializeUser(reader, &user) + + assertFields(t, meta.Fields, map[string]any{ + "AccountType": uint8(user.AccountType), + "Owner": solana.PublicKey(user.Owner), + "BumpSeed": user.BumpSeed, + "UserType": uint8(user.UserType), + "TenantPubKey": solana.PublicKey(user.TenantPubKey), + "DevicePubKey": solana.PublicKey(user.DevicePubKey), + "CyoaType": uint8(user.CyoaType), + "TunnelId": user.TunnelId, + "Status": uint8(user.Status), + "ValidatorPubKey": solana.PublicKey(user.ValidatorPubKey), + }) +} + +func TestFixtureMulticastGroup(t *testing.T) { + data, meta := loadFixture(t, "multicast_group") + reader := NewByteReader(data) + var mg MulticastGroup + DeserializeMulticastGroup(reader, &mg) + + assertFields(t, meta.Fields, map[string]any{ + "AccountType": uint8(mg.AccountType), + "Owner": solana.PublicKey(mg.Owner), + "BumpSeed": mg.BumpSeed, + "TenantPubKey": solana.PublicKey(mg.TenantPubKey), + "MaxBandwidth": mg.MaxBandwidth, + "Status": uint8(mg.Status), + "PublisherCount": mg.PublisherCount, + "SubscriberCount": mg.SubscriberCount, + }) +} + +func TestFixtureContributor(t *testing.T) { + data, meta := loadFixture(t, "contributor") + reader := NewByteReader(data) + var contrib Contributor + DeserializeContributor(reader, &contrib) + + assertFields(t, meta.Fields, map[string]any{ + "AccountType": uint8(contrib.AccountType), + "Owner": solana.PublicKey(contrib.Owner), + "BumpSeed": contrib.BumpSeed, + "Status": uint8(contrib.Status), + "ReferenceCount": contrib.ReferenceCount, + "OpsManagerPK": solana.PublicKey(contrib.OpsManagerPK), + }) +} + +func TestFixtureProgramConfig(t *testing.T) { + data, meta := loadFixture(t, "program_config") + reader := NewByteReader(data) + var pc ProgramConfig + DeserializeProgramConfig(reader, &pc) + + assertFields(t, meta.Fields, map[string]any{ + "AccountType": uint8(pc.AccountType), + "BumpSeed": pc.BumpSeed, + "VersionMajor": pc.Version.Major, + "VersionMinor": pc.Version.Minor, + "VersionPatch": pc.Version.Patch, + }) +} + +func TestFixtureAccessPass(t *testing.T) { + data, meta := loadFixture(t, "access_pass") + reader := NewByteReader(data) + var ap AccessPass + DeserializeAccessPass(reader, &ap) + + assertFields(t, meta.Fields, map[string]any{ + "AccountType": uint8(ap.AccountType), + "Owner": solana.PublicKey(ap.Owner), + "BumpSeed": ap.BumpSeed, + "AccessPassType": uint8(ap.AccessPassTypeTag), + "UserPayer": solana.PublicKey(ap.UserPayer), + "LastAccessEpoch": ap.LastAccessEpoch, + "ConnectionCount": ap.ConnectionCount, + "Status": uint8(ap.Status), + "Flags": ap.Flags, + }) +} + +func TestFixtureAccessPassValidator(t *testing.T) { + data, meta := loadFixture(t, "access_pass_validator") + reader := NewByteReader(data) + var ap AccessPass + DeserializeAccessPass(reader, &ap) + + assertFields(t, meta.Fields, map[string]any{ + "AccountType": uint8(ap.AccountType), + "Owner": solana.PublicKey(ap.Owner), + "BumpSeed": ap.BumpSeed, + "AccessPassType": uint8(ap.AccessPassTypeTag), + "AccessPassTypeValidatorPubkey": solana.PublicKey(ap.ValidatorPubKey), + "ClientIp": ap.ClientIp, + "UserPayer": solana.PublicKey(ap.UserPayer), + "LastAccessEpoch": ap.LastAccessEpoch, + "ConnectionCount": ap.ConnectionCount, + "Status": uint8(ap.Status), + "Flags": ap.Flags, + }) + + // Verify MGroupPubAllowlist + if len(ap.MGroupPubAllowlist) != 1 { + t.Fatalf("MGroupPubAllowlist: want len 1, got %d", len(ap.MGroupPubAllowlist)) + } + assertEq(t, "MGroupPubAllowlist[0]", + solana.MustPublicKeyFromBase58("ByHTNjGkgHhNakbovFQmw3VGBb6e5rbnBPGk3naDV8mD"), + solana.PublicKey(ap.MGroupPubAllowlist[0])) + + // Verify MGroupSubAllowlist + if len(ap.MGroupSubAllowlist) != 1 { + t.Fatalf("MGroupSubAllowlist: want len 1, got %d", len(ap.MGroupSubAllowlist)) + } + assertEq(t, "MGroupSubAllowlist[0]", + solana.MustPublicKeyFromBase58("C3Bs2Dzqa8C5zSinRkgDpyEVSbfQnohgmFadYytDCwRZ"), + solana.PublicKey(ap.MGroupSubAllowlist[0])) +} + +func assertFields(t *testing.T, expected []fieldValue, got map[string]any) { + t.Helper() + for _, f := range expected { + val, ok := got[f.Name] + if !ok { + continue + } + assertField(t, f, val) + } +} + +func assertField(t *testing.T, f fieldValue, got any) { + t.Helper() + switch f.Type { + case "u8": + want, _ := strconv.ParseUint(f.Value, 10, 8) + assertEq(t, f.Name, uint8(want), got) + case "u16": + want, _ := strconv.ParseUint(f.Value, 10, 16) + assertEq(t, f.Name, uint16(want), got) + case "u32": + want, _ := strconv.ParseUint(f.Value, 10, 32) + assertEq(t, f.Name, uint32(want), got) + case "u64": + want, _ := strconv.ParseUint(f.Value, 10, 64) + assertEq(t, f.Name, uint64(want), got) + case "pubkey": + want := solana.MustPublicKeyFromBase58(f.Value) + assertEq(t, f.Name, want, got) + case "string": + assertEq(t, f.Name, f.Value, got) + case "bool": + want := f.Value == "true" + assertEq(t, f.Name, want, got) + case "ipv4": + ip := got.([4]uint8) + gotStr := fmt.Sprintf("%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]) + assertEq(t, f.Name, f.Value, gotStr) + case "networkv4": + net := got.([5]uint8) + gotStr := formatNetworkV4(net) + assertEq(t, f.Name, f.Value, gotStr) + case "u128": + want, _ := strconv.ParseUint(f.Value, 10, 64) + assertEq(t, f.Name, Uint128{Low: want, High: 0}, got) + default: + t.Errorf("field %s: unknown type %q", f.Name, f.Type) + } +} + +func formatNetworkV4(n [5]uint8) string { + return fmt.Sprintf("%d.%d.%d.%d/%d", n[0], n[1], n[2], n[3], n[4]) +} + +func assertEq(t *testing.T, name string, want, got any) { + t.Helper() + if !reflect.DeepEqual(want, got) { + t.Errorf("%s: want %v, got %v", name, want, fmt.Sprintf("%v", got)) + } +} diff --git a/sdk/serviceability/go/pda.go b/sdk/serviceability/go/pda.go new file mode 100644 index 000000000..3895a6db0 --- /dev/null +++ b/sdk/serviceability/go/pda.go @@ -0,0 +1,24 @@ +package serviceability + +import ( + "github.com/gagliardetto/solana-go" +) + +var ( + seedPrefix = []byte("doublezero") + seedGlobalState = []byte("globalstate") + seedGlobalConfig = []byte("config") + seedProgramConfig = []byte("programconfig") +) + +func DeriveGlobalStatePDA(programID solana.PublicKey) (solana.PublicKey, uint8, error) { + return solana.FindProgramAddress([][]byte{seedPrefix, seedGlobalState}, programID) +} + +func DeriveGlobalConfigPDA(programID solana.PublicKey) (solana.PublicKey, uint8, error) { + return solana.FindProgramAddress([][]byte{seedPrefix, seedGlobalConfig}, programID) +} + +func DeriveProgramConfigPDA(programID solana.PublicKey) (solana.PublicKey, uint8, error) { + return solana.FindProgramAddress([][]byte{seedPrefix, seedProgramConfig}, programID) +} diff --git a/sdk/serviceability/go/pda_test.go b/sdk/serviceability/go/pda_test.go new file mode 100644 index 000000000..1e6ebf5c3 --- /dev/null +++ b/sdk/serviceability/go/pda_test.go @@ -0,0 +1,59 @@ +package serviceability + +import ( + "testing" + + "github.com/gagliardetto/solana-go" +) + +var testProgramID = solana.MustPublicKeyFromBase58("ser2VaTMAcYTaauMrTSfSrxBaUDq7BLNs2xfUugTAGv") + +func TestDeriveGlobalStatePDA(t *testing.T) { + addr, bump, err := DeriveGlobalStatePDA(testProgramID) + if err != nil { + t.Fatalf("DeriveGlobalStatePDA: %v", err) + } + if addr.IsZero() { + t.Error("derived zero address") + } + if bump == 0 { + t.Log("bump is 0 (valid but unusual)") + } + + addr2, bump2, err := DeriveGlobalStatePDA(testProgramID) + if err != nil { + t.Fatalf("DeriveGlobalStatePDA (2nd): %v", err) + } + if addr != addr2 || bump != bump2 { + t.Error("PDA derivation not deterministic") + } +} + +func TestDeriveGlobalConfigPDA(t *testing.T) { + addr, _, err := DeriveGlobalConfigPDA(testProgramID) + if err != nil { + t.Fatalf("DeriveGlobalConfigPDA: %v", err) + } + if addr.IsZero() { + t.Error("derived zero address") + } +} + +func TestDeriveProgramConfigPDA(t *testing.T) { + addr, _, err := DeriveProgramConfigPDA(testProgramID) + if err != nil { + t.Fatalf("DeriveProgramConfigPDA: %v", err) + } + if addr.IsZero() { + t.Error("derived zero address") + } +} + +func TestPDAsAreDifferent(t *testing.T) { + gs, _, _ := DeriveGlobalStatePDA(testProgramID) + gc, _, _ := DeriveGlobalConfigPDA(testProgramID) + pc, _, _ := DeriveProgramConfigPDA(testProgramID) + if gs == gc || gs == pc || gc == pc { + t.Error("different PDA types produced same address") + } +} diff --git a/sdk/serviceability/go/rpc.go b/sdk/serviceability/go/rpc.go new file mode 100644 index 000000000..5452ed3fc --- /dev/null +++ b/sdk/serviceability/go/rpc.go @@ -0,0 +1,50 @@ +package serviceability + +import ( + "io" + "net/http" + "time" + + "github.com/gagliardetto/solana-go/rpc" + "github.com/gagliardetto/solana-go/rpc/jsonrpc" +) + +const defaultMaxRetries = 5 + +// retryHTTPClient wraps an http.Client and retries on 429 Too Many Requests. +type retryHTTPClient struct { + inner *http.Client + maxRetries int +} + +func (c *retryHTTPClient) Do(req *http.Request) (*http.Response, error) { + for attempt := 0; ; attempt++ { + resp, err := c.inner.Do(req) + if err != nil { + return resp, err + } + if resp.StatusCode != http.StatusTooManyRequests || attempt >= c.maxRetries { + return resp, nil + } + _, _ = io.Copy(io.Discard, resp.Body) + resp.Body.Close() + backoff := time.Duration(attempt+1) * 2 * time.Second + time.Sleep(backoff) + } +} + +func (c *retryHTTPClient) CloseIdleConnections() { + c.inner.CloseIdleConnections() +} + +// NewRPCClient creates a Solana RPC client with automatic retry on 429 responses. +func NewRPCClient(url string) *rpc.Client { + httpClient := &retryHTTPClient{ + inner: http.DefaultClient, + maxRetries: defaultMaxRetries, + } + rpcClient := jsonrpc.NewClientWithOpts(url, &jsonrpc.RPCClientOpts{ + HTTPClient: httpClient, + }) + return rpc.NewWithCustomRPCClient(rpcClient) +} diff --git a/sdk/serviceability/go/state.go b/sdk/serviceability/go/state.go new file mode 100644 index 000000000..75547b1e6 --- /dev/null +++ b/sdk/serviceability/go/state.go @@ -0,0 +1,857 @@ +package serviceability + +import ( + "encoding/json" + "fmt" + "net" + + "github.com/mr-tron/base58" +) + +type AccountType uint8 + +const ( + GlobalStateType AccountType = 1 + GlobalConfigType AccountType = 2 + LocationType AccountType = 3 + ExchangeType AccountType = 4 + DeviceType AccountType = 5 + LinkType AccountType = 6 + UserType AccountType = 7 + MulticastGroupType AccountType = 8 + ProgramConfigType AccountType = 9 + ContributorType AccountType = 10 + AccessPassType AccountType = 11 +) + +type LocationStatus uint8 + +const ( + LocationStatusPending LocationStatus = 0 + LocationStatusActivated LocationStatus = 1 + LocationStatusSuspended LocationStatus = 2 +) + +func (s LocationStatus) String() string { + switch s { + case LocationStatusPending: + return "pending" + case LocationStatusActivated: + return "activated" + case LocationStatusSuspended: + return "suspended" + default: + return "unknown" + } +} + +type ExchangeStatus uint8 + +const ( + ExchangeStatusPending ExchangeStatus = 0 + ExchangeStatusActivated ExchangeStatus = 1 + ExchangeStatusSuspended ExchangeStatus = 2 +) + +func (e ExchangeStatus) String() string { + switch e { + case ExchangeStatusPending: + return "pending" + case ExchangeStatusActivated: + return "activated" + case ExchangeStatusSuspended: + return "suspended" + default: + return "unknown" + } +} + +type DeviceDeviceType uint8 + +const ( + DeviceDeviceTypeHybrid DeviceDeviceType = 0 + DeviceDeviceTypeTransit DeviceDeviceType = 1 + DeviceDeviceTypeEdge DeviceDeviceType = 2 +) + +func (d DeviceDeviceType) String() string { + switch d { + case DeviceDeviceTypeHybrid: + return "hybrid" + case DeviceDeviceTypeTransit: + return "transit" + case DeviceDeviceTypeEdge: + return "edge" + default: + return "unknown" + } +} + +type DeviceStatus uint8 + +const ( + DeviceStatusPending DeviceStatus = 0 + DeviceStatusActivated DeviceStatus = 1 + DeviceStatusDeleting DeviceStatus = 2 + DeviceStatusRejected DeviceStatus = 3 + DeviceStatusDrained DeviceStatus = 4 + DeviceStatusDeviceProvisioning DeviceStatus = 5 + DeviceStatusLinkProvisioning DeviceStatus = 6 +) + +func (d DeviceStatus) String() string { + switch d { + case DeviceStatusPending: + return "pending" + case DeviceStatusActivated: + return "activated" + case DeviceStatusDeleting: + return "deleting" + case DeviceStatusRejected: + return "rejected" + case DeviceStatusDrained: + return "drained" + case DeviceStatusDeviceProvisioning: + return "device-provisioning" + case DeviceStatusLinkProvisioning: + return "link-provisioning" + default: + return "unknown" + } +} + +func (d DeviceStatus) MarshalJSON() ([]byte, error) { + return json.Marshal(d.String()) +} + +type DeviceHealth uint8 + +const ( + DeviceHealthUnknown DeviceHealth = 0 + DeviceHealthPending DeviceHealth = 1 + DeviceHealthReadyForLinks DeviceHealth = 2 + DeviceHealthReadyForUsers DeviceHealth = 3 + DeviceHealthImpaired DeviceHealth = 4 +) + +func (d DeviceHealth) String() string { + switch d { + case DeviceHealthUnknown: + return "unknown" + case DeviceHealthPending: + return "pending" + case DeviceHealthReadyForLinks: + return "ready_for_links" + case DeviceHealthReadyForUsers: + return "ready_for_users" + case DeviceHealthImpaired: + return "impaired" + default: + return "unknown" + } +} + +func (d DeviceHealth) MarshalJSON() ([]byte, error) { + return json.Marshal(d.String()) +} + +type DeviceDesiredStatus uint8 + +const ( + DeviceDesiredStatusPending DeviceDesiredStatus = 0 + DeviceDesiredStatusActivated DeviceDesiredStatus = 1 + DeviceDesiredStatusDrained DeviceDesiredStatus = 6 +) + +func (d DeviceDesiredStatus) String() string { + switch d { + case DeviceDesiredStatusPending: + return "pending" + case DeviceDesiredStatusActivated: + return "activated" + case DeviceDesiredStatusDrained: + return "drained" + default: + return "unknown" + } +} + +func (d DeviceDesiredStatus) MarshalJSON() ([]byte, error) { + return json.Marshal(d.String()) +} + +type InterfaceStatus uint8 + +const ( + InterfaceStatusInvalid InterfaceStatus = 0 + InterfaceStatusUnmanaged InterfaceStatus = 1 + InterfaceStatusPending InterfaceStatus = 2 + InterfaceStatusActivated InterfaceStatus = 3 + InterfaceStatusDeleting InterfaceStatus = 4 + InterfaceStatusRejecting InterfaceStatus = 5 + InterfaceStatusUnlinked InterfaceStatus = 6 +) + +func (i InterfaceStatus) String() string { + switch i { + case InterfaceStatusInvalid: + return "invalid" + case InterfaceStatusUnmanaged: + return "unmanaged" + case InterfaceStatusPending: + return "pending" + case InterfaceStatusActivated: + return "activated" + case InterfaceStatusDeleting: + return "deleting" + case InterfaceStatusRejecting: + return "rejecting" + case InterfaceStatusUnlinked: + return "unlinked" + default: + return "unknown" + } +} + +func (i InterfaceStatus) MarshalJSON() ([]byte, error) { + return json.Marshal(i.String()) +} + +type InterfaceType uint8 + +const ( + InterfaceTypeInvalid InterfaceType = 0 + InterfaceTypeLoopback InterfaceType = 1 + InterfaceTypePhysical InterfaceType = 2 +) + +func (i InterfaceType) String() string { + switch i { + case InterfaceTypeInvalid: + return "invalid" + case InterfaceTypeLoopback: + return "loopback" + case InterfaceTypePhysical: + return "physical" + default: + return "unknown" + } +} + +func (i InterfaceType) MarshalJSON() ([]byte, error) { + return json.Marshal(i.String()) +} + +type LoopbackType uint8 + +const ( + LoopbackTypeNone LoopbackType = 0 + LoopbackTypeVpnv4 LoopbackType = 1 + LoopbackTypeIpv4 LoopbackType = 2 + LoopbackTypePimRpAddr LoopbackType = 3 + LoopbackTypeReserved LoopbackType = 4 +) + +func (l LoopbackType) String() string { + switch l { + case LoopbackTypeNone: + return "none" + case LoopbackTypeVpnv4: + return "vpnv4" + case LoopbackTypeIpv4: + return "ipv4" + case LoopbackTypePimRpAddr: + return "pim_rp_addr" + case LoopbackTypeReserved: + return "reserved" + default: + return "unknown" + } +} + +func (l LoopbackType) MarshalJSON() ([]byte, error) { + return json.Marshal(l.String()) +} + +type InterfaceCYOA uint8 + +const ( + InterfaceCYOANone InterfaceCYOA = 0 + InterfaceCYOAGREOverDIA InterfaceCYOA = 1 + InterfaceCYOAGREOverFabric InterfaceCYOA = 2 + InterfaceCYOAGREOverPrivatePeer InterfaceCYOA = 3 + InterfaceCYOAGREOverPublicPeer InterfaceCYOA = 4 + InterfaceCYOAGREOverCable InterfaceCYOA = 5 +) + +type InterfaceDIA uint8 + +const ( + InterfaceDIANone InterfaceDIA = 0 + InterfaceDIADIA InterfaceDIA = 1 +) + +type RoutingMode uint8 + +const ( + RoutingModeStatic RoutingMode = 0 + RoutingModeBGP RoutingMode = 1 +) + +type Interface struct { + Version uint8 + Status InterfaceStatus + Name string + InterfaceType InterfaceType + InterfaceCYOA InterfaceCYOA + InterfaceDIA InterfaceDIA + LoopbackType LoopbackType + Bandwidth uint64 + Cir uint64 + Mtu uint16 + RoutingMode RoutingMode + VlanId uint16 + IpNet [5]uint8 + NodeSegmentIdx uint16 + UserTunnelEndpoint bool +} + +const CurrentInterfaceVersion = 2 + +type Uint128 struct { + Low uint64 + High uint64 +} + +type GlobalState struct { + AccountType AccountType + BumpSeed uint8 + AccountIndex Uint128 + FoundationAllowlist [][32]byte + ActivatorAuthorityPK [32]byte + SentinelAuthorityPK [32]byte + ContributorAirdropLamports uint64 + UserAirdropLamports uint64 + HealthOraclePK [32]byte + QAAllowlist [][32]byte + PubKey [32]byte +} + +type GlobalConfig struct { + AccountType AccountType + Owner [32]byte + BumpSeed uint8 + LocalASN uint32 + RemoteASN uint32 + DeviceTunnelBlock [5]uint8 + UserTunnelBlock [5]uint8 + MulticastGroupBlock [5]uint8 + NextBGPCommunity uint16 + PubKey [32]byte +} + +type Location struct { + AccountType AccountType + Owner [32]byte + Index Uint128 + BumpSeed uint8 + Lat float64 + Lng float64 + LocId uint32 + Status LocationStatus + Code string + Name string + Country string + ReferenceCount uint32 + PubKey [32]byte +} + +type Exchange struct { + AccountType AccountType + Owner [32]byte + Index Uint128 + BumpSeed uint8 + Lat float64 + Lng float64 + BgpCommunity uint16 + Status ExchangeStatus + Code string + Name string + ReferenceCount uint32 + Device1PK [32]byte + Device2PK [32]byte + PubKey [32]byte +} + +type Device struct { + AccountType AccountType + Owner [32]byte + Index Uint128 + BumpSeed uint8 + LocationPubKey [32]byte + ExchangePubKey [32]byte + DeviceType DeviceDeviceType + PublicIp [4]uint8 + Status DeviceStatus + Code string + DzPrefixes [][5]uint8 + MetricsPublisherPubKey [32]byte + ContributorPubKey [32]byte + MgmtVrf string + Interfaces []Interface + ReferenceCount uint32 + UsersCount uint16 + MaxUsers uint16 + DeviceHealth DeviceHealth + DeviceDesiredStatus DeviceDesiredStatus + PubKey [32]byte +} + +func (d Device) MarshalJSON() ([]byte, error) { + type DeviceAlias Device + + jsonDevice := &struct { + DeviceAlias + Owner string `json:"Owner"` + LocationPubKey string `json:"LocationPubKey"` + ExchangePubKey string `json:"ExchangePubKey"` + PublicIp string `json:"PublicIp"` + DzPrefixes []string `json:"DzPrefixes"` + MetricsPublisherPubKey string `json:"MetricsPublisherPubKey"` + ContributorPubKey string `json:"ContributorPubKey"` + PubKey string `json:"PubKey"` + Status string `json:"Status"` + DeviceHealth string `json:"DeviceHealth"` + DeviceDesiredStatus string `json:"DeviceDesiredStatus"` + }{ + DeviceAlias: DeviceAlias(d), + } + + jsonDevice.Owner = base58.Encode(d.Owner[:]) + jsonDevice.LocationPubKey = base58.Encode(d.LocationPubKey[:]) + jsonDevice.ExchangePubKey = base58.Encode(d.ExchangePubKey[:]) + jsonDevice.MetricsPublisherPubKey = base58.Encode(d.MetricsPublisherPubKey[:]) + jsonDevice.ContributorPubKey = base58.Encode(d.ContributorPubKey[:]) + jsonDevice.PubKey = base58.Encode(d.PubKey[:]) + jsonDevice.PublicIp = net.IP(d.PublicIp[:]).String() + + prefixes := make([]string, len(d.DzPrefixes)) + for i, p := range d.DzPrefixes { + prefixes[i] = networkV4ToString(p) + } + jsonDevice.DzPrefixes = prefixes + jsonDevice.Status = d.Status.String() + jsonDevice.DeviceHealth = d.DeviceHealth.String() + jsonDevice.DeviceDesiredStatus = d.DeviceDesiredStatus.String() + + return json.Marshal(jsonDevice) +} + +type LinkLinkType uint8 + +const ( + LinkLinkTypeWAN LinkLinkType = 1 + LinkLinkTypeDZX LinkLinkType = 127 +) + +func (l LinkLinkType) String() string { + switch l { + case LinkLinkTypeWAN: + return "WAN" + case LinkLinkTypeDZX: + return "DZX" + default: + return "" + } +} + +func (l LinkLinkType) MarshalJSON() ([]byte, error) { + return json.Marshal(l.String()) +} + +type LinkStatus uint8 + +const ( + LinkStatusPending LinkStatus = 0 + LinkStatusActivated LinkStatus = 1 + LinkStatusDeleting LinkStatus = 2 + LinkStatusRejected LinkStatus = 3 + LinkStatusRequested LinkStatus = 4 + LinkStatusHardDrained LinkStatus = 5 + LinkStatusSoftDrained LinkStatus = 6 + LinkStatusProvisioning LinkStatus = 7 +) + +func (l LinkStatus) String() string { + switch l { + case LinkStatusPending: + return "pending" + case LinkStatusActivated: + return "activated" + case LinkStatusDeleting: + return "deleting" + case LinkStatusRejected: + return "rejected" + case LinkStatusRequested: + return "requested" + case LinkStatusHardDrained: + return "hard-drained" + case LinkStatusSoftDrained: + return "soft-drained" + case LinkStatusProvisioning: + return "provisioning" + default: + return "unknown" + } +} + +func (l LinkStatus) MarshalJSON() ([]byte, error) { + return json.Marshal(l.String()) +} + +type LinkHealth uint8 + +const ( + LinkHealthUnknown LinkHealth = 0 + LinkHealthPending LinkHealth = 1 + LinkHealthReadyForService LinkHealth = 2 + LinkHealthImpaired LinkHealth = 3 +) + +func (l LinkHealth) String() string { + switch l { + case LinkHealthUnknown: + return "unknown" + case LinkHealthPending: + return "pending" + case LinkHealthReadyForService: + return "ready_for_service" + case LinkHealthImpaired: + return "impaired" + default: + return "unknown" + } +} + +func (l LinkHealth) MarshalJSON() ([]byte, error) { + return json.Marshal(l.String()) +} + +type LinkDesiredStatus uint8 + +const ( + LinkDesiredStatusPending LinkDesiredStatus = 0 + LinkDesiredStatusActivated LinkDesiredStatus = 1 + LinkDesiredStatusHardDrained LinkDesiredStatus = 2 + LinkDesiredStatusSoftDrained LinkDesiredStatus = 3 +) + +func (l LinkDesiredStatus) String() string { + switch l { + case LinkDesiredStatusPending: + return "pending" + case LinkDesiredStatusActivated: + return "activated" + case LinkDesiredStatusHardDrained: + return "hard-drained" + case LinkDesiredStatusSoftDrained: + return "soft-drained" + default: + return "unknown" + } +} + +func (l LinkDesiredStatus) MarshalJSON() ([]byte, error) { + return json.Marshal(l.String()) +} + +type Link struct { + AccountType AccountType + Owner [32]byte + Index Uint128 + BumpSeed uint8 + SideAPubKey [32]byte + SideZPubKey [32]byte + LinkType LinkLinkType + Bandwidth uint64 + Mtu uint32 + DelayNs uint64 + JitterNs uint64 + TunnelId uint16 + TunnelNet [5]uint8 + Status LinkStatus + Code string + ContributorPubKey [32]byte + SideAIfaceName string + SideZIfaceName string + DelayOverrideNs uint64 + LinkHealth LinkHealth + LinkDesiredStatus LinkDesiredStatus + PubKey [32]byte +} + +type ContributorStatus uint8 + +const ( + ContributorStatusNone ContributorStatus = 0 + ContributorStatusActivated ContributorStatus = 1 + ContributorStatusSuspended ContributorStatus = 2 + ContributorStatusDeleting ContributorStatus = 3 +) + +func (s ContributorStatus) String() string { + switch s { + case ContributorStatusNone: + return "none" + case ContributorStatusActivated: + return "activated" + case ContributorStatusSuspended: + return "suspended" + case ContributorStatusDeleting: + return "deleting" + default: + return "unknown" + } +} + +func (s ContributorStatus) MarshalJSON() ([]byte, error) { + return json.Marshal(s.String()) +} + +type Contributor struct { + AccountType AccountType + Owner [32]byte + Index Uint128 + BumpSeed uint8 + Status ContributorStatus + Code string + ReferenceCount uint32 + OpsManagerPK [32]byte + PubKey [32]byte +} + +type UserUserType uint8 + +const ( + UserTypeIBRL UserUserType = 0 + UserTypeIBRLWithAllocIP UserUserType = 1 + UserTypeEdgeFiltering UserUserType = 2 + UserTypeMulticast UserUserType = 3 +) + +func (u UserUserType) String() string { + switch u { + case UserTypeIBRL: + return "ibrl" + case UserTypeIBRLWithAllocIP: + return "ibrl_with_allocated_ip" + case UserTypeEdgeFiltering: + return "edge_filtering" + case UserTypeMulticast: + return "multicast" + default: + return "unknown" + } +} + +type CyoaType uint8 + +const ( + CyoaTypeNone CyoaType = 0 + CyoaTypeGREOverDIA CyoaType = 1 + CyoaTypeGREOverFabric CyoaType = 2 + CyoaTypeGREOverPrivatePeer CyoaType = 3 + CyoaTypeGREOverPublicPeer CyoaType = 4 + CyoaTypeGREOverCable CyoaType = 5 +) + +func (c CyoaType) String() string { + switch c { + case CyoaTypeNone: + return "none" + case CyoaTypeGREOverDIA: + return "gre_over_dia" + case CyoaTypeGREOverFabric: + return "gre_over_fabric" + case CyoaTypeGREOverPrivatePeer: + return "gre_over_private_peering" + case CyoaTypeGREOverPublicPeer: + return "gre_over_public_peering" + case CyoaTypeGREOverCable: + return "gre_over_cable" + default: + return "unknown" + } +} + +type UserStatus uint8 + +const ( + UserStatusPending UserStatus = 0 + UserStatusActivated UserStatus = 1 + UserStatusDeleting UserStatus = 3 + UserStatusRejected UserStatus = 4 + UserStatusPendingBan UserStatus = 5 + UserStatusBanned UserStatus = 6 + UserStatusUpdating UserStatus = 7 + UserStatusOutOfCredits UserStatus = 8 +) + +func (u UserStatus) String() string { + switch u { + case UserStatusPending: + return "pending" + case UserStatusActivated: + return "activated" + case UserStatusDeleting: + return "deleting" + case UserStatusRejected: + return "rejected" + case UserStatusPendingBan: + return "pending_ban" + case UserStatusBanned: + return "banned" + case UserStatusUpdating: + return "updating" + case UserStatusOutOfCredits: + return "out_of_credits" + default: + return "unknown" + } +} + +func (u UserStatus) MarshalJSON() ([]byte, error) { + return json.Marshal(u.String()) +} + +type User struct { + AccountType AccountType + Owner [32]byte + Index Uint128 + BumpSeed uint8 + UserType UserUserType + TenantPubKey [32]byte + DevicePubKey [32]byte + CyoaType CyoaType + ClientIp [4]uint8 + DzIp [4]uint8 + TunnelId uint16 + TunnelNet [5]uint8 + Status UserStatus + Publishers [][32]byte + Subscribers [][32]byte + ValidatorPubKey [32]byte + PubKey [32]byte +} + +type MulticastGroupStatus uint8 + +const ( + MulticastGroupStatusPending MulticastGroupStatus = 0 + MulticastGroupStatusActivated MulticastGroupStatus = 1 + MulticastGroupStatusSuspended MulticastGroupStatus = 2 + MulticastGroupStatusDeleting MulticastGroupStatus = 3 + MulticastGroupStatusRejected MulticastGroupStatus = 4 +) + +func (s MulticastGroupStatus) String() string { + switch s { + case MulticastGroupStatusPending: + return "pending" + case MulticastGroupStatusActivated: + return "activated" + case MulticastGroupStatusSuspended: + return "suspended" + case MulticastGroupStatusDeleting: + return "deleting" + case MulticastGroupStatusRejected: + return "rejected" + default: + return "unknown" + } +} + +type MulticastGroup struct { + AccountType AccountType + Owner [32]byte + Index Uint128 + BumpSeed uint8 + TenantPubKey [32]byte + MulticastIp [4]uint8 + MaxBandwidth uint64 + Status MulticastGroupStatus + Code string + PublisherCount uint32 + SubscriberCount uint32 + PubKey [32]byte +} + +type ProgramVersion struct { + Major uint32 + Minor uint32 + Patch uint32 +} + +type ProgramConfig struct { + AccountType AccountType + BumpSeed uint8 + Version ProgramVersion + MinCompatVersion ProgramVersion +} + +type AccessPassTypeTag uint8 + +const ( + AccessPassTypePrepaid AccessPassTypeTag = 0 + AccessPassTypeSolanaValidator AccessPassTypeTag = 1 +) + +type AccessPassStatus uint8 + +const ( + AccessPassStatusRequested AccessPassStatus = 0 + AccessPassStatusConnected AccessPassStatus = 1 + AccessPassStatusDisconnected AccessPassStatus = 2 + AccessPassStatusExpired AccessPassStatus = 3 +) + +func (s AccessPassStatus) String() string { + switch s { + case AccessPassStatusRequested: + return "requested" + case AccessPassStatusConnected: + return "connected" + case AccessPassStatusDisconnected: + return "disconnected" + case AccessPassStatusExpired: + return "expired" + default: + return "unknown" + } +} + +type AccessPass struct { + AccountType AccountType + Owner [32]byte + BumpSeed uint8 + AccessPassTypeTag AccessPassTypeTag + ValidatorPubKey [32]byte // only present if SolanaValidator + ClientIp [4]uint8 + UserPayer [32]byte + LastAccessEpoch uint64 + ConnectionCount uint16 + Status AccessPassStatus + MGroupPubAllowlist [][32]byte + MGroupSubAllowlist [][32]byte + Flags uint8 + PubKey [32]byte +} + +func networkV4ToString(n [5]uint8) string { + prefixLen := n[4] + if prefixLen > 0 && prefixLen <= 32 { + ip := net.IP(n[:4]) + return fmt.Sprintf("%s/%d", ip.String(), prefixLen) + } + return "" +} diff --git a/sdk/serviceability/python/pyproject.toml b/sdk/serviceability/python/pyproject.toml new file mode 100644 index 000000000..c0846ecd9 --- /dev/null +++ b/sdk/serviceability/python/pyproject.toml @@ -0,0 +1,29 @@ +[project] +name = "doublezero-serviceability" +version = "0.0.1" +description = "DoubleZero Serviceability SDK" +requires-python = ">=3.10" +dependencies = [ + "doublezero-borsh-incremental", + "solana>=0.35", + "solders>=0.21", + "httpx>=0.27", +] + +[tool.pytest.ini_options] +testpaths = ["serviceability/tests"] + +[tool.hatch.build.targets.wheel] +packages = ["serviceability"] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[dependency-groups] +dev = [ + "pytest>=9.0.2", +] + +[tool.uv.sources] +doublezero-borsh-incremental = { path = "../../borsh-incremental/python", editable = true } diff --git a/sdk/serviceability/python/serviceability/__init__.py b/sdk/serviceability/python/serviceability/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sdk/serviceability/python/serviceability/client.py b/sdk/serviceability/python/serviceability/client.py new file mode 100644 index 000000000..10dd53b20 --- /dev/null +++ b/sdk/serviceability/python/serviceability/client.py @@ -0,0 +1,85 @@ +"""RPC client for fetching serviceability program accounts.""" + +from __future__ import annotations + +from typing import Protocol + +from solders.pubkey import Pubkey # type: ignore[import-untyped] +from solders.rpc.responses import GetAccountInfoResp # type: ignore[import-untyped] + +from serviceability.config import PROGRAM_IDS, LEDGER_RPC_URLS +from serviceability.rpc import new_rpc_client +from serviceability.state import ( + AccessPass, + Contributor, + Device, + Exchange, + GlobalConfig, + GlobalState, + Link, + Location, + MulticastGroup, + ProgramConfig, + User, +) + + +class SolanaClient(Protocol): + def get_account_info(self, pubkey: Pubkey) -> GetAccountInfoResp: ... + + +class ProgramData: + """Aggregate of all serviceability program accounts.""" + + def __init__(self) -> None: + self.global_state: GlobalState | None = None + self.global_config: GlobalConfig | None = None + self.program_config: ProgramConfig | None = None + self.locations: list[Location] = [] + self.exchanges: list[Exchange] = [] + self.devices: list[Device] = [] + self.links: list[Link] = [] + self.users: list[User] = [] + self.multicast_groups: list[MulticastGroup] = [] + self.contributors: list[Contributor] = [] + self.access_passes: list[AccessPass] = [] + + +class Client: + """Read-only client for serviceability program accounts.""" + + def __init__( + self, + solana_rpc: SolanaClient, + program_id: Pubkey, + ) -> None: + self._solana_rpc = solana_rpc + self._program_id = program_id + + @classmethod + def from_env(cls, env: str) -> Client: + """Create a client configured for the given environment. + + Args: + env: Environment name ("mainnet-beta", "testnet", "devnet", "localnet") + """ + return cls( + new_rpc_client(LEDGER_RPC_URLS[env]), + Pubkey.from_string(PROGRAM_IDS[env]), + ) + + @classmethod + def mainnet_beta(cls) -> Client: + return cls.from_env("mainnet-beta") + + @classmethod + def testnet(cls) -> Client: + return cls.from_env("testnet") + + @classmethod + def devnet(cls) -> Client: + return cls.from_env("devnet") + + @classmethod + def localnet(cls) -> Client: + return cls.from_env("localnet") diff --git a/sdk/serviceability/python/serviceability/config.py b/sdk/serviceability/python/serviceability/config.py new file mode 100644 index 000000000..6a24b3f74 --- /dev/null +++ b/sdk/serviceability/python/serviceability/config.py @@ -0,0 +1,15 @@ +"""Network configuration for the serviceability program.""" + +PROGRAM_IDS = { + "mainnet-beta": "ser2VaTMAcYTaauMrTSfSrxBaUDq7BLNs2xfUugTAGv", + "testnet": "DZtnuQ839pSaDMFG5q1ad2V95G82S5EC4RrB3Ndw2Heb", + "devnet": "GYhQDKuESrasNZGyhMJhGYFtbzNijYhcrN9poSqCQVah", + "localnet": "7CTniUa88iJKUHTrCkB4TjAoG6TD7AMivhQeuqN2LPtX", +} + +LEDGER_RPC_URLS = { + "mainnet-beta": "https://doublezero-mainnet-beta.rpcpool.com/db336024-e7a8-46b1-80e5-352dd77060ab", + "testnet": "https://doublezerolocalnet.rpcpool.com/8a4fd3f4-0977-449f-88c7-63d4b0f10f16", + "devnet": "https://doublezerolocalnet.rpcpool.com/8a4fd3f4-0977-449f-88c7-63d4b0f10f16", + "localnet": "http://localhost:8899", +} diff --git a/sdk/serviceability/python/serviceability/pda.py b/sdk/serviceability/python/serviceability/pda.py new file mode 100644 index 000000000..9968c6086 --- /dev/null +++ b/sdk/serviceability/python/serviceability/pda.py @@ -0,0 +1,20 @@ +"""PDA derivation for serviceability program accounts.""" + +from solders.pubkey import Pubkey # type: ignore[import-untyped] + +SEED_PREFIX = b"doublezero" +SEED_GLOBAL_STATE = b"globalstate" +SEED_GLOBAL_CONFIG = b"config" +SEED_PROGRAM_CONFIG = b"programconfig" + + +def derive_global_state_pda(program_id: Pubkey) -> tuple[Pubkey, int]: + return Pubkey.find_program_address([SEED_PREFIX, SEED_GLOBAL_STATE], program_id) + + +def derive_global_config_pda(program_id: Pubkey) -> tuple[Pubkey, int]: + return Pubkey.find_program_address([SEED_PREFIX, SEED_GLOBAL_CONFIG], program_id) + + +def derive_program_config_pda(program_id: Pubkey) -> tuple[Pubkey, int]: + return Pubkey.find_program_address([SEED_PREFIX, SEED_PROGRAM_CONFIG], program_id) diff --git a/sdk/serviceability/python/serviceability/rpc.py b/sdk/serviceability/python/serviceability/rpc.py new file mode 100644 index 000000000..601c786b3 --- /dev/null +++ b/sdk/serviceability/python/serviceability/rpc.py @@ -0,0 +1,48 @@ +"""RPC client helpers with retry on rate limiting.""" + +import time + +import httpx +from solana.rpc.api import Client as SolanaHTTPClient # type: ignore[import-untyped] + +_DEFAULT_MAX_RETRIES = 5 + + +class _RetryTransport(httpx.BaseTransport): + """HTTP transport that retries on 429 Too Many Requests.""" + + def __init__( + self, + wrapped: httpx.BaseTransport | None = None, + max_retries: int = _DEFAULT_MAX_RETRIES, + ) -> None: + self._wrapped = wrapped or httpx.HTTPTransport() + self._max_retries = max_retries + + def handle_request(self, request: httpx.Request) -> httpx.Response: + for attempt in range(self._max_retries + 1): + response = self._wrapped.handle_request(request) + if response.status_code != 429 or attempt >= self._max_retries: + return response + response.close() + time.sleep((attempt + 1) * 2) + return response # unreachable, but satisfies type checker + + +def new_rpc_client( + url: str, + timeout: float = 30, + max_retries: int = _DEFAULT_MAX_RETRIES, +) -> SolanaHTTPClient: + """Create a Solana RPC client with automatic retry on 429 responses.""" + client = SolanaHTTPClient(url, timeout=timeout) + # Replace the underlying httpx session with one using retry transport. + transport = _RetryTransport( + wrapped=httpx.HTTPTransport(), + max_retries=max_retries, + ) + client._provider.session = httpx.Client( + timeout=timeout, + transport=transport, + ) + return client diff --git a/sdk/serviceability/python/serviceability/state.py b/sdk/serviceability/python/serviceability/state.py new file mode 100644 index 000000000..b7ccda1d0 --- /dev/null +++ b/sdk/serviceability/python/serviceability/state.py @@ -0,0 +1,832 @@ +"""On-chain account data structures for the serviceability program. + +Binary layout uses Borsh serialization with a 1-byte AccountType discriminator +as the first byte. Deserialization uses cursor-based IncrementalReader from +borsh_incremental. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from enum import IntEnum + +from borsh_incremental import IncrementalReader +from solders.pubkey import Pubkey # type: ignore[import-untyped] + + +def _read_pubkey(r: IncrementalReader) -> Pubkey: + return Pubkey.from_bytes(r.read_pubkey_raw()) + + +def _read_pubkey_vec(r: IncrementalReader) -> list[Pubkey]: + raw = r.read_pubkey_raw_vec() + return [Pubkey.from_bytes(b) for b in raw] + + +def _try_read_pubkey_vec(r: IncrementalReader) -> list[Pubkey]: + raw = r.try_read_pubkey_raw_vec([]) + return [Pubkey.from_bytes(b) for b in raw] + + +# --------------------------------------------------------------------------- +# Account type discriminants +# --------------------------------------------------------------------------- + + +class AccountTypeEnum(IntEnum): + GLOBAL_STATE = 1 + GLOBAL_CONFIG = 2 + LOCATION = 3 + EXCHANGE = 4 + DEVICE = 5 + LINK = 6 + USER = 7 + MULTICAST_GROUP = 8 + PROGRAM_CONFIG = 9 + CONTRIBUTOR = 10 + ACCESS_PASS = 11 + + +# --------------------------------------------------------------------------- +# Status / type enums +# --------------------------------------------------------------------------- + + +class LocationStatus(IntEnum): + PENDING = 0 + ACTIVATED = 1 + SUSPENDED = 2 + + def __str__(self) -> str: + _names = {0: "pending", 1: "activated", 2: "suspended"} + return _names.get(self.value, "unknown") + + +class ExchangeStatus(IntEnum): + PENDING = 0 + ACTIVATED = 1 + SUSPENDED = 2 + + def __str__(self) -> str: + _names = {0: "pending", 1: "activated", 2: "suspended"} + return _names.get(self.value, "unknown") + + +class DeviceDeviceType(IntEnum): + HYBRID = 0 + TRANSIT = 1 + EDGE = 2 + + def __str__(self) -> str: + _names = {0: "hybrid", 1: "transit", 2: "edge"} + return _names.get(self.value, "unknown") + + +class DeviceStatus(IntEnum): + PENDING = 0 + ACTIVATED = 1 + DELETING = 2 + REJECTED = 3 + DRAINED = 4 + DEVICE_PROVISIONING = 5 + LINK_PROVISIONING = 6 + + def __str__(self) -> str: + _names = { + 0: "pending", + 1: "activated", + 2: "deleting", + 3: "rejected", + 4: "drained", + 5: "device-provisioning", + 6: "link-provisioning", + } + return _names.get(self.value, "unknown") + + +class DeviceHealth(IntEnum): + UNKNOWN = 0 + PENDING = 1 + READY_FOR_LINKS = 2 + READY_FOR_USERS = 3 + IMPAIRED = 4 + + def __str__(self) -> str: + _names = { + 0: "unknown", + 1: "pending", + 2: "ready_for_links", + 3: "ready_for_users", + 4: "impaired", + } + return _names.get(self.value, "unknown") + + +class DeviceDesiredStatus(IntEnum): + PENDING = 0 + ACTIVATED = 1 + DRAINED = 6 + + def __str__(self) -> str: + _names = {0: "pending", 1: "activated", 6: "drained"} + return _names.get(self.value, "unknown") + + +class InterfaceStatus(IntEnum): + INVALID = 0 + UNMANAGED = 1 + PENDING = 2 + ACTIVATED = 3 + DELETING = 4 + REJECTING = 5 + UNLINKED = 6 + + def __str__(self) -> str: + _names = { + 0: "invalid", + 1: "unmanaged", + 2: "pending", + 3: "activated", + 4: "deleting", + 5: "rejecting", + 6: "unlinked", + } + return _names.get(self.value, "unknown") + + +class InterfaceType(IntEnum): + INVALID = 0 + LOOPBACK = 1 + PHYSICAL = 2 + + def __str__(self) -> str: + _names = {0: "invalid", 1: "loopback", 2: "physical"} + return _names.get(self.value, "unknown") + + +class LoopbackType(IntEnum): + NONE = 0 + VPNV4 = 1 + IPV4 = 2 + PIM_RP_ADDR = 3 + RESERVED = 4 + + def __str__(self) -> str: + _names = {0: "none", 1: "vpnv4", 2: "ipv4", 3: "pim_rp_addr", 4: "reserved"} + return _names.get(self.value, "unknown") + + +class InterfaceCYOA(IntEnum): + NONE = 0 + GRE_OVER_DIA = 1 + GRE_OVER_FABRIC = 2 + GRE_OVER_PRIVATE_PEER = 3 + GRE_OVER_PUBLIC_PEER = 4 + GRE_OVER_CABLE = 5 + + def __str__(self) -> str: + _names = { + 0: "none", + 1: "gre_over_dia", + 2: "gre_over_fabric", + 3: "gre_over_private_peering", + 4: "gre_over_public_peering", + 5: "gre_over_cable", + } + return _names.get(self.value, "unknown") + + +class InterfaceDIA(IntEnum): + NONE = 0 + DIA = 1 + + def __str__(self) -> str: + _names = {0: "none", 1: "dia"} + return _names.get(self.value, "unknown") + + +class RoutingMode(IntEnum): + STATIC = 0 + BGP = 1 + + def __str__(self) -> str: + _names = {0: "static", 1: "bgp"} + return _names.get(self.value, "unknown") + + +class LinkLinkType(IntEnum): + WAN = 1 + DZX = 127 + + def __str__(self) -> str: + _names = {1: "WAN", 127: "DZX"} + return _names.get(self.value, "") + + +class LinkStatus(IntEnum): + PENDING = 0 + ACTIVATED = 1 + DELETING = 2 + REJECTED = 3 + REQUESTED = 4 + HARD_DRAINED = 5 + SOFT_DRAINED = 6 + PROVISIONING = 7 + + def __str__(self) -> str: + _names = { + 0: "pending", + 1: "activated", + 2: "deleting", + 3: "rejected", + 4: "requested", + 5: "hard-drained", + 6: "soft-drained", + 7: "provisioning", + } + return _names.get(self.value, "unknown") + + +class LinkHealth(IntEnum): + UNKNOWN = 0 + PENDING = 1 + READY_FOR_SERVICE = 2 + IMPAIRED = 3 + + def __str__(self) -> str: + _names = {0: "unknown", 1: "pending", 2: "ready_for_service", 3: "impaired"} + return _names.get(self.value, "unknown") + + +class LinkDesiredStatus(IntEnum): + PENDING = 0 + ACTIVATED = 1 + HARD_DRAINED = 2 + SOFT_DRAINED = 3 + + def __str__(self) -> str: + _names = {0: "pending", 1: "activated", 2: "hard-drained", 3: "soft-drained"} + return _names.get(self.value, "unknown") + + +class ContributorStatus(IntEnum): + NONE = 0 + ACTIVATED = 1 + SUSPENDED = 2 + DELETING = 3 + + def __str__(self) -> str: + _names = {0: "none", 1: "activated", 2: "suspended", 3: "deleting"} + return _names.get(self.value, "unknown") + + +class UserUserType(IntEnum): + IBRL = 0 + IBRL_WITH_ALLOC_IP = 1 + EDGE_FILTERING = 2 + MULTICAST = 3 + + def __str__(self) -> str: + _names = { + 0: "ibrl", + 1: "ibrl_with_allocated_ip", + 2: "edge_filtering", + 3: "multicast", + } + return _names.get(self.value, "unknown") + + +class CyoaType(IntEnum): + NONE = 0 + GRE_OVER_DIA = 1 + GRE_OVER_FABRIC = 2 + GRE_OVER_PRIVATE_PEER = 3 + GRE_OVER_PUBLIC_PEER = 4 + GRE_OVER_CABLE = 5 + + def __str__(self) -> str: + _names = { + 0: "none", + 1: "gre_over_dia", + 2: "gre_over_fabric", + 3: "gre_over_private_peering", + 4: "gre_over_public_peering", + 5: "gre_over_cable", + } + return _names.get(self.value, "unknown") + + +class UserStatus(IntEnum): + PENDING = 0 + ACTIVATED = 1 + DELETING = 3 + REJECTED = 4 + PENDING_BAN = 5 + BANNED = 6 + UPDATING = 7 + OUT_OF_CREDITS = 8 + + def __str__(self) -> str: + _names = { + 0: "pending", + 1: "activated", + 3: "deleting", + 4: "rejected", + 5: "pending_ban", + 6: "banned", + 7: "updating", + 8: "out_of_credits", + } + return _names.get(self.value, "unknown") + + +class MulticastGroupStatus(IntEnum): + PENDING = 0 + ACTIVATED = 1 + SUSPENDED = 2 + DELETING = 3 + REJECTED = 4 + + def __str__(self) -> str: + _names = { + 0: "pending", + 1: "activated", + 2: "suspended", + 3: "deleting", + 4: "rejected", + } + return _names.get(self.value, "unknown") + + +class AccessPassTypeTag(IntEnum): + PREPAID = 0 + SOLANA_VALIDATOR = 1 + + def __str__(self) -> str: + _names = {0: "prepaid", 1: "solana_validator"} + return _names.get(self.value, "unknown") + + +class AccessPassStatus(IntEnum): + REQUESTED = 0 + CONNECTED = 1 + DISCONNECTED = 2 + EXPIRED = 3 + + def __str__(self) -> str: + _names = {0: "requested", 1: "connected", 2: "disconnected", 3: "expired"} + return _names.get(self.value, "unknown") + + +# --------------------------------------------------------------------------- +# Account dataclasses +# --------------------------------------------------------------------------- + +CURRENT_INTERFACE_VERSION = 2 + + +@dataclass +class Interface: + version: int = 0 + status: InterfaceStatus = InterfaceStatus.INVALID + name: str = "" + interface_type: InterfaceType = InterfaceType.INVALID + interface_cyoa: InterfaceCYOA = InterfaceCYOA.NONE + interface_dia: InterfaceDIA = InterfaceDIA.NONE + loopback_type: LoopbackType = LoopbackType.NONE + bandwidth: int = 0 + cir: int = 0 + mtu: int = 0 + routing_mode: RoutingMode = RoutingMode.STATIC + vlan_id: int = 0 + ip_net: bytes = b"\x00" * 5 + node_segment_idx: int = 0 + user_tunnel_endpoint: bool = False + + @classmethod + def from_reader(cls, r: IncrementalReader) -> Interface: + iface = cls() + iface.version = r.read_u8() + if iface.version > CURRENT_INTERFACE_VERSION - 1: + return iface + if iface.version == 0: # V1 + iface.status = InterfaceStatus(r.read_u8()) + iface.name = r.read_string() + iface.interface_type = InterfaceType(r.read_u8()) + iface.loopback_type = LoopbackType(r.read_u8()) + iface.vlan_id = r.read_u16() + iface.ip_net = r.read_network_v4() + iface.node_segment_idx = r.read_u16() + iface.user_tunnel_endpoint = r.read_bool() + elif iface.version == 1: # V2 + iface.status = InterfaceStatus(r.read_u8()) + iface.name = r.read_string() + iface.interface_type = InterfaceType(r.read_u8()) + iface.interface_cyoa = InterfaceCYOA(r.read_u8()) + iface.interface_dia = InterfaceDIA(r.read_u8()) + iface.loopback_type = LoopbackType(r.read_u8()) + iface.bandwidth = r.read_u64() + iface.cir = r.read_u64() + iface.mtu = r.read_u16() + iface.routing_mode = RoutingMode(r.read_u8()) + iface.vlan_id = r.read_u16() + iface.ip_net = r.read_network_v4() + iface.node_segment_idx = r.read_u16() + iface.user_tunnel_endpoint = r.read_bool() + return iface + + +@dataclass +class GlobalState: + account_type: int = 0 + bump_seed: int = 0 + account_index: int = 0 + foundation_allowlist: list[Pubkey] = field(default_factory=list) + activator_authority_pk: Pubkey = Pubkey.default() + sentinel_authority_pk: Pubkey = Pubkey.default() + contributor_airdrop_lamports: int = 0 + user_airdrop_lamports: int = 0 + health_oracle_pk: Pubkey = Pubkey.default() + qa_allowlist: list[Pubkey] = field(default_factory=list) + + @classmethod + def from_bytes(cls, data: bytes) -> GlobalState: + r = IncrementalReader(data) + gs = cls() + gs.account_type = r.read_u8() + gs.bump_seed = r.read_u8() + gs.account_index = r.read_u128() + gs.foundation_allowlist = _read_pubkey_vec(r) + _read_pubkey_vec(r) # deprecated device_allowlist + _read_pubkey_vec(r) # deprecated user_allowlist + gs.activator_authority_pk = _read_pubkey(r) + gs.sentinel_authority_pk = _read_pubkey(r) + gs.contributor_airdrop_lamports = r.read_u64() + gs.user_airdrop_lamports = r.read_u64() + gs.health_oracle_pk = _read_pubkey(r) + gs.qa_allowlist = _try_read_pubkey_vec(r) + return gs + + +@dataclass +class GlobalConfig: + account_type: int = 0 + owner: Pubkey = Pubkey.default() + bump_seed: int = 0 + local_asn: int = 0 + remote_asn: int = 0 + device_tunnel_block: bytes = b"\x00" * 5 + user_tunnel_block: bytes = b"\x00" * 5 + multicast_group_block: bytes = b"\x00" * 5 + next_bgp_community: int = 0 + + @classmethod + def from_bytes(cls, data: bytes) -> GlobalConfig: + r = IncrementalReader(data) + gc = cls() + gc.account_type = r.read_u8() + gc.owner = _read_pubkey(r) + gc.bump_seed = r.read_u8() + gc.local_asn = r.read_u32() + gc.remote_asn = r.read_u32() + gc.device_tunnel_block = r.read_network_v4() + gc.user_tunnel_block = r.read_network_v4() + gc.multicast_group_block = r.read_network_v4() + gc.next_bgp_community = r.read_u16() + return gc + + +@dataclass +class Location: + account_type: int = 0 + owner: Pubkey = Pubkey.default() + index: int = 0 + bump_seed: int = 0 + lat: float = 0.0 + lng: float = 0.0 + loc_id: int = 0 + status: LocationStatus = LocationStatus.PENDING + code: str = "" + name: str = "" + country: str = "" + reference_count: int = 0 + + @classmethod + def from_bytes(cls, data: bytes) -> Location: + r = IncrementalReader(data) + loc = cls() + loc.account_type = r.read_u8() + loc.owner = _read_pubkey(r) + loc.index = r.read_u128() + loc.bump_seed = r.read_u8() + loc.lat = r.read_f64() + loc.lng = r.read_f64() + loc.loc_id = r.read_u32() + loc.status = LocationStatus(r.read_u8()) + loc.code = r.read_string() + loc.name = r.read_string() + loc.country = r.read_string() + loc.reference_count = r.read_u32() + return loc + + +@dataclass +class Exchange: + account_type: int = 0 + owner: Pubkey = Pubkey.default() + index: int = 0 + bump_seed: int = 0 + lat: float = 0.0 + lng: float = 0.0 + bgp_community: int = 0 + status: ExchangeStatus = ExchangeStatus.PENDING + code: str = "" + name: str = "" + reference_count: int = 0 + device1_pk: Pubkey = Pubkey.default() + device2_pk: Pubkey = Pubkey.default() + + @classmethod + def from_bytes(cls, data: bytes) -> Exchange: + r = IncrementalReader(data) + ex = cls() + ex.account_type = r.read_u8() + ex.owner = _read_pubkey(r) + ex.index = r.read_u128() + ex.bump_seed = r.read_u8() + ex.lat = r.read_f64() + ex.lng = r.read_f64() + ex.bgp_community = r.read_u16() + r.read_u16() # reserved padding + ex.status = ExchangeStatus(r.read_u8()) + ex.code = r.read_string() + ex.name = r.read_string() + ex.reference_count = r.read_u32() + ex.device1_pk = _read_pubkey(r) + ex.device2_pk = _read_pubkey(r) + return ex + + +@dataclass +class Device: + account_type: int = 0 + owner: Pubkey = Pubkey.default() + index: int = 0 + bump_seed: int = 0 + location_pub_key: Pubkey = Pubkey.default() + exchange_pub_key: Pubkey = Pubkey.default() + device_type: DeviceDeviceType = DeviceDeviceType.HYBRID + public_ip: bytes = b"\x00" * 4 + status: DeviceStatus = DeviceStatus.PENDING + code: str = "" + dz_prefixes: list[bytes] = field(default_factory=list) + metrics_publisher_pub_key: Pubkey = Pubkey.default() + contributor_pub_key: Pubkey = Pubkey.default() + mgmt_vrf: str = "" + interfaces: list[Interface] = field(default_factory=list) + reference_count: int = 0 + users_count: int = 0 + max_users: int = 0 + device_health: DeviceHealth = DeviceHealth.UNKNOWN + device_desired_status: DeviceDesiredStatus = DeviceDesiredStatus.PENDING + + @classmethod + def from_bytes(cls, data: bytes) -> Device: + r = IncrementalReader(data) + dev = cls() + dev.account_type = r.read_u8() + dev.owner = _read_pubkey(r) + dev.index = r.read_u128() + dev.bump_seed = r.read_u8() + dev.location_pub_key = _read_pubkey(r) + dev.exchange_pub_key = _read_pubkey(r) + dev.device_type = DeviceDeviceType(r.read_u8()) + dev.public_ip = r.read_ipv4() + dev.status = DeviceStatus(r.read_u8()) + dev.code = r.read_string() + dev.dz_prefixes = r.read_network_v4_vec() + dev.metrics_publisher_pub_key = _read_pubkey(r) + dev.contributor_pub_key = _read_pubkey(r) + dev.mgmt_vrf = r.read_string() + iface_len = r.read_u32() + dev.interfaces = [Interface.from_reader(r) for _ in range(iface_len)] + dev.reference_count = r.read_u32() + dev.users_count = r.read_u16() + dev.max_users = r.read_u16() + dev.device_health = DeviceHealth(r.read_u8()) + dev.device_desired_status = DeviceDesiredStatus(r.read_u8()) + return dev + + +@dataclass +class Link: + account_type: int = 0 + owner: Pubkey = Pubkey.default() + index: int = 0 + bump_seed: int = 0 + side_a_pub_key: Pubkey = Pubkey.default() + side_z_pub_key: Pubkey = Pubkey.default() + link_type: LinkLinkType = LinkLinkType.WAN + bandwidth: int = 0 + mtu: int = 0 + delay_ns: int = 0 + jitter_ns: int = 0 + tunnel_id: int = 0 + tunnel_net: bytes = b"\x00" * 5 + status: LinkStatus = LinkStatus.PENDING + code: str = "" + contributor_pub_key: Pubkey = Pubkey.default() + side_a_iface_name: str = "" + side_z_iface_name: str = "" + delay_override_ns: int = 0 + link_health: LinkHealth = LinkHealth.UNKNOWN + link_desired_status: LinkDesiredStatus = LinkDesiredStatus.PENDING + + @classmethod + def from_bytes(cls, data: bytes) -> Link: + r = IncrementalReader(data) + lk = cls() + lk.account_type = r.read_u8() + lk.owner = _read_pubkey(r) + lk.index = r.read_u128() + lk.bump_seed = r.read_u8() + lk.side_a_pub_key = _read_pubkey(r) + lk.side_z_pub_key = _read_pubkey(r) + lk.link_type = LinkLinkType(r.read_u8()) + lk.bandwidth = r.read_u64() + lk.mtu = r.read_u32() + lk.delay_ns = r.read_u64() + lk.jitter_ns = r.read_u64() + lk.tunnel_id = r.read_u16() + lk.tunnel_net = r.read_network_v4() + lk.status = LinkStatus(r.read_u8()) + lk.code = r.read_string() + lk.contributor_pub_key = _read_pubkey(r) + lk.side_a_iface_name = r.read_string() + lk.side_z_iface_name = r.read_string() + lk.delay_override_ns = r.read_u64() + lk.link_health = LinkHealth(r.read_u8()) + lk.link_desired_status = LinkDesiredStatus(r.read_u8()) + return lk + + +@dataclass +class User: + account_type: int = 0 + owner: Pubkey = Pubkey.default() + index: int = 0 + bump_seed: int = 0 + user_type: UserUserType = UserUserType.IBRL + tenant_pub_key: Pubkey = Pubkey.default() + device_pub_key: Pubkey = Pubkey.default() + cyoa_type: CyoaType = CyoaType.NONE + client_ip: bytes = b"\x00" * 4 + dz_ip: bytes = b"\x00" * 4 + tunnel_id: int = 0 + tunnel_net: bytes = b"\x00" * 5 + status: UserStatus = UserStatus.PENDING + publishers: list[Pubkey] = field(default_factory=list) + subscribers: list[Pubkey] = field(default_factory=list) + validator_pub_key: Pubkey = Pubkey.default() + + @classmethod + def from_bytes(cls, data: bytes) -> User: + r = IncrementalReader(data) + u = cls() + u.account_type = r.read_u8() + u.owner = _read_pubkey(r) + u.index = r.read_u128() + u.bump_seed = r.read_u8() + u.user_type = UserUserType(r.read_u8()) + u.tenant_pub_key = _read_pubkey(r) + u.device_pub_key = _read_pubkey(r) + u.cyoa_type = CyoaType(r.read_u8()) + u.client_ip = r.read_ipv4() + u.dz_ip = r.read_ipv4() + u.tunnel_id = r.read_u16() + u.tunnel_net = r.read_network_v4() + u.status = UserStatus(r.read_u8()) + u.publishers = _read_pubkey_vec(r) + u.subscribers = _read_pubkey_vec(r) + u.validator_pub_key = _read_pubkey(r) + return u + + +@dataclass +class MulticastGroup: + account_type: int = 0 + owner: Pubkey = Pubkey.default() + index: int = 0 + bump_seed: int = 0 + tenant_pub_key: Pubkey = Pubkey.default() + multicast_ip: bytes = b"\x00" * 4 + max_bandwidth: int = 0 + status: MulticastGroupStatus = MulticastGroupStatus.PENDING + code: str = "" + publisher_count: int = 0 + subscriber_count: int = 0 + + @classmethod + def from_bytes(cls, data: bytes) -> MulticastGroup: + r = IncrementalReader(data) + mg = cls() + mg.account_type = r.read_u8() + mg.owner = _read_pubkey(r) + mg.index = r.read_u128() + mg.bump_seed = r.read_u8() + mg.tenant_pub_key = _read_pubkey(r) + mg.multicast_ip = r.read_ipv4() + mg.max_bandwidth = r.read_u64() + mg.status = MulticastGroupStatus(r.read_u8()) + mg.code = r.read_string() + mg.publisher_count = r.read_u32() + mg.subscriber_count = r.read_u32() + return mg + + +@dataclass +class ProgramVersion: + major: int = 0 + minor: int = 0 + patch: int = 0 + + +@dataclass +class ProgramConfig: + account_type: int = 0 + bump_seed: int = 0 + version: ProgramVersion = field(default_factory=ProgramVersion) + min_compat_version: ProgramVersion = field(default_factory=ProgramVersion) + + @classmethod + def from_bytes(cls, data: bytes) -> ProgramConfig: + r = IncrementalReader(data) + pc = cls() + pc.account_type = r.read_u8() + pc.bump_seed = r.read_u8() + pc.version = ProgramVersion(r.read_u32(), r.read_u32(), r.read_u32()) + pc.min_compat_version = ProgramVersion(r.read_u32(), r.read_u32(), r.read_u32()) + return pc + + +@dataclass +class Contributor: + account_type: int = 0 + owner: Pubkey = Pubkey.default() + index: int = 0 + bump_seed: int = 0 + status: ContributorStatus = ContributorStatus.NONE + code: str = "" + reference_count: int = 0 + ops_manager_pk: Pubkey = Pubkey.default() + + @classmethod + def from_bytes(cls, data: bytes) -> Contributor: + r = IncrementalReader(data) + c = cls() + c.account_type = r.read_u8() + c.owner = _read_pubkey(r) + c.index = r.read_u128() + c.bump_seed = r.read_u8() + c.status = ContributorStatus(r.read_u8()) + c.code = r.read_string() + c.reference_count = r.read_u32() + c.ops_manager_pk = _read_pubkey(r) + return c + + +@dataclass +class AccessPass: + account_type: int = 0 + owner: Pubkey = Pubkey.default() + bump_seed: int = 0 + access_pass_type_tag: AccessPassTypeTag = AccessPassTypeTag.PREPAID + validator_pub_key: Pubkey | None = None + client_ip: bytes = b"\x00" * 4 + user_payer: Pubkey = Pubkey.default() + last_access_epoch: int = 0 + connection_count: int = 0 + status: AccessPassStatus = AccessPassStatus.REQUESTED + mgroup_pub_allowlist: list[Pubkey] = field(default_factory=list) + mgroup_sub_allowlist: list[Pubkey] = field(default_factory=list) + flags: int = 0 + + @classmethod + def from_bytes(cls, data: bytes) -> AccessPass: + r = IncrementalReader(data) + ap = cls() + ap.account_type = r.read_u8() + ap.owner = _read_pubkey(r) + ap.bump_seed = r.read_u8() + ap.access_pass_type_tag = AccessPassTypeTag(r.read_u8()) + if ap.access_pass_type_tag == AccessPassTypeTag.SOLANA_VALIDATOR: + ap.validator_pub_key = _read_pubkey(r) + ap.client_ip = r.read_ipv4() + ap.user_payer = _read_pubkey(r) + ap.last_access_epoch = r.read_u64() + ap.connection_count = r.read_u16() + ap.status = AccessPassStatus(r.read_u8()) + ap.mgroup_pub_allowlist = _read_pubkey_vec(r) + ap.mgroup_sub_allowlist = _read_pubkey_vec(r) + ap.flags = r.read_u8() + return ap diff --git a/sdk/serviceability/python/serviceability/tests/__init__.py b/sdk/serviceability/python/serviceability/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sdk/serviceability/python/serviceability/tests/test_compat.py b/sdk/serviceability/python/serviceability/tests/test_compat.py new file mode 100644 index 000000000..f2c831e6f --- /dev/null +++ b/sdk/serviceability/python/serviceability/tests/test_compat.py @@ -0,0 +1,145 @@ +"""Mainnet compatibility tests. + +These tests fetch live mainnet-beta data and verify that our struct +deserialization works against real on-chain accounts. + +Run with: + SERVICEABILITY_COMPAT_TEST=1 cd sdk/serviceability/python && uv run pytest -k compat -v + +Requires network access to Solana mainnet RPC. +""" + +import os +import struct + +import pytest +from solders.pubkey import Pubkey # type: ignore[import-untyped] + +from serviceability.config import PROGRAM_IDS, LEDGER_RPC_URLS +from serviceability.pda import ( + derive_global_config_pda, + derive_global_state_pda, + derive_program_config_pda, +) +from serviceability.state import GlobalConfig, GlobalState, ProgramConfig + + +def skip_unless_compat() -> None: + if not os.environ.get("SERVICEABILITY_COMPAT_TEST"): + pytest.skip("set SERVICEABILITY_COMPAT_TEST=1 to run compatibility tests against mainnet") + + +def _rpc_url() -> str: + return os.environ.get("SOLANA_RPC_URL", LEDGER_RPC_URLS["mainnet-beta"]) + + +def _program_id() -> Pubkey: + return Pubkey.from_string(PROGRAM_IDS["mainnet-beta"]) + + +def fetch_raw_account(addr: Pubkey) -> bytes: + from serviceability.rpc import new_rpc_client + + rpc = new_rpc_client(_rpc_url()) + resp = rpc.get_account_info(addr) + assert resp.value is not None, f"account not found: {addr}" + return bytes(resp.value.data) + + +def read_u8(raw: bytes, offset: int) -> int: + return raw[offset] + + +def read_u16(raw: bytes, offset: int) -> int: + return struct.unpack_from(" int: + return struct.unpack_from(" Pubkey: + return Pubkey.from_bytes(raw[offset : offset + 32]) + + +class TestCompatProgramConfig: + def test_deserialize(self) -> None: + skip_unless_compat() + + program_id = _program_id() + addr, _ = derive_program_config_pda(program_id) + raw = fetch_raw_account(addr) + + pc = ProgramConfig.from_bytes(raw) + + # ProgramConfig layout (all fixed-size): + # offset 0: AccountType (u8) + # offset 1: BumpSeed (u8) + # offset 2: Version.Major (u32) + # offset 6: Version.Minor (u32) + # offset 10: Version.Patch (u32) + # offset 14: MinCompatVersion.Major (u32) + # offset 18: MinCompatVersion.Minor (u32) + # offset 22: MinCompatVersion.Patch (u32) + assert pc.account_type == read_u8(raw, 0), "AccountType" + assert pc.bump_seed == read_u8(raw, 1), "BumpSeed" + assert pc.version.major == read_u32(raw, 2), "Version.Major" + assert pc.version.minor == read_u32(raw, 6), "Version.Minor" + assert pc.version.patch == read_u32(raw, 10), "Version.Patch" + assert pc.min_compat_version.major == read_u32(raw, 14), "MinCompatVersion.Major" + assert pc.min_compat_version.minor == read_u32(raw, 18), "MinCompatVersion.Minor" + assert pc.min_compat_version.patch == read_u32(raw, 22), "MinCompatVersion.Patch" + + assert pc.account_type == 9 + + +class TestCompatGlobalConfig: + def test_deserialize(self) -> None: + skip_unless_compat() + + program_id = _program_id() + addr, _ = derive_global_config_pda(program_id) + raw = fetch_raw_account(addr) + + gc = GlobalConfig.from_bytes(raw) + + # GlobalConfig layout (all fixed-size): + # offset 0: AccountType (u8) + # offset 1: Owner (32 bytes) + # offset 33: BumpSeed (u8) + # offset 34: LocalASN (u32) + # offset 38: RemoteASN (u32) + # offset 57: NextBGPCommunity (u16) + assert gc.account_type == read_u8(raw, 0), "AccountType" + assert gc.owner == read_pubkey(raw, 1), "Owner" + assert gc.bump_seed == read_u8(raw, 33), "BumpSeed" + assert gc.local_asn == read_u32(raw, 34), "LocalASN" + assert gc.remote_asn == read_u32(raw, 38), "RemoteASN" + assert gc.next_bgp_community == read_u16(raw, 57), "NextBGPCommunity" + + assert gc.account_type == 2 + assert gc.local_asn > 0, "LocalASN should be > 0 on mainnet" + + +class TestCompatGlobalState: + def test_deserialize(self) -> None: + skip_unless_compat() + + program_id = _program_id() + addr, _ = derive_global_state_pda(program_id) + raw = fetch_raw_account(addr) + + gs = GlobalState.from_bytes(raw) + + # GlobalState fixed layout (first 18 bytes before variable-length vecs): + # offset 0: AccountType (u8) + # offset 1: BumpSeed (u8) + assert gs.account_type == read_u8(raw, 0), "AccountType" + assert gs.bump_seed == read_u8(raw, 1), "BumpSeed" + + assert gs.account_type == 1 + + # Sanity checks. + assert gs.activator_authority_pk != Pubkey.default(), "ActivatorAuthorityPK is zero" + assert gs.sentinel_authority_pk != Pubkey.default(), "SentinelAuthorityPK is zero" + # health_oracle_pk may be zero on mainnet diff --git a/sdk/serviceability/python/serviceability/tests/test_enum_strings.py b/sdk/serviceability/python/serviceability/tests/test_enum_strings.py new file mode 100644 index 000000000..d4037deb8 --- /dev/null +++ b/sdk/serviceability/python/serviceability/tests/test_enum_strings.py @@ -0,0 +1,110 @@ +"""Test that enum __str__ outputs match the shared fixture file.""" + +import json +from pathlib import Path + +import pytest + +from serviceability.state import ( + AccessPassStatus, + AccessPassTypeTag, + ContributorStatus, + CyoaType, + DeviceDesiredStatus, + DeviceDeviceType, + DeviceHealth, + DeviceStatus, + ExchangeStatus, + InterfaceStatus, + InterfaceType, + LinkDesiredStatus, + LinkHealth, + LinkLinkType, + LinkStatus, + LocationStatus, + LoopbackType, + MulticastGroupStatus, + UserStatus, + UserUserType, +) + +FIXTURE_PATH = Path(__file__).resolve().parent.parent.parent.parent / "testdata" / "enum_strings.json" + +ENUM_MAP = { + "LocationStatus": LocationStatus, + "ExchangeStatus": ExchangeStatus, + "DeviceDeviceType": DeviceDeviceType, + "DeviceStatus": DeviceStatus, + "DeviceHealth": DeviceHealth, + "DeviceDesiredStatus": DeviceDesiredStatus, + "InterfaceStatus": InterfaceStatus, + "InterfaceType": InterfaceType, + "LoopbackType": LoopbackType, + "LinkLinkType": LinkLinkType, + "LinkStatus": LinkStatus, + "LinkHealth": LinkHealth, + "LinkDesiredStatus": LinkDesiredStatus, + "ContributorStatus": ContributorStatus, + "UserUserType": UserUserType, + "CyoaType": CyoaType, + "UserStatus": UserStatus, + "MulticastGroupStatus": MulticastGroupStatus, + "AccessPassTypeTag": AccessPassTypeTag, + "AccessPassStatus": AccessPassStatus, +} + + +def _load_fixture() -> dict: + return json.loads(FIXTURE_PATH.read_text()) + + +def _enum_test_cases() -> list[tuple[str, int, str]]: + """Yield (enum_name, int_value, expected_string) tuples.""" + fixture = _load_fixture() + cases = [] + for enum_name, mappings in fixture.items(): + if enum_name not in ENUM_MAP: + continue + for str_value, expected in mappings.items(): + cases.append((enum_name, int(str_value), expected)) + return cases + + +@pytest.mark.parametrize( + "enum_name,int_value,expected", + _enum_test_cases(), + ids=lambda x: str(x), +) +def test_enum_str(enum_name: str, int_value: int, expected: str) -> None: + enum_cls = ENUM_MAP[enum_name] + try: + instance = enum_cls(int_value) + except ValueError: + # Unknown value not defined as a member; skip since Python IntEnum + # cannot instantiate undefined members. Go/TS tests cover this case. + pytest.skip(f"{enum_name}({int_value}) is not a valid member") + return + assert str(instance) == expected, ( + f"{enum_name}({int_value}): expected {expected!r}, got {str(instance)!r}" + ) + + +def test_all_enum_members_in_fixture() -> None: + """Every Python IntEnum member must appear in the fixture. + + This catches the case where a variant is added in Python but not in + enum_strings.json. Since the fixture is shared across Go, Python, and + TypeScript, updating it will cause the other languages' tests to fail + until they add the new variant too. + """ + fixture = _load_fixture() + missing = [] + for enum_name, enum_cls in ENUM_MAP.items(): + fixture_values = fixture.get(enum_name, {}) + for member in enum_cls: + if str(member.value) not in fixture_values: + missing.append(f"{enum_name}.{member.name} ({member.value})") + assert not missing, ( + "Enum members missing from enum_strings.json fixture — add them so " + "other languages stay in sync:\n " + "\n ".join(missing) + ) diff --git a/sdk/serviceability/python/serviceability/tests/test_fixtures.py b/sdk/serviceability/python/serviceability/tests/test_fixtures.py new file mode 100644 index 000000000..3687e9022 --- /dev/null +++ b/sdk/serviceability/python/serviceability/tests/test_fixtures.py @@ -0,0 +1,330 @@ +"""Fixture-based compatibility tests.""" + +import json +from pathlib import Path + +from solders.pubkey import Pubkey # type: ignore[import-untyped] + +from serviceability.state import ( + AccessPass, + Contributor, + Device, + Exchange, + GlobalConfig, + GlobalState, + Link, + Location, + MulticastGroup, + ProgramConfig, + User, +) + +FIXTURES_DIR = Path(__file__).resolve().parent.parent.parent.parent / "testdata" / "fixtures" + + +def _load_fixture(name: str) -> tuple[bytes, dict]: + bin_data = (FIXTURES_DIR / f"{name}.bin").read_bytes() + meta = json.loads((FIXTURES_DIR / f"{name}.json").read_text()) + return bin_data, meta + + +def _assert_fields(expected_fields: list[dict], got: dict) -> None: + for f in expected_fields: + name = f["name"] + if name not in got: + continue + typ = f["typ"] + raw = f["value"] + actual = got[name] + if typ in ("u8", "u16", "u32", "u64"): + assert actual == int(raw), f"{name}: expected {raw}, got {actual}" + elif typ == "pubkey": + expected = Pubkey.from_string(raw) + assert actual == expected, f"{name}: expected {expected}, got {actual}" + elif typ == "string": + assert actual == raw, f"{name}: expected {raw!r}, got {actual!r}" + elif typ == "bool": + expected = raw == "true" + assert actual == expected, f"{name}: expected {expected}, got {actual}" + elif typ == "ipv4": + import ipaddress + + expected_bytes = ipaddress.IPv4Address(raw).packed + assert actual == expected_bytes, f"{name}: expected {raw}, got {actual}" + elif typ == "networkv4": + import ipaddress + + net = ipaddress.IPv4Network(raw) + expected_bytes = net.network_address.packed + bytes([net.prefixlen]) + assert actual == expected_bytes, f"{name}: expected {raw}, got {actual}" + elif typ == "u128": + assert actual == int(raw), f"{name}: expected {raw}, got {actual}" + + +class TestFixtureGlobalState: + def test_deserialize(self): + data, meta = _load_fixture("global_state") + gs = GlobalState.from_bytes(data) + _assert_fields( + meta["fields"], + { + "AccountType": gs.account_type, + "BumpSeed": gs.bump_seed, + "ContributorAirdropLamports": gs.contributor_airdrop_lamports, + "UserAirdropLamports": gs.user_airdrop_lamports, + "ActivatorAuthorityPk": gs.activator_authority_pk, + "SentinelAuthorityPk": gs.sentinel_authority_pk, + "HealthOraclePk": gs.health_oracle_pk, + }, + ) + + +class TestFixtureGlobalConfig: + def test_deserialize(self): + data, meta = _load_fixture("global_config") + gc = GlobalConfig.from_bytes(data) + _assert_fields( + meta["fields"], + { + "AccountType": gc.account_type, + "Owner": gc.owner, + "BumpSeed": gc.bump_seed, + "LocalAsn": gc.local_asn, + "RemoteAsn": gc.remote_asn, + "NextBgpCommunity": gc.next_bgp_community, + }, + ) + + +class TestFixtureLocation: + def test_deserialize(self): + data, meta = _load_fixture("location") + loc = Location.from_bytes(data) + _assert_fields( + meta["fields"], + { + "AccountType": loc.account_type, + "Owner": loc.owner, + "BumpSeed": loc.bump_seed, + "LocId": loc.loc_id, + "Status": loc.status, + "ReferenceCount": loc.reference_count, + }, + ) + + +class TestFixtureExchange: + def test_deserialize(self): + data, meta = _load_fixture("exchange") + ex = Exchange.from_bytes(data) + _assert_fields( + meta["fields"], + { + "AccountType": ex.account_type, + "Owner": ex.owner, + "BumpSeed": ex.bump_seed, + "BgpCommunity": ex.bgp_community, + "Status": ex.status, + "ReferenceCount": ex.reference_count, + "Device1Pk": ex.device1_pk, + "Device2Pk": ex.device2_pk, + }, + ) + + +class TestFixtureDevice: + def test_deserialize(self): + data, meta = _load_fixture("device") + dev = Device.from_bytes(data) + _assert_fields( + meta["fields"], + { + "AccountType": dev.account_type, + "Owner": dev.owner, + "Index": dev.index, + "BumpSeed": dev.bump_seed, + "DeviceType": dev.device_type, + "PublicIp": dev.public_ip, + "Status": dev.status, + "Code": dev.code, + "MgmtVrf": dev.mgmt_vrf, + "ReferenceCount": dev.reference_count, + "UsersCount": dev.users_count, + "MaxUsers": dev.max_users, + "DeviceHealth": dev.device_health, + "DesiredStatus": dev.device_desired_status, + "MetricsPublisherPk": dev.metrics_publisher_pub_key, + "ContributorPk": dev.contributor_pub_key, + }, + ) + # Verify interfaces + assert len(dev.interfaces) == 2 + assert dev.interfaces[0].name == "Loopback0" + assert dev.interfaces[1].name == "Ethernet1" + # Verify dz_prefixes + import ipaddress + + assert len(dev.dz_prefixes) == 1 + net = ipaddress.IPv4Network("10.10.0.0/24") + expected_prefix = net.network_address.packed + bytes([net.prefixlen]) + assert dev.dz_prefixes[0] == expected_prefix + # Verify code, mgmt_vrf, public_ip, index + assert dev.code == "dz1" + assert dev.mgmt_vrf == "mgmt" + assert dev.public_ip == ipaddress.IPv4Address("203.0.113.1").packed + assert dev.index == 7 + + +class TestFixtureLink: + def test_deserialize(self): + data, meta = _load_fixture("link") + lk = Link.from_bytes(data) + _assert_fields( + meta["fields"], + { + "AccountType": lk.account_type, + "Owner": lk.owner, + "BumpSeed": lk.bump_seed, + "LinkType": lk.link_type, + "Bandwidth": lk.bandwidth, + "Mtu": lk.mtu, + "DelayNs": lk.delay_ns, + "JitterNs": lk.jitter_ns, + "TunnelId": lk.tunnel_id, + "Status": lk.status, + "ContributorPk": lk.contributor_pub_key, + "DelayOverrideNs": lk.delay_override_ns, + "LinkHealth": lk.link_health, + "DesiredStatus": lk.link_desired_status, + }, + ) + + +class TestFixtureUser: + def test_deserialize(self): + data, meta = _load_fixture("user") + u = User.from_bytes(data) + _assert_fields( + meta["fields"], + { + "AccountType": u.account_type, + "Owner": u.owner, + "BumpSeed": u.bump_seed, + "UserType": u.user_type, + "TenantPk": u.tenant_pub_key, + "DevicePk": u.device_pub_key, + "CyoaType": u.cyoa_type, + "TunnelId": u.tunnel_id, + "Status": u.status, + "ValidatorPubkey": u.validator_pub_key, + }, + ) + + +class TestFixtureMulticastGroup: + def test_deserialize(self): + data, meta = _load_fixture("multicast_group") + mg = MulticastGroup.from_bytes(data) + _assert_fields( + meta["fields"], + { + "AccountType": mg.account_type, + "Owner": mg.owner, + "BumpSeed": mg.bump_seed, + "TenantPk": mg.tenant_pub_key, + "MaxBandwidth": mg.max_bandwidth, + "Status": mg.status, + "PublisherCount": mg.publisher_count, + "SubscriberCount": mg.subscriber_count, + }, + ) + + +class TestFixtureContributor: + def test_deserialize(self): + data, meta = _load_fixture("contributor") + c = Contributor.from_bytes(data) + _assert_fields( + meta["fields"], + { + "AccountType": c.account_type, + "Owner": c.owner, + "BumpSeed": c.bump_seed, + "Status": c.status, + "ReferenceCount": c.reference_count, + "OpsManagerPk": c.ops_manager_pk, + }, + ) + + +class TestFixtureProgramConfig: + def test_deserialize(self): + data, meta = _load_fixture("program_config") + pc = ProgramConfig.from_bytes(data) + _assert_fields( + meta["fields"], + { + "AccountType": pc.account_type, + "BumpSeed": pc.bump_seed, + "VersionMajor": pc.version.major, + "VersionMinor": pc.version.minor, + "VersionPatch": pc.version.patch, + }, + ) + + +class TestFixtureAccessPass: + def test_deserialize(self): + data, meta = _load_fixture("access_pass") + ap = AccessPass.from_bytes(data) + _assert_fields( + meta["fields"], + { + "AccountType": ap.account_type, + "Owner": ap.owner, + "BumpSeed": ap.bump_seed, + "AccessPassType": ap.access_pass_type_tag, + "UserPayer": ap.user_payer, + "LastAccessEpoch": ap.last_access_epoch, + "ConnectionCount": ap.connection_count, + "Status": ap.status, + "Flags": ap.flags, + }, + ) + + +class TestFixtureAccessPassValidator: + def test_deserialize(self): + data, meta = _load_fixture("access_pass_validator") + ap = AccessPass.from_bytes(data) + _assert_fields( + meta["fields"], + { + "AccountType": ap.account_type, + "Owner": ap.owner, + "BumpSeed": ap.bump_seed, + "AccessPassType": ap.access_pass_type_tag, + "AccessPassTypeValidatorPubkey": ap.validator_pub_key, + "ClientIp": ap.client_ip, + "UserPayer": ap.user_payer, + "LastAccessEpoch": ap.last_access_epoch, + "ConnectionCount": ap.connection_count, + "Status": ap.status, + "Flags": ap.flags, + }, + ) + assert ap.account_type == 11 + assert ap.bump_seed == 243 + assert ap.access_pass_type_tag == 1 + assert ap.validator_pub_key == Pubkey.from_string( + "BuP3jEYfnTCfB4UqQk9L37k2vaXsNuVsbWxrYbGDmL6s" + ) + import ipaddress + + assert ap.client_ip == ipaddress.IPv4Address("10.0.0.50").packed + assert ap.last_access_epoch == 1000 + assert ap.connection_count == 1 + assert ap.status == 1 + assert len(ap.mgroup_pub_allowlist) == 1 + assert len(ap.mgroup_sub_allowlist) == 1 + assert ap.flags == 3 diff --git a/sdk/serviceability/python/serviceability/tests/test_pda.py b/sdk/serviceability/python/serviceability/tests/test_pda.py new file mode 100644 index 000000000..1a252a0a4 --- /dev/null +++ b/sdk/serviceability/python/serviceability/tests/test_pda.py @@ -0,0 +1,36 @@ +from solders.pubkey import Pubkey # type: ignore[import-untyped] + +from serviceability.pda import ( + derive_global_config_pda, + derive_global_state_pda, + derive_program_config_pda, +) + +PROGRAM_ID = Pubkey.from_string("ser2VaTMAcYTaauMrTSfSrxBaUDq7BLNs2xfUugTAGv") + + +def test_derive_global_state_pda(): + addr, bump = derive_global_state_pda(PROGRAM_ID) + assert addr != Pubkey.default() + addr2, bump2 = derive_global_state_pda(PROGRAM_ID) + assert addr == addr2 + assert bump == bump2 + + +def test_derive_global_config_pda(): + addr, _ = derive_global_config_pda(PROGRAM_ID) + assert addr != Pubkey.default() + + +def test_derive_program_config_pda(): + addr, _ = derive_program_config_pda(PROGRAM_ID) + assert addr != Pubkey.default() + + +def test_pdas_are_different(): + gs, _ = derive_global_state_pda(PROGRAM_ID) + gc, _ = derive_global_config_pda(PROGRAM_ID) + pc, _ = derive_program_config_pda(PROGRAM_ID) + assert gs != gc + assert gs != pc + assert gc != pc diff --git a/sdk/serviceability/python/uv.lock b/sdk/serviceability/python/uv.lock new file mode 100644 index 000000000..1df0d2005 --- /dev/null +++ b/sdk/serviceability/python/uv.lock @@ -0,0 +1,373 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "construct" +version = "2.10.70" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/77/8c84b98eca70d245a2a956452f21d57930d22ab88cbeed9290ca630cf03f/construct-2.10.70.tar.gz", hash = "sha256:4d2472f9684731e58cc9c56c463be63baa1447d674e0d66aeb5627b22f512c29", size = 86337, upload-time = "2023-11-29T08:44:49.545Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/fb/08b3f4bf05da99aba8ffea52a558758def16e8516bc75ca94ff73587e7d3/construct-2.10.70-py3-none-any.whl", hash = "sha256:c80be81ef595a1a821ec69dc16099550ed22197615f4320b57cc9ce2a672cb30", size = 63020, upload-time = "2023-11-29T08:44:46.876Z" }, +] + +[[package]] +name = "construct-typing" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "construct" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/ae/659fe4866d89ef5a3a65cddbdd7b35882f4feb72db383821965f2fcea934/construct_typing-0.7.0.tar.gz", hash = "sha256:71d110dedff39bd3b603c734077032a7065bc597a49db1f5b03a211d05dbac23", size = 45104, upload-time = "2025-10-27T19:30:29.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/0c/2db6f7e1ae9795e436c6a0dc0bc38b12b8c8a228cb63203e24190b755b3b/construct_typing-0.7.0-py3-none-any.whl", hash = "sha256:c92383c6e8e5d07ba25811c8d5163820458d821e73bb1006541f43f89788646c", size = 24350, upload-time = "2025-10-27T19:30:27.505Z" }, +] + +[[package]] +name = "doublezero-borsh-incremental" +version = "0.0.1" +source = { editable = "../../borsh-incremental/python" } + +[package.metadata] + +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=9.0.2" }] + +[[package]] +name = "doublezero-serviceability" +version = "0.0.1" +source = { editable = "." } +dependencies = [ + { name = "doublezero-borsh-incremental" }, + { name = "httpx" }, + { name = "solana" }, + { name = "solders" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "doublezero-borsh-incremental", editable = "../../borsh-incremental/python" }, + { name = "httpx", specifier = ">=0.27" }, + { name = "solana", specifier = ">=0.35" }, + { name = "solders", specifier = ">=0.21" }, +] + +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=9.0.2" }] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jsonalias" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/45/ee7e17002cb7f3264f755ff6a1a72c55d1830e07808d643167d2a2277c4f/jsonalias-0.1.1.tar.gz", hash = "sha256:64f04d935397d579fc94509e1fcb6212f2d081235d9d6395bd10baedf760a769", size = 1095, upload-time = "2022-10-28T22:57:56.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/ed/05aebce69f78c104feff2ffcdd5a6f9d668a208aba3a8bf56e3750809fd8/jsonalias-0.1.1-py3-none-any.whl", hash = "sha256:a56d2888e6397812c606156504e861e8ec00e188005af149f003c787db3d3f18", size = 1312, upload-time = "2022-10-28T22:57:54.763Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "solana" +version = "0.36.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "construct-typing" }, + { name = "httpx" }, + { name = "solders" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/66/b8cd6e4d95bfe46798942ace31935e7799005a4e2180869dc7bac6b75be9/solana-0.36.11.tar.gz", hash = "sha256:2fdcf483674f4b88fe6510524bf3234a5837d19fe1815aa5a285f2739d28b3a3", size = 54516, upload-time = "2026-01-03T02:11:52.243Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/8d/807eebf0560759ad90464060e0d1d87ff5409beb6ed56104c553a83a976a/solana-0.36.11-py3-none-any.whl", hash = "sha256:1d659decc67a40ee1e9b5ded373a076b87cf3b4bd0645e120d16d9348c2025ba", size = 64786, upload-time = "2026-01-03T02:11:50.811Z" }, +] + +[[package]] +name = "solders" +version = "0.27.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonalias" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/25/80a81bb3dc4c70329dd0016edbdfbf2e8d8300a98ab9cd1a6ea0266bda7c/solders-0.27.1.tar.gz", hash = "sha256:7d8a24ad2f193afcdc02d6f3975917a7358b0f0ab7f4b3695b135ff2008222c8", size = 180923, upload-time = "2025-11-15T07:50:52.32Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/6b/0c0ee4766705824261779d00229fb95308d6b28422613e0e2af577f60ee3/solders-0.27.1-cp38-abi3-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:4dcd8e766bab24afbe9e0ae363d86f9810457e04b00c8a9149f69ca939ed587c", size = 24883435, upload-time = "2025-11-15T07:50:34.42Z" }, + { url = "https://files.pythonhosted.org/packages/33/1c/be04a1b26e18c409dd006d214198dc03f0b657c1cb34f4c83b763f8348f0/solders-0.27.1-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5d87b145cc0129095f9cff8c7f28d2e910bc5b5a4cf257c263b08a4b95f111dd", size = 6480729, upload-time = "2025-11-15T07:50:37.323Z" }, + { url = "https://files.pythonhosted.org/packages/48/03/98dc73c266b11ed5c13b3933510a1aa115becf97f45bec1a22da9d03ffa9/solders-0.27.1-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6082bbe46b7b1b2b005d046011f89fcae75fc5ea4f1a0ef5c2e9dfb5fe7930ce", size = 12744782, upload-time = "2025-11-15T07:50:39.283Z" }, + { url = "https://files.pythonhosted.org/packages/a0/39/35384d8fb80d05937bd9e8af7237cfe3f0d017c8aba357209d90d428f3a0/solders-0.27.1-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ccb821c2e4af43d976f312086f248a67352b3986e5f4c87af41cfeac6d8b5683", size = 6601257, upload-time = "2025-11-15T07:50:41.738Z" }, + { url = "https://files.pythonhosted.org/packages/8c/65/8989e521142473bf1130613476a4449e106bb97ed6cc86097f6f519b1234/solders-0.27.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:663a10566ae81f67c4515d4db5fbf51b735204741728c1a5cde11c4e019a51df", size = 7277802, upload-time = "2025-11-15T07:50:43.789Z" }, + { url = "https://files.pythonhosted.org/packages/f2/41/87ecf12cec0e7aa9c67b0cf1b8079fb28aa0af91e97328a3bd0c5e3001ba/solders-0.27.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:d14f05a77dbbf7966fb26f255c81302e6127550bdb66c2fdc99f522043fdf376", size = 7082541, upload-time = "2025-11-15T07:50:45.847Z" }, + { url = "https://files.pythonhosted.org/packages/33/b9/35e6f59b41bb205b26c7318fcdca43f3d59464fd3ddc13d36f36427f64d4/solders-0.27.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f778eeab411acec0a765a01c7b772f8eca8a8543d98276bd83cb826960da211b", size = 6845568, upload-time = "2025-11-15T07:50:47.698Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f3/14ed12d8d5047ababaca3271f82ebbf500ff74b6358f283962232103a12d/solders-0.27.1-cp38-abi3-win_amd64.whl", hash = "sha256:f3b787c29570a46d219c7a67543d8b0fadc73abda346653aa20e8eccd839e78b", size = 5295092, upload-time = "2025-11-15T07:50:50.517Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" }, + { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" }, + { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" }, + { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" }, + { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" }, + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" }, + { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" }, + { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" }, + { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] diff --git a/sdk/serviceability/testdata/enum_strings.json b/sdk/serviceability/testdata/enum_strings.json new file mode 100644 index 000000000..8aef6519b --- /dev/null +++ b/sdk/serviceability/testdata/enum_strings.json @@ -0,0 +1,22 @@ +{ + "LocationStatus": {"0": "pending", "1": "activated", "2": "suspended", "99": "unknown"}, + "ExchangeStatus": {"0": "pending", "1": "activated", "2": "suspended", "99": "unknown"}, + "DeviceDeviceType": {"0": "hybrid", "1": "transit", "2": "edge", "99": "unknown"}, + "DeviceStatus": {"0": "pending", "1": "activated", "2": "deleting", "3": "rejected", "4": "drained", "5": "device-provisioning", "6": "link-provisioning", "99": "unknown"}, + "DeviceHealth": {"0": "unknown", "1": "pending", "2": "ready_for_links", "3": "ready_for_users", "4": "impaired", "99": "unknown"}, + "DeviceDesiredStatus": {"0": "pending", "1": "activated", "6": "drained", "99": "unknown"}, + "InterfaceStatus": {"0": "invalid", "1": "unmanaged", "2": "pending", "3": "activated", "4": "deleting", "5": "rejecting", "6": "unlinked", "99": "unknown"}, + "InterfaceType": {"0": "invalid", "1": "loopback", "2": "physical", "99": "unknown"}, + "LoopbackType": {"0": "none", "1": "vpnv4", "2": "ipv4", "3": "pim_rp_addr", "4": "reserved", "99": "unknown"}, + "LinkLinkType": {"1": "WAN", "127": "DZX", "99": ""}, + "LinkStatus": {"0": "pending", "1": "activated", "2": "deleting", "3": "rejected", "4": "requested", "5": "hard-drained", "6": "soft-drained", "7": "provisioning", "99": "unknown"}, + "LinkHealth": {"0": "unknown", "1": "pending", "2": "ready_for_service", "3": "impaired", "99": "unknown"}, + "LinkDesiredStatus": {"0": "pending", "1": "activated", "2": "hard-drained", "3": "soft-drained", "99": "unknown"}, + "ContributorStatus": {"0": "none", "1": "activated", "2": "suspended", "3": "deleting", "99": "unknown"}, + "UserUserType": {"0": "ibrl", "1": "ibrl_with_allocated_ip", "2": "edge_filtering", "3": "multicast", "99": "unknown"}, + "CyoaType": {"0": "none", "1": "gre_over_dia", "2": "gre_over_fabric", "3": "gre_over_private_peering", "4": "gre_over_public_peering", "5": "gre_over_cable", "99": "unknown"}, + "UserStatus": {"0": "pending", "1": "activated", "3": "deleting", "4": "rejected", "5": "pending_ban", "6": "banned", "7": "updating", "8": "out_of_credits", "99": "unknown"}, + "MulticastGroupStatus": {"0": "pending", "1": "activated", "2": "suspended", "3": "deleting", "4": "rejected", "99": "unknown"}, + "AccessPassTypeTag": {"0": "prepaid", "1": "solana_validator", "99": "unknown"}, + "AccessPassStatus": {"0": "requested", "1": "connected", "2": "disconnected", "3": "expired", "99": "unknown"} +} diff --git a/sdk/serviceability/testdata/fixtures/access_pass.bin b/sdk/serviceability/testdata/fixtures/access_pass.bin new file mode 100644 index 000000000..ffdfc42f4 Binary files /dev/null and b/sdk/serviceability/testdata/fixtures/access_pass.bin differ diff --git a/sdk/serviceability/testdata/fixtures/access_pass.json b/sdk/serviceability/testdata/fixtures/access_pass.json new file mode 100644 index 000000000..ed077e2d1 --- /dev/null +++ b/sdk/serviceability/testdata/fixtures/access_pass.json @@ -0,0 +1,66 @@ +{ + "name": "AccessPass", + "account_type": 11, + "fields": [ + { + "name": "AccountType", + "value": "11", + "typ": "u8" + }, + { + "name": "Owner", + "value": "Ah7i6KV9hQHopcKJFfMHvSKvGQMzfofZ5uQsV5gJnrCf", + "typ": "pubkey" + }, + { + "name": "BumpSeed", + "value": "244", + "typ": "u8" + }, + { + "name": "AccessPassType", + "value": "0", + "typ": "u8" + }, + { + "name": "ClientIp", + "value": "198.51.100.20", + "typ": "ipv4" + }, + { + "name": "UserPayer", + "value": "Am27jpDEbEnXEJSGmAcjpN59XQvmNkmTfmikzGzJWes1", + "typ": "pubkey" + }, + { + "name": "LastAccessEpoch", + "value": "18446744073709551615", + "typ": "u64" + }, + { + "name": "ConnectionCount", + "value": "3", + "typ": "u16" + }, + { + "name": "Status", + "value": "1", + "typ": "u8" + }, + { + "name": "MgroupPubAllowlistLen", + "value": "0", + "typ": "u32" + }, + { + "name": "MgroupSubAllowlistLen", + "value": "0", + "typ": "u32" + }, + { + "name": "Flags", + "value": "1", + "typ": "u8" + } + ] +} \ No newline at end of file diff --git a/sdk/serviceability/testdata/fixtures/access_pass_validator.bin b/sdk/serviceability/testdata/fixtures/access_pass_validator.bin new file mode 100644 index 000000000..10894bb29 Binary files /dev/null and b/sdk/serviceability/testdata/fixtures/access_pass_validator.bin differ diff --git a/sdk/serviceability/testdata/fixtures/access_pass_validator.json b/sdk/serviceability/testdata/fixtures/access_pass_validator.json new file mode 100644 index 000000000..df9e82d83 --- /dev/null +++ b/sdk/serviceability/testdata/fixtures/access_pass_validator.json @@ -0,0 +1,81 @@ +{ + "name": "AccessPassValidator", + "account_type": 11, + "fields": [ + { + "name": "AccountType", + "value": "11", + "typ": "u8" + }, + { + "name": "Owner", + "value": "BmaESF6VznDEMgEtPjcSFGFaQZQKy1J4RnM5YCeEKinB", + "typ": "pubkey" + }, + { + "name": "BumpSeed", + "value": "243", + "typ": "u8" + }, + { + "name": "AccessPassType", + "value": "1", + "typ": "u8" + }, + { + "name": "AccessPassTypeValidatorPubkey", + "value": "BuP3jEYfnTCfB4UqQk9L37k2vaXsNuVsbWxrYbGDmL6s", + "typ": "pubkey" + }, + { + "name": "ClientIp", + "value": "10.0.0.50", + "typ": "ipv4" + }, + { + "name": "UserPayer", + "value": "BqUe5jpatchwmNMruEst9BzofZy6fxPy1eey3PxE3XSX", + "typ": "pubkey" + }, + { + "name": "LastAccessEpoch", + "value": "1000", + "typ": "u64" + }, + { + "name": "ConnectionCount", + "value": "1", + "typ": "u16" + }, + { + "name": "Status", + "value": "1", + "typ": "u8" + }, + { + "name": "MgroupPubAllowlistLen", + "value": "1", + "typ": "u32" + }, + { + "name": "MgroupPubAllowlist0", + "value": "ByHTNjGkgHhNakbovFQmw3VGBb6e5rbnBPGk3naDV8mD", + "typ": "pubkey" + }, + { + "name": "MgroupSubAllowlistLen", + "value": "1", + "typ": "u32" + }, + { + "name": "MgroupSubAllowlist0", + "value": "C3Bs2Dzqa8C5zSinRkgDpyEVSbfQnohgmFadYytDCwRZ", + "typ": "pubkey" + }, + { + "name": "Flags", + "value": "3", + "typ": "u8" + } + ] +} \ No newline at end of file diff --git a/sdk/serviceability/testdata/fixtures/contributor.bin b/sdk/serviceability/testdata/fixtures/contributor.bin new file mode 100644 index 000000000..5a53f62a7 Binary files /dev/null and b/sdk/serviceability/testdata/fixtures/contributor.bin differ diff --git a/sdk/serviceability/testdata/fixtures/contributor.json b/sdk/serviceability/testdata/fixtures/contributor.json new file mode 100644 index 000000000..b1a736866 --- /dev/null +++ b/sdk/serviceability/testdata/fixtures/contributor.json @@ -0,0 +1,46 @@ +{ + "name": "Contributor", + "account_type": 10, + "fields": [ + { + "name": "AccountType", + "value": "10", + "typ": "u8" + }, + { + "name": "Owner", + "value": "9cfBkPsoQ2NPHYPi7b69bcQG8FKfNc33k2UfRxiPFyd9", + "typ": "pubkey" + }, + { + "name": "Index", + "value": "550", + "typ": "u128" + }, + { + "name": "BumpSeed", + "value": "245", + "typ": "u8" + }, + { + "name": "Status", + "value": "1", + "typ": "u8" + }, + { + "name": "Code", + "value": "co01", + "typ": "string" + }, + { + "name": "ReferenceCount", + "value": "7", + "typ": "u32" + }, + { + "name": "OpsManagerPk", + "value": "9gZbPtbtHrs6hEWgd6MbVY9VPFtS5Z8xKtnYwA2NynHV", + "typ": "pubkey" + } + ] +} \ No newline at end of file diff --git a/sdk/serviceability/testdata/fixtures/device.bin b/sdk/serviceability/testdata/fixtures/device.bin new file mode 100644 index 000000000..ddf525c1a Binary files /dev/null and b/sdk/serviceability/testdata/fixtures/device.bin differ diff --git a/sdk/serviceability/testdata/fixtures/device.json b/sdk/serviceability/testdata/fixtures/device.json new file mode 100644 index 000000000..9e9b86bb2 --- /dev/null +++ b/sdk/serviceability/testdata/fixtures/device.json @@ -0,0 +1,231 @@ +{ + "name": "Device", + "account_type": 5, + "fields": [ + { + "name": "AccountType", + "value": "5", + "typ": "u8" + }, + { + "name": "Owner", + "value": "5Jq6NhSQCWgh9GhMZJ3aJJhdZdALBoX2NWjqDUrh8VK5", + "typ": "pubkey" + }, + { + "name": "Index", + "value": "7", + "typ": "u128" + }, + { + "name": "BumpSeed", + "value": "250", + "typ": "u8" + }, + { + "name": "LocationPk", + "value": "5NjW2CAV6MBQYxpL4oK2CESrpdj6tkcvxP3iigAgrHyR", + "typ": "pubkey" + }, + { + "name": "ExchangePk", + "value": "5SdufgtZzBg7xewJaJaU6AC65eHsbhiqYFMcDsUga6dm", + "typ": "pubkey" + }, + { + "name": "DeviceType", + "value": "2", + "typ": "u8" + }, + { + "name": "PublicIp", + "value": "203.0.113.1", + "typ": "ipv4" + }, + { + "name": "Status", + "value": "1", + "typ": "u8" + }, + { + "name": "Code", + "value": "dz1", + "typ": "string" + }, + { + "name": "DzPrefixesLen", + "value": "1", + "typ": "u32" + }, + { + "name": "DzPrefixes0", + "value": "10.10.0.0/24", + "typ": "networkv4" + }, + { + "name": "MetricsPublisherPk", + "value": "5WYKKBcet2AqNM4H5oquz5wKLereJepk87fVj4ngHuJ7", + "typ": "pubkey" + }, + { + "name": "ContributorPk", + "value": "5aSixgLjmrfYn3BFbK7Mt1gYbfRR1bvehyyPEG6g1hxT", + "typ": "pubkey" + }, + { + "name": "MgmtVrf", + "value": "mgmt", + "typ": "string" + }, + { + "name": "InterfacesLen", + "value": "2", + "typ": "u32" + }, + { + "name": "Interface0Version", + "value": "0", + "typ": "u8" + }, + { + "name": "Interface0Status", + "value": "3", + "typ": "u8" + }, + { + "name": "Interface0Name", + "value": "Loopback0", + "typ": "string" + }, + { + "name": "Interface0InterfaceType", + "value": "1", + "typ": "u8" + }, + { + "name": "Interface0LoopbackType", + "value": "1", + "typ": "u8" + }, + { + "name": "Interface0VlanId", + "value": "0", + "typ": "u16" + }, + { + "name": "Interface0IpNet", + "value": "10.0.0.1/32", + "typ": "networkv4" + }, + { + "name": "Interface0NodeSegmentIdx", + "value": "100", + "typ": "u16" + }, + { + "name": "Interface0UserTunnelEndpoint", + "value": "false", + "typ": "bool" + }, + { + "name": "Interface1Version", + "value": "1", + "typ": "u8" + }, + { + "name": "Interface1Status", + "value": "3", + "typ": "u8" + }, + { + "name": "Interface1Name", + "value": "Ethernet1", + "typ": "string" + }, + { + "name": "Interface1InterfaceType", + "value": "2", + "typ": "u8" + }, + { + "name": "Interface1InterfaceCYOA", + "value": "1", + "typ": "u8" + }, + { + "name": "Interface1InterfaceDIA", + "value": "1", + "typ": "u8" + }, + { + "name": "Interface1LoopbackType", + "value": "0", + "typ": "u8" + }, + { + "name": "Interface1Bandwidth", + "value": "10000000000", + "typ": "u64" + }, + { + "name": "Interface1Cir", + "value": "5000000000", + "typ": "u64" + }, + { + "name": "Interface1Mtu", + "value": "9000", + "typ": "u16" + }, + { + "name": "Interface1RoutingMode", + "value": "1", + "typ": "u8" + }, + { + "name": "Interface1VlanId", + "value": "100", + "typ": "u16" + }, + { + "name": "Interface1IpNet", + "value": "172.16.0.1/30", + "typ": "networkv4" + }, + { + "name": "Interface1NodeSegmentIdx", + "value": "200", + "typ": "u16" + }, + { + "name": "Interface1UserTunnelEndpoint", + "value": "true", + "typ": "bool" + }, + { + "name": "ReferenceCount", + "value": "12", + "typ": "u32" + }, + { + "name": "UsersCount", + "value": "5", + "typ": "u16" + }, + { + "name": "MaxUsers", + "value": "100", + "typ": "u16" + }, + { + "name": "DeviceHealth", + "value": "3", + "typ": "u8" + }, + { + "name": "DesiredStatus", + "value": "1", + "typ": "u8" + } + ] +} \ No newline at end of file diff --git a/sdk/serviceability/testdata/fixtures/exchange.bin b/sdk/serviceability/testdata/fixtures/exchange.bin new file mode 100644 index 000000000..0d79c2179 Binary files /dev/null and b/sdk/serviceability/testdata/fixtures/exchange.bin differ diff --git a/sdk/serviceability/testdata/fixtures/exchange.json b/sdk/serviceability/testdata/fixtures/exchange.json new file mode 100644 index 000000000..a1013fa37 --- /dev/null +++ b/sdk/serviceability/testdata/fixtures/exchange.json @@ -0,0 +1,76 @@ +{ + "name": "Exchange", + "account_type": 4, + "fields": [ + { + "name": "AccountType", + "value": "4", + "typ": "u8" + }, + { + "name": "Owner", + "value": "4ENa2mq3u8mGcCmmRDnRyUmyRU7ztbtX2dodAMtmbcjZ", + "typ": "pubkey" + }, + { + "name": "Index", + "value": "12", + "typ": "u128" + }, + { + "name": "BumpSeed", + "value": "251", + "typ": "u8" + }, + { + "name": "Lat", + "value": "52.3676", + "typ": "f64" + }, + { + "name": "Lng", + "value": "4.9041", + "typ": "f64" + }, + { + "name": "BgpCommunity", + "value": "10100", + "typ": "u16" + }, + { + "name": "Unused", + "value": "0", + "typ": "u16" + }, + { + "name": "Status", + "value": "1", + "typ": "u8" + }, + { + "name": "Code", + "value": "xams", + "typ": "string" + }, + { + "name": "Name", + "value": "Amsterdam IX", + "typ": "string" + }, + { + "name": "ReferenceCount", + "value": "5", + "typ": "u32" + }, + { + "name": "Device1Pk", + "value": "4JGygGZ8nyFz1ttjvj3ssQXCgUgmbYzRcW7WfZCmKRPu", + "typ": "pubkey" + }, + { + "name": "Device2Pk", + "value": "4NBPKmHDgokhRb1iSEKKmLGRwVFYJW6LCNRQAkWm3E4F", + "typ": "pubkey" + } + ] +} \ No newline at end of file diff --git a/sdk/serviceability/testdata/fixtures/generate-fixtures/Cargo.lock b/sdk/serviceability/testdata/fixtures/generate-fixtures/Cargo.lock new file mode 100644 index 000000000..e0f49004b --- /dev/null +++ b/sdk/serviceability/testdata/fixtures/generate-fixtures/Cargo.lock @@ -0,0 +1,1960 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "blake3" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "borsh" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115e54d64eb62cdebad391c19efc9dce4981c690c85a33a12199d99bb9546fee" +dependencies = [ + "borsh-derive 0.10.4", + "hashbrown 0.13.2", +] + +[[package]] +name = "borsh" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +dependencies = [ + "borsh-derive 1.6.0", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831213f80d9423998dd696e2c5345aba6be7a0bd8cd19e31c5243e13df1cef89" +dependencies = [ + "borsh-derive-internal", + "borsh-schema-derive-internal", + "proc-macro-crate 0.1.5", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "borsh-derive" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +dependencies = [ + "once_cell", + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "borsh-derive-internal" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65d6ba50644c98714aa2a70d13d7df3cd75cd2b523a2b452bf010443800976b3" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "borsh-incremental" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0faa79093f85698e0075c813bf87c52044e832e1f9baa5cb0f126e4c2b2d29dd" +dependencies = [ + "borsh 1.6.0", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "borsh-schema-derive-internal" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276691d96f063427be83e6692b86148e488ebba9f48f77788724ca027ba3b6d4" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8834bb1d8ee5dc048ee3124f2c7c1afcc6bc9aed03f11e9dfd8c69470a5db340" +dependencies = [ + "feature-probe", + "serde", +] + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.2.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "console_log" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89f72f65e8501878b8a004d5a1afb780987e2ce2b4532c562e367a72c57499f" +dependencies = [ + "log", + "web-sys", +] + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rand_core 0.6.4", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common", + "subtle", +] + +[[package]] +name = "doublezero-program-common" +version = "0.8.4" +dependencies = [ + "borsh 1.6.0", + "byteorder", + "ipnetwork", + "serde", + "solana-program", +] + +[[package]] +name = "doublezero-serviceability" +version = "0.8.4" +dependencies = [ + "bitflags", + "borsh 1.6.0", + "borsh-incremental", + "bytemuck", + "doublezero-program-common", + "ipnetwork", + "solana-program", + "thiserror", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "feature-probe" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "five8" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75b8549488b4715defcb0d8a8a1c1c76a80661b5fa106b4ca0e7fce59d7d875" +dependencies = [ + "five8_core", +] + +[[package]] +name = "five8_const" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26dec3da8bc3ef08f2c04f61eab298c3ab334523e55f076354d6d6f613799a7b" +dependencies = [ + "five8_core", +] + +[[package]] +name = "five8_core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2551bf44bc5f776c15044b9b94153a00198be06743e262afaaa61f11ac7523a5" + +[[package]] +name = "generate-serviceability-fixtures" +version = "0.0.0" +dependencies = [ + "borsh 1.6.0", + "doublezero-serviceability", + "serde", + "serde_json", + "solana-program", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + +[[package]] +name = "ipnetwork" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf370abdafd54d13e54a620e8c3e1145f28e46cc9d704bc6d94414559df41763" + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "libsecp256k1" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9d220bc1feda2ac231cb78c3d26f27676b8cf82c96971f7aeef3d0cf2797c73" +dependencies = [ + "arrayref", + "base64 0.12.3", + "digest 0.9.0", + "libsecp256k1-core", + "libsecp256k1-gen-ecmult", + "libsecp256k1-gen-genmult", + "rand 0.7.3", + "serde", + "sha2 0.9.9", +] + +[[package]] +name = "libsecp256k1-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f6ab710cec28cef759c5f18671a27dae2a5f952cdaaee1d8e2908cb2478a80" +dependencies = [ + "crunchy", + "digest 0.9.0", + "subtle", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccab96b584d38fac86a83f07e659f0deafd0253dc096dab5a36d53efe653c5c3" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67abfe149395e3aa1c48a2beb32b068e2334402df8181f818d3aee2b304c4f5d" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest 0.10.7", + "keccak", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "solana-account" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f949fe4edaeaea78c844023bfc1c898e0b1f5a100f8a8d2d0f85d0a7b090258" +dependencies = [ + "solana-account-info", + "solana-clock", + "solana-instruction", + "solana-pubkey", + "solana-sdk-ids", +] + +[[package]] +name = "solana-account-info" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8f5152a288ef1912300fc6efa6c2d1f9bb55d9398eb6c72326360b8063987da" +dependencies = [ + "bincode", + "serde", + "solana-program-error", + "solana-program-memory", + "solana-pubkey", +] + +[[package]] +name = "solana-address-lookup-table-interface" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1673f67efe870b64a65cb39e6194be5b26527691ce5922909939961a6e6b395" +dependencies = [ + "bincode", + "bytemuck", + "serde", + "serde_derive", + "solana-clock", + "solana-instruction", + "solana-pubkey", + "solana-sdk-ids", + "solana-slot-hashes", +] + +[[package]] +name = "solana-atomic-u64" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52e52720efe60465b052b9e7445a01c17550666beec855cce66f44766697bc2" +dependencies = [ + "parking_lot", +] + +[[package]] +name = "solana-big-mod-exp" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75db7f2bbac3e62cfd139065d15bcda9e2428883ba61fc8d27ccb251081e7567" +dependencies = [ + "num-bigint", + "num-traits", + "solana-define-syscall", +] + +[[package]] +name = "solana-bincode" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a3787b8cf9c9fe3dd360800e8b70982b9e5a8af9e11c354b6665dd4a003adc" +dependencies = [ + "bincode", + "serde", + "solana-instruction", +] + +[[package]] +name = "solana-blake3-hasher" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0801e25a1b31a14494fc80882a036be0ffd290efc4c2d640bfcca120a4672" +dependencies = [ + "blake3", + "solana-define-syscall", + "solana-hash", + "solana-sanitize", +] + +[[package]] +name = "solana-borsh" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "718333bcd0a1a7aed6655aa66bef8d7fb047944922b2d3a18f49cbc13e73d004" +dependencies = [ + "borsh 0.10.4", + "borsh 1.6.0", +] + +[[package]] +name = "solana-clock" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bb482ab70fced82ad3d7d3d87be33d466a3498eb8aa856434ff3c0dfc2e2e31" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-cpi" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc71126edddc2ba014622fc32d0f5e2e78ec6c5a1e0eb511b85618c09e9ea11" +dependencies = [ + "solana-account-info", + "solana-define-syscall", + "solana-instruction", + "solana-program-error", + "solana-pubkey", + "solana-stable-layout", +] + +[[package]] +name = "solana-decode-error" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c781686a18db2f942e70913f7ca15dc120ec38dcab42ff7557db2c70c625a35" +dependencies = [ + "num-traits", +] + +[[package]] +name = "solana-define-syscall" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae3e2abcf541c8122eafe9a625d4d194b4023c20adde1e251f94e056bb1aee2" + +[[package]] +name = "solana-epoch-rewards" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b575d3dd323b9ea10bb6fe89bf6bf93e249b215ba8ed7f68f1a3633f384db7" +dependencies = [ + "serde", + "serde_derive", + "solana-hash", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-epoch-schedule" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fce071fbddecc55d727b1d7ed16a629afe4f6e4c217bc8d00af3b785f6f67ed" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-example-mocks" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84461d56cbb8bb8d539347151e0525b53910102e4bced875d49d5139708e39d3" +dependencies = [ + "serde", + "serde_derive", + "solana-address-lookup-table-interface", + "solana-clock", + "solana-hash", + "solana-instruction", + "solana-keccak-hasher", + "solana-message", + "solana-nonce", + "solana-pubkey", + "solana-sdk-ids", + "solana-system-interface", + "thiserror", +] + +[[package]] +name = "solana-feature-gate-interface" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f5c5382b449e8e4e3016fb05e418c53d57782d8b5c30aa372fc265654b956d" +dependencies = [ + "bincode", + "serde", + "serde_derive", + "solana-account", + "solana-account-info", + "solana-instruction", + "solana-program-error", + "solana-pubkey", + "solana-rent", + "solana-sdk-ids", + "solana-system-interface", +] + +[[package]] +name = "solana-fee-calculator" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89bc408da0fb3812bc3008189d148b4d3e08252c79ad810b245482a3f70cd8d" +dependencies = [ + "log", + "serde", + "serde_derive", +] + +[[package]] +name = "solana-hash" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b96e9f0300fa287b545613f007dfe20043d7812bee255f418c1eb649c93b63" +dependencies = [ + "borsh 1.6.0", + "bytemuck", + "bytemuck_derive", + "five8", + "js-sys", + "serde", + "serde_derive", + "solana-atomic-u64", + "solana-sanitize", + "wasm-bindgen", +] + +[[package]] +name = "solana-instruction" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab5682934bd1f65f8d2c16f21cb532526fcc1a09f796e2cacdb091eee5774ad" +dependencies = [ + "bincode", + "borsh 1.6.0", + "getrandom 0.2.17", + "js-sys", + "num-traits", + "serde", + "serde_derive", + "serde_json", + "solana-define-syscall", + "solana-pubkey", + "wasm-bindgen", +] + +[[package]] +name = "solana-instructions-sysvar" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0e85a6fad5c2d0c4f5b91d34b8ca47118fc593af706e523cdbedf846a954f57" +dependencies = [ + "bitflags", + "solana-account-info", + "solana-instruction", + "solana-program-error", + "solana-pubkey", + "solana-sanitize", + "solana-sdk-ids", + "solana-serialize-utils", + "solana-sysvar-id", +] + +[[package]] +name = "solana-keccak-hasher" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7aeb957fbd42a451b99235df4942d96db7ef678e8d5061ef34c9b34cae12f79" +dependencies = [ + "sha3", + "solana-define-syscall", + "solana-hash", + "solana-sanitize", +] + +[[package]] +name = "solana-last-restart-slot" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6360ac2fdc72e7463565cd256eedcf10d7ef0c28a1249d261ec168c1b55cdd" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-loader-v2-interface" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8ab08006dad78ae7cd30df8eea0539e207d08d91eaefb3e1d49a446e1c49654" +dependencies = [ + "serde", + "serde_bytes", + "serde_derive", + "solana-instruction", + "solana-pubkey", + "solana-sdk-ids", +] + +[[package]] +name = "solana-loader-v3-interface" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f7162a05b8b0773156b443bccd674ea78bb9aa406325b467ea78c06c99a63a2" +dependencies = [ + "serde", + "serde_bytes", + "serde_derive", + "solana-instruction", + "solana-pubkey", + "solana-sdk-ids", + "solana-system-interface", +] + +[[package]] +name = "solana-loader-v4-interface" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "706a777242f1f39a83e2a96a2a6cb034cb41169c6ecbee2cf09cb873d9659e7e" +dependencies = [ + "serde", + "serde_bytes", + "serde_derive", + "solana-instruction", + "solana-pubkey", + "solana-sdk-ids", + "solana-system-interface", +] + +[[package]] +name = "solana-message" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1796aabce376ff74bf89b78d268fa5e683d7d7a96a0a4e4813ec34de49d5314b" +dependencies = [ + "bincode", + "blake3", + "lazy_static", + "serde", + "serde_derive", + "solana-bincode", + "solana-hash", + "solana-instruction", + "solana-pubkey", + "solana-sanitize", + "solana-sdk-ids", + "solana-short-vec", + "solana-system-interface", + "solana-transaction-error", + "wasm-bindgen", +] + +[[package]] +name = "solana-msg" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36a1a14399afaabc2781a1db09cb14ee4cc4ee5c7a5a3cfcc601811379a8092" +dependencies = [ + "solana-define-syscall", +] + +[[package]] +name = "solana-native-token" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61515b880c36974053dd499c0510066783f0cc6ac17def0c7ef2a244874cf4a9" + +[[package]] +name = "solana-nonce" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703e22eb185537e06204a5bd9d509b948f0066f2d1d814a6f475dafb3ddf1325" +dependencies = [ + "serde", + "serde_derive", + "solana-fee-calculator", + "solana-hash", + "solana-pubkey", + "solana-sha256-hasher", +] + +[[package]] +name = "solana-program" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98eca145bd3545e2fbb07166e895370576e47a00a7d824e325390d33bf467210" +dependencies = [ + "bincode", + "blake3", + "borsh 0.10.4", + "borsh 1.6.0", + "bs58", + "bytemuck", + "console_error_panic_hook", + "console_log", + "getrandom 0.2.17", + "lazy_static", + "log", + "memoffset", + "num-bigint", + "num-derive", + "num-traits", + "rand 0.8.5", + "serde", + "serde_bytes", + "serde_derive", + "solana-account-info", + "solana-address-lookup-table-interface", + "solana-atomic-u64", + "solana-big-mod-exp", + "solana-bincode", + "solana-blake3-hasher", + "solana-borsh", + "solana-clock", + "solana-cpi", + "solana-decode-error", + "solana-define-syscall", + "solana-epoch-rewards", + "solana-epoch-schedule", + "solana-example-mocks", + "solana-feature-gate-interface", + "solana-fee-calculator", + "solana-hash", + "solana-instruction", + "solana-instructions-sysvar", + "solana-keccak-hasher", + "solana-last-restart-slot", + "solana-loader-v2-interface", + "solana-loader-v3-interface", + "solana-loader-v4-interface", + "solana-message", + "solana-msg", + "solana-native-token", + "solana-nonce", + "solana-program-entrypoint", + "solana-program-error", + "solana-program-memory", + "solana-program-option", + "solana-program-pack", + "solana-pubkey", + "solana-rent", + "solana-sanitize", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-secp256k1-recover", + "solana-serde-varint", + "solana-serialize-utils", + "solana-sha256-hasher", + "solana-short-vec", + "solana-slot-hashes", + "solana-slot-history", + "solana-stable-layout", + "solana-stake-interface", + "solana-system-interface", + "solana-sysvar", + "solana-sysvar-id", + "solana-vote-interface", + "thiserror", + "wasm-bindgen", +] + +[[package]] +name = "solana-program-entrypoint" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32ce041b1a0ed275290a5008ee1a4a6c48f5054c8a3d78d313c08958a06aedbd" +dependencies = [ + "solana-account-info", + "solana-msg", + "solana-program-error", + "solana-pubkey", +] + +[[package]] +name = "solana-program-error" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ee2e0217d642e2ea4bee237f37bd61bb02aec60da3647c48ff88f6556ade775" +dependencies = [ + "borsh 1.6.0", + "num-traits", + "serde", + "serde_derive", + "solana-decode-error", + "solana-instruction", + "solana-msg", + "solana-pubkey", +] + +[[package]] +name = "solana-program-memory" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a5426090c6f3fd6cfdc10685322fede9ca8e5af43cd6a59e98bfe4e91671712" +dependencies = [ + "solana-define-syscall", +] + +[[package]] +name = "solana-program-option" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc677a2e9bc616eda6dbdab834d463372b92848b2bfe4a1ed4e4b4adba3397d0" + +[[package]] +name = "solana-program-pack" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "319f0ef15e6e12dc37c597faccb7d62525a509fec5f6975ecb9419efddeb277b" +dependencies = [ + "solana-program-error", +] + +[[package]] +name = "solana-pubkey" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b62adb9c3261a052ca1f999398c388f1daf558a1b492f60a6d9e64857db4ff1" +dependencies = [ + "borsh 0.10.4", + "borsh 1.6.0", + "bytemuck", + "bytemuck_derive", + "curve25519-dalek", + "five8", + "five8_const", + "getrandom 0.2.17", + "js-sys", + "num-traits", + "serde", + "serde_derive", + "solana-atomic-u64", + "solana-decode-error", + "solana-define-syscall", + "solana-sanitize", + "solana-sha256-hasher", + "wasm-bindgen", +] + +[[package]] +name = "solana-rent" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1aea8fdea9de98ca6e8c2da5827707fb3842833521b528a713810ca685d2480" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-sanitize" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61f1bc1357b8188d9c4a3af3fc55276e56987265eb7ad073ae6f8180ee54cecf" + +[[package]] +name = "solana-sdk-ids" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5d8b9cc68d5c88b062a33e23a6466722467dde0035152d8fb1afbcdf350a5f" +dependencies = [ + "solana-pubkey", +] + +[[package]] +name = "solana-sdk-macro" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86280da8b99d03560f6ab5aca9de2e38805681df34e0bb8f238e69b29433b9df" +dependencies = [ + "bs58", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "solana-secp256k1-recover" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa3120b6cdaa270f39444f5093a90a7b03d296d362878f7a6991d6de3bbe496" +dependencies = [ + "libsecp256k1", + "solana-define-syscall", + "thiserror", +] + +[[package]] +name = "solana-serde-varint" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a7e155eba458ecfb0107b98236088c3764a09ddf0201ec29e52a0be40857113" +dependencies = [ + "serde", +] + +[[package]] +name = "solana-serialize-utils" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "817a284b63197d2b27afdba829c5ab34231da4a9b4e763466a003c40ca4f535e" +dependencies = [ + "solana-instruction", + "solana-pubkey", + "solana-sanitize", +] + +[[package]] +name = "solana-sha256-hasher" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa3feb32c28765f6aa1ce8f3feac30936f16c5c3f7eb73d63a5b8f6f8ecdc44" +dependencies = [ + "sha2 0.10.9", + "solana-define-syscall", + "solana-hash", +] + +[[package]] +name = "solana-short-vec" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c54c66f19b9766a56fa0057d060de8378676cb64987533fa088861858fc5a69" +dependencies = [ + "serde", +] + +[[package]] +name = "solana-slot-hashes" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8691982114513763e88d04094c9caa0376b867a29577939011331134c301ce" +dependencies = [ + "serde", + "serde_derive", + "solana-hash", + "solana-sdk-ids", + "solana-sysvar-id", +] + +[[package]] +name = "solana-slot-history" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ccc1b2067ca22754d5283afb2b0126d61eae734fc616d23871b0943b0d935e" +dependencies = [ + "bv", + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sysvar-id", +] + +[[package]] +name = "solana-stable-layout" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f14f7d02af8f2bc1b5efeeae71bc1c2b7f0f65cd75bcc7d8180f2c762a57f54" +dependencies = [ + "solana-instruction", + "solana-pubkey", +] + +[[package]] +name = "solana-stake-interface" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5269e89fde216b4d7e1d1739cf5303f8398a1ff372a81232abbee80e554a838c" +dependencies = [ + "borsh 0.10.4", + "borsh 1.6.0", + "num-traits", + "serde", + "serde_derive", + "solana-clock", + "solana-cpi", + "solana-decode-error", + "solana-instruction", + "solana-program-error", + "solana-pubkey", + "solana-system-interface", + "solana-sysvar-id", +] + +[[package]] +name = "solana-system-interface" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94d7c18cb1a91c6be5f5a8ac9276a1d7c737e39a21beba9ea710ab4b9c63bc90" +dependencies = [ + "js-sys", + "num-traits", + "serde", + "serde_derive", + "solana-decode-error", + "solana-instruction", + "solana-pubkey", + "wasm-bindgen", +] + +[[package]] +name = "solana-sysvar" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c3595f95069f3d90f275bb9bd235a1973c4d059028b0a7f81baca2703815db" +dependencies = [ + "base64 0.22.1", + "bincode", + "bytemuck", + "bytemuck_derive", + "lazy_static", + "serde", + "serde_derive", + "solana-account-info", + "solana-clock", + "solana-define-syscall", + "solana-epoch-rewards", + "solana-epoch-schedule", + "solana-fee-calculator", + "solana-hash", + "solana-instruction", + "solana-instructions-sysvar", + "solana-last-restart-slot", + "solana-program-entrypoint", + "solana-program-error", + "solana-program-memory", + "solana-pubkey", + "solana-rent", + "solana-sanitize", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-slot-hashes", + "solana-slot-history", + "solana-stake-interface", + "solana-sysvar-id", +] + +[[package]] +name = "solana-sysvar-id" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5762b273d3325b047cfda250787f8d796d781746860d5d0a746ee29f3e8812c1" +dependencies = [ + "solana-pubkey", + "solana-sdk-ids", +] + +[[package]] +name = "solana-transaction-error" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a9dc8fdb61c6088baab34fc3a8b8473a03a7a5fd404ed8dd502fa79b67cb1" +dependencies = [ + "solana-instruction", + "solana-sanitize", +] + +[[package]] +name = "solana-vote-interface" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b80d57478d6599d30acc31cc5ae7f93ec2361a06aefe8ea79bc81739a08af4c3" +dependencies = [ + "bincode", + "num-derive", + "num-traits", + "serde", + "serde_derive", + "solana-clock", + "solana-decode-error", + "solana-hash", + "solana-instruction", + "solana-pubkey", + "solana-rent", + "solana-sdk-ids", + "solana-serde-varint", + "solana-serialize-utils", + "solana-short-vec", + "solana-system-interface", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ + "winnow", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.114", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "zerocopy" +version = "0.8.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7456cf00f0685ad319c5b1693f291a650eaf345e941d082fc4e03df8a03996ac" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1328722bbf2115db7e19d69ebcc15e795719e2d66b60827c6a69a117365e37a0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zmij" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1966f8ac2c1f76987d69a74d0e0f929241c10e78136434e3be70ff7f58f64214" diff --git a/sdk/serviceability/testdata/fixtures/generate-fixtures/Cargo.toml b/sdk/serviceability/testdata/fixtures/generate-fixtures/Cargo.toml new file mode 100644 index 000000000..112e2de3c --- /dev/null +++ b/sdk/serviceability/testdata/fixtures/generate-fixtures/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "generate-serviceability-fixtures" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies] +doublezero-serviceability = { path = "../../../../../smartcontract/programs/doublezero-serviceability", features = ["no-entrypoint"] } +borsh = "1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +solana-program = "2" diff --git a/sdk/serviceability/testdata/fixtures/generate-fixtures/src/main.rs b/sdk/serviceability/testdata/fixtures/generate-fixtures/src/main.rs new file mode 100644 index 000000000..a53a70e5d --- /dev/null +++ b/sdk/serviceability/testdata/fixtures/generate-fixtures/src/main.rs @@ -0,0 +1,703 @@ +//! Generates Borsh-serialized binary fixture files from the Rust serviceability structs +//! with known field values. The Go/TypeScript/Python SDK compatibility tests deserialize +//! these fixtures and verify that field values match. +//! +//! Run with: cargo run (from this directory) +//! Output: ../fixtures/*.bin and ../fixtures/*.json +//! +//! Key difference from revdist fixtures: these use Borsh serialization (not repr(C)/bytemuck), +//! and the 1-byte AccountType discriminator is the first byte of the Borsh serialization itself +//! (no separate 8-byte discriminator prefix). + +use std::fs; +use std::net::Ipv4Addr; +use std::path::Path; + +use doublezero_serviceability::programversion::ProgramVersion; +use doublezero_serviceability::state::{ + accesspass::{AccessPass, AccessPassStatus, AccessPassType}, + accounttype::AccountType, + contributor::{Contributor, ContributorStatus}, + device::{Device, DeviceDesiredStatus, DeviceHealth, DeviceStatus, DeviceType}, + exchange::{Exchange, ExchangeStatus}, + globalconfig::GlobalConfig, + globalstate::GlobalState, + interface::{ + Interface, InterfaceCYOA, InterfaceDIA, InterfaceStatus, InterfaceType, InterfaceV1, + InterfaceV2, LoopbackType, RoutingMode, + }, + link::{Link, LinkDesiredStatus, LinkHealth, LinkLinkType, LinkStatus}, + location::{Location, LocationStatus}, + multicastgroup::{MulticastGroup, MulticastGroupStatus}, + programconfig::ProgramConfig, + user::{User, UserCYOA, UserStatus, UserType}, +}; +use serde::Serialize; + + +#[derive(Serialize)] +struct FixtureMeta { + name: String, + account_type: u8, + fields: Vec, +} + +#[derive(Serialize)] +struct FieldValue { + name: String, + value: String, + #[serde(rename = "typ")] + typ: String, +} + +fn pubkey_from_byte(b: u8) -> solana_program::pubkey::Pubkey { + let mut bytes = [0u8; 32]; + bytes[0] = b; + solana_program::pubkey::Pubkey::new_from_array(bytes) +} + +fn pubkey_bs58(pk: &solana_program::pubkey::Pubkey) -> String { + pk.to_string() +} + +fn write_fixture(dir: &Path, name: &str, data: &[u8], meta: &FixtureMeta) { + fs::write(dir.join(format!("{name}.bin")), data).unwrap(); + let json = serde_json::to_string_pretty(meta).unwrap(); + fs::write(dir.join(format!("{name}.json")), json).unwrap(); + println!("wrote {name}.bin ({} bytes) and {name}.json", data.len()); +} + +fn main() { + let fixtures_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join(".."); + fs::create_dir_all(&fixtures_dir).unwrap(); + + generate_global_state(&fixtures_dir); + generate_global_config(&fixtures_dir); + generate_location(&fixtures_dir); + generate_exchange(&fixtures_dir); + generate_device(&fixtures_dir); + generate_link(&fixtures_dir); + generate_user(&fixtures_dir); + generate_multicast_group(&fixtures_dir); + generate_program_config(&fixtures_dir); + generate_contributor(&fixtures_dir); + generate_access_pass(&fixtures_dir); + generate_access_pass_validator(&fixtures_dir); + + println!(" +all fixtures generated in {}", fixtures_dir.display()); +} + +fn generate_global_state(dir: &Path) { + let foundation_pk = pubkey_from_byte(0x01); + let activator_pk = pubkey_from_byte(0x02); + let sentinel_pk = pubkey_from_byte(0x03); + let health_oracle_pk = pubkey_from_byte(0x04); + let qa_pk = pubkey_from_byte(0x05); + + let val = GlobalState { + account_type: AccountType::GlobalState, + bump_seed: 254, + account_index: 42, + foundation_allowlist: vec![foundation_pk], + _device_allowlist: vec![], + _user_allowlist: vec![], + activator_authority_pk: activator_pk, + sentinel_authority_pk: sentinel_pk, + contributor_airdrop_lamports: 1_000_000_000, + user_airdrop_lamports: 50_000, + health_oracle_pk, + qa_allowlist: vec![qa_pk], + }; + + let data = borsh::to_vec(&val).unwrap(); + + let meta = FixtureMeta { + name: "GlobalState".into(), + account_type: 1, + fields: vec![ + FieldValue { name: "AccountType".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "BumpSeed".into(), value: "254".into(), typ: "u8".into() }, + FieldValue { name: "AccountIndex".into(), value: "42".into(), typ: "u128".into() }, + FieldValue { name: "FoundationAllowlistLen".into(), value: "1".into(), typ: "u32".into() }, + FieldValue { name: "FoundationAllowlist0".into(), value: pubkey_bs58(&foundation_pk), typ: "pubkey".into() }, + FieldValue { name: "DeviceAllowlistLen".into(), value: "0".into(), typ: "u32".into() }, + FieldValue { name: "UserAllowlistLen".into(), value: "0".into(), typ: "u32".into() }, + FieldValue { name: "ActivatorAuthorityPk".into(), value: pubkey_bs58(&activator_pk), typ: "pubkey".into() }, + FieldValue { name: "SentinelAuthorityPk".into(), value: pubkey_bs58(&sentinel_pk), typ: "pubkey".into() }, + FieldValue { name: "ContributorAirdropLamports".into(), value: "1000000000".into(), typ: "u64".into() }, + FieldValue { name: "UserAirdropLamports".into(), value: "50000".into(), typ: "u64".into() }, + FieldValue { name: "HealthOraclePk".into(), value: pubkey_bs58(&health_oracle_pk), typ: "pubkey".into() }, + FieldValue { name: "QaAllowlistLen".into(), value: "1".into(), typ: "u32".into() }, + FieldValue { name: "QaAllowlist0".into(), value: pubkey_bs58(&qa_pk), typ: "pubkey".into() }, + ], + }; + + write_fixture(dir, "global_state", &data, &meta); +} + +fn generate_global_config(dir: &Path) { + let owner = pubkey_from_byte(0x10); + + let val = GlobalConfig { + account_type: AccountType::GlobalConfig, + owner, + bump_seed: 253, + local_asn: 65000, + remote_asn: 65001, + device_tunnel_block: "10.100.0.0/16".parse().unwrap(), + user_tunnel_block: "10.200.0.0/16".parse().unwrap(), + multicastgroup_block: "239.0.0.0/8".parse().unwrap(), + next_bgp_community: 10042, + }; + + let data = borsh::to_vec(&val).unwrap(); + + let meta = FixtureMeta { + name: "GlobalConfig".into(), + account_type: 2, + fields: vec![ + FieldValue { name: "AccountType".into(), value: "2".into(), typ: "u8".into() }, + FieldValue { name: "Owner".into(), value: pubkey_bs58(&owner), typ: "pubkey".into() }, + FieldValue { name: "BumpSeed".into(), value: "253".into(), typ: "u8".into() }, + FieldValue { name: "LocalAsn".into(), value: "65000".into(), typ: "u32".into() }, + FieldValue { name: "RemoteAsn".into(), value: "65001".into(), typ: "u32".into() }, + FieldValue { name: "DeviceTunnelBlock".into(), value: "10.100.0.0/16".into(), typ: "networkv4".into() }, + FieldValue { name: "UserTunnelBlock".into(), value: "10.200.0.0/16".into(), typ: "networkv4".into() }, + FieldValue { name: "MulticastGroupBlock".into(), value: "239.0.0.0/8".into(), typ: "networkv4".into() }, + FieldValue { name: "NextBgpCommunity".into(), value: "10042".into(), typ: "u16".into() }, + ], + }; + + write_fixture(dir, "global_config", &data, &meta); +} + +fn generate_location(dir: &Path) { + let owner = pubkey_from_byte(0x20); + + let val = Location { + account_type: AccountType::Location, + owner, + index: 4, + bump_seed: 252, + lat: 52.3676, + lng: 4.9041, + loc_id: 4818, + status: LocationStatus::Activated, + code: "ams".into(), + name: "Amsterdam".into(), + country: "NL".into(), + reference_count: 3, + }; + + let data = borsh::to_vec(&val).unwrap(); + + let meta = FixtureMeta { + name: "Location".into(), + account_type: 3, + fields: vec![ + FieldValue { name: "AccountType".into(), value: "3".into(), typ: "u8".into() }, + FieldValue { name: "Owner".into(), value: pubkey_bs58(&owner), typ: "pubkey".into() }, + FieldValue { name: "Index".into(), value: "4".into(), typ: "u128".into() }, + FieldValue { name: "BumpSeed".into(), value: "252".into(), typ: "u8".into() }, + FieldValue { name: "Lat".into(), value: "52.3676".into(), typ: "f64".into() }, + FieldValue { name: "Lng".into(), value: "4.9041".into(), typ: "f64".into() }, + FieldValue { name: "LocId".into(), value: "4818".into(), typ: "u32".into() }, + FieldValue { name: "Status".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "Code".into(), value: "ams".into(), typ: "string".into() }, + FieldValue { name: "Name".into(), value: "Amsterdam".into(), typ: "string".into() }, + FieldValue { name: "Country".into(), value: "NL".into(), typ: "string".into() }, + FieldValue { name: "ReferenceCount".into(), value: "3".into(), typ: "u32".into() }, + ], + }; + + write_fixture(dir, "location", &data, &meta); +} + +fn generate_exchange(dir: &Path) { + let owner = pubkey_from_byte(0x30); + let device1_pk = pubkey_from_byte(0x31); + let device2_pk = pubkey_from_byte(0x32); + + let val = Exchange { + account_type: AccountType::Exchange, + owner, + index: 12, + bump_seed: 251, + lat: 52.3676, + lng: 4.9041, + bgp_community: 10100, + unused: 0, + status: ExchangeStatus::Activated, + code: "xams".into(), + name: "Amsterdam IX".into(), + reference_count: 5, + device1_pk, + device2_pk, + }; + + let data = borsh::to_vec(&val).unwrap(); + + let meta = FixtureMeta { + name: "Exchange".into(), + account_type: 4, + fields: vec![ + FieldValue { name: "AccountType".into(), value: "4".into(), typ: "u8".into() }, + FieldValue { name: "Owner".into(), value: pubkey_bs58(&owner), typ: "pubkey".into() }, + FieldValue { name: "Index".into(), value: "12".into(), typ: "u128".into() }, + FieldValue { name: "BumpSeed".into(), value: "251".into(), typ: "u8".into() }, + FieldValue { name: "Lat".into(), value: "52.3676".into(), typ: "f64".into() }, + FieldValue { name: "Lng".into(), value: "4.9041".into(), typ: "f64".into() }, + FieldValue { name: "BgpCommunity".into(), value: "10100".into(), typ: "u16".into() }, + FieldValue { name: "Unused".into(), value: "0".into(), typ: "u16".into() }, + FieldValue { name: "Status".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "Code".into(), value: "xams".into(), typ: "string".into() }, + FieldValue { name: "Name".into(), value: "Amsterdam IX".into(), typ: "string".into() }, + FieldValue { name: "ReferenceCount".into(), value: "5".into(), typ: "u32".into() }, + FieldValue { name: "Device1Pk".into(), value: pubkey_bs58(&device1_pk), typ: "pubkey".into() }, + FieldValue { name: "Device2Pk".into(), value: pubkey_bs58(&device2_pk), typ: "pubkey".into() }, + ], + }; + + write_fixture(dir, "exchange", &data, &meta); +} + +fn generate_device(dir: &Path) { + let owner = pubkey_from_byte(0x40); + let location_pk = pubkey_from_byte(0x41); + let exchange_pk = pubkey_from_byte(0x42); + let metrics_publisher_pk = pubkey_from_byte(0x43); + let contributor_pk = pubkey_from_byte(0x44); + + let val = Device { + account_type: AccountType::Device, + owner, + index: 7, + bump_seed: 250, + location_pk, + exchange_pk, + device_type: DeviceType::Edge, + public_ip: Ipv4Addr::new(203, 0, 113, 1), + status: DeviceStatus::Activated, + code: "dz1".into(), + dz_prefixes: vec!["10.10.0.0/24".parse().unwrap()].into(), + metrics_publisher_pk, + contributor_pk, + mgmt_vrf: "mgmt".into(), + interfaces: vec![ + Interface::V1(InterfaceV1 { + status: InterfaceStatus::Activated, + name: "Loopback0".into(), + interface_type: InterfaceType::Loopback, + loopback_type: LoopbackType::Vpnv4, + vlan_id: 0, + ip_net: "10.0.0.1/32".parse().unwrap(), + node_segment_idx: 100, + user_tunnel_endpoint: false, + }), + Interface::V2(InterfaceV2 { + status: InterfaceStatus::Activated, + name: "Ethernet1".into(), + interface_type: InterfaceType::Physical, + interface_cyoa: InterfaceCYOA::GREOverDIA, + interface_dia: InterfaceDIA::DIA, + loopback_type: LoopbackType::None, + bandwidth: 10_000_000_000, + cir: 5_000_000_000, + mtu: 9000, + routing_mode: RoutingMode::BGP, + vlan_id: 100, + ip_net: "172.16.0.1/30".parse().unwrap(), + node_segment_idx: 200, + user_tunnel_endpoint: true, + }), + ], + reference_count: 12, + users_count: 5, + max_users: 100, + device_health: DeviceHealth::ReadyForUsers, + desired_status: DeviceDesiredStatus::Activated, + }; + + let data = borsh::to_vec(&val).unwrap(); + + let meta = FixtureMeta { + name: "Device".into(), + account_type: 5, + fields: vec![ + FieldValue { name: "AccountType".into(), value: "5".into(), typ: "u8".into() }, + FieldValue { name: "Owner".into(), value: pubkey_bs58(&owner), typ: "pubkey".into() }, + FieldValue { name: "Index".into(), value: "7".into(), typ: "u128".into() }, + FieldValue { name: "BumpSeed".into(), value: "250".into(), typ: "u8".into() }, + FieldValue { name: "LocationPk".into(), value: pubkey_bs58(&location_pk), typ: "pubkey".into() }, + FieldValue { name: "ExchangePk".into(), value: pubkey_bs58(&exchange_pk), typ: "pubkey".into() }, + FieldValue { name: "DeviceType".into(), value: "2".into(), typ: "u8".into() }, + FieldValue { name: "PublicIp".into(), value: "203.0.113.1".into(), typ: "ipv4".into() }, + FieldValue { name: "Status".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "Code".into(), value: "dz1".into(), typ: "string".into() }, + FieldValue { name: "DzPrefixesLen".into(), value: "1".into(), typ: "u32".into() }, + FieldValue { name: "DzPrefixes0".into(), value: "10.10.0.0/24".into(), typ: "networkv4".into() }, + FieldValue { name: "MetricsPublisherPk".into(), value: pubkey_bs58(&metrics_publisher_pk), typ: "pubkey".into() }, + FieldValue { name: "ContributorPk".into(), value: pubkey_bs58(&contributor_pk), typ: "pubkey".into() }, + FieldValue { name: "MgmtVrf".into(), value: "mgmt".into(), typ: "string".into() }, + FieldValue { name: "InterfacesLen".into(), value: "2".into(), typ: "u32".into() }, + // Interface 0 - V1 + FieldValue { name: "Interface0Version".into(), value: "0".into(), typ: "u8".into() }, + FieldValue { name: "Interface0Status".into(), value: "3".into(), typ: "u8".into() }, + FieldValue { name: "Interface0Name".into(), value: "Loopback0".into(), typ: "string".into() }, + FieldValue { name: "Interface0InterfaceType".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "Interface0LoopbackType".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "Interface0VlanId".into(), value: "0".into(), typ: "u16".into() }, + FieldValue { name: "Interface0IpNet".into(), value: "10.0.0.1/32".into(), typ: "networkv4".into() }, + FieldValue { name: "Interface0NodeSegmentIdx".into(), value: "100".into(), typ: "u16".into() }, + FieldValue { name: "Interface0UserTunnelEndpoint".into(), value: "false".into(), typ: "bool".into() }, + // Interface 1 - V2 + FieldValue { name: "Interface1Version".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "Interface1Status".into(), value: "3".into(), typ: "u8".into() }, + FieldValue { name: "Interface1Name".into(), value: "Ethernet1".into(), typ: "string".into() }, + FieldValue { name: "Interface1InterfaceType".into(), value: "2".into(), typ: "u8".into() }, + FieldValue { name: "Interface1InterfaceCYOA".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "Interface1InterfaceDIA".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "Interface1LoopbackType".into(), value: "0".into(), typ: "u8".into() }, + FieldValue { name: "Interface1Bandwidth".into(), value: "10000000000".into(), typ: "u64".into() }, + FieldValue { name: "Interface1Cir".into(), value: "5000000000".into(), typ: "u64".into() }, + FieldValue { name: "Interface1Mtu".into(), value: "9000".into(), typ: "u16".into() }, + FieldValue { name: "Interface1RoutingMode".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "Interface1VlanId".into(), value: "100".into(), typ: "u16".into() }, + FieldValue { name: "Interface1IpNet".into(), value: "172.16.0.1/30".into(), typ: "networkv4".into() }, + FieldValue { name: "Interface1NodeSegmentIdx".into(), value: "200".into(), typ: "u16".into() }, + FieldValue { name: "Interface1UserTunnelEndpoint".into(), value: "true".into(), typ: "bool".into() }, + FieldValue { name: "ReferenceCount".into(), value: "12".into(), typ: "u32".into() }, + FieldValue { name: "UsersCount".into(), value: "5".into(), typ: "u16".into() }, + FieldValue { name: "MaxUsers".into(), value: "100".into(), typ: "u16".into() }, + FieldValue { name: "DeviceHealth".into(), value: "3".into(), typ: "u8".into() }, + FieldValue { name: "DesiredStatus".into(), value: "1".into(), typ: "u8".into() }, + ], + }; + + write_fixture(dir, "device", &data, &meta); +} + +fn generate_link(dir: &Path) { + let owner = pubkey_from_byte(0x50); + let side_a_pk = pubkey_from_byte(0x51); + let side_z_pk = pubkey_from_byte(0x52); + let contributor_pk = pubkey_from_byte(0x53); + + let val = Link { + account_type: AccountType::Link, + owner, + index: 99, + bump_seed: 249, + side_a_pk, + side_z_pk, + link_type: LinkLinkType::WAN, + bandwidth: 10_000_000_000, + mtu: 9000, + delay_ns: 5_000_000, + jitter_ns: 100_000, + tunnel_id: 500, + tunnel_net: "169.254.1.0/30".parse().unwrap(), + status: LinkStatus::Activated, + code: "ams-fra".into(), + contributor_pk, + side_a_iface_name: "Ethernet2".into(), + side_z_iface_name: "Ethernet2".into(), + delay_override_ns: 0, + link_health: LinkHealth::ReadyForService, + desired_status: LinkDesiredStatus::Activated, + }; + + let data = borsh::to_vec(&val).unwrap(); + + let meta = FixtureMeta { + name: "Link".into(), + account_type: 6, + fields: vec![ + FieldValue { name: "AccountType".into(), value: "6".into(), typ: "u8".into() }, + FieldValue { name: "Owner".into(), value: pubkey_bs58(&owner), typ: "pubkey".into() }, + FieldValue { name: "Index".into(), value: "99".into(), typ: "u128".into() }, + FieldValue { name: "BumpSeed".into(), value: "249".into(), typ: "u8".into() }, + FieldValue { name: "SideAPk".into(), value: pubkey_bs58(&side_a_pk), typ: "pubkey".into() }, + FieldValue { name: "SideZPk".into(), value: pubkey_bs58(&side_z_pk), typ: "pubkey".into() }, + FieldValue { name: "LinkType".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "Bandwidth".into(), value: "10000000000".into(), typ: "u64".into() }, + FieldValue { name: "Mtu".into(), value: "9000".into(), typ: "u32".into() }, + FieldValue { name: "DelayNs".into(), value: "5000000".into(), typ: "u64".into() }, + FieldValue { name: "JitterNs".into(), value: "100000".into(), typ: "u64".into() }, + FieldValue { name: "TunnelId".into(), value: "500".into(), typ: "u16".into() }, + FieldValue { name: "TunnelNet".into(), value: "169.254.1.0/30".into(), typ: "networkv4".into() }, + FieldValue { name: "Status".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "Code".into(), value: "ams-fra".into(), typ: "string".into() }, + FieldValue { name: "ContributorPk".into(), value: pubkey_bs58(&contributor_pk), typ: "pubkey".into() }, + FieldValue { name: "SideAIfaceName".into(), value: "Ethernet2".into(), typ: "string".into() }, + FieldValue { name: "SideZIfaceName".into(), value: "Ethernet2".into(), typ: "string".into() }, + FieldValue { name: "DelayOverrideNs".into(), value: "0".into(), typ: "u64".into() }, + FieldValue { name: "LinkHealth".into(), value: "2".into(), typ: "u8".into() }, + FieldValue { name: "DesiredStatus".into(), value: "1".into(), typ: "u8".into() }, + ], + }; + + write_fixture(dir, "link", &data, &meta); +} + +fn generate_user(dir: &Path) { + let owner = pubkey_from_byte(0x60); + let tenant_pk = pubkey_from_byte(0x61); + let device_pk = pubkey_from_byte(0x62); + let publisher_pk = pubkey_from_byte(0x63); + let subscriber_pk = pubkey_from_byte(0x64); + let validator_pubkey = pubkey_from_byte(0x65); + + let val = User { + account_type: AccountType::User, + owner, + index: 200, + bump_seed: 248, + user_type: UserType::Multicast, + tenant_pk, + device_pk, + cyoa_type: UserCYOA::GREOverFabric, + client_ip: Ipv4Addr::new(198, 51, 100, 10), + dz_ip: Ipv4Addr::new(10, 200, 0, 1), + tunnel_id: 100, + tunnel_net: "169.254.100.0/30".parse().unwrap(), + status: UserStatus::Activated, + publishers: vec![publisher_pk], + subscribers: vec![subscriber_pk], + validator_pubkey, + }; + + let data = borsh::to_vec(&val).unwrap(); + + let meta = FixtureMeta { + name: "User".into(), + account_type: 7, + fields: vec![ + FieldValue { name: "AccountType".into(), value: "7".into(), typ: "u8".into() }, + FieldValue { name: "Owner".into(), value: pubkey_bs58(&owner), typ: "pubkey".into() }, + FieldValue { name: "Index".into(), value: "200".into(), typ: "u128".into() }, + FieldValue { name: "BumpSeed".into(), value: "248".into(), typ: "u8".into() }, + FieldValue { name: "UserType".into(), value: "3".into(), typ: "u8".into() }, + FieldValue { name: "TenantPk".into(), value: pubkey_bs58(&tenant_pk), typ: "pubkey".into() }, + FieldValue { name: "DevicePk".into(), value: pubkey_bs58(&device_pk), typ: "pubkey".into() }, + FieldValue { name: "CyoaType".into(), value: "2".into(), typ: "u8".into() }, + FieldValue { name: "ClientIp".into(), value: "198.51.100.10".into(), typ: "ipv4".into() }, + FieldValue { name: "DzIp".into(), value: "10.200.0.1".into(), typ: "ipv4".into() }, + FieldValue { name: "TunnelId".into(), value: "100".into(), typ: "u16".into() }, + FieldValue { name: "TunnelNet".into(), value: "169.254.100.0/30".into(), typ: "networkv4".into() }, + FieldValue { name: "Status".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "PublishersLen".into(), value: "1".into(), typ: "u32".into() }, + FieldValue { name: "Publishers0".into(), value: pubkey_bs58(&publisher_pk), typ: "pubkey".into() }, + FieldValue { name: "SubscribersLen".into(), value: "1".into(), typ: "u32".into() }, + FieldValue { name: "Subscribers0".into(), value: pubkey_bs58(&subscriber_pk), typ: "pubkey".into() }, + FieldValue { name: "ValidatorPubkey".into(), value: pubkey_bs58(&validator_pubkey), typ: "pubkey".into() }, + ], + }; + + write_fixture(dir, "user", &data, &meta); +} + +fn generate_multicast_group(dir: &Path) { + let owner = pubkey_from_byte(0x70); + let tenant_pk = pubkey_from_byte(0x71); + + let val = MulticastGroup { + account_type: AccountType::MulticastGroup, + owner, + index: 30, + bump_seed: 247, + tenant_pk, + multicast_ip: Ipv4Addr::new(239, 1, 1, 1), + max_bandwidth: 1_000_000_000, + status: MulticastGroupStatus::Activated, + code: "demo".into(), + publisher_count: 2, + subscriber_count: 10, + }; + + let data = borsh::to_vec(&val).unwrap(); + + let meta = FixtureMeta { + name: "MulticastGroup".into(), + account_type: 8, + fields: vec![ + FieldValue { name: "AccountType".into(), value: "8".into(), typ: "u8".into() }, + FieldValue { name: "Owner".into(), value: pubkey_bs58(&owner), typ: "pubkey".into() }, + FieldValue { name: "Index".into(), value: "30".into(), typ: "u128".into() }, + FieldValue { name: "BumpSeed".into(), value: "247".into(), typ: "u8".into() }, + FieldValue { name: "TenantPk".into(), value: pubkey_bs58(&tenant_pk), typ: "pubkey".into() }, + FieldValue { name: "MulticastIp".into(), value: "239.1.1.1".into(), typ: "ipv4".into() }, + FieldValue { name: "MaxBandwidth".into(), value: "1000000000".into(), typ: "u64".into() }, + FieldValue { name: "Status".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "Code".into(), value: "demo".into(), typ: "string".into() }, + FieldValue { name: "PublisherCount".into(), value: "2".into(), typ: "u32".into() }, + FieldValue { name: "SubscriberCount".into(), value: "10".into(), typ: "u32".into() }, + ], + }; + + write_fixture(dir, "multicast_group", &data, &meta); +} + +fn generate_program_config(dir: &Path) { + let val = ProgramConfig { + account_type: AccountType::ProgramConfig, + bump_seed: 246, + version: ProgramVersion { + major: 1, + minor: 2, + patch: 3, + }, + min_compatible_version: ProgramVersion { + major: 1, + minor: 0, + patch: 0, + }, + }; + + let data = borsh::to_vec(&val).unwrap(); + + let meta = FixtureMeta { + name: "ProgramConfig".into(), + account_type: 9, + fields: vec![ + FieldValue { name: "AccountType".into(), value: "9".into(), typ: "u8".into() }, + FieldValue { name: "BumpSeed".into(), value: "246".into(), typ: "u8".into() }, + FieldValue { name: "VersionMajor".into(), value: "1".into(), typ: "u32".into() }, + FieldValue { name: "VersionMinor".into(), value: "2".into(), typ: "u32".into() }, + FieldValue { name: "VersionPatch".into(), value: "3".into(), typ: "u32".into() }, + FieldValue { name: "MinCompatibleVersionMajor".into(), value: "1".into(), typ: "u32".into() }, + FieldValue { name: "MinCompatibleVersionMinor".into(), value: "0".into(), typ: "u32".into() }, + FieldValue { name: "MinCompatibleVersionPatch".into(), value: "0".into(), typ: "u32".into() }, + ], + }; + + write_fixture(dir, "program_config", &data, &meta); +} + +fn generate_contributor(dir: &Path) { + let owner = pubkey_from_byte(0x80); + let ops_manager_pk = pubkey_from_byte(0x81); + + let val = Contributor { + account_type: AccountType::Contributor, + owner, + index: 550, + bump_seed: 245, + status: ContributorStatus::Activated, + code: "co01".into(), + reference_count: 7, + ops_manager_pk, + }; + + let data = borsh::to_vec(&val).unwrap(); + + let meta = FixtureMeta { + name: "Contributor".into(), + account_type: 10, + fields: vec![ + FieldValue { name: "AccountType".into(), value: "10".into(), typ: "u8".into() }, + FieldValue { name: "Owner".into(), value: pubkey_bs58(&owner), typ: "pubkey".into() }, + FieldValue { name: "Index".into(), value: "550".into(), typ: "u128".into() }, + FieldValue { name: "BumpSeed".into(), value: "245".into(), typ: "u8".into() }, + FieldValue { name: "Status".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "Code".into(), value: "co01".into(), typ: "string".into() }, + FieldValue { name: "ReferenceCount".into(), value: "7".into(), typ: "u32".into() }, + FieldValue { name: "OpsManagerPk".into(), value: pubkey_bs58(&ops_manager_pk), typ: "pubkey".into() }, + ], + }; + + write_fixture(dir, "contributor", &data, &meta); +} + +fn generate_access_pass(dir: &Path) { + let owner = pubkey_from_byte(0x90); + let user_payer = pubkey_from_byte(0x91); + + let val = AccessPass { + account_type: AccountType::AccessPass, + owner, + bump_seed: 244, + accesspass_type: AccessPassType::Prepaid, + client_ip: Ipv4Addr::new(198, 51, 100, 20), + user_payer, + last_access_epoch: u64::MAX, + connection_count: 3, + status: AccessPassStatus::Connected, + mgroup_pub_allowlist: vec![], + mgroup_sub_allowlist: vec![], + flags: 0x01, + }; + + let data = borsh::to_vec(&val).unwrap(); + + let meta = FixtureMeta { + name: "AccessPass".into(), + account_type: 11, + fields: vec![ + FieldValue { name: "AccountType".into(), value: "11".into(), typ: "u8".into() }, + FieldValue { name: "Owner".into(), value: pubkey_bs58(&owner), typ: "pubkey".into() }, + FieldValue { name: "BumpSeed".into(), value: "244".into(), typ: "u8".into() }, + FieldValue { name: "AccessPassType".into(), value: "0".into(), typ: "u8".into() }, + FieldValue { name: "ClientIp".into(), value: "198.51.100.20".into(), typ: "ipv4".into() }, + FieldValue { name: "UserPayer".into(), value: pubkey_bs58(&user_payer), typ: "pubkey".into() }, + FieldValue { name: "LastAccessEpoch".into(), value: "18446744073709551615".into(), typ: "u64".into() }, + FieldValue { name: "ConnectionCount".into(), value: "3".into(), typ: "u16".into() }, + FieldValue { name: "Status".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "MgroupPubAllowlistLen".into(), value: "0".into(), typ: "u32".into() }, + FieldValue { name: "MgroupSubAllowlistLen".into(), value: "0".into(), typ: "u32".into() }, + FieldValue { name: "Flags".into(), value: "1".into(), typ: "u8".into() }, + ], + }; + + write_fixture(dir, "access_pass", &data, &meta); +} + +fn generate_access_pass_validator(dir: &Path) { + let owner = pubkey_from_byte(0xA0); + let user_payer = pubkey_from_byte(0xA1); + let validator_pk = pubkey_from_byte(0xA2); + let mgroup_pub = pubkey_from_byte(0xA3); + let mgroup_sub = pubkey_from_byte(0xA4); + + let val = AccessPass { + account_type: AccountType::AccessPass, + owner, + bump_seed: 243, + accesspass_type: AccessPassType::SolanaValidator(validator_pk), + client_ip: Ipv4Addr::new(10, 0, 0, 50), + user_payer, + last_access_epoch: 1000, + connection_count: 1, + status: AccessPassStatus::Connected, + mgroup_pub_allowlist: vec![mgroup_pub], + mgroup_sub_allowlist: vec![mgroup_sub], + flags: 0x03, + }; + + let data = borsh::to_vec(&val).unwrap(); + + let meta = FixtureMeta { + name: "AccessPassValidator".into(), + account_type: 11, + fields: vec![ + FieldValue { name: "AccountType".into(), value: "11".into(), typ: "u8".into() }, + FieldValue { name: "Owner".into(), value: pubkey_bs58(&owner), typ: "pubkey".into() }, + FieldValue { name: "BumpSeed".into(), value: "243".into(), typ: "u8".into() }, + FieldValue { name: "AccessPassType".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "AccessPassTypeValidatorPubkey".into(), value: pubkey_bs58(&validator_pk), typ: "pubkey".into() }, + FieldValue { name: "ClientIp".into(), value: "10.0.0.50".into(), typ: "ipv4".into() }, + FieldValue { name: "UserPayer".into(), value: pubkey_bs58(&user_payer), typ: "pubkey".into() }, + FieldValue { name: "LastAccessEpoch".into(), value: "1000".into(), typ: "u64".into() }, + FieldValue { name: "ConnectionCount".into(), value: "1".into(), typ: "u16".into() }, + FieldValue { name: "Status".into(), value: "1".into(), typ: "u8".into() }, + FieldValue { name: "MgroupPubAllowlistLen".into(), value: "1".into(), typ: "u32".into() }, + FieldValue { name: "MgroupPubAllowlist0".into(), value: pubkey_bs58(&mgroup_pub), typ: "pubkey".into() }, + FieldValue { name: "MgroupSubAllowlistLen".into(), value: "1".into(), typ: "u32".into() }, + FieldValue { name: "MgroupSubAllowlist0".into(), value: pubkey_bs58(&mgroup_sub), typ: "pubkey".into() }, + FieldValue { name: "Flags".into(), value: "3".into(), typ: "u8".into() }, + ], + }; + + write_fixture(dir, "access_pass_validator", &data, &meta); +} diff --git a/sdk/serviceability/testdata/fixtures/global_config.bin b/sdk/serviceability/testdata/fixtures/global_config.bin new file mode 100644 index 000000000..b56323f1b Binary files /dev/null and b/sdk/serviceability/testdata/fixtures/global_config.bin differ diff --git a/sdk/serviceability/testdata/fixtures/global_config.json b/sdk/serviceability/testdata/fixtures/global_config.json new file mode 100644 index 000000000..7128d2d13 --- /dev/null +++ b/sdk/serviceability/testdata/fixtures/global_config.json @@ -0,0 +1,51 @@ +{ + "name": "GlobalConfig", + "account_type": 2, + "fields": [ + { + "name": "AccountType", + "value": "2", + "typ": "u8" + }, + { + "name": "Owner", + "value": "25TXLvcMJNvRY4vb95G9Kpvf9A3LJCdWLswD47xvXsaX", + "typ": "pubkey" + }, + { + "name": "BumpSeed", + "value": "253", + "typ": "u8" + }, + { + "name": "LocalAsn", + "value": "65000", + "typ": "u32" + }, + { + "name": "RemoteAsn", + "value": "65001", + "typ": "u32" + }, + { + "name": "DeviceTunnelBlock", + "value": "10.100.0.0/16", + "typ": "networkv4" + }, + { + "name": "UserTunnelBlock", + "value": "10.200.0.0/16", + "typ": "networkv4" + }, + { + "name": "MulticastGroupBlock", + "value": "239.0.0.0/8", + "typ": "networkv4" + }, + { + "name": "NextBgpCommunity", + "value": "10042", + "typ": "u16" + } + ] +} \ No newline at end of file diff --git a/sdk/serviceability/testdata/fixtures/global_state.bin b/sdk/serviceability/testdata/fixtures/global_state.bin new file mode 100644 index 000000000..7d93f3087 Binary files /dev/null and b/sdk/serviceability/testdata/fixtures/global_state.bin differ diff --git a/sdk/serviceability/testdata/fixtures/global_state.json b/sdk/serviceability/testdata/fixtures/global_state.json new file mode 100644 index 000000000..0de99cd37 --- /dev/null +++ b/sdk/serviceability/testdata/fixtures/global_state.json @@ -0,0 +1,76 @@ +{ + "name": "GlobalState", + "account_type": 1, + "fields": [ + { + "name": "AccountType", + "value": "1", + "typ": "u8" + }, + { + "name": "BumpSeed", + "value": "254", + "typ": "u8" + }, + { + "name": "AccountIndex", + "value": "42", + "typ": "u128" + }, + { + "name": "FoundationAllowlistLen", + "value": "1", + "typ": "u32" + }, + { + "name": "FoundationAllowlist0", + "value": "4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM", + "typ": "pubkey" + }, + { + "name": "DeviceAllowlistLen", + "value": "0", + "typ": "u32" + }, + { + "name": "UserAllowlistLen", + "value": "0", + "typ": "u32" + }, + { + "name": "ActivatorAuthorityPk", + "value": "8opHzTAnfzRpPEx21XtnrVTX28YQuCpAjcn1PczScKh", + "typ": "pubkey" + }, + { + "name": "SentinelAuthorityPk", + "value": "CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3", + "typ": "pubkey" + }, + { + "name": "ContributorAirdropLamports", + "value": "1000000000", + "typ": "u64" + }, + { + "name": "UserAirdropLamports", + "value": "50000", + "typ": "u64" + }, + { + "name": "HealthOraclePk", + "value": "GcdayuLaLyrdmUu324nahyv33G5poQdLUEZ1nEytDeP", + "typ": "pubkey" + }, + { + "name": "QaAllowlistLen", + "value": "1", + "typ": "u32" + }, + { + "name": "QaAllowlist0", + "value": "LX3EUdRUBUa3TbsYXLEUdj9J3prXkWXvLYSWyYyc2Jj", + "typ": "pubkey" + } + ] +} \ No newline at end of file diff --git a/sdk/serviceability/testdata/fixtures/link.bin b/sdk/serviceability/testdata/fixtures/link.bin new file mode 100644 index 000000000..e988d5d64 Binary files /dev/null and b/sdk/serviceability/testdata/fixtures/link.bin differ diff --git a/sdk/serviceability/testdata/fixtures/link.json b/sdk/serviceability/testdata/fixtures/link.json new file mode 100644 index 000000000..b1f786b20 --- /dev/null +++ b/sdk/serviceability/testdata/fixtures/link.json @@ -0,0 +1,111 @@ +{ + "name": "Link", + "account_type": 6, + "fields": [ + { + "name": "AccountType", + "value": "6", + "typ": "u8" + }, + { + "name": "Owner", + "value": "6PHcid3kVtc7gLcwhNJid8dHhnCfV19XiPg3GbpcfMtb", + "typ": "pubkey" + }, + { + "name": "Index", + "value": "99", + "typ": "u128" + }, + { + "name": "BumpSeed", + "value": "249", + "typ": "u8" + }, + { + "name": "SideAPk", + "value": "6TC2N7mqPj6q62jvCsaAX4NWxnmSBxFSJFyvmo8cPAYw", + "typ": "pubkey" + }, + { + "name": "SideZPk", + "value": "6X6S1cVvHZbYVirtiNqcQz7kDoLCtuMLt8HpGzSc6yDH", + "typ": "pubkey" + }, + { + "name": "LinkType", + "value": "1", + "typ": "u8" + }, + { + "name": "Bandwidth", + "value": "10000000000", + "typ": "u64" + }, + { + "name": "Mtu", + "value": "9000", + "typ": "u32" + }, + { + "name": "DelayNs", + "value": "5000000", + "typ": "u64" + }, + { + "name": "JitterNs", + "value": "100000", + "typ": "u64" + }, + { + "name": "TunnelId", + "value": "500", + "typ": "u16" + }, + { + "name": "TunnelNet", + "value": "169.254.1.0/30", + "typ": "networkv4" + }, + { + "name": "Status", + "value": "1", + "typ": "u8" + }, + { + "name": "Code", + "value": "ams-fra", + "typ": "string" + }, + { + "name": "ContributorPk", + "value": "6azqf7E1BQ6FuQysDt74JuryUotybrTFTzbhnBkbpmsd", + "typ": "pubkey" + }, + { + "name": "SideAIfaceName", + "value": "Ethernet2", + "typ": "string" + }, + { + "name": "SideZIfaceName", + "value": "Ethernet2", + "typ": "string" + }, + { + "name": "DelayOverrideNs", + "value": "0", + "typ": "u64" + }, + { + "name": "LinkHealth", + "value": "2", + "typ": "u8" + }, + { + "name": "DesiredStatus", + "value": "1", + "typ": "u8" + } + ] +} \ No newline at end of file diff --git a/sdk/serviceability/testdata/fixtures/location.bin b/sdk/serviceability/testdata/fixtures/location.bin new file mode 100644 index 000000000..fd1450125 Binary files /dev/null and b/sdk/serviceability/testdata/fixtures/location.bin differ diff --git a/sdk/serviceability/testdata/fixtures/location.json b/sdk/serviceability/testdata/fixtures/location.json new file mode 100644 index 000000000..ab4735690 --- /dev/null +++ b/sdk/serviceability/testdata/fixtures/location.json @@ -0,0 +1,66 @@ +{ + "name": "Location", + "account_type": 3, + "fields": [ + { + "name": "AccountType", + "value": "3", + "typ": "u8" + }, + { + "name": "Owner", + "value": "39v3grDhbkqr58rBH9XHeerKHK5fbQG1gksR7Evr4kA3", + "typ": "pubkey" + }, + { + "name": "Index", + "value": "4", + "typ": "u128" + }, + { + "name": "BumpSeed", + "value": "252", + "typ": "u8" + }, + { + "name": "Lat", + "value": "52.3676", + "typ": "f64" + }, + { + "name": "Lng", + "value": "4.9041", + "typ": "f64" + }, + { + "name": "LocId", + "value": "4818", + "typ": "u32" + }, + { + "name": "Status", + "value": "1", + "typ": "u8" + }, + { + "name": "Code", + "value": "ams", + "typ": "string" + }, + { + "name": "Name", + "value": "Amsterdam", + "typ": "string" + }, + { + "name": "Country", + "value": "NL", + "typ": "string" + }, + { + "name": "ReferenceCount", + "value": "3", + "typ": "u32" + } + ] +} \ No newline at end of file diff --git a/sdk/serviceability/testdata/fixtures/multicast_group.bin b/sdk/serviceability/testdata/fixtures/multicast_group.bin new file mode 100644 index 000000000..e07726d22 Binary files /dev/null and b/sdk/serviceability/testdata/fixtures/multicast_group.bin differ diff --git a/sdk/serviceability/testdata/fixtures/multicast_group.json b/sdk/serviceability/testdata/fixtures/multicast_group.json new file mode 100644 index 000000000..7b373e7a7 --- /dev/null +++ b/sdk/serviceability/testdata/fixtures/multicast_group.json @@ -0,0 +1,61 @@ +{ + "name": "MulticastGroup", + "account_type": 8, + "fields": [ + { + "name": "AccountType", + "value": "8", + "typ": "u8" + }, + { + "name": "Owner", + "value": "8YCfQUGT6eSxkUU7yWq1GnUbz6HL5QQYQ9YTNqkTj73d", + "typ": "pubkey" + }, + { + "name": "Index", + "value": "30", + "typ": "u128" + }, + { + "name": "BumpSeed", + "value": "247", + "typ": "u8" + }, + { + "name": "TenantPk", + "value": "8c753xzXzUwgAAb6V26TAiDqF6r6nMWSz1rLt34TSuhy", + "typ": "pubkey" + }, + { + "name": "MulticastIp", + "value": "239.1.1.1", + "typ": "ipv4" + }, + { + "name": "MaxBandwidth", + "value": "1000000000", + "typ": "u64" + }, + { + "name": "Status", + "value": "1", + "typ": "u8" + }, + { + "name": "Code", + "value": "demo", + "typ": "string" + }, + { + "name": "PublisherCount", + "value": "2", + "typ": "u32" + }, + { + "name": "SubscriberCount", + "value": "10", + "typ": "u32" + } + ] +} \ No newline at end of file diff --git a/sdk/serviceability/testdata/fixtures/program_config.bin b/sdk/serviceability/testdata/fixtures/program_config.bin new file mode 100644 index 000000000..22619f171 Binary files /dev/null and b/sdk/serviceability/testdata/fixtures/program_config.bin differ diff --git a/sdk/serviceability/testdata/fixtures/program_config.json b/sdk/serviceability/testdata/fixtures/program_config.json new file mode 100644 index 000000000..303ab3e57 --- /dev/null +++ b/sdk/serviceability/testdata/fixtures/program_config.json @@ -0,0 +1,46 @@ +{ + "name": "ProgramConfig", + "account_type": 9, + "fields": [ + { + "name": "AccountType", + "value": "9", + "typ": "u8" + }, + { + "name": "BumpSeed", + "value": "246", + "typ": "u8" + }, + { + "name": "VersionMajor", + "value": "1", + "typ": "u32" + }, + { + "name": "VersionMinor", + "value": "2", + "typ": "u32" + }, + { + "name": "VersionPatch", + "value": "3", + "typ": "u32" + }, + { + "name": "MinCompatibleVersionMajor", + "value": "1", + "typ": "u32" + }, + { + "name": "MinCompatibleVersionMinor", + "value": "0", + "typ": "u32" + }, + { + "name": "MinCompatibleVersionPatch", + "value": "0", + "typ": "u32" + } + ] +} \ No newline at end of file diff --git a/sdk/serviceability/testdata/fixtures/user.bin b/sdk/serviceability/testdata/fixtures/user.bin new file mode 100644 index 000000000..28b29f709 Binary files /dev/null and b/sdk/serviceability/testdata/fixtures/user.bin differ diff --git a/sdk/serviceability/testdata/fixtures/user.json b/sdk/serviceability/testdata/fixtures/user.json new file mode 100644 index 000000000..22571353d --- /dev/null +++ b/sdk/serviceability/testdata/fixtures/user.json @@ -0,0 +1,96 @@ +{ + "name": "User", + "account_type": 7, + "fields": [ + { + "name": "AccountType", + "value": "7", + "typ": "u8" + }, + { + "name": "Owner", + "value": "7Tk94Yf6oGXYDQYXqSZrwxYwqwEznCn34GcFKinYCEU7", + "typ": "pubkey" + }, + { + "name": "Index", + "value": "200", + "typ": "u128" + }, + { + "name": "BumpSeed", + "value": "248", + "typ": "u8" + }, + { + "name": "UserType", + "value": "3", + "typ": "u8" + }, + { + "name": "TenantPk", + "value": "7XeYi3PBh72Fd6fWLwqJqtJB6womV9swe8v8pv6Xv38T", + "typ": "pubkey" + }, + { + "name": "DevicePk", + "value": "7bYxMY7GawWy2nnUrT6kjp3QMxNYC6yrE1E2L7QXdqno", + "typ": "pubkey" + }, + { + "name": "CyoaType", + "value": "2", + "typ": "u8" + }, + { + "name": "ClientIp", + "value": "198.51.100.10", + "typ": "ipv4" + }, + { + "name": "DzIp", + "value": "10.200.0.1", + "typ": "ipv4" + }, + { + "name": "TunnelId", + "value": "100", + "typ": "u16" + }, + { + "name": "TunnelNet", + "value": "169.254.100.0/30", + "typ": "networkv4" + }, + { + "name": "Status", + "value": "1", + "typ": "u8" + }, + { + "name": "PublishersLen", + "value": "1", + "typ": "u32" + }, + { + "name": "Publishers0", + "value": "7fTN12qMUn1gSUuTMxNCdjndcxwJu45kosXuqJiXMeT9", + "typ": "pubkey" + }, + { + "name": "SubscribersLen", + "value": "1", + "typ": "u32" + }, + { + "name": "Subscribers0", + "value": "7jMmeXZSNcWPrB2RsTdeXfXrsyW5c1BfPjqoLW2X5T7V", + "typ": "pubkey" + }, + { + "name": "ValidatorPubkey", + "value": "7oGBJ2HXGT17Fs9QNxu6RbH68z4rJxHZyc9gqhLWoFmq", + "typ": "pubkey" + } + ] +} \ No newline at end of file diff --git a/sdk/serviceability/typescript/bun.lock b/sdk/serviceability/typescript/bun.lock new file mode 100644 index 000000000..96329eccd --- /dev/null +++ b/sdk/serviceability/typescript/bun.lock @@ -0,0 +1,133 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "doublezero-serviceability", + "dependencies": { + "@solana/web3.js": "^1.98", + }, + "devDependencies": { + "@types/bun": "latest", + "typescript": "^5", + }, + }, + }, + "packages": { + "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], + + "@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], + + "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@solana/buffer-layout": ["@solana/buffer-layout@4.0.1", "", { "dependencies": { "buffer": "~6.0.3" } }, "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA=="], + + "@solana/codecs-core": ["@solana/codecs-core@2.3.0", "", { "dependencies": { "@solana/errors": "2.3.0" }, "peerDependencies": { "typescript": ">=5.3.3" } }, "sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw=="], + + "@solana/codecs-numbers": ["@solana/codecs-numbers@2.3.0", "", { "dependencies": { "@solana/codecs-core": "2.3.0", "@solana/errors": "2.3.0" }, "peerDependencies": { "typescript": ">=5.3.3" } }, "sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg=="], + + "@solana/errors": ["@solana/errors@2.3.0", "", { "dependencies": { "chalk": "^5.4.1", "commander": "^14.0.0" }, "peerDependencies": { "typescript": ">=5.3.3" }, "bin": { "errors": "bin/cli.mjs" } }, "sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ=="], + + "@solana/web3.js": ["@solana/web3.js@1.98.4", "", { "dependencies": { "@babel/runtime": "^7.25.0", "@noble/curves": "^1.4.2", "@noble/hashes": "^1.4.0", "@solana/buffer-layout": "^4.0.1", "@solana/codecs-numbers": "^2.1.0", "agentkeepalive": "^4.5.0", "bn.js": "^5.2.1", "borsh": "^0.7.0", "bs58": "^4.0.1", "buffer": "6.0.3", "fast-stable-stringify": "^1.0.0", "jayson": "^4.1.1", "node-fetch": "^2.7.0", "rpc-websockets": "^9.0.2", "superstruct": "^2.0.2" } }, "sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw=="], + + "@swc/helpers": ["@swc/helpers@0.5.18", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ=="], + + "@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], + + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + + "@types/node": ["@types/node@25.1.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA=="], + + "@types/uuid": ["@types/uuid@8.3.4", "", {}, "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw=="], + + "@types/ws": ["@types/ws@7.4.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww=="], + + "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], + + "base-x": ["base-x@3.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "bn.js": ["bn.js@5.2.2", "", {}, "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw=="], + + "borsh": ["borsh@0.7.0", "", { "dependencies": { "bn.js": "^5.2.0", "bs58": "^4.0.0", "text-encoding-utf-8": "^1.0.2" } }, "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA=="], + + "bs58": ["bs58@4.0.1", "", { "dependencies": { "base-x": "^3.0.2" } }, "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw=="], + + "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + + "bufferutil": ["bufferutil@4.1.0", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw=="], + + "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], + + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + + "delay": ["delay@5.0.0", "", {}, "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw=="], + + "es6-promise": ["es6-promise@4.2.8", "", {}, "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="], + + "es6-promisify": ["es6-promisify@5.0.0", "", { "dependencies": { "es6-promise": "^4.0.3" } }, "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ=="], + + "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], + + "eyes": ["eyes@0.1.8", "", {}, "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ=="], + + "fast-stable-stringify": ["fast-stable-stringify@1.0.0", "", {}, "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag=="], + + "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "isomorphic-ws": ["isomorphic-ws@4.0.1", "", { "peerDependencies": { "ws": "*" } }, "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w=="], + + "jayson": ["jayson@4.3.0", "", { "dependencies": { "@types/connect": "^3.4.33", "@types/node": "^12.12.54", "@types/ws": "^7.4.4", "commander": "^2.20.3", "delay": "^5.0.0", "es6-promisify": "^5.0.0", "eyes": "^0.1.8", "isomorphic-ws": "^4.0.1", "json-stringify-safe": "^5.0.1", "stream-json": "^1.9.1", "uuid": "^8.3.2", "ws": "^7.5.10" }, "bin": { "jayson": "bin/jayson.js" } }, "sha512-AauzHcUcqs8OBnCHOkJY280VaTiCm57AbuO7lqzcw7JapGj50BisE3xhksye4zlTSR1+1tAz67wLTl8tEH1obQ=="], + + "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + + "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], + + "rpc-websockets": ["rpc-websockets@9.3.3", "", { "dependencies": { "@swc/helpers": "^0.5.11", "@types/uuid": "^8.3.4", "@types/ws": "^8.2.2", "buffer": "^6.0.3", "eventemitter3": "^5.0.1", "uuid": "^8.3.2", "ws": "^8.5.0" }, "optionalDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" } }, "sha512-OkCsBBzrwxX4DoSv4Zlf9DgXKRB0MzVfCFg5MC+fNnf9ktr4SMWjsri0VNZQlDbCnGcImT6KNEv4ZoxktQhdpA=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "stream-chain": ["stream-chain@2.2.5", "", {}, "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA=="], + + "stream-json": ["stream-json@1.9.1", "", { "dependencies": { "stream-chain": "^2.2.5" } }, "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw=="], + + "superstruct": ["superstruct@2.0.2", "", {}, "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A=="], + + "text-encoding-utf-8": ["text-encoding-utf-8@1.0.2", "", {}, "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg=="], + + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "utf-8-validate": ["utf-8-validate@5.0.10", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ=="], + + "uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], + + "@solana/errors/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + + "jayson/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], + + "rpc-websockets/@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + + "rpc-websockets/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + } +} diff --git a/sdk/serviceability/typescript/package.json b/sdk/serviceability/typescript/package.json new file mode 100644 index 000000000..392661862 --- /dev/null +++ b/sdk/serviceability/typescript/package.json @@ -0,0 +1,21 @@ +{ + "name": "@doublezero/serviceability", + "version": "0.0.1", + "type": "module", + "main": "dist/serviceability/index.js", + "types": "dist/serviceability/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "prepublishOnly": "tsc" + }, + "dependencies": { + "@solana/web3.js": "^1.98" + }, + "devDependencies": { + "@types/bun": "latest", + "typescript": "^5" + } +} diff --git a/sdk/serviceability/typescript/serviceability/client.ts b/sdk/serviceability/typescript/serviceability/client.ts new file mode 100644 index 000000000..4e0b92261 --- /dev/null +++ b/sdk/serviceability/typescript/serviceability/client.ts @@ -0,0 +1,39 @@ +/** Read-only client for serviceability program accounts. */ + +import { Connection, PublicKey } from "@solana/web3.js"; +import { PROGRAM_IDS, LEDGER_RPC_URLS } from "./config.js"; +import { newConnection } from "./rpc.js"; + +export class Client { + private readonly connection: Connection; + private readonly programId: PublicKey; + + constructor(connection: Connection, programId: PublicKey) { + this.connection = connection; + this.programId = programId; + } + + /** Create a client configured for the given environment. */ + static forEnv(env: string): Client { + return new Client( + newConnection(LEDGER_RPC_URLS[env]), + new PublicKey(PROGRAM_IDS[env]), + ); + } + + static mainnetBeta(): Client { + return Client.forEnv("mainnet-beta"); + } + + static testnet(): Client { + return Client.forEnv("testnet"); + } + + static devnet(): Client { + return Client.forEnv("devnet"); + } + + static localnet(): Client { + return Client.forEnv("localnet"); + } +} diff --git a/sdk/serviceability/typescript/serviceability/config.ts b/sdk/serviceability/typescript/serviceability/config.ts new file mode 100644 index 000000000..18a564aab --- /dev/null +++ b/sdk/serviceability/typescript/serviceability/config.ts @@ -0,0 +1,18 @@ +/** Network configuration for the serviceability program. */ + +export const PROGRAM_IDS: Record = { + "mainnet-beta": "ser2VaTMAcYTaauMrTSfSrxBaUDq7BLNs2xfUugTAGv", + testnet: "DZtnuQ839pSaDMFG5q1ad2V95G82S5EC4RrB3Ndw2Heb", + devnet: "GYhQDKuESrasNZGyhMJhGYFtbzNijYhcrN9poSqCQVah", + localnet: "7CTniUa88iJKUHTrCkB4TjAoG6TD7AMivhQeuqN2LPtX", +}; + +export const LEDGER_RPC_URLS: Record = { + "mainnet-beta": + "https://doublezero-mainnet-beta.rpcpool.com/db336024-e7a8-46b1-80e5-352dd77060ab", + testnet: + "https://doublezerolocalnet.rpcpool.com/8a4fd3f4-0977-449f-88c7-63d4b0f10f16", + devnet: + "https://doublezerolocalnet.rpcpool.com/8a4fd3f4-0977-449f-88c7-63d4b0f10f16", + localnet: "http://localhost:8899", +}; diff --git a/sdk/serviceability/typescript/serviceability/enum_strings.test.ts b/sdk/serviceability/typescript/serviceability/enum_strings.test.ts new file mode 100644 index 000000000..54a572b65 --- /dev/null +++ b/sdk/serviceability/typescript/serviceability/enum_strings.test.ts @@ -0,0 +1,74 @@ +import { describe, test, expect } from "bun:test"; +import { readFileSync } from "fs"; +import path from "path"; +import { + locationStatusString, + exchangeStatusString, + deviceDeviceTypeString, + deviceStatusString, + deviceHealthString, + deviceDesiredStatusString, + interfaceStatusString, + interfaceTypeString, + loopbackTypeString, + interfaceCYOAString, + interfaceDIAString, + routingModeString, + linkLinkTypeString, + linkStatusString, + linkHealthString, + linkDesiredStatusString, + contributorStatusString, + userUserTypeString, + cyoaTypeString, + userStatusString, + multicastGroupStatusString, + accessPassTypeTagString, + accessPassStatusString, +} from "./state"; + +const fixtureData: Record> = JSON.parse( + readFileSync(path.resolve(__dirname, "../../testdata/enum_strings.json"), "utf-8"), +); + +const fnMap: Record string> = { + LocationStatus: locationStatusString, + ExchangeStatus: exchangeStatusString, + DeviceDeviceType: deviceDeviceTypeString, + DeviceStatus: deviceStatusString, + DeviceHealth: deviceHealthString, + DeviceDesiredStatus: deviceDesiredStatusString, + InterfaceStatus: interfaceStatusString, + InterfaceType: interfaceTypeString, + LoopbackType: loopbackTypeString, + InterfaceCYOA: interfaceCYOAString, + InterfaceDIA: interfaceDIAString, + RoutingMode: routingModeString, + LinkLinkType: linkLinkTypeString, + LinkStatus: linkStatusString, + LinkHealth: linkHealthString, + LinkDesiredStatus: linkDesiredStatusString, + ContributorStatus: contributorStatusString, + UserUserType: userUserTypeString, + CyoaType: cyoaTypeString, + UserStatus: userStatusString, + MulticastGroupStatus: multicastGroupStatusString, + AccessPassTypeTag: accessPassTypeTagString, + AccessPassStatus: accessPassStatusString, +}; + +describe("enum string functions", () => { + for (const [enumName, cases] of Object.entries(fixtureData)) { + const fn = fnMap[enumName]; + if (!fn) { + continue; + } + describe(enumName, () => { + for (const [value, expected] of Object.entries(cases)) { + test(`${enumName}(${value}) === "${expected}"`, () => { + expect(fn(Number(value))).toBe(expected); + }); + } + }); + } +}); diff --git a/sdk/serviceability/typescript/serviceability/index.ts b/sdk/serviceability/typescript/serviceability/index.ts new file mode 100644 index 000000000..553109ea9 --- /dev/null +++ b/sdk/serviceability/typescript/serviceability/index.ts @@ -0,0 +1,5 @@ +export * from "./config.js"; +export * from "./state.js"; +export * from "./pda.js"; +export * from "./client.js"; +export { newConnection } from "./rpc.js"; diff --git a/sdk/serviceability/typescript/serviceability/pda.ts b/sdk/serviceability/typescript/serviceability/pda.ts new file mode 100644 index 000000000..1beb17bf3 --- /dev/null +++ b/sdk/serviceability/typescript/serviceability/pda.ts @@ -0,0 +1,24 @@ +import { PublicKey } from "@solana/web3.js"; + +const SEED_PREFIX = Buffer.from("doublezero"); +const SEED_GLOBAL_STATE = Buffer.from("globalstate"); +const SEED_GLOBAL_CONFIG = Buffer.from("config"); +const SEED_PROGRAM_CONFIG = Buffer.from("programconfig"); + +export function deriveGlobalStatePda( + programId: PublicKey, +): [PublicKey, number] { + return PublicKey.findProgramAddressSync([SEED_PREFIX, SEED_GLOBAL_STATE], programId); +} + +export function deriveGlobalConfigPda( + programId: PublicKey, +): [PublicKey, number] { + return PublicKey.findProgramAddressSync([SEED_PREFIX, SEED_GLOBAL_CONFIG], programId); +} + +export function deriveProgramConfigPda( + programId: PublicKey, +): [PublicKey, number] { + return PublicKey.findProgramAddressSync([SEED_PREFIX, SEED_PROGRAM_CONFIG], programId); +} diff --git a/sdk/serviceability/typescript/serviceability/rpc.ts b/sdk/serviceability/typescript/serviceability/rpc.ts new file mode 100644 index 000000000..ab2f0ff83 --- /dev/null +++ b/sdk/serviceability/typescript/serviceability/rpc.ts @@ -0,0 +1,36 @@ +import { Connection, type ConnectionConfig } from "@solana/web3.js"; + +const DEFAULT_MAX_RETRIES = 5; + +/** + * Creates a Solana RPC Connection with retry on 429 Too Many Requests. + * + * The built-in @solana/web3.js retry uses short backoffs (500ms-4s) that + * may not be sufficient for rate-limited public RPC endpoints. This wrapper + * provides longer backoff intervals (2s, 4s, 6s, 8s, 10s). + */ +export function newConnection( + url: string, + config?: ConnectionConfig & { maxRetries?: number }, +): Connection { + const maxRetries = config?.maxRetries ?? DEFAULT_MAX_RETRIES; + const retryFetch = async ( + input: Parameters[0], + init?: Parameters[1], + ): Promise => { + for (let attempt = 0; ; attempt++) { + const response = await fetch(input, init); + if (response.status !== 429 || attempt >= maxRetries) { + return response; + } + await new Promise((resolve) => + setTimeout(resolve, (attempt + 1) * 2000), + ); + } + }; + return new Connection(url, { + ...config, + disableRetryOnRateLimit: true, + fetch: retryFetch as typeof fetch, + }); +} diff --git a/sdk/serviceability/typescript/serviceability/state.ts b/sdk/serviceability/typescript/serviceability/state.ts new file mode 100644 index 000000000..cdd8c8e93 --- /dev/null +++ b/sdk/serviceability/typescript/serviceability/state.ts @@ -0,0 +1,858 @@ +/** Account state types and Borsh deserialization for the serviceability program. */ + +import { PublicKey } from "@solana/web3.js"; +import { IncrementalReader } from "@doublezero/borsh-incremental"; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function readPubkey(r: IncrementalReader): PublicKey { + return new PublicKey(r.readPubkeyRaw()); +} + +function readPubkeyVec(r: IncrementalReader): PublicKey[] { + return r.readPubkeyRawVec().map((b) => new PublicKey(b)); +} + +function tryReadPubkeyVec(r: IncrementalReader): PublicKey[] { + return r.tryReadPubkeyRawVec([]).map((b) => new PublicKey(b)); +} + +// --------------------------------------------------------------------------- +// Account type discriminants +// --------------------------------------------------------------------------- + +export const ACCOUNT_TYPE_GLOBAL_STATE = 1; +export const ACCOUNT_TYPE_GLOBAL_CONFIG = 2; +export const ACCOUNT_TYPE_LOCATION = 3; +export const ACCOUNT_TYPE_EXCHANGE = 4; +export const ACCOUNT_TYPE_DEVICE = 5; +export const ACCOUNT_TYPE_LINK = 6; +export const ACCOUNT_TYPE_USER = 7; +export const ACCOUNT_TYPE_MULTICAST_GROUP = 8; +export const ACCOUNT_TYPE_PROGRAM_CONFIG = 9; +export const ACCOUNT_TYPE_CONTRIBUTOR = 10; +export const ACCOUNT_TYPE_ACCESS_PASS = 11; + +// --------------------------------------------------------------------------- +// Enum string mappings +// --------------------------------------------------------------------------- + +const LOCATION_STATUS_NAMES: Record = { + 0: "pending", + 1: "activated", + 2: "suspended", +}; +export function locationStatusString(v: number): string { + return LOCATION_STATUS_NAMES[v] ?? "unknown"; +} + +const EXCHANGE_STATUS_NAMES: Record = { + 0: "pending", + 1: "activated", + 2: "suspended", +}; +export function exchangeStatusString(v: number): string { + return EXCHANGE_STATUS_NAMES[v] ?? "unknown"; +} + +const DEVICE_DEVICE_TYPE_NAMES: Record = { + 0: "hybrid", + 1: "transit", + 2: "edge", +}; +export function deviceDeviceTypeString(v: number): string { + return DEVICE_DEVICE_TYPE_NAMES[v] ?? "unknown"; +} + +const DEVICE_STATUS_NAMES: Record = { + 0: "pending", + 1: "activated", + 2: "deleting", + 3: "rejected", + 4: "drained", + 5: "device-provisioning", + 6: "link-provisioning", +}; +export function deviceStatusString(v: number): string { + return DEVICE_STATUS_NAMES[v] ?? "unknown"; +} + +const DEVICE_HEALTH_NAMES: Record = { + 0: "unknown", + 1: "pending", + 2: "ready_for_links", + 3: "ready_for_users", + 4: "impaired", +}; +export function deviceHealthString(v: number): string { + return DEVICE_HEALTH_NAMES[v] ?? "unknown"; +} + +const DEVICE_DESIRED_STATUS_NAMES: Record = { + 0: "pending", + 1: "activated", + 6: "drained", +}; +export function deviceDesiredStatusString(v: number): string { + return DEVICE_DESIRED_STATUS_NAMES[v] ?? "unknown"; +} + +const INTERFACE_STATUS_NAMES: Record = { + 0: "invalid", + 1: "unmanaged", + 2: "pending", + 3: "activated", + 4: "deleting", + 5: "rejecting", + 6: "unlinked", +}; +export function interfaceStatusString(v: number): string { + return INTERFACE_STATUS_NAMES[v] ?? "unknown"; +} + +const INTERFACE_TYPE_NAMES: Record = { + 0: "invalid", + 1: "loopback", + 2: "physical", +}; +export function interfaceTypeString(v: number): string { + return INTERFACE_TYPE_NAMES[v] ?? "unknown"; +} + +const LOOPBACK_TYPE_NAMES: Record = { + 0: "none", + 1: "vpnv4", + 2: "ipv4", + 3: "pim_rp_addr", + 4: "reserved", +}; +export function loopbackTypeString(v: number): string { + return LOOPBACK_TYPE_NAMES[v] ?? "unknown"; +} + +const INTERFACE_CYOA_NAMES: Record = { + 0: "none", + 1: "gre_over_dia", + 2: "gre_over_fabric", + 3: "gre_over_private_peering", + 4: "gre_over_public_peering", + 5: "gre_over_cable", +}; +export function interfaceCYOAString(v: number): string { + return INTERFACE_CYOA_NAMES[v] ?? "unknown"; +} + +const INTERFACE_DIA_NAMES: Record = { + 0: "none", + 1: "dia", +}; +export function interfaceDIAString(v: number): string { + return INTERFACE_DIA_NAMES[v] ?? "unknown"; +} + +const ROUTING_MODE_NAMES: Record = { + 0: "static", + 1: "bgp", +}; +export function routingModeString(v: number): string { + return ROUTING_MODE_NAMES[v] ?? "unknown"; +} + +const LINK_LINK_TYPE_NAMES: Record = { + 1: "WAN", + 127: "DZX", +}; +export function linkLinkTypeString(v: number): string { + return LINK_LINK_TYPE_NAMES[v] ?? ""; +} + +const LINK_STATUS_NAMES: Record = { + 0: "pending", + 1: "activated", + 2: "deleting", + 3: "rejected", + 4: "requested", + 5: "hard-drained", + 6: "soft-drained", + 7: "provisioning", +}; +export function linkStatusString(v: number): string { + return LINK_STATUS_NAMES[v] ?? "unknown"; +} + +const LINK_HEALTH_NAMES: Record = { + 0: "unknown", + 1: "pending", + 2: "ready_for_service", + 3: "impaired", +}; +export function linkHealthString(v: number): string { + return LINK_HEALTH_NAMES[v] ?? "unknown"; +} + +const LINK_DESIRED_STATUS_NAMES: Record = { + 0: "pending", + 1: "activated", + 2: "hard-drained", + 3: "soft-drained", +}; +export function linkDesiredStatusString(v: number): string { + return LINK_DESIRED_STATUS_NAMES[v] ?? "unknown"; +} + +const CONTRIBUTOR_STATUS_NAMES: Record = { + 0: "none", + 1: "activated", + 2: "suspended", + 3: "deleting", +}; +export function contributorStatusString(v: number): string { + return CONTRIBUTOR_STATUS_NAMES[v] ?? "unknown"; +} + +const USER_USER_TYPE_NAMES: Record = { + 0: "ibrl", + 1: "ibrl_with_allocated_ip", + 2: "edge_filtering", + 3: "multicast", +}; +export function userUserTypeString(v: number): string { + return USER_USER_TYPE_NAMES[v] ?? "unknown"; +} + +const CYOA_TYPE_NAMES: Record = { + 0: "none", + 1: "gre_over_dia", + 2: "gre_over_fabric", + 3: "gre_over_private_peering", + 4: "gre_over_public_peering", + 5: "gre_over_cable", +}; +export function cyoaTypeString(v: number): string { + return CYOA_TYPE_NAMES[v] ?? "unknown"; +} + +const USER_STATUS_NAMES: Record = { + 0: "pending", + 1: "activated", + 3: "deleting", + 4: "rejected", + 5: "pending_ban", + 6: "banned", + 7: "updating", + 8: "out_of_credits", +}; +export function userStatusString(v: number): string { + return USER_STATUS_NAMES[v] ?? "unknown"; +} + +const MULTICAST_GROUP_STATUS_NAMES: Record = { + 0: "pending", + 1: "activated", + 2: "suspended", + 3: "deleting", + 4: "rejected", +}; +export function multicastGroupStatusString(v: number): string { + return MULTICAST_GROUP_STATUS_NAMES[v] ?? "unknown"; +} + +const ACCESS_PASS_TYPE_TAG_NAMES: Record = { + 0: "prepaid", + 1: "solana_validator", +}; +export function accessPassTypeTagString(v: number): string { + return ACCESS_PASS_TYPE_TAG_NAMES[v] ?? "unknown"; +} + +const ACCESS_PASS_STATUS_NAMES: Record = { + 0: "requested", + 1: "connected", + 2: "disconnected", + 3: "expired", +}; +export function accessPassStatusString(v: number): string { + return ACCESS_PASS_STATUS_NAMES[v] ?? "unknown"; +} + +// --------------------------------------------------------------------------- +// GlobalState +// --------------------------------------------------------------------------- + +export interface GlobalState { + accountType: number; + bumpSeed: number; + accountIndex: bigint; + foundationAllowlist: PublicKey[]; + activatorAuthorityPk: PublicKey; + sentinelAuthorityPk: PublicKey; + contributorAirdropLamports: bigint; + userAirdropLamports: bigint; + healthOraclePk: PublicKey; + qaAllowlist: PublicKey[]; +} + +export function deserializeGlobalState(data: Uint8Array): GlobalState { + const r = new IncrementalReader(data); + const accountType = r.readU8(); + const bumpSeed = r.readU8(); + const accountIndex = r.readU128(); + const foundationAllowlist = readPubkeyVec(r); + readPubkeyVec(r); // deprecated device_allowlist + readPubkeyVec(r); // deprecated user_allowlist + const activatorAuthorityPk = readPubkey(r); + const sentinelAuthorityPk = readPubkey(r); + const contributorAirdropLamports = r.readU64(); + const userAirdropLamports = r.readU64(); + const healthOraclePk = readPubkey(r); + const qaAllowlist = tryReadPubkeyVec(r); + return { + accountType, + bumpSeed, + accountIndex, + foundationAllowlist, + activatorAuthorityPk, + sentinelAuthorityPk, + contributorAirdropLamports, + userAirdropLamports, + healthOraclePk, + qaAllowlist, + }; +} + +// --------------------------------------------------------------------------- +// GlobalConfig +// --------------------------------------------------------------------------- + +export interface GlobalConfig { + accountType: number; + owner: PublicKey; + bumpSeed: number; + localAsn: number; + remoteAsn: number; + deviceTunnelBlock: Uint8Array; + userTunnelBlock: Uint8Array; + multicastGroupBlock: Uint8Array; + nextBgpCommunity: number; +} + +export function deserializeGlobalConfig(data: Uint8Array): GlobalConfig { + const r = new IncrementalReader(data); + return { + accountType: r.readU8(), + owner: readPubkey(r), + bumpSeed: r.readU8(), + localAsn: r.readU32(), + remoteAsn: r.readU32(), + deviceTunnelBlock: r.readNetworkV4(), + userTunnelBlock: r.readNetworkV4(), + multicastGroupBlock: r.readNetworkV4(), + nextBgpCommunity: r.readU16(), + }; +} + +// --------------------------------------------------------------------------- +// Location +// --------------------------------------------------------------------------- + +export interface Location { + accountType: number; + owner: PublicKey; + index: bigint; + bumpSeed: number; + lat: number; + lng: number; + locId: number; + status: number; + code: string; + name: string; + country: string; + referenceCount: number; +} + +export function deserializeLocation(data: Uint8Array): Location { + const r = new IncrementalReader(data); + return { + accountType: r.readU8(), + owner: readPubkey(r), + index: r.readU128(), + bumpSeed: r.readU8(), + lat: r.readF64(), + lng: r.readF64(), + locId: r.readU32(), + status: r.readU8(), + code: r.readString(), + name: r.readString(), + country: r.readString(), + referenceCount: r.readU32(), + }; +} + +// --------------------------------------------------------------------------- +// Exchange +// --------------------------------------------------------------------------- + +export interface Exchange { + accountType: number; + owner: PublicKey; + index: bigint; + bumpSeed: number; + lat: number; + lng: number; + bgpCommunity: number; + status: number; + code: string; + name: string; + referenceCount: number; + device1Pk: PublicKey; + device2Pk: PublicKey; +} + +export function deserializeExchange(data: Uint8Array): Exchange { + const r = new IncrementalReader(data); + const accountType = r.readU8(); + const owner = readPubkey(r); + const index = r.readU128(); + const bumpSeed = r.readU8(); + const lat = r.readF64(); + const lng = r.readF64(); + const bgpCommunity = r.readU16(); + r.readU16(); // unused padding + const status = r.readU8(); + const code = r.readString(); + const name = r.readString(); + const referenceCount = r.readU32(); + const device1Pk = readPubkey(r); + const device2Pk = readPubkey(r); + return { + accountType, + owner, + index, + bumpSeed, + lat, + lng, + bgpCommunity, + status, + code, + name, + referenceCount, + device1Pk, + device2Pk, + }; +} + +// --------------------------------------------------------------------------- +// Interface (versioned, embedded in Device) +// --------------------------------------------------------------------------- + +export interface DeviceInterface { + version: number; + status: number; + name: string; + interfaceType: number; + interfaceCyoa: number; + interfaceDia: number; + loopbackType: number; + bandwidth: bigint; + cir: bigint; + mtu: number; + routingMode: number; + vlanId: number; + ipNet: Uint8Array; + nodeSegmentIdx: number; + userTunnelEndpoint: boolean; +} + +const CURRENT_INTERFACE_VERSION = 2; + +function deserializeInterface(r: IncrementalReader): DeviceInterface { + const iface: DeviceInterface = { + version: 0, + status: 0, + name: "", + interfaceType: 0, + interfaceCyoa: 0, + interfaceDia: 0, + loopbackType: 0, + bandwidth: 0n, + cir: 0n, + mtu: 0, + routingMode: 0, + vlanId: 0, + ipNet: new Uint8Array(5), + nodeSegmentIdx: 0, + userTunnelEndpoint: false, + }; + + iface.version = r.readU8(); + if (iface.version > CURRENT_INTERFACE_VERSION - 1) { + return iface; + } + + if (iface.version === 0) { + // V1 + iface.status = r.readU8(); + iface.name = r.readString(); + iface.interfaceType = r.readU8(); + iface.loopbackType = r.readU8(); + iface.vlanId = r.readU16(); + iface.ipNet = r.readNetworkV4(); + iface.nodeSegmentIdx = r.readU16(); + iface.userTunnelEndpoint = r.readBool(); + } else if (iface.version === 1) { + // V2 + iface.status = r.readU8(); + iface.name = r.readString(); + iface.interfaceType = r.readU8(); + iface.interfaceCyoa = r.readU8(); + iface.interfaceDia = r.readU8(); + iface.loopbackType = r.readU8(); + iface.bandwidth = r.readU64(); + iface.cir = r.readU64(); + iface.mtu = r.readU16(); + iface.routingMode = r.readU8(); + iface.vlanId = r.readU16(); + iface.ipNet = r.readNetworkV4(); + iface.nodeSegmentIdx = r.readU16(); + iface.userTunnelEndpoint = r.readBool(); + } + + return iface; +} + +// --------------------------------------------------------------------------- +// Device +// --------------------------------------------------------------------------- + +export interface Device { + accountType: number; + owner: PublicKey; + index: bigint; + bumpSeed: number; + locationPubKey: PublicKey; + exchangePubKey: PublicKey; + deviceType: number; + publicIp: Uint8Array; + status: number; + code: string; + dzPrefixes: Uint8Array[]; + metricsPublisherPubKey: PublicKey; + contributorPubKey: PublicKey; + mgmtVrf: string; + interfaces: DeviceInterface[]; + referenceCount: number; + usersCount: number; + maxUsers: number; + deviceHealth: number; + deviceDesiredStatus: number; +} + +export function deserializeDevice(data: Uint8Array): Device { + const r = new IncrementalReader(data); + const accountType = r.readU8(); + const owner = readPubkey(r); + const index = r.readU128(); + const bumpSeed = r.readU8(); + const locationPubKey = readPubkey(r); + const exchangePubKey = readPubkey(r); + const deviceType = r.readU8(); + const publicIp = r.readIPv4(); + const status = r.readU8(); + const code = r.readString(); + const dzPrefixes = r.readNetworkV4Vec(); + const metricsPublisherPubKey = readPubkey(r); + const contributorPubKey = readPubkey(r); + const mgmtVrf = r.readString(); + + const ifaceLen = r.readU32(); + const interfaces: DeviceInterface[] = []; + for (let i = 0; i < ifaceLen; i++) { + interfaces.push(deserializeInterface(r)); + } + + const referenceCount = r.readU32(); + const usersCount = r.readU16(); + const maxUsers = r.readU16(); + const deviceHealth = r.readU8(); + const deviceDesiredStatus = r.readU8(); + + return { + accountType, + owner, + index, + bumpSeed, + locationPubKey, + exchangePubKey, + deviceType, + publicIp, + status, + code, + dzPrefixes, + metricsPublisherPubKey, + contributorPubKey, + mgmtVrf, + interfaces, + referenceCount, + usersCount, + maxUsers, + deviceHealth, + deviceDesiredStatus, + }; +} + +// --------------------------------------------------------------------------- +// Link +// --------------------------------------------------------------------------- + +export interface Link { + accountType: number; + owner: PublicKey; + index: bigint; + bumpSeed: number; + sideAPubKey: PublicKey; + sideZPubKey: PublicKey; + linkType: number; + bandwidth: bigint; + mtu: number; + delayNs: bigint; + jitterNs: bigint; + tunnelId: number; + tunnelNet: Uint8Array; + status: number; + code: string; + contributorPubKey: PublicKey; + sideAIfaceName: string; + sideZIfaceName: string; + delayOverrideNs: bigint; + linkHealth: number; + linkDesiredStatus: number; +} + +export function deserializeLink(data: Uint8Array): Link { + const r = new IncrementalReader(data); + return { + accountType: r.readU8(), + owner: readPubkey(r), + index: r.readU128(), + bumpSeed: r.readU8(), + sideAPubKey: readPubkey(r), + sideZPubKey: readPubkey(r), + linkType: r.readU8(), + bandwidth: r.readU64(), + mtu: r.readU32(), + delayNs: r.readU64(), + jitterNs: r.readU64(), + tunnelId: r.readU16(), + tunnelNet: r.readNetworkV4(), + status: r.readU8(), + code: r.readString(), + contributorPubKey: readPubkey(r), + sideAIfaceName: r.readString(), + sideZIfaceName: r.readString(), + delayOverrideNs: r.readU64(), + linkHealth: r.readU8(), + linkDesiredStatus: r.readU8(), + }; +} + +// --------------------------------------------------------------------------- +// User +// --------------------------------------------------------------------------- + +export interface User { + accountType: number; + owner: PublicKey; + index: bigint; + bumpSeed: number; + userType: number; + tenantPubKey: PublicKey; + devicePubKey: PublicKey; + cyoaType: number; + clientIp: Uint8Array; + dzIp: Uint8Array; + tunnelId: number; + tunnelNet: Uint8Array; + status: number; + publishers: PublicKey[]; + subscribers: PublicKey[]; + validatorPubKey: PublicKey; +} + +export function deserializeUser(data: Uint8Array): User { + const r = new IncrementalReader(data); + return { + accountType: r.readU8(), + owner: readPubkey(r), + index: r.readU128(), + bumpSeed: r.readU8(), + userType: r.readU8(), + tenantPubKey: readPubkey(r), + devicePubKey: readPubkey(r), + cyoaType: r.readU8(), + clientIp: r.readIPv4(), + dzIp: r.readIPv4(), + tunnelId: r.readU16(), + tunnelNet: r.readNetworkV4(), + status: r.readU8(), + publishers: readPubkeyVec(r), + subscribers: readPubkeyVec(r), + validatorPubKey: readPubkey(r), + }; +} + +// --------------------------------------------------------------------------- +// MulticastGroup +// --------------------------------------------------------------------------- + +export interface MulticastGroup { + accountType: number; + owner: PublicKey; + index: bigint; + bumpSeed: number; + tenantPubKey: PublicKey; + multicastIp: Uint8Array; + maxBandwidth: bigint; + status: number; + code: string; + publisherCount: number; + subscriberCount: number; +} + +export function deserializeMulticastGroup(data: Uint8Array): MulticastGroup { + const r = new IncrementalReader(data); + return { + accountType: r.readU8(), + owner: readPubkey(r), + index: r.readU128(), + bumpSeed: r.readU8(), + tenantPubKey: readPubkey(r), + multicastIp: r.readIPv4(), + maxBandwidth: r.readU64(), + status: r.readU8(), + code: r.readString(), + publisherCount: r.readU32(), + subscriberCount: r.readU32(), + }; +} + +// --------------------------------------------------------------------------- +// ProgramConfig +// --------------------------------------------------------------------------- + +export interface ProgramVersion { + major: number; + minor: number; + patch: number; +} + +export interface ProgramConfig { + accountType: number; + bumpSeed: number; + version: ProgramVersion; + minCompatVersion: ProgramVersion; +} + +function deserializeProgramVersion(r: IncrementalReader): ProgramVersion { + return { + major: r.readU32(), + minor: r.readU32(), + patch: r.readU32(), + }; +} + +export function deserializeProgramConfig(data: Uint8Array): ProgramConfig { + const r = new IncrementalReader(data); + return { + accountType: r.readU8(), + bumpSeed: r.readU8(), + version: deserializeProgramVersion(r), + minCompatVersion: deserializeProgramVersion(r), + }; +} + +// --------------------------------------------------------------------------- +// Contributor +// --------------------------------------------------------------------------- + +export interface Contributor { + accountType: number; + owner: PublicKey; + index: bigint; + bumpSeed: number; + status: number; + code: string; + referenceCount: number; + opsManagerPk: PublicKey; +} + +export function deserializeContributor(data: Uint8Array): Contributor { + const r = new IncrementalReader(data); + return { + accountType: r.readU8(), + owner: readPubkey(r), + index: r.readU128(), + bumpSeed: r.readU8(), + status: r.readU8(), + code: r.readString(), + referenceCount: r.readU32(), + opsManagerPk: readPubkey(r), + }; +} + +// --------------------------------------------------------------------------- +// AccessPass +// --------------------------------------------------------------------------- + +export interface AccessPass { + accountType: number; + owner: PublicKey; + bumpSeed: number; + accessPassType: number; + validatorPubKey: PublicKey | null; + clientIp: Uint8Array; + userPayer: PublicKey; + lastAccessEpoch: bigint; + connectionCount: number; + status: number; + mGroupPubAllowlist: PublicKey[]; + mGroupSubAllowlist: PublicKey[]; + flags: number; +} + +export function deserializeAccessPass(data: Uint8Array): AccessPass { + const r = new IncrementalReader(data); + const accountType = r.readU8(); + const owner = readPubkey(r); + const bumpSeed = r.readU8(); + const accessPassType = r.readU8(); + let validatorPubKey: PublicKey | null = null; + if (accessPassType === 1) { + // SolanaValidator + validatorPubKey = readPubkey(r); + } + const clientIp = r.readIPv4(); + const userPayer = readPubkey(r); + const lastAccessEpoch = r.readU64(); + const connectionCount = r.readU16(); + const status = r.readU8(); + const mGroupPubAllowlist = readPubkeyVec(r); + const mGroupSubAllowlist = readPubkeyVec(r); + const flags = r.readU8(); + return { + accountType, + owner, + bumpSeed, + accessPassType, + validatorPubKey, + clientIp, + userPayer, + lastAccessEpoch, + connectionCount, + status, + mGroupPubAllowlist, + mGroupSubAllowlist, + flags, + }; +} diff --git a/sdk/serviceability/typescript/serviceability/tests/compat.test.ts b/sdk/serviceability/typescript/serviceability/tests/compat.test.ts new file mode 100644 index 000000000..ad979f97c --- /dev/null +++ b/sdk/serviceability/typescript/serviceability/tests/compat.test.ts @@ -0,0 +1,162 @@ +/** + * Mainnet compatibility tests. + * + * These tests fetch live mainnet-beta data and verify that our struct + * deserialization works against real on-chain accounts. + * + * Run with: + * SERVICEABILITY_COMPAT_TEST=1 cd sdk/serviceability/typescript && bun test --grep compat + * + * Requires network access to Solana mainnet RPC. + */ + +import { describe, expect, test, setDefaultTimeout } from "bun:test"; + +// Compat tests hit public RPC endpoints which may be slow or rate-limited. +setDefaultTimeout(30_000); +import { Connection, PublicKey } from "@solana/web3.js"; +import { PROGRAM_IDS, LEDGER_RPC_URLS } from "../config.js"; +import { newConnection } from "../rpc.js"; +import { + deriveGlobalConfigPda, + deriveGlobalStatePda, + deriveProgramConfigPda, +} from "../pda.js"; +import { + deserializeGlobalConfig, + deserializeGlobalState, + deserializeProgramConfig, +} from "../state.js"; + +function skipUnlessCompat(): void { + if (!process.env.SERVICEABILITY_COMPAT_TEST) { + throw new Error("SKIP"); + } +} + +function rpcUrl(): string { + return process.env.SOLANA_RPC_URL || LEDGER_RPC_URLS["mainnet-beta"]; +} + +function programId(): PublicKey { + return new PublicKey(PROGRAM_IDS["mainnet-beta"]); +} + +async function fetchRawAccount(addr: PublicKey): Promise { + const conn = newConnection(rpcUrl()); + const info = await conn.getAccountInfo(addr); + if (info === null) throw new Error(`account not found: ${addr.toBase58()}`); + return info.data; +} + +function readU8(buf: Buffer, offset: number): number { + return buf.readUInt8(offset); +} + +function readU16LE(buf: Buffer, offset: number): number { + return buf.readUInt16LE(offset); +} + +function readU32LE(buf: Buffer, offset: number): number { + return buf.readUInt32LE(offset); +} + +function readPubkey(buf: Buffer, offset: number): PublicKey { + return new PublicKey(buf.subarray(offset, offset + 32)); +} + +describe("compat: ProgramConfig", () => { + test("deserialize matches raw bytes", async () => { + try { + skipUnlessCompat(); + } catch { + return; + } + + const pid = programId(); + const [addr] = deriveProgramConfigPda(pid); + const raw = await fetchRawAccount(addr); + + const pc = deserializeProgramConfig(new Uint8Array(raw)); + + // ProgramConfig layout (all fixed-size): + // offset 0: AccountType (u8) + // offset 1: BumpSeed (u8) + // offset 2: Version.Major (u32) + // offset 6: Version.Minor (u32) + // offset 10: Version.Patch (u32) + // offset 14: MinCompatVersion.Major (u32) + // offset 18: MinCompatVersion.Minor (u32) + // offset 22: MinCompatVersion.Patch (u32) + expect(pc.accountType).toBe(readU8(raw, 0)); + expect(pc.bumpSeed).toBe(readU8(raw, 1)); + expect(pc.version.major).toBe(readU32LE(raw, 2)); + expect(pc.version.minor).toBe(readU32LE(raw, 6)); + expect(pc.version.patch).toBe(readU32LE(raw, 10)); + expect(pc.minCompatVersion.major).toBe(readU32LE(raw, 14)); + expect(pc.minCompatVersion.minor).toBe(readU32LE(raw, 18)); + expect(pc.minCompatVersion.patch).toBe(readU32LE(raw, 22)); + + expect(pc.accountType).toBe(9); + }); +}); + +describe("compat: GlobalConfig", () => { + test("deserialize matches raw bytes", async () => { + try { + skipUnlessCompat(); + } catch { + return; + } + + const pid = programId(); + const [addr] = deriveGlobalConfigPda(pid); + const raw = await fetchRawAccount(addr); + + const gc = deserializeGlobalConfig(new Uint8Array(raw)); + + // GlobalConfig layout (all fixed-size): + // offset 0: AccountType (u8) + // offset 1: Owner (32 bytes) + // offset 33: BumpSeed (u8) + // offset 34: LocalASN (u32) + // offset 38: RemoteASN (u32) + // offset 57: NextBGPCommunity (u16) + expect(gc.accountType).toBe(readU8(raw, 0)); + expect(gc.owner.toBase58()).toBe(readPubkey(raw, 1).toBase58()); + expect(gc.bumpSeed).toBe(readU8(raw, 33)); + expect(gc.localAsn).toBe(readU32LE(raw, 34)); + expect(gc.remoteAsn).toBe(readU32LE(raw, 38)); + expect(gc.nextBgpCommunity).toBe(readU16LE(raw, 57)); + + expect(gc.accountType).toBe(2); + expect(gc.localAsn).toBeGreaterThan(0); + }); +}); + +describe("compat: GlobalState", () => { + test("deserialize and sanity check", async () => { + try { + skipUnlessCompat(); + } catch { + return; + } + + const pid = programId(); + const [addr] = deriveGlobalStatePda(pid); + const raw = await fetchRawAccount(addr); + + const gs = deserializeGlobalState(new Uint8Array(raw)); + + // GlobalState fixed layout (first 2 bytes before variable-length vecs): + expect(gs.accountType).toBe(readU8(raw, 0)); + expect(gs.bumpSeed).toBe(readU8(raw, 1)); + + expect(gs.accountType).toBe(1); + + // Sanity checks. + expect(gs.activatorAuthorityPk.equals(PublicKey.default)).toBe(false); + expect(gs.sentinelAuthorityPk.equals(PublicKey.default)).toBe(false); + // healthOraclePk may be zero on mainnet + }); +}); diff --git a/sdk/serviceability/typescript/serviceability/tests/fixtures.test.ts b/sdk/serviceability/typescript/serviceability/tests/fixtures.test.ts new file mode 100644 index 000000000..e90d3dcb1 --- /dev/null +++ b/sdk/serviceability/typescript/serviceability/tests/fixtures.test.ts @@ -0,0 +1,366 @@ +/** + * Fixture-based compatibility tests. + */ + +import { describe, expect, test } from "bun:test"; +import { readFileSync } from "fs"; +import { join } from "path"; +import { PublicKey } from "@solana/web3.js"; +import { + deserializeGlobalState, + deserializeGlobalConfig, + deserializeLocation, + deserializeExchange, + deserializeDevice, + deserializeLink, + deserializeUser, + deserializeMulticastGroup, + deserializeProgramConfig, + deserializeContributor, + deserializeAccessPass, +} from "../state.js"; + +const FIXTURES_DIR = join( + __dirname, + "..", + "..", + "..", + "testdata", + "fixtures", +); + +interface FieldValue { + name: string; + value: string; + typ: string; +} + +interface FixtureMeta { + name: string; + account_type: number; + fields: FieldValue[]; +} + +function loadFixture(name: string): [Uint8Array, FixtureMeta] { + const binData = new Uint8Array( + readFileSync(join(FIXTURES_DIR, `${name}.bin`)), + ); + const meta: FixtureMeta = JSON.parse( + readFileSync(join(FIXTURES_DIR, `${name}.json`), "utf-8"), + ); + return [binData, meta]; +} + +function formatIPv4(bytes: Uint8Array): string { + return `${bytes[0]}.${bytes[1]}.${bytes[2]}.${bytes[3]}`; +} + +function formatNetworkV4(bytes: Uint8Array): string { + return `${bytes[0]}.${bytes[1]}.${bytes[2]}.${bytes[3]}/${bytes[4]}`; +} + +function assertFields( + fields: FieldValue[], + got: Record, +): void { + for (const f of fields) { + if (!(f.name in got)) continue; + const actual = got[f.name]; + if (f.typ === "u8" || f.typ === "u16" || f.typ === "u32") { + expect(actual).toBe(Number(f.value)); + } else if (f.typ === "u64" || f.typ === "u128") { + expect(actual).toBe(BigInt(f.value)); + } else if (f.typ === "pubkey") { + expect((actual as PublicKey).toBase58()).toBe(f.value); + } else if (f.typ === "string") { + expect(actual).toBe(f.value); + } else if (f.typ === "bool") { + expect(actual).toBe(f.value === "true"); + } + } +} + +describe("GlobalState fixture", () => { + test("deserialize", () => { + const [data, meta] = loadFixture("global_state"); + const gs = deserializeGlobalState(data); + assertFields(meta.fields, { + AccountType: gs.accountType, + BumpSeed: gs.bumpSeed, + ContributorAirdropLamports: gs.contributorAirdropLamports, + UserAirdropLamports: gs.userAirdropLamports, + ActivatorAuthorityPk: gs.activatorAuthorityPk, + SentinelAuthorityPk: gs.sentinelAuthorityPk, + HealthOraclePk: gs.healthOraclePk, + }); + }); +}); + +describe("GlobalConfig fixture", () => { + test("deserialize", () => { + const [data, meta] = loadFixture("global_config"); + const gc = deserializeGlobalConfig(data); + assertFields(meta.fields, { + AccountType: gc.accountType, + Owner: gc.owner, + BumpSeed: gc.bumpSeed, + LocalAsn: gc.localAsn, + RemoteAsn: gc.remoteAsn, + NextBgpCommunity: gc.nextBgpCommunity, + }); + }); +}); + +describe("Location fixture", () => { + test("deserialize", () => { + const [data, meta] = loadFixture("location"); + const loc = deserializeLocation(data); + assertFields(meta.fields, { + AccountType: loc.accountType, + Owner: loc.owner, + BumpSeed: loc.bumpSeed, + LocId: loc.locId, + Status: loc.status, + ReferenceCount: loc.referenceCount, + }); + }); +}); + +describe("Exchange fixture", () => { + test("deserialize", () => { + const [data, meta] = loadFixture("exchange"); + const ex = deserializeExchange(data); + assertFields(meta.fields, { + AccountType: ex.accountType, + Owner: ex.owner, + BumpSeed: ex.bumpSeed, + BgpCommunity: ex.bgpCommunity, + Status: ex.status, + ReferenceCount: ex.referenceCount, + Device1Pk: ex.device1Pk, + Device2Pk: ex.device2Pk, + }); + }); +}); + +describe("Device fixture", () => { + test("deserialize", () => { + const [data, meta] = loadFixture("device"); + const dev = deserializeDevice(data); + assertFields(meta.fields, { + AccountType: dev.accountType, + Owner: dev.owner, + Index: dev.index, + BumpSeed: dev.bumpSeed, + DeviceType: dev.deviceType, + Status: dev.status, + Code: dev.code, + MgmtVrf: dev.mgmtVrf, + ReferenceCount: dev.referenceCount, + UsersCount: dev.usersCount, + MaxUsers: dev.maxUsers, + DeviceHealth: dev.deviceHealth, + DesiredStatus: dev.deviceDesiredStatus, + MetricsPublisherPk: dev.metricsPublisherPubKey, + ContributorPk: dev.contributorPubKey, + }); + + // index + expect(dev.index).toBe(7n); + + // publicIp + expect(formatIPv4(dev.publicIp)).toBe("203.0.113.1"); + + // code + expect(dev.code).toBe("dz1"); + + // mgmtVrf + expect(dev.mgmtVrf).toBe("mgmt"); + + // dzPrefixes + expect(dev.dzPrefixes).toHaveLength(1); + expect(formatNetworkV4(dev.dzPrefixes[0])).toBe("10.10.0.0/24"); + + // interfaces + expect(dev.interfaces).toHaveLength(2); + + // Interface 0 (V1 format, version byte 0) + const iface0 = dev.interfaces[0]; + expect(iface0.version).toBe(0); + expect(iface0.status).toBe(3); + expect(iface0.name).toBe("Loopback0"); + expect(iface0.interfaceType).toBe(1); + expect(iface0.loopbackType).toBe(1); + expect(iface0.vlanId).toBe(0); + expect(formatNetworkV4(iface0.ipNet)).toBe("10.0.0.1/32"); + expect(iface0.nodeSegmentIdx).toBe(100); + expect(iface0.userTunnelEndpoint).toBe(false); + + // Interface 1 (V2 format, version byte 1) + const iface1 = dev.interfaces[1]; + expect(iface1.version).toBe(1); + expect(iface1.status).toBe(3); + expect(iface1.name).toBe("Ethernet1"); + expect(iface1.interfaceType).toBe(2); + expect(iface1.interfaceCyoa).toBe(1); + expect(iface1.interfaceDia).toBe(1); + expect(iface1.loopbackType).toBe(0); + expect(iface1.bandwidth).toBe(10000000000n); + expect(iface1.cir).toBe(5000000000n); + expect(iface1.mtu).toBe(9000); + expect(iface1.routingMode).toBe(1); + expect(iface1.vlanId).toBe(100); + expect(formatNetworkV4(iface1.ipNet)).toBe("172.16.0.1/30"); + expect(iface1.nodeSegmentIdx).toBe(200); + expect(iface1.userTunnelEndpoint).toBe(true); + }); +}); + +describe("Link fixture", () => { + test("deserialize", () => { + const [data, meta] = loadFixture("link"); + const lk = deserializeLink(data); + assertFields(meta.fields, { + AccountType: lk.accountType, + Owner: lk.owner, + BumpSeed: lk.bumpSeed, + LinkType: lk.linkType, + Bandwidth: lk.bandwidth, + Mtu: lk.mtu, + DelayNs: lk.delayNs, + JitterNs: lk.jitterNs, + TunnelId: lk.tunnelId, + Status: lk.status, + ContributorPk: lk.contributorPubKey, + DelayOverrideNs: lk.delayOverrideNs, + LinkHealth: lk.linkHealth, + DesiredStatus: lk.linkDesiredStatus, + }); + }); +}); + +describe("User fixture", () => { + test("deserialize", () => { + const [data, meta] = loadFixture("user"); + const u = deserializeUser(data); + assertFields(meta.fields, { + AccountType: u.accountType, + Owner: u.owner, + BumpSeed: u.bumpSeed, + UserType: u.userType, + TenantPk: u.tenantPubKey, + DevicePk: u.devicePubKey, + CyoaType: u.cyoaType, + TunnelId: u.tunnelId, + Status: u.status, + ValidatorPubkey: u.validatorPubKey, + }); + }); +}); + +describe("MulticastGroup fixture", () => { + test("deserialize", () => { + const [data, meta] = loadFixture("multicast_group"); + const mg = deserializeMulticastGroup(data); + assertFields(meta.fields, { + AccountType: mg.accountType, + Owner: mg.owner, + BumpSeed: mg.bumpSeed, + TenantPk: mg.tenantPubKey, + MaxBandwidth: mg.maxBandwidth, + Status: mg.status, + PublisherCount: mg.publisherCount, + SubscriberCount: mg.subscriberCount, + }); + }); +}); + +describe("ProgramConfig fixture", () => { + test("deserialize", () => { + const [data, meta] = loadFixture("program_config"); + const pc = deserializeProgramConfig(data); + assertFields(meta.fields, { + AccountType: pc.accountType, + BumpSeed: pc.bumpSeed, + VersionMajor: pc.version.major, + VersionMinor: pc.version.minor, + VersionPatch: pc.version.patch, + }); + }); +}); + +describe("Contributor fixture", () => { + test("deserialize", () => { + const [data, meta] = loadFixture("contributor"); + const c = deserializeContributor(data); + assertFields(meta.fields, { + AccountType: c.accountType, + Owner: c.owner, + BumpSeed: c.bumpSeed, + Status: c.status, + ReferenceCount: c.referenceCount, + OpsManagerPk: c.opsManagerPk, + }); + }); +}); + +describe("AccessPass fixture", () => { + test("deserialize", () => { + const [data, meta] = loadFixture("access_pass"); + const ap = deserializeAccessPass(data); + assertFields(meta.fields, { + AccountType: ap.accountType, + Owner: ap.owner, + BumpSeed: ap.bumpSeed, + AccessPassType: ap.accessPassType, + UserPayer: ap.userPayer, + LastAccessEpoch: ap.lastAccessEpoch, + ConnectionCount: ap.connectionCount, + Status: ap.status, + Flags: ap.flags, + }); + }); +}); + +describe("AccessPassValidator fixture", () => { + test("deserialize", () => { + const [data, meta] = loadFixture("access_pass_validator"); + const ap = deserializeAccessPass(data); + assertFields(meta.fields, { + AccountType: ap.accountType, + Owner: ap.owner, + BumpSeed: ap.bumpSeed, + AccessPassType: ap.accessPassType, + AccessPassTypeValidatorPubkey: ap.validatorPubKey, + UserPayer: ap.userPayer, + LastAccessEpoch: ap.lastAccessEpoch, + ConnectionCount: ap.connectionCount, + Status: ap.status, + Flags: ap.flags, + }); + + // Verify specific values + expect(ap.accountType).toBe(11); + expect(ap.bumpSeed).toBe(243); + expect(ap.accessPassType).toBe(1); + expect(ap.validatorPubKey).not.toBeNull(); + expect(ap.validatorPubKey!.toBase58()).toBe( + "BuP3jEYfnTCfB4UqQk9L37k2vaXsNuVsbWxrYbGDmL6s", + ); + expect(formatIPv4(ap.clientIp)).toBe("10.0.0.50"); + expect(ap.lastAccessEpoch).toBe(1000n); + expect(ap.connectionCount).toBe(1); + expect(ap.status).toBe(1); + expect(ap.flags).toBe(3); + + // Allowlists + expect(ap.mGroupPubAllowlist).toHaveLength(1); + expect(ap.mGroupPubAllowlist[0].toBase58()).toBe( + "ByHTNjGkgHhNakbovFQmw3VGBb6e5rbnBPGk3naDV8mD", + ); + expect(ap.mGroupSubAllowlist).toHaveLength(1); + expect(ap.mGroupSubAllowlist[0].toBase58()).toBe( + "C3Bs2Dzqa8C5zSinRkgDpyEVSbfQnohgmFadYytDCwRZ", + ); + }); +}); diff --git a/sdk/serviceability/typescript/serviceability/tests/pda.test.ts b/sdk/serviceability/typescript/serviceability/tests/pda.test.ts new file mode 100644 index 000000000..827e58275 --- /dev/null +++ b/sdk/serviceability/typescript/serviceability/tests/pda.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, test } from "bun:test"; +import { PublicKey } from "@solana/web3.js"; +import { + deriveGlobalStatePda, + deriveGlobalConfigPda, + deriveProgramConfigPda, +} from "../pda.js"; + +const PROGRAM_ID = new PublicKey( + "ser2VaTMAcYTaauMrTSfSrxBaUDq7BLNs2xfUugTAGv", +); + +describe("PDA derivation", () => { + test("global state PDA is deterministic", () => { + const [addr1, bump1] = deriveGlobalStatePda(PROGRAM_ID); + const [addr2, bump2] = deriveGlobalStatePda(PROGRAM_ID); + expect(addr1.equals(addr2)).toBe(true); + expect(bump1).toBe(bump2); + }); + + test("global config PDA", () => { + const [addr] = deriveGlobalConfigPda(PROGRAM_ID); + expect(addr.equals(PublicKey.default)).toBe(false); + }); + + test("program config PDA", () => { + const [addr] = deriveProgramConfigPda(PROGRAM_ID); + expect(addr.equals(PublicKey.default)).toBe(false); + }); + + test("PDAs are different", () => { + const [gs] = deriveGlobalStatePda(PROGRAM_ID); + const [gc] = deriveGlobalConfigPda(PROGRAM_ID); + const [pc] = deriveProgramConfigPda(PROGRAM_ID); + expect(gs.equals(gc)).toBe(false); + expect(gs.equals(pc)).toBe(false); + expect(gc.equals(pc)).toBe(false); + }); +}); diff --git a/sdk/serviceability/typescript/tsconfig.json b/sdk/serviceability/typescript/tsconfig.json new file mode 100644 index 000000000..14d22eb2a --- /dev/null +++ b/sdk/serviceability/typescript/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "dist", + "declaration": true, + "types": ["bun"], + "baseUrl": ".", + "paths": { + "@doublezero/borsh-incremental": ["../../borsh-incremental/typescript/borsh-incremental/index.ts"] + } + }, + "include": ["serviceability/**/*.ts"] +} diff --git a/sdk/telemetry/go/client.go b/sdk/telemetry/go/client.go new file mode 100644 index 000000000..0d49ba2fe --- /dev/null +++ b/sdk/telemetry/go/client.go @@ -0,0 +1,80 @@ +package telemetry + +import ( + "context" + + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" +) + +type Client struct { + rpcClient *rpc.Client + programID solana.PublicKey +} + +func New(rpcClient *rpc.Client, programID solana.PublicKey) *Client { + return &Client{rpcClient: rpcClient, programID: programID} +} + +// NewForEnv creates a client configured for the given environment. +// Valid environments: "mainnet-beta", "testnet", "devnet", "localnet". +func NewForEnv(env string) *Client { + return New(NewRPCClient(LedgerRPCURLs[env]), ProgramIDs[env]) +} + +func NewMainnetBeta() *Client { + return NewForEnv("mainnet-beta") +} + +func NewTestnet() *Client { + return NewForEnv("testnet") +} + +func NewDevnet() *Client { + return NewForEnv("devnet") +} + +func NewLocalnet() *Client { + return NewForEnv("localnet") +} + +func (c *Client) GetDeviceLatencySamples( + ctx context.Context, + originDevicePK solana.PublicKey, + targetDevicePK solana.PublicKey, + linkPK solana.PublicKey, + epoch uint64, +) (*DeviceLatencySamples, error) { + addr, _, err := DeriveDeviceLatencySamplesPDA(c.programID, originDevicePK, targetDevicePK, linkPK, epoch) + if err != nil { + return nil, err + } + + info, err := c.rpcClient.GetAccountInfo(ctx, addr) + if err != nil { + return nil, err + } + + return DeserializeDeviceLatencySamples(info.Value.Data.GetBinary()) +} + +func (c *Client) GetInternetLatencySamples( + ctx context.Context, + collectorOraclePK solana.PublicKey, + dataProviderName string, + originLocationPK solana.PublicKey, + targetLocationPK solana.PublicKey, + epoch uint64, +) (*InternetLatencySamples, error) { + addr, _, err := DeriveInternetLatencySamplesPDA(c.programID, collectorOraclePK, dataProviderName, originLocationPK, targetLocationPK, epoch) + if err != nil { + return nil, err + } + + info, err := c.rpcClient.GetAccountInfo(ctx, addr) + if err != nil { + return nil, err + } + + return DeserializeInternetLatencySamples(info.Value.Data.GetBinary()) +} diff --git a/sdk/telemetry/go/config.go b/sdk/telemetry/go/config.go new file mode 100644 index 000000000..b8318ccb9 --- /dev/null +++ b/sdk/telemetry/go/config.go @@ -0,0 +1,18 @@ +package telemetry + +import "github.com/gagliardetto/solana-go" + +var ProgramIDs = map[string]solana.PublicKey{ + "mainnet-beta": solana.MustPublicKeyFromBase58("tE1exJ5VMyoC9ByZeSmgtNzJCFF74G9JAv338sJiqkC"), + "testnet": solana.MustPublicKeyFromBase58("3KogTMmVxc5eUHtjZnwm136H5P8tvPwVu4ufbGPvM7p1"), + "devnet": solana.MustPublicKeyFromBase58("C9xqH76NSm11pBS6maNnY163tWHT8Govww47uyEmSnoG"), + "localnet": solana.MustPublicKeyFromBase58("C9xqH76NSm11pBS6maNnY163tWHT8Govww47uyEmSnoG"), +} + +// LedgerRPCURLs are the DZ Ledger RPC URLs per environment. +var LedgerRPCURLs = map[string]string{ + "mainnet-beta": "https://doublezero-mainnet-beta.rpcpool.com/db336024-e7a8-46b1-80e5-352dd77060ab", + "testnet": "https://doublezerolocalnet.rpcpool.com/8a4fd3f4-0977-449f-88c7-63d4b0f10f16", + "devnet": "https://doublezerolocalnet.rpcpool.com/8a4fd3f4-0977-449f-88c7-63d4b0f10f16", + "localnet": "http://localhost:8899", +} diff --git a/sdk/telemetry/go/fixture_test.go b/sdk/telemetry/go/fixture_test.go new file mode 100644 index 000000000..37ac9e8a8 --- /dev/null +++ b/sdk/telemetry/go/fixture_test.go @@ -0,0 +1,129 @@ +package telemetry + +import ( + "encoding/json" + "os" + "path/filepath" + "reflect" + "runtime" + "strconv" + "testing" + + "github.com/gagliardetto/solana-go" +) + +type fixtureMeta struct { + Name string `json:"name"` + AccountType int `json:"account_type"` + Fields []fieldValue `json:"fields"` +} + +type fieldValue struct { + Name string `json:"name"` + Value string `json:"value"` + Type string `json:"typ"` +} + +func fixturesDir() string { + _, filename, _, _ := runtime.Caller(0) + return filepath.Join(filepath.Dir(filename), "..", "testdata", "fixtures") +} + +func loadFixture(t *testing.T, name string) ([]byte, fixtureMeta) { + t.Helper() + dir := fixturesDir() + + binData, err := os.ReadFile(filepath.Join(dir, name+".bin")) + if err != nil { + t.Fatalf("reading %s.bin: %v", name, err) + } + + jsonData, err := os.ReadFile(filepath.Join(dir, name+".json")) + if err != nil { + t.Fatalf("reading %s.json: %v", name, err) + } + + var meta fixtureMeta + if err := json.Unmarshal(jsonData, &meta); err != nil { + t.Fatalf("parsing %s.json: %v", name, err) + } + + return binData, meta +} + +func TestFixtureDeviceLatencySamples(t *testing.T) { + data, meta := loadFixture(t, "device_latency_samples") + d, err := DeserializeDeviceLatencySamples(data) + if err != nil { + t.Fatalf("DeserializeDeviceLatencySamples: %v", err) + } + + assertFields(t, meta.Fields, map[string]any{ + "AccountType": uint8(d.AccountType), + "Epoch": d.Epoch, + "OriginDeviceAgentPK": solana.PublicKey(d.OriginDeviceAgentPK), + "OriginDevicePK": solana.PublicKey(d.OriginDevicePK), + "TargetDevicePK": solana.PublicKey(d.TargetDevicePK), + "OriginDeviceLocationPK": solana.PublicKey(d.OriginDeviceLocationPK), + "TargetDeviceLocationPK": solana.PublicKey(d.TargetDeviceLocationPK), + "LinkPK": solana.PublicKey(d.LinkPK), + "SamplingIntervalMicroseconds": d.SamplingIntervalMicroseconds, + "StartTimestampMicroseconds": d.StartTimestampMicroseconds, + "NextSampleIndex": d.NextSampleIndex, + "SamplesCount": uint32(len(d.Samples)), + }) +} + +func TestFixtureInternetLatencySamples(t *testing.T) { + data, meta := loadFixture(t, "internet_latency_samples") + d, err := DeserializeInternetLatencySamples(data) + if err != nil { + t.Fatalf("DeserializeInternetLatencySamples: %v", err) + } + + assertFields(t, meta.Fields, map[string]any{ + "AccountType": uint8(d.AccountType), + "Epoch": d.Epoch, + "OracleAgentPK": solana.PublicKey(d.OracleAgentPK), + "OriginExchangePK": solana.PublicKey(d.OriginExchangePK), + "TargetExchangePK": solana.PublicKey(d.TargetExchangePK), + "SamplingIntervalMicroseconds": d.SamplingIntervalMicroseconds, + "StartTimestampMicroseconds": d.StartTimestampMicroseconds, + "NextSampleIndex": d.NextSampleIndex, + "SamplesCount": uint32(len(d.Samples)), + }) +} + +func assertFields(t *testing.T, expected []fieldValue, got map[string]any) { + t.Helper() + for _, f := range expected { + val, ok := got[f.Name] + if !ok { + continue + } + switch f.Type { + case "u8": + want, _ := strconv.ParseUint(f.Value, 10, 8) + assertEq(t, f.Name, uint8(want), val) + case "u16": + want, _ := strconv.ParseUint(f.Value, 10, 16) + assertEq(t, f.Name, uint16(want), val) + case "u32": + want, _ := strconv.ParseUint(f.Value, 10, 32) + assertEq(t, f.Name, uint32(want), val) + case "u64": + want, _ := strconv.ParseUint(f.Value, 10, 64) + assertEq(t, f.Name, uint64(want), val) + case "pubkey": + want := solana.MustPublicKeyFromBase58(f.Value) + assertEq(t, f.Name, want, val) + } + } +} + +func assertEq(t *testing.T, name string, want, got any) { + t.Helper() + if !reflect.DeepEqual(want, got) { + t.Errorf("%s: want %v, got %v", name, want, got) + } +} diff --git a/sdk/telemetry/go/pda.go b/sdk/telemetry/go/pda.go new file mode 100644 index 000000000..2adfa694f --- /dev/null +++ b/sdk/telemetry/go/pda.go @@ -0,0 +1,53 @@ +package telemetry + +import ( + "encoding/binary" + + "github.com/gagliardetto/solana-go" +) + +func DeriveDeviceLatencySamplesPDA( + programID solana.PublicKey, + originDevicePK solana.PublicKey, + targetDevicePK solana.PublicKey, + linkPK solana.PublicKey, + epoch uint64, +) (solana.PublicKey, uint8, error) { + epochBytes := make([]byte, 8) + binary.LittleEndian.PutUint64(epochBytes, epoch) + + seeds := [][]byte{ + []byte(TelemetrySeedPrefix), + []byte(DeviceLatencySamplesSeed), + originDevicePK[:], + targetDevicePK[:], + linkPK[:], + epochBytes, + } + + return solana.FindProgramAddress(seeds, programID) +} + +func DeriveInternetLatencySamplesPDA( + programID solana.PublicKey, + collectorOraclePK solana.PublicKey, + dataProviderName string, + originLocationPK solana.PublicKey, + targetLocationPK solana.PublicKey, + epoch uint64, +) (solana.PublicKey, uint8, error) { + epochBytes := make([]byte, 8) + binary.LittleEndian.PutUint64(epochBytes, epoch) + + seeds := [][]byte{ + []byte(TelemetrySeedPrefix), + []byte(InternetLatencySamplesSeed), + collectorOraclePK[:], + []byte(dataProviderName), + originLocationPK[:], + targetLocationPK[:], + epochBytes, + } + + return solana.FindProgramAddress(seeds, programID) +} diff --git a/sdk/telemetry/go/pda_test.go b/sdk/telemetry/go/pda_test.go new file mode 100644 index 000000000..f0de4aea4 --- /dev/null +++ b/sdk/telemetry/go/pda_test.go @@ -0,0 +1,45 @@ +package telemetry + +import ( + "testing" + + "github.com/gagliardetto/solana-go" +) + +var testProgramID = solana.MustPublicKeyFromBase58("tE1exJ5VMyoC9ByZeSmgtNzJCFF74G9JAv338sJiqkC") + +func TestDeriveDeviceLatencySamplesPDA(t *testing.T) { + origin := solana.MustPublicKeyFromBase58("11111111111111111111111111111112") + target := solana.MustPublicKeyFromBase58("11111111111111111111111111111113") + link := solana.MustPublicKeyFromBase58("11111111111111111111111111111114") + + addr, bump, err := DeriveDeviceLatencySamplesPDA(testProgramID, origin, target, link, 42) + if err != nil { + t.Fatalf("DeriveDeviceLatencySamplesPDA: %v", err) + } + if addr.IsZero() { + t.Error("derived zero address") + } + + addr2, bump2, err := DeriveDeviceLatencySamplesPDA(testProgramID, origin, target, link, 42) + if err != nil { + t.Fatalf("DeriveDeviceLatencySamplesPDA (2nd): %v", err) + } + if addr != addr2 || bump != bump2 { + t.Error("PDA derivation not deterministic") + } +} + +func TestDeriveInternetLatencySamplesPDA(t *testing.T) { + oracle := solana.MustPublicKeyFromBase58("11111111111111111111111111111112") + origin := solana.MustPublicKeyFromBase58("11111111111111111111111111111113") + target := solana.MustPublicKeyFromBase58("11111111111111111111111111111114") + + addr, _, err := DeriveInternetLatencySamplesPDA(testProgramID, oracle, "RIPE Atlas", origin, target, 42) + if err != nil { + t.Fatalf("DeriveInternetLatencySamplesPDA: %v", err) + } + if addr.IsZero() { + t.Error("derived zero address") + } +} diff --git a/sdk/telemetry/go/rpc.go b/sdk/telemetry/go/rpc.go new file mode 100644 index 000000000..d054642fb --- /dev/null +++ b/sdk/telemetry/go/rpc.go @@ -0,0 +1,50 @@ +package telemetry + +import ( + "io" + "net/http" + "time" + + "github.com/gagliardetto/solana-go/rpc" + "github.com/gagliardetto/solana-go/rpc/jsonrpc" +) + +const defaultMaxRetries = 5 + +// retryHTTPClient wraps an http.Client and retries on 429 Too Many Requests. +type retryHTTPClient struct { + inner *http.Client + maxRetries int +} + +func (c *retryHTTPClient) Do(req *http.Request) (*http.Response, error) { + for attempt := 0; ; attempt++ { + resp, err := c.inner.Do(req) + if err != nil { + return resp, err + } + if resp.StatusCode != http.StatusTooManyRequests || attempt >= c.maxRetries { + return resp, nil + } + _, _ = io.Copy(io.Discard, resp.Body) + resp.Body.Close() + backoff := time.Duration(attempt+1) * 2 * time.Second + time.Sleep(backoff) + } +} + +func (c *retryHTTPClient) CloseIdleConnections() { + c.inner.CloseIdleConnections() +} + +// NewRPCClient creates a Solana RPC client with automatic retry on 429 responses. +func NewRPCClient(url string) *rpc.Client { + httpClient := &retryHTTPClient{ + inner: http.DefaultClient, + maxRetries: defaultMaxRetries, + } + rpcClient := jsonrpc.NewClientWithOpts(url, &jsonrpc.RPCClientOpts{ + HTTPClient: httpClient, + }) + return rpc.NewWithCustomRPCClient(rpcClient) +} diff --git a/sdk/telemetry/go/state.go b/sdk/telemetry/go/state.go new file mode 100644 index 000000000..5b633067d --- /dev/null +++ b/sdk/telemetry/go/state.go @@ -0,0 +1,137 @@ +package telemetry + +import ( + "fmt" + + borsh "github.com/malbeclabs/doublezero/sdk/borsh-incremental/go" +) + +type AccountType uint8 + +const ( + AccountTypeDeviceLatencySamplesV0 AccountType = 1 + AccountTypeInternetLatencySamplesV0 AccountType = 2 + AccountTypeDeviceLatencySamples AccountType = 3 + AccountTypeInternetLatencySamples AccountType = 4 +) + +const ( + TelemetrySeedPrefix = "telemetry" + DeviceLatencySamplesSeed = "dzlatency" + InternetLatencySamplesSeed = "inetlatency" + + MaxDeviceLatencySamplesPerAccount = 35_000 + MaxInternetLatencySamplesPerAccount = 3_000 + + deviceLatencyHeaderSize = 1 + 8 + 32*6 + 8 + 8 + 4 + 128 +) + +type DeviceLatencySamples struct { + AccountType AccountType + Epoch uint64 + OriginDeviceAgentPK [32]byte + OriginDevicePK [32]byte + TargetDevicePK [32]byte + OriginDeviceLocationPK [32]byte + TargetDeviceLocationPK [32]byte + LinkPK [32]byte + SamplingIntervalMicroseconds uint64 + StartTimestampMicroseconds uint64 + NextSampleIndex uint32 + Samples []uint32 +} + +func DeserializeDeviceLatencySamples(data []byte) (*DeviceLatencySamples, error) { + if len(data) < deviceLatencyHeaderSize { + return nil, fmt.Errorf("data too short for device latency header: %d < %d", len(data), deviceLatencyHeaderSize) + } + + r := borsh.NewReader(data) + d := &DeviceLatencySamples{} + + v, _ := r.ReadU8() + d.AccountType = AccountType(v) + d.Epoch, _ = r.ReadU64() + d.OriginDeviceAgentPK, _ = r.ReadPubkey() + d.OriginDevicePK, _ = r.ReadPubkey() + d.TargetDevicePK, _ = r.ReadPubkey() + d.OriginDeviceLocationPK, _ = r.ReadPubkey() + d.TargetDeviceLocationPK, _ = r.ReadPubkey() + d.LinkPK, _ = r.ReadPubkey() + d.SamplingIntervalMicroseconds, _ = r.ReadU64() + d.StartTimestampMicroseconds, _ = r.ReadU64() + d.NextSampleIndex, _ = r.ReadU32() + + _, _ = r.ReadBytes(128) // _unused + + count := int(d.NextSampleIndex) + if count > MaxDeviceLatencySamplesPerAccount { + return nil, fmt.Errorf("next_sample_index %d exceeds max %d", count, MaxDeviceLatencySamplesPerAccount) + } + + d.Samples = make([]uint32, count) + for i := range count { + if r.Remaining() < 4 { + break + } + d.Samples[i], _ = r.ReadU32() + } + + return d, nil +} + +type InternetLatencySamples struct { + AccountType AccountType + Epoch uint64 + DataProviderName string + OracleAgentPK [32]byte + OriginExchangePK [32]byte + TargetExchangePK [32]byte + SamplingIntervalMicroseconds uint64 + StartTimestampMicroseconds uint64 + NextSampleIndex uint32 + Samples []uint32 +} + +func DeserializeInternetLatencySamples(data []byte) (*InternetLatencySamples, error) { + if len(data) < 10 { + return nil, fmt.Errorf("data too short") + } + + r := borsh.NewReader(data) + d := &InternetLatencySamples{} + + v, _ := r.ReadU8() + d.AccountType = AccountType(v) + d.Epoch, _ = r.ReadU64() + + var err error + d.DataProviderName, err = r.ReadString() + if err != nil { + return nil, fmt.Errorf("data_provider_name: %w", err) + } + + d.OracleAgentPK, _ = r.ReadPubkey() + d.OriginExchangePK, _ = r.ReadPubkey() + d.TargetExchangePK, _ = r.ReadPubkey() + d.SamplingIntervalMicroseconds, _ = r.ReadU64() + d.StartTimestampMicroseconds, _ = r.ReadU64() + d.NextSampleIndex, _ = r.ReadU32() + + _, _ = r.ReadBytes(128) // _unused + + count := int(d.NextSampleIndex) + if count > MaxInternetLatencySamplesPerAccount { + return nil, fmt.Errorf("next_sample_index %d exceeds max %d", count, MaxInternetLatencySamplesPerAccount) + } + + d.Samples = make([]uint32, count) + for i := range count { + if r.Remaining() < 4 { + break + } + d.Samples[i], _ = r.ReadU32() + } + + return d, nil +} diff --git a/sdk/telemetry/python/pyproject.toml b/sdk/telemetry/python/pyproject.toml new file mode 100644 index 000000000..45282fe8e --- /dev/null +++ b/sdk/telemetry/python/pyproject.toml @@ -0,0 +1,29 @@ +[project] +name = "doublezero-telemetry" +version = "0.0.1" +description = "DoubleZero Telemetry SDK" +requires-python = ">=3.10" +dependencies = [ + "doublezero-borsh-incremental", + "solana>=0.35", + "solders>=0.21", + "httpx>=0.27", +] + +[tool.pytest.ini_options] +testpaths = ["telemetry/tests"] + +[tool.hatch.build.targets.wheel] +packages = ["telemetry"] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[dependency-groups] +dev = [ + "pytest>=9.0.2", +] + +[tool.uv.sources] +doublezero-borsh-incremental = { path = "../../borsh-incremental/python", editable = true } diff --git a/sdk/telemetry/python/telemetry/__init__.py b/sdk/telemetry/python/telemetry/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sdk/telemetry/python/telemetry/client.py b/sdk/telemetry/python/telemetry/client.py new file mode 100644 index 000000000..1a4b6f5eb --- /dev/null +++ b/sdk/telemetry/python/telemetry/client.py @@ -0,0 +1,92 @@ +"""RPC client for fetching telemetry program accounts.""" + +from __future__ import annotations + +from typing import Protocol + +from solders.pubkey import Pubkey # type: ignore[import-untyped] +from solders.rpc.responses import GetAccountInfoResp # type: ignore[import-untyped] + +from telemetry.config import PROGRAM_IDS, LEDGER_RPC_URLS +from telemetry.rpc import new_rpc_client +from telemetry.pda import ( + derive_device_latency_samples_pda, + derive_internet_latency_samples_pda, +) +from telemetry.state import DeviceLatencySamples, InternetLatencySamples + + +class SolanaClient(Protocol): + def get_account_info(self, pubkey: Pubkey) -> GetAccountInfoResp: ... + + +class Client: + """Read-only client for telemetry program accounts.""" + + def __init__( + self, + solana_rpc: SolanaClient, + program_id: Pubkey, + ) -> None: + self._solana_rpc = solana_rpc + self._program_id = program_id + + @classmethod + def from_env(cls, env: str) -> Client: + """Create a client configured for the given environment. + + Args: + env: Environment name ("mainnet-beta", "testnet", "devnet", "localnet") + """ + return cls( + new_rpc_client(LEDGER_RPC_URLS[env]), + Pubkey.from_string(PROGRAM_IDS[env]), + ) + + @classmethod + def mainnet_beta(cls) -> Client: + return cls.from_env("mainnet-beta") + + @classmethod + def testnet(cls) -> Client: + return cls.from_env("testnet") + + @classmethod + def devnet(cls) -> Client: + return cls.from_env("devnet") + + @classmethod + def localnet(cls) -> Client: + return cls.from_env("localnet") + + def get_device_latency_samples( + self, + origin_device_pk: Pubkey, + target_device_pk: Pubkey, + link_pk: Pubkey, + epoch: int, + ) -> DeviceLatencySamples: + addr, _ = derive_device_latency_samples_pda( + self._program_id, origin_device_pk, target_device_pk, link_pk, epoch + ) + resp = self._solana_rpc.get_account_info(addr) + return DeviceLatencySamples.from_bytes(resp.value.data) + + def get_internet_latency_samples( + self, + collector_oracle_pk: Pubkey, + data_provider_name: str, + origin_location_pk: Pubkey, + target_location_pk: Pubkey, + epoch: int, + ) -> InternetLatencySamples: + addr, _ = derive_internet_latency_samples_pda( + self._program_id, + collector_oracle_pk, + data_provider_name, + origin_location_pk, + target_location_pk, + epoch, + ) + resp = self._solana_rpc.get_account_info(addr) + return InternetLatencySamples.from_bytes(resp.value.data) diff --git a/sdk/telemetry/python/telemetry/config.py b/sdk/telemetry/python/telemetry/config.py new file mode 100644 index 000000000..8c1e4f730 --- /dev/null +++ b/sdk/telemetry/python/telemetry/config.py @@ -0,0 +1,15 @@ +"""Network configuration for the telemetry program.""" + +PROGRAM_IDS = { + "mainnet-beta": "tE1exJ5VMyoC9ByZeSmgtNzJCFF74G9JAv338sJiqkC", + "testnet": "3KogTMmVxc5eUHtjZnwm136H5P8tvPwVu4ufbGPvM7p1", + "devnet": "C9xqH76NSm11pBS6maNnY163tWHT8Govww47uyEmSnoG", + "localnet": "C9xqH76NSm11pBS6maNnY163tWHT8Govww47uyEmSnoG", +} + +LEDGER_RPC_URLS = { + "mainnet-beta": "https://doublezero-mainnet-beta.rpcpool.com/db336024-e7a8-46b1-80e5-352dd77060ab", + "testnet": "https://doublezerolocalnet.rpcpool.com/8a4fd3f4-0977-449f-88c7-63d4b0f10f16", + "devnet": "https://doublezerolocalnet.rpcpool.com/8a4fd3f4-0977-449f-88c7-63d4b0f10f16", + "localnet": "http://localhost:8899", +} diff --git a/sdk/telemetry/python/telemetry/pda.py b/sdk/telemetry/python/telemetry/pda.py new file mode 100644 index 000000000..050ee07a6 --- /dev/null +++ b/sdk/telemetry/python/telemetry/pda.py @@ -0,0 +1,55 @@ +"""PDA derivation for telemetry program accounts.""" + +import struct + +from solders.pubkey import Pubkey # type: ignore[import-untyped] + +from telemetry.state import ( + TELEMETRY_SEED_PREFIX, + DEVICE_LATENCY_SAMPLES_SEED, + INTERNET_LATENCY_SAMPLES_SEED, +) + + +def derive_device_latency_samples_pda( + program_id: Pubkey, + origin_device_pk: Pubkey, + target_device_pk: Pubkey, + link_pk: Pubkey, + epoch: int, +) -> tuple[Pubkey, int]: + epoch_bytes = struct.pack(" tuple[Pubkey, int]: + epoch_bytes = struct.pack(" None: + self._wrapped = wrapped or httpx.HTTPTransport() + self._max_retries = max_retries + + def handle_request(self, request: httpx.Request) -> httpx.Response: + for attempt in range(self._max_retries + 1): + response = self._wrapped.handle_request(request) + if response.status_code != 429 or attempt >= self._max_retries: + return response + response.close() + time.sleep((attempt + 1) * 2) + return response # unreachable, but satisfies type checker + + +def new_rpc_client( + url: str, + timeout: float = 30, + max_retries: int = _DEFAULT_MAX_RETRIES, +) -> SolanaHTTPClient: + """Create a Solana RPC client with automatic retry on 429 responses.""" + client = SolanaHTTPClient(url, timeout=timeout) + # Replace the underlying httpx session with one using retry transport. + transport = _RetryTransport( + wrapped=httpx.HTTPTransport(), + max_retries=max_retries, + ) + client._provider.session = httpx.Client( + timeout=timeout, + transport=transport, + ) + return client diff --git a/sdk/telemetry/python/telemetry/state.py b/sdk/telemetry/python/telemetry/state.py new file mode 100644 index 000000000..5360e77ab --- /dev/null +++ b/sdk/telemetry/python/telemetry/state.py @@ -0,0 +1,141 @@ +"""On-chain account data structures for the telemetry program. + +Binary layout: 1-byte AccountType discriminator followed by Borsh-serialized +header fields, then raw u32 LE sample values (not a Borsh Vec — count is +determined by next_sample_index). +""" + +from __future__ import annotations + +from dataclasses import dataclass, field + +from borsh_incremental import IncrementalReader +from solders.pubkey import Pubkey # type: ignore[import-untyped] + + +TELEMETRY_SEED_PREFIX = b"telemetry" +DEVICE_LATENCY_SAMPLES_SEED = b"dzlatency" +INTERNET_LATENCY_SAMPLES_SEED = b"inetlatency" + +MAX_DEVICE_LATENCY_SAMPLES_PER_ACCOUNT = 35_000 +MAX_INTERNET_LATENCY_SAMPLES_PER_ACCOUNT = 3_000 + +DEVICE_LATENCY_HEADER_SIZE = 1 + 8 + 32 * 6 + 8 + 8 + 4 + 128 + + +def _read_pubkey(r: IncrementalReader) -> Pubkey: + return Pubkey.from_bytes(r.read_pubkey_raw()) + + +@dataclass +class DeviceLatencySamples: + account_type: int + epoch: int + origin_device_agent_pk: Pubkey + origin_device_pk: Pubkey + target_device_pk: Pubkey + origin_device_location_pk: Pubkey + target_device_location_pk: Pubkey + link_pk: Pubkey + sampling_interval_microseconds: int + start_timestamp_microseconds: int + next_sample_index: int + samples: list[int] = field(default_factory=list) + + @classmethod + def from_bytes(cls, data: bytes) -> DeviceLatencySamples: + if len(data) < DEVICE_LATENCY_HEADER_SIZE: + raise ValueError( + f"data too short for device latency header: {len(data)} < {DEVICE_LATENCY_HEADER_SIZE}" + ) + + r = IncrementalReader(data) + + account_type = r.read_u8() + epoch = r.read_u64() + origin_device_agent_pk = _read_pubkey(r) + origin_device_pk = _read_pubkey(r) + target_device_pk = _read_pubkey(r) + origin_device_location_pk = _read_pubkey(r) + target_device_location_pk = _read_pubkey(r) + link_pk = _read_pubkey(r) + sampling_interval = r.read_u64() + start_timestamp = r.read_u64() + next_sample_index = r.read_u32() + + r.read_bytes(128) # reserved + + count = min(next_sample_index, MAX_DEVICE_LATENCY_SAMPLES_PER_ACCOUNT) + samples: list[int] = [] + for _ in range(count): + if r.remaining < 4: + break + samples.append(r.read_u32()) + + return cls( + account_type=account_type, + epoch=epoch, + origin_device_agent_pk=origin_device_agent_pk, + origin_device_pk=origin_device_pk, + target_device_pk=target_device_pk, + origin_device_location_pk=origin_device_location_pk, + target_device_location_pk=target_device_location_pk, + link_pk=link_pk, + sampling_interval_microseconds=sampling_interval, + start_timestamp_microseconds=start_timestamp, + next_sample_index=next_sample_index, + samples=samples, + ) + + +@dataclass +class InternetLatencySamples: + account_type: int + epoch: int + data_provider_name: str + oracle_agent_pk: Pubkey + origin_exchange_pk: Pubkey + target_exchange_pk: Pubkey + sampling_interval_microseconds: int + start_timestamp_microseconds: int + next_sample_index: int + samples: list[int] = field(default_factory=list) + + @classmethod + def from_bytes(cls, data: bytes) -> InternetLatencySamples: + if len(data) < 10: + raise ValueError("data too short") + + r = IncrementalReader(data) + + account_type = r.read_u8() + epoch = r.read_u64() + data_provider_name = r.read_string() + oracle_agent_pk = _read_pubkey(r) + origin_exchange_pk = _read_pubkey(r) + target_exchange_pk = _read_pubkey(r) + sampling_interval = r.read_u64() + start_timestamp = r.read_u64() + next_sample_index = r.read_u32() + + r.read_bytes(128) # reserved + + count = min(next_sample_index, MAX_INTERNET_LATENCY_SAMPLES_PER_ACCOUNT) + samples: list[int] = [] + for _ in range(count): + if r.remaining < 4: + break + samples.append(r.read_u32()) + + return cls( + account_type=account_type, + epoch=epoch, + data_provider_name=data_provider_name, + oracle_agent_pk=oracle_agent_pk, + origin_exchange_pk=origin_exchange_pk, + target_exchange_pk=target_exchange_pk, + sampling_interval_microseconds=sampling_interval, + start_timestamp_microseconds=start_timestamp, + next_sample_index=next_sample_index, + samples=samples, + ) diff --git a/sdk/telemetry/python/telemetry/tests/__init__.py b/sdk/telemetry/python/telemetry/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sdk/telemetry/python/telemetry/tests/test_fixtures.py b/sdk/telemetry/python/telemetry/tests/test_fixtures.py new file mode 100644 index 000000000..308b8a633 --- /dev/null +++ b/sdk/telemetry/python/telemetry/tests/test_fixtures.py @@ -0,0 +1,77 @@ +"""Fixture-based compatibility tests.""" + +import json +from pathlib import Path + +from solders.pubkey import Pubkey # type: ignore[import-untyped] + +from telemetry.state import DeviceLatencySamples, InternetLatencySamples + +FIXTURES_DIR = Path(__file__).resolve().parent.parent.parent.parent / "testdata" / "fixtures" + + +def _load_fixture(name: str) -> tuple[bytes, dict]: + bin_data = (FIXTURES_DIR / f"{name}.bin").read_bytes() + meta = json.loads((FIXTURES_DIR / f"{name}.json").read_text()) + return bin_data, meta + + +def _assert_fields(expected_fields: list[dict], got: dict) -> None: + for f in expected_fields: + name = f["name"] + if name not in got: + continue + typ = f["typ"] + raw = f["value"] + actual = got[name] + if typ in ("u8", "u16", "u32", "u64"): + assert actual == int(raw), f"{name}: expected {raw}, got {actual}" + elif typ == "pubkey": + expected = Pubkey.from_string(raw) + assert actual == expected, f"{name}: expected {expected}, got {actual}" + elif typ == "string": + assert actual == raw, f"{name}: expected {raw}, got {actual}" + + +class TestFixtureDeviceLatencySamples: + def test_deserialize(self): + data, meta = _load_fixture("device_latency_samples") + d = DeviceLatencySamples.from_bytes(data) + _assert_fields( + meta["fields"], + { + "AccountType": d.account_type, + "Epoch": d.epoch, + "OriginDeviceAgentPK": d.origin_device_agent_pk, + "OriginDevicePK": d.origin_device_pk, + "TargetDevicePK": d.target_device_pk, + "OriginDeviceLocationPK": d.origin_device_location_pk, + "TargetDeviceLocationPK": d.target_device_location_pk, + "LinkPK": d.link_pk, + "SamplingIntervalMicroseconds": d.sampling_interval_microseconds, + "StartTimestampMicroseconds": d.start_timestamp_microseconds, + "NextSampleIndex": d.next_sample_index, + "SamplesCount": len(d.samples), + }, + ) + + +class TestFixtureInternetLatencySamples: + def test_deserialize(self): + data, meta = _load_fixture("internet_latency_samples") + d = InternetLatencySamples.from_bytes(data) + _assert_fields( + meta["fields"], + { + "AccountType": d.account_type, + "Epoch": d.epoch, + "DataProviderName": d.data_provider_name, + "OracleAgentPK": d.oracle_agent_pk, + "OriginExchangePK": d.origin_exchange_pk, + "TargetExchangePK": d.target_exchange_pk, + "SamplingIntervalMicroseconds": d.sampling_interval_microseconds, + "StartTimestampMicroseconds": d.start_timestamp_microseconds, + "NextSampleIndex": d.next_sample_index, + "SamplesCount": len(d.samples), + }, + ) diff --git a/sdk/telemetry/python/telemetry/tests/test_pda.py b/sdk/telemetry/python/telemetry/tests/test_pda.py new file mode 100644 index 000000000..444569b2d --- /dev/null +++ b/sdk/telemetry/python/telemetry/tests/test_pda.py @@ -0,0 +1,38 @@ +"""PDA derivation tests.""" + +from solders.pubkey import Pubkey # type: ignore[import-untyped] + +from telemetry.pda import ( + derive_device_latency_samples_pda, + derive_internet_latency_samples_pda, +) + +PROGRAM_ID = Pubkey.from_string("tE1exJ5VMyoC9ByZeSmgtNzJCFF74G9JAv338sJiqkC") + + +class TestDeriveDeviceLatencySamplesPDA: + def test_deterministic(self): + origin = Pubkey.from_string("11111111111111111111111111111112") + target = Pubkey.from_string("11111111111111111111111111111113") + link = Pubkey.from_string("11111111111111111111111111111114") + + addr1, bump1 = derive_device_latency_samples_pda( + PROGRAM_ID, origin, target, link, 42 + ) + addr2, bump2 = derive_device_latency_samples_pda( + PROGRAM_ID, origin, target, link, 42 + ) + assert addr1 == addr2 + assert bump1 == bump2 + + +class TestDeriveInternetLatencySamplesPDA: + def test_deterministic(self): + oracle = Pubkey.from_string("11111111111111111111111111111112") + origin = Pubkey.from_string("11111111111111111111111111111113") + target = Pubkey.from_string("11111111111111111111111111111114") + + addr1, _ = derive_internet_latency_samples_pda( + PROGRAM_ID, oracle, "RIPE Atlas", origin, target, 42 + ) + assert addr1 != Pubkey.default() diff --git a/sdk/telemetry/python/uv.lock b/sdk/telemetry/python/uv.lock new file mode 100644 index 000000000..d7c6c61a8 --- /dev/null +++ b/sdk/telemetry/python/uv.lock @@ -0,0 +1,373 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "construct" +version = "2.10.70" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/77/8c84b98eca70d245a2a956452f21d57930d22ab88cbeed9290ca630cf03f/construct-2.10.70.tar.gz", hash = "sha256:4d2472f9684731e58cc9c56c463be63baa1447d674e0d66aeb5627b22f512c29", size = 86337, upload-time = "2023-11-29T08:44:49.545Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/fb/08b3f4bf05da99aba8ffea52a558758def16e8516bc75ca94ff73587e7d3/construct-2.10.70-py3-none-any.whl", hash = "sha256:c80be81ef595a1a821ec69dc16099550ed22197615f4320b57cc9ce2a672cb30", size = 63020, upload-time = "2023-11-29T08:44:46.876Z" }, +] + +[[package]] +name = "construct-typing" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "construct" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/ae/659fe4866d89ef5a3a65cddbdd7b35882f4feb72db383821965f2fcea934/construct_typing-0.7.0.tar.gz", hash = "sha256:71d110dedff39bd3b603c734077032a7065bc597a49db1f5b03a211d05dbac23", size = 45104, upload-time = "2025-10-27T19:30:29.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/0c/2db6f7e1ae9795e436c6a0dc0bc38b12b8c8a228cb63203e24190b755b3b/construct_typing-0.7.0-py3-none-any.whl", hash = "sha256:c92383c6e8e5d07ba25811c8d5163820458d821e73bb1006541f43f89788646c", size = 24350, upload-time = "2025-10-27T19:30:27.505Z" }, +] + +[[package]] +name = "doublezero-borsh-incremental" +version = "0.0.1" +source = { editable = "../../borsh-incremental/python" } + +[package.metadata] + +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=9.0.2" }] + +[[package]] +name = "doublezero-telemetry" +version = "0.0.1" +source = { editable = "." } +dependencies = [ + { name = "doublezero-borsh-incremental" }, + { name = "httpx" }, + { name = "solana" }, + { name = "solders" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "doublezero-borsh-incremental", editable = "../../borsh-incremental/python" }, + { name = "httpx", specifier = ">=0.27" }, + { name = "solana", specifier = ">=0.35" }, + { name = "solders", specifier = ">=0.21" }, +] + +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=9.0.2" }] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jsonalias" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/45/ee7e17002cb7f3264f755ff6a1a72c55d1830e07808d643167d2a2277c4f/jsonalias-0.1.1.tar.gz", hash = "sha256:64f04d935397d579fc94509e1fcb6212f2d081235d9d6395bd10baedf760a769", size = 1095, upload-time = "2022-10-28T22:57:56.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/ed/05aebce69f78c104feff2ffcdd5a6f9d668a208aba3a8bf56e3750809fd8/jsonalias-0.1.1-py3-none-any.whl", hash = "sha256:a56d2888e6397812c606156504e861e8ec00e188005af149f003c787db3d3f18", size = 1312, upload-time = "2022-10-28T22:57:54.763Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "solana" +version = "0.36.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "construct-typing" }, + { name = "httpx" }, + { name = "solders" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/66/b8cd6e4d95bfe46798942ace31935e7799005a4e2180869dc7bac6b75be9/solana-0.36.11.tar.gz", hash = "sha256:2fdcf483674f4b88fe6510524bf3234a5837d19fe1815aa5a285f2739d28b3a3", size = 54516, upload-time = "2026-01-03T02:11:52.243Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/8d/807eebf0560759ad90464060e0d1d87ff5409beb6ed56104c553a83a976a/solana-0.36.11-py3-none-any.whl", hash = "sha256:1d659decc67a40ee1e9b5ded373a076b87cf3b4bd0645e120d16d9348c2025ba", size = 64786, upload-time = "2026-01-03T02:11:50.811Z" }, +] + +[[package]] +name = "solders" +version = "0.27.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonalias" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/25/80a81bb3dc4c70329dd0016edbdfbf2e8d8300a98ab9cd1a6ea0266bda7c/solders-0.27.1.tar.gz", hash = "sha256:7d8a24ad2f193afcdc02d6f3975917a7358b0f0ab7f4b3695b135ff2008222c8", size = 180923, upload-time = "2025-11-15T07:50:52.32Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/6b/0c0ee4766705824261779d00229fb95308d6b28422613e0e2af577f60ee3/solders-0.27.1-cp38-abi3-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:4dcd8e766bab24afbe9e0ae363d86f9810457e04b00c8a9149f69ca939ed587c", size = 24883435, upload-time = "2025-11-15T07:50:34.42Z" }, + { url = "https://files.pythonhosted.org/packages/33/1c/be04a1b26e18c409dd006d214198dc03f0b657c1cb34f4c83b763f8348f0/solders-0.27.1-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5d87b145cc0129095f9cff8c7f28d2e910bc5b5a4cf257c263b08a4b95f111dd", size = 6480729, upload-time = "2025-11-15T07:50:37.323Z" }, + { url = "https://files.pythonhosted.org/packages/48/03/98dc73c266b11ed5c13b3933510a1aa115becf97f45bec1a22da9d03ffa9/solders-0.27.1-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6082bbe46b7b1b2b005d046011f89fcae75fc5ea4f1a0ef5c2e9dfb5fe7930ce", size = 12744782, upload-time = "2025-11-15T07:50:39.283Z" }, + { url = "https://files.pythonhosted.org/packages/a0/39/35384d8fb80d05937bd9e8af7237cfe3f0d017c8aba357209d90d428f3a0/solders-0.27.1-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ccb821c2e4af43d976f312086f248a67352b3986e5f4c87af41cfeac6d8b5683", size = 6601257, upload-time = "2025-11-15T07:50:41.738Z" }, + { url = "https://files.pythonhosted.org/packages/8c/65/8989e521142473bf1130613476a4449e106bb97ed6cc86097f6f519b1234/solders-0.27.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:663a10566ae81f67c4515d4db5fbf51b735204741728c1a5cde11c4e019a51df", size = 7277802, upload-time = "2025-11-15T07:50:43.789Z" }, + { url = "https://files.pythonhosted.org/packages/f2/41/87ecf12cec0e7aa9c67b0cf1b8079fb28aa0af91e97328a3bd0c5e3001ba/solders-0.27.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:d14f05a77dbbf7966fb26f255c81302e6127550bdb66c2fdc99f522043fdf376", size = 7082541, upload-time = "2025-11-15T07:50:45.847Z" }, + { url = "https://files.pythonhosted.org/packages/33/b9/35e6f59b41bb205b26c7318fcdca43f3d59464fd3ddc13d36f36427f64d4/solders-0.27.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f778eeab411acec0a765a01c7b772f8eca8a8543d98276bd83cb826960da211b", size = 6845568, upload-time = "2025-11-15T07:50:47.698Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f3/14ed12d8d5047ababaca3271f82ebbf500ff74b6358f283962232103a12d/solders-0.27.1-cp38-abi3-win_amd64.whl", hash = "sha256:f3b787c29570a46d219c7a67543d8b0fadc73abda346653aa20e8eccd839e78b", size = 5295092, upload-time = "2025-11-15T07:50:50.517Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" }, + { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" }, + { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" }, + { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" }, + { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" }, + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" }, + { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" }, + { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" }, + { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] diff --git a/sdk/telemetry/testdata/fixtures/device_latency_samples.bin b/sdk/telemetry/testdata/fixtures/device_latency_samples.bin new file mode 100644 index 000000000..30e89af0d Binary files /dev/null and b/sdk/telemetry/testdata/fixtures/device_latency_samples.bin differ diff --git a/sdk/telemetry/testdata/fixtures/device_latency_samples.json b/sdk/telemetry/testdata/fixtures/device_latency_samples.json new file mode 100644 index 000000000..60eafaa6b --- /dev/null +++ b/sdk/telemetry/testdata/fixtures/device_latency_samples.json @@ -0,0 +1,66 @@ +{ + "name": "device_latency_samples", + "account_type": 3, + "fields": [ + { + "name": "AccountType", + "value": "3", + "typ": "u8" + }, + { + "name": "Epoch", + "value": "19800", + "typ": "u64" + }, + { + "name": "OriginDeviceAgentPK", + "value": "4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM", + "typ": "pubkey" + }, + { + "name": "OriginDevicePK", + "value": "8opHzTAnfzRpPEx21XtnrVTX28YQuCpAjcn1PczScKh", + "typ": "pubkey" + }, + { + "name": "TargetDevicePK", + "value": "CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3", + "typ": "pubkey" + }, + { + "name": "OriginDeviceLocationPK", + "value": "GcdayuLaLyrdmUu324nahyv33G5poQdLUEZ1nEytDeP", + "typ": "pubkey" + }, + { + "name": "TargetDeviceLocationPK", + "value": "LX3EUdRUBUa3TbsYXLEUdj9J3prXkWXvLYSWyYyc2Jj", + "typ": "pubkey" + }, + { + "name": "LinkPK", + "value": "QRSsyMWN1yHT9ir42bgNZUNZ4PdEhcSWCrL2AryKpy5", + "typ": "pubkey" + }, + { + "name": "SamplingIntervalMicroseconds", + "value": "5000000", + "typ": "u64" + }, + { + "name": "StartTimestampMicroseconds", + "value": "1700000000000000", + "typ": "u64" + }, + { + "name": "NextSampleIndex", + "value": "5", + "typ": "u32" + }, + { + "name": "SamplesCount", + "value": "5", + "typ": "u32" + } + ] +} \ No newline at end of file diff --git a/sdk/telemetry/testdata/fixtures/generate-fixtures/Cargo.lock b/sdk/telemetry/testdata/fixtures/generate-fixtures/Cargo.lock new file mode 100644 index 000000000..cec84946f --- /dev/null +++ b/sdk/telemetry/testdata/fixtures/generate-fixtures/Cargo.lock @@ -0,0 +1,3320 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "ark-bn254" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a22f4561524cd949590d78d7d4c5df8f592430d221f7f3c9497bbafd8972120f" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-std", +] + +[[package]] +name = "ark-ec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defd9a439d56ac24968cca0571f598a61bc8c55f71d50a89cda591cb750670ba" +dependencies = [ + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", + "derivative", + "hashbrown 0.13.2", + "itertools", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" +dependencies = [ + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", + "derivative", + "digest 0.10.7", + "itertools", + "num-bigint", + "num-traits", + "paste", + "rustc_version", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-poly" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d320bfc44ee185d899ccbadfa8bc31aab923ce1558716e1997a1e74057fe86bf" +dependencies = [ + "ark-ff", + "ark-serialize", + "ark-std", + "derivative", + "hashbrown 0.13.2", +] + +[[package]] +name = "ark-serialize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" +dependencies = [ + "ark-serialize-derive", + "ark-std", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae3281bc6d0fd7e549af32b52511e1302185bd688fd3359fa36423346ff682ea" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "blake3" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "borsh" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115e54d64eb62cdebad391c19efc9dce4981c690c85a33a12199d99bb9546fee" +dependencies = [ + "borsh-derive 0.10.4", + "hashbrown 0.13.2", +] + +[[package]] +name = "borsh" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +dependencies = [ + "borsh-derive 1.6.0", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831213f80d9423998dd696e2c5345aba6be7a0bd8cd19e31c5243e13df1cef89" +dependencies = [ + "borsh-derive-internal", + "borsh-schema-derive-internal", + "proc-macro-crate 0.1.5", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "borsh-derive" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +dependencies = [ + "once_cell", + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "borsh-derive-internal" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65d6ba50644c98714aa2a70d13d7df3cd75cd2b523a2b452bf010443800976b3" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "borsh-incremental" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0faa79093f85698e0075c813bf87c52044e832e1f9baa5cb0f126e4c2b2d29dd" +dependencies = [ + "borsh 1.6.0", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "borsh-schema-derive-internal" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276691d96f063427be83e6692b86148e488ebba9f48f77788724ca027ba3b6d4" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8834bb1d8ee5dc048ee3124f2c7c1afcc6bc9aed03f11e9dfd8c69470a5db340" +dependencies = [ + "feature-probe", + "serde", +] + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.2.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "cfg_eval" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45565fc9416b9896014f5732ac776f810ee53a66730c17e4020c3ec064a8f88f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "num-traits", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "console_log" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89f72f65e8501878b8a004d5a1afb780987e2ce2b4532c562e367a72c57499f" +dependencies = [ + "log", + "web-sys", +] + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f9d052967f590a76e62eb387bd0bbb1b000182c3cefe5364db6b7211651bc0" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rand_core 0.6.4", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.114", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "derivation-path" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e5c37193a1db1d8ed868c03ec7b152175f26160a5b740e5e484143877e0adf0" + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common", + "subtle", +] + +[[package]] +name = "doublezero-config" +version = "0.8.4" +dependencies = [ + "eyre", + "serde", + "solana-sdk", +] + +[[package]] +name = "doublezero-program-common" +version = "0.8.4" +dependencies = [ + "borsh 1.6.0", + "byteorder", + "ipnetwork", + "serde", + "solana-program", +] + +[[package]] +name = "doublezero-serviceability" +version = "0.8.4" +dependencies = [ + "bitflags", + "borsh 1.6.0", + "borsh-incremental", + "bytemuck", + "doublezero-program-common", + "ipnetwork", + "solana-program", + "thiserror", +] + +[[package]] +name = "doublezero-telemetry" +version = "0.8.4" +dependencies = [ + "borsh 1.6.0", + "borsh-incremental", + "doublezero-config", + "doublezero-program-common", + "doublezero-serviceability", + "solana-program", +] + +[[package]] +name = "ed25519" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +dependencies = [ + "curve25519-dalek 3.2.1", + "ed25519", + "rand 0.7.3", + "serde", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "ed25519-dalek-bip32" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d2be62a4061b872c8c0873ee4fc6f101ce7b889d039f019c5fa2af471a59908" +dependencies = [ + "derivation-path", + "ed25519-dalek", + "hmac 0.12.1", + "sha2 0.10.9", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "env_logger" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "feature-probe" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "five8" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75b8549488b4715defcb0d8a8a1c1c76a80661b5fa106b4ca0e7fce59d7d875" +dependencies = [ + "five8_core", +] + +[[package]] +name = "five8_const" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26dec3da8bc3ef08f2c04f61eab298c3ab334523e55f076354d6d6f613799a7b" +dependencies = [ + "five8_core", +] + +[[package]] +name = "five8_core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2551bf44bc5f776c15044b9b94153a00198be06743e262afaaa61f11ac7523a5" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "generate-telemetry-fixtures" +version = "0.0.0" +dependencies = [ + "borsh 1.6.0", + "doublezero-telemetry", + "serde", + "serde_json", + "solana-program", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hmac" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac-drbg" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1" +dependencies = [ + "digest 0.9.0", + "generic-array", + "hmac 0.8.1", +] + +[[package]] +name = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + +[[package]] +name = "ipnetwork" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf370abdafd54d13e54a620e8c3e1145f28e46cc9d704bc6d94414559df41763" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "libsecp256k1" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9d220bc1feda2ac231cb78c3d26f27676b8cf82c96971f7aeef3d0cf2797c73" +dependencies = [ + "arrayref", + "base64 0.12.3", + "digest 0.9.0", + "hmac-drbg", + "libsecp256k1-core", + "libsecp256k1-gen-ecmult", + "libsecp256k1-gen-genmult", + "rand 0.7.3", + "serde", + "sha2 0.9.9", + "typenum", +] + +[[package]] +name = "libsecp256k1-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f6ab710cec28cef759c5f18671a27dae2a5f952cdaaee1d8e2908cb2478a80" +dependencies = [ + "crunchy", + "digest 0.9.0", + "subtle", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccab96b584d38fac86a83f07e659f0deafd0253dc096dab5a36d53efe653c5c3" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67abfe149395e3aa1c48a2beb32b068e2334402df8181f818d3aee2b304c4f5d" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memmap2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "qstring" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d464fae65fff2680baf48019211ce37aaec0c78e9264c84a3e484717f965104e" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-big-array" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11fc7cc2c76d73e0f27ee52abbd64eec84d46f370c88371120433196934e4b7f" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_with" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +dependencies = [ + "serde_core", + "serde_with_macros", +] + +[[package]] +name = "serde_with_macros" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest 0.10.7", + "keccak", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "solana-account" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f949fe4edaeaea78c844023bfc1c898e0b1f5a100f8a8d2d0f85d0a7b090258" +dependencies = [ + "bincode", + "serde", + "serde_bytes", + "serde_derive", + "solana-account-info", + "solana-clock", + "solana-instruction", + "solana-pubkey", + "solana-sdk-ids", + "solana-sysvar", +] + +[[package]] +name = "solana-account-info" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8f5152a288ef1912300fc6efa6c2d1f9bb55d9398eb6c72326360b8063987da" +dependencies = [ + "bincode", + "serde", + "solana-program-error", + "solana-program-memory", + "solana-pubkey", +] + +[[package]] +name = "solana-address-lookup-table-interface" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1673f67efe870b64a65cb39e6194be5b26527691ce5922909939961a6e6b395" +dependencies = [ + "bincode", + "bytemuck", + "serde", + "serde_derive", + "solana-clock", + "solana-instruction", + "solana-pubkey", + "solana-sdk-ids", + "solana-slot-hashes", +] + +[[package]] +name = "solana-atomic-u64" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52e52720efe60465b052b9e7445a01c17550666beec855cce66f44766697bc2" +dependencies = [ + "parking_lot", +] + +[[package]] +name = "solana-big-mod-exp" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75db7f2bbac3e62cfd139065d15bcda9e2428883ba61fc8d27ccb251081e7567" +dependencies = [ + "num-bigint", + "num-traits", + "solana-define-syscall", +] + +[[package]] +name = "solana-bincode" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a3787b8cf9c9fe3dd360800e8b70982b9e5a8af9e11c354b6665dd4a003adc" +dependencies = [ + "bincode", + "serde", + "solana-instruction", +] + +[[package]] +name = "solana-blake3-hasher" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0801e25a1b31a14494fc80882a036be0ffd290efc4c2d640bfcca120a4672" +dependencies = [ + "blake3", + "solana-define-syscall", + "solana-hash", + "solana-sanitize", +] + +[[package]] +name = "solana-bn254" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4420f125118732833f36facf96a27e7b78314b2d642ba07fa9ffdacd8d79e243" +dependencies = [ + "ark-bn254", + "ark-ec", + "ark-ff", + "ark-serialize", + "bytemuck", + "solana-define-syscall", + "thiserror", +] + +[[package]] +name = "solana-borsh" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "718333bcd0a1a7aed6655aa66bef8d7fb047944922b2d3a18f49cbc13e73d004" +dependencies = [ + "borsh 0.10.4", + "borsh 1.6.0", +] + +[[package]] +name = "solana-client-traits" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83f0071874e629f29e0eb3dab8a863e98502ac7aba55b7e0df1803fc5cac72a7" +dependencies = [ + "solana-account", + "solana-commitment-config", + "solana-epoch-info", + "solana-hash", + "solana-instruction", + "solana-keypair", + "solana-message", + "solana-pubkey", + "solana-signature", + "solana-signer", + "solana-system-interface", + "solana-transaction", + "solana-transaction-error", +] + +[[package]] +name = "solana-clock" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bb482ab70fced82ad3d7d3d87be33d466a3498eb8aa856434ff3c0dfc2e2e31" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-cluster-type" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ace9fea2daa28354d107ea879cff107181d85cd4e0f78a2bedb10e1a428c97e" +dependencies = [ + "serde", + "serde_derive", + "solana-hash", +] + +[[package]] +name = "solana-commitment-config" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac49c4dde3edfa832de1697e9bcdb7c3b3f7cb7a1981b7c62526c8bb6700fb73" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "solana-compute-budget-interface" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8432d2c4c22d0499aa06d62e4f7e333f81777b3d7c96050ae9e5cb71a8c3aee4" +dependencies = [ + "borsh 1.6.0", + "serde", + "serde_derive", + "solana-instruction", + "solana-sdk-ids", +] + +[[package]] +name = "solana-cpi" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc71126edddc2ba014622fc32d0f5e2e78ec6c5a1e0eb511b85618c09e9ea11" +dependencies = [ + "solana-account-info", + "solana-define-syscall", + "solana-instruction", + "solana-program-error", + "solana-pubkey", + "solana-stable-layout", +] + +[[package]] +name = "solana-decode-error" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c781686a18db2f942e70913f7ca15dc120ec38dcab42ff7557db2c70c625a35" +dependencies = [ + "num-traits", +] + +[[package]] +name = "solana-define-syscall" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae3e2abcf541c8122eafe9a625d4d194b4023c20adde1e251f94e056bb1aee2" + +[[package]] +name = "solana-derivation-path" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "939756d798b25c5ec3cca10e06212bdca3b1443cb9bb740a38124f58b258737b" +dependencies = [ + "derivation-path", + "qstring", + "uriparse", +] + +[[package]] +name = "solana-ed25519-program" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1feafa1691ea3ae588f99056f4bdd1293212c7ece28243d7da257c443e84753" +dependencies = [ + "bytemuck", + "bytemuck_derive", + "ed25519-dalek", + "solana-feature-set", + "solana-instruction", + "solana-precompile-error", + "solana-sdk-ids", +] + +[[package]] +name = "solana-epoch-info" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ef6f0b449290b0b9f32973eefd95af35b01c5c0c34c569f936c34c5b20d77b" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "solana-epoch-rewards" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b575d3dd323b9ea10bb6fe89bf6bf93e249b215ba8ed7f68f1a3633f384db7" +dependencies = [ + "serde", + "serde_derive", + "solana-hash", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-epoch-rewards-hasher" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c5fd2662ae7574810904585fd443545ed2b568dbd304b25a31e79ccc76e81b" +dependencies = [ + "siphasher", + "solana-hash", + "solana-pubkey", +] + +[[package]] +name = "solana-epoch-schedule" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fce071fbddecc55d727b1d7ed16a629afe4f6e4c217bc8d00af3b785f6f67ed" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-example-mocks" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84461d56cbb8bb8d539347151e0525b53910102e4bced875d49d5139708e39d3" +dependencies = [ + "serde", + "serde_derive", + "solana-address-lookup-table-interface", + "solana-clock", + "solana-hash", + "solana-instruction", + "solana-keccak-hasher", + "solana-message", + "solana-nonce", + "solana-pubkey", + "solana-sdk-ids", + "solana-system-interface", + "thiserror", +] + +[[package]] +name = "solana-feature-gate-interface" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f5c5382b449e8e4e3016fb05e418c53d57782d8b5c30aa372fc265654b956d" +dependencies = [ + "bincode", + "serde", + "serde_derive", + "solana-account", + "solana-account-info", + "solana-instruction", + "solana-program-error", + "solana-pubkey", + "solana-rent", + "solana-sdk-ids", + "solana-system-interface", +] + +[[package]] +name = "solana-feature-set" +version = "2.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93b93971e289d6425f88e6e3cb6668c4b05df78b3c518c249be55ced8efd6b6d" +dependencies = [ + "ahash", + "lazy_static", + "solana-epoch-schedule", + "solana-hash", + "solana-pubkey", + "solana-sha256-hasher", +] + +[[package]] +name = "solana-fee-calculator" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89bc408da0fb3812bc3008189d148b4d3e08252c79ad810b245482a3f70cd8d" +dependencies = [ + "log", + "serde", + "serde_derive", +] + +[[package]] +name = "solana-fee-structure" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33adf673581c38e810bf618f745bf31b683a0a4a4377682e6aaac5d9a058dd4e" +dependencies = [ + "serde", + "serde_derive", + "solana-message", + "solana-native-token", +] + +[[package]] +name = "solana-genesis-config" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3725085d47b96d37fef07a29d78d2787fc89a0b9004c66eed7753d1e554989f" +dependencies = [ + "bincode", + "chrono", + "memmap2", + "serde", + "serde_derive", + "solana-account", + "solana-clock", + "solana-cluster-type", + "solana-epoch-schedule", + "solana-fee-calculator", + "solana-hash", + "solana-inflation", + "solana-keypair", + "solana-logger", + "solana-poh-config", + "solana-pubkey", + "solana-rent", + "solana-sdk-ids", + "solana-sha256-hasher", + "solana-shred-version", + "solana-signer", + "solana-time-utils", +] + +[[package]] +name = "solana-hard-forks" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c28371f878e2ead55611d8ba1b5fb879847156d04edea13693700ad1a28baf" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "solana-hash" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b96e9f0300fa287b545613f007dfe20043d7812bee255f418c1eb649c93b63" +dependencies = [ + "borsh 1.6.0", + "bytemuck", + "bytemuck_derive", + "five8", + "js-sys", + "serde", + "serde_derive", + "solana-atomic-u64", + "solana-sanitize", + "wasm-bindgen", +] + +[[package]] +name = "solana-inflation" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23eef6a09eb8e568ce6839573e4966850e85e9ce71e6ae1a6c930c1c43947de3" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "solana-instruction" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab5682934bd1f65f8d2c16f21cb532526fcc1a09f796e2cacdb091eee5774ad" +dependencies = [ + "bincode", + "borsh 1.6.0", + "getrandom 0.2.17", + "js-sys", + "num-traits", + "serde", + "serde_derive", + "serde_json", + "solana-define-syscall", + "solana-pubkey", + "wasm-bindgen", +] + +[[package]] +name = "solana-instructions-sysvar" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0e85a6fad5c2d0c4f5b91d34b8ca47118fc593af706e523cdbedf846a954f57" +dependencies = [ + "bitflags", + "solana-account-info", + "solana-instruction", + "solana-program-error", + "solana-pubkey", + "solana-sanitize", + "solana-sdk-ids", + "solana-serialize-utils", + "solana-sysvar-id", +] + +[[package]] +name = "solana-keccak-hasher" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7aeb957fbd42a451b99235df4942d96db7ef678e8d5061ef34c9b34cae12f79" +dependencies = [ + "sha3", + "solana-define-syscall", + "solana-hash", + "solana-sanitize", +] + +[[package]] +name = "solana-keypair" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd3f04aa1a05c535e93e121a95f66e7dcccf57e007282e8255535d24bf1e98bb" +dependencies = [ + "ed25519-dalek", + "ed25519-dalek-bip32", + "five8", + "rand 0.7.3", + "solana-derivation-path", + "solana-pubkey", + "solana-seed-derivable", + "solana-seed-phrase", + "solana-signature", + "solana-signer", + "wasm-bindgen", +] + +[[package]] +name = "solana-last-restart-slot" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6360ac2fdc72e7463565cd256eedcf10d7ef0c28a1249d261ec168c1b55cdd" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-loader-v2-interface" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8ab08006dad78ae7cd30df8eea0539e207d08d91eaefb3e1d49a446e1c49654" +dependencies = [ + "serde", + "serde_bytes", + "serde_derive", + "solana-instruction", + "solana-pubkey", + "solana-sdk-ids", +] + +[[package]] +name = "solana-loader-v3-interface" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f7162a05b8b0773156b443bccd674ea78bb9aa406325b467ea78c06c99a63a2" +dependencies = [ + "serde", + "serde_bytes", + "serde_derive", + "solana-instruction", + "solana-pubkey", + "solana-sdk-ids", + "solana-system-interface", +] + +[[package]] +name = "solana-loader-v4-interface" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "706a777242f1f39a83e2a96a2a6cb034cb41169c6ecbee2cf09cb873d9659e7e" +dependencies = [ + "serde", + "serde_bytes", + "serde_derive", + "solana-instruction", + "solana-pubkey", + "solana-sdk-ids", + "solana-system-interface", +] + +[[package]] +name = "solana-logger" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8e777ec1afd733939b532a42492d888ec7c88d8b4127a5d867eb45c6eb5cd5" +dependencies = [ + "env_logger", + "lazy_static", + "libc", + "log", + "signal-hook", +] + +[[package]] +name = "solana-message" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1796aabce376ff74bf89b78d268fa5e683d7d7a96a0a4e4813ec34de49d5314b" +dependencies = [ + "bincode", + "blake3", + "lazy_static", + "serde", + "serde_derive", + "solana-bincode", + "solana-hash", + "solana-instruction", + "solana-pubkey", + "solana-sanitize", + "solana-sdk-ids", + "solana-short-vec", + "solana-system-interface", + "solana-transaction-error", + "wasm-bindgen", +] + +[[package]] +name = "solana-msg" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36a1a14399afaabc2781a1db09cb14ee4cc4ee5c7a5a3cfcc601811379a8092" +dependencies = [ + "solana-define-syscall", +] + +[[package]] +name = "solana-native-token" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61515b880c36974053dd499c0510066783f0cc6ac17def0c7ef2a244874cf4a9" + +[[package]] +name = "solana-nonce" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703e22eb185537e06204a5bd9d509b948f0066f2d1d814a6f475dafb3ddf1325" +dependencies = [ + "serde", + "serde_derive", + "solana-fee-calculator", + "solana-hash", + "solana-pubkey", + "solana-sha256-hasher", +] + +[[package]] +name = "solana-nonce-account" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde971a20b8dbf60144d6a84439dda86b5466e00e2843091fe731083cda614da" +dependencies = [ + "solana-account", + "solana-hash", + "solana-nonce", + "solana-sdk-ids", +] + +[[package]] +name = "solana-offchain-message" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b526398ade5dea37f1f147ce55dae49aa017a5d7326606359b0445ca8d946581" +dependencies = [ + "num_enum", + "solana-hash", + "solana-packet", + "solana-pubkey", + "solana-sanitize", + "solana-sha256-hasher", + "solana-signature", + "solana-signer", +] + +[[package]] +name = "solana-packet" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "004f2d2daf407b3ec1a1ca5ec34b3ccdfd6866dd2d3c7d0715004a96e4b6d127" +dependencies = [ + "bincode", + "bitflags", + "cfg_eval", + "serde", + "serde_derive", + "serde_with", +] + +[[package]] +name = "solana-poh-config" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d650c3b4b9060082ac6b0efbbb66865089c58405bfb45de449f3f2b91eccee75" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "solana-precompile-error" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d87b2c1f5de77dfe2b175ee8dd318d196aaca4d0f66f02842f80c852811f9f8" +dependencies = [ + "num-traits", + "solana-decode-error", +] + +[[package]] +name = "solana-precompiles" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36e92768a57c652edb0f5d1b30a7d0bc64192139c517967c18600debe9ae3832" +dependencies = [ + "lazy_static", + "solana-ed25519-program", + "solana-feature-set", + "solana-message", + "solana-precompile-error", + "solana-pubkey", + "solana-sdk-ids", + "solana-secp256k1-program", + "solana-secp256r1-program", +] + +[[package]] +name = "solana-presigner" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81a57a24e6a4125fc69510b6774cd93402b943191b6cddad05de7281491c90fe" +dependencies = [ + "solana-pubkey", + "solana-signature", + "solana-signer", +] + +[[package]] +name = "solana-program" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98eca145bd3545e2fbb07166e895370576e47a00a7d824e325390d33bf467210" +dependencies = [ + "bincode", + "blake3", + "borsh 0.10.4", + "borsh 1.6.0", + "bs58", + "bytemuck", + "console_error_panic_hook", + "console_log", + "getrandom 0.2.17", + "lazy_static", + "log", + "memoffset", + "num-bigint", + "num-derive", + "num-traits", + "rand 0.8.5", + "serde", + "serde_bytes", + "serde_derive", + "solana-account-info", + "solana-address-lookup-table-interface", + "solana-atomic-u64", + "solana-big-mod-exp", + "solana-bincode", + "solana-blake3-hasher", + "solana-borsh", + "solana-clock", + "solana-cpi", + "solana-decode-error", + "solana-define-syscall", + "solana-epoch-rewards", + "solana-epoch-schedule", + "solana-example-mocks", + "solana-feature-gate-interface", + "solana-fee-calculator", + "solana-hash", + "solana-instruction", + "solana-instructions-sysvar", + "solana-keccak-hasher", + "solana-last-restart-slot", + "solana-loader-v2-interface", + "solana-loader-v3-interface", + "solana-loader-v4-interface", + "solana-message", + "solana-msg", + "solana-native-token", + "solana-nonce", + "solana-program-entrypoint", + "solana-program-error", + "solana-program-memory", + "solana-program-option", + "solana-program-pack", + "solana-pubkey", + "solana-rent", + "solana-sanitize", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-secp256k1-recover", + "solana-serde-varint", + "solana-serialize-utils", + "solana-sha256-hasher", + "solana-short-vec", + "solana-slot-hashes", + "solana-slot-history", + "solana-stable-layout", + "solana-stake-interface", + "solana-system-interface", + "solana-sysvar", + "solana-sysvar-id", + "solana-vote-interface", + "thiserror", + "wasm-bindgen", +] + +[[package]] +name = "solana-program-entrypoint" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32ce041b1a0ed275290a5008ee1a4a6c48f5054c8a3d78d313c08958a06aedbd" +dependencies = [ + "solana-account-info", + "solana-msg", + "solana-program-error", + "solana-pubkey", +] + +[[package]] +name = "solana-program-error" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ee2e0217d642e2ea4bee237f37bd61bb02aec60da3647c48ff88f6556ade775" +dependencies = [ + "borsh 1.6.0", + "num-traits", + "serde", + "serde_derive", + "solana-decode-error", + "solana-instruction", + "solana-msg", + "solana-pubkey", +] + +[[package]] +name = "solana-program-memory" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a5426090c6f3fd6cfdc10685322fede9ca8e5af43cd6a59e98bfe4e91671712" +dependencies = [ + "solana-define-syscall", +] + +[[package]] +name = "solana-program-option" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc677a2e9bc616eda6dbdab834d463372b92848b2bfe4a1ed4e4b4adba3397d0" + +[[package]] +name = "solana-program-pack" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "319f0ef15e6e12dc37c597faccb7d62525a509fec5f6975ecb9419efddeb277b" +dependencies = [ + "solana-program-error", +] + +[[package]] +name = "solana-pubkey" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b62adb9c3261a052ca1f999398c388f1daf558a1b492f60a6d9e64857db4ff1" +dependencies = [ + "borsh 0.10.4", + "borsh 1.6.0", + "bytemuck", + "bytemuck_derive", + "curve25519-dalek 4.1.3", + "five8", + "five8_const", + "getrandom 0.2.17", + "js-sys", + "num-traits", + "rand 0.8.5", + "serde", + "serde_derive", + "solana-atomic-u64", + "solana-decode-error", + "solana-define-syscall", + "solana-sanitize", + "solana-sha256-hasher", + "wasm-bindgen", +] + +[[package]] +name = "solana-quic-definitions" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf0d4d5b049eb1d0c35f7b18f305a27c8986fc5c0c9b383e97adaa35334379e" +dependencies = [ + "solana-keypair", +] + +[[package]] +name = "solana-rent" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1aea8fdea9de98ca6e8c2da5827707fb3842833521b528a713810ca685d2480" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-rent-collector" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "127e6dfa51e8c8ae3aa646d8b2672bc4ac901972a338a9e1cd249e030564fb9d" +dependencies = [ + "serde", + "serde_derive", + "solana-account", + "solana-clock", + "solana-epoch-schedule", + "solana-genesis-config", + "solana-pubkey", + "solana-rent", + "solana-sdk-ids", +] + +[[package]] +name = "solana-rent-debits" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f6f9113c6003492e74438d1288e30cffa8ccfdc2ef7b49b9e816d8034da18cd" +dependencies = [ + "solana-pubkey", + "solana-reward-info", +] + +[[package]] +name = "solana-reserved-account-keys" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4b22ea19ca2a3f28af7cd047c914abf833486bf7a7c4a10fc652fff09b385b1" +dependencies = [ + "lazy_static", + "solana-feature-set", + "solana-pubkey", + "solana-sdk-ids", +] + +[[package]] +name = "solana-reward-info" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18205b69139b1ae0ab8f6e11cdcb627328c0814422ad2482000fa2ca54ae4a2f" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "solana-sanitize" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61f1bc1357b8188d9c4a3af3fc55276e56987265eb7ad073ae6f8180ee54cecf" + +[[package]] +name = "solana-sdk" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cc0e4a7635b902791c44b6581bfb82f3ada32c5bc0929a64f39fe4bb384c86a" +dependencies = [ + "bincode", + "bs58", + "getrandom 0.1.16", + "js-sys", + "serde", + "serde_json", + "solana-account", + "solana-bn254", + "solana-client-traits", + "solana-cluster-type", + "solana-commitment-config", + "solana-compute-budget-interface", + "solana-decode-error", + "solana-derivation-path", + "solana-ed25519-program", + "solana-epoch-info", + "solana-epoch-rewards-hasher", + "solana-feature-set", + "solana-fee-structure", + "solana-genesis-config", + "solana-hard-forks", + "solana-inflation", + "solana-instruction", + "solana-keypair", + "solana-message", + "solana-native-token", + "solana-nonce-account", + "solana-offchain-message", + "solana-packet", + "solana-poh-config", + "solana-precompile-error", + "solana-precompiles", + "solana-presigner", + "solana-program", + "solana-program-memory", + "solana-pubkey", + "solana-quic-definitions", + "solana-rent-collector", + "solana-rent-debits", + "solana-reserved-account-keys", + "solana-reward-info", + "solana-sanitize", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-secp256k1-program", + "solana-secp256k1-recover", + "solana-secp256r1-program", + "solana-seed-derivable", + "solana-seed-phrase", + "solana-serde", + "solana-serde-varint", + "solana-short-vec", + "solana-shred-version", + "solana-signature", + "solana-signer", + "solana-system-transaction", + "solana-time-utils", + "solana-transaction", + "solana-transaction-context", + "solana-transaction-error", + "solana-validator-exit", + "thiserror", + "wasm-bindgen", +] + +[[package]] +name = "solana-sdk-ids" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5d8b9cc68d5c88b062a33e23a6466722467dde0035152d8fb1afbcdf350a5f" +dependencies = [ + "solana-pubkey", +] + +[[package]] +name = "solana-sdk-macro" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86280da8b99d03560f6ab5aca9de2e38805681df34e0bb8f238e69b29433b9df" +dependencies = [ + "bs58", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "solana-secp256k1-program" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f19833e4bc21558fe9ec61f239553abe7d05224347b57d65c2218aeeb82d6149" +dependencies = [ + "bincode", + "digest 0.10.7", + "libsecp256k1", + "serde", + "serde_derive", + "sha3", + "solana-feature-set", + "solana-instruction", + "solana-precompile-error", + "solana-sdk-ids", + "solana-signature", +] + +[[package]] +name = "solana-secp256k1-recover" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa3120b6cdaa270f39444f5093a90a7b03d296d362878f7a6991d6de3bbe496" +dependencies = [ + "borsh 1.6.0", + "libsecp256k1", + "solana-define-syscall", + "thiserror", +] + +[[package]] +name = "solana-secp256r1-program" +version = "2.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce0ae46da3071a900f02d367d99b2f3058fe2e90c5062ac50c4f20cfedad8f0f" +dependencies = [ + "bytemuck", + "openssl", + "solana-feature-set", + "solana-instruction", + "solana-precompile-error", + "solana-sdk-ids", +] + +[[package]] +name = "solana-seed-derivable" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beb82b5adb266c6ea90e5cf3967235644848eac476c5a1f2f9283a143b7c97f" +dependencies = [ + "solana-derivation-path", +] + +[[package]] +name = "solana-seed-phrase" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36187af2324f079f65a675ec22b31c24919cb4ac22c79472e85d819db9bbbc15" +dependencies = [ + "hmac 0.12.1", + "pbkdf2", + "sha2 0.10.9", +] + +[[package]] +name = "solana-serde" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1931484a408af466e14171556a47adaa215953c7f48b24e5f6b0282763818b04" +dependencies = [ + "serde", +] + +[[package]] +name = "solana-serde-varint" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a7e155eba458ecfb0107b98236088c3764a09ddf0201ec29e52a0be40857113" +dependencies = [ + "serde", +] + +[[package]] +name = "solana-serialize-utils" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "817a284b63197d2b27afdba829c5ab34231da4a9b4e763466a003c40ca4f535e" +dependencies = [ + "solana-instruction", + "solana-pubkey", + "solana-sanitize", +] + +[[package]] +name = "solana-sha256-hasher" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa3feb32c28765f6aa1ce8f3feac30936f16c5c3f7eb73d63a5b8f6f8ecdc44" +dependencies = [ + "sha2 0.10.9", + "solana-define-syscall", + "solana-hash", +] + +[[package]] +name = "solana-short-vec" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c54c66f19b9766a56fa0057d060de8378676cb64987533fa088861858fc5a69" +dependencies = [ + "serde", +] + +[[package]] +name = "solana-shred-version" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afd3db0461089d1ad1a78d9ba3f15b563899ca2386351d38428faa5350c60a98" +dependencies = [ + "solana-hard-forks", + "solana-hash", + "solana-sha256-hasher", +] + +[[package]] +name = "solana-signature" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64c8ec8e657aecfc187522fc67495142c12f35e55ddeca8698edbb738b8dbd8c" +dependencies = [ + "ed25519-dalek", + "five8", + "rand 0.8.5", + "serde", + "serde-big-array", + "serde_derive", + "solana-sanitize", +] + +[[package]] +name = "solana-signer" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c41991508a4b02f021c1342ba00bcfa098630b213726ceadc7cb032e051975b" +dependencies = [ + "solana-pubkey", + "solana-signature", + "solana-transaction-error", +] + +[[package]] +name = "solana-slot-hashes" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8691982114513763e88d04094c9caa0376b867a29577939011331134c301ce" +dependencies = [ + "serde", + "serde_derive", + "solana-hash", + "solana-sdk-ids", + "solana-sysvar-id", +] + +[[package]] +name = "solana-slot-history" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ccc1b2067ca22754d5283afb2b0126d61eae734fc616d23871b0943b0d935e" +dependencies = [ + "bv", + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sysvar-id", +] + +[[package]] +name = "solana-stable-layout" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f14f7d02af8f2bc1b5efeeae71bc1c2b7f0f65cd75bcc7d8180f2c762a57f54" +dependencies = [ + "solana-instruction", + "solana-pubkey", +] + +[[package]] +name = "solana-stake-interface" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5269e89fde216b4d7e1d1739cf5303f8398a1ff372a81232abbee80e554a838c" +dependencies = [ + "borsh 0.10.4", + "borsh 1.6.0", + "num-traits", + "serde", + "serde_derive", + "solana-clock", + "solana-cpi", + "solana-decode-error", + "solana-instruction", + "solana-program-error", + "solana-pubkey", + "solana-system-interface", + "solana-sysvar-id", +] + +[[package]] +name = "solana-system-interface" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94d7c18cb1a91c6be5f5a8ac9276a1d7c737e39a21beba9ea710ab4b9c63bc90" +dependencies = [ + "js-sys", + "num-traits", + "serde", + "serde_derive", + "solana-decode-error", + "solana-instruction", + "solana-pubkey", + "wasm-bindgen", +] + +[[package]] +name = "solana-system-transaction" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bd98a25e5bcba8b6be8bcbb7b84b24c2a6a8178d7fb0e3077a916855ceba91a" +dependencies = [ + "solana-hash", + "solana-keypair", + "solana-message", + "solana-pubkey", + "solana-signer", + "solana-system-interface", + "solana-transaction", +] + +[[package]] +name = "solana-sysvar" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c3595f95069f3d90f275bb9bd235a1973c4d059028b0a7f81baca2703815db" +dependencies = [ + "base64 0.22.1", + "bincode", + "bytemuck", + "bytemuck_derive", + "lazy_static", + "serde", + "serde_derive", + "solana-account-info", + "solana-clock", + "solana-define-syscall", + "solana-epoch-rewards", + "solana-epoch-schedule", + "solana-fee-calculator", + "solana-hash", + "solana-instruction", + "solana-instructions-sysvar", + "solana-last-restart-slot", + "solana-program-entrypoint", + "solana-program-error", + "solana-program-memory", + "solana-pubkey", + "solana-rent", + "solana-sanitize", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-slot-hashes", + "solana-slot-history", + "solana-stake-interface", + "solana-sysvar-id", +] + +[[package]] +name = "solana-sysvar-id" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5762b273d3325b047cfda250787f8d796d781746860d5d0a746ee29f3e8812c1" +dependencies = [ + "solana-pubkey", + "solana-sdk-ids", +] + +[[package]] +name = "solana-time-utils" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af261afb0e8c39252a04d026e3ea9c405342b08c871a2ad8aa5448e068c784c" + +[[package]] +name = "solana-transaction" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80657d6088f721148f5d889c828ca60c7daeedac9a8679f9ec215e0c42bcbf41" +dependencies = [ + "bincode", + "serde", + "serde_derive", + "solana-bincode", + "solana-feature-set", + "solana-hash", + "solana-instruction", + "solana-keypair", + "solana-message", + "solana-precompiles", + "solana-pubkey", + "solana-sanitize", + "solana-sdk-ids", + "solana-short-vec", + "solana-signature", + "solana-signer", + "solana-system-interface", + "solana-transaction-error", + "wasm-bindgen", +] + +[[package]] +name = "solana-transaction-context" +version = "2.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a312304361987a85b2ef2293920558e6612876a639dd1309daf6d0d59ef2fe" +dependencies = [ + "bincode", + "serde", + "serde_derive", + "solana-account", + "solana-instruction", + "solana-instructions-sysvar", + "solana-pubkey", + "solana-rent", + "solana-sdk-ids", +] + +[[package]] +name = "solana-transaction-error" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a9dc8fdb61c6088baab34fc3a8b8473a03a7a5fd404ed8dd502fa79b67cb1" +dependencies = [ + "serde", + "serde_derive", + "solana-instruction", + "solana-sanitize", +] + +[[package]] +name = "solana-validator-exit" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bbf6d7a3c0b28dd5335c52c0e9eae49d0ae489a8f324917faf0ded65a812c1d" + +[[package]] +name = "solana-vote-interface" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b80d57478d6599d30acc31cc5ae7f93ec2361a06aefe8ea79bc81739a08af4c3" +dependencies = [ + "bincode", + "num-derive", + "num-traits", + "serde", + "serde_derive", + "solana-clock", + "solana-decode-error", + "solana-hash", + "solana-instruction", + "solana-pubkey", + "solana-rent", + "solana-sdk-ids", + "solana-serde-varint", + "solana-serialize-utils", + "solana-short-vec", + "solana-system-interface", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ + "winnow", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "uriparse" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0200d0fc04d809396c2ad43f3c95da3582a2556eba8d453c1087f4120ee352ff" +dependencies = [ + "fnv", + "lazy_static", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.114", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "zerocopy" +version = "0.8.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7456cf00f0685ad319c5b1693f291a650eaf345e941d082fc4e03df8a03996ac" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1328722bbf2115db7e19d69ebcc15e795719e2d66b60827c6a69a117365e37a0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "zeroize" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "zmij" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1966f8ac2c1f76987d69a74d0e0f929241c10e78136434e3be70ff7f58f64214" diff --git a/sdk/telemetry/testdata/fixtures/generate-fixtures/Cargo.toml b/sdk/telemetry/testdata/fixtures/generate-fixtures/Cargo.toml new file mode 100644 index 000000000..e24d90213 --- /dev/null +++ b/sdk/telemetry/testdata/fixtures/generate-fixtures/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "generate-telemetry-fixtures" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies] +doublezero-telemetry = { path = "../../../../../smartcontract/programs/doublezero-telemetry", features = ["no-entrypoint"] } +borsh = "1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +solana-program = "2" diff --git a/sdk/telemetry/testdata/fixtures/generate-fixtures/src/main.rs b/sdk/telemetry/testdata/fixtures/generate-fixtures/src/main.rs new file mode 100644 index 000000000..2f4a29bb6 --- /dev/null +++ b/sdk/telemetry/testdata/fixtures/generate-fixtures/src/main.rs @@ -0,0 +1,155 @@ +//! Generates Borsh-serialized binary fixture files from the Rust telemetry structs +//! with known field values. The Go/TypeScript/Python SDK compatibility tests deserialize +//! these fixtures and verify that field values match. +//! +//! Run with: cargo run (from this directory) +//! Output: ../fixtures/*.bin and ../fixtures/*.json + +use std::fs; +use std::path::Path; + +use borsh::BorshSerialize; +use doublezero_telemetry::state::{ + accounttype::AccountType, + device_latency_samples::{DeviceLatencySamples, DeviceLatencySamplesHeader}, + internet_latency_samples::{InternetLatencySamples, InternetLatencySamplesHeader}, +}; +use serde::Serialize; +use solana_program::pubkey::Pubkey; + +#[derive(Serialize)] +struct FixtureMeta { + name: String, + account_type: u8, + fields: Vec, +} + +#[derive(Serialize)] +struct FieldValue { + name: String, + value: String, + #[serde(rename = "typ")] + typ: String, +} + +fn pubkey_from_byte(b: u8) -> Pubkey { + let mut bytes = [0u8; 32]; + bytes[0] = b; + Pubkey::new_from_array(bytes) +} + +fn pubkey_bs58(pk: &Pubkey) -> String { + pk.to_string() +} + +fn write_fixture(dir: &Path, name: &str, data: &[u8], meta: &FixtureMeta) { + fs::write(dir.join(format!("{name}.bin")), data).unwrap(); + let json = serde_json::to_string_pretty(meta).unwrap(); + fs::write(dir.join(format!("{name}.json")), json).unwrap(); + println!("wrote {name}.bin ({} bytes) and {name}.json", data.len()); +} + +fn main() { + let fixtures_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join(".."); + fs::create_dir_all(&fixtures_dir).unwrap(); + + generate_device_latency_samples(&fixtures_dir); + generate_internet_latency_samples(&fixtures_dir); + + println!("\nall fixtures generated in {}", fixtures_dir.display()); +} + +fn generate_device_latency_samples(dir: &Path) { + let agent_pk = pubkey_from_byte(0x01); + let origin_pk = pubkey_from_byte(0x02); + let target_pk = pubkey_from_byte(0x03); + let origin_loc_pk = pubkey_from_byte(0x04); + let target_loc_pk = pubkey_from_byte(0x05); + let link_pk = pubkey_from_byte(0x06); + + let samples: Vec = vec![100, 200, 300, 400, 500]; + let val = DeviceLatencySamples { + header: DeviceLatencySamplesHeader { + account_type: AccountType::DeviceLatencySamples, + epoch: 19800, + origin_device_agent_pk: agent_pk, + origin_device_pk: origin_pk, + target_device_pk: target_pk, + origin_device_location_pk: origin_loc_pk, + target_device_location_pk: target_loc_pk, + link_pk, + sampling_interval_microseconds: 5_000_000, + start_timestamp_microseconds: 1_700_000_000_000_000, + next_sample_index: samples.len() as u32, + _unused: [0; 128], + }, + samples, + }; + + let data = borsh::to_vec(&val).unwrap(); + + let meta = FixtureMeta { + name: "device_latency_samples".to_string(), + account_type: AccountType::DeviceLatencySamples as u8, + fields: vec![ + FieldValue { name: "AccountType".into(), value: "3".into(), typ: "u8".into() }, + FieldValue { name: "Epoch".into(), value: "19800".into(), typ: "u64".into() }, + FieldValue { name: "OriginDeviceAgentPK".into(), value: pubkey_bs58(&agent_pk), typ: "pubkey".into() }, + FieldValue { name: "OriginDevicePK".into(), value: pubkey_bs58(&origin_pk), typ: "pubkey".into() }, + FieldValue { name: "TargetDevicePK".into(), value: pubkey_bs58(&target_pk), typ: "pubkey".into() }, + FieldValue { name: "OriginDeviceLocationPK".into(), value: pubkey_bs58(&origin_loc_pk), typ: "pubkey".into() }, + FieldValue { name: "TargetDeviceLocationPK".into(), value: pubkey_bs58(&target_loc_pk), typ: "pubkey".into() }, + FieldValue { name: "LinkPK".into(), value: pubkey_bs58(&link_pk), typ: "pubkey".into() }, + FieldValue { name: "SamplingIntervalMicroseconds".into(), value: "5000000".into(), typ: "u64".into() }, + FieldValue { name: "StartTimestampMicroseconds".into(), value: "1700000000000000".into(), typ: "u64".into() }, + FieldValue { name: "NextSampleIndex".into(), value: "5".into(), typ: "u32".into() }, + FieldValue { name: "SamplesCount".into(), value: "5".into(), typ: "u32".into() }, + ], + }; + + write_fixture(dir, "device_latency_samples", &data, &meta); +} + +fn generate_internet_latency_samples(dir: &Path) { + let oracle_pk = pubkey_from_byte(0x11); + let origin_exchange_pk = pubkey_from_byte(0x12); + let target_exchange_pk = pubkey_from_byte(0x13); + + let samples: Vec = vec![1000, 2000, 3000, 4000, 5000]; + let val = InternetLatencySamples { + header: InternetLatencySamplesHeader { + account_type: AccountType::InternetLatencySamples, + epoch: 19800, + data_provider_name: "RIPE Atlas".to_string(), + oracle_agent_pk: oracle_pk, + origin_exchange_pk, + target_exchange_pk, + sampling_interval_microseconds: 60_000_000, + start_timestamp_microseconds: 1_700_000_000_000_000, + next_sample_index: samples.len() as u32, + _unused: [0; 128], + }, + samples, + }; + + let data = borsh::to_vec(&val).unwrap(); + + let meta = FixtureMeta { + name: "internet_latency_samples".to_string(), + account_type: AccountType::InternetLatencySamples as u8, + fields: vec![ + FieldValue { name: "AccountType".into(), value: "4".into(), typ: "u8".into() }, + FieldValue { name: "Epoch".into(), value: "19800".into(), typ: "u64".into() }, + FieldValue { name: "DataProviderName".into(), value: "RIPE Atlas".into(), typ: "string".into() }, + FieldValue { name: "OracleAgentPK".into(), value: pubkey_bs58(&oracle_pk), typ: "pubkey".into() }, + FieldValue { name: "OriginExchangePK".into(), value: pubkey_bs58(&origin_exchange_pk), typ: "pubkey".into() }, + FieldValue { name: "TargetExchangePK".into(), value: pubkey_bs58(&target_exchange_pk), typ: "pubkey".into() }, + FieldValue { name: "SamplingIntervalMicroseconds".into(), value: "60000000".into(), typ: "u64".into() }, + FieldValue { name: "StartTimestampMicroseconds".into(), value: "1700000000000000".into(), typ: "u64".into() }, + FieldValue { name: "NextSampleIndex".into(), value: "5".into(), typ: "u32".into() }, + FieldValue { name: "SamplesCount".into(), value: "5".into(), typ: "u32".into() }, + ], + }; + + write_fixture(dir, "internet_latency_samples", &data, &meta); +} diff --git a/sdk/telemetry/testdata/fixtures/internet_latency_samples.bin b/sdk/telemetry/testdata/fixtures/internet_latency_samples.bin new file mode 100644 index 000000000..eb0b7f102 Binary files /dev/null and b/sdk/telemetry/testdata/fixtures/internet_latency_samples.bin differ diff --git a/sdk/telemetry/testdata/fixtures/internet_latency_samples.json b/sdk/telemetry/testdata/fixtures/internet_latency_samples.json new file mode 100644 index 000000000..4571e77f0 --- /dev/null +++ b/sdk/telemetry/testdata/fixtures/internet_latency_samples.json @@ -0,0 +1,56 @@ +{ + "name": "internet_latency_samples", + "account_type": 4, + "fields": [ + { + "name": "AccountType", + "value": "4", + "typ": "u8" + }, + { + "name": "Epoch", + "value": "19800", + "typ": "u64" + }, + { + "name": "DataProviderName", + "value": "RIPE Atlas", + "typ": "string" + }, + { + "name": "OracleAgentPK", + "value": "29MvzRLSCDR8wm3ZeaXbDkftQAc719jQvkF6ZKGvFgEs", + "typ": "pubkey" + }, + { + "name": "OriginExchangePK", + "value": "2DGLdv4X63urMTAYA5o37gR7fBAsi6qKWcYz4WauyUuD", + "typ": "pubkey" + }, + { + "name": "TargetExchangePK", + "value": "2HAkHQnbytQZm9HWfb4V1cALvBjeR3wE6UrsZhtuhHZZ", + "typ": "pubkey" + }, + { + "name": "SamplingIntervalMicroseconds", + "value": "60000000", + "typ": "u64" + }, + { + "name": "StartTimestampMicroseconds", + "value": "1700000000000000", + "typ": "u64" + }, + { + "name": "NextSampleIndex", + "value": "5", + "typ": "u32" + }, + { + "name": "SamplesCount", + "value": "5", + "typ": "u32" + } + ] +} \ No newline at end of file diff --git a/sdk/telemetry/typescript/bun.lock b/sdk/telemetry/typescript/bun.lock new file mode 100644 index 000000000..332656468 --- /dev/null +++ b/sdk/telemetry/typescript/bun.lock @@ -0,0 +1,133 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "doublezero-telemetry", + "dependencies": { + "@solana/web3.js": "^1.98", + }, + "devDependencies": { + "@types/bun": "latest", + "typescript": "^5", + }, + }, + }, + "packages": { + "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], + + "@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], + + "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@solana/buffer-layout": ["@solana/buffer-layout@4.0.1", "", { "dependencies": { "buffer": "~6.0.3" } }, "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA=="], + + "@solana/codecs-core": ["@solana/codecs-core@2.3.0", "", { "dependencies": { "@solana/errors": "2.3.0" }, "peerDependencies": { "typescript": ">=5.3.3" } }, "sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw=="], + + "@solana/codecs-numbers": ["@solana/codecs-numbers@2.3.0", "", { "dependencies": { "@solana/codecs-core": "2.3.0", "@solana/errors": "2.3.0" }, "peerDependencies": { "typescript": ">=5.3.3" } }, "sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg=="], + + "@solana/errors": ["@solana/errors@2.3.0", "", { "dependencies": { "chalk": "^5.4.1", "commander": "^14.0.0" }, "peerDependencies": { "typescript": ">=5.3.3" }, "bin": { "errors": "bin/cli.mjs" } }, "sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ=="], + + "@solana/web3.js": ["@solana/web3.js@1.98.4", "", { "dependencies": { "@babel/runtime": "^7.25.0", "@noble/curves": "^1.4.2", "@noble/hashes": "^1.4.0", "@solana/buffer-layout": "^4.0.1", "@solana/codecs-numbers": "^2.1.0", "agentkeepalive": "^4.5.0", "bn.js": "^5.2.1", "borsh": "^0.7.0", "bs58": "^4.0.1", "buffer": "6.0.3", "fast-stable-stringify": "^1.0.0", "jayson": "^4.1.1", "node-fetch": "^2.7.0", "rpc-websockets": "^9.0.2", "superstruct": "^2.0.2" } }, "sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw=="], + + "@swc/helpers": ["@swc/helpers@0.5.18", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ=="], + + "@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], + + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + + "@types/node": ["@types/node@25.1.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA=="], + + "@types/uuid": ["@types/uuid@8.3.4", "", {}, "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw=="], + + "@types/ws": ["@types/ws@7.4.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww=="], + + "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], + + "base-x": ["base-x@3.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "bn.js": ["bn.js@5.2.2", "", {}, "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw=="], + + "borsh": ["borsh@0.7.0", "", { "dependencies": { "bn.js": "^5.2.0", "bs58": "^4.0.0", "text-encoding-utf-8": "^1.0.2" } }, "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA=="], + + "bs58": ["bs58@4.0.1", "", { "dependencies": { "base-x": "^3.0.2" } }, "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw=="], + + "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + + "bufferutil": ["bufferutil@4.1.0", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw=="], + + "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], + + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + + "delay": ["delay@5.0.0", "", {}, "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw=="], + + "es6-promise": ["es6-promise@4.2.8", "", {}, "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="], + + "es6-promisify": ["es6-promisify@5.0.0", "", { "dependencies": { "es6-promise": "^4.0.3" } }, "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ=="], + + "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], + + "eyes": ["eyes@0.1.8", "", {}, "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ=="], + + "fast-stable-stringify": ["fast-stable-stringify@1.0.0", "", {}, "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag=="], + + "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "isomorphic-ws": ["isomorphic-ws@4.0.1", "", { "peerDependencies": { "ws": "*" } }, "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w=="], + + "jayson": ["jayson@4.3.0", "", { "dependencies": { "@types/connect": "^3.4.33", "@types/node": "^12.12.54", "@types/ws": "^7.4.4", "commander": "^2.20.3", "delay": "^5.0.0", "es6-promisify": "^5.0.0", "eyes": "^0.1.8", "isomorphic-ws": "^4.0.1", "json-stringify-safe": "^5.0.1", "stream-json": "^1.9.1", "uuid": "^8.3.2", "ws": "^7.5.10" }, "bin": { "jayson": "bin/jayson.js" } }, "sha512-AauzHcUcqs8OBnCHOkJY280VaTiCm57AbuO7lqzcw7JapGj50BisE3xhksye4zlTSR1+1tAz67wLTl8tEH1obQ=="], + + "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + + "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], + + "rpc-websockets": ["rpc-websockets@9.3.3", "", { "dependencies": { "@swc/helpers": "^0.5.11", "@types/uuid": "^8.3.4", "@types/ws": "^8.2.2", "buffer": "^6.0.3", "eventemitter3": "^5.0.1", "uuid": "^8.3.2", "ws": "^8.5.0" }, "optionalDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" } }, "sha512-OkCsBBzrwxX4DoSv4Zlf9DgXKRB0MzVfCFg5MC+fNnf9ktr4SMWjsri0VNZQlDbCnGcImT6KNEv4ZoxktQhdpA=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "stream-chain": ["stream-chain@2.2.5", "", {}, "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA=="], + + "stream-json": ["stream-json@1.9.1", "", { "dependencies": { "stream-chain": "^2.2.5" } }, "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw=="], + + "superstruct": ["superstruct@2.0.2", "", {}, "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A=="], + + "text-encoding-utf-8": ["text-encoding-utf-8@1.0.2", "", {}, "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg=="], + + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "utf-8-validate": ["utf-8-validate@5.0.10", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ=="], + + "uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], + + "@solana/errors/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + + "jayson/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], + + "rpc-websockets/@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + + "rpc-websockets/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + } +} diff --git a/sdk/telemetry/typescript/package.json b/sdk/telemetry/typescript/package.json new file mode 100644 index 000000000..6219a4131 --- /dev/null +++ b/sdk/telemetry/typescript/package.json @@ -0,0 +1,21 @@ +{ + "name": "@doublezero/telemetry", + "version": "0.0.1", + "type": "module", + "main": "dist/telemetry/index.js", + "types": "dist/telemetry/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "prepublishOnly": "tsc" + }, + "dependencies": { + "@solana/web3.js": "^1.98" + }, + "devDependencies": { + "@types/bun": "latest", + "typescript": "^5" + } +} diff --git a/sdk/telemetry/typescript/telemetry/client.ts b/sdk/telemetry/typescript/telemetry/client.ts new file mode 100644 index 000000000..fd588fb7a --- /dev/null +++ b/sdk/telemetry/typescript/telemetry/client.ts @@ -0,0 +1,84 @@ +/** RPC client for fetching telemetry program accounts. */ + +import { Connection, PublicKey } from "@solana/web3.js"; +import { PROGRAM_IDS, LEDGER_RPC_URLS } from "./config.js"; +import { newConnection } from "./rpc.js"; +import { + deriveDeviceLatencySamplesPda, + deriveInternetLatencySamplesPda, +} from "./pda.js"; +import { + deserializeDeviceLatencySamples, + deserializeInternetLatencySamples, + type DeviceLatencySamples, + type InternetLatencySamples, +} from "./state.js"; + +export class Client { + constructor( + private connection: Connection, + private programId: PublicKey, + ) {} + + /** Create a client configured for the given environment. */ + static forEnv(env: string): Client { + return new Client( + newConnection(LEDGER_RPC_URLS[env]), + new PublicKey(PROGRAM_IDS[env]), + ); + } + + static mainnetBeta(): Client { + return Client.forEnv("mainnet-beta"); + } + + static testnet(): Client { + return Client.forEnv("testnet"); + } + + static devnet(): Client { + return Client.forEnv("devnet"); + } + + static localnet(): Client { + return Client.forEnv("localnet"); + } + + async getDeviceLatencySamples( + originDevicePK: PublicKey, + targetDevicePK: PublicKey, + linkPK: PublicKey, + epoch: number | bigint, + ): Promise { + const [addr] = deriveDeviceLatencySamplesPda( + this.programId, + originDevicePK, + targetDevicePK, + linkPK, + epoch, + ); + const info = await this.connection.getAccountInfo(addr); + if (!info) throw new Error("Account not found"); + return deserializeDeviceLatencySamples(new Uint8Array(info.data)); + } + + async getInternetLatencySamples( + collectorOraclePK: PublicKey, + dataProviderName: string, + originLocationPK: PublicKey, + targetLocationPK: PublicKey, + epoch: number | bigint, + ): Promise { + const [addr] = deriveInternetLatencySamplesPda( + this.programId, + collectorOraclePK, + dataProviderName, + originLocationPK, + targetLocationPK, + epoch, + ); + const info = await this.connection.getAccountInfo(addr); + if (!info) throw new Error("Account not found"); + return deserializeInternetLatencySamples(new Uint8Array(info.data)); + } +} diff --git a/sdk/telemetry/typescript/telemetry/config.ts b/sdk/telemetry/typescript/telemetry/config.ts new file mode 100644 index 000000000..328b7d06b --- /dev/null +++ b/sdk/telemetry/typescript/telemetry/config.ts @@ -0,0 +1,18 @@ +/** Network configuration for the telemetry program. */ + +export const PROGRAM_IDS: Record = { + "mainnet-beta": "tE1exJ5VMyoC9ByZeSmgtNzJCFF74G9JAv338sJiqkC", + testnet: "3KogTMmVxc5eUHtjZnwm136H5P8tvPwVu4ufbGPvM7p1", + devnet: "C9xqH76NSm11pBS6maNnY163tWHT8Govww47uyEmSnoG", + localnet: "C9xqH76NSm11pBS6maNnY163tWHT8Govww47uyEmSnoG", +}; + +export const LEDGER_RPC_URLS: Record = { + "mainnet-beta": + "https://doublezero-mainnet-beta.rpcpool.com/db336024-e7a8-46b1-80e5-352dd77060ab", + testnet: + "https://doublezerolocalnet.rpcpool.com/8a4fd3f4-0977-449f-88c7-63d4b0f10f16", + devnet: + "https://doublezerolocalnet.rpcpool.com/8a4fd3f4-0977-449f-88c7-63d4b0f10f16", + localnet: "http://localhost:8899", +}; diff --git a/sdk/telemetry/typescript/telemetry/index.ts b/sdk/telemetry/typescript/telemetry/index.ts new file mode 100644 index 000000000..4427eed3b --- /dev/null +++ b/sdk/telemetry/typescript/telemetry/index.ts @@ -0,0 +1,13 @@ +export { PROGRAM_IDS, LEDGER_RPC_URLS } from "./config.js"; +export { + type DeviceLatencySamples, + type InternetLatencySamples, + deserializeDeviceLatencySamples, + deserializeInternetLatencySamples, +} from "./state.js"; +export { + deriveDeviceLatencySamplesPda, + deriveInternetLatencySamplesPda, +} from "./pda.js"; +export { Client } from "./client.js"; +export { newConnection } from "./rpc.js"; diff --git a/sdk/telemetry/typescript/telemetry/pda.ts b/sdk/telemetry/typescript/telemetry/pda.ts new file mode 100644 index 000000000..49924a7e4 --- /dev/null +++ b/sdk/telemetry/typescript/telemetry/pda.ts @@ -0,0 +1,55 @@ +/** PDA derivation for telemetry program accounts. */ + +import { PublicKey } from "@solana/web3.js"; + +const TELEMETRY_SEED = Buffer.from("telemetry"); +const DEVICE_LATENCY_SEED = Buffer.from("dzlatency"); +const INTERNET_LATENCY_SEED = Buffer.from("inetlatency"); + +export function deriveDeviceLatencySamplesPda( + programId: PublicKey, + originDevicePK: PublicKey, + targetDevicePK: PublicKey, + linkPK: PublicKey, + epoch: number | bigint, +): [PublicKey, number] { + const epochBuf = Buffer.alloc(8); + epochBuf.writeBigUInt64LE(BigInt(epoch)); + + return PublicKey.findProgramAddressSync( + [ + TELEMETRY_SEED, + DEVICE_LATENCY_SEED, + originDevicePK.toBuffer(), + targetDevicePK.toBuffer(), + linkPK.toBuffer(), + epochBuf, + ], + programId, + ); +} + +export function deriveInternetLatencySamplesPda( + programId: PublicKey, + collectorOraclePK: PublicKey, + dataProviderName: string, + originLocationPK: PublicKey, + targetLocationPK: PublicKey, + epoch: number | bigint, +): [PublicKey, number] { + const epochBuf = Buffer.alloc(8); + epochBuf.writeBigUInt64LE(BigInt(epoch)); + + return PublicKey.findProgramAddressSync( + [ + TELEMETRY_SEED, + INTERNET_LATENCY_SEED, + collectorOraclePK.toBuffer(), + Buffer.from(dataProviderName), + originLocationPK.toBuffer(), + targetLocationPK.toBuffer(), + epochBuf, + ], + programId, + ); +} diff --git a/sdk/telemetry/typescript/telemetry/rpc.ts b/sdk/telemetry/typescript/telemetry/rpc.ts new file mode 100644 index 000000000..ab2f0ff83 --- /dev/null +++ b/sdk/telemetry/typescript/telemetry/rpc.ts @@ -0,0 +1,36 @@ +import { Connection, type ConnectionConfig } from "@solana/web3.js"; + +const DEFAULT_MAX_RETRIES = 5; + +/** + * Creates a Solana RPC Connection with retry on 429 Too Many Requests. + * + * The built-in @solana/web3.js retry uses short backoffs (500ms-4s) that + * may not be sufficient for rate-limited public RPC endpoints. This wrapper + * provides longer backoff intervals (2s, 4s, 6s, 8s, 10s). + */ +export function newConnection( + url: string, + config?: ConnectionConfig & { maxRetries?: number }, +): Connection { + const maxRetries = config?.maxRetries ?? DEFAULT_MAX_RETRIES; + const retryFetch = async ( + input: Parameters[0], + init?: Parameters[1], + ): Promise => { + for (let attempt = 0; ; attempt++) { + const response = await fetch(input, init); + if (response.status !== 429 || attempt >= maxRetries) { + return response; + } + await new Promise((resolve) => + setTimeout(resolve, (attempt + 1) * 2000), + ); + } + }; + return new Connection(url, { + ...config, + disableRetryOnRateLimit: true, + fetch: retryFetch as typeof fetch, + }); +} diff --git a/sdk/telemetry/typescript/telemetry/state.ts b/sdk/telemetry/typescript/telemetry/state.ts new file mode 100644 index 000000000..599632c31 --- /dev/null +++ b/sdk/telemetry/typescript/telemetry/state.ts @@ -0,0 +1,130 @@ +/** Account state types and deserialization for the telemetry program. */ + +import { PublicKey } from "@solana/web3.js"; +import { IncrementalReader } from "@doublezero/borsh-incremental"; + +const DEVICE_LATENCY_HEADER_SIZE = 1 + 8 + 32 * 6 + 8 + 8 + 4 + 128; +const MAX_DEVICE_LATENCY_SAMPLES = 35_000; +const MAX_INTERNET_LATENCY_SAMPLES = 3_000; + +export interface DeviceLatencySamples { + accountType: number; + epoch: bigint; + originDeviceAgentPK: PublicKey; + originDevicePK: PublicKey; + targetDevicePK: PublicKey; + originDeviceLocationPK: PublicKey; + targetDeviceLocationPK: PublicKey; + linkPK: PublicKey; + samplingIntervalMicroseconds: bigint; + startTimestampMicroseconds: bigint; + nextSampleIndex: number; + samples: number[]; +} + +export interface InternetLatencySamples { + accountType: number; + epoch: bigint; + dataProviderName: string; + oracleAgentPK: PublicKey; + originExchangePK: PublicKey; + targetExchangePK: PublicKey; + samplingIntervalMicroseconds: bigint; + startTimestampMicroseconds: bigint; + nextSampleIndex: number; + samples: number[]; +} + +function readPubkey(r: IncrementalReader): PublicKey { + return new PublicKey(r.readPubkeyRaw()); +} + +export function deserializeDeviceLatencySamples( + data: Uint8Array, +): DeviceLatencySamples { + if (data.length < DEVICE_LATENCY_HEADER_SIZE) { + throw new Error( + `data too short for device latency header: ${data.length} < ${DEVICE_LATENCY_HEADER_SIZE}`, + ); + } + + const r = new IncrementalReader(data); + + const accountType = r.readU8(); + const epoch = r.readU64(); + const originDeviceAgentPK = readPubkey(r); + const originDevicePK = readPubkey(r); + const targetDevicePK = readPubkey(r); + const originDeviceLocationPK = readPubkey(r); + const targetDeviceLocationPK = readPubkey(r); + const linkPK = readPubkey(r); + const samplingIntervalMicroseconds = r.readU64(); + const startTimestampMicroseconds = r.readU64(); + const nextSampleIndex = r.readU32(); + + r.readBytes(128); // _unused + + const count = Math.min(nextSampleIndex, MAX_DEVICE_LATENCY_SAMPLES); + const samples: number[] = []; + for (let i = 0; i < count; i++) { + if (r.remaining < 4) break; + samples.push(r.readU32()); + } + + return { + accountType, + epoch, + originDeviceAgentPK, + originDevicePK, + targetDevicePK, + originDeviceLocationPK, + targetDeviceLocationPK, + linkPK, + samplingIntervalMicroseconds, + startTimestampMicroseconds, + nextSampleIndex, + samples, + }; +} + +export function deserializeInternetLatencySamples( + data: Uint8Array, +): InternetLatencySamples { + if (data.length < 10) { + throw new Error("data too short"); + } + + const r = new IncrementalReader(data); + + const accountType = r.readU8(); + const epoch = r.readU64(); + const dataProviderName = r.readString(); + const oracleAgentPK = readPubkey(r); + const originExchangePK = readPubkey(r); + const targetExchangePK = readPubkey(r); + const samplingIntervalMicroseconds = r.readU64(); + const startTimestampMicroseconds = r.readU64(); + const nextSampleIndex = r.readU32(); + + r.readBytes(128); // _unused + + const count = Math.min(nextSampleIndex, MAX_INTERNET_LATENCY_SAMPLES); + const samples: number[] = []; + for (let i = 0; i < count; i++) { + if (r.remaining < 4) break; + samples.push(r.readU32()); + } + + return { + accountType, + epoch, + dataProviderName, + oracleAgentPK, + originExchangePK, + targetExchangePK, + samplingIntervalMicroseconds, + startTimestampMicroseconds, + nextSampleIndex, + samples, + }; +} diff --git a/sdk/telemetry/typescript/telemetry/tests/fixtures.test.ts b/sdk/telemetry/typescript/telemetry/tests/fixtures.test.ts new file mode 100644 index 000000000..e166e6c07 --- /dev/null +++ b/sdk/telemetry/typescript/telemetry/tests/fixtures.test.ts @@ -0,0 +1,102 @@ +/** + * Fixture-based compatibility tests. + */ + +import { describe, expect, test } from "bun:test"; +import { readFileSync } from "fs"; +import { join } from "path"; +import { PublicKey } from "@solana/web3.js"; +import { + deserializeDeviceLatencySamples, + deserializeInternetLatencySamples, +} from "../state.js"; + +const FIXTURES_DIR = join( + __dirname, + "..", + "..", + "..", + "testdata", + "fixtures", +); + +interface FieldValue { + name: string; + value: string; + typ: string; +} + +interface FixtureMeta { + name: string; + account_type: number; + fields: FieldValue[]; +} + +function loadFixture(name: string): [Uint8Array, FixtureMeta] { + const binData = new Uint8Array( + readFileSync(join(FIXTURES_DIR, `${name}.bin`)), + ); + const meta: FixtureMeta = JSON.parse( + readFileSync(join(FIXTURES_DIR, `${name}.json`), "utf-8"), + ); + return [binData, meta]; +} + +function assertFields( + fields: FieldValue[], + got: Record, +): void { + for (const f of fields) { + if (!(f.name in got)) continue; + const actual = got[f.name]; + if (f.typ === "u8" || f.typ === "u16" || f.typ === "u32") { + expect(actual).toBe(Number(f.value)); + } else if (f.typ === "u64") { + expect(actual).toBe(BigInt(f.value)); + } else if (f.typ === "pubkey") { + expect((actual as PublicKey).toBase58()).toBe(f.value); + } else if (f.typ === "string") { + expect(actual).toBe(f.value); + } + } +} + +describe("DeviceLatencySamples fixture", () => { + test("deserialize", () => { + const [data, meta] = loadFixture("device_latency_samples"); + const d = deserializeDeviceLatencySamples(data); + assertFields(meta.fields, { + AccountType: d.accountType, + Epoch: d.epoch, + OriginDeviceAgentPK: d.originDeviceAgentPK, + OriginDevicePK: d.originDevicePK, + TargetDevicePK: d.targetDevicePK, + OriginDeviceLocationPK: d.originDeviceLocationPK, + TargetDeviceLocationPK: d.targetDeviceLocationPK, + LinkPK: d.linkPK, + SamplingIntervalMicroseconds: d.samplingIntervalMicroseconds, + StartTimestampMicroseconds: d.startTimestampMicroseconds, + NextSampleIndex: d.nextSampleIndex, + SamplesCount: d.samples.length, + }); + }); +}); + +describe("InternetLatencySamples fixture", () => { + test("deserialize", () => { + const [data, meta] = loadFixture("internet_latency_samples"); + const d = deserializeInternetLatencySamples(data); + assertFields(meta.fields, { + AccountType: d.accountType, + Epoch: d.epoch, + DataProviderName: d.dataProviderName, + OracleAgentPK: d.oracleAgentPK, + OriginExchangePK: d.originExchangePK, + TargetExchangePK: d.targetExchangePK, + SamplingIntervalMicroseconds: d.samplingIntervalMicroseconds, + StartTimestampMicroseconds: d.startTimestampMicroseconds, + NextSampleIndex: d.nextSampleIndex, + SamplesCount: d.samples.length, + }); + }); +}); diff --git a/sdk/telemetry/typescript/telemetry/tests/pda.test.ts b/sdk/telemetry/typescript/telemetry/tests/pda.test.ts new file mode 100644 index 000000000..1a7caab8c --- /dev/null +++ b/sdk/telemetry/typescript/telemetry/tests/pda.test.ts @@ -0,0 +1,58 @@ +/** + * PDA derivation tests. + */ + +import { describe, expect, test } from "bun:test"; +import { PublicKey } from "@solana/web3.js"; +import { + deriveDeviceLatencySamplesPda, + deriveInternetLatencySamplesPda, +} from "../pda.js"; + +const PROGRAM_ID = new PublicKey( + "tE1exJ5VMyoC9ByZeSmgtNzJCFF74G9JAv338sJiqkC", +); + +describe("deriveDeviceLatencySamplesPda", () => { + test("deterministic", () => { + const origin = new PublicKey("11111111111111111111111111111112"); + const target = new PublicKey("11111111111111111111111111111113"); + const link = new PublicKey("11111111111111111111111111111114"); + + const [addr1, bump1] = deriveDeviceLatencySamplesPda( + PROGRAM_ID, + origin, + target, + link, + 42, + ); + const [addr2, bump2] = deriveDeviceLatencySamplesPda( + PROGRAM_ID, + origin, + target, + link, + 42, + ); + + expect(addr1.equals(addr2)).toBe(true); + expect(bump1).toBe(bump2); + }); +}); + +describe("deriveInternetLatencySamplesPda", () => { + test("non-zero", () => { + const oracle = new PublicKey("11111111111111111111111111111112"); + const origin = new PublicKey("11111111111111111111111111111113"); + const target = new PublicKey("11111111111111111111111111111114"); + + const [addr] = deriveInternetLatencySamplesPda( + PROGRAM_ID, + oracle, + "RIPE Atlas", + origin, + target, + 42, + ); + expect(addr.equals(PublicKey.default)).toBe(false); + }); +}); diff --git a/sdk/telemetry/typescript/tsconfig.json b/sdk/telemetry/typescript/tsconfig.json new file mode 100644 index 000000000..d14a5538e --- /dev/null +++ b/sdk/telemetry/typescript/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "dist", + "declaration": true, + "types": ["bun"], + "baseUrl": ".", + "paths": { + "@doublezero/borsh-incremental": ["../../borsh-incremental/typescript/borsh-incremental/index.ts"] + } + }, + "include": ["telemetry/**/*.ts"] +} diff --git a/smartcontract/sdk/go/client.go b/smartcontract/sdk/go/client.go index c9d187e85..6a09d7dc8 100644 --- a/smartcontract/sdk/go/client.go +++ b/smartcontract/sdk/go/client.go @@ -6,21 +6,25 @@ import ( "github.com/gagliardetto/solana-go" solanarpc "github.com/gagliardetto/solana-go/rpc" + "github.com/malbeclabs/doublezero/config" + revdist "github.com/malbeclabs/doublezero/sdk/revdist/go" "github.com/malbeclabs/doublezero/smartcontract/sdk/go/serviceability" "github.com/malbeclabs/doublezero/smartcontract/sdk/go/telemetry" ) type Client struct { - Serviceability *serviceability.Client - Telemetry *telemetry.Client + Serviceability *serviceability.Client + Telemetry *telemetry.Client + RevenueDistribution *revdist.Client } type Config struct { - Endpoint string - Signer *solana.PrivateKey - ServiceabilityProgramID solana.PublicKey - TelemetryProgramID solana.PublicKey + Endpoint string + Signer *solana.PrivateKey + ServiceabilityProgramID solana.PublicKey + TelemetryProgramID solana.PublicKey + RevenueDistributionProgramID solana.PublicKey } type Option func(*Config) @@ -51,6 +55,11 @@ func New(log *slog.Logger, endpoint string, opts ...Option) (*Client, error) { Serviceability: serviceability.New(rpcClient, cfg.ServiceabilityProgramID), Telemetry: telemetry.New(log, rpcClient, cfg.Signer, cfg.TelemetryProgramID), } + + if !cfg.RevenueDistributionProgramID.IsZero() { + c.RevenueDistribution = revdist.New(rpcClient, cfg.RevenueDistributionProgramID) + } + return c, nil } @@ -74,3 +83,10 @@ func WithTelemetryProgramID(programID string) Option { c.TelemetryProgramID = solana.MustPublicKeyFromBase58(programID) } } + +// Configure the revenue distribution program ID. +func WithRevenueDistributionProgramID(programID string) Option { + return func(c *Config) { + c.RevenueDistributionProgramID = solana.MustPublicKeyFromBase58(programID) + } +}