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
8 changes: 8 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
**/node_modules
**/.pnpm-store
**/__pycache__
**/*.pyc
**/.pytest_cache
**/.venv
**/dist
**/.git
70 changes: 62 additions & 8 deletions examples/typescript/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

188 changes: 188 additions & 0 deletions go/mechanisms/cardano/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// Package cardano provides x402 Cardano `exact` mechanism constants and types.
//
// The package mirrors the canonical TypeScript and Python implementations under
// typescript/packages/mechanisms/cardano and python/x402/mechanisms/cardano so
// error codes, network identifiers, and asset markers are byte-for-byte
// identical across all SDKs.
//
// The Go SDK is server-side only — it never decodes Cardano CBOR. All chain
// inspection happens in a remote facilitator. Use the
// `mechanisms/cardano/exact/server` subpackage to register the Cardano scheme
// with an x402.X402ResourceServer.
package cardano

import "time"

const (
// SchemeExact is the scheme identifier for the `exact` payment scheme.
SchemeExact = "exact"

// CAIP-2-ish network identifiers (per spec — NOT canonical CAIP-2).

// CardanoMainnet is the x402 network identifier for Cardano mainnet.
CardanoMainnet = "cardano:mainnet"
// CardanoPreprod is the x402 network identifier for Cardano preprod testnet.
CardanoPreprod = "cardano:preprod"
// CardanoPreview is the x402 network identifier for Cardano preview testnet.
CardanoPreview = "cardano:preview"

// Cardano network IDs encoded in transaction bodies.

// NetworkIDMainnet is the Cardano body network_id for mainnet.
NetworkIDMainnet = 1
// NetworkIDTestnet is the Cardano body network_id for any testnet.
NetworkIDTestnet = 0

// LovelaceAsset is the asset marker for native ADA. The facilitator
// special-cases this value: lovelace lives in an output's `coin` field
// rather than the multi-asset map, so verification compares against
// `output.coin` when the asset string is exactly "lovelace".
LovelaceAsset = "lovelace"

// USDMDefaultDecimals is the default decimal precision for USDM
// (and most stablecoins on Cardano).
USDMDefaultDecimals = 6

// USDMMainnetPolicyID is the USDM token policy ID on mainnet.
USDMMainnetPolicyID = "c48cbb3d5e57ed56e276bc45f99ab39abe94e6cd7ac39fb402da47ad"
// USDMPreprodPolicyID is the USDM token policy ID on preprod.
USDMPreprodPolicyID = "16a55b2a349361ff88c03788f93e1e966e5d689605d044fef722ddde"
// USDMAssetNameHex is the CIP-68 (333 prefix + "USDM") asset name hex.
USDMAssetNameHex = "0014df105553444d"

// USDMMainnetAsset is the full asset unit for USDM on mainnet.
USDMMainnetAsset = USDMMainnetPolicyID + "." + USDMAssetNameHex
// USDMPreprodAsset is the full asset unit for USDM on preprod.
USDMPreprodAsset = USDMPreprodPolicyID + "." + USDMAssetNameHex

// AssetTransferMethodDefault is the default asset-transfer method
// (sender pays + signs).
AssetTransferMethodDefault = "default"
// AssetTransferMethodMasumi is the Masumi smart-protocol method.
AssetTransferMethodMasumi = "masumi"
// AssetTransferMethodScript is the Plutus-script method.
AssetTransferMethodScript = "script"

// DefaultMaxTimeoutSeconds is the default `maxTimeoutSeconds` for
// Cardano payment requirements.
DefaultMaxTimeoutSeconds = 300

// SettlementTTL is the duplicate-settlement cache lifetime; matches the
// Python facilitator's default.
SettlementTTL = 120 * time.Second

// Validation regexes.

// CardanoAssetRegex accepts either the literal "lovelace" (native ADA) or
// `policyId.assetNameHex` for native tokens.
CardanoAssetRegex = `^(lovelace|[0-9a-fA-F]{56}\.[0-9a-fA-F]{0,64})$`
// CardanoAddressRegex accepts both mainnet and testnet bech32 addresses.
CardanoAddressRegex = `^(addr1|addr_test1)[0-9a-z]+$`
// CardanoUTXORefRegex validates a UTXO reference (txHashHex#index).
CardanoUTXORefRegex = `^[0-9a-fA-F]{64}#\d+$`
)

