Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
6af2d40
feat(go): reproducible codama-go codegen for payment-channels client
EfeDurmaz16 Jun 8, 2026
58f47f8
feat(go): add MPP client session intent
EfeDurmaz16 Jun 8, 2026
4d89527
fix(go): harden session watermark and commit-receipt handling
EfeDurmaz16 Jun 8, 2026
3e258f0
test(conformance): add session voucher-preimage cross-SDK vectors
EfeDurmaz16 Jun 8, 2026
2c2a389
fix(go): advance request nonce when reconciling a replayed settled cu…
EfeDurmaz16 Jun 8, 2026
d08c506
feat(go): allow overriding the payment-channels program id for non-ma…
EfeDurmaz16 Jun 9, 2026
e41e1ae
fix(go): clamp replayed-receipt reconcile + reject vouchers missing c…
EfeDurmaz16 Jun 9, 2026
16e944c
refactor(go): delegate VoucherData.MessageBytes to the canonical packer
EfeDurmaz16 Jun 10, 2026
5493090
feat(go): thread a per-call program id through payment-channel builders
EfeDurmaz16 Jun 12, 2026
7759393
feat(go): add challenge-driven payment-channel open builders
EfeDurmaz16 Jun 12, 2026
cbd2a87
feat(go): add metered SSE consumption for session streams
EfeDurmaz16 Jun 12, 2026
5d64e2c
feat(go): add session challenge selection with mode gating
EfeDurmaz16 Jun 12, 2026
c147ebf
docs(go): state the session client scope in the README and doc comments
EfeDurmaz16 Jun 12, 2026
dc8c0c1
refactor(go): clear staticcheck findings in the client package
EfeDurmaz16 Jun 12, 2026
2f09a0d
test(go): cover session opener and SSE stream error branches
EfeDurmaz16 Jun 12, 2026
1eeb2df
feat(go): add server-side session channel store
EfeDurmaz16 Jun 12, 2026
b2f8f64
feat(go): add ordered session voucher verifier
EfeDurmaz16 Jun 12, 2026
8fbf30a
feat(go): add server session intent handlers
EfeDurmaz16 Jun 12, 2026
ae86676
feat(go): add session idle-close lifecycle watchdog
EfeDurmaz16 Jun 12, 2026
2918d69
feat(go): add payment-channel settlement instruction builders
EfeDurmaz16 Jun 12, 2026
d2b44b6
feat(go): verify session open transactions and derive settlement
EfeDurmaz16 Jun 12, 2026
2418cf5
feat(go): record the settled signature on session channel state
EfeDurmaz16 Jun 12, 2026
21f2afb
feat(go): add server-broadcast session open submission
EfeDurmaz16 Jun 12, 2026
dbdc58f
feat(go): add the HTTP-facing session server method
EfeDurmaz16 Jun 12, 2026
ed3fd21
feat(go): add session metering side channel and middleware
EfeDurmaz16 Jun 12, 2026
5c7e897
feat(go): add server-side metered SSE stream writer
EfeDurmaz16 Jun 12, 2026
b3cd72e
test(go): add surfpool-gated session lifecycle e2e
EfeDurmaz16 Jun 12, 2026
eda39cb
test(go): cover session failure paths adversarially
EfeDurmaz16 Jun 12, 2026
7b1a50b
test(go): round-trip protocol-runner format verbs and shared errors
EfeDurmaz16 Jun 12, 2026
b78a575
chore(go): carve generated client and examples out of the coverage gate
EfeDurmaz16 Jun 12, 2026
2f36d8c
docs(go): mark the session server cell shipped in the README
EfeDurmaz16 Jun 12, 2026
186fc08
feat(go): add the playground-api example mirroring the TypeScript pla…
EfeDurmaz16 Jun 12, 2026
47feb17
test(go): smoke-test and e2e the playground API example
EfeDurmaz16 Jun 12, 2026
1c2f508
docs(go): document the playground API example
EfeDurmaz16 Jun 12, 2026
27852e5
ci(go): boot the playground example and run the payment-link Playwrig…
EfeDurmaz16 Jun 12, 2026
ce485c2
fix(go): reject empty-string transaction on push open instead of pani…
EfeDurmaz16 Jun 12, 2026
17cc056
fix(harness): honor PAY_KIT_HARNESS_PROTOCOL in the go client adapter
EfeDurmaz16 Jun 12, 2026
bc2e29f
fix(go): match playground stock endpoints to the typescript example s…
EfeDurmaz16 Jun 12, 2026
9b29beb
refactor(go): reuse paycore constants in the playground example
EfeDurmaz16 Jun 12, 2026
0f42673
refactor(go): consolidate playground helpers into pay-kit
EfeDurmaz16 Jun 12, 2026
6a80b6b
refactor(go): type the session challenge selector network as an enum
EfeDurmaz16 Jun 12, 2026
f1192a2
docs(go): describe behavior instead of porting provenance in comments
EfeDurmaz16 Jun 12, 2026
d68de71
docs(go): comment every struct field the session PR touches
EfeDurmaz16 Jun 12, 2026
064fe7f
fix(go): satisfy errcheck and staticcheck in playground example and s…
EfeDurmaz16 Jun 12, 2026
5d0c8d1
chore(go): ignore the local coverage output directory
EfeDurmaz16 Jun 12, 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
66 changes: 66 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ jobs:
github.com/solana-foundation/pay-kit/go/paycore \
github.com/solana-foundation/pay-kit/go/paycore/solanatx \
github.com/solana-foundation/pay-kit/go/paycore/signer \
github.com/solana-foundation/pay-kit/go/paycore/paymentchannels \
github.com/solana-foundation/pay-kit/go/paykit \
github.com/solana-foundation/pay-kit/go/protocols/mpp \
github.com/solana-foundation/pay-kit/go/protocols/mpp/core \
Expand Down Expand Up @@ -147,3 +148,68 @@ jobs:
# x402 server is the go paykit server under test).
X402_HARNESS_SERVERS: go
run: pnpm exec vitest run test/e2e.test.ts --testTimeout 180000

