mirror of
https://github.com/hanzoai/commerce.git
synced 2026-10-30 15:08:42 +00:00
An agent or a person signs one EIP-3009 authorization against its Circle Gateway balance, made out to the treasury, and is credited the moment Gateway locks it. No human, no subscription, cards untouched. GET /v1/billing/crypto/gateway terms + a nonce issued to the caller POST /v1/billing/crypto/gateway the signed payment GET /v1/billing/crypto/gateway/:id pending, accepted, settled, failed GET /v1/commerce/treasury superadmin: Gateway position payment/providers/circle replaces the Payments API v1 provider with a permissionless Gateway client and the GatewayWalletBatched EIP-712 encoding, proven against eth-account and Circle's testnet verifier. billing/gateway checks a payment before Gateway sees it (treasury, live network, whole cents in $1..$1,000, window, low-s signature from `from`, nonce bound to the caller), settles it through /v1/x402/settle, credits the host ledger keyed on the remittance, and reconciles by polling /v1/x402/transfers: Gateway's webhooks report no batched payment. Armed by CIRCLE_GATEWAY_TREASURY with CIRCLE_ENVIRONMENT; unset is inert.
197 lines
7.4 KiB
Go
197 lines
7.4 KiB
Go
// Copyright (c) 2014-present Hanzo AI, Inc.
|
|
// Licensed under MIT OR Apache-2.0. See LICENSE-MIT and LICENSE-APACHE.
|
|
|
|
package circle
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
)
|
|
|
|
// The batched-payment half of Gateway (gateway-nanopayments.yaml): a payer
|
|
// signs an EIP-3009 authorization against their Gateway balance, Gateway
|
|
// verifies it, locks the amount, queues it for batched onchain settlement and
|
|
// credits the payee's Gateway balance when the batch lands.
|
|
//
|
|
// Settle is used rather than POST /v1/batch/submit, which carries the same
|
|
// authorization to the same queue. Settle takes the payee's REQUIREMENTS
|
|
// beside the payment, so Gateway itself refuses a payment whose `to` is not
|
|
// the payee (address_mismatch) or whose value is not the amount
|
|
// (amount_mismatch); it answers every refusal with a typed errorReason rather
|
|
// than a bare 400; and its transaction id is the one GET /v1/x402/transfers/{id}
|
|
// reads, which is how an accepted payment is followed to settlement.
|
|
|
|
// Scheme is the x402 scheme Gateway batches: the payer authorizes EXACTLY the
|
|
// amount. Version is the x402 protocol version of its payloads.
|
|
const (
|
|
Scheme = "exact"
|
|
Version = 2
|
|
)
|
|
|
|
// Payment states, as GET /v1/x402/transfers reports them.
|
|
const (
|
|
Received = "received" // accepted; the payer's balance is locked
|
|
Batched = "batched" // included in a batch
|
|
Confirmed = "confirmed" // the batch landed onchain
|
|
Completed = "completed" // fully complete
|
|
Failed = "failed" // never charged
|
|
)
|
|
|
|
// Kind is one network Gateway batches on, from GET /v1/x402/supported.
|
|
type Kind struct {
|
|
Version int `json:"x402Version"`
|
|
Scheme string `json:"scheme"`
|
|
Network string `json:"network"` // CAIP-2, "eip155:<chain id>"
|
|
Extra Extra `json:"extra"`
|
|
}
|
|
|
|
// Extra is what a payer needs to sign on that network: the EIP-712 domain
|
|
// (name, version, verifyingContract — the GatewayWallet contract), the
|
|
// shortest validity Gateway accepts, and the tokens it batches.
|
|
type Extra struct {
|
|
Name string `json:"name"`
|
|
Version string `json:"version"`
|
|
VerifyingContract string `json:"verifyingContract"`
|
|
MinValidity int64 `json:"minValiditySeconds"`
|
|
Assets []Asset `json:"assets"`
|
|
}
|
|
|
|
// Asset is a token Gateway batches on one network. Versions are the
|
|
// authorization-domain versions it accepts for the token: "1" signs the
|
|
// four-field domain, "2" adds the token address as salt.
|
|
type Asset struct {
|
|
Symbol string `json:"symbol"`
|
|
Address string `json:"address"`
|
|
Decimals int `json:"decimals"`
|
|
Versions []string `json:"authorizationVersions"`
|
|
}
|
|
|
|
// Supported reads the networks and tokens Gateway batches, live.
|
|
func (c *Client) Supported(ctx context.Context) ([]Kind, error) {
|
|
var out struct {
|
|
Kinds []Kind `json:"kinds"`
|
|
}
|
|
err := c.do(ctx, http.MethodGet, "/v1/x402/supported", nil, nil, &out)
|
|
return out.Kinds, err
|
|
}
|
|
|
|
// Signing is the `extra` of a requirement: the EIP-712 domain the payer signed
|
|
// under, minus the chain id, which the network carries.
|
|
type Signing struct {
|
|
Name string `json:"name"`
|
|
Version string `json:"version"`
|
|
VerifyingContract string `json:"verifyingContract"`
|
|
}
|
|
|
|
// Requirements is what the payee asks for (x402 PaymentRequirements).
|
|
type Requirements struct {
|
|
Scheme string `json:"scheme"`
|
|
Network string `json:"network"`
|
|
Asset string `json:"asset"`
|
|
Amount string `json:"amount"` // base units
|
|
PayTo string `json:"payTo"`
|
|
MaxTimeoutSeconds int64 `json:"maxTimeoutSeconds"`
|
|
Extra Signing `json:"extra"`
|
|
}
|
|
|
|
// Payload is the payer's half: the authorization and its signature.
|
|
type Payload struct {
|
|
Authorization Authorization `json:"authorization"`
|
|
Signature string `json:"signature"`
|
|
}
|
|
|
|
// Resource names what a payment bought. Gateway requires it, all three fields,
|
|
// on every payment: its OpenAPI marks resource optional and only url required
|
|
// within it, and the live API answers 400 "paymentPayload.resource: Required",
|
|
// then "resource.description: Required, resource.mimeType: Required"
|
|
// (measured on testnet).
|
|
type Resource struct {
|
|
URL string `json:"url"`
|
|
Description string `json:"description"`
|
|
MimeType string `json:"mimeType"`
|
|
}
|
|
|
|
// Settlement is Gateway's answer to a settle. Success means Gateway verified
|
|
// the signature and LOCKED the payer's balance; the payment is now queued and
|
|
// Transaction names it. On a refusal Reason is Gateway's errorReason
|
|
// (insufficient_balance, nonce_already_used, invalid_signature, …).
|
|
type Settlement struct {
|
|
Success bool `json:"success"`
|
|
Reason string `json:"errorReason,omitempty"`
|
|
Payer string `json:"payer,omitempty"`
|
|
Transaction string `json:"transaction"`
|
|
Network string `json:"network"`
|
|
}
|
|
|
|
// Settle submits a signed authorization for batched settlement against req.
|
|
//
|
|
// A 200 is an answer either way and comes back as a Settlement. Anything else
|
|
// comes back as an error, and only Refused(err) means Gateway processed
|
|
// nothing; a timeout or a 5xx is an outcome the caller does not know.
|
|
func (c *Client) Settle(ctx context.Context, r Resource, req Requirements, p Payload) (Settlement, error) {
|
|
var out Settlement
|
|
err := c.do(ctx, http.MethodPost, "/v1/x402/settle", nil, submission(r, req, p), &out)
|
|
return out, err
|
|
}
|
|
|
|
// submission is the body settle and verify both take: the payment as the
|
|
// payer presents it (x402 PaymentPayload) and the payee's requirements.
|
|
func submission(r Resource, req Requirements, p Payload) any {
|
|
type payment struct {
|
|
Version int `json:"x402Version"`
|
|
Resource Resource `json:"resource"`
|
|
Accepted Requirements `json:"accepted"`
|
|
Payload Payload `json:"payload"`
|
|
}
|
|
return struct {
|
|
Payment payment `json:"paymentPayload"`
|
|
Requirements Requirements `json:"paymentRequirements"`
|
|
}{payment{Version, r, req, p}, req}
|
|
}
|
|
|
|
// Payment is a batched payment as Gateway records it. Amount is in base
|
|
// units; TxHash is the batch's settlement transaction, empty until there is
|
|
// one.
|
|
type Payment struct {
|
|
ID string `json:"id"`
|
|
Status string `json:"status"`
|
|
Token string `json:"token"`
|
|
SendingNetwork string `json:"sendingNetwork"`
|
|
RecipientNetwork string `json:"recipientNetwork"`
|
|
From string `json:"fromAddress"`
|
|
To string `json:"toAddress"`
|
|
Amount string `json:"amount"`
|
|
Nonce string `json:"nonce"`
|
|
TxHash string `json:"txHash"`
|
|
CreatedAt string `json:"createdAt"`
|
|
UpdatedAt string `json:"updatedAt"`
|
|
}
|
|
|
|
// Payment reads one batched payment by the id Settle answered with.
|
|
func (c *Client) Payment(ctx context.Context, id string) (Payment, error) {
|
|
var out Payment
|
|
if !uuid(id) {
|
|
return out, fmt.Errorf("circle gateway: payment id %q is not a UUID", id)
|
|
}
|
|
err := c.do(ctx, http.MethodGet, "/v1/x402/transfers/"+url.PathEscape(id), nil, nil, &out)
|
|
return out, err
|
|
}
|
|
|
|
// Find reads the payments one payer made with one nonce on one network.
|
|
// Gateway refuses a second use of a (payer, nonce), so a real answer holds at
|
|
// most one payment; it is a list because that is what the endpoint returns.
|
|
func (c *Client) Find(ctx context.Context, network, from, nonce string) ([]Payment, error) {
|
|
var out struct {
|
|
Transfers []Payment `json:"transfers"`
|
|
}
|
|
q := url.Values{}
|
|
q.Set("network", network)
|
|
q.Set("from", from)
|
|
q.Set("nonce", nonce)
|
|
q.Set("pageSize", "10")
|
|
err := c.do(ctx, http.MethodGet, "/v1/x402/transfers", q, nil, &out)
|
|
return out.Transfers, err
|
|
}
|