// Error code constants. These mirror the TypeScript / Python / Java SDKs
// byte-for-byte so cross-language error handling is straightforward.
const (
// ErrUnsupportedScheme — scheme is not "exact".
ErrUnsupportedScheme = "unsupported_scheme"
// ErrInvalidPayload — generic invalid Cardano payload (decode error,
// missing fields, etc.).
ErrInvalidPayload = "invalid_exact_cardano_payload"
// ErrTransactionDecodeFailed — transaction CBOR could not be decoded.
ErrTransactionDecodeFailed = "invalid_exact_cardano_payload_transaction_decode_failed"
// ErrNetworkIDMismatch — tx body's network_id disagrees with the network.
ErrNetworkIDMismatch = "invalid_exact_cardano_payload_network_id_mismatch"
// ErrNetworkMismatch — accepted.network differs from required network.
ErrNetworkMismatch = "network_mismatch"
// ErrRecipientMismatch — no transaction output pays the required recipient.
ErrRecipientMismatch = "invalid_exact_cardano_payload_recipient_mismatch"
// ErrAssetMismatch — recipient output exists but does not carry the required asset.
ErrAssetMismatch = "invalid_exact_cardano_payload_asset_mismatch"
// ErrAmountInsufficient — recipient + asset matched but amount < requirement.
ErrAmountInsufficient = "invalid_exact_cardano_payload_amount_insufficient"
// ErrNonceInvalid — nonce is not a valid UTXO reference (txHashHex#index).
ErrNonceInvalid = "invalid_exact_cardano_payload_nonce_invalid"
// ErrNonceNotInInputs — nonce UTXO reference is not present among the
// transaction inputs.
ErrNonceNotInInputs = "invalid_exact_cardano_payload_nonce_not_in_inputs"
// ErrNonceNotOnChain — nonce UTXO is unknown or already spent on chain.
ErrNonceNotOnChain = "invalid_exact_cardano_payload_nonce_not_on_chain"
// ErrTTLExpired — transaction's TTL slot is in the past.
ErrTTLExpired = "invalid_exact_cardano_payload_ttl_expired"
// ErrValidityNotYetValid — transaction's validity start slot is still in the future.
ErrValidityNotYetValid = "invalid_exact_cardano_payload_not_yet_valid"
// ErrChainLookupFailed — facilitator failed to query the chain.
ErrChainLookupFailed = "exact_cardano_facilitator_chain_lookup_failed"
// ErrSettlementFailed — submission to chain failed.
ErrSettlementFailed = "exact_cardano_settlement_failed"
// ErrSettlementNotConfirmed — submission accepted but the chain has not yet confirmed.
ErrSettlementNotConfirmed = "exact_cardano_settlement_not_confirmed"
// ErrDuplicateSettlement — repeated settlement attempt with the same transaction.
ErrDuplicateSettlement = "duplicate_settlement"
// ErrScriptAddressMismatch — reconstructed script address disagrees with declared payTo.
ErrScriptAddressMismatch = "invalid_exact_cardano_payload_script_address_mismatch"
// ErrCardanoSDKMissing — optional Cardano SDK is missing.
ErrCardanoSDKMissing = "exact_cardano_sdk_missing"
// ErrTransactionUnsigned — transaction has zero witnesses.
ErrTransactionUnsigned = "invalid_exact_cardano_payload_unsigned"
)

// NetworkConfig bundles per-network defaults.
type NetworkConfig struct {
// CAIP2 is the x402 network identifier (e.g. "cardano:preprod").
CAIP2 string
// NetworkID is the Cardano body network_id (1 or 0).
NetworkID int
// DefaultAsset is the asset unit used when a Money-style price is
// supplied without an explicit asset (empty for networks without a
// default — e.g. preview).
DefaultAsset string
}

