W0: add deterministic quote cost envelope and docs sync
Some checks are pending
check / secretapi (push) Waiting to run
Some checks are pending
check / secretapi (push) Waiting to run
This commit is contained in:
parent
cd969480a0
commit
c80b1db18b
@ -120,6 +120,16 @@ Policy gates:
|
||||
1. Store checkout requires active membership.
|
||||
2. Workspace admin install/support actions require `onramp_attested` assurance.
|
||||
|
||||
## Quote Cost Envelope
|
||||
|
||||
`POST /secret/membership/quote` and `POST /marketplace/checkout/quote` return a deterministic `cost_envelope` object.
|
||||
|
||||
The envelope is pre-execution pricing metadata and is authoritative for checkout presentation:
|
||||
|
||||
1. `checkout_total_atomic` and `checkout_total` are the user checkout totals.
|
||||
2. `provider_fee_policy=edut_absorbed` means on-ramp processing fees are absorbed by EDUT.
|
||||
3. `network_fee_policy=payer_wallet_pays_chain_gas` means chain gas remains wallet-dependent and separate from checkout total.
|
||||
|
||||
## Key Environment Variables
|
||||
|
||||
### Core
|
||||
@ -127,10 +137,14 @@ Policy gates:
|
||||
- `SECRET_API_LISTEN_ADDR` (default `:8080`)
|
||||
- `SECRET_API_DB_PATH` (default `./secret.db`)
|
||||
- `SECRET_API_ALLOWED_ORIGIN` (default `https://edut.ai`)
|
||||
- `SECRET_API_DEPLOYMENT_CLASS` (`development|staging|production`; default `development`)
|
||||
- `SECRET_API_MEMBER_POLL_INTERVAL_SECONDS` (default `30`)
|
||||
- `SECRET_API_CHAIN_ID` (default `84532`)
|
||||
- `SECRET_API_CHAIN_RPC_URL` (optional, enables on-chain tx receipt verification)
|
||||
- `SECRET_API_REQUIRE_ONCHAIN_TX_VERIFICATION` (default `false`; when `true`, membership confirm and marketplace checkout confirm fail closed without chain receipt verification)
|
||||
- `SECRET_API_REQUIRE_ONCHAIN_TX_VERIFICATION`:
|
||||
- if explicitly set, value is honored.
|
||||
- if unset, defaults to `true` when `SECRET_API_DEPLOYMENT_CLASS=production`, else `false`.
|
||||
- when enabled, membership confirm and marketplace checkout confirm fail closed without chain receipt verification.
|
||||
- `SECRET_API_ENTITLEMENT_CONTRACT` (optional; when set, marketplace quote emits purchase calldata for entitlement settlement contract)
|
||||
|
||||
### Membership
|
||||
|
||||
@ -470,6 +470,7 @@ func (a *app) handleMembershipQuote(w http.ResponseWriter, r *http.Request) {
|
||||
Currency: quote.Currency,
|
||||
AmountAtomic: quote.AmountAtomic,
|
||||
Decimals: quote.Decimals,
|
||||
CostEnvelope: newQuoteCostEnvelope(quote.Currency, quote.Decimals, quote.AmountAtomic),
|
||||
Deadline: quote.ExpiresAt.Format(time.RFC3339Nano),
|
||||
ContractAddress: quote.ContractAddress,
|
||||
Method: quote.Method,
|
||||
|
||||
@ -88,6 +88,7 @@ func TestMembershipDistinctPayerProof(t *testing.T) {
|
||||
if quote.Tx["from"] != payerAddr {
|
||||
t.Fatalf("tx.from mismatch: %+v", quote.Tx)
|
||||
}
|
||||
assertQuoteCostEnvelope(t, quote.CostEnvelope, quote.Currency, quote.Decimals, quote.AmountAtomic)
|
||||
}
|
||||
|
||||
func TestMembershipCompanySponsorWithoutOwnerProof(t *testing.T) {
|
||||
@ -1093,6 +1094,7 @@ func TestMarketplaceCheckoutBundlesMembershipAndMintsEntitlement(t *testing.T) {
|
||||
if quote.MerchantID != defaultMarketplaceMerchantID {
|
||||
t.Fatalf("expected default merchant id, got %+v", quote)
|
||||
}
|
||||
assertQuoteCostEnvelope(t, quote.CostEnvelope, quote.Currency, quote.Decimals, quote.TotalAmountAtomic)
|
||||
|
||||
confirm := postJSONExpect[marketplaceCheckoutConfirmResponse](t, a, "/marketplace/checkout/confirm", marketplaceCheckoutConfirmRequest{
|
||||
QuoteID: quote.QuoteID,
|
||||
@ -2025,6 +2027,49 @@ func mustKey(t *testing.T) *ecdsa.PrivateKey {
|
||||
return key
|
||||
}
|
||||
|
||||
func assertQuoteCostEnvelope(t *testing.T, got quoteCostEnvelope, wantCurrency string, wantDecimals int, wantAmountAtomic string) {
|
||||
t.Helper()
|
||||
if got.Version != quoteCostEnvelopeVersion {
|
||||
t.Fatalf("cost envelope version mismatch: got=%s want=%s", got.Version, quoteCostEnvelopeVersion)
|
||||
}
|
||||
if !strings.EqualFold(strings.TrimSpace(got.CheckoutCurrency), strings.TrimSpace(wantCurrency)) {
|
||||
t.Fatalf("cost envelope checkout currency mismatch: got=%s want=%s", got.CheckoutCurrency, wantCurrency)
|
||||
}
|
||||
if got.CheckoutDecimals != wantDecimals {
|
||||
t.Fatalf("cost envelope decimals mismatch: got=%d want=%d", got.CheckoutDecimals, wantDecimals)
|
||||
}
|
||||
if strings.TrimSpace(got.CheckoutTotalAtomic) != strings.TrimSpace(wantAmountAtomic) {
|
||||
t.Fatalf("cost envelope total atomic mismatch: got=%s want=%s", got.CheckoutTotalAtomic, wantAmountAtomic)
|
||||
}
|
||||
if strings.TrimSpace(got.CheckoutTotal) != strings.TrimSpace(formatAtomicAmount(wantAmountAtomic, wantDecimals)) {
|
||||
t.Fatalf("cost envelope total mismatch: got=%s want=%s", got.CheckoutTotal, formatAtomicAmount(wantAmountAtomic, wantDecimals))
|
||||
}
|
||||
if got.ProviderFeePolicy != quoteProviderFeePolicyEdutAbsorbed {
|
||||
t.Fatalf("cost envelope provider fee policy mismatch: got=%s", got.ProviderFeePolicy)
|
||||
}
|
||||
if !got.ProviderFeeIncluded {
|
||||
t.Fatalf("cost envelope provider fee should be included")
|
||||
}
|
||||
if got.ProviderFeeEstimateStatus != quoteProviderFeeEstimateStatusAbsorbed {
|
||||
t.Fatalf("cost envelope provider fee status mismatch: got=%s", got.ProviderFeeEstimateStatus)
|
||||
}
|
||||
if got.ProviderFeeEstimateAtomic != "0" {
|
||||
t.Fatalf("cost envelope provider fee estimate mismatch: got=%s", got.ProviderFeeEstimateAtomic)
|
||||
}
|
||||
if got.NetworkFeePolicy != quoteNetworkFeePolicyPayerPaysGas {
|
||||
t.Fatalf("cost envelope network fee policy mismatch: got=%s", got.NetworkFeePolicy)
|
||||
}
|
||||
if got.NetworkFeeCurrency != quoteNetworkFeeDefaultCurrency {
|
||||
t.Fatalf("cost envelope network fee currency mismatch: got=%s", got.NetworkFeeCurrency)
|
||||
}
|
||||
if got.NetworkFeeEstimateStatus != quoteNetworkFeeEstimateStatusWalletQuoted {
|
||||
t.Fatalf("cost envelope network fee status mismatch: got=%s", got.NetworkFeeEstimateStatus)
|
||||
}
|
||||
if got.NetworkFeeEstimateAtomic != "0" {
|
||||
t.Fatalf("cost envelope network fee estimate mismatch: got=%s", got.NetworkFeeEstimateAtomic)
|
||||
}
|
||||
}
|
||||
|
||||
func signTypedData(t *testing.T, key *ecdsa.PrivateKey, typedData apitypes.TypedData) string {
|
||||
t.Helper()
|
||||
hash, _, err := apitypes.TypedDataAndHash(typedData)
|
||||
|
||||
@ -13,6 +13,7 @@ type Config struct {
|
||||
ListenAddr string
|
||||
DBPath string
|
||||
AllowedOrigin string
|
||||
DeploymentClass string
|
||||
MemberPollIntervalSec int
|
||||
IntentTTL time.Duration
|
||||
QuoteTTL time.Duration
|
||||
@ -45,6 +46,7 @@ func loadConfig() Config {
|
||||
ListenAddr: env("SECRET_API_LISTEN_ADDR", ":8080"),
|
||||
DBPath: env("SECRET_API_DB_PATH", "./secret.db"),
|
||||
AllowedOrigin: env("SECRET_API_ALLOWED_ORIGIN", "https://edut.ai"),
|
||||
DeploymentClass: normalizeDeploymentClass(env("SECRET_API_DEPLOYMENT_CLASS", "development")),
|
||||
MemberPollIntervalSec: envInt("SECRET_API_MEMBER_POLL_INTERVAL_SECONDS", 30),
|
||||
IntentTTL: time.Duration(envInt("SECRET_API_INTENT_TTL_SECONDS", 900)) * time.Second,
|
||||
QuoteTTL: time.Duration(envInt("SECRET_API_QUOTE_TTL_SECONDS", 900)) * time.Second,
|
||||
@ -62,7 +64,7 @@ func loadConfig() Config {
|
||||
MintAmountAtomic: env("SECRET_API_MINT_AMOUNT_ATOMIC", "100000000"),
|
||||
MintDecimals: envInt("SECRET_API_MINT_DECIMALS", 6),
|
||||
ChainRPCURL: env("SECRET_API_CHAIN_RPC_URL", ""),
|
||||
RequireOnchainTxVerify: envBool("SECRET_API_REQUIRE_ONCHAIN_TX_VERIFICATION", false),
|
||||
RequireOnchainTxVerify: loadRequireOnchainTxVerify(),
|
||||
GovernanceRuntimeVersion: env("SECRET_API_GOV_RUNTIME_VERSION", "0.1.0"),
|
||||
GovernancePackageURL: env("SECRET_API_GOV_PACKAGE_URL", "https://cdn.edut.ai/governance/edutd-0.1.0.tar.gz"),
|
||||
GovernancePackageHash: strings.ToLower(env("SECRET_API_GOV_PACKAGE_HASH", "sha256:pending")),
|
||||
@ -98,12 +100,24 @@ func (c Config) Validate() error {
|
||||
if c.WalletSessionTTL <= 0 {
|
||||
return fmt.Errorf("SECRET_API_WALLET_SESSION_TTL_SECONDS must be positive")
|
||||
}
|
||||
if isProductionDeploymentClass(c.DeploymentClass) && !c.RequireOnchainTxVerify {
|
||||
return fmt.Errorf("production deployment requires SECRET_API_REQUIRE_ONCHAIN_TX_VERIFICATION=true")
|
||||
}
|
||||
if c.RequireOnchainTxVerify && strings.TrimSpace(c.ChainRPCURL) == "" {
|
||||
return fmt.Errorf("SECRET_API_REQUIRE_ONCHAIN_TX_VERIFICATION requires SECRET_API_CHAIN_RPC_URL")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadRequireOnchainTxVerify() bool {
|
||||
if raw, ok := os.LookupEnv("SECRET_API_REQUIRE_ONCHAIN_TX_VERIFICATION"); ok {
|
||||
if strings.TrimSpace(raw) != "" {
|
||||
return parseBool(raw, false)
|
||||
}
|
||||
}
|
||||
return isProductionDeploymentClass(env("SECRET_API_DEPLOYMENT_CLASS", "development"))
|
||||
}
|
||||
|
||||
func env(key, fallback string) string {
|
||||
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
|
||||
return v
|
||||
@ -128,6 +142,11 @@ func envBool(key string, fallback bool) bool {
|
||||
if raw == "" {
|
||||
return fallback
|
||||
}
|
||||
return parseBool(raw, fallback)
|
||||
}
|
||||
|
||||
func parseBool(raw string, fallback bool) bool {
|
||||
raw = strings.ToLower(strings.TrimSpace(raw))
|
||||
switch raw {
|
||||
case "1", "true", "yes", "on":
|
||||
return true
|
||||
@ -137,3 +156,21 @@ func envBool(key string, fallback bool) bool {
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeDeploymentClass(value string) string {
|
||||
v := strings.ToLower(strings.TrimSpace(value))
|
||||
switch v {
|
||||
case "prod", "production":
|
||||
return "production"
|
||||
case "staging", "stage":
|
||||
return "staging"
|
||||
case "dev", "development":
|
||||
return "development"
|
||||
default:
|
||||
return "development"
|
||||
}
|
||||
}
|
||||
|
||||
func isProductionDeploymentClass(value string) bool {
|
||||
return normalizeDeploymentClass(value) == "production"
|
||||
}
|
||||
|
||||
@ -2,8 +2,15 @@ package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func loadConfigIsolated(t *testing.T) Config {
|
||||
t.Helper()
|
||||
t.Setenv("SECRET_API_DEPLOYMENT_CLASS", "")
|
||||
t.Setenv("SECRET_API_REQUIRE_ONCHAIN_TX_VERIFICATION", "")
|
||||
return loadConfig()
|
||||
}
|
||||
|
||||
func TestConfigValidateAllowsDefaultLocalConfig(t *testing.T) {
|
||||
cfg := loadConfig()
|
||||
cfg := loadConfigIsolated(t)
|
||||
cfg.ChainID = 84532
|
||||
cfg.RequireOnchainTxVerify = false
|
||||
cfg.ChainRPCURL = ""
|
||||
@ -13,7 +20,7 @@ func TestConfigValidateAllowsDefaultLocalConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConfigValidateRejectsStrictVerificationWithoutRPC(t *testing.T) {
|
||||
cfg := loadConfig()
|
||||
cfg := loadConfigIsolated(t)
|
||||
cfg.RequireOnchainTxVerify = true
|
||||
cfg.ChainRPCURL = ""
|
||||
if err := cfg.Validate(); err == nil {
|
||||
@ -22,7 +29,7 @@ func TestConfigValidateRejectsStrictVerificationWithoutRPC(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConfigValidateRejectsNonPositiveChainID(t *testing.T) {
|
||||
cfg := loadConfig()
|
||||
cfg := loadConfigIsolated(t)
|
||||
cfg.ChainID = 0
|
||||
if err := cfg.Validate(); err == nil {
|
||||
t.Fatalf("expected chain id validation failure")
|
||||
@ -30,7 +37,7 @@ func TestConfigValidateRejectsNonPositiveChainID(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConfigValidateAllowsETHWhenDecimalsMatch(t *testing.T) {
|
||||
cfg := loadConfig()
|
||||
cfg := loadConfigIsolated(t)
|
||||
cfg.MintCurrency = "ETH"
|
||||
cfg.MintDecimals = 18
|
||||
cfg.MintAmountAtomic = "1"
|
||||
@ -40,7 +47,7 @@ func TestConfigValidateAllowsETHWhenDecimalsMatch(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConfigValidateRejectsETHWhenDecimalsMismatch(t *testing.T) {
|
||||
cfg := loadConfig()
|
||||
cfg := loadConfigIsolated(t)
|
||||
cfg.MintCurrency = "ETH"
|
||||
cfg.MintDecimals = 6
|
||||
if err := cfg.Validate(); err == nil {
|
||||
@ -49,7 +56,7 @@ func TestConfigValidateRejectsETHWhenDecimalsMismatch(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConfigValidateRejectsUSDCWhenDecimalsMismatch(t *testing.T) {
|
||||
cfg := loadConfig()
|
||||
cfg := loadConfigIsolated(t)
|
||||
cfg.MintCurrency = "USDC"
|
||||
cfg.MintDecimals = 18
|
||||
if err := cfg.Validate(); err == nil {
|
||||
@ -58,7 +65,7 @@ func TestConfigValidateRejectsUSDCWhenDecimalsMismatch(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConfigValidateRejectsInvalidMintAmount(t *testing.T) {
|
||||
cfg := loadConfig()
|
||||
cfg := loadConfigIsolated(t)
|
||||
cfg.MintAmountAtomic = "not-a-number"
|
||||
if err := cfg.Validate(); err == nil {
|
||||
t.Fatalf("expected mint amount validation failure")
|
||||
@ -67,7 +74,7 @@ func TestConfigValidateRejectsInvalidMintAmount(t *testing.T) {
|
||||
|
||||
func TestLoadConfigDefaultsWalletSessionRequired(t *testing.T) {
|
||||
t.Setenv("SECRET_API_REQUIRE_WALLET_SESSION", "")
|
||||
cfg := loadConfig()
|
||||
cfg := loadConfigIsolated(t)
|
||||
if !cfg.RequireWalletSession {
|
||||
t.Fatalf("expected wallet session to be required by default")
|
||||
}
|
||||
@ -75,8 +82,36 @@ func TestLoadConfigDefaultsWalletSessionRequired(t *testing.T) {
|
||||
|
||||
func TestLoadConfigWalletSessionCanBeDisabled(t *testing.T) {
|
||||
t.Setenv("SECRET_API_REQUIRE_WALLET_SESSION", "false")
|
||||
cfg := loadConfig()
|
||||
cfg := loadConfigIsolated(t)
|
||||
if cfg.RequireWalletSession {
|
||||
t.Fatalf("expected wallet session requirement to be disabled by env override")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigDefaultsOnchainVerificationFalseInDevelopment(t *testing.T) {
|
||||
t.Setenv("SECRET_API_DEPLOYMENT_CLASS", "development")
|
||||
t.Setenv("SECRET_API_REQUIRE_ONCHAIN_TX_VERIFICATION", "")
|
||||
cfg := loadConfig()
|
||||
if cfg.RequireOnchainTxVerify {
|
||||
t.Fatalf("expected strict onchain verification to default false in development")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigDefaultsOnchainVerificationTrueInProduction(t *testing.T) {
|
||||
t.Setenv("SECRET_API_DEPLOYMENT_CLASS", "production")
|
||||
t.Setenv("SECRET_API_REQUIRE_ONCHAIN_TX_VERIFICATION", "")
|
||||
cfg := loadConfig()
|
||||
if !cfg.RequireOnchainTxVerify {
|
||||
t.Fatalf("expected strict onchain verification to default true in production")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigValidateRejectsProductionWhenStrictVerificationDisabled(t *testing.T) {
|
||||
cfg := loadConfigIsolated(t)
|
||||
cfg.DeploymentClass = "production"
|
||||
cfg.RequireOnchainTxVerify = false
|
||||
cfg.ChainRPCURL = "https://example.invalid"
|
||||
if err := cfg.Validate(); err == nil {
|
||||
t.Fatalf("expected production config validation failure when strict onchain verification disabled")
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,11 +50,11 @@ func (a *app) marketplaceOffersForMerchant(merchantID string) []marketplaceOffer
|
||||
offers := []marketplaceOffer{
|
||||
{
|
||||
MerchantID: defaultMarketplaceMerchantID,
|
||||
OfferID: offerIDSoloCore,
|
||||
IssuerID: defaultMarketplaceMerchantID,
|
||||
Title: "EDUT Solo Core",
|
||||
Summary: "Single-principal governance runtime for personal operations.",
|
||||
Status: "active",
|
||||
OfferID: offerIDSoloCore,
|
||||
IssuerID: defaultMarketplaceMerchantID,
|
||||
Title: "EDUT Solo Core",
|
||||
Summary: "Single-principal governance runtime for personal operations.",
|
||||
Status: "active",
|
||||
Pricing: marketplaceOfferPrice{
|
||||
Currency: "USDC",
|
||||
AmountAtomic: marketplaceStandardOfferAtomic,
|
||||
@ -78,11 +78,11 @@ func (a *app) marketplaceOffersForMerchant(merchantID string) []marketplaceOffer
|
||||
},
|
||||
{
|
||||
MerchantID: defaultMarketplaceMerchantID,
|
||||
OfferID: offerIDWorkspaceCore,
|
||||
IssuerID: defaultMarketplaceMerchantID,
|
||||
Title: "EDUT Workspace Core",
|
||||
Summary: "Org-bound deterministic governance runtime for team operations.",
|
||||
Status: "active",
|
||||
OfferID: offerIDWorkspaceCore,
|
||||
IssuerID: defaultMarketplaceMerchantID,
|
||||
Title: "EDUT Workspace Core",
|
||||
Summary: "Org-bound deterministic governance runtime for team operations.",
|
||||
Status: "active",
|
||||
Pricing: marketplaceOfferPrice{
|
||||
Currency: "USDC",
|
||||
AmountAtomic: marketplaceStandardOfferAtomic,
|
||||
@ -106,11 +106,11 @@ func (a *app) marketplaceOffersForMerchant(merchantID string) []marketplaceOffer
|
||||
},
|
||||
{
|
||||
MerchantID: defaultMarketplaceMerchantID,
|
||||
OfferID: offerIDWorkspaceAI,
|
||||
IssuerID: defaultMarketplaceMerchantID,
|
||||
Title: "EDUT Workspace AI Layer",
|
||||
Summary: "AI reasoning layer for governed workspace operations.",
|
||||
Status: "active",
|
||||
OfferID: offerIDWorkspaceAI,
|
||||
IssuerID: defaultMarketplaceMerchantID,
|
||||
Title: "EDUT Workspace AI Layer",
|
||||
Summary: "AI reasoning layer for governed workspace operations.",
|
||||
Status: "active",
|
||||
Pricing: marketplaceOfferPrice{
|
||||
Currency: "USDC",
|
||||
AmountAtomic: marketplaceStandardOfferAtomic,
|
||||
@ -135,11 +135,11 @@ func (a *app) marketplaceOffersForMerchant(merchantID string) []marketplaceOffer
|
||||
},
|
||||
{
|
||||
MerchantID: defaultMarketplaceMerchantID,
|
||||
OfferID: offerIDWorkspaceLane24,
|
||||
IssuerID: defaultMarketplaceMerchantID,
|
||||
Title: "EDUT Workspace 24h Lane",
|
||||
Summary: "Autonomous execution lane capacity for workspace queue throughput.",
|
||||
Status: "active",
|
||||
OfferID: offerIDWorkspaceLane24,
|
||||
IssuerID: defaultMarketplaceMerchantID,
|
||||
Title: "EDUT Workspace 24h Lane",
|
||||
Summary: "Autonomous execution lane capacity for workspace queue throughput.",
|
||||
Status: "active",
|
||||
Pricing: marketplaceOfferPrice{
|
||||
Currency: "USDC",
|
||||
AmountAtomic: marketplaceStandardOfferAtomic,
|
||||
@ -163,11 +163,11 @@ func (a *app) marketplaceOffersForMerchant(merchantID string) []marketplaceOffer
|
||||
},
|
||||
{
|
||||
MerchantID: defaultMarketplaceMerchantID,
|
||||
OfferID: offerIDWorkspaceSovereign,
|
||||
IssuerID: defaultMarketplaceMerchantID,
|
||||
Title: "EDUT Workspace Sovereign Continuity",
|
||||
Summary: "Workspace continuity profile for stronger local/offline operation.",
|
||||
Status: "active",
|
||||
OfferID: offerIDWorkspaceSovereign,
|
||||
IssuerID: defaultMarketplaceMerchantID,
|
||||
Title: "EDUT Workspace Sovereign Continuity",
|
||||
Summary: "Workspace continuity profile for stronger local/offline operation.",
|
||||
Status: "active",
|
||||
Pricing: marketplaceOfferPrice{
|
||||
Currency: "USDC",
|
||||
AmountAtomic: marketplaceStandardOfferAtomic,
|
||||
@ -501,6 +501,7 @@ func (a *app) handleMarketplaceCheckoutQuote(w http.ResponseWriter, r *http.Requ
|
||||
TotalAmount: formatAtomicAmount(quote.TotalAmountAtomic, quote.Decimals),
|
||||
TotalAmountAtomic: quote.TotalAmountAtomic,
|
||||
Decimals: quote.Decimals,
|
||||
CostEnvelope: newQuoteCostEnvelope(quote.Currency, quote.Decimals, quote.TotalAmountAtomic),
|
||||
MembershipActivationIncluded: quote.MembershipIncluded,
|
||||
LineItems: lineItems,
|
||||
PolicyHash: quote.PolicyHash,
|
||||
|
||||
@ -3,16 +3,16 @@ package main
|
||||
import "time"
|
||||
|
||||
type marketplaceOffer struct {
|
||||
MerchantID string `json:"merchant_id,omitempty"`
|
||||
OfferID string `json:"offer_id"`
|
||||
IssuerID string `json:"issuer_id"`
|
||||
Title string `json:"title"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Pricing marketplaceOfferPrice `json:"pricing"`
|
||||
Policies marketplaceOfferPolicy `json:"policies"`
|
||||
ExecutionProfile marketplaceExecutionProfile `json:"execution_profile,omitempty"`
|
||||
SortOrder int `json:"-"`
|
||||
MerchantID string `json:"merchant_id,omitempty"`
|
||||
OfferID string `json:"offer_id"`
|
||||
IssuerID string `json:"issuer_id"`
|
||||
Title string `json:"title"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Pricing marketplaceOfferPrice `json:"pricing"`
|
||||
Policies marketplaceOfferPolicy `json:"policies"`
|
||||
ExecutionProfile marketplaceExecutionProfile `json:"execution_profile,omitempty"`
|
||||
SortOrder int `json:"-"`
|
||||
}
|
||||
|
||||
type marketplaceOfferPrice struct {
|
||||
@ -79,6 +79,7 @@ type marketplaceCheckoutQuoteResponse struct {
|
||||
TotalAmount string `json:"total_amount"`
|
||||
TotalAmountAtomic string `json:"total_amount_atomic"`
|
||||
Decimals int `json:"decimals"`
|
||||
CostEnvelope quoteCostEnvelope `json:"cost_envelope"`
|
||||
MembershipActivationIncluded bool `json:"membership_activation_included"`
|
||||
LineItems []marketplaceQuoteLineItem `json:"line_items"`
|
||||
PolicyHash string `json:"policy_hash"`
|
||||
|
||||
@ -68,21 +68,22 @@ type membershipQuoteRequest struct {
|
||||
}
|
||||
|
||||
type membershipQuoteResponse struct {
|
||||
QuoteID string `json:"quote_id"`
|
||||
ChainID int64 `json:"chain_id"`
|
||||
Currency string `json:"currency"`
|
||||
AmountAtomic string `json:"amount_atomic"`
|
||||
Decimals int `json:"decimals"`
|
||||
Deadline string `json:"deadline"`
|
||||
ContractAddress string `json:"contract_address"`
|
||||
Method string `json:"method"`
|
||||
Calldata string `json:"calldata"`
|
||||
Value string `json:"value"`
|
||||
OwnerWallet string `json:"owner_wallet,omitempty"`
|
||||
PayerWallet string `json:"payer_wallet,omitempty"`
|
||||
SponsorshipMode string `json:"sponsorship_mode,omitempty"`
|
||||
SponsorOrgRoot string `json:"sponsor_org_root_id,omitempty"`
|
||||
Tx map[string]any `json:"tx"`
|
||||
QuoteID string `json:"quote_id"`
|
||||
ChainID int64 `json:"chain_id"`
|
||||
Currency string `json:"currency"`
|
||||
AmountAtomic string `json:"amount_atomic"`
|
||||
Decimals int `json:"decimals"`
|
||||
CostEnvelope quoteCostEnvelope `json:"cost_envelope"`
|
||||
Deadline string `json:"deadline"`
|
||||
ContractAddress string `json:"contract_address"`
|
||||
Method string `json:"method"`
|
||||
Calldata string `json:"calldata"`
|
||||
Value string `json:"value"`
|
||||
OwnerWallet string `json:"owner_wallet,omitempty"`
|
||||
PayerWallet string `json:"payer_wallet,omitempty"`
|
||||
SponsorshipMode string `json:"sponsorship_mode,omitempty"`
|
||||
SponsorOrgRoot string `json:"sponsor_org_root_id,omitempty"`
|
||||
Tx map[string]any `json:"tx"`
|
||||
}
|
||||
|
||||
type membershipConfirmRequest struct {
|
||||
|
||||
44
backend/secretapi/quote_cost.go
Normal file
44
backend/secretapi/quote_cost.go
Normal file
@ -0,0 +1,44 @@
|
||||
package main
|
||||
|
||||
const (
|
||||
quoteCostEnvelopeVersion = "edut.quote_cost_envelope.v1"
|
||||
quoteProviderFeePolicyEdutAbsorbed = "edut_absorbed"
|
||||
quoteProviderFeeEstimateStatusAbsorbed = "absorbed_by_edut"
|
||||
quoteNetworkFeePolicyPayerPaysGas = "payer_wallet_pays_chain_gas"
|
||||
quoteNetworkFeeEstimateStatusWalletQuoted = "wallet_estimate_required"
|
||||
quoteNetworkFeeDefaultCurrency = "ETH"
|
||||
)
|
||||
|
||||
type quoteCostEnvelope struct {
|
||||
Version string `json:"version"`
|
||||
CheckoutCurrency string `json:"checkout_currency"`
|
||||
CheckoutDecimals int `json:"checkout_decimals"`
|
||||
CheckoutTotalAtomic string `json:"checkout_total_atomic"`
|
||||
CheckoutTotal string `json:"checkout_total"`
|
||||
ProviderFeePolicy string `json:"provider_fee_policy"`
|
||||
ProviderFeeIncluded bool `json:"provider_fee_included"`
|
||||
ProviderFeeEstimateStatus string `json:"provider_fee_estimate_status"`
|
||||
ProviderFeeEstimateAtomic string `json:"provider_fee_estimate_atomic"`
|
||||
NetworkFeePolicy string `json:"network_fee_policy"`
|
||||
NetworkFeeCurrency string `json:"network_fee_currency"`
|
||||
NetworkFeeEstimateStatus string `json:"network_fee_estimate_status"`
|
||||
NetworkFeeEstimateAtomic string `json:"network_fee_estimate_atomic"`
|
||||
}
|
||||
|
||||
func newQuoteCostEnvelope(checkoutCurrency string, checkoutDecimals int, checkoutTotalAtomic string) quoteCostEnvelope {
|
||||
return quoteCostEnvelope{
|
||||
Version: quoteCostEnvelopeVersion,
|
||||
CheckoutCurrency: checkoutCurrency,
|
||||
CheckoutDecimals: checkoutDecimals,
|
||||
CheckoutTotalAtomic: checkoutTotalAtomic,
|
||||
CheckoutTotal: formatAtomicAmount(checkoutTotalAtomic, checkoutDecimals),
|
||||
ProviderFeePolicy: quoteProviderFeePolicyEdutAbsorbed,
|
||||
ProviderFeeIncluded: true,
|
||||
ProviderFeeEstimateStatus: quoteProviderFeeEstimateStatusAbsorbed,
|
||||
ProviderFeeEstimateAtomic: "0",
|
||||
NetworkFeePolicy: quoteNetworkFeePolicyPayerPaysGas,
|
||||
NetworkFeeCurrency: quoteNetworkFeeDefaultCurrency,
|
||||
NetworkFeeEstimateStatus: quoteNetworkFeeEstimateStatusWalletQuoted,
|
||||
NetworkFeeEstimateAtomic: "0",
|
||||
}
|
||||
}
|
||||
@ -70,6 +70,21 @@ Success (`200`):
|
||||
"total_amount": "1100.00",
|
||||
"total_amount_atomic": "1100000000",
|
||||
"decimals": 6,
|
||||
"cost_envelope": {
|
||||
"version": "edut.quote_cost_envelope.v1",
|
||||
"checkout_currency": "USDC",
|
||||
"checkout_decimals": 6,
|
||||
"checkout_total_atomic": "1100000000",
|
||||
"checkout_total": "1100",
|
||||
"provider_fee_policy": "edut_absorbed",
|
||||
"provider_fee_included": true,
|
||||
"provider_fee_estimate_status": "absorbed_by_edut",
|
||||
"provider_fee_estimate_atomic": "0",
|
||||
"network_fee_policy": "payer_wallet_pays_chain_gas",
|
||||
"network_fee_currency": "ETH",
|
||||
"network_fee_estimate_status": "wallet_estimate_required",
|
||||
"network_fee_estimate_atomic": "0"
|
||||
},
|
||||
"membership_activation_included": true,
|
||||
"line_items": [
|
||||
{
|
||||
|
||||
@ -160,12 +160,26 @@ Success (`200`):
|
||||
"quote_id": "mq_01HZZX4F8VQXJ6A57R8P3SCB2W",
|
||||
"chain_id": 8453,
|
||||
"currency": "USDC",
|
||||
"amount": "100.00",
|
||||
"amount_atomic": "100000000",
|
||||
"decimals": 6,
|
||||
"cost_envelope": {
|
||||
"version": "edut.quote_cost_envelope.v1",
|
||||
"checkout_currency": "USDC",
|
||||
"checkout_decimals": 6,
|
||||
"checkout_total_atomic": "100000000",
|
||||
"checkout_total": "100",
|
||||
"provider_fee_policy": "edut_absorbed",
|
||||
"provider_fee_included": true,
|
||||
"provider_fee_estimate_status": "absorbed_by_edut",
|
||||
"provider_fee_estimate_atomic": "0",
|
||||
"network_fee_policy": "payer_wallet_pays_chain_gas",
|
||||
"network_fee_currency": "ETH",
|
||||
"network_fee_estimate_status": "wallet_estimate_required",
|
||||
"network_fee_estimate_atomic": "0"
|
||||
},
|
||||
"deadline": "2026-02-17T07:36:12Z",
|
||||
"contract_address": "0x1111111111111111111111111111111111111111",
|
||||
"method": "mintMembership",
|
||||
"method": "mintMembership(address)",
|
||||
"calldata": "0xdeadbeef",
|
||||
"value": "0x0",
|
||||
"owner_wallet": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5",
|
||||
|
||||
@ -222,7 +222,7 @@ components:
|
||||
description: If true, quote may bundle first-time membership fee into total.
|
||||
CheckoutQuoteResponse:
|
||||
type: object
|
||||
required: [quote_id, wallet, offer_id, currency, amount_atomic, total_amount_atomic, policy_hash, expires_at]
|
||||
required: [quote_id, wallet, offer_id, currency, amount_atomic, total_amount_atomic, decimals, cost_envelope, policy_hash, expires_at]
|
||||
properties:
|
||||
quote_id:
|
||||
type: string
|
||||
@ -255,6 +255,8 @@ components:
|
||||
type: string
|
||||
decimals:
|
||||
type: integer
|
||||
cost_envelope:
|
||||
$ref: '#/components/schemas/QuoteCostEnvelope'
|
||||
membership_activation_included:
|
||||
type: boolean
|
||||
line_items:
|
||||
@ -292,6 +294,55 @@ components:
|
||||
type: integer
|
||||
currency:
|
||||
type: string
|
||||
QuoteCostEnvelope:
|
||||
type: object
|
||||
required:
|
||||
- version
|
||||
- checkout_currency
|
||||
- checkout_decimals
|
||||
- checkout_total_atomic
|
||||
- checkout_total
|
||||
- provider_fee_policy
|
||||
- provider_fee_included
|
||||
- provider_fee_estimate_status
|
||||
- provider_fee_estimate_atomic
|
||||
- network_fee_policy
|
||||
- network_fee_currency
|
||||
- network_fee_estimate_status
|
||||
- network_fee_estimate_atomic
|
||||
properties:
|
||||
version:
|
||||
type: string
|
||||
enum: [edut.quote_cost_envelope.v1]
|
||||
checkout_currency:
|
||||
type: string
|
||||
checkout_decimals:
|
||||
type: integer
|
||||
checkout_total_atomic:
|
||||
type: string
|
||||
checkout_total:
|
||||
type: string
|
||||
provider_fee_policy:
|
||||
type: string
|
||||
enum: [edut_absorbed]
|
||||
provider_fee_included:
|
||||
type: boolean
|
||||
provider_fee_estimate_status:
|
||||
type: string
|
||||
enum: [absorbed_by_edut]
|
||||
provider_fee_estimate_atomic:
|
||||
type: string
|
||||
network_fee_policy:
|
||||
type: string
|
||||
enum: [payer_wallet_pays_chain_gas]
|
||||
network_fee_currency:
|
||||
type: string
|
||||
enum: [ETH]
|
||||
network_fee_estimate_status:
|
||||
type: string
|
||||
enum: [wallet_estimate_required]
|
||||
network_fee_estimate_atomic:
|
||||
type: string
|
||||
CheckoutConfirmRequest:
|
||||
type: object
|
||||
required: [quote_id, wallet, offer_id, tx_hash, chain_id]
|
||||
|
||||
@ -355,7 +355,7 @@ components:
|
||||
type: string
|
||||
MembershipQuoteResponse:
|
||||
type: object
|
||||
required: [quote_id, chain_id, currency, amount_atomic, deadline, contract_address]
|
||||
required: [quote_id, chain_id, currency, amount_atomic, decimals, cost_envelope, deadline, contract_address]
|
||||
properties:
|
||||
quote_id:
|
||||
type: string
|
||||
@ -364,12 +364,12 @@ components:
|
||||
currency:
|
||||
type: string
|
||||
enum: [USDC]
|
||||
amount:
|
||||
type: string
|
||||
amount_atomic:
|
||||
type: string
|
||||
decimals:
|
||||
type: integer
|
||||
cost_envelope:
|
||||
$ref: '#/components/schemas/QuoteCostEnvelope'
|
||||
deadline:
|
||||
type: string
|
||||
format: date-time
|
||||
@ -393,6 +393,55 @@ components:
|
||||
tx:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
QuoteCostEnvelope:
|
||||
type: object
|
||||
required:
|
||||
- version
|
||||
- checkout_currency
|
||||
- checkout_decimals
|
||||
- checkout_total_atomic
|
||||
- checkout_total
|
||||
- provider_fee_policy
|
||||
- provider_fee_included
|
||||
- provider_fee_estimate_status
|
||||
- provider_fee_estimate_atomic
|
||||
- network_fee_policy
|
||||
- network_fee_currency
|
||||
- network_fee_estimate_status
|
||||
- network_fee_estimate_atomic
|
||||
properties:
|
||||
version:
|
||||
type: string
|
||||
enum: [edut.quote_cost_envelope.v1]
|
||||
checkout_currency:
|
||||
type: string
|
||||
checkout_decimals:
|
||||
type: integer
|
||||
checkout_total_atomic:
|
||||
type: string
|
||||
checkout_total:
|
||||
type: string
|
||||
provider_fee_policy:
|
||||
type: string
|
||||
enum: [edut_absorbed]
|
||||
provider_fee_included:
|
||||
type: boolean
|
||||
provider_fee_estimate_status:
|
||||
type: string
|
||||
enum: [absorbed_by_edut]
|
||||
provider_fee_estimate_atomic:
|
||||
type: string
|
||||
network_fee_policy:
|
||||
type: string
|
||||
enum: [payer_wallet_pays_chain_gas]
|
||||
network_fee_currency:
|
||||
type: string
|
||||
enum: [ETH]
|
||||
network_fee_estimate_status:
|
||||
type: string
|
||||
enum: [wallet_estimate_required]
|
||||
network_fee_estimate_atomic:
|
||||
type: string
|
||||
MembershipConfirmRequest:
|
||||
type: object
|
||||
required: [designation_code, quote_id, tx_hash, address, chain_id]
|
||||
|
||||
@ -259,11 +259,26 @@ Response:
|
||||
"quote_id": "mq_...",
|
||||
"chain_id": 8453,
|
||||
"currency": "USDC",
|
||||
"amount": "100.00",
|
||||
"amount_atomic": "100000000",
|
||||
"decimals": 6,
|
||||
"cost_envelope": {
|
||||
"version": "edut.quote_cost_envelope.v1",
|
||||
"checkout_currency": "USDC",
|
||||
"checkout_decimals": 6,
|
||||
"checkout_total_atomic": "100000000",
|
||||
"checkout_total": "100",
|
||||
"provider_fee_policy": "edut_absorbed",
|
||||
"provider_fee_included": true,
|
||||
"provider_fee_estimate_status": "absorbed_by_edut",
|
||||
"provider_fee_estimate_atomic": "0",
|
||||
"network_fee_policy": "payer_wallet_pays_chain_gas",
|
||||
"network_fee_currency": "ETH",
|
||||
"network_fee_estimate_status": "wallet_estimate_required",
|
||||
"network_fee_estimate_atomic": "0"
|
||||
},
|
||||
"deadline": "2026-02-17T07:36:12Z",
|
||||
"contract_address": "0x...",
|
||||
"method": "mintMembership",
|
||||
"method": "mintMembership(address)",
|
||||
"calldata": "0x..."
|
||||
}
|
||||
```
|
||||
|
||||
@ -597,9 +597,14 @@ function formatAtomicAmount(atomicRaw, decimalsRaw) {
|
||||
|
||||
function formatQuoteDisplay(quote) {
|
||||
if (!quote || typeof quote !== 'object') return null;
|
||||
const currency = quote.currency ? String(quote.currency) : '';
|
||||
const envelope = (quote.cost_envelope && typeof quote.cost_envelope === 'object') ? quote.cost_envelope : null;
|
||||
const currency = envelope && envelope.checkout_currency
|
||||
? String(envelope.checkout_currency)
|
||||
: (quote.currency ? String(quote.currency) : '');
|
||||
let amount = null;
|
||||
if (quote.amount !== undefined && quote.amount !== null) {
|
||||
if (envelope && envelope.checkout_total !== undefined && envelope.checkout_total !== null) {
|
||||
amount = String(envelope.checkout_total);
|
||||
} else if (quote.amount !== undefined && quote.amount !== null) {
|
||||
amount = String(quote.amount);
|
||||
} else if (quote.display_amount !== undefined && quote.display_amount !== null) {
|
||||
amount = String(quote.display_amount);
|
||||
|
||||
@ -795,9 +795,12 @@
|
||||
|
||||
const quotePayload = await postJson('/marketplace/checkout/quote', payload);
|
||||
const quoteId = quotePayload.quote_id || 'unknown';
|
||||
const envelope = (quotePayload.cost_envelope && typeof quotePayload.cost_envelope === 'object')
|
||||
? quotePayload.cost_envelope
|
||||
: null;
|
||||
const currency = (envelope && envelope.checkout_currency) || quotePayload.currency || 'unknown';
|
||||
const amount = quotePayload.amount || quotePayload.amount_atomic || 'unknown';
|
||||
const total = quotePayload.total_amount || quotePayload.total_amount_atomic || amount;
|
||||
const currency = quotePayload.currency || 'unknown';
|
||||
const total = (envelope && envelope.checkout_total) || quotePayload.total_amount || quotePayload.total_amount_atomic || amount;
|
||||
const lines = Array.isArray(quotePayload.line_items) ? quotePayload.line_items : [];
|
||||
let breakdown = '';
|
||||
if (lines.length > 0) {
|
||||
@ -808,10 +811,16 @@
|
||||
return '- ' + label + ': ' + value + ' ' + unit;
|
||||
}).join('\\n');
|
||||
}
|
||||
let feePolicy = '';
|
||||
if (envelope) {
|
||||
feePolicy = '\\nFee policy: provider=' + (envelope.provider_fee_policy || 'unknown') +
|
||||
', network=' + (envelope.network_fee_policy || 'unknown') + '.';
|
||||
}
|
||||
setCheckoutLog(
|
||||
'Quote ready for ' + state.selectedOfferId + ': ' + quoteId +
|
||||
' (license ' + amount + ' ' + currency + ', total ' + total + ' ' + currency + ').' +
|
||||
breakdown
|
||||
breakdown +
|
||||
feePolicy
|
||||
);
|
||||
} catch (err) {
|
||||
setCheckoutLog('Quote request failed: ' + err.message + '. API wiring pending.');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user