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.
|
1. Store checkout requires active membership.
|
||||||
2. Workspace admin install/support actions require `onramp_attested` assurance.
|
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
|
## Key Environment Variables
|
||||||
|
|
||||||
### Core
|
### Core
|
||||||
@ -127,10 +137,14 @@ Policy gates:
|
|||||||
- `SECRET_API_LISTEN_ADDR` (default `:8080`)
|
- `SECRET_API_LISTEN_ADDR` (default `:8080`)
|
||||||
- `SECRET_API_DB_PATH` (default `./secret.db`)
|
- `SECRET_API_DB_PATH` (default `./secret.db`)
|
||||||
- `SECRET_API_ALLOWED_ORIGIN` (default `https://edut.ai`)
|
- `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_MEMBER_POLL_INTERVAL_SECONDS` (default `30`)
|
||||||
- `SECRET_API_CHAIN_ID` (default `84532`)
|
- `SECRET_API_CHAIN_ID` (default `84532`)
|
||||||
- `SECRET_API_CHAIN_RPC_URL` (optional, enables on-chain tx receipt verification)
|
- `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)
|
- `SECRET_API_ENTITLEMENT_CONTRACT` (optional; when set, marketplace quote emits purchase calldata for entitlement settlement contract)
|
||||||
|
|
||||||
### Membership
|
### Membership
|
||||||
|
|||||||
@ -470,6 +470,7 @@ func (a *app) handleMembershipQuote(w http.ResponseWriter, r *http.Request) {
|
|||||||
Currency: quote.Currency,
|
Currency: quote.Currency,
|
||||||
AmountAtomic: quote.AmountAtomic,
|
AmountAtomic: quote.AmountAtomic,
|
||||||
Decimals: quote.Decimals,
|
Decimals: quote.Decimals,
|
||||||
|
CostEnvelope: newQuoteCostEnvelope(quote.Currency, quote.Decimals, quote.AmountAtomic),
|
||||||
Deadline: quote.ExpiresAt.Format(time.RFC3339Nano),
|
Deadline: quote.ExpiresAt.Format(time.RFC3339Nano),
|
||||||
ContractAddress: quote.ContractAddress,
|
ContractAddress: quote.ContractAddress,
|
||||||
Method: quote.Method,
|
Method: quote.Method,
|
||||||
|
|||||||
@ -88,6 +88,7 @@ func TestMembershipDistinctPayerProof(t *testing.T) {
|
|||||||
if quote.Tx["from"] != payerAddr {
|
if quote.Tx["from"] != payerAddr {
|
||||||
t.Fatalf("tx.from mismatch: %+v", quote.Tx)
|
t.Fatalf("tx.from mismatch: %+v", quote.Tx)
|
||||||
}
|
}
|
||||||
|
assertQuoteCostEnvelope(t, quote.CostEnvelope, quote.Currency, quote.Decimals, quote.AmountAtomic)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMembershipCompanySponsorWithoutOwnerProof(t *testing.T) {
|
func TestMembershipCompanySponsorWithoutOwnerProof(t *testing.T) {
|
||||||
@ -1093,6 +1094,7 @@ func TestMarketplaceCheckoutBundlesMembershipAndMintsEntitlement(t *testing.T) {
|
|||||||
if quote.MerchantID != defaultMarketplaceMerchantID {
|
if quote.MerchantID != defaultMarketplaceMerchantID {
|
||||||
t.Fatalf("expected default merchant id, got %+v", quote)
|
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{
|
confirm := postJSONExpect[marketplaceCheckoutConfirmResponse](t, a, "/marketplace/checkout/confirm", marketplaceCheckoutConfirmRequest{
|
||||||
QuoteID: quote.QuoteID,
|
QuoteID: quote.QuoteID,
|
||||||
@ -2025,6 +2027,49 @@ func mustKey(t *testing.T) *ecdsa.PrivateKey {
|
|||||||
return key
|
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 {
|
func signTypedData(t *testing.T, key *ecdsa.PrivateKey, typedData apitypes.TypedData) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
hash, _, err := apitypes.TypedDataAndHash(typedData)
|
hash, _, err := apitypes.TypedDataAndHash(typedData)
|
||||||
|
|||||||
@ -13,6 +13,7 @@ type Config struct {
|
|||||||
ListenAddr string
|
ListenAddr string
|
||||||
DBPath string
|
DBPath string
|
||||||
AllowedOrigin string
|
AllowedOrigin string
|
||||||
|
DeploymentClass string
|
||||||
MemberPollIntervalSec int
|
MemberPollIntervalSec int
|
||||||
IntentTTL time.Duration
|
IntentTTL time.Duration
|
||||||
QuoteTTL time.Duration
|
QuoteTTL time.Duration
|
||||||
@ -45,6 +46,7 @@ func loadConfig() Config {
|
|||||||
ListenAddr: env("SECRET_API_LISTEN_ADDR", ":8080"),
|
ListenAddr: env("SECRET_API_LISTEN_ADDR", ":8080"),
|
||||||
DBPath: env("SECRET_API_DB_PATH", "./secret.db"),
|
DBPath: env("SECRET_API_DB_PATH", "./secret.db"),
|
||||||
AllowedOrigin: env("SECRET_API_ALLOWED_ORIGIN", "https://edut.ai"),
|
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),
|
MemberPollIntervalSec: envInt("SECRET_API_MEMBER_POLL_INTERVAL_SECONDS", 30),
|
||||||
IntentTTL: time.Duration(envInt("SECRET_API_INTENT_TTL_SECONDS", 900)) * time.Second,
|
IntentTTL: time.Duration(envInt("SECRET_API_INTENT_TTL_SECONDS", 900)) * time.Second,
|
||||||
QuoteTTL: time.Duration(envInt("SECRET_API_QUOTE_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"),
|
MintAmountAtomic: env("SECRET_API_MINT_AMOUNT_ATOMIC", "100000000"),
|
||||||
MintDecimals: envInt("SECRET_API_MINT_DECIMALS", 6),
|
MintDecimals: envInt("SECRET_API_MINT_DECIMALS", 6),
|
||||||
ChainRPCURL: env("SECRET_API_CHAIN_RPC_URL", ""),
|
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"),
|
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"),
|
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")),
|
GovernancePackageHash: strings.ToLower(env("SECRET_API_GOV_PACKAGE_HASH", "sha256:pending")),
|
||||||
@ -98,12 +100,24 @@ func (c Config) Validate() error {
|
|||||||
if c.WalletSessionTTL <= 0 {
|
if c.WalletSessionTTL <= 0 {
|
||||||
return fmt.Errorf("SECRET_API_WALLET_SESSION_TTL_SECONDS must be positive")
|
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) == "" {
|
if c.RequireOnchainTxVerify && strings.TrimSpace(c.ChainRPCURL) == "" {
|
||||||
return fmt.Errorf("SECRET_API_REQUIRE_ONCHAIN_TX_VERIFICATION requires SECRET_API_CHAIN_RPC_URL")
|
return fmt.Errorf("SECRET_API_REQUIRE_ONCHAIN_TX_VERIFICATION requires SECRET_API_CHAIN_RPC_URL")
|
||||||
}
|
}
|
||||||
return nil
|
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 {
|
func env(key, fallback string) string {
|
||||||
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
|
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
|
||||||
return v
|
return v
|
||||||
@ -128,6 +142,11 @@ func envBool(key string, fallback bool) bool {
|
|||||||
if raw == "" {
|
if raw == "" {
|
||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
|
return parseBool(raw, fallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseBool(raw string, fallback bool) bool {
|
||||||
|
raw = strings.ToLower(strings.TrimSpace(raw))
|
||||||
switch raw {
|
switch raw {
|
||||||
case "1", "true", "yes", "on":
|
case "1", "true", "yes", "on":
|
||||||
return true
|
return true
|
||||||
@ -137,3 +156,21 @@ func envBool(key string, fallback bool) bool {
|
|||||||
return fallback
|
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"
|
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) {
|
func TestConfigValidateAllowsDefaultLocalConfig(t *testing.T) {
|
||||||
cfg := loadConfig()
|
cfg := loadConfigIsolated(t)
|
||||||
cfg.ChainID = 84532
|
cfg.ChainID = 84532
|
||||||
cfg.RequireOnchainTxVerify = false
|
cfg.RequireOnchainTxVerify = false
|
||||||
cfg.ChainRPCURL = ""
|
cfg.ChainRPCURL = ""
|
||||||
@ -13,7 +20,7 @@ func TestConfigValidateAllowsDefaultLocalConfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigValidateRejectsStrictVerificationWithoutRPC(t *testing.T) {
|
func TestConfigValidateRejectsStrictVerificationWithoutRPC(t *testing.T) {
|
||||||
cfg := loadConfig()
|
cfg := loadConfigIsolated(t)
|
||||||
cfg.RequireOnchainTxVerify = true
|
cfg.RequireOnchainTxVerify = true
|
||||||
cfg.ChainRPCURL = ""
|
cfg.ChainRPCURL = ""
|
||||||
if err := cfg.Validate(); err == nil {
|
if err := cfg.Validate(); err == nil {
|
||||||
@ -22,7 +29,7 @@ func TestConfigValidateRejectsStrictVerificationWithoutRPC(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigValidateRejectsNonPositiveChainID(t *testing.T) {
|
func TestConfigValidateRejectsNonPositiveChainID(t *testing.T) {
|
||||||
cfg := loadConfig()
|
cfg := loadConfigIsolated(t)
|
||||||
cfg.ChainID = 0
|
cfg.ChainID = 0
|
||||||
if err := cfg.Validate(); err == nil {
|
if err := cfg.Validate(); err == nil {
|
||||||
t.Fatalf("expected chain id validation failure")
|
t.Fatalf("expected chain id validation failure")
|
||||||
@ -30,7 +37,7 @@ func TestConfigValidateRejectsNonPositiveChainID(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigValidateAllowsETHWhenDecimalsMatch(t *testing.T) {
|
func TestConfigValidateAllowsETHWhenDecimalsMatch(t *testing.T) {
|
||||||
cfg := loadConfig()
|
cfg := loadConfigIsolated(t)
|
||||||
cfg.MintCurrency = "ETH"
|
cfg.MintCurrency = "ETH"
|
||||||
cfg.MintDecimals = 18
|
cfg.MintDecimals = 18
|
||||||
cfg.MintAmountAtomic = "1"
|
cfg.MintAmountAtomic = "1"
|
||||||
@ -40,7 +47,7 @@ func TestConfigValidateAllowsETHWhenDecimalsMatch(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigValidateRejectsETHWhenDecimalsMismatch(t *testing.T) {
|
func TestConfigValidateRejectsETHWhenDecimalsMismatch(t *testing.T) {
|
||||||
cfg := loadConfig()
|
cfg := loadConfigIsolated(t)
|
||||||
cfg.MintCurrency = "ETH"
|
cfg.MintCurrency = "ETH"
|
||||||
cfg.MintDecimals = 6
|
cfg.MintDecimals = 6
|
||||||
if err := cfg.Validate(); err == nil {
|
if err := cfg.Validate(); err == nil {
|
||||||
@ -49,7 +56,7 @@ func TestConfigValidateRejectsETHWhenDecimalsMismatch(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigValidateRejectsUSDCWhenDecimalsMismatch(t *testing.T) {
|
func TestConfigValidateRejectsUSDCWhenDecimalsMismatch(t *testing.T) {
|
||||||
cfg := loadConfig()
|
cfg := loadConfigIsolated(t)
|
||||||
cfg.MintCurrency = "USDC"
|
cfg.MintCurrency = "USDC"
|
||||||
cfg.MintDecimals = 18
|
cfg.MintDecimals = 18
|
||||||
if err := cfg.Validate(); err == nil {
|
if err := cfg.Validate(); err == nil {
|
||||||
@ -58,7 +65,7 @@ func TestConfigValidateRejectsUSDCWhenDecimalsMismatch(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigValidateRejectsInvalidMintAmount(t *testing.T) {
|
func TestConfigValidateRejectsInvalidMintAmount(t *testing.T) {
|
||||||
cfg := loadConfig()
|
cfg := loadConfigIsolated(t)
|
||||||
cfg.MintAmountAtomic = "not-a-number"
|
cfg.MintAmountAtomic = "not-a-number"
|
||||||
if err := cfg.Validate(); err == nil {
|
if err := cfg.Validate(); err == nil {
|
||||||
t.Fatalf("expected mint amount validation failure")
|
t.Fatalf("expected mint amount validation failure")
|
||||||
@ -67,7 +74,7 @@ func TestConfigValidateRejectsInvalidMintAmount(t *testing.T) {
|
|||||||
|
|
||||||
func TestLoadConfigDefaultsWalletSessionRequired(t *testing.T) {
|
func TestLoadConfigDefaultsWalletSessionRequired(t *testing.T) {
|
||||||
t.Setenv("SECRET_API_REQUIRE_WALLET_SESSION", "")
|
t.Setenv("SECRET_API_REQUIRE_WALLET_SESSION", "")
|
||||||
cfg := loadConfig()
|
cfg := loadConfigIsolated(t)
|
||||||
if !cfg.RequireWalletSession {
|
if !cfg.RequireWalletSession {
|
||||||
t.Fatalf("expected wallet session to be required by default")
|
t.Fatalf("expected wallet session to be required by default")
|
||||||
}
|
}
|
||||||
@ -75,8 +82,36 @@ func TestLoadConfigDefaultsWalletSessionRequired(t *testing.T) {
|
|||||||
|
|
||||||
func TestLoadConfigWalletSessionCanBeDisabled(t *testing.T) {
|
func TestLoadConfigWalletSessionCanBeDisabled(t *testing.T) {
|
||||||
t.Setenv("SECRET_API_REQUIRE_WALLET_SESSION", "false")
|
t.Setenv("SECRET_API_REQUIRE_WALLET_SESSION", "false")
|
||||||
cfg := loadConfig()
|
cfg := loadConfigIsolated(t)
|
||||||
if cfg.RequireWalletSession {
|
if cfg.RequireWalletSession {
|
||||||
t.Fatalf("expected wallet session requirement to be disabled by env override")
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -501,6 +501,7 @@ func (a *app) handleMarketplaceCheckoutQuote(w http.ResponseWriter, r *http.Requ
|
|||||||
TotalAmount: formatAtomicAmount(quote.TotalAmountAtomic, quote.Decimals),
|
TotalAmount: formatAtomicAmount(quote.TotalAmountAtomic, quote.Decimals),
|
||||||
TotalAmountAtomic: quote.TotalAmountAtomic,
|
TotalAmountAtomic: quote.TotalAmountAtomic,
|
||||||
Decimals: quote.Decimals,
|
Decimals: quote.Decimals,
|
||||||
|
CostEnvelope: newQuoteCostEnvelope(quote.Currency, quote.Decimals, quote.TotalAmountAtomic),
|
||||||
MembershipActivationIncluded: quote.MembershipIncluded,
|
MembershipActivationIncluded: quote.MembershipIncluded,
|
||||||
LineItems: lineItems,
|
LineItems: lineItems,
|
||||||
PolicyHash: quote.PolicyHash,
|
PolicyHash: quote.PolicyHash,
|
||||||
|
|||||||
@ -79,6 +79,7 @@ type marketplaceCheckoutQuoteResponse struct {
|
|||||||
TotalAmount string `json:"total_amount"`
|
TotalAmount string `json:"total_amount"`
|
||||||
TotalAmountAtomic string `json:"total_amount_atomic"`
|
TotalAmountAtomic string `json:"total_amount_atomic"`
|
||||||
Decimals int `json:"decimals"`
|
Decimals int `json:"decimals"`
|
||||||
|
CostEnvelope quoteCostEnvelope `json:"cost_envelope"`
|
||||||
MembershipActivationIncluded bool `json:"membership_activation_included"`
|
MembershipActivationIncluded bool `json:"membership_activation_included"`
|
||||||
LineItems []marketplaceQuoteLineItem `json:"line_items"`
|
LineItems []marketplaceQuoteLineItem `json:"line_items"`
|
||||||
PolicyHash string `json:"policy_hash"`
|
PolicyHash string `json:"policy_hash"`
|
||||||
|
|||||||
@ -73,6 +73,7 @@ type membershipQuoteResponse struct {
|
|||||||
Currency string `json:"currency"`
|
Currency string `json:"currency"`
|
||||||
AmountAtomic string `json:"amount_atomic"`
|
AmountAtomic string `json:"amount_atomic"`
|
||||||
Decimals int `json:"decimals"`
|
Decimals int `json:"decimals"`
|
||||||
|
CostEnvelope quoteCostEnvelope `json:"cost_envelope"`
|
||||||
Deadline string `json:"deadline"`
|
Deadline string `json:"deadline"`
|
||||||
ContractAddress string `json:"contract_address"`
|
ContractAddress string `json:"contract_address"`
|
||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
|
|||||||
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": "1100.00",
|
||||||
"total_amount_atomic": "1100000000",
|
"total_amount_atomic": "1100000000",
|
||||||
"decimals": 6,
|
"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,
|
"membership_activation_included": true,
|
||||||
"line_items": [
|
"line_items": [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -160,12 +160,26 @@ Success (`200`):
|
|||||||
"quote_id": "mq_01HZZX4F8VQXJ6A57R8P3SCB2W",
|
"quote_id": "mq_01HZZX4F8VQXJ6A57R8P3SCB2W",
|
||||||
"chain_id": 8453,
|
"chain_id": 8453,
|
||||||
"currency": "USDC",
|
"currency": "USDC",
|
||||||
"amount": "100.00",
|
|
||||||
"amount_atomic": "100000000",
|
"amount_atomic": "100000000",
|
||||||
"decimals": 6,
|
"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",
|
"deadline": "2026-02-17T07:36:12Z",
|
||||||
"contract_address": "0x1111111111111111111111111111111111111111",
|
"contract_address": "0x1111111111111111111111111111111111111111",
|
||||||
"method": "mintMembership",
|
"method": "mintMembership(address)",
|
||||||
"calldata": "0xdeadbeef",
|
"calldata": "0xdeadbeef",
|
||||||
"value": "0x0",
|
"value": "0x0",
|
||||||
"owner_wallet": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5",
|
"owner_wallet": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5",
|
||||||
|
|||||||
@ -222,7 +222,7 @@ components:
|
|||||||
description: If true, quote may bundle first-time membership fee into total.
|
description: If true, quote may bundle first-time membership fee into total.
|
||||||
CheckoutQuoteResponse:
|
CheckoutQuoteResponse:
|
||||||
type: object
|
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:
|
properties:
|
||||||
quote_id:
|
quote_id:
|
||||||
type: string
|
type: string
|
||||||
@ -255,6 +255,8 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
decimals:
|
decimals:
|
||||||
type: integer
|
type: integer
|
||||||
|
cost_envelope:
|
||||||
|
$ref: '#/components/schemas/QuoteCostEnvelope'
|
||||||
membership_activation_included:
|
membership_activation_included:
|
||||||
type: boolean
|
type: boolean
|
||||||
line_items:
|
line_items:
|
||||||
@ -292,6 +294,55 @@ components:
|
|||||||
type: integer
|
type: integer
|
||||||
currency:
|
currency:
|
||||||
type: string
|
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:
|
CheckoutConfirmRequest:
|
||||||
type: object
|
type: object
|
||||||
required: [quote_id, wallet, offer_id, tx_hash, chain_id]
|
required: [quote_id, wallet, offer_id, tx_hash, chain_id]
|
||||||
|
|||||||
@ -355,7 +355,7 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
MembershipQuoteResponse:
|
MembershipQuoteResponse:
|
||||||
type: object
|
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:
|
properties:
|
||||||
quote_id:
|
quote_id:
|
||||||
type: string
|
type: string
|
||||||
@ -364,12 +364,12 @@ components:
|
|||||||
currency:
|
currency:
|
||||||
type: string
|
type: string
|
||||||
enum: [USDC]
|
enum: [USDC]
|
||||||
amount:
|
|
||||||
type: string
|
|
||||||
amount_atomic:
|
amount_atomic:
|
||||||
type: string
|
type: string
|
||||||
decimals:
|
decimals:
|
||||||
type: integer
|
type: integer
|
||||||
|
cost_envelope:
|
||||||
|
$ref: '#/components/schemas/QuoteCostEnvelope'
|
||||||
deadline:
|
deadline:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
@ -393,6 +393,55 @@ components:
|
|||||||
tx:
|
tx:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: true
|
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:
|
MembershipConfirmRequest:
|
||||||
type: object
|
type: object
|
||||||
required: [designation_code, quote_id, tx_hash, address, chain_id]
|
required: [designation_code, quote_id, tx_hash, address, chain_id]
|
||||||
|
|||||||
@ -259,11 +259,26 @@ Response:
|
|||||||
"quote_id": "mq_...",
|
"quote_id": "mq_...",
|
||||||
"chain_id": 8453,
|
"chain_id": 8453,
|
||||||
"currency": "USDC",
|
"currency": "USDC",
|
||||||
"amount": "100.00",
|
|
||||||
"amount_atomic": "100000000",
|
"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",
|
"deadline": "2026-02-17T07:36:12Z",
|
||||||
"contract_address": "0x...",
|
"contract_address": "0x...",
|
||||||
"method": "mintMembership",
|
"method": "mintMembership(address)",
|
||||||
"calldata": "0x..."
|
"calldata": "0x..."
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@ -597,9 +597,14 @@ function formatAtomicAmount(atomicRaw, decimalsRaw) {
|
|||||||
|
|
||||||
function formatQuoteDisplay(quote) {
|
function formatQuoteDisplay(quote) {
|
||||||
if (!quote || typeof quote !== 'object') return null;
|
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;
|
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);
|
amount = String(quote.amount);
|
||||||
} else if (quote.display_amount !== undefined && quote.display_amount !== null) {
|
} else if (quote.display_amount !== undefined && quote.display_amount !== null) {
|
||||||
amount = String(quote.display_amount);
|
amount = String(quote.display_amount);
|
||||||
|
|||||||
@ -795,9 +795,12 @@
|
|||||||
|
|
||||||
const quotePayload = await postJson('/marketplace/checkout/quote', payload);
|
const quotePayload = await postJson('/marketplace/checkout/quote', payload);
|
||||||
const quoteId = quotePayload.quote_id || 'unknown';
|
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 amount = quotePayload.amount || quotePayload.amount_atomic || 'unknown';
|
||||||
const total = quotePayload.total_amount || quotePayload.total_amount_atomic || amount;
|
const total = (envelope && envelope.checkout_total) || quotePayload.total_amount || quotePayload.total_amount_atomic || amount;
|
||||||
const currency = quotePayload.currency || 'unknown';
|
|
||||||
const lines = Array.isArray(quotePayload.line_items) ? quotePayload.line_items : [];
|
const lines = Array.isArray(quotePayload.line_items) ? quotePayload.line_items : [];
|
||||||
let breakdown = '';
|
let breakdown = '';
|
||||||
if (lines.length > 0) {
|
if (lines.length > 0) {
|
||||||
@ -808,10 +811,16 @@
|
|||||||
return '- ' + label + ': ' + value + ' ' + unit;
|
return '- ' + label + ': ' + value + ' ' + unit;
|
||||||
}).join('\\n');
|
}).join('\\n');
|
||||||
}
|
}
|
||||||
|
let feePolicy = '';
|
||||||
|
if (envelope) {
|
||||||
|
feePolicy = '\\nFee policy: provider=' + (envelope.provider_fee_policy || 'unknown') +
|
||||||
|
', network=' + (envelope.network_fee_policy || 'unknown') + '.';
|
||||||
|
}
|
||||||
setCheckoutLog(
|
setCheckoutLog(
|
||||||
'Quote ready for ' + state.selectedOfferId + ': ' + quoteId +
|
'Quote ready for ' + state.selectedOfferId + ': ' + quoteId +
|
||||||
' (license ' + amount + ' ' + currency + ', total ' + total + ' ' + currency + ').' +
|
' (license ' + amount + ' ' + currency + ', total ' + total + ' ' + currency + ').' +
|
||||||
breakdown
|
breakdown +
|
||||||
|
feePolicy
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setCheckoutLog('Quote request failed: ' + err.message + '. API wiring pending.');
|
setCheckoutLog('Quote request failed: ' + err.message + '. API wiring pending.');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user