Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
2a323ff
feat(go): paykit umbrella skeleton + signer + middleware (WIP)
EfeDurmaz16 May 28, 2026
69851d2
feat(go): paykit umbrella + signer + MPP/x402 adapters + harness server
EfeDurmaz16 May 28, 2026
553469c
chore(go): drop accidentally committed build artifacts + add to gitig…
EfeDurmaz16 May 28, 2026
86bf41d
test(go): adapter + signer + middleware unit suite + CI gate at 85
EfeDurmaz16 May 28, 2026
be280ef
refactor(go): rename schemes -> protocols + apply caveat #3 #6 #7
EfeDurmaz16 May 28, 2026
34e72e0
test(go): bump combined coverage to 90.2%, ratchet CI gate to 90
EfeDurmaz16 May 28, 2026
000ded1
fix(go): MPP feePayer wiring + lint errcheck/staticcheck issues
EfeDurmaz16 May 28, 2026
d5a4fef
refactor(go): apply Ludo PR #146 review (typed structs, paths, examples)
EfeDurmaz16 May 28, 2026
40ebcdd
fix(go,harness): wire settlement-header alias + MPP_INTEROP_MINT for …
EfeDurmaz16 May 28, 2026
6fa1f8a
fix(go,harness): bypass umbrella for MPP path + ship original tx byte…
EfeDurmaz16 May 28, 2026
64d7745
fix(harness): inline manual MPP handler + scope CI to charge-basic+x4…
EfeDurmaz16 May 28, 2026
d1767b2
refactor(go,harness): apply Ludo PR #146 round-3 review
EfeDurmaz16 May 28, 2026
bb8223a
chore(harness): drop accidentally committed paykit-server binary
EfeDurmaz16 May 28, 2026
08bc7ae
fix(harness): MPP charge takes decimal price, not integer base units
EfeDurmaz16 May 28, 2026
4df2752
fix(go): x402 structural verifier, byte-preserving cosign, MPP race +…
EfeDurmaz16 May 28, 2026
9b14e35
feat(go,harness): canonical L6 codes + broaden interop matrix to 10 c…
EfeDurmaz16 May 28, 2026
4b70c09
refactor(go): align package layout with the Rust crate structure
EfeDurmaz16 May 28, 2026
537944c
ci(go): repoint workflows, asset generator, and docs to the new layout
EfeDurmaz16 May 28, 2026
3842c98
test(harness): repoint compute-budget cap rows to real pay-kit paths
EfeDurmaz16 May 28, 2026
2ea646c
ci: drop duplicate Go test job from ci.yml, leave go.yml as the owner
EfeDurmaz16 May 28, 2026
b255c6f
test(go): consolidate *_branch_test.go into their sibling test files
EfeDurmaz16 May 28, 2026
240a018
feat(x402): add a Solana exact client and fix the Token-2022 program
EfeDurmaz16 May 28, 2026
153d62c
test(go): raise coverage to 91% and gate on it
EfeDurmaz16 May 28, 2026
0c25c42
test(interop): wire the Go x402 client into the interop matrix
EfeDurmaz16 May 28, 2026
39d2759
fix(x402,interop): advertise the funded mint and actually run x402 in CI
EfeDurmaz16 May 28, 2026
17c26f7
docs(paykit): refer to the protocols/ packages, not schemes/
EfeDurmaz16 May 28, 2026
37b7221
docs(go): restore the simple-server example README
EfeDurmaz16 May 28, 2026
1d66b4c
docs: bump root README Go coverage badge 84% -> 91%
EfeDurmaz16 May 28, 2026
30871fa
refactor(go): move shared Solana tx helpers to paycore/solanatx
EfeDurmaz16 May 29, 2026
0e155ae
chore(go): remove the empty kms/ placeholder dir
EfeDurmaz16 May 29, 2026
f1395e9
docs(go): align README with the umbrella, like the sibling SDKs
EfeDurmaz16 May 29, 2026
5660bbc
refactor(go): rename Denom -> Currency; rename test + trim gitignore …
EfeDurmaz16 May 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
rust/src/server/html/template.gen.html linguist-generated
rust/src/server/html/payment_ui.gen.js linguist-generated
rust/src/server/html/service_worker.gen.js linguist-generated
go/server/html/template.gen.html linguist-generated
go/server/html/payment-ui.gen.js linguist-generated
go/server/html/service-worker.gen.js linguist-generated
go/protocols/mpp/server/html/template.gen.html linguist-generated
go/protocols/mpp/server/html/payment-ui.gen.js linguist-generated
go/protocols/mpp/server/html/service-worker.gen.js linguist-generated
lua/mpp/server/html_assets/gen.lua linguist-generated
python/src/solana_mpp/server/html/template.gen.html linguist-generated
python/src/solana_mpp/server/html/service_worker.gen.js linguist-generated
40 changes: 6 additions & 34 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,9 @@ jobs:
- name: Verify committed gen files are up to date
working-directory: .
run: |
if ! git diff --quiet -- rust/src/server/html/ go/server/html/ lua/mpp/server/html_assets/ python/src/solana_mpp/server/html/; then
if ! git diff --quiet -- rust/src/server/html/ go/protocols/mpp/server/html/ lua/mpp/server/html_assets/ python/src/solana_mpp/server/html/; then
echo "::error::Generated files are out of date. Run 'just html-build' and commit the results."
git diff --stat -- rust/src/server/html/ go/server/html/ lua/mpp/server/html_assets/ python/src/solana_mpp/server/html/
git diff --stat -- rust/src/server/html/ go/protocols/mpp/server/html/ lua/mpp/server/html_assets/ python/src/solana_mpp/server/html/
exit 1
fi
- name: Upload HTML build artifacts
Expand All @@ -135,7 +135,7 @@ jobs:
path: |
html/dist/
rust/src/server/html/
go/server/html/
go/protocols/mpp/server/html/
lua/mpp/server/html_assets/
python/src/solana_mpp/server/html/
typescript/packages/mpp/src/server/html-assets.gen.ts
Expand Down Expand Up @@ -211,37 +211,9 @@ jobs:
path: rust/target/surfpool-reports/
if-no-files-found: ignore

