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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ jobs:
with: { go-version: '1.26.5' }
- run: go test -short -race ./...
- run: go vet ./...
# go/e2e is a separate module, so ./... never reaches it. Vet it here or the
# e2e is a separate module, so ./... never reaches it. Vet it here or the
# harness rots unnoticed until someone runs it.
- run: go vet ./...
working-directory: go/e2e
- run: gofmt -l -d go | tee /tmp/fmt && test ! -s /tmp/fmt
working-directory: e2e
- run: gofmt -l -d . | tee /tmp/fmt && test ! -s /tmp/fmt
# A dependency imported directly but recorded as indirect still builds, so
# nothing else here would notice the manifest drifting from the imports.
- run: go mod tidy -diff
Expand Down
2 changes: 1 addition & 1 deletion .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ version: 2

builds:
- id: gnofacilitator
main: ./go/cmd/gnofacilitator
main: ./cmd/gnofacilitator
binary: gnofacilitator
# Static: the image is distroless, which has no libc to link against.
env: [CGO_ENABLED=0]
Expand Down
36 changes: 19 additions & 17 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
# gnoverse/x402 — the gno payment mechanism for x402, its facilitator, and the
# harnesses that prove a payment happens.
#
# Sources are laid out by language, go/ and js/, but both manifests live at the
# root where tooling expects them: `go test ./...` and `npm test` work without
# changing directory, and setup-go, dependabot and pkg.go.dev all find them.
# Sources are laid out by mechanism role — server/, facilitator/, client/ — and
# both manifests live at the root where tooling expects them: `go test ./...` and
# `npm test` work without changing directory, and setup-go, dependabot and
# pkg.go.dev all find them.
#
# The test layers are separate on purpose, cheapest first:
# test the library. No network, no chain, no npm.
# js-test the buyer mechanism, including the sign doc byte-equality check.
# test-e2e one real payment, through a real in-memory node and the JS buyer.
#
# go/e2e is its own Go module, so `test` cannot start a chain by accident and
# needs no build tag to stay out of the way.
# e2e is its own Go module, so `test` cannot start a chain by accident and needs
# no build tag to stay out of the way.

# A failed `go build -o` or `npm run build` must not leave a partial artifact that
# a later run mistakes for finished work.
Expand All @@ -22,12 +23,12 @@ SHELL := /bin/sh
GO ?= go
NPM ?= npm

E2E := go/e2e
E2E := e2e
BIN := bin
FACILITATOR := $(BIN)/gnofacilitator

# The JS mechanism emits under js/, not the root dist/ that goreleaser owns.
JS_DIST := js/dist
# The client mechanism emits under client/, not the root dist/ goreleaser owns.
JS_DIST := client/dist
JS_CLIENT := $(JS_DIST)/client.mjs

# A released binary is stamped by goreleaser; a local one says where it came from.
Expand All @@ -44,7 +45,7 @@ all: build ## Build everything (default)
# cache already decides what to recompile, and a manual source list goes stale.
build: ## Build the facilitator into bin/
mkdir -p $(BIN)
$(GO) build -ldflags "$(LDFLAGS)" -o $(FACILITATOR) ./go/cmd/gnofacilitator
$(GO) build -ldflags "$(LDFLAGS)" -o $(FACILITATOR) ./cmd/gnofacilitator

test: ## Run the library tests
$(GO) test ./...
Expand All @@ -53,9 +54,10 @@ test: ## Run the library tests
test-e2e: $(JS_CLIENT) ## Pay a gno seller end to end against a real in-memory node
cd $(E2E) && $(GO) test -count=1 ./...

# The buyers in js/ are stock @x402/* clients. They are opt-in so a Go-only
# checkout is never blocked on npm. buy.mjs imports the mechanism by package name,
# the way a stranger would, so the package must be built and not merely installed.
# The buyer the payment test drives is a stock @x402/* client. These targets are
# opt-in so a Go-only checkout is never blocked on npm. e2e/buyer.mjs imports the
# mechanism by package name, the way a stranger would, so the package must be
# built and not merely installed.
js: $(JS_CLIENT) ## Install and build the JS mechanism

