diff --git a/CLAUDE.md b/CLAUDE.md index 95a55b526..451ce72f7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -369,8 +369,8 @@ the backing and custody to cover it before the epoch relay can realize yield. **Flow authoring — underwriting, syndication and the emergency stop.** The bootstrap deploys `sysio.andon`, `sysio.bond` and `sysio.synd` and configures them unconditionally: `PanicAccount` + `EmergencyStop` (after `BootstrapNodeOwner`: the -panic account `Constants.PANIC_ACCOUNT`, `sysio.andon::setpanic`, -`addpuller(sysio.synd)`) and `SyndicationConfig` (after `LiqConfig`: +panic account `Constants.PANIC_ACCOUNT`, funded native `pull`/`clear` permissions +and action links) and `SyndicationConfig` (after `ShadowLiqTokens`: `sysio.bond::setconfig` and one `sysio.synd::setconfig` per shadow pair, from `Steps.registry.SyndicationConfigRegistrations`, with no fees). A flow that needs a fee or another bucket sets its own `synd::setconfig` in `plan()` — governance, not diff --git a/README.md b/README.md index 0380b35d3..a743f581b 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,6 @@ pnpm workspace (no nx/turbo/lerna); everything lives under `packages/`. | `cluster-tool` | `@wireio/cluster-tool` | Core harness: process managers, chain clients, bootstrap, **`wire-cluster-tool` CLI** | | `flow-operator-collateral-deposit` | `@wireio/test-flow-operator-collateral-deposit` | Node-operator collateral deposit + withdraw remit | | `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 syndication reaches the destination wallet; reported yield is fully released | | `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 | @@ -157,10 +156,9 @@ and outbound queue entries are intermediate checks. | `flow-syndication-rate-limit` | Actual bond and FIFO/bucket assertions; accepted redemption must reach the external wallet | | `flow-emergency-stop` | Actual bond/challenge and deferred payouts; repaired-custody probe deposits must also reach the wallet | -The other seven enabled flows (`flow-batch-operator-slashing`, +The other six enabled flows (`flow-batch-operator-slashing`, `flow-batch-operator-termination`, `flow-emissions-soak`, `flow-node-owner-nft`, -`flow-operator-collateral-deposit`, `flow-producer-registration`, -`flow-yield-distribution`) submit no user LIQ syndication/desyndication. Imported +`flow-operator-collateral-deposit`, `flow-producer-registration`) exercise their own paths. Imported bootstrap collateral and reward funding have their own assertions. The eight former disabled swap/reserve flows have been removed. @@ -424,19 +422,10 @@ depot needs them too: |---|---| | `OPPContracts` | deploys `sysio.andon` before the contracts that read its cord (`sysio.swap`, `sysio.liq`, `sysio.bond`, `sysio.synd`), and `sysio.bond` before `sysio.synd`, all privileged through `setsyscode`; `OPPCodeGrants` then grants `@sysio.code` to `sysio.bond` and `sysio.synd` (`sysio.andon` sends no inline action) | | `PanicAccount` | creates `andon.panic`, the account that may pull and clear the cord besides `sysio` | -| `EmergencyStop` | `sysio.andon::setpanic(andon.panic)`, then `addpuller(sysio.synd)`, so a custody shortfall pulls the cord — both before any liq token is registered | -| `SyndicationConfig` | after `LiqConfig`: `sysio.bond::setconfig` (hold bond 1000 bps), one `sysio.synd::setconfig` per shadow pair (`ETHEREUM`/`LIQETH`, `SOLANA`/`LIQSOL`: no fees, buckets of one million tokens, a 60 s challenge window, a one-token challenge charge), then verifies that each shadow is at the depot's 9 decimals and each pair is an active liq token with an active binding | - -`setpanic` and `addpuller` run after the four cord readers are deployed, not -before them as `docs/contract-upgrade-order.md` orders a live upgrade: the panic -account must exist first, and the bootstrap creates it only once the bootstrap -node owner can grant its resource policy, which is after `OPPContracts`. The -order is still safe, because what the rule protects is traffic: a reader that -runs before `setpanic` reads the cord as clear, and during the bootstrap no -traffic reaches the readers: the liq tokens are registered only after -`EmergencyStop`, and no envelope is accepted until `EpochBootstrap` starts the -first epoch. By then the panic account is named and `sysio.synd` is a registered -puller. +| `EmergencyStop` | funds and delegates native `pull`/`clear` permissions to `andon.panic` and `sysio`, then links each action before any liq token is registered | +| `SyndicationConfig` | after `ShadowLiqTokens`: `sysio.bond::setconfig` (hold bond 1000 bps), one `sysio.synd::setconfig` per shadow pair (`ETHEREUM`/`LIQETH`, `SOLANA`/`LIQSOL`: no fees, buckets of one million tokens, a 60 s challenge window, a one-token challenge charge and shared 1,000,000-unit gross return minimum), then verifies that each shadow is at the depot's 9 decimals and each pair is an active liq token with an active binding | + +Native action permissions are armed after the cord readers deploy and before registry setup or epoch traffic. Privileged `sysio.synd` can pull the cord without a contract-managed puller registry. Each outpost gets its own emergency stop and per-transfer syndication maximum in local mode: diff --git a/docs/local-setup.md b/docs/local-setup.md index 9c45a69eb..76a964fad 100644 --- a/docs/local-setup.md +++ b/docs/local-setup.md @@ -319,7 +319,7 @@ Each `flow-*` package is a standalone scenario that bootstraps its own cluster. | `flow-batch-operator-slashing` | `flow-liq-syndication` | `flow-syndication-challenge` | | `flow-batch-operator-termination` | `flow-liq-yield` | `flow-syndication-rate-limit` | | `flow-emergency-stop` | `flow-node-owner-nft` | `flow-syndication-underwriting` | -| `flow-emissions-soak` | `flow-operator-collateral-deposit` | `flow-yield-distribution` | +| `flow-emissions-soak` | `flow-operator-collateral-deposit` | | | `flow-producer-registration` | | | Launch one with `scripts/run-flow.mjs` and watch it with `scripts/flow-heartbeat-monitor.mjs` (one monitor diff --git a/docs/production-bootstrap.md b/docs/production-bootstrap.md index 228845571..6dce878e9 100644 --- a/docs/production-bootstrap.md +++ b/docs/production-bootstrap.md @@ -327,7 +327,7 @@ before the first node-owner link in Stage 10 and operator `createlink` calls in | `compute_bps` | `4000` | 40% → producers + batch ops | | `capex_bps` | `2000` | 20% → `sysio.ops` | | `governance_bps` | `1000` | 10% → `sysio.gov` | -| *(implicit capital reserve)* | `3000` | `10000 − compute − capex − governance`; stays on `sysio`, drained by `fundclaim` | +| *(implicit capital reserve)* | `3000` | `10000 − compute − capex − governance`; stays on `sysio`; no automatic LIQ or DClaim capital draw | | `producer_bps` | `7000` | compute split: 70% producers | | `batch_op_bps` | `3000` | compute split: 30% batch ops | | `standby_end_rank` | `28` | producers ranked ≤28 are standby-eligible | @@ -444,7 +444,6 @@ contract/mint addresses via the same shapes. registered liq token: `{sym:"9,LIQETH", chain_code:slug("ETHEREUM"), token_code:slug("LIQETH")}` and `{sym:"9,LIQSOL", chain_code:slug("SOLANA"), token_code:slug("LIQSOL")}`. Syndicated outpost custody is minted 1:1 into these symbols. -36. `sysio.liq::setkicker({bps:200})` — `[sysio.liq@active]` — the T5 yield kicker. 37. **Underwriting + syndication rules:** - `sysio.bond::setconfig({hold_bps:1000})` — `[sysio.bond@active]` — the hold bond, in bps of a request's covered amount (the contract default, set explicitly). @@ -459,6 +458,7 @@ contract/mint addresses via the same shapes. | `window_sec` | `60` | challenge window before `sysio.bond::approve` can succeed **(cluster; production: longer)** | | `bounty` | `0` | bounty posted on each envelope's `sysio.bond` request | | `challenge_extra` | `1000000000` | one token charged to a challenger on top of the hold bond (the contract refuses `0`) | +| `min_desyndicate` | `1000000` | required gross return floor: 0.001 at nine decimals, shared by both pairs; an arbitrary launch choice based on 0.001 SOL | The tooling then verifies that every shadow symbol is at the depot frame's precision and that each `(chain, token)` pair is an active `TOKEN_KIND_LIQ` token with an active binding (otherwise `sysio.msgch` diff --git a/package.json b/package.json index 0b4e19e7f..af17a6e23 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "@types/node": "25.5.0", "@types/proper-lockfile": "^4.1.4", "@wireio/opp-typescript-models": "^1.0.51", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/shared": "^1.0.96", "@wireio/shared-node": "^1.0.96", "eslint": "^10.7.0", diff --git a/packages/cluster-tool-shared/package.json b/packages/cluster-tool-shared/package.json index 17dd57a23..0b8a8b206 100644 --- a/packages/cluster-tool-shared/package.json +++ b/packages/cluster-tool-shared/package.json @@ -34,7 +34,7 @@ "dependencies": { "@3fv/prelude-ts": "^0.8.42", "@wireio/opp-typescript-models": "^1.0.51", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/shared": "^1.0.96", "zod": "^4.4.3" }, diff --git a/packages/cluster-tool-shared/tests/config/ExternalOutpostConfig.test.ts b/packages/cluster-tool-shared/tests/config/ExternalOutpostConfig.test.ts index 3e5e8f20c..b71c058ba 100644 --- a/packages/cluster-tool-shared/tests/config/ExternalOutpostConfig.test.ts +++ b/packages/cluster-tool-shared/tests/config/ExternalOutpostConfig.test.ts @@ -13,7 +13,7 @@ describe("ExternalOutpostConfig", () => { const config: ExternalOutpostConfig = { ethereum: { addressFile: "outpost-addrs.json", - abiFiles: ["eth-abis/OPP.json", "eth-abis/OperatorRegistry.json"], + abiFiles: ["eth-abis/OPP.json", "eth-abis/OPPInbound.json"], chainId: 1 }, solana: { idlFile: "solana-idls/opp_outpost.json" } diff --git a/packages/cluster-tool/README.md b/packages/cluster-tool/README.md index a3235db47..db98c9d1e 100644 --- a/packages/cluster-tool/README.md +++ b/packages/cluster-tool/README.md @@ -384,8 +384,7 @@ A flow is a `FlowScenario` composed onto the same engine via `flow-emergency-stop`, `flow-emissions-soak`, `flow-liq-syndication`, `flow-liq-yield`, `flow-node-owner-nft`, `flow-operator-collateral-deposit`, `flow-producer-registration`, `flow-syndication-challenge`, -`flow-syndication-rate-limit`, `flow-syndication-underwriting`, -`flow-yield-distribution` — for end-to-end examples. +`flow-syndication-rate-limit`, `flow-syndication-underwriting` — for end-to-end examples. --- diff --git a/packages/cluster-tool/package.json b/packages/cluster-tool/package.json index 93e4b55c1..7b94b79e8 100644 --- a/packages/cluster-tool/package.json +++ b/packages/cluster-tool/package.json @@ -40,7 +40,7 @@ "@wireio/debugging-server": "workspace:*", "@wireio/debugging-shared": "workspace:*", "@wireio/opp-typescript-models": "^1.0.51", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/shared": "^1.0.96", "@wireio/shared-node": "^1.0.96", "archiver": "^8.0.0", diff --git a/packages/cluster-tool/src/flow/SyndicationScenario.ts b/packages/cluster-tool/src/flow/SyndicationScenario.ts index fbb6f7458..2a38ab8a9 100644 --- a/packages/cluster-tool/src/flow/SyndicationScenario.ts +++ b/packages/cluster-tool/src/flow/SyndicationScenario.ts @@ -371,6 +371,36 @@ export namespace SyndicationScenario { Assert.ok(row, "missing scenario envelope") return row } + /** + * The decoded `SyndicateLIQ` attestations the Solana outpost relayed to the depot for `user`'s + * wallet and `amount`, read from the cluster's OPP debugging artifacts. + * + * @param ctx - The build context whose cluster path holds the artifacts. + * @param user - The syndicating user, identified by their persisted wallet. + * @param amount - The syndicated amount, in base units. + * @return Every matching attestation; empty until the debugging plugin has persisted the + * envelope that carries one. + */ + export async function readDecodedSyndications( + ctx: ClusterBuildContext, + user: User, + amount: bigint + ): Promise { + const wallet = publicKey(ctx, user) + return ( + await readEnvelopeAttestations( + oppDebuggingPath(ctx.config.clusterPath), + DebugOutpostEndpointsType.OUTPOST_SOLANA_DEPOT, + AttestationType.SYNDICATE_LIQ + ) + ) + .map(bytes => SyndicateLIQ.fromBinary(bytes)) + .filter( + message => + message.amount?.amount === amount && + Buffer.from(message.user?.address).toString("hex") === wallet + ) + } /** Wait for intake, prove it is held and capture the consensus epoch and request. */ export function planVerifyIntake( actor: Report.Actor, @@ -387,6 +417,7 @@ export namespace SyndicationScenario { name, description, async ctx => { + let matching: SyndicateLIQ[] = [] await pollUntil( name, async () => { @@ -399,6 +430,10 @@ export namespace SyndicationScenario { publicKey(ctx, user) ) if (!envelope) return false + // The debugging plugin persists the relayed envelope after the depot accepts it, so + // the decoded attestation can trail the held row; wait for both. + matching = await readDecodedSyndications(ctx, user, amount) + if (matching.length === 0) return false ctx.outputs.set(epoch, envelope.epoch_index) ctx.outputs.set(request, envelope.request_id) return true @@ -406,20 +441,6 @@ export namespace SyndicationScenario { ProtocolTiming.SingleHopBudgetMs, PollMs ) - const messages = ( - await readEnvelopeAttestations( - oppDebuggingPath(ctx.config.clusterPath), - DebugOutpostEndpointsType.OUTPOST_SOLANA_DEPOT, - AttestationType.SYNDICATE_LIQ - ) - ).map(bytes => SyndicateLIQ.fromBinary(bytes)), - matching = messages.filter( - message => - message.amount?.amount === amount && - Buffer.from(message.user?.address).toString("hex") === - publicKey(ctx, user) - ) - Assert.ok(matching.length > 0, "missing decoded syndication") Assert.ok( matching.every(message => message.totalSyndicated >= amount), "custody attestation is below its own syndication" diff --git a/packages/cluster-tool/src/flow/oppEnvelopeScan.ts b/packages/cluster-tool/src/flow/oppEnvelopeScan.ts index ae04d5ca9..db9892449 100644 --- a/packages/cluster-tool/src/flow/oppEnvelopeScan.ts +++ b/packages/cluster-tool/src/flow/oppEnvelopeScan.ts @@ -14,10 +14,9 @@ import { * serialized OPP envelope bytes each side of a cross-chain edge emitted or * consumed (file naming: `--.data`). * - * Negative-assertion flows (variance revert, private-reserve gating) use these - * to prove a `SWAP_REVERT` attestation circulated without pulling the full - * proto decoder into the scenario: the attestation's type tag has a fixed, - * unambiguous byte encoding inside the envelope payload. + * Flows use these to prove an attestation of a given type circulated without + * pulling the full proto decoder into the scenario: the attestation's type tag + * has a fixed, unambiguous byte encoding inside the envelope payload. */ /** @@ -87,26 +86,6 @@ export function envelopeDataContains( ) } -/** - * Whether a `SWAP_REVERT` attestation has circulated on `direction` — the - * canonical negative-path proof for the variance-revert and private-reserve - * gating flows. - * - * @param oppDebuggingDirectory - The cluster's `data/opp-debugging/` path. - * @param direction - The cross-chain edge to scan (default: depot → Ethereum). - * @returns Whether any matching envelope carries a SWAP_REVERT attestation. - */ -export function containsSwapRevert( - oppDebuggingDirectory: string, - direction: DebugOutpostEndpointsType = DebugOutpostEndpointsType.DEPOT_OUTPOST_ETHEREUM -): boolean { - return envelopeDataContains( - oppDebuggingDirectory, - direction, - attestationEntryTag(AttestationType.SWAP_REVERT) - ) -} - /** * Whether a `SYNDICATE_LIQ` attestation has circulated on `direction` — the * outpost → depot proof that a syndication enqueued on the outpost reached an diff --git a/packages/cluster-tool/src/orchestration/ClusterBuildDefaults.ts b/packages/cluster-tool/src/orchestration/ClusterBuildDefaults.ts index e9442caeb..24fad4910 100644 --- a/packages/cluster-tool/src/orchestration/ClusterBuildDefaults.ts +++ b/packages/cluster-tool/src/orchestration/ClusterBuildDefaults.ts @@ -52,12 +52,6 @@ const ProducerSysGrant = "1000000.0000 SYS" const WireSupply = "1000000000.000000000 WIRE" /** WIRE's ABI symbol — the system token every swap pair quotes its second leg in. */ const WireSymbol = "9,WIRE" -/** - * `sysio.liq`'s T5 kicker: the share of every yield intake requested from T5 - * and folded into the same index bump, in basis points. Mirrors the contract - * default, so a cluster pays yield the way the network will. - */ -const LiqKickerBps = 200 /** * WIRE the panic account is funded with: none. It only signs `sysio.andon::pull` * and `clear`, which move no funds; its resource policy pays for them. @@ -972,7 +966,7 @@ export namespace ClusterBuildDefaults { ) // The shadow-liq system, in the order sysio.liq's README gives a deployment: // the swap's governance + system token, one shadow symbol per registered liq - // token, the T5 kicker. Registry setup a real depot performs too — unconditional. + // token. Registry setup a real depot performs too — unconditional. ClusterBuildPhase.create( prerequisites, "SwapConfig", @@ -998,19 +992,6 @@ export namespace ClusterBuildDefaults { "Open the shadow liq symbols on sysio.liq", {} ) - ClusterBuildPhase.create( - prerequisites, - "LiqConfig", - "Configure sysio.liq" - ).push( - Steps.contracts.sysio.liq.planSetkicker( - Actor.Sysio, - "configure-liq-kicker", - "set the T5 yield kicker", - {}, - { bps: LiqKickerBps } - ) - ) // Underwriting and syndication: sysio.bond's hold bond, each shadow pair's // sysio.synd rules, and the verify of the syndication preconditions. A pair with // no syndconfig row releases nothing, so a real depot configures it too — diff --git a/packages/cluster-tool/src/orchestration/steps/RegistrySteps.ts b/packages/cluster-tool/src/orchestration/steps/RegistrySteps.ts index 31588b1c1..3ba7bb0b4 100644 --- a/packages/cluster-tool/src/orchestration/steps/RegistrySteps.ts +++ b/packages/cluster-tool/src/orchestration/steps/RegistrySteps.ts @@ -118,6 +118,8 @@ export namespace RegistrySteps { * contract refuses a challenge whose charge is zero, so this is never 0. */ export const SyndicationChallengeExtra = 1_000_000_000 + /** Shared gross return floor: 0.001 SOL at nine decimals, also used for LIQETH. */ + export const MinimumDesyndication = 1_000_000 /** Durable bonder identity; flows resolve its ED/EM keys from readMockSyndicationBonder. */ export const MockSyndicationBonderLabel = "mock-syndication-bonder" @@ -474,7 +476,8 @@ export namespace RegistrySteps { desynd_refill: SyndicationBucketSize, window_sec: ProtocolTiming.SyndicationChallengeWindowSec, bounty: SyndicationBounty, - challenge_extra: SyndicationChallengeExtra + challenge_extra: SyndicationChallengeExtra, + min_desyndicate: MinimumDesyndication })) /** Seed chains + tokens + chain-token bindings. */ @@ -548,20 +551,17 @@ export namespace RegistrySteps { code: "ETHEREUM", outpost: { opp_addr: ethAddress("OPP"), - opp_inbound_addr: ethAddress("OPPInbound"), - operator_registry_addr: "", - source_deposit_addr: "" + opp_inbound_addr: ethAddress("OPPInbound") } }) - // One Solana program serves every role, so it goes in `opp_addr` alone — - // `sysio.chains` rejects an SVM row that fills the role-specific fields. + // One Solana program receives inbound and emits outbound envelopes, so it + // goes in `opp_addr` alone — `sysio.chains` rejects an SVM row that sets + // `opp_inbound_addr`. await chains.actions.setoutpost.invoke({ code: "SOLANA", outpost: { opp_addr: artifacts.solanaProgramId, - opp_inbound_addr: "", - operator_registry_addr: "", - source_deposit_addr: "" + opp_inbound_addr: "" } }) } @@ -751,9 +751,7 @@ export namespace RegistrySteps { // daemons skip a chain whose addresses are not set yet. const emptyOutpost: SysioContracts.SysioChainsOutpostAddrsType = { opp_addr: "", - opp_inbound_addr: "", - operator_registry_addr: "", - source_deposit_addr: "" + opp_inbound_addr: "" } const chainRegistrations: SysioContracts.SysioChainsRegchainAction[] = [ { diff --git a/packages/cluster-tool/src/orchestration/steps/contracts/sysio/LiqContractSteps.ts b/packages/cluster-tool/src/orchestration/steps/contracts/sysio/LiqContractSteps.ts index ee9d81c0a..701f79019 100644 --- a/packages/cluster-tool/src/orchestration/steps/contracts/sysio/LiqContractSteps.ts +++ b/packages/cluster-tool/src/orchestration/steps/contracts/sysio/LiqContractSteps.ts @@ -8,14 +8,13 @@ import { } from "../../../ClusterBuildStep.js" import type { StepInput } from "../../../StepRunner.js" -const { SysioContractName, SysioContractAccount } = SysioContracts +const { SysioContractName } = SysioContracts /** * Steps for `sysio.liq` actions — the shadow liq token. * * The contract-signed actions (`create`, `regliqpool`, `recredit`) ride the - * client's default authorization, the contract account. `setkicker` is - * governance's: the contract requires the system account. `claim` is signed by + * client's default authorization, the contract account. `claim` is signed by * the holder named in its data. Sweeping parked shadow and desyndicating are * `sysio.synd`'s actions (`Steps.contracts.sysio.synd`). */ @@ -30,7 +29,9 @@ export namespace LiqContractSteps { * `sysio.liq::create` — open one shadow symbol, bound to an active liq token * of the chain and token registries. Signed by the contract. */ - export function planCreate( + export function planCreate< + C extends ClusterBuildContext = ClusterBuildContext + >( actor: Report.Actor, name: string, description: string, @@ -59,50 +60,6 @@ export namespace LiqContractSteps { .actions.create.invoke(input.data) } - /** Input for {@link planSetkicker} — the generated `liq::setkicker` data. */ - export interface SetkickerInput extends StepInput { - readonly kind: "LiqContractSteps.SetkickerInput" - readonly data: SysioContracts.SysioLiqSetkickerAction - } - - /** - * `sysio.liq::setkicker` — set the T5 kicker folded into every yield intake, - * in basis points of the intake. Governance's action: signed by the system - * account, the authority council proposals execute as. - */ - export function planSetkicker( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - data: SysioContracts.SysioLiqSetkickerAction - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { kind: "LiqContractSteps.SetkickerInput", data }, - runSetkicker - ) - } - - /** Named runner — `sysio.liq::setkicker`, signed by the system account. */ - export async function runSetkicker( - ctx: C, - input: SetkickerInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - await ctx.wire - .getSysioContract(SysioContractName.liq) - .actions.setkicker.invoke(input.data, { - authorization: WireClient.activeAuthorization( - SysioContractAccount[SysioContractName.system] - ) - }) - } - /** Input for {@link planRegliqpool} — the generated `liq::regliqpool` data. */ export interface RegliqpoolInput extends StepInput { readonly kind: "LiqContractSteps.RegliqpoolInput" @@ -116,7 +73,9 @@ export namespace LiqContractSteps { * gates it to the epoch-0 bootstrap window, so it only ever runs * pre-EpochBootstrap. Signed by the contract. */ - export function planRegliqpool( + export function planRegliqpool< + C extends ClusterBuildContext = ClusterBuildContext + >( actor: Report.Actor, name: string, description: string, @@ -155,7 +114,9 @@ export namespace LiqContractSteps { * `sysio.liq::claim` — pay a holder every subunit of WIRE owed to their * shadow row and settle it at the current index. Signed by the holder. */ - export function planClaim( + export function planClaim< + C extends ClusterBuildContext = ClusterBuildContext + >( actor: Report.Actor, name: string, description: string, @@ -198,7 +159,9 @@ export namespace LiqContractSteps { * a release the outpost neither paid nor stored (`docs/sysio-synd.md`, the * recredit rule). Signed by the contract. */ - export function planRecredit( + export function planRecredit< + C extends ClusterBuildContext = ClusterBuildContext + >( actor: Report.Actor, name: string, description: string, diff --git a/packages/cluster-tool/src/tools/ethereum/EthereumYieldEmitterTool.ts b/packages/cluster-tool/src/tools/ethereum/EthereumYieldEmitterTool.ts deleted file mode 100644 index b3872e9bf..000000000 --- a/packages/cluster-tool/src/tools/ethereum/EthereumYieldEmitterTool.ts +++ /dev/null @@ -1,165 +0,0 @@ -/** - * EthYieldEmitterTool — wraps `MockYieldEmitter.sol` (deployed by - * wire-ethereum's `deployLocal.ts` script under `contracts/test/outpost/`) - * with an ergonomic helper for the flow-yield-distribution test. - * - * The on-chain contract is a permissioned poke-emit fake: an admin - * records synthetic per-staker positions, then triggers a single tx - * that fans STAKING_REWARD attestations onto OPP's outbound queue — - * the same queue the post-launch StakingManager will use. Once an - * attestation lands there, the batch-operator nodeop plugin picks it - * up, ferries it through OPP envelope consensus, and the depot - * dispatches it as `sysio.dclaim::onreward`. - */ - -import Assert from "node:assert" -import { ethers } from "ethers" - -import { loadOutpostContract, resolveLatestNonce } from "../../utils/ethereumUtils.js" - -/** - * Minimal `ethers` surface of `MockYieldEmitter.sol`. Typed structurally - * so the caller can pass any compatible contract instance (typechain, - * mock, hand-rolled stub). - */ -export interface MockYieldEmitterContract extends ethers.BaseContract { - setChainAndTokenCode: ( - chainCode: bigint, - tokenCode: bigint, - overrides?: ethers.Overrides - ) => Promise - stake: ( - staker: string, - amount: bigint, - overrides?: ethers.Overrides - ) => Promise - emitYield: ( - stakers: string[], - wireAccounts: string[], - rewardAmounts: bigint[], - shareBpses: number[], - externalEpochRef: bigint, - rewardEpochIndex: number, - overrides?: ethers.Overrides - ) => Promise - stakeOf: (staker: string) => Promise - totalStaked: () => Promise - lastExternalEpochRef: (staker: string) => Promise - getAddress: () => Promise -} - -/** Per-staker entry in an `emitYield` batch. */ -export interface YieldEntry { - /** ETH address (0x-prefixed) of the staker. */ - staker: string - /** - * WIRE account name the depot will credit. May be `""` for - * not-yet-AuthEx-linked stakers — the depot then parks the reward by - * `staker_native_address` (the ETH addr) until the link sweep moves - * it (`sysio.dclaim::linkswept`). - */ - wireAccount: string - /** Reward amount in native base units (wei for ETH; the contract - * passes through to the depot which converts via PrecisionLib). */ - rewardAmount: bigint - /** Informational share-in-bps for the depot's audit logging. */ - shareBps: number -} - -/** - * Load the hardhat-emitted `MockYieldEmitter.json` artifact from the - * wire-ethereum repo, look up its deployed address from the matching - * `outpost-addrs.json`, and return an ethers Contract instance bound - * to `signer`. The address file is the canonical handoff the rest of - * the harness already consumes; we read it the same way. - */ -export function loadMockYieldEmitter( - ethereumPath: string, - outpostAddrs: Record, - signer: ethers.Signer -): MockYieldEmitterContract { - return loadOutpostContract( - ethereumPath, - outpostAddrs, - "MockYieldEmitter", - ["test", "outpost"], - signer - ) -} - -/** - * Record a stake entry on the emitter. Idempotent in the sense that - * subsequent calls add to the staker's recorded balance. - * - * @param contract Emitter bound to a signer holding the AccessManager - * admin / configured role. - * @param staker ETH address whose stake is being recorded. - * @param amount Native base-unit amount to add to `staker`'s position. - */ -export async function recordStake( - contract: MockYieldEmitterContract, - staker: string, - amount: bigint -): Promise { - Assert.ok(amount > 0n, "EthYieldEmitterTool: stake amount must be positive") - const nonce = await resolveLatestNonce(contract) - const tx = await contract.stake(staker, amount, { nonce }) - const receipt = await tx.wait(1) - Assert.ok( - receipt !== null && receipt.status === 1, - `EthYieldEmitterTool: stake tx reverted (status=${receipt?.status ?? "null"})` - ) - return receipt -} - -/** - * Fan a batch of STAKING_REWARD attestations through OPP outbound. The - * batch must: - * - * * Be non-empty. - * * Use a single monotonic `externalEpochRef` across the batch — the - * contract enforces strict monotonicity per staker (`onreward` - * dedupes against this on the depot side). - * * Have at most one entry per staker (the contract's monotonic - * check would reject a same-`externalEpochRef` repeat). - * - * @param contract Emitter bound to admin signer. - * @param entries Per-staker reward triples. - * @param externalEpochRef Monotonic-per-staker reference. The depot - * rejects any inbound STAKING_REWARD whose - * `external_epoch_ref` is `<=` the staker's - * last processed value. - * @param rewardEpochIndex WIRE epoch index — informational; the - * depot logs it on the credit row. - */ -export async function emitYieldBatch( - contract: MockYieldEmitterContract, - entries: YieldEntry[], - externalEpochRef: bigint, - rewardEpochIndex: number -): Promise { - Assert.ok(entries.length > 0, "EthYieldEmitterTool: empty entries") - Assert.ok(externalEpochRef > 0n, "EthYieldEmitterTool: externalEpochRef must be positive") - - const stakers = entries.map(e => e.staker) - const wireAccounts = entries.map(e => e.wireAccount) - const rewardAmounts = entries.map(e => e.rewardAmount) - const shareBpses = entries.map(e => e.shareBps) - - const nonce = await resolveLatestNonce(contract) - const tx = await contract.emitYield( - stakers, - wireAccounts, - rewardAmounts, - shareBpses, - externalEpochRef, - rewardEpochIndex, - { nonce } - ) - const receipt = await tx.wait(1) - Assert.ok( - receipt !== null && receipt.status === 1, - `EthYieldEmitterTool: emitYield tx reverted (status=${receipt?.status ?? "null"})` - ) - return receipt -} diff --git a/packages/cluster-tool/src/tools/ethereum/index.ts b/packages/cluster-tool/src/tools/ethereum/index.ts index a99e201f1..ef43ebe07 100644 --- a/packages/cluster-tool/src/tools/ethereum/index.ts +++ b/packages/cluster-tool/src/tools/ethereum/index.ts @@ -1,3 +1,2 @@ -export * from "./EthereumYieldEmitterTool.js" export * from "./EthereumNodeOwnerNftTool.js" export * from "./EthereumSyndicationTool.js" diff --git a/packages/cluster-tool/src/tools/solana/SolanaFundingTool.ts b/packages/cluster-tool/src/tools/solana/SolanaFundingTool.ts index 54bf4ba1e..3a64d6d6b 100644 --- a/packages/cluster-tool/src/tools/solana/SolanaFundingTool.ts +++ b/packages/cluster-tool/src/tools/solana/SolanaFundingTool.ts @@ -40,10 +40,6 @@ const log = getLogger(__filename) * `@solana/spl-token`'s `createMint`, which relies on a WebSocket subscription * for confirmation the test validator doesn't reliably serve) and confirmed via * the polling {@link confirmSignature}. - * - * Recipient ATAs for swap *destinations* are NOT pre-created — the on-chain - * `handle_swap_remit` SPL branch creates them on demand with the Reserve PDA as - * rent payer. */ export namespace SolanaFundingTool { /** Minimum SPL mint decimal scale. */ diff --git a/packages/cluster-tool/src/utils/ethereumUtils.ts b/packages/cluster-tool/src/utils/ethereumUtils.ts index 2a41b4496..d31adf64d 100644 --- a/packages/cluster-tool/src/utils/ethereumUtils.ts +++ b/packages/cluster-tool/src/utils/ethereumUtils.ts @@ -37,7 +37,7 @@ export const EvmAddressPattern = /^0x[0-9a-fA-F]{40}$/ * `/artifacts/contracts/<...artifactSubpath>/.sol/ * .json`, and bind it to `runner` via {@link contractView}. The * ONE artifact-loading path every per-contract loader (`loadBar`, - * `loadMockWireNodes`, `loadMockYieldEmitter`, …) delegates to. + * `loadMockWireNodes`, …) delegates to. * * @param ethereumPath - The wire-ethereum repo root (artifact tree parent). * @param outpostAddrs - The `outpost-addrs.json` address map. diff --git a/packages/cluster-tool/tests/flow/SyndicationScenario.test.ts b/packages/cluster-tool/tests/flow/SyndicationScenario.test.ts index 45dc90532..cff361116 100644 --- a/packages/cluster-tool/tests/flow/SyndicationScenario.test.ts +++ b/packages/cluster-tool/tests/flow/SyndicationScenario.test.ts @@ -1,5 +1,15 @@ +import Fs from "node:fs" +import Os from "node:os" +import Path from "node:path" import { AuthExLinkTool } from "@wireio/cluster-tool/tools/all" import { Keypair } from "@solana/web3.js" +import { oppDebuggingPath } from "@wireio/debugging-shared" +import { + AttestationType, + ChainKind, + DebugOutpostEndpointsType, + SyndicateLIQ +} from "@wireio/opp-typescript-models" import { SysioContracts } from "@wireio/sdk-core" import { SyndicationScenario } from "@wireio/cluster-tool/flow" import { @@ -12,11 +22,22 @@ import { Report } from "@wireio/cluster-tool/report" import { SolanaFundingTool } from "@wireio/cluster-tool/tools/solana" import { WireSyndicationTool } from "@wireio/cluster-tool/tools/wire" import { fixtureContext } from "../config/clusterBuildContextFixture.js" +import { + attestationEntry, + envelopeBytes, + writeOppArtifact +} from "./oppArtifactFixture.js" const signal = new AbortController().signal, { Actor } = Report, { SysioContractName } = SysioContracts, - account = "synd.user" + account = "synd.user", + /** The syndicated amount every decoded-intake case relays, in base units. */ + intakeAmount = 2_000_000_001n, + /** The epoch the held fixture envelope reached consensus in. */ + intakeEpoch = 4, + /** The bond request the held fixture envelope carries. */ + intakeRequest = 11 /** Expose shared composition to exercise its production setup and cleanup boundaries. */ class Scenario extends SyndicationScenario { @@ -145,7 +166,8 @@ describe("syndication scenario accounting", () => { desynd_refill: 4, window_sec: 60, bounty: 5, - challenge_extra: 6 + challenge_extra: 6, + min_desyndicate: 1 } ctx.outputs.set(SyndicationScenario.OriginalConfigKey, config) await step.runner(ctx, step.input, signal) @@ -239,3 +261,162 @@ describe("shared syndication user writes", () => { expect(link).toHaveBeenCalledTimes(1) }) }) + +describe("decoded syndication intake", () => { + const user: SyndicationScenario.User = { + account, + keypairName: account, + linked: true + } + let clusterPath: string + + /** A `SyndicateLIQ` from `wallet` for `amount`, as the Solana outpost relays it. */ + function syndication( + wallet: Keypair, + amount: bigint, + totalSyndicated: bigint + ): Uint8Array { + return SyndicateLIQ.toBinary({ + chainCode: 0n, + user: { kind: ChainKind.SVM, address: wallet.publicKey.toBytes() }, + amount: { tokenCode: SyndicationScenario.TokenCode, amount }, + sequence: 1n, + totalSyndicated + }) + } + + /** Persist one Solana → depot envelope carrying `syndications`. */ + function relay(...syndications: Uint8Array[]): void { + writeOppArtifact( + oppDebuggingPath(clusterPath), + DebugOutpostEndpointsType.OUTPOST_SOLANA_DEPOT, + envelopeBytes( + syndications.map(data => + attestationEntry(AttestationType.SYNDICATE_LIQ, data) + ) + ) + ) + } + + /** The depot's held envelope row for the fixture intake. */ + function heldEnvelope(): SysioContracts.SysioSyndEnvelopeRowType { + return { + chain_code: SyndicationScenario.Chain, + token_code: SyndicationScenario.Token, + epoch_index: intakeEpoch, + digest: "", + synd_total: intakeAmount.toString(), + yield_total: 0, + item_count: 1, + state: SysioContracts.SysioSyndEnvelopeState.HELD, + request_id: intakeRequest, + released: 0, + burned: 0, + outcome: SysioContracts.SysioSyndRequestOutcome.PENDING, + forfeit: 0, + bounty_returned: 0, + hold_share: 0, + hold_beneficiary: "", + share_pending: false + } + } + + beforeEach(() => { + clusterPath = Fs.mkdtempSync(Path.join(Os.tmpdir(), "synd-intake-test-")) + }) + afterEach(() => { + Fs.rmSync(clusterPath, { recursive: true, force: true }) + }) + + it("decodes only the user's syndication of the amount", async () => { + const ctx = fixtureContext({ clusterPath }), + wallet = Keypair.generate() + jest.spyOn(SolanaFundingTool, "loadKeypair").mockReturnValue(wallet) + relay( + syndication(wallet, intakeAmount, intakeAmount), + syndication(wallet, intakeAmount + 1n, intakeAmount + 1n), + syndication(Keypair.generate(), intakeAmount, intakeAmount) + ) + const decoded = await SyndicationScenario.readDecodedSyndications( + ctx, + user, + intakeAmount + ) + expect(decoded).toHaveLength(1) + expect(decoded[0].amount.amount).toBe(intakeAmount) + expect(Buffer.from(decoded[0].user.address)).toEqual( + wallet.publicKey.toBuffer() + ) + }) + + it("decodes nothing before the relayed envelope is persisted", async () => { + const ctx = fixtureContext({ clusterPath }) + jest + .spyOn(SolanaFundingTool, "loadKeypair") + .mockReturnValue(Keypair.generate()) + relay() + await expect( + SyndicationScenario.readDecodedSyndications(ctx, user, intakeAmount) + ).resolves.toEqual([]) + }) + + it("waits for the persisted attestation after the depot holds the envelope", async () => { + const ctx = fixtureContext({ clusterPath }), + wallet = Keypair.generate(), + epoch = outputKey("test.intake.epoch", "consensus epoch"), + request = outputKey( + "test.intake.request", + "bond request" + ), + step = SyndicationScenario.planVerifyIntake( + Actor.Sysio, + "verify-intake", + "verify intake", + {}, + user, + intakeAmount, + epoch, + request + ) + jest.spyOn(SolanaFundingTool, "loadKeypair").mockReturnValue(wallet) + // The depot holds the envelope from the first poll; the debugging plugin persists the + // relayed artifact only after that first read. + const read = jest + .spyOn(WireSyndicationTool, "readHeldEnvelope") + .mockImplementationOnce(async () => heldEnvelope()) + .mockImplementation(async () => { + relay(syndication(wallet, intakeAmount, intakeAmount)) + return heldEnvelope() + }) + await step.runner(ctx, step.input, signal) + expect(read.mock.calls.length).toBeGreaterThan(1) + expect(ctx.outputs.get(epoch)).toBe(intakeEpoch) + expect(ctx.outputs.get(request)).toBe(intakeRequest) + }) + + it("refuses a custody attestation below its own syndication", async () => { + const ctx = fixtureContext({ clusterPath }), + wallet = Keypair.generate(), + step = SyndicationScenario.planVerifyIntake( + Actor.Sysio, + "verify-intake", + "verify intake", + {}, + user, + intakeAmount, + outputKey("test.intake.epoch", "consensus epoch"), + outputKey( + "test.intake.request", + "bond request" + ) + ) + jest.spyOn(SolanaFundingTool, "loadKeypair").mockReturnValue(wallet) + jest + .spyOn(WireSyndicationTool, "readHeldEnvelope") + .mockResolvedValue(heldEnvelope()) + relay(syndication(wallet, intakeAmount, intakeAmount - 1n)) + await expect(step.runner(ctx, step.input, signal)).rejects.toThrow( + "below its own syndication" + ) + }) +}) diff --git a/packages/cluster-tool/tests/flow/oppArtifactFixture.ts b/packages/cluster-tool/tests/flow/oppArtifactFixture.ts new file mode 100644 index 000000000..ddfa3c2b1 --- /dev/null +++ b/packages/cluster-tool/tests/flow/oppArtifactFixture.ts @@ -0,0 +1,79 @@ +import Fs from "node:fs" +import Path from "node:path" +import { + DebugEnvelopeMetadataRecord, + DebugOutpostEndpointsType, + Envelope, + type AttestationType, + type AttestationEntry +} from "@wireio/opp-typescript-models" + +/** The fixed checksum segment of every fixture artifact key. */ +export const FixtureArtifactChecksum = "abcdef0123456789" +/** Digits the epoch segment of an artifact key is zero-padded to. */ +export const ArtifactEpochDigits = 8 + +/** + * Write one OPP debugging artifact PAIR for `direction` carrying `payload`. + * + * Both halves, because the cluster writes both and the scanners consume different ones: the + * byte-tag scan reads `.data` alone, while `readEnvelopeAttestations` enumerates `.metadata` + * keys and reads the pair. + * + * @param directory - The OPP debugging directory; created when missing. + * @param direction - The relay direction the artifact key names. + * @param payload - The serialized envelope bytes written as `.data`. + * @param epoch - The epoch the artifact key names. + */ +export function writeOppArtifact( + directory: string, + direction: DebugOutpostEndpointsType, + payload: Buffer, + epoch = 1 +): void { + const baseKey = `${String(epoch).padStart(ArtifactEpochDigits, "0")}-${DebugOutpostEndpointsType[direction]}-${FixtureArtifactChecksum}` + Fs.mkdirSync(directory, { recursive: true }) + Fs.writeFileSync(Path.join(directory, `${baseKey}.data`), payload) + Fs.writeFileSync( + Path.join(directory, `${baseKey}.metadata`), + Buffer.from( + DebugEnvelopeMetadataRecord.toBinary({ + checksum: BigInt(payload.length), + batchOpNames: [] + }) + ) + ) +} + +/** + * One `AttestationEntry` with its `dataSize` derived from the payload. + * + * @param type - The attestation's wire discriminant. + * @param data - The serialized attestation payload. + * @return The entry an envelope message carries. + */ +export function attestationEntry( + type: AttestationType, + data: Uint8Array +): AttestationEntry { + return { type, dataSize: data.length, data } +} + +/** + * A one-message envelope carrying `attestations`, serialized as a `.data` artifact would be. + * + * @param attestations - The entries the single message carries. + * @return The envelope's wire bytes. + */ +export function envelopeBytes(attestations: AttestationEntry[]): Buffer { + return Buffer.from( + Envelope.toBinary({ + envelopeHash: new Uint8Array(), + epochTimestamp: 0n, + epochIndex: 1, + epochEnvelopeIndex: 0, + previousEnvelopeHash: new Uint8Array(), + messages: [{ header: undefined, payload: { version: 0, attestations } }] + }) + ) +} diff --git a/packages/cluster-tool/tests/flow/oppEnvelopeScan.test.ts b/packages/cluster-tool/tests/flow/oppEnvelopeScan.test.ts index 35805d3c6..4e5be09bc 100644 --- a/packages/cluster-tool/tests/flow/oppEnvelopeScan.test.ts +++ b/packages/cluster-tool/tests/flow/oppEnvelopeScan.test.ts @@ -3,25 +3,24 @@ import Os from "node:os" import Path from "node:path" import { AttestationType, - DebugEnvelopeMetadataRecord, DebugOutpostEndpointsType, - Envelope, - LIQYield, - type AttestationEntry + LIQYield } from "@wireio/opp-typescript-models" import { attestationEntryTag, containsDesyndicateLIQ, containsLIQYield, - containsSwapRevert, containsSyndicateLIQ, envelopeDataContains, readEnvelopeAttestations, varintBytes } from "@wireio/cluster-tool/flow" +import { + attestationEntry, + envelopeBytes, + writeOppArtifact +} from "./oppArtifactFixture.js" -/** The known wire encoding of `ATTESTATION_TYPE_SWAP_REVERT` (60955). */ -const SwapRevertTagBytes = [0x08, 0x9b, 0xdc, 0x03] /** The known wire encoding of `ATTESTATION_TYPE_SYNDICATE_LIQ` (60963). */ const SyndicateLIQTagBytes = [0x08, 0xa3, 0xdc, 0x03] /** The known wire encoding of `ATTESTATION_TYPE_LIQ_YIELD` (60964). */ @@ -44,32 +43,6 @@ const FixtureLIQYieldTotalSyndicated = 500_000_000n describe("oppEnvelopeScan", () => { let oppDirectory: string - /** - * Write one artifact PAIR for `direction` carrying `payload`. - * - * Both halves, because the cluster writes both and the two scanners consume - * different ones: the byte-tag scan reads `.data` alone, while - * `readEnvelopeAttestations` goes through `readEnvelopeRecordsFromDir`, which - * enumerates `.metadata` keys and reads the pair. - */ - function writeArtifact( - direction: DebugOutpostEndpointsType, - payload: Buffer, - epoch = 1 - ): void { - const baseKey = `${String(epoch).padStart(8, "0")}-${DebugOutpostEndpointsType[direction]}-abcdef0123456789` - Fs.writeFileSync(Path.join(oppDirectory, `${baseKey}.data`), payload) - Fs.writeFileSync( - Path.join(oppDirectory, `${baseKey}.metadata`), - Buffer.from( - DebugEnvelopeMetadataRecord.toBinary({ - checksum: BigInt(payload.length), - batchOpNames: [] - }) - ) - ) - } - beforeEach(() => { oppDirectory = Fs.mkdtempSync(Path.join(Os.tmpdir(), "opp-scan-test-")) }) @@ -83,19 +56,14 @@ describe("oppEnvelopeScan", () => { expect(varintBytes(0x7f)).toEqual([0x7f]) }) it("encodes multi-group values least-significant group first", () => { - expect(varintBytes(AttestationType.SWAP_REVERT)).toEqual( - SwapRevertTagBytes.slice(1) + expect(varintBytes(AttestationType.SYNDICATE_LIQ)).toEqual( + SyndicateLIQTagBytes.slice(1) ) }) }) describe("attestationEntryTag", () => { - it("prefixes the field-1 varint tag to the enum's varint", () => { - expect([...attestationEntryTag(AttestationType.SWAP_REVERT)]).toEqual( - SwapRevertTagBytes - ) - }) - it("encodes each liq-syndication type distinctly", () => { + it("prefixes the field-1 varint tag to each liq-syndication type's varint", () => { expect([...attestationEntryTag(AttestationType.SYNDICATE_LIQ)]).toEqual( SyndicateLIQTagBytes ) @@ -108,58 +76,87 @@ describe("oppEnvelopeScan", () => { }) }) - describe("envelopeDataContains / containsSwapRevert", () => { + describe("envelopeDataContains", () => { it("is false for a missing directory", () => { - expect(containsSwapRevert(Path.join(oppDirectory, "absent"))).toBe(false) + expect( + envelopeDataContains( + Path.join(oppDirectory, "absent"), + DebugOutpostEndpointsType.OUTPOST_SOLANA_DEPOT, + attestationEntryTag(AttestationType.SYNDICATE_LIQ) + ) + ).toBe(false) }) it("is false when no artifact carries the pattern", () => { - writeArtifact( - DebugOutpostEndpointsType.DEPOT_OUTPOST_ETHEREUM, + writeOppArtifact( + oppDirectory, + DebugOutpostEndpointsType.OUTPOST_SOLANA_DEPOT, Buffer.from([0x01, 0x02, 0x03]) ) - expect(containsSwapRevert(oppDirectory)).toBe(false) + expect( + envelopeDataContains( + oppDirectory, + DebugOutpostEndpointsType.OUTPOST_SOLANA_DEPOT, + attestationEntryTag(AttestationType.SYNDICATE_LIQ) + ) + ).toBe(false) }) - it("finds the SWAP_REVERT tag inside a matching-direction artifact", () => { - writeArtifact( - DebugOutpostEndpointsType.DEPOT_OUTPOST_ETHEREUM, - Buffer.from([0xff, ...SwapRevertTagBytes, 0xff]) + it("finds the tag inside a matching-direction artifact", () => { + writeOppArtifact( + oppDirectory, + DebugOutpostEndpointsType.OUTPOST_SOLANA_DEPOT, + Buffer.from([0xff, ...SyndicateLIQTagBytes, 0xff]) ) - expect(containsSwapRevert(oppDirectory)).toBe(true) + expect( + envelopeDataContains( + oppDirectory, + DebugOutpostEndpointsType.OUTPOST_SOLANA_DEPOT, + attestationEntryTag(AttestationType.SYNDICATE_LIQ) + ) + ).toBe(true) }) it("ignores artifacts from other directions", () => { - writeArtifact( + writeOppArtifact( + oppDirectory, DebugOutpostEndpointsType.DEPOT_OUTPOST_SOLANA, - Buffer.from(SwapRevertTagBytes) + Buffer.from(SyndicateLIQTagBytes) ) - expect(containsSwapRevert(oppDirectory)).toBe(false) expect( - containsSwapRevert( + envelopeDataContains( oppDirectory, - DebugOutpostEndpointsType.DEPOT_OUTPOST_SOLANA + DebugOutpostEndpointsType.OUTPOST_SOLANA_DEPOT, + attestationEntryTag(AttestationType.SYNDICATE_LIQ) + ) + ).toBe(false) + expect( + envelopeDataContains( + oppDirectory, + DebugOutpostEndpointsType.DEPOT_OUTPOST_SOLANA, + attestationEntryTag(AttestationType.SYNDICATE_LIQ) ) ).toBe(true) }) - it("scans for arbitrary attestation tags via envelopeDataContains", () => { - writeArtifact( + it("distinguishes attestation types by their tag", () => { + writeOppArtifact( + oppDirectory, DebugOutpostEndpointsType.OUTPOST_ETHEREUM_DEPOT, - Buffer.from([...attestationEntryTag(AttestationType.SWAP_REQUEST)]) + Buffer.from([...attestationEntryTag(AttestationType.LIQ_YIELD)]) ) expect( envelopeDataContains( oppDirectory, DebugOutpostEndpointsType.OUTPOST_ETHEREUM_DEPOT, - attestationEntryTag(AttestationType.SWAP_REQUEST) + attestationEntryTag(AttestationType.LIQ_YIELD) ) ).toBe(true) expect( envelopeDataContains( oppDirectory, DebugOutpostEndpointsType.OUTPOST_ETHEREUM_DEPOT, - attestationEntryTag(AttestationType.SWAP_REVERT) + attestationEntryTag(AttestationType.DESYNDICATE_LIQ) ) ).toBe(false) }) @@ -167,7 +164,8 @@ describe("oppEnvelopeScan", () => { describe("containsDesyndicateLIQ", () => { it("defaults to the depot → Solana direction the redemption travels", () => { - writeArtifact( + writeOppArtifact( + oppDirectory, DebugOutpostEndpointsType.DEPOT_OUTPOST_SOLANA, Buffer.from(DesyndicateLIQTagBytes) ) @@ -185,7 +183,8 @@ describe("oppEnvelopeScan", () => { }) it("does not match the two inbound liq types", () => { - writeArtifact( + writeOppArtifact( + oppDirectory, DebugOutpostEndpointsType.DEPOT_OUTPOST_SOLANA, Buffer.from([...SyndicateLIQTagBytes, ...LIQYieldTagBytes]) ) @@ -195,7 +194,8 @@ describe("oppEnvelopeScan", () => { describe("containsSyndicateLIQ / containsLIQYield", () => { it("default to the direction each type actually travels", () => { - writeArtifact( + writeOppArtifact( + oppDirectory, DebugOutpostEndpointsType.OUTPOST_SOLANA_DEPOT, Buffer.from([...SyndicateLIQTagBytes, ...LIQYieldTagBytes]) ) @@ -210,7 +210,8 @@ describe("oppEnvelopeScan", () => { }) it("does not confuse the three adjacent enum values", () => { - writeArtifact( + writeOppArtifact( + oppDirectory, DebugOutpostEndpointsType.OUTPOST_SOLANA_DEPOT, Buffer.from(SyndicateLIQTagBytes) ) @@ -219,7 +220,8 @@ describe("oppEnvelopeScan", () => { }) it("does not match a syndication tag on the WRONG direction", () => { - writeArtifact( + writeOppArtifact( + oppDirectory, DebugOutpostEndpointsType.OUTPOST_ETHEREUM_DEPOT, Buffer.from(SyndicateLIQTagBytes) ) @@ -233,7 +235,8 @@ describe("oppEnvelopeScan", () => { }) it("does not match a DESYNDICATE_LIQ tag as either outbound liq type", () => { - writeArtifact( + writeOppArtifact( + oppDirectory, DebugOutpostEndpointsType.OUTPOST_SOLANA_DEPOT, Buffer.from(DesyndicateLIQTagBytes) ) @@ -243,27 +246,6 @@ describe("oppEnvelopeScan", () => { }) describe("readEnvelopeAttestations", () => { - /** One `AttestationEntry` with its `dataSize` derived from the payload. */ - function entry(type: AttestationType, data: Uint8Array): AttestationEntry { - return { type, dataSize: data.length, data } - } - - /** A one-message envelope carrying `attestations`, serialized as a `.data` artifact would be. */ - function envelopeBytes(attestations: AttestationEntry[]): Buffer { - return Buffer.from( - Envelope.toBinary({ - envelopeHash: new Uint8Array(), - epochTimestamp: 0n, - epochIndex: 1, - epochEnvelopeIndex: 0, - previousEnvelopeHash: new Uint8Array(), - messages: [ - { header: undefined, payload: { version: 0, attestations } } - ] - }) - ) - } - /** The fixture `LIQYield` payload every case below round-trips. */ const liqYieldPayload = LIQYield.toBinary({ chainCode: FixtureLIQYieldChainCode, @@ -287,11 +269,12 @@ describe("oppEnvelopeScan", () => { }) it("returns the payload of every matching attestation, decodable by its message class", async () => { - writeArtifact( + writeOppArtifact( + oppDirectory, DebugOutpostEndpointsType.OUTPOST_SOLANA_DEPOT, envelopeBytes([ - entry(AttestationType.SYNDICATE_LIQ, Uint8Array.of(1, 2)), - entry(AttestationType.LIQ_YIELD, liqYieldPayload) + attestationEntry(AttestationType.SYNDICATE_LIQ, Uint8Array.of(1, 2)), + attestationEntry(AttestationType.LIQ_YIELD, liqYieldPayload) ]) ) const payloads = await readEnvelopeAttestations( @@ -308,9 +291,12 @@ describe("oppEnvelopeScan", () => { }) it("ignores other directions and other types", async () => { - writeArtifact( + writeOppArtifact( + oppDirectory, DebugOutpostEndpointsType.OUTPOST_ETHEREUM_DEPOT, - envelopeBytes([entry(AttestationType.LIQ_YIELD, liqYieldPayload)]) + envelopeBytes([ + attestationEntry(AttestationType.LIQ_YIELD, liqYieldPayload) + ]) ) await expect( readEnvelopeAttestations( @@ -329,14 +315,18 @@ describe("oppEnvelopeScan", () => { }) it("skips an artifact that does not decode rather than throwing", async () => { - writeArtifact( + writeOppArtifact( + oppDirectory, DebugOutpostEndpointsType.OUTPOST_SOLANA_DEPOT, Buffer.from([0xff, 0xff, 0xff, 0xff]), 2 ) - writeArtifact( + writeOppArtifact( + oppDirectory, DebugOutpostEndpointsType.OUTPOST_SOLANA_DEPOT, - envelopeBytes([entry(AttestationType.LIQ_YIELD, liqYieldPayload)]), + envelopeBytes([ + attestationEntry(AttestationType.LIQ_YIELD, liqYieldPayload) + ]), 3 ) await expect( diff --git a/packages/cluster-tool/tests/orchestration/ClusterBuildDefaultsMockLiqPools.test.ts b/packages/cluster-tool/tests/orchestration/ClusterBuildDefaultsMockLiqPools.test.ts index c7caad732..6c2414464 100644 --- a/packages/cluster-tool/tests/orchestration/ClusterBuildDefaultsMockLiqPools.test.ts +++ b/packages/cluster-tool/tests/orchestration/ClusterBuildDefaultsMockLiqPools.test.ts @@ -27,7 +27,7 @@ describe("ClusterBuildDefaults — mock-liq-pool gating", () => { } } - it("always configures the swap, opens the shadow symbols and sets the kicker, in that order", async () => { + it("always configures the swap, opens the shadow symbols and configures syndication, in that order", async () => { const cluster = await ClusterBuildDefaults.create(baseOptions()) const names = collectPhaseNames(cluster.children) expect(names.indexOf("SwapConfig")).toBeGreaterThan( @@ -36,11 +36,8 @@ describe("ClusterBuildDefaults — mock-liq-pool gating", () => { expect(names.indexOf("ShadowLiqTokens")).toBe( names.indexOf("SwapConfig") + 1 ) - expect(names.indexOf("LiqConfig")).toBe( - names.indexOf("ShadowLiqTokens") + 1 - ) expect(names.indexOf("SyndicationConfig")).toBeGreaterThan( - names.indexOf("LiqConfig") + names.indexOf("ShadowLiqTokens") ) expect(names).not.toContain("UnderwriterConfig") expect(names).not.toContain("ReserveConfig") @@ -60,7 +57,7 @@ describe("ClusterBuildDefaults — mock-liq-pool gating", () => { }) const names = collectPhaseNames(cluster.children) expect(names).toContain("MockLiqPools") - // gated phase follows the kicker and the per-pair syndication config, pre-EpochBootstrap + // gated phase follows the per-pair syndication config, pre-EpochBootstrap expect(names.indexOf("MockLiqPools")).toBe( names.indexOf("SyndicationConfig") + 1 ) diff --git a/packages/cluster-tool/tests/orchestration/ClusterBuildDefaultsSyndication.test.ts b/packages/cluster-tool/tests/orchestration/ClusterBuildDefaultsSyndication.test.ts index 8cd0b909a..088e2a1d6 100644 --- a/packages/cluster-tool/tests/orchestration/ClusterBuildDefaultsSyndication.test.ts +++ b/packages/cluster-tool/tests/orchestration/ClusterBuildDefaultsSyndication.test.ts @@ -58,7 +58,6 @@ describe("ClusterBuildDefaults — underwriting, syndication and the emergency s "EmergencyStop", "Registry", "ShadowLiqTokens", - "LiqConfig", "SyndicationConfig", "EpochBootstrap" ]) @@ -69,7 +68,7 @@ describe("ClusterBuildDefaults — underwriting, syndication and the emergency s names.indexOf("PanicAccount") + 1 ) expect(names.indexOf("SyndicationConfig")).toBe( - names.indexOf("LiqConfig") + 1 + names.indexOf("ShadowLiqTokens") + 1 ) }) diff --git a/packages/cluster-tool/tests/orchestration/steps/ExternalClusterConfigSteps.test.ts b/packages/cluster-tool/tests/orchestration/steps/ExternalClusterConfigSteps.test.ts index 32f62c6d9..f9f79df22 100644 --- a/packages/cluster-tool/tests/orchestration/steps/ExternalClusterConfigSteps.test.ts +++ b/packages/cluster-tool/tests/orchestration/steps/ExternalClusterConfigSteps.test.ts @@ -102,15 +102,12 @@ const ExternalEthereumRpcUrl = "https://ethereum-rpc.external.example/" const ExternalSolanaRpcUrl = "https://solana-rpc.external.example/" /** - * The deployed ETH outpost addresses the operator daemons' argv resolves - * through — every key `OperatorDaemonTool.assertAddress` demands (batch reads - * OPP + OPPInbound, underwriter reads OperatorRegistry + ReserveManager). + * The deployed ETH outpost addresses the batch-operator daemon's argv resolves + * through (OPP + OPPInbound). */ const OutpostAddresses = { OPP: "0x1111111111111111111111111111111111111111", - OPPInbound: "0x2222222222222222222222222222222222222222", - OperatorRegistry: "0x3333333333333333333333333333333333333333", - ReserveManager: "0x4444444444444444444444444444444444444444" + OPPInbound: "0x2222222222222222222222222222222222222222" } /** The one hardhat artifact the ABI generation needs (`ethereumAbiFiles.length > 0`). */ diff --git a/packages/cluster-tool/tests/orchestration/steps/ExternalOutpostSteps.test.ts b/packages/cluster-tool/tests/orchestration/steps/ExternalOutpostSteps.test.ts index 841d9332b..3184dd3b0 100644 --- a/packages/cluster-tool/tests/orchestration/steps/ExternalOutpostSteps.test.ts +++ b/packages/cluster-tool/tests/orchestration/steps/ExternalOutpostSteps.test.ts @@ -37,9 +37,7 @@ describe("Steps.externalOutpost (materialize + publish)", () => { addressFile, JSON.stringify({ OPP: OppAddress, - OPPInbound: "0x2222222222222222222222222222222222222222", - OperatorRegistry: "0x3333333333333333333333333333333333333333", - ReserveManager: "0x4444444444444444444444444444444444444444" + OPPInbound: "0x2222222222222222222222222222222222222222" }) ) Fs.writeFileSync( diff --git a/packages/cluster-tool/tests/orchestration/steps/RegistrySteps.test.ts b/packages/cluster-tool/tests/orchestration/steps/RegistrySteps.test.ts index 61273e3b8..b28f1acd0 100644 --- a/packages/cluster-tool/tests/orchestration/steps/RegistrySteps.test.ts +++ b/packages/cluster-tool/tests/orchestration/steps/RegistrySteps.test.ts @@ -143,6 +143,13 @@ describe("Steps.registry — underwriting and syndication configuration", () => }) }) + it("uses the same 0.001 SOL equivalent gross minimum for both shadow tokens", () => { + expect(Steps.registry.MinimumDesyndication).toBe(1_000_000) + rows.forEach(row => + expect(row.min_desyndicate).toBe(Steps.registry.MinimumDesyndication) + ) + }) + it("takes the challenge window from ProtocolTiming and a non-zero challenge charge", () => { rows.forEach(row => { expect(row.window_sec).toBe( diff --git a/packages/cluster-tool/tests/orchestration/steps/contracts/sysio/ChainsContractSteps.test.ts b/packages/cluster-tool/tests/orchestration/steps/contracts/sysio/ChainsContractSteps.test.ts index 2573bb91b..d7241c872 100644 --- a/packages/cluster-tool/tests/orchestration/steps/contracts/sysio/ChainsContractSteps.test.ts +++ b/packages/cluster-tool/tests/orchestration/steps/contracts/sysio/ChainsContractSteps.test.ts @@ -15,9 +15,7 @@ describe("Steps.contracts.sysio.chains", () => { // setoutpost once the daemon artifacts resolve. outpost: { opp_addr: "", - opp_inbound_addr: "", - operator_registry_addr: "", - source_deposit_addr: "" + opp_inbound_addr: "" } } const step = Steps.contracts.sysio.chains.planRegchain( diff --git a/packages/cluster-tool/tests/orchestration/steps/contracts/sysio/LiqContractSteps.test.ts b/packages/cluster-tool/tests/orchestration/steps/contracts/sysio/LiqContractSteps.test.ts index fb8d6b524..dd0b6fdc9 100644 --- a/packages/cluster-tool/tests/orchestration/steps/contracts/sysio/LiqContractSteps.test.ts +++ b/packages/cluster-tool/tests/orchestration/steps/contracts/sysio/LiqContractSteps.test.ts @@ -31,21 +31,6 @@ describe("Steps.contracts.sysio.liq", () => { expect(typeof step.runner).toBe("function") }) - it("setkicker carries the liq::setkicker data", () => { - const data: SysioContracts.SysioLiqSetkickerAction = { bps: 200 } - const step = Steps.contracts.sysio.liq.planSetkicker( - Report.Actor.Sysio, - "configure-liq-kicker", - "set the T5 yield kicker", - {}, - data - ) - expect(step.input.kind).toBe("LiqContractSteps.SetkickerInput") - expect(step.input.data).toBe(data) - expect(step.input.data.bps).toBe(200) - expect(typeof step.runner).toBe("function") - }) - it("regliqpool carries the liq::regliqpool data", () => { const data: SysioContracts.SysioLiqRegliqpoolAction = { chain_code: "SOLANA", @@ -116,7 +101,9 @@ describe("Steps.contracts.sysio.liq", () => { /** A fixture context whose `getSysioContract` hands back one shared `sysio.liq` client. */ function liqContext() { const ctx = fixtureContext(), - contract = ctx.wire.getSysioContract(SysioContracts.SysioContractName.liq) + contract = ctx.wire.getSysioContract( + SysioContracts.SysioContractName.liq + ) jest.spyOn(ctx.wire, "getSysioContract").mockReturnValue(contract) return { ctx, contract } } @@ -125,7 +112,9 @@ describe("Steps.contracts.sysio.liq", () => { it("pushes liq::recredit under the contract's own authority", async () => { const { ctx, contract } = liqContext(), - invoke = jest.spyOn(contract.actions.recredit, "invoke").mockResolvedValue(undefined) + invoke = jest + .spyOn(contract.actions.recredit, "invoke") + .mockResolvedValue(undefined) await Steps.contracts.sysio.liq.runRecredit( ctx, { kind: "LiqContractSteps.RecreditInput", data }, @@ -136,7 +125,9 @@ describe("Steps.contracts.sysio.liq", () => { it("pushes nothing once the step is aborted", async () => { const { ctx, contract } = liqContext(), - invoke = jest.spyOn(contract.actions.recredit, "invoke").mockResolvedValue(undefined), + invoke = jest + .spyOn(contract.actions.recredit, "invoke") + .mockResolvedValue(undefined), controller = new AbortController() controller.abort() await expect( diff --git a/packages/cluster-tool/tests/orchestration/steps/contracts/sysio/SyndContractSteps.test.ts b/packages/cluster-tool/tests/orchestration/steps/contracts/sysio/SyndContractSteps.test.ts index 0d77b07bf..79b34312c 100644 --- a/packages/cluster-tool/tests/orchestration/steps/contracts/sysio/SyndContractSteps.test.ts +++ b/packages/cluster-tool/tests/orchestration/steps/contracts/sysio/SyndContractSteps.test.ts @@ -40,7 +40,8 @@ describe("Steps.contracts.sysio.synd", () => { desynd_refill: 1_000_000_000_000, window_sec: 60, bounty: 0, - challenge_extra: 1_000_000_000 + challenge_extra: 1_000_000_000, + min_desyndicate: 1_000_000 } const step = Steps.contracts.sysio.synd.planSetconfig( Report.Actor.Sysio, @@ -68,7 +69,8 @@ describe("Steps.contracts.sysio.synd", () => { desynd_refill: 1_000_000_000_000, window_sec: 60, bounty: 0, - challenge_extra: 1_000_000_000 + challenge_extra: 1_000_000_000, + min_desyndicate: 1_000_000 } await Steps.contracts.sysio.synd.runSetconfig( ctx, @@ -313,7 +315,8 @@ describe("Steps.contracts.sysio.synd", () => { desynd_refill: 1_000_000_000_000, window_sec: 60, bounty: 0, - challenge_extra: 1_000_000_000 + challenge_extra: 1_000_000_000, + min_desyndicate: 1_000_000 } controller.abort() await expect( diff --git a/packages/cluster-tool/tests/orchestration/steps/processes/NodeopProcessSteps.test.ts b/packages/cluster-tool/tests/orchestration/steps/processes/NodeopProcessSteps.test.ts index 581463656..6368bb76b 100644 --- a/packages/cluster-tool/tests/orchestration/steps/processes/NodeopProcessSteps.test.ts +++ b/packages/cluster-tool/tests/orchestration/steps/processes/NodeopProcessSteps.test.ts @@ -65,9 +65,7 @@ const artifactsFixture: OperatorDaemonArtifacts = { ethereumAbiFiles: ["/cluster/data/eth-abis/OPP.json"], ethereumAddresses: { OPP: "0x1111111111111111111111111111111111111111", - OPPInbound: "0x2222222222222222222222222222222222222222", - OperatorRegistry: "0x3333333333333333333333333333333333333333", - ReserveManager: "0x4444444444444444444444444444444444444444" + OPPInbound: "0x2222222222222222222222222222222222222222" }, solanaProgramId: "GrqvbZLCLkfeSQqvE7rL8XKHVWjNhAG2faLsY8yr9tD5", solanaIdlFile: "/cluster/data/solana-idls/liqsol_core.json" diff --git a/packages/cluster-tool/tests/tools/wire/OperatorDaemonTool.test.ts b/packages/cluster-tool/tests/tools/wire/OperatorDaemonTool.test.ts index 29a40322b..650a92602 100644 --- a/packages/cluster-tool/tests/tools/wire/OperatorDaemonTool.test.ts +++ b/packages/cluster-tool/tests/tools/wire/OperatorDaemonTool.test.ts @@ -74,9 +74,7 @@ const artifacts: OperatorDaemonArtifacts = { ], ethereumAddresses: { OPP: "0x1111111111111111111111111111111111111111", - OPPInbound: "0x2222222222222222222222222222222222222222", - OperatorRegistry: "0x3333333333333333333333333333333333333333", - ReserveManager: "0x4444444444444444444444444444444444444444" + OPPInbound: "0x2222222222222222222222222222222222222222" }, solanaProgramId: "GrqvbZLCLkfeSQqvE7rL8XKHVWjNhAG2faLsY8yr9tD5", solanaIdlFile: "/cluster/data/solana-idls/liqsol_core.json" diff --git a/packages/debugging-client-shared/package.json b/packages/debugging-client-shared/package.json index 0c2473636..ffe3f7598 100644 --- a/packages/debugging-client-shared/package.json +++ b/packages/debugging-client-shared/package.json @@ -37,7 +37,7 @@ "@wireio/cluster-tool-shared": "workspace:*", "@wireio/debugging-shared": "workspace:*", "@wireio/opp-typescript-models": "^1.0.51", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/shared": "^1.0.96", "bluebird": "^3.7.2", "eventemitter3": "^5.0.4", diff --git a/packages/debugging-client-tool-tui/package.json b/packages/debugging-client-tool-tui/package.json index 11bc934dc..3607c66d1 100644 --- a/packages/debugging-client-tool-tui/package.json +++ b/packages/debugging-client-tool-tui/package.json @@ -40,7 +40,7 @@ "@wireio/debugging-client-shared": "workspace:*", "@wireio/debugging-shared": "workspace:*", "@wireio/opp-typescript-models": "^1.0.51", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/shared": "^1.0.96", "bluebird": "^3.7.2", "eventemitter3": "^5.0.4", diff --git a/packages/debugging-client-tool-tui/src/features/opp/util/AttestationCodec.ts b/packages/debugging-client-tool-tui/src/features/opp/util/AttestationCodec.ts index 4f1bf7c23..4ae040180 100644 --- a/packages/debugging-client-tool-tui/src/features/opp/util/AttestationCodec.ts +++ b/packages/debugging-client-tool-tui/src/features/opp/util/AttestationCodec.ts @@ -1,24 +1,9 @@ import { - AttestationProcessingError, AttestationType, BatchOperatorGroups, - ReserveBalanceSheet, - ChallengeOperatorHash, - ChallengeRequest, - DepositRevert, NodeOwnerRegistration, OperatorAction, Operators, - PretokenPurchase, - PretokenYield, - StakeResult, - StakeUpdate, - StakingReward, - SwapRemit, - SwapRequest, - SwapRevert, - UnderwriteIntentCommit, - WireTokenPurchase, type AttestationEntry } from "@wireio/opp-typescript-models" @@ -35,9 +20,9 @@ interface AttestationMessageType { /** * Single source of truth mapping every `AttestationType` enum value → the * `MessageType` that knows how to decode its `data` bytes. Unmapped values - * (e.g. `UNSPECIFIED`, the few `STAKE`/`UNSTAKE` / pre-launch names for - * which no companion class is exported by the generated package) fall - * through to a raw-record render in the panel — no decode is attempted. + * (e.g. `UNSPECIFIED`, or a type whose payload has no registered message + * class here) fall through to a raw-record render in the panel — no decode is + * attempted. * * Keep the keys aligned with `AttestationType`'s identifiers; renames in the * generated package surface as compile errors here. @@ -46,27 +31,11 @@ export const AttestationDecoders: Partial< Record > = { [AttestationType.OPERATOR_ACTION]: OperatorAction, - [AttestationType.PRETOKEN_PURCHASE]: PretokenPurchase, - [AttestationType.PRETOKEN_YIELD]: PretokenYield, - [AttestationType.RESERVE_BALANCE_SHEET]: ReserveBalanceSheet, - [AttestationType.STAKE_UPDATE]: StakeUpdate, - [AttestationType.WIRE_TOKEN_PURCHASE]: WireTokenPurchase, - [AttestationType.CHALLENGE_RESPONSE]: ChallengeOperatorHash, - [AttestationType.SWAP_REQUEST]: SwapRequest, - [AttestationType.SWAP_REMIT]: SwapRemit, - [AttestationType.CHALLENGE_REQUEST]: ChallengeRequest, [AttestationType.OPERATORS]: Operators, [AttestationType.BATCH_OPERATOR_GROUPS]: BatchOperatorGroups, - // The FULL registration payload (actor, keys, account, tier) — what BAR's - // commitNode emits and sysio.msgch's dispatch_node_owner_reg decodes; the - // legacy 3-field NodeOwnerReg message is no longer emitted by any outpost. - [AttestationType.NODE_OWNER_REG]: NodeOwnerRegistration, - [AttestationType.STAKING_REWARD]: StakingReward, - [AttestationType.STAKE_RESULT]: StakeResult, - [AttestationType.ATTESTATION_PROCESSING_ERROR]: AttestationProcessingError, - [AttestationType.UNDERWRITE_INTENT_COMMIT]: UnderwriteIntentCommit, - [AttestationType.SWAP_REVERT]: SwapRevert, - [AttestationType.DEPOSIT_REVERT]: DepositRevert + // The registration payload (actor, keys, account, tier) — what BAR's + // commitNode emits and sysio.msgch's dispatch_node_owner_reg decodes. + [AttestationType.NODE_OWNER_REG]: NodeOwnerRegistration } /** Successful decode — the typed message the UI pretty-prints. */ diff --git a/packages/debugging-client-tool-tui/tests/features/opp/util/AttestationCodec.test.ts b/packages/debugging-client-tool-tui/tests/features/opp/util/AttestationCodec.test.ts index e957539df..d58269349 100644 --- a/packages/debugging-client-tool-tui/tests/features/opp/util/AttestationCodec.test.ts +++ b/packages/debugging-client-tool-tui/tests/features/opp/util/AttestationCodec.test.ts @@ -79,6 +79,33 @@ describe("decodeAttestation", () => { expect(result.reason).toMatch(/no decoder/) }) + it.each([ + 60950, 60958, 3001, 3002, 3004, 43520, 60928, 60932, 60934, 60945, 60951, + 60952, 60955, 60962 + ])( + "renders removed wire type %s as raw without blocking active decoding", + type => { + const entry: AttestationEntry = { + type: type as AttestationType, + dataSize: 1, + data: new Uint8Array([0x0a]) + } + expect(decodeAttestation(entry)).toEqual({ + kind: "raw", + reason: `no decoder registered for AttestationType=${type}`, + entry + }) + const bytes = encodeBatchOperatorGroups() + expect( + decodeAttestation({ + type: AttestationType.BATCH_OPERATOR_GROUPS, + dataSize: bytes.length, + data: bytes + }).kind + ).toBe("decoded") + } + ) + it("falls back to `raw` when decode throws on bogus bytes", () => { const entry: AttestationEntry = { type: AttestationType.BATCH_OPERATOR_GROUPS, diff --git a/packages/debugging-client-tool/package.json b/packages/debugging-client-tool/package.json index b19695d80..431aabe63 100644 --- a/packages/debugging-client-tool/package.json +++ b/packages/debugging-client-tool/package.json @@ -36,7 +36,7 @@ "@wireio/debugging-client-shared": "workspace:*", "@wireio/opp-typescript-models": "^1.0.51", "@wireio/shared": "^1.0.96", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "get-port": "^7.2.0", "lodash": "^4.17.21", "ts-pattern": "^5.9.0", diff --git a/packages/debugging-server/package.json b/packages/debugging-server/package.json index 90c8263a0..a741ce3bc 100644 --- a/packages/debugging-server/package.json +++ b/packages/debugging-server/package.json @@ -38,7 +38,7 @@ "@wireio/cluster-tool-shared": "workspace:*", "@wireio/debugging-shared": "workspace:*", "@wireio/opp-typescript-models": "^1.0.51", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/shared": "^1.0.96", "bluebird": "^3.7.2", "express": "^5.2.1", diff --git a/packages/flow-batch-operator-slashing/package.json b/packages/flow-batch-operator-slashing/package.json index 43e529816..b3a533357 100644 --- a/packages/flow-batch-operator-slashing/package.json +++ b/packages/flow-batch-operator-slashing/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "@wireio/cluster-tool": "workspace:*", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/opp-typescript-models": "^1.0.51", "@wireio/shared": "^1.0.96", "ethers": "^6.14.0" diff --git a/packages/flow-batch-operator-termination/package.json b/packages/flow-batch-operator-termination/package.json index 66276f3c7..970855f2f 100644 --- a/packages/flow-batch-operator-termination/package.json +++ b/packages/flow-batch-operator-termination/package.json @@ -13,7 +13,7 @@ "@coral-xyz/anchor": "^0.31.0", "@solana/web3.js": "^1.98.0", "@wireio/cluster-tool": "workspace:*", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/opp-typescript-models": "^1.0.51", "@wireio/shared": "^1.0.96" }, diff --git a/packages/flow-emergency-stop/package.json b/packages/flow-emergency-stop/package.json index 2a92ffd7f..bc41566a5 100644 --- a/packages/flow-emergency-stop/package.json +++ b/packages/flow-emergency-stop/package.json @@ -13,7 +13,7 @@ "dependencies": { "@wireio/cluster-tool": "workspace:*", "@wireio/debugging-shared": "workspace:*", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/opp-typescript-models": "^1.0.51", "source-map-support": "^0.5.21", "@coral-xyz/anchor": "^0.31.0", diff --git a/packages/flow-emissions-soak/README.md b/packages/flow-emissions-soak/README.md index b823af063..6582e134a 100644 --- a/packages/flow-emissions-soak/README.md +++ b/packages/flow-emissions-soak/README.md @@ -20,17 +20,6 @@ end-to-end. ## What it does NOT verify -- **`sysio.system::fundclaim` cap semantics from PR 354.** That code path - fires only on `sysio.dclaim::onreward`, which is driven by - STAKING_REWARD attestations arriving from the outposts. As of writing, - wire-ethereum `StakingManager.sol` is a rename-only placeholder - (`revert()` bodies); outpost reward emission is on a separate - developer track. A companion exhaust flow will be added in a follow-up - PR once that emission track lands. - - `capital_shortfall_total` is asserted to stay at `0` in this soak — - trivially true today because no `fundclaim` calls occur. - - **Pre-funding of `sysio.dclaim`.** At production launch the dclaim account is credited with the pre-launch capital allocation up-front. This soak replicates that with an in-test `sysio.token::transfer` @@ -105,7 +94,7 @@ SOAK_DURATION_MS=$((30 * 60 * 1000)) node scripts/run-flow.mjs flow-emissions-so **Scaffold only.** The bootstrap wiring (`ClusterManager.ts` Phase 15b/c) is in place; the seed + sample-claim test bodies are stubbed as `it.todo` pending the regen of `@wireio/sdk-core` types against the -post-PR-354 wire-sysio ABIs (`sysio.system::setemitcfg`, `fundclaim`, +post-PR-354 wire-sysio ABIs (`sysio.system::setemitcfg`, `sysio.dclaim::*`). The `convertImportSeed` helper has full unit-test coverage in this file diff --git a/packages/flow-emissions-soak/package.json b/packages/flow-emissions-soak/package.json index 1fb4fcdc2..8d67d9e94 100644 --- a/packages/flow-emissions-soak/package.json +++ b/packages/flow-emissions-soak/package.json @@ -12,7 +12,7 @@ "dependencies": { "@solana/web3.js": "^1.98.0", "@wireio/cluster-tool": "workspace:*", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/opp-typescript-models": "^1.0.51", "@wireio/shared": "^1.0.96", "ethers": "^6.14.0" diff --git a/packages/flow-emissions-soak/src/EmissionsSoakScenario.ts b/packages/flow-emissions-soak/src/EmissionsSoakScenario.ts index 1f5f34d10..24503b819 100644 --- a/packages/flow-emissions-soak/src/EmissionsSoakScenario.ts +++ b/packages/flow-emissions-soak/src/EmissionsSoakScenario.ts @@ -84,13 +84,7 @@ async function readCapConfig( * holds their ETH wallets) complete AuthEx linking, whose inline * `linkswept` sweeps `unmapped_tokens` into `pending_claims` → `claim` * pays each staker its exact seeded WIRE. dclaim is pre-funded from - * `sysio` for the synthetic load (the importseed path never calls - * `fundclaim`; only the onreward path does). - * - * **Out of scope:** `sysio.system::fundclaim` cap semantics — that path fires - * only on `sysio.dclaim::onreward` (STAKING_REWARD attestations), pending the - * outpost emission track. `capital_shortfall_total` is asserted `== 0` - * throughout (trivially true today because no `fundclaim` calls occur). + * `sysio` for the synthetic load; imports and claims use only that funding. * * Phases: `ConfigureEmissions` → `GenerateAndImport` → `SetupClaimers` → * `StabilityLoop` → `Claim`. @@ -193,18 +187,13 @@ export class EmissionsSoakScenario extends FlowScenario { verifyStep( Actor.Sysio, "t5-state-initialized", - "t5state exists with non-negative distribution and zero capital shortfall", + "t5state exists with non-negative distribution", async ctx => { const t5 = await readT5State(ctx) Assert.ok( Number(t5.total_distributed) >= 0, "total_distributed must be non-negative" ) - Assert.strictEqual( - Number(t5.capital_shortfall_total), - 0, - "capital_shortfall_total must start at 0" - ) } ) ) @@ -372,18 +361,18 @@ export class EmissionsSoakScenario extends FlowScenario { ) // ── 4. StabilityLoop — sample t5state across the soak window; monotonic - // accrual, zero shortfall, headroom respected. ── + // accrual, headroom respected. ── ClusterBuildPhase.create( cluster, "StabilityLoop", - `Sample t5state for ${Constants.SoakDurationMs}ms; monotonic accrual + zero shortfall`, + `Sample t5state for ${Constants.SoakDurationMs}ms; monotonic accrual within headroom`, [], soakOptions ).push( verifyStep( Actor.Sysio, "soak-monotonic-accrual", - "total_distributed advances monotonically within headroom; capital_shortfall_total stays 0", + "total_distributed advances monotonically within headroom", async (ctx, signal) => { const emissionConfig = await readEmissionConfig(ctx), headroom = @@ -399,20 +388,12 @@ export class EmissionsSoakScenario extends FlowScenario { await sleep(Constants.SampleIntervalMs) const t5 = await readT5State(ctx), distributed = BigInt(t5.total_distributed), - shortfall = BigInt(t5.capital_shortfall_total), elapsedSec = Math.round((Date.now() - startWallMs) / 1000) - log.info( - `[soak] +${elapsedSec}s distributed=${distributed} shortfall=${shortfall}` - ) + log.info(`[soak] +${elapsedSec}s distributed=${distributed}`) Assert.ok( distributed >= lastDistributed, `total_distributed regressed: ${distributed} < ${lastDistributed}` ) - Assert.strictEqual( - shortfall, - 0n, - "unexpected capital shortfall during the soak" - ) lastDistributed = distributed sampleCount += 1 } @@ -434,7 +415,7 @@ export class EmissionsSoakScenario extends FlowScenario { ) // ── 5. Claim — snapshot balances, claim per staker, verify EXACT deltas - // and a still-zero capital shortfall. ── + // and treasury distribution within headroom. ── ClusterBuildPhase.create( cluster, "Claim", @@ -492,19 +473,6 @@ export class EmissionsSoakScenario extends FlowScenario { }) ) } - ), - verifyStep( - Actor.Sysio, - "final-shortfall-zero", - "capital_shortfall_total is still 0 after every claim (no fundclaim calls on this path)", - async ctx => { - const t5 = await readT5State(ctx) - Assert.strictEqual( - BigInt(t5.capital_shortfall_total), - 0n, - "capital_shortfall_total moved without an onreward-driven claim" - ) - } ) ) } diff --git a/packages/flow-emissions-soak/src/EmissionsSoakScenarioConstants.ts b/packages/flow-emissions-soak/src/EmissionsSoakScenarioConstants.ts index d8c2dfba8..4a54cb243 100644 --- a/packages/flow-emissions-soak/src/EmissionsSoakScenarioConstants.ts +++ b/packages/flow-emissions-soak/src/EmissionsSoakScenarioConstants.ts @@ -77,8 +77,7 @@ export namespace EmissionsSoakScenarioConstants { ControlledStakerSourceUnits / WireAtomicDivisor /** * WIRE atomic pre-funded from `sysio` to `sysio.dclaim` to cover every - * controlled-staker claim (the importseed path never calls `fundclaim`; only - * the onreward path does — a real launch pre-funds dclaim the same way). + * controlled-staker claim; a real launch pre-funds DClaim the same way. */ export const ClaimPreFundAtomic = PerStakerClaimAtomic * BigInt(ControlledStakerCount) diff --git a/packages/flow-liq-syndication/package.json b/packages/flow-liq-syndication/package.json index e49e1957b..c20865cd7 100644 --- a/packages/flow-liq-syndication/package.json +++ b/packages/flow-liq-syndication/package.json @@ -13,7 +13,7 @@ "dependencies": { "@wireio/cluster-tool": "workspace:*", "@wireio/debugging-shared": "workspace:*", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/opp-typescript-models": "^1.0.51" }, "devDependencies": { diff --git a/packages/flow-liq-yield/package.json b/packages/flow-liq-yield/package.json index 381af15d8..b717fca0b 100644 --- a/packages/flow-liq-yield/package.json +++ b/packages/flow-liq-yield/package.json @@ -12,7 +12,7 @@ "dependencies": { "@wireio/cluster-tool": "workspace:*", "@wireio/debugging-shared": "workspace:*", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/opp-typescript-models": "^1.0.51" }, "devDependencies": { diff --git a/packages/flow-node-owner-nft/package.json b/packages/flow-node-owner-nft/package.json index 59c95771c..6756d7c7d 100644 --- a/packages/flow-node-owner-nft/package.json +++ b/packages/flow-node-owner-nft/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "@wireio/cluster-tool": "workspace:*", - "@wireio/sdk-core": "^1.0.96" + "@wireio/sdk-core": "^1.0.97" }, "devDependencies": { "@types/node": "25.5.0" diff --git a/packages/flow-operator-collateral-deposit/package.json b/packages/flow-operator-collateral-deposit/package.json index e4530332e..89c57d879 100644 --- a/packages/flow-operator-collateral-deposit/package.json +++ b/packages/flow-operator-collateral-deposit/package.json @@ -13,7 +13,7 @@ "@coral-xyz/anchor": "^0.31.0", "@solana/web3.js": "^1.98.0", "@wireio/cluster-tool": "workspace:*", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/opp-typescript-models": "^1.0.51", "@wireio/shared": "^1.0.96", "@wireio/shared-node": "^1.0.96", diff --git a/packages/flow-producer-registration/package.json b/packages/flow-producer-registration/package.json index 404ab1826..cc3ae5d35 100644 --- a/packages/flow-producer-registration/package.json +++ b/packages/flow-producer-registration/package.json @@ -14,7 +14,7 @@ "@coral-xyz/anchor": "^0.31.0", "@solana/web3.js": "^1.98.0", "@wireio/cluster-tool": "workspace:*", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/opp-typescript-models": "^1.0.51", "@wireio/shared": "^1.0.96", "@wireio/shared-node": "^1.0.96", diff --git a/packages/flow-syndication-challenge/package.json b/packages/flow-syndication-challenge/package.json index bab0e3624..0c9b8eca6 100644 --- a/packages/flow-syndication-challenge/package.json +++ b/packages/flow-syndication-challenge/package.json @@ -13,7 +13,7 @@ "dependencies": { "@wireio/cluster-tool": "workspace:*", "@wireio/debugging-shared": "workspace:*", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/opp-typescript-models": "^1.0.51", "source-map-support": "^0.5.21", "@wireio/shared": "^1.0.96" diff --git a/packages/flow-syndication-challenge/src/SyndicationChallengeScenarioConstants.ts b/packages/flow-syndication-challenge/src/SyndicationChallengeScenarioConstants.ts index 1b8a3bd35..e4192d78c 100644 --- a/packages/flow-syndication-challenge/src/SyndicationChallengeScenarioConstants.ts +++ b/packages/flow-syndication-challenge/src/SyndicationChallengeScenarioConstants.ts @@ -3,6 +3,7 @@ import { SysioContracts } from "@wireio/sdk-core" import { outputKey, ProtocolTiming, + Steps, SyndicationScenario } from "@wireio/cluster-tool" @@ -46,7 +47,8 @@ export namespace SyndicationChallengeScenarioConstants { desynd_refill: 0, window_sec: ProtocolTiming.SyndicationChallengeWindowSec, bounty: String(Bounty), - challenge_extra: String(Extra) + challenge_extra: String(Extra), + min_desyndicate: Steps.registry.MinimumDesyndication } /** Fee-pot seed envelope. */ export const SeedEpoch = outputKey( diff --git a/packages/flow-syndication-rate-limit/package.json b/packages/flow-syndication-rate-limit/package.json index 69c538258..b7e1bf77f 100644 --- a/packages/flow-syndication-rate-limit/package.json +++ b/packages/flow-syndication-rate-limit/package.json @@ -13,7 +13,7 @@ "dependencies": { "@wireio/cluster-tool": "workspace:*", "@wireio/debugging-shared": "workspace:*", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/opp-typescript-models": "^1.0.51", "source-map-support": "^0.5.21" }, diff --git a/packages/flow-syndication-rate-limit/src/SyndicationRateLimitScenarioConstants.ts b/packages/flow-syndication-rate-limit/src/SyndicationRateLimitScenarioConstants.ts index 758c7d4fc..27a6ebb15 100644 --- a/packages/flow-syndication-rate-limit/src/SyndicationRateLimitScenarioConstants.ts +++ b/packages/flow-syndication-rate-limit/src/SyndicationRateLimitScenarioConstants.ts @@ -3,6 +3,7 @@ import { SysioContracts } from "@wireio/sdk-core" import { outputKey, ProtocolTiming, + Steps, SyndicationScenario } from "@wireio/cluster-tool" @@ -47,7 +48,8 @@ export namespace SyndicationRateLimitScenarioConstants { desynd_refill: 0, window_sec: ProtocolTiming.SyndicationChallengeWindowSec, bounty: 0, - challenge_extra: "1000000000" + challenge_extra: "1000000000", + min_desyndicate: Steps.registry.MinimumDesyndication } /** A's envelope epoch. */ export const Epoch = outputKey("rate.epoch", "burst envelope epoch") diff --git a/packages/flow-syndication-underwriting/package.json b/packages/flow-syndication-underwriting/package.json index f3e6f8adf..d3d920fc9 100644 --- a/packages/flow-syndication-underwriting/package.json +++ b/packages/flow-syndication-underwriting/package.json @@ -13,7 +13,7 @@ "dependencies": { "@wireio/cluster-tool": "workspace:*", "@wireio/debugging-shared": "workspace:*", - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/opp-typescript-models": "^1.0.51", "source-map-support": "^0.5.21" }, diff --git a/packages/flow-syndication-underwriting/src/SyndicationUnderwritingScenarioConstants.ts b/packages/flow-syndication-underwriting/src/SyndicationUnderwritingScenarioConstants.ts index ace464623..0daccb007 100644 --- a/packages/flow-syndication-underwriting/src/SyndicationUnderwritingScenarioConstants.ts +++ b/packages/flow-syndication-underwriting/src/SyndicationUnderwritingScenarioConstants.ts @@ -3,6 +3,7 @@ import { SysioContracts } from "@wireio/sdk-core" import { outputKey, ProtocolTiming, + Steps, SyndicationScenario } from "@wireio/cluster-tool" @@ -38,7 +39,8 @@ export namespace SyndicationUnderwritingScenarioConstants { desynd_refill: "100000000000", window_sec: ProtocolTiming.SyndicationChallengeWindowSec, bounty: 0, - challenge_extra: "1000000000" + challenge_extra: "1000000000", + min_desyndicate: Steps.registry.MinimumDesyndication } /** Initial ledger and custody balances. */ export const Before = outputKey( diff --git a/packages/flow-yield-distribution/README.md b/packages/flow-yield-distribution/README.md deleted file mode 100644 index b74baae96..000000000 --- a/packages/flow-yield-distribution/README.md +++ /dev/null @@ -1,60 +0,0 @@ -# flow-yield-distribution - -Validates the cross-chain yield-distribution path end-to-end against a -fresh cluster: - -``` -MockYieldEmitter.sol::emitYield (ETH outpost — Solidity fake) - → STAKING_REWARD attestation - → batch operator ferries via OPP envelope - → sysio.msgch dispatches as - sysio.dclaim::onreward - → sysio.system::fundclaim - → sysio.token::transfer (sysio → sysio.dclaim) - → credit into sysio.dclaim::pclaims (if AuthEx-linked) - OR sysio.dclaim::unmapped_tokens (if not yet linked) -``` - -Asserts: - -1. **Reward arrives in pclaims/unmapped.** After a yield emission, a - pclaim row appears for the linked staker, OR an unmapped row for the - unlinked staker. -2. **Fundclaim caps fire correctly.** `sysio` balance decreases by - `min(emission, accounting_available, balance_available)` — never - over-spending. `t5_state.capital_shortfall_total` accrues when the - accounting bucket can't cover the credit. -3. **Dedupe.** Re-emitting the same `external_epoch_ref` is a no-op on - the depot (the dclaim reward-cursor row guards against replay). - -## Ethereum only, for now - -The flow drives the ETH outpost alone. `MockYieldEmitter.sol` stands in -for a real emitter because `StakingManager.sol` is a rename-only -placeholder with no STAKING_REWARD path. - -There is no Solana leg: the SOL staking surface is a separate developer -track and `liqsol_core` emits no STAKING_REWARD, so a SOL leg could only -be a harness-side attestation injection — which the harness no longer -does. It returns when the outpost produces the attestation itself. - -## Running - -Like every flow, run it with the canonical runner + heartbeat monitor pair -(see the repo README's "Running flows" / "Monitoring a live flow run" and -`wire-platform-manifest/.claude/rules/run-flows-via-canonical-scripts.md`): - -```bash -# From the wire-tools-ts root: -node scripts/run-flow.mjs flow-yield-distribution \ - --cluster-path /tmp/wire-flow-yield \ - --wire-build-path $HOME/code/wire/wire-sysio/build \ - --ethereum-path $HOME/code/wire/wire-ethereum \ - --solana-path $HOME/code/wire/wire-solana - -# In a second terminal — the mandatory six-probe heartbeat: -node scripts/flow-heartbeat-monitor.mjs --cluster-path /tmp/wire-flow-yield -``` - -Cluster data lands under `--cluster-path` (env `WIRE_CLUSTER_PATH`); omit it -and the harness generates a fresh temp dir per run. diff --git a/packages/flow-yield-distribution/package.json b/packages/flow-yield-distribution/package.json deleted file mode 100644 index d6d52619f..000000000 --- a/packages/flow-yield-distribution/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "@wireio/test-flow-yield-distribution", - "version": "0.1.8", - "private": true, - "type": "commonjs", - "description": "Flow: fake-yield STAKING_REWARD → sysio.dclaim::onreward → sysio.system::fundclaim", - "scripts": { - "build": "tsc -b tsconfig.json", - "test": "node lib/index.js", - "clean": "rm -rf lib" - }, - "dependencies": { - "@wireio/cluster-tool": "workspace:*", - "@wireio/sdk-core": "^1.0.96", - "@wireio/opp-typescript-models": "^1.0.51", - "ethers": "^6.14.0" - }, - "devDependencies": { - "@types/node": "25.5.0" - } -} diff --git a/packages/flow-yield-distribution/src/YieldDistributionScenario.ts b/packages/flow-yield-distribution/src/YieldDistributionScenario.ts deleted file mode 100644 index d7422a894..000000000 --- a/packages/flow-yield-distribution/src/YieldDistributionScenario.ts +++ /dev/null @@ -1,440 +0,0 @@ -import Assert from "node:assert" -import { ethers } from "ethers" -import { ChainKind } from "@wireio/opp-typescript-models" -import { SysioContracts } from "@wireio/sdk-core" -import { - AuthExLinkTool, - ClusterBuildPhase, - ClusterBuildStep, - FlowScenario, - Report, - ethereumPrivateKeyFromWallet, - outputKey, - pollUntil, - provisionWireUser, - sleep, - verifyStep, - type ClusterBuild, - type ClusterBuildContext, - type ClusterBuildOptions, - type StepInput -} from "@wireio/cluster-tool" -import { YieldDistributionScenarioConstants as Constants } from "./YieldDistributionScenarioConstants.js" -import { YieldDistributionScenarioEmitSteps as EmitSteps } from "./steps/index.js" - -const { SysioContractName } = SysioContracts -const { Actor } = Report - -// ── reads (execute freely inside verify steps) ────────────────────────────── - -/** All `sysio.dclaim::pclaims` rows (a read). */ -async function readPclaimsRows( - ctx: ClusterBuildContext -): Promise { - const { rows } = await ctx.wire - .getSysioContract(SysioContractName.dclaim) - .tables.pclaims.query({ limit: Constants.TableQueryLimit }) - return rows -} - -/** How many `pclaims` rows credit `wireAccount` (a read — the dedupe metric). */ -async function readPclaimsCount( - ctx: ClusterBuildContext, - wireAccount: string -): Promise { - return (await readPclaimsRows(ctx)).filter( - row => row.wire_account === wireAccount - ).length -} - -/** All `sysio.dclaim::unmapped` rows (a read). */ -async function readUnmappedRows( - ctx: ClusterBuildContext -): Promise { - const { rows } = await ctx.wire - .getSysioContract(SysioContractName.dclaim) - .tables.unmapped.query({ limit: Constants.TableQueryLimit }) - return rows -} - -/** The `sysio::t5state` emissions-accounting singleton (a read). */ -async function readT5State( - ctx: ClusterBuildContext -): Promise { - const { rows } = await ctx.wire - .getSysioContract(SysioContractName.system) - .tables.t5state.query({ limit: 1 }) - Assert.ok( - rows.length >= 1, - "sysio::t5state has no row — was initt5 run at bootstrap?" - ) - return rows[0] -} - -/** - * `capital_shortfall_total` as a number (a read). Large int64 columns can - * arrive as JSON strings, so the conversion is runtime normalization, not - * ceremony — the expected value here is 0 either way. - */ -async function readCapitalShortfallTotal( - ctx: ClusterBuildContext -): Promise { - return Number((await readT5State(ctx)).capital_shortfall_total) -} - -/** Lowercased, `0x`-stripped hex of an ETH address — dclaim's `unmapped.native_pubkey` spelling. */ -function ethereumNativePubkey(address: string): string { - return address.toLowerCase().replace(/^0x/, "") -} - -// ── SetupStakers step inputs + named runners ──────────────────────────────── - -/** Input for the linked-staker provisioning step. */ -interface ProvisionLinkedStakerInput extends StepInput { - readonly kind: "YieldDistributionScenario.ProvisionLinkedStakerInput" - /** WIRE account name to create + resource-policy. */ - readonly account: string -} - -/** - * Named runner — mint the linked staker's ETH wallet into `ctx.outputs`, then - * provision its WIRE account (creation under the dev key + the standard - * resource policy, via the shared harness helper). - */ -async function runProvisionLinkedStaker( - ctx: ClusterBuildContext, - input: ProvisionLinkedStakerInput, - signal: AbortSignal -): Promise { - signal.throwIfAborted() - ctx.outputs.set( - YieldDistributionScenario.LinkedStakerWalletKey, - ethers.Wallet.createRandom() - ) - await provisionWireUser(ctx.wire, input.account) -} - -/** Input for the authex-link step. */ -interface AuthexLinkLinkedStakerInput extends StepInput { - readonly kind: "YieldDistributionScenario.AuthexLinkLinkedStakerInput" - /** WIRE account being linked to its ETH wallet. */ - readonly account: string -} - -/** Named runner — ONE `sysio.authex::createlink` write binding the staker's ETH wallet to its WIRE account. */ -async function runAuthexLinkLinkedStaker( - ctx: ClusterBuildContext, - input: AuthexLinkLinkedStakerInput, - signal: AbortSignal -): Promise { - signal.throwIfAborted() - const wallet = ctx.outputs.assert( - YieldDistributionScenario.LinkedStakerWalletKey - ) - await AuthExLinkTool.createLink(ctx.wire, { - chainKind: ChainKind.EVM, - account: input.account, - privateKey: ethereumPrivateKeyFromWallet(wallet), - ethereumWallet: wallet - }) -} - -/** - * Named runner — mint the UNLINKED staker's ETH wallet into `ctx.outputs`. - * Deliberately NO WIRE account and NO authex link: the depot parking its - * reward in `unmapped` IS the scenario under test. - */ -async function runProvisionUnlinkedStaker( - ctx: ClusterBuildContext, - _input: null, - signal: AbortSignal -): Promise { - signal.throwIfAborted() - ctx.outputs.set( - YieldDistributionScenario.UnlinkedStakerWalletKey, - ethers.Wallet.createRandom() - ) -} - -/** - * Yield distribution — synthetic STAKING_REWARD attestations driven through - * the ETHEREUM outpost into the depot's reward-distribution path - * (`sysio.dclaim::onreward` → `sysio.system::fundclaim` → `sysio.token`), - * porting the old jest suite's four tests 1:1: - * - * 1. **SetupStakers** — the AuthEx-LINKED staker (WIRE account + `createlink`) - * and the UNLINKED staker (a bare ETH wallet), one provisioning step each. - * 2. **EmitEthereumLinked** — `MockYieldEmitter.emitYield` → batchop ferry → - * `pclaims` row for the linked account; `capital_shortfall_total` stays - * unchanged (emissions cover the credit). - * 3. **EmitEthereumUnlinked** — the same emit with an empty WIRE account → - * the depot parks the credit in `unmapped` keyed by the ETH address. - * 4. **DedupReplay** — snapshot the linked `pclaims` count, re-emit the SAME - * `external_epoch_ref` (the emitter's monotonic check reverts), and after a - * settle window the count is unchanged. - */ -export class YieldDistributionScenario extends FlowScenario { - readonly name = "flow-yield-distribution" - readonly description = - "Fake STAKING_REWARD emissions on the Ethereum outpost: pclaims credit (linked), unmapped park (unlinked), and external_epoch_ref replay dedupe" - - override readonly defaults: ClusterBuildOptions = { - epochDurationSec: Constants.EpochDurationSec, - producerCount: Constants.ProducerCount, - batchOperatorCount: Constants.BatchOperatorCount, - underwriterCount: Constants.UnderwriterCount - } - - plan(cluster: ClusterBuild): void { - const emitStepOptions = { timeoutMs: Constants.EmitStepTimeoutMs }, - propagationStepOptions = { - timeoutMs: - Constants.PropagationTimeoutMs + Constants.PollDeadlineBufferMs - }, - dedupeStepOptions = { - timeoutMs: Constants.dedupeSettleMs() + Constants.PollDeadlineBufferMs - } - - // ── 1. SetupStakers — linked (account + authex link) vs unlinked (bare wallet) ── - ClusterBuildPhase.create( - cluster, - "SetupStakers", - "Provision the AuthEx-linked staker (WIRE account + createlink) and the unlinked staker" - ).push( - ClusterBuildStep.create( - Actor.User, - "provision-linked-staker", - `provision WIRE account ${Constants.LinkedStakerAccount} + mint its ETH wallet`, - {}, - { - kind: "YieldDistributionScenario.ProvisionLinkedStakerInput", - account: Constants.LinkedStakerAccount - }, - runProvisionLinkedStaker - ), - ClusterBuildStep.create( - Actor.User, - "authex-link-linked-staker", - `sysio.authex::createlink binds ${Constants.LinkedStakerAccount} to its ETH wallet`, - {}, - { - kind: "YieldDistributionScenario.AuthexLinkLinkedStakerInput", - account: Constants.LinkedStakerAccount - }, - runAuthexLinkLinkedStaker - ), - ClusterBuildStep.create( - Actor.User, - "provision-unlinked-staker", - "mint the unlinked staker's ETH wallet (no WIRE account, no authex link)", - {}, - null, - runProvisionUnlinkedStaker - ) - ) - - // ── 2. EmitEthereumLinked — emitYield → pclaims row; shortfall unchanged ── - ClusterBuildPhase.create( - cluster, - "EmitEthereumLinked", - "MockYieldEmitter emits a linked-staker STAKING_REWARD; the depot credits pclaims" - ).push( - verifyStep( - Actor.Sysio, - "snapshot-capital-shortfall", - "record t5state.capital_shortfall_total before the emission", - async ctx => { - ctx.outputs.set( - YieldDistributionScenario.CapitalShortfallBeforeKey, - await readCapitalShortfallTotal(ctx) - ) - } - ), - EmitSteps.planEthereumEmit( - Actor.EthereumOutpost, - "emit-ethereum-linked", - `emit ${Constants.EthereumRewardPerStaker} wei STAKING_REWARD for ${Constants.LinkedStakerAccount}`, - emitStepOptions, - YieldDistributionScenario.LinkedStakerWalletKey, - Constants.LinkedStakerAccount, - Constants.EthereumRewardPerStaker, - Constants.FullShareBps, - Constants.LinkedStakerExternalEpochRef, - Constants.RewardEpochIndex - ), - verifyStep( - Actor.Sysio, - "pclaims-row-linked", - `a NEW pclaims row appears for ${Constants.LinkedStakerAccount} (new account — any row is new)`, - async ctx => { - await pollUntil( - `pclaims row for ${Constants.LinkedStakerAccount} (ETH)`, - async () => - (await readPclaimsCount(ctx, Constants.LinkedStakerAccount)) >= 1, - Constants.PropagationTimeoutMs, - Constants.PropagationPollMs - ) - }, - propagationStepOptions - ), - verifyStep( - Actor.Sysio, - "capital-shortfall-unchanged", - "t5state.capital_shortfall_total is unchanged (emissions cover the credit)", - async ctx => { - const before = ctx.outputs.assert( - YieldDistributionScenario.CapitalShortfallBeforeKey - ) - Assert.strictEqual( - await readCapitalShortfallTotal(ctx), - before, - "capital_shortfall_total moved — emissions did not cover the credit" - ) - } - ) - ) - - // ── 3. EmitEthereumUnlinked — emitYield("") → unmapped park ── - ClusterBuildPhase.create( - cluster, - "EmitEthereumUnlinked", - "MockYieldEmitter emits an unlinked-staker STAKING_REWARD; the depot parks it in unmapped" - ).push( - verifyStep( - Actor.Sysio, - "snapshot-unmapped-ethereum", - "record the unmapped row count before the emission", - async ctx => { - ctx.outputs.set( - YieldDistributionScenario.EthereumUnmappedCountBeforeKey, - (await readUnmappedRows(ctx)).length - ) - } - ), - EmitSteps.planEthereumEmit( - Actor.EthereumOutpost, - "emit-ethereum-unlinked", - `emit ${Constants.EthereumRewardPerStaker} wei STAKING_REWARD for the unlinked staker`, - emitStepOptions, - YieldDistributionScenario.UnlinkedStakerWalletKey, - Constants.UnlinkedWireAccount, - Constants.EthereumRewardPerStaker, - Constants.FullShareBps, - Constants.UnlinkedStakerExternalEpochRef, - Constants.RewardEpochIndex - ), - verifyStep( - Actor.Sysio, - "unmapped-row-unlinked", - "an unmapped row appears keyed by the unlinked staker's ETH address", - async ctx => { - const wallet = ctx.outputs.assert( - YieldDistributionScenario.UnlinkedStakerWalletKey - ), - expectedPubkey = ethereumNativePubkey(wallet.address) - await pollUntil( - "unmapped row for the unlinked ETH staker", - async () => - (await readUnmappedRows(ctx)).some( - row => row.native_pubkey.toLowerCase() === expectedPubkey - ), - Constants.PropagationTimeoutMs, - Constants.PropagationPollMs - ) - }, - propagationStepOptions - ), - verifyStep( - Actor.Sysio, - "unmapped-count-grew-ethereum", - "the unmapped row count grew past the snapshot", - async ctx => { - const before = ctx.outputs.assert( - YieldDistributionScenario.EthereumUnmappedCountBeforeKey - ) - const after = (await readUnmappedRows(ctx)).length - Assert.ok( - after > before, - `unmapped count ${after} did not grow past ${before}` - ) - } - ) - ) - - // ── 4. DedupReplay — same external_epoch_ref reverts; nothing credits ── - ClusterBuildPhase.create( - cluster, - "DedupReplay", - "Replaying the linked staker's external_epoch_ref reverts at the emitter and credits nothing" - ).push( - verifyStep( - Actor.Sysio, - "snapshot-pclaims-count", - `record ${Constants.LinkedStakerAccount}'s pclaims row count before the replay`, - async ctx => { - ctx.outputs.set( - YieldDistributionScenario.ReplayPclaimsCountBeforeKey, - await readPclaimsCount(ctx, Constants.LinkedStakerAccount) - ) - } - ), - EmitSteps.planEthereumEmitReplay( - Actor.EthereumOutpost, - "emit-ethereum-replay", - "re-emit the SAME external_epoch_ref — the emitter's monotonic check must revert", - emitStepOptions, - YieldDistributionScenario.LinkedStakerWalletKey, - Constants.LinkedStakerAccount, - Constants.EthereumRewardPerStaker, - Constants.FullShareBps, - Constants.RewardEpochIndex - ), - verifyStep( - Actor.Sysio, - "pclaims-count-unchanged", - `pclaims count unchanged after a ${Constants.DedupeSettleEpochs}-epoch settle window`, - async ctx => { - const before = ctx.outputs.assert( - YieldDistributionScenario.ReplayPclaimsCountBeforeKey - ) - await sleep(Constants.dedupeSettleMs()) - Assert.strictEqual( - await readPclaimsCount(ctx, Constants.LinkedStakerAccount), - before, - "replay was not deduped — the linked staker's pclaims count changed" - ) - }, - dedupeStepOptions - ) - ) - } -} - -/** Typed cross-step output keys for the yield-distribution scenario. */ -export namespace YieldDistributionScenario { - /** The AuthEx-linked staker's ETH wallet (minted by SetupStakers). */ - export const LinkedStakerWalletKey = outputKey( - "yieldDistribution.linkedStakerWallet", - "the AuthEx-linked staker's ETH wallet" - ) - /** The UNLINKED staker's ETH wallet (no WIRE account, no authex link). */ - export const UnlinkedStakerWalletKey = outputKey( - "yieldDistribution.unlinkedStakerWallet", - "the unlinked staker's ETH wallet" - ) - /** `t5state.capital_shortfall_total` snapshotted before the linked emission. */ - export const CapitalShortfallBeforeKey = outputKey( - "yieldDistribution.capitalShortfallBefore", - "t5state.capital_shortfall_total before the linked emission" - ) - /** `unmapped` row count snapshotted before the unlinked ETH emission. */ - export const EthereumUnmappedCountBeforeKey = outputKey( - "yieldDistribution.ethereumUnmappedCountBefore", - "unmapped row count before the unlinked ETH emission" - ) - /** The linked staker's `pclaims` row count snapshotted before the replay. */ - export const ReplayPclaimsCountBeforeKey = outputKey( - "yieldDistribution.replayPclaimsCountBefore", - "the linked staker's pclaims row count before the replayed emission" - ) -} diff --git a/packages/flow-yield-distribution/src/YieldDistributionScenarioConstants.ts b/packages/flow-yield-distribution/src/YieldDistributionScenarioConstants.ts deleted file mode 100644 index 7d9619278..000000000 --- a/packages/flow-yield-distribution/src/YieldDistributionScenarioConstants.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { ProtocolTiming } from "@wireio/cluster-tool" - -/** - * Constants for the yield-distribution flow. Rewards, share bps, external - * epoch refs, and timing carry over from the previously-validated jest suite - * (2026-06): per-staker rewards are sized so the depot's emission-accounting - * bucket comfortably covers them, the propagation deadline budgets a full - * emitter → OPP ferry → depot dispatch round trip, and the dedupe settle - * window derives from the epoch duration so the flow scales with it. - */ -export namespace YieldDistributionScenarioConstants { - /** Epoch duration (s) — the `sysio.epoch::setconfig` floor is 60. */ - export const EpochDurationSec = 60 - /** Producer nodes stood up by the bootstrap (the old suite's `producerCount`). */ - export const ProducerCount = 3 - /** Batch operators ferrying OPP envelopes (the old suite's `batchOperatorCount`). */ - export const BatchOperatorCount = 3 - /** Underwriters provisioned by the bootstrap (the old suite's `underwriterCount`). */ - export const UnderwriterCount = 0 - - /** The AuthEx-linked staker's WIRE account — its reward lands in `sysio.dclaim::pclaims`. */ - export const LinkedStakerAccount = "yield.lnk" - /** - * The emitter's "unlinked" marker: an empty WIRE account makes the depot park - * the reward in `sysio.dclaim::unmapped` keyed by the staker's native address. - */ - export const UnlinkedWireAccount = "" - - /** Per-staker ETH-side reward (wei — the depot scales via PrecisionLib). */ - export const EthereumRewardPerStaker = 1_000_000n - /** Informational share-in-bps stamped on every emission (100%). */ - export const FullShareBps = 10_000 - /** Informational WIRE epoch index stamped on every emission. */ - export const RewardEpochIndex = 1 - - /** The linked staker's `external_epoch_ref` (the old suite's per-run counter, value 1). */ - export const LinkedStakerExternalEpochRef = 1n - /** The unlinked ETH staker's `external_epoch_ref` (counter value 2). */ - export const UnlinkedStakerExternalEpochRef = 2n - - /** Deadline for an attestation to round-trip emitter → batchop ferry → depot - * table — a single outpost→depot hop (envelope class). */ - export const PropagationTimeoutMs = ProtocolTiming.SingleHopBudgetMs - /** Interval between depot-table polls (ms). */ - export const PropagationPollMs = 2_000 - /** Row page size for `sysio.dclaim` table scans (matches the old suite's read limit). */ - export const TableQueryLimit = 5_000 - /** Ceiling on each emit write step (one outpost tx + confirmation). */ - export const EmitStepTimeoutMs = 60_000 - /** Buffer added on top of each poll/settle deadline for the enclosing step timeout (ms). */ - export const PollDeadlineBufferMs = 30_000 - /** 1 s in ms — multiplies epoch counts into ms deadlines. */ - export const MsPerSecond = 1_000 - /** Epochs the dedupe verify waits before re-counting `pclaims` (settle window). */ - export const DedupeSettleEpochs = 2 - - /** - * Rejection signature of a replayed `external_epoch_ref` — the - * `MockYieldEmitter` per-staker monotonic check reverts the tx. - */ - export const ReplayRejectionPattern = - /externalEpochRef not monotonic|reverted/i - - /** Settle window slept before asserting the replayed emission credited nothing. */ - export function dedupeSettleMs(): number { - return EpochDurationSec * DedupeSettleEpochs * MsPerSecond - } -} diff --git a/packages/flow-yield-distribution/src/index.ts b/packages/flow-yield-distribution/src/index.ts deleted file mode 100644 index fb2ce8d09..000000000 --- a/packages/flow-yield-distribution/src/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { FlowCLI } from "@wireio/cluster-tool" -import { YieldDistributionScenario } from "./YieldDistributionScenario.js" - -/** Run the yield-distribution flow as an executable — exit code = report success. */ -async function main(): Promise { - const report = await FlowCLI.create(YieldDistributionScenario).run() - process.exit(report.succeeded ? 0 : 1) -} - -void main() diff --git a/packages/flow-yield-distribution/src/steps/YieldDistributionScenarioEmitSteps.ts b/packages/flow-yield-distribution/src/steps/YieldDistributionScenarioEmitSteps.ts deleted file mode 100644 index 6bb7cbd55..000000000 --- a/packages/flow-yield-distribution/src/steps/YieldDistributionScenarioEmitSteps.ts +++ /dev/null @@ -1,261 +0,0 @@ -/** - * YieldDistributionScenarioEmitSteps — Step factories for the flow's synthetic - * STAKING_REWARD writes. Every emit is its OWN {@link ClusterBuildStep} so the - * `Report` records it: {@link YieldDistributionScenarioEmitSteps.planEthereumEmit} - * (one `MockYieldEmitter.emitYield(...)` tx) and {@link - * YieldDistributionScenarioEmitSteps.planEthereumEmitReplay} (the SAME - * `external_epoch_ref` re-emitted — the emitter's per-staker monotonic check - * MUST revert it). Emitter / contract loading are pure value helpers executed - * INSIDE the runners. - * - * The flow is Ethereum-only: the Solana outpost emits STAKING_REWARD from no - * real path (its staking surface is a separate developer track), and the - * harness no longer injects attestations. The SOL leg returns when the outpost - * produces one. - */ - -/** - * YieldDistributionScenarioEmitSteps — Step factories for the flow's synthetic - * STAKING_REWARD writes. Every emit is its OWN {@link ClusterBuildStep} so the - * `Report` records it: {@link YieldDistributionScenarioEmitSteps.planEthereumEmit} - * (one `MockYieldEmitter.emitYield(...)` tx) and {@link - * YieldDistributionScenarioEmitSteps.planEthereumEmitReplay} (the SAME - * `external_epoch_ref` re-emitted — the emitter's per-staker monotonic check - * MUST revert it). Emitter / contract loading are pure value helpers executed - * INSIDE the runners. - * - * The flow is Ethereum-only: the Solana outpost emits STAKING_REWARD from no - * real path (its staking surface is a separate developer track), and the - * harness no longer injects attestations. The SOL leg returns when the outpost - * produces one. - */ -import Assert from "node:assert" -import { ethers } from "ethers" -import { - OperatorDaemonArtifactsKey, - Report, - emitYieldBatch, - loadMockYieldEmitter, - ClusterBuildStep, - type ClusterBuildContext, - type ClusterBuildStepOptions, - type MockYieldEmitterContract, - type OutputKey, - type StepInput -} from "@wireio/cluster-tool" -import { YieldDistributionScenarioConstants as Constants } from "../YieldDistributionScenarioConstants.js" - -export namespace YieldDistributionScenarioEmitSteps { - // ── Step: ETH-side STAKING_REWARD (`MockYieldEmitter.emitYield`) ────────── - - /** Input for {@link planEthereumEmit} — one `MockYieldEmitter.emitYield(...)` write. */ - export interface EthereumEmitInput extends StepInput { - readonly kind: "YieldDistributionScenarioEmitSteps.EthereumEmitInput" - /** Output key holding the staker's ETH wallet (minted by SetupStakers). */ - readonly stakerWalletKey: OutputKey - /** WIRE account the depot credits — `""` parks the reward in `unmapped`. */ - readonly wireAccount: string - /** Reward in wei (the depot scales via PrecisionLib). */ - readonly rewardAmount: bigint - /** Informational share-in-bps for the depot's audit logging. */ - readonly shareBps: number - /** Monotonic-per-staker reference the emitter + depot dedupe against. */ - readonly externalEpochRef: bigint - /** Informational WIRE epoch index logged on the credit row. */ - readonly rewardEpochIndex: number - } - - /** - * A single `MockYieldEmitter.emitYield(...)` STAKING_REWARD write, signed by - * the run deployer (anvil #0 — holds the AccessManager role granted by - * `deployLocal.ts`). The attestation lands on OPP's outbound queue and the - * batch operators ferry it to the depot's `sysio.dclaim::onreward`. - * - * @param actor - The narrative subject (the Ethereum outpost emits). - * @param name - Step name (report row). - * @param description - One-line description. - * @param options - Per-step tuning (e.g. `timeoutMs`). - * @param stakerWalletKey - Output key of the staker's ETH wallet. - * @param wireAccount - WIRE account to credit (`""` → unmapped park). - * @param rewardAmount - Reward in wei. - * @param shareBps - Informational share-in-bps. - * @param externalEpochRef - Monotonic-per-staker reference. - * @param rewardEpochIndex - Informational WIRE epoch index. - * @returns The definition step. - */ - export function planEthereumEmit< - C extends ClusterBuildContext = ClusterBuildContext - >( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - stakerWalletKey: OutputKey, - wireAccount: string, - rewardAmount: bigint, - shareBps: number, - externalEpochRef: bigint, - rewardEpochIndex: number - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { - kind: "YieldDistributionScenarioEmitSteps.EthereumEmitInput", - stakerWalletKey, - wireAccount, - rewardAmount, - shareBps, - externalEpochRef, - rewardEpochIndex - }, - runEthereumEmit - ) - } - - /** Named runner — ONE `MockYieldEmitter.emitYield(...)` write for one staker. */ - export async function runEthereumEmit( - ctx: C, - input: EthereumEmitInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - const staker = ctx.outputs.assert(input.stakerWalletKey) - await emitYieldBatch( - loadEmitter(ctx), - [ - { - staker: staker.address, - wireAccount: input.wireAccount, - rewardAmount: input.rewardAmount, - shareBps: input.shareBps - } - ], - input.externalEpochRef, - input.rewardEpochIndex - ) - } - - // ── Step: replayed ETH-side emission (same `external_epoch_ref`) ────────── - - /** Input for {@link planEthereumEmitReplay} — the replayed-`external_epoch_ref` write attempt. */ - export interface EthereumEmitReplayInput extends StepInput { - readonly kind: "YieldDistributionScenarioEmitSteps.EthereumEmitReplayInput" - /** Output key holding the staker's ETH wallet (the linked staker). */ - readonly stakerWalletKey: OutputKey - /** WIRE account of the original emission being replayed. */ - readonly wireAccount: string - /** Reward in wei (same as the original emission). */ - readonly rewardAmount: bigint - /** Informational share-in-bps. */ - readonly shareBps: number - /** Informational WIRE epoch index. */ - readonly rewardEpochIndex: number - } - - /** - * Re-emit the staker's LAST `external_epoch_ref` (read back from the emitter's - * `lastExternalEpochRef`, exactly like the old suite) and assert the tx - * REVERTS on the per-staker monotonic check. The write is attempted; the - * revert is the expected outcome, so a replay that LANDS fails the step. - * - * @param actor - The narrative subject (the Ethereum outpost emits). - * @param name - Step name (report row). - * @param description - One-line description. - * @param options - Per-step tuning (e.g. `timeoutMs`). - * @param stakerWalletKey - Output key of the linked staker's ETH wallet. - * @param wireAccount - WIRE account of the original emission. - * @param rewardAmount - Reward in wei (same as the original emission). - * @param shareBps - Informational share-in-bps. - * @param rewardEpochIndex - Informational WIRE epoch index. - * @returns The definition step. - */ - export function planEthereumEmitReplay< - C extends ClusterBuildContext = ClusterBuildContext - >( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - stakerWalletKey: OutputKey, - wireAccount: string, - rewardAmount: bigint, - shareBps: number, - rewardEpochIndex: number - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { - kind: "YieldDistributionScenarioEmitSteps.EthereumEmitReplayInput", - stakerWalletKey, - wireAccount, - rewardAmount, - shareBps, - rewardEpochIndex - }, - runEthereumEmitReplay - ) - } - - /** Named runner — attempt ONE replayed `emitYield(...)` write; assert it reverts. */ - export async function runEthereumEmitReplay( - ctx: C, - input: EthereumEmitReplayInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - const staker = ctx.outputs.assert(input.stakerWalletKey) - const emitter = loadEmitter(ctx) - // The staker's last processed ref (a read) — the exact value the original - // emission recorded, so re-submitting it violates strict monotonicity. - const replayedRef = await emitter.lastExternalEpochRef(staker.address) - Assert.ok( - replayedRef > 0n, - `YieldDistributionScenarioEmitSteps.ethereumEmitReplay: no prior emission recorded for ${staker.address}` - ) - await Assert.rejects( - () => - emitYieldBatch( - emitter, - [ - { - staker: staker.address, - wireAccount: input.wireAccount, - rewardAmount: input.rewardAmount, - shareBps: input.shareBps - } - ], - replayedRef, - input.rewardEpochIndex - ), - Constants.ReplayRejectionPattern, - `expected the replayed external_epoch_ref ${replayedRef} to revert on the emitter's monotonic check` - ) - } - - // ── value helpers (artifact loads — executed INSIDE runners) ────────────── - - /** - * Resolve `MockYieldEmitter` from the run's deploy artifacts, bound to the - * run deployer signer (anvil #0 — the AccessManager admin `deployLocal.ts` - * configured). Address from `outpost-addrs.json`; ABI from the hardhat - * artifact — both via the harness loaders. - * - * @param ctx - The build context (supplies `ethereumPath` + the deployer signer). - * @returns The emitter bound to the deployer. - */ - export function loadEmitter( - ctx: C - ): MockYieldEmitterContract { - return loadMockYieldEmitter( - ctx.config.ethereumPath, - ctx.outputs.assert(OperatorDaemonArtifactsKey).ethereumAddresses, - ctx.ethereum.wallet.signer - ) - } -} diff --git a/packages/flow-yield-distribution/src/steps/index.ts b/packages/flow-yield-distribution/src/steps/index.ts deleted file mode 100644 index 6ab1064dd..000000000 --- a/packages/flow-yield-distribution/src/steps/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./YieldDistributionScenarioEmitSteps.js" diff --git a/packages/flow-yield-distribution/tsconfig.json b/packages/flow-yield-distribution/tsconfig.json deleted file mode 100644 index 82b017421..000000000 --- a/packages/flow-yield-distribution/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../../etc/tsconfig/tsconfig.base.jest.json", - "compilerOptions": { - - }, - "include": [], - "references": [ - { "path": "../cluster-tool/tsconfig.json" }, - { "path": "./tsconfig.src.json" } - ] -} diff --git a/packages/flow-yield-distribution/tsconfig.src.json b/packages/flow-yield-distribution/tsconfig.src.json deleted file mode 100644 index e67621b57..000000000 --- a/packages/flow-yield-distribution/tsconfig.src.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "../../etc/tsconfig/tsconfig.base.jest.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "lib", - "paths": { - "@wireio/cluster-tool": ["../cluster-tool/src"] - } - }, - "include": ["src"], - "references": [ - { "path": "../cluster-tool/tsconfig.json" } - ] -} diff --git a/packages/test-app-server/package.json b/packages/test-app-server/package.json index 37e8f43c8..676801105 100644 --- a/packages/test-app-server/package.json +++ b/packages/test-app-server/package.json @@ -11,7 +11,7 @@ "clean": "rm -rf lib dist" }, "dependencies": { - "@wireio/sdk-core": "^1.0.96", + "@wireio/sdk-core": "^1.0.97", "@wireio/shared": "^1.0.96", "@wireio/wallet-ext-sdk": "^0.1.84", "buffer": "^6.0.3", diff --git a/tsconfig.json b/tsconfig.json index a71e1fd4d..c2b1efd25 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,7 +21,6 @@ { "path": "./packages/flow-batch-operator-slashing/tsconfig.json" }, { "path": "./packages/flow-node-owner-nft/tsconfig.json" }, { "path": "./packages/flow-emissions-soak/tsconfig.json" }, - { "path": "./packages/flow-yield-distribution/tsconfig.json" }, { "path": "./packages/flow-liq-syndication/tsconfig.json" }, { "path": "./packages/flow-liq-yield/tsconfig.json" }, { "path": "./packages/flow-syndication-underwriting/tsconfig.json" },