Skip to content

Commit 5868b8a

Browse files
authored
Merge branch 'main' into fix/all-path-multi-hop
2 parents a0f2639 + 267c30d commit 5868b8a

5 files changed

Lines changed: 559 additions & 2 deletions

File tree

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ keeps routing and x402 payment policy in the tunnel process, and avoids requirin
3737
vendor lock-in.
3838

3939
- **Built-in x402 Payments** - Routed HTTP paths can require Sui gasless
40-
USDC x402 payment before proxying. Browser apps can import `/x402/client.js`,
41-
and native clients can call `/x402/prepare` directly and send `X-PAYMENT`.
40+
USDC or Casper wCSPR x402 payment before proxying. Browser apps can import
41+
`/x402/client.js`, and native clients can call `/x402/prepare` directly and
42+
send `X-PAYMENT`.
4243

4344
## Comparison
4445

docs/src/routes/configuration/+page.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,17 @@ send the signed payload as `X-PAYMENT`. Payment is still enforced by the tunnel
277277
on the paid route prefix. Tunnel paid routes default to Sui mainnet and use Sui
278278
testnet when `x402_testnet = true`.
279279

280+
#### x402 payment networks
281+
282+
| Network | Asset | Decimals | Facilitator |
283+
|---|---|---|---|
284+
| `sui:mainnet`, `sui:testnet` | USDC gasless stablecoin | 6 | Built-in Sui facilitator |
285+
| `casper:casper`, `casper:casper-test` | wCSPR CEP-18 token | 9 | `https://x402-facilitator.cspr.cloud`, overridable per payment |
286+
287+
Casper has no Go chain SDK, so `casper:*` payments delegate `/verify` and
288+
`/settle` to a remote x402 facilitator over HTTP. The wCSPR CEP-18 contract
289+
hash differs per network deployment, so it must be set as the payment asset.
290+
280291
For a task-oriented walkthrough, see [Portal Agent](/portal-agent).
281292

282293
### `identity.json`