test-go:
name: Go tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version-file: go/go.mod
cache-dependency-path: go/go.sum
- name: Run tests with coverage
working-directory: go
env:
GOCACHE: /tmp/go-build-cache
run: |
# Narrow `go test` to library packages so coverage measures SDK
# surface area only (cmd/ + internal/testutil hold fixtures and
# are excluded; mirrors the per-package narrowing pattern from
# PR #101). Gate set to the cross-SDK baseline of 90; library
# packages currently aggregate at ~85% because client,
# internal/utils, and server have known coverage gaps.
# The threshold WILL fail until gap-fill PRs land; surfaced as
# an explicit finding per PR #102 review thread rather than
# lowering the gate.
go test ./ ./client ./internal/utils ./protocol ./protocol/core ./protocol/intents ./server -coverprofile=coverage.out -covermode=atomic
./scripts/check_coverage.sh coverage.out 90
- name: Upload Go coverage
if: always()
uses: actions/upload-artifact@v7
with:
name: go-coverage
path: go/coverage.out
# Go tests, lint, coverage, and interop live in the dedicated go.yml
# workflow (triggered on pull_request and workflow_call). Kept out of
# ci.yml so there is a single "Go tests" check rather than two.

integration:
name: Integration tests
Expand Down
95 changes: 86 additions & 9 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,22 @@ jobs:
GOCACHE: /tmp/go-build-cache
run: |
go test \
github.com/solana-foundation/pay-kit/go \
github.com/solana-foundation/pay-kit/go/client \
github.com/solana-foundation/pay-kit/go/errorcodes \
github.com/solana-foundation/pay-kit/go/internal/utils \
github.com/solana-foundation/pay-kit/go/protocol \
github.com/solana-foundation/pay-kit/go/protocol/core \
github.com/solana-foundation/pay-kit/go/protocol/intents \
github.com/solana-foundation/pay-kit/go/server \
github.com/solana-foundation/pay-kit/go/paycore \
github.com/solana-foundation/pay-kit/go/paycore/solanatx \
github.com/solana-foundation/pay-kit/go/paykit \
github.com/solana-foundation/pay-kit/go/signer \
github.com/solana-foundation/pay-kit/go/protocols/mpp \
github.com/solana-foundation/pay-kit/go/protocols/mpp/core \
github.com/solana-foundation/pay-kit/go/protocols/mpp/wire \
github.com/solana-foundation/pay-kit/go/protocols/mpp/intents \
github.com/solana-foundation/pay-kit/go/protocols/mpp/server \
github.com/solana-foundation/pay-kit/go/protocols/mpp/client \
github.com/solana-foundation/pay-kit/go/protocols/mpp/errorcodes \
github.com/solana-foundation/pay-kit/go/protocols/x402 \
github.com/solana-foundation/pay-kit/go/protocols/x402/client \
-coverprofile=coverage.out \
-covermode=atomic
./scripts/check_coverage.sh coverage.out 90
./scripts/check_coverage.sh coverage.out 91
- name: Run all Go packages
working-directory: go
env:
Expand Down Expand Up @@ -70,3 +75,75 @@ jobs:
version: v2.12.2
args: --timeout=5m
skip-cache: true

