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 ( +
+

{label}

+

{value}

+
+ ); +} 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",