Skip to content

Commit 5b79c3e

Browse files
authored
Batch-settlement: go-ts parity (#2227)
* fix PendingRequest and refund cleanup * fix lint * remove duplicate payer info from storage * fix PAYMENT-RESPONSE field ordering * clean up comments * consolidate ProcessSettlementWithExtensions into ProcessSettlement
1 parent f2d2dcc commit 5b79c3e

64 files changed

Lines changed: 678 additions & 554 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

examples/go/clients/batch-settlement/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Batch-Settlement Client (Go)
22

3-
Go port of [`examples/typescript/clients/batch-settlement`](../../../typescript/clients/batch-settlement). Sequential batch-settlement payment client. Opens a payment channel on the first request (deposit) and pays subsequent requests with off-chain vouchers that update the cumulative claimable amount.
3+
Sequential batch-settlement payment client. Opens a payment channel on the first request (deposit) and pays subsequent requests with off-chain vouchers that update the cumulative claimable amount.
44

55
## Run
66

@@ -11,7 +11,7 @@ cp .env-example .env
1111
go run .
1212
```
1313

14-
The companion server is in `examples/go/servers/batch-settlement` and the facilitator is in `examples/go/facilitator/batch-settlement`. The Go and TS clients share the same env keys, default route (`/weather`), and behavior — point the same `.env` at either binary.
14+
The companion server is in `examples/go/servers/batch-settlement` and the facilitator is in `examples/go/facilitator/batch-settlement`.
1515

1616
## Voucher Signer Delegation
1717

examples/go/clients/batch-settlement/main.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func main() {
6565
Salt: channelSalt,
6666
}
6767

68-
// Optional dedicated voucher-signing key (matches TS EVM_VOUCHER_SIGNER_PRIVATE_KEY).
68+
// Optional dedicated voucher-signing key.
6969
if voucherKey := os.Getenv("EVM_VOUCHER_SIGNER_PRIVATE_KEY"); voucherKey != "" {
7070
voucherSigner, err := evmsigners.NewClientSignerFromPrivateKey(voucherKey)
7171
if err != nil {
@@ -171,7 +171,7 @@ func readJSON(resp *http.Response) (interface{}, error) {
171171
return out, nil
172172
}
173173

174-
func extractSettleResponse(resp *http.Response) (*map[string]interface{}, error) {
174+
func extractSettleResponse(resp *http.Response) (*x402.SettleResponse, error) {
175175
header := resp.Header.Get("PAYMENT-RESPONSE")
176176
if header == "" {
177177
header = resp.Header.Get("X-PAYMENT-RESPONSE")
@@ -183,7 +183,7 @@ func extractSettleResponse(resp *http.Response) (*map[string]interface{}, error)
183183
if err != nil {
184184
return nil, err
185185
}
186-
var out map[string]interface{}
186+
var out x402.SettleResponse
187187
if err := json.Unmarshal(decoded, &out); err != nil {
188188
return nil, err
189189
}

examples/go/facilitator/batch-settlement/.env-example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ EVM_RPC_URL=https://sepolia.base.org
44
# Optional dedicated authorizer key (defaults to EVM_PRIVATE_KEY)
55
EVM_RECEIVER_AUTHORIZER_PRIVATE_KEY=
66

7-
# Optional (default 4022). Same as examples/typescript/facilitator/batch-settlement.
7+
# Optional listen port (default 4022).
88
PORT=

examples/go/facilitator/batch-settlement/README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ cp .env-example .env
2323
go run .
2424
```
2525

26-
Listens on `http://localhost:4022` by default (`PORT` overrides; same as the
27-
TypeScript facilitator example).
26+
Listens on `http://localhost:4022` by default (`PORT` overrides).
2827

2928
## Environment
3029

examples/go/servers/batch-settlement/README.md

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Batch-Settlement Server (Go)
22

3-
Go port of [`examples/typescript/servers/batch-settlement`](../../../typescript/servers/batch-settlement). Demo resource server using the batch-settlement scheme: a client opens a payment channel with a single deposit; subsequent paid requests update an off-chain voucher. The `ChannelManager` periodically claims and settles onchain.
3+
Demo resource server using the batch-settlement scheme: a client opens a payment channel with a single deposit; subsequent paid requests update an off-chain voucher. The `ChannelManager` periodically claims and settles onchain.
44

55
The route demonstrates **dynamic pricing**: the client authorizes up to `$0.01` per request, and the handler bills a random fraction of that via `Settlement-Overrides`.
66

@@ -15,18 +15,6 @@ go run .
1515

1616
The server listens on `http://localhost:4021` and exposes `GET /weather`. Pair with `examples/go/clients/batch-settlement` and `examples/go/facilitator/batch-settlement`.
1717

18-
### Cross-SDK local testing
19-
20-
The Go and TS servers share the same `.env-example` keys, route (`GET /weather`), response shape (`{report: {weather, temperature}}`), and channel-manager cadences, so you can swap one for the other without changing client config:
21-
22-
```bash
23-
# Go server
24-
EVM_ADDRESS=0x... FACILITATOR_URL=http://localhost:4022 go run .
25-
26-
# TS server (same .env)
27-
cd examples/typescript/servers/batch-settlement && pnpm dev
28-
```
29-
3018
## Environment
3119

3220
| Variable | Required | Description |
@@ -39,7 +27,7 @@ cd examples/typescript/servers/batch-settlement && pnpm dev
3927

4028
## Auto-settlement
4129

42-
The example wires up a `ChannelManager` with the same triggers as the TS demo:
30+
The example wires up a `ChannelManager` with simple local-demo triggers:
4331

4432
- **Claim** every 60 s.
4533
- **Settle** every 120 s (sweeps claimed funds to `payTo`).

examples/go/servers/batch-settlement/go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ require (
3535
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
3636
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
3737
golang.org/x/crypto v0.46.0 // indirect
38+
golang.org/x/net v0.48.0 // indirect
3839
golang.org/x/sync v0.19.0 // indirect
3940
golang.org/x/sys v0.39.0 // indirect
41+
golang.org/x/text v0.32.0 // indirect
4042
)

examples/go/servers/batch-settlement/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,8 @@ golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
178178
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
179179
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
180180
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
181+
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
182+
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
181183
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
182184
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
183185
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

examples/go/servers/batch-settlement/main.go

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import (
2121
batchedserver "github.com/x402-foundation/x402/go/mechanisms/evm/batch-settlement/server"
2222
)
2323

24-
// Mirrors examples/typescript/servers/batch-settlement/index.ts.
2524
const (
2625
defaultPort = "4021"
2726
network = x402.Network("eip155:84532")
@@ -43,7 +42,7 @@ func main() {
4342
os.Exit(1)
4443
}
4544

46-
// TS code default: 86400 (1 day). Falls back to that when env unset.
45+
// Default channel withdraw delay is 1 day when the env var is unset.
4746
withdrawDelay := 86400
4847
if v := os.Getenv("DEFERRED_WITHDRAW_DELAY_SECONDS"); v != "" {
4948
if n, err := strconv.Atoi(v); err == nil {
@@ -83,7 +82,7 @@ func main() {
8382
SettleIntervalSecs: 120,
8483
RefundIntervalSecs: 180,
8584
MaxClaimsPerBatch: 100,
86-
// Refund channels after 3 minutes of inactivity (mirrors TS demo).
85+
// Refund channels after 3 minutes of inactivity.
8786
SelectRefundChannels: func(channels []*batchedserver.ChannelSession, ctx batchedserver.AutoSettlementContext) ([]*batchedserver.ChannelSession, error) {
8887
out := make([]*batchedserver.ChannelSession, 0, len(channels))
8988
for _, c := range channels {
@@ -114,7 +113,7 @@ func main() {
114113
},
115114
})
116115

117-
// SIGINT-only graceful shutdown (no SIGTERM), mirroring TS.
116+
// Flush pending channel work during interactive shutdown.
118117
sigCh := make(chan os.Signal, 1)
119118
signal.Notify(sigCh, syscall.SIGINT)
120119

@@ -135,8 +134,7 @@ func main() {
135134

136135
mux := http.NewServeMux()
137136
mux.HandleFunc("GET /weather", func(w http.ResponseWriter, r *http.Request) {
138-
// Bill a random fraction of maxPrice (1–100%) to demonstrate
139-
// usage-based pricing. Mirrors TS demo verbatim.
137+
// Bill a random fraction of maxPrice (1-100%) to demonstrate usage-based pricing.
140138
chargedPercent := 1 + rand.Intn(100)
141139
nethttpmw.SetSettlementOverrides(w, &x402.SettlementOverrides{
142140
Amount: fmt.Sprintf("%d%%", chargedPercent),

go/SERVER.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ settleResult := httpServer.ProcessSettlement(ctx, payload, requirements, nil, &x
152152
Request: &reqCtx,
153153
ResponseBody: responseBody,
154154
ResponseHeaders: responseHeaders,
155-
})
155+
}, nil)
156156
```
157157

158158
### 4. Facilitator Client

go/http/client.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,13 +529,50 @@ func decodePaymentRequiredHeader(header string) (x402.PaymentRequired, error) {
529529

530530
// encodePaymentResponseHeader encodes a settlement response as base64
531531
func encodePaymentResponseHeader(response x402.SettleResponse) (string, error) {
532+
response = withTypedChannelStateExtra(response)
532533
data, err := json.Marshal(response)
533534
if err != nil {
534535
return "", fmt.Errorf("failed to marshal settle response: %w", err)
535536
}
536537
return base64.StdEncoding.EncodeToString(data), nil
537538
}
538539

540+
type paymentResponseChannelStateExtra struct {
541+
ChannelId string `json:"channelId,omitempty"`
542+
Balance string `json:"balance,omitempty"`
543+
TotalClaimed string `json:"totalClaimed,omitempty"`
544+
WithdrawRequestedAt interface{} `json:"withdrawRequestedAt,omitempty"`
545+
RefundNonce string `json:"refundNonce,omitempty"`
546+
ChargedCumulativeAmount string `json:"chargedCumulativeAmount,omitempty"`
547+
}
548+
549+
func withTypedChannelStateExtra(response x402.SettleResponse) x402.SettleResponse {
550+
raw, ok := response.Extra["channelState"].(map[string]interface{})
551+
if !ok {
552+
return response
553+
}
554+
555+
extra := make(map[string]interface{}, len(response.Extra))
556+
for key, value := range response.Extra {
557+
extra[key] = value
558+
}
559+
extra["channelState"] = paymentResponseChannelStateExtra{
560+
ChannelId: stringField(raw, "channelId"),
561+
Balance: stringField(raw, "balance"),
562+
TotalClaimed: stringField(raw, "totalClaimed"),
563+
WithdrawRequestedAt: raw["withdrawRequestedAt"],
564+
RefundNonce: stringField(raw, "refundNonce"),
565+
ChargedCumulativeAmount: stringField(raw, "chargedCumulativeAmount"),
566+
}
567+
response.Extra = extra
568+
return response
569+
}
570+
571+
func stringField(data map[string]interface{}, key string) string {
572+
value, _ := data[key].(string)
573+
return value
574+
}
575+
539576
// decodePaymentResponseHeader decodes a base64 payment response header
540577
func decodePaymentResponseHeader(header string) (*x402.SettleResponse, error) {
541578
data, err := base64.StdEncoding.DecodeString(header)

0 commit comments

Comments
 (0)