Skip to content

Commit 091c241

Browse files
authored
Merge pull request #19 from rabbitprincess/feature/client
Feature/client
2 parents 7c6d0aa + 1a7ef88 commit 091c241

File tree

16 files changed

+293
-87
lines changed

16 files changed

+293
-87
lines changed

.vscode/launch.json

Lines changed: 0 additions & 19 deletions
This file was deleted.

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
ROOT_DIR := $(dir $(realpath $(lastword $(MAKEFILE_LIST))))
22

3-
run:
3+
build:
4+
go build -o $(ROOT_DIR)/bin/facilitator $(ROOT_DIR)/cmd/facilitator
5+
go build -o $(ROOT_DIR)/bin/client $(ROOT_DIR)/cmd/client
6+
7+
run-facilitator:
48
go run $(ROOT_DIR)/cmd/facilitator \
59
--config $(ROOT_DIR)/config.toml
610

README.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,35 @@
1414
| Sui | 🚧 Planned | |
1515
| Tron | 🚧 Planned | |
1616

17-
# How to run
17+
## How to run
18+
19+
### Build binary
20+
```bash
21+
make build
22+
```
23+
24+
### Run x402-facilitator using docker
1825
```bash
1926
docker compose up
2027
```
2128

29+
### Run x402-client
30+
```
31+
Usage:
32+
client [flags]
33+
34+
Flags:
35+
-A, --amount string Amount to send
36+
-F, --from string Sender address
37+
-h, --help help for x402-client
38+
-n, --network string Blockchain network to use (default "base-sepolia")
39+
-P, --privkey string Sender private key
40+
-s, --scheme string Scheme to use (default "evm")
41+
-T, --to string Recipient address
42+
-t, --token string token contract for sending (default "USDC")
43+
-u, --url string Base URL of the facilitator server (default "http://localhost:9090")
44+
```
45+
2246
## Api Specification
2347
After starting the service, open your browser to:
2448
```

api/client/client.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package client
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/base64"
7+
"encoding/json"
8+
"fmt"
9+
"io"
10+
"net/http"
11+
"net/url"
12+
13+
"github.com/rabbitprincess/x402-facilitator/types"
14+
)
15+
16+
type Client struct {
17+
BaseURL *url.URL
18+
HTTPClient *http.Client
19+
CreateAuthHeader func() (map[string]map[string]string, error)
20+
}
21+
22+
func NewClient(baseURL string) (*Client, error) {
23+
parsed, err := url.Parse(baseURL)
24+
if err != nil {
25+
return nil, fmt.Errorf("invalid base URL: %w", err)
26+
}
27+
return &Client{
28+
BaseURL: parsed,
29+
HTTPClient: http.DefaultClient,
30+
}, nil
31+
}
32+
33+
// Supported fetches the list of supported schemes.
34+
func (c *Client) Supported(ctx context.Context) ([]types.SupportedKind, error) {
35+
var result []types.SupportedKind
36+
if err := c.doRequest(ctx, http.MethodGet, "/supported", nil, "", &result); err != nil {
37+
return nil, err
38+
}
39+
return result, nil
40+
}
41+
42+
func (c *Client) Verify(ctx context.Context, payload *types.PaymentPayload, req *types.PaymentRequirements) (*types.PaymentVerifyResponse, error) {
43+
payloadJson, err := json.Marshal(payload)
44+
if err != nil {
45+
return nil, fmt.Errorf("marshal payment payload: %w", err)
46+
}
47+
header := base64.StdEncoding.EncodeToString(payloadJson)
48+
49+
body := types.PaymentVerifyRequest{
50+
X402Version: int(types.X402VersionV1),
51+
PaymentHeader: header,
52+
PaymentRequirements: *req,
53+
}
54+
55+
var resp types.PaymentVerifyResponse
56+
if err := c.doRequest(ctx, http.MethodPost, "/verify", body, "verify", &resp); err != nil {
57+
return nil, err
58+
}
59+
return &resp, nil
60+
}
61+
62+
// Settle sends a payment settlement request.
63+
func (c *Client) Settle(ctx context.Context, payload *types.PaymentPayload, req *types.PaymentRequirements) (*types.PaymentSettleResponse, error) {
64+
payloadJson, err := json.Marshal(payload)
65+
if err != nil {
66+
return nil, fmt.Errorf("marshal payment payload: %w", err)
67+
}
68+
header := base64.StdEncoding.EncodeToString(payloadJson)
69+
70+
body := types.PaymentSettleRequest{
71+
X402Version: int(types.X402VersionV1),
72+
PaymentHeader: header,
73+
PaymentRequirements: *req,
74+
}
75+
76+
var resp types.PaymentSettleResponse
77+
if err := c.doRequest(ctx, http.MethodPost, "/settle", body, "settle", &resp); err != nil {
78+
return nil, err
79+
}
80+
return &resp, nil
81+
}
82+
83+
func (c *Client) doRequest(ctx context.Context, method, path string, body any, authKey string, out any) error {
84+
// Build URL
85+
u := c.BaseURL.ResolveReference(&url.URL{Path: path})
86+
87+
// Prepare body
88+
var reader io.Reader
89+
if body != nil {
90+
payload, err := json.Marshal(body)
91+
if err != nil {
92+
return fmt.Errorf("marshal request body: %w", err)
93+
}
94+
reader = bytes.NewReader(payload)
95+
}
96+
97+
// Create HTTP request
98+
req, err := http.NewRequestWithContext(ctx, method, u.String(), reader)
99+
if err != nil {
100+
return err
101+
}
102+
if body != nil {
103+
req.Header.Set("Content-Type", "application/json")
104+
}
105+
106+
if authKey != "" && c.CreateAuthHeader != nil {
107+
hdrs, err := c.CreateAuthHeader()
108+
if err != nil {
109+
return fmt.Errorf("create auth headers: %w", err)
110+
}
111+
if section, ok := hdrs[authKey]; ok {
112+
for k, v := range section {
113+
req.Header.Set(k, v)
114+
}
115+
}
116+
}
117+
118+
// Execute
119+
resp, err := c.HTTPClient.Do(req)
120+
if err != nil {
121+
return err
122+
}
123+
defer resp.Body.Close()
124+
125+
if resp.StatusCode != http.StatusOK {
126+
data, _ := io.ReadAll(resp.Body)
127+
return fmt.Errorf("%s %s failed: status %d, body: %s", method, path, resp.StatusCode, string(data))
128+
}
129+
130+
if out != nil {
131+
if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
132+
return fmt.Errorf("decode %s response: %w", path, err)
133+
}
134+
}
135+
return nil
136+
}

api/server.go

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"encoding/json"
66
"net/http"
77

8-
"github.com/go-playground/validator/v10"
98
"github.com/labstack/echo/v4"
109
echomiddleware "github.com/labstack/echo/v4/middleware"
1110
_ "github.com/rabbitprincess/x402-facilitator/api/swagger"
@@ -19,9 +18,6 @@ import (
1918
// @title x402 Facilitator API
2019
// @version 1.0
2120
// @description API server for x402 payment facilitator
22-
// @host localhost:8080
23-
// @BasePath /
24-
// @schemes http
2521
type server struct {
2622
*echo.Echo
2723
facilitator facilitator.Facilitator
@@ -51,10 +47,6 @@ func NewServer(facilitator facilitator.Facilitator) *server {
5147
return s
5248
}
5349

54-
var (
55-
validate = validator.New(validator.WithRequiredStructEnabled())
56-
)
57-
5850
// Settle handles payment settlement requests
5951
// @Summary Settle payment
6052
// @Description Settle a payment using the facilitator
@@ -73,9 +65,7 @@ func (s *server) Settle(c echo.Context) error {
7365
if err := json.NewDecoder(c.Request().Body).Decode(requirement); err != nil {
7466
return echo.NewHTTPError(http.StatusBadRequest, "Received malformed settlement request")
7567
}
76-
if err := validate.Struct(requirement); err != nil {
77-
return echo.NewHTTPError(http.StatusBadRequest, "Received invalid settlement request")
78-
}
68+
7969
payment := &types.PaymentPayload{}
8070
paymentDecoded, err := base64.StdEncoding.DecodeString(requirement.PaymentHeader)
8171
if err != nil {
@@ -84,9 +74,7 @@ func (s *server) Settle(c echo.Context) error {
8474
if err := json.Unmarshal(paymentDecoded, payment); err != nil {
8575
return echo.NewHTTPError(http.StatusBadRequest, "Received malformed Payment header")
8676
}
87-
if err := validate.Struct(payment); err != nil {
88-
return echo.NewHTTPError(http.StatusBadRequest, "Received invalid Payment header")
89-
}
77+
9078
settle, err := s.facilitator.Settle(ctx, payment, &requirement.PaymentRequirements)
9179
if err != nil {
9280
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
@@ -113,9 +101,6 @@ func (s *server) Verify(c echo.Context) error {
113101
if err := json.NewDecoder(c.Request().Body).Decode(requirement); err != nil {
114102
return echo.NewHTTPError(http.StatusBadRequest, "Received malformed payment requirements")
115103
}
116-
if err := validate.Struct(requirement); err != nil {
117-
return echo.NewHTTPError(http.StatusBadRequest, "Received invalid payment requirements")
118-
}
119104

120105
// validate payment payload
121106
payment := &types.PaymentPayload{}
@@ -126,9 +111,6 @@ func (s *server) Verify(c echo.Context) error {
126111
if err := json.Unmarshal(paymentDecoded, payment); err != nil {
127112
return echo.NewHTTPError(http.StatusBadRequest, "Received malformed Payment header")
128113
}
129-
if err := validate.Struct(payment); err != nil {
130-
return echo.NewHTTPError(http.StatusBadRequest, "Received invalid Payment header")
131-
}
132114

133115
verified, err := s.facilitator.Verify(ctx, payment, &requirement.PaymentRequirements)
134116
if err != nil {

api/swagger/docs.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -294,9 +294,9 @@ const docTemplate = `{
294294
// SwaggerInfo holds exported Swagger Info so clients can modify it
295295
var SwaggerInfo = &swag.Spec{
296296
Version: "1.0",
297-
Host: "localhost:8080",
298-
BasePath: "/",
299-
Schemes: []string{"http"},
297+
Host: "",
298+
BasePath: "",
299+
Schemes: []string{},
300300
Title: "x402 Facilitator API",
301301
Description: "API server for x402 payment facilitator",
302302
InfoInstanceName: "swagger",

api/swagger/swagger.json

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
11
{
2-
"schemes": [
3-
"http"
4-
],
52
"swagger": "2.0",
63
"info": {
74
"description": "API server for x402 payment facilitator",
85
"title": "x402 Facilitator API",
96
"contact": {},
107
"version": "1.0"
118
},
12-
"host": "localhost:8080",
13-
"basePath": "/",
149
"paths": {
1510
"/settle": {
1611
"post": {

api/swagger/swagger.yaml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
basePath: /
21
definitions:
32
echo.HTTPError:
43
properties:
@@ -106,7 +105,6 @@ definitions:
106105
scheme:
107106
type: string
108107
type: object
109-
host: localhost:8080
110108
info:
111109
contact: {}
112110
description: API server for x402 payment facilitator
@@ -192,6 +190,4 @@ paths:
192190
summary: Verify payment
193191
tags:
194192
- payments
195-
schemes:
196-
- http
197193
swagger: "2.0"

0 commit comments

Comments
 (0)