js-test: $(JS_CLIENT) ## Run the JS mechanism's tests
Expand All @@ -66,9 +68,9 @@ node_modules: package.json package-lock.json
touch $@

# Both levels are listed because Make's $(wildcard) does not recurse, and the
# sources sit one directory deep: js/src/exact/ mirrors the scheme/role subpath
# sources sit one directory deep: client/src/exact/ mirrors the scheme/role subpath
# the package publishes.
TS_SOURCES := $(wildcard js/src/*.ts) $(wildcard js/src/exact/*.ts)
TS_SOURCES := $(wildcard client/src/*.ts) $(wildcard client/src/exact/*.ts)

# The emitted client, not the directory that holds it. A directory's mtime says
# nothing about whether the build finished — and .DELETE_ON_ERROR: cannot rescue
Expand All @@ -81,18 +83,18 @@ $(JS_CLIENT): node_modules tsconfig.json tsdown.config.ts $(TS_SOURCES)
lint: ## Vet both Go modules and check formatting
$(GO) vet ./...
cd $(E2E) && $(GO) vet ./...
gofmt -l go | tee /dev/stderr | (! read -r first)
gofmt -l . | tee /dev/stderr | (! read -r first)

fmt: ## Format the Go sources
gofmt -w go
gofmt -w .

# node_modules is a fetched dependency rather than an artifact, and refetching it
# is expensive, so it survives. Remove it by hand to start over.
clean: ## Remove build artifacts
rm -rf $(BIN) $(JS_DIST) dist

install: ## Install the facilitator into GOPATH/bin
$(GO) install -ldflags "$(LDFLAGS)" ./go/cmd/gnofacilitator
$(GO) install -ldflags "$(LDFLAGS)" ./cmd/gnofacilitator

help: ## List the targets
@grep -hE '^[a-zA-Z0-9_.$$()-]+:.*## ' $(MAKEFILE_LIST) \
Expand Down
30 changes: 14 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@ carries them there, not in the body:
}] }
```

Runnable, against a facilitator and a real chain → **[go/examples/weather](go/examples/weather)**.
Runnable, against a facilitator and a real chain → **[examples/weather](examples/weather)**.

## Pay for it

```js
import { ExactGnoScheme } from "./js/dist/client.mjs";
import { ExactGnoScheme } from "./client/dist/client.mjs";

const client = new x402Client().register("gno:*", new ExactGnoScheme(wallet));
const paid = wrapFetchWithPayment(fetch, client);
Expand All @@ -78,30 +78,28 @@ const res = await paid("https://api.example.com/weather");

Recognising the 402, selecting an entry, encoding `PAYMENT-SIGNATURE` and retrying are
`@x402/fetch`'s own code, unmodified. The payment is signed against the chain id the **offer** names
rather than the one the wallet's node reports — see [js/README.md](js/README.md) for what that does
and does not buy you.
rather than the one the wallet's node reports — see [client/README.md](client/README.md) for what
that does and does not buy you.

## What's in here

An x402 mechanism has three roles — **server** (the seller), **facilitator**, **client** (the buyer)
— and the ecosystem names them the same way in both languages: `@x402/evm` publishes
— and the ecosystem names them the same way in every language: `@x402/evm` publishes
`./exact/server`, `./exact/facilitator` and `./exact/client`, and its Go module has the matching
directories. This fills the same grid for gno; which language a role is written in is an
implementation detail of that role.
directories. The top level here is that grid. Which language a role is written in is an
implementation detail of the role, so no directory names one.

| Role | Path | What |
|------|------|------|
| server | `go/mechanisms/gno/exact/server/` | upstream's `SchemeNetworkServer`, implemented for gno |
| facilitator | `go/facilitator/` | verification, settlement, and the `/verify` `/settle` `/supported` service |
| client | `js/src/exact/client.ts` | upstream's `SchemeNetworkClient`, TypeScript |
| server | `server/exact/` | upstream's `SchemeNetworkServer`, implemented for gno |
| facilitator | `facilitator/` | verification, settlement, and the `/verify` `/settle` `/supported` service |
| client | `client/src/exact/client.ts` | upstream's `SchemeNetworkClient`, TypeScript |

Plus `go/cmd/gnofacilitator/` (the binary), `go/examples/weather/` (a priced endpoint you can run
and curl) and `go/e2e/` (one real payment through a real node — its own Go module).
Plus `cmd/gnofacilitator/` (the binary), `examples/weather/` (a priced endpoint you can run and
curl) and `e2e/` (one real payment through a real node — its own Go module).

Under that, sources split by language into `go/` and `js/` the way
[x402-foundation/x402](https://github.com/x402-foundation/x402) splits its own. Both manifests —
`go.mod` and `package.json` — live at the root, so `go test ./...`, `npm test` and every `make`
target run from here.
Both manifests — `go.mod` and `package.json` — live at the root, so `go test ./...`, `npm test` and
every `make` target run from here.

## Payment model

Expand Down
15 changes: 8 additions & 7 deletions js/README.md → client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
Pay a gno.land chain with x402. One line on a stock client — no fork, no patch, no upstream change.

Build it from the repository root with `make js`. There is no registry release, so a consumer works
against this repository — `buy.mjs` beside these sources is a working buyer that does exactly that.
against this repository — [`e2e/buyer.mjs`](../e2e/buyer.mjs), the buyer the payment test drives, is
a working example of exactly that.

```js
import { x402Client } from "@x402/core/client";
import { wrapFetchWithPayment } from "@x402/fetch";
import { ExactGnoScheme } from "./js/dist/client.mjs";
import { ExactGnoScheme } from "./client/dist/client.mjs";

const client = new x402Client();
client.register("gno:*", new ExactGnoScheme(wallet)); // ← the only gno-aware line
Expand All @@ -21,8 +22,8 @@ const res = await paid("https://api.example.com/weather");
required: a gno sequence is sequential, so only the chain knows the next one.

Inside this repository the same import resolves through the manifest's `exports` map instead of the
built path, which is what `buy.mjs` does — so the map is exercised by the payment test rather than
only on a future install.
built path, which is what `e2e/buyer.mjs` does — so the map is exercised by the payment test rather
than only on a future install.

## What it is

Expand Down Expand Up @@ -72,11 +73,11 @@ asset that is not `ugnot`, an amount that is not a positive integer, an absent `

## Development

Sources are here in `js/src`, laid out by the subpath they publish; the manifest and the emit are at
the repository root, so these run from there.
Sources are here in `client/src`, laid out by the subpath they publish, and the emit lands beside
them; the manifest is at the repository root, so these run from there.

```sh
npm run typecheck # TypeScript 7 (the native compiler)
npm test # vitest
npm run build # typecheck, then emit js/dist/
npm run build # typecheck, then emit client/dist/
```
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
gnoclient "github.com/gnolang/gno/gno.land/pkg/gnoclient"
rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/client"

"github.com/gnoverse/x402/go/facilitator"
"github.com/gnoverse/x402/facilitator"
)

// version is stamped at build time with -ldflags "-X main.version=…". A binary
Expand Down
10 changes: 7 additions & 3 deletions js/buy.mjs → e2e/buyer.mjs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
// A stock x402 client buys from a gno seller.
// A stock x402 client buys from a gno seller. This is the buyer the payment test
// drives: pay_test.go hands it to the txtar scenarios as $X402_BUYER. Running it
// by hand against a live seller is the same path, with a real node.
//
// The only gno-aware line is the register() call. Everything the protocol
// requires — issuing the request, recognising the 402, decoding the offer,
// selecting an accepts[] entry, encoding PAYMENT-SIGNATURE, retrying — is
// @x402/fetch's own code, unmodified, exactly as it does for Base or Solana.
//
// Prints a JSON report on stdout describing what happened, so a caller checks
// the outcome rather than trusting an exit code. Diagnostics go to stderr.
// Prints a JSON report on stdout describing what happened, so a caller checks the
// outcome rather than trusting an exit code. The scenarios assert on status,
// payer and transaction, so those field names are a contract. Diagnostics go to
// stderr.
import { GnoJSONRPCProvider, GnoWallet } from "@gnolang/gno-js-client";
import { decodePaymentResponseHeader, wrapFetchWithPayment, x402Client } from "@x402/fetch";

Expand Down
4 changes: 2 additions & 2 deletions go/e2e/go.mod → e2e/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
//
// A nested module is excluded from the parent's ./... , so `go test ./...` at the
// root never starts a chain. `make e2e` runs this.
module github.com/gnoverse/x402/go/e2e
module github.com/gnoverse/x402/e2e

go 1.26.5

// Never published, and nothing ever `go get`s it — so a relative replace is the
// honest way to reach the library under test rather than a version that does not
// exist yet.
replace github.com/gnoverse/x402 => ../..
replace github.com/gnoverse/x402 => ..

require (
github.com/gnolang/gno v0.0.0-20260624072255-1822034ddd17
Expand Down
File renamed without changes.
26 changes: 12 additions & 14 deletions go/e2e/pay_test.go → e2e/pay_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ import (
x402http "github.com/x402-foundation/x402/go/v2/http"
nethttpmw "github.com/x402-foundation/x402/go/v2/http/nethttp"

"github.com/gnoverse/x402/go/facilitator"
gnoexact "github.com/gnoverse/x402/go/mechanisms/gno/exact/server"
"github.com/gnoverse/x402/facilitator"
gnoexact "github.com/gnoverse/x402/server/exact"
)

// The harness's in-memory node serves this chain, so it is the CAIP-2 reference
Expand Down Expand Up @@ -58,29 +58,27 @@ func TestAStockClientPaysAGnoSeller(t *testing.T) {
testscript.Run(t, params)
}

// buyerScript locates the JS buyer, two levels up: this module is go/e2e and the
// JS tree is the repository's own js/, a sibling of go/.
// buyerScript is the buyer this harness drives, a sibling of this file: the whole
// claim under test is that a stock client pays, so the buyer is part of the
// harness rather than something borrowed from elsewhere in the tree.
//
// A missing buy.mjs FAILS rather than skips. It is committed, so its absence means
// the path is wrong — which is how a repository reshuffle turns this whole test
// into a silent pass. Only the built output is allowed to be absent, because
// building it needs npm and a Go-only checkout should not be blocked on that.
// A missing buyer.mjs FAILS rather than skips, because it is committed. Only the
// built mechanism is allowed to be absent, since building it needs npm and a
// Go-only checkout should not be blocked on that.
func buyerScript(t *testing.T) string {
t.Helper()

wd, err := os.Getwd()
require.NoError(t, err)

repo := filepath.Join(wd, "..", "..")

buyer := filepath.Join(repo, "js", "buy.mjs")
buyer := filepath.Join(wd, "buyer.mjs")
_, err = os.Stat(buyer)
require.NoError(t, err, "the JS buyer is committed, so this path is wrong")

// buy.mjs imports the mechanism by package name, so the package has to be
// buyer.mjs imports the mechanism by package name, so the package has to be
// installed and built, not merely present in the tree. The emit lands in
// js/dist, which the root package.json's exports map points at.
if _, err := os.Stat(filepath.Join(repo, "js", "dist", "client.mjs")); err != nil {
// client/dist, which the root package.json's exports map points at.
if _, err := os.Stat(filepath.Join(wd, "..", "client", "dist", "client.mjs")); err != nil {
t.Skipf("the JS mechanism is not built (%v); run `make js`", err)
}
return buyer
Expand Down
File renamed without changes.
9 changes: 5 additions & 4 deletions go/examples/weather/README.md → examples/weather/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,20 +88,21 @@ omitted.

## Paying it

There is no Go client mechanism yet, so a Go buyer cannot pay this. The JS buyer in `js/` can:
There is no Go client mechanism yet, so a Go buyer cannot pay this. The JS buyer the payment test
drives can, and it is the same buyer either way:

```sh
make js # install and build the client mechanism
X402_SELLER_URL=http://localhost:8080/weather X402_GNO_RPC=<RPC URL> node js/buy.mjs
X402_SELLER_URL=http://localhost:8080/weather X402_GNO_RPC=<RPC URL> node e2e/buyer.mjs
```

It registers `ExactGnoScheme` with a stock `@x402/fetch` client — that one `register("gno:*", …)`
call is the only gno-aware line — and signs with the well-known test1 mnemonic. **Fund that account
first**, and point `X402_GNO_RPC` at the same node the facilitator uses: the buyer reads its account
sequence from the chain, so this leg needs a real node rather than the unreachable one a 402 tolerates.

The paid loop itself is covered: `go/e2e` runs this exact configuration — the same middleware, the
same `accepts[]` entry, the same 250000ugnot price, this same `js/buy.mjs` — against an in-process
The paid loop itself is covered: `e2e` runs this exact configuration — the same middleware, the
same `accepts[]` entry, the same 250000ugnot price, this same `buyer.mjs` — against an in-process
node on every pull request, and asserts the seller's balance moved. What has not been run is the
command sequence on this page: three processes started by hand against a live chain, with a funded
account.
Expand Down
2 changes: 1 addition & 1 deletion go/examples/weather/main.go → examples/weather/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import (
x402http "github.com/x402-foundation/x402/go/v2/http"
nethttpmw "github.com/x402-foundation/x402/go/v2/http/nethttp"

gnoexact "github.com/gnoverse/x402/go/mechanisms/gno/exact/server"
gnoexact "github.com/gnoverse/x402/server/exact"
)

// The facilitator is queried while the middleware is being built, so this bounds
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func interopRequirements() PaymentRequirements {
func interopPayload(t *testing.T) SchemePayload {
t.Helper()
raw, err := os.ReadFile(filepath.Join("testdata", interopFixture))
require.NoError(t, err, "interop fixture missing — regenerate with go generate ./go/facilitator")
require.NoError(t, err, "interop fixture missing — regenerate with go generate ./facilitator")
return SchemePayload{Transaction: strings.TrimSpace(string(raw))}
}

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"description": "The gno payment mechanism for the x402 'exact' scheme. Register it on a stock x402 client and it signs payments for the gno chain an offer names.",
"license": "Apache-2.0",
"homepage": "https://gno.land/",
"//repository": "npm resolves the package page's relative links against this, and the published page is the repository root README rather than js/README.md.",
"//repository": "npm resolves the package page's relative links against this, and the published page is the repository root README rather than client/README.md.",
"repository": {
"type": "git",
"url": "git+https://github.com/gnoverse/x402.git"
Expand All @@ -18,13 +18,13 @@
"payments"
],
"files": [
"js/dist/"
"client/dist/"
],
"//exports": "The subpath is the ecosystem's own grid — @x402/evm publishes ./exact/client, ./exact/server and ./exact/facilitator, so a buyer that already imports one reaches for the same path here. gno's server half is Go, so ./exact/client is the only cell this package fills.",
"exports": {
"./exact/client": {
"types": "./js/dist/client.d.mts",
"default": "./js/dist/client.mjs"
"types": "./client/dist/client.d.mts",
"default": "./client/dist/client.mjs"
},
"./package.json": "./package.json"
},
Expand Down
Loading