// NetworkConfigs maps each x402 Cardano network identifier to its config.
var NetworkConfigs = map[string]NetworkConfig{
CardanoMainnet: {
CAIP2: CardanoMainnet,
NetworkID: NetworkIDMainnet,
DefaultAsset: USDMMainnetAsset,
},
CardanoPreprod: {
CAIP2: CardanoPreprod,
NetworkID: NetworkIDTestnet,
DefaultAsset: USDMPreprodAsset,
},
CardanoPreview: {
CAIP2: CardanoPreview,
NetworkID: NetworkIDTestnet,
DefaultAsset: "",
},
}

// IsCardanoNetwork reports whether the supplied identifier names a supported
// Cardano network.
func IsCardanoNetwork(network string) bool {
_, ok := NetworkConfigs[network]
return ok
}

// GetNetworkConfig returns the network configuration for a Cardano network,
// or an error when the network is not supported.
func GetNetworkConfig(network string) (NetworkConfig, error) {
cfg, ok := NetworkConfigs[network]
if !ok {
return NetworkConfig{}, &UnsupportedNetworkError{Network: network}
}
return cfg, nil
}

// UnsupportedNetworkError is returned when a network identifier is not a
// recognised Cardano network.
type UnsupportedNetworkError struct {
Network string
}

func (e *UnsupportedNetworkError) Error() string {
return "unsupported Cardano network: " + e.Network
}
68 changes: 68 additions & 0 deletions go/mechanisms/cardano/constants_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package cardano

import (
"regexp"
"testing"
)

func TestIsCardanoNetwork(t *testing.T) {
cases := map[string]bool{
"cardano:mainnet": true,
"cardano:preprod": true,
"cardano:preview": true,
"eip155:8453": false,
"solana:mainnet": false,
"": false,
}
for input, want := range cases {
got := IsCardanoNetwork(input)
if got != want {
t.Errorf("IsCardanoNetwork(%q) = %v, want %v", input, got, want)
}
}
}

func TestGetNetworkConfig(t *testing.T) {
cfg, err := GetNetworkConfig(CardanoMainnet)
if err != nil || cfg.NetworkID != NetworkIDMainnet {
t.Fatalf("expected mainnet network id 1, got cfg=%+v err=%v", cfg, err)
}
cfg, err = GetNetworkConfig(CardanoPreprod)
if err != nil || cfg.NetworkID != NetworkIDTestnet {
t.Fatalf("expected preprod network id 0, got cfg=%+v err=%v", cfg, err)
}
if _, err := GetNetworkConfig("not-a-network"); err == nil {
t.Fatal("expected error for unknown network")
}
}

func TestAssetRegex(t *testing.T) {
re := regexp.MustCompile(CardanoAssetRegex)
policy := "c48cbb3d5e57ed56e276bc45f99ab39abe94e6cd7ac39fb402da47ad"
cases := map[string]bool{
"lovelace": true,
policy + ".0014df105553444d": true,
"LOVELACE": false, // case-sensitive in spec
"0xtoken": false,
policy: false, // no dot
}
for input, want := range cases {
if got := re.MatchString(input); got != want {
t.Errorf("asset regex(%q) = %v, want %v", input, got, want)
}
}
}

func TestUTXORefRegex(t *testing.T) {
re := regexp.MustCompile(CardanoUTXORefRegex)
tx := "708bbf0c346333a3503b216a4d093b693aa295d302c6af464ecd6a976159a45b"
if !re.MatchString(tx + "#3") {
t.Error("expected utxo ref regex to accept canonical form")
}
if re.MatchString(tx) {
t.Error("utxo ref regex must require '#index'")
}
if re.MatchString("xy#0") {
t.Error("utxo ref regex must require 64-hex tx hash")
}
}
Loading
Loading