Add strict on-chain verification mode for membership confirmations
This commit is contained in:
parent
70c2a4fe8f
commit
a03eaaa493
@ -72,6 +72,7 @@ Company-first sponsor path is also supported:
|
|||||||
- `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 fails closed without chain receipt verification)
|
||||||
|
|
||||||
### Membership
|
### Membership
|
||||||
|
|
||||||
|
|||||||
@ -404,6 +404,10 @@ func (a *app) handleMembershipConfirm(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusBadRequest, "quote context mismatch")
|
writeError(w, http.StatusBadRequest, "quote context mismatch")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if a.cfg.RequireOnchainTxVerify && strings.TrimSpace(a.cfg.ChainRPCURL) == "" {
|
||||||
|
writeErrorCode(w, http.StatusServiceUnavailable, "chain_verification_unavailable", "chain rpc required for membership confirmation")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := verifyMintedOnChain(context.Background(), a.cfg, req.TxHash, address); err != nil {
|
if err := verifyMintedOnChain(context.Background(), a.cfg, req.TxHash, address); err != nil {
|
||||||
writeError(w, http.StatusConflict, fmt.Sprintf("tx verification pending/failed: %v", err))
|
writeError(w, http.StatusConflict, fmt.Sprintf("tx verification pending/failed: %v", err))
|
||||||
|
|||||||
@ -150,6 +150,59 @@ func TestMembershipCompanySponsorWithoutOwnerProof(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMembershipConfirmRequiresChainRPCWhenStrictVerificationEnabled(t *testing.T) {
|
||||||
|
a, cfg, cleanup := newTestApp(t)
|
||||||
|
defer cleanup()
|
||||||
|
a.cfg.RequireOnchainTxVerify = true
|
||||||
|
a.cfg.ChainRPCURL = ""
|
||||||
|
|
||||||
|
ownerKey := mustKey(t)
|
||||||
|
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
|
||||||
|
|
||||||
|
intentRes := postJSONExpect[tWalletIntentResponse](t, a, "/secret/wallet/intent", walletIntentRequest{
|
||||||
|
Address: ownerAddr,
|
||||||
|
Origin: "https://edut.ai",
|
||||||
|
Locale: "en",
|
||||||
|
ChainID: cfg.ChainID,
|
||||||
|
}, http.StatusOK)
|
||||||
|
|
||||||
|
issuedAt, err := time.Parse(time.RFC3339Nano, intentRes.IssuedAt)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse issued_at: %v", err)
|
||||||
|
}
|
||||||
|
td := buildTypedData(cfg, designationRecord{
|
||||||
|
Code: intentRes.DesignationCode,
|
||||||
|
DisplayToken: intentRes.DisplayToken,
|
||||||
|
Nonce: intentRes.Nonce,
|
||||||
|
IssuedAt: issuedAt,
|
||||||
|
Origin: "https://edut.ai",
|
||||||
|
})
|
||||||
|
sig := signTypedData(t, ownerKey, td)
|
||||||
|
verifyRes := postJSONExpect[tWalletVerifyResponse](t, a, "/secret/wallet/verify", walletVerifyRequest{
|
||||||
|
IntentID: intentRes.IntentID,
|
||||||
|
Address: ownerAddr,
|
||||||
|
ChainID: cfg.ChainID,
|
||||||
|
Signature: sig,
|
||||||
|
}, http.StatusOK)
|
||||||
|
|
||||||
|
quote := postJSONExpect[membershipQuoteResponse](t, a, "/secret/membership/quote", membershipQuoteRequest{
|
||||||
|
DesignationCode: verifyRes.DesignationCode,
|
||||||
|
Address: ownerAddr,
|
||||||
|
ChainID: cfg.ChainID,
|
||||||
|
}, http.StatusOK)
|
||||||
|
|
||||||
|
fail := postJSONExpect[map[string]string](t, a, "/secret/membership/confirm", membershipConfirmRequest{
|
||||||
|
DesignationCode: verifyRes.DesignationCode,
|
||||||
|
QuoteID: quote.QuoteID,
|
||||||
|
TxHash: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
|
Address: ownerAddr,
|
||||||
|
ChainID: cfg.ChainID,
|
||||||
|
}, http.StatusServiceUnavailable)
|
||||||
|
if fail["code"] != "chain_verification_unavailable" {
|
||||||
|
t.Fatalf("expected chain_verification_unavailable code, got %+v", fail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGovernanceInstallOwnerOnlyAndConfirm(t *testing.T) {
|
func TestGovernanceInstallOwnerOnlyAndConfirm(t *testing.T) {
|
||||||
a, cfg, cleanup := newTestApp(t)
|
a, cfg, cleanup := newTestApp(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|||||||
@ -25,6 +25,7 @@ type Config struct {
|
|||||||
MintAmountAtomic string
|
MintAmountAtomic string
|
||||||
MintDecimals int
|
MintDecimals int
|
||||||
ChainRPCURL string
|
ChainRPCURL string
|
||||||
|
RequireOnchainTxVerify bool
|
||||||
GovernanceRuntimeVersion string
|
GovernanceRuntimeVersion string
|
||||||
GovernancePackageURL string
|
GovernancePackageURL string
|
||||||
GovernancePackageHash string
|
GovernancePackageHash string
|
||||||
@ -53,6 +54,7 @@ func loadConfig() Config {
|
|||||||
MintAmountAtomic: env("SECRET_API_MINT_AMOUNT_ATOMIC", "5000000000000000"),
|
MintAmountAtomic: env("SECRET_API_MINT_AMOUNT_ATOMIC", "5000000000000000"),
|
||||||
MintDecimals: envInt("SECRET_API_MINT_DECIMALS", 18),
|
MintDecimals: envInt("SECRET_API_MINT_DECIMALS", 18),
|
||||||
ChainRPCURL: env("SECRET_API_CHAIN_RPC_URL", ""),
|
ChainRPCURL: env("SECRET_API_CHAIN_RPC_URL", ""),
|
||||||
|
RequireOnchainTxVerify: envBool("SECRET_API_REQUIRE_ONCHAIN_TX_VERIFICATION", false),
|
||||||
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")),
|
||||||
@ -81,3 +83,18 @@ func envInt(key string, fallback int) int {
|
|||||||
}
|
}
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func envBool(key string, fallback bool) bool {
|
||||||
|
raw := strings.ToLower(strings.TrimSpace(os.Getenv(key)))
|
||||||
|
if raw == "" {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
switch raw {
|
||||||
|
case "1", "true", "yes", "on":
|
||||||
|
return true
|
||||||
|
case "0", "false", "no", "off":
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user