@mysten-incubation/devstack 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +9 -10
  2. package/dashboard-ui/assets/{grpc-CpkDu4SA.js → grpc-s7Ztk9wv.js} +1 -1
  3. package/dashboard-ui/assets/{index-jLPRmjst.js → index-DrOd0m4F.js} +2 -2
  4. package/dashboard-ui/index.html +1 -1
  5. package/dist/contracts/routable.d.mts +1 -1
  6. package/dist/index.d.mts +3 -3
  7. package/dist/orchestrators/boot.mjs +2 -1
  8. package/dist/orchestrators/boot.mjs.map +1 -1
  9. package/dist/orchestrators/router/file-provider.mjs +11 -8
  10. package/dist/orchestrators/router/file-provider.mjs.map +1 -1
  11. package/dist/orchestrators/router/service.mjs +2 -2
  12. package/dist/orchestrators/router/service.mjs.map +1 -1
  13. package/dist/plugins/deepbook/index.d.mts +36 -6
  14. package/dist/plugins/deepbook/index.mjs +52 -16
  15. package/dist/plugins/deepbook/index.mjs.map +1 -1
  16. package/dist/plugins/seal/codegen.d.mts +5 -0
  17. package/dist/plugins/seal/codegen.mjs +53 -10
  18. package/dist/plugins/seal/codegen.mjs.map +1 -1
  19. package/dist/plugins/seal/index.d.mts +64 -19
  20. package/dist/plugins/seal/index.mjs +26 -36
  21. package/dist/plugins/seal/index.mjs.map +1 -1
  22. package/dist/plugins/seal/mode/fork-known.d.mts +0 -2
  23. package/dist/plugins/seal/mode/fork-known.mjs +11 -22
  24. package/dist/plugins/seal/mode/fork-known.mjs.map +1 -1
  25. package/dist/plugins/seal/mode/live.d.mts +36 -13
  26. package/dist/plugins/seal/mode/live.mjs +104 -43
  27. package/dist/plugins/seal/mode/live.mjs.map +1 -1
  28. package/dist/plugins/seal/mode/local-keygen.mjs +2 -1
  29. package/dist/plugins/seal/mode/local-keygen.mjs.map +1 -1
  30. package/dist/plugins/seal/registry-publish.d.mts +15 -3
  31. package/dist/plugins/seal/registry-publish.mjs.map +1 -1
  32. package/dist/plugins/sui/index.d.mts +10 -10
  33. package/dist/plugins/walrus/client-services.d.mts +1 -0
  34. package/dist/plugins/walrus/client-services.mjs +115 -0
  35. package/dist/plugins/walrus/client-services.mjs.map +1 -0
  36. package/dist/plugins/walrus/deploy.mjs +11 -5
  37. package/dist/plugins/walrus/deploy.mjs.map +1 -1
  38. package/dist/plugins/walrus/errors.d.mts +1 -1
  39. package/dist/plugins/walrus/errors.mjs.map +1 -1
  40. package/dist/plugins/walrus/index.d.mts +138 -9
  41. package/dist/plugins/walrus/index.mjs +72 -20
  42. package/dist/plugins/walrus/index.mjs.map +1 -1
  43. package/dist/plugins/walrus/mode/known-deploy.d.mts +5 -1
  44. package/dist/plugins/walrus/mode/known-deploy.mjs +14 -11
  45. package/dist/plugins/walrus/mode/known-deploy.mjs.map +1 -1
  46. package/dist/plugins/walrus/mode/local-cluster.d.mts +14 -1
  47. package/dist/plugins/walrus/mode/local-cluster.mjs +54 -12
  48. package/dist/plugins/walrus/mode/local-cluster.mjs.map +1 -1
  49. package/dist/plugins/walrus/routable.mjs +12 -15
  50. package/dist/plugins/walrus/routable.mjs.map +1 -1
  51. package/dist/plugins/walrus/snapshot.mjs +3 -2
  52. package/dist/plugins/walrus/snapshot.mjs.map +1 -1
  53. package/dist/plugins/walrus/storage-nodes.mjs +31 -33
  54. package/dist/plugins/walrus/storage-nodes.mjs.map +1 -1
  55. package/dist/substrate/runtime/config-validation.mjs +1 -1
  56. package/images/walrus/Dockerfile +17 -4
  57. package/images/walrus/deploy-walrus.sh +7 -4
  58. package/images/walrus/run-walrus-client-service.sh +103 -0
  59. package/package.json +2 -2
  60. package/dist/plugins/seal/service.mjs +0 -17
  61. package/dist/plugins/seal/service.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"known-deploy.mjs","names":[],"sources":["../../../../src/plugins/walrus/mode/known-deploy.ts"],"sourcesContent":["// Walrus mode — known deployment (testnet / mainnet / *-fork via\n// upstream translation).\n//\n// Distilled-doc reference (06-walrus.md §\"Lifecycle: Startup —\n// known deployment\"). Purely synchronous body — no ordered phases.\n// The factory validates required deployment fields and returns a\n// resolved value whose tags are eager `Layer.succeed(...)` constants.\n//\n// Distilled-doc invariants honored:\n// - 15: each of `proxyUrl / aggregatorUrl / publisherUrl` surfaces\n// INDEPENDENTLY. Encoded as `string | null` in the resolved\n// shape — a given field is null only when THAT specific URL\n// is unresolved (registry default + explicit override both\n// absent). The plugin's projection step publishes each tag\n// when its own URL is present; a missing publisher URL does\n// not suppress an available proxy/aggregator URL.\n// - 16: throw synchronously when `nodes` is missing for a\n// registered network. Testnet has 100+ nodes that the\n// `@mysten/walrus` SDK fetches dynamically; pinning them\n// statically would be misleading.\n\nimport { Effect, type Scope } from 'effect';\n\nimport { expectNonEmptyString } from '../../../substrate/runtime/config-validation.ts';\nimport { walrusConfigError, type WalrusConfigError } from '../errors.ts';\nimport type { WalrusStorageNode } from '../storage-nodes.ts';\n\n/** Known networks supported by the registry lookup. */\nexport type WalrusKnownNetwork = 'testnet' | 'mainnet' | 'devnet';\n\n/** Options for the known-deployment mode. */\nexport interface WalrusKnownDeploymentOptions {\n\t/** Network shortcut — looks up per-field defaults from a registry\n\t * baked into this package. */\n\treadonly network?: WalrusKnownNetwork;\n\t/** Per-field overrides. Required when `network` is omitted. */\n\treadonly systemObjectId?: string;\n\treadonly stakingPoolId?: string;\n\treadonly exchangeIds?: ReadonlyArray<string>;\n\t/** Explicit storage-node committee. Required (throws if missing\n\t * even when `network` is set — distilled-doc invariant 16). */\n\treadonly nodes?: ReadonlyArray<WalrusStorageNode>;\n\treadonly aggregatorUrl?: string;\n\treadonly publisherUrl?: string;\n\treadonly proxyUrl?: string;\n}\n\n/** Resolved known-deployment boot artifacts. */\nexport interface KnownDeploymentBootResult {\n\treadonly mode: 'known';\n\t/** The known network's name (`testnet`/`mainnet`/`devnet`) — its stable\n\t * identity. A known remote deployment has no per-boot genesis digest, so\n\t * the network name IS its `chainId` for codegen/snapshot keying. */\n\treadonly network: string;\n\treadonly systemObjectId: string;\n\treadonly stakingPoolId: string;\n\treadonly exchangeIds: ReadonlyArray<string>;\n\treadonly nodes: ReadonlyArray<WalrusStorageNode>;\n\t/** Null only when the proxy URL itself is unresolved (no explicit\n\t * `proxyUrl` override and no registry/aggregator/publisher\n\t * fallback). Surfaces independently of `aggregatorUrl` /\n\t * `publisherUrl` — distilled-doc invariant 15. */\n\treadonly proxyUrl: string | null;\n\treadonly aggregatorUrl: string | null;\n\treadonly publisherUrl: string | null;\n}\n\n/** Known-deployment registry — baked-in record per network. The\n * values here are the `knownDeployments.walrus.{testnet, mainnet}`\n * entries. Devnet has no canonical record today. */\nconst KNOWN_DEPLOYMENT_REGISTRY: Readonly<\n\tRecord<\n\t\tWalrusKnownNetwork,\n\t\t{\n\t\t\treadonly network: string;\n\t\t\treadonly systemObjectId?: string;\n\t\t\treadonly stakingPoolId?: string;\n\t\t\treadonly exchangeIds?: ReadonlyArray<string>;\n\t\t\treadonly aggregatorUrl?: string;\n\t\t\treadonly publisherUrl?: string;\n\t\t\treadonly proxyUrl?: string;\n\t\t}\n\t>\n> = {\n\ttestnet: {\n\t\tnetwork: 'testnet',\n\t\t// Real ids must be supplied via the explicit options form\n\t\t// (`walrusFor(testnet).known({ systemObjectId, stakingPoolId, ... })`).\n\t\t// The known-deployment lookup table only canonicalises the URLs;\n\t\t// the on-chain ids are network-specific and live outside this\n\t\t// package.\n\t\tsystemObjectId: undefined,\n\t\tstakingPoolId: undefined,\n\t\texchangeIds: [],\n\t\taggregatorUrl: 'https://aggregator.walrus-testnet.walrus.space',\n\t\tpublisherUrl: 'https://publisher.walrus-testnet.walrus.space',\n\t\tproxyUrl: 'https://aggregator.walrus-testnet.walrus.space',\n\t},\n\tmainnet: {\n\t\tnetwork: 'mainnet',\n\t\tsystemObjectId: undefined,\n\t\tstakingPoolId: undefined,\n\t\texchangeIds: [],\n\t\taggregatorUrl: 'https://aggregator.walrus.space',\n\t\tpublisherUrl: 'https://publisher.walrus.space',\n\t\tproxyUrl: 'https://aggregator.walrus.space',\n\t},\n\tdevnet: {\n\t\tnetwork: 'devnet',\n\t},\n};\n\n/** Synchronous factory-time validation + projection. Throws (NOT\n * Effect-fail) on missing required fields so misconfiguration trips\n * at the `defineDevstack` call site rather than at deferred\n * Layer.build time. Distilled-doc invariants 14 + 16. */\nexport const resolveKnownDeploymentOptions = (\n\topts: WalrusKnownDeploymentOptions,\n): KnownDeploymentBootResult => {\n\tconst reg = opts.network ? KNOWN_DEPLOYMENT_REGISTRY[opts.network] : undefined;\n\n\tconst systemObjectId = expectNonEmptyString(opts.systemObjectId ?? reg?.systemObjectId, {\n\t\tfield: 'systemObjectId',\n\t\tmkError: ({ field }) =>\n\t\t\twalrusConfigError(\n\t\t\t\tfield,\n\t\t\t\t`walrusKnownDeployment: 'systemObjectId' is required (pass it explicitly, or pass network with a registered entry)`,\n\t\t\t),\n\t});\n\n\tconst stakingPoolId = expectNonEmptyString(opts.stakingPoolId ?? reg?.stakingPoolId, {\n\t\tfield: 'stakingPoolId',\n\t\tmkError: ({ field }) =>\n\t\t\twalrusConfigError(\n\t\t\t\tfield,\n\t\t\t\t`walrusKnownDeployment: 'stakingPoolId' is required (pass it explicitly, or pass network with a registered entry)`,\n\t\t\t),\n\t});\n\n\tconst nodes = opts.nodes;\n\tif (!nodes) {\n\t\t// Distilled-doc invariant 16: even when `network` is set, the\n\t\t// nodes must be explicit. Testnet has 100+ nodes the SDK\n\t\t// fetches dynamically; pinning would be misleading.\n\t\tthrow walrusConfigError(\n\t\t\t'nodes',\n\t\t\t`walrusKnownDeployment: explicit 'nodes' committee is required — ` +\n\t\t\t\t`Walrus ${opts.network ?? 'custom'} has nodes fetched dynamically by the SDK`,\n\t\t\t`pass an empty array if you accept the SDK-driven committee lookup, ` +\n\t\t\t\t`or use walrus()/walrusFor(network).local({...}) for a local self-hosted cluster`,\n\t\t);\n\t}\n\n\tconst aggregatorUrl = opts.aggregatorUrl ?? reg?.aggregatorUrl ?? null;\n\tconst publisherUrl = opts.publisherUrl ?? reg?.publisherUrl ?? null;\n\tconst proxyUrl = opts.proxyUrl ?? reg?.proxyUrl ?? aggregatorUrl ?? publisherUrl ?? null;\n\n\t// Invariant 15: surface null per individual URL when missing so the\n\t// plugin's projection can conditionally publish each tag. Previously\n\t// any single missing URL nullified all three, dropping user-supplied\n\t// values for the URLs they did provide.\n\treturn {\n\t\tmode: 'known' as const,\n\t\tnetwork: reg?.network ?? 'custom',\n\t\tsystemObjectId,\n\t\tstakingPoolId,\n\t\texchangeIds: opts.exchangeIds ?? reg?.exchangeIds ?? [],\n\t\tnodes,\n\t\tproxyUrl,\n\t\taggregatorUrl,\n\t\tpublisherUrl,\n\t};\n};\n\n/** \"Boot\" the known deployment. Purely synchronous projection inside\n * Effect.gen — no I/O, no Scope work. Surfaces as Effect for\n * uniformity with the local-cluster boot signature. */\nexport const bootKnownDeployment = (\n\topts: WalrusKnownDeploymentOptions,\n): Effect.Effect<KnownDeploymentBootResult, WalrusConfigError, Scope.Scope> =>\n\tEffect.try({\n\t\ttry: () => resolveKnownDeploymentOptions(opts),\n\t\t// `resolveKnownDeploymentOptions` throws our typed config error\n\t\t// shape. STYLE_GUIDE §2 forbids bare error casts; runtime-guard\n\t\t// the `_tag` so an unexpected synchronous throw (e.g. a future\n\t\t// helper that throws a stock `TypeError`) is wrapped instead of\n\t\t// silently mis-tagged as `WalrusConfigError`.\n\t\tcatch: (err): WalrusConfigError => {\n\t\t\tif (\n\t\t\t\ttypeof err === 'object' &&\n\t\t\t\terr !== null &&\n\t\t\t\t'_tag' in err &&\n\t\t\t\t(err as { readonly _tag?: unknown })._tag === 'WalrusConfigError'\n\t\t\t) {\n\t\t\t\treturn err as WalrusConfigError;\n\t\t\t}\n\t\t\treturn walrusConfigError(\n\t\t\t\t'unknown',\n\t\t\t\t`walrusKnownDeployment: unexpected non-typed throw inside resolveKnownDeploymentOptions: ${\n\t\t\t\t\terr instanceof Error ? err.message : String(err)\n\t\t\t\t}`,\n\t\t\t\tundefined,\n\t\t\t\terr,\n\t\t\t);\n\t\t},\n\t});\n"],"mappings":";;;;;;;AAsEA,MAAM,4BAaF;CACH,SAAS;EACR,SAAS;EAMT,gBAAgB,KAAA;EAChB,eAAe,KAAA;EACf,aAAa,CAAC;EACd,eAAe;EACf,cAAc;EACd,UAAU;CACX;CACA,SAAS;EACR,SAAS;EACT,gBAAgB,KAAA;EAChB,eAAe,KAAA;EACf,aAAa,CAAC;EACd,eAAe;EACf,cAAc;EACd,UAAU;CACX;CACA,QAAQ,EACP,SAAS,SACV;AACD;;;;;AAMA,MAAa,iCACZ,SAC+B;CAC/B,MAAM,MAAM,KAAK,UAAU,0BAA0B,KAAK,WAAW,KAAA;CAErE,MAAM,iBAAiB,qBAAqB,KAAK,kBAAkB,KAAK,gBAAgB;EACvF,OAAO;EACP,UAAU,EAAE,YACX,kBACC,OACA,mHACD;CACF,CAAC;CAED,MAAM,gBAAgB,qBAAqB,KAAK,iBAAiB,KAAK,eAAe;EACpF,OAAO;EACP,UAAU,EAAE,YACX,kBACC,OACA,kHACD;CACF,CAAC;CAED,MAAM,QAAQ,KAAK;CACnB,IAAI,CAAC,OAIJ,MAAM,kBACL,SACA,0EACW,KAAK,WAAW,SAAS,4CACpC,oJAED;CAGD,MAAM,gBAAgB,KAAK,iBAAiB,KAAK,iBAAiB;CAClE,MAAM,eAAe,KAAK,gBAAgB,KAAK,gBAAgB;CAC/D,MAAM,WAAW,KAAK,YAAY,KAAK,YAAY,iBAAiB,gBAAgB;CAMpF,OAAO;EACN,MAAM;EACN,SAAS,KAAK,WAAW;EACzB;EACA;EACA,aAAa,KAAK,eAAe,KAAK,eAAe,CAAC;EACtD;EACA;EACA;EACA;CACD;AACD;;;;AAKA,MAAa,uBACZ,SAEA,OAAO,IAAI;CACV,WAAW,8BAA8B,IAAI;CAM7C,QAAQ,QAA2B;EAClC,IACC,OAAO,QAAQ,YACf,QAAQ,QACR,UAAU,OACT,IAAoC,SAAS,qBAE9C,OAAO;EAER,OAAO,kBACN,WACA,2FACC,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,KAEhD,KAAA,GACA,GACD;CACD;AACD,CAAC"}
