From eb1fba512e3126fb113d4a9ae1f07d8960ae4acb Mon Sep 17 00:00:00 2001
From: jhfnetboy
Date: Mon, 6 Jul 2026 22:02:44 +0700
Subject: [PATCH 1/4] feat(operator): DVT node-registration wizard scaffold
(CC-17 / aastar-sdk#279)
Guided operator onboarding for DVT signing-node registration, reusing the
existing operator deploy wizard scaffolding (StepCard / WizardButton /
WizardProgress / FormField / useTxStep) and the Track-C browser-signing path.
Flow: connect -> eligibility -> key & PoP -> register -> done. The eligibility
step short-circuits to done when the operator already runs a node. The BLS
secret key is generated/imported in-browser and never persisted or sent to the
backend; nodeId is previewed from the PoP before any tx.
All chain/crypto calls sit behind lib/sdk/dvtOperator.ts, stubbed with
DVT_SDK_READY=false until @aastar/sdk 0.37.4/0.38.0 (PR #288) publishes
dvtOperatorActions + buildDvtPop. Wiring is a one-file change (flip the flag,
add the import, fill the WIRE-HERE bodies). Adds a DVT-registration entry card
to the operator hub and zh/en i18n. Also adds .goutou.json (repoId: yaaa).
Claude-Session: https://claude.ai/code/session_01RajETCvboSvhadpqMbekNx
---
.goutou.json | 4 +
.../components/PendingSdkNote.tsx | 20 +++
.../app/operator/dvt-register/page.tsx | 125 +++++++++++++
.../dvt-register/steps/Step1Connect.tsx | 69 ++++++++
.../dvt-register/steps/Step2Eligibility.tsx | 132 ++++++++++++++
.../dvt-register/steps/Step3KeyPoP.tsx | 148 ++++++++++++++++
.../dvt-register/steps/Step4Register.tsx | 98 ++++++++++
.../dvt-register/steps/StepComplete.tsx | 63 +++++++
.../app/operator/dvt-register/steps/types.ts | 44 +++++
aastar-frontend/app/operator/page.tsx | 16 +-
aastar-frontend/lib/i18n/locales/en.json | 75 ++++++++
aastar-frontend/lib/i18n/locales/zh.json | 75 ++++++++
aastar-frontend/lib/sdk/dvtOperator.ts | 167 ++++++++++++++++++
13 files changed, 1035 insertions(+), 1 deletion(-)
create mode 100644 .goutou.json
create mode 100644 aastar-frontend/app/operator/dvt-register/components/PendingSdkNote.tsx
create mode 100644 aastar-frontend/app/operator/dvt-register/page.tsx
create mode 100644 aastar-frontend/app/operator/dvt-register/steps/Step1Connect.tsx
create mode 100644 aastar-frontend/app/operator/dvt-register/steps/Step2Eligibility.tsx
create mode 100644 aastar-frontend/app/operator/dvt-register/steps/Step3KeyPoP.tsx
create mode 100644 aastar-frontend/app/operator/dvt-register/steps/Step4Register.tsx
create mode 100644 aastar-frontend/app/operator/dvt-register/steps/StepComplete.tsx
create mode 100644 aastar-frontend/app/operator/dvt-register/steps/types.ts
create mode 100644 aastar-frontend/lib/sdk/dvtOperator.ts
diff --git a/.goutou.json b/.goutou.json
new file mode 100644
index 0000000..6036da3
--- /dev/null
+++ b/.goutou.json
@@ -0,0 +1,4 @@
+{
+ "repoId": "yaaa",
+ "coordProjectId": "03253073-c822-4a5a-b169-cb39976200c3"
+}
diff --git a/aastar-frontend/app/operator/dvt-register/components/PendingSdkNote.tsx b/aastar-frontend/app/operator/dvt-register/components/PendingSdkNote.tsx
new file mode 100644
index 0000000..f3eec30
--- /dev/null
+++ b/aastar-frontend/app/operator/dvt-register/components/PendingSdkNote.tsx
@@ -0,0 +1,20 @@
+"use client";
+
+/**
+ * Inline notice shown while the DVT SDK actions are not yet published (PR #288).
+ * Makes it unmistakable that on-chain steps are stubbed pending @aastar/sdk.
+ *
+ * @module app/operator/dvt-register/components/PendingSdkNote
+ */
+import { useTranslation } from "react-i18next";
+import { WrenchIcon } from "@heroicons/react/24/outline";
+
+export default function PendingSdkNote() {
+ const { t } = useTranslation();
+ return (
+
+
+ {t("dvtRegister.pendingSdk")}
+
+ );
+}
diff --git a/aastar-frontend/app/operator/dvt-register/page.tsx b/aastar-frontend/app/operator/dvt-register/page.tsx
new file mode 100644
index 0000000..fdf1956
--- /dev/null
+++ b/aastar-frontend/app/operator/dvt-register/page.tsx
@@ -0,0 +1,125 @@
+"use client";
+
+/**
+ * DVT node-operator registration wizard (aastar-sdk#279 / PR #288).
+ *
+ * Turns the manual operator registration script into a guided flow, fully
+ * client-side: the operator's own browser wallet (viem WalletClient from
+ * WalletContext) signs `registerWithProof` against the AAStarValidator, and the
+ * BLS secret key never leaves the browser.
+ *
+ * connect → eligibility → key & PoP → register → done
+ *
+ * The eligibility step short-circuits to `done` when the operator already runs a
+ * node. On-chain/crypto calls are behind the {@link lib/sdk/dvtOperator} boundary,
+ * stubbed until @aastar/sdk 0.37.4/0.38.0 publishes the DVT actions.
+ *
+ * @module app/operator/dvt-register/page
+ */
+import { useCallback, useMemo, useState } from "react";
+import { useRouter } from "next/navigation";
+import { useTranslation } from "react-i18next";
+import { CpuChipIcon, ExclamationTriangleIcon } from "@heroicons/react/24/outline";
+import Layout from "@/components/Layout";
+import { useWallet } from "@/contexts/WalletContext";
+import WizardProgress from "@/app/operator/deploy/components/WizardProgress";
+import WizardButton from "@/app/operator/deploy/components/WizardButton";
+import { initialDvtWizardData, type DvtStepProps, type DvtWizardData } from "./steps/types";
+import Step1Connect from "./steps/Step1Connect";
+import Step2Eligibility from "./steps/Step2Eligibility";
+import Step3KeyPoP from "./steps/Step3KeyPoP";
+import Step4Register from "./steps/Step4Register";
+import StepComplete from "./steps/StepComplete";
+
+const STEP_KEYS = ["connect", "eligibility", "key", "register", "done"] as const;
+const LABEL_KEYS: Record<(typeof STEP_KEYS)[number], string> = {
+ connect: "dvtRegister.labels.connect",
+ eligibility: "dvtRegister.labels.eligibility",
+ key: "dvtRegister.labels.key",
+ register: "dvtRegister.labels.register",
+ done: "dvtRegister.labels.done",
+};
+
+export default function DvtRegisterPage() {
+ const { t } = useTranslation();
+ const router = useRouter();
+ const { address, walletClient } = useWallet();
+ const [step, setStep] = useState(0);
+ const [data, setData] = useState(initialDvtWizardData);
+
+ const update = useCallback((patch: Partial) => {
+ setData(prev => ({ ...prev, ...patch }));
+ }, []);
+
+ const onNext = useCallback(() => setStep(s => Math.min(s + 1, STEP_KEYS.length - 1)), []);
+ const onBack = useCallback(() => setStep(s => Math.max(s - 1, 0)), []);
+ const onComplete = useCallback(() => setStep(STEP_KEYS.length - 1), []);
+ const restart = useCallback(() => {
+ setData(initialDvtWizardData);
+ setStep(0);
+ }, []);
+
+ const currentKey = STEP_KEYS[step];
+ const needWallet = step > 0 && (!address || !walletClient);
+
+ const stepProps: DvtStepProps | null =
+ address && walletClient
+ ? { address, walletClient, data, update, onNext, onBack, onComplete }
+ : null;
+
+ const labels = useMemo(() => STEP_KEYS.map(k => t(LABEL_KEYS[k])), [t]);
+
+ const renderStep = () => {
+ if (currentKey === "connect") {
+ return ;
+ }
+ if (needWallet || !stepProps) {
+ return (
+
+
+
+ {t("dvtRegister.page.walletDisconnected")}
+
+
+ {t("dvtRegister.page.reconnectPrompt")}
+
+ {t("dvtRegister.page.backToStart")}
+
+ );
+ }
+ switch (currentKey) {
+ case "eligibility":
+ return ;
+ case "key":
+ return ;
+ case "register":
+ return ;
+ case "done":
+ return (
+ router.push("/operator")} onRestart={restart} />
+ );
+ default:
+ return null;
+ }
+ };
+
+ return (
+
+
+
+
+
+ {t("dvtRegister.page.title")}
+
+
+ {t("dvtRegister.page.subtitle")}
+
+
+
+
+
+ {renderStep()}
+
+
+ );
+}
diff --git a/aastar-frontend/app/operator/dvt-register/steps/Step1Connect.tsx b/aastar-frontend/app/operator/dvt-register/steps/Step1Connect.tsx
new file mode 100644
index 0000000..f8badb0
--- /dev/null
+++ b/aastar-frontend/app/operator/dvt-register/steps/Step1Connect.tsx
@@ -0,0 +1,69 @@
+"use client";
+
+/**
+ * Step 1 — connect the operator wallet and introduce DVT node registration.
+ * No on-chain writes here.
+ *
+ * @module app/operator/dvt-register/steps/Step1Connect
+ */
+import { useTranslation } from "react-i18next";
+import { WalletIcon, ShieldCheckIcon } from "@heroicons/react/24/outline";
+import { useWallet } from "@/contexts/WalletContext";
+import StepCard from "@/app/operator/deploy/components/StepCard";
+import WizardButton from "@/app/operator/deploy/components/WizardButton";
+
+interface Step1Props {
+ onNext: () => void;
+}
+
+export default function Step1Connect({ onNext }: Step1Props) {
+ const { t } = useTranslation();
+ const { address, isConnecting, hasInjectedWallet, connect } = useWallet();
+
+ return (
+ }
+ footer={
+ <>
+
+ {address ? (
+
+ {address.slice(0, 6)}…{address.slice(-4)}
+
+ ) : (
+ t("dvtRegister.step1.walletNotConnected")
+ )}
+
+ {address ? (
+ {t("dvtRegister.common.continue")}
+ ) : (
+
+ {hasInjectedWallet
+ ? t("dvtRegister.step1.connectWallet")
+ : t("dvtRegister.step1.noWalletFound")}
+
+ )}
+ >
+ }
+ >
+
+
+
+
+ {t("dvtRegister.step1.aboutTitle")}
+
+
+
+ {[1, 2, 3].map(n => (
+ -
+ •
+ {t(`dvtRegister.step1.about${n}`)}
+
+ ))}
+
+
+
+ );
+}
diff --git a/aastar-frontend/app/operator/dvt-register/steps/Step2Eligibility.tsx b/aastar-frontend/app/operator/dvt-register/steps/Step2Eligibility.tsx
new file mode 100644
index 0000000..6a0a0ca
--- /dev/null
+++ b/aastar-frontend/app/operator/dvt-register/steps/Step2Eligibility.tsx
@@ -0,0 +1,132 @@
+"use client";
+
+/**
+ * Step 2 — check on-chain eligibility.
+ *
+ * - `operatorNode(operator)` non-zero → operator already runs a node: short-circuit
+ * to the final step showing the bound nodeId.
+ * - else surface stake status (`requireStake` / `minStake`); if the operator has
+ * not staked ROLE_DVT, guide them to staking before continuing.
+ *
+ * While the DVT SDK is unpublished (PR #288) the read is stubbed: the step shows
+ * the pending notice and lets the operator walk the rest of the wizard.
+ *
+ * @module app/operator/dvt-register/steps/Step2Eligibility
+ */
+import { useCallback, useEffect, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { formatEther } from "viem";
+import { ClipboardDocumentCheckIcon, ArrowPathIcon } from "@heroicons/react/24/outline";
+import StepCard from "@/app/operator/deploy/components/StepCard";
+import WizardButton from "@/app/operator/deploy/components/WizardButton";
+import {
+ DVT_SDK_READY,
+ fetchDvtEligibility,
+ type DvtEligibility,
+ type Hex,
+} from "@/lib/sdk/dvtOperator";
+import PendingSdkNote from "../components/PendingSdkNote";
+import type { DvtStepProps } from "./types";
+
+export default function Step2Eligibility({
+ address,
+ walletClient,
+ data,
+ update,
+ onNext,
+ onBack,
+ onComplete,
+}: DvtStepProps) {
+ const { t } = useTranslation();
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState();
+ const eligibility = data.eligibility;
+
+ const load = useCallback(async () => {
+ if (!DVT_SDK_READY) return;
+ setLoading(true);
+ setError(undefined);
+ try {
+ const result: DvtEligibility = await fetchDvtEligibility(walletClient, address as Hex);
+ update({ eligibility: result });
+ } catch (err) {
+ setError(err instanceof Error ? err.message : String(err));
+ } finally {
+ setLoading(false);
+ }
+ }, [address, walletClient, update]);
+
+ useEffect(() => {
+ if (DVT_SDK_READY && !eligibility) void load();
+ }, [eligibility, load]);
+
+ const alreadyRegistered = !!eligibility?.boundNodeId;
+
+ return (
+ }
+ error={error}
+ footer={
+ <>
+ {onBack && (
+
+ {t("dvtRegister.common.back")}
+
+ )}
+ {alreadyRegistered ? (
+ onComplete?.()}>
+ {t("dvtRegister.step2.viewNode")}
+
+ ) : (
+
+ {t("dvtRegister.common.continue")}
+
+ )}
+ >
+ }
+ >
+ {!DVT_SDK_READY && }
+
+ {DVT_SDK_READY && loading && (
+
+
+ {t("dvtRegister.step2.checking")}
+
+ )}
+
+ {alreadyRegistered && eligibility?.boundNodeId && (
+
+
+ {t("dvtRegister.step2.alreadyRegistered")}
+
+
+ {eligibility.boundNodeId}
+
+
+ )}
+
+ {!alreadyRegistered && eligibility && (
+
+
+
- {t("dvtRegister.step2.stakeOpen")}
+ -
+ {eligibility.stakeOpen
+ ? t("dvtRegister.step2.stakeOpenYes")
+ : t("dvtRegister.step2.stakeOpenNo")}
+
+
+
+
{t("dvtRegister.step2.minStake")}
+
+ {formatEther(eligibility.minStake)} GTOKEN
+
+
+
+ )}
+
+ {t("dvtRegister.step2.stakeHint")}
+
+ );
+}
diff --git a/aastar-frontend/app/operator/dvt-register/steps/Step3KeyPoP.tsx b/aastar-frontend/app/operator/dvt-register/steps/Step3KeyPoP.tsx
new file mode 100644
index 0000000..2c0610d
--- /dev/null
+++ b/aastar-frontend/app/operator/dvt-register/steps/Step3KeyPoP.tsx
@@ -0,0 +1,148 @@
+"use client";
+
+/**
+ * Step 3 — obtain the BLS node key and derive the proof-of-possession.
+ *
+ * The operator either generates a fresh key in the browser or pastes an existing
+ * one. `buildDvtPop` (pure/local in the SDK) then derives `{ publicKey, popPoint,
+ * popSig, nodeId }`, letting us preview the nodeId before any tx.
+ *
+ * SECURITY: the BLS secret key is held only in in-memory wizard state — it is
+ * never written to storage nor sent to the backend. The UI warns the operator to
+ * back it up offline.
+ *
+ * While the DVT SDK is unpublished (PR #288), `buildDvtPop` is stubbed: key
+ * generation/import still works locally, and the nodeId preview shows a pending
+ * notice.
+ *
+ * @module app/operator/dvt-register/steps/Step3KeyPoP
+ */
+import { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { KeyIcon, ExclamationTriangleIcon, SparklesIcon } from "@heroicons/react/24/outline";
+import StepCard from "@/app/operator/deploy/components/StepCard";
+import WizardButton from "@/app/operator/deploy/components/WizardButton";
+import FormField from "@/app/operator/deploy/components/FormField";
+import {
+ DVT_SDK_READY,
+ buildDvtPop,
+ generateBlsSecretKey,
+ normalizeBlsSecretKey,
+ type DvtPop,
+ type Hex,
+} from "@/lib/sdk/dvtOperator";
+import PendingSdkNote from "../components/PendingSdkNote";
+import type { DvtStepProps } from "./types";
+
+export default function Step3KeyPoP({ data, update, onNext, onBack }: DvtStepProps) {
+ const { t } = useTranslation();
+ const [keyInput, setKeyInput] = useState(data.blsSecretKey ?? "");
+ const [error, setError] = useState();
+
+ const applyKey = (secret: Hex) => {
+ setError(undefined);
+ // buildDvtPop is pure/local once wired; keep the derived PoP alongside the key.
+ let pop: DvtPop | undefined;
+ if (DVT_SDK_READY) {
+ try {
+ pop = buildDvtPop(secret);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : String(err));
+ }
+ }
+ update({ blsSecretKey: secret, pop });
+ };
+
+ const handleGenerate = () => {
+ const secret = generateBlsSecretKey();
+ setKeyInput(secret);
+ applyKey(secret);
+ };
+
+ const handleImport = () => {
+ try {
+ const secret = normalizeBlsSecretKey(keyInput);
+ setKeyInput(secret);
+ applyKey(secret);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : String(err));
+ }
+ };
+
+ const hasKey = !!data.blsSecretKey;
+
+ return (
+ }
+ error={error}
+ footer={
+ <>
+ {onBack && (
+
+ {t("dvtRegister.common.back")}
+
+ )}
+
+ {t("dvtRegister.common.continue")}
+
+ >
+ }
+ >
+
+
+ {t("dvtRegister.step3.keyWarning")}
+
+
+
+
+
+
+
+
+ {t("dvtRegister.step3.import")}
+
+
+
+
+
+ {hasKey && (
+
+
{t("dvtRegister.step3.previewTitle")}
+ {data.pop ? (
+ <>
+
+
+ >
+ ) : (
+
+ )}
+
+ )}
+
+ );
+}
+
+function Field({ label, value }: { label: string; value: string }) {
+ return (
+
+ );
+}
diff --git a/aastar-frontend/app/operator/dvt-register/steps/Step4Register.tsx b/aastar-frontend/app/operator/dvt-register/steps/Step4Register.tsx
new file mode 100644
index 0000000..d4e7941
--- /dev/null
+++ b/aastar-frontend/app/operator/dvt-register/steps/Step4Register.tsx
@@ -0,0 +1,98 @@
+"use client";
+
+/**
+ * Step 4 — submit the registration and confirm on-chain.
+ *
+ * `register({ blsSecretKey })` builds the PoP and submits in one tx; after the
+ * receipt we read `isRegistered(nodeId)` back to confirm. `useTxStep` drives the
+ * pending/confirmed/error UI, consistent with the deploy wizard.
+ *
+ * While the DVT SDK is unpublished (PR #288) the submit is disabled behind the
+ * pending notice.
+ *
+ * @module app/operator/dvt-register/steps/Step4Register
+ */
+import { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { PaperAirplaneIcon } from "@heroicons/react/24/outline";
+import StepCard from "@/app/operator/deploy/components/StepCard";
+import WizardButton from "@/app/operator/deploy/components/WizardButton";
+import { useTxStep } from "@/app/operator/deploy/components/useTxStep";
+import { DVT_SDK_READY, isDvtNodeRegistered, registerDvtNode } from "@/lib/sdk/dvtOperator";
+import PendingSdkNote from "../components/PendingSdkNote";
+import type { DvtStepProps } from "./types";
+
+export default function Step4Register({
+ walletClient,
+ data,
+ update,
+ onNext,
+ onBack,
+}: DvtStepProps) {
+ const { t } = useTranslation();
+ const { status, txHash, error, isBusy, run } = useTxStep();
+ const [confirmError, setConfirmError] = useState();
+
+ const nodeId = data.pop?.nodeId;
+
+ const handleRegister = async () => {
+ if (!data.blsSecretKey) return;
+ const hash = await run(
+ async () => {
+ const { hash } = await registerDvtNode(walletClient, data.blsSecretKey!);
+ return hash;
+ },
+ {
+ loadingMsg: t("dvtRegister.step4.submitting"),
+ successMsg: t("dvtRegister.step4.submitted"),
+ }
+ );
+ if (!hash) return;
+ update({ registerTxHash: hash });
+ // Best-effort read-back; failure here doesn't undo a confirmed tx.
+ try {
+ if (nodeId) await isDvtNodeRegistered(walletClient, nodeId);
+ } catch (err) {
+ setConfirmError(err instanceof Error ? err.message : String(err));
+ }
+ onNext();
+ };
+
+ return (
+ }
+ status={status}
+ txHash={txHash}
+ error={error ?? confirmError}
+ footer={
+ <>
+ {onBack && (
+
+ {t("dvtRegister.common.back")}
+
+ )}
+
+ {t("dvtRegister.step4.register")}
+
+ >
+ }
+ >
+ {!DVT_SDK_READY && }
+
+ {nodeId && (
+
+
{t("dvtRegister.step3.nodeId")}
+
{nodeId}
+
+ )}
+
+ {t("dvtRegister.step4.hint")}
+
+ );
+}
diff --git a/aastar-frontend/app/operator/dvt-register/steps/StepComplete.tsx b/aastar-frontend/app/operator/dvt-register/steps/StepComplete.tsx
new file mode 100644
index 0000000..5e182b4
--- /dev/null
+++ b/aastar-frontend/app/operator/dvt-register/steps/StepComplete.tsx
@@ -0,0 +1,63 @@
+"use client";
+
+/**
+ * Final step — show the registered nodeId (fresh registration or the pre-existing
+ * bound node from the eligibility short-circuit) and the tx link.
+ *
+ * @module app/operator/dvt-register/steps/StepComplete
+ */
+import { useTranslation } from "react-i18next";
+import { CheckCircleIcon, ArrowTopRightOnSquareIcon } from "@heroicons/react/24/outline";
+import StepCard, { explorerTx } from "@/app/operator/deploy/components/StepCard";
+import WizardButton from "@/app/operator/deploy/components/WizardButton";
+import type { DvtWizardData } from "./types";
+
+interface StepCompleteProps {
+ data: DvtWizardData;
+ onDone: () => void;
+ onRestart: () => void;
+}
+
+export default function StepComplete({ data, onDone, onRestart }: StepCompleteProps) {
+ const { t } = useTranslation();
+ const nodeId = data.pop?.nodeId ?? data.eligibility?.boundNodeId ?? undefined;
+
+ return (
+ }
+ footer={
+ <>
+
+ {t("dvtRegister.done.registerAnother")}
+
+ {t("dvtRegister.done.backToOperator")}
+ >
+ }
+ >
+ {nodeId && (
+
+
+ {t("dvtRegister.step3.nodeId")}
+
+
+ {nodeId}
+
+
+ )}
+
+ {data.registerTxHash && (
+
+ {t("dvtRegister.done.viewTx")}
+
+
+ )}
+
+ );
+}
diff --git a/aastar-frontend/app/operator/dvt-register/steps/types.ts b/aastar-frontend/app/operator/dvt-register/steps/types.ts
new file mode 100644
index 0000000..876a041
--- /dev/null
+++ b/aastar-frontend/app/operator/dvt-register/steps/types.ts
@@ -0,0 +1,44 @@
+"use client";
+
+/**
+ * Shared types for the DVT node-operator registration wizard.
+ *
+ * Registration is signed in the operator's own browser wallet (viem WalletClient
+ * from WalletContext); the BLS **secret key lives only in this in-memory wizard
+ * state** — never persisted to storage, never sent to the backend.
+ *
+ * @module app/operator/dvt-register/steps/types
+ */
+import type { Address, WalletClient } from "viem";
+import type { DvtEligibility, DvtPop, Hex } from "@/lib/sdk/dvtOperator";
+
+/** Cross-step wizard state, owned by page.tsx and threaded into each step. */
+export interface DvtWizardData {
+ /** On-chain eligibility snapshot (operatorNode / requireStake / minStake). */
+ eligibility: DvtEligibility | null;
+ /**
+ * BLS node secret key — in-memory only, never persisted or transmitted.
+ * Present once the operator generates or imports a key in the key step.
+ */
+ blsSecretKey?: Hex;
+ /** Proof-of-possession + nodeId derived locally from `blsSecretKey`. */
+ pop?: DvtPop;
+ /** Registration tx hash once submitted. */
+ registerTxHash?: Hex;
+}
+
+export const initialDvtWizardData: DvtWizardData = {
+ eligibility: null,
+};
+
+/** Props every step component receives from the wizard container. */
+export interface DvtStepProps {
+ address: Address;
+ walletClient: WalletClient;
+ data: DvtWizardData;
+ update: (patch: Partial) => void;
+ onNext: () => void;
+ onBack?: () => void;
+ /** Jump straight to the final step (used by the already-registered short-circuit). */
+ onComplete?: () => void;
+}
diff --git a/aastar-frontend/app/operator/page.tsx b/aastar-frontend/app/operator/page.tsx
index 83fbc20..9ddbc68 100644
--- a/aastar-frontend/app/operator/page.tsx
+++ b/aastar-frontend/app/operator/page.tsx
@@ -10,6 +10,7 @@ import {
ArrowPathIcon,
RocketLaunchIcon,
WrenchScrewdriverIcon,
+ CpuChipIcon,
} from "@heroicons/react/24/outline";
import Layout from "@/components/Layout";
import { operatorAPI } from "@/lib/api";
@@ -135,7 +136,7 @@ export default function OperatorPage() {
)}
{/* Quick Actions — entry points to the operator write flows */}
-
+
+
{/* My Operator Status */}
diff --git a/aastar-frontend/lib/i18n/locales/en.json b/aastar-frontend/lib/i18n/locales/en.json
index b68ab38..c2fa9af 100644
--- a/aastar-frontend/lib/i18n/locales/en.json
+++ b/aastar-frontend/lib/i18n/locales/en.json
@@ -771,6 +771,81 @@
"desc": "Manage xPNTs, top up your Paymaster, and view operator status."
}
},
+ "dvtRegister": {
+ "pendingSdk": "On-chain actions are waiting on @aastar/sdk 0.37.4/0.38.0 (aastar-sdk#279 / PR #288). The UI is ready and will light up once the SDK ships.",
+ "common": {
+ "continue": "Continue",
+ "back": "Back"
+ },
+ "labels": {
+ "connect": "Connect",
+ "eligibility": "Eligibility",
+ "key": "Key & PoP",
+ "register": "Register",
+ "done": "Done"
+ },
+ "card": {
+ "title": "DVT node registration",
+ "desc": "Guide operators through stake + PoP + registration — no more manual scripts."
+ },
+ "page": {
+ "title": "DVT node registration",
+ "subtitle": "Guide an operator through staking, building the PoP, and registering a DVT signing node.",
+ "walletDisconnected": "Wallet disconnected",
+ "reconnectPrompt": "Registration needs your operator wallet connected. Please restart the wizard.",
+ "backToStart": "Back to start"
+ },
+ "step1": {
+ "title": "Connect operator wallet",
+ "description": "DVT node registration is signed by your own EOA in the browser.",
+ "walletNotConnected": "Wallet not connected",
+ "connectWallet": "Connect wallet",
+ "noWalletFound": "No wallet found",
+ "aboutTitle": "About DVT node registration",
+ "about1": "Stake gate: ROLE_DVT + GToken ≥ minimum stake",
+ "about2": "BLS proof-of-possession (PoP) built locally from the node key — the key never leaves the browser",
+ "about3": "One operator, one node; nodeId = keccak256(public key)"
+ },
+ "step2": {
+ "title": "Check eligibility",
+ "description": "Confirm whether you are already registered and whether your stake qualifies.",
+ "checking": "Reading on-chain state…",
+ "alreadyRegistered": "This operator already has a bound node:",
+ "viewNode": "View node",
+ "stakeOpen": "Stake registration open",
+ "stakeOpenYes": "Yes",
+ "stakeOpenNo": "No",
+ "minStake": "Minimum stake",
+ "stakeHint": "If you have not staked ROLE_DVT, stake via GTokenStaking before continuing."
+ },
+ "step3": {
+ "title": "BLS node key & PoP",
+ "description": "Generate or import the node BLS secret key; derive the PoP and nodeId locally.",
+ "keyWarning": "⚠️ The BLS secret key is sensitive. It stays only in this browser tab and is never sent to the backend. Back it up offline — losing it means losing the node identity.",
+ "keyLabel": "BLS secret key (0x + 64 hex chars)",
+ "keyHint": "Paste an existing key, or generate a fresh one below.",
+ "import": "Import",
+ "generate": "Generate a new key in the browser",
+ "previewTitle": "Derived preview",
+ "nodeId": "nodeId",
+ "publicKey": "Public key (G1)"
+ },
+ "step4": {
+ "title": "Submit registration",
+ "description": "Build the PoP and call registerWithProof in one transaction.",
+ "submitting": "Confirm in your wallet…",
+ "submitted": "Registration transaction submitted",
+ "register": "Register node",
+ "hint": "After confirmation we read isRegistered back to confirm the node is on-chain."
+ },
+ "done": {
+ "title": "Registration complete",
+ "description": "Your DVT signing node is registered.",
+ "registerAnother": "Register another",
+ "backToOperator": "Back to operator hub",
+ "viewTx": "View transaction"
+ }
+ },
"operatorDashboard": {
"title": "Operator Portal",
"loadError": "Failed to load operator data",
diff --git a/aastar-frontend/lib/i18n/locales/zh.json b/aastar-frontend/lib/i18n/locales/zh.json
index a1f8135..7cc04a0 100644
--- a/aastar-frontend/lib/i18n/locales/zh.json
+++ b/aastar-frontend/lib/i18n/locales/zh.json
@@ -771,6 +771,81 @@
"desc": "管理 xPNTs、为 Paymaster 充值、查看运营者状态。"
}
},
+ "dvtRegister": {
+ "pendingSdk": "链上操作等待 @aastar/sdk 0.37.4/0.38.0 发布(aastar-sdk#279 / PR #288)。界面已就绪,SDK 落地后自动可用。",
+ "common": {
+ "continue": "继续",
+ "back": "返回"
+ },
+ "labels": {
+ "connect": "连接",
+ "eligibility": "资格",
+ "key": "密钥与 PoP",
+ "register": "注册",
+ "done": "完成"
+ },
+ "card": {
+ "title": "DVT 节点注册",
+ "desc": "把 DVT 签名节点质押 + PoP + 注册做成引导流程,不再手搓脚本。"
+ },
+ "page": {
+ "title": "DVT 节点注册",
+ "subtitle": "引导 operator 完成质押、生成 PoP、注册 DVT 签名节点。",
+ "walletDisconnected": "钱包已断开",
+ "reconnectPrompt": "注册需要连接你的 operator 钱包,请重新开始向导。",
+ "backToStart": "回到开始"
+ },
+ "step1": {
+ "title": "连接 operator 钱包",
+ "description": "DVT 节点注册由你自己的 EOA 在浏览器内签名。",
+ "walletNotConnected": "钱包未连接",
+ "connectWallet": "连接钱包",
+ "noWalletFound": "未检测到钱包",
+ "aboutTitle": "关于 DVT 节点注册",
+ "about1": "质押门槛:ROLE_DVT + GToken ≥ 最低质押额",
+ "about2": "BLS 拥有权证明(PoP):在本地用节点私钥生成,私钥不出浏览器",
+ "about3": "一 operator 一节点,nodeId = keccak256(公钥)"
+ },
+ "step2": {
+ "title": "检查注册资格",
+ "description": "确认是否已注册,以及质押是否达标。",
+ "checking": "读取链上状态中…",
+ "alreadyRegistered": "该 operator 已绑定节点:",
+ "viewNode": "查看节点",
+ "stakeOpen": "质押注册已开放",
+ "stakeOpenYes": "是",
+ "stakeOpenNo": "否",
+ "minStake": "最低质押额",
+ "stakeHint": "若未质押 ROLE_DVT,请先通过 GTokenStaking 质押后再继续。"
+ },
+ "step3": {
+ "title": "BLS 节点密钥与 PoP",
+ "description": "生成或导入节点 BLS 私钥,本地推导 PoP 与 nodeId。",
+ "keyWarning": "⚠️ BLS 私钥属敏感数据,仅保存在本浏览器标签内,绝不上传后端。请务必离线备份,丢失将无法找回节点身份。",
+ "keyLabel": "BLS 私钥(0x + 64 位十六进制)",
+ "keyHint": "粘贴已有私钥,或点下方生成一个全新的。",
+ "import": "导入",
+ "generate": "在浏览器生成新密钥",
+ "previewTitle": "推导结果预览",
+ "nodeId": "nodeId",
+ "publicKey": "公钥(G1)"
+ },
+ "step4": {
+ "title": "提交注册",
+ "description": "构造 PoP 并调用 registerWithProof,一步完成。",
+ "submitting": "请在钱包中确认…",
+ "submitted": "注册交易已提交",
+ "register": "注册节点",
+ "hint": "交易确认后会读回 isRegistered 以确认节点已上链。"
+ },
+ "done": {
+ "title": "注册完成",
+ "description": "你的 DVT 签名节点已注册。",
+ "registerAnother": "再注册一个",
+ "backToOperator": "返回运营中心",
+ "viewTx": "查看交易"
+ }
+ },
"operatorDashboard": {
"title": "运营者门户",
"loadError": "加载运营者数据失败",
diff --git a/aastar-frontend/lib/sdk/dvtOperator.ts b/aastar-frontend/lib/sdk/dvtOperator.ts
new file mode 100644
index 0000000..c22308c
--- /dev/null
+++ b/aastar-frontend/lib/sdk/dvtOperator.ts
@@ -0,0 +1,167 @@
+/**
+ * DVT node-operator registration — SDK boundary (aastar-sdk#279 / PR #288).
+ *
+ * DVT signing nodes register on the AAStarValidator via `registerWithProof`
+ * (YetAnotherAA-Validator #165): stake gate (ROLE_DVT + GToken ≥ minStake) + a
+ * BLS proof-of-possession (`e(pk, popPoint) == e(G1, popSig)`) + one-operator-one-node,
+ * with `nodeId = keccak256(pubkey)`. Registration is signed in the operator's own
+ * browser wallet (viem WalletClient from WalletContext) — never the backend key,
+ * and the BLS **secret key never leaves the browser**.
+ *
+ * The typed action surface below is the contract published by @repo:sdk:
+ *
+ * import { buildDvtPop, dvtOperatorActions } from "@aastar/sdk/core";
+ * const pop = buildDvtPop(blsSecretKey); // { publicKey, popPoint, popSig, nodeId }
+ * const dvt = walletClient.extend(dvtOperatorActions(validatorAddr));
+ * await dvt.requireStake(); await dvt.minStake();
+ * const bound = await dvt.operatorNode({ operator });
+ * const { hash, pop } = await dvt.register({ blsSecretKey });
+ *
+ * Those exports ship in @aastar/sdk 0.37.4 / 0.38.0 (branch feat/dvt-register-api-279);
+ * the installed workspace is 0.35.0, so importing them now would break type-check.
+ * Until it publishes, `DVT_SDK_READY` is false and the chain-touching calls throw
+ * {@link DvtSdkPendingError} — the wizard renders a clear "waiting for SDK" state
+ * rather than failing to build. Wiring is a one-file change: flip `DVT_SDK_READY`,
+ * add the import, and replace each `WIRE-HERE` stub body. The local, SDK-independent
+ * bits (BLS key generation + hex validation) already work.
+ *
+ * @module lib/sdk/dvtOperator
+ */
+import type { WalletClient } from "viem";
+
+/** Flip to true once @aastar/sdk 0.37.4/0.38.0 (PR #288) is installed and wired below. */
+export const DVT_SDK_READY = false;
+
+export type Hex = `0x${string}`;
+
+/** Proof-of-possession tuple produced locally from a BLS secret key. */
+export interface DvtPop {
+ /** EIP-2537 128-byte G1 public key (hex). */
+ publicKey: Hex;
+ /** G2 message point the PoP signs over (hex). */
+ popPoint: Hex;
+ /** BLS signature `sk · popPoint` (hex). */
+ popSig: Hex;
+ /** `keccak256(publicKey)` — the on-chain node id. */
+ nodeId: Hex;
+}
+
+/** Operator's on-chain eligibility snapshot for DVT registration. */
+export interface DvtEligibility {
+ /** Node id already bound to this operator, or null if unregistered. */
+ boundNodeId: Hex | null;
+ /** Whether stake-gated registration is currently open (`requireStake`). */
+ stakeOpen: boolean;
+ /** ROLE_DVT stake threshold in wei (`minStake`). */
+ minStake: bigint;
+}
+
+/** Result of a one-shot `register({ blsSecretKey })`. */
+export interface DvtRegisterResult {
+ hash: Hex;
+ pop: DvtPop;
+}
+
+/** Thrown by chain/crypto calls while the SDK is not yet published (PR #288). */
+export class DvtSdkPendingError extends Error {
+ constructor() {
+ super(
+ "DVT SDK not yet available (pending @aastar/sdk 0.37.4/0.38.0, aastar-sdk#279 / PR #288)."
+ );
+ this.name = "DvtSdkPendingError";
+ }
+}
+
+// ── Local helpers (SDK-independent, already functional) ──────────────────────
+
+/** True if `input` is a 0x-prefixed 32-byte (64 hex char) scalar. */
+export function isBlsSecretKeyHex(input: string): input is Hex {
+ return /^0x[0-9a-fA-F]{64}$/.test(input.trim());
+}
+
+/**
+ * Normalise a pasted BLS secret key to canonical lowercase `0x…64` hex.
+ * @throws if the input is not a 32-byte hex scalar.
+ */
+export function normalizeBlsSecretKey(input: string): Hex {
+ const v = input.trim();
+ if (!isBlsSecretKeyHex(v)) {
+ throw new Error("Invalid BLS secret key: expected a 0x-prefixed 32-byte hex string.");
+ }
+ return v.toLowerCase() as Hex;
+}
+
+/**
+ * Generate a random 32-byte BLS secret key in the browser (never leaves the tab).
+ * A uniformly random 256-bit value is < the BLS12-381 scalar order with
+ * overwhelming probability; if the SDK later exposes a canonical generator that
+ * reduces mod r, prefer it at the WIRE-HERE site.
+ */
+export function generateBlsSecretKey(): Hex {
+ const bytes = new Uint8Array(32);
+ crypto.getRandomValues(bytes);
+ const hex = Array.from(bytes, b => b.toString(16).padStart(2, "0")).join("");
+ return `0x${hex}` as Hex;
+}
+
+// ── SDK-backed surface (WIRE-HERE when PR #288 publishes) ────────────────────
+
+/**
+ * Derive the proof-of-possession + nodeId from a BLS secret key. Pure/local in
+ * the SDK too (no network), so once wired this works offline.
+ */
+export function buildDvtPop(_blsSecretKey: Hex): DvtPop {
+ // WIRE-HERE: return buildDvtPop(_blsSecretKey) from "@aastar/sdk/core".
+ throw new DvtSdkPendingError();
+}
+
+/** Read the operator's registration + stake eligibility. */
+export async function fetchDvtEligibility(
+ _walletClient: WalletClient,
+ _operator: Hex
+): Promise {
+ // WIRE-HERE:
+ // const dvt = _walletClient.extend(dvtOperatorActions(DVT_VALIDATOR_ADDRESS));
+ // const [boundNodeId, stakeOpen, minStake] = await Promise.all([
+ // dvt.operatorNode({ operator: _operator }),
+ // dvt.requireStake(),
+ // dvt.minStake(),
+ // ]);
+ // return { boundNodeId: isZero(boundNodeId) ? null : boundNodeId, stakeOpen, minStake };
+ throw new DvtSdkPendingError();
+}
+
+/** One-shot: build PoP from the secret key and submit `register`. */
+export async function registerDvtNode(
+ _walletClient: WalletClient,
+ _blsSecretKey: Hex
+): Promise {
+ // WIRE-HERE:
+ // const dvt = _walletClient.extend(dvtOperatorActions(DVT_VALIDATOR_ADDRESS));
+ // return dvt.register({ blsSecretKey: _blsSecretKey });
+ throw new DvtSdkPendingError();
+}
+
+/**
+ * HSM path: submit a PoP produced outside the browser via `registerWithProof`.
+ */
+export async function registerDvtNodeWithProof(
+ _walletClient: WalletClient,
+ _pop: Pick
+): Promise {
+ // WIRE-HERE:
+ // const dvt = _walletClient.extend(dvtOperatorActions(DVT_VALIDATOR_ADDRESS));
+ // return dvt.registerWithProof(_pop);
+ throw new DvtSdkPendingError();
+}
+
+/** Confirm a node id is registered on-chain (post-tx read-back). */
+export async function isDvtNodeRegistered(
+ _walletClient: WalletClient,
+ _nodeId: Hex
+): Promise {
+ // WIRE-HERE:
+ // const dvt = _walletClient.extend(dvtOperatorActions(DVT_VALIDATOR_ADDRESS));
+ // return dvt.isRegistered(_nodeId);
+ throw new DvtSdkPendingError();
+}
From 9c3aeee765add77284e39ab768ea71c8469c41f7 Mon Sep 17 00:00:00 2001
From: jhfnetboy
Date: Mon, 6 Jul 2026 22:22:58 +0700
Subject: [PATCH 2/4] feat(operator): wire DVT wizard to @aastar/sdk 0.38.0 DVT
actions (CC-17)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
@aastar/sdk 0.38.0 ships buildDvtPop + dvtOperatorActions + DVT_VALIDATOR_ADDRESS
(aastar-sdk#288, on-chain registerWithProof E2E LIVE PASS on Sepolia, tx
0x216a7ed5…, Codex APPROVE). Fill in the lib/sdk/dvtOperator.ts boundary:
- flip DVT_SDK_READY, drop the pending-stub scaffolding
- buildDvtPop -> sdk buildDvtPop; eligibility/register/isRegistered ->
dvtOperatorActions(DVT_VALIDATOR_ADDRESS)(client): reads on a public client,
register on the operator's wallet client
- validator address from the SDK canonical DVT_VALIDATOR_ADDRESS (post
ensureSdkConfig), never hardcoded
Bump @aastar/sdk ^0.35.0 -> ^0.38.0 in aastar-frontend (surgical lockfile
version/resolved/integrity; deps unchanged 0.35->0.38 so no tree churn; backend
range ^0.35.0 still satisfied by the hoisted 0.38.0). Frontend + backend
type-check, frontend eslint, and backend build all pass.
Claude-Session: https://claude.ai/code/session_01RajETCvboSvhadpqMbekNx
---
aastar-frontend/lib/sdk/dvtOperator.ts | 118 ++++++++++++-------------
aastar-frontend/package.json | 2 +-
package-lock.json | 8 +-
3 files changed, 60 insertions(+), 68 deletions(-)
diff --git a/aastar-frontend/lib/sdk/dvtOperator.ts b/aastar-frontend/lib/sdk/dvtOperator.ts
index c22308c..739c407 100644
--- a/aastar-frontend/lib/sdk/dvtOperator.ts
+++ b/aastar-frontend/lib/sdk/dvtOperator.ts
@@ -1,5 +1,5 @@
/**
- * DVT node-operator registration — SDK boundary (aastar-sdk#279 / PR #288).
+ * DVT node-operator registration — SDK boundary (aastar-sdk#279).
*
* DVT signing nodes register on the AAStarValidator via `registerWithProof`
* (YetAnotherAA-Validator #165): stake gate (ROLE_DVT + GToken ≥ minStake) + a
@@ -8,29 +8,25 @@
* browser wallet (viem WalletClient from WalletContext) — never the backend key,
* and the BLS **secret key never leaves the browser**.
*
- * The typed action surface below is the contract published by @repo:sdk:
- *
- * import { buildDvtPop, dvtOperatorActions } from "@aastar/sdk/core";
- * const pop = buildDvtPop(blsSecretKey); // { publicKey, popPoint, popSig, nodeId }
- * const dvt = walletClient.extend(dvtOperatorActions(validatorAddr));
- * await dvt.requireStake(); await dvt.minStake();
- * const bound = await dvt.operatorNode({ operator });
- * const { hash, pop } = await dvt.register({ blsSecretKey });
- *
- * Those exports ship in @aastar/sdk 0.37.4 / 0.38.0 (branch feat/dvt-register-api-279);
- * the installed workspace is 0.35.0, so importing them now would break type-check.
- * Until it publishes, `DVT_SDK_READY` is false and the chain-touching calls throw
- * {@link DvtSdkPendingError} — the wizard renders a clear "waiting for SDK" state
- * rather than failing to build. Wiring is a one-file change: flip `DVT_SDK_READY`,
- * add the import, and replace each `WIRE-HERE` stub body. The local, SDK-independent
- * bits (BLS key generation + hex validation) already work.
+ * Backed by @aastar/sdk 0.38.0 `@aastar/sdk/core` (aastar-sdk#288, E2E LIVE PASS
+ * on Sepolia, tx 0x216a7ed5…): `buildDvtPop` derives the PoP + nodeId locally
+ * (no network); `dvtOperatorActions(validator)(client)` decorates a viem client
+ * with the typed registry actions. The validator address comes from the SDK's
+ * canonical `DVT_VALIDATOR_ADDRESS` (set by `applyConfig` via `ensureSdkConfig`),
+ * never hardcoded here.
*
* @module lib/sdk/dvtOperator
*/
import type { WalletClient } from "viem";
+import {
+ buildDvtPop as sdkBuildDvtPop,
+ dvtOperatorActions,
+ DVT_VALIDATOR_ADDRESS,
+} from "@aastar/sdk/core";
+import { ensureSdkConfig, getPublicClient } from "./client";
-/** Flip to true once @aastar/sdk 0.37.4/0.38.0 (PR #288) is installed and wired below. */
-export const DVT_SDK_READY = false;
+/** DVT SDK actions are wired (kept for the wizard's step gating). */
+export const DVT_SDK_READY = true;
export type Hex = `0x${string}`;
@@ -62,17 +58,12 @@ export interface DvtRegisterResult {
pop: DvtPop;
}
-/** Thrown by chain/crypto calls while the SDK is not yet published (PR #288). */
-export class DvtSdkPendingError extends Error {
- constructor() {
- super(
- "DVT SDK not yet available (pending @aastar/sdk 0.37.4/0.38.0, aastar-sdk#279 / PR #288)."
- );
- this.name = "DvtSdkPendingError";
- }
+/** A bytes32 that is all zeros — the registry's "unset" sentinel for node ids. */
+function isZeroHash(value: string): boolean {
+ return /^0x0*$/i.test(value);
}
-// ── Local helpers (SDK-independent, already functional) ──────────────────────
+// ── Local helpers (SDK-independent) ──────────────────────────────────────────
/** True if `input` is a 0x-prefixed 32-byte (64 hex char) scalar. */
export function isBlsSecretKeyHex(input: string): input is Hex {
@@ -94,8 +85,8 @@ export function normalizeBlsSecretKey(input: string): Hex {
/**
* Generate a random 32-byte BLS secret key in the browser (never leaves the tab).
* A uniformly random 256-bit value is < the BLS12-381 scalar order with
- * overwhelming probability; if the SDK later exposes a canonical generator that
- * reduces mod r, prefer it at the WIRE-HERE site.
+ * overwhelming probability; on the ~2⁻¹²⁸ chance it is ≥ order, `buildDvtPop`
+ * throws and the operator simply regenerates.
*/
export function generateBlsSecretKey(): Hex {
const bytes = new Uint8Array(32);
@@ -104,64 +95,65 @@ export function generateBlsSecretKey(): Hex {
return `0x${hex}` as Hex;
}
-// ── SDK-backed surface (WIRE-HERE when PR #288 publishes) ────────────────────
+// ── SDK-backed surface (@aastar/sdk 0.38.0) ──────────────────────────────────
+
+/** DVT registry actions bound to a read-only public client. */
+function readActions() {
+ ensureSdkConfig();
+ return dvtOperatorActions(DVT_VALIDATOR_ADDRESS)(getPublicClient());
+}
+
+/** DVT registry actions bound to the operator's signing wallet client. */
+function writeActions(walletClient: WalletClient) {
+ ensureSdkConfig();
+ return dvtOperatorActions(DVT_VALIDATOR_ADDRESS)(walletClient);
+}
/**
- * Derive the proof-of-possession + nodeId from a BLS secret key. Pure/local in
- * the SDK too (no network), so once wired this works offline.
+ * Derive the proof-of-possession + nodeId from a BLS secret key. Pure/local
+ * (no network), so this works offline once the operator has a key.
+ * @throws if the secret key is out of range (0 or ≥ curve order).
*/
-export function buildDvtPop(_blsSecretKey: Hex): DvtPop {
- // WIRE-HERE: return buildDvtPop(_blsSecretKey) from "@aastar/sdk/core".
- throw new DvtSdkPendingError();
+export function buildDvtPop(blsSecretKey: Hex): DvtPop {
+ return sdkBuildDvtPop(blsSecretKey);
}
/** Read the operator's registration + stake eligibility. */
export async function fetchDvtEligibility(
_walletClient: WalletClient,
- _operator: Hex
+ operator: Hex
): Promise {
- // WIRE-HERE:
- // const dvt = _walletClient.extend(dvtOperatorActions(DVT_VALIDATOR_ADDRESS));
- // const [boundNodeId, stakeOpen, minStake] = await Promise.all([
- // dvt.operatorNode({ operator: _operator }),
- // dvt.requireStake(),
- // dvt.minStake(),
- // ]);
- // return { boundNodeId: isZero(boundNodeId) ? null : boundNodeId, stakeOpen, minStake };
- throw new DvtSdkPendingError();
+ const dvt = readActions();
+ const [boundNodeId, stakeOpen, minStake] = await Promise.all([
+ dvt.operatorNode({ operator }),
+ dvt.requireStake(),
+ dvt.minStake(),
+ ]);
+ return { boundNodeId: isZeroHash(boundNodeId) ? null : boundNodeId, stakeOpen, minStake };
}
/** One-shot: build PoP from the secret key and submit `register`. */
export async function registerDvtNode(
- _walletClient: WalletClient,
- _blsSecretKey: Hex
+ walletClient: WalletClient,
+ blsSecretKey: Hex
): Promise {
- // WIRE-HERE:
- // const dvt = _walletClient.extend(dvtOperatorActions(DVT_VALIDATOR_ADDRESS));
- // return dvt.register({ blsSecretKey: _blsSecretKey });
- throw new DvtSdkPendingError();
+ return writeActions(walletClient).register({ blsSecretKey });
}
/**
* HSM path: submit a PoP produced outside the browser via `registerWithProof`.
*/
export async function registerDvtNodeWithProof(
- _walletClient: WalletClient,
- _pop: Pick
+ walletClient: WalletClient,
+ pop: Pick
): Promise {
- // WIRE-HERE:
- // const dvt = _walletClient.extend(dvtOperatorActions(DVT_VALIDATOR_ADDRESS));
- // return dvt.registerWithProof(_pop);
- throw new DvtSdkPendingError();
+ return writeActions(walletClient).registerWithProof(pop);
}
/** Confirm a node id is registered on-chain (post-tx read-back). */
export async function isDvtNodeRegistered(
_walletClient: WalletClient,
- _nodeId: Hex
+ nodeId: Hex
): Promise {
- // WIRE-HERE:
- // const dvt = _walletClient.extend(dvtOperatorActions(DVT_VALIDATOR_ADDRESS));
- // return dvt.isRegistered(_nodeId);
- throw new DvtSdkPendingError();
+ return readActions().isRegistered({ nodeId });
}
diff --git a/aastar-frontend/package.json b/aastar-frontend/package.json
index f9c50df..6342c90 100644
--- a/aastar-frontend/package.json
+++ b/aastar-frontend/package.json
@@ -18,7 +18,7 @@
"test:e2e:ui": "playwright test"
},
"dependencies": {
- "@aastar/sdk": "^0.35.0",
+ "@aastar/sdk": "^0.38.0",
"@headlessui/react": "^2.2.10",
"@heroicons/react": "^2.2.0",
"@simplewebauthn/browser": "^13.3.0",
diff --git a/package-lock.json b/package-lock.json
index c3a16cd..28dbf52 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -82,7 +82,7 @@
"aastar-frontend": {
"version": "0.1.0",
"dependencies": {
- "@aastar/sdk": "^0.35.0",
+ "@aastar/sdk": "^0.38.0",
"@headlessui/react": "^2.2.10",
"@heroicons/react": "^2.2.0",
"@simplewebauthn/browser": "^13.3.0",
@@ -1428,9 +1428,9 @@
}
},
"node_modules/@aastar/sdk": {
- "version": "0.35.0",
- "resolved": "https://registry.npmmirror.com/@aastar/sdk/-/sdk-0.35.0.tgz",
- "integrity": "sha512-9hm2K88FssUBNtaO7AYA3CkJqWy4AANNRUi8QV77URmXfeBl9pxSj7lJhzkP0BFQG+o3BNiizVDdnCP6XN+Ffg==",
+ "version": "0.38.0",
+ "resolved": "https://registry.npmmirror.com/@aastar/sdk/-/sdk-0.38.0.tgz",
+ "integrity": "sha512-KiBBSrdL60KJIOM1ZHPQtQPc6diFliJkUMpaEbI6zkkL5Lbd5A8TM7aNosGXTToX5wSyc6gRTBDly4DNKQXa6g==",
"license": "Apache-2.0",
"dependencies": {
"@simplewebauthn/browser": "^13.2.2",
From d99a5af19417ee651821bcd29936dbd87d92b47b Mon Sep 17 00:00:00 2001
From: jhfnetboy
Date: Mon, 6 Jul 2026 22:48:38 +0700
Subject: [PATCH 3/4] fix(operator): address PR #424 review (BLS keygen range,
read-back, key commit)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- F1 [Medium] generateBlsSecretKey: a single 256-bit draw is ≥ the BLS12-381
scalar order ~55% of the time (r ≈ 0.453·2^256), so "Generate" produced a key
buildDvtPop rejects roughly half the time. Rejection-sample until the draw is
in [1, r-1]; fix the bogus "~2^-128" comment.
- F2 [Low] Step4: isDvtNodeRegistered's boolean was awaited but ignored — a
false read-back now surfaces a warning (tx already confirmed, so still advance).
- M1 [Low] Step3 applyKey: an out-of-range imported key was still written to
wizard state (Continue enabled) despite buildDvtPop throwing. Commit the key
only when its PoP derives; clear it on failure.
Frontend type-check + eslint green.
Claude-Session: https://claude.ai/code/session_01RajETCvboSvhadpqMbekNx
---
.../dvt-register/steps/Step3KeyPoP.tsx | 19 ++++++--------
.../dvt-register/steps/Step4Register.tsx | 7 ++++--
aastar-frontend/lib/i18n/locales/en.json | 3 ++-
aastar-frontend/lib/i18n/locales/zh.json | 3 ++-
aastar-frontend/lib/sdk/dvtOperator.ts | 25 +++++++++++++------
5 files changed, 34 insertions(+), 23 deletions(-)
diff --git a/aastar-frontend/app/operator/dvt-register/steps/Step3KeyPoP.tsx b/aastar-frontend/app/operator/dvt-register/steps/Step3KeyPoP.tsx
index 2c0610d..c750cfe 100644
--- a/aastar-frontend/app/operator/dvt-register/steps/Step3KeyPoP.tsx
+++ b/aastar-frontend/app/operator/dvt-register/steps/Step3KeyPoP.tsx
@@ -24,11 +24,9 @@ import StepCard from "@/app/operator/deploy/components/StepCard";
import WizardButton from "@/app/operator/deploy/components/WizardButton";
import FormField from "@/app/operator/deploy/components/FormField";
import {
- DVT_SDK_READY,
buildDvtPop,
generateBlsSecretKey,
normalizeBlsSecretKey,
- type DvtPop,
type Hex,
} from "@/lib/sdk/dvtOperator";
import PendingSdkNote from "../components/PendingSdkNote";
@@ -41,16 +39,15 @@ export default function Step3KeyPoP({ data, update, onNext, onBack }: DvtStepPro
const applyKey = (secret: Hex) => {
setError(undefined);
- // buildDvtPop is pure/local once wired; keep the derived PoP alongside the key.
- let pop: DvtPop | undefined;
- if (DVT_SDK_READY) {
- try {
- pop = buildDvtPop(secret);
- } catch (err) {
- setError(err instanceof Error ? err.message : String(err));
- }
+ // Only commit the key once its PoP derives — an out-of-range key (e.g. a
+ // pasted scalar ≥ curve order) must not advance the wizard to registration.
+ try {
+ const pop = buildDvtPop(secret);
+ update({ blsSecretKey: secret, pop });
+ } catch (err) {
+ setError(err instanceof Error ? err.message : String(err));
+ update({ blsSecretKey: undefined, pop: undefined });
}
- update({ blsSecretKey: secret, pop });
};
const handleGenerate = () => {
diff --git a/aastar-frontend/app/operator/dvt-register/steps/Step4Register.tsx b/aastar-frontend/app/operator/dvt-register/steps/Step4Register.tsx
index d4e7941..f13c633 100644
--- a/aastar-frontend/app/operator/dvt-register/steps/Step4Register.tsx
+++ b/aastar-frontend/app/operator/dvt-register/steps/Step4Register.tsx
@@ -49,9 +49,12 @@ export default function Step4Register({
);
if (!hash) return;
update({ registerTxHash: hash });
- // Best-effort read-back; failure here doesn't undo a confirmed tx.
+ // Best-effort read-back: the tx already confirmed, so a false/failed check
+ // only warns (e.g. RPC lag) — it never undoes a landed registration.
try {
- if (nodeId) await isDvtNodeRegistered(walletClient, nodeId);
+ if (nodeId && !(await isDvtNodeRegistered(walletClient, nodeId))) {
+ setConfirmError(t("dvtRegister.step4.confirmFailed"));
+ }
} catch (err) {
setConfirmError(err instanceof Error ? err.message : String(err));
}
diff --git a/aastar-frontend/lib/i18n/locales/en.json b/aastar-frontend/lib/i18n/locales/en.json
index c2fa9af..30a2318 100644
--- a/aastar-frontend/lib/i18n/locales/en.json
+++ b/aastar-frontend/lib/i18n/locales/en.json
@@ -836,7 +836,8 @@
"submitting": "Confirm in your wallet…",
"submitted": "Registration transaction submitted",
"register": "Register node",
- "hint": "After confirmation we read isRegistered back to confirm the node is on-chain."
+ "hint": "After confirmation we read isRegistered back to confirm the node is on-chain.",
+ "confirmFailed": "The transaction landed, but isRegistered still reads false (possibly RPC lag). Re-check the node status in the operator hub shortly."
},
"done": {
"title": "Registration complete",
diff --git a/aastar-frontend/lib/i18n/locales/zh.json b/aastar-frontend/lib/i18n/locales/zh.json
index 7cc04a0..006ebdb 100644
--- a/aastar-frontend/lib/i18n/locales/zh.json
+++ b/aastar-frontend/lib/i18n/locales/zh.json
@@ -836,7 +836,8 @@
"submitting": "请在钱包中确认…",
"submitted": "注册交易已提交",
"register": "注册节点",
- "hint": "交易确认后会读回 isRegistered 以确认节点已上链。"
+ "hint": "交易确认后会读回 isRegistered 以确认节点已上链。",
+ "confirmFailed": "交易已上链,但读回 isRegistered 暂为 false(可能是节点 RPC 有延迟)。请稍后在运营中心复查节点状态。"
},
"done": {
"title": "注册完成",
diff --git a/aastar-frontend/lib/sdk/dvtOperator.ts b/aastar-frontend/lib/sdk/dvtOperator.ts
index 739c407..ec299f4 100644
--- a/aastar-frontend/lib/sdk/dvtOperator.ts
+++ b/aastar-frontend/lib/sdk/dvtOperator.ts
@@ -83,16 +83,25 @@ export function normalizeBlsSecretKey(input: string): Hex {
}
/**
- * Generate a random 32-byte BLS secret key in the browser (never leaves the tab).
- * A uniformly random 256-bit value is < the BLS12-381 scalar order with
- * overwhelming probability; on the ~2⁻¹²⁸ chance it is ≥ order, `buildDvtPop`
- * throws and the operator simply regenerates.
+ * BLS12-381 scalar field order r. A random 256-bit value is ≥ r ~55% of the time
+ * (r ≈ 0.453·2²⁵⁶), so a single draw is NOT reliably a valid secret key.
+ */
+const BLS12_381_R = 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001n;
+
+/**
+ * Generate a random BLS secret key in the browser (never leaves the tab).
+ * Rejection-samples until the 256-bit draw lands in [1, r-1] — the valid scalar
+ * range `buildDvtPop` requires — so the result never trips its range check.
+ * Success probability per draw is ~45%, so this loops ~2 times on average.
*/
export function generateBlsSecretKey(): Hex {
- const bytes = new Uint8Array(32);
- crypto.getRandomValues(bytes);
- const hex = Array.from(bytes, b => b.toString(16).padStart(2, "0")).join("");
- return `0x${hex}` as Hex;
+ for (;;) {
+ const bytes = new Uint8Array(32);
+ crypto.getRandomValues(bytes);
+ const hex = Array.from(bytes, b => b.toString(16).padStart(2, "0")).join("");
+ const value = BigInt(`0x${hex}`);
+ if (value >= 1n && value < BLS12_381_R) return `0x${hex}` as Hex;
+ }
}
// ── SDK-backed surface (@aastar/sdk 0.38.0) ──────────────────────────────────
From 3755e89db1313338bd14fddb576b5fab5a1ba199 Mon Sep 17 00:00:00 2001
From: jhfnetboy
Date: Mon, 6 Jul 2026 23:08:38 +0700
Subject: [PATCH 4/4] fix(deps): bump backend @aastar/sdk range to ^0.38.0 to
unbreak npm ci
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The prior commit bumped only aastar-frontend to ^0.38.0. For 0.x versions
`^0.35.0` means >=0.35.0 <0.36.0, so the backend's untouched ^0.35.0 no longer
matched the hoisted 0.38.0 — `npm ci` failed the lockfile sync check ("Missing:
@aastar/sdk@0.35.0"), taking every CI job down at the install step. Bump the
backend range (+ its lockfile stanza) to ^0.38.0; backend already type-checks
and builds against 0.38.0. `npm ci --dry-run` sync gate passes.
Claude-Session: https://claude.ai/code/session_01RajETCvboSvhadpqMbekNx
---
aastar/package.json | 2 +-
package-lock.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/aastar/package.json b/aastar/package.json
index ddb1ef8..31f11f6 100644
--- a/aastar/package.json
+++ b/aastar/package.json
@@ -50,7 +50,7 @@
"typescript": "^5.9.3"
},
"dependencies": {
- "@aastar/sdk": "^0.35.0",
+ "@aastar/sdk": "^0.38.0",
"@nestjs/common": "^11.1.6",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.1.18",
diff --git a/package-lock.json b/package-lock.json
index 28dbf52..1b579b0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -25,7 +25,7 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
- "@aastar/sdk": "^0.35.0",
+ "@aastar/sdk": "^0.38.0",
"@nestjs/common": "^11.1.6",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.1.18",