Skip to content

Commit 9312937

Browse files
authored
Merge pull request #22 from nacorid/main
feat: Add before/after verify/settle hooks
1 parent 8ad21be commit 9312937

File tree

3 files changed

+206
-2
lines changed

3 files changed

+206
-2
lines changed

http/facilitator.go

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ import (
2525
// does not serialize calls to the provider.
2626
type AuthorizationProvider func(*http.Request) string
2727

28+
// OnBeforeFunc is a function that returns an error to abort an operation.
29+
type OnBeforeFunc func(context.Context, x402.PaymentPayload, x402.PaymentRequirement) error
30+
31+
// OnAfterVerifyFunc is a function that is called after a Verify operation completes
32+
type OnAfterVerifyFunc func(context.Context, x402.PaymentPayload, x402.PaymentRequirement, *facilitator.VerifyResponse, error)
33+
34+
// OnAfterSettleFunc is a function that is called after a Settle operation completes
35+
type OnAfterSettleFunc func(context.Context, x402.PaymentPayload, x402.PaymentRequirement, *x402.SettlementResponse, error)
36+
2837
// FacilitatorClient is a client for communicating with x402 facilitator services.
2938
type FacilitatorClient struct {
3039
BaseURL string
@@ -41,6 +50,20 @@ type FacilitatorClient struct {
4150
// This is useful for dynamic tokens that may need to be refreshed.
4251
// If set, this takes precedence over the static Authorization field.
4352
AuthorizationProvider AuthorizationProvider
53+
54+
// OnBeforeVerify is called before the Verify operation starts.
55+
// If it returns an error, the operation is aborted immediately.
56+
OnBeforeVerify OnBeforeFunc
57+
58+
// OnAfterVerify is called after the Verify operation completes (success or failure).
59+
OnAfterVerify OnAfterVerifyFunc
60+
61+
// OnBeforeSettle is called before the Settle operation starts.
62+
// If it returns an error, the operation is aborted immediately.
63+
OnBeforeSettle OnBeforeFunc
64+
65+
// OnAfterSettle is called after the Settle operation completes (success or failure).
66+
OnAfterSettle OnAfterSettleFunc
4467
}
4568

4669
// setAuthorizationHeader sets the Authorization header on the request if configured.
@@ -67,6 +90,12 @@ type FacilitatorRequest struct {
6790

6891
// Verify verifies a payment authorization without executing the transaction.
6992
func (c *FacilitatorClient) Verify(ctx context.Context, payment x402.PaymentPayload, requirement x402.PaymentRequirement) (*facilitator.VerifyResponse, error) {
93+
if c.OnBeforeVerify != nil {
94+
if err := c.OnBeforeVerify(ctx, payment, requirement); err != nil {
95+
return nil, err
96+
}
97+
}
98+
7099
// Create request payload
71100
req := FacilitatorRequest{
72101
X402Version: 1,
@@ -98,7 +127,7 @@ func (c *FacilitatorClient) Verify(ctx context.Context, payment x402.PaymentPayl
98127
Multiplier: 2.0,
99128
}
100129

101-
return retry.WithRetry(ctx, config, isFacilitatorUnavailableError, func() (*facilitator.VerifyResponse, error) {
130+
resp, resultErr := retry.WithRetry(ctx, config, isFacilitatorUnavailableError, func() (*facilitator.VerifyResponse, error) {
102131
// Use provided context, apply timeout only if not already set
103132
reqCtx := ctx
104133
if _, hasDeadline := ctx.Deadline(); !hasDeadline && c.Timeouts.VerifyTimeout > 0 {
@@ -153,6 +182,12 @@ func (c *FacilitatorClient) Verify(ctx context.Context, payment x402.PaymentPayl
153182

154183
return &verifyResp, nil
155184
})
185+
186+
if c.OnAfterVerify != nil {
187+
c.OnAfterVerify(ctx, payment, requirement, resp, resultErr)
188+
}
189+
190+
return resp, resultErr
156191
}
157192

158193
// Supported queries the facilitator for supported payment types.
@@ -193,6 +228,12 @@ func (c *FacilitatorClient) Supported(ctx context.Context) (*facilitator.Support
193228

194229
// Settle executes a verified payment on the blockchain.
195230
func (c *FacilitatorClient) Settle(ctx context.Context, payment x402.PaymentPayload, requirement x402.PaymentRequirement) (*x402.SettlementResponse, error) {
231+
if c.OnBeforeSettle != nil {
232+
if err := c.OnBeforeSettle(ctx, payment, requirement); err != nil {
233+
return nil, err
234+
}
235+
}
236+
196237
// Create request payload
197238
req := FacilitatorRequest{
198239
X402Version: 1,
@@ -224,7 +265,7 @@ func (c *FacilitatorClient) Settle(ctx context.Context, payment x402.PaymentPayl
224265
Multiplier: 2.0,
225266
}
226267

227-
return retry.WithRetry(ctx, config, isFacilitatorUnavailableError, func() (*x402.SettlementResponse, error) {
268+
resp, resultErr := retry.WithRetry(ctx, config, isFacilitatorUnavailableError, func() (*x402.SettlementResponse, error) {
228269
// Use provided context, apply timeout only if not already set
229270
reqCtx := ctx
230271
if _, hasDeadline := ctx.Deadline(); !hasDeadline && c.Timeouts.SettleTimeout > 0 {
@@ -271,6 +312,12 @@ func (c *FacilitatorClient) Settle(ctx context.Context, payment x402.PaymentPayl
271312

272313
return &settlementResp, nil
273314
})
315+
316+
if c.OnAfterSettle != nil {
317+
c.OnAfterSettle(ctx, payment, requirement, resp, resultErr)
318+
}
319+
320+
return resp, resultErr
274321
}
275322

276323
// EnrichRequirements fetches supported payment types from the facilitator and

http/facilitator_test.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,80 @@ func TestFacilitatorClient_Verify_WithoutAuthorization(t *testing.T) {
234234
}
235235
}
236236

237+
func TestFacilitatorClient_Verify_Hooks(t *testing.T) {
238+
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
239+
response := facilitator.VerifyResponse{IsValid: true}
240+
w.Header().Set("Content-Type", "application/json")
241+
if err := json.NewEncoder(w).Encode(response); err != nil {
242+
t.Errorf("Failed to encode response: %v", err)
243+
}
244+
}))
245+
defer mockServer.Close()
246+
247+
var beforeCalled, afterCalled bool
248+
var capturedPayload x402.PaymentPayload
249+
250+
client := &FacilitatorClient{
251+
BaseURL: mockServer.URL,
252+
Client: &http.Client{},
253+
Timeouts: x402.DefaultTimeouts,
254+
OnBeforeVerify: func(ctx context.Context, p x402.PaymentPayload, r x402.PaymentRequirement) error {
255+
beforeCalled = true
256+
capturedPayload = p
257+
return nil
258+
},
259+
OnAfterVerify: func(ctx context.Context, p x402.PaymentPayload, r x402.PaymentRequirement, resp *facilitator.VerifyResponse, err error) {
260+
afterCalled = true
261+
if err != nil {
262+
t.Errorf("OnAfterVerify received unexpected error: %v", err)
263+
}
264+
if resp == nil || !resp.IsValid {
265+
t.Error("OnAfterVerify did not receive valid response")
266+
}
267+
},
268+
}
269+
270+
payload := x402.PaymentPayload{X402Version: 1, Scheme: "exact"}
271+
requirement := x402.PaymentRequirement{Scheme: "exact"}
272+
273+
_, err := client.Verify(context.Background(), payload, requirement)
274+
if err != nil {
275+
t.Fatalf("Verify failed: %v", err)
276+
}
277+
278+
if !beforeCalled {
279+
t.Error("OnBeforeVerify was not called")
280+
}
281+
if !afterCalled {
282+
t.Error("OnAfterVerify was not called")
283+
}
284+
if capturedPayload.Scheme != "exact" {
285+
t.Error("OnBeforeVerify did not receive correct payload")
286+
}
287+
}
288+
289+
func TestFacilitatorClient_Verify_OnBeforeAbort(t *testing.T) {
290+
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
291+
t.Error("Server was reached despite OnBeforeVerify error")
292+
}))
293+
defer mockServer.Close()
294+
295+
expectedErr := x402.ErrVerificationFailed
296+
297+
client := &FacilitatorClient{
298+
BaseURL: mockServer.URL,
299+
Client: &http.Client{},
300+
OnBeforeVerify: func(ctx context.Context, pp x402.PaymentPayload, pr x402.PaymentRequirement) error {
301+
return expectedErr
302+
},
303+
}
304+
305+
_, err := client.Verify(context.Background(), x402.PaymentPayload{}, x402.PaymentRequirement{})
306+
if err != expectedErr {
307+
t.Errorf("Expected error %v, got %v", expectedErr, err)
308+
}
309+
}
310+
237311
func TestFacilitatorClient_Settle_WithStaticAuthorization(t *testing.T) {
238312
expectedAuth := "Bearer settle-api-key"
239313

@@ -396,3 +470,66 @@ func TestFacilitatorClient_Settle(t *testing.T) {
396470
t.Error("Expected transaction hash")
397471
}
398472
}
473+
474+
func TestFacilitatorClient_Settle_Hooks(t *testing.T) {
475+
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
476+
response := x402.SettlementResponse{Success: true, Transaction: "0x123"}
477+
w.Header().Set("Content-Type", "application/json")
478+
if err := json.NewEncoder(w).Encode(response); err != nil {
479+
t.Errorf("Failed to encode response: %v", err)
480+
}
481+
}))
482+
defer mockServer.Close()
483+
484+
var beforeCalled, afterCalled bool
485+
486+
client := &FacilitatorClient{
487+
BaseURL: mockServer.URL,
488+
Client: &http.Client{},
489+
Timeouts: x402.DefaultTimeouts,
490+
OnBeforeSettle: func(ctx context.Context, p x402.PaymentPayload, r x402.PaymentRequirement) error {
491+
beforeCalled = true
492+
return nil
493+
},
494+
OnAfterSettle: func(ctx context.Context, p x402.PaymentPayload, r x402.PaymentRequirement, resp *x402.SettlementResponse, err error) {
495+
afterCalled = true
496+
if resp == nil || resp.Transaction != "0x123" {
497+
t.Error("OnAfterSettle did not receive correct response")
498+
}
499+
},
500+
}
501+
502+
_, err := client.Settle(context.Background(), x402.PaymentPayload{}, x402.PaymentRequirement{})
503+
if err != nil {
504+
t.Fatalf("Settle failed: %v", err)
505+
}
506+
507+
if !beforeCalled {
508+
t.Error("OnBeforeSettle was not called")
509+
}
510+
if !afterCalled {
511+
t.Error("OnAfterSettle was not called")
512+
}
513+
}
514+
515+
func TestFacilitatorClient_Settle_OnBeforeAbort(t *testing.T) {
516+
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
517+
t.Error("Server was reached despite OnBeforeSettle error")
518+
}))
519+
defer mockServer.Close()
520+
521+
expectedErr := x402.ErrSettlementFailed
522+
523+
client := &FacilitatorClient{
524+
BaseURL: mockServer.URL,
525+
Client: &http.Client{},
526+
OnBeforeSettle: func(ctx context.Context, p x402.PaymentPayload, r x402.PaymentRequirement) error {
527+
return expectedErr
528+
},
529+
}
530+
531+
_, err := client.Settle(context.Background(), x402.PaymentPayload{}, x402.PaymentRequirement{})
532+
if err != expectedErr {
533+
t.Errorf("Expected error %v, got %v", expectedErr, err)
534+
}
535+
}

http/middleware.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,24 @@ type Config struct {
3232
// If set, this takes precedence over FacilitatorAuthorization.
3333
FacilitatorAuthorizationProvider AuthorizationProvider
3434

35+
// Facilitator hooks for custom logic before/after verify and settle operations
36+
FacilitatorOnBeforeVerify OnBeforeFunc
37+
FacilitatorOnAfterVerify OnAfterVerifyFunc
38+
FacilitatorOnBeforeSettle OnBeforeFunc
39+
FacilitatorOnAfterSettle OnAfterSettleFunc
40+
3541
// FallbackFacilitatorAuthorization is a static Authorization header value for the fallback facilitator.
3642
FallbackFacilitatorAuthorization string
3743

3844
// FallbackFacilitatorAuthorizationProvider is a function that returns an Authorization header value
3945
// for the fallback facilitator. If set, this takes precedence over FallbackFacilitatorAuthorization.
4046
FallbackFacilitatorAuthorizationProvider AuthorizationProvider
47+
48+
// FallbackFacilitator hooks for custom logic before/after verify and settle operations
49+
FallbackFacilitatorOnBeforeVerify OnBeforeFunc
50+
FallbackFacilitatorOnAfterVerify OnAfterVerifyFunc
51+
FallbackFacilitatorOnBeforeSettle OnBeforeFunc
52+
FallbackFacilitatorOnAfterSettle OnAfterSettleFunc
4153
}
4254

4355
// contextKey is a custom type for context keys to avoid collisions.
@@ -58,6 +70,10 @@ func NewX402Middleware(config *Config) func(http.Handler) http.Handler {
5870
Timeouts: x402.DefaultTimeouts,
5971
Authorization: config.FacilitatorAuthorization,
6072
AuthorizationProvider: config.FacilitatorAuthorizationProvider,
73+
OnBeforeVerify: config.FacilitatorOnBeforeVerify,
74+
OnAfterVerify: config.FacilitatorOnAfterVerify,
75+
OnBeforeSettle: config.FacilitatorOnBeforeSettle,
76+
OnAfterSettle: config.FacilitatorOnAfterSettle,
6177
}
6278

6379
// Create fallback facilitator client if configured
@@ -69,6 +85,10 @@ func NewX402Middleware(config *Config) func(http.Handler) http.Handler {
6985
Timeouts: x402.DefaultTimeouts,
7086
Authorization: config.FallbackFacilitatorAuthorization,
7187
AuthorizationProvider: config.FallbackFacilitatorAuthorizationProvider,
88+
OnBeforeVerify: config.FallbackFacilitatorOnBeforeVerify,
89+
OnAfterVerify: config.FallbackFacilitatorOnAfterVerify,
90+
OnBeforeSettle: config.FallbackFacilitatorOnBeforeSettle,
91+
OnAfterSettle: config.FallbackFacilitatorOnAfterSettle,
7292
}
7393
}
7494

0 commit comments

Comments
 (0)