1
+ {"version":3,"file":"known-deploy.mjs","names":[],"sources":["../../../../src/plugins/walrus/mode/known-deploy.ts"],"sourcesContent":["// Walrus mode — known deployment (testnet / mainnet / *-fork via\n// upstream translation).\n//\n// Distilled-doc reference (06-walrus.md §\"Lifecycle: Startup —\n// known deployment\"). Purely synchronous body — no ordered phases.\n// The factory validates required deployment fields and returns a\n// resolved value whose tags are eager `Layer.succeed(...)` constants.\n//\n// Distilled-doc invariants honored:\n// - 15: each of `proxyUrl / aggregatorUrl / publisherUrl` surfaces\n// INDEPENDENTLY. Encoded as `string | null` in the resolved\n// shape — a given field is null only when THAT specific URL\n// is unresolved (registry default + explicit override both\n// absent). The plugin's projection step publishes each tag\n// when its own URL is present; a missing publisher URL does\n// not suppress an available proxy/aggregator URL.\n// - 16: throw synchronously when `nodes` is missing for a\n// registered network. The on-chain ids\n// (`systemObjectId` / `stakingPoolId` / `exchangeIds`) ARE\n// stable and come from the `@mysten/walrus` SDK package config\n// — we default them from the SDK and let callers override.\n// Only the node committee stays explicit: testnet has 100+\n// nodes that the SDK fetches dynamically, so we don't pin them.\n\nimport { MAINNET_WALRUS_PACKAGE_CONFIG, TESTNET_WALRUS_PACKAGE_CONFIG } from '@mysten/walrus';\nimport { Effect, type Scope } from 'effect';\n\nimport { expectNonEmptyString } from '../../../substrate/runtime/config-validation.ts';\nimport { walrusConfigError, type WalrusConfigError } from '../errors.ts';\nimport type { WalrusStorageNode } from '../storage-nodes.ts';\n\n/** Known networks supported by the registry lookup. */\nexport type WalrusKnownNetwork = 'testnet' | 'mainnet' | 'devnet';\n\n/** Options for the known-deployment mode. */\nexport interface WalrusKnownDeploymentOptions {\n\t/** Network shortcut — looks up per-field defaults from a registry\n\t * baked into this package. */\n\treadonly network?: WalrusKnownNetwork;\n\t/** Per-field overrides for the on-chain ids. For `testnet`/`mainnet`\n\t * these default from the `@mysten/walrus` SDK package config, so\n\t * they're optional; for `devnet` (or when `network` is omitted)\n\t * `systemObjectId`/`stakingPoolId` are required. An explicit value\n\t * here always wins over the registry default. */\n\treadonly systemObjectId?: string;\n\treadonly stakingPoolId?: string;\n\treadonly exchangeIds?: ReadonlyArray<string>;\n\t/** Explicit storage-node committee. Required (throws if missing\n\t * even when `network` is set — distilled-doc invariant 16). */\n\treadonly nodes?: ReadonlyArray<WalrusStorageNode>;\n\treadonly aggregatorUrl?: string;\n\treadonly publisherUrl?: string;\n\treadonly proxyUrl?: string;\n}\n\n/** Resolved known-deployment boot artifacts. */\nexport interface KnownDeploymentBootResult {\n\treadonly mode: 'known';\n\t/** The known network's name (`testnet`/`mainnet`/`devnet`) — its stable\n\t * identity. A known remote deployment has no per-boot genesis digest, so\n\t * the network name IS its `chainId` for codegen/snapshot keying. */\n\treadonly network: string;\n\treadonly systemObjectId: string;\n\treadonly stakingPoolId: string;\n\treadonly exchangeIds: ReadonlyArray<string>;\n\treadonly nodes: ReadonlyArray<WalrusStorageNode>;\n\t/** Null only when the read/proxy URL itself is unresolved (no explicit\n\t * `proxyUrl` override, registry proxy URL, or aggregator URL). Surfaces\n\t * independently of `aggregatorUrl` / `publisherUrl` — distilled-doc\n\t * invariant 15. */\n\treadonly proxyUrl: string | null;\n\treadonly aggregatorUrl: string | null;\n\treadonly publisherUrl: string | null;\n}\n\n/** Known-deployment registry — baked-in record per network. The\n * on-chain ids default from the `@mysten/walrus` SDK package config\n * (`{TESTNET,MAINNET}_WALRUS_PACKAGE_CONFIG`) so a caller only needs\n * to supply `nodes`; the aggregator/publisher/proxy URLs are\n * devstack-owned (the SDK ships none). Devnet has no canonical SDK\n * record today, so it keeps no id defaults. */\nconst KNOWN_DEPLOYMENT_REGISTRY: Readonly<\n\tRecord<\n\t\tWalrusKnownNetwork,\n\t\t{\n\t\t\treadonly network: string;\n\t\t\treadonly systemObjectId?: string;\n\t\t\treadonly stakingPoolId?: string;\n\t\t\treadonly exchangeIds?: ReadonlyArray<string>;\n\t\t\treadonly aggregatorUrl?: string;\n\t\t\treadonly publisherUrl?: string;\n\t\t\treadonly proxyUrl?: string;\n\t\t}\n\t>\n> = {\n\ttestnet: {\n\t\tnetwork: 'testnet',\n\t\t// On-chain ids default from the SDK package config; callers may\n\t\t// still override per-field. The URLs below are devstack-owned —\n\t\t// the SDK ships none.\n\t\tsystemObjectId: TESTNET_WALRUS_PACKAGE_CONFIG.systemObjectId,\n\t\tstakingPoolId: TESTNET_WALRUS_PACKAGE_CONFIG.stakingPoolId,\n\t\texchangeIds: TESTNET_WALRUS_PACKAGE_CONFIG.exchangeIds,\n\t\taggregatorUrl: 'https://aggregator.walrus-testnet.walrus.space',\n\t\tpublisherUrl: 'https://publisher.walrus-testnet.walrus.space',\n\t\tproxyUrl: 'https://aggregator.walrus-testnet.walrus.space',\n\t},\n\tmainnet: {\n\t\tnetwork: 'mainnet',\n\t\t// On-chain ids default from the SDK package config; callers may\n\t\t// still override per-field. Mainnet ships no `exchangeIds` in the\n\t\t// SDK config (testnet-only faucet exchange), so it stays absent.\n\t\tsystemObjectId: MAINNET_WALRUS_PACKAGE_CONFIG.systemObjectId,\n\t\tstakingPoolId: MAINNET_WALRUS_PACKAGE_CONFIG.stakingPoolId,\n\t\taggregatorUrl: 'https://aggregator.walrus.space',\n\t\tpublisherUrl: 'https://publisher.walrus.space',\n\t\tproxyUrl: 'https://aggregator.walrus.space',\n\t},\n\tdevnet: {\n\t\tnetwork: 'devnet',\n\t},\n};\n\n/** Synchronous factory-time validation + projection. Throws (NOT\n * Effect-fail) on missing required fields so misconfiguration trips\n * at the `defineDevstack` call site rather than at deferred\n * Layer.build time. Distilled-doc invariants 14 + 16. */\nexport const resolveKnownDeploymentOptions = (\n\topts: WalrusKnownDeploymentOptions,\n): KnownDeploymentBootResult => {\n\tconst reg = opts.network ? KNOWN_DEPLOYMENT_REGISTRY[opts.network] : undefined;\n\n\t// On-chain ids: caller override (`opts.*`) wins, else the registry\n\t// default sourced from the `@mysten/walrus` SDK package config.\n\t// Testnet/mainnet default from the SDK so only `nodes` is required;\n\t// devnet has no SDK record, so these stay required there.\n\tconst systemObjectId = expectNonEmptyString(opts.systemObjectId ?? reg?.systemObjectId, {\n\t\tfield: 'systemObjectId',\n\t\tmkError: ({ field }) =>\n\t\t\twalrusConfigError(\n\t\t\t\tfield,\n\t\t\t\t`walrusKnownDeployment: 'systemObjectId' is required (testnet/mainnet default from the @mysten/walrus SDK; pass it explicitly for devnet or networks without an SDK record)`,\n\t\t\t),\n\t});\n\n\tconst stakingPoolId = expectNonEmptyString(opts.stakingPoolId ?? reg?.stakingPoolId, {\n\t\tfield: 'stakingPoolId',\n\t\tmkError: ({ field }) =>\n\t\t\twalrusConfigError(\n\t\t\t\tfield,\n\t\t\t\t`walrusKnownDeployment: 'stakingPoolId' is required (testnet/mainnet default from the @mysten/walrus SDK; pass it explicitly for devnet or networks without an SDK record)`,\n\t\t\t),\n\t});\n\n\tconst nodes = opts.nodes;\n\tif (!nodes) {\n\t\t// Distilled-doc invariant 16: even when `network` is set, the\n\t\t// node committee must be explicit. The on-chain ids come from\n\t\t// the SDK package config, but the committee is dynamic — testnet\n\t\t// has 100+ nodes the SDK fetches at runtime, so we don't pin it.\n\t\tthrow walrusConfigError(\n\t\t\t'nodes',\n\t\t\t`walrusKnownDeployment: explicit 'nodes' committee is required — ` +\n\t\t\t\t`Walrus ${opts.network ?? 'custom'} has nodes fetched dynamically by the SDK`,\n\t\t\t`pass an empty array if you accept the SDK-driven committee lookup, ` +\n\t\t\t\t`or use walrus()/walrusFor(network).local({...}) for a local self-hosted cluster`,\n\t\t);\n\t}\n\n\tconst aggregatorUrl = opts.aggregatorUrl ?? reg?.aggregatorUrl ?? null;\n\tconst publisherUrl = opts.publisherUrl ?? reg?.publisherUrl ?? null;\n\tconst proxyUrl = opts.proxyUrl ?? reg?.proxyUrl ?? aggregatorUrl ?? null;\n\n\t// Invariant 15: surface null per individual URL when missing so the\n\t// plugin's projection can conditionally publish each tag. Previously\n\t// any single missing URL nullified all three, dropping user-supplied\n\t// values for the URLs they did provide.\n\treturn {\n\t\tmode: 'known' as const,\n\t\tnetwork: reg?.network ?? 'custom',\n\t\tsystemObjectId,\n\t\tstakingPoolId,\n\t\texchangeIds: opts.exchangeIds ?? reg?.exchangeIds ?? [],\n\t\tnodes,\n\t\tproxyUrl,\n\t\taggregatorUrl,\n\t\tpublisherUrl,\n\t};\n};\n\n/** \"Boot\" the known deployment. Purely synchronous projection inside\n * Effect.gen — no I/O, no Scope work. Surfaces as Effect for\n * uniformity with the local-cluster boot signature. */\nexport const bootKnownDeployment = (\n\topts: WalrusKnownDeploymentOptions,\n): Effect.Effect<KnownDeploymentBootResult, WalrusConfigError, Scope.Scope> =>\n\tEffect.try({\n\t\ttry: () => resolveKnownDeploymentOptions(opts),\n\t\t// `resolveKnownDeploymentOptions` throws our typed config error\n\t\t// shape. STYLE_GUIDE §2 forbids bare error casts; runtime-guard\n\t\t// the `_tag` so an unexpected synchronous throw (e.g. a future\n\t\t// helper that throws a stock `TypeError`) is wrapped instead of\n\t\t// silently mis-tagged as `WalrusConfigError`.\n\t\tcatch: (err): WalrusConfigError => {\n\t\t\tif (\n\t\t\t\ttypeof err === 'object' &&\n\t\t\t\terr !== null &&\n\t\t\t\t'_tag' in err &&\n\t\t\t\t(err as { readonly _tag?: unknown })._tag === 'WalrusConfigError'\n\t\t\t) {\n\t\t\t\treturn err as WalrusConfigError;\n\t\t\t}\n\t\t\treturn walrusConfigError(\n\t\t\t\t'unknown',\n\t\t\t\t`walrusKnownDeployment: unexpected non-typed throw inside resolveKnownDeploymentOptions: ${\n\t\t\t\t\terr instanceof Error ? err.message : String(err)\n\t\t\t\t}`,\n\t\t\t\tundefined,\n\t\t\t\terr,\n\t\t\t);\n\t\t},\n\t});\n"],"mappings":";;;;;;;;;;;AAiFA,MAAM,4BAaF;CACH,SAAS;EACR,SAAS;EAIT,gBAAgB,8BAA8B;EAC9C,eAAe,8BAA8B;EAC7C,aAAa,8BAA8B;EAC3C,eAAe;EACf,cAAc;EACd,UAAU;CACX;CACA,SAAS;EACR,SAAS;EAIT,gBAAgB,8BAA8B;EAC9C,eAAe,8BAA8B;EAC7C,eAAe;EACf,cAAc;EACd,UAAU;CACX;CACA,QAAQ,EACP,SAAS,SACV;AACD;;;;;AAMA,MAAa,iCACZ,SAC+B;CAC/B,MAAM,MAAM,KAAK,UAAU,0BAA0B,KAAK,WAAW,KAAA;CAMrE,MAAM,iBAAiB,qBAAqB,KAAK,kBAAkB,KAAK,gBAAgB;EACvF,OAAO;EACP,UAAU,EAAE,YACX,kBACC,OACA,4KACD;CACF,CAAC;CAED,MAAM,gBAAgB,qBAAqB,KAAK,iBAAiB,KAAK,eAAe;EACpF,OAAO;EACP,UAAU,EAAE,YACX,kBACC,OACA,2KACD;CACF,CAAC;CAED,MAAM,QAAQ,KAAK;CACnB,IAAI,CAAC,OAKJ,MAAM,kBACL,SACA,0EACW,KAAK,WAAW,SAAS,4CACpC,oJAED;CAGD,MAAM,gBAAgB,KAAK,iBAAiB,KAAK,iBAAiB;CAClE,MAAM,eAAe,KAAK,gBAAgB,KAAK,gBAAgB;CAC/D,MAAM,WAAW,KAAK,YAAY,KAAK,YAAY,iBAAiB;CAMpE,OAAO;EACN,MAAM;EACN,SAAS,KAAK,WAAW;EACzB;EACA;EACA,aAAa,KAAK,eAAe,KAAK,eAAe,CAAC;EACtD;EACA;EACA;EACA;CACD;AACD;;;;AAKA,MAAa,uBACZ,SAEA,OAAO,IAAI;CACV,WAAW,8BAA8B,IAAI;CAM7C,QAAQ,QAA2B;EAClC,IACC,OAAO,QAAQ,YACf,QAAQ,QACR,UAAU,OACT,IAAoC,SAAS,qBAE9C,OAAO;EAER,OAAO,kBACN,WACA,2FACC,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,KAEhD,KAAA,GACA,GACD;CACD;AACD,CAAC"}
@@ -28,7 +28,20 @@ interface WalrusLocalClusterOptions {
28
28
  /** Per-node TCP ready-probe timeout (ms). Defaults to
29
29
  * `DEFAULT_NODE_READY_TIMEOUT_MS`. */
30
30
  readonly readyTimeoutMs?: number;
31
+ /** Local app-facing aggregator service. Defaults to enabled. Set
32
+ * `false` to expose only storage-node routes. */
33
+ readonly aggregator?: boolean | WalrusLocalServiceOptions;
34
+ /** Local app-facing publisher service. Defaults to enabled. Set
35
+ * `false` to run storage nodes without the simple publish API. */
36
+ readonly publisher?: boolean | WalrusLocalPublisherOptions;
31
37
  }
38
+ interface WalrusLocalServiceOptions {
39
+ /** In-container port passed to the release service's
40
+ * `--bind-address 0.0.0.0:<port>`. Defaults to the Walrus CLI's
41
+ * publisher/aggregator port. */
42
+ readonly port?: number;
43
+ }
44
+ type WalrusLocalPublisherOptions = WalrusLocalServiceOptions;
32
45
  //#endregion
33
- export { WalrusLocalClusterOptions };
46
+ export { WalrusLocalClusterOptions, WalrusLocalPublisherOptions, WalrusLocalServiceOptions };
34
47
  //# sourceMappingURL=local-cluster.d.mts.map
@@ -1,13 +1,14 @@
1
1
  import { contentHash } from "../../../substrate/brand.mjs";
2
2
  import { setCurrentPluginPhase } from "../../../substrate/runtime/current-plugin.mjs";
3
- import { expectPositiveInteger } from "../../../substrate/runtime/config-validation.mjs";
3
+ import { expectPort, expectPositiveInteger } from "../../../substrate/runtime/config-validation.mjs";
4
4
  import { walrusConfigError, walrusPluginError } from "../errors.mjs";
5
- import { computePublicHostname, startStorageNodes } from "../storage-nodes.mjs";
5
+ import { computeCommitteeHostname, startStorageNodes } from "../storage-nodes.mjs";
6
6
  import "../routable.mjs";
7
7
  import { resolveWalExchange } from "../wal-swap.mjs";
8
8
  import { makeWalFaucetStrategy, resolveWalCoinType, walPackageIdFromCoinType } from "../faucet-strategy.mjs";
9
9
  import { deployWalrusContracts } from "../deploy.mjs";
10
10
  import { resolveCargoImage } from "../bootstrap-assets/cargo-image.mjs";
11
+ import { startWalrusClientServices } from "../client-services.mjs";
11
12
  import { Effect } from "effect";
12
13
  //#region src/plugins/walrus/mode/local-cluster.ts