portal/x402/casper.go

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
package x402
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"strings"
8+
9+
"github.com/cockroachdb/apd/v3"
10+
facilitatorclient "github.com/gosuda/x402-facilitator/api/client"
11+
facilitatorcore "github.com/gosuda/x402-facilitator/facilitator"
12+
facilitatortypes "github.com/gosuda/x402-facilitator/types"
13+
14+
"github.com/gosuda/portal-tunnel/v2/types"
15+
)
16+
17+
const (
18+
CasperMainnetNetwork = "casper:casper"
19+
CasperTestnetNetwork = "casper:casper-test"
20+
21+
// DefaultCasperFacilitatorURL is the hosted x402 facilitator used when a
22+
// Casper payment does not configure its own endpoint.
23+
DefaultCasperFacilitatorURL = "https://x402-facilitator.cspr.cloud"
24+
25+
// csprDecimals is the motes precision shared by CSPR and the wCSPR CEP-18 token.
26+
csprDecimals = 9
27+
)
28+
29+
var casperNetworkDisplayNames = map[string]string{
30+
CasperMainnetNetwork: "Casper Mainnet",
31+
CasperTestnetNetwork: "Casper Testnet",
32+
}
33+
34+
// CasperNetwork returns the CAIP-2 Casper network for a mainnet/testnet choice.
35+
func CasperNetwork(testnet bool) string {
36+
if testnet {
37+
return CasperTestnetNetwork
38+
}
39+
return CasperMainnetNetwork
40+
}
41+
42+
// IsCasperNetwork reports whether network is a Casper CAIP-2 identifier.
43+
func IsCasperNetwork(network string) bool {
44+
return strings.HasPrefix(strings.ToLower(strings.TrimSpace(network)), "casper:")
45+
}
46+
47+
// NormalizeCasperAddress canonicalizes a Casper account hash or public key.
48+
func NormalizeCasperAddress(address string) string {
49+
address = strings.ToLower(strings.TrimSpace(address))
50+
if address == "" {
51+
return ""
52+
}
53+
if rest, ok := strings.CutPrefix(address, "account-hash-"); ok {
54+
return "account-hash-" + rest
55+
}
56+
return address
57+
}
58+
59+
// CSPRAmountToAtomic converts a human wCSPR amount, such as "0.01", to motes
60+
// for the x402 facilitator. CSPR and wCSPR both have 9 decimals.
61+
func CSPRAmountToAtomic(amount string) (string, error) {
62+
amount = strings.TrimSpace(amount)
63+
if amount == "" {
64+
return "", errors.New("x402 wCSPR payment amount is required")
65+
}
66+
d, _, err := new(apd.Decimal).SetString(amount)
67+
if err != nil {
68+
return "", fmt.Errorf("x402 wCSPR payment amount must be a decimal CSPR amount: %s", amount)
69+
}
70+
d.Exponent += int32(csprDecimals)
71+
d.Reduce(d)
72+
if d.Form != apd.Finite || d.Sign() <= 0 {
73+
return "", fmt.Errorf("x402 wCSPR payment amount must be positive: %s", amount)
74+
}
75+
if d.Exponent < 0 {
76+
return "", fmt.Errorf("x402 wCSPR payment amount supports up to %d decimals: %s", csprDecimals, amount)
77+
}
78+
return fmt.Sprintf("%f", d), nil
79+
}
80+
81+
// FormatCSPRAtomicAmount renders a motes amount as a human wCSPR amount.
82+
func FormatCSPRAtomicAmount(amount string) string {
83+
amount = strings.TrimSpace(amount)
84+
if amount == "" {
85+
return ""
86+
}
87+
d, _, err := new(apd.Decimal).SetString(amount)
88+
if err != nil {
89+
return amount + " motes"
90+
}
91+
d.Exponent -= int32(csprDecimals)
92+
d.Reduce(d)
93+
if d.Form != apd.Finite || d.Sign() < 0 {
94+
return amount + " motes"
95+
}
96+
return fmt.Sprintf("%f wCSPR", d)
97+
}
98+
99+
var _ facilitatorcore.Facilitator = (*casperFacilitator)(nil)
100+
101+
// casperFacilitator verifies and settles Casper payments through a remote x402
102+
// facilitator. Casper has no Go chain SDK, so verify/settle are delegated over
103+
// HTTP the same way any hosted x402 facilitator is used.
104+
type casperFacilitator struct {
105+
network string
106+
client *facilitatorclient.Client
107+
}
108+
109+
func newCasperFacilitator(network string, endpoints ...string) (facilitatorcore.Facilitator, error) {
110+
network = strings.ToLower(strings.TrimSpace(network))
111+
if network == "" {
112+
network = CasperMainnetNetwork
113+
}
114+
if _, ok := casperNetworkDisplayNames[network]; !ok {
115+
return nil, fmt.Errorf("unsupported Casper network %q", network)
116+
}
117+
url := DefaultCasperFacilitatorURL
118+
for _, endpoint := range endpoints {
119+
if endpoint = strings.TrimSpace(endpoint); endpoint != "" {
120+
url = endpoint
121+
break
122+
}
123+
}
124+
client, err := facilitatorclient.NewClient(url)
125+
if err != nil {
126+
return nil, fmt.Errorf("create casper x402 facilitator: %w", err)
127+
}
128+
return &casperFacilitator{network: network, client: client}, nil
129+
}
130+
131+
func (f *casperFacilitator) Verify(ctx context.Context, payment *facilitatortypes.PaymentPayload, req *facilitatortypes.PaymentRequirements) (*facilitatortypes.PaymentVerifyResponse, error) {
132+
if payment == nil || req == nil {
133+
return nil, errors.New("casper x402 verify requires a payment payload and requirements")
134+
}
135+
return f.client.Verify(ctx, payment, req)
136+
}
137+
138+
func (f *casperFacilitator) Settle(ctx context.Context, payment *facilitatortypes.PaymentPayload, req *facilitatortypes.PaymentRequirements) (*facilitatortypes.PaymentSettleResponse, error) {
139+
if payment == nil || req == nil {
140+
return nil, errors.New("casper x402 settle requires a payment payload and requirements")
141+
}
142+
return f.client.Settle(ctx, payment, req)
143+
}
144+
145+
func (f *casperFacilitator) Supported() *facilitatortypes.SupportedResponse {
146+
return &facilitatortypes.SupportedResponse{
147+
Kinds: []facilitatortypes.SupportedKind{{
148+
X402Version: int(facilitatortypes.X402VersionV2),
149+
Scheme: string(facilitatortypes.Exact),
150+
Network: f.network,
151+
}},
152+
Extensions: []string{},
153+
Signers: map[string][]string{},
154+
}
155+
}
156+
157+
// NewCasperPayment builds a wCSPR x402 payment contract settled by a Casper
158+
// x402 facilitator. The wCSPR CEP-18 contract hash must be configured through
159+
// payment.Asset because it differs per network deployment.
160+
func NewCasperPayment(payment types.X402Payment) (*Payment, error) {
161+
network := strings.TrimSpace(payment.Network)
162+
if network == "" {
163+
network = CasperNetwork(payment.Testnet)
164+
}
165+
network = strings.ToLower(network)
166+
if _, ok := casperNetworkDisplayNames[network]; !ok {
167+
return nil, fmt.Errorf("unsupported Casper network %q", network)
168+
}
169+
asset := strings.ToLower(strings.TrimSpace(payment.Asset))
170+
if asset == "" {
171+
return nil, errors.New("x402 wCSPR payment requires the wCSPR CEP-18 contract hash")
172+
}
173+
payTo := NormalizeCasperAddress(payment.PayTo)
174+
if payTo == "" {
175+
return nil, errors.New("x402 wCSPR payment requires a Casper pay-to address")
176+
}
177+
amount, err := CSPRAmountToAtomic(payment.Amount)
178+
if err != nil {
179+
return nil, err
180+
}
181+
maxTimeoutSeconds := payment.MaxTimeoutSeconds
182+
if maxTimeoutSeconds <= 0 {
183+
maxTimeoutSeconds = defaultMaxTimeoutSeconds
184+
}
185+
requirements := facilitatortypes.PaymentRequirements{
186+
Scheme: string(facilitatortypes.Exact),
187+
Network: network,
188+
Asset: asset,
189+
Amount: amount,
190+
PayTo: payTo,
191+
MaxTimeoutSeconds: maxTimeoutSeconds,
192+
Extra: map[string]interface{}{
193+
"asset": "wCSPR",
194+
"assetTransferMethod": "casper-cep18-transfer",
195+
"decimals": csprDecimals,
196+
},
197+
}
198+
endpoints := append([]string(nil), payment.Endpoints...)
199+
facilitator, err := newCasperFacilitator(requirements.Network, endpoints...)
200+
if err != nil {
201+
return nil, err
202+
}
203+
204+
payment.Testnet = strings.EqualFold(requirements.Network, CasperTestnetNetwork)
205+
payment.Network = requirements.Network
206+
payment.NetworkName = casperNetworkDisplayNames[requirements.Network]
207+
payment.Asset = requirements.Asset
208+
payment.PayTo = requirements.PayTo
209+
payment.Amount = requirements.Amount
210+
payment.MaxTimeoutSeconds = requirements.MaxTimeoutSeconds
211+
payment.Endpoints = endpoints
212+
payment.ResourcePath = strings.TrimSpace(payment.ResourcePath)
213+
payment.ResourceDescription = strings.TrimSpace(payment.ResourceDescription)
214+
payment.ResourceMimeType = strings.TrimSpace(payment.ResourceMimeType)
215+
216+
return &Payment{
217+
payment: payment,
218+
facilitator: facilitator,
219+
requirements: requirements,
220+
}, nil
221+
}

0 commit comments

Comments
 (0)