Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 0 additions & 19 deletions .vscode/launch.json

This file was deleted.

6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
ROOT_DIR := $(dir $(realpath $(lastword $(MAKEFILE_LIST))))

run:
build:
go build -o $(ROOT_DIR)/bin/facilitator $(ROOT_DIR)/cmd/facilitator
go build -o $(ROOT_DIR)/bin/client $(ROOT_DIR)/cmd/client

run-facilitator:
go run $(ROOT_DIR)/cmd/facilitator \
--config $(ROOT_DIR)/config.toml

Expand Down
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,35 @@
| Sui | 🚧 Planned | |
| Tron | 🚧 Planned | |

# How to run
## How to run

### Build binary
```bash
make build
```

### Run x402-facilitator using docker
```bash
docker compose up
```

### Run x402-client
```
Usage:
client [flags]

Flags:
-A, --amount string Amount to send
-F, --from string Sender address
-h, --help help for x402-client
-n, --network string Blockchain network to use (default "base-sepolia")
-P, --privkey string Sender private key
-s, --scheme string Scheme to use (default "evm")
-T, --to string Recipient address
-t, --token string token contract for sending (default "USDC")
-u, --url string Base URL of the facilitator server (default "http://localhost:9090")
```

## Api Specification
After starting the service, open your browser to:
```
Expand Down
136 changes: 136 additions & 0 deletions api/client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package client

import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"

"github.com/rabbitprincess/x402-facilitator/types"
)

type Client struct {
BaseURL *url.URL
HTTPClient *http.Client
CreateAuthHeader func() (map[string]map[string]string, error)
}

func NewClient(baseURL string) (*Client, error) {
parsed, err := url.Parse(baseURL)
if err != nil {
return nil, fmt.Errorf("invalid base URL: %w", err)
}
return &Client{
BaseURL: parsed,
HTTPClient: http.DefaultClient,
}, nil
}

// Supported fetches the list of supported schemes.
func (c *Client) Supported(ctx context.Context) ([]types.SupportedKind, error) {
var result []types.SupportedKind
if err := c.doRequest(ctx, http.MethodGet, "/supported", nil, "", &result); err != nil {
return nil, err
}
return result, nil
}

func (c *Client) Verify(ctx context.Context, payload *types.PaymentPayload, req *types.PaymentRequirements) (*types.PaymentVerifyResponse, error) {
payloadJson, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("marshal payment payload: %w", err)
}
header := base64.StdEncoding.EncodeToString(payloadJson)

body := types.PaymentVerifyRequest{
X402Version: int(types.X402VersionV1),
PaymentHeader: header,
PaymentRequirements: *req,
}

var resp types.PaymentVerifyResponse
if err := c.doRequest(ctx, http.MethodPost, "/verify", body, "verify", &resp); err != nil {
return nil, err
}
return &resp, nil
}

// Settle sends a payment settlement request.
func (c *Client) Settle(ctx context.Context, payload *types.PaymentPayload, req *types.PaymentRequirements) (*types.PaymentSettleResponse, error) {
payloadJson, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("marshal payment payload: %w", err)
}
header := base64.StdEncoding.EncodeToString(payloadJson)

body := types.PaymentSettleRequest{
X402Version: int(types.X402VersionV1),
PaymentHeader: header,
PaymentRequirements: *req,
}

var resp types.PaymentSettleResponse
if err := c.doRequest(ctx, http.MethodPost, "/settle", body, "settle", &resp); err != nil {
return nil, err
}
return &resp, nil
}

func (c *Client) doRequest(ctx context.Context, method, path string, body any, authKey string, out any) error {
// Build URL
u := c.BaseURL.ResolveReference(&url.URL{Path: path})

// Prepare body
var reader io.Reader
if body != nil {
payload, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("marshal request body: %w", err)
}
reader = bytes.NewReader(payload)
}

// Create HTTP request
req, err := http.NewRequestWithContext(ctx, method, u.String(), reader)
if err != nil {
return err
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}

if authKey != "" && c.CreateAuthHeader != nil {
hdrs, err := c.CreateAuthHeader()
if err != nil {
return fmt.Errorf("create auth headers: %w", err)
}
if section, ok := hdrs[authKey]; ok {
for k, v := range section {
req.Header.Set(k, v)
}
}
}

// Execute
resp, err := c.HTTPClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
data, _ := io.ReadAll(resp.Body)
return fmt.Errorf("%s %s failed: status %d, body: %s", method, path, resp.StatusCode, string(data))
}

if out != nil {
if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
return fmt.Errorf("decode %s response: %w", path, err)
}
}
return nil
}
22 changes: 2 additions & 20 deletions api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"encoding/json"
"net/http"

"github.com/go-playground/validator/v10"
"github.com/labstack/echo/v4"
echomiddleware "github.com/labstack/echo/v4/middleware"
_ "github.com/rabbitprincess/x402-facilitator/api/swagger"
Expand All @@ -19,9 +18,6 @@ import (
// @title x402 Facilitator API
// @version 1.0
// @description API server for x402 payment facilitator
// @host localhost:8080
// @BasePath /
// @schemes http
type server struct {
*echo.Echo
facilitator facilitator.Facilitator
Expand Down Expand Up @@ -51,10 +47,6 @@ func NewServer(facilitator facilitator.Facilitator) *server {
return s
}

var (
validate = validator.New(validator.WithRequiredStructEnabled())
)

// Settle handles payment settlement requests
// @Summary Settle payment
// @Description Settle a payment using the facilitator
Expand All @@ -73,9 +65,7 @@ func (s *server) Settle(c echo.Context) error {
if err := json.NewDecoder(c.Request().Body).Decode(requirement); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Received malformed settlement request")
}
if err := validate.Struct(requirement); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Received invalid settlement request")
}

payment := &types.PaymentPayload{}
paymentDecoded, err := base64.StdEncoding.DecodeString(requirement.PaymentHeader)
if err != nil {
Expand All @@ -84,9 +74,7 @@ func (s *server) Settle(c echo.Context) error {
if err := json.Unmarshal(paymentDecoded, payment); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Received malformed Payment header")
}
if err := validate.Struct(payment); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Received invalid Payment header")
}

settle, err := s.facilitator.Settle(ctx, payment, &requirement.PaymentRequirements)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
Expand All @@ -113,9 +101,6 @@ func (s *server) Verify(c echo.Context) error {
if err := json.NewDecoder(c.Request().Body).Decode(requirement); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Received malformed payment requirements")
}
if err := validate.Struct(requirement); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Received invalid payment requirements")
}

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

verified, err := s.facilitator.Verify(ctx, payment, &requirement.PaymentRequirements)
if err != nil {
Expand Down
6 changes: 3 additions & 3 deletions api/swagger/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,9 +294,9 @@ const docTemplate = `{
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{
Version: "1.0",
Host: "localhost:8080",
BasePath: "/",
Schemes: []string{"http"},
Host: "",
BasePath: "",
Schemes: []string{},
Title: "x402 Facilitator API",
Description: "API server for x402 payment facilitator",
InfoInstanceName: "swagger",
Expand Down
5 changes: 0 additions & 5 deletions api/swagger/swagger.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
{
"schemes": [
"http"
],
"swagger": "2.0",
"info": {
"description": "API server for x402 payment facilitator",
"title": "x402 Facilitator API",
"contact": {},
"version": "1.0"
},
"host": "localhost:8080",
"basePath": "/",
"paths": {
"/settle": {
"post": {
Expand Down
4 changes: 0 additions & 4 deletions api/swagger/swagger.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
basePath: /
definitions:
echo.HTTPError:
properties:
Expand Down Expand Up @@ -106,7 +105,6 @@ definitions:
scheme:
type: string
type: object
host: localhost:8080
info:
contact: {}
description: API server for x402 payment facilitator
Expand Down Expand Up @@ -192,6 +190,4 @@ paths:
summary: Verify payment
tags:
- payments
schemes:
- http
swagger: "2.0"
Loading
Loading