13
14
  /** Synchronous factory-time validation. Throws (NOT Effect-fail) so
@@ -34,7 +35,22 @@ const resolveLocalClusterOptions = (opts) => {
34
35
  suiVersion: opts.suiVersion ?? "devnet-v1.71.0",
35
36
  containerApiPort: opts.containerApiPort ?? 9185,
36
37
  epochDuration: opts.epochDuration ?? "24h",
37
- readyTimeoutMs: opts.readyTimeoutMs ?? 6e4
38
+ readyTimeoutMs: opts.readyTimeoutMs ?? 6e4,
39
+ ...resolveLocalServicesOptions(opts)
40
+ };
41
+ };
42
+ const expectOptionalPort = (value, field) => value === void 0 ? void 0 : expectPort(value, {
43
+ field,
44
+ mkError: ({ field: f, message, hint }) => walrusConfigError(f, `walrusLocalCluster: ${f} ${message}`, hint)
45
+ });
46
+ const resolveLocalServiceBase = (value, fieldPrefix) => {
47
+ if (value === false) return null;
48
+ return { port: expectOptionalPort((value === void 0 || value === true ? {} : value).port, `${fieldPrefix}.port`) ?? 31415 };
49
+ };
50
+ const resolveLocalServicesOptions = (opts) => {
51
+ return {
52
+ aggregator: resolveLocalServiceBase(opts.aggregator, "aggregator"),
53
+ publisher: resolveLocalServiceBase(opts.publisher, "publisher")
38
54
  };
39
55
  };
40
56
  /** Boot the local cluster. Returns the resolved value the plugin
@@ -50,7 +66,6 @@ const resolveLocalClusterOptions = (opts) => {
50
66
  * the artifact publisher primitive; the produce body runs the walrus deploy
51
67
  * one-shot.
52
68
  * - Storage nodes — parallel boot via `startStorageNodes`.
53
- * - Proxy URL pick — `nodes[0].rpcUrl`.
54
69
  * - WAL faucet strategy — constructed if `state.exchangeObject`
55
70
  * exists. Surfaced on the boot result; the barrel registers
56
71
  * the contributor decl.
@@ -76,13 +91,13 @@ const bootLocalCluster = (deps, opts) => Effect.gen(function* () {
76
91
  app: deps.app,
77
92
  stack: deps.stack
78
93
  }).pipe(Effect.catch((cause) => Effect.fail(walrusPluginError("cluster-network", `walrus: ensureNetwork('${deps.walrusNetworkName}') failed: ${cause.reason}: ${cause.detail}`, { cause }))));
79
- const publicHosts = Array.from({ length: opts.nodeCount }, (_, i) => computePublicHostname(deps.app, deps.stack, i)).join(",");
94
+ const committeeHosts = Array.from({ length: opts.nodeCount }, (_, i) => computeCommitteeHostname(i)).join(",");
80
95
  const listeningIps = Array.from({ length: opts.nodeCount }, (_, i) => `${deps.subnetPrefix}.${10 + i}`).join(",");
81
96
  yield* setCurrentPluginPhase(`deploying Walrus contracts (${opts.nodeCount} node${opts.nodeCount === 1 ? "" : "s"}, ${opts.shards} shards)`);
82
97
  const { state } = yield* deployWalrusContracts(deps.publisher, deps.probe, deps.runtime, {
83
98
  walrusName: opts.name,
84
99
  chainId: deps.suiChainId,
85
- contentHash: contentHash(`walrus|${opts.version}|${opts.suiVersion}|${opts.nodeCount}|${opts.shards}|${opts.epochDuration}`),
100
+ contentHash: contentHash(`walrus|${opts.version}|${opts.suiVersion}|${opts.nodeCount}|${opts.shards}|${opts.epochDuration}|committee=${committeeHosts}|listen=${listeningIps}`),
86
101
  outputDirHostPath: deps.deployHostMountPath,
87
102
  stackRoot: deps.stackRoot,
88
103
  suiRpcUrlInNetwork: deps.suiRpcUrlInNetwork,
@@ -91,7 +106,7 @@ const bootLocalCluster = (deps, opts) => Effect.gen(function* () {
91
106
  committeeSize: opts.nodeCount,
92
107
  shards: opts.shards,
93
108
  epochDuration: opts.epochDuration,
94
- publicHostsCsv: publicHosts,
109
+ publicHostsCsv: committeeHosts,
95
110
  listeningIpsCsv: listeningIps,
96
111
  walrusImage,
97
112
  suiNetworkName: deps.suiNetworkName
@@ -104,6 +119,14 @@ const bootLocalCluster = (deps, opts) => Effect.gen(function* () {
104
119
  state.exchangeObject ?? "no-exchange"
105
120
  ].join("|");
106
121
  const nodeImageScope = state.walrusPackageId.replace(/^0x/, "").slice(0, 12);
122
+ const resolveServiceImage = (role) => resolveCargoImage(deps.runtime, {
123
+ walrusRef: opts.version,
124
+ suiVersion: opts.suiVersion,
125
+ owner: {
126
+ app: deps.app,
127
+ stack: deps.stack
128
+ }
129
+ }, `${walrusImage.tag}-${nodeImageScope}-${role}`);
107
130
  const nodeImages = yield* Effect.forEach(Array.from({ length: opts.nodeCount }, (_, i) => i), (i) => resolveCargoImage(deps.runtime, {
108
131
  walrusRef: opts.version,
109
132
  suiVersion: opts.suiVersion,
@@ -112,6 +135,10 @@ const bootLocalCluster = (deps, opts) => Effect.gen(function* () {
112
135
  stack: deps.stack
113
136
  }
114
137
  }, `${walrusImage.tag}-${nodeImageScope}-node-${i}`));
138
+ const clientServiceImages = yield* Effect.all({
139
+ aggregator: opts.aggregator === null ? Effect.succeed(null) : resolveServiceImage("aggregator"),
140
+ publisher: opts.publisher === null ? Effect.succeed(null) : resolveServiceImage("publisher")
141
+ });
115
142
  yield* setCurrentPluginPhase(`starting ${opts.nodeCount} Walrus storage node${opts.nodeCount === 1 ? "" : "s"}`);
116
143
  const { nodes } = yield* startStorageNodes(deps.runtime, {
117
144
  app: deps.app,
@@ -128,8 +155,25 @@ const bootLocalCluster = (deps, opts) => Effect.gen(function* () {
128
155
  deployConfigHash,
129
156
  readyTimeoutMs: opts.readyTimeoutMs
130
157
  });
131
- if (nodes.length === 0) return yield* Effect.fail(walrusPluginError("proxy", "walrus: at least one storage node is required"));
132
- const proxyUrl = nodes[0].rpcUrl;
158
+ if (nodes.length === 0) return yield* Effect.fail(walrusPluginError("storage-node", "walrus: at least one storage node is required"));
159
+ yield* setCurrentPluginPhase("starting Walrus client services");
160
+ const clientServices = yield* startWalrusClientServices(deps.runtime, {
161
+ app: deps.app,
162
+ stack: deps.stack,
163
+ walrusName: opts.name,
164
+ images: clientServiceImages,
165
+ options: {
166
+ aggregator: opts.aggregator,
167
+ publisher: opts.publisher
168
+ },
169
+ walrusNetworkName: deps.walrusNetworkName,
170
+ suiNetworkName: deps.suiNetworkName,
171
+ deployHostMountPath: deps.deployHostMountPath,
172
+ stackRoot: deps.stackRoot,
173
+ deployConfigHash,
174
+ suiRpcUrlInNetwork: deps.suiRpcUrlInNetwork,
175
+ readyTimeoutMs: opts.readyTimeoutMs
176
+ });
133
177
  yield* setCurrentPluginPhase("resolving WAL exchange");
134
178
  const exchange = yield* resolveWalExchange(deps.probe, state.exchangeObject);
135
179
  const walFaucetStrategy = exchange ? makeWalFaucetStrategy({
@@ -149,11 +193,9 @@ const bootLocalCluster = (deps, opts) => Effect.gen(function* () {
149
193
  walrusPackageId: state.walrusPackageId,
150
194
  walPackageId: resolvedWalPackageId,
151
195
  nodes,
152
- aggregatorUrl: proxyUrl,
153
- publisherUrl: proxyUrl,
154
- proxyUrl,
155
196
  exchangeObjectId: state.exchangeObject,
156
197
  exchange,
198
+ clientServices,
157
199
  walFaucetStrategy,
158
200
  walCoinType: resolvedWalCoinType
159
201
  };
@@ -1 +1 @@
1
- {"version":3,"file":"local-cluster.mjs","names":["brandContentHash"],"sources":["../../../../src/plugins/walrus/mode/local-cluster.ts"],"sourcesContent":["// Walrus mode — local cluster.\n//\n// Distilled-doc reference (06-walrus.md §\"Lifecycle: Startup —\n// local cluster\"). Eight ordered phases:\n//\n// 0. Yield deps (Sui, Identity, ChainProbe, faucet strategy opt).\n// 1. Image build — upstream cargo image (bootstrap asset) +\n// wrapper image (inline; content-addressed on\n// the upstream tag + suiVersion).\n// 1b. Docker network create — per-stack `/24` via\n// `Docker.networkCreate(networkName, {subnet})`.\n// 2. Deploy contracts — via `ArtifactPublisher`. The\n// publisher folds chainId into the cache\n// key; verify probes BOTH the on-disk\n// `deploy` file AND on-chain object\n// existence; produce runs the deploy\n// one-shot.\n// 3. Register committee — omitted entirely (no placeholder).\n// 4. Storage nodes — parallel boot of N storage-node containers.\n// 5. Exchange resolution — on-chain getObject + parse type.\n// Degrades to undefined on\n// OBJECT_NOT_FOUND.\n// 6. Proxy URL pick — nodes[0].rpcUrl is the representative\n// aggregator/publisher.\n// 7a. WAL faucet strategy register — opt-in when an exchange exists.\n// 8. Registries — package + endpoint + walrus-state.\n//\n// What this file owns: the dispatch shape + the typed options\n// surface + the synchronous factory-time validation.\n//\n// What this file delegates: container/Move heavy paths live in\n// `storage-nodes.ts`, `deploy.ts`, and the `ArtifactPublisher`\n// primitive.\n\nimport { Effect, FileSystem, Path, type Scope } from 'effect';\n\nimport type {\n\tArtifactPublishError,\n\tArtifactPublisher,\n} from '../../../primitives/artifact-publisher.ts';\nimport type { ChainProbe } from '../../../contracts/chain-probe.ts';\nimport type { ContainerRuntime } from '../../../contracts/container-runtime.ts';\nimport type { SuiProbeKey } from '../../sui/index.ts';\nimport { contentHash as brandContentHash } from '../../../substrate/brand.ts';\nimport { expectPositiveInteger } from '../../../substrate/runtime/config-validation.ts';\nimport { setCurrentPluginPhase } from '../../../substrate/runtime/current-plugin.ts';\nimport { walrusConfigError, walrusPluginError, type WalrusError } from '../errors.ts';\nimport { WALRUS_MAX_NODE_COUNT } from '../routable.ts';\nimport type { WalrusStorageNode } from '../storage-nodes.ts';\nimport {\n\tDEFAULT_NODE_READY_TIMEOUT_MS,\n\tDEFAULT_CONTAINER_API_PORT,\n\tWALRUS_NODE_IP_BASE,\n\tcomputePublicHostname,\n\tstartStorageNodes,\n} from '../storage-nodes.ts';\nimport { deployWalrusContracts, type CachedDeployState } from '../deploy.ts';\nimport {\n\tDEFAULT_SUI_VERSION,\n\tDEFAULT_WALRUS_REF,\n\tresolveCargoImage,\n} from '../bootstrap-assets/cargo-image.ts';\nimport {\n\tmakeWalFaucetStrategy,\n\twalPackageIdFromCoinType,\n\tresolveWalCoinType,\n\ttype WalFaucetStrategy,\n} from '../faucet-strategy.ts';\nimport { resolveWalExchange, type WalExchangeHandle, type WalSwapSdk } from '../wal-swap.ts';\n\n/** Options for the local-cluster mode (06-walrus.md\n * §\"Configuration\"). */\nexport interface WalrusLocalClusterOptions {\n\t/** Engine row name + on-disk dir suffix + registry key.\n\t * Default `'walrus'`. */\n\treadonly name?: string;\n\t/** Number of storage-node containers. Must be `>= 1` (synchronous\n\t * factory-time guard). Default `1`. */\n\treadonly nodeCount?: number;\n\t/** Total shards distributed across the committee. Must be\n\t * `>= nodeCount`. Default `100` (distilled-doc default block). */\n\treadonly shards?: number;\n\t/** Pinned walrus release — drives the wrapper image build.\n\t * Default `'testnet-v1.49.1'`. */\n\treadonly version?: string;\n\t/** Sui release whose binary the wrapper image bakes (distilled-\n\t * doc invariant 24). Default `'devnet-v1.71.0'`. */\n\treadonly suiVersion?: string;\n\t/** Port each storage node binds inside the container. Default\n\t * 9185 (same as the router entrypoint — invariant 9). */\n\treadonly containerApiPort?: number;\n\t/** Walrus epoch length passed to `walrus-deploy --epoch-duration`.\n\t * Default `'24h'`. */\n\treadonly epochDuration?: string;\n\t/** Per-node TCP ready-probe timeout (ms). Defaults to\n\t * `DEFAULT_NODE_READY_TIMEOUT_MS`. */\n\treadonly readyTimeoutMs?: number;\n}\n\n/** Resolved local-cluster boot artifacts. */\nexport interface LocalClusterBootResult {\n\treadonly mode: 'local';\n\treadonly deploy: CachedDeployState;\n\treadonly walrusPackageId: string;\n\treadonly walPackageId: string;\n\treadonly nodes: ReadonlyArray<WalrusStorageNode>;\n\treadonly aggregatorUrl: string;\n\treadonly publisherUrl: string;\n\treadonly proxyUrl: string;\n\treadonly exchangeObjectId: string | undefined;\n\treadonly exchange: WalExchangeHandle | null;\n\t/** WAL faucet strategy — present only when the local cluster has a\n\t * non-empty exchange object. The barrel registers this onto the\n\t * `coinType:<fullCoinType>` strategy contributor decl. `null`\n\t * otherwise. */\n\treadonly walFaucetStrategy: WalFaucetStrategy | null;\n\treadonly walCoinType: string;\n}\n\n/** Defaults applied to options. */\nexport interface ResolvedLocalClusterOptions {\n\treadonly name: string;\n\treadonly nodeCount: number;\n\treadonly shards: number;\n\treadonly version: string;\n\treadonly suiVersion: string;\n\treadonly containerApiPort: number;\n\treadonly epochDuration: string;\n\treadonly readyTimeoutMs: number;\n}\n\n/** Synchronous factory-time validation. Throws (NOT Effect-fail) so\n * misconfiguration trips at the `defineDevstack` call site rather\n * than at deferred Layer.build time. Distilled-doc invariant 11. */\nexport const resolveLocalClusterOptions = (\n\topts: WalrusLocalClusterOptions,\n): ResolvedLocalClusterOptions => {\n\tconst nodeCount = expectPositiveInteger(opts.nodeCount ?? 1, {\n\t\tfield: 'nodeCount',\n\t\tmkError: ({ field, message, hint }) =>\n\t\t\twalrusConfigError(\n\t\t\t\tfield,\n\t\t\t\t`walrusLocalCluster: nodeCount must be >= 1 (got ${String(opts.nodeCount ?? 1)})`,\n\t\t\t\thint ?? message,\n\t\t\t),\n\t\thint: 'set nodeCount to a positive integer (default 1)',\n\t});\n\tif (nodeCount > WALRUS_MAX_NODE_COUNT) {\n\t\tthrow walrusConfigError(\n\t\t\t'nodeCount',\n\t\t\t`walrusLocalCluster: nodeCount (${nodeCount}) exceeds the maximum ${WALRUS_MAX_NODE_COUNT} ` +\n\t\t\t\t`pre-declared Traefik entrypoints (walrus-node-0..${WALRUS_MAX_NODE_COUNT - 1}).`,\n\t\t\t`reduce nodeCount or raise WALRUS_MAX_NODE_COUNT in plugins/walrus/routable.ts`,\n\t\t);\n\t}\n\tconst shards = expectPositiveInteger(opts.shards ?? 100, {\n\t\tfield: 'shards',\n\t\tmkError: ({ field, message, hint }) =>\n\t\t\twalrusConfigError(\n\t\t\t\tfield,\n\t\t\t\t`walrusLocalCluster: shards must be a positive integer (got ${String(opts.shards ?? 100)})`,\n\t\t\t\thint ?? message,\n\t\t\t),\n\t\thint: 'set shards to a positive integer (default 100)',\n\t});\n\tif (shards < nodeCount) {\n\t\tthrow walrusConfigError(\n\t\t\t'shards',\n\t\t\t`walrusLocalCluster: shards (${shards}) must be >= nodeCount (${nodeCount})`,\n\t\t\t`set shards >= ${nodeCount} (default 100)`,\n\t\t);\n\t}\n\treturn {\n\t\tname: opts.name ?? 'walrus',\n\t\tnodeCount,\n\t\tshards,\n\t\tversion: opts.version ?? DEFAULT_WALRUS_REF,\n\t\tsuiVersion: opts.suiVersion ?? DEFAULT_SUI_VERSION,\n\t\tcontainerApiPort: opts.containerApiPort ?? DEFAULT_CONTAINER_API_PORT,\n\t\tepochDuration: opts.epochDuration ?? '24h',\n\t\treadyTimeoutMs: opts.readyTimeoutMs ?? DEFAULT_NODE_READY_TIMEOUT_MS,\n\t};\n};\n\n/** Dependencies the local service consumes at acquire-time. */\nexport interface LocalClusterDeps {\n\treadonly runtime: ContainerRuntime;\n\treadonly publisher: ArtifactPublisher;\n\treadonly probe: ChainProbe<SuiProbeKey>;\n\treadonly suiSdk: WalSwapSdk;\n\treadonly suiChainId: string;\n\treadonly suiRpcUrlInNetwork: string;\n\treadonly walrusFaucetUrlInNetwork: string;\n\treadonly waitForFundsReady: Effect.Effect<void, unknown>;\n\treadonly app: string;\n\treadonly stack: string;\n\t/** Pre-allocated /24 prefix from `subnetForStack`. */\n\treadonly subnetPrefix: string;\n\treadonly walrusNetworkName: string;\n\treadonly suiNetworkName: string;\n\treadonly deployHostMountPath: string;\n\t/** On-disk per-stack root from `StackPathsService.stackRoot`. The\n\t * walrus bind-mount is computed off this; `deployHostMountPath`\n\t * MUST live under it. */\n\treadonly stackRoot: string;\n}\n\n/** Boot the local cluster. Returns the resolved value the plugin\n * projects onto its tags.\n *\n * Steps:\n * - Image build — `resolveCargoImage` (bootstrap asset).\n * Honors `WALRUS_CARGO_IMAGE_OVERRIDE` for the pre-baked path.\n * - Docker network ensure — `runtime.ensureNetwork(walrusNet)`.\n * The sui network is owned by the sui plugin; we attach to it\n * as a secondary network on each storage node.\n * - Deploy contracts — `deployWalrusContracts` dispatches through\n * the artifact publisher primitive; the produce body runs the walrus deploy\n * one-shot.\n * - Storage nodes — parallel boot via `startStorageNodes`.\n * - Proxy URL pick — `nodes[0].rpcUrl`.\n * - WAL faucet strategy — constructed if `state.exchangeObject`\n * exists. Surfaced on the boot result; the barrel registers\n * the contributor decl.\n *\n * Per-step failures surface as `WalrusPluginError` with the phase\n * vocabulary from `errors.ts`; artifact publisher failures pass through as\n * `ArtifactPublishError`. The plugin narration vocabulary anchors\n * on these phase tags.\n */\nexport const bootLocalCluster = (\n\tdeps: LocalClusterDeps,\n\topts: ResolvedLocalClusterOptions,\n): Effect.Effect<\n\tLocalClusterBootResult,\n\tWalrusError | ArtifactPublishError,\n\tScope.Scope | FileSystem.FileSystem | Path.Path\n> =>\n\tEffect.gen(function* () {\n\t\t// ---- cargo image (bootstrap asset) -----------------------\n\t\t// The cargo image is content-addressed; the resolver owns the\n\t\t// cache check + the registry-tag override fast path.\n\t\tyield* setCurrentPluginPhase(`resolving Walrus image ${opts.version}`);\n\t\tconst walrusImage = yield* resolveCargoImage(deps.runtime, {\n\t\t\twalrusRef: opts.version,\n\t\t\tsuiVersion: opts.suiVersion,\n\t\t\towner: { app: deps.app, stack: deps.stack },\n\t\t});\n\n\t\t// ---- docker network ensure ------------------------------\n\t\tyield* setCurrentPluginPhase(`ensuring Walrus network ${deps.walrusNetworkName}`);\n\t\tyield* deps.runtime\n\t\t\t.ensureNetwork({\n\t\t\t\tname: deps.walrusNetworkName,\n\t\t\t\tapp: deps.app,\n\t\t\t\tstack: deps.stack,\n\t\t\t})\n\t\t\t.pipe(\n\t\t\t\tEffect.catch((cause) =>\n\t\t\t\t\tEffect.fail(\n\t\t\t\t\t\twalrusPluginError(\n\t\t\t\t\t\t\t'cluster-network',\n\t\t\t\t\t\t\t`walrus: ensureNetwork('${deps.walrusNetworkName}') failed: ${cause.reason}: ${cause.detail}`,\n\t\t\t\t\t\t\t{ cause },\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t);\n\n\t\t// ---- deploy via ArtifactPublisher ----------------\n\t\t// The deploy one-shot needs the walrus image + sui network so it\n\t\t// can dial the per-stack sui RPC + faucet over docker DNS.\n\t\t//\n\t\t// Per-node IP CSV is derived from the subnet prefix; per-node\n\t\t// hostnames likewise. Both must be in lockstep with the\n\t\t// storage-node boot below — the deploy step writes per-node\n\t\t// committee records on chain using these.\n\t\tconst publicHosts = Array.from({ length: opts.nodeCount }, (_, i) =>\n\t\t\tcomputePublicHostname(deps.app, deps.stack, i),\n\t\t).join(',');\n\t\tconst listeningIps = Array.from(\n\t\t\t{ length: opts.nodeCount },\n\t\t\t(_, i) => `${deps.subnetPrefix}.${WALRUS_NODE_IP_BASE + i}`,\n\t\t).join(',');\n\n\t\tyield* setCurrentPluginPhase(\n\t\t\t`deploying Walrus contracts (${opts.nodeCount} node${opts.nodeCount === 1 ? '' : 's'}, ${opts.shards} shards)`,\n\t\t);\n\t\tconst { state } = yield* deployWalrusContracts(deps.publisher, deps.probe, deps.runtime, {\n\t\t\twalrusName: opts.name,\n\t\t\tchainId: deps.suiChainId,\n\t\t\tcontentHash: brandContentHash(\n\t\t\t\t`walrus|${opts.version}|${opts.suiVersion}|${opts.nodeCount}|${opts.shards}|${opts.epochDuration}`,\n\t\t\t),\n\t\t\toutputDirHostPath: deps.deployHostMountPath,\n\t\t\tstackRoot: deps.stackRoot,\n\t\t\tsuiRpcUrlInNetwork: deps.suiRpcUrlInNetwork,\n\t\t\twalrusFaucetUrlInNetwork: deps.walrusFaucetUrlInNetwork,\n\t\t\twaitForFundsReady: deps.waitForFundsReady,\n\t\t\tcommitteeSize: opts.nodeCount,\n\t\t\tshards: opts.shards,\n\t\t\tepochDuration: opts.epochDuration,\n\t\t\tpublicHostsCsv: publicHosts,\n\t\t\tlisteningIpsCsv: listeningIps,\n\t\t\twalrusImage,\n\t\t\tsuiNetworkName: deps.suiNetworkName,\n\t\t});\n\t\tconst deployConfigHash = [\n\t\t\t'walrus-deploy',\n\t\t\tstate.walrusPackageId,\n\t\t\tstate.systemObject,\n\t\t\tstate.stakingObject,\n\t\t\tstate.exchangeObject ?? 'no-exchange',\n\t\t].join('|');\n\n\t\t// Per-node image tags. Each storage node runs under its OWN tag (a cheap\n\t\t// re-tag of the one content-cached cargo build) so its committed writable\n\t\t// layer — the RocksDB holding that node's slivers — promotes to a DISTINCT\n\t\t// image on snapshot restore. A single shared tag would make the restore\n\t\t// image-promote collapse all N committed layers onto one tag\n\t\t// (last-write-wins): N-1 nodes boot on the wrong layer and every\n\t\t// pre-snapshot blob is orphaned. The tag is scoped by the deployed\n\t\t// `walrusPackageId` — stable across boot1 -> restore -> boot2 (the deploy\n\t\t// cache reuses the same id, so the restore's promote target matches what\n\t\t// boot2 requests), but DISTINCT per fresh deployment, so a later cold boot\n\t\t// on a new chain never inherits a prior run's committed node layers (which\n\t\t// carry a stale committee and would reject writes). The base `walrusImage`\n\t\t// (shared) still backs the transient deploy one-shot (`--rm`, unsnapshotted).\n\t\tconst nodeImageScope = state.walrusPackageId.replace(/^0x/, '').slice(0, 12);\n\t\tconst nodeImages = yield* Effect.forEach(\n\t\t\tArray.from({ length: opts.nodeCount }, (_, i) => i),\n\t\t\t(i) =>\n\t\t\t\tresolveCargoImage(\n\t\t\t\t\tdeps.runtime,\n\t\t\t\t\t{\n\t\t\t\t\t\twalrusRef: opts.version,\n\t\t\t\t\t\tsuiVersion: opts.suiVersion,\n\t\t\t\t\t\towner: { app: deps.app, stack: deps.stack },\n\t\t\t\t\t},\n\t\t\t\t\t`${walrusImage.tag}-${nodeImageScope}-node-${i}`,\n\t\t\t\t),\n\t\t);\n\n\t\t// ---- storage nodes — parallel boot ----------------------\n\t\tyield* setCurrentPluginPhase(\n\t\t\t`starting ${opts.nodeCount} Walrus storage node${opts.nodeCount === 1 ? '' : 's'}`,\n\t\t);\n\t\tconst { nodes } = yield* startStorageNodes(deps.runtime, {\n\t\t\tapp: deps.app,\n\t\t\tstack: deps.stack,\n\t\t\twalrusName: opts.name,\n\t\t\timages: nodeImages,\n\t\t\tnodeCount: opts.nodeCount,\n\t\t\tsubnetPrefix: deps.subnetPrefix,\n\t\t\tcontainerApiPort: opts.containerApiPort,\n\t\t\twalrusNetworkName: deps.walrusNetworkName,\n\t\t\tsuiNetworkName: deps.suiNetworkName,\n\t\t\tdeployHostMountPath: deps.deployHostMountPath,\n\t\t\tstackRoot: deps.stackRoot,\n\t\t\tdeployConfigHash,\n\t\t\treadyTimeoutMs: opts.readyTimeoutMs,\n\t\t});\n\n\t\t// ---- proxy URL pick — nodes[0].rpcUrl -------------------\n\t\t// `nodeCount >= 1` is enforced synchronously already; this is\n\t\t// defense-in-depth.\n\t\tif (nodes.length === 0) {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\twalrusPluginError('proxy', 'walrus: at least one storage node is required'),\n\t\t\t);\n\t\t}\n\t\tconst proxyUrl = nodes[0]!.rpcUrl;\n\n\t\t// ---- exchange resolution + WAL funding strategy ------\n\t\t// Register a `coinType:<fullCoinType>` strategy that swaps the\n\t\t// requesting account's SUI into WAL on demand. Accounts opt in via\n\t\t// their normal `funding` list.\n\t\tyield* setCurrentPluginPhase('resolving WAL exchange');\n\t\tconst exchange = yield* resolveWalExchange(deps.probe, state.exchangeObject);\n\t\tconst walFaucetStrategy: WalFaucetStrategy | null = exchange\n\t\t\t? makeWalFaucetStrategy({\n\t\t\t\t\texchange,\n\t\t\t\t\tsdk: deps.suiSdk,\n\t\t\t\t})\n\t\t\t: null;\n\t\tconst resolvedWalCoinType = yield* resolveWalCoinType({\n\t\t\tprobe: deps.probe,\n\t\t\ttreasuryObjectId: state.treasuryObject,\n\t\t\tdeployPackageId: state.walrusPackageId,\n\t\t\trequireTreasuryObject: exchange !== null,\n\t\t});\n\t\tconst resolvedWalPackageId = walPackageIdFromCoinType(resolvedWalCoinType);\n\n\t\treturn {\n\t\t\tmode: 'local' as const,\n\t\t\tdeploy: state,\n\t\t\twalrusPackageId: state.walrusPackageId,\n\t\t\twalPackageId: resolvedWalPackageId,\n\t\t\tnodes,\n\t\t\taggregatorUrl: proxyUrl,\n\t\t\tpublisherUrl: proxyUrl,\n\t\t\tproxyUrl,\n\t\t\texchangeObjectId: state.exchangeObject,\n\t\t\texchange,\n\t\t\twalFaucetStrategy,\n\t\t\twalCoinType: resolvedWalCoinType,\n\t\t};\n\t});\n"],"mappings":";;;;;;;;;;;;;;;AAsIA,MAAa,8BACZ,SACiC;CACjC,MAAM,YAAY,sBAAsB,KAAK,aAAa,GAAG;EAC5D,OAAO;EACP,UAAU,EAAE,OAAO,SAAS,WAC3B,kBACC,OACA,mDAAmD,OAAO,KAAK,aAAa,CAAC,EAAE,IAC/E,QAAQ,OACT;EACD,MAAM;CACP,CAAC;CACD,IAAI,YAAA,GACH,MAAM,kBACL,aACA,kCAAkC,UAAU,+EAE5C,+EACD;CAED,MAAM,SAAS,sBAAsB,KAAK,UAAU,KAAK;EACxD,OAAO;EACP,UAAU,EAAE,OAAO,SAAS,WAC3B,kBACC,OACA,8DAA8D,OAAO,KAAK,UAAU,GAAG,EAAE,IACzF,QAAQ,OACT;EACD,MAAM;CACP,CAAC;CACD,IAAI,SAAS,WACZ,MAAM,kBACL,UACA,+BAA+B,OAAO,0BAA0B,UAAU,IAC1E,iBAAiB,UAAU,eAC5B;CAED,OAAO;EACN,MAAM,KAAK,QAAQ;EACnB;EACA;EACA,SAAS,KAAK,WAAA;EACd,YAAY,KAAK,cAAA;EACjB,kBAAkB,KAAK,oBAAA;EACvB,eAAe,KAAK,iBAAiB;EACrC,gBAAgB,KAAK,kBAAA;CACtB;AACD;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,MAAa,oBACZ,MACA,SAMA,OAAO,IAAI,aAAa;CAIvB,OAAO,sBAAsB,0BAA0B,KAAK,SAAS;CACrE,MAAM,cAAc,OAAO,kBAAkB,KAAK,SAAS;EAC1D,WAAW,KAAK;EAChB,YAAY,KAAK;EACjB,OAAO;GAAE,KAAK,KAAK;GAAK,OAAO,KAAK;EAAM;CAC3C,CAAC;CAGD,OAAO,sBAAsB,2BAA2B,KAAK,mBAAmB;CAChF,OAAO,KAAK,QACV,cAAc;EACd,MAAM,KAAK;EACX,KAAK,KAAK;EACV,OAAO,KAAK;CACb,CAAC,CAAC,CACD,KACA,OAAO,OAAO,UACb,OAAO,KACN,kBACC,mBACA,0BAA0B,KAAK,kBAAkB,aAAa,MAAM,OAAO,IAAI,MAAM,UACrF,EAAE,MAAM,CACT,CACD,CACD,CACD;CAUD,MAAM,cAAc,MAAM,KAAK,EAAE,QAAQ,KAAK,UAAU,IAAI,GAAG,MAC9D,sBAAsB,KAAK,KAAK,KAAK,OAAO,CAAC,CAC9C,CAAC,CAAC,KAAK,GAAG;CACV,MAAM,eAAe,MAAM,KAC1B,EAAE,QAAQ,KAAK,UAAU,IACxB,GAAG,MAAM,GAAG,KAAK,aAAa,GAAA,KAAyB,GACzD,CAAC,CAAC,KAAK,GAAG;CAEV,OAAO,sBACN,+BAA+B,KAAK,UAAU,OAAO,KAAK,cAAc,IAAI,KAAK,IAAI,IAAI,KAAK,OAAO,SACtG;CACA,MAAM,EAAE,UAAU,OAAO,sBAAsB,KAAK,WAAW,KAAK,OAAO,KAAK,SAAS;EACxF,YAAY,KAAK;EACjB,SAAS,KAAK;EACd,aAAaA,YACZ,UAAU,KAAK,QAAQ,GAAG,KAAK,WAAW,GAAG,KAAK,UAAU,GAAG,KAAK,OAAO,GAAG,KAAK,eACpF;EACA,mBAAmB,KAAK;EACxB,WAAW,KAAK;EAChB,oBAAoB,KAAK;EACzB,0BAA0B,KAAK;EAC/B,mBAAmB,KAAK;EACxB,eAAe,KAAK;EACpB,QAAQ,KAAK;EACb,eAAe,KAAK;EACpB,gBAAgB;EAChB,iBAAiB;EACjB;EACA,gBAAgB,KAAK;CACtB,CAAC;CACD,MAAM,mBAAmB;EACxB;EACA,MAAM;EACN,MAAM;EACN,MAAM;EACN,MAAM,kBAAkB;CACzB,CAAC,CAAC,KAAK,GAAG;CAeV,MAAM,iBAAiB,MAAM,gBAAgB,QAAQ,OAAO,EAAE,CAAC,CAAC,MAAM,GAAG,EAAE;CAC3E,MAAM,aAAa,OAAO,OAAO,QAChC,MAAM,KAAK,EAAE,QAAQ,KAAK,UAAU,IAAI,GAAG,MAAM,CAAC,IACjD,MACA,kBACC,KAAK,SACL;EACC,WAAW,KAAK;EAChB,YAAY,KAAK;EACjB,OAAO;GAAE,KAAK,KAAK;GAAK,OAAO,KAAK;EAAM;CAC3C,GACA,GAAG,YAAY,IAAI,GAAG,eAAe,QAAQ,GAC9C,CACF;CAGA,OAAO,sBACN,YAAY,KAAK,UAAU,sBAAsB,KAAK,cAAc,IAAI,KAAK,KAC9E;CACA,MAAM,EAAE,UAAU,OAAO,kBAAkB,KAAK,SAAS;EACxD,KAAK,KAAK;EACV,OAAO,KAAK;EACZ,YAAY,KAAK;EACjB,QAAQ;EACR,WAAW,KAAK;EAChB,cAAc,KAAK;EACnB,kBAAkB,KAAK;EACvB,mBAAmB,KAAK;EACxB,gBAAgB,KAAK;EACrB,qBAAqB,KAAK;EAC1B,WAAW,KAAK;EAChB;EACA,gBAAgB,KAAK;CACtB,CAAC;CAKD,IAAI,MAAM,WAAW,GACpB,OAAO,OAAO,OAAO,KACpB,kBAAkB,SAAS,+CAA+C,CAC3E;CAED,MAAM,WAAW,MAAM,EAAE,CAAE;CAM3B,OAAO,sBAAsB,wBAAwB;CACrD,MAAM,WAAW,OAAO,mBAAmB,KAAK,OAAO,MAAM,cAAc;CAC3E,MAAM,oBAA8C,WACjD,sBAAsB;EACtB;EACA,KAAK,KAAK;CACX,CAAC,IACA;CACH,MAAM,sBAAsB,OAAO,mBAAmB;EACrD,OAAO,KAAK;EACZ,kBAAkB,MAAM;EACxB,iBAAiB,MAAM;EACvB,uBAAuB,aAAa;CACrC,CAAC;CACD,MAAM,uBAAuB,yBAAyB,mBAAmB;CAEzE,OAAO;EACN,MAAM;EACN,QAAQ;EACR,iBAAiB,MAAM;EACvB,cAAc;EACd;EACA,eAAe;EACf,cAAc;EACd;EACA,kBAAkB,MAAM;EACxB;EACA;EACA,aAAa;CACd;AACD,CAAC"}
1
+ {"version":3,"file":"local-cluster.mjs","names":["brandContentHash"],"sources":["../../../../src/plugins/walrus/mode/local-cluster.ts"],"sourcesContent":["// Walrus mode — local cluster.\n//\n// Distilled-doc reference (06-walrus.md §\"Lifecycle: Startup —\n// local cluster\"). Eight ordered phases:\n//\n// 0. Yield deps (Sui, Identity, ChainProbe, faucet strategy opt).\n// 1. Image build — upstream cargo image (bootstrap asset) +\n// wrapper image (inline; content-addressed on\n// the upstream tag + suiVersion).\n// 1b. Docker network create — per-stack `/24` via\n// `Docker.networkCreate(networkName, {subnet})`.\n// 2. Deploy contracts — via `ArtifactPublisher`. The\n// publisher folds chainId into the cache\n// key; verify probes BOTH the on-disk\n// `deploy` file AND on-chain object\n// existence; produce runs the deploy\n// one-shot.\n// 3. Register committee — omitted entirely (no placeholder).\n// 4. Storage nodes — parallel boot of N storage-node containers.\n// 5. Exchange resolution — on-chain getObject + parse type.\n// Degrades to undefined on\n// OBJECT_NOT_FOUND.\n// 6. WAL faucet strategy register — opt-in when an exchange exists.\n// 7. Registries — package + endpoint + walrus-state.\n//\n// What this file owns: the dispatch shape + the typed options\n// surface + the synchronous factory-time validation.\n//\n// What this file delegates: container/Move heavy paths live in\n// `storage-nodes.ts`, `deploy.ts`, and the `ArtifactPublisher`\n// primitive.\n\nimport { Effect, FileSystem, Path, type Scope } from 'effect';\n\nimport type {\n\tArtifactPublishError,\n\tArtifactPublisher,\n} from '../../../primitives/artifact-publisher.ts';\nimport type { ChainProbe } from '../../../contracts/chain-probe.ts';\nimport type { ContainerRuntime } from '../../../contracts/container-runtime.ts';\nimport type { SuiProbeKey } from '../../sui/index.ts';\nimport { contentHash as brandContentHash } from '../../../substrate/brand.ts';\nimport { expectPort, expectPositiveInteger } from '../../../substrate/runtime/config-validation.ts';\nimport { setCurrentPluginPhase } from '../../../substrate/runtime/current-plugin.ts';\nimport { walrusConfigError, walrusPluginError, type WalrusError } from '../errors.ts';\nimport { WALRUS_MAX_NODE_COUNT } from '../routable.ts';\nimport type { WalrusStorageNode } from '../storage-nodes.ts';\nimport {\n\tDEFAULT_NODE_READY_TIMEOUT_MS,\n\tDEFAULT_CONTAINER_API_PORT,\n\tWALRUS_NODE_IP_BASE,\n\tcomputeCommitteeHostname,\n\tstartStorageNodes,\n} from '../storage-nodes.ts';\nimport { deployWalrusContracts, type CachedDeployState } from '../deploy.ts';\nimport {\n\tDEFAULT_SUI_VERSION,\n\tDEFAULT_WALRUS_REF,\n\tresolveCargoImage,\n} from '../bootstrap-assets/cargo-image.ts';\nimport {\n\tDEFAULT_WALRUS_CLIENT_SERVICE_PORT,\n\tstartWalrusClientServices,\n\ttype WalrusClientServices,\n} from '../client-services.ts';\nimport {\n\tmakeWalFaucetStrategy,\n\twalPackageIdFromCoinType,\n\tresolveWalCoinType,\n\ttype WalFaucetStrategy,\n} from '../faucet-strategy.ts';\nimport { resolveWalExchange, type WalExchangeHandle, type WalSwapSdk } from '../wal-swap.ts';\n\n/** Options for the local-cluster mode (06-walrus.md\n * §\"Configuration\"). */\nexport interface WalrusLocalClusterOptions {\n\t/** Engine row name + on-disk dir suffix + registry key.\n\t * Default `'walrus'`. */\n\treadonly name?: string;\n\t/** Number of storage-node containers. Must be `>= 1` (synchronous\n\t * factory-time guard). Default `1`. */\n\treadonly nodeCount?: number;\n\t/** Total shards distributed across the committee. Must be\n\t * `>= nodeCount`. Default `100` (distilled-doc default block). */\n\treadonly shards?: number;\n\t/** Pinned walrus release — drives the wrapper image build.\n\t * Default `'testnet-v1.49.1'`. */\n\treadonly version?: string;\n\t/** Sui release whose binary the wrapper image bakes (distilled-\n\t * doc invariant 24). Default `'devnet-v1.71.0'`. */\n\treadonly suiVersion?: string;\n\t/** Port each storage node binds inside the container. Default\n\t * 9185 (same as the router entrypoint — invariant 9). */\n\treadonly containerApiPort?: number;\n\t/** Walrus epoch length passed to `walrus-deploy --epoch-duration`.\n\t * Default `'24h'`. */\n\treadonly epochDuration?: string;\n\t/** Per-node TCP ready-probe timeout (ms). Defaults to\n\t * `DEFAULT_NODE_READY_TIMEOUT_MS`. */\n\treadonly readyTimeoutMs?: number;\n\t/** Local app-facing aggregator service. Defaults to enabled. Set\n\t * `false` to expose only storage-node routes. */\n\treadonly aggregator?: boolean | WalrusLocalServiceOptions;\n\t/** Local app-facing publisher service. Defaults to enabled. Set\n\t * `false` to run storage nodes without the simple publish API. */\n\treadonly publisher?: boolean | WalrusLocalPublisherOptions;\n}\n\nexport interface WalrusLocalServiceOptions {\n\t/** In-container port passed to the release service's\n\t * `--bind-address 0.0.0.0:<port>`. Defaults to the Walrus CLI's\n\t * publisher/aggregator port. */\n\treadonly port?: number;\n}\n\nexport type WalrusLocalPublisherOptions = WalrusLocalServiceOptions;\n\n/** Resolved local-cluster boot artifacts. */\nexport interface LocalClusterBootResult {\n\treadonly mode: 'local';\n\treadonly deploy: CachedDeployState;\n\treadonly walrusPackageId: string;\n\treadonly walPackageId: string;\n\treadonly nodes: ReadonlyArray<WalrusStorageNode>;\n\treadonly exchangeObjectId: string | undefined;\n\treadonly exchange: WalExchangeHandle | null;\n\treadonly clientServices: WalrusClientServices;\n\t/** WAL faucet strategy — present only when the local cluster has a\n\t * non-empty exchange object. The barrel registers this onto the\n\t * `coinType:<fullCoinType>` strategy contributor decl. `null`\n\t * otherwise. */\n\treadonly walFaucetStrategy: WalFaucetStrategy | null;\n\treadonly walCoinType: string;\n}\n\n/** Defaults applied to options. */\nexport interface ResolvedLocalClusterOptions {\n\treadonly name: string;\n\treadonly nodeCount: number;\n\treadonly shards: number;\n\treadonly version: string;\n\treadonly suiVersion: string;\n\treadonly containerApiPort: number;\n\treadonly epochDuration: string;\n\treadonly readyTimeoutMs: number;\n\treadonly aggregator: ResolvedWalrusLocalServiceOptions | null;\n\treadonly publisher: ResolvedWalrusLocalPublisherOptions | null;\n}\n\nexport interface ResolvedWalrusLocalServiceOptions {\n\treadonly port: number;\n}\n\nexport type ResolvedWalrusLocalPublisherOptions = ResolvedWalrusLocalServiceOptions;\n\n/** Synchronous factory-time validation. Throws (NOT Effect-fail) so\n * misconfiguration trips at the `defineDevstack` call site rather\n * than at deferred Layer.build time. Distilled-doc invariant 11. */\nexport const resolveLocalClusterOptions = (\n\topts: WalrusLocalClusterOptions,\n): ResolvedLocalClusterOptions => {\n\tconst nodeCount = expectPositiveInteger(opts.nodeCount ?? 1, {\n\t\tfield: 'nodeCount',\n\t\tmkError: ({ field, message, hint }) =>\n\t\t\twalrusConfigError(\n\t\t\t\tfield,\n\t\t\t\t`walrusLocalCluster: nodeCount must be >= 1 (got ${String(opts.nodeCount ?? 1)})`,\n\t\t\t\thint ?? message,\n\t\t\t),\n\t\thint: 'set nodeCount to a positive integer (default 1)',\n\t});\n\tif (nodeCount > WALRUS_MAX_NODE_COUNT) {\n\t\tthrow walrusConfigError(\n\t\t\t'nodeCount',\n\t\t\t`walrusLocalCluster: nodeCount (${nodeCount}) exceeds the maximum ${WALRUS_MAX_NODE_COUNT} ` +\n\t\t\t\t`pre-declared Traefik entrypoints (walrus-node-0..${WALRUS_MAX_NODE_COUNT - 1}).`,\n\t\t\t`reduce nodeCount or raise WALRUS_MAX_NODE_COUNT in plugins/walrus/routable.ts`,\n\t\t);\n\t}\n\tconst shards = expectPositiveInteger(opts.shards ?? 100, {\n\t\tfield: 'shards',\n\t\tmkError: ({ field, message, hint }) =>\n\t\t\twalrusConfigError(\n\t\t\t\tfield,\n\t\t\t\t`walrusLocalCluster: shards must be a positive integer (got ${String(opts.shards ?? 100)})`,\n\t\t\t\thint ?? message,\n\t\t\t),\n\t\thint: 'set shards to a positive integer (default 100)',\n\t});\n\tif (shards < nodeCount) {\n\t\tthrow walrusConfigError(\n\t\t\t'shards',\n\t\t\t`walrusLocalCluster: shards (${shards}) must be >= nodeCount (${nodeCount})`,\n\t\t\t`set shards >= ${nodeCount} (default 100)`,\n\t\t);\n\t}\n\treturn {\n\t\tname: opts.name ?? 'walrus',\n\t\tnodeCount,\n\t\tshards,\n\t\tversion: opts.version ?? DEFAULT_WALRUS_REF,\n\t\tsuiVersion: opts.suiVersion ?? DEFAULT_SUI_VERSION,\n\t\tcontainerApiPort: opts.containerApiPort ?? DEFAULT_CONTAINER_API_PORT,\n\t\tepochDuration: opts.epochDuration ?? '24h',\n\t\treadyTimeoutMs: opts.readyTimeoutMs ?? DEFAULT_NODE_READY_TIMEOUT_MS,\n\t\t...resolveLocalServicesOptions(opts),\n\t};\n};\n\nconst expectOptionalPort = (value: number | undefined, field: string): number | undefined =>\n\tvalue === undefined\n\t\t? undefined\n\t\t: expectPort(value, {\n\t\t\t\tfield,\n\t\t\t\tmkError: ({ field: f, message, hint }) =>\n\t\t\t\t\twalrusConfigError(f, `walrusLocalCluster: ${f} ${message}`, hint),\n\t\t\t});\n\nconst resolveLocalServiceBase = (\n\tvalue: boolean | WalrusLocalServiceOptions | undefined,\n\tfieldPrefix: 'aggregator' | 'publisher',\n): ResolvedWalrusLocalServiceOptions | null => {\n\tif (value === false) return null;\n\tconst authored = value === undefined || value === true ? {} : value;\n\treturn {\n\t\tport:\n\t\t\texpectOptionalPort(authored.port, `${fieldPrefix}.port`) ??\n\t\t\tDEFAULT_WALRUS_CLIENT_SERVICE_PORT,\n\t};\n};\n\nconst resolveLocalServicesOptions = (\n\topts: WalrusLocalClusterOptions,\n): Pick<ResolvedLocalClusterOptions, 'aggregator' | 'publisher'> => {\n\treturn {\n\t\taggregator: resolveLocalServiceBase(opts.aggregator, 'aggregator'),\n\t\tpublisher: resolveLocalServiceBase(opts.publisher, 'publisher'),\n\t};\n};\n\n/** Dependencies the local service consumes at acquire-time. */\nexport interface LocalClusterDeps {\n\treadonly runtime: ContainerRuntime;\n\treadonly publisher: ArtifactPublisher;\n\treadonly probe: ChainProbe<SuiProbeKey>;\n\treadonly suiSdk: WalSwapSdk;\n\treadonly suiChainId: string;\n\treadonly suiRpcUrlInNetwork: string;\n\treadonly walrusFaucetUrlInNetwork: string;\n\treadonly waitForFundsReady: Effect.Effect<void, unknown>;\n\treadonly app: string;\n\treadonly stack: string;\n\t/** Pre-allocated /24 prefix from `subnetForStack`. */\n\treadonly subnetPrefix: string;\n\treadonly walrusNetworkName: string;\n\treadonly suiNetworkName: string;\n\treadonly deployHostMountPath: string;\n\t/** On-disk per-stack root from `StackPathsService.stackRoot`. The\n\t * walrus bind-mount is computed off this; `deployHostMountPath`\n\t * MUST live under it. */\n\treadonly stackRoot: string;\n}\n\n/** Boot the local cluster. Returns the resolved value the plugin\n * projects onto its tags.\n *\n * Steps:\n * - Image build — `resolveCargoImage` (bootstrap asset).\n * Honors `WALRUS_CARGO_IMAGE_OVERRIDE` for the pre-baked path.\n * - Docker network ensure — `runtime.ensureNetwork(walrusNet)`.\n * The sui network is owned by the sui plugin; we attach to it\n * as a secondary network on each storage node.\n * - Deploy contracts — `deployWalrusContracts` dispatches through\n * the artifact publisher primitive; the produce body runs the walrus deploy\n * one-shot.\n * - Storage nodes — parallel boot via `startStorageNodes`.\n * - WAL faucet strategy — constructed if `state.exchangeObject`\n * exists. Surfaced on the boot result; the barrel registers\n * the contributor decl.\n *\n * Per-step failures surface as `WalrusPluginError` with the phase\n * vocabulary from `errors.ts`; artifact publisher failures pass through as\n * `ArtifactPublishError`. The plugin narration vocabulary anchors\n * on these phase tags.\n */\nexport const bootLocalCluster = (\n\tdeps: LocalClusterDeps,\n\topts: ResolvedLocalClusterOptions,\n): Effect.Effect<\n\tLocalClusterBootResult,\n\tWalrusError | ArtifactPublishError,\n\tScope.Scope | FileSystem.FileSystem | Path.Path\n> =>\n\tEffect.gen(function* () {\n\t\t// ---- cargo image (bootstrap asset) -----------------------\n\t\t// The cargo image is content-addressed; the resolver owns the\n\t\t// cache check + the registry-tag override fast path.\n\t\tyield* setCurrentPluginPhase(`resolving Walrus image ${opts.version}`);\n\t\tconst walrusImage = yield* resolveCargoImage(deps.runtime, {\n\t\t\twalrusRef: opts.version,\n\t\t\tsuiVersion: opts.suiVersion,\n\t\t\towner: { app: deps.app, stack: deps.stack },\n\t\t});\n\n\t\t// ---- docker network ensure ------------------------------\n\t\tyield* setCurrentPluginPhase(`ensuring Walrus network ${deps.walrusNetworkName}`);\n\t\tyield* deps.runtime\n\t\t\t.ensureNetwork({\n\t\t\t\tname: deps.walrusNetworkName,\n\t\t\t\tapp: deps.app,\n\t\t\t\tstack: deps.stack,\n\t\t\t})\n\t\t\t.pipe(\n\t\t\t\tEffect.catch((cause) =>\n\t\t\t\t\tEffect.fail(\n\t\t\t\t\t\twalrusPluginError(\n\t\t\t\t\t\t\t'cluster-network',\n\t\t\t\t\t\t\t`walrus: ensureNetwork('${deps.walrusNetworkName}') failed: ${cause.reason}: ${cause.detail}`,\n\t\t\t\t\t\t\t{ cause },\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t);\n\n\t\t// ---- deploy via ArtifactPublisher ----------------\n\t\t// The deploy one-shot needs the walrus image + sui network so it\n\t\t// can dial the per-stack sui RPC + faucet over docker DNS.\n\t\t//\n\t\t// Per-node committee hostnames are Docker-network aliases, because the\n\t\t// real Rust publisher/aggregator read these public_host values from chain\n\t\t// and dial storage nodes from inside the Walrus network. Router-facing\n\t\t// `.localhost` hostnames are still produced by `startStorageNodes` for\n\t\t// devstack's public diagnostic URLs.\n\t\tconst committeeHosts = Array.from({ length: opts.nodeCount }, (_, i) =>\n\t\t\tcomputeCommitteeHostname(i),\n\t\t).join(',');\n\t\tconst listeningIps = Array.from(\n\t\t\t{ length: opts.nodeCount },\n\t\t\t(_, i) => `${deps.subnetPrefix}.${WALRUS_NODE_IP_BASE + i}`,\n\t\t).join(',');\n\n\t\tyield* setCurrentPluginPhase(\n\t\t\t`deploying Walrus contracts (${opts.nodeCount} node${opts.nodeCount === 1 ? '' : 's'}, ${opts.shards} shards)`,\n\t\t);\n\t\tconst { state } = yield* deployWalrusContracts(deps.publisher, deps.probe, deps.runtime, {\n\t\t\twalrusName: opts.name,\n\t\t\tchainId: deps.suiChainId,\n\t\t\tcontentHash: brandContentHash(\n\t\t\t\t`walrus|${opts.version}|${opts.suiVersion}|${opts.nodeCount}|${opts.shards}|${opts.epochDuration}|committee=${committeeHosts}|listen=${listeningIps}`,\n\t\t\t),\n\t\t\toutputDirHostPath: deps.deployHostMountPath,\n\t\t\tstackRoot: deps.stackRoot,\n\t\t\tsuiRpcUrlInNetwork: deps.suiRpcUrlInNetwork,\n\t\t\twalrusFaucetUrlInNetwork: deps.walrusFaucetUrlInNetwork,\n\t\t\twaitForFundsReady: deps.waitForFundsReady,\n\t\t\tcommitteeSize: opts.nodeCount,\n\t\t\tshards: opts.shards,\n\t\t\tepochDuration: opts.epochDuration,\n\t\t\tpublicHostsCsv: committeeHosts,\n\t\t\tlisteningIpsCsv: listeningIps,\n\t\t\twalrusImage,\n\t\t\tsuiNetworkName: deps.suiNetworkName,\n\t\t});\n\t\tconst deployConfigHash = [\n\t\t\t'walrus-deploy',\n\t\t\tstate.walrusPackageId,\n\t\t\tstate.systemObject,\n\t\t\tstate.stakingObject,\n\t\t\tstate.exchangeObject ?? 'no-exchange',\n\t\t].join('|');\n\n\t\t// Per-node image tags. Each storage node runs under its OWN tag (a cheap\n\t\t// re-tag of the one content-cached cargo build) so its committed writable\n\t\t// layer — the RocksDB holding that node's slivers — promotes to a DISTINCT\n\t\t// image on snapshot restore. A single shared tag would make the restore\n\t\t// image-promote collapse all N committed layers onto one tag\n\t\t// (last-write-wins): N-1 nodes boot on the wrong layer and every\n\t\t// pre-snapshot blob is orphaned. The tag is scoped by the deployed\n\t\t// `walrusPackageId` — stable across boot1 -> restore -> boot2 (the deploy\n\t\t// cache reuses the same id, so the restore's promote target matches what\n\t\t// boot2 requests), but DISTINCT per fresh deployment, so a later cold boot\n\t\t// on a new chain never inherits a prior run's committed node layers (which\n\t\t// carry a stale committee and would reject writes). The base `walrusImage`\n\t\t// (shared) still backs the transient deploy one-shot (`--rm`, unsnapshotted).\n\t\tconst nodeImageScope = state.walrusPackageId.replace(/^0x/, '').slice(0, 12);\n\t\tconst resolveServiceImage = (role: 'aggregator' | 'publisher') =>\n\t\t\tresolveCargoImage(\n\t\t\t\tdeps.runtime,\n\t\t\t\t{\n\t\t\t\t\twalrusRef: opts.version,\n\t\t\t\t\tsuiVersion: opts.suiVersion,\n\t\t\t\t\towner: { app: deps.app, stack: deps.stack },\n\t\t\t\t},\n\t\t\t\t`${walrusImage.tag}-${nodeImageScope}-${role}`,\n\t\t\t);\n\t\tconst nodeImages = yield* Effect.forEach(\n\t\t\tArray.from({ length: opts.nodeCount }, (_, i) => i),\n\t\t\t(i) =>\n\t\t\t\tresolveCargoImage(\n\t\t\t\t\tdeps.runtime,\n\t\t\t\t\t{\n\t\t\t\t\t\twalrusRef: opts.version,\n\t\t\t\t\t\tsuiVersion: opts.suiVersion,\n\t\t\t\t\t\towner: { app: deps.app, stack: deps.stack },\n\t\t\t\t\t},\n\t\t\t\t\t`${walrusImage.tag}-${nodeImageScope}-node-${i}`,\n\t\t\t\t),\n\t\t);\n\t\tconst clientServiceImages = yield* Effect.all({\n\t\t\taggregator:\n\t\t\t\topts.aggregator === null ? Effect.succeed(null) : resolveServiceImage('aggregator'),\n\t\t\tpublisher: opts.publisher === null ? Effect.succeed(null) : resolveServiceImage('publisher'),\n\t\t});\n\n\t\t// ---- storage nodes — parallel boot ----------------------\n\t\tyield* setCurrentPluginPhase(\n\t\t\t`starting ${opts.nodeCount} Walrus storage node${opts.nodeCount === 1 ? '' : 's'}`,\n\t\t);\n\t\tconst { nodes } = yield* startStorageNodes(deps.runtime, {\n\t\t\tapp: deps.app,\n\t\t\tstack: deps.stack,\n\t\t\twalrusName: opts.name,\n\t\t\timages: nodeImages,\n\t\t\tnodeCount: opts.nodeCount,\n\t\t\tsubnetPrefix: deps.subnetPrefix,\n\t\t\tcontainerApiPort: opts.containerApiPort,\n\t\t\twalrusNetworkName: deps.walrusNetworkName,\n\t\t\tsuiNetworkName: deps.suiNetworkName,\n\t\t\tdeployHostMountPath: deps.deployHostMountPath,\n\t\t\tstackRoot: deps.stackRoot,\n\t\t\tdeployConfigHash,\n\t\t\treadyTimeoutMs: opts.readyTimeoutMs,\n\t\t});\n\n\t\t// `nodeCount >= 1` is enforced synchronously already; this is\n\t\t// defense-in-depth.\n\t\tif (nodes.length === 0) {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\twalrusPluginError('storage-node', 'walrus: at least one storage node is required'),\n\t\t\t);\n\t\t}\n\n\t\t// ---- app-facing Rust publisher / aggregator services ----\n\t\tyield* setCurrentPluginPhase('starting Walrus client services');\n\t\tconst clientServices = yield* startWalrusClientServices(deps.runtime, {\n\t\t\tapp: deps.app,\n\t\t\tstack: deps.stack,\n\t\t\twalrusName: opts.name,\n\t\t\timages: clientServiceImages,\n\t\t\toptions: {\n\t\t\t\taggregator: opts.aggregator,\n\t\t\t\tpublisher: opts.publisher,\n\t\t\t},\n\t\t\twalrusNetworkName: deps.walrusNetworkName,\n\t\t\tsuiNetworkName: deps.suiNetworkName,\n\t\t\tdeployHostMountPath: deps.deployHostMountPath,\n\t\t\tstackRoot: deps.stackRoot,\n\t\t\tdeployConfigHash,\n\t\t\tsuiRpcUrlInNetwork: deps.suiRpcUrlInNetwork,\n\t\t\treadyTimeoutMs: opts.readyTimeoutMs,\n\t\t});\n\n\t\t// ---- exchange resolution + WAL funding strategy ------\n\t\t// Register a `coinType:<fullCoinType>` strategy that swaps the\n\t\t// requesting account's SUI into WAL on demand. Accounts opt in via\n\t\t// their normal `funding` list.\n\t\tyield* setCurrentPluginPhase('resolving WAL exchange');\n\t\tconst exchange = yield* resolveWalExchange(deps.probe, state.exchangeObject);\n\t\tconst walFaucetStrategy: WalFaucetStrategy | null = exchange\n\t\t\t? makeWalFaucetStrategy({\n\t\t\t\t\texchange,\n\t\t\t\t\tsdk: deps.suiSdk,\n\t\t\t\t})\n\t\t\t: null;\n\t\tconst resolvedWalCoinType = yield* resolveWalCoinType({\n\t\t\tprobe: deps.probe,\n\t\t\ttreasuryObjectId: state.treasuryObject,\n\t\t\tdeployPackageId: state.walrusPackageId,\n\t\t\trequireTreasuryObject: exchange !== null,\n\t\t});\n\t\tconst resolvedWalPackageId = walPackageIdFromCoinType(resolvedWalCoinType);\n\n\t\treturn {\n\t\t\tmode: 'local' as const,\n\t\t\tdeploy: state,\n\t\t\twalrusPackageId: state.walrusPackageId,\n\t\t\twalPackageId: resolvedWalPackageId,\n\t\t\tnodes,\n\t\t\texchangeObjectId: state.exchangeObject,\n\t\t\texchange,\n\t\t\tclientServices,\n\t\t\twalFaucetStrategy,\n\t\t\twalCoinType: resolvedWalCoinType,\n\t\t};\n\t});\n"],"mappings":"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"}
@@ -39,9 +39,10 @@ const makeLocalRoutables = (args) => {
39
39
  containerPort
40
40
  },
41
41
  cors: true,
42
- wireProtocol: "http"
42
+ wireProtocol: "https"
43
43
  }));