playground-go:
name: "Payment links E2E: Playground (Go)"
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: 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 Surfnet helper dependencies
working-directory: harness
run: pnpm install --frozen-lockfile
- name: Start Surfnet
working-directory: .
run: |
node harness/start-surfnet-proxy.mjs &
ready=0
for i in $(seq 1 50); do
if curl -sf -X POST http://localhost:8899 \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"getHealth","params":[]}' \
| grep -q '"result":"ok"'; then
ready=1
break
fi
sleep 0.2
done
test "$ready" -eq 1
- name: Build Go playground API
working-directory: go
env:
GOCACHE: /tmp/go-build-cache
run: go build ./examples/playground-api
- name: Start Go playground API
working-directory: go
env:
GOCACHE: /tmp/go-build-cache
PORT: "3002"
NETWORK: localnet
RPC_URL: http://localhost:8899
MPP_SECRET_KEY: playground-ci-secret
run: go run ./examples/playground-api &
- name: Wait for playground server
working-directory: .
run: |
for i in $(seq 1 30); do
curl -sf http://localhost:3002/api/v1/health && break
sleep 1
done
- name: Install HTML dependencies & Playwright
working-directory: html
run: npm install && npx playwright install chromium
- name: Run Playwright tests (Go playground)
working-directory: html
run: FORTUNE_PATH=/api/v1/fortune npm run test:e2e:go
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ __pycache__/
.claude/
.gocache
.build/
go/build/

# Generated API docs — see `just docs`. Single tree at the repo root;
# each language emits markdown into `docs/api/<lang>/`.
Expand Down
6 changes: 6 additions & 0 deletions go/.golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ linters:
exclusions:
paths:
- examples/simple-server
# Codama-generated on-chain program clients (regenerated via
# `pnpm run payment-channels:go` in skills/.../codegen). These files carry
# a "DO NOT EDIT" header, so lint findings can't be fixed in place; the
# generator's output is byte-for-byte reproducible and matches the
# gagliardetto/solana-go generated-client conventions.
- protocols/programs/.*/.*\.go
rules:
- path: _test\.go
linters:
Expand Down
30 changes: 25 additions & 5 deletions go/Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,37 @@ test:
fmt:
gofmt -s -w .

# Lint pipeline: gofmt-check + go vet + staticcheck
# Lint pipeline: gofmt-check + go vet + staticcheck.
# Codama-generated program clients under protocols/programs/<name>/ carry a
# "DO NOT EDIT" header and follow gagliardetto/solana-go's generated-client
# idioms (unkeyed VariantType literals, embedded-field selectors), which trip
# `go vet -composites` and staticcheck QF1008. They are byte-for-byte
# reproducible from idl/<name>.json, so we exclude the generated dirs from the
# authored-code lint gates — mirroring the golangci-lint path exclusion and the
# Rust crate's pure-passthrough src/generated/ treatment.
lint:
test -z "$(gofmt -s -l . | tee /dev/stderr)"
go vet ./...
go run honnef.co/go/tools/cmd/staticcheck@latest ./...
# -composites is disabled module-wide because the generated clients (and any
# package importing them, e.g. the parity guard) surface gagliardetto's
# unkeyed VariantType literals; golangci-lint's govet (the CI gate) already
# runs with composites off, so this keeps the local justfile vet aligned.
go vet -composites=false $(go list ./... | grep -v '/protocols/programs/paymentchannels$')
go run honnef.co/go/tools/cmd/staticcheck@latest $(go list ./... | grep -v '/protocols/programs/paymentchannels$')

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