interop-go:
name: Go PayKit interop (mpp charge + x402 exact)
needs: test-go
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version-file: go/go.mod
cache-dependency-path: go/go.sum
- uses: dtolnay/rust-toolchain@stable
- uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
rust/target
key: ${{ runner.os }}-cargo-go-interop-${{ hashFiles('rust/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-go-interop-
- uses: pnpm/action-setup@v5
with:
package_json_file: package.json
- uses: actions/setup-node@v5
with:
node-version: 22
cache: pnpm
cache-dependency-path: typescript/pnpm-lock.yaml
- name: Install TypeScript workspace
working-directory: typescript
run: pnpm install --frozen-lockfile
- name: Build TypeScript package
working-directory: typescript
run: pnpm --filter @solana/mpp build
- name: Build Rust interop client
working-directory: rust
run: cargo build -p solana-mpp --bin interop_client
- name: Build Rust x402 interop client
working-directory: rust
run: cargo build -p solana-x402 --bin interop_client
- name: Install interop harness
working-directory: harness
run: pnpm install --frozen-lockfile
- name: Typecheck interop harness
working-directory: harness
run: pnpm typecheck
- name: Build Go paykit harness server
working-directory: harness/go-server
run: go build -o paykit-server .
- name: Run Go PayKit interop matrix (mpp charge + x402 exact)
working-directory: harness
env:
# Only the go server runs in this job, so every generated pair
# targets it: no testNamePattern filter and no skipped rows.
MPP_INTEROP_CLIENTS: typescript
MPP_INTEROP_SERVERS: go
MPP_INTEROP_INTENTS: charge,x402-exact
# selectInteropScenarios reads MPP_INTEROP_SCENARIOS for the
# whole active set (x402 included), so x402-exact-basic must be
# listed here. Charge scenarios the go server is eligible for:
# basic, splits (Token + Token-2022), symbol resolution,
# idempotent replay store, compute-budget cap, over-split
# rejection, sum-equals-amount, and the two L6 fault-matrix
# codes (wrong_network / charge_request_mismatch); plus the
# x402 exact scheme.
MPP_INTEROP_SCENARIOS: charge-basic,charge-split-ata,charge-symbol-usdc-localnet,charge-token2022-split-ata,charge-split-ata-idempotent,charge-compute-budget-over-cap,charge-splits-too-many,charge-splits-sum-equals-amount,charge-network-mismatch,charge-cross-route-replay,x402-exact-basic
X402_INTEROP_CLIENTS: rust-x402,go-x402
# Pin the x402 server set to the main go server (the rust-x402 /
# ts-x402 server fixtures default on; exclude them so the only
# x402 server is the go paykit server under test).
X402_INTEROP_SERVERS: go
run: pnpm exec vitest run test/e2e.test.ts --testTimeout 180000
6 changes: 0 additions & 6 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,10 @@ Cargo.lock
.npm-pack/
DUMP.md
package/
go/payment-link-server
__pycache__/
.coverage
.venv/
*.pyc
harness/go-client/go-client
.claude/
.gocache
mpp-sdk-self-learning/
.build/
go/coverage.out
notes/codex-review/
notes/codex-review-*.md
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ The interop harness can run a full client/server cross-product, but CI keeps the
|----------|----------|-------|
| TypeScript | ![TS](https://img.shields.io/badge/coverage-67_tests-blue) | `just ts-test` |
| Rust | ![Rust](https://img.shields.io/badge/coverage-271_tests-blue) | `just rs-test` |
| Go | ![Go](https://img.shields.io/badge/coverage-84%25-green) | `just go-test` |
| Go | ![Go](https://img.shields.io/badge/coverage-91%25-green) | `just go-test` |
| Python | ![Python](https://img.shields.io/badge/coverage-87%25-green) | `just py-test` |
| Lua | ![Lua](https://img.shields.io/badge/coverage-41_tests-blue) | `just lua-test` |
| Ruby | ![Ruby](https://img.shields.io/badge/coverage-98%25-green) | `just rb-test-cover` |
Expand Down Expand Up @@ -139,7 +139,7 @@ receipt = await mpp.verify_credential(credential)
<summary>Go</summary>

```go
import "github.com/solana-foundation/pay-kit/go/server"
import "github.com/solana-foundation/pay-kit/go/protocols/mpp/server"

m, _ := server.New(server.Config{
Recipient: "RecipientPubkey...",
Expand Down
2 changes: 1 addition & 1 deletion docs/security/compute-budget-caps.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ this monorepo.
| Lua | `lua/mpp/server/solana_verify.lua:20` | `MAX_COMPUTE_UNIT_PRICE_MICROLAMPORTS` |
| Lua (#103) | `lua/mpp/methods/solana/instructions.lua:31` | `MAX_COMPUTE_UNIT_LIMIT` (pending PR #103 merge) |
| Lua (#103) | `lua/mpp/methods/solana/instructions.lua:32` | `MAX_COMPUTE_UNIT_PRICE_MICROLAMPORTS` (pending PR #103 merge) |
| Go (#101) | `go/server/server.go` (`maxComputeUnitLimit`) | pending PR #101 merge |
| Go (#101) | `go/protocols/mpp/server/server.go` (`maxComputeUnitLimit`) | pending PR #101 merge |
| Python (#106) | `python/src/solana_mpp/server/mpp.py` | pending PR #106 merge |

`harness/test/compute-budget-caps.test.ts` parses each file above
Expand Down
6 changes: 3 additions & 3 deletions docs/security/fee-payer-drain.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Client crafts a transaction where the fee-payer is placed at a non-canonical sig

### 4. Tampered-Details Attack (Client-Supplied `methodDetails.feePayerKey`)

The MPP charge request carries `methodDetails.feePayerKey` (string, base58 pubkey; this is the canonical wire field across all SDKs, see [`typescript/packages/mpp/src/Methods.ts`](../../typescript/packages/mpp/src/Methods.ts) L62, [`go/protocol/solana.go`](../../go/protocol/solana.go) L115, [`python/src/solana_mpp/protocol/solana.py`](../../python/src/solana_mpp/protocol/solana.py) L120, [`rust/crates/mpp/src/protocol/solana.rs`](../../rust/crates/mpp/src/protocol/solana.rs) L394, [`php/src/Server/SolanaChargeTransactionVerifier.php`](../../php/src/Server/SolanaChargeTransactionVerifier.php) L304, [`ruby/lib/mpp/methods/solana/verifier.rb`](../../ruby/lib/mpp/methods/solana/verifier.rb), [`lua/mpp/server/init.lua`](../../lua/mpp/server/init.lua) L85). A malicious client supplies `methodDetails.feePayerKey = ATTACKER_PUBKEY` while the server's actual signing key is `SERVER_PUBKEY`. If the verifier trusts the client-supplied details field as the source of truth for "who is the fee-payer", it will validate guards (source != fee-payer, slot, etc.) against `ATTACKER_PUBKEY`. The real `SERVER_PUBKEY` then signs a transaction that drains itself.
The MPP charge request carries `methodDetails.feePayerKey` (string, base58 pubkey; this is the canonical wire field across all SDKs, see [`typescript/packages/mpp/src/Methods.ts`](../../typescript/packages/mpp/src/Methods.ts) L62, [`go/paycore/solana.go`](../../go/paycore/solana.go) L115, [`python/src/solana_mpp/protocol/solana.py`](../../python/src/solana_mpp/protocol/solana.py) L120, [`rust/crates/mpp/src/protocol/solana.rs`](../../rust/crates/mpp/src/protocol/solana.rs) L394, [`php/src/Server/SolanaChargeTransactionVerifier.php`](../../php/src/Server/SolanaChargeTransactionVerifier.php) L304, [`ruby/lib/mpp/methods/solana/verifier.rb`](../../ruby/lib/mpp/methods/solana/verifier.rb), [`lua/mpp/server/init.lua`](../../lua/mpp/server/init.lua) L85). A malicious client supplies `methodDetails.feePayerKey = ATTACKER_PUBKEY` while the server's actual signing key is `SERVER_PUBKEY`. If the verifier trusts the client-supplied details field as the source of truth for "who is the fee-payer", it will validate guards (source != fee-payer, slot, etc.) against `ATTACKER_PUBKEY`. The real `SERVER_PUBKEY` then signs a transaction that drains itself.

Source of truth MUST be the server-context fee-payer pubkey (the public key of the server's signer keypair), never a client-controlled field.

Expand Down Expand Up @@ -68,7 +68,7 @@ A passing fee-payer co-sign path is the conjunction of all four. Missing any one
| Ruby | [`ruby/lib/mpp/methods/solana/verifier.rb`](../../ruby/lib/mpp/methods/solana/verifier.rb): `validate_allowlist` (L191), `expected_fee_payer` (L100), source-vs-fee-payer guards at L128, L156, L158 |
| Lua | [`lua/mpp/server/solana_verify.lua`](../../lua/mpp/server/solana_verify.lua): `verify_instruction_allowlist` (L330), invoked from the main verify path at L140 |
| Python | `python/src/solana_mpp/server/mpp.py`: `_validate_instruction_allowlist` (lands with [#106](https://github.com/solana-foundation/mpp-sdk/pull/106)) |
| Go | `go/server/server.go`: allowlist branch inside `verifyTransaction` (lands with [#101](https://github.com/solana-foundation/mpp-sdk/pull/101)) |
| Go | `go/protocols/mpp/server/server.go`: allowlist branch inside `verifyTransaction` (lands with [#101](https://github.com/solana-foundation/mpp-sdk/pull/101)) |

The Rust path is the spine. PHP, Ruby, Lua, Python, and Go port the same four invariants with language-idiomatic surfaces.

Expand All @@ -83,7 +83,7 @@ Every SDK ships regression tests that fail closed when the corresponding invaria
| Rust | `fee_payer_must_be_transaction_fee_payer` (L2884), `fee_payer_cannot_fund_sol_payment_transfer` (L2906), `parsed_allowlist_rejects_extra_spl_transfer_after_required_transfer` (L4906), `spl_fee_payer_rejects_top_level_ata_creation` (L3138), `b34_rejects_push_credential_on_fee_payer_route` (L4136) | [`rust/crates/mpp/src/server/charge.rs`](../../rust/crates/mpp/src/server/charge.rs) (in-crate `#[cfg(test)]` module) |
| Ruby | `test_rejects_fee_payer_funding_sol_transfer` (L258), `test_rejects_fee_payer_missing_key_and_mismatch` (L339), `test_rejects_spl_wrong_destination_and_fee_payer_authority` (L569) | [`ruby/test/server_test.rb`](../../ruby/test/server_test.rb) |
| PHP | `testRejectsFeePayerFundingNativeSolPayment` (L148), `testRejectsFeePayerMismatch` (L209), `testRejectsMissingFeePayerKey` (L220), `testRejectsFeePayerAuthorizingSplTransfer` (L266), `testRejectsFeePayerTokenAccountFundingSplTransfer` (L281) | [`php/tests/SolanaChargeTransactionVerifierTest.php`](../../php/tests/SolanaChargeTransactionVerifierTest.php) |
| Go | Drain regression suite (lands with [#101](https://github.com/solana-foundation/mpp-sdk/pull/101)) | `go/server/server_test.go` |
| Go | Drain regression suite (lands with [#101](https://github.com/solana-foundation/mpp-sdk/pull/101)) | `go/protocols/mpp/server/server_test.go` |

Python is the reference suite for new ports because it exercises all four attack shapes plus positive controls. Other languages MAY ship a smaller suite as long as every one of the four attack shapes is covered by at least one negative test, with at least one positive control proving the legitimate payment still passes.

Expand Down
50 changes: 33 additions & 17 deletions go/Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,43 @@ set shell := ["bash", "-uc"]
default:
@just --list

# Build Go SDK
# Install Go module dependencies
install:
go mod download

# Validate module + check it builds
build:
mkdir -p /tmp/go-build-cache
GOCACHE=/tmp/go-build-cache go build ./...
go build ./...

# Test Go SDK
# Run the full Go test suite
test:
mkdir -p /tmp/go-build-cache
GOCACHE=/tmp/go-build-cache go test ./...
go test ./...

# Format Go SDK
# Apply gofmt + goimports in place
fmt:
gofmt -w $(find . -name '*.go' -type f | sort)
gofmt -s -w .

# Lint Go SDK
# Lint pipeline: gofmt-check + go vet + staticcheck
lint:
gofmt -l $(find . -name '*.go' -not -path './vendor/*')
golangci-lint run --timeout=5m

# Run Go coverage with a minimum threshold of 70%
test-cover:
mkdir -p /tmp/go-build-cache
GOCACHE=/tmp/go-build-cache go test ./... -coverprofile=coverage.out -covermode=atomic
GOCACHE=/tmp/go-build-cache ./scripts/check_coverage.sh coverage.out 70
test -z "$(gofmt -s -l . | tee /dev/stderr)"
go vet ./...
go run honnef.co/go/tools/cmd/staticcheck@latest ./...

# Run module-deps audit (govulncheck)
audit:
go run golang.org/x/vuln/cmd/govulncheck@latest ./...

# Test with coverage gate (defaults to 90)
test-cover gate="90":
mkdir -p build
go test -coverprofile=build/coverage.out ./...
go tool cover -func=build/coverage.out
@awk -v gate={{gate}} '/^total:/ {pct=$NF+0; if (pct+0 < gate+0) {printf "coverage %s below gate %d%%\n",$NF,gate; exit 1} else {printf "coverage %s meets gate %d%%\n",$NF,gate}}' <(go tool cover -func=build/coverage.out)

# Run every local gate (build + lint + audit + test-cover)
check: build lint audit test-cover

# Boot the dual-protocol example server
serve-example port="4567":
go run ./examples/paykit-server

Loading
Loading