44
- const aggregator = {
44
+ const serviceRoutes = [];
45
+ if (args.aggregator !== void 0 && args.aggregator !== null) serviceRoutes.push({
45
46
  kind: "routable",
46
47
  endpointName: WALRUS_AGGREGATOR_ENDPOINT_NAME,
47
48
  dispatchId: {
@@ -50,13 +51,13 @@ const makeLocalRoutables = (args) => {
50
51
  },
51
52
  upstream: {
52
53
  type: "container",
53
- containerName: containerNameFor(0),
54
- containerPort
54
+ containerName: args.aggregator.containerName,
55
+ containerPort: args.aggregator.containerPort
55
56
  },
56
57
  cors: true,
57
58
  wireProtocol: "http"
58
- };
59
- const publisher = {
59
+ });
60
+ if (args.publisher !== void 0 && args.publisher !== null) serviceRoutes.push({
60
61
  kind: "routable",
61
62
  endpointName: WALRUS_PUBLISHER_ENDPOINT_NAME,
62
63
  dispatchId: {
@@ -65,19 +66,15 @@ const makeLocalRoutables = (args) => {
65
66
  },
66
67
  upstream: {
67
68
  type: "container",
68
- containerName: containerNameFor(0),
69
- containerPort
69
+ containerName: args.publisher.containerName,
70
+ containerPort: args.publisher.containerPort
70
71
  },
71
72
  cors: true,
72
73
  wireProtocol: "http"
73
- };
74
- return [
75
- ...perNodeRoutes,
76
- aggregator,
77
- publisher
78
- ];
74
+ });
75
+ return [...perNodeRoutes, ...serviceRoutes];
79
76
  };
80
77
  //#endregion
81
- export { WALRUS_ENTRYPOINTS, makeLocalRoutables };
78
+ export { WALRUS_AGGREGATOR_ENDPOINT_NAME, WALRUS_ENTRYPOINTS, WALRUS_PUBLISHER_ENDPOINT_NAME, makeLocalRoutables };
82
79
 
83
80
  //# sourceMappingURL=routable.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"routable.mjs","names":[],"sources":["../../../src/plugins/walrus/routable.ts"],"sourcesContent":["// Walrus plugin — Routable contributions.\n//\n// Distilled-doc reference (06-walrus.md §\"Routes registered\"):\n// each storage node carries one Traefik route. The router binds host\n// port 9185 once globally and dispatches by `Host:` header to the\n// per-stack backend — `routerHostname(identity, 'walrus-node-<i>')`.\n//\n// For the local cluster:\n// - N × `walrus-node-<i>` routes — one per storage node, with\n// `cors: true` (walrus storage REST API lacks CORS headers).\n// - 1 × `walrus-aggregator` aliassame backend as `walrus-node-0`,\n// surfaces the conventional alias for SDK consumers.\n// - 1 × `walrus-publisher` aliascollapsed onto the same backend\n// (distilled-doc §\"Endpoints\" \"collapsed onto a single Traefik\n// vhost\").\n//\n// Known-deployment publishes no routes — the aggregator/publisher\n// URLs land on the codegen-emitted `WalrusBindings.{aggregator,\n// publisher}Url` instead.\n\nimport type { EntrypointDecl, RoutableDecl } from '../../contracts/routable.ts';\nimport { WALRUS_ROUTER_PORT } from './storage-nodes.ts';\n\nexport const WALRUS_NODE_ENDPOINT_PREFIX = 'walrus-node-' as const;\nexport const WALRUS_AGGREGATOR_ENDPOINT_NAME = 'walrus-aggregator' as const;\nexport const WALRUS_PUBLISHER_ENDPOINT_NAME = 'walrus-publisher' as const;\n\n/** Upper bound on `nodeCount` — Traefik entrypoints are bound at boot,\n * so the cluster's per-node routes need pre-declared entrypoint names.\n * Validated at factory time in `mode/local-cluster.ts:resolveOptions`. */\nexport const WALRUS_MAX_NODE_COUNT = 8;\n\nexport const WALRUS_ENTRYPOINTS: ReadonlyArray<EntrypointDecl> = [\n\t...Array.from(\n\t\t{ length: WALRUS_MAX_NODE_COUNT },\n\t\t(_, i): EntrypointDecl => ({\n\t\t\tname: `${WALRUS_NODE_ENDPOINT_PREFIX}${i}`,\n\t\t\tport: WALRUS_ROUTER_PORT,\n\t\t\tprotocol: 'http',\n\t\t}),\n\t),\n\t{ name: WALRUS_AGGREGATOR_ENDPOINT_NAME, port: WALRUS_ROUTER_PORT, protocol: 'http' },\n\t{ name: WALRUS_PUBLISHER_ENDPOINT_NAME, port: WALRUS_ROUTER_PORT, protocol: 'http' },\n];\n\n/** Build the Routable contributions for the local cluster. `nodeCount`\n * drives the per-node fan-out; the plugin's service key + route role\n * identify the dispatch target. */\nexport const makeLocalRoutables = (args: {\n\treadonly app: string;\n\treadonly stack: string;\n\treadonly walrusName: string;\n\treadonly serviceKey: string;\n\treadonly nodeCount: number;\n\treadonly containerApiPort?: number;\n}): ReadonlyArray<RoutableDecl> => {\n\tconst containerPort = args.containerApiPort ?? WALRUS_ROUTER_PORT;\n\tconst containerNameFor = (i: number): string =>\n\t\t`devstack-${args.app}-${args.stack}-walrus-${args.walrusName}-node-${i}`;\n\tconst perNodeRoutes: ReadonlyArray<RoutableDecl> = Array.from(\n\t\t{ length: args.nodeCount },\n\t\t(_, i): RoutableDecl => ({\n\t\t\tkind: 'routable',\n\t\t\tendpointName: `${WALRUS_NODE_ENDPOINT_PREFIX}${i}`,\n\t\t\tdispatchId: {\n\t\t\t\tserviceKey: args.serviceKey,\n\t\t\t\trole: `${WALRUS_NODE_ENDPOINT_PREFIX}${i}`,\n\t\t\t},\n\t\t\tupstream: {\n\t\t\t\ttype: 'container',\n\t\t\t\tcontainerName: containerNameFor(i),\n\t\t\t\tcontainerPort,\n\t\t\t},\n\t\t\t// Distilled-doc §\"Routes registered\": cors: true (walrus\n\t\t\t// storage REST API lacks CORS headers).\n\t\t\tcors: true,\n\t\t\twireProtocol: 'http',\n\t\t}),\n\t);\n\n\t// Aggregator + publisher aliases — collapsed onto node-0 per the\n\t// distilled doc. The aggregator and publisher are conventional\n\t// endpoint names that downstream HTTP consumers look up via the\n\t// substrate's endpoint registry.\n\tconst aggregator: RoutableDecl = {\n\t\tkind: 'routable',\n\t\tendpointName: WALRUS_AGGREGATOR_ENDPOINT_NAME,\n\t\tdispatchId: {\n\t\t\tserviceKey: args.serviceKey,\n\t\t\trole: WALRUS_AGGREGATOR_ENDPOINT_NAME,\n\t\t},\n\t\tupstream: { type: 'container', containerName: containerNameFor(0), containerPort },\n\t\tcors: true,\n\t\twireProtocol: 'http',\n\t};\n\tconst publisher: RoutableDecl = {\n\t\tkind: 'routable',\n\t\tendpointName: WALRUS_PUBLISHER_ENDPOINT_NAME,\n\t\tdispatchId: {\n\t\t\tserviceKey: args.serviceKey,\n\t\t\trole: WALRUS_PUBLISHER_ENDPOINT_NAME,\n\t\t},\n\t\tupstream: { type: 'container', containerName: containerNameFor(0), containerPort },\n\t\tcors: true,\n\t\twireProtocol: 'http',\n\t};\n\n\treturn [...perNodeRoutes, aggregator, publisher];\n};\n"],"mappings":";;AAuBA,MAAa,8BAA8B;AAC3C,MAAa,kCAAkC;AAC/C,MAAa,iCAAiC;AAO9C,MAAa,qBAAoD;CAChE,GAAG,MAAM,KACR,EAAE,QAAA,EAA8B,IAC/B,GAAG,OAAuB;EAC1B,MAAM,GAAG,8BAA8B;EACvC,MAAM;EACN,UAAU;CACX,EACD;CACA;EAAE,MAAM;EAAiC,MAAM;EAAoB,UAAU;CAAO;CACpF;EAAE,MAAM;EAAgC,MAAM;EAAoB,UAAU;CAAO;AACpF;;;;AAKA,MAAa,sBAAsB,SAOA;CAClC,MAAM,gBAAgB,KAAK,oBAAA;CAC3B,MAAM,oBAAoB,MACzB,YAAY,KAAK,IAAI,GAAG,KAAK,MAAM,UAAU,KAAK,WAAW,QAAQ;CACtE,MAAM,gBAA6C,MAAM,KACxD,EAAE,QAAQ,KAAK,UAAU,IACxB,GAAG,OAAqB;EACxB,MAAM;EACN,cAAc,GAAG,8BAA8B;EAC/C,YAAY;GACX,YAAY,KAAK;GACjB,MAAM,GAAG,8BAA8B;EACxC;EACA,UAAU;GACT,MAAM;GACN,eAAe,iBAAiB,CAAC;GACjC;EACD;EAGA,MAAM;EACN,cAAc;CACf,EACD;CAMA,MAAM,aAA2B;EAChC,MAAM;EACN,cAAc;EACd,YAAY;GACX,YAAY,KAAK;GACjB,MAAM;EACP;EACA,UAAU;GAAE,MAAM;GAAa,eAAe,iBAAiB,CAAC;GAAG;EAAc;EACjF,MAAM;EACN,cAAc;CACf;CACA,MAAM,YAA0B;EAC/B,MAAM;EACN,cAAc;EACd,YAAY;GACX,YAAY,KAAK;GACjB,MAAM;EACP;EACA,UAAU;GAAE,MAAM;GAAa,eAAe,iBAAiB,CAAC;GAAG;EAAc;EACjF,MAAM;EACN,cAAc;CACf;CAEA,OAAO;EAAC,GAAG;EAAe;EAAY;CAAS;AAChD"}
1
+ {"version":3,"file":"routable.mjs","names":[],"sources":["../../../src/plugins/walrus/routable.ts"],"sourcesContent":["// Walrus plugin — Routable contributions.\n//\n// Distilled-doc reference (06-walrus.md §\"Routes registered\"):\n// each storage node carries one Traefik route. The router binds host\n// port 9185 once globally and dispatches by `Host:` header to the\n// per-stack backend — `routerHostname(identity, 'walrus-node-<i>')`.\n//\n// For the local cluster:\n// - N × `walrus-node-<i>` routes — one per storage node, with\n// `cors: true` (walrus storage REST API lacks CORS headers). These\n// are HTTP public routes backed by HTTPS upstreams because Walrus\n// storage nodes self-sign TLS with their network key.\n// - 1 × `walrus-aggregator` routerelease `walrus aggregator`\n// service container\n// exposing `GET /v1/blobs/:id` through a single app-facing URL.\n// - 1 × `walrus-publisher` routerelease `walrus publisher`\n// service container\n// exposing `PUT /v1/blobs` through a single app-facing URL.\n//\n// Known-deployment publishes no routes — the aggregator/publisher\n// URLs land on the codegen-emitted `WalrusBindings.{aggregator,\n// publisher}Url` instead.\n\nimport type { EntrypointDecl, RoutableDecl } from '../../contracts/routable.ts';\nimport { WALRUS_ROUTER_PORT } from './storage-nodes.ts';\nimport type { WalrusClientService } from './client-services.ts';\n\nexport const WALRUS_NODE_ENDPOINT_PREFIX = 'walrus-node-' as const;\nexport const WALRUS_AGGREGATOR_ENDPOINT_NAME = 'walrus-aggregator' as const;\nexport const WALRUS_PUBLISHER_ENDPOINT_NAME = 'walrus-publisher' as const;\n\n/** Upper bound on `nodeCount` — Traefik entrypoints are bound at boot,\n * so the cluster's per-node routes need pre-declared entrypoint names.\n * Validated at factory time in `mode/local-cluster.ts:resolveOptions`. */\nexport const WALRUS_MAX_NODE_COUNT = 8;\n\nexport const WALRUS_ENTRYPOINTS: ReadonlyArray<EntrypointDecl> = [\n\t...Array.from(\n\t\t{ length: WALRUS_MAX_NODE_COUNT },\n\t\t(_, i): EntrypointDecl => ({\n\t\t\tname: `${WALRUS_NODE_ENDPOINT_PREFIX}${i}`,\n\t\t\tport: WALRUS_ROUTER_PORT,\n\t\t\tprotocol: 'http',\n\t\t}),\n\t),\n\t{ name: WALRUS_AGGREGATOR_ENDPOINT_NAME, port: WALRUS_ROUTER_PORT, protocol: 'http' },\n\t{ name: WALRUS_PUBLISHER_ENDPOINT_NAME, port: WALRUS_ROUTER_PORT, protocol: 'http' },\n];\n\n/** Build the Routable contributions for the local cluster. `nodeCount`\n * drives the per-node fan-out; the plugin's service key + route role\n * identify the dispatch target. */\nexport const makeLocalRoutables = (args: {\n\treadonly app: string;\n\treadonly stack: string;\n\treadonly walrusName: string;\n\treadonly serviceKey: string;\n\treadonly nodeCount: number;\n\treadonly containerApiPort?: number;\n\treadonly aggregator?: WalrusClientService | null;\n\treadonly publisher?: WalrusClientService | null;\n}): ReadonlyArray<RoutableDecl> => {\n\tconst containerPort = args.containerApiPort ?? WALRUS_ROUTER_PORT;\n\tconst containerNameFor = (i: number): string =>\n\t\t`devstack-${args.app}-${args.stack}-walrus-${args.walrusName}-node-${i}`;\n\tconst perNodeRoutes: ReadonlyArray<RoutableDecl> = Array.from(\n\t\t{ length: args.nodeCount },\n\t\t(_, i): RoutableDecl => ({\n\t\t\tkind: 'routable',\n\t\t\tendpointName: `${WALRUS_NODE_ENDPOINT_PREFIX}${i}`,\n\t\t\tdispatchId: {\n\t\t\t\tserviceKey: args.serviceKey,\n\t\t\t\trole: `${WALRUS_NODE_ENDPOINT_PREFIX}${i}`,\n\t\t\t},\n\t\t\tupstream: {\n\t\t\t\ttype: 'container',\n\t\t\t\tcontainerName: containerNameFor(i),\n\t\t\t\tcontainerPort,\n\t\t\t},\n\t\t\t// Distilled-doc §\"Routes registered\": cors: true (walrus\n\t\t\t// storage REST API lacks CORS headers).\n\t\t\tcors: true,\n\t\t\twireProtocol: 'https',\n\t\t}),\n\t);\n\n\tconst serviceRoutes: RoutableDecl[] = [];\n\tif (args.aggregator !== undefined && args.aggregator !== null) {\n\t\tserviceRoutes.push({\n\t\t\tkind: 'routable',\n\t\t\tendpointName: WALRUS_AGGREGATOR_ENDPOINT_NAME,\n\t\t\tdispatchId: {\n\t\t\t\tserviceKey: args.serviceKey,\n\t\t\t\trole: WALRUS_AGGREGATOR_ENDPOINT_NAME,\n\t\t\t},\n\t\t\tupstream: {\n\t\t\t\ttype: 'container',\n\t\t\t\tcontainerName: args.aggregator.containerName,\n\t\t\t\tcontainerPort: args.aggregator.containerPort,\n\t\t\t},\n\t\t\tcors: true,\n\t\t\twireProtocol: 'http',\n\t\t});\n\t}\n\tif (args.publisher !== undefined && args.publisher !== null) {\n\t\tserviceRoutes.push({\n\t\t\tkind: 'routable',\n\t\t\tendpointName: WALRUS_PUBLISHER_ENDPOINT_NAME,\n\t\t\tdispatchId: {\n\t\t\t\tserviceKey: args.serviceKey,\n\t\t\t\trole: WALRUS_PUBLISHER_ENDPOINT_NAME,\n\t\t\t},\n\t\t\tupstream: {\n\t\t\t\ttype: 'container',\n\t\t\t\tcontainerName: args.publisher.containerName,\n\t\t\t\tcontainerPort: args.publisher.containerPort,\n\t\t\t},\n\t\t\tcors: true,\n\t\t\twireProtocol: 'http',\n\t\t});\n\t}\n\n\treturn [...perNodeRoutes, ...serviceRoutes];\n};\n"],"mappings":";;AA2BA,MAAa,8BAA8B;AAC3C,MAAa,kCAAkC;AAC/C,MAAa,iCAAiC;AAO9C,MAAa,qBAAoD;CAChE,GAAG,MAAM,KACR,EAAE,QAAA,EAA8B,IAC/B,GAAG,OAAuB;EAC1B,MAAM,GAAG,8BAA8B;EACvC,MAAM;EACN,UAAU;CACX,EACD;CACA;EAAE,MAAM;EAAiC,MAAM;EAAoB,UAAU;CAAO;CACpF;EAAE,MAAM;EAAgC,MAAM;EAAoB,UAAU;CAAO;AACpF;;;;AAKA,MAAa,sBAAsB,SASA;CAClC,MAAM,gBAAgB,KAAK,oBAAA;CAC3B,MAAM,oBAAoB,MACzB,YAAY,KAAK,IAAI,GAAG,KAAK,MAAM,UAAU,KAAK,WAAW,QAAQ;CACtE,MAAM,gBAA6C,MAAM,KACxD,EAAE,QAAQ,KAAK,UAAU,IACxB,GAAG,OAAqB;EACxB,MAAM;EACN,cAAc,GAAG,8BAA8B;EAC/C,YAAY;GACX,YAAY,KAAK;GACjB,MAAM,GAAG,8BAA8B;EACxC;EACA,UAAU;GACT,MAAM;GACN,eAAe,iBAAiB,CAAC;GACjC;EACD;EAGA,MAAM;EACN,cAAc;CACf,EACD;CAEA,MAAM,gBAAgC,CAAC;CACvC,IAAI,KAAK,eAAe,KAAA,KAAa,KAAK,eAAe,MACxD,cAAc,KAAK;EAClB,MAAM;EACN,cAAc;EACd,YAAY;GACX,YAAY,KAAK;GACjB,MAAM;EACP;EACA,UAAU;GACT,MAAM;GACN,eAAe,KAAK,WAAW;GAC/B,eAAe,KAAK,WAAW;EAChC;EACA,MAAM;EACN,cAAc;CACf,CAAC;CAEF,IAAI,KAAK,cAAc,KAAA,KAAa,KAAK,cAAc,MACtD,cAAc,KAAK;EAClB,MAAM;EACN,cAAc;EACd,YAAY;GACX,YAAY,KAAK;GACjB,MAAM;EACP;EACA,UAAU;GACT,MAAM;GACN,eAAe,KAAK,UAAU;GAC9B,eAAe,KAAK,UAAU;EAC/B;EACA,MAAM;EACN,cAAc;CACf,CAAC;CAGF,OAAO,CAAC,GAAG,eAAe,GAAG,aAAa;AAC3C"}
@@ -9,7 +9,7 @@ import { Effect } from "effect";
9
9
  * `nodeCount` (local mode only) drives the per-node managed-container
10
10
  * decls. Each storage node carries `role: storage-node-${i}` so the
11
11
  * substrate's label filter pauses/commits each independently. */
12
- const makeSnapshotable = (mode, app, stack, walrusName, network, nodeCount = 1) => {
12
+ const makeSnapshotable = (mode, app, stack, walrusName, network, nodeCount = 1, clientServiceRoles = []) => {
13
13
  const labels = (role) => ({
14
14
  app,
15
15
  stack,
@@ -19,10 +19,11 @@ const makeSnapshotable = (mode, app, stack, walrusName, network, nodeCount = 1)
19
19
  switch (mode) {
20
20
  case "local": {
21
21
  const perNodeContainers = Array.from({ length: nodeCount }, (_, i) => labels(`storage-node-${i}`));
22
+ const serviceContainers = clientServiceRoles.map(labels);
22
23
  return {
23
24
  kind: "snapshotable",
24
25
  subtrees: [`walrus/${walrusName}/deploy/`],
25
- managedContainers: perNodeContainers,
26
+ managedContainers: [...perNodeContainers, ...serviceContainers],
26
27
  missingTolerance: "fine",
27
28
  preRestore: Effect.succeed({
28
29
  kind: "walrus",
@@ -1 +1 @@
1
- {"version":3,"file":"snapshot.mjs","names":[],"sources":["../../../src/plugins/walrus/snapshot.ts"],"sourcesContent":["// Walrus plugin — Snapshotable contribution.\n//\n// Distilled-doc reference (06-walrus.md §\"Persistence model\" +\n// §\"Hard requirements\" item 7):\n// - `runtime/walrus/<name>/deploy/` MUST ride the snapshot tar.\n// It holds storage-node private keys + per-node configs that\n// `walrus-deploy` wrote; without them, a cached \"walrus is already\n// deployed\" artifact cannot be honored on resume.\n// - Local cluster: N storage-node containers' writable layers\n// hold RocksDB at `/var/walrus/storage` managed-containers\n// declaration; runtime adapter pauses then `docker commit`.\n// Per-node label tuple `role: storage-node-${i}` so the snapshot\n// orchestrator's label filter resolves each node distinctly.\n// - Known-deployment: no containers, no subtrees. The shape still\n// exists so the identity guard fires on restore.\n//\n// Identity guard: contributes the deploy mode + network + (when\n// local) the `name` discriminator to the pre-restore identity\n// record. A snapshot taken in local mode restored under known\n// mode (or vice versa) refuses BEFORE any destructive mutation.\n\nimport { Effect } from 'effect';\n\nimport type { ContainerLabelTuple, SnapshotableDecl } from '../../contracts/snapshotable.ts';\n\n/** Discriminator passed to `makeSnapshotable` — narrower than the\n * full mode union because the snapshot shape only cares about\n * \"local with containers\" vs \"known with nothing\". */\nexport type WalrusSnapshotMode = 'local' | 'known';\n\n/** Build the Snapshotable contribution.\n *\n * `app` / `stack` / `walrusName` are resolved at the plugin's\n * acquire-time so the snapshot's identity guard can match the\n * plugin's actual container labels.\n *\n * `nodeCount` (local mode only) drives the per-node managed-container\n * decls. Each storage node carries `role: storage-node-${i}` so the\n * substrate's label filter pauses/commits each independently. */\nexport const makeSnapshotable = (\n\tmode: WalrusSnapshotMode,\n\tapp: string,\n\tstack: string,\n\twalrusName: string,\n\tnetwork: string,\n\tnodeCount = 1,\n): SnapshotableDecl => {\n\tconst labels = (role: string): ContainerLabelTuple => ({\n\t\tapp,\n\t\tstack,\n\t\tplugin: 'walrus',\n\t\trole,\n\t});\n\n\tswitch (mode) {\n\t\tcase 'local': {\n\t\t\tconst perNodeContainers: ReadonlyArray<ContainerLabelTuple> = Array.from(\n\t\t\t\t{ length: nodeCount },\n\t\t\t\t(_, i) => labels(`storage-node-${i}`),\n\t\t\t);\n\t\t\treturn {\n\t\t\t\tkind: 'snapshotable',\n\t\t\t\t// `runtime/walrus/<name>/deploy/` — the deploy one-shot's\n\t\t\t\t// output dir. The substrate's runtime-dir root is\n\t\t\t\t// resolved at acquire; the path here is relative to it.\n\t\t\t\tsubtrees: [`walrus/${walrusName}/deploy/`],\n\t\t\t\tmanagedContainers: perNodeContainers,\n\t\t\t\tmissingTolerance: 'fine',\n\t\t\t\t// Storage nodes need >10s to flush + checkpoint RocksDB\n\t\t\t\t// on `docker stop`; the snapshot quiesce mirrors the\n\t\t\t\t// stop grace (distilled-doc invariant 22). Default\n\t\t\t\t// \"pause container\" gives us this for free in the\n\t\t\t\t// substrate's adapter.\n\t\t\t\tpreRestore: Effect.succeed({\n\t\t\t\t\tkind: 'walrus' as const,\n\t\t\t\t\tmode: 'local' as const,\n\t\t\t\t\tname: walrusName,\n\t\t\t\t\tnodeCount,\n\t\t\t\t\tnetwork,\n\t\t\t\t}),\n\t\t\t\tpostRestore: Effect.void,\n\t\t\t\t// Storage-node keystores are secret material; the\n\t\t\t\t// substrate preserves 0o600 on round-trip.\n\t\t\t\tsecretMaterial: true,\n\t\t\t};\n\t\t}\n\t\tcase 'known': {\n\t\t\treturn {\n\t\t\t\tkind: 'snapshotable',\n\t\t\t\tsubtrees: [],\n\t\t\t\tmissingTolerance: 'fine',\n\t\t\t\tpreRestore: Effect.succeed({\n\t\t\t\t\tkind: 'walrus' as const,\n\t\t\t\t\tmode: 'known' as const,\n\t\t\t\t\tnetwork,\n\t\t\t\t}),\n\t\t\t};\n\t\t}\n\t}\n};\n"],"mappings":";;;;;;;;;;;AAuCA,MAAa,oBACZ,MACA,KACA,OACA,YACA,SACA,YAAY,MACU;CACtB,MAAM,UAAU,UAAuC;EACtD;EACA;EACA,QAAQ;EACR;CACD;CAEA,QAAQ,MAAR;EACC,KAAK,SAAS;GACb,MAAM,oBAAwD,MAAM,KACnE,EAAE,QAAQ,UAAU,IACnB,GAAG,MAAM,OAAO,gBAAgB,GAAG,CACrC;GACA,OAAO;IACN,MAAM;IAIN,UAAU,CAAC,UAAU,WAAW,SAAS;IACzC,mBAAmB;IACnB,kBAAkB;IAMlB,YAAY,OAAO,QAAQ;KAC1B,MAAM;KACN,MAAM;KACN,MAAM;KACN;KACA;IACD,CAAC;IACD,aAAa,OAAO;IAGpB,gBAAgB;GACjB;EACD;EACA,KAAK,SACJ,OAAO;GACN,MAAM;GACN,UAAU,CAAC;GACX,kBAAkB;GAClB,YAAY,OAAO,QAAQ;IAC1B,MAAM;IACN,MAAM;IACN;GACD,CAAC;EACF;CAEF;AACD"}
1
+ {"version":3,"file":"snapshot.mjs","names":[],"sources":["../../../src/plugins/walrus/snapshot.ts"],"sourcesContent":["// Walrus plugin — Snapshotable contribution.\n//\n// Distilled-doc reference (06-walrus.md §\"Persistence model\" +\n// §\"Hard requirements\" item 7):\n// - `runtime/walrus/<name>/deploy/` MUST ride the snapshot tar.\n// It holds storage-node private keys + per-node configs that\n// `walrus-deploy` wrote; without them, a cached \"walrus is already\n// deployed\" artifact cannot be honored on resume.\n// - Local cluster: N storage-node containers plus enabled release\n// publisher/aggregator service containers are declared as managed\n// containers; runtime adapter pauses then `docker commit`. Per-role\n// labels keep every committed layer distinct on restore.\n// - Known-deployment: no containers, no subtrees. The shape still\n// exists so the identity guard fires on restore.\n//\n// Identity guard: contributes the deploy mode + network + (when\n// local) the `name` discriminator to the pre-restore identity\n// record. A snapshot taken in local mode restored under known\n// mode (or vice versa) refuses BEFORE any destructive mutation.\n\nimport { Effect } from 'effect';\n\nimport type { ContainerLabelTuple, SnapshotableDecl } from '../../contracts/snapshotable.ts';\n\n/** Discriminator passed to `makeSnapshotable` — narrower than the\n * full mode union because the snapshot shape only cares about\n * \"local with containers\" vs \"known with nothing\". */\nexport type WalrusSnapshotMode = 'local' | 'known';\n\n/** Build the Snapshotable contribution.\n *\n * `app` / `stack` / `walrusName` are resolved at the plugin's\n * acquire-time so the snapshot's identity guard can match the\n * plugin's actual container labels.\n *\n * `nodeCount` (local mode only) drives the per-node managed-container\n * decls. Each storage node carries `role: storage-node-${i}` so the\n * substrate's label filter pauses/commits each independently. */\nexport const makeSnapshotable = (\n\tmode: WalrusSnapshotMode,\n\tapp: string,\n\tstack: string,\n\twalrusName: string,\n\tnetwork: string,\n\tnodeCount = 1,\n\tclientServiceRoles: ReadonlyArray<'aggregator' | 'publisher'> = [],\n): SnapshotableDecl => {\n\tconst labels = (role: string): ContainerLabelTuple => ({\n\t\tapp,\n\t\tstack,\n\t\tplugin: 'walrus',\n\t\trole,\n\t});\n\n\tswitch (mode) {\n\t\tcase 'local': {\n\t\t\tconst perNodeContainers: ReadonlyArray<ContainerLabelTuple> = Array.from(\n\t\t\t\t{ length: nodeCount },\n\t\t\t\t(_, i) => labels(`storage-node-${i}`),\n\t\t\t);\n\t\t\tconst serviceContainers = clientServiceRoles.map(labels);\n\t\t\treturn {\n\t\t\t\tkind: 'snapshotable',\n\t\t\t\t// `runtime/walrus/<name>/deploy/` — the deploy one-shot's\n\t\t\t\t// output dir. The substrate's runtime-dir root is\n\t\t\t\t// resolved at acquire; the path here is relative to it.\n\t\t\t\tsubtrees: [`walrus/${walrusName}/deploy/`],\n\t\t\t\tmanagedContainers: [...perNodeContainers, ...serviceContainers],\n\t\t\t\tmissingTolerance: 'fine',\n\t\t\t\t// Storage nodes need >10s to flush + checkpoint RocksDB\n\t\t\t\t// on `docker stop`; the snapshot quiesce mirrors the\n\t\t\t\t// stop grace (distilled-doc invariant 22). Default\n\t\t\t\t// \"pause container\" gives us this for free in the\n\t\t\t\t// substrate's adapter.\n\t\t\t\tpreRestore: Effect.succeed({\n\t\t\t\t\tkind: 'walrus' as const,\n\t\t\t\t\tmode: 'local' as const,\n\t\t\t\t\tname: walrusName,\n\t\t\t\t\tnodeCount,\n\t\t\t\t\tnetwork,\n\t\t\t\t}),\n\t\t\t\tpostRestore: Effect.void,\n\t\t\t\t// Storage-node keystores are secret material; the\n\t\t\t\t// substrate preserves 0o600 on round-trip.\n\t\t\t\tsecretMaterial: true,\n\t\t\t};\n\t\t}\n\t\tcase 'known': {\n\t\t\treturn {\n\t\t\t\tkind: 'snapshotable',\n\t\t\t\tsubtrees: [],\n\t\t\t\tmissingTolerance: 'fine',\n\t\t\t\tpreRestore: Effect.succeed({\n\t\t\t\t\tkind: 'walrus' as const,\n\t\t\t\t\tmode: 'known' as const,\n\t\t\t\t\tnetwork,\n\t\t\t\t}),\n\t\t\t};\n\t\t}\n\t}\n};\n"],"mappings":";;;;;;;;;;;AAsCA,MAAa,oBACZ,MACA,KACA,OACA,YACA,SACA,YAAY,GACZ,qBAAgE,CAAC,MAC3C;CACtB,MAAM,UAAU,UAAuC;EACtD;EACA;EACA,QAAQ;EACR;CACD;CAEA,QAAQ,MAAR;EACC,KAAK,SAAS;GACb,MAAM,oBAAwD,MAAM,KACnE,EAAE,QAAQ,UAAU,IACnB,GAAG,MAAM,OAAO,gBAAgB,GAAG,CACrC;GACA,MAAM,oBAAoB,mBAAmB,IAAI,MAAM;GACvD,OAAO;IACN,MAAM;IAIN,UAAU,CAAC,UAAU,WAAW,SAAS;IACzC,mBAAmB,CAAC,GAAG,mBAAmB,GAAG,iBAAiB;IAC9D,kBAAkB;IAMlB,YAAY,OAAO,QAAQ;KAC1B,MAAM;KACN,MAAM;KACN,MAAM;KACN;KACA;IACD,CAAC;IACD,aAAa,OAAO;IAGpB,gBAAgB;GACjB;EACD;EACA,KAAK,SACJ,OAAO;GACN,MAAM;GACN,UAAU,CAAC;GACX,kBAAkB;GAClB,YAAY,OAAO,QAAQ;IAC1B,MAAM;IACN,MAAM;IACN;GACD,CAAC;EACF;CAEF;AACD"}
@@ -24,9 +24,16 @@ const computePublicHostname = (app, stack, nodeIndex) => {
24
24
  const base = `walrus-node-${nodeIndex}.${app}.localhost`;
25
25
  return stack === "main" ? base : `walrus-node-${nodeIndex}.${stack}.${app}.localhost`;
26
26
  };
27
+ /** Docker-network hostname written into the on-chain Walrus committee.
28
+ * The real Rust publisher/aggregator read these public_host values from
29
+ * chain and dial storage nodes from inside the Walrus network. Do not use
30
+ * `.localhost` here: many resolvers special-case that suffix to loopback
31
+ * inside the client-service container. */
32
+ const computeCommitteeHostname = (nodeIndex) => `dryrun-node-${nodeIndex}`;
27
33
  const storageNodeConfigHash = (parts) => [
28
34
  parts.deployConfigHash,
29
35
  `node=${parts.nodeIndex}`,
36
+ `committee-host=${parts.committeeHostname}`,
30
37
  `mount=${parts.deploySourceHostPath}->${parts.deployMountTarget}`,
31
38
  `api=${parts.containerApiPort}`,
32
39
  `net=${parts.walrusNetworkName},${parts.suiNetworkName}`
@@ -34,11 +41,11 @@ const storageNodeConfigHash = (parts) => [
34
41
  const buildWalrusNetworkName = (app, stack, walrusName) => `devstack-${app}-${stack}-walrus-${walrusName}-net`;
35
42
  /** Per-node TCP ready-probe interval. */
36
43
  const NODE_READY_PROBE_INTERVAL_MS = 500;
37
- /** Walrus storage-node REST health endpoint (TLS disabled plain HTTP
38
- * behind the router; see images/walrus/deploy-walrus.sh which rebinds
39
- * `rest_api_address` on 0.0.0.0 and sets `disable_tls: true`). The node
40
- * serves `GET /v1/health` once its API listener binds; the JSON body's
41
- * `nodeStatus` field reports the committee-sync state. */
44
+ /** Walrus storage-node REST health endpoint. Local nodes keep Walrus'
45
+ * self-signed TLS enabled because the Rust publisher/aggregator clients
46
+ * authenticate storage nodes by their network public key and always dial
47
+ * HTTPS. The router still exposes an HTTP localhost URL and proxies to
48
+ * the HTTPS upstream for browser/dev convenience. */
42
49
  const NODE_HEALTH_PATH = "/v1/health";
43
50
  /** Recovery / not-yet-serving markers in a `/v1/health` body. A node that
44
51
  * just recreated (fresh `docker run` from a committed/restored image — the
@@ -78,32 +85,20 @@ const isWriteReadyHealthBody = (body) => {
78
85
  * outer teardown — collapses shutdown wall-time from `N×grace` to
79
86
  * `~max(grace)` (distilled-doc invariant 21).
80
87
  * 3. For each `i ∈ [0..N)`:
81
- * a. `ensureContainer` on the wrapper image with pinned IP,
82
- * label tuple, primary network attach (walrus net), recreate
83
- * on config-change.
84
- * b. TCP ready probe against `<publicHostname>:<routerPort>`
85
- * with bounded retries until the outer `readyTimeoutMs`.
88
+ * a. `ensureContainer` on the wrapper image with label tuple,
89
+ * primary network attach (walrus net) plus committee DNS alias,
90
+ * recreate on config-change.
91
+ * b. In-container `/v1/health` ready probe with bounded retries
92
+ * until the outer `readyTimeoutMs`.
86
93
  * 4. Run [3] under `Effect.all({concurrency: 'unbounded'})` — boot
87
94
  * parallelism collapses to ~max(perNode).
88
95
  *
89
96
  * WHAT THE SUBSTRATE STILL OWES:
90
97
  * - Secondary network attach (`Docker.networkConnect(suiNet)`).
91
- * `EnsureContainerSpec.networkAttach` accepts an array, so passing
92
- * `[walrusNet, suiNet]` would dual-home in one shot, but the
93
- * runtime adapter currently treats the first entry as the primary
94
- * and post-attaches the rest; the per-node pinned IP only attaches
95
- * to the primary. The dual-home is documented inline below and
96
- * relies on that adapter behavior. Storage nodes do not request
97
- * faucet funds during boot; the secondary network is retained for
98
- * future Sui-side in-network calls.
99
- * - Per-node `networkAlias` (`walrus-node-<i>.localhost`). The
100
- * contract doesn't expose alias plumbing yet; peer containers dial by
101
- * container name, which docker DNS publishes. The architecture
102
- * revision is tracked in `index.ts`.
103
- * - Stop-grace duration propagation. The contract's `stop` takes a
104
- * Duration but `ensureContainer` doesn't yet accept a default
105
- * grace; the runtime adapter defaults to 10s. RocksDB needs 20s
106
- * (distilled-doc invariant 22) — see opportunity below.
98
+ * The runtime adapter treats [0] as primary and post-attaches the
99
+ * rest; the primary Walrus attach carries the committee DNS alias.
100
+ * Storage nodes do not request faucet funds during boot; the
101
+ * secondary network is retained for future Sui-side in-network calls.
107
102
  */
108
103
  const startStorageNodes = (runtime, spec) => Effect.gen(function* () {
109
104
  if (spec.nodeCount < 1) return yield* Effect.fail(walrusPluginError("storage-node", `storage-nodes: nodeCount must be >= 1 (got ${spec.nodeCount})`));
@@ -119,9 +114,8 @@ const startStorageNodes = (runtime, spec) => Effect.gen(function* () {
119
114
  });
120
115
  const indices = Array.from({ length: spec.nodeCount }, (_, i) => i);
121
116
  const bootOne = (i) => Effect.gen(function* () {
122
- const containerIp = `${spec.subnetPrefix}.${10 + i}`;
123
117
  const containerName = `devstack-${spec.app}-${spec.stack}-walrus-${spec.walrusName}-node-${i}`;
124
- const nodeHostname = `dryrun-node-${i}`;
118
+ const nodeHostname = computeCommitteeHostname(i);
125
119
  const publicHostname = computePublicHostname(spec.app, spec.stack, i);
126
120
  yield* setCurrentPluginPhase(`creating storage-node-${i} container ${containerName}`);
127
121
  const ensureNode = ensureManagedContainer({
@@ -139,6 +133,7 @@ const startStorageNodes = (runtime, spec) => Effect.gen(function* () {
139
133
  configHash: storageNodeConfigHash({
140
134
  deployConfigHash: spec.deployConfigHash,
141
135
  nodeIndex: i,
136
+ committeeHostname: nodeHostname,
142
137
  deploySourceHostPath: deployMount.sourceHostPath,
143
138
  deployMountTarget: deployMount.mountTarget,
144
139
  containerApiPort: spec.containerApiPort,
@@ -150,7 +145,10 @@ const startStorageNodes = (runtime, spec) => Effect.gen(function* () {
150
145
  DEPLOY_OUTPUT_DIR: deployMount.outputDirInContainer
151
146
  },
152
147
  stopGraceSeconds,
153
- networkAttach: [spec.walrusNetworkName, spec.suiNetworkName],
148
+ networkAttach: [{
149
+ name: spec.walrusNetworkName,
150
+ aliases: [nodeHostname]
151
+ }, spec.suiNetworkName],
154
152
  extraHosts: HOST_GATEWAY_EXTRA_HOSTS,
155
153
  mounts: [{
156
154
  source: deployMount.sourceHostPath,
@@ -158,7 +156,7 @@ const startStorageNodes = (runtime, spec) => Effect.gen(function* () {
158
156
  readonly: true
159
157
  }]
160
158
  },
161
- mapError: (cause) => walrusPluginError("storage-node", `walrus storage-node-${i} (ip=${containerIp}) ensureContainer failed: ${cause.reason}: ${cause.detail}`, { cause })
159
+ mapError: (cause) => walrusPluginError("storage-node", `walrus storage-node-${i} (host=${nodeHostname}) ensureContainer failed: ${cause.reason}: ${cause.detail}`, { cause })
162
160
  });
163
161
  const handle = yield* Scope.provide(ensureNode, nodeStopScope);
164
162
  yield* setCurrentPluginPhase(`waiting for storage-node-${i} write-readiness on 127.0.0.1:${spec.containerApiPort}`);
@@ -169,7 +167,7 @@ const startStorageNodes = (runtime, spec) => Effect.gen(function* () {
169
167
  probe: () => runtime.exec(handle, [
170
168
  "sh",
171
169
  "-c",
172
- `nc -z 127.0.0.1 ${spec.containerApiPort} || exit 1; curl -fs http://127.0.0.1:${spec.containerApiPort}${NODE_HEALTH_PATH} || exit 1`
170
+ `nc -z 127.0.0.1 ${spec.containerApiPort} || exit 1; curl -fks https://127.0.0.1:${spec.containerApiPort}${NODE_HEALTH_PATH} || exit 1`
173
171
  ]).pipe(Effect.map((result) => {
174
172
  if (result.exitCode !== 0) return exitCodeProbeResult(result);
175
173
  if (isWriteReadyHealthBody(result.stdout)) return true;
@@ -182,7 +180,7 @@ const startStorageNodes = (runtime, spec) => Effect.gen(function* () {
182
180
  };
183
181
  }))
184
182
  }).pipe(Effect.mapError((cause) => {
185
- if (cause instanceof ProbeTimeoutError) return walrusPluginError("storage-node", `storage-node-${i} never became write-ready within ${Duration.toMillis(readyTimeout)}ms (container=${containerName}, probe=127.0.0.1:${spec.containerApiPort}${NODE_HEALTH_PATH}, publicUrl=http://${publicHostname}:${WALRUS_ROUTER_PORT}). The container was created, but walrus-node did not bind its API port or did not finish re-syncing its committee/epoch to a serving nodeStatus (a node recreated from a committed/restored snapshot image must catch up before it accepts blob writes). Check the storage-node container logs for the faucet, WAL exchange, committee recovery, or walrus-node run step that stalled.`, { cause: cause.lastError ?? cause.lastNotReady ?? cause });
183
+ if (cause instanceof ProbeTimeoutError) return walrusPluginError("storage-node", `storage-node-${i} never became write-ready within ${Duration.toMillis(readyTimeout)}ms (container=${containerName}, probe=https://127.0.0.1:${spec.containerApiPort}${NODE_HEALTH_PATH}, publicUrl=http://${publicHostname}:${WALRUS_ROUTER_PORT}). The container was created, but walrus-node did not bind its API port or did not finish re-syncing its committee/epoch to a serving nodeStatus (a node recreated from a committed/restored snapshot image must catch up before it accepts blob writes). Check the storage-node container logs for the faucet, WAL exchange, committee recovery, or walrus-node run step that stalled.`, { cause: cause.lastError ?? cause.lastNotReady ?? cause });
186
184
  return walrusPluginError("storage-node", `storage-node-${i} ready-probe exec failed: ${cause.reason}: ${cause.detail}`, { cause });
187
185
  }));
188
186
  return {
@@ -195,6 +193,6 @@ const startStorageNodes = (runtime, spec) => Effect.gen(function* () {
195
193
  return { nodes: yield* Effect.all(indices.map(bootOne), { concurrency: "unbounded" }) };
196
194
  });
197
195
  //#endregion
198
- export { DEFAULT_CONTAINER_API_PORT, DEFAULT_NODE_READY_TIMEOUT_MS, WALRUS_ROUTER_PORT, buildWalrusNetworkName, computePublicHostname, startStorageNodes };
196
+ export { DEFAULT_CONTAINER_API_PORT, DEFAULT_NODE_READY_TIMEOUT_MS, WALRUS_ROUTER_PORT, buildWalrusNetworkName, computeCommitteeHostname, startStorageNodes };
199
197
 
200
198
  //# sourceMappingURL=storage-nodes.mjs.map