Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions .pnpmfile.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,26 @@
const Path = require("path")
const Fs = require("node:fs")

// Resolve relative to this file's location
const wireLibPackagesPath = Path.resolve(
__dirname,
"..",
"wire-libraries-ts",
"packages"
/**
* Sibling repo roots. They default to the platform layout — `../wire-libraries-ts`
* and `../wire-sysio` beside this repo — and a feature worktree whose siblings are
* ALSO worktrees points them at those with `WIRE_LIBRARIES_TS_PATH` and
* `WIRE_SYSIO_PATH`, so its `@wireio/*` links resolve to the sibling branches it
* is developed against instead of the shared checkouts. The `Linked …` lines
* `pnpm install` prints are the record of what resolved.
*/
const wireLibrariesPath = Path.resolve(
process.env.WIRE_LIBRARIES_TS_PATH ?? Path.resolve(__dirname, "..", "wire-libraries-ts")
)
const wireSysioPath = Path.resolve(
process.env.WIRE_SYSIO_PATH ?? Path.resolve(__dirname, "..", "wire-sysio")
)

const wireLibPackagesPath = Path.join(wireLibrariesPath, "packages")

const wireOPPPkgPaths = ["typescript"].map(target => [
`@wireio/opp-${target}-models`,
Path.resolve(__dirname, "..", "wire-sysio", "build", "opp", target)
Path.join(wireSysioPath, "build", "opp", target)
])

/**
Expand Down
28 changes: 19 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ pnpm workspaces (no nx/turbo/lerna). All packages under `packages/`:
|---------|---------|
| `cluster-tool` (`@wireio/cluster-tool`) | THE core library: orchestration engine (PhaseGroup → Phase → Step → Report), process managers, chain clients, config/bind resolution, Steps palette, flow substrate (`FlowCLI`/`FlowScenario`), CLI |
| `cluster-tool-shared` (`@wireio/cluster-tool-shared`) | Zod schema-first persisted shapes (`ClusterConfig`, `BindConfig`, `ClusterState`, `SignatureProviderConfig`, `ExternalOutpostConfig`, `ExternalClusterConfig`, `ChainTokenAmount`, `QueryEngineConfig`) behind the generic `SchemaCodec` (validate-both-ends serialize/deserialize) |
| `flow-*` (16 packages) | One scenario each — standalone executables built on `FlowCLI.create(<Name>Scenario).run()`; batch-operator lifecycle (slashing/termination), collateral, reserves, emissions soak, node-owner NFT, yield distribution, liq syndication, and the six swap variants |
| `flow-*` (17 packages) | One scenario each — standalone executables built on `FlowCLI.create(<Name>Scenario).run()`; batch-operator lifecycle (slashing/termination), collateral, reserves, emissions soak, node-owner NFT, yield distribution, liq syndication, liq yield, and the six swap variants |
| `debugging-shared` / `debugging-server` / `debugging-client-shared` / `debugging-client-tool` / `debugging-client-tool-tui` | OPP debugging surface: shared types + storage paths, ingest server, RPC client, CLI, TUI |
| `test-app-server` | Fixture app server used by debugging tests |

Expand Down Expand Up @@ -294,7 +294,9 @@ against already-deployed REMOTE ETH+SOL outposts — no local anvil/validator),
override merged over the resolved defaults; a remote anvil/solana address
requires `--external-outpost-config`), `--enable-mock-reserves` (default off —
seed the 8 mock (chain, token) PRIMARY reserves at bootstrap; a real / external
depot leaves these unseeded), `--api-count <N>` (default 0 — plan N API nodes:
depot leaves these unseeded), `--enable-mock-liq-pools` (default off — seed the
2 mock shadow-liq yield pools on `sysio.swap`, minting the pool shadow from
nothing; a real / external depot never does), `--api-count <N>` (default 0 — plan N API nodes:
non-producing nodeops in the p2p mesh, port pairs under `bind.nodeop.ports.api`,
loading `sysio::query_engine_plugin`, `trace_api_plugin` in every deployment
kind, and never `producer_api_plugin`), and
Expand Down Expand Up @@ -342,12 +344,17 @@ inline signing key (the GHA workflow is SSM-only, so published archives do not).
the SAME `applyClusterBuildOptionsArgs` surface every flow uses (env vars
`WIRE_*` seed the path flags). Exit code mirrors the bootstrap Report.

**Flow authoring — mock reserves.** A flow that reads the mock (chain, token)
PRIMARY reserves sets `enableMockReserves: true` in its `Scenario.defaults` (the
same mechanism it uses for `operatorsPerEpoch` / collateral) — never in `plan()`.
The bootstrap seeds them during epoch 0; a flow's `plan()` phases always run AFTER
`EpochBootstrap` advances epoch 0→1, and the depot gates `regreserve` to epoch 0,
so `regreserve` can never be called from a flow phase.
**Flow authoring — mock reserves and liq pools.** A flow that reads the mock
(chain, token) PRIMARY reserves sets `enableMockReserves: true` in its
`Scenario.defaults` (the same mechanism it uses for `operatorsPerEpoch` /
collateral) — never in `plan()`. The bootstrap seeds them during epoch 0; a
flow's `plan()` phases always run AFTER `EpochBootstrap` advances epoch 0→1, and
the depot gates `regreserve` to epoch 0, so `regreserve` can never be called from
a flow phase. The mock shadow-liq yield pools (`enableMockLiqPools: true`,
`sysio.liq::regliqpool`) follow the same rule for the same reason. The shadow
symbols themselves (`sysio.liq::create`, one per registered liq token), the swap's
`setconfig` and the kicker are registry setup a real depot performs too, so the
bootstrap does those unconditionally.

**Flow authoring — API nodes.** A flow that needs an API node sets `apiCount`
(and, if needed, `queryEngine`) in its `Scenario.defaults`; the nodes start in
Expand Down Expand Up @@ -391,7 +398,10 @@ the `ApiNodes` group right before `OperatorNodes`.
`.pnpmfile.cjs` hooks resolve `@wireio/*` packages from sibling repos
(`../wire-libraries-ts/packages/` → `sdk-core`/`shared`/`shared-node`;
`wire-sysio/build/opp/typescript` → `@wireio/opp-typescript-models`). They
link automatically on `pnpm install` when the siblings exist.
link automatically on `pnpm install` when the siblings exist. A feature
worktree developed against sibling WORKTREES points the hook at them —
`WIRE_LIBRARIES_TS_PATH=<libs-worktree> WIRE_SYSIO_PATH=<sysio-worktree> pnpm install`
— and the `Linked …` lines it prints are the record of what resolved.

> **Never depend on `@wireio/opp-solidity-models` here** — it is
> `wire-ethereum`-only (`opp-models-packages.md`).
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ pnpm workspace (no nx/turbo/lerna); everything lives under `packages/`.
| `flow-batch-operator-termination` | `@wireio/test-flow-batch-operator-termination` | Batch-operator termination via delivery underperformance |
| `flow-yield-distribution` | `@wireio/test-flow-yield-distribution` | `STAKING_REWARD` (Ethereum) → `sysio.dclaim::onreward` → `fundclaim` |
| `flow-liq-syndication` | `@wireio/test-flow-liq-syndication` | Real `synd` / `report_liq_yield` emit `SYNDICATE_LIQ` / `LIQ_YIELD`; depot keeps advancing |
| `flow-liq-yield` | `@wireio/test-flow-liq-yield` | Syndicated liqSOL parked → linked → credited; reported yield minted, sold through `sysio.swap`, claimed as WIRE; `DESYNDICATE_LIQ` paid on the outpost |
| `flow-emissions-soak` | `@wireio/test-flow-emissions-soak` | Multi-hour emissions + `sysio.dclaim` payout soak |
| `debugging-*` / `test-app-server` | `@wireio/debugging-*` | OPP debugging server, client tooling, TUI, shared types |

Expand Down Expand Up @@ -353,6 +354,7 @@ command comes first).
| `--terminate-window-ms` | | — | termination evaluation window in ms |
| `--bind-all` | | `false` | bind every daemon to `0.0.0.0` instead of loopback |
| `--enable-mock-reserves` | | `false` | seed the 8 mock (chain, token) PRIMARY reserves at bootstrap |
| `--enable-mock-liq-pools` | | `false` | seed the 2 mock shadow-liq yield pools (LIQETH, LIQSOL) on `sysio.swap` at bootstrap — mints the pool shadow from nothing, so a real / external depot never sets it |
| `--api-count` | | `0` | API nodes — non-producing nodeops meshed with bios + producers, serving `/v1/chain/*` and the query engine's `POST /v1/query/execute`; never `producer_api_plugin` |
| `--query-engine-read-mode` | | nodeop's own (`head`) | read mode of the API nodes' query engine (`head` or `irreversible`); renders `read-mode` only when set |
| `--query-engine-<limit>` | | plugin default | one per `query-*` limit (`worker-threads`, `max-in-flight`, `max-query-bytes`, `timeout-ms`, `max-capture-ms`, `max-abi-bytes`, `max-scan-rows`, `max-raw-bytes`, `max-memory-bytes`, `max-groups`, `max-result-rows`, `max-response-bytes`), rendered as `query-<limit>` into the API nodes' config.ini only when set; any of the thirteen requires `--api-count` ≥ 1 |
Expand Down
7 changes: 7 additions & 0 deletions packages/cluster-tool-shared/src/config/ClusterConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,13 @@ export const ClusterConfigSchema = z.object({
* unless a caller (or a flow's scenario defaults) opts in.
*/
enableMockReserves: z.boolean().default(false),
/**
* Whether the bootstrap seeds the 2 mock shadow-liq yield pools on
* `sysio.swap` (the `--enable-mock-liq-pools` create flag). Schema-defaulted
* `false` so pre-existing configs — and every real/external depot — mint no
* unbacked shadow unless a caller (or a flow's scenario defaults) opts in.
*/
enableMockLiqPools: z.boolean().default(false),
/**
* Which command produced this tree (SHARED-25 AC#4). Schema-defaulted
* {@link ClusterDeploymentKind.local} so pre-existing configs — and every
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ describe("ClusterConfig shape", () => {
externalOutposts: null,
debuggingServerEnabled: true,
enableMockReserves: false,
enableMockLiqPools: false,
deploymentKind: ClusterDeploymentKind.local,
chainStateDbSizeMb: DefaultChainStateDbSizeMb,
queryEngine: createUnsetQueryEngineConfig(),
Expand Down Expand Up @@ -121,13 +122,14 @@ describe("ClusterConfig shape", () => {
expect(rehydrated).toEqual(config)
})

it("loads a legacy config (no signatureProvider/awsClusterNodeConfig/externalOutposts/debuggingServerEnabled/enableMockReserves/deploymentKind/chainStateDbSizeMb) via schema defaults", () => {
it("loads a legacy config (no signatureProvider/awsClusterNodeConfig/externalOutposts/debuggingServerEnabled/enableMockReserves/enableMockLiqPools/deploymentKind/chainStateDbSizeMb) via schema defaults", () => {
const parsed = JSON.parse(ClusterConfigSchemaCodec.serialize(config))
delete parsed.signatureProvider
delete parsed.awsClusterNodeConfig
delete parsed.externalOutposts
delete parsed.debuggingServerEnabled
delete parsed.enableMockReserves
delete parsed.enableMockLiqPools
delete parsed.deploymentKind
delete parsed.chainStateDbSizeMb
const rehydrated = ClusterConfigSchemaCodec.deserialize(
Expand All @@ -141,6 +143,7 @@ describe("ClusterConfig shape", () => {
expect(rehydrated.externalOutposts).toBeNull()
expect(rehydrated.debuggingServerEnabled).toBe(true)
expect(rehydrated.enableMockReserves).toBe(false)
expect(rehydrated.enableMockLiqPools).toBe(false)
// A config predating either field loads as the CREATE shape: trace_api on
// every role, and nodeop's own stock chain-state DB size.
expect(rehydrated.deploymentKind).toBe(ClusterDeploymentKind.local)
Expand Down
10 changes: 8 additions & 2 deletions packages/cluster-tool/src/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ export namespace Constants {
"sysio.reserv",
"sysio.chalg",
"sysio.dclaim",
"sysio.swap",
"sysio.liq",
"sysio.gov",
"sysio.ops",
"dev.owner1"
Expand Down Expand Up @@ -320,7 +322,9 @@ export namespace Constants {
"sysio.uwrit": "contracts/sysio.uwrit",
"sysio.reserv": "contracts/sysio.reserv",
"sysio.chalg": "contracts/sysio.chalg",
"sysio.dclaim": "contracts/sysio.dclaim"
"sysio.dclaim": "contracts/sysio.dclaim",
"sysio.swap": "contracts/sysio.swap",
"sysio.liq": "contracts/sysio.liq"
} as const

export type OppContractName = keyof typeof OPP_CONTRACT_PATHS
Expand All @@ -335,7 +339,9 @@ export namespace Constants {
"sysio.uwrit",
"sysio.reserv",
"sysio.chalg",
"sysio.dclaim"
"sysio.dclaim",
"sysio.swap",
"sysio.liq"
] as const

/** Plugins loaded on a batch-operator node. */
Expand Down
4 changes: 4 additions & 0 deletions packages/cluster-tool/src/cli/ClusterBuildOptionsArgs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,10 @@ export function buildOptionShape(
false,
"seed the 8 mock (chain, token) PRIMARY reserves at bootstrap"
),
enableMockLiqPools: leaf(
false,
"seed the 2 mock shadow-liq yield pools (LIQETH, LIQSOL) at bootstrap"
),
bind: buildBindShape(defaults),
bindConfig: optionalLeaf(
OptionLeafType.string,
Expand Down
14 changes: 11 additions & 3 deletions packages/cluster-tool/src/clients/solana/utils/signatureUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ export interface ConfirmSignatureOptions {

/**
* Poll `getSignatureStatus(signature)` until the transaction is
* confirmed/finalized, it reports an error, or `deadlineMs` elapses (then
* confirmed/finalized WITHOUT an error, it reports an error (thrown at once: a
* transaction the runtime processed with an error is still included in a
* block, so it reaches those statuses too), or `deadlineMs` elapses (then
* throws). Each status RPC is bounded by `rpcTimeoutMs` via {@link withTimeout}
* so a hung validator makes the individual poll reject rather than block
* forever — the deadline budget is always honoured.
Expand Down Expand Up @@ -103,13 +105,19 @@ export async function confirmSignature(
)
pollCount++

// The error is checked FIRST: a transaction the runtime processed with an
// error is still included in a block, so its status reaches `confirmed`
// and `finalized` too, and returning on the status would report an
// on-chain failure as success (flow-liq-yield run 4: a liqSOL deposit
// refused by `EpochRewardsActive` passed as OK, and the next step found
// the ATA it never created).
if (txError)
throw new Error(`${label} tx failed: ${JSON.stringify(txError)}`)
if (
confirmationStatus === SolanaClient.ConfirmationStatus.confirmed ||
confirmationStatus === SolanaClient.ConfirmationStatus.finalized
)
return
if (txError)
throw new Error(`${label} tx failed: ${JSON.stringify(txError)}`)

if (options.rebroadcast && Date.now() - lastRebroadcast >= rebroadcastMs) {
await withTimeout(
Expand Down
16 changes: 16 additions & 0 deletions packages/cluster-tool/src/clients/wire/WireClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1000,6 +1000,22 @@ export namespace WireClient {
return JSON.stringify({ [field]: Name.from(account).value.toString() })
}

/**
* The exact `get_table_rows` range of ONE row of a KV table keyed by a single `symbol_code`
* field: the node takes `lower_bound` inclusive and `upper_bound` EXCLUSIVE, so the range is
* `[code, code + 1)`. A row whose VALUE carries no identity (`sysio.liq::yieldidx`) can only be
* read this way — a lower bound alone hands back the NEXT symbol's row when the one asked for
* does not exist. Same JSON-object encoding as {@link nameKeyBound}; a bare code string fails
* at parse time exactly like a bare account name does.
*/
export function symbolCodeKeyRange(field: string, code: string): TableQueryArgs {
const { value } = Asset.SymbolCode.from(code)
return {
lowerBound: JSON.stringify({ [field]: value.toString() }),
upperBound: JSON.stringify({ [field]: value.adding(1).toString() })
}
}

/**
* The single field a claimable-balance read consumes, shared by `sysio.reserv::wireclaims` and
* `sysio.system::payclaims` — both rows carry `balance` in atomic units, serialized as a string
Expand Down
8 changes: 8 additions & 0 deletions packages/cluster-tool/src/config/ClusterBuildOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ export interface ClusterBuildOptions {
* to the bootstrap window (epoch 0), so this only ever seeds pre-EpochBootstrap.
*/
enableMockReserves?: boolean
/**
* Seed the 2 mock shadow-liq yield pools (LIQETH, LIQSOL) on `sysio.swap` at
* bootstrap (`--enable-mock-liq-pools`). Default `false` at every layer — an
* external / real-world depot mints NO unbacked shadow unless a caller (or a
* flow's scenario `defaults`) opts in. The depot contract gates `regliqpool`
* to the bootstrap window (epoch 0), so this only ever seeds pre-EpochBootstrap.
*/
enableMockLiqPools?: boolean
// nodeop tuning
/**
* Uniform nodeop chain-state DB size in MiB for every node (SHARED-31). Omit
Expand Down
1 change: 1 addition & 0 deletions packages/cluster-tool/src/config/ClusterConfigProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ export namespace ClusterConfigProvider {
externalOutposts,
debuggingServerEnabled: true,
enableMockReserves: options.enableMockReserves ?? false,
enableMockLiqPools: options.enableMockLiqPools ?? false,
// `resolve` is the CREATE path, so the tree it describes is always local;
// `create-external-config`'s Rebind re-stamps its merged config `external`.
deploymentKind: ClusterDeploymentKind.local,
Expand Down
21 changes: 21 additions & 0 deletions packages/cluster-tool/src/flow/oppEnvelopeScan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,27 @@ export function containsLIQYield(
)
}

/**
* Whether a `DESYNDICATE_LIQ` attestation has circulated on `direction` — the
* depot → outpost proof that a holder's `sysio.liq::desyndicate` reached an
* envelope the outpost pays from (`flow-liq-yield`).
*
* @param oppDebuggingDirectory - The cluster's `data/opp-debugging/` path.
* @param direction - The cross-chain edge to scan (default: depot → Solana,
* the only outpost that redeems syndicated liq today).
* @returns Whether any matching envelope carries a DESYNDICATE_LIQ attestation.
*/
export function containsDesyndicateLIQ(
oppDebuggingDirectory: string,
direction: DebugOutpostEndpointsType = DebugOutpostEndpointsType.DEPOT_OUTPOST_SOLANA
): boolean {
return envelopeDataContains(
oppDebuggingDirectory,
direction,
attestationEntryTag(AttestationType.DESYNDICATE_LIQ)
)
}

/**
* The raw attestation payloads of every `type` attestation carried by any
* `.data` envelope artifact for `direction`.
Expand Down
Loading
Loading