Add canton-daml settlement backend (additive port)#5
Draft
anvztor wants to merge 12 commits into
Draft
Conversation
Net-new files only; upstream files untouched. - LICENSE-canton-port: Apache-2.0 scoped to net-new files (governance issue to follow for repo-level licensing); explicitly does not relicense inherited upstream files. - Makefile: top-level orchestration with canton-up/down/smoke/e2e targets (stubs reference scripts/ to be filled in subsequent stages). - go.work: lists goatx402-sdk-server-go (existing) only; canton modules added by Stage 2/3. - .github/workflows/canton.yml: Stage 0 stub that sanity-checks the baseline upstream module continues to build. - scripts/.gitkeep: placeholder for canton bootstrap scripts. - docs/canton/: port-plan v3 + preflight-notes captured before Stage 1. Stage 0 acceptance: go build ./goatx402-sdk-server-go/... and go vet exit 0 on a fresh clone with go.work added. Refs port-plan.html §6 Stage 0.
Net-new files only. Brings up a fresh canton container alongside any
existing dev canton on the host by namespacing the ports to 5031/5032/5038/5039
(legacy 5011 range remains free for other dev work).
Added:
goatx402-canton/daml/{daml.yaml,Payment.daml,Scripts/Topup.daml}
goatx402-canton/{bootstrap.canton,simple-topology.conf}
scripts/{canton-up,canton-down,canton-smoke,init-custodial-keys}.sh
scripts/init-custodial-keys.bats
Changes vs canton-payment source:
- canton-up.sh: self-contained docker-only mode (drops the SDK fallback;
docker-compose unification in Task #9 standardises on this); image pinned
by digest (sha256:98068c06…); ports default to the 5030 range.
- bootstrap.canton: marker string updated to "=== goatx402 canton localnet
ready ===" (canton-up.sh greps for this).
- canton-smoke.sh: DAML_DIR rewritten to goatx402-canton/daml; CANTON_PORT
default 5031.
Stage 1 acceptance:
$ bash scripts/canton-up.sh → container up, marker present, exit 0
$ docker ps --filter name=canton-localnet-goatx402 → Up
Stage 1 known gap: canton-smoke.sh still requires the `daml` CLI on host.
Task #9 (docker-compose unification) will wrap this in
digitalasset/daml-sdk:2.10.0 so no host install is needed.
Refs port-plan.html §6 Stage 1.
Pure additive port. Upstream module untouched.
Added (net-new top-level modules):
goatx402-receipt/ (was pkg/receipt/, kept standalone per §1 decision)
goatx402-facilitator/ (was facilitator/)
docs/canton-receipt.schema.json (referenced by receipt_test.go via
walk-up filepath lookup)
Module-rename inventory applied (§5):
github.com/goat-network/goat-canton-payment/pkg/receipt →
github.com/goatnetwork/goatx402-receipt
github.com/goat-network/goat-canton-payment/facilitator →
github.com/goatnetwork/goatx402-facilitator
Replace directive updated in facilitator/go.mod.
Both modules: go 1.25.0.
.proto files: go_package option rewritten (for future protoc regen).
.pb.go files: re-copied unmodified from source (sed-rewriting them
broke the embedded length-prefixed descriptor blobs,
causing protobuf init panic; do NOT sed .pb.go).
go.work: extended to include both new modules.
Stage 2 acceptance:
$ grep -r 'goat-canton-payment' . --exclude-dir=archive --exclude-dir=.git
(zero hits outside docs/archive/)
$ go vet ./goatx402-receipt/... ./goatx402-facilitator/... ./goatx402-sdk-server-go/...
(exit 0)
$ go test -short ./goatx402-receipt/... ./goatx402-facilitator/...
(all packages OK, including the proto init and schema validation paths)
.gitignore: added state/, logs/, .daml/, go.sum.local for the upcoming
e2e + Daml-build flows.
Refs port-plan.html §6 Stage 2 + §5 module-rename inventory.
Net-new modules: goatx402-merchant/ (was merchant/, demo paywall + offline receipt verifier) goatx402-canton-cli/ (was client-cli/, x402-canton CLI client) Module-rename inventory applied: github.com/goat-network/goat-canton-payment/merchant → github.com/goatnetwork/goatx402-merchant github.com/goat-network/goat-canton-payment/client-cli → github.com/goatnetwork/goatx402-canton-cli Replace directives updated to point at ../goatx402-receipt. go.work: extended to 4 canton modules + upstream goatx402-sdk-server-go. Stage 3 acceptance: $ go build ./goatx402-merchant/... ./goatx402-canton-cli/... (exit 0) $ go vet ./goatx402-merchant/... ./goatx402-canton-cli/... (exit 0) $ go test ./goatx402-merchant/... ./goatx402-canton-cli/... (all pass) Refs port-plan.html §6 Stage 3 + §5 module-rename inventory.
Net-new: goatx402-canton-demo/ — Vite + React/TS SPA demo client for the canton-daml scheme. Renamed package.json from @goat-canton-payment/client-web to goatx402-canton-demo. No source-code imports referenced the parent repo path; src/ moved verbatim. Build artifacts excluded (node_modules/, dist/); regenerated via pnpm install && pnpm run build at deployment time. Stage 4 acceptance deferred to Stage 5 e2e (Playwright cross-sdk-parity). Refs port-plan.html §6 Stage 4.
Net-new orchestration so the whole canton x402 stack starts with a
single `docker compose up -d` from a clean checkout. Replaces the
host-side daml-SDK + multi-script bring-up of canton-payment with a
fully container-driven workflow.
Services (top-level docker-compose.yml):
canton-localnet long-running canton participant + domain (image
digest-pinned per docs/canton/preflight-notes.md)
daml-bootstrap one-shot: build DAR, upload, allocate parties,
topup Alice with 100 USD-canton; writes
state/source-holding.json; built from
digitalasset/daml-sdk:2.10.0
facilitator Go service (multi-stage build → distroless/static),
:8080, talks to canton-localnet over the compose
network
merchant Go service, :7070, verifies receipts offline,
points at facilitator: via service name
canton-demo Vite SPA built with pnpm, served via nginx, :4173
e2e-cli opt-in (profile=e2e); runs the x402-canton CLI to
exercise the full flow inside the compose network
Dockerfiles:
goatx402-facilitator/Dockerfile
goatx402-merchant/Dockerfile
goatx402-canton-cli/Dockerfile (also used by e2e-cli service)
goatx402-canton-demo/Dockerfile (multi-stage: node→nginx)
goatx402-canton/Dockerfile.bootstrap (multi-stage: daml-sdk + tools)
Bootstrap script:
goatx402-canton/canton-bootstrap.sh — runs inside daml-bootstrap container;
idempotent DAR upload + party alloc + topup; writes
state/source-holding.json for facilitator + merchant to consume.
Ports remain namespaced to 5031/5032/5038/5039 (canton) + 8080 (facilitator)
+ 7070 (merchant) + 4173 (demo) so this stack coexists with any other
canton dev environment on the host.
.dockerignore: speeds up build context transfers (drops .git, docs,
node_modules, dist, bin, .daml, state, logs).
Stage 5 + Task #9 acceptance is the next step — `docker compose up -d`
end-to-end smoke against the new compose stack.
Refs port-plan.html §6 Stage 5 + Task #9.
Net-new docs (branch-relative; no upstream paths touched):
docs/canton/README.md
Top-level orientation: quick-start (docker compose up -d),
verification checks, module map, scheme rationale.
docs/canton/operator-handbook.md
Production hardening: real OIDC, HSM-backed signing keys,
persistent postgres, monitoring, backup. Ported from canton-payment
with paths rewritten for branch layout.
docs/canton/x402-canton-mapping.md
How the x402 envelope, scheme, accepts[], and proof relate to
Canton primitives (Holding, PaymentRequest, Pay choice, receipt).
Ported with branch-relative paths.
Refs port-plan.html §6 Stage 8.
Replaces the Stage 0 stub. Three jobs:
baseline — go build + go vet on goatx402-sdk-server-go (proves
upstream module still works under the new branch).
canton-modules — go build + go vet + go test -short on all 4 canton
modules; plus a stale-import grep gate (no stray
"goat-canton-payment" Go imports outside .pb.go and
the documented CanaryMessage string).
canton-stack — docker compose up the full stack against canton
localnet; verifies facilitator /healthz and that
merchant returns 402 without X-PAYMENT. Slow but
the only job that proves the docker compose path
actually works end-to-end.
Caches Go's build cache + module cache (~/.cache/go-build, ~/go/pkg/mod).
Uploads docker compose logs as workflow artifact on failure for triage.
Refs port-plan.html §6 Stage 7.
The daml-sdk:2.10.0 image (multi-GB) was hanging on a registry retry
loop, blocking the docker-compose up -d path. Replaced with a
host-build-once / commit-once strategy:
- goatx402-canton/dist/payment-0.0.1.dar (333 KB, committed)
Pre-built with daml SDK 2.10.0 on the host. Re-build with
`cd goatx402-canton/daml && daml build` when Payment.daml changes.
- bootstrap.canton (extended): now uploads the DAR + allocates
every party the stack needs (Issuer, Alice, facilitator, merchant)
in a single canton-console transaction at canton-localnet start.
- docker-compose.yml: removed the daml-bootstrap service entirely.
facilitator now depends_on canton-localnet directly.
- scripts/canton-init.sh + Makefile target `make canton-init`:
the one remaining one-shot step (mint initial Holding for Alice
via Daml Script Topup) runs on the host. Requires `daml` on PATH
or at ~/.daml/bin/daml. Idempotent; safe to re-run.
- .gitignore: explicit exception for goatx402-canton/dist/payment-*.dar
so the DAR ships with the repo even though other dist/ dirs stay
gitignored.
- Removed: goatx402-canton/Dockerfile.bootstrap +
goatx402-canton/canton-bootstrap.sh — superseded.
- Service Dockerfiles: facilitator / merchant / canton-cli now
generate a slim go.work inside the builder (only the modules they
actually use), avoiding "module listed in go.work file" errors
when sibling modules aren't COPY'd in.
Workflow becomes:
$ docker compose up -d # canton + facilitator + merchant + canton-demo
$ make canton-init # one-time data seed for Alice
$ # ready for e2e
Refs port-plan.html §6 Stage 5 + Task #9.
… need
End-to-end smoke now passes. After:
$ docker compose up -d canton-localnet
$ # (wait for "goatx402 canton localnet ready" marker)
$ make canton-init
$ docker compose up -d facilitator merchant canton-demo
All four containers come up healthy:
- canton-localnet: gRPC LedgerAPI :5031 (DAR uploaded, parties allocated)
- facilitator: HTTP :8080 → /healthz {status: ok}, /readyz {status: ready}
- merchant: HTTP :7070 → /resource returns HTTP 402 + canton-daml envelope
- canton-demo: HTTP :4173 → SPA serves 200
Changes (single fix-up commit so the bring-up sequence is correct):
scripts/gen-signing-key.go (new)
Tiny Go helper that emits base64(raw 64-byte ed25519 private key) and
base64(raw 32-byte ed25519 public key) — the formats LoadParticipantSigningKey
and loadPubKey expect. openssl PEM/PKCS#8 doesn't match either.
scripts/canton-init.sh
- Filter parties by party-id prefix instead of display_name (canton
parties.enable() doesn't set display_name).
- Fix Daml-Script field name: TopupArgs has `payer`, not `owner`.
- Use jq --arg (string) for contract_id, not --argjson (was failing
on raw hex without quotes).
- Generate participant signing key via gen-signing-key.go (correct format).
- Write state/facilitator.env + state/merchant.env so docker-compose
can `env_file:` load TRUSTED_ISSUER_MAP, CURRENCY_ALLOW_LIST,
MERCHANT_PARTY_ID, MERCHANT_TRUSTED_ISSUER, MERCHANT_RESOURCE,
MERCHANT_AMOUNT, MERCHANT_CURRENCY.
docker-compose.yml
- facilitator: env_file: ./state/facilitator.env + new env var
DEV_SOURCE_HOLDING_FIXTURE_PATH=/state/source-holding-map.json so
the dev source-holding endpoint can resolve Alice's mint.
- merchant: env_file: ./state/merchant.env (replaces the file-path
hacks like TRUSTED_ISSUER_FILE / MERCHANT_FILE which the merchant
config doesn't actually support).
Stage 5 + Task #9 acceptance gates: green.
Refs port-plan.html §6 Stage 5.
- facilitator: distroless/static → distroless/base (CGO sqlite needs glibc)
- facilitator: explicit GOOS=linux on build
- merchant : CGO_ENABLED=0 + GOOS=linux (static binary; works on distroless/static)
- canton-cli : CGO_ENABLED=0 + GOOS=linux (same)
- canton-demo: node:20 → node:22; pin pnpm 9.15.0 (pnpm 11 hit
ERR_UNKNOWN_BUILTIN_MODULE on node:20); drop --frozen-lockfile
(the package name was renamed from @goat-canton-payment/client-web)
Without these fixes the containers built but exited with
"exec /usr/local/bin/X: no such file or directory" (distroless/static
missing glibc loader for CGO binary) or never built at all (node/pnpm
incompat).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR description — canton/initial-port → main
Branch:
anvztor/x402:canton/initial-port→GOATNetwork/x402:mainSummary
Adds a complete
canton-damlsettlement backend to the x402 referenceimplementation: Daml templates, a self-hosted facilitator (Go), a demo
merchant (Go), a CLI client (Go), and a browser SPA — all reachable via
a single
docker compose up -d.The contribution is 100 % additive. Every upstream file
(
goatx402-sdk-server-go,goatx402-sdk,goatx402-sdk-server-ts,goatx402-demo,goatx402-contract,API.md,DEVELOPER_FAST.md,ONBOARDING.md,README.md) is unchanged.What's new
Upstream EVM stack files: not touched.
Design decisions (recorded in
docs/canton/port-plan.html§1)github.com/goatnetwork/<pkgname>to match upstream'sexisting
github.com/goatnetwork/goatx402-sdk-servershape (no/x402/path segment).
goatx402-receipt/), not foldedinto facilitator. Reason: facilitator's
internal/api/orders.go:446currently has canonical helpers that
pkg/receiptdoesn't export;folding would carry that duplication forward.
on request.
LICENSE-canton-port. Does not relicense any inherited upstream file.A separate governance issue is being opened proposing a repo-level
LICENSE for maintainers to decide.
goatx402-sdk-server-gois aclient SDK to GoatX402 Core (EVM). Its request/response bodies don't
match the canton facilitator's (canton uses
{merchant, payer, amount, currency, trustedIssuer, …}, Core uses{dapp_order_id, chain_id, token_symbol, …}), so just swappingAuthSchemewouldn't enablecanton routing. A Canton-aware SDK client is out of scope for this
port — happy to follow up if desired.
Testing
Quick start
E2E
Acceptance gates (G1-G4 from
port-plan.html)What's intentionally not in this PR
API.mdadditions registering thecanton-damlscheme — proposeas a follow-up doc PR once this lands.
goatx402-sdk,goatx402-sdk-server-ts) — samepattern as the Go SDK refactor (also out of scope here).
EVM callback contract.
a sibling, not a replacement.
Things to look at first
If you only have 10 minutes, read in this order:
docs/canton/port-plan.html§1-§4 — what's added and why.docs/canton/x402-canton-mapping.md— how the x402 envelope maps toCanton primitives.
goatx402-canton/daml/Payment.daml(74 lines) — the entire Daml model.goatx402-facilitator/internal/api/router.go(~50 lines) — the HTTP surface.docker-compose.yml— how the pieces fit at runtime.Open questions
github.com/goatnetwork/<pkg>to match
goatx402-sdk-server-go's existing shape. Comfortable, orprefer
github.com/goatnetwork/x402/<pkg>?goatx402-sdk-server-go) orin a separate
GOATNetwork/x402-cantonrepo with cross-repo CI?canton-damlregistry entryto
API.md's scheme list?Companion change in
GOATNetwork/giftcardA parallel branch
feat/canton-paymentaddsCantonX402SDKingift-api/internal/x402_canton/so giftcard can use the canton facilitatoras an alternative settlement backend. That branch is local-only — the
giftcard repo has forks disabled, so it cannot be pushed via the standard
GitHub fork-PR flow. Available on request as a patch series or via direct
push by a maintainer.
Generated from
docs/canton/port-plan.html. Internal review by Claude × Codexcross-review (3 rounds; Round 3 verdict: ship with minor clarifications,
all applied). See
docs/canton/port-plan.htmlfooter for review trail.