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_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 fails closed without chain receipt verification)
|
||||
|
||||
### Membership
|
||||
|
||||
|
||||
@ -404,6 +404,10 @@ func (a *app) handleMembershipConfirm(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusBadRequest, "quote context mismatch")
|
||||
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 {
|
||||
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) {
|
||||
a, cfg, cleanup := newTestApp(t)
|
||||
defer cleanup()
|
||||
|
||||
@ -25,6 +25,7 @@ type Config struct {
|
||||
MintAmountAtomic string
|
||||
MintDecimals int
|
||||
ChainRPCURL string
|
||||
RequireOnchainTxVerify bool
|
||||
GovernanceRuntimeVersion string
|
||||
GovernancePackageURL string
|
||||
GovernancePackageHash string
|
||||
@ -53,6 +54,7 @@ func loadConfig() Config {
|
||||
MintAmountAtomic: env("SECRET_API_MINT_AMOUNT_ATOMIC", "5000000000000000"),
|
||||
MintDecimals: envInt("SECRET_API_MINT_DECIMALS", 18),
|
||||
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"),
|
||||
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")),
|
||||
@ -81,3 +83,18 @@ func envInt(key string, fallback int) int {
|
||||
}
|
||||
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