Skip to content

refactor(go): idiomatic Go cleanup across the SDK#214

Open
EfeDurmaz16 wants to merge 12 commits into
solana-foundation:mainfrom
EfeDurmaz16:fix/go-idiomatic-cleanup
Open

refactor(go): idiomatic Go cleanup across the SDK#214
EfeDurmaz16 wants to merge 12 commits into
solana-foundation:mainfrom
EfeDurmaz16:fix/go-idiomatic-cleanup

Conversation

@EfeDurmaz16

@EfeDurmaz16 EfeDurmaz16 commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

What

A pass over the Go SDK to bring it in line with idiomatic Go, sourced from the
go.dev Code Review Comments, Effective Go, and the Uber Go Style Guide. The
work started from a line-by-line read of every non-generated .go file and
grouped the findings into four real bugs, a set of structural fixes, a style
sweep, and a Go 1.26 modernization pass that is now enforced in CI. Codama-
generated code under protocols/programs/paymentchannels is left untouched.

Wire shapes are unchanged: the Id to ID rename keeps its json:"id" tag,
and the challenge-builder refactor produces byte-identical output. The
cross-SDK interop harness and the existing unit suites cover the behavior.

Commits

Each commit is one logical step and stands on its own:

  • stop dropping two real errors in the mpp path — the compute-unit price
    and limit builders, and the session-store lock map, were discarding errors
    and leaking map entries.
  • return errors from adapter interfaces instead of dropping them
    Adapter.AcceptsEntry/ChallengeHeaders and UsageAdapter now return
    (value, error), so a base58 or serialization failure reaches the caller.
    The 402 writer logs and drops the offending protocol rather than emitting a
    half-built header, signerBridge decodes the operator pubkey once at
    construction where the error can propagate, and the test-only
    ContextWith*ForTests helpers move to export_test.go.
  • replace SetRPCForTests with an RPCClient config option — drops a
    test-only method from the public X402Upto surface and injects the RPC
    client through a new UptoConfig.RPCClient field instead.
  • rename Id to ID per Go initialism conventionPaymentIdentifierInfo.Id
    becomes ID; the JSON tag stays id.
  • use functional options for optional MPP challenge fields — collapses
    NewChallengeWithSecret/NewChallengeWithSecretFull into one constructor
    whose optional expires/digest/description/opaque fields are set via
    named ChallengeOption values, so the three same-typed optional strings can
    no longer be transposed at a call site. Also fixes the wire package doc
    comment (was core) and renames a parameter that shadowed the cap builtin.
  • idiomatic cleanups across cmd, paycore, and protocol packages
    strconv.FormatUint/ParseUint over fmt.Sprintf/Sscanf, dropping a
    single-case switch, a redundant []byte conversion, and a redundant import
    alias.
  • modernize with go fix (Go 1.26) — applies the language/stdlib modernizers
    across every non-generated file: interface{} to any, if/else to
    min/max, 3-clause counters to for i := range n, slices/maps helpers,
    and strings.SplitSeq. Includes the one loop the rebase pulled in from
    main's concurrent-replay test (fix(mpp): close concurrent-signature-replay TOCTOU in TS charge verify #211), so the gate below starts green.
  • gate on go fix modernization — a Go lint step runs
    go/scripts/check-go-fix.sh, which fails when go fix -inline=false ./...
    would still change any non-generated source, so the modernization can't
    silently regress. The inline analyzer is excluded (it applies opt-in
    //go:fix inline directives and is cache sensitive); generated codama clients
    are excluded by their header. On failure the gate prints the suggested diff.

Verifying

go build ./...                    # clean
go vet ./...                      # clean (generated paymentchannels excepted)
go test ./...                     # all packages pass
gofmt -l                          # empty
bash go/scripts/check-go-fix.sh   # go-fix gate OK

Self-review notes

  • The RPCClient field is a legitimate public option (custom transport), not a
    disguised test hook. The single cross-package caller that needed the old
    method now builds its engine through the extracted buildUptoConfig helper.
  • The adapter interface change ripples into every fake and call site in the
    test suites; those are updated in the same commit so each commit builds.
  • ProcessOpen/ProcessClose carry new doc comments flagging that the HTTP
    method layer deliberately reimplements those invariants as a superset; this
    is documentation only, no code path changed.
  • The branch is rebased on current main, so its CI runs against the real merge
    base. The gate is scoped to the deterministic modernizers on purpose: an
    earlier version that ran the full go fix flagged an un-modernized loop that
    lived on main rather than in this PR, which would fail unrelated Go PRs on
    base-branch drift.

@greptile-apps

greptile-apps Bot commented Jul 2, 2026

Copy link
Copy Markdown

Greptile Summary

This PR performs a comprehensive idiomatic Go cleanup pass over the SDK: fixing four real bugs (error propagation in the MPP compute-budget path, session-store lock-map leak, dropped pubkey-decode errors in signerBridge, dropped serialization errors in the adapter interfaces), migrating from gagliardetto/solana-go to solana-foundation/solana-go/v2, and applying Go 1.26 modernizers enforced by a new CI gate.

  • Bug fixes: BuildComputeUnitPrice/Limit errors now propagate instead of being silently dropped; MemoryChannelStore.DeleteChannel now cleans up per-channel mutex entries to prevent unbounded map growth; AcceptsEntry/ChallengeHeaders on all adapters now surface build failures via error returns.
  • Structural refactoring: functional-options pattern for NewChallengeWithSecret eliminates silent positional-argument transpositions; appendChallenge helper makes the regular 402 path atomically add both the accepts entry and the challenge headers so neither can appear without the other; signerBridge pubkey is decoded once at construction where the error can propagate.
  • Modernization: interface{}any, slices/maps helpers, strings.SplitSeq, min/max builtins, and a check-go-fix.sh CI gate that enforces these going forward.

Confidence Score: 4/5

Safe to merge with one known residual gap: the usage-adapter 402 path still adds its accepts entry and challenge headers in two independent calls, so a partial offer can still be emitted if one succeeds and the other fails — the same asymmetry the regular adapter path now avoids via the atomic appendChallenge helper.

All four stated bug fixes are correct and well-tested: compute-budget errors now propagate instead of being swallowed, the session-store lock map is cleaned up on deletion, signerBridge decodes the pubkey once where errors can surface, and the adapter interface changes propagate build failures to callers. The appendChallenge refactor makes the regular 402 path properly atomic. The functional-options challenge constructor eliminates a class of silent positional-argument bugs. The go fix CI gate enforces ongoing modernization. The one remaining gap is in writeUsage402 and the settlement-error branch of RequireUsageFunc, where UsageAcceptsEntry and UsageChallengeHeaders are still called and committed independently — a pattern the regular path specifically avoids.

go/paykit/usage.go — writeUsage402 and the settlement-error branch in RequireUsageFunc should apply the same all-or-nothing logic used by appendChallenge in the regular adapter path.

Important Files Changed

Filename Overview
go/paykit/middleware.go Introduces appendChallenge helper that atomically adds accepts entry + challenge headers; refactors containsProtocol to slices.Contains; moves test helpers to export_test.go.
go/paykit/usage.go Adds error returns to UsageAdapter interface and handles them in writeUsage402 and the settlement-error branch, but the accepts entry and challenge headers are still added independently (non-atomic), which can yield a partial 402 if one call fails while the other succeeds.
go/paykit/client.go Adapter interface updated: AcceptsEntry and ChallengeHeaders now return (value, error) so build failures reach callers instead of being swallowed silently.
go/protocols/mpp/server/session_store.go Fixes lock-map leak: DeleteChannel now removes the per-channel entry from s.locks alongside s.data, preventing unbounded map growth over the process lifetime.
go/protocols/mpp/wire/challenge.go Refactors NewChallengeWithSecret to accept functional ChallengeOption values so expires/digest/description strings cannot be silently transposed; retains NewChallengeWithSecretFull wrapper for backward compatibility.
go/paykit/adapters/mpp/adapter.go AcceptsEntry/ChallengeHeaders now propagate errors; signerBridge decodes pubkey once at construction; decimals lookup consolidated into paycore.DefaultDecimalsForCurrency.
go/protocols/x402/upto.go Adds UptoConfig.RPCClient field for clean transport injection; removes SetRPCForTests from the public surface; RPC client is now selected once at construction.
go/protocols/mpp/client/charge.go Fixes real bug: BuildComputeUnitPrice and BuildComputeUnitLimit errors now propagate (previously silently dropped), so a failed instruction builder no longer produces a malformed transaction.
go/scripts/check-go-fix.sh New CI gate that fails the build when go fix -inline=false would still change any non-generated source, enforcing ongoing Go 1.26 modernization. Generated files are excluded via header grep.
go/go.mod Migrates from gagliardetto/solana-go to solana-foundation/solana-go/v2 v2.0.0-rc; removes the replace directive and associated indirect deps.

Reviews (10): Last reviewed commit: "ci(ruby): refresh audit-safe dependencie..." | Re-trigger Greptile

Comment thread go/protocols/x402/upto_test.go Outdated
Comment on lines 683 to 684
engine.rpc = fakeRPC

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Tests bypass RPCClient config field: These tests still assign engine.rpc directly (an unexported field) rather than using the new UptoConfig.RPCClient field that this PR introduces to replace SetRPCForTests. This pattern appears across all upto_test.go test cases that need a fake RPC. The stated goal of adding RPCClient was to give callers (including tests) a clean injection point, but the tests themselves don't exercise that path — so the new field is untested through its public API.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

MemoryChannelStore.DeleteChannel deleted from s.data but never s.locks, so the
per-channel mutex map grew without bound for the process lifetime; delete from
both under the same mu.

BuildCredential silently skipped the compute-unit price/limit instructions when
their builders errored (if ix, err := ...; err == nil), so a charge could be
signed without a compute-unit limit. Propagate the error like the surrounding
code does.
…ckages

strconv.FormatUint/ParseUint over fmt.Sprintf/Sscanf for integer
conversion, drop single-case switch and redundant []byte and import
aliases, and MixedCaps/initialism fixes. No behavior change.
Collapse NewChallengeWithSecret/NewChallengeWithSecretFull into one
constructor whose optional expires/digest/description/opaque fields are
set via named ChallengeOption values, so the three same-typed optional
strings can no longer be transposed silently at a call site. Also fix
the wire package doc comment (was 'core') and rename the cap parameter
that shadowed the builtin.
PaymentIdentifierInfo.Id becomes ID; the JSON tag stays 'id' so the
wire shape is unchanged.
Drop the test-only SetRPCForTests method from the public X402Upto and
inject the RPC client through a new UptoConfig.RPCClient field instead.
Extract the adapter's paykit.Config to UptoConfig mapping into
buildUptoConfig so the adapter test can inject a deterministic client
without a public test seam.
Adapter.AcceptsEntry/ChallengeHeaders and UsageAdapter now return
(value, error) so a base58 or serialization failure surfaces to the
caller instead of being swallowed. The 402 challenge writer logs and
drops the offending protocol rather than emitting a half-built header,
and signerBridge decodes the operator pubkey once at construction where
the error can propagate. Move ContextWith*ForTests helpers out of the
production surface into export_test.go.
The interface refactor added error-return branches (adapter accepts/
challenge builders, RPC config, binding checks) that dropped statement
coverage below the 91% CI gate. Add in-package tests for the exact and
mpp adapters (Protocol, ChallengeHeaders, VerifyAcceptedBinding,
normalizeNetwork, missing-secret and bad-recipient constructors) and the
402 challenge-writer error-drop paths so each new branch is exercised.
The upto tests set engine.rpc directly (an unexported field) after
construction, which bypassed the public RPCClient config field this PR
introduced to replace SetRPCForTests. Pass RPCClient in the UptoConfig
literal so the tests exercise the same injection path real callers use.
Ran go fix across the Go SDK and harness modules: interface{} -> any, the
slices/maps stdlib helpers, strings.SplitSeq iteration, min/max builtins, and
range-over-int/loopvar rewrites. Mechanical, no behavior change; the generated
codama client under protocols/programs/paymentchannels is left untouched.
build, vet, gofmt, and the full go test suite pass.
Add scripts/check-go-fix.sh and run it in the Go lint job so CI fails
when the Go 1.26 language/stdlib modernizers (go fix -inline=false) would
still change any non-generated source, keeping the SDK on newer idioms
(any, min/max, new(expr), range-over-int, slices/maps, SplitSeq, wg.Go)
without manual review comments. The inline analyzer is excluded because
it applies opt-in //go:fix inline directives and is cache sensitive;
generated codama clients are excluded by their AUTOGENERATED header. On
failure the gate prints the suggested diff so the fix is copy-pasteable.
@EfeDurmaz16 EfeDurmaz16 force-pushed the fix/go-idiomatic-cleanup branch from 94ee9db to 6ff0a6c Compare July 5, 2026 08:27
@EfeDurmaz16

Copy link
Copy Markdown
Collaborator Author

@greptile-apps review

Comment thread go/paykit/adapters/mpp/adapter.go Outdated
Comment thread go/protocols/mpp/client/payment_channels.go Outdated
@EfeDurmaz16

Copy link
Copy Markdown
Collaborator Author

@greptile-apps please run a fresh full review of the latest commit.

@lgalabru

lgalabru commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Merge conflicts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants