Add live money readiness controls
This commit is contained in:
@@ -36,6 +36,7 @@ TERMS_VERSION=
|
||||
PRIVACY_VERSION=
|
||||
RISK_DISCLOSURE_VERSION=
|
||||
MARKET_POLICY_STORE=
|
||||
CONSENT_STORE=
|
||||
|
||||
# Wallet / deposit-wallet provider
|
||||
WALLET_PROVIDER=Circle
|
||||
@@ -74,6 +75,7 @@ SENTRY_ORG=
|
||||
SENTRY_PROJECT=
|
||||
SENTRY_WEBHOOK_URL=
|
||||
INCIDENT_WEBHOOK_URL=
|
||||
RISK_ADMIN_TOKEN=
|
||||
ADMIN_ALERT_EMAIL=
|
||||
CUSTOMER_SUPPORT_EMAIL=
|
||||
RISK_ADMIN_WALLET=
|
||||
|
||||
@@ -96,5 +96,15 @@ Provider event counts are visible at
|
||||
- Live Polymarket order placement.
|
||||
- Automatic agent trading with real funds.
|
||||
|
||||
## Newly wired backend endpoints
|
||||
|
||||
- Policies: `https://polymarket-site-eta.vercel.app/api/policies`
|
||||
- Consent recording: `https://polymarket-site-eta.vercel.app/api/consent`
|
||||
- Server risk profile: `https://polymarket-site-eta.vercel.app/api/risk-profile`
|
||||
- Incident alerts: `https://polymarket-site-eta.vercel.app/api/incident`
|
||||
|
||||
The consent and incident endpoints are intentionally gated: consent needs real
|
||||
approved policy versions, and incident alerts need `RISK_ADMIN_TOKEN`.
|
||||
|
||||
Those should stay locked until legal review, eligibility checks, signed-order
|
||||
execution, reconciliation, audit logs, and incident response are complete.
|
||||
|
||||
@@ -18,6 +18,13 @@ interfaces visible without moving funds or placing orders.
|
||||
- `/api/webhooks/clerk`, `/api/webhooks/veriff`, and `/api/webhooks/circle`
|
||||
receive provider events and store them in Neon.
|
||||
- `/api/provider-events` reports provider event counts for a quick health check.
|
||||
- `/api/policies` exposes the active policy/readiness state.
|
||||
- `/api/consent` records versioned user consent after approved versions are
|
||||
configured.
|
||||
- `/api/risk-profile` stores wallet, jurisdiction, allocation, and agent
|
||||
permission settings server-side.
|
||||
- `/api/incident` records incident events and can notify the configured incident
|
||||
webhook after `RISK_ADMIN_TOKEN` is set.
|
||||
- The Live Money tab shows launch checks, wallet/deposit settings, agent risk
|
||||
limits, provider webhook URLs, a dry-run order console, and an audit preview.
|
||||
|
||||
|
||||
+101
@@ -52,6 +52,31 @@ export async function ensureSchema() {
|
||||
)
|
||||
`;
|
||||
|
||||
await db`
|
||||
create table if not exists user_consents (
|
||||
id bigserial primary key,
|
||||
user_id text not null,
|
||||
terms_version text not null,
|
||||
privacy_version text not null,
|
||||
risk_disclosure_version text not null,
|
||||
jurisdiction text,
|
||||
payload jsonb not null default '{}'::jsonb,
|
||||
accepted_at timestamptz not null default now()
|
||||
)
|
||||
`;
|
||||
|
||||
await db`
|
||||
create table if not exists risk_profiles (
|
||||
user_id text primary key,
|
||||
wallet_address text,
|
||||
jurisdiction text,
|
||||
deposit_limit numeric not null default 0,
|
||||
withdraw_reserve numeric not null default 0,
|
||||
agent_permissions jsonb not null default '{}'::jsonb,
|
||||
updated_at timestamptz not null default now()
|
||||
)
|
||||
`;
|
||||
|
||||
schemaReady = true;
|
||||
}
|
||||
|
||||
@@ -93,6 +118,82 @@ export async function recordAuditEvent(eventType, detail) {
|
||||
`;
|
||||
}
|
||||
|
||||
export async function recordUserConsent(consent) {
|
||||
await ensureSchema();
|
||||
const db = sql();
|
||||
const rows = await db`
|
||||
insert into user_consents (
|
||||
user_id,
|
||||
terms_version,
|
||||
privacy_version,
|
||||
risk_disclosure_version,
|
||||
jurisdiction,
|
||||
payload
|
||||
) values (
|
||||
${consent.userId},
|
||||
${consent.termsVersion},
|
||||
${consent.privacyVersion},
|
||||
${consent.riskDisclosureVersion},
|
||||
${consent.jurisdiction || null},
|
||||
${JSON.stringify(consent.payload || {})}::jsonb
|
||||
)
|
||||
returning id, accepted_at
|
||||
`;
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
export async function upsertRiskProfile(profile) {
|
||||
await ensureSchema();
|
||||
const db = sql();
|
||||
const rows = await db`
|
||||
insert into risk_profiles (
|
||||
user_id,
|
||||
wallet_address,
|
||||
jurisdiction,
|
||||
deposit_limit,
|
||||
withdraw_reserve,
|
||||
agent_permissions,
|
||||
updated_at
|
||||
) values (
|
||||
${profile.userId},
|
||||
${profile.walletAddress || null},
|
||||
${profile.jurisdiction || null},
|
||||
${Number(profile.depositLimit || 0)},
|
||||
${Number(profile.withdrawReserve || 0)},
|
||||
${JSON.stringify(profile.agentPermissions || {})}::jsonb,
|
||||
now()
|
||||
)
|
||||
on conflict (user_id) do update set
|
||||
wallet_address = excluded.wallet_address,
|
||||
jurisdiction = excluded.jurisdiction,
|
||||
deposit_limit = excluded.deposit_limit,
|
||||
withdraw_reserve = excluded.withdraw_reserve,
|
||||
agent_permissions = excluded.agent_permissions,
|
||||
updated_at = now()
|
||||
returning user_id, updated_at
|
||||
`;
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
export async function latestRiskProfile(userId) {
|
||||
await ensureSchema();
|
||||
const db = sql();
|
||||
const rows = await db`
|
||||
select
|
||||
user_id,
|
||||
wallet_address,
|
||||
jurisdiction,
|
||||
deposit_limit,
|
||||
withdraw_reserve,
|
||||
agent_permissions,
|
||||
updated_at
|
||||
from risk_profiles
|
||||
where user_id = ${userId}
|
||||
limit 1
|
||||
`;
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
export async function providerEventSummary() {
|
||||
await ensureSchema();
|
||||
const db = sql();
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { recordAuditEvent, recordUserConsent } from "./_db.js";
|
||||
|
||||
function activeVersions() {
|
||||
return {
|
||||
termsVersion: process.env.TERMS_VERSION || "",
|
||||
privacyVersion: process.env.PRIVACY_VERSION || "",
|
||||
riskDisclosureVersion: process.env.RISK_DISCLOSURE_VERSION || "",
|
||||
};
|
||||
}
|
||||
|
||||
export default async function handler(req, res) {
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
|
||||
if (req.method !== "POST") {
|
||||
res.setHeader("Allow", "POST");
|
||||
return res.status(405).json({ ok: false, error: "Method not allowed" });
|
||||
}
|
||||
|
||||
try {
|
||||
const body = typeof req.body === "string" ? JSON.parse(req.body || "{}") : (req.body || {});
|
||||
const userId = String(body.user_id || "").trim();
|
||||
const jurisdiction = String(body.jurisdiction || "").trim();
|
||||
const accepted = Boolean(body.accept_terms && body.accept_privacy && body.accept_risk);
|
||||
const versions = activeVersions();
|
||||
const missing = [];
|
||||
|
||||
if (!userId) missing.push("user_id");
|
||||
if (!accepted) missing.push("accept_terms, accept_privacy, and accept_risk");
|
||||
if (!versions.termsVersion) missing.push("TERMS_VERSION");
|
||||
if (!versions.privacyVersion) missing.push("PRIVACY_VERSION");
|
||||
if (!versions.riskDisclosureVersion) missing.push("RISK_DISCLOSURE_VERSION");
|
||||
|
||||
if (missing.length) {
|
||||
return res.status(400).json({ ok: false, error: "Consent is not ready to record", missing });
|
||||
}
|
||||
|
||||
const recorded = await recordUserConsent({
|
||||
userId,
|
||||
jurisdiction,
|
||||
...versions,
|
||||
payload: {
|
||||
source: "live_money_readiness",
|
||||
user_agent: req.headers["user-agent"] || "",
|
||||
},
|
||||
});
|
||||
await recordAuditEvent("USER_CONSENT_RECORDED", { user_id: userId, jurisdiction, ...versions });
|
||||
|
||||
return res.status(200).json({ ok: true, recorded });
|
||||
} catch (err) {
|
||||
return res.status(500).json({ ok: false, error: err?.message || "Consent recording failed" });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { recordAuditEvent } from "./_db.js";
|
||||
|
||||
async function notifyIncidentWebhook(payload) {
|
||||
const url = process.env.INCIDENT_WEBHOOK_URL || process.env.SENTRY_WEBHOOK_URL;
|
||||
if (!url) return { sent: false, reason: "No incident webhook is configured" };
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
return { sent: response.ok, status: response.status };
|
||||
}
|
||||
|
||||
export default async function handler(req, res) {
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
|
||||
if (req.method !== "POST") {
|
||||
res.setHeader("Allow", "POST");
|
||||
return res.status(405).json({ ok: false, error: "Method not allowed" });
|
||||
}
|
||||
|
||||
try {
|
||||
const adminToken = process.env.RISK_ADMIN_TOKEN || "";
|
||||
if (adminToken && req.headers["x-admin-token"] !== adminToken) {
|
||||
return res.status(401).json({ ok: false, error: "Invalid admin token" });
|
||||
}
|
||||
if (!adminToken) {
|
||||
return res.status(503).json({ ok: false, error: "Incident endpoint is locked until RISK_ADMIN_TOKEN is configured" });
|
||||
}
|
||||
|
||||
const body = typeof req.body === "string" ? JSON.parse(req.body || "{}") : (req.body || {});
|
||||
const payload = {
|
||||
at: new Date().toISOString(),
|
||||
severity: String(body.severity || "warning"),
|
||||
title: String(body.title || "Polymarket Arena incident"),
|
||||
detail: String(body.detail || ""),
|
||||
live_trading_enabled: process.env.LIVE_TRADING_ENABLED === "true",
|
||||
};
|
||||
|
||||
await recordAuditEvent("INCIDENT_REPORTED", payload);
|
||||
const notification = await notifyIncidentWebhook(payload);
|
||||
return res.status(200).json({ ok: true, notification, payload });
|
||||
} catch (err) {
|
||||
return res.status(500).json({ ok: false, error: err?.message || "Incident report failed" });
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { recordAuditEvent } from "./_db.js";
|
||||
|
||||
const REQUIRED_ENV = [
|
||||
["PRODUCTION_APP_URL", "Production app URL"],
|
||||
["AUTH_PROVIDER", "Real account authentication provider"],
|
||||
@@ -16,6 +18,7 @@ const REQUIRED_ENV = [
|
||||
["PRIVACY_VERSION", "Approved privacy policy version"],
|
||||
["RISK_DISCLOSURE_VERSION", "Approved risk disclosure version"],
|
||||
["MARKET_POLICY_STORE", "Market eligibility and category policy store"],
|
||||
["CONSENT_STORE", "Versioned user consent store"],
|
||||
["PAYMENTS_PROVIDER", "Deposit and withdrawal provider"],
|
||||
["PAYMENTS_API_KEY", "Payments provider API key"],
|
||||
["PAYMENTS_WEBHOOK_SECRET", "Payments webhook secret"],
|
||||
@@ -36,6 +39,7 @@ const REQUIRED_ENV = [
|
||||
["AUDIT_LOG_STORE", "Durable audit log store"],
|
||||
["MONITORING_DSN", "Error and performance monitoring"],
|
||||
["INCIDENT_WEBHOOK_URL", "Incident alert webhook"],
|
||||
["RISK_ADMIN_TOKEN", "Risk admin incident-control token"],
|
||||
["ADMIN_ALERT_EMAIL", "Admin alert destination"],
|
||||
["CUSTOMER_SUPPORT_EMAIL", "Customer support contact"],
|
||||
];
|
||||
@@ -54,6 +58,7 @@ const ENV_ALIASES = {
|
||||
ACCOUNTING_EXPORT_STORE: ["DATABASE_URL", "NEON_DATABASE_URL"],
|
||||
RATE_LIMIT_STORE: ["DATABASE_URL", "NEON_DATABASE_URL"],
|
||||
AUDIT_LOG_STORE: ["DATABASE_URL", "NEON_DATABASE_URL"],
|
||||
CONSENT_STORE: ["DATABASE_URL", "NEON_DATABASE_URL"],
|
||||
MONITORING_DSN: ["SENTRY_DSN"],
|
||||
INCIDENT_WEBHOOK_URL: ["SENTRY_WEBHOOK_URL"],
|
||||
};
|
||||
@@ -223,6 +228,7 @@ export default async function handler(req, res) {
|
||||
};
|
||||
|
||||
if (!status.live_trading_enabled) {
|
||||
await recordAuditEvent("ORDER_INTENT_BLOCKED", auditEvent);
|
||||
return res.status(423).json({
|
||||
ok: false,
|
||||
dry_run: true,
|
||||
@@ -232,6 +238,7 @@ export default async function handler(req, res) {
|
||||
});
|
||||
}
|
||||
|
||||
await recordAuditEvent("ORDER_INTENT_NOT_IMPLEMENTED", auditEvent);
|
||||
return res.status(501).json({
|
||||
ok: false,
|
||||
dry_run: true,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
function envValue(key, fallback = "") {
|
||||
return process.env[key] || fallback;
|
||||
}
|
||||
|
||||
function splitList(value) {
|
||||
return String(value || "")
|
||||
.split(",")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
const MARKET_RULES = [
|
||||
"No trading for restricted jurisdictions or users who fail eligibility checks.",
|
||||
"No market categories may be enabled until a written market policy is approved.",
|
||||
"Agent orders must obey per-user allocation caps, per-market caps, stop rules, and emergency pause.",
|
||||
"Users must be able to revoke agent permissions and withdraw available funds.",
|
||||
"Every signal, consent, order intent, fill, cancellation, deposit, and withdrawal must be audit logged.",
|
||||
];
|
||||
|
||||
export default async function handler(req, res) {
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
if (req.method !== "GET") {
|
||||
res.setHeader("Allow", "GET");
|
||||
return res.status(405).json({ ok: false, error: "Method not allowed" });
|
||||
}
|
||||
|
||||
const versions = {
|
||||
terms: envValue("TERMS_VERSION"),
|
||||
privacy: envValue("PRIVACY_VERSION"),
|
||||
risk_disclosure: envValue("RISK_DISCLOSURE_VERSION"),
|
||||
};
|
||||
const approved = Boolean(versions.terms && versions.privacy && versions.risk_disclosure && envValue("MARKET_POLICY_STORE"));
|
||||
|
||||
return res.status(200).json({
|
||||
ok: true,
|
||||
approved,
|
||||
status: approved ? "configured" : "draft_not_approved",
|
||||
versions,
|
||||
restricted_jurisdictions: splitList(envValue("RESTRICTED_JURISDICTIONS")),
|
||||
market_policy_store: envValue("MARKET_POLICY_STORE"),
|
||||
live_trading_enabled: process.env.LIVE_TRADING_ENABLED === "true",
|
||||
rules: MARKET_RULES,
|
||||
note: approved
|
||||
? "Policy versions are configured, but live trading still requires all other launch checks."
|
||||
: "Draft policy guardrails are available. Legal approval and versioned documents are still required before real money.",
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { latestRiskProfile, recordAuditEvent, upsertRiskProfile } from "./_db.js";
|
||||
|
||||
function validateProfile(profile) {
|
||||
const errors = [];
|
||||
if (!profile.userId) errors.push("user_id");
|
||||
if (Number(profile.depositLimit) < 0) errors.push("deposit_limit must be 0 or greater");
|
||||
if (Number(profile.withdrawReserve) < 0) errors.push("withdraw_reserve must be 0 or greater");
|
||||
|
||||
Object.entries(profile.agentPermissions || {}).forEach(([agentId, permission]) => {
|
||||
if (Number(permission.maxAllocation || 0) < 0) errors.push(`${agentId} maxAllocation must be 0 or greater`);
|
||||
const maxPositionPct = Number(permission.maxPositionPct || 0);
|
||||
if (maxPositionPct < 0 || maxPositionPct > 100) errors.push(`${agentId} maxPositionPct must be between 0 and 100`);
|
||||
if (permission.autoTrade && !permission.manualApprove) {
|
||||
errors.push(`${agentId} cannot enable unattended real-money trading before final approval`);
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
export default async function handler(req, res) {
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
|
||||
try {
|
||||
if (req.method === "GET") {
|
||||
const userId = String(req.query?.user_id || "").trim();
|
||||
if (!userId) return res.status(400).json({ ok: false, error: "user_id is required" });
|
||||
const profile = await latestRiskProfile(userId);
|
||||
return res.status(200).json({ ok: true, profile });
|
||||
}
|
||||
|
||||
if (req.method === "POST") {
|
||||
const body = typeof req.body === "string" ? JSON.parse(req.body || "{}") : (req.body || {});
|
||||
const profile = {
|
||||
userId: String(body.user_id || "").trim(),
|
||||
walletAddress: String(body.wallet_address || "").trim(),
|
||||
jurisdiction: String(body.jurisdiction || "").trim(),
|
||||
depositLimit: Number(body.deposit_limit || 0),
|
||||
withdrawReserve: Number(body.withdraw_reserve || 0),
|
||||
agentPermissions: body.agent_permissions && typeof body.agent_permissions === "object" ? body.agent_permissions : {},
|
||||
};
|
||||
const errors = validateProfile(profile);
|
||||
if (errors.length) return res.status(400).json({ ok: false, error: "Invalid risk profile", details: errors });
|
||||
|
||||
const saved = await upsertRiskProfile(profile);
|
||||
await recordAuditEvent("RISK_PROFILE_UPDATED", {
|
||||
user_id: profile.userId,
|
||||
jurisdiction: profile.jurisdiction,
|
||||
agent_count: Object.keys(profile.agentPermissions).length,
|
||||
});
|
||||
|
||||
return res.status(200).json({ ok: true, saved, live_trading_enabled: false });
|
||||
}
|
||||
|
||||
res.setHeader("Allow", "GET, POST");
|
||||
return res.status(405).json({ ok: false, error: "Method not allowed" });
|
||||
} catch (err) {
|
||||
return res.status(500).json({ ok: false, error: err?.message || "Risk profile request failed" });
|
||||
}
|
||||
}
|
||||
+77
-6
@@ -602,6 +602,25 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
|
||||
<div class="card-h"><h3>Provider webhooks</h3><span class="small muted">events are stored in Neon</span></div>
|
||||
<div id="liveWebhookStatus" class="small muted">Checking webhook setup...</div>
|
||||
</div>
|
||||
<div class="grid2">
|
||||
<div class="card">
|
||||
<div class="card-h"><h3>Policies & consent</h3><span class="small muted">required before deposits</span></div>
|
||||
<div id="livePolicyStatus" class="locked-panel">Checking policy status...</div>
|
||||
<div class="account-row">
|
||||
<label class="small muted"><input type="checkbox" id="liveAcceptTerms"> terms</label>
|
||||
<label class="small muted"><input type="checkbox" id="liveAcceptPrivacy"> privacy</label>
|
||||
<label class="small muted"><input type="checkbox" id="liveAcceptRisk"> risk disclosure</label>
|
||||
<button class="btn" id="recordConsentBtn">Record consent</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-h"><h3>Server controls</h3><span class="small muted">live safety gates</span></div>
|
||||
<div class="locked-panel">
|
||||
<b>Production switch remains off</b>
|
||||
<div class="small muted">The backend can now store policies, consent, risk profiles, webhooks, incidents, and dry-run order intents. Real deposits and live orders still require final approval and real CLOB/deposit-wallet setup.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ ABOUT ============ -->
|
||||
@@ -687,6 +706,7 @@ let PAPER_SEARCH_RESULTS = null;
|
||||
let PAPER_MARKET_OFFSET = 0;
|
||||
let PAPER_MARKET_QUERY = "";
|
||||
let LIVE_BACKEND_STATUS = null;
|
||||
let LIVE_POLICY_STATUS = null;
|
||||
let PROVIDER_CONFIG = null;
|
||||
const catColor = (c) => CAT_COLORS[c] || CAT_COLORS.Other;
|
||||
const getFocus = () => localStorage.getItem(FOCUS_KEY) || "All";
|
||||
@@ -2347,18 +2367,24 @@ function saveLiveState(s,detail){
|
||||
}
|
||||
async function fetchLiveBackendStatus(){
|
||||
try{
|
||||
const r=await fetch("/api/live",{cache:"no-store"});
|
||||
LIVE_BACKEND_STATUS=await r.json();
|
||||
const [liveRes,policyRes]=await Promise.all([
|
||||
fetch("/api/live",{cache:"no-store"}),
|
||||
fetch("/api/policies",{cache:"no-store"}),
|
||||
]);
|
||||
LIVE_BACKEND_STATUS=await liveRes.json();
|
||||
LIVE_POLICY_STATUS=await policyRes.json();
|
||||
}catch(e){
|
||||
LIVE_BACKEND_STATUS={ok:false,locked_reason:"Live backend status endpoint is unavailable.",providers:[]};
|
||||
LIVE_POLICY_STATUS={ok:false,note:"Policy status endpoint is unavailable."};
|
||||
}
|
||||
renderLiveBackendStatus();
|
||||
}
|
||||
function renderLiveBackendStatus(){
|
||||
const root=$("liveBackendStatus");if(!root)return;
|
||||
const hookRoot=$("liveWebhookStatus");
|
||||
const policyRoot=$("livePolicyStatus");
|
||||
const s=LIVE_BACKEND_STATUS;
|
||||
if(!s){root.innerHTML="Checking backend readiness...";if(hookRoot)hookRoot.innerHTML="Checking webhook setup...";return;}
|
||||
if(!s){root.innerHTML="Checking backend readiness...";if(hookRoot)hookRoot.innerHTML="Checking webhook setup...";if(policyRoot)policyRoot.innerHTML="Checking policy status...";return;}
|
||||
const providers=(s.providers||[]).map(p=>`<div class="leader-row"><span>${esc(p.label)}${p.expected?`<div class="small muted">${esc(p.expected)}</div>`:""}</span><b class="${p.configured?"pos-val":"neg-val"}">${p.configured?"configured":"missing"}</b></div>`).join("");
|
||||
const stack=(s.provider_stack||[]).map(p=>`<div class="leader-row"><span><b>${esc(p.provider)}</b><div class="small muted">${esc(p.role)}</div><div class="small muted">${(p.checks||[]).map(c=>`${esc(c.key)}: ${c.configured?"ok":"missing"}`).join(" · ")}</div></span><b class="${p.configured?"pos-val":"neg-val"}">${p.configured?"ready":"needs keys"}</b></div>`).join("");
|
||||
const next=(s.next_required||[]).slice(0,12).map(x=>`<li>${esc(x)}</li>`).join("");
|
||||
@@ -2368,6 +2394,12 @@ function renderLiveBackendStatus(){
|
||||
const hooks=(s.webhooks||[]).map(h=>`<div class="leader-row"><span><b>${esc(h.provider)}</b><div class="small muted">${esc(h.label)}</div></span><a class="market-link" href="${esc(h.url)}" target="_blank" rel="noopener">${esc(h.url)}</a></div>`).join("");
|
||||
hookRoot.innerHTML=hooks||"Webhook URLs will appear after backend readiness loads.";
|
||||
}
|
||||
if(policyRoot){
|
||||
const p=LIVE_POLICY_STATUS||{};
|
||||
const versions=p.versions||{};
|
||||
const rules=(p.rules||[]).map(rule=>`<li>${esc(rule)}</li>`).join("");
|
||||
policyRoot.innerHTML=`<b>${p.approved?"Policy versions configured":"Policy documents not approved yet"}</b><div class="small muted" style="margin:6px 0">${esc(p.note||"")}</div><div class="leader-row"><span>Terms</span><b>${esc(versions.terms||"missing")}</b></div><div class="leader-row"><span>Privacy</span><b>${esc(versions.privacy||"missing")}</b></div><div class="leader-row"><span>Risk disclosure</span><b>${esc(versions.risk_disclosure||"missing")}</b></div>${rules?`<ol class="steps">${rules}</ol>`:""}`;
|
||||
}
|
||||
}
|
||||
async function dryRunLiveOrderIntent(){
|
||||
const s=loadLiveState();
|
||||
@@ -2389,6 +2421,38 @@ async function dryRunLiveOrderIntent(){
|
||||
toast("Order dry-run failed.");
|
||||
}
|
||||
}
|
||||
async function saveRiskProfileToServer(s){
|
||||
const body={
|
||||
user_id:"local-readiness-user",
|
||||
wallet_address:s.walletAddress,
|
||||
jurisdiction:s.jurisdiction,
|
||||
deposit_limit:s.depositLimit,
|
||||
withdraw_reserve:s.withdrawReserve,
|
||||
agent_permissions:s.agents,
|
||||
};
|
||||
const r=await fetch("/api/risk-profile",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)});
|
||||
const d=await r.json();
|
||||
if(!r.ok)throw new Error(d.error||"Risk profile save failed");
|
||||
return d;
|
||||
}
|
||||
async function recordLiveConsent(){
|
||||
const s=loadLiveState();
|
||||
try{
|
||||
const r=await fetch("/api/consent",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({
|
||||
user_id:"local-readiness-user",
|
||||
jurisdiction:s.jurisdiction,
|
||||
accept_terms:$("liveAcceptTerms").checked,
|
||||
accept_privacy:$("liveAcceptPrivacy").checked,
|
||||
accept_risk:$("liveAcceptRisk").checked,
|
||||
})});
|
||||
const d=await r.json();
|
||||
if(!r.ok)throw new Error(`${d.error}: ${(d.missing||[]).join(", ")}`);
|
||||
saveLiveState(s,"Recorded live-money consent versions");
|
||||
renderLiveMoneyTab();toast("Consent recorded.");
|
||||
}catch(e){
|
||||
toast(e.message||"Consent is not ready to record yet.");
|
||||
}
|
||||
}
|
||||
function renderLiveMoneyTab(){
|
||||
const root=$("liveChecklist");if(!root)return;
|
||||
const s=loadLiveState(),done=LIVE_CHECKS.filter(([k])=>s.checks[k]).length;
|
||||
@@ -2530,16 +2594,23 @@ $("paperSignOutBtn").addEventListener("click",()=>{
|
||||
sessionStorage.removeItem(paperSessionKey(acct.id));
|
||||
renderPaperTab();toast("Signed out of paper account.");
|
||||
});
|
||||
$("saveLiveProfileBtn").addEventListener("click",()=>{
|
||||
$("saveLiveProfileBtn").addEventListener("click",async()=>{
|
||||
const s=loadLiveState();
|
||||
s.walletAddress=$("liveWalletAddress").value.trim();
|
||||
s.jurisdiction=$("liveJurisdiction").value;
|
||||
s.depositLimit=Number($("liveDepositLimit").value||0);
|
||||
s.withdrawReserve=Number($("liveWithdrawReserve").value||0);
|
||||
saveLiveState(s,"Updated live-money wallet/funds profile");
|
||||
renderLiveMoneyTab();toast("Live-money readiness profile saved.");
|
||||
try{
|
||||
await saveRiskProfileToServer(s);
|
||||
saveLiveState(s,"Updated live-money wallet/funds profile and server risk profile");
|
||||
renderLiveMoneyTab();toast("Live-money readiness profile saved to Neon.");
|
||||
}catch(e){
|
||||
saveLiveState(s,"Updated local live-money wallet/funds profile");
|
||||
renderLiveMoneyTab();toast(e.message||"Saved locally, but server risk profile failed.");
|
||||
}
|
||||
});
|
||||
$("dryRunOrderBtn").addEventListener("click",()=>dryRunLiveOrderIntent());
|
||||
$("recordConsentBtn").addEventListener("click",()=>recordLiveConsent());
|
||||
window.addEventListener("focus",()=>refreshFromCloudAndRender(true));
|
||||
document.addEventListener("visibilitychange",()=>{
|
||||
if(!document.hidden)refreshFromCloudAndRender(true);
|
||||
|
||||
Reference in New Issue
Block a user