@@ -351,10 +351,11 @@ function toXOnly(pubkeyHex: string): string {
351351export type DeprecatedSignerSpec = string | { priv : string ; cutoffDate ?: number } ;
352352
353353/**
354- * Perform a real server-signer rotation: recreate `arkd-wallet` with the given
355- * active signer (and optional deprecated signers, each with an optional cutoff
356- * date), then restart `arkd` so it re-reads the signer set. Resolves once
357- * `/v1/info` reflects the rotation.
354+ * Perform a real server-signer rotation by delegating to the regtest CLI's
355+ * `set-signers`: it recreates `arkd-wallet` with the given active signer (and
356+ * optional deprecated signers, each with an optional cutoff date), unlocks it,
357+ * and restarts `arkd` so it re-reads the signer set. Resolves once `/v1/info`
358+ * reflects the exact rotation.
358359 *
359360 * Keys are **private** keys (hex), matching the `ARKD_WALLET_SIGNER_KEY` /
360361 * `ARKD_WALLET_DEPRECATED_SIGNER_KEYS` fixture env. The fixture must hold the
@@ -371,57 +372,32 @@ export async function rotateArkdSigner(params: {
371372 typeof d === "string" ? { priv : d , cutoffDate : undefined as number | undefined } : d ,
372373 ) ;
373374
374- // Build the recreate subprocess env exactly as `regtest.mjs` would: defaults
375- // + the package `.env.regtest`, so `ARKD_WALLET_IMAGE` resolves to the
376- // rotation-capable local image and ports match. A bare vitest process does
377- // not have this env, and the compose default image has no rotation support.
378- // `regtest` is symlinked into the package dir by scripts/regtest.sh, so this
379- // path resolves at e2e runtime.
380- const { loadEnv } = await import ( "../../regtest/lib/env.mjs" ) ;
381- loadEnv ( "regtest" , ".env.regtest" ) ;
382-
383- // arkd parses each entry as `<hexkey>[:<unix-seconds cutoff>]`.
384- const env = {
385- ...process . env ,
386- ARKD_WALLET_SIGNER_KEY : activeSignerPriv ,
387- ARKD_WALLET_DEPRECATED_SIGNER_KEYS : deprecated
388- . map ( ( d ) => ( d . cutoffDate != null ? `${ d . priv } :${ d . cutoffDate } ` : d . priv ) )
389- . join ( "," ) ,
390- } ;
391-
392- // Recreate ONLY arkd-wallet, reusing the named `ark_wallet_datadir` volume
393- // so the on-chain wallet seed/state survive (never pass --volumes). The
394- // three-file, profiled, project-named form matches the running stack
395- // (regtest/lib/compose.mjs); the Go reference's single-file/profile-less
396- // form would target the wrong compose project. Both `base` and `ark`
397- // profiles must be enabled: `--no-deps` keeps arkd-wallet's dependencies
398- // (bitcoin, nbxplorer — both `base`-gated) from being recreated, but they
399- // must still be DEFINED in the project or compose rejects arkd-wallet's
400- // `depends_on` as an "undefined service".
401- execSync (
402- [
403- "docker compose" ,
404- "-p arkade-regtest" ,
405- "-f regtest/docker/compose.base.yml" ,
406- "-f regtest/docker/compose.ark.yml" ,
407- "-f test/e2e/compose.rotation.yml" ,
408- "--profile base --profile ark" ,
409- "up -d --force-recreate --no-deps arkd-wallet" ,
410- ] . join ( " " ) ,
411- { stdio : "pipe" , env } ,
412- ) ;
375+ // Delegate the rotation to the regtest CLI's `set-signers`, which owns the
376+ // recreate → unlock → restart-arkd → readiness mechanism (and the env/compose
377+ // plumbing). `regtest` is symlinked into the package dir by scripts/regtest.sh;
378+ // `--env .env.regtest` makes the CLI use THIS package's regtest env (image,
379+ // ports) — the same override the stack was started with — so the recreated
380+ // arkd-wallet matches the running stack. arkd parses each deprecated entry as
381+ // `<hexkey>[:<unix-seconds cutoff>]`.
382+ const deprecatedArg = deprecated
383+ . map ( ( d ) => ( d . cutoffDate != null ? `${ d . priv } :${ d . cutoffDate } ` : d . priv ) )
384+ . join ( "," ) ;
385+ try {
386+ execSync (
387+ `node regtest/regtest.mjs set-signers --env .env.regtest --active ${ activeSignerPriv } ` +
388+ ( deprecatedArg ? ` --deprecated ${ deprecatedArg } ` : "" ) ,
389+ { stdio : "pipe" } ,
390+ ) ;
391+ } catch ( err ) {
392+ const e = err as { stderr ?: Buffer ; stdout ?: Buffer ; message ?: string } ;
393+ throw new Error (
394+ `rotateArkdSigner: set-signers failed: ${ e . stderr ?. toString ( ) || e . stdout ?. toString ( ) || e . message } ` ,
395+ ) ;
396+ }
413397
414- // arkd caches the signer pubkey set at startup, so it must restart to pick
415- // up the rotated wallet. On this stack arkd auto-unlocks the recreated
416- // wallet via its env unlocker (ARKD_UNLOCKER_TYPE=env) on restart, so no
417- // admin unlock call is needed (unlike the Go reference's stack).
418- execSync ( "docker container stop arkd" , { stdio : "pipe" } ) ;
419- await new Promise ( ( r ) => setTimeout ( r , 5000 ) ) ;
420- execSync ( "docker container start arkd" , { stdio : "pipe" , env } ) ;
421-
422- // Wait for arkd readiness AND for the rotation to be observable: RPCs racing
423- // the restart get "server not ready", so poll until `/v1/info` both succeeds
424- // and reports the new active signer + the full deprecated set.
398+ // The CLI already waited for arkd to re-sync and verified the deprecated
399+ // COUNT; assert the EXACT pubkeys here (identity, not just count) and return
400+ // the resulting info. Poll briefly in case `/v1/info` is still settling.
425401 const expectedActive = toXOnly (
426402 hex . encode ( await SingleKey . fromHex ( activeSignerPriv ) . xOnlyPublicKey ( ) ) ,
427403 ) ;
0 commit comments