# Test with coverage gate (defaults to 90)
# Test with coverage gate (defaults to 90).
# The codama-generated payment-channels client and the runnable examples are
# filtered from the profile before the gate: the generated client is
# byte-for-byte reproducible from idl/payment-channels.json (the same
# carve-out the lint recipe applies), and examples are demo binaries that the
# other SDKs in this repo also keep out of their coverage denominators.
test-cover gate="90":
mkdir -p build
go test -coverprofile=build/coverage.out ./...
go test -coverprofile=build/coverage.raw.out ./...
grep -v -e '/protocols/programs/paymentchannels/' -e '/examples/' build/coverage.raw.out > 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)

Expand All @@ -43,3 +60,6 @@ check: build lint audit test-cover
serve-example port="4567":
go run ./examples/simple-server

# Boot the playground API example (same endpoints as typescript/examples/playground-api)
serve-playground port="3000":
PORT={{port}} go run ./examples/playground-api
60 changes: 59 additions & 1 deletion go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ The Solana charge intent, in both pull (client-signed) and push
|---|:---:|:---:|
| `mpp/charge/pull` | ✅ | ✅ |
| `mpp/charge/push` | ✅ | ✅ |
| `mpp/session` | | |
| `mpp/session` | | |
| `mpp/subscription` | — | — |

For `mpp/charge/pull`: the server owns the full lifecycle. It issues
Expand All @@ -130,6 +130,64 @@ with `getTransaction`, rejects failed or missing metadata, reuses the
same structural transaction verifier as pull mode, consumes the
signature through replay storage, and emits the same receipt shape.

For `mpp/session`: both sides ship.

Client side:

- session challenge parsing and selection (`ParseSessionChallenge`,
`SelectSessionChallenge` with network/currency/mode filters; omitted
or empty `modes` means push-only),
- payment-channel open builders driven by the challenge (deposit
defaults to the cap, grace period 900s, random salt, token program
resolved from the currency so Token-2022 mints work, operator as fee
payer with a payer partial-sign, challenge `recentBlockhash` echo,
`PendingServerSignature` placeholder) for push and pull/clientVoucher,
- `ActiveSession` voucher signing with the prepare/record watermark
split, `SessionConsumer` for metered deliveries, and the metered SSE
layer (`SseDecoder`, `MeteredSseSession`, `MeteredSseStream`,
`HTTPCommitTransport`).

Server side (`NewSession`, mirroring the TypeScript `session()` method
over the rust `SessionServer` core):

- HMAC-bound 402 session challenges (`Session.Challenge`): cap clamped
to the server max, `minVoucherDelta` only when positive, `modes`
omitted when push-only, `pullVoucherStrategy` only when pull is
offered, optional `recentBlockhash` prefetch via the configured RPC
client,
- credential verification (`Session.VerifyCredential`) dispatching the
open / voucher / commit / topUp / close actions over an atomic
per-channel `ChannelStore` with the harness-tested voucher check
order, idempotent open replays that never reset the watermark, and a
re-drivable close until a settlement signature is recorded,
- on-chain open handling: structural `VerifyOpenTx` for client-broadcast
opens (legacy and v0 encodings, payload signature binding, channel
PDA re-derivation) and `SubmitOpenTx` server broadcast that completes
the fee-payer signature and waits for confirmation,
- the reserve/commit metering side channel (`Session.Routes`) hosts
mount at `POST /__402/session/deliveries` and
`POST /__402/session/commit` (a TypeScript-server extension, not in
the rust crate), plus `SessionMiddleware` for `net/http` routes,
- a server-side metered SSE writer (`MeteredStream`) emitting the
`mpp.metering` / `mpp.usage` / `[DONE]` frames the client decoder
consumes,
- an idle-close watchdog (`CloseDelay`) and close settlement
(settle_and_finalize + Ed25519 precompile + distribute in one
merchant-signed transaction), both of which settle on-chain only when
a merchant `Signer` and an `RPC` client are configured; without them
payload claims are trusted as provided, matching rust with `rpc_url`
unset.

Out of scope: pull/operatedVoucher (multi-delegate program builders) on
both sides, including the `initMultiDelegateTx` submission seam in the
TypeScript open handler, the SPL `approve` delegation transaction for
non-channel pull opens (the on-chain delegation happens out of band),
and a `SessionFetch`-style drop-in fetch wrapper. The TypeScript
`SessionFetchClient` semantics that wrapper would own (per-channel
commit watermark reset on re-open, failed-commit retryability without
latching) therefore have no Go counterpart; the `ActiveSession`
prepare/record split is the building block callers compose instead.

## Examples

One runnable example ships with this package:
Expand Down
Loading
Loading