From ed871bd7fa072ec273b896e0ab247072427cd37d Mon Sep 17 00:00:00 2001 From: David J Parrott Date: Sun, 4 Oct 2026 16:07:31 -0400 Subject: [PATCH 1/4] Align syndication flows with operational state Replace lifetime reporting assertions with per-envelope obligations, outstanding returns, and actual destination wallet settlement. Exercise explicit custody reconciliation while preserving the separate Andon clear step, and describe independently claimable WIRE rewards. Add regressions for incorrect return identities and short settlement. Validation: build passed; 53 shared tool and 43 focused flow tests passed. Underwriting and emergency live runs passed. The rate-limit live run passed depot checks but stalled on Solana compute exhaustion before external wallet completion could be verified. --- .../src/tools/wire/WireSyndicationTool.ts | 7 +- .../tools/wire/WireSyndicationTool.test.ts | 10 +- .../src/EmergencyStopScenario.ts | 42 ++++-- .../src/EmergencyStopScenarioConstants.ts | 2 +- .../src/steps/EmergencyStopSteps.ts | 20 +++ .../tests/EmergencyStopSteps.test.cjs | 56 ++++++++ .../src/SyndicationRateLimitScenario.ts | 41 ++---- .../outputs/SyndicationRateLimitSnapshot.ts | 6 +- .../SyndicationRateLimitSettlement.test.cjs | 130 +++++++++++++++--- .../package.json | 3 +- .../src/SyndicationUnderwritingScenario.ts | 21 +-- .../SyndicationUnderwritingSnapshot.ts | 2 - ...SyndicationUnderwritingSettlement.test.cjs | 69 ++++++++++ 13 files changed, 317 insertions(+), 92 deletions(-) create mode 100644 packages/flow-syndication-underwriting/tests/SyndicationUnderwritingSettlement.test.cjs diff --git a/packages/cluster-tool/src/tools/wire/WireSyndicationTool.ts b/packages/cluster-tool/src/tools/wire/WireSyndicationTool.ts index 7e48f8e06..e8637ec73 100644 --- a/packages/cluster-tool/src/tools/wire/WireSyndicationTool.ts +++ b/packages/cluster-tool/src/tools/wire/WireSyndicationTool.ts @@ -215,7 +215,7 @@ export namespace WireSyndicationTool { ) } - /** READ the `sysio.synd::ledger` row of one pair: its running sums and queue cursor. */ + /** READ the `sysio.synd::ledger` row of one pair: its queue cursors and retained-epoch floor. */ export async function readLedger( ctx: C, chainCode: string, @@ -293,7 +293,7 @@ export namespace WireSyndicationTool { return result.rows.find(row => isCode(row.token_code, tokenCode)) } - /** READ every `sysio.synd::mismatch` row: each custody shortfall an outpost reported. */ + /** READ every `sysio.synd::mismatch` row: the latest active custody incident for each outpost/token pair. */ export async function readMismatches( ctx: C ): Promise { @@ -822,7 +822,8 @@ export namespace WireSyndicationTool { /** * Close a bonded request: ONE Phase of two Steps — `sysio.bond::approve`, sent once the * request's challenge window has passed ({@link runApproveAfterWindow}), then - * `sysio.bond::claim` of what the request owes `account`. Both are permissionless; + * `sysio.bond::claim` of the request-token entitlement owed to `account`. Earned WIRE + * is banked separately for `sysio.bond::claimwire`. Both actions here are permissionless; * `account` signs them. Self-registers on `parent`. * * @param parent - The build root or enclosing PhaseGroup. diff --git a/packages/cluster-tool/tests/tools/wire/WireSyndicationTool.test.ts b/packages/cluster-tool/tests/tools/wire/WireSyndicationTool.test.ts index e989b9ac6..d530b25e9 100644 --- a/packages/cluster-tool/tests/tools/wire/WireSyndicationTool.test.ts +++ b/packages/cluster-tool/tests/tools/wire/WireSyndicationTool.test.ts @@ -122,7 +122,8 @@ function request( held_at: "1970-01-01T00:00:00.000", resolved_at: "1970-01-01T00:00:00.000", resolved_index: "0", - forfeit_pending: 0 + forfeit_pending: 0, + outcome_acknowledged: false } } @@ -210,10 +211,9 @@ describe("WireSyndicationTool — sysio.synd reads", () => { { chain_code: Solana, token_code: Liqsol, - syndicated_sum: 5_000_000_000, - yield_sum: 0, - desyndicated_sum: 0, - queue_epoch: 4 + queue_epoch: 4, + underwriting_epoch: 4, + retained_from_epoch: 0 } ]) serve(clients.synd.tables.syndconfig, [ diff --git a/packages/flow-emergency-stop/src/EmergencyStopScenario.ts b/packages/flow-emergency-stop/src/EmergencyStopScenario.ts index b47f2598f..47e242d1b 100644 --- a/packages/flow-emergency-stop/src/EmergencyStopScenario.ts +++ b/packages/flow-emergency-stop/src/EmergencyStopScenario.ts @@ -752,20 +752,40 @@ export class EmergencyStopScenario extends SyndicationScenario { } ) EmergencyStopSteps.assertRecoveryMismatches(rows, message.sequence) - for (const original of ctx.outputs.assert(Constants.Mismatches)) - Assert.deepStrictEqual( - rows.find( - row => - row.chain_code === original.chain_code && - BigInt(row.sequence) === BigInt(original.sequence) - ), - original + for (const original of ctx.outputs.assert(Constants.Mismatches)) { + const current = rows.filter( + row => + row.chain_code === original.chain_code && + row.token_code === original.token_code ) + Assert.strictEqual(current.length, 1) + Assert.ok(BigInt(current[0].sequence) >= BigInt(original.sequence)) + } Assert.ok((await WireSyndicationTool.readCord(ctx)).pulled) ctx.outputs.set(Constants.Mismatches, rows) }, verify ), + EmergencyStopSteps.planAction( + Actor.Sysio, + "reconcile-shortfall", + "attest repaired custody and remove the active incident", + write, + Action.reconcileSolana + ), + verifyStep( + Actor.Sysio, + "reconciled-still-stopped", + "incident resolved while the emergency cord remains pulled", + async ctx => { + Assert.deepStrictEqual( + await WireSyndicationTool.readMismatches(ctx), + [] + ) + Assert.ok((await WireSyndicationTool.readCord(ctx)).pulled) + ctx.outputs.set(Constants.Mismatches, []) + } + ), Steps.contracts.sysio.andon.planClear( Actor.Sysio, "clear-shortfall", @@ -779,7 +799,7 @@ export class EmergencyStopScenario extends SyndicationScenario { verifyStep( Actor.Sysio, "no-new-mismatch", - "historical evidence preserved with cord clear", + "no active incident with cord clear", async ctx => { const mismatches = await WireSyndicationTool.readMismatches(ctx) StepExtraRecorder.note("Complete mismatch evidence after clear", { @@ -993,7 +1013,7 @@ export class EmergencyStopScenario extends SyndicationScenario { ) const { rows, more } = await ctx.wire .getSysioContract(SysioContractName.synd) - .tables.desyndlog.query({ limit: SyndicationScenario.QueryLimit }) + .tables.returns.query({ limit: SyndicationScenario.QueryLimit }) Assert.ok(!more) ctx.outputs.set( Constants.LastRequest, @@ -1034,7 +1054,7 @@ export class EmergencyStopScenario extends SyndicationScenario { async () => { const { rows, more } = await ctx.wire .getSysioContract(SysioContractName.synd) - .tables.desyndlog.query({ + .tables.returns.query({ limit: SyndicationScenario.QueryLimit }) Assert.ok(!more) diff --git a/packages/flow-emergency-stop/src/EmergencyStopScenarioConstants.ts b/packages/flow-emergency-stop/src/EmergencyStopScenarioConstants.ts index 30b144045..798fb7bae 100644 --- a/packages/flow-emergency-stop/src/EmergencyStopScenarioConstants.ts +++ b/packages/flow-emergency-stop/src/EmergencyStopScenarioConstants.ts @@ -114,7 +114,7 @@ export namespace EmergencyStopScenarioConstants { /** Last request before each redemption. */ export const LastRequest = outputKey( "emergency.lastRequest", - "desyndlog baseline" + "returns baseline" ) /** Permanent, deliberate mismatch evidence. */ export const Mismatches = outputKey< diff --git a/packages/flow-emergency-stop/src/steps/EmergencyStopSteps.ts b/packages/flow-emergency-stop/src/steps/EmergencyStopSteps.ts index bb8e5f824..ba91c5492 100644 --- a/packages/flow-emergency-stop/src/steps/EmergencyStopSteps.ts +++ b/packages/flow-emergency-stop/src/steps/EmergencyStopSteps.ts @@ -49,6 +49,7 @@ export namespace EmergencyStopSteps { payEthereum = "payEthereum", recreditSolana = "recreditSolana", donateSolana = "donateSolana", + reconcileSolana = "reconcileSolana", recreditEthereum = "recreditEthereum", donateEthereum = "donateEthereum" } @@ -219,6 +220,25 @@ export namespace EmergencyStopSteps { signal ) ) + .with(Action.reconcileSolana, async () => { + const reported = await SolanaLiqSyndicationTool.readPoolBalance(ctx) + await ctx.wire + .getSysioContract(SysioContracts.SysioContractName.synd) + .actions.reconcile.invoke( + { + chain_code: SyndicationScenario.Chain, + token_code: SyndicationScenario.Token, + reported: reported.toString() + }, + { + authorization: WireClient.activeAuthorization( + SysioContracts.SysioContractAccount[ + SysioContracts.SysioContractName.system + ] + ) + } + ) + }) .with(Action.donateSolana, () => SolanaLiqSyndicationTool.runDonateToPool( ctx, diff --git a/packages/flow-emergency-stop/tests/EmergencyStopSteps.test.cjs b/packages/flow-emergency-stop/tests/EmergencyStopSteps.test.cjs index dc70a6ef2..832b61bb3 100644 --- a/packages/flow-emergency-stop/tests/EmergencyStopSteps.test.cjs +++ b/packages/flow-emergency-stop/tests/EmergencyStopSteps.test.cjs @@ -308,6 +308,8 @@ test("scenario plans both causes and outpost-first recovery without executing wr "backed-message", "backed-intake", "repair-message-admitted", + "reconcile-shortfall", + "reconciled-still-stopped", "clear-shortfall", "no-new-mismatch" ] @@ -580,3 +582,57 @@ test("pre-clear verifications reject changed records or balances on either outpo await Assert.rejects(step.runner(ctx, step.input, signal)) } }) + +test("reconciliation attests current custody to the depot exactly once", async () => { + const ctx = context() + mock.method( + SolanaLiqSyndicationTool, + "readPoolBalance", + async () => 123456789012345678n + ) + const calls = [] + ctx.wire.getSysioContract = name => { + Assert.equal(name, "synd") + return { + actions: { + reconcile: { + invoke: async (data, options) => calls.push({ data, options }) + } + } + } + } + await run(Action.reconcileSolana, ctx) + Assert.equal(calls.length, 1) + Assert.deepEqual(calls[0].data, { + chain_code: SyndicationScenario.Chain, + token_code: SyndicationScenario.Token, + reported: "123456789012345678" + }) + Assert.ok(calls[0].options.authorization) +}) + +for (const defect of ["none", "incident", "cleared"]) { + test(`reconciliation verification requires resolved incident and separate stop: ${defect}`, async () => { + const { + ClusterBuild, + WireSyndicationTool + } = require("@wireio/cluster-tool") + const { EmergencyStopScenario } = require("../lib/EmergencyStopScenario.js") + const ctx = context(), + cluster = ClusterBuild.forContext(ctx) + new EmergencyStopScenario().plan(cluster) + const step = cluster.children + .find(phase => phase.name === "AutomaticDepotPull") + .steps.find(value => value.name === "reconciled-still-stopped") + mock.method(WireSyndicationTool, "readMismatches", async () => + defect === "incident" ? [{ sequence: "1" }] : [] + ) + mock.method(WireSyndicationTool, "readCord", async () => ({ + pulled: defect !== "cleared" + })) + if (defect === "none") { + await step.runner(ctx, step.input, signal) + Assert.deepEqual(ctx.outputs.assert(Constants.Mismatches), []) + } else await Assert.rejects(step.runner(ctx, step.input, signal)) + }) +} diff --git a/packages/flow-syndication-rate-limit/src/SyndicationRateLimitScenario.ts b/packages/flow-syndication-rate-limit/src/SyndicationRateLimitScenario.ts index fdfa0b2e5..fa025a638 100644 --- a/packages/flow-syndication-rate-limit/src/SyndicationRateLimitScenario.ts +++ b/packages/flow-syndication-rate-limit/src/SyndicationRateLimitScenario.ts @@ -401,11 +401,11 @@ export class SyndicationRateLimitScenario extends SyndicationScenario { verifyStep( Actor.User, "snapshot-redemption", - "record balance, supply, ledger and queued records", + "record balance, supply and outstanding returns", async ctx => { const { rows, more } = await ctx.wire .getSysioContract(SysioContractName.synd) - .tables.desyndlog.query({ limit: SyndicationScenario.QueryLimit }) + .tables.returns.query({ limit: SyndicationScenario.QueryLimit }) Assert.ok(!more) ctx.outputs.set(Constants.BeforeDesyndication, { balance: await SyndicationScenario.readBalance( @@ -420,16 +420,7 @@ export class SyndicationRateLimitScenario extends SyndicationScenario { ) ).supply ), - sum: BigInt( - ( - await WireSyndicationTool.readLedger( - ctx, - SyndicationScenario.Chain, - SyndicationScenario.Token - ) - ).desyndicated_sum - ), - logs: rows.length + returns: rows.length }) } ), @@ -447,7 +438,7 @@ export class SyndicationRateLimitScenario extends SyndicationScenario { const before = ctx.outputs.assert(Constants.BeforeDesyndication), { rows } = await ctx.wire .getSysioContract(SysioContractName.synd) - .tables.desyndlog.query({ limit: SyndicationScenario.QueryLimit }) + .tables.returns.query({ limit: SyndicationScenario.QueryLimit }) Assert.match( ctx.outputs.assert(Constants.Refusal), /desyndication exceeds the current budget/ @@ -467,7 +458,7 @@ export class SyndicationRateLimitScenario extends SyndicationScenario { ), before.supply ) - Assert.strictEqual(rows.length, before.logs) + Assert.strictEqual(rows.length, before.returns) } ), verifyStep( @@ -497,12 +488,12 @@ export class SyndicationRateLimitScenario extends SyndicationScenario { verifyStep( Actor.User, "within-budget-queued", - "exact debit, burn and desyndlog prove accepted queued redemption", + "exact debit, burn and outstanding return prove accepted queued redemption", async ctx => { const before = ctx.outputs.assert(Constants.BeforeDesyndication), { rows, more } = await ctx.wire .getSysioContract(SysioContractName.synd) - .tables.desyndlog.query({ + .tables.returns.query({ limit: SyndicationScenario.QueryLimit }), bucket = await WireSyndicationTool.readBucket( @@ -512,7 +503,7 @@ export class SyndicationRateLimitScenario extends SyndicationScenario { SysioSyndBucketDirection.DESYNDICATION ) Assert.ok(!more) - Assert.strictEqual(rows.length, before.logs + 1) + Assert.strictEqual(rows.length, before.returns + 1) Assert.strictEqual( BigInt(rows[rows.length - 1].amount), Constants.DesyndicationAmount @@ -533,16 +524,12 @@ export class SyndicationRateLimitScenario extends SyndicationScenario { before.supply - Constants.DesyndicationAmount ) Assert.strictEqual( - BigInt( - ( - await WireSyndicationTool.readLedger( - ctx, - SyndicationScenario.Chain, - SyndicationScenario.Token - ) - ).desyndicated_sum - ), - before.sum + Constants.DesyndicationAmount + rows[rows.length - 1].holder, + Constants.UserA.account + ) + Assert.strictEqual( + rows[rows.length - 1].token_code, + SyndicationScenario.Token ) Assert.strictEqual( BigInt(bucket.level), diff --git a/packages/flow-syndication-rate-limit/src/outputs/SyndicationRateLimitSnapshot.ts b/packages/flow-syndication-rate-limit/src/outputs/SyndicationRateLimitSnapshot.ts index 828afe74e..f0ef0a1fe 100644 --- a/packages/flow-syndication-rate-limit/src/outputs/SyndicationRateLimitSnapshot.ts +++ b/packages/flow-syndication-rate-limit/src/outputs/SyndicationRateLimitSnapshot.ts @@ -4,8 +4,6 @@ export interface SyndicationRateLimitSnapshot { readonly balance: bigint /** Pre-operation supply. */ readonly supply: bigint - /** Pre-operation sum. */ - readonly sum: bigint - /** Pre-operation logs. */ - readonly logs: number + /** Number of outstanding returns before the operation. */ + readonly returns: number } diff --git a/packages/flow-syndication-rate-limit/tests/SyndicationRateLimitSettlement.test.cjs b/packages/flow-syndication-rate-limit/tests/SyndicationRateLimitSettlement.test.cjs index f18a42c24..e2121b7e1 100644 --- a/packages/flow-syndication-rate-limit/tests/SyndicationRateLimitSettlement.test.cjs +++ b/packages/flow-syndication-rate-limit/tests/SyndicationRateLimitSettlement.test.cjs @@ -1,31 +1,119 @@ const Assert = require("node:assert/strict") -const {test, afterEach, mock} = require("node:test") -const {ClusterBuild, OutputStore, SyndicationScenario, SolanaFundingTool, SolanaLiqSyndicationTool, WireSyndicationTool} = require("@wireio/cluster-tool") +const { test, afterEach, mock } = require("node:test") +const { + ClusterBuild, + OutputStore, + SyndicationScenario, + SolanaFundingTool, + SolanaLiqSyndicationTool, + WireSyndicationTool +} = require("@wireio/cluster-tool") const StepTools = require("@wireio/cluster-tool/lib/cjs/orchestration/StepTools.js") -const {SyndicationRateLimitScenario} = require("../lib/SyndicationRateLimitScenario.js") -const {SyndicationRateLimitScenarioConstants: Constants} = require("../lib/SyndicationRateLimitScenarioConstants.js") +const { + SyndicationRateLimitScenario +} = require("../lib/SyndicationRateLimitScenario.js") +const { + SyndicationRateLimitScenarioConstants: Constants +} = require("../lib/SyndicationRateLimitScenarioConstants.js") afterEach(() => mock.restoreAll()) function steps(node) { return node.steps ? [...node.steps] : node.children.flatMap(steps) } -for (const complete of [true, false]) test(`settlement requires the complete destination balance: ${complete}`, async () => { - const ctx = {outputs: new OutputStore(), config: {dataPath: "/tmp/unused-unit-wallet"}}, - build = ClusterBuild.forContext(ctx), baseline = 123456n, - expected = baseline + Constants.DesyndicationAmount, - balance = complete ? expected : expected - 1n - new SyndicationRateLimitScenario().plan(build) - const step = steps(build).find(value => value.name === "return-in-wallet") - Assert.ok(step, "flow must contain its final wallet settlement assertion") - mock.method(StepTools, "pollUntil", async (label, predicate) => { - Assert.ok(await predicate(), "destination remains incomplete") +for (const complete of [true, false]) + test(`settlement requires the complete destination balance: ${complete}`, async () => { + const ctx = { + outputs: new OutputStore(), + config: { dataPath: "/tmp/unused-unit-wallet" } + }, + build = ClusterBuild.forContext(ctx), + baseline = 123456n, + expected = baseline + Constants.DesyndicationAmount, + balance = complete ? expected : expected - 1n + new SyndicationRateLimitScenario().plan(build) + const step = steps(build).find(value => value.name === "return-in-wallet") + Assert.ok(step, "flow must contain its final wallet settlement assertion") + mock.method(StepTools, "pollUntil", async (label, predicate) => { + Assert.ok(await predicate(), "destination remains incomplete") + }) + mock.method( + SolanaLiqSyndicationTool, + "readLiqsolBalance", + async () => balance + ) + + ctx.outputs.set(Constants.ExternalBalanceBefore, baseline) + + const run = () => step.runner(ctx, step.input, new AbortController().signal) + if (complete) await run() + else await Assert.rejects(run, /destination remains incomplete/) }) - mock.method(SolanaLiqSyndicationTool, "readLiqsolBalance", async () => balance) - - ctx.outputs.set(Constants.ExternalBalanceBefore, baseline) - const run = () => step.runner(ctx, step.input, new AbortController().signal) - if (complete) await run() - else await Assert.rejects(run, /destination remains incomplete/) -}) +// Outstanding-return assertions must reject incorrect obligations even when the burn is correct. +for (const fault of [ + "none", + "amount", + "holder", + "token", + "balance", + "supply" +]) { + test(`queued redemption checks operational obligation and exact balances: ${fault}`, async () => { + const before = { + balance: Constants.DesyndicationAmount * 3n, + supply: Constants.DesyndicationAmount * 8n, + returns: 0 + }, + row = { + amount: Constants.DesyndicationAmount.toString(), + holder: Constants.UserA.account, + token_code: SyndicationScenario.Token + }, + ctx = { + outputs: new OutputStore(), + config: { dataPath: "/tmp/unused-unit-wallet" }, + wire: { + getSysioContract: () => ({ + tables: { + returns: { query: async () => ({ rows: [row], more: false }) } + } + }) + } + }, + build = ClusterBuild.forContext(ctx) + if (fault === "amount") + row.amount = (Constants.DesyndicationAmount - 1n).toString() + if (fault === "holder") row.holder = Constants.UserB.account + if (fault === "token") row.token_code = "LIQETH" + new SyndicationRateLimitScenario().plan(build) + ctx.outputs.set(Constants.BeforeDesyndication, before) + mock.method( + SyndicationScenario, + "readBalance", + async () => + before.balance - + Constants.DesyndicationAmount + + (fault === "balance" ? 1n : 0n) + ) + mock.method(WireSyndicationTool, "readShadowStat", async () => ({ + supply: SyndicationScenario.quantity( + before.supply - + Constants.DesyndicationAmount + + (fault === "supply" ? 1n : 0n) + ) + })) + mock.method(WireSyndicationTool, "readBucket", async () => ({ + level: ( + Constants.DesyndicationBurst - Constants.DesyndicationAmount + ).toString() + })) + const step = steps(build).find( + value => value.name === "within-budget-queued" + ) + Assert.ok(step) + const run = () => step.runner(ctx, step.input, new AbortController().signal) + if (fault === "none") await run() + else await Assert.rejects(run) + }) +} diff --git a/packages/flow-syndication-underwriting/package.json b/packages/flow-syndication-underwriting/package.json index 37a1240b3..b4e64fd73 100644 --- a/packages/flow-syndication-underwriting/package.json +++ b/packages/flow-syndication-underwriting/package.json @@ -7,7 +7,8 @@ "scripts": { "build": "tsc -b tsconfig.json", "test": "node lib/index.js", - "clean": "rm -rf lib" + "clean": "rm -rf lib", + "test:unit": "node --test tests/*.test.cjs" }, "dependencies": { "@wireio/cluster-tool": "workspace:*", diff --git a/packages/flow-syndication-underwriting/src/SyndicationUnderwritingScenario.ts b/packages/flow-syndication-underwriting/src/SyndicationUnderwritingScenario.ts index bfaba86c7..4f57f488f 100644 --- a/packages/flow-syndication-underwriting/src/SyndicationUnderwritingScenario.ts +++ b/packages/flow-syndication-underwriting/src/SyndicationUnderwritingScenario.ts @@ -60,13 +60,8 @@ export class SyndicationUnderwritingScenario extends SyndicationScenario { verifyStep( Actor.Sysio, "snapshot-before", - "record supply custody, bonder, fees and ledger", + "record custody, bonder and fee balances", async ctx => { - const ledger = await WireSyndicationTool.readLedger( - ctx, - SyndicationScenario.Chain, - SyndicationScenario.Token - ) ctx.outputs.set(Constants.Before, { holder: await SyndicationScenario.readBalance( ctx, @@ -76,8 +71,7 @@ export class SyndicationUnderwritingScenario extends SyndicationScenario { ctx, SyndicationScenario.Bonder ), - fees: await SyndicationScenario.readFees(ctx), - syndicated: ledger ? BigInt(ledger.syndicated_sum) : 0n + fees: await SyndicationScenario.readFees(ctx) }) Assert.strictEqual( await SyndicationScenario.readBalance(ctx, Constants.User.account), @@ -244,11 +238,6 @@ export class SyndicationUnderwritingScenario extends SyndicationScenario { second = await SyndicationScenario.readEnvelope( ctx, Constants.SecondEpoch - ), - ledger = await WireSyndicationTool.readLedger( - ctx, - SyndicationScenario.Chain, - SyndicationScenario.Token ) Assert.strictEqual(BigInt(first.released), Constants.Amount) Assert.strictEqual( @@ -259,10 +248,8 @@ export class SyndicationUnderwritingScenario extends SyndicationScenario { await SyndicationScenario.readFees(ctx), before.fees + fee ) - Assert.strictEqual( - BigInt(ledger.syndicated_sum), - before.syndicated + Constants.Amount + Constants.LaterAmount - ) + Assert.strictEqual(BigInt(first.synd_total), Constants.Amount) + Assert.strictEqual(BigInt(second.synd_total), Constants.LaterAmount) Assert.ok( matchesProtoEnum( second.state, diff --git a/packages/flow-syndication-underwriting/src/outputs/SyndicationUnderwritingSnapshot.ts b/packages/flow-syndication-underwriting/src/outputs/SyndicationUnderwritingSnapshot.ts index 362f600ab..60d57d99b 100644 --- a/packages/flow-syndication-underwriting/src/outputs/SyndicationUnderwritingSnapshot.ts +++ b/packages/flow-syndication-underwriting/src/outputs/SyndicationUnderwritingSnapshot.ts @@ -6,6 +6,4 @@ export interface SyndicationUnderwritingSnapshot { readonly bonder: bigint /** Pre-operation fees. */ readonly fees: bigint - /** Pre-operation syndicated. */ - readonly syndicated: bigint } diff --git a/packages/flow-syndication-underwriting/tests/SyndicationUnderwritingSettlement.test.cjs b/packages/flow-syndication-underwriting/tests/SyndicationUnderwritingSettlement.test.cjs new file mode 100644 index 000000000..f5d8d6458 --- /dev/null +++ b/packages/flow-syndication-underwriting/tests/SyndicationUnderwritingSettlement.test.cjs @@ -0,0 +1,69 @@ +const Assert = require("node:assert/strict") +const { test, afterEach, mock } = require("node:test") +const { + ClusterBuild, + OutputStore, + SyndicationScenario +} = require("@wireio/cluster-tool") +const { SysioContracts } = require("@wireio/sdk-core") +const { + SyndicationUnderwritingScenario +} = require("../lib/SyndicationUnderwritingScenario.js") +const { + SyndicationUnderwritingScenarioConstants: Constants +} = require("../lib/SyndicationUnderwritingScenarioConstants.js") +afterEach(() => mock.restoreAll()) + +/** Flatten the planned flow so tests exercise the actual verification runner. */ +function steps(node) { + return node.steps ? [...node.steps] : node.children.flatMap(steps) +} + +// Per-envelope accounting replaces historical totals without losing exact-amount coverage. +for (const fault of ["none", "first", "second", "wallet", "fees"]) { + test(`underwriting release checks both envelope amounts and actual credit: ${fault}`, async () => { + const ctx = { + outputs: new OutputStore(), + config: { dataPath: "/tmp/unused-unit-wallet" } + }, + build = ClusterBuild.forContext(ctx), + fee = + (Constants.Amount * BigInt(Constants.FeeBps)) / + SyndicationScenario.BasisPoints + new SyndicationUnderwritingScenario().plan(build) + ctx.outputs.set(Constants.Before, { holder: 0n, bonder: 0n, fees: 0n }) + mock.method(SyndicationScenario, "readEnvelope", async (_ctx, key) => + key === Constants.FirstEpoch + ? { + released: Constants.Amount.toString(), + synd_total: ( + Constants.Amount + (fault === "first" ? 1n : 0n) + ).toString() + } + : { + synd_total: ( + Constants.LaterAmount + (fault === "second" ? 1n : 0n) + ).toString(), + state: SysioContracts.SysioSyndEnvelopeState.REQUESTED, + request_id: 2 + } + ) + mock.method( + SyndicationScenario, + "readBalance", + async () => Constants.Amount - fee - (fault === "wallet" ? 1n : 0n) + ) + mock.method( + SyndicationScenario, + "readFees", + async () => fee + (fault === "fees" ? 1n : 0n) + ) + const step = steps(build).find( + value => value.name === "first-released-next-requested" + ) + Assert.ok(step) + const run = () => step.runner(ctx, step.input, new AbortController().signal) + if (fault === "none") await run() + else await Assert.rejects(run) + }) +} From 95a66e2d2f7d6a23b310140e6e6c48560528c021 Mon Sep 17 00:00:00 2001 From: David J Parrott Date: Sun, 4 Oct 2026 20:45:53 -0400 Subject: [PATCH 2/4] Remove retired flows and bootstrap support Delete eight disabled reserve, swap and underwriter flow packages and their dedicated clients, seeding flags and daemon wiring. Default the retired daemon count to zero and reject nonzero values. Preserve all 13 enabled flows and their final destination settlement assertions. Validation: build; 248 suites and 2,642 tests; retained flow-unit tests; runner checks; live LIQ syndication through final wallet settlement. --- CLAUDE.md | 54 +- README.md | 42 +- .../src/config/ClusterConfig.ts | 17 - .../tests/config/ClusterConfig.test.ts | 15 +- packages/cluster-tool/src/Constants.ts | 19 +- .../src/cli/ClusterBuildOptionsArgs.ts | 32 +- .../src/clients/wire/WireClient.ts | 105 +- .../src/config/BindConfigProvider.ts | 2 +- .../src/config/ClusterBuildOptions.ts | 21 - .../src/config/ClusterConfigProvider.ts | 20 +- .../src/flow/contexts/SwapScenarioContext.ts | 166 -- .../cluster-tool/src/flow/contexts/index.ts | 1 - .../src/orchestration/ClusterBuildDefaults.ts | 169 +- .../ethereum/EthereumOutpostBootstrapper.ts | 211 +-- .../ethereum/EthereumOutpostSteps.ts | 31 +- .../solana/SolanaOutpostBootstrapper.ts | 369 +---- .../solana/SolanaOutpostSteps.ts | 46 +- .../steps/ExternalOutpostSteps.ts | 26 +- .../src/orchestration/steps/RegistrySteps.ts | 130 +- .../contracts/sysio/ReservContractSteps.ts | 187 --- .../contracts/sysio/UwritContractSteps.ts | 49 - .../steps/contracts/sysio/index.ts | 4 - .../steps/processes/NodeopProcessSteps.ts | 42 +- .../tools/ethereum/EthereumCollateralTool.ts | 454 ------ .../src/tools/ethereum/EthereumFundingTool.ts | 301 ---- .../src/tools/ethereum/EthereumSwapTool.ts | 395 ----- .../cluster-tool/src/tools/ethereum/index.ts | 3 - .../src/tools/solana/SolanaCollateralTool.ts | 344 ----- .../src/tools/solana/SolanaSwapTool.ts | 270 ---- .../cluster-tool/src/tools/solana/index.ts | 2 - .../src/tools/wire/OperatorDaemonTool.ts | 117 +- .../src/tools/wire/WireCollateralTool.ts | 110 +- .../src/tools/wire/WireReserveTool.ts | 675 -------- .../src/tools/wire/WireSyndicationTool.ts | 12 +- .../src/tools/wire/WireUnderwriterTool.ts | 685 --------- packages/cluster-tool/src/tools/wire/index.ts | 2 - .../tests/cli/ClusterBuildOptionsArgs.test.ts | 104 +- .../tests/cli/CreateCommand.test.ts | 18 +- .../tests/clients/wire/WireClient.test.ts | 94 +- .../tests/cluster/ClusterState.test.ts | 16 +- .../cluster/processes/NodeopProcess.test.ts | 38 +- .../config/ClusterConfigProvider.test.ts | 45 +- .../tests/config/NodeConfig.test.ts | 126 +- .../tests/config/clusterConfigFixture.ts | 6 +- .../flow/contexts/SwapScenarioContext.test.ts | 260 ---- .../ClusterBuildDefaultsMockLiqPools.test.ts | 37 +- .../ClusterBuildDefaultsMockReserves.test.ts | 53 - ...lusterBuildDefaultsOperatorFunding.test.ts | 18 +- ...sterBuildDefaultsUnderwriterConfig.test.ts | 84 - .../solana/SolanaOutpostBootstrapper.test.ts | 113 +- .../solana/SolanaOutpostSteps.test.ts | 141 +- .../steps/ExternalClusterConfigSteps.test.ts | 53 +- .../steps/ExternalOutpostSteps.test.ts | 28 +- .../orchestration/steps/KeySteps.test.ts | 49 +- .../orchestration/steps/RegistrySteps.test.ts | 94 +- .../sysio/ReservContractSteps.test.ts | 123 -- .../sysio/UwritContractSteps.test.ts | 38 - .../processes/NodeopProcessSteps.test.ts | 175 ++- .../ethereum/EthereumCollateralTool.test.ts | 93 -- .../ethereum/EthereumFundingTool.test.ts | 24 - .../tools/ethereum/EthereumSwapTool.test.ts | 13 - .../tools/solana/SolanaCollateralTool.test.ts | 92 -- .../tools/wire/OperatorDaemonTool.test.ts | 221 ++- .../tests/tools/wire/WireReserveTool.test.ts | 563 ------- .../tools/wire/WireUnderwriterTool.test.ts | 317 ---- .../tests/local/fixtureCluster.ts | 2 - .../tests/store/cluster/ClusterSlice.test.ts | 2 - .../src/TerminationScenario.ts | 67 +- .../src/EmergencyStopScenario.ts | 2 +- .../src/EmissionsSoakScenarioConstants.ts | 2 +- .../src/LIQSyndicationScenarioConstants.ts | 2 +- .../src/LIQYieldScenarioConstants.ts | 2 +- .../src/NodeOwnerNftScenarioConstants.ts | 5 +- .../steps/NodeOwnerNftScenarioCommitSteps.ts | 11 +- .../steps/NodeOwnerNftScenarioMintSteps.ts | 9 +- .../src/ProducerRegistrationScenario.ts | 4 +- .../ProducerRegistrationScenarioConstants.ts | 4 +- .../tests/producer-collateral.test.cjs | 9 +- packages/flow-reserve-lifecycle/package.json | 22 - .../src/ReserveLifecycleScenario.ts | 646 -------- .../src/ReserveLifecycleScenarioConstants.ts | 151 -- packages/flow-reserve-lifecycle/src/index.ts | 10 - .../ReserveLifecycleScenarioOwnerSteps.ts | 138 -- .../ReserveLifecycleScenarioReserveSteps.ts | 551 ------- packages/flow-reserve-lifecycle/tsconfig.json | 11 - .../flow-reserve-lifecycle/tsconfig.src.json | 14 - packages/flow-swap-from-wire/package.json | 20 - .../src/SwapFromWireScenario.ts | 578 ------- .../src/SwapFromWireScenarioConstants.ts | 88 -- .../src/SwapFromWireScenarioOutputs.ts | 39 - packages/flow-swap-from-wire/src/index.ts | 10 - .../steps/SwapFromWireScenarioUwritSteps.ts | 121 -- .../flow-swap-from-wire/src/steps/index.ts | 1 - packages/flow-swap-from-wire/tsconfig.json | 11 - .../flow-swap-from-wire/tsconfig.src.json | 26 - .../flow-swap-non-native-tokens/package.json | 25 - .../src/SwapNonNativeScenario.ts | 624 -------- .../src/SwapNonNativeScenarioConstants.ts | 144 -- .../flow-swap-non-native-tokens/src/index.ts | 10 - .../steps/SwapNonNativeScenarioTokenSteps.ts | 1185 -------------- .../flow-swap-non-native-tokens/tsconfig.json | 11 - .../tsconfig.src.json | 26 - .../flow-swap-private-reserves/package.json | 26 - .../src/SwapPrivateReservesScenario.ts | 1359 ----------------- .../SwapPrivateReservesScenarioArtifacts.ts | 114 -- .../SwapPrivateReservesScenarioConstants.ts | 194 --- .../src/SwapPrivateReservesScenarioOutputs.ts | 61 - .../flow-swap-private-reserves/src/index.ts | 10 - .../SwapPrivateReservesScenarioOwnerSteps.ts | 206 --- ...SwapPrivateReservesScenarioReserveSteps.ts | 353 ----- .../SwapPrivateReservesScenarioSwapSteps.ts | 235 --- .../flow-swap-private-reserves/tsconfig.json | 11 - .../tsconfig.src.json | 32 - packages/flow-swap-to-wire/package.json | 20 - .../src/SwapToWireScenario.ts | 801 ---------- .../src/SwapToWireScenarioConstants.ts | 123 -- packages/flow-swap-to-wire/src/index.ts | 10 - packages/flow-swap-to-wire/tsconfig.json | 11 - packages/flow-swap-to-wire/tsconfig.src.json | 26 - .../flow-swap-variance-revert/package.json | 23 - .../src/SwapVarianceRevertScenario.ts | 445 ------ .../SwapVarianceRevertScenarioConstants.ts | 114 -- .../flow-swap-variance-revert/src/index.ts | 10 - .../flow-swap-variance-revert/tsconfig.json | 11 - .../tsconfig.src.json | 26 - .../flow-swap-with-underwriting/package.json | 21 - .../src/SwapWithUnderwritingScenario.ts | 603 -------- .../SwapWithUnderwritingScenarioConstants.ts | 153 -- .../flow-swap-with-underwriting/src/index.ts | 10 - .../SwapWithUnderwritingScenarioSwapSteps.ts | 260 ---- .../src/steps/index.ts | 1 - .../flow-swap-with-underwriting/tsconfig.json | 11 - .../tsconfig.src.json | 26 - .../src/SyndicationChallengeScenario.ts | 2 +- .../src/SyndicationRateLimitScenario.ts | 2 +- .../src/SyndicationUnderwritingScenario.ts | 2 +- .../flow-underwriter-slashing/package.json | 21 - .../src/UnderwriterSlashingScenario.ts | 722 --------- .../UnderwriterSlashingScenarioConstants.ts | 118 -- .../src/UnderwriterSlashingScenarioOutputs.ts | 82 - .../flow-underwriter-slashing/src/index.ts | 10 - ...derwriterSlashingScenarioChallengeSteps.ts | 527 ------- .../UnderwriterSlashingScenarioSwapSteps.ts | 124 -- .../src/steps/index.ts | 2 - .../flow-underwriter-slashing/tsconfig.json | 11 - .../tsconfig.src.json | 26 - .../src/YieldDistributionScenarioConstants.ts | 5 +- .../YieldDistributionScenarioEmitSteps.ts | 24 +- tsconfig.json | 8 - 149 files changed, 1188 insertions(+), 18551 deletions(-) delete mode 100644 packages/cluster-tool/src/flow/contexts/SwapScenarioContext.ts delete mode 100644 packages/cluster-tool/src/orchestration/steps/contracts/sysio/ReservContractSteps.ts delete mode 100644 packages/cluster-tool/src/orchestration/steps/contracts/sysio/UwritContractSteps.ts delete mode 100644 packages/cluster-tool/src/tools/ethereum/EthereumCollateralTool.ts delete mode 100644 packages/cluster-tool/src/tools/ethereum/EthereumFundingTool.ts delete mode 100644 packages/cluster-tool/src/tools/ethereum/EthereumSwapTool.ts delete mode 100644 packages/cluster-tool/src/tools/solana/SolanaCollateralTool.ts delete mode 100644 packages/cluster-tool/src/tools/solana/SolanaSwapTool.ts delete mode 100644 packages/cluster-tool/src/tools/wire/WireReserveTool.ts delete mode 100644 packages/cluster-tool/src/tools/wire/WireUnderwriterTool.ts delete mode 100644 packages/cluster-tool/tests/flow/contexts/SwapScenarioContext.test.ts delete mode 100644 packages/cluster-tool/tests/orchestration/ClusterBuildDefaultsMockReserves.test.ts delete mode 100644 packages/cluster-tool/tests/orchestration/ClusterBuildDefaultsUnderwriterConfig.test.ts delete mode 100644 packages/cluster-tool/tests/orchestration/steps/contracts/sysio/ReservContractSteps.test.ts delete mode 100644 packages/cluster-tool/tests/orchestration/steps/contracts/sysio/UwritContractSteps.test.ts delete mode 100644 packages/cluster-tool/tests/tools/ethereum/EthereumCollateralTool.test.ts delete mode 100644 packages/cluster-tool/tests/tools/ethereum/EthereumFundingTool.test.ts delete mode 100644 packages/cluster-tool/tests/tools/ethereum/EthereumSwapTool.test.ts delete mode 100644 packages/cluster-tool/tests/tools/solana/SolanaCollateralTool.test.ts delete mode 100644 packages/cluster-tool/tests/tools/wire/WireReserveTool.test.ts delete mode 100644 packages/cluster-tool/tests/tools/wire/WireUnderwriterTool.test.ts delete mode 100644 packages/flow-reserve-lifecycle/package.json delete mode 100644 packages/flow-reserve-lifecycle/src/ReserveLifecycleScenario.ts delete mode 100644 packages/flow-reserve-lifecycle/src/ReserveLifecycleScenarioConstants.ts delete mode 100644 packages/flow-reserve-lifecycle/src/index.ts delete mode 100644 packages/flow-reserve-lifecycle/src/steps/ReserveLifecycleScenarioOwnerSteps.ts delete mode 100644 packages/flow-reserve-lifecycle/src/steps/ReserveLifecycleScenarioReserveSteps.ts delete mode 100644 packages/flow-reserve-lifecycle/tsconfig.json delete mode 100644 packages/flow-reserve-lifecycle/tsconfig.src.json delete mode 100644 packages/flow-swap-from-wire/package.json delete mode 100644 packages/flow-swap-from-wire/src/SwapFromWireScenario.ts delete mode 100644 packages/flow-swap-from-wire/src/SwapFromWireScenarioConstants.ts delete mode 100644 packages/flow-swap-from-wire/src/SwapFromWireScenarioOutputs.ts delete mode 100644 packages/flow-swap-from-wire/src/index.ts delete mode 100644 packages/flow-swap-from-wire/src/steps/SwapFromWireScenarioUwritSteps.ts delete mode 100644 packages/flow-swap-from-wire/src/steps/index.ts delete mode 100644 packages/flow-swap-from-wire/tsconfig.json delete mode 100644 packages/flow-swap-from-wire/tsconfig.src.json delete mode 100644 packages/flow-swap-non-native-tokens/package.json delete mode 100644 packages/flow-swap-non-native-tokens/src/SwapNonNativeScenario.ts delete mode 100644 packages/flow-swap-non-native-tokens/src/SwapNonNativeScenarioConstants.ts delete mode 100644 packages/flow-swap-non-native-tokens/src/index.ts delete mode 100644 packages/flow-swap-non-native-tokens/src/steps/SwapNonNativeScenarioTokenSteps.ts delete mode 100644 packages/flow-swap-non-native-tokens/tsconfig.json delete mode 100644 packages/flow-swap-non-native-tokens/tsconfig.src.json delete mode 100644 packages/flow-swap-private-reserves/package.json delete mode 100644 packages/flow-swap-private-reserves/src/SwapPrivateReservesScenario.ts delete mode 100644 packages/flow-swap-private-reserves/src/SwapPrivateReservesScenarioArtifacts.ts delete mode 100644 packages/flow-swap-private-reserves/src/SwapPrivateReservesScenarioConstants.ts delete mode 100644 packages/flow-swap-private-reserves/src/SwapPrivateReservesScenarioOutputs.ts delete mode 100644 packages/flow-swap-private-reserves/src/index.ts delete mode 100644 packages/flow-swap-private-reserves/src/steps/SwapPrivateReservesScenarioOwnerSteps.ts delete mode 100644 packages/flow-swap-private-reserves/src/steps/SwapPrivateReservesScenarioReserveSteps.ts delete mode 100644 packages/flow-swap-private-reserves/src/steps/SwapPrivateReservesScenarioSwapSteps.ts delete mode 100644 packages/flow-swap-private-reserves/tsconfig.json delete mode 100644 packages/flow-swap-private-reserves/tsconfig.src.json delete mode 100644 packages/flow-swap-to-wire/package.json delete mode 100644 packages/flow-swap-to-wire/src/SwapToWireScenario.ts delete mode 100644 packages/flow-swap-to-wire/src/SwapToWireScenarioConstants.ts delete mode 100644 packages/flow-swap-to-wire/src/index.ts delete mode 100644 packages/flow-swap-to-wire/tsconfig.json delete mode 100644 packages/flow-swap-to-wire/tsconfig.src.json delete mode 100644 packages/flow-swap-variance-revert/package.json delete mode 100644 packages/flow-swap-variance-revert/src/SwapVarianceRevertScenario.ts delete mode 100644 packages/flow-swap-variance-revert/src/SwapVarianceRevertScenarioConstants.ts delete mode 100644 packages/flow-swap-variance-revert/src/index.ts delete mode 100644 packages/flow-swap-variance-revert/tsconfig.json delete mode 100644 packages/flow-swap-variance-revert/tsconfig.src.json delete mode 100644 packages/flow-swap-with-underwriting/package.json delete mode 100644 packages/flow-swap-with-underwriting/src/SwapWithUnderwritingScenario.ts delete mode 100644 packages/flow-swap-with-underwriting/src/SwapWithUnderwritingScenarioConstants.ts delete mode 100644 packages/flow-swap-with-underwriting/src/index.ts delete mode 100644 packages/flow-swap-with-underwriting/src/steps/SwapWithUnderwritingScenarioSwapSteps.ts delete mode 100644 packages/flow-swap-with-underwriting/src/steps/index.ts delete mode 100644 packages/flow-swap-with-underwriting/tsconfig.json delete mode 100644 packages/flow-swap-with-underwriting/tsconfig.src.json delete mode 100644 packages/flow-underwriter-slashing/package.json delete mode 100644 packages/flow-underwriter-slashing/src/UnderwriterSlashingScenario.ts delete mode 100644 packages/flow-underwriter-slashing/src/UnderwriterSlashingScenarioConstants.ts delete mode 100644 packages/flow-underwriter-slashing/src/UnderwriterSlashingScenarioOutputs.ts delete mode 100644 packages/flow-underwriter-slashing/src/index.ts delete mode 100644 packages/flow-underwriter-slashing/src/steps/UnderwriterSlashingScenarioChallengeSteps.ts delete mode 100644 packages/flow-underwriter-slashing/src/steps/UnderwriterSlashingScenarioSwapSteps.ts delete mode 100644 packages/flow-underwriter-slashing/src/steps/index.ts delete mode 100644 packages/flow-underwriter-slashing/tsconfig.json delete mode 100644 packages/flow-underwriter-slashing/tsconfig.src.json diff --git a/CLAUDE.md b/CLAUDE.md index e577e234b..95a55b526 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -89,7 +89,7 @@ pnpm workspaces (no nx/turbo/lerna). All packages under `packages/`: |---------|---------| | `cluster-tool` (`@wireio/cluster-tool`) | THE core library: orchestration engine (PhaseGroup → Phase → Step → Report), process managers, chain clients, config/bind resolution, Steps palette, flow substrate (`FlowCLI`/`FlowScenario`), CLI | | `cluster-tool-shared` (`@wireio/cluster-tool-shared`) | Zod schema-first persisted shapes (`ClusterConfig`, `BindConfig`, `ClusterState`, `SignatureProviderConfig`, `ExternalOutpostConfig`, `ExternalClusterConfig`, `ChainTokenAmount`, `QueryEngineConfig`) behind the generic `SchemaCodec` (validate-both-ends serialize/deserialize) | -| `flow-*` (21 packages; 13 enabled, 8 disabled at launch) | One scenario each — standalone executables built on `FlowCLI.create(Scenario).run()`; batch-operator lifecycle (slashing/termination), collateral, reserves, emissions soak, node-owner NFT, yield distribution, liq syndication, liq yield, and the six swap variants | +| `flow-*` (13 enabled packages) | One scenario each — standalone executables built on `FlowCLI.create(Scenario).run()`; batch-operator lifecycle, depot collateral, emissions, node-owner NFT, yield, LIQ syndication and underwriting, and emergency stop | | `debugging-shared` / `debugging-server` / `debugging-client-shared` / `debugging-client-tool` / `debugging-client-tool-tui` | OPP debugging surface: shared types + storage paths, ingest server, RPC client, CLI, TUI | | `test-app-server` | Fixture app server used by debugging tests | @@ -300,13 +300,7 @@ secret-id pattern), plus `--external-outpost-config ` (bootstrap the depot against already-deployed REMOTE ETH+SOL outposts — no local anvil/validator), `--bind-config ` (a complete `BindConfig` used verbatim, or a partial override merged over the resolved defaults; a remote anvil/solana address -requires `--external-outpost-config`), `--enable-mock-reserves` (default off — -seed the 8 mock (chain, token) PRIMARY reserves at bootstrap; a real / external -depot leaves these unseeded), `--enable-launch-withheld-operations` (default -off — run the Solana outpost bootstrap calls the launch build of the program -withholds with `OperationDisabled`: `init_reserve`, `create_reserve_native` and the -mock SPL reserve provisioning that writes `sol-mock-mints.json`; a default cluster -skips them), `--enable-mock-liq-pools` (default off — seed the +requires `--external-outpost-config`), `--enable-mock-liq-pools` (default off — seed the 2 mock shadow-liq yield pools on `sysio.swap` and back the shadow in outpost custody), `--enable-mock-syndication-import` (default off — import the mock bonder's LIQSOL/LIQETH positions during epoch zero and back all mock shadow), `--api-count ` (default 0 — plan N API nodes: @@ -357,14 +351,8 @@ inline signing key (the GHA workflow is SSM-only, so published archives do not). the SAME `applyClusterBuildOptionsArgs` surface every flow uses (env vars `WIRE_*` seed the path flags). Exit code mirrors the bootstrap Report. -**Flow authoring — mock reserves, liq pools and syndication import.** A flow that reads the mock -(chain, token) PRIMARY reserves sets `enableMockReserves: true` in its -`Scenario.defaults` (the same mechanism it uses for `operatorsPerEpoch` / -collateral) — never in `plan()`. The bootstrap seeds them during epoch 0; a -flow's `plan()` phases always run AFTER `EpochBootstrap` advances epoch 0→1, and -the depot gates `regreserve` to epoch 0, so `regreserve` can never be called from -a flow phase. The mock shadow-liq yield pools (`enableMockLiqPools: true`, -`sysio.liq::regliqpool`) follow the same rule for the same reason. The shadow +**Flow authoring — mock LIQ pools and syndication import.** The mock shadow-liq yield pools (`enableMockLiqPools: true`, +`sysio.liq::regliqpool`) must be seeded during epoch zero through scenario defaults, before flow phases run. The shadow symbols themselves (`sysio.liq::create`, one per registered liq token), the swap's `setconfig` and the kicker are registry setup a real depot performs too, so the bootstrap does those unconditionally. A flow needing the first bonder sets @@ -401,37 +389,9 @@ are `SolanaLiqSyndicationTool` (`planSetFrozen`, `planDonateToPool`, PDA) and `EthereumSyndicationTool` (`planSyndicate`, `planDonateToPool`, `planSetPaused`, `planPayPendingDesyndication`). -**Flow authoring — launch-withheld operations.** `enableLaunchWithheldOperations: -true` makes the bootstrap RUN the Solana outpost calls the launch build of the -program withholds (`init_reserve`, `create_reserve_native`, the mock SPL reserve -provisioning), so a flow that sets it requires a Solana program build without the -launch restrictions; against the launch build those calls return -`OperationDisabled` (6086). A flow sets it in its `Scenario.defaults` — never in -`plan()` — when its scenario needs the bootstrap-seeded Solana reserves or the mock -SPL mints. The flag does not decide which flows the launch program can run: the -eight that set it are **disabled at launch** (`flow-reserve-lifecycle`, `flow-swap-from-wire`, -`flow-swap-non-native-tokens`, `flow-swap-private-reserves`, `flow-swap-to-wire`, -`flow-swap-variance-revert`, `flow-swap-with-underwriting`, -`flow-underwriter-slashing`) need Solana operations the launch build withholds, and -`flow-yield-distribution` injects attestations through the withheld -`add_attestation` instruction — it does not set the flag, which governs only -bootstrap calls. The collateral, producer-registration and batch-operator-termination -flows bond on the depot, do not set the flag, and run against the launch program. - -Each of the eight disabled flows opts into `enableLaunchWithheldOperations: true` -and exercises -reserves/swaps, which the launch Solana program refuses: wire-solana `89565920` -("Disable unaudited reserve and swap operations") returns `OperationDisabled` -6086 at `InitReserve` during bootstrap. There is no launch replacement for these -operations; they return when reserves/swaps are enabled after audit. - -Their `package.json` live-flow script is named `test:disabled` instead of `test`. -Both local `scripts/run-flow.mjs` discovery (including exact names, short names, -regex and the picker) and CI's `run-flows.mjs` require a `test` script, so these -packages are skipped. To re-enable a flow after audit, rename `test:disabled` -back to `test` in its `package.json` (one line per flow). All packages remain in -`pnpm build`; root `pnpm test` still runs Jest and every `test:unit` script. -These eight packages currently have no unit tests. +**Removed functionality.** The eight reserve/swap flow packages and their bootstrap +flags have been deleted. Only batch operators run OPP daemons. Syndication bond +providers act through `sysio.bond`; they do not run swap-underwriter daemons. **Flow authoring — API nodes.** A flow that needs an API node sets `apiCount` (and, if needed, `queryEngine`) in its `Scenario.defaults`; the nodes start in diff --git a/README.md b/README.md index 62f3e04db..8adbcfd20 100644 --- a/README.md +++ b/README.md @@ -131,19 +131,11 @@ 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-swap-with-underwriting` | `@wireio/test-flow-swap-with-underwriting` | **Disabled at launch (reserves/swaps withheld)** — Bidirectional SWAP (ETH ↔ SOL) with underwriting | -| `flow-swap-non-native-tokens` | `@wireio/test-flow-swap-non-native-tokens` | **Disabled at launch (reserves/swaps withheld)** — SWAP of non-native tokens (USDC / USDT / LIQ) | -| `flow-swap-variance-revert` | `@wireio/test-flow-swap-variance-revert` | **Disabled at launch (reserves/swaps withheld)** — Swap variance-tolerance revert | | `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 | -| `flow-reserve-lifecycle` | `@wireio/test-flow-reserve-lifecycle` | **Disabled at launch (reserves/swaps withheld)** | -| `flow-swap-from-wire` | `@wireio/test-flow-swap-from-wire` | **Disabled at launch (reserves/swaps withheld)** | -| `flow-swap-to-wire` | `@wireio/test-flow-swap-to-wire` | **Disabled at launch (reserves/swaps withheld)** | -| `flow-swap-private-reserves` | `@wireio/test-flow-swap-private-reserves` | **Disabled at launch (reserves/swaps withheld)** | -| `flow-underwriter-slashing` | `@wireio/test-flow-underwriter-slashing` | **Disabled at launch (reserves/swaps withheld)** | | `debugging-*` / `test-app-server` | `@wireio/debugging-*` | OPP debugging server, client tooling, TUI, shared types | Flow packages depend on the harness via `workspace:*`. @@ -169,7 +161,7 @@ The other seven enabled flows (`flow-batch-operator-slashing`, `flow-operator-collateral-deposit`, `flow-producer-registration`, `flow-yield-distribution`) submit no user LIQ syndication/desyndication. Imported bootstrap collateral and reward funding have their own assertions. The eight -disabled swap/reserve flows remain disabled and have no direct LIQ syndication call. +former disabled swap/reserve flows have been removed. `WireSyndicationTool.planResolveEnvelope` is an explicit `sysio.bond::rslvvalid` shortcut for an unbonded OPEN request. It refuses provider-funded, challenged or @@ -197,22 +189,11 @@ payout after the existing queued-redemption assertions. TypeScript compilation, stopped at the bootstrap toolchain check because the shell selected Solana 4.0.3; the successful runs above selected the installed pinned 4.2.0 toolchain explicitly. -### Disabled reserve and swap flows +### Removed reserve and swap flows -Each of the eight disabled flows opts into `enableLaunchWithheldOperations: true` -and exercises -reserves/swaps, which the launch Solana program refuses: wire-solana `89565920` -("Disable unaudited reserve and swap operations") returns `OperationDisabled` -6086 at `InitReserve` during bootstrap. There is no launch replacement for these -operations; they return when reserves/swaps are enabled after audit. - -Their `package.json` live-flow script is named `test:disabled` instead of `test`. -Both local `scripts/run-flow.mjs` discovery (including exact names, short names, -regex and the picker) and CI's `run-flows.mjs` require a `test` script, so these -packages are skipped. To re-enable a flow after audit, rename `test:disabled` -back to `test` in its `package.json` (one line per flow). All packages remain in -`pnpm build`; root `pnpm test` still runs Jest and every `test:unit` script. -These eight packages currently have no unit tests. +The eight former reserve/swap flow packages, their dedicated helpers and bootstrap +flags have been deleted. The 13 remaining flows exercise supported functionality. +Syndication underwriting uses depot bond providers, without swap-underwriter daemons. ## Running flows @@ -418,7 +399,7 @@ wire-cluster-tool create \ --producer-count=5 \ --node-count=1 \ --batch-operator-count=3 \ - --underwriter-count=1 \ + --underwriter-count=0 \ --epoch-duration-sec=60 \ --ethereum-path=/data/shared/code/wire-platform/wire-ethereum \ --solana-path=/data/shared/code/wire-platform/wire-solana \ @@ -486,7 +467,7 @@ command comes first). | `--node-count` | `-n` | `1` | producer node **processes** to launch | | `--producer-count` | `-p` | `1` | producer **accounts** to register on-chain | | `--batch-operator-count` | `-b` | `3` | batch operators | -| `--underwriter-count` | `-u` | `1` | underwriters | +| `--underwriter-count` | `-u` | `0` | compatibility option; nonzero values are rejected | | `--epoch-duration-sec` | | `60` | minimum epoch duration in seconds (the depot floor — `sysio.epoch::setconfig` rejects lower) | | `--warmup-epochs` | | `1` | epochs before an operator goes `WARMUP` → `ACTIVE` | | `--cooldown-epochs` | | `1` | epochs before an operator can deregister after `COOLDOWN` | @@ -495,8 +476,6 @@ command comes first). | `--terminate-max-percent-misses24h` | | — | 24h missed-delivery percentage termination threshold | | `--terminate-window-ms` | | — | termination evaluation window in ms | | `--bind-all` | | `false` | bind every daemon to `0.0.0.0` instead of loopback | -| `--enable-mock-reserves` | | `false` | seed the 8 mock (chain, token) PRIMARY reserves at bootstrap | -| `--enable-launch-withheld-operations` | | `false` | run the Solana outpost bootstrap calls the launch build withholds (`init_reserve`, `create_reserve_native`, mock SPL reserves); a flow that sets it (in its scenario defaults, when it needs the bootstrap-seeded Solana reserves or the mock SPL mints) requires a Solana program build without the launch restrictions, which answer these calls with `OperationDisabled` (6086). The flag does not decide which flows the launch program can run: the eight opted-in flows need withheld Solana operations, and `flow-yield-distribution` needs the withheld `add_attestation` instruction without opting in (see CLAUDE.md, "Flow authoring — launch-withheld operations") | | `--enable-mock-liq-pools` | | `false` | seed the mock LIQETH/LIQSOL yield pools during epoch zero and fund outpost custody to back all outstanding shadow | | `--enable-mock-syndication-import` | | `false` | import the mock bonder's LIQSOL and LIQETH positions during epoch zero, seal import, and back all mock shadow in outpost custody | | `--api-count` | | `0` | API nodes — non-producing nodeops meshed with bios + producers, serving `/v1/chain/*` and the query engine's `POST /v1/query/execute`; never `producer_api_plugin` | @@ -575,11 +554,8 @@ config; the Solana program id is parsed from the IDL): } ``` -External mode ALSO requires an EXPLICIT `--underwriter-count 0`. The flag -defaults to `1`, so omitting it asks for one underwriter — and an external -cluster has no local outpost for an underwriter to bond collateral on, so -`create` fails fast naming whichever cause applies ("you asked for N" vs "you -omitted it and got the default"). +Swap-underwriter daemon counts default to zero in all deployment modes. Nonzero +values are rejected; syndication bond providers do not need a daemon. At `create` the harness verifies the external endpoints are reachable (`eth_chainId` matches the configured `chainId`; Solana `getVersion` responds) diff --git a/packages/cluster-tool-shared/src/config/ClusterConfig.ts b/packages/cluster-tool-shared/src/config/ClusterConfig.ts index 490ecff16..198bee177 100644 --- a/packages/cluster-tool-shared/src/config/ClusterConfig.ts +++ b/packages/cluster-tool-shared/src/config/ClusterConfig.ts @@ -278,23 +278,6 @@ export const ClusterConfigSchema = z.object({ * `create-external-config --no-debugging-server` (run without a debugging server). */ debuggingServerEnabled: z.boolean().default(true), - /** - * Whether the bootstrap seeds the 8 mock (chain, token) PRIMARY reserves - * (the `--enable-mock-reserves` create flag). Schema-defaulted `false` so - * pre-existing configs — and every real/external depot — stay reserve-free - * unless a caller (or a flow's scenario defaults) opts in. - */ - enableMockReserves: z.boolean().default(false), - /** - * Whether the Solana outpost bootstrap runs the calls the launch policy - * withholds (the `--enable-launch-withheld-operations` create flag): - * `init_reserve`, `create_reserve_native` and the mock SPL reserve - * provisioning. Schema-defaulted `false` so pre-existing configs — and every - * default cluster — never call them (the launch build of the program answers - * each with `OperationDisabled`) unless a flow's scenario defaults opt in — which - * requires a Solana program build without the launch restrictions. - */ - enableLaunchWithheldOperations: z.boolean().default(false), /** * Whether the bootstrap seeds the 2 mock shadow-liq yield pools on * `sysio.swap` (the `--enable-mock-liq-pools` create flag). Schema-defaulted diff --git a/packages/cluster-tool-shared/tests/config/ClusterConfig.test.ts b/packages/cluster-tool-shared/tests/config/ClusterConfig.test.ts index ad6230338..627cd0f77 100644 --- a/packages/cluster-tool-shared/tests/config/ClusterConfig.test.ts +++ b/packages/cluster-tool-shared/tests/config/ClusterConfig.test.ts @@ -90,8 +90,6 @@ describe("ClusterConfig shape", () => { awsClusterNodeConfig: null, externalOutposts: null, debuggingServerEnabled: true, - enableMockReserves: false, - enableLaunchWithheldOperations: false, enableMockLiqPools: false, enableMockSyndicationImport: false, deploymentKind: ClusterDeploymentKind.local, @@ -126,12 +124,17 @@ describe("ClusterConfig shape", () => { it("persists explicit mock syndication opt-in and rejects a non-boolean flag", () => { const enabled = ClusterConfigSchemaCodec.deserialize( - ClusterConfigSchemaCodec.serialize({ ...config, enableMockSyndicationImport: true }) + ClusterConfigSchemaCodec.serialize({ + ...config, + enableMockSyndicationImport: true + }) ) expect(enabled.enableMockSyndicationImport).toBe(true) const parsed = JSON.parse(ClusterConfigSchemaCodec.serialize(config)) parsed.enableMockSyndicationImport = "true" - expect(() => ClusterConfigSchemaCodec.deserialize(JSON.stringify(parsed))).toThrow() + expect(() => + ClusterConfigSchemaCodec.deserialize(JSON.stringify(parsed)) + ).toThrow() }) it("loads a legacy config (no signatureProvider/awsClusterNodeConfig/externalOutposts/debuggingServerEnabled/enableMockReserves/enableLaunchWithheldOperations/enableMockLiqPools/deploymentKind/chainStateDbSizeMb) via schema defaults", () => { @@ -140,8 +143,6 @@ describe("ClusterConfig shape", () => { delete parsed.awsClusterNodeConfig delete parsed.externalOutposts delete parsed.debuggingServerEnabled - delete parsed.enableMockReserves - delete parsed.enableLaunchWithheldOperations delete parsed.enableMockSyndicationImport delete parsed.enableMockLiqPools delete parsed.deploymentKind @@ -156,8 +157,6 @@ describe("ClusterConfig shape", () => { expect(rehydrated.awsClusterNodeConfig).toBeNull() expect(rehydrated.externalOutposts).toBeNull() expect(rehydrated.debuggingServerEnabled).toBe(true) - expect(rehydrated.enableMockReserves).toBe(false) - expect(rehydrated.enableLaunchWithheldOperations).toBe(false) expect(rehydrated.enableMockLiqPools).toBe(false) expect(rehydrated.enableMockSyndicationImport).toBe(false) // A config predating either field loads as the CREATE shape: trace_api on diff --git a/packages/cluster-tool/src/Constants.ts b/packages/cluster-tool/src/Constants.ts index 8a5b5f1ba..74b52e93b 100644 --- a/packages/cluster-tool/src/Constants.ts +++ b/packages/cluster-tool/src/Constants.ts @@ -1,5 +1,5 @@ import { Option } from "@3fv/prelude-ts" -import { Hash, KeyType, PrivateKey } from "@wireio/sdk-core" +import { Hash, KeyType, PrivateKey, SlugName } from "@wireio/sdk-core" import type { QueryEngineLimitKey } from "@wireio/cluster-tool-shared" @@ -11,6 +11,10 @@ import type { QueryEngineLimitKey } from "@wireio/cluster-tool-shared" * `config/BindConfigProvider.ts`, which owns binding. */ export namespace Constants { + /** Depot asset frame shared by native collateral and syndication. */ + export const DepotPrecisionCap = 9 + export const WireChainCode = Number(SlugName.from("WIRE")) + export const WireTokenCode = WireChainCode /** Seed for the deterministic dev K1 key pair (matches the genesis bios key). */ const DefaultK1KeyPairSeed = "nathan" @@ -113,8 +117,6 @@ export namespace Constants { "sysio.epoch", "sysio.opreg", "sysio.msgch", - "sysio.uwrit", - "sysio.reserv", "sysio.chalg", "sysio.dclaim", "sysio.swap", @@ -329,8 +331,6 @@ export namespace Constants { "sysio.epoch": "contracts/sysio.epoch", "sysio.opreg": "contracts/sysio.opreg", "sysio.msgch": "contracts/sysio.msgch", - "sysio.uwrit": "contracts/sysio.uwrit", - "sysio.reserv": "contracts/sysio.reserv", "sysio.chalg": "contracts/sysio.chalg", "sysio.dclaim": "contracts/sysio.dclaim", "sysio.swap": "contracts/sysio.swap", @@ -353,8 +353,6 @@ export namespace Constants { "sysio.epoch", "sysio.opreg", "sysio.msgch", - "sysio.uwrit", - "sysio.reserv", "sysio.chalg", "sysio.dclaim", "sysio.swap", @@ -372,13 +370,6 @@ export namespace Constants { "sysio::cron_plugin" ] as const - /** Plugins loaded on an underwriter node. */ - export const UNDERWRITER_PLUGINS = [ - "sysio::underwriter_plugin", - "sysio::outpost_ethereum_client_plugin", - "sysio::outpost_solana_client_plugin" - ] as const - /** Lowercase ASCII alphabet — single-character operator-account suffixes (wraps via modulo). */ const LowercaseAlphabet = "abcdefghijklmnopqrstuvwxyz" diff --git a/packages/cluster-tool/src/cli/ClusterBuildOptionsArgs.ts b/packages/cluster-tool/src/cli/ClusterBuildOptionsArgs.ts index 74777f379..65880ede1 100644 --- a/packages/cluster-tool/src/cli/ClusterBuildOptionsArgs.ts +++ b/packages/cluster-tool/src/cli/ClusterBuildOptionsArgs.ts @@ -44,7 +44,7 @@ const CliDefault = { nodeCount: 1, producerCount: 1, batchOperatorCount: 3, - underwriterCount: 1, + underwriterCount: 0, apiCount: 0, adHocCount: 0, epochDurationSec: 60 @@ -124,9 +124,7 @@ export class OptionLeafSpec { * walk. */ export type OptionShapeNode = - | OptionLeafSpec - | OptionShapeNode[] - | OptionShapeObject + OptionLeafSpec | OptionShapeNode[] | OptionShapeObject /** A nested object of shape nodes (named — no inline object types). */ export interface OptionShapeObject { @@ -546,16 +544,6 @@ export function buildOptionShape( ), // ── network binding ── bindAll: leaf(false, "bind every daemon to 0.0.0.0 instead of loopback"), - // ── mock data seeding (default false → external / real depots get no fake reserves) ── - enableMockReserves: leaf( - false, - "seed the 8 mock (chain, token) PRIMARY reserves at bootstrap" - ), - // ── launch policy (default false → the Solana outpost bootstrap skips the calls the launch build withholds) ── - enableLaunchWithheldOperations: leaf( - false, - "run the Solana outpost bootstrap calls the launch policy withholds (init_reserve, create_reserve_native, mock SPL reserves)" - ), enableMockLiqPools: leaf( false, "seed the 2 mock shadow-liq yield pools (LIQETH, LIQSOL) at bootstrap" @@ -712,7 +700,9 @@ export function applyClusterBuildOptionsArgs( environmentPathDefaults(environment), defaults ) - const withShape = flattenOptionLeaves(buildOptionShape(seededDefaults)).reduce( + const withShape = flattenOptionLeaves( + buildOptionShape(seededDefaults) + ).reduce( (instance, optionLeaf) => instance.option( optionLeaf.flag, @@ -821,10 +811,7 @@ function isIndexSegment(segment: string): boolean { } /** Read a child by segment (arrays accept numeric-string keys uniformly). */ -function childOf( - node: OptionTreeContainer, - segment: string -): OptionTreeValue { +function childOf(node: OptionTreeContainer, segment: string): OptionTreeValue { return (node as OptionTreeObject)[segment] ?? null } @@ -1043,8 +1030,7 @@ function assertDocumentLeafValue( `${documentLabel(file)}: "${optionLeaf.path.join(".")}" must be a ${optionLeaf.type} (got ${documentLeafType(scalar)} ${JSON.stringify(scalar)})` ) Assert.ok( - optionLeaf.choices == null || - optionLeaf.choices.includes(String(scalar)), + optionLeaf.choices == null || optionLeaf.choices.includes(String(scalar)), `${documentLabel(file)}: "${optionLeaf.path.join(".")}" must be one of ${optionLeaf.choices?.join(" | ")} (got ${JSON.stringify(scalar)})` ) return scalar @@ -1342,9 +1328,7 @@ export function mergeSignatureProviderSSM( * @param argv - The parsed yargs result. * @returns The validated AWS placement, or `null`. */ -export function toAWSClusterNodeConfig( - argv: OptionArgv -): AWSClusterNodeConfig { +export function toAWSClusterNodeConfig(argv: OptionArgv): AWSClusterNodeConfig { const raw = readArg(argv, AWSClusterNodeConfigFlag) if (!isString(raw) || raw.trim().length === 0) { return null diff --git a/packages/cluster-tool/src/clients/wire/WireClient.ts b/packages/cluster-tool/src/clients/wire/WireClient.ts index 630a45b53..e6c7e1f6e 100644 --- a/packages/cluster-tool/src/clients/wire/WireClient.ts +++ b/packages/cluster-tool/src/clients/wire/WireClient.ts @@ -372,7 +372,9 @@ export class WireClient { `get_supported_protocol_features failed: ${response.statusText}` ) const features = await response.json() - return Array.isArray(features) ? (features as WireClient.ProtocolFeature[]) : [] + return Array.isArray(features) + ? (features as WireClient.ProtocolFeature[]) + : [] } // ── RPC getters (v6 `.value` unwrap retained) ──────────────────────────── @@ -412,7 +414,9 @@ export class WireClient { asOption(entry) .map(present => ({ version: Number(present.version), - producers: present.producers.map(producer => String(producer.producer_name)) + producers: present.producers.map(producer => + String(producer.producer_name) + ) })) .getOrUndefined() return { @@ -459,35 +463,6 @@ export class WireClient { return BigInt(whole) * 1_000_000_000n + BigInt(frac.padEnd(9, "0")) } - /** - * Pull `account`'s claimable WIRE from `sysio.reserv` — swap-to-WIRE payouts and swap-from-WIRE - * refunds. - * - * Those settlement paths credit a balance instead of transferring: `sysio.token::transfer` - * notifies its recipient, and the chain runs notified receivers with no exception isolation, so - * a pushed payout let the recipient abort the enclosing transaction — which for `refundwire` - * meant halting the epoch drain chain-wide. The claim carries the claimant's own authority, so a - * hostile recipient can only block itself. - * - * Throws when nothing is owed; check {@link getWireClaimable} first if that is not a failure. - */ - async claimWire(account: string, permission = WireClient.ActivePermission) { - return this.invoke("sysio.reserv", "claimwire", { account }, [{ actor: account, permission }]) - } - - /** - * WIRE owed to `account` but not yet claimed, or 0n when there is no row. - * - * Raw `getTableRows` rather than the typed contract-table accessor - * (`prefer-typed-contract-table-accessors.md`) because `wireclaims` is new and does not reach - * the typed surface until `@wireio/sdk-core` publishes the regenerated `SysioContractTypes`. - * Switch to `getSysioContract(SysioContractName.reserv).tables.wireclaims.query()` once that - * version is released and this package's dependency is bumped. - */ - async getWireClaimable(account: string): Promise { - return this.claimableBalance("sysio.reserv", "wireclaims", "account", account) - } - /** * One claimable row's balance, or 0n when the account has none. * @@ -545,40 +520,52 @@ export class WireClient { // Convenience getters delegate to the typed contract-table accessor // (prefer-typed-contract-table-accessors.md) — never a raw getTableRows. getOperators() { - return this.getSysioContract(SysioContractName.opreg).tables.operators.query() + return this.getSysioContract( + SysioContractName.opreg + ).tables.operators.query() } getProducers() { - return this.getSysioContract(SysioContractName.system).tables.producers.query() + return this.getSysioContract( + SysioContractName.system + ).tables.producers.query() } getWithdrawQueue() { - return this.getSysioContract(SysioContractName.opreg).tables.wtdwqueue.query() + return this.getSysioContract( + SysioContractName.opreg + ).tables.wtdwqueue.query() } getEpochState() { - return this.getSysioContract(SysioContractName.epoch).tables.epochstate.query() + return this.getSysioContract( + SysioContractName.epoch + ).tables.epochstate.query() } getEpochConfig() { - return this.getSysioContract(SysioContractName.epoch).tables.epochcfg.query() + return this.getSysioContract( + SysioContractName.epoch + ).tables.epochcfg.query() } getChains() { return this.getSysioContract(SysioContractName.chains).tables.chains.query() } getMessages() { - return this.getSysioContract(SysioContractName.msgch).tables.messages.query() + return this.getSysioContract( + SysioContractName.msgch + ).tables.messages.query() } getEnvelopes() { - return this.getSysioContract(SysioContractName.msgch).tables.envelopes.query() + return this.getSysioContract( + SysioContractName.msgch + ).tables.envelopes.query() } getAttestations() { - return this.getSysioContract(SysioContractName.msgch).tables.attestations.query() + return this.getSysioContract( + SysioContractName.msgch + ).tables.attestations.query() } getOutboundEnvelopes() { - return this.getSysioContract(SysioContractName.msgch).tables.outenvelopes.query() - } - getUwRequests() { - return this.getSysioContract(SysioContractName.uwrit).tables.uwreqs.query() - } - getLocks() { - return this.getSysioContract(SysioContractName.uwrit).tables.locks.query() + return this.getSysioContract( + SysioContractName.msgch + ).tables.outenvelopes.query() } /** Raw `clio get table` (positional account+table, scope via -S). */ @@ -594,7 +581,9 @@ export class WireClient { } /** Fetch a block by number/id via /v1/chain/get_block. */ - async getBlock(blockNumOrId: number | string): Promise { + async getBlock( + blockNumOrId: number | string + ): Promise { const resp = await fetch(`${this.config.nodeopUrl}/v1/chain/get_block`, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -606,9 +595,7 @@ export class WireClient { } /** Fetch a transaction trace via /v1/trace_api/get_transaction_trace. */ - async getTransaction( - id: string - ): Promise { + async getTransaction(id: string): Promise { const resp = await fetch( `${this.config.nodeopUrl}/v1/trace_api/get_transaction_trace`, { @@ -1017,7 +1004,10 @@ export namespace WireClient { * does not exist. Same JSON-object encoding as {@link nameKeyBound}; a bare code string fails * at parse time exactly like a bare account name does. */ - export function symbolCodeKeyRange(field: string, code: string): TableQueryArgs { + export function symbolCodeKeyRange( + field: string, + code: string + ): TableQueryArgs { const { value } = Asset.SymbolCode.from(code) return { lowerBound: JSON.stringify({ [field]: value.toString() }), @@ -1049,7 +1039,8 @@ export namespace WireClient { skipWait?: boolean finality?: FinalityType } - export type ContractOf = SysioContractMapping[Name] + export type ContractOf = + SysioContractMapping[Name] export type ActionName = Extract< keyof ContractOf["actions"], string @@ -1104,7 +1095,9 @@ export namespace WireClient { Name extends SysioContractName, Table extends TableName > { - query(args?: TableQueryArgs): Promise>> + query( + args?: TableQueryArgs + ): Promise>> } export interface SysioContractClient { readonly actions: { @@ -1194,10 +1187,12 @@ export namespace WireClient { match(result.kind) .with( FinalityOutcome.unappliable, - () => `${label}: transaction can never apply (expiration window closed while absent)` + () => + `${label}: transaction can never apply (expiration window closed while absent)` ) .otherwise( - () => `${label}: finality unresolved — NOT re-pushed (the transaction may be applied)` + () => + `${label}: finality unresolved — NOT re-pushed (the transaction may be applied)` ), { context: { ...result } } ) diff --git a/packages/cluster-tool/src/config/BindConfigProvider.ts b/packages/cluster-tool/src/config/BindConfigProvider.ts index 9b6436348..60fb51399 100644 --- a/packages/cluster-tool/src/config/BindConfigProvider.ts +++ b/packages/cluster-tool/src/config/BindConfigProvider.ts @@ -208,7 +208,7 @@ export namespace BindConfigProvider { */ export const DefaultAdHocCount = 0 export const DefaultBatchCount = 3 - export const DefaultUnderwriterCount = 1 + export const DefaultUnderwriterCount = 0 /** * API-node pairs claimed when a caller names no count — none. Raising it * makes every cluster and flow that omits `apiCount` plan that many API nodes. diff --git a/packages/cluster-tool/src/config/ClusterBuildOptions.ts b/packages/cluster-tool/src/config/ClusterBuildOptions.ts index 3c941ef64..a7f37f2a7 100644 --- a/packages/cluster-tool/src/config/ClusterBuildOptions.ts +++ b/packages/cluster-tool/src/config/ClusterBuildOptions.ts @@ -59,27 +59,6 @@ export interface ClusterBuildOptions { // network binding bindAll?: boolean bind?: BindOptions - // mock data seeding - /** - * Seed the 8 mock (chain, token) PRIMARY reserves at bootstrap - * (`--enable-mock-reserves`). Default `false` at every layer — an - * external / real-world depot gets NO fake reserves unless a caller (or a - * flow's scenario `defaults`) opts in. The depot contract gates `regreserve` - * to the bootstrap window (epoch 0), so this only ever seeds pre-EpochBootstrap. - */ - enableMockReserves?: boolean - /** - * Run the Solana outpost bootstrap calls the launch policy withholds - * (`--enable-launch-withheld-operations`): `init_reserve`, - * `create_reserve_native`, and the mock SPL reserve provisioning - * (`create_reserve_spl_authority` / `init_liqsol_reserve` + `sol-mock-mints.json`). - * The launch build of the program answers each with `OperationDisabled`, so - * the default `false` (at every layer) bootstraps a cluster that never calls - * them. A flow whose scenario needs the bootstrap-seeded Solana reserves or the - * mock SPL mints opts in through its scenario `defaults`, and then requires a - * Solana program build without the launch restrictions. - */ - enableLaunchWithheldOperations?: boolean /** * Seed the 2 mock shadow-liq yield pools (LIQETH, LIQSOL) on `sysio.swap` at * bootstrap (`--enable-mock-liq-pools`). Default `false` at every layer — an diff --git a/packages/cluster-tool/src/config/ClusterConfigProvider.ts b/packages/cluster-tool/src/config/ClusterConfigProvider.ts index 84fe1cf18..62c536fea 100644 --- a/packages/cluster-tool/src/config/ClusterConfigProvider.ts +++ b/packages/cluster-tool/src/config/ClusterConfigProvider.ts @@ -159,7 +159,7 @@ export namespace ClusterConfigProvider { export const DefaultProducerCount = 21 export const DefaultNodeCount = 1 export const DefaultBatchOperatorCount = 3 - export const DefaultUnderwriterCount = 1 + export const DefaultUnderwriterCount = 0 /** API nodes planned when a caller names no count — aliases the bind resolver's default. */ export const DefaultApiCount = BindConfigProvider.DefaultApiCount export const DefaultEpochDurationSec = 90 @@ -222,7 +222,8 @@ export namespace ClusterConfigProvider { cooldownEpochs: options.cooldownEpochs ?? 1, terminateMaxConsecutiveMisses: options.terminateMaxConsecutiveMisses ?? null, - terminateMaxPercentMisses24h: options.terminateMaxPercentMisses24h ?? null, + terminateMaxPercentMisses24h: + options.terminateMaxPercentMisses24h ?? null, terminateWindowMs: options.terminateWindowMs ?? null, ethereumPath: assertOption(options.ethereumPath, "ethereumPath"), solanaPath: assertOption(options.solanaPath, "solanaPath"), @@ -247,9 +248,6 @@ export namespace ClusterConfigProvider { awsClusterNodeConfig, externalOutposts, debuggingServerEnabled: true, - enableMockReserves: options.enableMockReserves ?? false, - enableLaunchWithheldOperations: - options.enableLaunchWithheldOperations ?? false, enableMockLiqPools: options.enableMockLiqPools ?? false, enableMockSyndicationImport: options.enableMockSyndicationImport ?? false, // `resolve` is the CREATE path, so the tree it describes is always local; @@ -452,11 +450,8 @@ export namespace ClusterConfigProvider { */ function assertExternalOutpostTopology( options: ClusterBuildOptions, - externalOutposts: ExternalOutpostConfig + _externalOutposts: ExternalOutpostConfig ): void { - if (externalOutposts == null) { - return - } const { underwriterCount = DefaultUnderwriterCount } = options, cause = options.underwriterCount == null @@ -464,8 +459,7 @@ export namespace ClusterConfigProvider { : `underwriterCount was set to ${options.underwriterCount}` Assert.ok( underwriterCount === 0, - `externalOutposts (--external-outpost-config) requires 0 underwriters, but ${cause}. ` + - "An external cluster has no local outpost to bond underwriter collateral on — pass an EXPLICIT --underwriter-count 0." + `Swap-underwriter daemons have been removed; underwriterCount must be 0, but ${cause}.` ) } @@ -548,7 +542,9 @@ export namespace ClusterConfigProvider { * @param options - The caller options (carries `bind`, `bindConfig`, counts). * @returns The resolved bind config. */ - async function resolveBind(options: ClusterBuildOptions): Promise { + async function resolveBind( + options: ClusterBuildOptions + ): Promise { const { bind: cliBind = {} } = options, topology: ClusterTopologyOptions = { producerCount: options.nodeCount, diff --git a/packages/cluster-tool/src/flow/contexts/SwapScenarioContext.ts b/packages/cluster-tool/src/flow/contexts/SwapScenarioContext.ts deleted file mode 100644 index d54f6d065..000000000 --- a/packages/cluster-tool/src/flow/contexts/SwapScenarioContext.ts +++ /dev/null @@ -1,166 +0,0 @@ -import Assert from "node:assert" -import { SysioContracts } from "@wireio/sdk-core" -import { ClusterBuildContext } from "../../orchestration/ClusterBuildContext.js" -import { WireReserveTool } from "../../tools/wire/WireReserveTool.js" -import { slugValue } from "../../utils/slugUtils.js" - -const { SysioContractName } = SysioContracts - -/** - * One reserve row's `(chain, wire)` book. Re-exported from - * {@link WireReserveTool} — the AMM math and the scenario reads must agree on - * one book shape, and the math owns it. - */ -export type ReserveBook = WireReserveTool.ReserveBook - -/** The source + destination books snapshotted around a swap phase. */ -export interface Books { - src: ReserveBook - dst: ReserveBook -} - -/** One reserve's owner-fee rate and the revenue it has earned. */ -export interface ReserveOwnerFee { - /** The WIRE account that owns the reserve and may claim its fees. */ - owner: string - /** The reserve's configured owner fee, in basis points. */ - feeBps: number - /** Unclaimed WIRE held in `sysio.reserv` custody for the owner. */ - accrued: bigint - /** Monotonic audit total — every WIRE this reserve has ever earned. */ - lifetime: bigint -} - -/** - * Shared scenario context for the swap flows — the depot-side query surface - * every swap direction reads: reserve books (`sysio.reserv::reserves`), the - * underwrite request for a chain pair (`sysio.uwrit::uwreqs`), and the lock - * vector backing a request (`sysio.uwrit::locks`). All reads go through the - * typed contract table accessors; scenarios subclass this (or use it directly) - * and call the helpers from verify-step runners. - */ -export class SwapScenarioContext extends ClusterBuildContext { - /** - * One reserve row's `(chain, wire)` book by its slug triple (a read). - * - * @param chainCode - The reserve's chain slug value. - * @param tokenCode - The reserve's token slug value. - * @param reserveCode - The reserve's own slug value. - * @returns The reserve's chain-side + WIRE-side book amounts. - * @throws When no reserve row matches the triple. - */ - async reserveBook( - chainCode: number, - tokenCode: number, - reserveCode: number - ): Promise { - const { rows } = await this.wire - .getSysioContract(SysioContractName.reserv) - .tables.reserves.query() - const row = rows.find( - reserve => - slugValue(reserve.chain_code) === chainCode && - slugValue(reserve.token_code) === tokenCode && - slugValue(reserve.reserve_code) === reserveCode - ) - Assert.ok(row, `reserve ${chainCode}/${tokenCode}/${reserveCode} not found`) - return { - chain: BigInt(row.reserve_chain_amount), - wire: BigInt(row.reserve_wire_amount), - connectorWeightBps: Number(row.connector_weight_bps), - ownerFeeBps: Number(row.owner_fee_bps) - } - } - - /** - * The underwrite request row for a `(source chain, destination chain)` pair - * (a read). - * - * @param srcChainCode - The source chain slug value. - * @param dstChainCode - The destination chain slug value. - * @returns The matching `uwreqs` row, or nothing when the depot has not - * created one yet. - */ - async uwreq( - srcChainCode: number, - dstChainCode: number - ): Promise { - const { rows } = await this.wire - .getSysioContract(SysioContractName.uwrit) - .tables.uwreqs.query() - return rows.find( - request => - slugValue(request.src_chain_code) === srcChainCode && - slugValue(request.dst_chain_code) === dstChainCode - ) - } - - /** - * The locks backing an underwrite request (a read). - * - * @param uwreqId - The `uwreqs` row id. - * @returns Every `locks` row referencing the request. - */ - async locksForUwreq( - uwreqId: number - ): Promise { - const { rows } = await this.wire - .getSysioContract(SysioContractName.uwrit) - .tables.locks.query() - return rows.filter(lock => Number(lock.uwreq_id) === uwreqId) - } - - /** - * One reserve's owner-fee state (a read) — its configured rate plus the - * revenue it has earned. `accrued` is unclaimed WIRE sitting in - * `sysio.reserv` custody until the owner calls `claimrsvfee`; `lifetime` is - * the monotonic audit total a claim never reduces. - * - * @param chainCode - The reserve's chain slug value. - * @param tokenCode - The reserve's token slug value. - * @param reserveCode - The reserve's own slug value. - * @returns The reserve's owner, fee rate, and earned/unclaimed amounts. - * @throws When no reserve row matches the triple. - */ - async reserveOwnerFee( - chainCode: number, - tokenCode: number, - reserveCode: number - ): Promise { - const { rows } = await this.wire - .getSysioContract(SysioContractName.reserv) - .tables.reserves.query() - const row = rows.find( - reserve => - slugValue(reserve.chain_code) === chainCode && - slugValue(reserve.token_code) === tokenCode && - slugValue(reserve.reserve_code) === reserveCode - ) - Assert.ok(row, `reserve ${chainCode}/${tokenCode}/${reserveCode} not found`) - return { - owner: row.owner, - feeBps: Number(row.owner_fee_bps), - accrued: BigInt(row.owner_fee_accrued), - lifetime: BigInt(row.owner_fee_lifetime) - } - } - - /** - * An underwriter's accrued swap-fee row (`sysio.reserv::uwfees`) — the - * underwriter half of every WIRE-leg fee their winning commits settled, - * held in `sysio.reserv` custody until that account calls `claimuwfee` - * (a read). - * - * @param underwriter - The underwriter's WIRE account name. - * @returns The matching `uwfees` row, or nothing when the account has never - * won a swap (no row exists until the first accrual). - */ - async underwriterFees( - underwriter: string - ): Promise { - const { rows } = await this.wire - .getSysioContract(SysioContractName.reserv) - .tables.uwfees.query() - return rows.find(row => row.underwriter === underwriter) - } -} diff --git a/packages/cluster-tool/src/flow/contexts/index.ts b/packages/cluster-tool/src/flow/contexts/index.ts index d71e3f70d..e69de29bb 100644 --- a/packages/cluster-tool/src/flow/contexts/index.ts +++ b/packages/cluster-tool/src/flow/contexts/index.ts @@ -1 +0,0 @@ -export * from "./SwapScenarioContext.js" diff --git a/packages/cluster-tool/src/orchestration/ClusterBuildDefaults.ts b/packages/cluster-tool/src/orchestration/ClusterBuildDefaults.ts index ce6d78f83..aa7825f85 100644 --- a/packages/cluster-tool/src/orchestration/ClusterBuildDefaults.ts +++ b/packages/cluster-tool/src/orchestration/ClusterBuildDefaults.ts @@ -13,7 +13,10 @@ import { Constants, ProtocolTiming } from "../Constants.js" import { BatchOperatorSchedule } from "../config/BatchOperatorSchedule.js" import { DaemonConfig } from "../config/DaemonConfig.js" import { NodeConfig, NodeRole, producerName } from "../config/NodeConfig.js" -import { readNodeOwner, readNodeOwnerReg } from "../tools/ethereum/EthereumNodeOwnerNftTool.js" +import { + readNodeOwner, + readNodeOwnerReg +} from "../tools/ethereum/EthereumNodeOwnerNftTool.js" import { AuthExLinkTool } from "../tools/all/AuthExLinkTool.js" import { pollUntil, verifyStep } from "./StepTools.js" import type { ClusterBuildOptions } from "../config/ClusterBuildOptions.js" @@ -60,26 +63,6 @@ const LiqKickerBps = 200 * and `clear`, which move no funds; its resource policy pays for them. */ const PanicAccountWireFunding = 0n -/** WIRE-leg swap fee (bps) + collateral-lock challenge window (dev). */ -const SwapFeeBps = 30 -const CollateralLockDurationMs = 600_000 -/** - * Minimum `swapfromwire` escrow (9-dec base units). The contract default is - * 5 WIRE; dev clusters lower it to exactly the 0.1 WIRE escrow the - * swap-from-WIRE flow pushes — the same way they shorten the collateral-lock - * window — so the enqueue boundary stays exercised without re-baselining flow - * economics. - */ -const MinFromWireAmount = 100_000_000 -/** - * Fee (bps of the escrow) forfeited on caller-fault drain-time reverts of - * queued `swapfromwire` rows (zero quote / missed variance at `drainfwq`), - * routed like the settlement fee. Mirrors the contract default — the 5% launch - * value — so a cluster prices revert churn the way the network will. Happy-path - * flows never pay it and system-caused reverts refund in full, so no flow's - * economics depend on this number. - */ -const FromWireRevertFeeBps = 500 /** * Nodeop processes started concurrently within a node-start group. * @@ -93,31 +76,6 @@ const FromWireRevertFeeBps = 500 */ const NodeStartConcurrency = 4 -/** - * Epochs a PENDING uwreq may wait for its underwriter race before - * `sysio.uwrit::pruneuwreqs` expires it (refund/revert + EXPIRED). Mirrors - * the contract default; flow races resolve within an epoch, so the timeout - * only fires for genuinely abandoned requests (SEC-129 / WSA-223). - */ -const UwreqPendingTimeoutEpochs = 10 -/** - * Epochs a terminal (COMPLETED / REJECTED / EXPIRED) uwreq row is retained - * for audit before `sysio.uwrit::pruneuwreqs` erases it. Mirrors the - * contract default (SEC-129 / WSA-223). - */ -const UwreqRetentionEpochs = 10 -/** - * Stage 2 of the swap-fee split: the share of each fee's rewards pool routed to - * the `sysio` emissions treasury instead of the batch-operator rewards bucket. - * Mirrors `sysio.reserv::DEFAULT_FEE_EMISSIONS_SHARE_BPS`. - * - * Zero keeps every swap fee inside `sysio.reserv` custody at settlement (the - * underwriter half as a `uwfees` accrual, the rest in the rewards bucket), which - * is what the swap flows' custody assertions expect. Raising it makes exactly - * that share leave custody per settlement. - */ -const FeeEmissionsShareBps = 0 - /** Epoch envelope-log retention. */ const EnvelopeLogRetentionEpochs = 10 /** Dev-default `terminate_max_consecutive_misses` (per-flow overridable via ClusterConfig). */ @@ -199,29 +157,29 @@ export namespace ClusterBuildDefaults { batchOperators = range(config.batchOperatorCount).map(index => Constants.batchOperatorLabel(index) ), - underwriters = range(config.underwriterCount).map(index => - Constants.underwriterLabel(index) - ), plannedNodes = NodeConfig.plan(config), - producerNodes = plannedNodes.filter(node => node.role === NodeRole.producer), + producerNodes = plannedNodes.filter( + node => node.role === NodeRole.producer + ), apiNodes = plannedNodes.filter(node => node.role === NodeRole.api), // External-outpost mode: the ETH + SOL outposts already run on real chains // (`config.externalOutposts`), so skip the local anvil/validator starts + // outpost deploys and publish the operator-daemon artifacts from the // external config instead (verifying the endpoints are reachable). isExternalOutpost = config.externalOutposts != null - const bootstrapNodeOwnerRegistration: SysioContracts.SysioRoaNodeownregAction = { - owner: Constants.BOOTSTRAP_NODE_OWNER, - tier: NodeOwnerTier.T1, - eth_pub_key: bootstrapNodeOwnerEth.publicKey, - // NOT a free-form payload field: `sysio.roa::nodeownreg` runs - // `active_key_matches(owner, wire_pub_key)` and soft-fails the claim - // with ACCOUNT_KEY_MISMATCH (a REJECTED audit row, no revert) unless - // this key can satisfy the account's `active` authority BY ITSELF — - // i.e. it must be exactly the `newnameduser.pubkey` below. - wire_pub_key: nodeOwner.wire.publicKey, - eth_address: bootstrapNodeOwnerEth.nativeAddress - } + const bootstrapNodeOwnerRegistration: SysioContracts.SysioRoaNodeownregAction = + { + owner: Constants.BOOTSTRAP_NODE_OWNER, + tier: NodeOwnerTier.T1, + eth_pub_key: bootstrapNodeOwnerEth.publicKey, + // NOT a free-form payload field: `sysio.roa::nodeownreg` runs + // `active_key_matches(owner, wire_pub_key)` and soft-fails the claim + // with ACCOUNT_KEY_MISMATCH (a REJECTED audit row, no revert) unless + // this key can satisfy the account's `active` authority BY ITSELF — + // i.e. it must be exactly the `newnameduser.pubkey` below. + wire_pub_key: nodeOwner.wire.publicKey, + eth_address: bootstrapNodeOwnerEth.nativeAddress + } // ═══ Cluster Prerequisites — processes, keys, contracts, registry, producers ═══ const prerequisites = ClusterBuildPhaseGroup.create( @@ -567,8 +525,6 @@ export namespace ClusterBuildDefaults { SysioContractName.epoch, SysioContractName.opreg, SysioContractName.msgch, - SysioContractName.uwrit, - SysioContractName.reserv, SysioContractName.chalg, SysioContractName.dclaim, SysioContractName.andon, @@ -650,7 +606,8 @@ export namespace ClusterBuildDefaults { {}, { account: label, - type: SysioContracts.SysioOpregOperatortype.OPERATOR_TYPE_PRODUCER, + type: SysioContracts.SysioOpregOperatortype + .OPERATOR_TYPE_PRODUCER, is_bootstrapped: true } ) @@ -945,7 +902,7 @@ export namespace ClusterBuildDefaults { ) } - // ── registry + optional mock reserves + underwriter config ── + // ── registry + syndication configuration ── ClusterBuildPhase.create( prerequisites, "Registry", @@ -958,19 +915,6 @@ export namespace ClusterBuildDefaults { {} ) ) - // Mock (chain, token) PRIMARY reserves — opt-in via `--enable-mock-reserves` - // (default off, so a real / external depot never gets fake reserves). Seeded - // HERE, pre-EpochBootstrap, because the contract gates `regreserve` to the - // epoch-0 bootstrap window — a flow phase (which always runs after epoch - // 0→1) could never seed them. - if (config.enableMockReserves) { - Steps.registry.planMockReserves( - prerequisites, - "MockReserves", - "Seed the 8 mock (chain, token) PRIMARY reserves", - {} - ) - } // 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. @@ -1046,45 +990,12 @@ export namespace ClusterBuildDefaults { Steps.mockShadowBacking.planSolana(prerequisites) Steps.mockShadowBacking.planEthereum(prerequisites) } - ClusterBuildPhase.create( - prerequisites, - "UnderwriterConfig", - "Configure sysio.uwrit" - ).push( - Steps.contracts.sysio.uwrit.planSetconfig( - Actor.Sysio, - "configure-uwrit", - "set the underwriter config", - {}, - { - fee_bps: SwapFeeBps, - collateral_lock_duration_ms: CollateralLockDurationMs, - min_fromwire_amount: MinFromWireAmount, - fromwire_revert_fee_bps: FromWireRevertFeeBps, - uwreq_pending_timeout_epochs: UwreqPendingTimeoutEpochs, - uwreq_retention_epochs: UwreqRetentionEpochs - } - ) - ) - ClusterBuildPhase.create( - prerequisites, - "ReserveConfig", - "Configure sysio.reserv fee routing" - ).push( - Steps.contracts.sysio.reserv.planSetconfig( - Actor.Sysio, - "configure-reserv", - "set the swap-fee routing config", - {}, - { fee_emissions_share_bps: FeeEmissionsShareBps } - ) - ) // ═══ Cluster Post Contract Deployment — batch/uw operators, nodes, first epoch ═══ const postContractDeployment = ClusterBuildPhaseGroup.create( cluster, "Cluster Post Contract Deployment", - "Provision batch operators + underwriters, start API nodes (when planned) + operator nodes, bootstrap the first epoch" + "Provision batch operators, start API and operator nodes, bootstrap the first epoch" ) // The operator daemons' shared prerequisites: the in-process OPP debugging @@ -1125,14 +1036,14 @@ export namespace ClusterBuildDefaults { ) ) - // Bootstrapped batch operators + underwriters via the ONE mechanism. Fee-payer + // Bootstrapped batch operators use the shared provisioning mechanism. Fee-payer // funding only — deposit flows provision their own non-bootstrapped ops with // collateral funding on top. const isSSM = config.signatureProvider.type === SignatureProviderType.SSM WireOperatorProvisioningTool.planOperatorAccountProvisioning( postContractDeployment, "Create batchops & uws", - "Provision the bootstrapped batch operators + underwriters", + "Provision the bootstrapped batch operators", {}, [ ...batchOperators.map((label, index) => ({ @@ -1146,21 +1057,6 @@ export namespace ClusterBuildDefaults { // mnemonic anvil never funded. See BatchOperatorEthereumFundingWei. airdropSolanaLamports: BatchOperatorAirdropLamports, ...(isSSM ? { fundEthereumWei: BatchOperatorEthereumFundingWei } : {}) - })), - ...underwriters.map((label, index) => ({ - label, - type: OperatorType.UNDERWRITER, - // The offset is the length of the PREFIX-FILTERED batch-operator - // array, not `config.batchOperatorCount`: only a list already - // narrowed to `batchop.` entries guarantees the ordinal the HD-index - // rule is defined against. `index` is that same filtered array's - // iterator index for underwriters. - ethereumHdIndex: Constants.underwriterEthereumHdIndex( - batchOperators.length, - index - ), - isBootstrapped: false, - ...(isSSM ? { fundEthereumWei: BatchOperatorEthereumFundingWei } : {}) })) ] ) @@ -1198,17 +1094,9 @@ export namespace ClusterBuildDefaults { "OperatorNodes", "Start operator nodes", plannedNodes.filter(node => NodeConfig.isOperatorRole(node.role)), - node => - node.batchOperatorLabel != null ? Actor.BatchOperator : Actor.Underwriter + () => Actor.BatchOperator ) - // The underwriter_plugin defers its startup preflight until the chain - // plugin reports the node synced (head within `sync_recency_ms` of now, - // via the controller's accepted_block signal), so a first boot that - // starts at genesis simply waits out its replay — no relaunch needed. - // The generic `Steps.processes.nodeop.restart` machinery remains for - // scenarios that need a real restart. - // ── first epoch ── // Step ORDER is load-bearing: both roster seeds read the schedule // `schbatchgps` just materialized, and must land before `msgch::bootstrap` @@ -1504,7 +1392,8 @@ export namespace ClusterBuildDefaults { config.terminateMaxConsecutiveMisses ?? DefaultTerminateMaxConsecutiveMisses, terminate_max_pct_misses_24h: - config.terminateMaxPercentMisses24h ?? DefaultTerminateMaxPercentMisses24h, + config.terminateMaxPercentMisses24h ?? + DefaultTerminateMaxPercentMisses24h, terminate_window_ms: config.terminateWindowMs ?? DefaultTerminateWindowMs, req_prod_collat: config.requiredProducerCollateral.map(toChainMinBond), req_batchop_collat: diff --git a/packages/cluster-tool/src/orchestration/ethereum/EthereumOutpostBootstrapper.ts b/packages/cluster-tool/src/orchestration/ethereum/EthereumOutpostBootstrapper.ts index d5f304240..b8fd164fd 100644 --- a/packages/cluster-tool/src/orchestration/ethereum/EthereumOutpostBootstrapper.ts +++ b/packages/cluster-tool/src/orchestration/ethereum/EthereumOutpostBootstrapper.ts @@ -17,7 +17,10 @@ import { withFileLock } from "../../utils/fsUtils.js" import { scaleTimeoutMs } from "../../utils/asyncUtils.js" -import { EvmAddressPattern, loadOutpostContract } from "../../utils/ethereumUtils.js" +import { + EvmAddressPattern, + loadOutpostContract +} from "../../utils/ethereumUtils.js" const log = getLogger(__filename) const execFileAsync = promisify(execFile) @@ -91,16 +94,13 @@ export interface EthereumOutpostBootstrapperConfig extends Required { - const outpostAddressesFile = Path.join( - this.config.deploymentsPath, - "outpost-addrs.json" - ) - if (!Fs.existsSync(outpostAddressesFile)) { - log.warn( - "[ethereum] seedReserveManager: outpost-addrs.json missing, skipping" - ) - return - } - const addresses = JSON.parse(Fs.readFileSync(outpostAddressesFile, "utf-8")) - const { - ReserveManager: reserveManagerAddress, - MockUsdc: mockUsdcAddress, - MockUsdt: mockUsdtAddress - } = addresses - if (reserveManagerAddress == null) { - log.warn( - "[ethereum] seedReserveManager: ReserveManager address missing, skipping" - ) - return - } - - // Bind the deployer (anvil HD index 0) — the same identity deployLocal.ts - // used as `owner`, so its minted MockUSDC/USDT balances are available here. - const provider = new ethers.JsonRpcProvider(rpcUrl) - const deployer = new ethers.Wallet( - this.accounts[EthereumOutpostBootstrapper.DeployerAccountIndex] - .privateKey, - provider - ) - let nonce = await deployer.getNonce("pending") - log.info( - `[ethereum] seedReserveManager start (deployer=${deployer.address}, nonce=${nonce})` - ) - - const nativeSeed = ethers.parseEther( - EthereumOutpostBootstrapper.NativeSeedEther - ) - const stableSeed = ethers.parseUnits( - EthereumOutpostBootstrapper.StableSeedUnits, - EthereumOutpostBootstrapper.StableDecimals - ) - - log.info( - `[ethereum] seed ${ethers.formatEther(nativeSeed)} ETH (nonce ${nonce})` - ) - const ethTx = await deployer.sendTransaction({ - to: reserveManagerAddress, - value: nativeSeed, - nonce: nonce++ - }) - await ethTx.wait() - - const transferStable = async ( - tokenAddress: string, - label: string - ): Promise => { - log.info( - `[ethereum] seed ${ethers.formatUnits(stableSeed, 6)} ${label} (nonce ${nonce})` - ) - const token = new ethers.Contract( - tokenAddress, - EthereumOutpostBootstrapper.Erc20Abi, - deployer - ) - const tx = await token.transfer(reserveManagerAddress, stableSeed, { - nonce: nonce++ - }) - await tx.wait() - } - if (mockUsdcAddress != null) await transferStable(mockUsdcAddress, "USDC") - if (mockUsdtAddress != null) await transferStable(mockUsdtAddress, "USDT") - - // LIQETH only if the LiqEth deploy went through (toggled off for outpost-only runs). - const { LiqEth: liqEthAddress } = addresses - if (liqEthAddress != null) { - const liqEthSeed = ethers.parseEther( - EthereumOutpostBootstrapper.NativeSeedEther - ) - const liqEth = new ethers.Contract( - liqEthAddress, - EthereumOutpostBootstrapper.Erc20Abi, - deployer - ) - const ownerLiqEthBalance: bigint = await liqEth.balanceOf( - deployer.address - ) - if (ownerLiqEthBalance >= liqEthSeed) { - log.info( - `[ethereum] seed ${ethers.formatEther(liqEthSeed)} LIQETH (nonce ${nonce})` - ) - const liqEthTx = await liqEth.transfer( - reserveManagerAddress, - liqEthSeed, - { nonce: nonce++ } - ) - await liqEthTx.wait() - } else { - log.info( - `[ethereum] skip LIQETH seed (deployer balance ${ethers.formatEther(ownerLiqEthBalance)} < ${ethers.formatEther(liqEthSeed)})` - ) - } - } - log.info("[ethereum] seedReserveManager complete") - } - /** * Seed the ETH outpost's batch-operator roster from the depot's REAL schedule * via `OPPInbound.installInitialRoster` — the SOL-376 `opp_bootstrap` shape. @@ -453,9 +333,12 @@ export class EthereumOutpostBootstrapper { * * @param seed - the depot's window as EVM addresses + the slot serving epoch 1. */ - async oppBootstrap(seed: EthereumOutpostBootstrapper.OppBootstrapSeed): Promise { + async oppBootstrap( + seed: EthereumOutpostBootstrapper.OppBootstrapSeed + ): Promise { const { ethereumPath, deploymentsPath, rpcUrl } = this.config, - initialGroups = EthereumOutpostBootstrapper.initialBatchOperatorGroups(seed), + initialGroups = + EthereumOutpostBootstrapper.initialBatchOperatorGroups(seed), outpostAddressesFile = Path.join( deploymentsPath, EthereumOutpostBootstrapper.OutpostAddressesFile @@ -476,22 +359,24 @@ export class EthereumOutpostBootstrapper { )[EthereumOutpostBootstrapper.DeployerAccountIndex].privateKey, provider ), - manager = loadOutpostContract( - ethereumPath, - outpostAddresses, - EthereumOutpostBootstrapper.OutpostManagerContractName, - [...EthereumOutpostBootstrapper.OutpostArtifactSubpath], - deployer, - EthereumOutpostBootstrapper.OutpostManagerContractName, - EthereumOutpostBootstrapper.OutpostManagerArtifactName - ), - oppInbound = loadOutpostContract( - ethereumPath, - outpostAddresses, - EthereumOutpostBootstrapper.OppInboundContractName, - [...EthereumOutpostBootstrapper.OutpostArtifactSubpath], - deployer - ), + manager = + loadOutpostContract( + ethereumPath, + outpostAddresses, + EthereumOutpostBootstrapper.OutpostManagerContractName, + [...EthereumOutpostBootstrapper.OutpostArtifactSubpath], + deployer, + EthereumOutpostBootstrapper.OutpostManagerContractName, + EthereumOutpostBootstrapper.OutpostManagerArtifactName + ), + oppInbound = + loadOutpostContract( + ethereumPath, + outpostAddresses, + EthereumOutpostBootstrapper.OppInboundContractName, + [...EthereumOutpostBootstrapper.OutpostArtifactSubpath], + deployer + ), oppInboundAddress = await oppInbound.getAddress(), activeGroup = seed.window.groups[seed.activeGroupIndex] @@ -523,7 +408,9 @@ export class EthereumOutpostBootstrapper { } finally { provider.destroy() } - log.info("[ethereum] installInitialRoster: ETH outpost roster seeded on the depot's schedule") + log.info( + "[ethereum] installInitialRoster: ETH outpost roster seeded on the depot's schedule" + ) } } @@ -561,7 +448,10 @@ export namespace EthereumOutpostBootstrapper { /** The `OutpostManager` surface the seed drives — the post-handoff admin path. */ export interface OutpostManagerExecuteView { - execute(target: string, data: string): Promise + execute( + target: string, + data: string + ): Promise } /** The `OPPInbound` surface the seed reads back through. */ @@ -602,16 +492,23 @@ export namespace EthereumOutpostBootstrapper { * @return the tuple, ready to ABI-encode as the call's one argument. * @throws on any of the rejections above. */ - export function initialBatchOperatorGroups(seed: OppBootstrapSeed): InitialBatchOperatorGroups { + export function initialBatchOperatorGroups( + seed: OppBootstrapSeed + ): InitialBatchOperatorGroups { const { window, activeGroupIndex } = seed, { groups, epochDurationSec } = window - Assert.ok(groups.length > 0, "initial roster: at least one batch-operator group is required") + Assert.ok( + groups.length > 0, + "initial roster: at least one batch-operator group is required" + ) Assert.ok( Number.isSafeInteger(epochDurationSec) && epochDurationSec > 0, `initial roster: epochDurationSec must be a positive integer (got ${epochDurationSec})` ) Assert.ok( - Number.isSafeInteger(activeGroupIndex) && activeGroupIndex >= 0 && activeGroupIndex < groups.length, + Number.isSafeInteger(activeGroupIndex) && + activeGroupIndex >= 0 && + activeGroupIndex < groups.length, `initial roster: activeGroupIndex ${activeGroupIndex} is out of range for ${groups.length} group(s)` ) return { @@ -784,7 +681,10 @@ export namespace EthereumOutpostBootstrapper { ): void { const highest = Math.max( Constants.batchOperatorEthereumHdIndex(batchOperatorCount - 1), - Constants.underwriterEthereumHdIndex(batchOperatorCount, underwriterCount - 1) + Constants.underwriterEthereumHdIndex( + batchOperatorCount, + underwriterCount - 1 + ) ) Assert.ok( highest < PanicAccountIndex, @@ -830,17 +730,6 @@ export namespace EthereumOutpostBootstrapper { export const HardhatStderrTailChars = 1_000 /** Chars of Hardhat stdout logged after a run. */ export const HardhatStdoutTailChars = 500 - /** Native ETH amount seeded into ReserveManager. */ - export const NativeSeedEther = "100" - /** Stable-coin amount seeded into ReserveManager (whole units). */ - export const StableSeedUnits = "100" - /** Decimals for the mock stable-coins (USDC / USDT). */ - export const StableDecimals = 6 - /** Minimal ERC-20 ABI for the custody-seeding transfers. */ - export const Erc20Abi = [ - "function transfer(address,uint256) returns (bool)", - "function balanceOf(address) view returns (uint256)" - ] as const /** Resolve the default (overridable) options. `accountCount` tracks the run * anvil's `--accounts` so every generated account is pre-funded. */ diff --git a/packages/cluster-tool/src/orchestration/ethereum/EthereumOutpostSteps.ts b/packages/cluster-tool/src/orchestration/ethereum/EthereumOutpostSteps.ts index 32042cc6e..879d77a9b 100644 --- a/packages/cluster-tool/src/orchestration/ethereum/EthereumOutpostSteps.ts +++ b/packages/cluster-tool/src/orchestration/ethereum/EthereumOutpostSteps.ts @@ -153,7 +153,7 @@ export namespace EthereumOutpostSteps { /** * Deploy the Ethereum outpost against the already-running run anvil * (`Steps.processes.anvil.start` must precede this in the phase): deploy the - * `wire-ethereum` contracts, seed the ReserveManager, and write the annotated + * `wire-ethereum` contracts and write the annotated * accounts file (later phases re-read `accounts.json` / `outpost-addrs.json` * from disk). Input-less — paths + the anvil port come from `ctx.config`. */ @@ -193,7 +193,9 @@ export namespace EthereumOutpostSteps { ctx.config.bind.anvil.port, toDialAddress(ctx.config.bind.anvil.address) ), - deploymentsPath: ClusterConfigProvider.ethereumDeploymentsPath(ctx.config), + deploymentsPath: ClusterConfigProvider.ethereumDeploymentsPath( + ctx.config + ), initialRoster } } @@ -225,13 +227,22 @@ export namespace EthereumOutpostSteps { * window is read off `sysio.epoch::epochstate` and the operators off * `ctx.keyStore` at run time. */ - export function planOppBootstrap( + export function planOppBootstrap< + C extends ClusterBuildContext = ClusterBuildContext + >( actor: Report.Actor, name: string, description: string, options: ClusterBuildStepOptions ): ClusterBuildStep { - return ClusterBuildStep.create(actor, name, description, options, null, runOppBootstrap) + return ClusterBuildStep.create( + actor, + name, + description, + options, + null, + runOppBootstrap + ) } /** Named runner — `EthereumOutpostBootstrapper.oppBootstrap`. */ @@ -253,7 +264,9 @@ export namespace EthereumOutpostSteps { epochState.current_batch_op_group, ctx.config.epochDurationSec ) - await new EthereumOutpostBootstrapper(bootstrapperOptions(ctx, seed.window)).oppBootstrap(seed) + await new EthereumOutpostBootstrapper( + bootstrapperOptions(ctx, seed.window) + ).oppBootstrap(seed) } /** @@ -279,7 +292,9 @@ export namespace EthereumOutpostSteps { activeGroupIndex: number, epochDurationSec: number ): EthereumOutpostBootstrapper.OppBootstrapSeed { - const operatorByAccount = new Map(batchOperators.map(operator => [operator.account, operator])) + const operatorByAccount = new Map( + batchOperators.map(operator => [operator.account, operator]) + ) return { window: { groups: window.map(accountNames => @@ -325,7 +340,9 @@ export namespace EthereumOutpostSteps { maxSyndicationPerTransfer: BigInt( EthereumOutpostBootstrapper.MaxSyndicationPerTransferWei ), - yieldDeadband: BigInt(EthereumOutpostBootstrapper.YieldDeadbandDepotUnits), + yieldDeadband: BigInt( + EthereumOutpostBootstrapper.YieldDeadbandDepotUnits + ), liqTokenCode: EthereumSyndicationTool.liqEthTokenCode(), liqTokenPrecision: EthereumSyndicationTool.LiqEthTokenPrecision } diff --git a/packages/cluster-tool/src/orchestration/solana/SolanaOutpostBootstrapper.ts b/packages/cluster-tool/src/orchestration/solana/SolanaOutpostBootstrapper.ts index fde027335..3c4c08a87 100644 --- a/packages/cluster-tool/src/orchestration/solana/SolanaOutpostBootstrapper.ts +++ b/packages/cluster-tool/src/orchestration/solana/SolanaOutpostBootstrapper.ts @@ -8,17 +8,17 @@ import { LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js" -import { TOKEN_PROGRAM_ID } from "@solana/spl-token" + import { SlugName } from "@wireio/sdk-core" import { OperatorStatus, OperatorType } from "@wireio/opp-typescript-models" -import { mapSeries } from "../../utils/asyncUtils.js" + import { SolanaClient } from "../../clients/solana/SolanaClient.js" import { confirmSignature } from "../../clients/solana/utils/signatureUtils.js" import { getLogger } from "../../logging/Logger.js" import { StepExtraRecorder } from "../../report/tools/StepExtraRecorder.js" import { retry } from "../../utils/asyncUtils.js" import { mkdirs } from "../../utils/fsUtils.js" -import { slugNameToLittleEndianBuffer } from "../../utils/slugUtils.js" + import { SolanaFundingTool } from "../../tools/solana/SolanaFundingTool.js" import { SolanaOutpostProgramTool } from "../../tools/solana/SolanaOutpostProgramTool.js" @@ -38,13 +38,6 @@ export interface SolanaOutpostBootstrapperOptions { * When `null`, SPL provisioning is skipped (native SOL still works). */ clusterDataPath?: string | null - /** - * Run the launch-withheld calls (`init_reserve`, `create_reserve_native`, mock - * SPL reserve provisioning). Default `false`: the launch build of the program - * answers each with `OperationDisabled`. Sourced from - * `ClusterConfig.enableLaunchWithheldOperations` — never a second setting. - */ - enableLaunchWithheldOperations?: boolean } /** Resolved {@link SolanaOutpostBootstrapper} config (derived defaults filled in). */ @@ -53,7 +46,6 @@ export interface SolanaOutpostBootstrapperConfig { rpcUrl: string deployerKeypairFile: string clusterDataPath: string | null - enableLaunchWithheldOperations: boolean } /** @@ -93,9 +85,7 @@ export class SolanaOutpostBootstrapper { (options.clusterDataPath != null ? SolanaFundingTool.deployerKeypairFile(options.clusterDataPath) : SolanaOutpostBootstrapper.defaultDeployerKeypairFile()), - clusterDataPath: options.clusterDataPath ?? null, - enableLaunchWithheldOperations: - options.enableLaunchWithheldOperations ?? false + clusterDataPath: options.clusterDataPath ?? null } this.connection = new Connection( options.rpcUrl, @@ -144,7 +134,8 @@ export class SolanaOutpostBootstrapper { log.info("=== Solana outpost bootstrap ===") const { solanaPath } = this.config, - programKeypairFile = SolanaOutpostProgramTool.programKeypairFile(solanaPath) + programKeypairFile = + SolanaOutpostProgramTool.programKeypairFile(solanaPath) this.programId = SolanaOutpostProgramTool.programId(solanaPath) if (this.programId != null) { log.info( @@ -236,21 +227,6 @@ export class SolanaOutpostBootstrapper { return SolanaOutpostProgramTool.derivePda(programId, Buffer.from(seed)) } - /** Derive a `(token_code, reserve_code)`-scoped PDA (reserve / reserve_vault). */ - private deriveReserveScopedAddress( - programId: PublicKey, - seed: string, - tokenCode: number, - reserveCode: number - ): PublicKey { - return SolanaOutpostProgramTool.derivePda( - programId, - Buffer.from(seed), - slugNameToLittleEndianBuffer(tokenCode), - slugNameToLittleEndianBuffer(reserveCode) - ) - } - private async initializePDAs(deployer: Keypair): Promise { const programId = this.programId Assert.ok(programId != null, "initializePDAs: programId required") @@ -378,273 +354,6 @@ export class SolanaOutpostBootstrapper { "set_token_precision(SOL)" ) log.info("SOL native-token precision registered") - - await this.seedWithheldOperations(deployer, program, configPda, solTokenCode) - } - - /** - * Run the launch-withheld outpost bootstrap calls — `init_reserve`, - * `create_reserve_native`, and the mock SPL reserve provisioning — when - * `enableLaunchWithheldOperations` is set; otherwise skip them and log the - * policy. The launch build of the program answers each with - * `OperationDisabled`, and the inbound path does not need them: `epoch_in` - * declares `reserve_aggregate` as an `UncheckedAccount` with a seeds check - * only, so an uninitialised PDA passes. - * - * @param deployer - outpost admin / payer keypair. - * @param program - the loaded `liqsol_core` Anchor program. - * @param configPda - the `OutpostConfig` PDA. - * @param solTokenCode - the native-SOL token slug code. - * @return `true` when the withheld calls ran, `false` when the policy skipped them. - */ - async seedWithheldOperations( - deployer: Keypair, - program: anchor.Program, - configPda: PublicKey, - solTokenCode: anchor.BN - ): Promise { - if (!this.config.enableLaunchWithheldOperations) { - log.info(SolanaOutpostBootstrapper.LaunchWithheldOperationsSkippedMessage) - return false - } - const programId = this.programId - Assert.ok(programId != null, "seedWithheldOperations: programId required") - // Initialize the ReserveAggregate PDA (`init_reserve`). - const reserveAggregatePda = this.deriveProgramAddress( - programId, - SolanaOutpostBootstrapper.PdaSeed.ReserveAggregate - ) - log.info(` reserveAggregate: ${reserveAggregatePda.toBase58()}`) - const initReserveTransaction = await program.methods - .initReserve() - .accounts({ - payer: deployer.publicKey, - ...this.getAdminAccounts(deployer), - config: configPda, - reserveAggregate: reserveAggregatePda, - systemProgram: anchor.web3.SystemProgram.programId - }) - .signers([deployer]) - .transaction() - await this.runSimpleAuthorityInstruction( - deployer, - initReserveTransaction, - "init_reserve" - ) - log.info("SOL ReserveAggregate PDA initialized") - - // Bootstrap-seeded native SOL reserve — the outpost-side mirror of the - // depot's SOLANA/SOL/PRIMARY row. `create_reserve_native` is the - // authority-gated, NATIVE-only bootstrap-symmetry IX (no SPL Mint/ATA, no - // RESERVE_CREATE attestation; status=Active set inline). - const solReserveCode = new anchor.BN( - SlugName.from(SolanaOutpostBootstrapper.PrimaryReserveCodename) - ) - const nativeReserveAmount = new anchor.BN( - SolanaOutpostBootstrapper.BootstrapNativeReserveLamports - ) - const solReservePda = this.deriveReserveScopedAddress( - programId, - SolanaOutpostBootstrapper.PdaSeed.Reserve, - SlugName.from(SolanaOutpostBootstrapper.SolTokenCodename), - SlugName.from(SolanaOutpostBootstrapper.PrimaryReserveCodename) - ) - log.info(` reserve (SOL/PRIMARY): ${solReservePda.toBase58()}`) - const createReserveTransaction = await program.methods - .createReserveNative( - solTokenCode, - solReserveCode, - nativeReserveAmount, - nativeReserveAmount, - SolanaOutpostBootstrapper.BootstrapConnectorWeightBps, - "SOLANA-SOL/WIRE primary reserve", - "Bootstrap-seeded native SOL ↔ WIRE reserve (outpost-side custody)" - ) - .accounts({ - payer: deployer.publicKey, - ...this.getAdminAccounts(deployer), - config: configPda, - reserve: solReservePda, - systemProgram: anchor.web3.SystemProgram.programId - }) - .signers([deployer]) - .transaction() - await this.runSimpleAuthorityInstruction( - deployer, - createReserveTransaction, - "create_reserve_native" - ) - log.info( - `SOL native reserve seeded (PDA=${solReservePda.toBase58()}, lamports=${SolanaOutpostBootstrapper.BootstrapNativeReserveLamports})` - ) - - if (this.config.clusterDataPath != null) - await this.provisionSplReserves(deployer, program, configPda) - return true - } - - /** - * Provision mock SPL reserves (USDCSOL, USDTSOL, LIQSOL): create each mint, - * fund the deployer ATA, bind via `set_token_address` + `set_token_precision`, - * then `create_reserve_spl_authority` to allocate the per-reserve vault seeded - * with bootstrap liquidity. Persists the mint pubkeys to - * `/sol-mock-mints.json` for depot-side token registration. - */ - private async provisionSplReserves( - deployer: Keypair, - program: anchor.Program, - configPda: PublicKey - ): Promise { - const clusterDataPath = this.config.clusterDataPath - const programId = this.programId - Assert.ok( - clusterDataPath != null, - "provisionSplReserves: clusterDataPath required" - ) - Assert.ok(programId != null, "provisionSplReserves: programId required") - log.info( - "[solana] provisioning mock SPL reserves (USDCSOL, USDTSOL, LIQSOL)..." - ) - - const primaryCode = new anchor.BN( - SlugName.from(SolanaOutpostBootstrapper.PrimaryReserveCodename) - ) - const persisted: SolanaOutpostBootstrapper.PersistedSplMint[] = [] - - // Every registered SPL mint can back a CollateralPosition, and an - // OPERATOR_ACTION(SLASH) settles that custody into the reserve_aggregate's - // canonical ATA (opp/inbound.rs `process_slash_action` SPL branch). The - // program never creates ATAs, so a missing one makes the slash log-and-skip - // — silently dropping the seizure, which carries no return attestation to - // re-drive it. Pre-create the aggregate ATA per mint here so every SPL slash - // has a live destination (SOL-380). - const reserveAggregatePda = this.deriveProgramAddress( - programId, - SolanaOutpostBootstrapper.PdaSeed.ReserveAggregate - ) - - // Sequential: each step depends on the previous landing on-chain. - await mapSeries( - SolanaOutpostBootstrapper.SplReserveSpecifications, - async specification => { - const code = SlugName.from(specification.codeName) - const codeBigNumber = new anchor.BN(code) - log.info( - `[solana] - creating mock SPL mint for ${specification.codeName} (decimals=${specification.decimals})` - ) - const mint = await SolanaFundingTool.createMockSplMint( - this.connection, - deployer, - specification.decimals - ) - log.info(`[solana] mint=${mint.toBase58()}`) - - const deployerAta = await SolanaFundingTool.mintMockSplToUser( - this.connection, - deployer, - mint, - deployer.publicKey, - specification.chainAmount * 2n - ) - log.info( - `[solana] deployer ATA funded (ata=${deployerAta.toBase58()})` - ) - - const setAddressTransaction = await program.methods - .setTokenAddress(codeBigNumber, mint) - .accounts({ ...this.getAdminAccounts(deployer), config: configPda }) - .signers([deployer]) - .transaction() - await this.runSimpleAuthorityInstruction( - deployer, - setAddressTransaction, - `set_token_address(${specification.codeName})` - ) - - const setPrecisionTransaction = await program.methods - .setTokenPrecision(codeBigNumber, specification.decimals) - .accounts({ ...this.getAdminAccounts(deployer), config: configPda }) - .signers([deployer]) - .transaction() - await this.runSimpleAuthorityInstruction( - deployer, - setPrecisionTransaction, - `set_token_precision(${specification.codeName})` - ) - - // reserve_aggregate is a PDA (off-curve owner) — the SPL slash destination. - const aggregateAta = - await SolanaFundingTool.ensureAssociatedTokenAccount( - this.connection, - deployer, - mint, - reserveAggregatePda, - true - ) - log.info( - `[solana] reserve_aggregate ATA ensured (ata=${aggregateAta.toBase58()})` - ) - - const reservePda = this.deriveReserveScopedAddress( - programId, - SolanaOutpostBootstrapper.PdaSeed.Reserve, - code, - SlugName.from(SolanaOutpostBootstrapper.PrimaryReserveCodename) - ) - const reserveVaultPda = this.deriveReserveScopedAddress( - programId, - SolanaOutpostBootstrapper.PdaSeed.ReserveVault, - code, - SlugName.from(SolanaOutpostBootstrapper.PrimaryReserveCodename) - ) - const chainAmount = new anchor.BN(specification.chainAmount.toString()) - const createTransaction = await program.methods - .createReserveSplAuthority( - codeBigNumber, - primaryCode, - chainAmount, - chainAmount, - SolanaOutpostBootstrapper.BootstrapConnectorWeightBps, - `SOLANA-${specification.codeName}/WIRE primary reserve`, - `Bootstrap-seeded mock ${specification.codeName} ↔ WIRE reserve (outpost-side custody)` - ) - .accounts({ - payer: deployer.publicKey, - ...this.getAdminAccounts(deployer), - config: configPda, - reserve: reservePda, - reserveVault: reserveVaultPda, - mint, - adminAta: deployerAta, - tokenProgram: TOKEN_PROGRAM_ID, - systemProgram: anchor.web3.SystemProgram.programId, - rent: anchor.web3.SYSVAR_RENT_PUBKEY - }) - .signers([deployer]) - .transaction() - await this.runSimpleAuthorityInstruction( - deployer, - createTransaction, - `create_reserve_spl_authority(${specification.codeName}/PRIMARY)` - ) - - persisted.push({ - code, - mint: mint.toBase58(), - decimals: specification.decimals - }) - log.info( - `[solana] reserve PDA seeded (${specification.codeName}/PRIMARY)` - ) - } - ) - - mkdirs(clusterDataPath) - const persistedFile = Path.join(clusterDataPath, "sol-mock-mints.json") - Fs.writeFileSync(persistedFile, JSON.stringify(persisted, null, 2)) - log.info( - `[solana] persisted ${persisted.length} mock SPL mint(s) to ${persistedFile}` - ) } /** @@ -768,9 +477,18 @@ export class SolanaOutpostBootstrapper { epochDurationSec: number ): Promise { const { operators, groupMembers } = seed - Assert.ok(operators.length > 0, "oppBootstrap: at least one operator is required") - Assert.ok(groupMembers.length > 0, "oppBootstrap: at least one group member is required") - Assert.ok(epochDurationSec > 0, "oppBootstrap: epochDurationSec must be positive") + Assert.ok( + operators.length > 0, + "oppBootstrap: at least one operator is required" + ) + Assert.ok( + groupMembers.length > 0, + "oppBootstrap: at least one group member is required" + ) + Assert.ok( + epochDurationSec > 0, + "oppBootstrap: epochDurationSec must be positive" + ) Assert.ok( SolanaOutpostBootstrapper.oppBootstrapEncodedBytes( operators.length, @@ -803,7 +521,10 @@ export class SolanaOutpostBootstrapper { const Seed = SolanaOutpostBootstrapper.PdaSeed, configPda = this.deriveProgramAddress(programId, Seed.OutpostConfig), - operatorRegistryPda = this.deriveProgramAddress(programId, Seed.OperatorRegistry) + operatorRegistryPda = this.deriveProgramAddress( + programId, + Seed.OperatorRegistry + ) log.info( `opp_bootstrap: seeding ${operators.length} operator(s), group of ${groupMembers.length}, epoch_duration=${epochDurationSec}s` @@ -818,26 +539,22 @@ export class SolanaOutpostBootstrapper { }) .signers([deployer]) .transaction() - await this.runSimpleAuthorityInstruction(deployer, transaction, "opp_bootstrap") + await this.runSimpleAuthorityInstruction( + deployer, + transaction, + "opp_bootstrap" + ) log.info("opp_bootstrap: SOL outpost roster seeded — registry_initialized") } } export namespace SolanaOutpostBootstrapper { - /** Logged when the launch policy skips the withheld outpost operations. */ - export const LaunchWithheldOperationsSkippedMessage = - "launch policy: skipping init_reserve, create_reserve_native and mock SPL reserve provisioning (enable with --enable-launch-withheld-operations)" /** Total attempts allowed for each airdrop / RPC retry block. */ export const AirdropRetryAttempts = 3 /** Delay between airdrop / RPC retries (ms). */ export const AirdropRetryDelayMs = 2_000 /** Default airdrop size (SOL) for the deployer + refunded signing accounts. */ export const DefaultAirdropSol = 100 - /** - * Lamports the bootstrap-seeded native SOL Reserve PDA is funded with — sized - * for ~40 swap-with-underwriting runs (~0.5 SOL each) plus the rent floor. - */ - export const BootstrapNativeReserveLamports = 20 * LAMPORTS_PER_SOL /** Bancor connector weight (basis points) for the bootstrap-seeded reserves. */ export const BootstrapConnectorWeightBps = 5000 @@ -847,8 +564,6 @@ export namespace SolanaOutpostBootstrapper { export const SolTokenCodename = "SOL" /** Native SOL chain decimals (lamports) — bound via `set_token_precision`. */ export const SolTokenDecimals = 9 - /** Default reserve codename. */ - export const PrimaryReserveCodename = "PRIMARY" /** Program-derived-address seeds — MUST match `wire-solana/programs/liqsol-core/src/states/opp_states.rs`. */ export namespace PdaSeed { @@ -858,9 +573,6 @@ export namespace SolanaOutpostBootstrapper { export const InboundEnvelopes = "inbound_envelopes" export const OutboundEnvelopes = "outbound_envelopes" export const LatestOutboundEnvelope = "latest_outbound_envelope" - export const ReserveAggregate = "reserve_aggregate" - export const Reserve = "reserve" - export const ReserveVault = "reserve_vault" /** * Per-`(operator, token_code)` bonded-collateral position. BOTH deposit * instructions declare it `init_if_needed, payer = depositor`, so it is @@ -1001,33 +713,6 @@ export namespace SolanaOutpostBootstrapper { decimals: number } - /** A mock SPL reserve to provision: codename, decimals, bootstrap chain-side amount. */ - export interface SplReserveSpecification { - codeName: string - decimals: number - chainAmount: bigint - } - - /** - * The mock SPL reserves provisioned at bootstrap. USDCSOL/USDTSOL use 6 - * decimals (mainnet parity); LIQSOL uses 9 (depot parity). Distinct SOL-side - * slug_names (`USDCSOL`/`USDTSOL`) per the v6 "two Token rows per pair" rule. - */ - export const SplReserveSpecifications: ReadonlyArray = - [ - { - codeName: "USDCSOL", - decimals: 6, - chainAmount: 1_000_000n * 1_000_000n - }, - { - codeName: "USDTSOL", - decimals: 6, - chainAmount: 1_000_000n * 1_000_000n - }, - { codeName: "LIQSOL", decimals: 9, chainAmount: 20n * 1_000_000_000n } - ] - /** Default deployer keypair file (`~/.config/solana/id.json`). */ export function defaultDeployerKeypairFile(): string { return Path.join(process.env.HOME || "~", ".config", "solana", "id.json") diff --git a/packages/cluster-tool/src/orchestration/solana/SolanaOutpostSteps.ts b/packages/cluster-tool/src/orchestration/solana/SolanaOutpostSteps.ts index 2634ee205..fbb531544 100644 --- a/packages/cluster-tool/src/orchestration/solana/SolanaOutpostSteps.ts +++ b/packages/cluster-tool/src/orchestration/solana/SolanaOutpostSteps.ts @@ -44,13 +44,22 @@ export namespace SolanaOutpostSteps { * token registration). Input-less — paths + RPC come from `ctx.config` / * `ctx.solana`; the validator must already be running. */ - export function planDeploy( + export function planDeploy< + C extends ClusterBuildContext = ClusterBuildContext + >( actor: Report.Actor, name: string, description: string, options: ClusterBuildStepOptions ): ClusterBuildStep { - return ClusterBuildStep.create(actor, name, description, options, null, runDeploy) + return ClusterBuildStep.create( + actor, + name, + description, + options, + null, + runDeploy + ) } /** Named runner — `SolanaOutpostBootstrapper.bootstrap`. */ @@ -63,8 +72,7 @@ export namespace SolanaOutpostSteps { await new SolanaOutpostBootstrapper({ solanaPath: ctx.config.solanaPath, rpcUrl: ctx.solana.rpcUrl, - clusterDataPath: ctx.config.dataPath, - enableLaunchWithheldOperations: ctx.config.enableLaunchWithheldOperations + clusterDataPath: ctx.config.dataPath }).bootstrap() } @@ -76,13 +84,22 @@ export namespace SolanaOutpostSteps { * its first envelope. Input-less — the roster + group are resolved from * `ctx.keyStore` and the depot's `sysio.epoch::epochstate`. */ - export function planOppBootstrap( + export function planOppBootstrap< + C extends ClusterBuildContext = ClusterBuildContext + >( actor: Report.Actor, name: string, description: string, options: ClusterBuildStepOptions ): ClusterBuildStep { - return ClusterBuildStep.create(actor, name, description, options, null, runOppBootstrap) + return ClusterBuildStep.create( + actor, + name, + description, + options, + null, + runOppBootstrap + ) } /** Named runner — `SolanaOutpostBootstrapper.oppBootstrap`. */ @@ -151,7 +168,9 @@ export namespace SolanaOutpostSteps { ) } - const operatorByAccount = new Map(batchOperators.map(operator => [operator.account, operator])), + const operatorByAccount = new Map( + batchOperators.map(operator => [operator.account, operator]) + ), groupOperators = seedAccounts.map(accountName => { const operator = operatorByAccount.get(accountName) Assert.ok( @@ -164,12 +183,13 @@ export namespace SolanaOutpostSteps { ) return operator }) - const operators: SolanaOutpostBootstrapper.BootstrapOperator[] = groupOperators.map(operator => ({ - wireName: new anchor.BN(Name.from(operator.account).value.toString()), - solAddress: new PublicKey(solanaNativePublicKey(operator.solana)), - role: OperatorType.BATCH, - status: OperatorStatus.ACTIVE - })), + const operators: SolanaOutpostBootstrapper.BootstrapOperator[] = + groupOperators.map(operator => ({ + wireName: new anchor.BN(Name.from(operator.account).value.toString()), + solAddress: new PublicKey(solanaNativePublicKey(operator.solana)), + role: OperatorType.BATCH, + status: OperatorStatus.ACTIVE + })), groupMembers = operators.map(operator => operator.solAddress) return { operators, groupMembers } } diff --git a/packages/cluster-tool/src/orchestration/steps/ExternalOutpostSteps.ts b/packages/cluster-tool/src/orchestration/steps/ExternalOutpostSteps.ts index e9cc6b09c..bd3653eaf 100644 --- a/packages/cluster-tool/src/orchestration/steps/ExternalOutpostSteps.ts +++ b/packages/cluster-tool/src/orchestration/steps/ExternalOutpostSteps.ts @@ -33,18 +33,9 @@ const log = getLogger(__filename) */ export namespace ExternalOutpostSteps { /** ETH outpost address keys the daemons require (parity with local prep). */ - const RequiredEthereumAddressKeys = [ - "OPP", - "OPPInbound", - "OperatorRegistry", - "ReserveManager" - ] as const + const RequiredEthereumAddressKeys = ["OPP", "OPPInbound"] as const /** SOL outpost IDL instructions the daemons require (parity with local prep). */ - const RequiredSolanaIdlInstructions = [ - "epoch_in", - "commit_underwrite", - "request_swap" - ] as const + const RequiredSolanaIdlInstructions = ["epoch_in"] as const /** Minimum head-block advance that proves the depot is producing blocks. */ export const HeadAdvanceMinBlocks = 2 @@ -115,7 +106,9 @@ export namespace ExternalOutpostSteps { "ExternalOutpostSteps.planMaterialize requires config.externalOutposts (external-outpost mode only)" ) const dataPath = ctx.config.dataPath, - deploymentsDir = ClusterConfigProvider.ethereumDeploymentsPath(ctx.config), + deploymentsDir = ClusterConfigProvider.ethereumDeploymentsPath( + ctx.config + ), abiDir = Path.join(dataPath, OperatorDaemonTool.EthereumAbiSubpath), idlDir = Path.join(dataPath, OperatorDaemonTool.SolanaIdlSubpath), materialize = (source: string, destination: string): void => { @@ -418,7 +411,9 @@ export namespace ExternalOutpostSteps { `an outbound envelope is queued for every registered outpost (${expectedLabel})`, async () => { const { rows: outbound } = await ctx.wire.getOutboundEnvelopes(), - queued = new Set(outbound.map(row => packedSlugValue(row.chain_code))) + queued = new Set( + outbound.map(row => packedSlugValue(row.chain_code)) + ) return expected.every(code => queued.has(code)) }, OutboundEnvelopesPollBudgetMs, @@ -439,7 +434,10 @@ export namespace ExternalOutpostSteps { ) throw new NestedError( "external-outpost bootstrap gate: the depot queued no outbound envelope for every registered outpost", - { cause: error, context: { expected: expectedLabel, depotEnvelopeCount } } + { + cause: error, + context: { expected: expectedLabel, depotEnvelopeCount } + } ) } } diff --git a/packages/cluster-tool/src/orchestration/steps/RegistrySteps.ts b/packages/cluster-tool/src/orchestration/steps/RegistrySteps.ts index 2aa5222f6..31588b1c1 100644 --- a/packages/cluster-tool/src/orchestration/steps/RegistrySteps.ts +++ b/packages/cluster-tool/src/orchestration/steps/RegistrySteps.ts @@ -2,11 +2,20 @@ import Assert from "node:assert" import Fs from "node:fs" import Path from "node:path" import { PublicKey as SolanaPublicKey } from "@solana/web3.js" -import { KeyType, PrivateKey, PublicKey, SlugName, SysioContracts } from "@wireio/sdk-core" +import { + KeyType, + PrivateKey, + PublicKey, + SlugName, + SysioContracts +} from "@wireio/sdk-core" import { KeyGenerator } from "../../clients/wire/KeyGenerator.js" import { keyPairFromPrivate, solanaKeypair } from "../../utils/keyPairUtils.js" import type { StepInput } from "../StepRunner.js" -import { MockSyndicationBonderKey, type MockSyndicationBonderOutput } from "../outputs/MockSyndicationBonderOutput.js" +import { + MockSyndicationBonderKey, + type MockSyndicationBonderOutput +} from "../outputs/MockSyndicationBonderOutput.js" import { KeySteps } from "./KeySteps.js" import { eachSeries } from "../../utils/asyncUtils.js" import { AnvilProcess } from "../../cluster/processes/AnvilProcess.js" @@ -24,7 +33,6 @@ import { WireSyndicationTool } from "../../tools/wire/WireSyndicationTool.js" import { SolanaFundingTool } from "../../tools/solana/SolanaFundingTool.js" import { BondContractSteps } from "./contracts/sysio/BondContractSteps.js" import { LiqContractSteps } from "./contracts/sysio/LiqContractSteps.js" -import { ReservContractSteps } from "./contracts/sysio/ReservContractSteps.js" import { SyndContractSteps } from "./contracts/sysio/SyndContractSteps.js" import { OperatorDaemonArtifactsKey } from "../outputs/OperatorDaemonArtifacts.js" @@ -50,49 +58,6 @@ const { * bootstrap epoch-0 window, so the phase only ever runs pre-EpochBootstrap). */ export namespace RegistrySteps { - /** Bootstrap reserve chain/wire seed amount (each token's depot frame = `min(native, 9)` decimals). */ - const ReserveSeedAmount = 10_000_000_000 - /** Bancor connector weight (bps) for every bootstrap reserve. */ - const ConnectorWeightBps = 5000 - /** Codenames whose reserves carry native 6-dec precision (stablecoins). */ - const StableCodenames = ["USDC", "USDT", "USDCSOL", "USDTSOL"] - /** Reserve code every mock reserve registers under. */ - const PrimaryReserveCodename = "PRIMARY" - /** Divisor on a stablecoin reserve's chain seed (its 6-dec frame vs the 9-dec default). */ - const StableChainSeedDivisor = 1000 - /** `source_token_precision` for a stablecoin reserve (native 6-dec). */ - const StableTokenPrecision = 6 - /** `source_token_precision` for every non-stablecoin reserve (depot 9-dec frame). */ - const DefaultTokenPrecision = 9 - /** - * The 8 mock (chain, token) reserve pairs — `[chainCodename, tokenCodename, - * label]`. Private source both {@link MockReserveRegistrations} (the rows) and - * {@link planMockReserves} (the per-step names) derive from, in this order. - */ - const MockReservePairs = [ - ["ETHEREUM", "ETH", "native ETH"], - ["ETHEREUM", "LIQETH", "liqETH"], - ["ETHEREUM", "USDC", "USDC (mock ERC-20)"], - ["ETHEREUM", "USDT", "USDT (mock ERC-20)"], - ["SOLANA", "SOL", "native SOL"], - ["SOLANA", "LIQSOL", "liqSOL"], - ["SOLANA", "USDCSOL", "USDC (mock SPL)"], - ["SOLANA", "USDTSOL", "USDT (mock SPL)"] - ] as const - - /** - * The 8 mock (chain, token) PRIMARY `sysio.reserv::regreserve` rows — fully - * static (string codenames + numeric constants, no deploy-artifact reads), - * byte-identical to the pre-split unconditional seeding. Shared by - * {@link planMockReserves} (one Report step per row) and its unit test. The - * contract gates `regreserve` to the bootstrap window (epoch 0), so these seed - * ONLY during bootstrap — never from a flow phase. - */ - export const MockReserveRegistrations: SysioContracts.SysioReservRegreserveAction[] = - MockReservePairs.map(([chainCodename, tokenCodename, label]) => - toReserveRegistration(chainCodename, tokenCodename, label) - ) - /** * The shadow liq symbols the bootstrap opens on `sysio.liq` — `[chainCodename, * tokenCodename]`, one per liq token {@link runSeedRegistry} registers. Private @@ -584,8 +549,8 @@ export namespace RegistrySteps { outpost: { opp_addr: ethAddress("OPP"), opp_inbound_addr: ethAddress("OPPInbound"), - operator_registry_addr: ethAddress("OperatorRegistry"), - source_deposit_addr: ethAddress("ReserveManager") + operator_registry_addr: "", + source_deposit_addr: "" } }) // One Solana program serves every role, so it goes in `opp_addr` alone — @@ -601,43 +566,6 @@ export namespace RegistrySteps { }) } - /** - * Seed the 8 mock (chain, token) PRIMARY reserves as ONE - * {@link ClusterBuildPhase} of per-reserve `sysio.reserv::regreserve` steps — - * every reserve write is its own Report-validated step (the rows are fully - * static, from {@link MockReserveRegistrations}). Composed ONLY when - * `--enable-mock-reserves` is set; the depot contract gates `regreserve` to - * the bootstrap window (epoch 0), so this phase only ever runs - * pre-EpochBootstrap and can never be reached from a flow phase. - * Self-registers on `parent`. - * - * @param parent - The build root or enclosing PhaseGroup. - * @param name - Short phase name. - * @param description - Human-readable phase description. - * @param options - Step option overrides threaded to every reserve step. - * @returns The self-registered reserve-seeding phase. - */ - export function planMockReserves< - C extends ClusterBuildContext = ClusterBuildContext - >( - parent: ClusterBuildParent, - name: string, - description: string, - options: ClusterBuildStepOptions - ): ClusterBuildPhase { - const steps: ClusterBuildStep.Any[] = MockReservePairs.map( - ([chainCodename, tokenCodename], index) => - ReservContractSteps.planRegreserve( - Report.Actor.Sysio, - `seed-reserve-${chainCodename.toLowerCase()}-${tokenCodename.toLowerCase()}`, - `seed the ${chainCodename}/${tokenCodename} PRIMARY reserve`, - options, - MockReserveRegistrations[index] - ) - ) - return ClusterBuildPhase.create(parent, name, description, steps) - } - /** * ONE phase of per-shadow `sysio.liq::create` steps — the shadow symbols the * depot mints syndicated liq into, one per registered liq token. Registry @@ -944,38 +872,6 @@ export namespace RegistrySteps { ) } - // ── reserve-row builder (fully static — no deploy artifacts) ── - - /** - * Build one static `regreserve` row for a (chain, token) PRIMARY reserve: - * stablecoins carry native 6-dec precision + a ÷1000 chain seed, everything - * else the depot's 9-dec frame. Byte-identical to the pre-split seeding. - */ - function toReserveRegistration( - chainCodename: string, - tokenCodename: string, - label: string - ): SysioContracts.SysioReservRegreserveAction { - const stable = StableCodenames.includes(tokenCodename) - return { - chain_code: chainCodename, - token_code: tokenCodename, - reserve_code: PrimaryReserveCodename, - name: `${chainCodename}-${tokenCodename}/WIRE primary reserve`, - description: `Bootstrap-seeded ${label} ↔ WIRE reserve`, - initial_chain_amount: stable - ? ReserveSeedAmount / StableChainSeedDivisor - : ReserveSeedAmount, - initial_wire_amount: ReserveSeedAmount, - source_token_precision: stable - ? StableTokenPrecision - : DefaultTokenPrecision, - connector_weight_bps: ConnectorWeightBps, - is_private: false, - owner: "" - } - } - // ── token-row builders (native = empty addr; the rest carry a ChainAddress) ── function nativeToken( diff --git a/packages/cluster-tool/src/orchestration/steps/contracts/sysio/ReservContractSteps.ts b/packages/cluster-tool/src/orchestration/steps/contracts/sysio/ReservContractSteps.ts deleted file mode 100644 index 7ac372bf7..000000000 --- a/packages/cluster-tool/src/orchestration/steps/contracts/sysio/ReservContractSteps.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { SysioContracts } from "@wireio/sdk-core" -import { WireClient } from "../../../../clients/wire/WireClient.js" -import { Report } from "../../../../report/Report.js" -import { ClusterBuildContext } from "../../../ClusterBuildContext.js" -import { - ClusterBuildStep, - type ClusterBuildStepOptions -} from "../../../ClusterBuildStep.js" -import type { StepInput } from "../../../StepRunner.js" - -const { SysioContractName } = SysioContracts - -/** - * Steps for `sysio.reserv` actions. - * - * The owner-fee actions are signed by the RESERVE'S OWNER (`WireClient.activeAuthorization`), - * not by the contract. The owner is not part of either action's data (the contract reads it - * off the row), so it rides the step input. - */ -export namespace ReservContractSteps { - /** Input for {@link planRegreserve} — the generated `reserv::regreserve` data. */ - export interface RegreserveInput extends StepInput { - readonly kind: "ReservContractSteps.RegreserveInput" - readonly data: SysioContracts.SysioReservRegreserveAction - } - - /** `sysio.reserv::regreserve` — seed one `(chain, token, reserve)` reserve book. */ - export function planRegreserve( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - data: SysioContracts.SysioReservRegreserveAction - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { kind: "ReservContractSteps.RegreserveInput", data }, - runRegreserve - ) - } - - /** Named runner — `sysio.reserv::regreserve`. */ - export async function runRegreserve( - ctx: C, - input: RegreserveInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - await ctx.wire - .getSysioContract(SysioContractName.reserv) - .actions.regreserve.invoke(input.data) - } - - /** Input for {@link planSetconfig} — the generated `reserv::setconfig` data. */ - export interface SetconfigInput extends StepInput { - readonly kind: "ReservContractSteps.SetconfigInput" - readonly data: SysioContracts.SysioReservSetconfigAction - } - - /** - * `sysio.reserv::setconfig` — set the fee-routing config. Its one field, - * `fee_emissions_share_bps`, is stage 2 of the swap-fee split: the share of - * each fee's rewards pool sent to the `sysio` emissions treasury instead of - * the batch-operator rewards bucket. Zero (the default) keeps every fee inside - * `sysio.reserv` custody at settlement. - */ - export function planSetconfig( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - data: SysioContracts.SysioReservSetconfigAction - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { kind: "ReservContractSteps.SetconfigInput", data }, - runSetconfig - ) - } - - /** Named runner — `sysio.reserv::setconfig`. */ - export async function runSetconfig( - ctx: C, - input: SetconfigInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - await ctx.wire - .getSysioContract(SysioContractName.reserv) - .actions.setconfig.invoke(input.data) - } - - /** Input for {@link planSetrsvfee} — the generated `reserv::setrsvfee` data. */ - export interface SetrsvfeeInput extends StepInput { - readonly kind: "ReservContractSteps.SetrsvfeeInput" - readonly data: SysioContracts.SysioReservSetrsvfeeAction - /** The reserve's owner — the required signer, absent from the action data. */ - readonly owner: string - } - - /** - * `sysio.reserv::setrsvfee` — set one reserve's owner fee, the independent - * per-reserve fee its liquidity earns on every swap that draws from it. Signed - * by the reserve's `owner`; the rate is `0` (charge nothing) or `[1, 9900]`. - */ - export function planSetrsvfee( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - data: SysioContracts.SysioReservSetrsvfeeAction, - owner: string - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { kind: "ReservContractSteps.SetrsvfeeInput", data, owner }, - runSetrsvfee - ) - } - - /** Named runner — `sysio.reserv::setrsvfee`, signed by the reserve owner. */ - export async function runSetrsvfee( - ctx: C, - input: SetrsvfeeInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - await ctx.wire - .getSysioContract(SysioContractName.reserv) - .actions.setrsvfee.invoke(input.data, { - authorization: WireClient.activeAuthorization(input.owner) - }) - } - - /** Input for {@link planClaimrsvfee} — the generated `reserv::claimrsvfee` data. */ - export interface ClaimrsvfeeInput extends StepInput { - readonly kind: "ReservContractSteps.ClaimrsvfeeInput" - readonly data: SysioContracts.SysioReservClaimrsvfeeAction - /** The reserve's owner — the required signer AND the payout recipient. */ - readonly owner: string - } - - /** - * `sysio.reserv::claimrsvfee` — pay one reserve's accrued owner fee out to its - * owner as real WIRE and zero the accrual. Signed by the reserve's `owner`. - */ - export function planClaimrsvfee( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - data: SysioContracts.SysioReservClaimrsvfeeAction, - owner: string - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { kind: "ReservContractSteps.ClaimrsvfeeInput", data, owner }, - runClaimrsvfee - ) - } - - /** Named runner — `sysio.reserv::claimrsvfee`, signed by the reserve owner. */ - export async function runClaimrsvfee( - ctx: C, - input: ClaimrsvfeeInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - await ctx.wire - .getSysioContract(SysioContractName.reserv) - .actions.claimrsvfee.invoke(input.data, { - authorization: WireClient.activeAuthorization(input.owner) - }) - } -} diff --git a/packages/cluster-tool/src/orchestration/steps/contracts/sysio/UwritContractSteps.ts b/packages/cluster-tool/src/orchestration/steps/contracts/sysio/UwritContractSteps.ts deleted file mode 100644 index bbdcc0bdb..000000000 --- a/packages/cluster-tool/src/orchestration/steps/contracts/sysio/UwritContractSteps.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { SysioContracts } from "@wireio/sdk-core" -import { Report } from "../../../../report/Report.js" -import { ClusterBuildContext } from "../../../ClusterBuildContext.js" -import { - ClusterBuildStep, - type ClusterBuildStepOptions -} from "../../../ClusterBuildStep.js" -import type { StepInput } from "../../../StepRunner.js" - -const { SysioContractName } = SysioContracts - -/** Steps for `sysio.uwrit` (underwriting) actions. */ -export namespace UwritContractSteps { - /** Input for {@link planSetconfig} — the generated `uwrit::setconfig` data. */ - export interface SetconfigInput extends StepInput { - readonly kind: "UwritContractSteps.SetconfigInput" - readonly data: SysioContracts.SysioUwritSetconfigAction - } - - /** `sysio.uwrit::setconfig` — WIRE-leg swap fee + collateral-lock challenge window. */ - export function planSetconfig( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - data: SysioContracts.SysioUwritSetconfigAction - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { kind: "UwritContractSteps.SetconfigInput", data }, - runSetconfig - ) - } - - /** Named runner — `sysio.uwrit::setconfig`. */ - export async function runSetconfig( - ctx: C, - input: SetconfigInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - await ctx.wire - .getSysioContract(SysioContractName.uwrit) - .actions.setconfig.invoke(input.data) - } -} diff --git a/packages/cluster-tool/src/orchestration/steps/contracts/sysio/index.ts b/packages/cluster-tool/src/orchestration/steps/contracts/sysio/index.ts index d5cb0a3f6..d39d31e09 100644 --- a/packages/cluster-tool/src/orchestration/steps/contracts/sysio/index.ts +++ b/packages/cluster-tool/src/orchestration/steps/contracts/sysio/index.ts @@ -7,14 +7,12 @@ import { EpochContractSteps } from "./EpochContractSteps.js" import { LiqContractSteps } from "./LiqContractSteps.js" import { MsgchContractSteps } from "./MsgchContractSteps.js" import { OpregContractSteps } from "./OpregContractSteps.js" -import { ReservContractSteps } from "./ReservContractSteps.js" import { RoaContractSteps } from "./RoaContractSteps.js" import { SwapContractSteps } from "./SwapContractSteps.js" import { SyndContractSteps } from "./SyndContractSteps.js" import { SystemContractSteps } from "./SystemContractSteps.js" import { TokenContractSteps } from "./TokenContractSteps.js" import { TokensContractSteps } from "./TokensContractSteps.js" -import { UwritContractSteps } from "./UwritContractSteps.js" /** * Step-layer mirror of the `sysio.*` system contracts: one sub-namespace per @@ -32,12 +30,10 @@ export namespace SysioContractSteps { export import liq = LiqContractSteps export import msgch = MsgchContractSteps export import opreg = OpregContractSteps - export import reserv = ReservContractSteps export import roa = RoaContractSteps export import swap = SwapContractSteps export import synd = SyndContractSteps export import system = SystemContractSteps export import token = TokenContractSteps export import tokens = TokensContractSteps - export import uwrit = UwritContractSteps } diff --git a/packages/cluster-tool/src/orchestration/steps/processes/NodeopProcessSteps.ts b/packages/cluster-tool/src/orchestration/steps/processes/NodeopProcessSteps.ts index c7ada0fe7..7d33da258 100644 --- a/packages/cluster-tool/src/orchestration/steps/processes/NodeopProcessSteps.ts +++ b/packages/cluster-tool/src/orchestration/steps/processes/NodeopProcessSteps.ts @@ -223,17 +223,19 @@ export namespace NodeopProcessSteps { ctx: ClusterBuildContext, node: NodeConfig ): OperatorAccount[] { - return match(node.role) - .with(NodeRole.bios, () => [ - ctx.keyStore.operator(NodeConfig.BiosName) ?? BiosOperator - ]) - .with(NodeRole.producer, () => producerOperators(ctx, node)) - .with(NodeRole.batch_operator, NodeRole.underwriter, () => [ - ctx.keyStore.assertOperator(assertOperatorLabel(node)) - ]) - // An API node acts for no account: nothing to sign, nothing to resolve. - .with(NodeRole.api, () => []) - .exhaustive() + return ( + match(node.role) + .with(NodeRole.bios, () => [ + ctx.keyStore.operator(NodeConfig.BiosName) ?? BiosOperator + ]) + .with(NodeRole.producer, () => producerOperators(ctx, node)) + .with(NodeRole.batch_operator, NodeRole.underwriter, () => [ + ctx.keyStore.assertOperator(assertOperatorLabel(node)) + ]) + // An API node acts for no account: nothing to sign, nothing to resolve. + .with(NodeRole.api, () => []) + .exhaustive() + ) } /** @@ -253,7 +255,9 @@ export namespace NodeopProcessSteps { // `filter` would collapse the offender to "one of them", and naming it is the whole value of // this failure -- it says which provisioning step never ran. return asOption(node.producers) - .tap(labels => labels.forEach(label => ctx.keyStore.assertOperator(label))) + .tap(labels => + labels.forEach(label => ctx.keyStore.assertOperator(label)) + ) .map(labels => labels.map(label => ctx.keyStore.operator(label))) .get() } @@ -285,8 +289,16 @@ export namespace NodeopProcessSteps { const artifacts = ctx.outputs.assert(OperatorDaemonArtifactsKey), network = OperatorDaemonTool.networkFromConfig(ctx.config), keySourceFor = ClusterConfigProvider.signatureProviderSource(ctx.config) - return node.role === NodeRole.batch_operator - ? OperatorDaemonTool.batchOperatorArgs(operator, artifacts, network, keySourceFor) - : OperatorDaemonTool.underwriterArgs(operator, artifacts, network, keySourceFor) + Assert.equal( + node.role, + NodeRole.batch_operator, + "Only batch operators run an OPP daemon" + ) + return OperatorDaemonTool.batchOperatorArgs( + operator, + artifacts, + network, + keySourceFor + ) } } diff --git a/packages/cluster-tool/src/tools/ethereum/EthereumCollateralTool.ts b/packages/cluster-tool/src/tools/ethereum/EthereumCollateralTool.ts deleted file mode 100644 index b958fd1e8..000000000 --- a/packages/cluster-tool/src/tools/ethereum/EthereumCollateralTool.ts +++ /dev/null @@ -1,454 +0,0 @@ -/** - * EthereumCollateralTool — Step factories for the Ethereum-outpost collateral - * writes. Every on-chain WRITE is its OWN {@link ClusterBuildStep} so the - * `Report` records it: {@link planDeposit} (native ETH), {@link planErc20Approval} - * (the ERC-20 allowance write), {@link planNonNativeDeposit} (the ERC-20 deposit - * write). Each runner resolves the operator identity from `ctx.keyStore`, binds the - * `OperatorRegistry` to the operator's derived wallet, and performs exactly ONE - * write. The contract-surface types + artifact resolution are pure value helpers - * used INSIDE the runners. - */ - -import Assert from "node:assert" -import Fs from "node:fs" -import { NestedError } from "@wireio/shared" -import Path from "node:path" -import { ethers } from "ethers" -import { match } from "ts-pattern" -import { OperatorType } from "@wireio/opp-typescript-models" -import { ClusterBuildContext } from "../../orchestration/ClusterBuildContext.js" -import { - ClusterBuildStep, - type ClusterBuildStepOptions -} from "../../orchestration/ClusterBuildStep.js" -import type { StepInput } from "../../orchestration/StepRunner.js" -import { - ethereumCompressedPubkey, - ethereumSigner -} from "../../utils/keyPairUtils.js" -import { Report } from "../../report/Report.js" -import { retry } from "../../utils/asyncUtils.js" -import { - contractView, - ethereumRevertReason, - resolveLatestNonce -} from "../../utils/ethereumUtils.js" -import { ClusterConfigProvider } from "../../config/ClusterConfigProvider.js" - -/** Positional argument tuple of `OperatorRegistry.depositNonNative(...)`. */ -type DepositNonNativeArgs = [ - chainCode: bigint, - tokenCode: bigint, - reserveCode: bigint, - operatorType: number, - compressedPubkey: string | Uint8Array, - amount: bigint, - overrides?: ethers.Overrides -] - -/** Structural surface of the `OperatorRegistry` writes this tool binds. */ -export interface OperatorRegistryContract extends ethers.BaseContract { - deposit: ( - operatorType: number, - compressedPubkey: string | Uint8Array, - tokenCode: bigint, - amount: bigint, - overrides?: ethers.Overrides - ) => Promise - depositNonNative: ( - ...args: DepositNonNativeArgs - ) => Promise - nativeTokenCode: () => Promise - getAddress: () => Promise -} - -/** Structural surface of an ERC-20 `approve` write. */ -export interface Erc20ApprovableContract extends ethers.BaseContract { - approve: ( - spender: string, - amount: bigint, - overrides?: ethers.Overrides - ) => Promise -} - -export namespace EthereumCollateralTool { - // ── Step: native ETH planDeposit (`OperatorRegistry.deposit`) ──────────────── - - /** Input for {@link planDeposit} — one native-ETH collateral deposit write. */ - export interface DepositInput extends StepInput { - readonly kind: "EthereumCollateralTool.DepositInput" - /** Operator's durable `label` handle — resolved from `ctx.keyStore` (NOT its on-chain `account`). */ - readonly operatorLabel: string - readonly operatorType: OperatorType - /** 8-byte slug_name (`uint64`) of the deposited token (native `ETH`). */ - readonly tokenCode: bigint - /** Wei to escrow — forwarded as `msg.value` for the native token. */ - readonly amount: bigint - } - - /** A single native-ETH collateral deposit write to `OperatorRegistry.deposit(...)`. */ - export function planDeposit< - C extends ClusterBuildContext = ClusterBuildContext - >( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - operatorLabel: string, - operatorType: OperatorType, - tokenCode: bigint, - amount: bigint - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { - kind: "EthereumCollateralTool.DepositInput", - operatorLabel, - operatorType, - tokenCode, - amount - }, - runDeposit - ) - } - - /** Named runner — ONE `OperatorRegistry.deposit(...)` write, signed by the operator wallet. */ - export async function runDeposit( - ctx: C, - input: DepositInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - Assert.ok( - input.amount > 0n, - "EthereumCollateralTool.planDeposit: amount must be positive" - ) - const operator = ctx.keyStore.assertOperator(input.operatorLabel) - const compressedPubkey = ethereumCompressedPubkey(operator.ethereum) - Assert.ok( - compressedPubkey.byteLength === 33, - `EthereumCollateralTool.planDeposit: compressedPubkey must be 33 bytes, got ${compressedPubkey.byteLength}` - ) - const registry = loadOperatorRegistry( - ctx, - ethereumSigner(operator.ethereum, ctx.ethereum.provider) - ) - const isNative = input.tokenCode === (await registry.nativeTokenCode()) - const nonce = await resolveLatestNonce(registry) - const overrides: ethers.Overrides = isNative - ? { value: input.amount, nonce } - : { nonce } - const response = await registry.deposit( - input.operatorType, - compressedPubkey, - input.tokenCode, - input.amount, - overrides - ) - const receipt = await response.wait(1) - Assert.ok( - receipt?.status === 1, - `EthereumCollateralTool.planDeposit: reverted (status=${receipt?.status ?? "null"})` - ) - } - - // ── Step: ERC-20 allowance (`approve`) ─────────────────────────────────── - - /** Input for {@link planErc20Approval} — one ERC-20 allowance write. */ - export interface ApproveErc20Input extends StepInput { - readonly kind: "EthereumCollateralTool.ApproveErc20Input" - /** Operator's durable `label` handle — resolved from `ctx.keyStore` (NOT its on-chain `account`). */ - readonly operatorLabel: string - /** - * Mock token NAME (`"USDC"` / `"USDT"` / `"LIQETH"`) — the config-level - * identity. Token + OperatorRegistry ADDRESSES are deploy artifacts that - * do not exist when the step is CONSTRUCTED, so the runner resolves both. - */ - readonly tokenName: string - readonly amount: bigint - } - - /** A single `ERC20.approve(OperatorRegistry, amount)` write, signed by the operator wallet. */ - export function planErc20Approval< - C extends ClusterBuildContext = ClusterBuildContext - >( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - operatorLabel: string, - tokenName: string, - amount: bigint - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { - kind: "EthereumCollateralTool.ApproveErc20Input", - operatorLabel, - tokenName, - amount - }, - runErc20Approval - ) - } - - /** Named runner — resolve token + OperatorRegistry addresses, then ONE `ERC20.approve(...)` write. */ - export async function runErc20Approval( - ctx: C, - input: ApproveErc20Input, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - const operator = ctx.keyStore.assertOperator(input.operatorLabel) - const tokenAddress = mockErc20Address( - ClusterConfigProvider.ethereumDeploymentsPath(ctx.config), - input.tokenName - ) - const spender = loadOutpostAddresses( - ClusterConfigProvider.ethereumDeploymentsPath(ctx.config) - ).OperatorRegistry - Assert.ok( - spender != null && /^0x[0-9a-fA-F]{40}$/.test(spender), - `EthereumCollateralTool.approve: OperatorRegistry not in outpost-addrs.json (got ${spender})` - ) - const erc20 = contractView( - tokenAddress, - Erc20ApproveAbi, - ethereumSigner(operator.ethereum, ctx.ethereum.provider) - ) - const nonce = await resolveLatestNonce(erc20) - const response = await erc20.approve(spender, input.amount, { nonce }) - const receipt = await response.wait(1) - Assert.ok( - receipt?.status === 1, - `EthereumCollateralTool.approve: reverted (status=${receipt?.status ?? "null"})` - ) - } - - // ── Step: ERC-20 planDeposit (`depositNonNative`) ──────────────────────────── - - /** Input for {@link planNonNativeDeposit} — one ERC-20 collateral deposit write. */ - export interface DepositNonNativeInput extends StepInput { - readonly kind: "EthereumCollateralTool.DepositNonNativeInput" - /** Operator's durable `label` handle — resolved from `ctx.keyStore` (NOT its on-chain `account`). */ - readonly operatorLabel: string - readonly chainCode: bigint - readonly tokenCode: bigint - readonly reserveCode: bigint - readonly operatorType: OperatorType - readonly amount: bigint - } - - /** - * A single `OperatorRegistry.depositNonNative(...)` write. The ERC-20 must be - * pre-approved by an {@link planErc20Approval} Step earlier in the same phase. - */ - export function planNonNativeDeposit< - C extends ClusterBuildContext = ClusterBuildContext - >( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - operatorLabel: string, - chainCode: bigint, - tokenCode: bigint, - reserveCode: bigint, - operatorType: OperatorType, - amount: bigint - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { - kind: "EthereumCollateralTool.DepositNonNativeInput", - operatorLabel, - chainCode, - tokenCode, - reserveCode, - operatorType, - amount - }, - runNonNativeDeposit - ) - } - - /** Attempts for the staticCall dry-run gating the non-native deposit send. */ - const NonNativeDepositAttempts = 3 - /** Backoff between non-native deposit dry-run attempts (ms). */ - const NonNativeDepositRetryDelayMs = 500 - - /** - * Named runner — ONE `OperatorRegistry.depositNonNative(...)` write. - * - * The send is gated by a `staticCall` dry-run inside a bounded retry: the - * deposit can transiently revert after the bootstrap's anvil dump-state → - * load-state cycle (the first eth_call against freshly-loaded state can - * present stale OperatorRegistry / ReserveManager storage, then settle on a - * re-issue). Only a PERSISTENT revert surfaces — with its decoded - * `require(cond, "msg")` reason, which a mined status-0 receipt never carries. - */ - export async function runNonNativeDeposit( - ctx: C, - input: DepositNonNativeInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - Assert.ok( - input.amount > 0n, - "EthereumCollateralTool.planNonNativeDeposit: amount must be positive" - ) - const operator = ctx.keyStore.assertOperator(input.operatorLabel) - const registry = loadOperatorRegistry( - ctx, - ethereumSigner(operator.ethereum, ctx.ethereum.provider) - ) - const compressedPubkey = ethereumCompressedPubkey(operator.ethereum) - await retry( - () => - registry - .getFunction("depositNonNative") - .staticCall( - input.chainCode, - input.tokenCode, - input.reserveCode, - input.operatorType, - compressedPubkey, - input.amount - ), - { - maxAttempts: NonNativeDepositAttempts, - delayMs: NonNativeDepositRetryDelayMs, - label: `depositNonNative dry-run (${input.operatorLabel})` - } - ).catch(error => { - throw new NestedError( - "EthereumCollateralTool.runNonNativeDeposit: depositNonNative would revert", - { - cause: error, - context: { - revertReason: ethereumRevertReason(error), - chainCode: input.chainCode, - tokenCode: input.tokenCode, - reserveCode: input.reserveCode, - operatorType: input.operatorType, - amount: input.amount - } - } - ) - }) - const nonce = await resolveLatestNonce(registry) - const response = await registry.depositNonNative( - input.chainCode, - input.tokenCode, - input.reserveCode, - input.operatorType, - compressedPubkey, - input.amount, - { nonce } - ) - const receipt = await response.wait(1) - Assert.ok( - receipt?.status === 1, - `EthereumCollateralTool.planNonNativeDeposit: reverted (status=${receipt?.status ?? "null"})` - ) - } - - // ── value helpers (reads / artifact loads — executed INSIDE runners) ───── - - /** Minimal ERC-20 ABI for the `approve` write. */ - const Erc20ApproveAbi: ethers.InterfaceAbi = [ - "function approve(address spender, uint256 amount) returns (bool)" - ] - - /** - * Resolve the `OperatorRegistry` contract from the run's deploy artifacts, - * bound to `wallet`. Address from THIS cluster's deployments dir - * (`ClusterConfigProvider.ethereumDeploymentsPath`); ABI from the hardhat artifact. - */ - export function loadOperatorRegistry( - ctx: C, - wallet: ethers.Signer - ): OperatorRegistryContract { - const address = loadOutpostAddresses( - ClusterConfigProvider.ethereumDeploymentsPath(ctx.config) - ).OperatorRegistry - Assert.ok( - address != null && /^0x[0-9a-fA-F]{40}$/.test(address), - `EthereumCollateralTool: OperatorRegistry not in outpost-addrs.json (got ${address})` - ) - const abi = loadOutpostAbi(ctx.config.ethereumPath, "OperatorRegistry") - return contractView(address, abi, wallet) - } - - /** - * Read the outpost deploy-address map from THIS cluster's deployments dir - * (`ClusterConfigProvider.ethereumDeploymentsPath` — per-run, parallel-safe). - */ - export function loadOutpostAddresses( - deploymentsPath: string - ): Record { - const addressesPath = Path.join(deploymentsPath, "outpost-addrs.json") - Assert.ok( - Fs.existsSync(addressesPath), - `EthereumCollateralTool: outpost addresses not found at ${addressesPath}` - ) - return JSON.parse(Fs.readFileSync(addressesPath, "utf8")) - } - - /** - * Resolve a mock ERC-20's deployed address by token NAME from THIS cluster's - * deploy artifacts. Runners call this at RUN time — the address does not - * exist when steps are constructed (the outpost deploys later in the same - * build), and a configured collateral leg whose mock is missing is a hard - * failure, never a silent skip. - */ - export function mockErc20Address( - deploymentsPath: string, - tokenName: string - ): string { - const addresses = loadOutpostAddresses(deploymentsPath), - address = match(tokenName) - .with("USDC", () => addresses.MockUsdc) - .with("USDT", () => addresses.MockUsdt) - .with( - "LIQETH", - () => addresses.LiqEth ?? addresses.LiqETH ?? addresses.LiqEthToken - ) - .otherwise(() => null) - Assert.ok( - address != null && /^0x[0-9a-fA-F]{40}$/.test(address), - `EthereumCollateralTool: no deployed mock ERC-20 for ${tokenName} ` + - `(outpost-addrs.json keys: ${Object.keys(addresses).join(", ")})` - ) - return address - } - - /** Read a hardhat-built ABI for an outpost contract by name. */ - export function loadOutpostAbi( - ethereumPath: string, - contractName: string - ): ethers.InterfaceAbi { - const artifactPath = Path.join( - ethereumPath, - "artifacts", - "contracts", - "outpost", - `${contractName}.sol`, - `${contractName}.json` - ) - Assert.ok( - Fs.existsSync(artifactPath), - `EthereumCollateralTool: ABI artifact not found at ${artifactPath}` - ) - return JSON.parse(Fs.readFileSync(artifactPath, "utf8")).abi - } -} diff --git a/packages/cluster-tool/src/tools/ethereum/EthereumFundingTool.ts b/packages/cluster-tool/src/tools/ethereum/EthereumFundingTool.ts deleted file mode 100644 index 25718dca2..000000000 --- a/packages/cluster-tool/src/tools/ethereum/EthereumFundingTool.ts +++ /dev/null @@ -1,301 +0,0 @@ -/** - * Erc20FundingTool — test-cluster helpers for funding user wallets with - * mock ERC-20 balances and signing EIP-2612 permits. - * - * The harness's `ETHBootstrapper` deploys `MockUsdc`, `MockUsdt`, and - * `MockUsdtFeeOnTransfer` as part of the local cluster spin-up; both - * mocks expose an ungated `mint(address, uint256)` for test funding. - * This module is the thin wrapper around those mocks. - * - * Production deployments never run this code — they consume the - * canonical mainnet USDC / USDT contracts and fund users through - * normal exchange flows. - * - * @see wire-ethereum/contracts/test/outpost/MockUsdc.sol - * @see wire-ethereum/contracts/test/outpost/MockUsdt.sol - */ - -import Assert from "node:assert" -import { ethers } from "ethers" - -import { ClusterBuildContext } from "../../orchestration/ClusterBuildContext.js" -import { - ClusterBuildStep, - type ClusterBuildStepOptions -} from "../../orchestration/ClusterBuildStep.js" -import type { StepInput } from "../../orchestration/StepRunner.js" -import { Report } from "../../report/Report.js" -import { contractView, resolveLatestNonce } from "../../utils/ethereumUtils.js" -import { ethereumSigner } from "../../utils/keyPairUtils.js" -import { EthereumCollateralTool } from "./EthereumCollateralTool.js" -import { ClusterConfigProvider } from "../../config/ClusterConfigProvider.js" - -/** - * Minimal structural surface for an ERC-20 mock that exposes - * `mint(to, amount)`. All test mocks under `contracts/test/outpost/` - * conform to this shape; `MockLiqETH`, `MockUsdc`, `MockUsdt`, and - * `MockUsdtFeeOnTransfer` all carry the same `mint` signature. - */ -export interface MintableErc20 extends ethers.BaseContract { - mint: ( - to: string, - amount: bigint, - overrides?: ethers.Overrides - ) => Promise -} - -/** - * Mint `amount` of `mockErc20` to `recipient`. Confirms via `tx.wait()`. - * - * Resolves the runner's `pending` nonce explicitly and passes it as an - * `Overrides` field. ethers v6's auto-nonce population has surfaced - * intermittent NONCE_EXPIRED rejections from anvil when two `.mint(...)` - * calls are awaited back-to-back from the same `Wallet` instance — the - * second call re-fetches the count *before* anvil has marked the first - * tx as broadcast, so both txs encode the same nonce. Resolving the - * nonce ourselves and waiting on `tx.wait()` between calls makes the - * sequencing explicit and deterministic. - * - * @param mockErc20 Mock ERC-20 with an ungated `mint(to, amount)` method. - * Must be bound to a signer that holds the (ungated) - * mint capability. - * @param recipient EVM address of the user to fund. - * @param amount Token units to mint (chain-native base units — - * 6-decimal for USDC/USDT, 18-decimal for LIQETH). - * @return Mined transaction hash. - * @throws If the tx reverts or the receipt status is non-1. - */ -export async function mintMockErc20ToUser( - mockErc20: MintableErc20, - recipient: string, - amount: bigint -): Promise { - Assert.ok( - ethers.isAddress(recipient), - `Erc20FundingTool: recipient is not a valid address: ${recipient}` - ) - Assert.ok(amount > 0n, "Erc20FundingTool: mint amount must be > 0") - - const nonce = await resolveLatestNonce(mockErc20) - const tx = await mockErc20.mint(recipient, amount, { nonce }) - const receipt = await tx.wait(1) - Assert.ok( - receipt !== null && receipt.status === 1 && receipt.blockNumber > 0, - `Erc20FundingTool: mint tx not confirmed (status=${receipt?.status ?? "null"})` - ) - Assert.ok( - receipt !== null && receipt.status === 1, - `Erc20FundingTool: mint tx reverted (status=${receipt?.status ?? "null"})` - ) - return receipt.hash -} - -/** - * Minimal structural surface for an ERC-20 implementing - * EIP-2612 (`name() / nonces(owner) / DOMAIN_SEPARATOR()` plus the - * standard `permit` ABI). All `MockUsdc`-style mocks conform; mainnet - * USDC does too. `MockUsdt` (no permit) does NOT — callers MUST use - * the approval path for it. - */ -export interface Erc20PermitTarget extends ethers.BaseContract { - /** Token `name()` — used as the EIP-712 domain `name`. */ - name: () => Promise - /** Current permit nonce for `owner`. */ - nonces: (owner: string) => Promise - /** Contract address (typed as `getAddress()` on ethers contracts). */ - getAddress: () => Promise -} - -/** - * EIP-2612 permit signature components, ready to pass into - * `ReserveManager.requestSwapErc20WithPermit` / - * `requestReserveCreateErc20WithPermit` as the `PermitSig` struct. - */ -export interface PermitSignature { - deadline: bigint - v: number - r: string - s: string -} - -/** - * Produce a signed EIP-2612 permit for `owner` granting `spender` - * approval to transfer `value` units of `token` until `deadline`. - * - * @param owner Signer that holds the tokens — its private key signs - * the typed-data payload. - * @param token Contract exposing `name()` + `nonces(owner)` + - * `getAddress()`. Must be EIP-2612 compatible. - * @param spender Address that will be approved (typically - * `ReserveManager`). - * @param value Token units to approve (chain-native base units). - * @param deadline Unix-second deadline beyond which the permit is - * invalid. Pass `BigInt(Math.floor(Date.now()/1000) + 3600)` - * for a one-hour window. - * @return `(deadline, v, r, s)` ready to pack into a `PermitSig` struct. - */ -export async function signErc20Permit( - owner: ethers.Signer, - token: Erc20PermitTarget, - spender: string, - value: bigint, - deadline: bigint -): Promise { - Assert.ok( - ethers.isAddress(spender), - `Erc20FundingTool: spender is not a valid address: ${spender}` - ) - Assert.ok(value > 0n, "Erc20FundingTool: permit value must be > 0") - Assert.ok(deadline > 0n, "Erc20FundingTool: permit deadline must be > 0") - - const provider = owner.provider - Assert.ok(provider !== null, "Erc20FundingTool: owner signer has no provider") - - const ownerAddr = await owner.getAddress() - const [network, nonce, name, tokenAddr] = await Promise.all([ - provider.getNetwork(), - token.nonces(ownerAddr), - token.name(), - token.getAddress() - ]) - - const signature = await owner.signTypedData( - { - name, - version: "1", - chainId: network.chainId, - verifyingContract: tokenAddr - }, - { - Permit: [ - { name: "owner", type: "address" }, - { name: "spender", type: "address" }, - { name: "value", type: "uint256" }, - { name: "nonce", type: "uint256" }, - { name: "deadline", type: "uint256" } - ] - }, - { - owner: ownerAddr, - spender, - value, - nonce, - deadline - } - ) - const split = ethers.Signature.from(signature) - return { - deadline, - v: split.v, - r: split.r, - s: split.s - } -} - -/** - * Minimal ABI to balance-check + self-mint a mock ERC-20 for collateral funding. - * The mocks under `contracts/test/outpost/` expose an ungated `mint(to, amount)`, - * so an operator wallet can mint itself the balance it will `approve` + deposit. - */ -const MintWithBalanceAbi: ethers.InterfaceAbi = [ - "function balanceOf(address) view returns (uint256)", - "function mint(address to, uint256 amount)" -] - -/** - * The typed view over {@link MintWithBalanceAbi}: a {@link MintableErc20} that - * also exposes the `balanceOf` read the top-up path needs to mint only the - * shortfall. - */ -interface MintableErc20WithBalance extends MintableErc20 { - balanceOf: (address: string) => Promise -} - -/** - * EthereumFundingTool — Step factories for the mock-token funding WRITES the - * underwriter collateral flow needs before an ERC-20 `depositNonNative`. Every - * WRITE is its OWN {@link ClusterBuildStep} so the `Report` records it; the - * runner resolves the operator identity from `ctx.keyStore` by its durable - * `label` and performs exactly ONE mint. The plain `mintMockErc20ToUser` / `signErc20Permit` helpers above stay - * flow-layer utilities (they take a pre-bound contract / signer). - */ -export namespace EthereumFundingTool { - /** Input for {@link planErc20Mint} — one mock-ERC-20 self-mint to the operator wallet. */ - export interface MintErc20Input extends StepInput { - readonly kind: "EthereumFundingTool.MintErc20Input" - /** - * Operator's durable `label` handle — its ETH wallet is resolved from - * `ctx.keyStore` (NOT its on-chain `account`) and self-mints. - */ - readonly operatorLabel: string - /** - * Mock token NAME (`"USDC"` / `"USDT"` / `"LIQETH"`) — the config-level - * identity. The contract ADDRESS is a deploy artifact that does not exist - * when the step is CONSTRUCTED (the outpost deploys later in the same - * build), so the runner resolves it at run time. - */ - readonly tokenName: string - /** Token base units to bring the wallet's balance up to. */ - readonly amount: bigint - } - - /** - * A single mock-ERC-20 self-mint, topping the operator wallet up to `amount`. - * The operator's ETH wallet is resolved from `ctx.keyStore` by its durable - * `label` (self-mints via the mock's ungated `mint`); idempotent — a wallet - * already at/above `amount` no-ops. - */ - export function planErc20Mint< - C extends ClusterBuildContext = ClusterBuildContext - >( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - operatorLabel: string, - tokenName: string, - amount: bigint - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { - kind: "EthereumFundingTool.MintErc20Input", - operatorLabel, - tokenName, - amount - }, - runErc20Mint - ) - } - - /** Named runner — resolve the mock's deployed address, read the balance, then - * ONE `mint(...)` of the shortfall. */ - export async function runErc20Mint( - ctx: C, - input: MintErc20Input, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - Assert.ok( - input.amount > 0n, - "EthereumFundingTool.planErc20Mint: amount must be positive" - ) - const operator = ctx.keyStore.assertOperator(input.operatorLabel) - const signer = ethereumSigner(operator.ethereum, ctx.ethereum.provider) - const tokenAddress = EthereumCollateralTool.mockErc20Address( - ClusterConfigProvider.ethereumDeploymentsPath(ctx.config), - input.tokenName - ) - const token = contractView( - tokenAddress, - MintWithBalanceAbi, - signer - ) - const current = await token.balanceOf(signer.address) - if (current >= input.amount) return - await mintMockErc20ToUser(token, signer.address, input.amount - current) - } -} diff --git a/packages/cluster-tool/src/tools/ethereum/EthereumSwapTool.ts b/packages/cluster-tool/src/tools/ethereum/EthereumSwapTool.ts deleted file mode 100644 index 84dc51781..000000000 --- a/packages/cluster-tool/src/tools/ethereum/EthereumSwapTool.ts +++ /dev/null @@ -1,395 +0,0 @@ -/** - * EthereumSwapTool — user-side helper for triggering Ethereum → - * other-chain swaps via the Ethereum outpost's `ReserveManager.requestSwap`. - * - * Mirrors the {@link depositETHCollateral} shape: stateless, takes a - * connected `ReserveManager` contract (bound to the user's signer), packs - * the structured swap parameters, and awaits receipt confirmation. - * - * The contract emits `SwapRequested(user, sourceTokenCode, ..., targetAmount, - * targetToleranceBps)`, increments its own outpost-side reserve balance - * accounting, and queues a `SWAP_REQUEST` OPP attestation. Batch operators - * relay the envelope; the depot's `sysio.uwrit::createuwreq` opens a - * UWREQ row; the underwriter race resolves; a SWAP_REMIT returns inbound - * on the destination outpost and pays the recipient there. - * - * @see ReserveManager.sol:requestSwap - */ - -import Assert from "node:assert" -import { ethers } from "ethers" - -import { - resolveLatestNonce, - type EthereumPayableOverrides -} from "../../utils/ethereumUtils.js" - -/** - * Minimal `ethers` contract surface this helper relies on. Typed structurally - * so the caller can pass an `ethers.Contract`, a typechain-generated - * `ReserveManager`, or a compatible mock. - */ -export interface ReserveManagerRequestSwapContract extends ethers.BaseContract { - requestSwap: ( - sourceTokenCode: bigint, - sourceReserveCode: bigint, - targetChainCode: bigint, - targetTokenCode: bigint, - targetReserveCode: bigint, - targetRecipient: Uint8Array | string, - targetAmount: bigint, - targetToleranceBps: number, - overrides: EthereumPayableOverrides - ) => Promise -} - -/** - * Structured arguments for a SWAP_REQUEST emission. All slug_name codes - * are passed as `bigint` so the typechain ABI binding accepts them - * without lossy `number` conversion (slug_names round-trip cleanly - * through 2^53, but bigint is the safe shape). - */ -export interface EthereumSwapRequest { - /** slug_name of the source token on this outpost (must be native this pass). */ - sourceTokenCode: bigint - /** slug_name of the source reserve. */ - sourceReserveCode: bigint - /** Wei to escrow into the source reserve and quote as the swap input. */ - sourceAmountWei: bigint - /** slug_name of the target chain (e.g. `SlugName.from("SOLANA")`). */ - targetChainCode: bigint - /** slug_name of the target token (e.g. `SlugName.from("SOL")`). */ - targetTokenCode: bigint - /** slug_name of the target reserve. */ - targetReserveCode: bigint - /** - * Raw recipient address on the target chain. 32 bytes for Solana - * (ed25519 pubkey), 20 bytes for EVM destinations. - */ - targetRecipient: Uint8Array - /** - * User-specified minimum acceptable destination amount in - * destination-chain base units (e.g. lamports for SOL). The depot's - * variance check rejects with `SwapRevert` if drift exceeds - * `targetToleranceBps`. - */ - targetAmount: bigint - /** Acceptable variance in basis points (e.g. 50 = 0.5%). */ - targetToleranceBps: number -} - -/** - * Result of a successful `requestSwap` submission. - */ -export interface EthereumSwapResult { - /** Mined transaction hash. */ - transactionHash: string - /** Block number containing the request tx. */ - blockNumber: number - /** Cumulative gas used by the request tx. */ - gasUsed: bigint -} - -/** - * Submit a SWAP_REQUEST emission via the Ethereum outpost. - * - * Native ETH only this pass — the contract reverts with - * `WIRE_SwapSourceNotNative` for non-native source tokens. ERC-20 - * source-side custody lands with the `flow-swap-non-native-tokens` - * follow-on plan. - * - * @param reserveManager Contract bound to the user's signer - * (`contract.connect(wallet)`). - * @param request Structured swap parameters. - * @return Receipt details for cluster-state heartbeat reconciliation. - * @throws If the tx reverts or the receipt status is non-1. - */ -export async function requestEthereumSwap( - reserveManager: ReserveManagerRequestSwapContract, - request: EthereumSwapRequest -): Promise { - Assert.ok(request.sourceAmountWei > 0n, - "EthereumSwapTool: sourceAmountWei must be > 0") - Assert.ok(request.targetRecipient.byteLength > 0, - "EthereumSwapTool: targetRecipient must be non-empty") - Assert.ok(request.targetAmount > 0n, - "EthereumSwapTool: targetAmount must be > 0") - Assert.ok(request.targetToleranceBps >= 0 && request.targetToleranceBps <= 10_000, - `EthereumSwapTool: targetToleranceBps must be in [0, 10000], got ${request.targetToleranceBps}`) - - const nonce = await resolveLatestNonce(reserveManager) - const tx = await reserveManager.requestSwap( - request.sourceTokenCode, - request.sourceReserveCode, - request.targetChainCode, - request.targetTokenCode, - request.targetReserveCode, - request.targetRecipient, - request.targetAmount, - request.targetToleranceBps, - { value: request.sourceAmountWei, nonce } - ) - const receipt = await tx.wait(1) - Assert.ok( - receipt !== null && receipt.status === 1, - `EthereumSwapTool: requestSwap tx reverted (status=${receipt?.status ?? "null"})` - ) - return { - transactionHash: receipt.hash, - blockNumber: receipt.blockNumber, - gasUsed: receipt.gasUsed - } -} - -// ───────────────────────────────────────────────────────────────────── -// ERC-20 source-side swap helpers -// ───────────────────────────────────────────────────────────────────── - -/** - * EIP-2612 permit signature bundle ready to pass into - * `ReserveManager.requestSwapErc20WithPermit` (struct `PermitSig`). - * Matches the on-chain layout: `(deadline, v, r, s)`. - */ -export interface EthereumPermitSig { - /** Permit deadline (unix seconds). */ - deadline: bigint - /** Recovery id from the signature. */ - v: number - /** Signature r component. */ - r: string - /** Signature s component. */ - s: string -} - -/** - * Calldata-facing `SwapArgs` struct that mirrors - * `ReserveManagerLib.SwapArgs` on the contract. Carries `sourceAmount` - * directly (in chain-native token units — 6-decimal base units for - * USDC/USDT, 18-decimal wei for LIQETH). - */ -export interface EthereumSwapArgs { - sourceTokenCode: bigint - sourceReserveCode: bigint - sourceAmount: bigint - targetChainCode: bigint - targetTokenCode: bigint - targetReserveCode: bigint - targetRecipient: Uint8Array - targetAmount: bigint - targetToleranceBps: number -} - -/** Structural surface for the new ERC-20 entry points. */ -export interface ReserveManagerErc20SwapContract extends ethers.BaseContract { - requestSwapErc20WithPermit: ( - args: EthereumSwapArgs, - permitSig: EthereumPermitSig, - overrides?: ethers.Overrides - ) => Promise - requestSwapErc20WithApproval: ( - args: EthereumSwapArgs, - overrides?: ethers.Overrides - ) => Promise -} - -/** - * Submit a SWAP_REQUEST emission for an ERC-20 source token via inline - * EIP-2612 permit. The whole call is atomic: permit + transferFrom + - * fee-on-transfer guard + outbound queue all happen in a single tx. - * - * Used by `flow-swap-non-native-tokens` for USDC / USDT / LIQETH source - * legs. Native-ETH source still flows through `requestEthereumSwap`. - */ -export async function requestEthereumSwapErc20WithPermit( - reserveManager: ReserveManagerErc20SwapContract, - args: EthereumSwapArgs, - permitSig: EthereumPermitSig -): Promise { - Assert.ok(args.sourceAmount > 0n, - "EthereumSwapTool: sourceAmount must be > 0") - Assert.ok(args.targetRecipient.byteLength > 0, - "EthereumSwapTool: targetRecipient must be non-empty") - Assert.ok(args.targetAmount > 0n, - "EthereumSwapTool: targetAmount must be > 0") - Assert.ok(args.targetToleranceBps >= 0 && args.targetToleranceBps <= 10_000, - `EthereumSwapTool: targetToleranceBps must be in [0, 10000], got ${args.targetToleranceBps}`) - - const nonce = await resolveLatestNonce(reserveManager) - const tx = await reserveManager.requestSwapErc20WithPermit(args, permitSig, { nonce }) - const receipt = await tx.wait(1) - Assert.ok( - receipt !== null && receipt.status === 1, - `EthereumSwapTool: requestSwapErc20WithPermit tx reverted (status=${receipt?.status ?? "null"})` - ) - return { - transactionHash: receipt.hash, - blockNumber: receipt.blockNumber, - gasUsed: receipt.gasUsed - } -} - -/** - * Submit a SWAP_REQUEST emission for an ERC-20 source token via a - * pre-set allowance. Production fallback for legacy ERC-20s that don't - * implement EIP-2612 (mainnet USDT, etc.). Caller MUST have already - * called `IERC20(sourceToken).approve(reserveManager, sourceAmount)` in - * a prior transaction. - */ -export async function requestEthereumSwapErc20WithApproval( - reserveManager: ReserveManagerErc20SwapContract, - args: EthereumSwapArgs -): Promise { - Assert.ok(args.sourceAmount > 0n, - "EthereumSwapTool: sourceAmount must be > 0") - Assert.ok(args.targetRecipient.byteLength > 0, - "EthereumSwapTool: targetRecipient must be non-empty") - Assert.ok(args.targetAmount > 0n, - "EthereumSwapTool: targetAmount must be > 0") - Assert.ok(args.targetToleranceBps >= 0 && args.targetToleranceBps <= 10_000, - `EthereumSwapTool: targetToleranceBps must be in [0, 10000], got ${args.targetToleranceBps}`) - - const nonce = await resolveLatestNonce(reserveManager) - const tx = await reserveManager.requestSwapErc20WithApproval(args, { nonce }) - const receipt = await tx.wait(1) - Assert.ok( - receipt !== null && receipt.status === 1, - `EthereumSwapTool: requestSwapErc20WithApproval tx reverted (status=${receipt?.status ?? "null"})` - ) - return { - transactionHash: receipt.hash, - blockNumber: receipt.blockNumber, - gasUsed: receipt.gasUsed - } -} - -// ───────────────────────────────────────────────────────────────────── -// ERC-20 reserve-create helpers -// ───────────────────────────────────────────────────────────────────── - -/** - * Local mirror of `ReserveManager.sol::LocalReserveStatus` (zero-indexed - * one-byte storage enum: PENDING=0, ACTIVE=1, CANCELLED=2). Deliberately - * DISTINCT from the depot's proto `ReserveStatus` (UNKNOWN=0, PENDING=1, - * ACTIVE=2, CANCELLED=3) — the translation happens at the outpost dispatch - * boundary, so substituting the proto enum would shift every comparison by - * one. Consumed by the reserve flows when reading the outpost's local - * `getReserve(...)` record. - */ -export enum EthereumLocalReserveStatus { - PENDING = 0, - ACTIVE = 1, - CANCELLED = 2 -} - -/** - * The `ReserveManager.getReserve(tokenCode, reserveCode)` local record as the - * reserve flows read it: only `status` is consumed, compared against - * {@link EthereumLocalReserveStatus}. Ethers decodes the contract's one-byte - * storage enum as a `bigint`, so callers narrow with `Number(record.status)`. - */ -export interface EthereumLocalReserveRecord { - /** Outpost-local `ReserveManager.sol::LocalReserveStatus` ordinal. */ - status: bigint -} - -/** - * Calldata-facing `ReserveCreateArgs` struct mirroring - * `ReserveManagerLib.ReserveCreateArgs` on the contract. - * - * `externalTokenAmount` is RAW chain-native units (the escrow the - * contract takes custody of); the outpost converts it to the depot - * 9-decimal frame at the boundary for the RESERVE_CREATE attestation's - * `ReserveAmount` — callers never pre-scale. - */ -export interface EthereumReserveCreateArgs { - tokenCode: bigint - reserveCode: bigint - externalTokenAmount: bigint - requestedWireAmount: bigint - connectorWeightBps: number - name: string - description: string - /** Private reserves pair only with same-owner counterparts. */ - isPrivate: boolean - /** - * The caller's 33-byte compressed secp256k1 public key (hex) — the - * contract verifies it derives to the sending wallet, and the depot - * resolves the creator's authex link from it. - */ - creatorPubKey: string -} - -/** Structural surface for the ERC-20 reserve-create entries. */ -export interface ReserveManagerErc20ReserveCreateContract extends ethers.BaseContract { - requestReserveCreateErc20WithPermit: ( - args: EthereumReserveCreateArgs, - permitSig: EthereumPermitSig, - overrides?: ethers.Overrides - ) => Promise - requestReserveCreateErc20WithApproval: ( - args: EthereumReserveCreateArgs, - overrides?: ethers.Overrides - ) => Promise -} - -/** - * Open a new ERC-20 reserve on the Ethereum outpost via inline - * EIP-2612 permit. Permissionless — any user can call. - * - * Funds escrow into the contract atomically with the permit consumption - * and the RESERVE_CREATE outbound attestation queue. The depot's - * `sysio.reserv::oncrtreserve` handler inserts the depot-side row; - * subsequent depot `match` action flips status to ACTIVE and emits - * RESERVE_READY back, which the outpost handles inline. - */ -export async function requestEthereumReserveCreateErc20WithPermit( - reserveManager: ReserveManagerErc20ReserveCreateContract, - args: EthereumReserveCreateArgs, - permitSig: EthereumPermitSig -): Promise { - Assert.ok(args.externalTokenAmount > 0n, - "EthereumSwapTool: externalTokenAmount must be > 0") - Assert.ok(args.reserveCode !== 0n, - "EthereumSwapTool: reserveCode must be non-zero") - - const nonce = await resolveLatestNonce(reserveManager) - const tx = await reserveManager.requestReserveCreateErc20WithPermit(args, permitSig, { nonce }) - const receipt = await tx.wait(1) - Assert.ok( - receipt !== null && receipt.status === 1, - `EthereumSwapTool: requestReserveCreateErc20WithPermit tx reverted (status=${receipt?.status ?? "null"})` - ) - return { - transactionHash: receipt.hash, - blockNumber: receipt.blockNumber, - gasUsed: receipt.gasUsed - } -} - -/** - * Open a new ERC-20 reserve via a pre-set allowance. Companion to - * `requestEthereumReserveCreateErc20WithPermit` for legacy ERC-20s - * that don't implement EIP-2612. - */ -export async function requestEthereumReserveCreateErc20WithApproval( - reserveManager: ReserveManagerErc20ReserveCreateContract, - args: EthereumReserveCreateArgs -): Promise { - Assert.ok(args.externalTokenAmount > 0n, - "EthereumSwapTool: externalTokenAmount must be > 0") - Assert.ok(args.reserveCode !== 0n, - "EthereumSwapTool: reserveCode must be non-zero") - - const nonce = await resolveLatestNonce(reserveManager) - const tx = await reserveManager.requestReserveCreateErc20WithApproval(args, { nonce }) - const receipt = await tx.wait(1) - Assert.ok( - receipt !== null && receipt.status === 1, - `EthereumSwapTool: requestReserveCreateErc20WithApproval tx reverted (status=${receipt?.status ?? "null"})` - ) - return { - transactionHash: receipt.hash, - blockNumber: receipt.blockNumber, - gasUsed: receipt.gasUsed - } -} diff --git a/packages/cluster-tool/src/tools/ethereum/index.ts b/packages/cluster-tool/src/tools/ethereum/index.ts index 40a2f771f..a99e201f1 100644 --- a/packages/cluster-tool/src/tools/ethereum/index.ts +++ b/packages/cluster-tool/src/tools/ethereum/index.ts @@ -1,6 +1,3 @@ -export * from "./EthereumFundingTool.js" -export * from "./EthereumCollateralTool.js" -export * from "./EthereumSwapTool.js" export * from "./EthereumYieldEmitterTool.js" export * from "./EthereumNodeOwnerNftTool.js" export * from "./EthereumSyndicationTool.js" diff --git a/packages/cluster-tool/src/tools/solana/SolanaCollateralTool.ts b/packages/cluster-tool/src/tools/solana/SolanaCollateralTool.ts deleted file mode 100644 index 97d053112..000000000 --- a/packages/cluster-tool/src/tools/solana/SolanaCollateralTool.ts +++ /dev/null @@ -1,344 +0,0 @@ -/** - * SolanaCollateralTool — Step factories for the Solana-outpost - * `opp-outpost::deposit` / `deposit_non_native` collateral writes. Every Anchor - * WRITE is its OWN {@link ClusterBuildStep} so the `Report` records it: - * {@link planDeposit} (native SOL) and {@link planNonNativeDeposit} (SPL). Each runner - * resolves the operator identity from `ctx.keyStore` by its durable `label`, - * builds the `opp_outpost` program bound to the operator's keypair, resolves the - * program PDAs, and submits exactly ONE deposit ix. PDA derivation + IDL/program loading are pure value - * helpers used INSIDE the runners. - */ - -import Assert from "node:assert" -import * as anchor from "@coral-xyz/anchor" -import { - PublicKey, - SystemProgram, - SYSVAR_RENT_PUBKEY, - type Keypair -} from "@solana/web3.js" -import { - ASSOCIATED_TOKEN_PROGRAM_ID, - TOKEN_PROGRAM_ID, - getAssociatedTokenAddressSync -} from "@solana/spl-token" -import { OperatorType } from "@wireio/opp-typescript-models" -import { SolanaFundingTool } from "./SolanaFundingTool.js" -import { SolanaOutpostProgramTool } from "./SolanaOutpostProgramTool.js" -import { SolanaOutpostBootstrapper } from "../../orchestration/solana/SolanaOutpostBootstrapper.js" -import { confirmSignature } from "../../clients/solana/utils/signatureUtils.js" -import { ClusterBuildContext } from "../../orchestration/ClusterBuildContext.js" -import { - ClusterBuildStep, - type ClusterBuildStepOptions -} from "../../orchestration/ClusterBuildStep.js" -import type { StepInput } from "../../orchestration/StepRunner.js" -import { solanaKeypair } from "../../utils/keyPairUtils.js" -import { slugNameToLittleEndianBuffer } from "../../utils/slugUtils.js" -import { Report } from "../../report/Report.js" - -/** PDA seeds — kept in sync with `wire-solana/programs/liqsol-core/src/states/opp_states.rs`. */ -const OutpostConfigSeed = Buffer.from("outpost_config") -const OutboundMessageBufferSeed = Buffer.from("outbound_message_buffer") -const OperatorRegistrySeed = Buffer.from("operator_registry") -const VaultSeed = Buffer.from("outpost_vault") -/** - * The seeds this tool derives come from the ONE registry — - * {@link SolanaOutpostBootstrapper.PdaSeed}, whose entries mirror - * `opp_states.rs`. Never re-spell a seed literal here. - * - * `CollateralVault` is the per-`token_code` SPL collateral vault - * (`deposit_non_native.rs`); `CollateralPosition` is the per-`(operator, - * token_code)` bonded balance; `ReserveAggregate` is the seizure-destination - * authority for SPL collateral — `deposit_non_native` declares it alongside its - * ATA (`init_if_needed, payer = depositor`) so the SLASH destination exists from - * the first moment seizable collateral for a `(token_code, mint)` does, because - * the dispatch path has no payer and cannot open it later. - */ -const CollateralVaultSeed = Buffer.from( - SolanaOutpostBootstrapper.PdaSeed.CollateralVault -) -const CollateralPositionSeed = Buffer.from( - SolanaOutpostBootstrapper.PdaSeed.CollateralPosition -) -const ReserveAggregateSeed = Buffer.from( - SolanaOutpostBootstrapper.PdaSeed.ReserveAggregate -) -export namespace SolanaCollateralTool { - // ── Step: native SOL planDeposit (`opp-outpost::deposit`) ──────────────────── - - /** Input for {@link planDeposit} — one native-SOL collateral deposit write. */ - export interface DepositInput extends StepInput { - readonly kind: "SolanaCollateralTool.DepositInput" - /** Operator's durable `label` handle — resolved from `ctx.keyStore` (NOT its on-chain `account`). */ - readonly operatorLabel: string - readonly operatorType: OperatorType - /** 8-byte slug_name (`uint64`) of the deposited token (native `SOL`). */ - readonly tokenCode: bigint - /** Lamports to escrow. */ - readonly amount: bigint - } - - /** A single native-SOL collateral deposit write to `opp-outpost::deposit`. */ - export function planDeposit< - C extends ClusterBuildContext = ClusterBuildContext - >( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - operatorLabel: string, - operatorType: OperatorType, - tokenCode: bigint, - amount: bigint - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { - kind: "SolanaCollateralTool.DepositInput", - operatorLabel, - operatorType, - tokenCode, - amount - }, - runDeposit - ) - } - - /** Named runner — ONE `opp-outpost::deposit` ix, signed by the operator keypair. */ - export async function runDeposit( - ctx: C, - input: DepositInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - Assert.ok( - input.amount > 0n, - "SolanaCollateralTool.planDeposit: amount must be positive" - ) - const operator = ctx.keyStore.assertOperator(input.operatorLabel) - const keypair = solanaKeypair(operator.solana) - const program = loadOppOutpostProgram(ctx, keypair) - const programId = program.programId - const transaction = await program.methods - .deposit( - input.operatorType, - new anchor.BN(input.tokenCode.toString()), - new anchor.BN(input.amount.toString()) - ) - .accounts({ - depositor: keypair.publicKey, - config: SolanaOutpostProgramTool.derivePda( - programId, - OutpostConfigSeed - ), - operatorRegistry: SolanaOutpostProgramTool.derivePda( - programId, - OperatorRegistrySeed - ), - outboundMessageBuffer: SolanaOutpostProgramTool.derivePda( - programId, - OutboundMessageBufferSeed - ), - vault: SolanaOutpostProgramTool.derivePda(programId, VaultSeed), - collateralPosition: collateralPositionPda( - programId, - keypair.publicKey, - input.tokenCode - ), - systemProgram: SystemProgram.programId - }) - .signers([keypair]) - .transaction() - const signature = await ctx.solana.connection.sendTransaction( - transaction, - [keypair], - { skipPreflight: false } - ) - await confirmSignature( - ctx.solana.connection, - signature, - "SolanaCollateralTool.planDeposit" - ) - } - - // ── Step: SPL planDeposit (`opp-outpost::deposit_non_native`) ──────────────── - - /** Input for {@link planNonNativeDeposit} — one SPL collateral deposit write. */ - export interface DepositNonNativeInput extends StepInput { - readonly kind: "SolanaCollateralTool.DepositNonNativeInput" - /** Operator's durable `label` handle — resolved from `ctx.keyStore` (NOT its on-chain `account`). */ - readonly operatorLabel: string - readonly chainCode: bigint - /** - * Token slug code — the config-level identity. The SPL mint ADDRESS is a - * deploy artifact the runner resolves at run time (it does not exist when - * the step is constructed). - */ - readonly tokenCode: bigint - readonly reserveCode: bigint - readonly operatorType: OperatorType - readonly amount: bigint - } - - /** A single `opp-outpost::deposit_non_native` SPL write, signed by the operator keypair. */ - export function planNonNativeDeposit< - C extends ClusterBuildContext = ClusterBuildContext - >( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - operatorLabel: string, - chainCode: bigint, - tokenCode: bigint, - reserveCode: bigint, - operatorType: OperatorType, - amount: bigint - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { - kind: "SolanaCollateralTool.DepositNonNativeInput", - operatorLabel, - chainCode, - tokenCode, - reserveCode, - operatorType, - amount - }, - runNonNativeDeposit - ) - } - - /** Named runner — ONE `opp-outpost::deposit_non_native` SPL write. */ - export async function runNonNativeDeposit( - ctx: C, - input: DepositNonNativeInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - Assert.ok( - input.amount > 0n, - "SolanaCollateralTool.planNonNativeDeposit: amount must be positive" - ) - const operator = ctx.keyStore.assertOperator(input.operatorLabel) - const keypair = solanaKeypair(operator.solana) - const program = loadOppOutpostProgram(ctx, keypair) - const programId = program.programId - const mint = new PublicKey( - SolanaFundingTool.solMintAddress(ctx.config.dataPath, input.tokenCode) - ) - const reserveAggregatePda = SolanaOutpostProgramTool.derivePda( - programId, - ReserveAggregateSeed - ) - const transaction = await program.methods - .depositNonNative( - new anchor.BN(input.chainCode.toString()), - new anchor.BN(input.tokenCode.toString()), - new anchor.BN(input.reserveCode.toString()), - input.operatorType, - new anchor.BN(input.amount.toString()) - ) - .accounts({ - depositor: keypair.publicKey, - config: SolanaOutpostProgramTool.derivePda( - programId, - OutpostConfigSeed - ), - operatorRegistry: SolanaOutpostProgramTool.derivePda( - programId, - OperatorRegistrySeed - ), - outboundMessageBuffer: SolanaOutpostProgramTool.derivePda( - programId, - OutboundMessageBufferSeed - ), - mint, - depositorAta: getAssociatedTokenAddressSync(mint, keypair.publicKey), - collateralVault: SolanaOutpostProgramTool.derivePda( - programId, - CollateralVaultSeed, - slugNameToLittleEndianBuffer(input.tokenCode) - ), - collateralPosition: collateralPositionPda( - programId, - keypair.publicKey, - input.tokenCode - ), - reserveAggregate: reserveAggregatePda, - // The aggregate is a PDA, so its ATA is derived off-curve. - reserveAggregateAta: getAssociatedTokenAddressSync( - mint, - reserveAggregatePda, - true - ), - tokenProgram: TOKEN_PROGRAM_ID, - associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, - systemProgram: SystemProgram.programId, - rent: SYSVAR_RENT_PUBKEY - }) - .signers([keypair]) - .transaction() - const signature = await ctx.solana.connection.sendTransaction( - transaction, - [keypair], - { skipPreflight: false } - ) - await confirmSignature( - ctx.solana.connection, - signature, - "SolanaCollateralTool.planNonNativeDeposit" - ) - } - - // ── value helpers (PDA / IDL loads — executed INSIDE runners) ──────────── - - /** - * Derive the per-`(operator, tokenCode)` `CollateralPosition` PDA that both - * `deposit` and `depositNonNative` require, and that the terminal - * `WITHDRAW_REMIT` / `SLASH` / `DEPOSIT_REVERT` handlers resolve out of - * `remaining_accounts`. - * - * @param programId - The deployed `liqsol_core` program id. - * @param operator - The depositing operator's Solana pubkey. - * @param tokenCode - The 8-byte slug_name of the collateral token. - * @returns The `CollateralPosition` PDA address. - */ - export function collateralPositionPda( - programId: PublicKey, - operator: PublicKey, - tokenCode: bigint - ): PublicKey { - return SolanaOutpostProgramTool.derivePda( - programId, - CollateralPositionSeed, - operator.toBuffer(), - slugNameToLittleEndianBuffer(tokenCode) - ) - } - - /** - * Build the OPP outpost Anchor program (hosted in `liqsol_core` since the - * clean-room rewrite) bound to `keypair` (its own signer for the deposit - * ix). IDL from `/target/idl/liqsol_core.json`; connection from - * `ctx.solana`. - */ - export function loadOppOutpostProgram( - ctx: C, - keypair: Keypair - ): anchor.Program { - return SolanaOutpostProgramTool.loadProgram( - ctx.solana.connection, - keypair, - ctx.config.solanaPath - ) - } -} diff --git a/packages/cluster-tool/src/tools/solana/SolanaSwapTool.ts b/packages/cluster-tool/src/tools/solana/SolanaSwapTool.ts deleted file mode 100644 index ab65db8bd..000000000 --- a/packages/cluster-tool/src/tools/solana/SolanaSwapTool.ts +++ /dev/null @@ -1,270 +0,0 @@ -/** - * SolanaSwapTool — user-side helper for triggering Solana → - * other-chain swaps via the Solana outpost's `opp-outpost::request_swap` - * instruction. - * - * Mirrors the {@link depositSOLCollateral} pattern: resolves the program's - * expected PDAs (`config`, `outbound_message_buffer`, `reserve`), submits - * the `request_swap` ix signed by the user, and confirms the signature - * via the same `getSignatureStatus` polling loop the rest of the harness - * uses. - * - * The instruction transfers `sourceAmount` lamports from the user into - * the per-`(sourceTokenCode, sourceReserveCode)` Reserve PDA (native - * escrow), credits the on-chain `Reserve.external_token_amount`, and - * queues a `SWAP_REQUEST` attestation onto the outbound buffer. The - * matching SwapRemit returns inbound on the destination outpost and - * pays the recipient there. - * - * @see request_swap.rs - */ - -import Assert from "node:assert" -import * as anchor from "@coral-xyz/anchor" -import { Connection, Keypair, PublicKey, SystemProgram } from "@solana/web3.js" -import { - TOKEN_PROGRAM_ID, - getAssociatedTokenAddressSync -} from "@solana/spl-token" -import { confirmSignature } from "../../clients/solana/utils/signatureUtils.js" -import { slugNameToLittleEndianBuffer } from "../../utils/slugUtils.js" - -/** PDA seeds — kept in sync with `wire-solana/programs/liqsol-core/src/states/opp_states.rs`. */ -const OUTPOST_CONFIG_SEED = Buffer.from("outpost_config") -const OUTBOUND_MESSAGE_BUFFER_SEED = Buffer.from("outbound_message_buffer") -const RESERVE_SEED = Buffer.from("reserve") -const RESERVE_VAULT_SEED = Buffer.from("reserve_vault") - -/** - * Structured arguments for a SOL-source SWAP_REQUEST emission. All - * slug_name codes are passed as `bigint`; Anchor's IDL handler wraps - * them in `anchor.BN` for Borsh encoding. - */ -export interface SolanaSwapRequest { - sourceTokenCode: bigint - sourceReserveCode: bigint - /** Lamports to escrow into the source reserve. */ - sourceAmount: bigint - targetChainCode: bigint - targetTokenCode: bigint - targetReserveCode: bigint - /** - * Raw recipient address on the target chain. 20 bytes for EVM, - * 32 bytes for SVM destinations. - */ - targetRecipient: Uint8Array - targetAmount: bigint - targetToleranceBps: number -} - -/** - * Submit a SOL → other-chain SWAP_REQUEST via the Solana outpost's - * `request_swap` ix. - * - * Native SOL only this pass — non-native source tokens revert with - * `SwapSourceNotNative`. SPL source-side custody lands with the - * `flow-swap-non-native-tokens` follow-on plan. - * - * @param connection RPC connection (typically `solClient.connection`). - * @param program Anchor program bound to the deployed `opp_outpost` IDL. - * @param user Keypair signing the swap. Must hold at least - * `sourceAmount` lamports plus tx fees. - * @param request Structured swap parameters. - * @return The transaction signature on confirm. - */ -export async function requestSolanaSwap( - connection: Connection, - program: anchor.Program, - user: Keypair, - request: SolanaSwapRequest -): Promise { - Assert.ok( - request.sourceAmount > 0n, - "SolanaSwapTool: sourceAmount must be > 0" - ) - Assert.ok( - request.targetRecipient.byteLength > 0, - "SolanaSwapTool: targetRecipient must be non-empty" - ) - Assert.ok( - request.targetAmount > 0n, - "SolanaSwapTool: targetAmount must be > 0" - ) - Assert.ok( - request.targetToleranceBps >= 0 && request.targetToleranceBps <= 10_000, - `SolanaSwapTool: targetToleranceBps must be in [0, 10000], got ${request.targetToleranceBps}` - ) - - const programId = program.programId - const [configPda] = PublicKey.findProgramAddressSync( - [OUTPOST_CONFIG_SEED], - programId - ) - const [outboundMessageBufferPda] = PublicKey.findProgramAddressSync( - [OUTBOUND_MESSAGE_BUFFER_SEED], - programId - ) - - // Reserve PDA — derived from `RESERVE_SEED` + LE-encoded source codes. - const sourceTokenCodeLE = slugNameToLittleEndianBuffer( - request.sourceTokenCode - ) - const sourceReserveCodeLE = slugNameToLittleEndianBuffer( - request.sourceReserveCode - ) - const [reservePda] = PublicKey.findProgramAddressSync( - [RESERVE_SEED, sourceTokenCodeLE, sourceReserveCodeLE], - programId - ) - - const tx = await program.methods - .requestSwap( - new anchor.BN(request.sourceTokenCode.toString()), - new anchor.BN(request.sourceReserveCode.toString()), - new anchor.BN(request.sourceAmount.toString()), - new anchor.BN(request.targetChainCode.toString()), - new anchor.BN(request.targetTokenCode.toString()), - new anchor.BN(request.targetReserveCode.toString()), - Buffer.from(request.targetRecipient), - new anchor.BN(request.targetAmount.toString()), - request.targetToleranceBps - ) - .accounts({ - user: user.publicKey, - config: configPda, - reserve: reservePda, - outboundMessageBuffer: outboundMessageBufferPda, - systemProgram: SystemProgram.programId - }) - .signers([user]) - .transaction() - - const sig = await connection.sendTransaction(tx, [user], { - skipPreflight: false - }) - await confirmSignature(connection, sig, "SolanaSwapTool request_swap") - return sig -} - -/** - * Structured arguments for a SPL-source SWAP_REQUEST emission. The - * source `mint` is looked up against `OutpostConfig.token_addresses_by_code` - * inside the program; the harness still needs to pass it as an account - * (Anchor IDL requires the mint Pubkey at the account-validation layer). - */ -export interface SolanaSwapSplRequest { - sourceTokenCode: bigint - sourceReserveCode: bigint - /** Source amount in chain-native units (6-dec for USDC/USDT, etc.). */ - sourceAmount: bigint - /** SPL mint Pubkey for `sourceTokenCode`. */ - sourceMint: PublicKey - targetChainCode: bigint - targetTokenCode: bigint - targetReserveCode: bigint - targetRecipient: Uint8Array - targetAmount: bigint - targetToleranceBps: number -} - -/** - * Submit a SOL → other-chain SWAP_REQUEST via the Solana outpost's - * `request_swap_spl` ix. Source-side custody comes from the user's - * Associated Token Account (ATA) for `sourceMint`; the on-chain ix - * transfers `sourceAmount` token units into the per-reserve - * `reserve_vault` PDA. - * - * @param connection RPC connection. - * @param program Anchor program bound to the deployed `opp_outpost` IDL. - * @param user Keypair signing the swap. Must hold an ATA for - * `sourceMint` with at least `sourceAmount` units + - * enough SOL for fees. - * @param request Structured SPL swap parameters. - * @return The transaction signature on confirm. - */ -export async function requestSolanaSwapSpl( - connection: Connection, - program: anchor.Program, - user: Keypair, - request: SolanaSwapSplRequest -): Promise { - Assert.ok( - request.sourceAmount > 0n, - "SolanaSwapTool: sourceAmount must be > 0" - ) - Assert.ok( - request.targetRecipient.byteLength > 0, - "SolanaSwapTool: targetRecipient must be non-empty" - ) - Assert.ok( - request.targetAmount > 0n, - "SolanaSwapTool: targetAmount must be > 0" - ) - Assert.ok( - request.targetToleranceBps >= 0 && request.targetToleranceBps <= 10_000, - `SolanaSwapTool: targetToleranceBps must be in [0, 10000], got ${request.targetToleranceBps}` - ) - - const programId = program.programId - const [configPda] = PublicKey.findProgramAddressSync( - [OUTPOST_CONFIG_SEED], - programId - ) - const [outboundMessageBufferPda] = PublicKey.findProgramAddressSync( - [OUTBOUND_MESSAGE_BUFFER_SEED], - programId - ) - - const sourceTokenCodeLE = slugNameToLittleEndianBuffer( - request.sourceTokenCode - ) - const sourceReserveCodeLE = slugNameToLittleEndianBuffer( - request.sourceReserveCode - ) - const [reservePda] = PublicKey.findProgramAddressSync( - [RESERVE_SEED, sourceTokenCodeLE, sourceReserveCodeLE], - programId - ) - const [reserveVaultPda] = PublicKey.findProgramAddressSync( - [RESERVE_VAULT_SEED, sourceTokenCodeLE, sourceReserveCodeLE], - programId - ) - - // The user's ATA for `sourceMint` MUST exist before this call — the - // SwapDeposit funding flow is responsible (e.g. `SplFundingTool`). - const userAta = getAssociatedTokenAddressSync( - request.sourceMint, - user.publicKey - ) - - const tx = await program.methods - .requestSwapSpl( - new anchor.BN(request.sourceTokenCode.toString()), - new anchor.BN(request.sourceReserveCode.toString()), - new anchor.BN(request.sourceAmount.toString()), - new anchor.BN(request.targetChainCode.toString()), - new anchor.BN(request.targetTokenCode.toString()), - new anchor.BN(request.targetReserveCode.toString()), - Buffer.from(request.targetRecipient), - new anchor.BN(request.targetAmount.toString()), - request.targetToleranceBps - ) - .accounts({ - user: user.publicKey, - config: configPda, - reserve: reservePda, - reserveVault: reserveVaultPda, - mint: request.sourceMint, - userAta, - outboundMessageBuffer: outboundMessageBufferPda, - tokenProgram: TOKEN_PROGRAM_ID - }) - .signers([user]) - .transaction() - - const sig = await connection.sendTransaction(tx, [user], { - skipPreflight: false - }) - await confirmSignature(connection, sig, "SolanaSwapTool request_swap_spl") - return sig -} diff --git a/packages/cluster-tool/src/tools/solana/index.ts b/packages/cluster-tool/src/tools/solana/index.ts index 169b1966b..6dc66941d 100644 --- a/packages/cluster-tool/src/tools/solana/index.ts +++ b/packages/cluster-tool/src/tools/solana/index.ts @@ -1,8 +1,6 @@ export * from "./LiqsolPdaSeed.js" export * from "./SolanaAnchorEnumTool.js" export * from "./SolanaAnchorScriptTool.js" -export * from "./SolanaCollateralTool.js" export * from "./SolanaFundingTool.js" export * from "./SolanaLiqSyndicationTool.js" export * from "./SolanaOutpostProgramTool.js" -export * from "./SolanaSwapTool.js" diff --git a/packages/cluster-tool/src/tools/wire/OperatorDaemonTool.ts b/packages/cluster-tool/src/tools/wire/OperatorDaemonTool.ts index 24953c979..22ec707d7 100644 --- a/packages/cluster-tool/src/tools/wire/OperatorDaemonTool.ts +++ b/packages/cluster-tool/src/tools/wire/OperatorDaemonTool.ts @@ -61,15 +61,6 @@ export namespace OperatorDaemonTool { "sysio::cron_plugin" ] as const - /** Plugins an underwriter daemon loads. */ - export const UnderwriterPlugins = [ - "sysio::underwriter_plugin", - "sysio::outpost_ethereum_client_plugin", - "sysio::outpost_solana_client_plugin", - ExternalDebuggingPlugin, - "sysio::cron_plugin" - ] as const - /** `plugins` with the external-debugging sink removed when the server is off. */ const debuggingGatedPlugins = ( plugins: readonly string[], @@ -85,15 +76,6 @@ export namespace OperatorDaemonTool { export const BatchEpochPollMs = 15_000 /** batch_operator_plugin delivery timeout (ms; nominal — scaled at arg build). */ export const BatchDeliveryTimeoutMs = 30_000 - /** - * underwriter_plugin outpost action timeout (ms; nominal — scaled at arg - * build). Mirrors the plugin default; passed EXPLICITLY so the flow timing - * scale reaches it: on a starved shared-host validator a commit tx can - * take >15s to confirm, and the plugin then re-submits every scan cycle - * forever (run 28700849707: uwreq 35's SOL leg timed out at 15s per - * attempt for 12 minutes while the ETH leg sat confirmed). - */ - export const UnderwriterActionTimeoutMs = 30_000 /** The `sysio.chains` codename identifying the ETH outpost. */ export const EthereumChainCodename = "ETHEREUM" /** The `sysio.chains` codename identifying the SOL outpost. */ @@ -110,32 +92,18 @@ export namespace OperatorDaemonTool { export const EthereumClientId = EthereumChainCodename /** See {@link EthereumClientId} — the SOL client id is likewise the chain code. */ export const SolanaClientId = SolanaChainCodename - /** ETH source-deposit function the underwriter verifies before committing. */ - export const EthereumSourceDepositFunction = "requestSwap" - /** SOL source-deposit instruction the underwriter verifies before committing. */ - export const SolanaSourceDepositInstruction = "request_swap" /** SOL inbound-delivery instruction the batch operator invokes. */ export const SolanaEpochInInstruction = "epoch_in" - /** SOL underwriter-commit instruction. */ - export const SolanaCommitUnderwriteInstruction = "commit_underwrite" /** * OPP outpost instructions the daemons invoke — asserted present in the * copied IDL so a wrong or stale IDL fails at artifact preparation, not at * the first delivery. */ export const RequiredSolanaIdlInstructions = [ - SolanaEpochInInstruction, - SolanaCommitUnderwriteInstruction, - SolanaSourceDepositInstruction + SolanaEpochInInstruction ] as const /** OPP outpost contracts whose ABIs (with embedded addresses) the plugins load. */ - export const EthereumAbiContractNames = [ - "OPP", - "OPPInbound", - "BAR", - "ReserveManager", - "OperatorRegistry" - ] as const + export const EthereumAbiContractNames = ["OPP", "OPPInbound", "BAR"] as const /** Cluster-data subpath holding the generated `{contractName, address, abi}` files. */ export const EthereumAbiSubpath = "eth-abis" /** Cluster-data subpath holding the copied OPP outpost IDL. */ @@ -173,10 +141,7 @@ export namespace OperatorDaemonTool { return { ethereumRpcUrl: config.externalOutposts?.ethereum.rpcUrl ?? - toURL( - config.bind.anvil.port, - toDialAddress(config.bind.anvil.address) - ), + toURL(config.bind.anvil.port, toDialAddress(config.bind.anvil.address)), // External-outpost mode carries the REAL chain id; else the anvil default. ethereumChainId: config.externalOutposts?.ethereum.chainId ?? @@ -251,7 +216,8 @@ export namespace OperatorDaemonTool { const abiDir = mkdirs(Path.join(dataPath, EthereumAbiSubpath)) const ethereumAbiFiles = EthereumAbiContractNames.map(contractName => { // Deployment aliases retain the address/plugin name after Solidity renames. - const artifactContractName = contractName === "BAR" ? "BARV2" : contractName + const artifactContractName = + contractName === "BAR" ? "BARV2" : contractName const artifactFile = Path.join( ethereumPath, "artifacts", @@ -408,7 +374,12 @@ export namespace OperatorDaemonTool { assertOutpostKeys(operator) return [ ...pair(`--${Constants.READ_MODE_OPTION}`, NodeopReadMode.irreversible), - ...pluginArgs(debuggingGatedPlugins(BatchOperatorPlugins, network.debuggingServerEnabled)), + ...pluginArgs( + debuggingGatedPlugins( + BatchOperatorPlugins, + network.debuggingServerEnabled + ) + ), ...pair( "--signature-provider", KeyGenerator.toSignatureProvider( @@ -440,64 +411,6 @@ export namespace OperatorDaemonTool { ] } - /** - * The full extra-arg block for an UNDERWRITER daemon: read-mode + plugins + the - * operator's WIRE signature provider + underwriter plugin config + both outpost - * client specs + the source-deposit verification targets. - */ - export function underwriterArgs( - operator: OperatorAccount, - artifacts: OperatorDaemonArtifacts, - network: OperatorDaemonNetwork, - keySourceFor: ClusterConfigProvider.SignatureProviderSourceFor - ): string[] { - Assert.ok( - operator.type === OperatorType.UNDERWRITER, - `underwriterArgs: ${operator.label} is a ${OperatorType[operator.type]}, not an underwriter` - ) - assertOutpostKeys(operator) - return [ - ...pair(`--${Constants.READ_MODE_OPTION}`, NodeopReadMode.irreversible), - ...pluginArgs(debuggingGatedPlugins(UnderwriterPlugins, network.debuggingServerEnabled)), - ...pair( - "--signature-provider", - KeyGenerator.toSignatureProvider( - operator.wire, - undefined, - keySourceFor(operator.label, KeyType.K1) - ) - ), - ...pair("--underwriter-account", operator.account), - ...pair( - "--underwriter-action-timeout-ms", - String(scaleTimeoutMs(UnderwriterActionTimeoutMs)) - ), - ...(network.debuggingServerEnabled - ? pair("--ext-debugging-server", network.debuggingServerUrl) - : []), - ...outpostClientArgs(operator, artifacts, network, keySourceFor), - // No per-chain outpost flags: the underwriter serves every ACTIVE - // `sysio.chains` row, reads each one's OperatorRegistry / source-deposit - // address off that row, and reaches it through the RPC client registered - // under the chain's code. - ...pair( - "--underwriter-eth-source-deposit-function", - EthereumSourceDepositFunction - ), - ...pair( - "--underwriter-sol-source-deposit-instruction", - SolanaSourceDepositInstruction - ), - ...pair("--solana-idl-file", artifacts.solanaIdlFile), - // The outpost interface is hosted in liqsol_core since the clean-room - // rewrite; nodeop's compiled-in default IDL name is opp_outpost. - ...pair( - "--solana-outpost-program-name", - SolanaOutpostProgramTool.ProgramName - ) - ] - } - // ── Step: start an operator's daemon (process spawn — its own Step) ─────── /** Input for {@link planDaemonStart}. */ @@ -554,16 +467,16 @@ export namespace OperatorDaemonTool { .with(OperatorType.BATCH, () => batchOperatorArgs(operator, artifacts, network, keySourceFor) ) - .with(OperatorType.UNDERWRITER, () => - underwriterArgs(operator, artifacts, network, keySourceFor) - ) .otherwise(() => { throw new Error( `startDaemon: ${input.label} is a ${OperatorType[operator.type]}, not an OPP operator` ) }) - const ports = BindConfigProvider.claimAdHocPorts(ctx.config.bind, input.label) + const ports = BindConfigProvider.claimAdHocPorts( + ctx.config.bind, + input.label + ) // startWithRecovery (not bare create+start): a flow rerun reuses the // daemon's data dir, so an unclean prior stop leaves a dirty chainbase // this launch must recover from, same as the planned-node paths. diff --git a/packages/cluster-tool/src/tools/wire/WireCollateralTool.ts b/packages/cluster-tool/src/tools/wire/WireCollateralTool.ts index 91e909555..edc053c13 100644 --- a/packages/cluster-tool/src/tools/wire/WireCollateralTool.ts +++ b/packages/cluster-tool/src/tools/wire/WireCollateralTool.ts @@ -1,3 +1,4 @@ +import { Constants } from "../../Constants.js" /** * WireCollateralTool — Step factories for depot-native operator collateral on * `sysio.opreg`. Every on-chain WRITE is its OWN {@link ClusterBuildStep} so the @@ -20,7 +21,10 @@ import Assert from "node:assert" -import type { ChainTokenAmount, CollateralRequirement } from "@wireio/cluster-tool-shared" +import type { + ChainTokenAmount, + CollateralRequirement +} from "@wireio/cluster-tool-shared" import { TokenAmount } from "@wireio/opp-typescript-models" import { SlugName, SysioContracts } from "@wireio/sdk-core" import { getLogger } from "@wireio/shared" @@ -38,7 +42,6 @@ import { OpregContractSteps } from "../../orchestration/steps/contracts/sysio/Op import { TokenContractSteps } from "../../orchestration/steps/contracts/sysio/TokenContractSteps.js" import { Report } from "../../report/Report.js" import { slugValue } from "../../utils/slugUtils.js" -import { WireReserveTool } from "./WireReserveTool.js" import { formatWireAsset } from "./WireUserTool.js" const log = getLogger(__filename) @@ -85,7 +88,7 @@ export namespace WireCollateralTool { * The WIRE token's packed slug code as the `bigint` a `TokenAmount` carries — the token * every WIRE bond, claim and payout here is keyed on. */ - export const WireTokenCode = BigInt(WireReserveTool.WireTokenCode) + export const WireTokenCode = BigInt(Constants.WireTokenCode) // ── Step: WIRE funding (`sysio.token::transfer`) ───────────────────────── @@ -104,7 +107,9 @@ export namespace WireCollateralTool { * * @throws If `collateral` is not a WIRE amount on the WIRE chain. */ - export function planFunding( + export function planFunding< + C extends ClusterBuildContext = ClusterBuildContext + >( actor: Report.Actor, name: string, description: string, @@ -173,7 +178,9 @@ export namespace WireCollateralTool { * @throws If `collateral` is not a WIRE amount on the WIRE chain — only WIRE has a * harness funding path. */ - export function planDeposit( + export function planDeposit< + C extends ClusterBuildContext = ClusterBuildContext + >( actor: Report.Actor, name: string, description: string, @@ -248,7 +255,9 @@ export namespace WireCollateralTool { * * @throws If `collateral` is not on the WIRE chain. */ - export function planWithdrawal( + export function planWithdrawal< + C extends ClusterBuildContext = ClusterBuildContext + >( actor: Report.Actor, name: string, description: string, @@ -278,7 +287,9 @@ export namespace WireCollateralTool { const operator = ctx.keyStore.assertOperator(input.operatorLabel), { tokenCode, amount } = input.collateral.amount, priorRequestIds = new Set( - (await readWithdrawRequests(ctx, operator.account)).map(row => BigInt(row.request_id)) + (await readWithdrawRequests(ctx, operator.account)).map(row => + BigInt(row.request_id) + ) ) await OpregContractSteps.runWithdraw( ctx, @@ -297,7 +308,7 @@ export namespace WireCollateralTool { const queued = (await readWithdrawRequests(ctx, operator.account)).some( row => !priorRequestIds.has(BigInt(row.request_id)) && - slugValue(row.chain_code) === WireReserveTool.WireChainCode && + slugValue(row.chain_code) === Constants.WireChainCode && BigInt(slugValue(row.token_code)) === tokenCode && BigInt(row.amount) === amount ) @@ -336,7 +347,9 @@ export namespace WireCollateralTool { account: string ): Promise { try { - const latest = (await findOperatorRow(ctx, account))?.recent_actions.at(-1) + const latest = (await findOperatorRow(ctx, account))?.recent_actions.at( + -1 + ) return latest != null && !latest.success ? `contract reason: ${latest.error_message}` : UnreadableRefusalReason @@ -365,7 +378,9 @@ export namespace WireCollateralTool { } /** A single `opreg::claimremit` of the operator's `remitclaims{account, token}` row. */ - export function planClaimremit( + export function planClaimremit< + C extends ClusterBuildContext = ClusterBuildContext + >( actor: Report.Actor, name: string, description: string, @@ -435,7 +450,9 @@ export namespace WireCollateralTool { * Place it immediately before the write it measures, with no other transfer to the * operator in between. */ - export function planRecordWireBalance( + export function planRecordWireBalance< + C extends ClusterBuildContext = ClusterBuildContext + >( actor: Report.Actor, name: string, description: string, @@ -460,7 +477,10 @@ export namespace WireCollateralTool { ): Promise { signal.throwIfAborted() const account = ctx.keyStore.assertOperator(input.operatorLabel).account - ctx.outputs.set(wireBalanceKey(input.operatorLabel), await ctx.wire.getWireBalance(account)) + ctx.outputs.set( + wireBalanceKey(input.operatorLabel), + await ctx.wire.getWireBalance(account) + ) } /** Input for {@link planVerifyWireBalanceIncrease}. */ @@ -477,7 +497,9 @@ export namespace WireCollateralTool { * {@link planRecordWireBalance} recorded it — the `claimremit` transfer arrived, in full * and no more. One read: the payout is confirmed irreversible before this Step runs. */ - export function planVerifyWireBalanceIncrease( + export function planVerifyWireBalanceIncrease< + C extends ClusterBuildContext = ClusterBuildContext + >( actor: Report.Actor, name: string, description: string, @@ -490,13 +512,19 @@ export namespace WireCollateralTool { name, description, options, - { kind: "WireCollateralTool.VerifyWireBalanceIncreaseInput", operatorLabel, increase }, + { + kind: "WireCollateralTool.VerifyWireBalanceIncreaseInput", + operatorLabel, + increase + }, runVerifyWireBalanceIncrease ) } /** Named runner — compare the operator's liquid WIRE with the recorded balance. */ - export async function runVerifyWireBalanceIncrease( + export async function runVerifyWireBalanceIncrease< + C extends ClusterBuildContext + >( ctx: C, input: VerifyWireBalanceIncreaseInput, signal: AbortSignal @@ -532,7 +560,9 @@ export namespace WireCollateralTool { * * @throws If `collateral` is not on the WIRE chain. */ - export function planVerifyBalanceRow( + export function planVerifyBalanceRow< + C extends ClusterBuildContext = ClusterBuildContext + >( actor: Report.Actor, name: string, description: string, @@ -546,7 +576,11 @@ export namespace WireCollateralTool { name, description, options, - { kind: "WireCollateralTool.VerifyBalanceRowInput", operatorLabel, collateral }, + { + kind: "WireCollateralTool.VerifyBalanceRowInput", + operatorLabel, + collateral + }, runVerifyBalanceRow ) } @@ -578,7 +612,7 @@ export namespace WireCollateralTool { ): Promise { const entry = (await findOperatorRow(ctx, account))?.balances.find( balance => - slugValue(balance.chain_code) === WireReserveTool.WireChainCode && + slugValue(balance.chain_code) === Constants.WireChainCode && BigInt(slugValue(balance.token_code)) === tokenCode ) return entry == null ? 0n : BigInt(entry.balance) @@ -595,7 +629,10 @@ export namespace WireCollateralTool { ctx: C, operatorLabel: string ): Promise { - return findOperatorRow(ctx, ctx.keyStore.assertOperator(operatorLabel).account) + return findOperatorRow( + ctx, + ctx.keyStore.assertOperator(operatorLabel).account + ) } /** The `operators` row for an on-chain `account`, from one complete read; absent when there is none. */ @@ -631,7 +668,9 @@ export namespace WireCollateralTool { * runner polls for up to {@link RemitClaimEpochCount} effective epochs. A missing row * reads as zero, so a zero expectation verifies a claim that `claimremit` has paid. */ - export function planVerifyRemitClaim( + export function planVerifyRemitClaim< + C extends ClusterBuildContext = ClusterBuildContext + >( actor: Report.Actor, name: string, description: string, @@ -644,7 +683,11 @@ export namespace WireCollateralTool { name, description, options, - { kind: "WireCollateralTool.VerifyRemitClaimInput", operatorLabel, claim }, + { + kind: "WireCollateralTool.VerifyRemitClaimInput", + operatorLabel, + claim + }, runVerifyRemitClaim ) } @@ -679,10 +722,10 @@ export namespace WireCollateralTool { .tables.remitclaims.query({ limit: TableRowLimit }) assertCompleteRead(result.more, RemitClaimsTable) const claim = result.rows.find( - row => - row.account === account && - BigInt(slugValue(row.token_code)) === tokenCode - ) + row => + row.account === account && + BigInt(slugValue(row.token_code)) === tokenCode + ) return claim == null ? 0n : BigInt(claim.balance) } @@ -706,7 +749,10 @@ export namespace WireCollateralTool { * runners later read as `ctx.config.epochDurationSec`. */ export function remitClaimStepTimeoutMs(epochDurationSec: number): number { - return remitClaimDeadlineMs(epochDurationSec) + ProtocolTiming.PollDeadlineBufferMs + return ( + remitClaimDeadlineMs(epochDurationSec) + + ProtocolTiming.PollDeadlineBufferMs + ) } // ── value helpers ──────────────────────────────────────────────────────── @@ -714,7 +760,7 @@ export namespace WireCollateralTool { /** A WIRE amount on the WIRE chain — the collateral every depot-native bond here moves. */ export function createWireCollateral(amount: bigint): ChainTokenAmount { return { - chain_code: WireReserveTool.WireChainCode, + chain_code: Constants.WireChainCode, amount: createWireClaim(amount) } } @@ -735,14 +781,16 @@ export namespace WireCollateralTool { * @throws If `minimumBond` is not positive, or exceeds what the persisted number field holds * exactly. */ - export function createWireRequirement(minimumBond: bigint): CollateralRequirement { + export function createWireRequirement( + minimumBond: bigint + ): CollateralRequirement { Assert.ok( minimumBond > 0n && minimumBond <= BigInt(Number.MAX_SAFE_INTEGER), `WireCollateralTool: a minimum bond must be positive and at most ${Number.MAX_SAFE_INTEGER}, got ${minimumBond}` ) return { - chainCode: WireReserveTool.WireChainCode, - tokenCode: WireReserveTool.WireTokenCode, + chainCode: Constants.WireChainCode, + tokenCode: Constants.WireTokenCode, minimumBond: Number(minimumBond) } } @@ -755,7 +803,7 @@ export namespace WireCollateralTool { /** Assert a collateral amount sits on the WIRE chain, where every depot-native row lives. */ export function assertDepotCollateral(collateral: ChainTokenAmount): void { Assert.ok( - collateral.chain_code === WireReserveTool.WireChainCode, + collateral.chain_code === Constants.WireChainCode, `WireCollateralTool: depot-native collateral is keyed on the WIRE chain, got ${SlugName.toString(collateral.chain_code)}` ) } diff --git a/packages/cluster-tool/src/tools/wire/WireReserveTool.ts b/packages/cluster-tool/src/tools/wire/WireReserveTool.ts deleted file mode 100644 index bdebd1165..000000000 --- a/packages/cluster-tool/src/tools/wire/WireReserveTool.ts +++ /dev/null @@ -1,675 +0,0 @@ -import Assert from "node:assert" -import { SlugName, SysioContracts } from "@wireio/sdk-core" -import type { WireClient } from "../../clients/wire/WireClient.js" -import { slugValue } from "../../utils/slugUtils.js" - -const { SysioContractName } = SysioContracts - -/** - * AMM / reserve math + reads for the depot's swap surface — the TypeScript - * mirror of `sysio::opp::amm` (constant-product output, WIRE-leg fee split) - * plus the client-side `swapquote` evaluation over the live - * `sysio.reserv::reserves` books. Pure value helpers (called inside step - * runners) and reads only — every swap WRITE is a Step owned by the swap - * tools. - */ -export namespace WireReserveTool { - /** Basis-point denominator (10000 = 100%). */ - export const BpsTotal = 10_000 - - /** - * The winning underwriter's share of the WIRE-leg fee, in basis points (the - * remainder is the batch-operator rewards share). Mirrors - * `sysio.reserv::FEE_UNDERWRITER_SHARE_BPS` (5000 = a 50/50 underwriter / - * batch-operator split). - * - * The underwriter half stays in `sysio.reserv` custody until that account - * calls `claimuwfee`. What happens to the rewards pool is stage 2 — see - * {@link FeeEmissionsShareBps}. - */ - export const FeeUnderwriterShareBps = 5_000 - - /** - * Stage 2 of the split: the share of each fee's REWARDS POOL (what stage 1 - * leaves after the underwriter's cut) transferred to the `sysio` emissions - * treasury, in basis points. Mirrors - * `sysio.reserv::DEFAULT_FEE_EMISSIONS_SHARE_BPS` and the live - * `reserve_config.fee_emissions_share_bps` the bootstrap seeds. - * - * The default is ZERO — the whole pool accrues to the batch-operator rewards - * bucket and NO part of a swap fee leaves custody at settlement, so a flow's - * custody assertion sees only the swap's own transfers. A cluster configured - * with a non-zero share moves exactly that share out at settlement time. - */ - export const FeeEmissionsShareBps = 0 - - /** The WIRE chain's slug value (the depot leg of every quote). */ - export const WireChainCode = Number(SlugName.from("WIRE")) - /** The WIRE token's slug value. */ - export const WireTokenCode = Number(SlugName.from("WIRE")) - - /** - * Upper bound for a single-page scan of `sysio.reserv::reserves`. The table - * grows linearly in configured pairs — a couple hundred is enough headroom - * for any cluster `swapquote` would target. - */ - export const MaxReservesScan = 256 - - /** - * Decomposition of a WIRE-leg swap fee — the TypeScript mirror of - * `sysio::opp::amm::wire_fee`. Every field is an exact integer quantity: - * all five shares sum to `fee` and `net + fee === wireAmount`, with no - * rounding leak. - */ - export interface WireFee { - /** Total fee charged on the WIRE leg. */ - fee: bigint - /** - * Portion accrued to the swap's winning underwriter — held in - * `sysio.reserv` custody until that account calls `claimuwfee`. - */ - underwriterShare: bigint - /** - * Portion accrued to the batch-operator rewards bucket — held in - * `sysio.reserv` custody until `payepoch` drains it. - */ - rewardShare: bigint - /** - * Portion transferred to the `sysio` emissions treasury — the ONLY part of - * a fee that leaves `sysio.reserv` custody at settlement, and zero unless - * the cluster configures a non-zero share. - */ - emissionsShare: bigint - /** - * The SOURCE reserve owner's fee — an independent, per-reserve fee on the - * same WIRE leg, accrued to that reserve's `owner_fee_accrued` and claimed - * via `claimrsvfee`. Zero for a WIRE-source swap (no source reserve). - */ - srcReserveShare: bigint - /** The DESTINATION reserve owner's fee. Zero for a swap paying out WIRE. */ - dstReserveShare: bigint - /** `wireAmount - fee`: the net WIRE that continues through the swap. */ - net: bigint - } - - /** - * Constant-product output — matches `sysio.reserv::cp_output` bit-for-bit - * (floored integer division, uint128-safe via bigint). Returns `0n` when any - * side is zero or negative. - * - * @param reserveSource - The source-side reserve depth. - * @param reserveDestination - The destination-side reserve depth. - * @param sourceAmount - The amount entering the source side. - * @returns The destination amount the curve yields. - */ - export function cpOutput( - reserveSource: bigint, - reserveDestination: bigint, - sourceAmount: bigint - ): bigint { - if (reserveSource <= 0n || reserveDestination <= 0n || sourceAmount <= 0n) - return 0n - return (reserveDestination * sourceAmount) / (reserveSource + sourceAmount) - } - - /** - * Sum of the two pool-side weights, in basis points — the TypeScript mirror - * of `sysio::opp::amm::WEIGHT_TOTAL_BPS`. A reserve's `connector_weight_bps` - * is the WIRE-side weight; the token side gets the remainder. - */ - export const WeightTotalBps = 10_000 - - /** - * The `connector_weight_bps` at which a reserve is symmetric (50/50). At this - * weight {@link outGivenIn} reduces to pure constant product and takes the - * EXACT integer path — no fixed-point error. - */ - export const SymmetricConnectorWeightBps = 5_000 - - /** Fractional bits for the log2/exp2 fixed-point internals (Q60). */ - const FpBits = 60n - /** `1.0` in Q60. */ - const FpOne = 1n << FpBits - - /** - * `2^(2^-i)` in Q60 for i = 1..60 — the table {@link exp2Frac} multiplies - * through to build `2^f` from the binary fraction of `f`. Copied verbatim - * from `sysio::opp::amm::EXP2_FRAC_TBL`; the two must stay bit-identical or - * quotes and settlement diverge. - */ - const Exp2FracTable: readonly bigint[] = [ - 0x16a09e667f3bcc91n, 0x1306fe0a31b7152en, - 0x1172b83c7d517addn, 0x10b5586cf9890f63n, - 0x1059b0d31585743bn, 0x102c9a3e778060een, - 0x10163da9fb33356en, 0x100b1afa5abcbed6n, - 0x10058c86da1c09ean, 0x1002c605e2e8cec5n, - 0x100162f3904051fan, 0x1000b175effdc76cn, - 0x100058ba01fb9f97n, 0x10002c5cc37da949n, - 0x1000162e525ee054n, 0x10000b17255775c0n, - 0x1000058b91b5bc9bn, 0x100002c5c89d5ec7n, - 0x10000162e43f4f83n, 0x100000b1721bcfcan, - 0x10000058b90cf1e7n, 0x1000002c5c863b74n, - 0x100000162e430e5an, 0x1000000b17218355n, - 0x100000058b90c0b5n, 0x10000002c5c8601dn, - 0x1000000162e42fffn, 0x10000000b17217fcn, - 0x1000000058b90bfdn, 0x100000002c5c85fen, - 0x10000000162e42ffn, 0x100000000b172180n, - 0x10000000058b90c0n, 0x1000000002c5c860n, - 0x100000000162e430n, 0x1000000000b17218n, - 0x100000000058b90cn, 0x10000000002c5c86n, - 0x1000000000162e43n, 0x10000000000b1721n, - 0x1000000000058b91n, 0x100000000002c5c8n, - 0x10000000000162e4n, 0x100000000000b172n, - 0x10000000000058b9n, 0x1000000000002c5dn, - 0x100000000000162en, 0x1000000000000b17n, - 0x100000000000058cn, 0x10000000000002c6n, - 0x1000000000000163n, 0x10000000000000b1n, - 0x1000000000000059n, 0x100000000000002cn, - 0x1000000000000016n, 0x100000000000000bn, - 0x1000000000000006n, 0x1000000000000003n, - 0x1000000000000001n, 0x1000000000000001n - ] - - /** - * `log2(x)` for a Q60 value `>= FpOne` (real `x >= 1`), returned in Q60. - * Bit-by-bit mantissa squaring — the mirror of `amm::log2_fp`. - */ - function log2Fp(xFp: bigint): bigint { - let x = xFp, - result = 0n - // Integer part: bring the mantissa into [FpOne, 2*FpOne). - while (x >= FpOne << 1n) { - x >>= 1n - result += FpOne - } - // Fractional part: square repeatedly; each carry past 2 contributes a bit. - let b = FpOne >> 1n - for (let i = 0n; i < FpBits; i++) { - x = (x * x) >> FpBits - if (x >= FpOne << 1n) { - result += b - x >>= 1n - } - b >>= 1n - } - return result - } - - /** - * `2^f` for `f` in `[0, FpOne)`, returned in Q60 within `[FpOne, 2*FpOne)`. - * The mirror of `amm::exp2_frac`. - */ - function exp2Frac(fraction: bigint): bigint { - let f = fraction, - y = FpOne - for (let i = 0; i < Number(FpBits); i++) { - f <<= 1n - if (f >= FpOne) { - f -= FpOne - y = (y * Exp2FracTable[i]) >> FpBits - } - } - return y - } - - /** - * `(num/den)^(expNum/expDen)` for a base in `(0,1]` and a positive exponent, - * returned in Q60 within `(0, FpOne]`. The mirror of `amm::pow_frac_fp`, - * evaluated as `2^(-e * log2(den/num))`. - */ - function powFracFp( - num: bigint, - den: bigint, - expNum: bigint, - expDen: bigint - ): bigint { - if (expDen === 0n) return FpOne - // base >= 1 -> 1 (only base == 1 reaches here). - if (num >= den) return FpOne - - const xFp = (den << FpBits) / num, - lr = log2Fp(xFp), - g = (lr * expNum) / expDen, - gi = g >> FpBits - // 2^(-g) below 1 ULP -> 0. - if (gi >= FpBits + 2n) return 0n - const gf = g - (gi << FpBits), - e2 = exp2Frac(gf), - inv = (FpOne * FpOne) / e2 - return inv >> gi - } - - /** - * Weighted constant-product output — the TypeScript mirror of - * `sysio::opp::amm::out_given_in`, the curve the depot actually settles on: - * - * ``` - * amountOut = balanceOut * (1 - (balanceIn/(balanceIn+amountIn))^(wIn/wOut)) - * ``` - * - * Equal weights take the EXACT integer constant-product path - * ({@link cpOutput}); unequal weights evaluate the fractional power in Q60 - * fixed point, with the subtracted term rounded UP so the output is floored — - * the reserve never over-pays. Both branches are bit-identical to the - * contract. - * - * @param balanceIn - The input side's pool depth. - * @param weightIn - The input side's weight in bps. - * @param balanceOut - The output side's pool depth. - * @param weightOut - The output side's weight in bps. - * @param amountIn - The amount entering the input side. - * @returns `floor(amountOut)`, capped at `balanceOut`, or `0n` on degenerate - * input. - */ - export function outGivenIn( - balanceIn: bigint, - weightIn: bigint, - balanceOut: bigint, - weightOut: bigint, - amountIn: bigint - ): bigint { - if (balanceIn <= 0n || balanceOut <= 0n || amountIn <= 0n) return 0n - if (weightIn <= 0n || weightOut <= 0n) return 0n - - // Symmetric pool: pure constant product, exact integer arithmetic. - if (weightIn === weightOut) { - const out = cpOutput(balanceIn, balanceOut, amountIn) - return out >= balanceOut ? balanceOut : out - } - - const bpow = powFracFp(balanceIn, balanceIn + amountIn, weightIn, weightOut), - // Round the subtracted term UP so `out` is floored. - term = (balanceOut * bpow + (FpOne - 1n)) >> FpBits - return term >= balanceOut ? 0n : balanceOut - term - } - - /** - * Quote a reserve's TOKEN side into WIRE — the mirror of - * `sysio::opp::amm::token_to_wire`. This is the source-side hop of a swap. - * - * @param reserveChainAmount - The reserve's token-side depth. - * @param reserveWireAmount - The reserve's WIRE-side depth. - * @param connectorWeightBps - The reserve's WIRE-side weight in bps. - * @param amountToken - The token amount entering the reserve. - * @returns The gross WIRE the curve yields (pre-fee). - */ - export function tokenToWire( - reserveChainAmount: bigint, - reserveWireAmount: bigint, - connectorWeightBps: number, - amountToken: bigint - ): bigint { - const cw = BigInt(connectorWeightBps) - return outGivenIn( - reserveChainAmount, - BigInt(WeightTotalBps) - cw, - reserveWireAmount, - cw, - amountToken - ) - } - - /** - * Quote a reserve's WIRE side into its TOKEN — the mirror of - * `sysio::opp::amm::wire_to_token`. This is the destination-side hop of a - * swap, and consumes the POST-fee WIRE leg. - * - * @param reserveWireAmount - The reserve's WIRE-side depth. - * @param reserveChainAmount - The reserve's token-side depth. - * @param connectorWeightBps - The reserve's WIRE-side weight in bps. - * @param amountWire - The WIRE amount entering the reserve. - * @returns The token amount the curve yields. - */ - export function wireToToken( - reserveWireAmount: bigint, - reserveChainAmount: bigint, - connectorWeightBps: number, - amountWire: bigint - ): bigint { - const cw = BigInt(connectorWeightBps) - return outGivenIn( - reserveWireAmount, - cw, - reserveChainAmount, - BigInt(WeightTotalBps) - cw, - amountWire - ) - } - - /** - * Split a gross WIRE amount into its swap fee and remainder, mirroring - * `sysio::opp::amm::split_wire_fee` bit-for-bit (floored integer math). - * - * `feeBps` is deliberately REQUIRED — the depot charges whatever the - * `sysio.uwrit::uwconfig` singleton holds (the bootstrap seeds it via - * `setconfig`), so callers read the live value ({@link readFeeBps}) instead - * of relying on a hardcoded default that silently drifts from the cluster. - * - * @param wireAmount - The gross WIRE leg — the constant-product intermediate - * for a token source, or the user's escrowed WIRE for a from-WIRE swap. - * @param feeBps - Fee in basis points — pass the live `uwconfig.fee_bps`. - * @param underwriterShareBps - The winning underwriter's share of the fee in - * bps (defaults to {@link FeeUnderwriterShareBps}). Pass `0` for a path with - * no winning underwriter — a revert refund — which sends the whole fee into - * the rewards pool, exactly as `sysio.reserv::refundwire` does. - * @param emissionsShareBps - The emissions treasury's share OF THE REWARDS - * POOL (not of the whole fee) in bps, mirroring the live - * `reserve_config.fee_emissions_share_bps`; defaults to - * {@link FeeEmissionsShareBps} (zero). - * @param srcReserveFeeBps - The SOURCE reserve's `owner_fee_bps` — an - * independent per-reserve fee on the same leg. Zero for a WIRE source. - * @param dstReserveFeeBps - The DESTINATION reserve's `owner_fee_bps`. Zero - * when the swap pays out WIRE. - * @returns The {@link WireFee} decomposition. - */ - export function splitWireFee( - wireAmount: bigint, - feeBps: number, - underwriterShareBps: number = FeeUnderwriterShareBps, - emissionsShareBps: number = FeeEmissionsShareBps, - srcReserveFeeBps = 0, - dstReserveFeeBps = 0 - ): WireFee { - const bps = BigInt(BpsTotal), - clamp = (value: number) => BigInt(Math.min(Math.max(value, 0), BpsTotal)), - // Every rate applies to the SAME gross leg, so the fees are additive and - // order-independent — matching `sysio::opp::amm::split_wire_fee`. - srcReserveShare = (wireAmount * clamp(srcReserveFeeBps)) / bps, - dstReserveShare = (wireAmount * clamp(dstReserveFeeBps)) / bps, - networkFee = (wireAmount * clamp(feeBps)) / bps, - underwriterShare = (networkFee * clamp(underwriterShareBps)) / bps, - rewardsPool = networkFee - underwriterShare, - emissionsShare = (rewardsPool * clamp(emissionsShareBps)) / bps, - fee = networkFee + srcReserveShare + dstReserveShare - return { - fee, - underwriterShare, - rewardShare: rewardsPool - emissionsShare, - emissionsShare, - srcReserveShare, - dstReserveShare, - net: fee >= wireAmount ? 0n : wireAmount - fee - } - } - - /** One reserve row's `(chain, wire)` book and its curve weight. */ - export interface ReserveBook { - /** The reserve's chain-side (token) depth. */ - chain: bigint - /** The reserve's WIRE-side depth. */ - wire: bigint - /** - * The reserve's `connector_weight_bps` — its WIRE-side weight, the token - * side taking the remainder of {@link WeightTotalBps}. Required rather - * than defaulted: a book quoted at the wrong weight rides a different - * curve than the depot settles on, and at - * {@link SymmetricConnectorWeightBps} (the common case) the error is - * invisible, so a default would hide the mistake exactly where it is - * hardest to notice. - */ - connectorWeightBps: number - /** - * The reserve owner's own fee on the WIRE leg, in basis points — the row's - * `owner_fee_bps`, charged independently of (and on the same gross leg as) - * the network fee. - * - * Required for the same reason as {@link connectorWeightBps}, and for a - * reason this field learned the hard way: a PUBLIC reserve carries `0`, so - * a quote that silently omits the owner fee is exactly right on twelve of - * the thirteen flows and wrong only where an owner fee is actually - * configured. Defaulting it would restore that blind spot. - */ - ownerFeeBps: number - } - - /** - * Post-fee swap quote along the depot curve — the TypeScript mirror of - * `sysio::opp::amm::quote_swap`, evaluated over book values the caller - * already holds. Pass `null` for a WIRE endpoint: the depot IS the WIRE side, - * so that leg consults no reserve. - * - * The WIRE-leg fee is charged BETWEEN the hops — the source side gives up the - * full gross intermediate, and only the post-fee remainder converts into the - * destination token. Quoting the hops fee-free overstates the destination - * amount by roughly `feeBps`, which is what the depot settles short of: since - * `sysio.uwrit` derives `dst_amount` from this same expression, a fee-free - * quote is not a settlement amount, it is a settlement amount plus the fee. - * - * @param source - The source reserve's book, or `null` for a WIRE source. - * @param destination - The destination reserve's book, or `null` for a WIRE - * destination. - * @param amountIn - The source amount in the source leg's depot-frame units. - * @param feeBps - The WIRE-leg fee in basis points — pass the live - * `uwconfig.fee_bps` ({@link readFeeBps}). - * @returns The destination amount the recipient receives, or `0n` when the - * input or either hop is degenerate — the on-chain "no quote available" - * convention. - */ - export function quoteSwap( - source: ReserveBook | null, - destination: ReserveBook | null, - amountIn: bigint, - feeBps: number - ): bigint { - if (amountIn <= 0n) return 0n - // WIRE → WIRE is a plain transfer: no curve, no fee. - if (source == null && destination == null) return amountIn - const wireLeg = - source == null - ? amountIn - : tokenToWire( - source.chain, - source.wire, - source.connectorWeightBps, - amountIn - ) - if (wireLeg === 0n) return 0n - // Both reserve owners charge on the SAME gross leg the network fee rides, - // so all three come out of one `splitWireFee` — mirroring the contract's - // `opp::amm::quote_swap`. A WIRE endpoint has no reserve on that side and - // therefore charges no owner fee, whatever the book would have said. - const { net } = splitWireFee( - wireLeg, - feeBps, - FeeUnderwriterShareBps, - FeeEmissionsShareBps, - source?.ownerFeeBps ?? 0, - destination?.ownerFeeBps ?? 0 - ) - // A WIRE destination receives the post-fee WIRE leg directly. - if (destination == null) return net - return wireToToken( - destination.wire, - destination.chain, - destination.connectorWeightBps, - net - ) - } - - /** - * The drift window a swap's destination payout may deviate from its quoted - * target — `target × toleranceBps / 10000` (floored). - * - * @param target - The quoted destination amount. - * @param toleranceBps - The variance tolerance in basis points. - * @returns The allowed absolute drift. - */ - export function varianceDrift(target: bigint, toleranceBps: number): bigint { - return (target * BigInt(toleranceBps)) / BigInt(BpsTotal) - } - - /** - * The depot-frame precision CAP. A token's depot precision is - * `min(nativeDecimals, DepotPrecisionCap)` — a token at or below the cap - * (6-dec stablecoins, 9-dec lamports) rides OPP envelopes at its NATIVE - * precision (identity), and only an above-cap token (18-dec wei) scales. - * Mirrors Ethereum `PrecisionLib.DEPOT_PRECISION` and Solana - * `precision::DEPOT_PRECISION_DECIMALS`; changing it requires a - * coordinated depot + outpost migration. - */ - export const DepotPrecisionCap = 9 - - /** - * The depot-frame precision the depot records for a token — - * `min(nativeDecimals, DepotPrecisionCap)` (mirror of Ethereum's - * `PrecisionLib.depotPrecision`). - * - * @param nativeDecimals - The token's chain-native decimal scale. - * @returns The token's depot-frame precision. - */ - export function depotPrecision(nativeDecimals: number): number { - Assert.ok( - Number.isInteger(nativeDecimals) && nativeDecimals > 0, - `WireReserveTool.depotPrecision: invalid native decimals ${nativeDecimals}` - ) - return Math.min(nativeDecimals, DepotPrecisionCap) - } - - /** - * Convert a chain-native amount to the token's depot frame — the exact - * scaling the source outpost applies before stamping an outbound - * `SwapRequest.source_amount` (mirror of Ethereum's `PrecisionLib.toDepot` - * and Solana's `precision::to_depot`). Per-token precision: an at-or-below-cap - * token is identity; only an above-cap token downscales. - * - * @param nativeAmount - Amount in chain-native base units (wei, lamports, ERC-20 units). - * @param nativeDecimals - The token's chain-native decimal scale. - * @returns The amount in the token's depot-frame units (floored when downscaling). - */ - export function toDepot( - nativeAmount: bigint, - nativeDecimals: number - ): bigint { - const precision = depotPrecision(nativeDecimals) - return nativeDecimals > precision - ? nativeAmount / 10n ** BigInt(nativeDecimals - precision) - : nativeAmount - } - - /** - * Convert a depot-frame amount to chain-native base units — the exact - * scaling the destination outpost applies when paying out a remit (mirror - * of Ethereum's `PrecisionLib.fromDepot` and Solana's `precision::from_depot`). - * Per-token precision: an at-or-below-cap token is identity; only an - * above-cap token upscales. - * - * @param depotAmount - Amount in the token's depot-frame units. - * @param nativeDecimals - The destination token's chain-native decimal scale. - * @returns The amount in chain-native base units. - */ - export function fromDepot( - depotAmount: bigint, - nativeDecimals: number - ): bigint { - const precision = depotPrecision(nativeDecimals) - return nativeDecimals > precision - ? depotAmount * 10n ** BigInt(nativeDecimals - precision) - : depotAmount - } - - /** - * The live WIRE-leg fee (bps) from the `sysio.uwrit::uwconfig` singleton — - * the exact value the bootstrap's `setconfig` seeded and the depot charges - * (a read). - * - * @param wire - The depot client. - * @returns The configured `fee_bps`. - */ - export async function readFeeBps(wire: WireClient): Promise { - const { rows } = await wire - .getSysioContract(SysioContractName.uwrit) - .tables.uwconfig.query() - return Number(rows[0]?.fee_bps ?? 0) - } - - /** One reserve's identifying slug triple. */ - export interface ReserveTriple { - /** The reserve's chain slug value. */ - chainCode: number - /** The reserve's token slug value. */ - tokenCode: number - /** The reserve's own slug value. */ - reserveCode: number - } - - /** Input for {@link swapquote}. */ - export interface SwapQuoteRequest { - /** The source leg's reserve triple (WIRE/WIRE for a from-WIRE swap). */ - from: ReserveTriple - /** The source amount, in the source leg's depot-frame units. */ - fromAmount: bigint - /** The destination leg's reserve triple (WIRE/WIRE for a to-WIRE swap). */ - to: ReserveTriple - } - - /** Whether a triple denotes the WIRE leg (no reserve consulted). */ - function isWireLeg(triple: ReserveTriple): boolean { - return ( - triple.chainCode === WireChainCode && triple.tokenCode === WireTokenCode - ) - } - - /** - * Cross-chain swap quote — the read-only `sysio.reserv::swapquote` surface, - * evaluated client-side from the live `reserves` table and the live - * `uwconfig.fee_bps` (reads). A thin composition of {@link quoteSwap} over - * those rows, so callers can assert expected quotes before issuing a - * SWAP_REQUEST and get the amount the depot will actually settle. - * - * The fee is read here rather than accepted as a parameter: `sysio.uwrit` - * reads `uwconfig.fee_bps` fresh at both ingestion and settlement, and a - * caller who forgets to apply it gets a quote that is short by exactly the - * fee — silently, since a loose variance tolerance still admits the request. - * - * @param wire - The depot client. - * @param request - The source triple + amount and destination triple. - * @returns The destination amount, or `0n` when any required reserve row is - * missing — matching the on-chain "no quote available" convention. - */ - export async function swapquote( - wire: WireClient, - request: SwapQuoteRequest - ): Promise { - const { from, fromAmount, to } = request - if (fromAmount <= 0n) return 0n - const fromIsWire = isWireLeg(from), - toIsWire = isWireLeg(to) - if (fromIsWire && toIsWire) return fromAmount - - const { rows } = await wire - .getSysioContract(SysioContractName.reserv) - .tables.reserves.query({ limit: MaxReservesScan }) - const bookFor = (triple: ReserveTriple): ReserveBook | undefined => { - const reserve = rows.find( - row => - slugValue(row.chain_code) === triple.chainCode && - slugValue(row.token_code) === triple.tokenCode && - slugValue(row.reserve_code) === triple.reserveCode - ) - return reserve == null - ? undefined - : { - chain: BigInt(reserve.reserve_chain_amount), - wire: BigInt(reserve.reserve_wire_amount), - connectorWeightBps: Number(reserve.connector_weight_bps), - ownerFeeBps: Number(reserve.owner_fee_bps) - } - } - - // `null` denotes the WIRE leg (no reserve consulted); a missing row for a - // non-WIRE leg is "no quote available". - let source: ReserveBook | null = null - if (!fromIsWire) { - const book = bookFor(from) - if (book == null) return 0n - source = book - } - let destination: ReserveBook | null = null - if (!toIsWire) { - const book = bookFor(to) - if (book == null) return 0n - destination = book - } - return quoteSwap(source, destination, fromAmount, await readFeeBps(wire)) - } -} diff --git a/packages/cluster-tool/src/tools/wire/WireSyndicationTool.ts b/packages/cluster-tool/src/tools/wire/WireSyndicationTool.ts index e8637ec73..cf7336281 100644 --- a/packages/cluster-tool/src/tools/wire/WireSyndicationTool.ts +++ b/packages/cluster-tool/src/tools/wire/WireSyndicationTool.ts @@ -1,3 +1,4 @@ +import { Constants } from "../../Constants.js" /** * WireSyndicationTool — reads of the depot's syndication state (`sysio.synd`, * `sysio.bond`, `sysio.andon`, the `sysio.liq` shadow ledger and the @@ -40,7 +41,6 @@ import { BondContractSteps } from "../../orchestration/steps/contracts/sysio/Bon import { Report } from "../../report/Report.js" import { matchesProtoEnum } from "../../utils/predicateUtils.js" import { slugValue } from "../../utils/slugUtils.js" -import { WireReserveTool } from "./WireReserveTool.js" const { SysioContractName, @@ -928,7 +928,7 @@ export namespace WireSyndicationTool { /** * Verify each shadow's `sysio.liq::stat` supply is at the depot frame's precision - * ({@link WireReserveTool.DepotPrecisionCap}, the frame the Ethereum pool floors its + * ({@link Constants.DepotPrecisionCap}, the frame the Ethereum pool floors its * custody to), which is at least {@link BondIncrementDecimals} — below it `sysio.bond` * cannot bond the token and every envelope of the pair stalls WAITING. */ @@ -959,8 +959,8 @@ export namespace WireSyndicationTool { ): Promise { signal.throwIfAborted() Assert.ok( - WireReserveTool.DepotPrecisionCap >= BondIncrementDecimals, - `WireSyndicationTool: the depot frame (${WireReserveTool.DepotPrecisionCap}) is below sysio.bond's increment decimals (${BondIncrementDecimals})` + Constants.DepotPrecisionCap >= BondIncrementDecimals, + `WireSyndicationTool: the depot frame (${Constants.DepotPrecisionCap}) is below sysio.bond's increment decimals (${BondIncrementDecimals})` ) const precisions = await Promise.all( input.symbolCodes.map(async symbolCode => ({ @@ -970,11 +970,11 @@ export namespace WireSyndicationTool { })) ) const wrong = precisions.filter( - ({ precision }) => precision !== WireReserveTool.DepotPrecisionCap + ({ precision }) => precision !== Constants.DepotPrecisionCap ) Assert.ok( wrong.length === 0, - `WireSyndicationTool: shadow precision must be ${WireReserveTool.DepotPrecisionCap}; got ` + + `WireSyndicationTool: shadow precision must be ${Constants.DepotPrecisionCap}; got ` + wrong .map(({ symbolCode, precision }) => `${symbolCode}=${precision}`) .join(", ") diff --git a/packages/cluster-tool/src/tools/wire/WireUnderwriterTool.ts b/packages/cluster-tool/src/tools/wire/WireUnderwriterTool.ts deleted file mode 100644 index df7a87b0c..000000000 --- a/packages/cluster-tool/src/tools/wire/WireUnderwriterTool.ts +++ /dev/null @@ -1,685 +0,0 @@ -/** - * WireUnderwriterTool — the underwriter collateral surface. The pure VALUE - * helpers (defaults / JSON-config parsing) compute the per-underwriter collateral - * plan; {@link WireUnderwriterTool.planCollateralDeposit} is the orchestration-unit factory that - * turns that plan into a {@link ClusterBuildPhaseGroup} of per-underwriter - * {@link ClusterBuildPhase}s, each holding the per-`(chain, token)` deposit Steps - * (so the `Report` records every individual bond). Every on-chain WRITE is its own - * Step, delegated to the chain-specific collateral / funding tools; the plan - * computation stays plain functions used INSIDE the factory. - * - * The underwriter accounts are ASSUMED already provisioned into THE cluster - * key store (`ctx.keyStore` — their typed ETH + SOL keys ride each - * {@link OperatorAccount}); wiring that provisioning is the bootstrap / flow `beforeAll` - * concern — this tool only returns the deposit PhaseGroup. - */ - -import Assert from "node:assert" -import Fs from "node:fs" - -import type { - ChainTokenAmount, - ChainTokenAmountSchema, - ClusterConfig -} from "@wireio/cluster-tool-shared" -import { match } from "ts-pattern" -import type { z } from "zod" - -import { - ChainKind, - OperatorType, - TokenAmount, - TokenKind -} from "@wireio/opp-typescript-models" -import { SlugName } from "@wireio/sdk-core" -import { getLogger, NestedError } from "@wireio/shared" - -import { ClusterBuildContext } from "../../orchestration/ClusterBuildContext.js" -import { ClusterBuildPhase } from "../../orchestration/ClusterBuildPhase.js" -import { ClusterBuildPhaseGroup } from "../../orchestration/ClusterBuildPhaseGroup.js" -import { - ClusterBuildStep, - type ClusterBuildStepOptions -} from "../../orchestration/ClusterBuildStep.js" -import type { ClusterBuildParent } from "../../orchestration/ClusterBuildPhaseBase.js" -import { Report } from "../../report/Report.js" -import { EthereumCollateralTool } from "../ethereum/EthereumCollateralTool.js" -import { EthereumFundingTool } from "../ethereum/EthereumFundingTool.js" -import { SolanaCollateralTool } from "../solana/SolanaCollateralTool.js" -import { SolanaFundingTool } from "../solana/SolanaFundingTool.js" -import { WireCollateralTool } from "./WireCollateralTool.js" - -const log = getLogger(__filename) - -/** - * Underwriter collateral surface — defaults, JSON config parsing, and the deposit - * PhaseGroup factory. Grouped together (rather than three sibling files) because - * every entry point operates on the same `ChainTokenAmount[][]` shape and lives - * under the same conceptual concern: "what bond should each underwriter post, and - * how does the harness apply that plan?". - */ -export namespace WireUnderwriterTool { - // ── Defaults (pure VALUE helpers) ─────────────────────────────────────── - - /** - * Default per-(chain, token) deposit amount when neither - * `--underwriter-collateral-json-file` nor a programmatic override - * is provided. The spec at "Underwriter Collateral Config for - * `cluster-tool`" calls for `1000` of each token, but the - * underlying chains have widely-varying smallest-unit conventions: - * 1000 lamports on Solana sits below the rent-exempt threshold the - * `opp_outpost::deposit` ix triggers when it resizes the - * operator-registry PDA, so a literal-1000 default fails at - * simulation time. The value here (`1_000_000_000` base units) - * matches flow-batch-operator-termination's batch-operator deposit - * (`FLOW_E_REQ_SOL_MIN_BOND`) and is the smallest amount - * empirically known to clear PDA rent growth on Solana while - * remaining negligible on Ethereum (1e9 wei = 10⁻⁹ ETH) and on - * WIRE. - * - * Encoded as `bigint` to match the `TokenAmount.amount` proto - * field (`@protobuf-ts/runtime` decodes `int64` as `bigint`). - * Operators that need realistic magnitudes set - * `--underwriter-collateral-json-file` with explicit per-leg - * amounts. - */ - export const DefaultAmount: bigint = 1_000_000_000n - - /** - * One {@link DefaultPairs} entry: a `(chain_code, token_code)` slug_name pair - * plus the {@link ChainKind} that selects the per-chain deposit backend. - */ - export interface DefaultCollateralPair { - chainCode: number - tokenCode: number - /** Discriminant the per-chain deposit dispatch matches on. */ - chainKind: ChainKind - } - - /** - * Default (chain_code, token_code) slug_name pairs deposited to every - * underwriter when no `--underwriter-collateral-json-file` is supplied. - * Tracks the integrated-outpost set; if a new outpost is added (Sui, etc.), - * add the corresponding `(chainCode, tokenCode)` pair here so the default - * deposits cover it without requiring every caller to specify a config - * file. - */ - export const DefaultPairs: ReadonlyArray = [ - { - chainCode: SlugName.from("WIRE"), - tokenCode: SlugName.from("WIRE"), - chainKind: ChainKind.WIRE - }, - { - chainCode: SlugName.from("ETHEREUM"), - tokenCode: SlugName.from("ETH"), - chainKind: ChainKind.EVM - }, - { - chainCode: SlugName.from("SOLANA"), - tokenCode: SlugName.from("SOL"), - chainKind: ChainKind.SVM - } - ] as const - - /** - * Reserve slug_name that the outpost `depositNonNative` writes pair a non-native - * token against. All mock stablecoins register under the PRIMARY reserve. - */ - export const PrimaryReserveCode: bigint = BigInt(SlugName.from("PRIMARY")) - - /** - * Extra lamports (on top of the deposit amount) an underwriter's SOL keypair is - * topped up to before a SOL deposit — covers the deposit's transaction fees and - * the rent of the PDAs and ATAs it creates, with enough headroom that a run never - * stalls on an under-funded operator wallet. Raising it only costs faucet lamports; - * lowering it risks a deposit failing on insufficient funds. - */ - export const SolAirdropHeadroomLamports: bigint = 5_000_000_000n - - /** - * Build the default underwriter-collateral set: one - * {@link ChainTokenAmount} per {@link DefaultPairs} entry, each - * amounting to {@link DefaultAmount} base units. - * - * Each entry pairs the harness-local `chain_code` (slug_name / uint64) with - * a proto-generated `TokenAmount` carrying the matching `token_code` + - * `bigint` amount. - * - * @returns A fresh array (the caller may mutate without aliasing - * the defaults). Returns the per-underwriter list shape — - * fan-out to all underwriters happens in {@link load}. - */ - export function buildDefault(): ChainTokenAmount[] { - return DefaultPairs.map(({ chainCode, tokenCode }) => ({ - chain_code: chainCode, - amount: TokenAmount.create({ - tokenCode: BigInt(tokenCode), - amount: DefaultAmount - }) - })) - } - - // ── JSON config parsing (pure VALUE helpers) ──────────────────────────── - - /** - * Parse a JSON value (already loaded from disk) into the canonical - * length-`underwriterCount` per-underwriter shape stored on - * `ClusterConfig.underwriterCollateral`. The input value may be - * in either of two shapes per the spec at "Underwriter Collateral - * Config for `cluster-tool`": - * - * * **Uniform** — `Array`. Applied to every - * underwriter. Fan-out-expanded to `underwriterCount` copies. - * * **Varied** — `Array>`. Outer array - * length MUST equal `underwriterCount`; otherwise this - * throws. - * - * Both shapes are parsed via `@protobuf-ts/runtime` JSON serdes - * against the proto-generated `TokenAmount` model, so callers get - * full field-level validation without the harness re-implementing - * the schema. The output preserves the hydrated proto-message - * instances — `amount.amount` is a `bigint`. - * - * @param json Already-parsed JSON value - * (`JSON.parse(fileContents)`). - * @param underwriterCount Number of underwriters in the cluster. - * @returns Length-`underwriterCount` array, one entry-list per - * underwriter. - * @throws If the input is neither uniform nor varied shape, OR - * if a varied input's outer length does not match - * `underwriterCount`, OR if any inner `ChainTokenAmount` fails - * proto-level validation. - */ - export function parseJson( - json: unknown, - underwriterCount: number - ): ChainTokenAmount[][] { - Assert.ok( - Array.isArray(json), - "underwriter collateral JSON must be an array" - ) - Assert.ok( - underwriterCount > 0, - `underwriterCount must be positive, got ${underwriterCount}` - ) - - const items = json as unknown[] - if (items.length === 0) { - // Treat an empty array as "use defaults" so an operator that - // wants to drop in an empty file as a placeholder gets the - // same shape they would have got with no flag at all. - return Array.from({ length: underwriterCount }, () => buildDefault()) - } - - // Uniform vs varied detection: the inner element of a varied - // input is itself an array; the inner element of a uniform - // input is an object literal. We trust the first element shape - // to discriminate (a mixed-shape input is malformed). - const head = items[0] - const isVaried = Array.isArray(head) - - if (isVaried) { - Assert.ok( - items.length === underwriterCount, - `underwriter collateral (varied shape): outer array length ${items.length} ` + - `must equal --underwriters (${underwriterCount})` - ) - return items.map((entry, idx) => { - Assert.ok( - Array.isArray(entry), - `underwriter collateral (varied shape): entry ${idx} must be an array` - ) - return entry.map(raw => parseChainTokenAmountJson(raw)) - }) - } - - // Uniform shape: parse once, fan out to every underwriter. - const uniform = items.map(raw => parseChainTokenAmountJson(raw)) - return Array.from({ length: underwriterCount }, () => uniform.slice()) - } - - /** - * Resolve the final `ClusterConfig.underwriterCollateral` value - * from the CLI surface. If a file path is supplied, it's read + - * parsed via {@link parseJson}. Otherwise the defaults from - * {@link buildDefault} are fanned out to every underwriter. - * - * @param filePath Optional path to the JSON config file. - * When nullish, defaults are used. - * @param underwriterCount Number of underwriters in the cluster. - * @returns Length-`underwriterCount` array, one entry-list per - * underwriter. - * @example - * // No file → defaults (DefaultAmount base units of - * // WIRE/ETH/SOL per underwriter). - * WireUnderwriterTool.load(null, 3) - * // With file → parsed per the file's shape (uniform or varied). - * WireUnderwriterTool.load("/path/to/file.json", 3) - */ - export function load( - filePath: string | null, - underwriterCount: number - ): ChainTokenAmount[][] { - Assert.ok( - underwriterCount > 0, - `underwriterCount must be positive, got ${underwriterCount}` - ) - if (!filePath) { - return Array.from({ length: underwriterCount }, () => buildDefault()) - } - Assert.ok( - Fs.existsSync(filePath), - `--underwriter-collateral-json-file: ${filePath} does not exist` - ) - const raw = Fs.readFileSync(filePath, "utf8") - let parsed: unknown - try { - parsed = JSON.parse(raw) - } catch (err) { - throw new NestedError( - `--underwriter-collateral-json-file: ${filePath} is not valid JSON`, - { cause: err } - ) - } - return parseJson(parsed, underwriterCount) - } - - // ── Deposit PhaseGroup factory (RETURNS the orchestration unit) ───────── - - /** - * Build the underwriter collateral-deposit {@link ClusterBuildPhaseGroup}: one - * child {@link ClusterBuildPhase} per underwriter, each holding — per - * `(chain, token)` collateral entry — the deposit Step(s). Every on-chain WRITE - * is its own Step so the `Report` validates each individual bond: - * - * * EVM native → {@link EthereumCollateralTool.planDeposit}. - * * EVM non-native (ERC-20) → {@link EthereumFundingTool.planErc20Mint} + - * {@link EthereumCollateralTool.planErc20Approval} + - * {@link EthereumCollateralTool.planNonNativeDeposit}. - * * SVM native → {@link SolanaFundingTool.planAirdrop} (fund the escrow) + - * {@link SolanaCollateralTool.planDeposit}. - * * SVM non-native (SPL) → {@link SolanaFundingTool.planAirdrop} + - * {@link SolanaFundingTool.planSplMint} + {@link SolanaCollateralTool.planNonNativeDeposit}. - * * WIRE native → {@link WireCollateralTool.planDeposit} (fund the operator with - * WIRE, then bond it on the depot). - * * WIRE non-native (a shadow LIQ symbol) → skipped with a warning: the harness has no - * funding path for shadow tokens. - * - * The deposit Steps resolve the operator identity from `ctx.keyStore` by its - * durable `label` handle ({@link ClusterKeyStore.assertOperator}); the - * operators are ASSUMED already provisioned - * (bootstrap / flow `beforeAll` concern). Self-registers on `parent`. - * - * @param parent - The build root or enclosing PhaseGroup. - * @param name - Short PhaseGroup name. - * @param description - Human-readable description. - * @param options - Step option overrides threaded to every deposit Step. - * @param underwriterLabels - Underwriter durable `label` handles (`ClusterKeyStore` keys), one per collateral plan entry. - * @param collateral - Per-underwriter collateral plan (from {@link load}); its - * length MUST equal `underwriterLabels.length`. - * @returns The self-registered deposit PhaseGroup. - * @throws If `collateral.length !== underwriterLabels.length`. - */ - export function planCollateralDeposit< - C extends ClusterBuildContext = ClusterBuildContext - >( - parent: ClusterBuildParent, - name: string, - description: string, - options: ClusterBuildStepOptions, - underwriterLabels: string[], - collateral: ChainTokenAmount[][] - ): ClusterBuildPhaseGroup { - Assert.ok( - collateral.length === underwriterLabels.length, - `WireUnderwriterTool.planCollateralDeposit: collateral plan length (${collateral.length}) ` + - `must equal underwriter count (${underwriterLabels.length})` - ) - const config = parent.context.config - const group = ClusterBuildPhaseGroup.create(parent, name, description) - underwriterLabels.forEach((underwriterLabel, index) => { - const steps = collateral[index].flatMap(entry => - planDepositStepsForEntry(config, options, underwriterLabel, entry) - ) - ClusterBuildPhase.create( - group, - `${underwriterLabel}-collateral`, - `underwriter ${underwriterLabel} collateral deposits`, - steps - ) - }) - return group - } -} - -// ── Module-internal plumbing (NOT exported from the namespace) ──────────── - -/** - * Bootstrap-time slug_name ↔ enum routing tables. The on-chain data model uses - * `slug_name` (uint64 packed) for chain and token primary keys; the harness - * still has to dispatch to per-chain outpost deposit helpers and pass the - * appropriate `ChainKind` / `TokenKind` enum value to outpost-side contract - * code. These maps cover the bootstrap set; if/when more outposts come online, - * extend with the matching entries. - */ -const ChainKindByCodename: ReadonlyMap = new Map([ - [SlugName.from("WIRE"), ChainKind.WIRE], - [SlugName.from("ETHEREUM"), ChainKind.EVM], - [SlugName.from("SOLANA"), ChainKind.SVM] -]) - -const TokenKindByCodename: ReadonlyMap = new Map([ - [SlugName.from("WIRE"), TokenKind.NATIVE], - [SlugName.from("ETH"), TokenKind.NATIVE], - [SlugName.from("SOL"), TokenKind.NATIVE], - [SlugName.from("LIQETH"), TokenKind.LIQ], - [SlugName.from("LIQSOL"), TokenKind.LIQ], - // Mock stablecoins deployed by `deployLocal.ts` (ETH) / - // `SolanaOutpostBootstrapper` (SOL). The depot's `Token` table assigns each - // its own slug_name code per the v6 "TWO Token rows per cross-chain pair" - // decision — same underlying asset on each chain but distinct codes so the - // primary key doesn't collide. - [SlugName.from("USDC"), TokenKind.ERC20], - [SlugName.from("USDT"), TokenKind.ERC20], - [SlugName.from("USDCSOL"), TokenKind.SPL], - [SlugName.from("USDTSOL"), TokenKind.SPL] -]) - -/** - * Resolve a packed `chain_code` slug_name to its protobuf `ChainKind` VM-family - * discriminant. Falls back to `ChainKind.UNKNOWN` so unknown chains land in - * the deposit-dispatch's "unsupported" branch and emit a structured warn - * rather than throwing — the cluster shouldn't crash on a typo-encoded code. - */ -function chainKindForCodename(chainCode: number): ChainKind { - return ChainKindByCodename.get(chainCode) ?? ChainKind.UNKNOWN -} - -/** - * Resolve a packed `token_code` slug_name to its protobuf `TokenKind` - * token-standard discriminant. Falls back to `TokenKind.UNKNOWN` so - * unrecognised tokens flow into the warn-and-continue branch rather than - * triggering a hard failure. - */ -function tokenKindForCodename(tokenCode: number): TokenKind { - return TokenKindByCodename.get(tokenCode) ?? TokenKind.UNKNOWN -} - -/** - * The `cluster-config.json` JSON form of a {@link ChainTokenAmount} — the zod - * INPUT side of `ChainTokenAmountSchema`, whose `amount` is still the raw - * proto-JSON value `TokenAmount.fromJson` accepts. DERIVED from the shared - * schema, never re-declared, so a schema change lands here through the compiler. - */ -type ChainTokenAmountJson = z.input - -/** - * Parse one entry of the `cluster-config.json`-shaped `ChainTokenAmount` - * JSON form back into the harness-local in-memory shape: `chain_code` - * passes through as a plain `number`, `amount` is rehydrated through - * `TokenAmount.fromJson` so the int64 amount restores to `bigint`. - * - * @throws if `raw` is not a `{ chain_code, amount }` object literal. - */ -function parseChainTokenAmountJson(raw: unknown): ChainTokenAmount { - Assert.ok( - raw && typeof raw === "object" && "chain_code" in raw && "amount" in raw, - "ChainTokenAmount JSON must be a `{chain_code, amount}` object literal" - ) - const r = raw as ChainTokenAmountJson - return { - chain_code: r.chain_code, - amount: TokenAmount.fromJson( - r.amount as Parameters[0] - ) - } -} - -/** - * Compute the deposit Step(s) for one `(chain, token)` collateral entry — a pure - * build-time dispatch on the entry's `ChainKind` / `TokenKind`. A read (the kind - * lookup + any artifact resolution for non-native tokens) that decides which - * Steps to emit runs directly here; each emitted Step is a validated on-chain - * write recorded by the Report. - */ -function planDepositStepsForEntry( - config: ClusterConfig, - options: ClusterBuildStepOptions, - underwriterLabel: string, - entry: ChainTokenAmount -): ClusterBuildStep.Any[] { - Assert.ok( - entry.amount, - "WireUnderwriterTool: ChainTokenAmount.amount is required" - ) - const chainCode = BigInt(entry.chain_code), - tokenCode = BigInt(entry.amount.tokenCode), - tokenCodeNum = Number(entry.amount.tokenCode), - chainName = SlugName.toString(entry.chain_code), - tokenName = SlugName.toString(tokenCodeNum), - chainKind = chainKindForCodename(entry.chain_code), - tokenKind = tokenKindForCodename(tokenCodeNum), - amount = entry.amount.amount - - if (amount <= 0n) return [] - - return match({ chainKind, tokenKind }) - .with({ chainKind: ChainKind.EVM, tokenKind: TokenKind.NATIVE }, () => - planEthereumNativeSteps( - options, - underwriterLabel, - chainName, - tokenName, - tokenCode, - amount - ) - ) - .with({ chainKind: ChainKind.EVM }, () => - planEthereumNonNativeSteps( - options, - underwriterLabel, - chainName, - tokenName, - chainCode, - tokenCode, - amount - ) - ) - .with({ chainKind: ChainKind.SVM, tokenKind: TokenKind.NATIVE }, () => - planSolanaNativeSteps( - options, - underwriterLabel, - chainName, - tokenName, - tokenCode, - amount - ) - ) - .with({ chainKind: ChainKind.SVM }, () => - planSolanaNonNativeSteps( - options, - underwriterLabel, - chainName, - tokenName, - chainCode, - tokenCode, - amount - ) - ) - .with({ chainKind: ChainKind.WIRE, tokenKind: TokenKind.NATIVE }, () => - WireCollateralTool.planDeposit( - Report.Actor.Underwriter, - `${underwriterLabel}-${chainName}-${tokenName}-deposit`, - `bond ${amount} ${tokenName} on the depot (sysio.opreg::deposit)`, - options, - underwriterLabel, - entry - ) - ) - .with({ chainKind: ChainKind.WIRE }, () => { - log.warn( - `[WireUnderwriterTool] ${underwriterLabel}: skipping WIRE/${tokenName} entry — ` + - `the harness has no funding path for shadow tokens, so it cannot bond one` - ) - return [] as ClusterBuildStep.Any[] - }) - .otherwise(() => { - log.warn( - `[WireUnderwriterTool] ${underwriterLabel}: skipping unsupported chain ${chainName}/${tokenName}` - ) - return [] as ClusterBuildStep.Any[] - }) -} - -/** EVM native deposit — one `OperatorRegistry.deposit` write. */ -function planEthereumNativeSteps( - options: ClusterBuildStepOptions, - underwriterLabel: string, - chainName: string, - tokenName: string, - tokenCode: bigint, - amount: bigint -): ClusterBuildStep.Any[] { - return [ - EthereumCollateralTool.planDeposit( - Report.Actor.Underwriter, - `${underwriterLabel}-${chainName}-${tokenName}-deposit`, - `deposit ${amount} ${tokenName} on ${chainName} (native)`, - options, - underwriterLabel, - OperatorType.UNDERWRITER, - tokenCode, - amount - ) - ] -} - -/** - * EVM non-native deposit — mint → approve → depositNonNative (three writes). - * The step SET derives from config alone; the mock-token + OperatorRegistry - * ADDRESSES are deploy artifacts the RUNNERS resolve at run time (they do not - * exist when the build constructs its steps — the outpost deploys later in the - * same build; the old factory-time read silently skipped every non-native leg). - */ -function planEthereumNonNativeSteps( - options: ClusterBuildStepOptions, - underwriterLabel: string, - chainName: string, - tokenName: string, - chainCode: bigint, - tokenCode: bigint, - amount: bigint -): ClusterBuildStep.Any[] { - return [ - EthereumFundingTool.planErc20Mint( - Report.Actor.Underwriter, - `${underwriterLabel}-${tokenName}-mint`, - `mint ${amount} mock ${tokenName} to ${underwriterLabel}`, - options, - underwriterLabel, - tokenName, - amount - ), - EthereumCollateralTool.planErc20Approval( - Report.Actor.Underwriter, - `${underwriterLabel}-${tokenName}-approve`, - `approve ${amount} ${tokenName} to OperatorRegistry`, - options, - underwriterLabel, - tokenName, - amount - ), - EthereumCollateralTool.planNonNativeDeposit( - Report.Actor.Underwriter, - `${underwriterLabel}-${chainName}-${tokenName}-deposit`, - `deposit ${amount} ${tokenName} on ${chainName} (ERC-20)`, - options, - underwriterLabel, - chainCode, - tokenCode, - WireUnderwriterTool.PrimaryReserveCode, - OperatorType.UNDERWRITER, - amount - ) - ] -} - -/** SVM native deposit — airdrop the escrow, then one `opp-outpost::deposit` write. */ -function planSolanaNativeSteps( - options: ClusterBuildStepOptions, - underwriterLabel: string, - chainName: string, - tokenName: string, - tokenCode: bigint, - amount: bigint -): ClusterBuildStep.Any[] { - return [ - SolanaFundingTool.planAirdrop( - Report.Actor.Underwriter, - `${underwriterLabel}-${chainName}-airdrop`, - `fund ${underwriterLabel} SOL keypair for the ${tokenName} deposit`, - options, - underwriterLabel, - amount + WireUnderwriterTool.SolAirdropHeadroomLamports - ), - SolanaCollateralTool.planDeposit( - Report.Actor.Underwriter, - `${underwriterLabel}-${chainName}-${tokenName}-deposit`, - `deposit ${amount} ${tokenName} on ${chainName} (native)`, - options, - underwriterLabel, - OperatorType.UNDERWRITER, - tokenCode, - amount - ) - ] -} - -/** - * SVM non-native deposit — airdrop → mint SPL → depositNonNative. The step SET - * derives from config alone; the SPL mint ADDRESS is a deploy artifact the - * RUNNERS resolve at run time (`sol-mock-mints.json` does not exist when the - * build constructs its steps — the outpost deploys later in the same build). - */ -function planSolanaNonNativeSteps( - options: ClusterBuildStepOptions, - underwriterLabel: string, - chainName: string, - tokenName: string, - chainCode: bigint, - tokenCode: bigint, - amount: bigint -): ClusterBuildStep.Any[] { - return [ - SolanaFundingTool.planAirdrop( - Report.Actor.Underwriter, - `${underwriterLabel}-${chainName}-airdrop`, - `fund ${underwriterLabel} SOL keypair for the ${tokenName} deposit`, - options, - underwriterLabel, - amount + WireUnderwriterTool.SolAirdropHeadroomLamports - ), - SolanaFundingTool.planSplMint( - Report.Actor.Underwriter, - `${underwriterLabel}-${tokenName}-mint`, - `mint ${amount} mock ${tokenName} to ${underwriterLabel}`, - options, - underwriterLabel, - tokenCode, - amount - ), - SolanaCollateralTool.planNonNativeDeposit( - Report.Actor.Underwriter, - `${underwriterLabel}-${chainName}-${tokenName}-deposit`, - `deposit ${amount} ${tokenName} on ${chainName} (SPL)`, - options, - underwriterLabel, - chainCode, - tokenCode, - WireUnderwriterTool.PrimaryReserveCode, - OperatorType.UNDERWRITER, - amount - ) - ] -} diff --git a/packages/cluster-tool/src/tools/wire/index.ts b/packages/cluster-tool/src/tools/wire/index.ts index 9e87dd6fa..84a703fb1 100644 --- a/packages/cluster-tool/src/tools/wire/index.ts +++ b/packages/cluster-tool/src/tools/wire/index.ts @@ -4,7 +4,5 @@ export * from "./WireOperatorProvisioningTool.js" export * from "./OperatorDaemonTool.js" export * from "./ProducerNodeTool.js" export * from "./WireCollateralTool.js" -export * from "./WireUnderwriterTool.js" export * from "./WireDclaimSeedTool.js" -export * from "./WireReserveTool.js" export * from "./WireSyndicationTool.js" diff --git a/packages/cluster-tool/tests/cli/ClusterBuildOptionsArgs.test.ts b/packages/cluster-tool/tests/cli/ClusterBuildOptionsArgs.test.ts index d0562ef45..0f702b0bd 100644 --- a/packages/cluster-tool/tests/cli/ClusterBuildOptionsArgs.test.ts +++ b/packages/cluster-tool/tests/cli/ClusterBuildOptionsArgs.test.ts @@ -190,7 +190,6 @@ describe("flattenOptionLeaves + buildOptionShape", () => { "epoch-duration-sec", "force", "bind-all", - "enable-mock-reserves", "chain-state-db-size-mb", "bind-kiod-port", "bind-kiod-address", @@ -438,7 +437,10 @@ describe("WIRE_* environment seeding (the run-flow.mjs / e2e-gate contract)", () }) it("the environment (per-invocation operator intent) beats scenario defaults", () => { - const options = register({ clusterPath: "/tmp/scenario-cluster" }, environment) + const options = register( + { clusterPath: "/tmp/scenario-cluster" }, + environment + ) expect(options.get("cluster-path")?.default).toBe("/tmp/env-cluster") }) @@ -482,7 +484,9 @@ describe("toClusterBuildOptions reverse parse", () => { { "epoch-duration-sec": 60 }, { requiredBatchOperatorCollateral } ) - expect(options.requiredBatchOperatorCollateral).toEqual(requiredBatchOperatorCollateral) + expect(options.requiredBatchOperatorCollateral).toEqual( + requiredBatchOperatorCollateral + ) // absent defaults stay absent — flags never set these leaves expect(options.requiredUnderwriterCollateral).toBeUndefined() }) @@ -533,33 +537,23 @@ describe("toClusterBuildOptions reverse parse", () => { expect(toClusterBuildOptions({ "bind-all": true }).bindAll).toBe(true) expect(toClusterBuildOptions({ "bind-all": false }).bindAll).toBe(false) expect( - toClusterBuildOptions({ "enable-mock-reserves": true }).enableMockReserves - ).toBe(true) - expect( - toClusterBuildOptions({ "enable-mock-reserves": false }).enableMockReserves - ).toBe(false) - expect( - toClusterBuildOptions({ "enable-launch-withheld-operations": true }) - .enableLaunchWithheldOperations + toClusterBuildOptions({ "enable-mock-liq-pools": true }) + .enableMockLiqPools ).toBe(true) expect( - toClusterBuildOptions({ "enable-launch-withheld-operations": false }) - .enableLaunchWithheldOperations - ).toBe(false) - expect( - toClusterBuildOptions({ "enable-mock-liq-pools": true }).enableMockLiqPools - ).toBe(true) - expect( - toClusterBuildOptions({ "enable-mock-liq-pools": false }).enableMockLiqPools + toClusterBuildOptions({ "enable-mock-liq-pools": false }) + .enableMockLiqPools ).toBe(false) }) it("round trips the mock syndication import leaf", () => { expect( - toClusterBuildOptions({ "enable-mock-syndication-import": true }).enableMockSyndicationImport + toClusterBuildOptions({ "enable-mock-syndication-import": true }) + .enableMockSyndicationImport ).toBe(true) expect( - toClusterBuildOptions({ "enable-mock-syndication-import": false }).enableMockSyndicationImport + toClusterBuildOptions({ "enable-mock-syndication-import": false }) + .enableMockSyndicationImport ).toBe(false) }) @@ -611,10 +605,6 @@ describe("register → parse round-trip", () => { expect(options.epochDurationSec).toBe(60) expect(options.nodeCount).toBe(1) expect(options.bindAll).toBe(false) - // no opt-in ⇒ the default-false mock-data flags survive as false - expect(options.enableMockReserves).toBe(false) - // …and so does the default-false launch-policy opt-in - expect(options.enableLaunchWithheldOperations).toBe(false) expect(options.enableMockLiqPools).toBe(false) expect(options.enableMockSyndicationImport).toBe(false) // unseeded (null-default) bind ports never materialize @@ -641,8 +631,6 @@ describe("register → parse round-trip", () => { terminateMaxConsecutiveMisses: 5, terminateMaxPercentMisses24h: 99, terminateWindowMs: 3_600_000, - enableMockReserves: true, - enableLaunchWithheldOperations: true, enableMockLiqPools: true, enableMockSyndicationImport: true, chainStateDbSizeMb: 8_192, @@ -661,10 +649,6 @@ describe("register → parse round-trip", () => { expect(options.terminateMaxConsecutiveMisses).toBe(5) expect(options.terminateMaxPercentMisses24h).toBe(99) expect(options.terminateWindowMs).toBe(3_600_000) - // the scenario-defaults opt-in path the 6 reserve-needing flows rely on - expect(options.enableMockReserves).toBe(true) - // …and the launch-policy opt-in the swap and reserve flows rely on - expect(options.enableLaunchWithheldOperations).toBe(true) // …and the one the liq-yield flow's pools ride expect(options.enableMockLiqPools).toBe(true) expect(options.enableMockSyndicationImport).toBe(true) @@ -741,11 +725,15 @@ describe("mergeSignatureProviderSSM", () => { }) it("falls back to the options-file document's signatureProvider.ssm", () => { - const merged = mergeSignatureProviderSSM({}, {}, { - signatureProvider: { - ssm: { awsSecretIdPattern: "/from/{cluster}/{account}/{keyType}" } + const merged = mergeSignatureProviderSSM( + {}, + {}, + { + signatureProvider: { + ssm: { awsSecretIdPattern: "/from/{cluster}/{account}/{keyType}" } + } } - }) + ) expect(merged.signatureProvider?.ssm?.awsSecretIdPattern).toBe( "/from/{cluster}/{account}/{keyType}" ) @@ -795,7 +783,10 @@ describe("mergeSignatureProviderSSM", () => { describe("raw command-line reads", () => { it("readCommandLineFlag reads both `--flag value` and `--flag=value`", () => { expect( - readCommandLineFlag(["create", "--cluster-path", "/tmp/a"], ClusterPathFlag) + readCommandLineFlag( + ["create", "--cluster-path", "/tmp/a"], + ClusterPathFlag + ) ).toBe("/tmp/a") expect( readCommandLineFlag(["create", "--cluster-path=/tmp/b"], ClusterPathFlag) @@ -803,18 +794,26 @@ describe("raw command-line reads", () => { }) it("readCommandLineFlag returns null when the flag is absent", () => { - expect(readCommandLineFlag(["create", "-d", "/tmp/a"], ClusterPathFlag)).toBeNull() + expect( + readCommandLineFlag(["create", "-d", "/tmp/a"], ClusterPathFlag) + ).toBeNull() }) it("hasCommandLineFlag sees the long form, the `=` form, and the short alias", () => { - expect(hasCommandLineFlag(["--cluster-path", "/x"], ClusterPathFlag)).toBe(true) - expect(hasCommandLineFlag(["--cluster-path=/x"], ClusterPathFlag)).toBe(true) + expect(hasCommandLineFlag(["--cluster-path", "/x"], ClusterPathFlag)).toBe( + true + ) + expect(hasCommandLineFlag(["--cluster-path=/x"], ClusterPathFlag)).toBe( + true + ) expect(hasCommandLineFlag(["-d", "/x"], ClusterPathFlag)).toBe(true) expect(hasCommandLineFlag(["-d=/x"], ClusterPathFlag)).toBe(true) }) it("hasCommandLineFlag is false for a flag that only arrives as a yargs default", () => { - expect(hasCommandLineFlag(["create", "--force"], ClusterPathFlag)).toBe(false) + expect(hasCommandLineFlag(["create", "--force"], ClusterPathFlag)).toBe( + false + ) }) }) @@ -852,7 +851,10 @@ describe("--cluster-build-options-file", () => { clusterPath: "/tmp/doc-cluster", epochDurationSec: 30, bindAll: true, - bind: { kiod: { port: 1234 }, nodeop: { ports: { bios: { http: 5555 } } } }, + bind: { + kiod: { port: 1234 }, + nodeop: { ports: { bios: { http: 5555 } } } + }, logging: { levels: { console: "debug" } } }) ) @@ -868,7 +870,9 @@ describe("--cluster-build-options-file", () => { const loaded = loadClusterBuildOptionsFile( writeDocument({ nodeCount: 2, - bind: { nodeop: { ports: { producers: [{ http: 7000 }, { http: 7001 }] } } } + bind: { + nodeop: { ports: { producers: [{ http: 7000 }, { http: 7001 }] } } + } }) ) expect(loaded.bind?.nodeop?.ports?.producers?.[1]?.http).toBe(7001) @@ -934,12 +938,16 @@ describe("--cluster-build-options-file", () => { }) ) expect(loaded.signatureProvider?.type).toBe(SignatureProviderType.SSM) - expect(loaded.signatureProvider?.ssm?.awsSecretIdPattern).toBe(SecretIdPattern) + expect(loaded.signatureProvider?.ssm?.awsSecretIdPattern).toBe( + SecretIdPattern + ) }) it("rejects an unknown option, naming its dotted path", () => { expect(() => - loadClusterBuildOptionsFile(writeDocument({ bind: { kiod: { prot: 1 } } })) + loadClusterBuildOptionsFile( + writeDocument({ bind: { kiod: { prot: 1 } } }) + ) ).toThrow(/unknown option "bind\.kiod\.prot"/) expect(() => loadClusterBuildOptionsFile(writeDocument({ nope: 1 })) @@ -982,7 +990,9 @@ describe("--cluster-build-options-file", () => { it("rejects awsClusterNodeConfig, pointing at its own flag", () => { expect(() => loadClusterBuildOptionsFile( - writeDocument({ awsClusterNodeConfig: { account: "dev", regions: ["us-east-1"] } }) + writeDocument({ + awsClusterNodeConfig: { account: "dev", regions: ["us-east-1"] } + }) ) ).toThrow(new RegExp(`--${AWSClusterNodeConfigFlag}`)) }) @@ -1065,6 +1075,8 @@ describe("--aws-cluster-node-config", () => { mergeAWSClusterNodeConfig({}, { [AWSClusterNodeConfigFlag]: file }) .awsClusterNodeConfig?.account ).toBe(AWSAccountName.test) - expect(mergeAWSClusterNodeConfig({}, {}).awsClusterNodeConfig).toBeUndefined() + expect( + mergeAWSClusterNodeConfig({}, {}).awsClusterNodeConfig + ).toBeUndefined() }) }) diff --git a/packages/cluster-tool/tests/cli/CreateCommand.test.ts b/packages/cluster-tool/tests/cli/CreateCommand.test.ts index 79c35f3ec..c535740a9 100644 --- a/packages/cluster-tool/tests/cli/CreateCommand.test.ts +++ b/packages/cluster-tool/tests/cli/CreateCommand.test.ts @@ -2,7 +2,10 @@ import Fs from "node:fs" import Os from "node:os" import Path from "node:path" import type { Argv } from "yargs" -import { AWSAccountName, SignatureProviderType } from "@wireio/cluster-tool-shared" +import { + AWSAccountName, + SignatureProviderType +} from "@wireio/cluster-tool-shared" const createMock = jest.fn() @@ -111,8 +114,8 @@ describe("createCreateCommand", () => { expect(options.has("cluster-path")).toBe(true) expect(options.has("build-path")).toBe(true) expect(options.has("epoch-duration-sec")).toBe(true) - expect(options.has("enable-mock-reserves")).toBe(true) - expect(options.has("enable-launch-withheld-operations")).toBe(true) + expect(options.has("enable-mock-reserves")).toBe(false) + expect(options.has("enable-launch-withheld-operations")).toBe(false) expect(options.has("api-count")).toBe(true) expect(options.has(toQueryEngineFlag("readMode"))).toBe(true) // out-of-shape flags — registered so `.strict()` accepts them + `--help` lists them @@ -127,7 +130,9 @@ describe("createCreateCommand", () => { batchOperatorCount: 9 }) const commandLine = ["create", `--${ClusterBuildOptionsFileFlag}`, file] - expect(registeredDefault(commandLine, "cluster-path")).toBe("/tmp/from-file") + expect(registeredDefault(commandLine, "cluster-path")).toBe( + "/tmp/from-file" + ) expect(registeredDefault(commandLine, "epoch-duration-sec")).toBe(42) expect(registeredDefault(commandLine, "batch-operator-count")).toBe(9) }) @@ -158,7 +163,10 @@ describe("createCreateCommand", () => { it("keeps the loaded document per-command — a second command sees none of it", () => { const file = writeOptionsFile({ epochDurationSec: 42 }) expect( - registeredDefault(["create", `--${ClusterBuildOptionsFileFlag}`, file], "epoch-duration-sec") + registeredDefault( + ["create", `--${ClusterBuildOptionsFileFlag}`, file], + "epoch-duration-sec" + ) ).toBe(42) // a fresh command built from a bare command line falls back to the CLI default expect(registeredDefault(["create"], "epoch-duration-sec")).toBe(60) diff --git a/packages/cluster-tool/tests/clients/wire/WireClient.test.ts b/packages/cluster-tool/tests/clients/wire/WireClient.test.ts index 1db8f27bc..f516cef7a 100644 --- a/packages/cluster-tool/tests/clients/wire/WireClient.test.ts +++ b/packages/cluster-tool/tests/clients/wire/WireClient.test.ts @@ -187,36 +187,39 @@ describe("WireClient", () => { .spyOn(client, "getTableRows") .mockImplementation(async (query: any) => { captured.push(query) - return { rows: [{ account: "wirercpt", balance: "1234" }], more: false } as never + return { + rows: [{ account_name: "wirercpt", balance: "1234" }], + more: false + } as never }) it("sends a lower bound and NO upper bound", async () => { const client = new WireClient(config), captured: any[] = [] rowsFor(client, captured) - await client.getWireClaimable("wirercpt") + await client.getPayClaimable("wirercpt") const [query] = captured - expect(query.lowerBound).toBe(WireClient.nameKeyBound("account", "wirercpt")) + expect(query.lowerBound).toBe( + WireClient.nameKeyBound("account_name", "wirercpt") + ) expect(query.upperBound).toBeUndefined() }) it("returns the balance when the row belongs to the account", async () => { const client = new WireClient(config) rowsFor(client, []) - expect(await client.getWireClaimable("wirercpt")).toBe(1234n) + expect(await client.getPayClaimable("wirercpt")).toBe(1234n) }) it("returns 0n when the walk lands on the NEXT account's row", async () => { // lower_bound returns the first row at-or-after the key, so an account with no row reads // back a stranger's. Without the identity check this reported someone else's balance. const client = new WireClient(config) - jest - .spyOn(client, "getTableRows") - .mockResolvedValue({ - rows: [{ account: "wireother", balance: "999" }], - more: false - } as never) - expect(await client.getWireClaimable("wirercpt")).toBe(0n) + jest.spyOn(client, "getTableRows").mockResolvedValue({ + rows: [{ account_name: "wireother", balance: "999" }], + more: false + } as never) + expect(await client.getPayClaimable("wirercpt")).toBe(0n) }) it("returns 0n when the table has no rows at all", async () => { @@ -224,19 +227,21 @@ describe("WireClient", () => { jest .spyOn(client, "getTableRows") .mockResolvedValue({ rows: [], more: false } as never) - expect(await client.getWireClaimable("wirercpt")).toBe(0n) + expect(await client.getPayClaimable("wirercpt")).toBe(0n) }) it("reads payclaims through its own key + row field names", async () => { const client = new WireClient(config), captured: any[] = [] - jest.spyOn(client, "getTableRows").mockImplementation(async (query: any) => { - captured.push(query) - return { - rows: [{ account_name: "wirercpt", balance: "77" }], - more: false - } as never - }) + jest + .spyOn(client, "getTableRows") + .mockImplementation(async (query: any) => { + captured.push(query) + return { + rows: [{ account_name: "wirercpt", balance: "77" }], + more: false + } as never + }) expect(await client.getPayClaimable("wirercpt")).toBe(77n) expect(captured[0].table).toBe("payclaims") expect(captured[0].lowerBound).toBe( @@ -261,7 +266,9 @@ describe("WireClient", () => { it("carries the name's raw uint64 as a decimal string", () => { // key_types is ["uint64"], and a name's raw value exceeds Number.MAX_SAFE_INTEGER, so it // must not ride as a JSON number. - const { account } = JSON.parse(WireClient.nameKeyBound("account", "wirercpt")) + const { account } = JSON.parse( + WireClient.nameKeyBound("account", "wirercpt") + ) expect(typeof account).toBe("string") expect(account).toMatch(/^[0-9]+$/) expect(BigInt(account)).toBeGreaterThan(BigInt(Number.MAX_SAFE_INTEGER)) @@ -269,9 +276,11 @@ describe("WireClient", () => { it("honours the per-table key field name", () => { // wireclaims keys on `account`; payclaims keys on `account_name`. One helper, two shapes. - expect(Object.keys(JSON.parse(WireClient.nameKeyBound("account_name", "wirercpt")))).toEqual([ - "account_name" - ]) + expect( + Object.keys( + JSON.parse(WireClient.nameKeyBound("account_name", "wirercpt")) + ) + ).toEqual(["account_name"]) }) it("round-trips distinct accounts to distinct bounds", () => { @@ -287,7 +296,10 @@ describe("WireClient", () => { // bare code string fails at parse time (`Unexpected char '76' in "LIQSOL"`) exactly like a // bare account name does — which is what took flow-liq-yield's first yield read down. it("spans exactly [code, code + 1) as JSON objects keyed by the ABI key field", () => { - const { lowerBound, upperBound } = WireClient.symbolCodeKeyRange("symbol_code", "LIQSOL"), + const { lowerBound, upperBound } = WireClient.symbolCodeKeyRange( + "symbol_code", + "LIQSOL" + ), lower = JSON.parse(lowerBound), upper = JSON.parse(upperBound) expect(Object.keys(lower)).toEqual(["symbol_code"]) @@ -297,15 +309,21 @@ describe("WireClient", () => { }) it("carries the code's raw uint64, so two codes' ranges never overlap", () => { - const at = (bound: string): bigint => BigInt(JSON.parse(bound).symbol_code), + const at = (bound: string): bigint => + BigInt(JSON.parse(bound).symbol_code), sol = WireClient.symbolCodeKeyRange("symbol_code", "LIQSOL"), eth = WireClient.symbolCodeKeyRange("symbol_code", "LIQETH") expect(at(sol.lowerBound)).not.toBe(at(eth.lowerBound)) - expect(at(sol.lowerBound) >= at(eth.upperBound) || at(sol.upperBound) <= at(eth.lowerBound)).toBe(true) + expect( + at(sol.lowerBound) >= at(eth.upperBound) || + at(sol.upperBound) <= at(eth.lowerBound) + ).toBe(true) }) it("refuses a code that is not a symbol code", () => { - expect(() => WireClient.symbolCodeKeyRange("symbol_code", "liqsol")).toThrow() + expect(() => + WireClient.symbolCodeKeyRange("symbol_code", "liqsol") + ).toThrow() }) }) @@ -424,16 +442,18 @@ describe("WireClient", () => { describe("getProducerSchedule", () => { it("projects sdk-core's Name-typed entries onto plain producer names, in slot order", async () => { const client = new WireClient(config) - jest.spyOn(client.api.v1.chain, "get_producer_schedule").mockResolvedValue({ - active: { - version: 3, - producers: [ - { producer_name: "defproducera" }, - { producer_name: "defproducerb" } - ] - }, - pending: { version: 4, producers: [{ producer_name: "flowprod" }] } - } as never) + jest + .spyOn(client.api.v1.chain, "get_producer_schedule") + .mockResolvedValue({ + active: { + version: 3, + producers: [ + { producer_name: "defproducera" }, + { producer_name: "defproducerb" } + ] + }, + pending: { version: 4, producers: [{ producer_name: "flowprod" }] } + } as never) const schedule = await client.getProducerSchedule() expect(schedule.active).toEqual({ version: 3, diff --git a/packages/cluster-tool/tests/cluster/ClusterState.test.ts b/packages/cluster-tool/tests/cluster/ClusterState.test.ts index 450d4474f..e9aaeb12b 100644 --- a/packages/cluster-tool/tests/cluster/ClusterState.test.ts +++ b/packages/cluster-tool/tests/cluster/ClusterState.test.ts @@ -105,15 +105,10 @@ describe("ClusterState", () => { expect(persistedRoleOf(NodeRole.producer)).toBe( ClusterStateNodeRole.producer ) - // The snapshot records a node's PROCESS kind, so BOTH operator kinds - // land on its single `operator` member — the persisted shape is - // deliberately unchanged by the depot-side role split. + // The snapshot records the batch daemon as an operator process. expect(persistedRoleOf(NodeRole.batch_operator)).toBe( ClusterStateNodeRole.operator ) - expect(persistedRoleOf(NodeRole.underwriter)).toBe( - ClusterStateNodeRole.operator - ) // An API node keeps its OWN persisted kind: it is neither a producer (no // keys, no `resumeProduction`) nor an operator (no daemon args). expect(persistedRoleOf(NodeRole.api)).toBe(ClusterStateNodeRole.api) @@ -229,7 +224,6 @@ describe("ClusterState", () => { ctx.keyStore.setOperator(materialized) expect(() => ClusterState.captureKeys(ctx)).toThrow(/has no account/) }) - }) describe("§5.6 — an SSM cluster persists key REFERENCES, never key material", () => { @@ -308,7 +302,9 @@ describe("ClusterState", () => { record => record.account === producer ) expect(entry.wire.awsSecretId).toBe(`/wire/test/${node.name}/K1`) - expect(entry.wireFinalizer.awsSecretId).toBe(`/wire/test/${node.name}/BLS`) + expect(entry.wireFinalizer.awsSecretId).toBe( + `/wire/test/${node.name}/BLS` + ) expect(entry.wire.privateKey).toBeUndefined() expect(entry.wireFinalizer.privateKey).toBeUndefined() }) @@ -320,7 +316,9 @@ describe("ClusterState", () => { ) // ExternalClusterConfigSteps.keyProviderFor + the genesis finalizer key // read these regardless of who holds the secret. - expect(keys.nodes[0].wireFinalizer.proofOfPossession).toBe("SIG_BLS_node0") + expect(keys.nodes[0].wireFinalizer.proofOfPossession).toBe( + "SIG_BLS_node0" + ) expect(operator?.ethereum?.address).toBe(BatchOperator.ethereum.address) expect(keys.nodes[0].wire.publicKey).toBe("PUB_K1_node0") }) diff --git a/packages/cluster-tool/tests/cluster/processes/NodeopProcess.test.ts b/packages/cluster-tool/tests/cluster/processes/NodeopProcess.test.ts index a867101f5..155e3f155 100644 --- a/packages/cluster-tool/tests/cluster/processes/NodeopProcess.test.ts +++ b/packages/cluster-tool/tests/cluster/processes/NodeopProcess.test.ts @@ -164,7 +164,9 @@ describe("NodeopProcess", () => { NodeopProcess.create(manager, { node: node("keyless", NodeRole.producer, ["sysio"]) }) - ).rejects.toThrow(/requires one producer OperatorAccount per hosted producer/) + ).rejects.toThrow( + /requires one producer OperatorAccount per hosted producer/ + ) }) // The BIOS shape, which an all-labels assertion rejected outright and took every e2e flow with @@ -240,7 +242,10 @@ describe("NodeopProcess", () => { } } const nodeop = await NodeopProcess.create(manager, { - node: node("two-producers", NodeRole.producer, ["defproducera", "defproducerb"]), + node: node("two-producers", NodeRole.producer, [ + "defproducera", + "defproducerb" + ]), operators: [producerOperator("defproducera"), second] }) expect(valuesOf(nodeop.args, "--producer-name")).toEqual([ @@ -259,11 +264,18 @@ describe("NodeopProcess", () => { // accounts' slots silently unproduced — the assert makes the positional assumption explicit. const other: OperatorAccount = { ...producerOperator("defproducerb"), - wire: { type: KeyType.K1, publicKey: "PUB_K1_other", privateKey: "PVT_K1_other" } + wire: { + type: KeyType.K1, + publicKey: "PUB_K1_other", + privateKey: "PVT_K1_other" + } } await expect( NodeopProcess.create(manager, { - node: node("split-keys", NodeRole.producer, ["defproducera", "defproducerb"]), + node: node("split-keys", NodeRole.producer, [ + "defproducera", + "defproducerb" + ]), operators: [producerOperator("defproducera"), other] }) ).rejects.toThrow(/share the node's block-signing K1/) @@ -1134,15 +1146,10 @@ describe("NodeopProcess", () => { const nodeop = await NodeopProcess.create(manager, { node: node("peered", NodeRole.batch_operator) }) - // 1 producer node + 3 batch ops + 1 underwriter + 1 API node + bios + + // 1 producer node + 3 batch ops + 1 API node + bios + // ad-hoc headroom const allowance = - 1 + - 3 + - 1 + - 1 + - NodeConfig.BiosNodeCount + - NodeConfig.AdHocDaemonPeerHeadroom + 1 + 3 + 1 + NodeConfig.BiosNodeCount + NodeConfig.AdHocDaemonPeerHeadroom expect(nodeop.args).toEqual( expect.arrayContaining(["--p2p-max-nodes-per-host", String(allowance)]) ) @@ -1154,15 +1161,10 @@ describe("NodeopProcess", () => { const nodeop = await NodeopProcess.create(manager, { node: node("meshed", NodeRole.batch_operator) }) - // 1 producer node + 3 batch ops + 1 underwriter + 1 API node + bios + + // 1 producer node + 3 batch ops + 1 API node + bios + // ad-hoc headroom const allowance = - 1 + - 3 + - 1 + - 1 + - NodeConfig.BiosNodeCount + - NodeConfig.AdHocDaemonPeerHeadroom + 1 + 3 + 1 + NodeConfig.BiosNodeCount + NodeConfig.AdHocDaemonPeerHeadroom expect(nodeop.args).toEqual( expect.arrayContaining(["--max-clients", String(allowance)]) ) diff --git a/packages/cluster-tool/tests/config/ClusterConfigProvider.test.ts b/packages/cluster-tool/tests/config/ClusterConfigProvider.test.ts index 642a24ece..fa99686ab 100644 --- a/packages/cluster-tool/tests/config/ClusterConfigProvider.test.ts +++ b/packages/cluster-tool/tests/config/ClusterConfigProvider.test.ts @@ -128,11 +128,14 @@ describe("ClusterConfigProvider", () => { describe("toSecretId", () => { it("renders the canonical 4-segment {cluster}/{account}/{keyType} pattern", () => { expect( - ClusterConfigProvider.toSecretId("/wire/{cluster}/{account}/{keyType}", { - cluster: AWSAccountName.test, - account: "batchop.a", - keyType: "K1" - }) + ClusterConfigProvider.toSecretId( + "/wire/{cluster}/{account}/{keyType}", + { + cluster: AWSAccountName.test, + account: "batchop.a", + keyType: "K1" + } + ) ).toBe("/wire/test/batchop.a/K1") }) @@ -263,9 +266,8 @@ describe("ClusterConfigProvider", () => { describe("signatureProviderSource", () => { it("KEY → the inline default source for every key (byte-identical)", () => { - const source = ClusterConfigProvider.signatureProviderSource( - fixtureConfig() - ) + const source = + ClusterConfigProvider.signatureProviderSource(fixtureConfig()) expect(source("node_00", KeyType.K1)).toEqual({ type: SignatureProviderType.KEY }) @@ -308,7 +310,8 @@ describe("ClusterConfigProvider", () => { type: SignatureProviderType.SSM, ssm: { awsRegions: ["us-east-1"], - awsSecretIdPattern: "/wire/{cluster}/{account}/{keyType}/{version}", + awsSecretIdPattern: + "/wire/{cluster}/{account}/{keyType}/{version}", version: "v7" } }, @@ -684,9 +687,8 @@ describe("ClusterConfigProvider", () => { }) it("merges a PARTIAL bind config over resolver defaults (file pins the kiod port)", async () => { - const kiodPort = await BindConfigProvider.findAvailable( - PartialMergeKiodPin - ) + const kiodPort = + await BindConfigProvider.findAvailable(PartialMergeKiodPin) // findAvailable LOCKS the port in get-port's in-process cache. Release // the locks so resolve's PINNED draw can re-acquire the very port the // registry just vetted — without this the pin fails deterministically. @@ -720,7 +722,10 @@ describe("ClusterConfigProvider", () => { it("accepts an explicit flag when the document does not author clusterPath", () => { expect(() => - ClusterConfigProvider.assertClusterPathSource({ epochDurationSec: 60 }, true) + ClusterConfigProvider.assertClusterPathSource( + { epochDurationSec: 60 }, + true + ) ).not.toThrow() expect(() => ClusterConfigProvider.assertClusterPathSource(null, true) @@ -769,14 +774,10 @@ describe("ClusterConfigProvider", () => { ).rejects.toThrow(/underwriterCount was set to 3/) }) - it("rejects an OMITTED underwriterCount — the default is ONE underwriter, not zero", async () => { - await expect( - ClusterConfigProvider.resolve(externalOptions()) - ).rejects.toThrow( - new RegExp( - `underwriterCount was omitted, which defaults to ${ClusterConfigProvider.DefaultUnderwriterCount}` - ) - ) + it("defaults to no retired swap-underwriter daemons", async () => { + const config = await ClusterConfigProvider.resolve(externalOptions()) + expect(config.underwriterCount).toBe(0) + expect(config.bind.nodeop.ports.underwriters).toEqual([]) }) it("accepts an EXPLICIT underwriterCount of 0", async () => { @@ -787,7 +788,7 @@ describe("ClusterConfigProvider", () => { expect(config.externalOutposts).not.toBeNull() }) - it("leaves LOCAL mode's underwriters untouched", async () => { + it("defaults local mode to zero retired daemons", async () => { const config = await ClusterConfigProvider.resolve({ clusterPath: Path.join(environment.rootPath, "local-cluster"), buildPath: environment.buildPath, diff --git a/packages/cluster-tool/tests/config/NodeConfig.test.ts b/packages/cluster-tool/tests/config/NodeConfig.test.ts index 6051b415a..bdb0638c9 100644 --- a/packages/cluster-tool/tests/config/NodeConfig.test.ts +++ b/packages/cluster-tool/tests/config/NodeConfig.test.ts @@ -65,9 +65,7 @@ describe("NodeConfig", () => { NodeRole.underwriter ]) expect( - Object.values(NodeRole).filter(role => - NodeConfig.isOperatorRole(role) - ) + Object.values(NodeRole).filter(role => NodeConfig.isOperatorRole(role)) ).toEqual([...NodeConfig.OperatorRoles]) }) }) @@ -81,14 +79,13 @@ describe("NodeConfig", () => { // Driven off `NodeRole` so a new role is covered by construction. LOCAL is // the regression pin: the harness's WireClient reads traces off producer[0], // so a role-blind tightening here breaks every flow. - it.each(Object.values(NodeRole))( - "keeps it on a LOCAL cluster's %s node", - role => { - expect(NodeConfig.runsTraceApiPlugin(nodeOfRole(localNodes, role))).toBe( - true - ) - } - ) + it.each( + Object.values(NodeRole).filter(role => role !== NodeRole.underwriter) + )("keeps it on a LOCAL cluster's %s node", role => { + expect(NodeConfig.runsTraceApiPlugin(nodeOfRole(localNodes, role))).toBe( + true + ) + }) it.each([NodeRole.bios, NodeRole.producer])( "drops it from the production-shaped EXTERNAL tree's %s node", @@ -99,7 +96,7 @@ describe("NodeConfig", () => { } ) - it.each([NodeRole.batch_operator, NodeRole.underwriter, NodeRole.api])( + it.each([NodeRole.batch_operator, NodeRole.api])( "keeps it on an EXTERNAL cluster's %s node (operators are non-public; API nodes are the chain-read surface)", role => { expect( @@ -121,7 +118,9 @@ describe("NodeConfig", () => { fixtureConfig({ deploymentKind: ClusterDeploymentKind.external }) ) - it.each(Object.values(NodeRole))("answers for a %s node", role => { + it.each( + Object.values(NodeRole).filter(role => role !== NodeRole.underwriter) + )("answers for a %s node", role => { const node = nodeOfRole(local, role) expect(NodeConfig.runsQueryEnginePlugin(node)).toBe(role === NodeRole.api) expect(NodeConfig.runsProducerApiPlugin(node)).toBe(role !== NodeRole.api) @@ -129,14 +128,13 @@ describe("NodeConfig", () => { // Unlike the trace-api gate, neither predicate reads the deployment kind: // an EXTERNAL tree answers exactly as a local one. - it.each(Object.values(NodeRole))( - "answers the same for an EXTERNAL cluster's %s node", - role => { - const node = nodeOfRole(external, role) - expect(NodeConfig.runsQueryEnginePlugin(node)).toBe(role === NodeRole.api) - expect(NodeConfig.runsProducerApiPlugin(node)).toBe(role !== NodeRole.api) - } - ) + it.each( + Object.values(NodeRole).filter(role => role !== NodeRole.underwriter) + )("answers the same for an EXTERNAL cluster's %s node", role => { + const node = nodeOfRole(external, role) + expect(NodeConfig.runsQueryEnginePlugin(node)).toBe(role === NodeRole.api) + expect(NodeConfig.runsProducerApiPlugin(node)).toBe(role !== NodeRole.api) + }) }) describe("producerName", () => { @@ -234,17 +232,15 @@ describe("NodeConfig", () => { const nodes = NodeConfig.plan(fixtureConfig()) it("plans bios + producer + operator + api nodes from the bind topology", () => { - expect(nodes).toHaveLength(7) // 1 bios + 1 producer + 3 batch + 1 underwriter + 1 api + expect(nodes).toHaveLength(6) // 1 bios + 1 producer + 3 batch + 1 api expect(nodes[0].role).toBe(NodeRole.bios) expect(nodes[0].name).toBe(NodeConfig.BiosName) const operators = nodes.filter(n => NodeConfig.isOperatorRole(n.role)) - expect(operators).toHaveLength(4) - expect( - operators.filter(n => n.batchOperatorLabel !== null) - ).toHaveLength(3) - expect(operators.filter(n => n.underwriterLabel !== null)).toHaveLength( - 1 + expect(operators).toHaveLength(3) + expect(operators.filter(n => n.batchOperatorLabel !== null)).toHaveLength( + 3 ) + expect(operators.filter(n => n.underwriterLabel !== null)).toHaveLength(0) }) it("gives each operator node its EXPLICIT kind, alongside its durable label", () => { @@ -255,7 +251,7 @@ describe("NodeConfig", () => { ), underwriters = nodes.filter(n => n.role === NodeRole.underwriter) expect(batchOperators).toHaveLength(3) - expect(underwriters).toHaveLength(1) + expect(underwriters).toHaveLength(0) batchOperators.forEach(n => { expect(n.batchOperatorLabel).not.toBeNull() expect(n.underwriterLabel).toBeNull() @@ -268,9 +264,7 @@ describe("NodeConfig", () => { it("meshes the non-operator set — bios + producers + api nodes peer with each other", () => { const mesh = nodes.filter(n => !NodeConfig.isOperatorRole(n.role)) - mesh.forEach(n => - expect(n.peerEndpoints).toHaveLength(mesh.length - 1) - ) + mesh.forEach(n => expect(n.peerEndpoints).toHaveLength(mesh.length - 1)) }) it("attaches each operator to exactly ONE producer, not the mesh", () => { @@ -313,15 +307,15 @@ describe("NodeConfig", () => { expect(batchOps).toContain("batchop.a") expect( nodes.find(n => n.underwriterLabel !== null)?.underwriterLabel - ).toBe("uwrit.a") + ).toBeUndefined() }) describe("api nodes", () => { - it("appends api nodes AFTER the underwriters, continuing the node_NN numbering", () => { + it("appends api nodes AFTER the batch operators, continuing the node_NN numbering", () => { const api = nodes.filter(n => n.role === NodeRole.api) expect(api).toHaveLength(1) - expect(api[0].name).toBe("node_05") // bios, node_00 producer, 01-03 batch, 04 underwriter - expect(api[0].index).toBe(5) + expect(api[0].name).toBe("node_04") // bios, node_00 producer, 01-03 batch + expect(api[0].index).toBe(4) expect(api[0].producers).toEqual([]) expect(api[0].batchOperatorLabel).toBeNull() expect(api[0].underwriterLabel).toBeNull() @@ -506,13 +500,14 @@ describe("NodeConfig", () => { // deadline kv here would resurrect on the post-bootstrap form the very // value its argv deliberately omits (a CLI flag only wins when emitted). // Every role, because the ini is role-blind for these keys. - it.each(["max-transaction-time", "abi-serializer-max-time-ms", "http-max-response-time-ms"])( - "renders NO `%s` kv on any role's ini", - key => { - expect(nodes.length).toBeGreaterThan(0) - nodes.forEach(node => expect(node.ini.render()).not.toContain(key)) - } - ) + it.each([ + "max-transaction-time", + "abi-serializer-max-time-ms", + "http-max-response-time-ms" + ])("renders NO `%s` kv on any role's ini", key => { + expect(nodes.length).toBeGreaterThan(0) + nodes.forEach(node => expect(node.ini.render()).not.toContain(key)) + }) // SHARED-25 AC#4 (D3): the ini plugin LINE follows the same gate the argv // does, so a published tree cannot load the plugin through `--config-dir` @@ -672,14 +667,15 @@ describe("NodeConfig", () => { }) }) - it.each(Object.values(NodeRole).filter(role => role !== NodeRole.api))( - "keeps the query engine plugin OFF a %s node's ini", - role => { - expect(nodeOfRole(nodes, role).ini.render()).not.toContain( - Constants.QUERY_ENGINE_PLUGIN - ) - } - ) + it.each( + Object.values(NodeRole).filter( + role => role !== NodeRole.api && role !== NodeRole.underwriter + ) + )("keeps the query engine plugin OFF a %s node's ini", role => { + expect(nodeOfRole(nodes, role).ini.render()).not.toContain( + Constants.QUERY_ENGINE_PLUGIN + ) + }) it("keeps read-mode explicit on operators (irreversible) and absent on bios / producer / api nodes", () => { const operators = nodes.filter(n => NodeConfig.isOperatorRole(n.role)) @@ -739,9 +735,7 @@ describe("NodeConfig", () => { const { loggers } = JSON.parse(node.logging.render()) expect(loggers.length).toBeGreaterThan(0) loggers.forEach((logger: RenderedLogger) => - expect(logger.level).toBe( - NodeConfigLoggingRenderer.NodeopLogLevel.warn - ) + expect(logger.level).toBe(NodeConfigLoggingRenderer.NodeopLogLevel.warn) ) }) @@ -917,14 +911,20 @@ describe("NodeConfig — ad-hoc nodes", () => { describe("createAdHoc", () => { it("composes a PRODUCING node for a producer: its one account, the producer role, the producer mesh", () => { - const producer = fixtureOperatorAccount("flowprod", OperatorType.PRODUCER, "flowprod"), + const producer = fixtureOperatorAccount( + "flowprod", + OperatorType.PRODUCER, + "flowprod" + ), node = NodeConfig.createAdHoc(cluster, producer, ports) expect(node.role).toBe(NodeRole.producer) expect(node.producers).toEqual(["flowprod"]) expect(node.name).toBe(NodeConfig.adHocNodeName("flowprod")) expect(node.index).toBe(NodeConfig.AdHocIndex) expect(node.ports).toBe(ports) - expect(node.peerEndpoints).toEqual(NodeConfig.producerPeerEndpoints(cluster)) + expect(node.peerEndpoints).toEqual( + NodeConfig.producerPeerEndpoints(cluster) + ) expect(node.batchOperatorLabel).toBeNull() expect(node.underwriterLabel).toBeNull() expect(node.cluster).toBe(cluster) @@ -939,7 +939,10 @@ describe("NodeConfig — ad-hoc nodes", () => { // would read as a label that resolves to nothing. Producing for the ON-CHAIN account is // still guaranteed -- `NodeopProcess.buildArgs` renders `--producer-name` from the // OperatorAccount, which is asserted in NodeopProcess.test.ts. - const producer = fixtureOperatorAccount("flowprod", OperatorType.PRODUCER), + const producer = fixtureOperatorAccount( + "flowprod", + OperatorType.PRODUCER + ), node = NodeConfig.createAdHoc(cluster, producer, ports) expect(node.producers).toEqual([producer.label]) expect(node.producers).not.toEqual([producer.account]) @@ -954,11 +957,16 @@ describe("NodeConfig — ad-hoc nodes", () => { expect(node.batchOperatorLabel).toBe("batchop.x") expect(node.underwriterLabel).toBeNull() expect(node.name).toBe(NodeConfig.adHocNodeName("batchop.x")) - expect(node.peerEndpoints).toEqual(NodeConfig.producerPeerEndpoints(cluster)) + expect(node.peerEndpoints).toEqual( + NodeConfig.producerPeerEndpoints(cluster) + ) }) it("composes a non-producing UNDERWRITER node carrying the operator's label", () => { - const operator = fixtureOperatorAccount("uwrit.x", OperatorType.UNDERWRITER), + const operator = fixtureOperatorAccount( + "uwrit.x", + OperatorType.UNDERWRITER + ), node = NodeConfig.createAdHoc(cluster, operator, ports) expect(node.role).toBe(NodeRole.underwriter) expect(node.producers).toEqual([]) diff --git a/packages/cluster-tool/tests/config/clusterConfigFixture.ts b/packages/cluster-tool/tests/config/clusterConfigFixture.ts index 87c99063b..4afa47e43 100644 --- a/packages/cluster-tool/tests/config/clusterConfigFixture.ts +++ b/packages/cluster-tool/tests/config/clusterConfigFixture.ts @@ -38,7 +38,7 @@ export const PersistedFixture: ClusterConfig = { producerCount: 21, nodeCount: 1, batchOperatorCount: 3, - underwriterCount: 1, + underwriterCount: 0, apiCount: 1, epochDurationSec: 60, operatorsPerEpoch: null, @@ -62,7 +62,7 @@ export const PersistedFixture: ClusterConfig = { }, producers: [pair(0)], batch: [pair(1), pair(2), pair(3)], - underwriters: [pair(4)], + underwriters: [], // One API node, so every role-driven test (`nodeOfRole`, `it.each(Object.values(NodeRole))`) // finds a planned node of the api role. api: [pair(7)], @@ -118,8 +118,6 @@ export const PersistedFixture: ClusterConfig = { awsClusterNodeConfig: null, externalOutposts: null, debuggingServerEnabled: true, - enableMockReserves: false, - enableLaunchWithheldOperations: false, enableMockLiqPools: false, enableMockSyndicationImport: false, deploymentKind: ClusterDeploymentKind.local, diff --git a/packages/cluster-tool/tests/flow/contexts/SwapScenarioContext.test.ts b/packages/cluster-tool/tests/flow/contexts/SwapScenarioContext.test.ts deleted file mode 100644 index 5bb82ea53..000000000 --- a/packages/cluster-tool/tests/flow/contexts/SwapScenarioContext.test.ts +++ /dev/null @@ -1,260 +0,0 @@ -import { SlugName, SysioContracts } from "@wireio/sdk-core" -import { SwapScenarioContext } from "@wireio/cluster-tool/flow" -import { getLogger } from "@wireio/cluster-tool/logging" -import { WireReserveTool } from "@wireio/cluster-tool/tools/wire" -import type { WireClient } from "@wireio/cluster-tool/clients/wire" -import { fixtureConfig } from "../../config/clusterConfigFixture.js" - -const { - SysioContractName, - SysioReservChainkind, - SysioReservReservestatus, - SysioUwritAttestationtype, - SysioUwritUnderwriterequeststatus -} = SysioContracts - -/** Code cells as the depot's ABI renders them: `slug_name` is a builtin, so a code is its canonical spelling. */ -const EthereumChainSlug = "ETHEREUM", - SolanaChainSlug = "SOLANA", - EthTokenSlug = "ETH", - SolTokenSlug = "SOL", - PrimaryReserveSlug = "PRIMARY" -/** The same codes packed, as the context's lookups take them. */ -const EthereumChain = SlugName.from(EthereumChainSlug), - SolanaChain = SlugName.from(SolanaChainSlug), - EthToken = SlugName.from(EthTokenSlug), - SolToken = SlugName.from(SolTokenSlug), - PrimaryReserve = SlugName.from(PrimaryReserveSlug) -const { SymmetricConnectorWeightBps } = WireReserveTool - -/** A complete `reserves` row with zero defaults; override the fields under test. */ -function reserveRow( - overrides: Partial -): SysioContracts.SysioReservReserveRowType { - return { - chain_code: "", - token_code: "", - reserve_code: "", - name: "", - description: "", - status: SysioReservReservestatus.RESERVE_STATUS_ACTIVE, - reserve_chain_amount: 0, - reserve_wire_amount: 0, - source_token_precision: 0, - connector_weight_bps: SymmetricConnectorWeightBps, - creator_addr: { kind: SysioReservChainkind.CHAIN_KIND_UNKNOWN, address: "" }, - requested_wire_amount: 0, - external_token_amount: 0, - registered_at_ms: 0, - activated_at_ms: 0, - cancelled_at_ms: 0, - is_private: false, - owner: "", - creator_pub_key: "", - owner_fee_bps: 0, - owner_fee_accrued: 0, - owner_fee_lifetime: 0, - ...overrides - } -} - -/** A complete `uwreqs` row with zero defaults; override the fields under test. */ -function uwreqRow( - overrides: Partial -): SysioContracts.SysioUwritUwRequestTType { - return { - id: 0, - type: SysioUwritAttestationtype.ATTESTATION_TYPE_UNSPECIFIED, - status: SysioUwritUnderwriterequeststatus.UNDERWRITE_REQUEST_STATUS_PENDING, - src_chain_code: "", - src_token_code: "", - src_reserve_code: "", - src_amount: 0, - dst_chain_code: "", - dst_token_code: "", - dst_reserve_code: "", - dst_amount: 0, - target_amount: 0, - variance_tolerance_bps: 0, - source_tx_id: "", - depositor: "", - commits_by: [], - winner: "", - committed_at_ms: 0, - settled_at_ms: 0, - expires_at_epoch: 0, - attestation_inbound_data: "", - attestation_outbound_data: "", - challenge_id: 0, - ...overrides - } -} - -/** A complete `locks` row with zero defaults; override the fields under test. */ -function lockRow( - overrides: Partial -): SysioContracts.SysioUwritLockEntryType { - return { - lock_id: 0, - uwreq_id: 0, - underwriter: "", - chain_code: "", - token_code: "", - reserve_code: "", - amount: 0, - created_at_ms: 0, - expires_at_ms: 0, - challenge_id: 0, - ...overrides - } -} - -/** Table fixtures served by the stubbed typed accessors. */ -interface TableFixtures { - reserves: SysioContracts.SysioReservReserveRowType[] - uwreqs: SysioContracts.SysioUwritUwRequestTType[] - locks: SysioContracts.SysioUwritLockEntryType[] -} - -/** A context whose `wire.getSysioContract` serves the fixtures (reads only). */ -function newContext(fixtures: TableFixtures): SwapScenarioContext { - const context = new SwapScenarioContext(fixtureConfig(), getLogger("swap-ctx-test")) - const table = (rows: Row[]) => ({ - query: async () => ({ rows, more: false }) - }) - const clientByName = { - [SysioContractName.reserv]: { tables: { reserves: table(fixtures.reserves) } }, - [SysioContractName.uwrit]: { - tables: { uwreqs: table(fixtures.uwreqs), locks: table(fixtures.locks) } - } - } - jest - .spyOn(context, "wire", "get") - .mockReturnValue({ - getSysioContract: (name: SysioContracts.SysioContractName) => clientByName[name] - } as WireClient) - return context -} - -describe("SwapScenarioContext", () => { - const fixtures: TableFixtures = { - reserves: [ - reserveRow({ - chain_code: EthereumChainSlug, - token_code: EthTokenSlug, - reserve_code: PrimaryReserveSlug, - reserve_chain_amount: 1_000, - reserve_wire_amount: 2_000 - }), - reserveRow({ - chain_code: SolanaChainSlug, - token_code: SolTokenSlug, - reserve_code: PrimaryReserveSlug, - reserve_chain_amount: 3_000, - reserve_wire_amount: 4_000, - // An owned reserve that charges and has already earned. - owner: "privowner", - owner_fee_bps: 200, - owner_fee_accrued: 500, - owner_fee_lifetime: 900 - }) - ], - uwreqs: [ - uwreqRow({ - id: 7, - src_chain_code: EthereumChainSlug, - dst_chain_code: SolanaChainSlug - }) - ], - locks: [ - lockRow({ lock_id: 1, uwreq_id: 7, chain_code: EthereumChainSlug }), - lockRow({ lock_id: 2, uwreq_id: 7, chain_code: SolanaChainSlug }), - lockRow({ lock_id: 3, uwreq_id: 9 }) - ] - } - - describe("reserveBook", () => { - it("returns the matching reserve's (chain, wire) book as bigints", async () => { - const book = await newContext(fixtures).reserveBook( - EthereumChain, - EthToken, - PrimaryReserve - ) - expect(book).toEqual({ - chain: 1_000n, - wire: 2_000n, - connectorWeightBps: SymmetricConnectorWeightBps, - ownerFeeBps: 0 - }) - }) - it("carries a private reserve's non-zero owner fee onto the book", async () => { - // The book is what `quoteSwap` prices against, so an owner fee that never - // reaches it silently over-quotes the destination — the private-reserve - // regression. A public reserve's 0 makes that omission invisible. - const book = await newContext(fixtures).reserveBook( - SolanaChain, - SolToken, - PrimaryReserve - ) - expect(book.ownerFeeBps).toBe(200) - }) - it("throws when no reserve matches the triple", async () => { - await expect( - newContext(fixtures).reserveBook(999, EthToken, PrimaryReserve) - ).rejects.toThrow(/not found/) - }) - }) - - describe("reserveOwnerFee", () => { - it("returns the owner, rate, and earned amounts as bigints", async () => { - const fee = await newContext(fixtures).reserveOwnerFee( - SolanaChain, - SolToken, - PrimaryReserve - ) - expect(fee).toEqual({ - owner: "privowner", - feeBps: 200, - accrued: 500n, - lifetime: 900n - }) - // `lifetime` outliving `accrued` is the signature of a prior claim. - expect(fee.lifetime).toBeGreaterThan(fee.accrued) - }) - it("reports a fee-free, owner-less reserve as all zeroes", async () => { - const fee = await newContext(fixtures).reserveOwnerFee( - EthereumChain, - EthToken, - PrimaryReserve - ) - expect(fee).toEqual({ owner: "", feeBps: 0, accrued: 0n, lifetime: 0n }) - }) - it("throws when no reserve matches the triple", async () => { - await expect( - newContext(fixtures).reserveOwnerFee(999, EthToken, PrimaryReserve) - ).rejects.toThrow(/not found/) - }) - }) - - describe("uwreq", () => { - it("finds the request by its (source, destination) chain pair", async () => { - const request = await newContext(fixtures).uwreq(EthereumChain, SolanaChain) - expect(request?.id).toBe(7) - }) - it("is empty when the depot has not created the request", async () => { - expect( - await newContext(fixtures).uwreq(SolanaChain, EthereumChain) - ).toBeUndefined() - }) - }) - - describe("locksForUwreq", () => { - it("returns every lock referencing the request id", async () => { - const locks = await newContext(fixtures).locksForUwreq(7) - expect(locks.map(lock => lock.lock_id)).toEqual([1, 2]) - }) - it("is empty for an unknown request id", async () => { - expect(await newContext(fixtures).locksForUwreq(42)).toEqual([]) - }) - }) -}) diff --git a/packages/cluster-tool/tests/orchestration/ClusterBuildDefaultsMockLiqPools.test.ts b/packages/cluster-tool/tests/orchestration/ClusterBuildDefaultsMockLiqPools.test.ts index ee84c1179..c7caad732 100644 --- a/packages/cluster-tool/tests/orchestration/ClusterBuildDefaultsMockLiqPools.test.ts +++ b/packages/cluster-tool/tests/orchestration/ClusterBuildDefaultsMockLiqPools.test.ts @@ -30,10 +30,20 @@ describe("ClusterBuildDefaults — mock-liq-pool gating", () => { it("always configures the swap, opens the shadow symbols and sets the kicker, in that order", async () => { const cluster = await ClusterBuildDefaults.create(baseOptions()) const names = collectPhaseNames(cluster.children) - expect(names.indexOf("SwapConfig")).toBeGreaterThan(names.indexOf("Registry")) - expect(names.indexOf("ShadowLiqTokens")).toBe(names.indexOf("SwapConfig") + 1) - expect(names.indexOf("LiqConfig")).toBe(names.indexOf("ShadowLiqTokens") + 1) - expect(names.indexOf("UnderwriterConfig")).toBeGreaterThan(names.indexOf("LiqConfig")) + expect(names.indexOf("SwapConfig")).toBeGreaterThan( + names.indexOf("Registry") + ) + 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") + ) + expect(names).not.toContain("UnderwriterConfig") + expect(names).not.toContain("ReserveConfig") }) it("omits the MockLiqPools phase by default (no --enable-mock-liq-pools)", async () => { @@ -51,18 +61,11 @@ 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 - expect(names.indexOf("MockLiqPools")).toBe(names.indexOf("SyndicationConfig") + 1) - expect(names.indexOf("MockLiqPools")).toBeLessThan(names.indexOf("EpochBootstrap")) - }) - - it("keeps MockReserves directly after Registry when both mock surfaces are enabled", async () => { - const cluster = await ClusterBuildDefaults.create({ - ...baseOptions(), - enableMockReserves: true, - enableMockLiqPools: true - }) - const names = collectPhaseNames(cluster.children) - expect(names.indexOf("MockReserves")).toBe(names.indexOf("Registry") + 1) - expect(names.indexOf("SwapConfig")).toBe(names.indexOf("MockReserves") + 1) + expect(names.indexOf("MockLiqPools")).toBe( + names.indexOf("SyndicationConfig") + 1 + ) + expect(names.indexOf("MockLiqPools")).toBeLessThan( + names.indexOf("EpochBootstrap") + ) }) }) diff --git a/packages/cluster-tool/tests/orchestration/ClusterBuildDefaultsMockReserves.test.ts b/packages/cluster-tool/tests/orchestration/ClusterBuildDefaultsMockReserves.test.ts deleted file mode 100644 index d25d5213e..000000000 --- a/packages/cluster-tool/tests/orchestration/ClusterBuildDefaultsMockReserves.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import Path from "node:path" -import { ClusterBuildDefaults } from "@wireio/cluster-tool/orchestration" -import { - fixtureResolveEnvironment, - type ResolveEnvironment -} from "../config/resolveEnvironmentFixture.js" - -import { collectPhaseNames } from "./clusterBuildFixture.js" - -describe("ClusterBuildDefaults — mock-reserve gating", () => { - let environment: ResolveEnvironment - - beforeEach(() => { - environment = fixtureResolveEnvironment("mock-reserves-") - }) - - afterEach(() => { - environment.cleanup() - }) - - function baseOptions() { - return { - clusterPath: Path.join(environment.rootPath, "cluster"), - buildPath: environment.buildPath, - ethereumPath: "/fake/eth", - solanaPath: "/fake/sol" - } - } - - it("omits the MockReserves phase by default (no --enable-mock-reserves)", async () => { - const cluster = await ClusterBuildDefaults.create(baseOptions()) - const names = collectPhaseNames(cluster.children) - expect(names).toContain("Registry") - expect(names).not.toContain("MockReserves") - // the launch-policy flag resolves false by default too (asserted on THIS build: - // every extra `create` lengthens the host-global bind-registry lock queue) - expect(cluster.config.enableLaunchWithheldOperations).toBe(false) - }) - - it("adds MockReserves immediately after Registry when enableMockReserves is set", async () => { - const cluster = await ClusterBuildDefaults.create({ - ...baseOptions(), - enableMockReserves: true, - enableLaunchWithheldOperations: true - }) - const names = collectPhaseNames(cluster.children) - expect(names).toContain("MockReserves") - // …and an opt-in reaches the resolved config (the flag the swap/reserve flows set) - expect(cluster.config.enableLaunchWithheldOperations).toBe(true) - // gated phase is registered directly after the Registry phase, pre-EpochBootstrap - expect(names.indexOf("MockReserves")).toBe(names.indexOf("Registry") + 1) - }) -}) diff --git a/packages/cluster-tool/tests/orchestration/ClusterBuildDefaultsOperatorFunding.test.ts b/packages/cluster-tool/tests/orchestration/ClusterBuildDefaultsOperatorFunding.test.ts index 9c5d6ba67..cfe769209 100644 --- a/packages/cluster-tool/tests/orchestration/ClusterBuildDefaultsOperatorFunding.test.ts +++ b/packages/cluster-tool/tests/orchestration/ClusterBuildDefaultsOperatorFunding.test.ts @@ -109,13 +109,9 @@ describe("ClusterBuildDefaults — bootstrapped operator ETH fee funding", () => // funded, so an unfunded wallet cannot pay gas for its outbound deliveries // and the ETH outpost's envelope is never written (epoch stalls at 1). expect(funded.length).toBeGreaterThan(0) - expect(funded).toContain(`${Constants.batchOperatorLabel(0)}${EthereumFundingStepSuffix}`) - }) - - it("funds the underwriters too, not just the batch operators", async () => { - const cluster = await ClusterBuildDefaults.create(ssmOptions()) - const funded = fundingSteps(collectStepNames(cluster.children)) - expect(funded).toContain(`${Constants.underwriterLabel(0)}${EthereumFundingStepSuffix}`) + expect(funded).toContain( + `${Constants.batchOperatorLabel(0)}${EthereumFundingStepSuffix}` + ) }) it("composes NO ETH funding under the default KEY provider", async () => { @@ -131,7 +127,11 @@ describe("ClusterBuildDefaults — bootstrapped operator ETH fee funding", () => names.filter(name => name.endsWith("-airdrop-solana")) const keyCluster = await ClusterBuildDefaults.create(baseOptions()) const ssmCluster = await ClusterBuildDefaults.create(ssmOptions()) - expect(solanaAirdrops(collectStepNames(keyCluster.children)).length).toBeGreaterThan(0) - expect(solanaAirdrops(collectStepNames(ssmCluster.children)).length).toBeGreaterThan(0) + expect( + solanaAirdrops(collectStepNames(keyCluster.children)).length + ).toBeGreaterThan(0) + expect( + solanaAirdrops(collectStepNames(ssmCluster.children)).length + ).toBeGreaterThan(0) }) }) diff --git a/packages/cluster-tool/tests/orchestration/ClusterBuildDefaultsUnderwriterConfig.test.ts b/packages/cluster-tool/tests/orchestration/ClusterBuildDefaultsUnderwriterConfig.test.ts deleted file mode 100644 index 6cbd0c7a8..000000000 --- a/packages/cluster-tool/tests/orchestration/ClusterBuildDefaultsUnderwriterConfig.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import Path from "node:path" -import type { SysioContracts } from "@wireio/sdk-core" -import { - ClusterBuildPhase, - ClusterBuildPhaseGroup, - ClusterBuildDefaults, - type ClusterBuildContext, - type ClusterBuildPhaseBase, - type Steps -} from "@wireio/cluster-tool/orchestration" -import { - fixtureResolveEnvironment, - type ResolveEnvironment -} from "../config/resolveEnvironmentFixture.js" - -/** The bootstrap phase holding the single `uwrit::setconfig` step. */ -const UnderwriterConfigPhaseName = "UnderwriterConfig" -/** That phase's one step. */ -const ConfigureUwritStepName = "configure-uwrit" - -/** - * Depth-first search of the composed tree for the phase registered under - * `name`. A {@link ClusterBuildPhaseGroup} carries `children`; a - * {@link ClusterBuildPhase} is the leaf that carries `steps`. - */ -function findPhase( - children: ReadonlyArray>, - name: string -): ClusterBuildPhase { - return children.reduce>( - (found, child) => - found ?? - (child instanceof ClusterBuildPhase && child.name === name - ? child - : child instanceof ClusterBuildPhaseGroup - ? findPhase(child.children, name) - : undefined), - undefined - ) -} - -describe("ClusterBuildDefaults — sysio.uwrit launch config", () => { - let environment: ResolveEnvironment - - beforeEach(() => { - environment = fixtureResolveEnvironment("uwrit-config-") - }) - - afterEach(() => { - environment.cleanup() - }) - - /** - * The bootstrap's `uwrit::setconfig` payload. It is built inline inside - * `ClusterBuildDefaults.create`, so the composed tree is the only seam. - */ - async function underwriterConfigData(): Promise { - const cluster = await ClusterBuildDefaults.create({ - clusterPath: Path.join(environment.rootPath, "cluster"), - buildPath: environment.buildPath, - ethereumPath: "/fake/eth", - solanaPath: "/fake/sol" - }) - const phase = findPhase(cluster.children, UnderwriterConfigPhaseName) - expect(phase).toBeDefined() - const step = phase.steps.find(candidate => candidate.name === ConfigureUwritStepName) - expect(step).toBeDefined() - return (step.input as Steps.contracts.sysio.uwrit.SetconfigInput).data - } - - // The value that matters: a cluster must price from-WIRE revert churn the way - // the network will (the 5% launch value), not at the 0.1% placeholder the - // constant carried before it. Nothing else pins this — the contract's own - // `drainfwq` tests set their rate via `setconfig`, so a stale harness mirror - // is invisible to them AND to every flow (none drives a caller-fault - // drain-time revert). This assertion is the only guard against that drift. - it("seeds the 5% launch from-WIRE revert fee, mirroring the contract default", async () => { - expect((await underwriterConfigData()).fromwire_revert_fee_bps).toBe(500) - }) - - it("seeds the WIRE-leg network swap fee the fee-split flows assert against", async () => { - expect((await underwriterConfigData()).fee_bps).toBe(30) - }) -}) diff --git a/packages/cluster-tool/tests/orchestration/solana/SolanaOutpostBootstrapper.test.ts b/packages/cluster-tool/tests/orchestration/solana/SolanaOutpostBootstrapper.test.ts index 56f2ffa4b..1dd2eca35 100644 --- a/packages/cluster-tool/tests/orchestration/solana/SolanaOutpostBootstrapper.test.ts +++ b/packages/cluster-tool/tests/orchestration/solana/SolanaOutpostBootstrapper.test.ts @@ -1,10 +1,9 @@ -import Path from "node:path" import * as anchor from "@coral-xyz/anchor" -import { Connection, Keypair, PublicKey } from "@solana/web3.js" +import { PublicKey } from "@solana/web3.js" import { OperatorStatus, OperatorType } from "@wireio/opp-typescript-models" import { SolanaOutpostBootstrapper } from "@wireio/cluster-tool/orchestration" import { BindConfigProvider } from "@wireio/cluster-tool/config" -import { getLogger } from "@wireio/cluster-tool/logging" + import { toURL } from "@wireio/cluster-tool/utils" /** A minimal roster entry — the asserts under test never read its contents. */ @@ -16,25 +15,16 @@ const bootstrapOperator = (): SolanaOutpostBootstrapper.BootstrapOperator => ({ }) /** A seed of `size` paired roster entries + group members (roster IS the group). */ -const seedOfSize = (size: number): SolanaOutpostBootstrapper.OppBootstrapSeed => { +const seedOfSize = ( + size: number +): SolanaOutpostBootstrapper.OppBootstrapSeed => { const operators = Array.from({ length: size }, bootstrapOperator) - return { operators, groupMembers: operators.map(operator => operator.solAddress) } + return { + operators, + groupMembers: operators.map(operator => operator.solAddress) + } } -describe("SolanaOutpostBootstrapper.SplReserveSpecifications", () => { - it("provisions USDCSOL / USDTSOL / LIQSOL with the expected decimals", () => { - const byCode = new Map( - SolanaOutpostBootstrapper.SplReserveSpecifications.map(spec => [ - spec.codeName, - spec - ]) - ) - expect(byCode.get("USDCSOL")?.decimals).toBe(6) - expect(byCode.get("USDTSOL")?.decimals).toBe(6) - expect(byCode.get("LIQSOL")?.decimals).toBe(9) - }) -}) - describe("SolanaOutpostBootstrapper.PdaSeed", () => { it("carries the liqsol global_config seed matching the on-chain program", () => { // MUST match wire-solana/programs/liqsol-core/src/states/global_config.rs @@ -152,88 +142,3 @@ describe("SolanaOutpostBootstrapper.oppBootstrap argument validation", () => { ).rejects.toThrow(/program keypair missing/) }) }) - -const BootstrapperSourceFile = Path.resolve( - __dirname, - "../../../src/orchestration/solana/SolanaOutpostBootstrapper.ts" -) - -describe("SolanaOutpostBootstrapper.seedWithheldOperations (launch policy)", () => { - const ProgramName = "launch_policy_fixture" - const ProgramVersion = "0.0.0" - const IdlSpecVersion = "0.1.0" - let rpcUrl: string - - beforeAll(async () => { - rpcUrl = toURL( - await BindConfigProvider.findAvailable( - BindConfigProvider.DefaultSolanaRpc - ) - ) - }) - - /** An instruction-less program: any `program.methods.` call would throw, and no RPC is ever issued. */ - function createEmptyProgram(deployer: Keypair): anchor.Program { - const provider = new anchor.AnchorProvider( - new Connection(rpcUrl), - new anchor.Wallet(deployer), - {} - ) - return new anchor.Program( - { - address: PublicKey.default.toBase58(), - metadata: { - name: ProgramName, - version: ProgramVersion, - spec: IdlSpecVersion - }, - instructions: [] - }, - provider - ) - } - - it("skips every withheld call by default and reports it did not run", async () => { - const deployer = Keypair.generate(), - bootstrapper = new SolanaOutpostBootstrapper({ - solanaPath: "/repo/sol", - rpcUrl - }) - // the bootstrapper's `log` is `getLogger(__filename)`, cached per category - const info = jest.spyOn(getLogger(BootstrapperSourceFile), "info") - try { - await expect( - bootstrapper.seedWithheldOperations( - deployer, - createEmptyProgram(deployer), - PublicKey.default, - new anchor.BN(1) - ) - ).resolves.toBe(false) - expect(info).toHaveBeenCalledWith( - SolanaOutpostBootstrapper.LaunchWithheldOperationsSkippedMessage - ) - } finally { - info.mockRestore() - } - }) - - it("opens the gate once the policy flag is set (reaches the programId assertion)", async () => { - const deployer = Keypair.generate(), - bootstrapper = new SolanaOutpostBootstrapper({ - solanaPath: "/repo/sol", - rpcUrl, - enableLaunchWithheldOperations: true - }) - // programId is absent without a program keypair → the enabled path asserts - // before touching the chain, proving the gate opened. - await expect( - bootstrapper.seedWithheldOperations( - deployer, - createEmptyProgram(deployer), - PublicKey.default, - new anchor.BN(1) - ) - ).rejects.toThrow(/programId required/) - }) -}) diff --git a/packages/cluster-tool/tests/orchestration/solana/SolanaOutpostSteps.test.ts b/packages/cluster-tool/tests/orchestration/solana/SolanaOutpostSteps.test.ts index 4d5f23c79..0847ae72f 100644 --- a/packages/cluster-tool/tests/orchestration/solana/SolanaOutpostSteps.test.ts +++ b/packages/cluster-tool/tests/orchestration/solana/SolanaOutpostSteps.test.ts @@ -1,11 +1,11 @@ -import { BN } from "@coral-xyz/anchor" -import { Keypair, PublicKey } from "@solana/web3.js" import { OperatorType } from "@wireio/opp-typescript-models" -import { SolanaOutpostBootstrapper, Steps } from "@wireio/cluster-tool/orchestration" +import { + SolanaOutpostBootstrapper, + Steps +} from "@wireio/cluster-tool/orchestration" import { Report } from "@wireio/cluster-tool/report" import { solanaNativePublicKey } from "@wireio/cluster-tool/utils" -import { fixtureContext } from "../../config/clusterBuildContextFixture.js" import { fixtureOperatorAccount } from "../outputs/operatorAccountFixture.js" /** A batch `OperatorAccount` under an explicit chain account name. */ @@ -24,43 +24,6 @@ describe("Steps.solanaOutpost.deploy", () => { expect(step.input).toBeNull() expect(typeof step.runner).toBe("function") }) - - describe("runDeploy forwards the cluster's launch-withheld opt-in to the bootstrapper", () => { - afterEach(() => jest.restoreAllMocks()) - - /** Run `runDeploy` with `bootstrap` stubbed; return the bootstrapper it constructed. */ - async function deployedBootstrapper(enabled: boolean): Promise { - const constructed: SolanaOutpostBootstrapper[] = [] - jest - .spyOn(SolanaOutpostBootstrapper.prototype, "bootstrap") - .mockImplementation(async function (this: SolanaOutpostBootstrapper) { - constructed.push(this) - }) - await Steps.solanaOutpost.runDeploy( - fixtureContext({ enableLaunchWithheldOperations: enabled }), - null, - new AbortController().signal - ) - expect(constructed).toHaveLength(1) - return constructed[0] - } - - // Both gate paths return before the program is used: closed returns false, open asserts - // the programId (absent without a program keypair) first — so no program is needed. - it("opens the withheld-operations gate when the cluster opts in", async () => { - const bootstrapper = await deployedBootstrapper(true) - await expect( - bootstrapper.seedWithheldOperations(Keypair.generate(), null, PublicKey.default, new BN(1)) - ).rejects.toThrow(/programId required/) - }) - - it("keeps the gate closed when the cluster does not opt in", async () => { - const bootstrapper = await deployedBootstrapper(false) - await expect( - bootstrapper.seedWithheldOperations(Keypair.generate(), null, PublicKey.default, new BN(1)) - ).resolves.toBe(false) - }) - }) }) describe("Steps.solanaOutpost.oppBootstrap", () => { @@ -83,16 +46,27 @@ describe("Steps.solanaOutpost.oppBootstrap", () => { batchOperator("batch-op-02", "batchopc") ] // The depot's epoch-1 group is a strict subset of the provisioned roster. - const seed = Steps.solanaOutpost.resolveOppBootstrapSeed(operators, ["batchopa", "batchopc"]) + const seed = Steps.solanaOutpost.resolveOppBootstrapSeed(operators, [ + "batchopa", + "batchopc" + ]) // Roster = the group's operators only — the minimal signable seed... expect(seed.operators).toHaveLength(2) - expect(seed.operators[0].solAddress.toBase58()).toBe(solanaNativePublicKey(operators[0].solana)) - expect(seed.operators[1].solAddress.toBase58()).toBe(solanaNativePublicKey(operators[2].solana)) + expect(seed.operators[0].solAddress.toBase58()).toBe( + solanaNativePublicKey(operators[0].solana) + ) + expect(seed.operators[1].solAddress.toBase58()).toBe( + solanaNativePublicKey(operators[2].solana) + ) // ...group = the same members, in order, mapped to their SOL keys. expect(seed.groupMembers).toHaveLength(2) - expect(seed.groupMembers[0].toBase58()).toBe(solanaNativePublicKey(operators[0].solana)) - expect(seed.groupMembers[1].toBase58()).toBe(solanaNativePublicKey(operators[2].solana)) + expect(seed.groupMembers[0].toBase58()).toBe( + solanaNativePublicKey(operators[0].solana) + ) + expect(seed.groupMembers[1].toBase58()).toBe( + solanaNativePublicKey(operators[2].solana) + ) // Every roster entry is BATCH + ACTIVE with a positive wire-name encoding. seed.operators.forEach(operator => { expect(operator.role).toBe(OperatorType.BATCH) @@ -107,15 +81,27 @@ describe("Steps.solanaOutpost.oppBootstrap", () => { // against a buffer that admits at most // `SolanaOutpostBootstrapper.MaxOppBootstrapGroupMembers` members. const operators = Array.from({ length: 21 }, (_, index) => - batchOperator(`batch-op-${index}`, `batchop${String.fromCharCode(97 + index)}`) + batchOperator( + `batch-op-${index}`, + `batchop${String.fromCharCode(97 + index)}` + ) ), - groupAccounts = [operators[0].account, operators[7].account, operators[20].account] - const seed = Steps.solanaOutpost.resolveOppBootstrapSeed(operators, groupAccounts) + groupAccounts = [ + operators[0].account, + operators[7].account, + operators[20].account + ] + const seed = Steps.solanaOutpost.resolveOppBootstrapSeed( + operators, + groupAccounts + ) expect(seed.operators).toHaveLength(groupAccounts.length) expect(seed.groupMembers).toHaveLength(groupAccounts.length) seed.groupMembers.forEach((member, index) => - expect(member.toBase58()).toBe(seed.operators[index].solAddress.toBase58()) + expect(member.toBase58()).toBe( + seed.operators[index].solAddress.toBase58() + ) ) }) @@ -124,20 +110,36 @@ describe("Steps.solanaOutpost.oppBootstrap", () => { // group in ONE transaction, so only the first `MaxOppBootstrapOperators` // are seeded. const operators = Array.from({ length: 9 }, (_, index) => - batchOperator(`batch-op-${index}`, `batchop${String.fromCharCode(97 + index)}`) + batchOperator( + `batch-op-${index}`, + `batchop${String.fromCharCode(97 + index)}` + ) ), groupAccounts = operators.map(operator => operator.account) - const seed = Steps.solanaOutpost.resolveOppBootstrapSeed(operators, groupAccounts) + const seed = Steps.solanaOutpost.resolveOppBootstrapSeed( + operators, + groupAccounts + ) - expect(groupAccounts.length).toBeGreaterThan(Steps.solanaOutpost.MaxOppBootstrapOperators) - expect(seed.operators).toHaveLength(Steps.solanaOutpost.MaxOppBootstrapOperators) - expect(seed.groupMembers).toHaveLength(Steps.solanaOutpost.MaxOppBootstrapOperators) + expect(groupAccounts.length).toBeGreaterThan( + Steps.solanaOutpost.MaxOppBootstrapOperators + ) + expect(seed.operators).toHaveLength( + Steps.solanaOutpost.MaxOppBootstrapOperators + ) + expect(seed.groupMembers).toHaveLength( + Steps.solanaOutpost.MaxOppBootstrapOperators + ) // The cap keeps the group's LEADING members, in order. seed.operators.forEach((operator, index) => - expect(operator.solAddress.toBase58()).toBe(solanaNativePublicKey(operators[index].solana)) + expect(operator.solAddress.toBase58()).toBe( + solanaNativePublicKey(operators[index].solana) + ) ) seed.groupMembers.forEach((member, index) => - expect(member.toBase58()).toBe(seed.operators[index].solAddress.toBase58()) + expect(member.toBase58()).toBe( + seed.operators[index].solAddress.toBase58() + ) ) }) @@ -145,12 +147,23 @@ describe("Steps.solanaOutpost.oppBootstrap", () => { // The 6th member is unprovisioned — beyond the cap it is never looked up, // so it cannot fail a seed it is not part of. const operators = Array.from({ length: 5 }, (_, index) => - batchOperator(`batch-op-${index}`, `batchop${String.fromCharCode(97 + index)}`) + batchOperator( + `batch-op-${index}`, + `batchop${String.fromCharCode(97 + index)}` + ) ), - groupAccounts = [...operators.map(operator => operator.account), "ghostop"] - - const seed = Steps.solanaOutpost.resolveOppBootstrapSeed(operators, groupAccounts) - expect(seed.operators).toHaveLength(Steps.solanaOutpost.MaxOppBootstrapOperators) + groupAccounts = [ + ...operators.map(operator => operator.account), + "ghostop" + ] + + const seed = Steps.solanaOutpost.resolveOppBootstrapSeed( + operators, + groupAccounts + ) + expect(seed.operators).toHaveLength( + Steps.solanaOutpost.MaxOppBootstrapOperators + ) }) it("pins MaxOppBootstrapOperators under the transaction-packet ceiling", () => { @@ -171,7 +184,9 @@ describe("Steps.solanaOutpost.oppBootstrap", () => { it("throws a DISTINCT error when a group member carries no Solana key", () => { // Present in the roster but unprovisioned on SOL — a different failure from // "not found", and the message must say which. - const operators = [{ ...batchOperator("batch-op-00", "batchopa"), solana: null }] + const operators = [ + { ...batchOperator("batch-op-00", "batchopa"), solana: null } + ] expect(() => Steps.solanaOutpost.resolveOppBootstrapSeed(operators, ["batchopa"]) ).toThrow(/batchopa has no Solana key/) diff --git a/packages/cluster-tool/tests/orchestration/steps/ExternalClusterConfigSteps.test.ts b/packages/cluster-tool/tests/orchestration/steps/ExternalClusterConfigSteps.test.ts index febd01245..32f62c6d9 100644 --- a/packages/cluster-tool/tests/orchestration/steps/ExternalClusterConfigSteps.test.ts +++ b/packages/cluster-tool/tests/orchestration/steps/ExternalClusterConfigSteps.test.ts @@ -176,9 +176,11 @@ function outpostIdl(programId: string) { return { address: programId, metadata: { name: SolanaOutpostProgramTool.ProgramName }, - instructions: OperatorDaemonTool.RequiredSolanaIdlInstructions.map(name => ({ - name - })) + instructions: OperatorDaemonTool.RequiredSolanaIdlInstructions.map( + name => ({ + name + }) + ) } } @@ -225,7 +227,10 @@ function shiftPorts(value: unknown, delta: number): unknown { if (Array.isArray(value)) return value.map(entry => shiftPorts(entry, delta)) if (value != null && typeof value === "object") { return Object.fromEntries( - Object.entries(value).map(([key, entry]) => [key, shiftPorts(entry, delta)]) + Object.entries(value).map(([key, entry]) => [ + key, + shiftPorts(entry, delta) + ]) ) } return value @@ -428,7 +433,11 @@ describe("Steps.externalClusterConfig (create-external-config pipeline)", () => ctx.keyStore.pushNodes({ index: 0, keys: { - wire: { type: KeyType.K1, publicKey: "PUB_K1_n0", privateKey: "PVT_K1_n0" }, + wire: { + type: KeyType.K1, + publicKey: "PUB_K1_n0", + privateKey: "PVT_K1_n0" + }, wireFinalizer: { type: KeyType.BLS, publicKey: "PUB_BLS_n0", @@ -943,7 +952,8 @@ describe("Steps.externalClusterConfig (create-external-config pipeline)", () => // kind (they are the cluster's chain-read surface), so they sit outside // this producing set. const producingNodes = NodeConfig.plan(merged).filter( - node => !NodeConfig.isOperatorRole(node.role) && node.role !== NodeRole.api + node => + !NodeConfig.isOperatorRole(node.role) && node.role !== NodeRole.api ) expect(producingNodes.length).toBeGreaterThan(0) producingNodes.forEach(node => { @@ -1091,8 +1101,12 @@ describe("Steps.externalClusterConfig (create-external-config pipeline)", () => expect(merged.externalOutposts.ethereum.rpcUrl).toBe(ExternalEthereumRpcUrl) expect(merged.externalOutposts.solana.rpcUrl).toBe(ExternalSolanaRpcUrl) expect(merged.externalOutposts.ethereum.chainId).toBe(ExternalChainId) - expect(merged.externalOutposts.ethereum.addressFile.startsWith(externalDir)).toBe(true) - expect(merged.externalOutposts.solana.idlFile.startsWith(externalDir)).toBe(true) + expect( + merged.externalOutposts.ethereum.addressFile.startsWith(externalDir) + ).toBe(true) + expect(merged.externalOutposts.solana.idlFile.startsWith(externalDir)).toBe( + true + ) // The 4-field ETH client spec: id, provider, the AUTHORITATIVE endpoint, // and the config's REAL chain id (never the anvil default). @@ -1180,7 +1194,11 @@ describe("Steps.externalClusterConfig (create-external-config pipeline)", () => }) it("persists debuggingServerEnabled:false when create-external-config --no-debugging-server is set", async () => { - const ctx = runContext(externalBindFile, PersistedFixture.signatureProvider, true) + const ctx = runContext( + externalBindFile, + PersistedFixture.signatureProvider, + true + ) await External.runLoadExternalBind(ctx, null, signal) await External.runClone(ctx, null, signal) await External.runRebind(ctx, null, signal) @@ -1199,7 +1217,9 @@ describe("Steps.externalClusterConfig (create-external-config pipeline)", () => await External.runClone(ctx, null, signal) await External.runRebind(ctx, null, signal) // The local fixture has debuggingServerEnabled: true → inherited unchanged. - expect(ctx.outputs.assert(External.MergedConfigKey).debuggingServerEnabled).toBe(true) + expect( + ctx.outputs.assert(External.MergedConfigKey).debuggingServerEnabled + ).toBe(true) }) it("Validate composes one verify step per cross-check (fail-fast order)", () => { @@ -1283,7 +1303,7 @@ describe("Steps.externalClusterConfig (create-external-config pipeline)", () => const ctx = runContext(bindFile) await External.runLoadExternalBind(ctx, null, signal) await expect(External.runVerifyNodeMapping(ctx, signal)).rejects.toThrow( - /cluster-state has 7 nodes but the external bind describes 6/ + /cluster-state has 6 nodes but the external bind describes 5/ ) }) @@ -1360,7 +1380,10 @@ describe("Steps.externalClusterConfig (create-external-config pipeline)", () => const { ctx, scriptFile } = await emitWithCollidingDbSize() // The mask is POSITIONAL, not by value: an un-rebound local endpoint // carrying the identical digits must still hard-fail the scan. - Fs.appendFileSync(scriptFile, `\n# leftover endpoint: ${staleLocalPort}\n`) + Fs.appendFileSync( + scriptFile, + `\n# leftover endpoint: ${staleLocalPort}\n` + ) await expect(External.runVerify(ctx, null, signal)).rejects.toThrow( new RegExp(`still contains the local bind port ${staleLocalPort}`) ) @@ -1446,7 +1469,7 @@ describe("Steps.externalClusterConfig (create-external-config pipeline)", () => /** An advertise address only the LOCAL bind carries; the external bind never does. */ const localAdvertiseAddress = "10.60.7.10" - it.each(["producers", "batch", "underwriters", "api", "adHoc"] as const)( + it.each(["producers", "batch", "api", "adHoc"] as const)( "flags a leftover advertise address of a nodeop.ports.%s pair", async pairList => { const localBind = structuredClone(PersistedFixture.bind) @@ -1671,8 +1694,6 @@ describe("Steps.externalClusterConfig (create-external-config pipeline)", () => // Every emitted provider is an SSM ref… expect(emittedSecretIds.length).toBe(emittedProviderCount) // …and every one of them names a parameter create actually wrote. - emittedSecretIds.forEach(secretId => - expect(published).toContain(secretId) - ) + emittedSecretIds.forEach(secretId => expect(published).toContain(secretId)) }) }) diff --git a/packages/cluster-tool/tests/orchestration/steps/ExternalOutpostSteps.test.ts b/packages/cluster-tool/tests/orchestration/steps/ExternalOutpostSteps.test.ts index 897f3adab..841d9332b 100644 --- a/packages/cluster-tool/tests/orchestration/steps/ExternalOutpostSteps.test.ts +++ b/packages/cluster-tool/tests/orchestration/steps/ExternalOutpostSteps.test.ts @@ -69,8 +69,12 @@ describe("Steps.externalOutpost (materialize + publish)", () => { it("materializes the config-referenced files into the canonical data dir", async () => { const ctx = externalContext() await Steps.externalOutpost.runMaterialize(ctx, null, signal) - const deploymentsDir = ClusterConfigProvider.ethereumDeploymentsPath(ctx.config) - expect(Fs.existsSync(Path.join(deploymentsDir, "outpost-addrs.json"))).toBe(true) + const deploymentsDir = ClusterConfigProvider.ethereumDeploymentsPath( + ctx.config + ) + expect(Fs.existsSync(Path.join(deploymentsDir, "outpost-addrs.json"))).toBe( + true + ) expect( Fs.existsSync( Path.join(dataPath, OperatorDaemonTool.EthereumAbiSubpath, "OPP.json") @@ -93,11 +97,13 @@ describe("Steps.externalOutpost (materialize + publish)", () => { await Steps.externalOutpost.runPublishArtifacts(ctx, null, signal) const artifacts = ctx.outputs.get(OperatorDaemonArtifactsKey) expect(artifacts?.ethereumAddresses.OPP).toBe(OppAddress) - expect(artifacts?.ethereumAbiFiles.some(file => file.endsWith("OPP.json"))).toBe( - true - ) + expect( + artifacts?.ethereumAbiFiles.some(file => file.endsWith("OPP.json")) + ).toBe(true) expect(artifacts?.solanaProgramId).toBe(ProgramId) - expect(artifacts?.solanaIdlFile).toContain(OperatorDaemonTool.SolanaIdlFilename) + expect(artifacts?.solanaIdlFile).toContain( + OperatorDaemonTool.SolanaIdlFilename + ) }) it("materialize fails fast when a referenced source file is absent", async () => { @@ -131,13 +137,13 @@ describe("Steps.externalOutpost (materialize + publish)", () => { it("publish fails when a required SOL IDL instruction is missing", async () => { Fs.writeFileSync( idlFile, - JSON.stringify({ address: ProgramId, instructions: [{ name: "epoch_in" }] }) + JSON.stringify({ address: ProgramId, instructions: [] }) ) const ctx = externalContext() await Steps.externalOutpost.runMaterialize(ctx, null, signal) await expect( Steps.externalOutpost.runPublishArtifacts(ctx, null, signal) - ).rejects.toThrow(/missing the 'commit_underwrite' instruction/) + ).rejects.toThrow(/missing the 'epoch_in' instruction/) }) describe("outbound-envelope bootstrap gate", () => { @@ -258,9 +264,9 @@ describe("Steps.externalOutpost (materialize + publish)", () => { }) it("pins the step ceiling ABOVE its inner poll budget", () => { - expect( - Steps.externalOutpost.OutboundEnvelopesTimeoutMs - ).toBeGreaterThan(Steps.externalOutpost.OutboundEnvelopesPollBudgetMs) + expect(Steps.externalOutpost.OutboundEnvelopesTimeoutMs).toBeGreaterThan( + Steps.externalOutpost.OutboundEnvelopesPollBudgetMs + ) }) it("planOutboundEnvelopesQueued defaults its ceiling to the named constant", () => { diff --git a/packages/cluster-tool/tests/orchestration/steps/KeySteps.test.ts b/packages/cluster-tool/tests/orchestration/steps/KeySteps.test.ts index 5f42f5ff2..4de4c29a8 100644 --- a/packages/cluster-tool/tests/orchestration/steps/KeySteps.test.ts +++ b/packages/cluster-tool/tests/orchestration/steps/KeySteps.test.ts @@ -11,7 +11,11 @@ import { } from "@wireio/cluster-tool-shared" import { Constants } from "@wireio/cluster-tool/Constants" import { KeyGenerator } from "@wireio/cluster-tool/clients/wire" -import { ClusterConfigProvider, NodeConfig, NodeRole } from "@wireio/cluster-tool/config" +import { + ClusterConfigProvider, + NodeConfig, + NodeRole +} from "@wireio/cluster-tool/config" import { EthereumMnemonicKey, EthereumOutpostBootstrapper, @@ -19,7 +23,10 @@ import { } from "@wireio/cluster-tool/orchestration" import { Report } from "@wireio/cluster-tool/report" import type { WireFinalizerKeyPair } from "@wireio/cluster-tool/types/KeyPair" -import { fixtureConfig, PersistedFixture } from "../../config/clusterConfigFixture.js" +import { + fixtureConfig, + PersistedFixture +} from "../../config/clusterConfigFixture.js" import { fixtureContext } from "../../config/clusterBuildContextFixture.js" /** The `SSMClient` constructor config — region-less means the ambient region. */ @@ -168,7 +175,10 @@ describe("Steps.keys", () => { function fakeExecutables(): Partial { return { buildPath: dir, - executables: { ...PersistedFixture.executables, clio: Path.join(dir, "clio") } + executables: { + ...PersistedFixture.executables, + clio: Path.join(dir, "clio") + } } } @@ -222,7 +232,7 @@ describe("Steps.keys", () => { it("enumerates the GENESIS identity, every producer ACCOUNT, and every operator — but NO producer node", () => { // Fixture topology: bios K1+BLS (2) + node owner K1 (1) + 21 producer ACCOUNTS × K1+BLS - // (42) + (3 batch + 1 underwriter) operators × 3 K1/EM/ED (12) = 57. + // (42) + 3 batch operators × 3 K1/EM/ED (9) = 54. // // The producer-account rows are the point: each account owns its finalizer key (a shared // one would fail `regfinkey`'s global uniqueness check), and BOTH halves of an account's @@ -235,7 +245,7 @@ describe("Steps.keys", () => { const producerAccounts = NodeConfig.plan(config) .filter(node => node.role === NodeRole.producer) .flatMap(node => node.producers) - expect(publications).toHaveLength(15 + producerAccounts.length * 2) + expect(publications).toHaveLength(12 + producerAccounts.length * 2) NodeConfig.plan(config) .filter(node => node.role === NodeRole.producer) .forEach(node => @@ -331,9 +341,9 @@ describe("Steps.keys", () => { }) it("carries no version when the cluster's SSM settings declare none", () => { - expect(publications.every(publication => publication.version == null)).toBe( - true - ) + expect( + publications.every(publication => publication.version == null) + ).toBe(true) }) it("renders the OPTIONAL {version} token when the pattern authors it, and carries it as the tag value", () => { @@ -439,10 +449,10 @@ describe("Steps.keys", () => { config, Steps.keys.SignatureKeyPublishPhase.afterOperators ) - // (3 batch + 1 underwriter) × (K1 + EM + ED) = 12. Producer ACCOUNTS are operator-source + // 3 batch operators × (K1 + EM + ED) = 9. Producer ACCOUNTS are operator-source // too, but publish in the beforeNodes phase (their nodes fetch those keys at launch), so // they are deliberately NOT here. - expect(phase.steps).toHaveLength(12) + expect(phase.steps).toHaveLength(9) expect( phase.steps.every(step => !step.name.startsWith("publish-defproducer")) ).toBe(true) @@ -1116,7 +1126,11 @@ describe("Steps.keys", () => { // The walker's own batch-operator rows are the SAME shape — one renderer, // so a flow's id and a bootstrap id can never drift apart. expect( - Steps.keys.operatorKeyPublications(config, "batchop.a", OperatorType.BATCH) + Steps.keys.operatorKeyPublications( + config, + "batchop.a", + OperatorType.BATCH + ) ).toEqual( Steps.keys .signatureProviderKeyPublications(config) @@ -1126,15 +1140,20 @@ describe("Steps.keys", () => { it("knows which labels the bootstrap publishes itself — a flow's label is never among them", () => { expect( - Steps.keys.isPublishedAtBootstrap(config, Constants.batchOperatorLabel(0)) + Steps.keys.isPublishedAtBootstrap( + config, + Constants.batchOperatorLabel(0) + ) ).toBe(true) expect( Steps.keys.isPublishedAtBootstrap(config, Constants.underwriterLabel(0)) + ).toBe(false) + expect( + Steps.keys.isPublishedAtBootstrap(config, NodeConfig.BiosName) ).toBe(true) - expect(Steps.keys.isPublishedAtBootstrap(config, NodeConfig.BiosName)).toBe( - true + expect(Steps.keys.isPublishedAtBootstrap(config, "depositoraaa")).toBe( + false ) - expect(Steps.keys.isPublishedAtBootstrap(config, "depositoraaa")).toBe(false) }) }) diff --git a/packages/cluster-tool/tests/orchestration/steps/RegistrySteps.test.ts b/packages/cluster-tool/tests/orchestration/steps/RegistrySteps.test.ts index 2b69a3637..61273e3b8 100644 --- a/packages/cluster-tool/tests/orchestration/steps/RegistrySteps.test.ts +++ b/packages/cluster-tool/tests/orchestration/steps/RegistrySteps.test.ts @@ -29,70 +29,6 @@ describe("Steps.registry", () => { expect(typeof step.runner).toBe("function") }) - describe("planMockReserves", () => { - const PrimaryCode = "PRIMARY" - const StableCode = "USDC" - const NativeCode = "ETH" - // ReserveSeedAmount 10_000_000_000; stablecoins ÷1000 at precision 6, others precision 9. - const FullChainSeed = 10_000_000_000 - const StableChainSeed = 10_000_000 - const ConnectorWeightBps = 5000 - - it("returns a Phase of 8 static Sysio regreserve steps", () => { - const phase = Steps.registry.planMockReserves( - newBuild(), - "MockReserves", - "seed mock reserves", - {} - ) - expect(phase).toBeInstanceOf(ClusterBuildPhase) - expect(phase.steps).toHaveLength(8) - expect( - phase.steps.every(step => step.actor === Report.Actor.Sysio) - ).toBe(true) - }) - - it("names each step seed-reserve--, all unique", () => { - const phase = Steps.registry.planMockReserves( - newBuild(), - "MockReserves", - "d", - {} - ) - const names = phase.steps.map(step => step.name) - expect(new Set(names).size).toBe(8) - expect(names).toContain("seed-reserve-ethereum-eth") - expect(names).toContain("seed-reserve-solana-usdcsol") - }) - - it("carries a RegreserveInput with PRIMARY code + 5000 connector on every row", () => { - const phase = Steps.registry.planMockReserves( - newBuild(), - "MockReserves", - "d", - {} - ) - phase.steps.forEach(step => { - expect(step.input.kind).toBe("ReservContractSteps.RegreserveInput") - expect(step.input.data.reserve_code).toBe(PrimaryCode) - expect(step.input.data.connector_weight_bps).toBe(ConnectorWeightBps) - expect(step.input.data.is_private).toBe(false) - }) - }) - - it("seeds stablecoins at precision 6 with a ÷1000 chain seed, others at full/9", () => { - const rows = Steps.registry.MockReserveRegistrations - expect(rows).toHaveLength(8) - const stable = rows.find(row => row.token_code === StableCode) - const native = rows.find(row => row.token_code === NativeCode) - expect(stable?.source_token_precision).toBe(6) - expect(stable?.initial_chain_amount).toBe(StableChainSeed) - expect(stable?.initial_wire_amount).toBe(FullChainSeed) - expect(native?.source_token_precision).toBe(9) - expect(native?.initial_chain_amount).toBe(FullChainSeed) - }) - }) - describe("planShadowLiqTokens", () => { const LiqethCode = "LIQETH" const SolanaCode = "SOLANA" @@ -106,9 +42,9 @@ describe("Steps.registry", () => { ) expect(phase).toBeInstanceOf(ClusterBuildPhase) expect(phase.steps).toHaveLength(2) - expect( - phase.steps.every(step => step.actor === Report.Actor.Sysio) - ).toBe(true) + expect(phase.steps.every(step => step.actor === Report.Actor.Sysio)).toBe( + true + ) expect(phase.steps.map(step => step.name)).toEqual([ "create-shadow-liqeth", "create-shadow-liqsol" @@ -148,9 +84,9 @@ describe("Steps.registry", () => { ) expect(phase).toBeInstanceOf(ClusterBuildPhase) expect(phase.steps).toHaveLength(2) - expect( - phase.steps.every(step => step.actor === Report.Actor.Sysio) - ).toBe(true) + expect(phase.steps.every(step => step.actor === Report.Actor.Sysio)).toBe( + true + ) expect(phase.steps.map(step => step.name)).toEqual([ "seed-liq-pool-ethereum-liqeth", "seed-liq-pool-solana-liqsol" @@ -209,7 +145,9 @@ describe("Steps.registry — underwriting and syndication configuration", () => it("takes the challenge window from ProtocolTiming and a non-zero challenge charge", () => { rows.forEach(row => { - expect(row.window_sec).toBe(ProtocolTiming.SyndicationChallengeWindowSec) + expect(row.window_sec).toBe( + ProtocolTiming.SyndicationChallengeWindowSec + ) expect(Number(row.challenge_extra)).toBeGreaterThan(0) }) }) @@ -217,8 +155,14 @@ describe("Steps.registry — underwriting and syndication configuration", () => it("keeps every amount within an asset's range", () => { const AssetAmountMax = 2n ** 62n - 1n rows.forEach(row => { - ;[row.synd_burst, row.synd_refill, row.desynd_burst, row.desynd_refill, row.challenge_extra].forEach( - amount => expect(BigInt(amount)).toBeLessThanOrEqual(AssetAmountMax) + ;[ + row.synd_burst, + row.synd_refill, + row.desynd_burst, + row.desynd_refill, + row.challenge_extra + ].forEach(amount => + expect(BigInt(amount)).toBeLessThanOrEqual(AssetAmountMax) ) }) }) @@ -242,7 +186,9 @@ describe("Steps.registry — underwriting and syndication configuration", () => "verify-liq-token-ethereum-liqeth", "verify-liq-token-solana-liqsol" ]) - expect(phase.steps.every(step => step.actor === Report.Actor.Sysio)).toBe(true) + expect(phase.steps.every(step => step.actor === Report.Actor.Sysio)).toBe( + true + ) }) it("sets sysio.bond's hold bond to the contract default", () => { diff --git a/packages/cluster-tool/tests/orchestration/steps/contracts/sysio/ReservContractSteps.test.ts b/packages/cluster-tool/tests/orchestration/steps/contracts/sysio/ReservContractSteps.test.ts deleted file mode 100644 index f6d570fd0..000000000 --- a/packages/cluster-tool/tests/orchestration/steps/contracts/sysio/ReservContractSteps.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { Steps } from "@wireio/cluster-tool/orchestration" -import { Report } from "@wireio/cluster-tool/report" -import { SysioContracts } from "@wireio/sdk-core" - -describe("Steps.contracts.sysio.reserv", () => { - it("regreserve carries the reserv::regreserve data", () => { - const data: SysioContracts.SysioReservRegreserveAction = { - chain_code: "ETHEREUM", - token_code: "ETH", - reserve_code: "PRIMARY", - name: "ETHEREUM-ETH/WIRE primary reserve", - description: "Bootstrap-seeded native ETH ↔ WIRE reserve", - initial_chain_amount: 10_000_000_000, - initial_wire_amount: 10_000_000_000, - source_token_precision: 9, - connector_weight_bps: 5000, - is_private: false, - owner: "" - } - const step = Steps.contracts.sysio.reserv.planRegreserve( - Report.Actor.Sysio, - "seed-ethereum-eth", - "seed the ETHEREUM-ETH/WIRE primary reserve", - {}, - data - ) - expect(step.actor).toBe(Report.Actor.Sysio) - expect(step.input.kind).toBe("ReservContractSteps.RegreserveInput") - expect(step.input.data).toBe(data) - expect(step.input.data.connector_weight_bps).toBe(5000) - expect(typeof step.runner).toBe("function") - }) - - it("setconfig carries the reserv::setconfig fee-routing data", () => { - // Stage 2 of the swap-fee split: the share of each fee's rewards pool sent - // to the emissions treasury instead of the batch-operator rewards bucket. - const data: SysioContracts.SysioReservSetconfigAction = { - fee_emissions_share_bps: 0 - } - const step = Steps.contracts.sysio.reserv.planSetconfig( - Report.Actor.Sysio, - "configure-reserv", - "set the swap-fee routing config", - {}, - data - ) - expect(step.actor).toBe(Report.Actor.Sysio) - expect(step.input.kind).toBe("ReservContractSteps.SetconfigInput") - expect(step.input.data).toBe(data) - expect(step.input.data.fee_emissions_share_bps).toBe(0) - expect(typeof step.runner).toBe("function") - }) - - it("setconfig accepts a non-zero emissions share", () => { - const step = Steps.contracts.sysio.reserv.planSetconfig( - Report.Actor.Sysio, - "configure-reserv", - "route half the rewards pool to the treasury", - {}, - { fee_emissions_share_bps: 5_000 } - ) - expect(step.input.data.fee_emissions_share_bps).toBe(5_000) - }) - - /** The reserve triple both owner-fee actions address. */ - const reserveTriple = { - chain_code: "ETHEREUM", - token_code: "ETH", - reserve_code: "PRIVATE" - } - - it("setrsvfee carries the rate AND the owner, who is the required signer", () => { - // The owner is NOT part of the action data — the contract reads it off the - // row — so the step input carries it for the authorization. - const data: SysioContracts.SysioReservSetrsvfeeAction = { - ...reserveTriple, - owner_fee_bps: 100 - } - const step = Steps.contracts.sysio.reserv.planSetrsvfee( - Report.Actor.User, - "set-ethereum-reserve-fee", - "owner charges 100 bps on the private ETH reserve", - {}, - data, - "privowner" - ) - expect(step.actor).toBe(Report.Actor.User) - expect(step.input.kind).toBe("ReservContractSteps.SetrsvfeeInput") - expect(step.input.data).toBe(data) - expect(step.input.data.owner_fee_bps).toBe(100) - expect(step.input.owner).toBe("privowner") - expect(typeof step.runner).toBe("function") - }) - - it("setrsvfee carries a zero rate — the disable case", () => { - const step = Steps.contracts.sysio.reserv.planSetrsvfee( - Report.Actor.User, - "clear-reserve-fee", - "owner stops charging", - {}, - { ...reserveTriple, owner_fee_bps: 0 }, - "privowner" - ) - expect(step.input.data.owner_fee_bps).toBe(0) - expect(step.input.owner).toBe("privowner") - }) - - it("claimrsvfee carries the reserve triple AND the owner", () => { - const data: SysioContracts.SysioReservClaimrsvfeeAction = { ...reserveTriple } - const step = Steps.contracts.sysio.reserv.planClaimrsvfee( - Report.Actor.User, - "claim-ethereum-reserve-fee", - "owner claims the private ETH reserve's accrued owner fee", - {}, - data, - "privowner" - ) - expect(step.input.kind).toBe("ReservContractSteps.ClaimrsvfeeInput") - expect(step.input.data).toBe(data) - expect(step.input.owner).toBe("privowner") - expect(typeof step.runner).toBe("function") - }) -}) diff --git a/packages/cluster-tool/tests/orchestration/steps/contracts/sysio/UwritContractSteps.test.ts b/packages/cluster-tool/tests/orchestration/steps/contracts/sysio/UwritContractSteps.test.ts deleted file mode 100644 index 0a87a68c4..000000000 --- a/packages/cluster-tool/tests/orchestration/steps/contracts/sysio/UwritContractSteps.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Steps } from "@wireio/cluster-tool/orchestration" -import { Report } from "@wireio/cluster-tool/report" -import { SysioContracts } from "@wireio/sdk-core" - -describe("Steps.contracts.sysio.uwrit", () => { - it("setconfig carries the uwrit::setconfig data", () => { - // `sysio.uwrit::setconfig` grew the swap-from-WIRE ingress rails (the - // escrow floor `min_fromwire_amount` + caller-fault drain-revert fee - // `fromwire_revert_fee_bps`) and the SEC-129 / WSA-223 UWREQ lifecycle - // knobs (`uwreq_pending_timeout_epochs` + `uwreq_retention_epochs`). The - // action type requires all six fields — a payload missing any pair fails - // chain-side serialization. - const data: SysioContracts.SysioUwritSetconfigAction = { - fee_bps: 30, - collateral_lock_duration_ms: 600_000, - min_fromwire_amount: 100_000_000, - fromwire_revert_fee_bps: 10, - uwreq_pending_timeout_epochs: 10, - uwreq_retention_epochs: 10 - } - const step = Steps.contracts.sysio.uwrit.planSetconfig( - Report.Actor.Sysio, - "configure-uwrit", - "set the underwriter config", - {}, - data - ) - expect(step.actor).toBe(Report.Actor.Sysio) - expect(step.input.kind).toBe("UwritContractSteps.SetconfigInput") - expect(step.input.data).toBe(data) - expect(step.input.data.fee_bps).toBe(30) - expect(step.input.data.min_fromwire_amount).toBe(100_000_000) - expect(step.input.data.fromwire_revert_fee_bps).toBe(10) - expect(step.input.data.uwreq_pending_timeout_epochs).toBe(10) - expect(step.input.data.uwreq_retention_epochs).toBe(10) - expect(typeof step.runner).toBe("function") - }) -}) 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 b18a39dea..581463656 100644 --- a/packages/cluster-tool/tests/orchestration/steps/processes/NodeopProcessSteps.test.ts +++ b/packages/cluster-tool/tests/orchestration/steps/processes/NodeopProcessSteps.test.ts @@ -24,7 +24,8 @@ import { PersistedFixture } from "../../../config/clusterConfigFixture.js" import { fixtureOperatorAccount } from "../../../orchestration/outputs/operatorAccountFixture.js" /** anvil's deterministic mnemonic — HD-derived wallets are stable + well-known. */ -const AnvilMnemonic = "test test test test test test test test test test test junk" +const AnvilMnemonic = + "test test test test test test test test test test test junk" /** * A fully-keyed OperatorAccount fixture for the given label/type — REAL @@ -45,7 +46,11 @@ function operatorAccount(label: string, type: OperatorType): OperatorAccount { publicationLabel: label, account: `wireno.${label}`, type, - wire: { type: KeyType.K1, publicKey: `PUB_K1_${label}`, privateKey: `PVT_K1_${label}` }, + wire: { + type: KeyType.K1, + publicKey: `PUB_K1_${label}`, + privateKey: `PVT_K1_${label}` + }, ethereum: ethereumKeyPairFromWallet(wallet), solana: { type: KeyType.ED, @@ -233,7 +238,10 @@ describe("Steps.processes.nodeop", () => { ctx.outputs.set(OperatorDaemonArtifactsKey, artifactsFixture) // A CONSTRUCTED (never started) process satisfies the running-nodeop // assertion; `stop()` is a no-op without a child and `remove()` accepts it. - await NodeopProcess.create(ctx.processManager, { node, operators: [operator] }) + await NodeopProcess.create(ctx.processManager, { + node, + operators: [operator] + }) const depotHead = 42 jest.spyOn(WireClient.prototype, "getHead").mockResolvedValue(depotHead) @@ -291,7 +299,9 @@ describe("Steps.processes.nodeop", () => { } ctx.keyStore.setOperator(seeded) const node = testNode(ctx, NodeRole.bios, 0, "bios") - expect(Steps.processes.nodeop.resolveOperators(ctx, node)).toEqual([seeded]) + expect(Steps.processes.nodeop.resolveOperators(ctx, node)).toEqual([ + seeded + ]) }) it("producer node resolves ONE provisioned account per hosted producer, in order", () => { @@ -299,23 +309,25 @@ describe("Steps.processes.nodeop", () => { // uniqueness `regfinkey` demands. Resolution must hand back all of them, not just the // first, or the node launches able to vote for only one of the producers it runs. const ctx = fixtureContext(), - hosted: OperatorAccount[] = ["defproducera", "defproducerb"].map(label => ({ - label, - publicationLabel: label, - account: label, - type: OperatorType.PRODUCER, - wire: { - type: KeyType.K1 as const, - publicKey: "PUB_K1_node1", - privateKey: "PVT_K1_node1" - }, - wireFinalizer: { - type: KeyType.BLS as const, - publicKey: `PUB_BLS_${label}`, - privateKey: `PVT_BLS_${label}`, - proofOfPossession: `SIG_BLS_${label}` - } - })) + hosted: OperatorAccount[] = ["defproducera", "defproducerb"].map( + label => ({ + label, + publicationLabel: label, + account: label, + type: OperatorType.PRODUCER, + wire: { + type: KeyType.K1 as const, + publicKey: "PUB_K1_node1", + privateKey: "PVT_K1_node1" + }, + wireFinalizer: { + type: KeyType.BLS as const, + publicKey: `PUB_BLS_${label}`, + privateKey: `PVT_BLS_${label}`, + proofOfPossession: `SIG_BLS_${label}` + } + }) + ) hosted.forEach(operator => ctx.keyStore.setOperator(operator)) const node = testNode(ctx, NodeRole.producer, 1, "node_01", [ "defproducera", @@ -327,9 +339,12 @@ describe("Steps.processes.nodeop", () => { "defproducerb" ]) // One shared block-signing K1, two DISTINCT finalizer keys. - expect(new Set(operators.map(operator => operator.wire.publicKey)).size).toBe(1) expect( - new Set(operators.map(operator => operator.wireFinalizer?.publicKey)).size + new Set(operators.map(operator => operator.wire.publicKey)).size + ).toBe(1) + expect( + new Set(operators.map(operator => operator.wireFinalizer?.publicKey)) + .size ).toBe(2) }) @@ -337,7 +352,9 @@ describe("Steps.processes.nodeop", () => { // Synthesizing a stand-in from the node's keys would launch a node holding a finalizer // key no producer ever registered — it would sign nothing the policy accepts. const ctx = fixtureContext() - const node = testNode(ctx, NodeRole.producer, 1, "node_01", ["defproducera"]) + const node = testNode(ctx, NodeRole.producer, 1, "node_01", [ + "defproducera" + ]) expect(() => Steps.processes.nodeop.resolveOperators(ctx, node)).toThrow( /has not been provisioned/ ) @@ -345,18 +362,43 @@ describe("Steps.processes.nodeop", () => { it("batch-operator node resolves the provisioned account from ctx.keyStore", () => { const ctx = fixtureContext() - const provisioned = fixtureOperatorAccount("batchopaaaa", OperatorType.BATCH) + const provisioned = fixtureOperatorAccount( + "batchopaaaa", + OperatorType.BATCH + ) ctx.keyStore.setOperator(provisioned) - const node = testNode(ctx, NodeRole.batch_operator, 2, "node_02", [], "batchopaaaa") - expect(Steps.processes.nodeop.resolveOperators(ctx, node)).toEqual([provisioned]) + const node = testNode( + ctx, + NodeRole.batch_operator, + 2, + "node_02", + [], + "batchopaaaa" + ) + expect(Steps.processes.nodeop.resolveOperators(ctx, node)).toEqual([ + provisioned + ]) }) it("underwriter node resolves the provisioned account from ctx.keyStore", () => { const ctx = fixtureContext() - const provisioned = fixtureOperatorAccount("underwriteraaaa", OperatorType.UNDERWRITER) + const provisioned = fixtureOperatorAccount( + "underwriteraaaa", + OperatorType.UNDERWRITER + ) ctx.keyStore.setOperator(provisioned) - const node = testNode(ctx, NodeRole.underwriter, 3, "node_03", [], null, "underwriteraaaa") - expect(Steps.processes.nodeop.resolveOperators(ctx, node)).toEqual([provisioned]) + const node = testNode( + ctx, + NodeRole.underwriter, + 3, + "node_03", + [], + null, + "underwriteraaaa" + ) + expect(Steps.processes.nodeop.resolveOperators(ctx, node)).toEqual([ + provisioned + ]) }) it("throws when an operator node names no batch/underwriter label", () => { @@ -379,7 +421,14 @@ describe("Steps.processes.nodeop", () => { it("throws when the named operator label has not been provisioned in ctx.keyStore", () => { const ctx = fixtureContext() - const node = testNode(ctx, NodeRole.batch_operator, 5, "node_05", [], "unprovisioned") + const node = testNode( + ctx, + NodeRole.batch_operator, + 5, + "node_05", + [], + "unprovisioned" + ) expect(() => Steps.processes.nodeop.resolveOperators(ctx, node)).toThrow( /has not been provisioned/ ) @@ -401,7 +450,9 @@ describe("Steps.processes.nodeop", () => { it("returns [] for a producer node", () => { const ctx = fixtureContext() - const node = testNode(ctx, NodeRole.producer, 1, "node_01", ["defproducera"]) + const node = testNode(ctx, NodeRole.producer, 1, "node_01", [ + "defproducera" + ]) expect( Steps.processes.nodeop.resolveOperatorDaemonArgs( ctx, @@ -431,8 +482,19 @@ describe("Steps.processes.nodeop", () => { const ctx = fixtureContext() ctx.outputs.set(OperatorDaemonArtifactsKey, artifactsFixture) const account = fixtureOperatorAccount("batchopaaaa", OperatorType.BATCH) - const node = testNode(ctx, NodeRole.batch_operator, 2, "node_02", [], "batchopaaaa") - const args = Steps.processes.nodeop.resolveOperatorDaemonArgs(ctx, node, account) + const node = testNode( + ctx, + NodeRole.batch_operator, + 2, + "node_02", + [], + "batchopaaaa" + ) + const args = Steps.processes.nodeop.resolveOperatorDaemonArgs( + ctx, + node, + account + ) expect(args).toEqual( expect.arrayContaining([ "--batch-operator-account", @@ -442,31 +504,46 @@ describe("Steps.processes.nodeop", () => { // The depot matches this argv against `sysio.opreg::operators`, which is // keyed by the ON-CHAIN account — passing the handle would start a daemon // that silently matches no operator row. - expect(valuesOf(args, "--batch-operator-account")).toEqual([account.account]) - expect(valuesOf(args, "--batch-operator-account")).not.toEqual([account.label]) + expect(valuesOf(args, "--batch-operator-account")).toEqual([ + account.account + ]) + expect(valuesOf(args, "--batch-operator-account")).not.toEqual([ + account.label + ]) }) - it("builds underwriter daemon args for an underwriter node", () => { + it("rejects the retired swap-underwriter daemon role", () => { const ctx = fixtureContext() ctx.outputs.set(OperatorDaemonArtifactsKey, artifactsFixture) - const account = fixtureOperatorAccount("underwriteraaaa", OperatorType.UNDERWRITER) - const node = testNode(ctx, NodeRole.underwriter, 3, "node_03", [], null, "underwriteraaaa") - const args = Steps.processes.nodeop.resolveOperatorDaemonArgs(ctx, node, account) - expect(args).toEqual( - expect.arrayContaining([ - "--underwriter-account", - "wireno.underwriteraaaa" - ]) + const account = fixtureOperatorAccount( + "underwriteraaaa", + OperatorType.UNDERWRITER + ) + const node = testNode( + ctx, + NodeRole.underwriter, + 3, + "node_03", + [], + null, + "underwriteraaaa" ) - // Same chain-boundary rule as `--batch-operator-account`. - expect(valuesOf(args, "--underwriter-account")).toEqual([account.account]) - expect(valuesOf(args, "--underwriter-account")).not.toEqual([account.label]) + expect(() => + Steps.processes.nodeop.resolveOperatorDaemonArgs(ctx, node, account) + ).toThrow(/Only batch operators run an OPP daemon/) }) it("throws when the operator daemon artifacts have not been prepared yet", () => { const ctx = fixtureContext() const account = fixtureOperatorAccount("batchopbbbb", OperatorType.BATCH) - const node = testNode(ctx, NodeRole.batch_operator, 4, "node_04", [], "batchopbbbb") + const node = testNode( + ctx, + NodeRole.batch_operator, + 4, + "node_04", + [], + "batchopbbbb" + ) expect(() => Steps.processes.nodeop.resolveOperatorDaemonArgs(ctx, node, account) ).toThrow(/Missing asserted output/) diff --git a/packages/cluster-tool/tests/tools/ethereum/EthereumCollateralTool.test.ts b/packages/cluster-tool/tests/tools/ethereum/EthereumCollateralTool.test.ts deleted file mode 100644 index 4144cc41a..000000000 --- a/packages/cluster-tool/tests/tools/ethereum/EthereumCollateralTool.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import Fs from "node:fs" -import Os from "node:os" -import Path from "node:path" -import { OperatorType } from "@wireio/opp-typescript-models" -import { EthereumCollateralTool } from "@wireio/cluster-tool/tools/ethereum" -import { Report } from "@wireio/cluster-tool/report" -import { fixtureContext } from "../../config/clusterBuildContextFixture.js" - -describe("EthereumCollateralTool.mockErc20Address", () => { - let deploymentsPath: string - beforeAll(() => { - deploymentsPath = Fs.mkdtempSync(Path.join(Os.tmpdir(), "eth-deployments-")) - Fs.writeFileSync( - Path.join(deploymentsPath, "outpost-addrs.json"), - JSON.stringify({ - MockUsdc: "0x00000000000000000000000000000000000000a1", - MockUsdt: "0x00000000000000000000000000000000000000a2", - OperatorRegistry: "0x00000000000000000000000000000000000000a3" - }) - ) - }) - afterAll(() => { - Fs.rmSync(deploymentsPath, { recursive: true, force: true }) - }) - - it("resolves USDC / USDT to their deployed mock addresses", () => { - expect(EthereumCollateralTool.mockErc20Address(deploymentsPath, "USDC")).toBe( - "0x00000000000000000000000000000000000000a1" - ) - expect(EthereumCollateralTool.mockErc20Address(deploymentsPath, "USDT")).toBe( - "0x00000000000000000000000000000000000000a2" - ) - }) - - it("throws LOUDLY for a token with no deployed mock (never a silent skip)", () => { - expect(() => - EthereumCollateralTool.mockErc20Address(deploymentsPath, "LIQETH") - ).toThrow(/no deployed mock ERC-20 for LIQETH/) - }) - - it("throws when the deploy artifacts are absent entirely", () => { - expect(() => - EthereumCollateralTool.mockErc20Address("/no/such/deployments", "USDC") - ).toThrow(/outpost addresses not found/) - }) -}) - -describe("EthereumCollateralTool.planNonNativeDeposit", () => { - it("captures the full typed input and binds the named runner", () => { - const step = EthereumCollateralTool.planNonNativeDeposit( - Report.Actor.Underwriter, - "deposit-usdc", - "Underwriter bonds USDC collateral", - {}, - "uwrit.a", - 2n, - 7n, - 3n, - OperatorType.UNDERWRITER, - 1_000_000n - ) - expect(step.input).toEqual({ - kind: "EthereumCollateralTool.DepositNonNativeInput", - operatorLabel: "uwrit.a", - chainCode: 2n, - tokenCode: 7n, - reserveCode: 3n, - operatorType: OperatorType.UNDERWRITER, - amount: 1_000_000n - }) - expect(step.runner).toBe(EthereumCollateralTool.runNonNativeDeposit) - }) - - it("runner rejects a non-positive amount before touching any client", async () => { - // The amount guard fires before any client getter is touched. - const ctx = fixtureContext() - await expect( - EthereumCollateralTool.runNonNativeDeposit( - ctx, - { - kind: "EthereumCollateralTool.DepositNonNativeInput", - operatorLabel: "uwrit.a", - chainCode: 2n, - tokenCode: 7n, - reserveCode: 3n, - operatorType: OperatorType.UNDERWRITER, - amount: 0n - }, - new AbortController().signal - ) - ).rejects.toThrow(/amount must be positive/) - }) -}) diff --git a/packages/cluster-tool/tests/tools/ethereum/EthereumFundingTool.test.ts b/packages/cluster-tool/tests/tools/ethereum/EthereumFundingTool.test.ts deleted file mode 100644 index cfea617b2..000000000 --- a/packages/cluster-tool/tests/tools/ethereum/EthereumFundingTool.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { EthereumFundingTool } from "@wireio/cluster-tool/tools/ethereum" -import { Report } from "@wireio/cluster-tool/report" - -describe("EthereumFundingTool step factories", () => { - describe("mintErc20", () => { - it("builds a mint Step carrying the operator + token + amount input", () => { - const step = EthereumFundingTool.planErc20Mint( - Report.Actor.Underwriter, - "uwa-usdc-mint", - "mint usdc to uwa", - {}, - "uwa", - "USDC", - 42n - ) - expect(step.actor).toBe(Report.Actor.Underwriter) - expect(step.name).toBe("uwa-usdc-mint") - expect(step.input.kind).toBe("EthereumFundingTool.MintErc20Input") - expect(step.input.operatorLabel).toBe("uwa") - expect(step.input.tokenName).toBe("USDC") - expect(step.input.amount).toBe(42n) - }) - }) -}) diff --git a/packages/cluster-tool/tests/tools/ethereum/EthereumSwapTool.test.ts b/packages/cluster-tool/tests/tools/ethereum/EthereumSwapTool.test.ts deleted file mode 100644 index ee045c63c..000000000 --- a/packages/cluster-tool/tests/tools/ethereum/EthereumSwapTool.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { EthereumLocalReserveStatus } from "@wireio/cluster-tool/tools/ethereum" - -describe("EthereumSwapTool", () => { - describe("EthereumLocalReserveStatus", () => { - it("mirrors ReserveManager.sol::LocalReserveStatus's zero-indexed values", () => { - // Deliberately ≠ the proto ReserveStatus (UNKNOWN=0, PENDING=1, ...) — - // shifting these by one breaks every outpost-local status comparison. - expect(EthereumLocalReserveStatus.PENDING).toBe(0) - expect(EthereumLocalReserveStatus.ACTIVE).toBe(1) - expect(EthereumLocalReserveStatus.CANCELLED).toBe(2) - }) - }) -}) diff --git a/packages/cluster-tool/tests/tools/solana/SolanaCollateralTool.test.ts b/packages/cluster-tool/tests/tools/solana/SolanaCollateralTool.test.ts deleted file mode 100644 index 794c4d08f..000000000 --- a/packages/cluster-tool/tests/tools/solana/SolanaCollateralTool.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { PublicKey } from "@solana/web3.js" -import { SolanaCollateralTool } from "@wireio/cluster-tool/tools/solana" - -/** - * The seed contract these helpers mirror lives in - * `wire-solana/programs/liqsol-core/src/states/opp_states.rs` - * (`COLLATERAL_POSITION_SEED`) and the `Deposit` / `DepositNonNative` account - * structs in `.../instructions/opp/deposit.rs`. The program is built with - * Anchor `resolution = false`, so NOTHING is auto-derived from the IDL — every - * account is supplied explicitly and these derivations are load-bearing. - */ -describe("SolanaCollateralTool.collateralPositionPda", () => { - const programId = PublicKey.unique() - const operator = PublicKey.unique() - /** An arbitrary fixed token code — the derivation is value-agnostic. ASCII "SOL", NOT SlugName.from("SOL"). */ - const tokenCode = 0x534f4cn - - it("matches an independent derivation of the on-chain seed list", () => { - const tokenCodeLeBytes = Buffer.alloc(8) - tokenCodeLeBytes.writeBigUInt64LE(tokenCode) - const [expected] = PublicKey.findProgramAddressSync( - [ - Buffer.from("collateral_position"), - operator.toBuffer(), - tokenCodeLeBytes - ], - programId - ) - expect( - SolanaCollateralTool.collateralPositionPda( - programId, - operator, - tokenCode - ).toBase58() - ).toBe(expected.toBase58()) - }) - - it("is deterministic for the same (operator, tokenCode)", () => { - expect( - SolanaCollateralTool.collateralPositionPda( - programId, - operator, - tokenCode - ).toBase58() - ).toBe( - SolanaCollateralTool.collateralPositionPda( - programId, - operator, - tokenCode - ).toBase58() - ) - }) - - it("gives a DISTINCT position per operator and per tokenCode", () => { - const otherOperator = PublicKey.unique() - const base = SolanaCollateralTool.collateralPositionPda( - programId, - operator, - tokenCode - ) - expect( - SolanaCollateralTool.collateralPositionPda( - programId, - otherOperator, - tokenCode - ).toBase58() - ).not.toBe(base.toBase58()) - expect( - SolanaCollateralTool.collateralPositionPda( - programId, - operator, - tokenCode + 1n - ).toBase58() - ).not.toBe(base.toBase58()) - }) - - it("is scoped to the program id", () => { - expect( - SolanaCollateralTool.collateralPositionPda( - PublicKey.unique(), - operator, - tokenCode - ).toBase58() - ).not.toBe( - SolanaCollateralTool.collateralPositionPda( - programId, - operator, - tokenCode - ).toBase58() - ) - }) -}) diff --git a/packages/cluster-tool/tests/tools/wire/OperatorDaemonTool.test.ts b/packages/cluster-tool/tests/tools/wire/OperatorDaemonTool.test.ts index c9672b704..29a40322b 100644 --- a/packages/cluster-tool/tests/tools/wire/OperatorDaemonTool.test.ts +++ b/packages/cluster-tool/tests/tools/wire/OperatorDaemonTool.test.ts @@ -39,7 +39,8 @@ import type { ExternalOutpostConfig } from "@wireio/cluster-tool-shared" import { fixtureOperatorAccount } from "../../orchestration/outputs/operatorAccountFixture.js" /** anvil's deterministic mnemonic — HD-derived wallets are stable + well-known. */ -const AnvilMnemonic = "test test test test test test test test test test test junk" +const AnvilMnemonic = + "test test test test test test test test test test test junk" function operatorAccount(label: string, type: OperatorType): OperatorAccount { const wallet = ethers.HDNodeWallet.fromMnemonic( @@ -52,7 +53,11 @@ function operatorAccount(label: string, type: OperatorType): OperatorAccount { publicationLabel: label, account: `wireno.${label}`, type, - wire: { type: KeyType.K1, publicKey: `PUB_K1_${label}`, privateKey: `PVT_K1_${label}` }, + wire: { + type: KeyType.K1, + publicKey: `PUB_K1_${label}`, + privateKey: `PVT_K1_${label}` + }, ethereum: ethereumKeyPairFromWallet(wallet), solana: { type: KeyType.ED, @@ -63,7 +68,10 @@ function operatorAccount(label: string, type: OperatorType): OperatorAccount { } const artifacts: OperatorDaemonArtifacts = { - ethereumAbiFiles: ["/cluster/data/eth-abis/OPP.json", "/cluster/data/eth-abis/OPPInbound.json"], + ethereumAbiFiles: [ + "/cluster/data/eth-abis/OPP.json", + "/cluster/data/eth-abis/OPPInbound.json" + ], ethereumAddresses: { OPP: "0x1111111111111111111111111111111111111111", OPPInbound: "0x2222222222222222222222222222222222222222", @@ -210,7 +218,7 @@ describe("OperatorDaemonTool", () => { ).toBe(ExternalChainId) }) - it("carries the resolved endpoints into the daemon argv (batch + underwriter)", () => { + it("carries the resolved endpoints into the daemon argv (batch)", () => { const network = OperatorDaemonTool.networkFromConfig( fixtureConfig({ externalOutposts: externalOutposts( @@ -224,13 +232,6 @@ describe("OperatorDaemonTool", () => { artifacts, network, keySourceFor - ), - underwriter = operatorAccount("uwritbbbbbb", OperatorType.UNDERWRITER), - underwriterArgs = OperatorDaemonTool.underwriterArgs( - underwriter, - artifacts, - network, - keySourceFor ) expect(valuesOf(batchArgs, "--outpost-ethereum-client")).toEqual([ `${OperatorDaemonTool.EthereumClientId},eth-wireno.batchopcccc,${ExternalEthereumRpcUrl},${ExternalChainId}` @@ -238,12 +239,6 @@ describe("OperatorDaemonTool", () => { expect(valuesOf(batchArgs, "--outpost-solana-client")).toEqual([ `${OperatorDaemonTool.SolanaClientId},sol-wireno.batchopcccc,${ExternalSolanaRpcUrl}` ]) - expect(valuesOf(underwriterArgs, "--outpost-ethereum-client")).toEqual([ - `${OperatorDaemonTool.EthereumClientId},eth-${underwriter.account},${ExternalEthereumRpcUrl},${ExternalChainId}` - ]) - expect(valuesOf(underwriterArgs, "--outpost-solana-client")).toEqual([ - `${OperatorDaemonTool.SolanaClientId},sol-${underwriter.account},${ExternalSolanaRpcUrl}` - ]) }) }) @@ -284,49 +279,24 @@ describe("OperatorDaemonTool", () => { recoverySpy.mockRestore() } }) - - it("composes an UNDERWRITER-role node for an underwriter operator", async () => { - // The mirrored arm: `daemonNodeConfig` picks the role off the operator's - // type, so a wrong pick would silently give an underwriter the batch - // plugin set (and vice versa). - const ctx = fixtureContext() - ProcessManager.setClusterPath(ctx.config.clusterPath) - const operator = operatorAccount("uwritdddddd", OperatorType.UNDERWRITER) - ctx.keyStore.setOperator(operator) - ctx.outputs.set(OperatorDaemonArtifactsKey, artifacts) - const recoverySpy = jest - .spyOn(NodeopProcess, "startWithRecovery") - .mockResolvedValue(undefined) - try { - await OperatorDaemonTool.runDaemonStart( - ctx, - { kind: "OperatorDaemonTool.StartDaemonInput", label: "uwritdddddd" }, - new AbortController().signal - ) - expect(recoverySpy).toHaveBeenCalledWith( - ctx.processManager, - expect.objectContaining({ - operators: [operator], - node: expect.objectContaining({ role: NodeRole.underwriter }), - extraArgs: expect.arrayContaining(["--underwriter-account"]) - }) - ) - expect(recoverySpy.mock.calls[0][1].postBootstrap).toBeUndefined() - } finally { - recoverySpy.mockRestore() - } - }) }) describe("batchOperatorArgs", () => { const operator = fixtureOperatorAccount("batchopaaaa", OperatorType.BATCH) - const args = OperatorDaemonTool.batchOperatorArgs(operator, artifacts, network, keySourceFor) + const args = OperatorDaemonTool.batchOperatorArgs( + operator, + artifacts, + network, + keySourceFor + ) it("loads the batch plugin set at irreversible read-mode", () => { expect(valuesOf(args, `--${Constants.READ_MODE_OPTION}`)).toEqual([ NodeopReadMode.irreversible ]) - expect(valuesOf(args, "--plugin")).toEqual([...OperatorDaemonTool.BatchOperatorPlugins]) + expect(valuesOf(args, "--plugin")).toEqual([ + ...OperatorDaemonTool.BatchOperatorPlugins + ]) }) it("drops the external-debugging plugin AND --ext-debugging-server when the debugging server is disabled", () => { @@ -355,7 +325,9 @@ describe("OperatorDaemonTool", () => { // + the ETH and SOL outpost providers, named per-operator expect(providers.length).toBe(3) // Provider NAMES are built from the CHAIN account, not the durable handle. - expect(providers[1]).toMatch(/^eth-wireno\.batchopaaaa,ethereum,ethereum,0x[0-9a-fA-F]{128},KEY:0x/) + expect(providers[1]).toMatch( + /^eth-wireno\.batchopaaaa,ethereum,ethereum,0x[0-9a-fA-F]{128},KEY:0x/ + ) expect(providers[2]).toMatch(/^sol-wireno\.batchopaaaa,solana,solana,/) }) @@ -363,11 +335,21 @@ describe("OperatorDaemonTool", () => { // The depot matches this argv against `sysio.opreg::operators`, which is // keyed by the ON-CHAIN account — passing the handle would start a daemon // that silently matches no operator row. - expect(valuesOf(args, "--batch-operator-account")).toEqual([operator.account]) - expect(valuesOf(args, "--batch-operator-account")).not.toEqual([operator.label]) - expect(valuesOf(args, "--batch-epoch-poll-ms")).toEqual([String(OperatorDaemonTool.BatchEpochPollMs)]) - expect(valuesOf(args, "--batch-delivery-timeout-ms")).toEqual([String(OperatorDaemonTool.BatchDeliveryTimeoutMs)]) - expect(valuesOf(args, "--ext-debugging-server")).toEqual([network.debuggingServerUrl]) + expect(valuesOf(args, "--batch-operator-account")).toEqual([ + operator.account + ]) + expect(valuesOf(args, "--batch-operator-account")).not.toEqual([ + operator.label + ]) + expect(valuesOf(args, "--batch-epoch-poll-ms")).toEqual([ + String(OperatorDaemonTool.BatchEpochPollMs) + ]) + expect(valuesOf(args, "--batch-delivery-timeout-ms")).toEqual([ + String(OperatorDaemonTool.BatchDeliveryTimeoutMs) + ]) + expect(valuesOf(args, "--ext-debugging-server")).toEqual([ + network.debuggingServerUrl + ]) // The client id IS the chain code: both daemons look a chain's RPC client // up under its `sysio.chains` code, so any other id is invisible to them. expect(valuesOf(args, "--outpost-ethereum-client")).toEqual([ @@ -376,8 +358,12 @@ describe("OperatorDaemonTool", () => { expect(valuesOf(args, "--outpost-solana-client")).toEqual([ `SOLANA,sol-${operator.account},${network.solanaRpcUrl}` ]) - expect(valuesOf(args, "--ethereum-abi-file")).toEqual(artifacts.ethereumAbiFiles) - expect(valuesOf(args, "--solana-idl-file")).toEqual([artifacts.solanaIdlFile]) + expect(valuesOf(args, "--ethereum-abi-file")).toEqual( + artifacts.ethereumAbiFiles + ) + expect(valuesOf(args, "--solana-idl-file")).toEqual([ + artifacts.solanaIdlFile + ]) // The cleanroom hosts the outpost interface in liqsol_core — nodeop's // IDL-name gate must be pointed at it. expect(valuesOf(args, "--solana-outpost-program-name")).toEqual([ @@ -453,66 +439,6 @@ describe("OperatorDaemonTool", () => { }) }) - describe("underwriterArgs", () => { - const operator = fixtureOperatorAccount("uwritaaaaaa", OperatorType.UNDERWRITER) - const args = OperatorDaemonTool.underwriterArgs(operator, artifacts, network, keySourceFor) - - it("passes the SCALED action timeout (flow timing scale reaches the daemon)", () => { - process.env.WIRE_FLOW_TIMEOUT_SCALE = "4" - try { - const scaled = OperatorDaemonTool.underwriterArgs(operator, artifacts, network, keySourceFor) - expect(valuesOf(scaled, "--underwriter-action-timeout-ms")).toEqual([ - String(OperatorDaemonTool.UnderwriterActionTimeoutMs * 4) - ]) - } finally { - delete process.env.WIRE_FLOW_TIMEOUT_SCALE - } - }) - - it("runs the underwriter daemon at irreversible read-mode", () => { - expect(valuesOf(args, `--${Constants.READ_MODE_OPTION}`)).toEqual([ - NodeopReadMode.irreversible - ]) - }) - - it("loads the underwriter plugin set + source-deposit verification targets", () => { - expect(valuesOf(args, "--plugin")).toEqual([...OperatorDaemonTool.UnderwriterPlugins]) - // Same chain-boundary rule as `--batch-operator-account`. - expect(valuesOf(args, "--underwriter-account")).toEqual([operator.account]) - expect(valuesOf(args, "--underwriter-account")).not.toEqual([operator.label]) - expect(valuesOf(args, "--underwriter-eth-source-deposit-function")).toEqual(["requestSwap"]) - expect(valuesOf(args, "--underwriter-sol-source-deposit-instruction")).toEqual(["request_swap"]) - expect(valuesOf(args, "--solana-idl-file")).toEqual([artifacts.solanaIdlFile]) - expect(valuesOf(args, "--solana-outpost-program-name")).toEqual([ - SolanaOutpostProgramTool.ProgramName - ]) - }) - - it("drops the external-debugging plugin AND --ext-debugging-server when the debugging server is disabled", () => { - const disabledArgs = OperatorDaemonTool.underwriterArgs( - operator, - artifacts, - { ...network, debuggingServerEnabled: false }, - keySourceFor - ) - expect(valuesOf(disabledArgs, "--plugin")).toEqual( - OperatorDaemonTool.UnderwriterPlugins.filter( - plugin => plugin !== OperatorDaemonTool.ExternalDebuggingPlugin - ) - ) - expect(valuesOf(disabledArgs, "--ext-debugging-server")).toEqual([]) - }) - - it("emits no per-chain outpost flags (nodeop rejects them)", () => { - // The underwriter serves every ACTIVE sysio.chains row, reads each one's - // contract addresses off that row, and reaches it through the RPC client - // registered under the chain code — nothing left to declare per node. - expect(valuesOf(args, "--underwriter-enabled")).toEqual([]) - expect(valuesOf(args, "--underwriter-eth-outpost")).toEqual([]) - expect(valuesOf(args, "--underwriter-sol-outpost")).toEqual([]) - }) - }) - describe("runArtifactPreparation", () => { let dir: string beforeAll(() => { @@ -537,17 +463,32 @@ describe("OperatorDaemonTool", () => { BAR: "0xbbb0000000000000000000000000000000000bbb" }) ) - const oppArtifactDir = Path.join(ethereumPath, "artifacts", "contracts", "outpost", "OPP.sol") + const oppArtifactDir = Path.join( + ethereumPath, + "artifacts", + "contracts", + "outpost", + "OPP.sol" + ) Fs.mkdirSync(oppArtifactDir, { recursive: true }) Fs.writeFileSync( Path.join(oppArtifactDir, "OPP.json"), JSON.stringify({ abi: [{ type: "event", name: "OPPEnvelope" }] }) ) - const barArtifactDir = Path.join(ethereumPath, "artifacts", "contracts", "outpost", "BAR.sol") + const barArtifactDir = Path.join( + ethereumPath, + "artifacts", + "contracts", + "outpost", + "BAR.sol" + ) Fs.mkdirSync(barArtifactDir, { recursive: true }) Fs.writeFileSync( Path.join(barArtifactDir, "BARV2.json"), - JSON.stringify({ contractName: "BARV2", abi: [{ type: "event", name: "NodeCommitted" }] }) + JSON.stringify({ + contractName: "BARV2", + abi: [{ type: "event", name: "NodeCommitted" }] + }) ) // SOL fixtures: committed liqsol_core program keypair + generated IDL // (metadata.name = liqsol_core; instructions cover the daemon-invoked @@ -563,7 +504,9 @@ describe("OperatorDaemonTool", () => { Path.join(solanaPath, "target", "idl", "liqsol_core.json"), JSON.stringify({ metadata: { name: "liqsol_core" }, - instructions: OperatorDaemonTool.RequiredSolanaIdlInstructions.map(name => ({ name })) + instructions: OperatorDaemonTool.RequiredSolanaIdlInstructions.map( + name => ({ name }) + ) }) ) @@ -576,18 +519,28 @@ describe("OperatorDaemonTool", () => { ethereumPath, solanaPath }) - await OperatorDaemonTool.runArtifactPreparation(ctx, null, new AbortController().signal) + await OperatorDaemonTool.runArtifactPreparation( + ctx, + null, + new AbortController().signal + ) const prepared = ctx.outputs.assert(OperatorDaemonArtifactsKey) expect(prepared.solanaProgramId).toBe(programKeypair.publicKey.toBase58()) expect(Fs.existsSync(prepared.solanaIdlFile)).toBe(true) // Verbatim copy under the liqsol_core filename — metadata.name is NOT // rewritten (nodeop is pointed at it via --solana-outpost-program-name). - expect(Path.basename(prepared.solanaIdlFile)).toBe(OperatorDaemonTool.SolanaIdlFilename) - const copiedIdl = JSON.parse(Fs.readFileSync(prepared.solanaIdlFile, "utf-8")) + expect(Path.basename(prepared.solanaIdlFile)).toBe( + OperatorDaemonTool.SolanaIdlFilename + ) + const copiedIdl = JSON.parse( + Fs.readFileSync(prepared.solanaIdlFile, "utf-8") + ) expect(copiedIdl.metadata.name).toBe(SolanaOutpostProgramTool.ProgramName) expect(prepared.ethereumAbiFiles.length).toBe(2) - const abi = JSON.parse(Fs.readFileSync(prepared.ethereumAbiFiles[0], "utf-8")) + const abi = JSON.parse( + Fs.readFileSync(prepared.ethereumAbiFiles[0], "utf-8") + ) expect(abi).toEqual({ contractName: "OPP", address: "0xaaa0000000000000000000000000000000000aaa", @@ -612,7 +565,13 @@ describe("OperatorDaemonTool", () => { Path.join(ethereumDeploymentsPath, "outpost-addrs.json"), JSON.stringify({ OPP: "0xaaa0000000000000000000000000000000000aaa" }) ) - const oppArtifactDir = Path.join(ethereumPath, "artifacts", "contracts", "outpost", "OPP.sol") + const oppArtifactDir = Path.join( + ethereumPath, + "artifacts", + "contracts", + "outpost", + "OPP.sol" + ) Fs.mkdirSync(oppArtifactDir, { recursive: true }) Fs.writeFileSync( Path.join(oppArtifactDir, "OPP.json"), @@ -641,7 +600,11 @@ describe("OperatorDaemonTool", () => { solanaPath }) await expect( - OperatorDaemonTool.runArtifactPreparation(ctx, null, new AbortController().signal) + OperatorDaemonTool.runArtifactPreparation( + ctx, + null, + new AbortController().signal + ) ).rejects.toThrow(/missing the 'epoch_in' instruction/) }) }) diff --git a/packages/cluster-tool/tests/tools/wire/WireReserveTool.test.ts b/packages/cluster-tool/tests/tools/wire/WireReserveTool.test.ts deleted file mode 100644 index 5d704a645..000000000 --- a/packages/cluster-tool/tests/tools/wire/WireReserveTool.test.ts +++ /dev/null @@ -1,563 +0,0 @@ -import { SlugName, SysioContracts } from "@wireio/sdk-core" -import type { WireClient } from "@wireio/cluster-tool/clients/wire" -import { WireReserveTool } from "@wireio/cluster-tool/tools/wire" - -const { SysioContractName } = SysioContracts -const { - BpsTotal, - cpOutput, - outGivenIn, - quoteSwap, - splitWireFee, - swapquote, - SymmetricConnectorWeightBps, - tokenToWire, - varianceDrift, - wireToToken -} = WireReserveTool - -/** The old dev-cluster fee the recorded SwapFeeMath assertions were baselined on. */ -const LegacySwapFeeBps = 10 - -/** - * A code cell as the depot's ABI renders it. Indexed off the generated row so it - * tracks the ABI: `slug_name` is a builtin now, so a code arrives as its canonical - * spelling and no per-contract `Sysio*SlugNameType` wrapper is generated. - */ -type ReserveSlugCode = SysioContracts.SysioReservReserveRowType["chain_code"] - -/** A minimal reserves row carrying only the fields swapquote consults. */ -interface QuoteReserveFixture { - chain_code: ReserveSlugCode - token_code: ReserveSlugCode - reserve_code: ReserveSlugCode - reserve_chain_amount: number - reserve_wire_amount: number - connector_weight_bps: number - /** The reserve owner's fee on the WIRE leg — `0` for a public reserve. */ - owner_fee_bps: number -} - -const EthereumChainSlug = "ETHEREUM", - EthTokenSlug = "ETH", - SolanaChainSlug = "SOLANA", - SolTokenSlug = "SOL", - PrimaryReserveSlug = "PRIMARY", - EthereumChain = SlugName.from(EthereumChainSlug), - EthToken = SlugName.from(EthTokenSlug), - SolanaChain = SlugName.from(SolanaChainSlug), - SolToken = SlugName.from(SolTokenSlug), - PrimaryReserve = SlugName.from(PrimaryReserveSlug) - -/** A WireClient stub whose reserv/uwrit typed accessors serve the fixtures. */ -function stubWire(reserves: QuoteReserveFixture[], feeBps = 30): WireClient { - const table = (rows: Row[]) => ({ - query: async () => ({ rows, more: false }) - }) - const clientByName = { - [SysioContractName.reserv]: { tables: { reserves: table(reserves) } }, - [SysioContractName.uwrit]: { - tables: { - uwconfig: table([{ fee_bps: feeBps, collateral_lock_duration_ms: 0 }]) - } - } - } - return { - getSysioContract: (name: SysioContracts.SysioContractName) => - clientByName[name] - } as WireClient -} - -describe("WireReserveTool", () => { - describe("cpOutput", () => { - it("matches the depot's constant-product floor math", () => { - // 1e10 books, 1e8 in → floor(1e10 × 1e8 / (1e10 + 1e8)) = 99_009_900 - expect(cpOutput(10_000_000_000n, 10_000_000_000n, 100_000_000n)).toBe( - 99_009_900n - ) - }) - it("is 0n when any side is empty", () => { - expect(cpOutput(0n, 10n, 5n)).toBe(0n) - expect(cpOutput(10n, 0n, 5n)).toBe(0n) - expect(cpOutput(10n, 10n, 0n)).toBe(0n) - }) - }) - - describe("splitWireFee (the recorded SwapFeeMath assertions)", () => { - it("0.1% fee with the 50/50 underwriter / batch-operator split", () => { - const fee = splitWireFee(99_009_900n, LegacySwapFeeBps) - expect(fee.fee).toBe(99_009n) - expect(fee.net).toBe(98_910_891n) - expect(fee.underwriterShare).toBe(49_504n) - expect(fee.rewardShare).toBe(49_505n) - }) - it("holds the exact-integer invariants across amounts", () => { - const amounts = [ - 1n, - 7n, - 1_000_000n, - 99_009_900n, - 100_969_310n, - 80_000_000_000n - ] - amounts.forEach(amount => { - const fee = splitWireFee(amount, LegacySwapFeeBps) - expect(fee.underwriterShare + fee.rewardShare).toBe(fee.fee) - expect(fee.net + fee.fee).toBe(amount) - }) - }) - it("floors toward zero", () => { - expect(splitWireFee(5_000n, LegacySwapFeeBps).fee).toBe(5n) - expect(splitWireFee(5_001n, LegacySwapFeeBps).fee).toBe(5n) - }) - it("honours an explicit fee + underwriter share", () => { - const fee = splitWireFee(1_000_000n, 100, BpsTotal) - expect(fee.fee).toBe(10_000n) - expect(fee.underwriterShare).toBe(10_000n) - expect(fee.rewardShare).toBe(0n) - expect(fee.net).toBe(990_000n) - }) - it("a zero underwriter share sends the whole fee to rewards (the revert path)", () => { - // `sysio.reserv::refundwire` passes 0 — a revert has no winning - // underwriter, so nothing accrues to `uwfees`. - const fee = splitWireFee(1_000_000n, 100, 0) - expect(fee.fee).toBe(10_000n) - expect(fee.underwriterShare).toBe(0n) - expect(fee.rewardShare).toBe(10_000n) - }) - it("defaults to a zero emissions share — no fee leaves custody", () => { - const fee = splitWireFee(1_000_000n, 1_000) - expect(fee.emissionsShare).toBe(0n) - expect(fee.underwriterShare).toBe(50_000n) - expect(fee.rewardShare).toBe(50_000n) - }) - it("the emissions share divides the REWARDS POOL, not the whole fee", () => { - // fee 100_000 → underwriter 50_000, pool 50_000; 40% OF THE POOL = 20_000. - const fee = splitWireFee(1_000_000n, 1_000, 5_000, 4_000) - expect(fee.underwriterShare).toBe(50_000n) - expect(fee.emissionsShare).toBe(20_000n) - expect(fee.rewardShare).toBe(30_000n) - }) - it("a full emissions share leaves batch operators nothing, underwriter untouched", () => { - const fee = splitWireFee(1_000_000n, 1_000, 5_000, BpsTotal) - expect(fee.underwriterShare).toBe(50_000n) - expect(fee.emissionsShare).toBe(50_000n) - expect(fee.rewardShare).toBe(0n) - }) - it("charges each participating reserve's owner fee off the same gross leg", () => { - // WIRE-281 "per reserve": a chain-to-chain swap pays THREE fees. All rates - // apply to the gross leg, so they are additive and order-independent. - // 1_000_000 leg: network 10% = 100_000; src 1% = 10_000; dst 2% = 20_000. - const fee = splitWireFee(1_000_000n, 1_000, 5_000, 0, 100, 200) - expect(fee.srcReserveShare).toBe(10_000n) - expect(fee.dstReserveShare).toBe(20_000n) - expect(fee.underwriterShare).toBe(50_000n) - expect(fee.rewardShare).toBe(50_000n) - expect(fee.fee).toBe(130_000n) - expect(fee.net).toBe(870_000n) - }) - it("a single-reserve path charges only its own side", () => { - // paywire: source reserve only. applyfromwire: destination only. - const source = splitWireFee(1_000_000n, 1_000, 5_000, 0, 100, 0) - expect(source.srcReserveShare).toBe(10_000n) - expect(source.dstReserveShare).toBe(0n) - const destination = splitWireFee(1_000_000n, 1_000, 5_000, 0, 0, 200) - expect(destination.srcReserveShare).toBe(0n) - expect(destination.dstReserveShare).toBe(20_000n) - }) - it("defaults both reserve rates to zero — a fee-free reserve pair", () => { - const fee = splitWireFee(1_000_000n, 1_000, 5_000) - expect(fee.srcReserveShare).toBe(0n) - expect(fee.dstReserveShare).toBe(0n) - expect(fee.fee).toBe(100_000n) - }) - it("saturates net at zero rather than going negative when rates fill the leg", () => { - const fee = splitWireFee(1_000n, BpsTotal, 5_000, 0, BpsTotal, BpsTotal) - expect(fee.net).toBe(0n) - }) - it("holds five-way conservation with reserve fees stacked on", () => { - const amounts = [1n, 7n, 999n, 1_000_003n, 1_000_000_000_000n] - amounts.forEach(amount => { - const fee = splitWireFee(amount, 137, 5_000, 3_333, 97, 211) - expect( - fee.underwriterShare + - fee.rewardShare + - fee.emissionsShare + - fee.srcReserveShare + - fee.dstReserveShare - ).toBe(fee.fee) - expect(fee.net + fee.fee).toBe(amount) - }) - }) - it("holds three-way conservation across odd amounts and shares", () => { - const emissionsShares = [0, 1, 3_333, 5_000, 9_999, BpsTotal], - amounts = [1n, 7n, 999n, 1_000_003n, 1_000_000_000_000n] - emissionsShares.forEach(emissions => - amounts.forEach(amount => { - const fee = splitWireFee(amount, 137, 5_000, emissions) - expect( - fee.underwriterShare + fee.rewardShare + fee.emissionsShare - ).toBe(fee.fee) - expect(fee.net + fee.fee).toBe(amount) - }) - ) - }) - it("clamps bps into [0, 10000]", () => { - expect(splitWireFee(1_000n, -5).fee).toBe(0n) - expect(splitWireFee(1_000n, 20_000).fee).toBe(1_000n) - // The share bps clamp the same way, on both ends. - expect(splitWireFee(1_000_000n, BpsTotal, -5).underwriterShare).toBe(0n) - expect(splitWireFee(1_000_000n, BpsTotal, 20_000).underwriterShare).toBe(1_000_000n) - }) - it("zero amount yields all-zero shares", () => { - const fee = splitWireFee(0n, LegacySwapFeeBps) - expect(fee.fee).toBe(0n) - expect(fee.underwriterShare).toBe(0n) - expect(fee.rewardShare).toBe(0n) - expect(fee.net).toBe(0n) - }) - }) - - describe("varianceDrift", () => { - it("floors target × bps / 10000", () => { - expect(varianceDrift(98_000_000n, 200)).toBe(1_960_000n) - expect(varianceDrift(3n, 500)).toBe(0n) - }) - }) - - describe("toDepot / fromDepot (per-token depot precision = min(native, 9))", () => { - it("depotPrecision caps at 9 and passes sub-cap precision through", () => { - expect(WireReserveTool.depotPrecision(6)).toBe(6) - expect(WireReserveTool.depotPrecision(9)).toBe(9) - expect(WireReserveTool.depotPrecision(18)).toBe(9) - }) - - it("carries an at-or-below-cap token at NATIVE precision (6-dec identity)", () => { - expect(WireReserveTool.toDepot(100_000n, 6)).toBe(100_000n) - expect(WireReserveTool.fromDepot(100_000n, 6)).toBe(100_000n) - }) - - it("is identity at exactly 9 decimals (lamports)", () => { - expect(WireReserveTool.toDepot(10_000_000_000n, 9)).toBe(10_000_000_000n) - expect(WireReserveTool.fromDepot(10_000_000_000n, 9)).toBe( - 10_000_000_000n - ) - }) - - it("downscales an above-cap token (18-dec wei → ÷1e9, floored)", () => { - expect(WireReserveTool.toDepot(1_500_000_000_999_999_999n, 18)).toBe( - 1_500_000_000n - ) - }) - - it("fromDepot upscales an above-cap token (18-dec wei → ×1e9)", () => { - expect(WireReserveTool.fromDepot(4_754_411_063n, 18)).toBe( - 4_754_411_063_000_000_000n - ) - }) - - it("rejects a zero / non-integer decimals argument", () => { - expect(() => WireReserveTool.toDepot(1n, 0)).toThrow( - /invalid native decimals/ - ) - expect(() => WireReserveTool.fromDepot(1n, 1.5)).toThrow( - /invalid native decimals/ - ) - expect(() => WireReserveTool.depotPrecision(-1)).toThrow( - /invalid native decimals/ - ) - }) - }) - - describe("readFeeBps", () => { - it("reads the live uwconfig singleton", async () => { - await expect(WireReserveTool.readFeeBps(stubWire([], 30))).resolves.toBe( - 30 - ) - }) - }) - - // Every expected value below was produced by COMPILING - // `sysio.opp.common/amm_math.hpp` and printing `out_given_in` for the same - // inputs — not derived by hand. A mirror that merely approximates the depot - // is not a mirror; these pin bit-identity, including the Q60 log2/exp2 path. - describe("outGivenIn (the sysio::opp::amm::out_given_in mirror)", () => { - const Balance = 10_000_000_000n, - AmountIn = 100_000_000n - - it("takes the EXACT constant-product path at equal weights", () => { - expect( - outGivenIn(Balance, 5_000n, Balance, 5_000n, AmountIn) - ).toBe(99_009_900n) - // The symmetric case must agree with the pure integer curve exactly. - expect(outGivenIn(Balance, 5_000n, Balance, 5_000n, AmountIn)).toBe( - cpOutput(Balance, Balance, AmountIn) - ) - }) - - it.each([ - [2_000n, 8_000n, 24_844_912n], - [8_000n, 2_000n, 390_196_555n], - [1_000n, 9_000n, 11_049_813n], - [9_000n, 1_000n, 856_601_757n], - [2_500n, 7_500n, 33_112_825n] - ])( - "matches the contract at wIn=%s wOut=%s", - (weightIn, weightOut, expected) => { - expect( - outGivenIn(Balance, weightIn, Balance, weightOut, AmountIn) - ).toBe(expected) - } - ) - - it("matches the contract at a larger input on the same pool", () => { - expect( - outGivenIn(Balance, 2_500n, Balance, 7_500n, 1_000_000_000n) - ).toBe(312_706_938n) - }) - - it("matches the contract on an asymmetric-depth pool", () => { - expect( - outGivenIn(5_000_000_000n, 3_000n, 20_000_000_000n, 7_000n, 250_000_000n) - ).toBe(413_859_413n) - }) - - it("diverges from equal-weight constant product off 50/50", () => { - // The whole point of the fix: cpOutput is only the curve at cw == 5000. - expect( - outGivenIn(Balance, 2_000n, Balance, 8_000n, AmountIn) - ).not.toBe(cpOutput(Balance, Balance, AmountIn)) - }) - - it("is 0n on a degenerate balance, weight, or amount", () => { - expect(outGivenIn(0n, 5_000n, Balance, 5_000n, AmountIn)).toBe(0n) - expect(outGivenIn(Balance, 5_000n, 0n, 5_000n, AmountIn)).toBe(0n) - expect(outGivenIn(Balance, 0n, Balance, 5_000n, AmountIn)).toBe(0n) - expect(outGivenIn(Balance, 5_000n, Balance, 0n, AmountIn)).toBe(0n) - expect(outGivenIn(Balance, 5_000n, Balance, 5_000n, 0n)).toBe(0n) - }) - - it("never pays out more than the output side holds", () => { - expect( - outGivenIn(1n, 5_000n, Balance, 5_000n, 10n ** 18n) - ).toBeLessThanOrEqual(Balance) - }) - }) - - describe("tokenToWire / wireToToken (the convenience mirrors)", () => { - const Balance = 10_000_000_000n, - Amount = 100_000_000n - - it("route connectorWeightBps to the correct SIDE", () => { - // cw is the WIRE-side weight: token→wire has it as weightOut, - // wire→token as weightIn. Swapping them is the easy mistake, and these - // two values differ by more than an order of magnitude at cw=8000. - expect(tokenToWire(Balance, Balance, 8_000, Amount)).toBe(24_844_912n) - expect(wireToToken(Balance, Balance, 8_000, Amount)).toBe(390_196_555n) - }) - - it("collapse to constant product at the symmetric weight", () => { - const cp = cpOutput(Balance, Balance, Amount) - expect( - tokenToWire(Balance, Balance, SymmetricConnectorWeightBps, Amount) - ).toBe(cp) - expect( - wireToToken(Balance, Balance, SymmetricConnectorWeightBps, Amount) - ).toBe(cp) - }) - }) - - describe("quoteSwap (the sysio::opp::amm::quote_swap mirror)", () => { - const book = { - chain: 10_000_000_000n, - wire: 10_000_000_000n, - connectorWeightBps: SymmetricConnectorWeightBps, - ownerFeeBps: 0 - } - - it("charges the WIRE-leg fee BETWEEN the hops, not on the output", () => { - const gross = cpOutput(book.chain, book.wire, 100_000_000n), - net = splitWireFee(gross, 30).net - // Fee before the destination conversion — NOT cp(...gross) reduced after. - expect(quoteSwap(book, book, 100_000_000n, 30)).toBe( - cpOutput(book.wire, book.chain, net) - ) - expect(quoteSwap(book, book, 100_000_000n, 30)).not.toBe( - splitWireFee(cpOutput(book.wire, book.chain, gross), 30).net - ) - }) - it("a WIRE destination receives the post-fee WIRE leg itself", () => { - const gross = cpOutput(book.chain, book.wire, 100_000_000n) - expect(quoteSwap(book, null, 100_000_000n, 30)).toBe( - splitWireFee(gross, 30).net - ) - }) - it("a WIRE source feeds the escrow straight into the WIRE leg", () => { - expect(quoteSwap(null, book, 100_000_000n, 30)).toBe( - cpOutput(book.wire, book.chain, splitWireFee(100_000_000n, 30).net) - ) - }) - it("rides the WEIGHTED curve when the reserve is not 50/50", () => { - // Contract value for cw=8000 on both legs at 30 bps. - const weighted = { - chain: 10_000_000_000n, - wire: 10_000_000_000n, - connectorWeightBps: 8_000, - ownerFeeBps: 0 - } - expect(quoteSwap(weighted, weighted, 100_000_000n, 30)).toBe(98_470_966n) - // …and that is NOT what the equal-weight curve would have quoted. - expect(quoteSwap(weighted, weighted, 100_000_000n, 30)).not.toBe( - quoteSwap(book, book, 100_000_000n, 30) - ) - }) - it("WIRE → WIRE is a plain transfer — no curve, no fee", () => { - expect(quoteSwap(null, null, 42n, 30)).toBe(42n) - }) - // The regression this whole field exists for: a PUBLIC reserve carries - // ownerFeeBps 0, so a quote that drops the owner fee is right everywhere - // except the one flow that configures one — where it silently over-quotes - // the destination and the books assertion fails by the owner fee. - it("charges BOTH reserve owners on the same gross leg as the network fee", () => { - const owned = { ...book, ownerFeeBps: 100 }, - gross = cpOutput(book.chain, book.wire, 100_000_000n) - expect(quoteSwap(owned, owned, 100_000_000n, 30)).toBe( - cpOutput(book.wire, book.chain, splitWireFee(gross, 30, 0, 0, 100, 100).net) - ) - // Strictly less than the same books with no owner fee — the exact gap - // that made the private-reserve flow's target over-predict. - expect(quoteSwap(owned, owned, 100_000_000n, 30)).toBeLessThan( - quoteSwap(book, book, 100_000_000n, 30) - ) - }) - it("a WIRE endpoint charges no owner fee on its side", () => { - // Only the SOURCE reserve's owner fee can apply when the destination is - // WIRE — mirroring the contract's `src_is_wire ? 0 : …` carve-out. - const owned = { ...book, ownerFeeBps: 250 }, - gross = cpOutput(book.chain, book.wire, 100_000_000n) - expect(quoteSwap(owned, null, 100_000_000n, 30)).toBe( - splitWireFee(gross, 30, 0, 0, 250, 0).net - ) - expect(quoteSwap(null, owned, 100_000_000n, 30)).toBe( - cpOutput( - book.wire, - book.chain, - splitWireFee(100_000_000n, 30, 0, 0, 0, 250).net - ) - ) - }) - it("is 0n on degenerate input or a dry hop", () => { - expect(quoteSwap(book, book, 0n, 30)).toBe(0n) - expect(quoteSwap(book, book, -1n, 30)).toBe(0n) - expect( - quoteSwap( - { - chain: 0n, - wire: 0n, - connectorWeightBps: SymmetricConnectorWeightBps, - ownerFeeBps: 0 - }, - book, - 100n, - 30 - ) - ).toBe(0n) - }) - }) - - describe("swapquote", () => { - const reserves: QuoteReserveFixture[] = [ - { - chain_code: EthereumChainSlug, - token_code: EthTokenSlug, - reserve_code: PrimaryReserveSlug, - reserve_chain_amount: 10_000_000_000, - reserve_wire_amount: 10_000_000_000, - connector_weight_bps: SymmetricConnectorWeightBps, - owner_fee_bps: 0 - }, - { - chain_code: SolanaChainSlug, - token_code: SolTokenSlug, - reserve_code: PrimaryReserveSlug, - reserve_chain_amount: 10_000_000_000, - reserve_wire_amount: 10_000_000_000, - connector_weight_bps: SymmetricConnectorWeightBps, - owner_fee_bps: 0 - } - ] - const ethereumTriple = { - chainCode: EthereumChain, - tokenCode: EthToken, - reserveCode: PrimaryReserve - }, - solanaTriple = { - chainCode: SolanaChain, - tokenCode: SolToken, - reserveCode: PrimaryReserve - }, - wireTriple = { - chainCode: WireReserveTool.WireChainCode, - tokenCode: WireReserveTool.WireTokenCode, - reserveCode: PrimaryReserve - } - - it("full hop routes source → WIRE → destination through both books, fee between the hops", async () => { - const quote = await swapquote(stubWire(reserves), { - from: ethereumTriple, - fromAmount: 100_000_000n, - to: solanaTriple - }) - // Gross intermediate cp(1e10, 1e10, 1e8) = 99_009_900; the 30 bps fee - // comes off it, and only the net converts on the destination curve. - const net = splitWireFee(99_009_900n, 30).net - expect(quote).toBe(cpOutput(10_000_000_000n, 10_000_000_000n, net)) - expect(quote).toBe(97_747_972n) - }) - it("to-WIRE consults only the source book and pays the post-fee leg", async () => { - const quote = await swapquote(stubWire(reserves), { - from: ethereumTriple, - fromAmount: 100_000_000n, - to: wireTriple - }) - expect(quote).toBe(splitWireFee(99_009_900n, 30).net) - }) - it("from-WIRE takes the fee off the escrow before the destination curve", async () => { - const quote = await swapquote(stubWire(reserves), { - from: wireTriple, - fromAmount: 100_000_000n, - to: solanaTriple - }) - const net = splitWireFee(100_000_000n, 30).net - expect(quote).toBe(cpOutput(10_000_000_000n, 10_000_000_000n, net)) - expect(quote).toBe(98_715_803n) - }) - it("honours the live uwconfig fee rather than a hardcoded rate", async () => { - const quote = await swapquote(stubWire(reserves, 0), { - from: wireTriple, - fromAmount: 100_000_000n, - to: solanaTriple - }) - // A zero-fee cluster is the only case where the quote is the raw curve. - expect(quote).toBe(99_009_900n) - }) - it("WIRE → WIRE passes through 1:1", async () => { - await expect( - swapquote(stubWire([]), { - from: wireTriple, - fromAmount: 42n, - to: wireTriple - }) - ).resolves.toBe(42n) - }) - it("is 0n when a required reserve row is missing", async () => { - await expect( - swapquote(stubWire([]), { - from: ethereumTriple, - fromAmount: 100_000_000n, - to: solanaTriple - }) - ).resolves.toBe(0n) - }) - }) -}) diff --git a/packages/cluster-tool/tests/tools/wire/WireUnderwriterTool.test.ts b/packages/cluster-tool/tests/tools/wire/WireUnderwriterTool.test.ts deleted file mode 100644 index 93f357ba7..000000000 --- a/packages/cluster-tool/tests/tools/wire/WireUnderwriterTool.test.ts +++ /dev/null @@ -1,317 +0,0 @@ -import Fs from "node:fs" -import Os from "node:os" -import Path from "node:path" -import type { ChainTokenAmount } from "@wireio/cluster-tool-shared" -import { TokenAmount } from "@wireio/opp-typescript-models" -import { SlugName } from "@wireio/sdk-core" -import { Logger } from "@wireio/shared" - -import { - ClusterBuild, - ClusterBuildContext, - ClusterBuildPhase, - ClusterBuildPhaseGroup, - type StepInput -} from "@wireio/cluster-tool/orchestration" -import { getLogger } from "@wireio/cluster-tool/logging" -import { Report } from "@wireio/cluster-tool/report" -import { WireUnderwriterTool } from "@wireio/cluster-tool/tools/wire" -import { fixtureConfig } from "../../config/clusterConfigFixture.js" - -const WireChain = SlugName.from("WIRE") -const EthChain = SlugName.from("ETHEREUM") -const SolChain = SlugName.from("SOLANA") -const EthToken = SlugName.from("ETH") -const SolToken = SlugName.from("SOL") -const UsdcToken = SlugName.from("USDC") - -/** A fresh build root (a `ClusterBuildParent`) for the deposit factory to register on. */ -function newBuild(): ClusterBuild { - return ClusterBuild.forContext( - new ClusterBuildContext(fixtureConfig(), getLogger("uw-test")) - ) -} - -/** Build one `ChainTokenAmount` entry for a native (chain, token) pair. */ -function entry( - chainCode: number, - tokenCode: number, - amount: bigint -): ChainTokenAmount { - return { - chain_code: chainCode, - amount: TokenAmount.create({ tokenCode: BigInt(tokenCode), amount }) - } -} - -/** The step input kinds of a group's Nth child phase (a `ClusterBuildPhase`). */ -function stepKinds(group: ClusterBuildPhaseGroup, index: number): string[] { - return (group.children[index] as ClusterBuildPhase).steps.map( - step => (step.input as StepInput).kind - ) -} - -describe("WireUnderwriterTool", () => { - // ── Pure VALUE helpers ───────────────────────────────────────────────── - - describe("buildDefault", () => { - it("builds one entry per default pair (WIRE, ETH, SOL) at DefaultAmount", () => { - const built = WireUnderwriterTool.buildDefault() - expect(built.map(e => e.chain_code)).toEqual([ - WireChain, - EthChain, - SolChain - ]) - expect( - built.every(e => e.amount.amount === WireUnderwriterTool.DefaultAmount) - ).toBe(true) - expect(built[1].amount.tokenCode).toBe(BigInt(EthToken)) - expect(built[2].amount.tokenCode).toBe(BigInt(SolToken)) - }) - - it("returns a fresh array (callers may mutate without aliasing defaults)", () => { - const a = WireUnderwriterTool.buildDefault() - const b = WireUnderwriterTool.buildDefault() - expect(a).not.toBe(b) - }) - }) - - describe("parseJson", () => { - const uniformJson = [ - { - chain_code: EthChain, - amount: TokenAmount.toJson( - TokenAmount.create({ tokenCode: BigInt(EthToken), amount: 5n }) - ) - } - ] - - it("fans a uniform (Array) input out to every underwriter", () => { - const parsed = WireUnderwriterTool.parseJson(uniformJson, 3) - expect(parsed).toHaveLength(3) - parsed.forEach(list => { - expect(list).toHaveLength(1) - expect(list[0].chain_code).toBe(EthChain) - expect(list[0].amount.amount).toBe(5n) - }) - }) - - it("keeps a varied (Array>) input per underwriter", () => { - const varied = [uniformJson, [...uniformJson, ...uniformJson]] - const parsed = WireUnderwriterTool.parseJson(varied, 2) - expect(parsed).toHaveLength(2) - expect(parsed[0]).toHaveLength(1) - expect(parsed[1]).toHaveLength(2) - }) - - it("throws when a varied input's outer length != underwriterCount", () => { - expect(() => WireUnderwriterTool.parseJson([uniformJson], 2)).toThrow( - /outer array length/ - ) - }) - - it("throws on a non-array input", () => { - expect(() => WireUnderwriterTool.parseJson({ nope: true }, 1)).toThrow( - /must be an array/ - ) - }) - - it("treats an empty array as 'use defaults' per underwriter", () => { - const parsed = WireUnderwriterTool.parseJson([], 2) - expect(parsed).toHaveLength(2) - expect(parsed[0].map(e => e.chain_code)).toEqual([ - WireChain, - EthChain, - SolChain - ]) - }) - }) - - describe("load", () => { - it("returns the fanned-out defaults when no file path is given", () => { - const loaded = WireUnderwriterTool.load(null, 2) - expect(loaded).toHaveLength(2) - loaded.forEach(list => - expect(list.map(e => e.chain_code)).toEqual([ - WireChain, - EthChain, - SolChain - ]) - ) - }) - - it("throws when underwriterCount is not positive", () => { - expect(() => WireUnderwriterTool.load(null, 0)).toThrow( - /must be positive/ - ) - }) - - it("parses a supplied JSON file (uniform shape)", () => { - const dir = Fs.mkdtempSync(Path.join(Os.tmpdir(), "uw-collat-")) - const file = Path.join(dir, "collateral.json") - Fs.writeFileSync( - file, - JSON.stringify([ - { - chain_code: EthChain, - amount: TokenAmount.toJson( - TokenAmount.create({ tokenCode: BigInt(EthToken), amount: 7n }) - ) - } - ]) - ) - try { - const loaded = WireUnderwriterTool.load(file, 2) - expect(loaded).toHaveLength(2) - expect(loaded[0][0].amount.amount).toBe(7n) - } finally { - Fs.rmSync(dir, { recursive: true, force: true }) - } - }) - - it("throws when the file path does not exist", () => { - expect(() => - WireUnderwriterTool.load("/no/such/collateral.json", 1) - ).toThrow(/does not exist/) - }) - }) - - // ── Deposit PhaseGroup factory ───────────────────────────────────────── - - describe("deposit", () => { - it("returns a PhaseGroup with one Phase per underwriter", () => { - const group = WireUnderwriterTool.planCollateralDeposit( - newBuild(), - "uw-collateral", - "underwriter collateral", - {}, - ["uwa", "uwb"], - [ - WireUnderwriterTool.buildDefault(), - [entry(EthChain, EthToken, WireUnderwriterTool.DefaultAmount)] - ] - ) - expect(group).toBeInstanceOf(ClusterBuildPhaseGroup) - expect(group.children).toHaveLength(2) - expect(group.children.map(child => child.name)).toEqual([ - "uwa-collateral", - "uwb-collateral" - ]) - }) - - it("emits WIRE fund/bond + ETH-native deposit + SOL airdrop/deposit for the default plan", () => { - const group = WireUnderwriterTool.planCollateralDeposit( - newBuild(), - "uw-collateral", - "underwriter collateral", - {}, - ["uwa"], - [WireUnderwriterTool.buildDefault()] - ) - // WIRE → fund + depot bond; ETH native → 1 deposit; SOL native → airdrop + deposit. - expect(stepKinds(group, 0)).toEqual([ - "WireCollateralTool.FundingInput", - "WireCollateralTool.DepositInput", - "EthereumCollateralTool.DepositInput", - "SolanaFundingTool.AirdropInput", - "SolanaCollateralTool.DepositInput" - ]) - }) - - it("emits a single ETH-native deposit Step for a single ETH entry", () => { - const group = WireUnderwriterTool.planCollateralDeposit( - newBuild(), - "uw-collateral", - "d", - {}, - ["uwb"], - [[entry(EthChain, EthToken, WireUnderwriterTool.DefaultAmount)]] - ) - expect(stepKinds(group, 0)).toEqual([ - "EthereumCollateralTool.DepositInput" - ]) - expect((group.children[0] as ClusterBuildPhase).steps[0].actor).toBe( - Report.Actor.Underwriter - ) - }) - - it("bonds a WIRE entry on the depot through WireCollateralTool (fund, then deposit)", () => { - const group = WireUnderwriterTool.planCollateralDeposit( - newBuild(), - "uw-collateral", - "d", - {}, - ["uwc"], - [[entry(WireChain, WireChain, WireUnderwriterTool.DefaultAmount)]] - ) - const steps = (group.children[0] as ClusterBuildPhase).steps - expect(stepKinds(group, 0)).toEqual([ - "WireCollateralTool.FundingInput", - "WireCollateralTool.DepositInput" - ]) - expect(steps.map(step => step.name)).toEqual([ - "uwc-WIRE-WIRE-deposit-fund", - "uwc-WIRE-WIRE-deposit" - ]) - expect(steps.every(step => step.actor === Report.Actor.Underwriter)).toBe(true) - }) - - it("emits no Step for a non-native token on the WIRE chain (no harness funding path)", () => { - const logged = jest.spyOn(Logger.prototype, "log") - try { - const group = WireUnderwriterTool.planCollateralDeposit( - newBuild(), - "uw-collateral", - "d", - {}, - ["uwe"], - [[entry(WireChain, SlugName.from("LIQETH"), WireUnderwriterTool.DefaultAmount)]] - ) - expect(stepKinds(group, 0)).toEqual([]) - expect(logged).toHaveBeenCalledWith( - "warn", - expect.stringMatching(/uwe: skipping WIRE\/LIQETH entry — the harness has no funding path for shadow tokens/) - ) - } finally { - logged.mockRestore() - } - }) - - it("emits the non-native ETH steps from config alone — deploy artifacts resolve at RUN time", () => { - // The mock-token address does not exist when the build constructs its - // steps (the outpost deploys later in the same build) — the step SET - // must come from the collateral plan, never from an artifact probe. - // The old factory-time read silently skipped every non-native leg - // (2026-07-02 flow-swap-non-native-tokens incident). - const group = WireUnderwriterTool.planCollateralDeposit( - newBuild(), - "uw-collateral", - "d", - {}, - ["uwd"], - [[entry(EthChain, UsdcToken, WireUnderwriterTool.DefaultAmount)]] - ) - expect(stepKinds(group, 0)).toEqual([ - "EthereumFundingTool.MintErc20Input", - "EthereumCollateralTool.ApproveErc20Input", - "EthereumCollateralTool.DepositNonNativeInput" - ]) - }) - - it("throws when the collateral plan length != underwriter count", () => { - expect(() => - WireUnderwriterTool.planCollateralDeposit( - newBuild(), - "uw-collateral", - "d", - {}, - ["only-one"], - [ - WireUnderwriterTool.buildDefault(), - WireUnderwriterTool.buildDefault() - ] - ) - ).toThrow(/must equal underwriter count/) - }) - }) -}) diff --git a/packages/debugging-client-shared/tests/local/fixtureCluster.ts b/packages/debugging-client-shared/tests/local/fixtureCluster.ts index 9f90e4951..299e984fe 100644 --- a/packages/debugging-client-shared/tests/local/fixtureCluster.ts +++ b/packages/debugging-client-shared/tests/local/fixtureCluster.ts @@ -157,8 +157,6 @@ export function makeFixtureCluster(): FixtureCluster { awsClusterNodeConfig: null, externalOutposts: null, debuggingServerEnabled: true, - enableMockReserves: false, - enableLaunchWithheldOperations: false, enableMockLiqPools: false, enableMockSyndicationImport: false, deploymentKind: ClusterDeploymentKind.local, diff --git a/packages/debugging-client-tool-tui/tests/store/cluster/ClusterSlice.test.ts b/packages/debugging-client-tool-tui/tests/store/cluster/ClusterSlice.test.ts index 707be001e..76c9b02ff 100644 --- a/packages/debugging-client-tool-tui/tests/store/cluster/ClusterSlice.test.ts +++ b/packages/debugging-client-tool-tui/tests/store/cluster/ClusterSlice.test.ts @@ -101,8 +101,6 @@ const stubConfig: ClusterConfig = { awsClusterNodeConfig: null, externalOutposts: null, debuggingServerEnabled: true, - enableMockReserves: false, - enableLaunchWithheldOperations: false, enableMockLiqPools: false, enableMockSyndicationImport: false, deploymentKind: ClusterDeploymentKind.local, diff --git a/packages/flow-batch-operator-termination/src/TerminationScenario.ts b/packages/flow-batch-operator-termination/src/TerminationScenario.ts index 1b07c941e..2d2384514 100644 --- a/packages/flow-batch-operator-termination/src/TerminationScenario.ts +++ b/packages/flow-batch-operator-termination/src/TerminationScenario.ts @@ -3,14 +3,14 @@ import { SysioContracts } from "@wireio/sdk-core" import { OperatorType } from "@wireio/opp-typescript-models" import { ClusterBuildPhase, - EthereumCollateralTool, + OperatorDaemonArtifactsKey, FlowScenario, ProtocolTiming, Report, Steps, WireCollateralTool, WireOperatorProvisioningTool, - WireReserveTool, + Constants as DepotConstants, getLogger, matchesProtoEnum, packedSlugValue, @@ -27,7 +27,6 @@ const log = getLogger(__filename) const { SysioOpregActiontype, SysioOpregOperatorstatus } = SysioContracts const { Actor } = Report - /** The doomed operator's node-owner-generated WIRE account, resolved from the key store by its label. */ function doomedOperatorAccount(ctx: ClusterBuildContext): string { return ctx.keyStore.assertOperator(Constants.DoomedOperatorLabel).account @@ -53,7 +52,10 @@ async function readScheduleGroups( async function readWithdrawRemitChainCodes( ctx: ClusterBuildContext ): Promise> { - const operator = await WireCollateralTool.readOperatorRow(ctx, Constants.DoomedOperatorLabel) + const operator = await WireCollateralTool.readOperatorRow( + ctx, + Constants.DoomedOperatorLabel + ) const remits = (operator?.recent_actions ?? []).filter( entry => matchesProtoEnum( @@ -74,7 +76,7 @@ async function readWithdrawRemitChainCodes( * depot-native bond meets the required minimum, and that on termination the * whole bond is credited back to it and paid out on `claimremit`: * - * 1. **ChainHealth** — WIRE produces blocks; anvil's `OperatorRegistry` has + * 1. **ChainHealth** — WIRE produces blocks; anvil's `OPP` has * code; the SOL test-validator answers. * 2. **ProvisionOperator** — the ONE provisioning mechanism creates the doomed * operator (unique WIRE key, ETH + SOL identities, authex links, @@ -111,22 +113,28 @@ export class TerminationScenario extends FlowScenario { // Without a requirement `meets_role_min` refuses every non-bootstrapped // batch operator, so the UNKNOWN → ACTIVE assertions are meaningful only // against a minimum the flow installs. - requiredBatchOperatorCollateral: [WireCollateralTool.createWireRequirement(Constants.MinimumBond)] + requiredBatchOperatorCollateral: [ + WireCollateralTool.createWireRequirement(Constants.MinimumBond) + ] } plan(cluster: ClusterBuild): void { const quickStepOptions = { timeoutMs: Constants.QuickVerifyTimeoutMs }, scheduleWindowStepOptions = { timeoutMs: - Constants.scheduleWindowDeadlineMs() + ProtocolTiming.PollDeadlineBufferMs + Constants.scheduleWindowDeadlineMs() + + ProtocolTiming.PollDeadlineBufferMs }, terminationStepOptions = { timeoutMs: - Constants.terminationDeadlineMs() + ProtocolTiming.PollDeadlineBufferMs + Constants.terminationDeadlineMs() + + ProtocolTiming.PollDeadlineBufferMs }, // The same epoch duration the claim verify's runner reads (`ctx.config.epochDurationSec`). remitClaimStepOptions = { - timeoutMs: WireCollateralTool.remitClaimStepTimeoutMs(cluster.context.config.epochDurationSec) + timeoutMs: WireCollateralTool.remitClaimStepTimeoutMs( + cluster.context.config.epochDurationSec + ) } // ── 1. Substrate health (WIRE / ETH outpost / SOL validator) ── @@ -151,18 +159,14 @@ export class TerminationScenario extends FlowScenario { verifyStep( Actor.EthereumOutpost, "ethereum-outpost-reachable", - "anvil answers and OperatorRegistry has deployed code", + "anvil answers and OPP has deployed code", async ctx => { - const registry = EthereumCollateralTool.loadOperatorRegistry( - ctx, - ctx.ethereum.wallet.signer - ) - const code = await ctx.ethereum.provider.getCode( - await registry.getAddress() - ) + const address = ctx.outputs.assert(OperatorDaemonArtifactsKey) + .ethereumAddresses.OPP + const code = await ctx.ethereum.provider.getCode(address) Assert.ok( code.length > Constants.MinimumContractCodeLength, - `OperatorRegistry has no code on anvil (getCode returned ${code.length} chars)` + `OPP has no code on anvil (getCode returned ${code.length} chars)` ) }, quickStepOptions @@ -211,7 +215,10 @@ export class TerminationScenario extends FlowScenario { "registered-status-unknown", "operator registered non-bootstrapped with status UNKNOWN (no deposits yet)", async ctx => { - const operator = await WireCollateralTool.readOperatorRow(ctx, Constants.DoomedOperatorLabel) + const operator = await WireCollateralTool.readOperatorRow( + ctx, + Constants.DoomedOperatorLabel + ) Assert.ok( operator != null, `${Constants.DoomedOperatorLabel} missing from sysio.opreg::operators` @@ -260,7 +267,10 @@ export class TerminationScenario extends FlowScenario { "below-minimum-stays-unknown", "status stays UNKNOWN while the bond is under the minimum", async ctx => { - const operator = await WireCollateralTool.readOperatorRow(ctx, Constants.DoomedOperatorLabel) + const operator = await WireCollateralTool.readOperatorRow( + ctx, + Constants.DoomedOperatorLabel + ) Assert.ok( operator != null && matchesProtoEnum( @@ -303,7 +313,10 @@ export class TerminationScenario extends FlowScenario { "the bond meets the minimum → status is OPERATOR_STATUS_ACTIVE", async ctx => { // `deposit` re-evaluates eligibility inline, so one read after its Step is final. - const operator = await WireCollateralTool.readOperatorRow(ctx, Constants.DoomedOperatorLabel) + const operator = await WireCollateralTool.readOperatorRow( + ctx, + Constants.DoomedOperatorLabel + ) Assert.ok( operator != null && matchesProtoEnum( @@ -368,7 +381,10 @@ export class TerminationScenario extends FlowScenario { await pollUntil( `${Constants.DoomedOperatorLabel} status flips to TERMINATED`, async () => { - const operator = await WireCollateralTool.readOperatorRow(ctx, Constants.DoomedOperatorLabel) + const operator = await WireCollateralTool.readOperatorRow( + ctx, + Constants.DoomedOperatorLabel + ) return ( operator != null && matchesProtoEnum( @@ -389,7 +405,10 @@ export class TerminationScenario extends FlowScenario { "termination-row-populated", "terminated_at > 0 and status_reason non-empty on the operator row", async ctx => { - const operator = await WireCollateralTool.readOperatorRow(ctx, Constants.DoomedOperatorLabel) + const operator = await WireCollateralTool.readOperatorRow( + ctx, + Constants.DoomedOperatorLabel + ) Assert.ok( operator != null, `${Constants.DoomedOperatorLabel} missing from sysio.opreg::operators` @@ -429,7 +448,7 @@ export class TerminationScenario extends FlowScenario { async ctx => { const chainCodes = await readWithdrawRemitChainCodes(ctx) Assert.ok( - chainCodes.has(WireReserveTool.WireChainCode), + chainCodes.has(DepotConstants.WireChainCode), `${Constants.DoomedOperatorLabel} has no success-true WITHDRAW_REMIT for the WIRE chain` ) }, diff --git a/packages/flow-emergency-stop/src/EmergencyStopScenario.ts b/packages/flow-emergency-stop/src/EmergencyStopScenario.ts index 47e242d1b..2c6986d6d 100644 --- a/packages/flow-emergency-stop/src/EmergencyStopScenario.ts +++ b/packages/flow-emergency-stop/src/EmergencyStopScenario.ts @@ -50,7 +50,7 @@ export class EmergencyStopScenario extends SyndicationScenario { epochDurationSec: SyndicationScenario.EpochDurationSec, producerCount: 3, batchOperatorCount: 3, - underwriterCount: 1 + underwriterCount: 0 } plan(cluster: ClusterBuild): void { this.planSetup(cluster, [Constants.User]) diff --git a/packages/flow-emissions-soak/src/EmissionsSoakScenarioConstants.ts b/packages/flow-emissions-soak/src/EmissionsSoakScenarioConstants.ts index 1d959a9a8..d8c2dfba8 100644 --- a/packages/flow-emissions-soak/src/EmissionsSoakScenarioConstants.ts +++ b/packages/flow-emissions-soak/src/EmissionsSoakScenarioConstants.ts @@ -50,7 +50,7 @@ export namespace EmissionsSoakScenarioConstants { */ export const BatchOperatorCount = 21 /** Bootstrapped underwriters in the bootstrap roster. */ - export const UnderwriterCount = 1 + export const UnderwriterCount = 0 // ── Controlled stakers (the test holds their ETH wallets) ───────────────── diff --git a/packages/flow-liq-syndication/src/LIQSyndicationScenarioConstants.ts b/packages/flow-liq-syndication/src/LIQSyndicationScenarioConstants.ts index a81b7a5d1..5c1b670b4 100644 --- a/packages/flow-liq-syndication/src/LIQSyndicationScenarioConstants.ts +++ b/packages/flow-liq-syndication/src/LIQSyndicationScenarioConstants.ts @@ -20,7 +20,7 @@ export namespace LIQSyndicationScenarioConstants { /** Batch operators ferrying OPP envelopes (the group that must relay the attestations). */ export const BatchOperatorCount = 3 /** Underwriters provisioned by the bootstrap. */ - export const UnderwriterCount = 1 + export const UnderwriterCount = 0 /** Registered chain slug code of the Solana outpost — what `OutpostConfig.chain_code` stamps. */ export const SolanaChainCode: bigint = BigInt( diff --git a/packages/flow-liq-yield/src/LIQYieldScenarioConstants.ts b/packages/flow-liq-yield/src/LIQYieldScenarioConstants.ts index c1cdbbf8b..f50e4427f 100644 --- a/packages/flow-liq-yield/src/LIQYieldScenarioConstants.ts +++ b/packages/flow-liq-yield/src/LIQYieldScenarioConstants.ts @@ -20,7 +20,7 @@ export namespace LIQYieldScenarioConstants { /** Batch operators ferrying OPP envelopes and cranking the yield path. */ export const BatchOperatorCount = 3 /** Underwriters provisioned by the bootstrap. */ - export const UnderwriterCount = 1 + export const UnderwriterCount = 0 /** Registered chain slug code of the Solana outpost — what `OutpostConfig.chain_code` stamps. */ export const SolanaChainCode: bigint = BigInt( diff --git a/packages/flow-node-owner-nft/src/NodeOwnerNftScenarioConstants.ts b/packages/flow-node-owner-nft/src/NodeOwnerNftScenarioConstants.ts index 502fd8491..3febf5f59 100644 --- a/packages/flow-node-owner-nft/src/NodeOwnerNftScenarioConstants.ts +++ b/packages/flow-node-owner-nft/src/NodeOwnerNftScenarioConstants.ts @@ -17,7 +17,7 @@ export namespace NodeOwnerNftScenarioConstants { /** Bootstrapped batch operators stood up by the bootstrap. */ export const BatchOperatorCount = 3 /** Bootstrapped underwriters stood up by the bootstrap. */ - export const UnderwriterCount = 1 + export const UnderwriterCount = 0 /** The dev K1 key each fixture owner account is created under (controls `owner@active`). */ export const DEV_K1_PUBLIC_KEY = Constants.DEV_K1_PUBLIC_KEY @@ -26,7 +26,8 @@ export namespace NodeOwnerNftScenarioConstants { * A second, distinct Wire K1 key (from `clio create key --k1`) — the * wrong-key claim names it while the account is NOT controlled by it. */ - export const OtherWireKey = "PUB_K1_84yPGCSNRdSTrdpYnfzWun477PzuKR4L4R8eYumxqLjoG8s2Jo" + export const OtherWireKey = + "PUB_K1_84yPGCSNRdSTrdpYnfzWun477PzuKR4L4R8eYumxqLjoG8s2Jo" /** Happy-path owner — created under the dev key, registers CONFIRMED at tier 1. */ export const HappyPathAccount = "nfta" diff --git a/packages/flow-node-owner-nft/src/steps/NodeOwnerNftScenarioCommitSteps.ts b/packages/flow-node-owner-nft/src/steps/NodeOwnerNftScenarioCommitSteps.ts index d93804150..e09c6d5cd 100644 --- a/packages/flow-node-owner-nft/src/steps/NodeOwnerNftScenarioCommitSteps.ts +++ b/packages/flow-node-owner-nft/src/steps/NodeOwnerNftScenarioCommitSteps.ts @@ -2,8 +2,7 @@ import Assert from "node:assert" import { ClusterBuildContext, ClusterBuildStep, - ClusterConfigProvider, - EthereumCollateralTool, + OperatorDaemonArtifactsKey, NodeOwnerTier, Report, approveNodeEscrow, @@ -42,12 +41,12 @@ export namespace NodeOwnerNftScenarioCommitSteps { * @param ctx - The build context (ethereum path + anvil client). * @returns The signer-bound contract surface. */ - export function resolveBar(ctx: C): BarContract { + export function resolveBar( + ctx: C + ): BarContract { return loadBar( ctx.config.ethereumPath, - EthereumCollateralTool.loadOutpostAddresses( - ClusterConfigProvider.ethereumDeploymentsPath(ctx.config) - ), + ctx.outputs.assert(OperatorDaemonArtifactsKey).ethereumAddresses, ctx.ethereum.wallet.signer ) } diff --git a/packages/flow-node-owner-nft/src/steps/NodeOwnerNftScenarioMintSteps.ts b/packages/flow-node-owner-nft/src/steps/NodeOwnerNftScenarioMintSteps.ts index 5d28d2bd0..4f93bb252 100644 --- a/packages/flow-node-owner-nft/src/steps/NodeOwnerNftScenarioMintSteps.ts +++ b/packages/flow-node-owner-nft/src/steps/NodeOwnerNftScenarioMintSteps.ts @@ -2,7 +2,7 @@ import Assert from "node:assert" import { ClusterBuildContext, ClusterBuildStep, - EthereumCollateralTool, + OperatorDaemonArtifactsKey, loadMockWireNodes, mintNodeNFT, NodeOwnerTier, @@ -10,8 +10,7 @@ import { Report, type ClusterBuildStepOptions, type MockWireNodesContract, - type StepInput, - ClusterConfigProvider + type StepInput } from "@wireio/cluster-tool" /** @@ -43,9 +42,7 @@ export namespace NodeOwnerNftScenarioMintSteps { ): MockWireNodesContract { return loadMockWireNodes( ctx.config.ethereumPath, - EthereumCollateralTool.loadOutpostAddresses( - ClusterConfigProvider.ethereumDeploymentsPath(ctx.config) - ), + ctx.outputs.assert(OperatorDaemonArtifactsKey).ethereumAddresses, ctx.ethereum.wallet.signer ) } diff --git a/packages/flow-producer-registration/src/ProducerRegistrationScenario.ts b/packages/flow-producer-registration/src/ProducerRegistrationScenario.ts index 2655e42b4..145659684 100644 --- a/packages/flow-producer-registration/src/ProducerRegistrationScenario.ts +++ b/packages/flow-producer-registration/src/ProducerRegistrationScenario.ts @@ -5,7 +5,7 @@ import { ClusterBuildPhase, SyndicationScenario, SyndicationUserSteps, - WireReserveTool, + Constants as DepotConstants, Constants as ClusterToolConstants, FlowScenario, ProducerNodeTool, @@ -147,7 +147,7 @@ export class ProducerRegistrationScenario extends FlowScenario { enableMockSyndicationImport: true, // Both entries are mandatory for the non-bootstrapped producer to leave UNKNOWN. requiredProducerCollateral: Constants.CollateralTokens.map(token => ({ - chainCode: WireReserveTool.WireChainCode, + chainCode: DepotConstants.WireChainCode, tokenCode: Number(Constants.claim(token, 0n).tokenCode), minimumBond: Number(Constants.MinimumBond) })) diff --git a/packages/flow-producer-registration/src/ProducerRegistrationScenarioConstants.ts b/packages/flow-producer-registration/src/ProducerRegistrationScenarioConstants.ts index e11f340a1..b7cce01ab 100644 --- a/packages/flow-producer-registration/src/ProducerRegistrationScenarioConstants.ts +++ b/packages/flow-producer-registration/src/ProducerRegistrationScenarioConstants.ts @@ -3,7 +3,7 @@ import { TokenAmount } from "@wireio/opp-typescript-models" import { Constants, ProtocolTiming, - WireReserveTool + Constants as DepotConstants } from "@wireio/cluster-tool" /** @@ -78,7 +78,7 @@ export namespace ProducerRegistrationScenarioConstants { /** Both shadow symbols are collateral on WIRE, not on their origin chains. */ export function collateral(token: CollateralToken, amount: bigint) { return { - chain_code: WireReserveTool.WireChainCode, + chain_code: DepotConstants.WireChainCode, amount: claim(token, amount) } } diff --git a/packages/flow-producer-registration/tests/producer-collateral.test.cjs b/packages/flow-producer-registration/tests/producer-collateral.test.cjs index 84458aa7d..7e9b09f57 100644 --- a/packages/flow-producer-registration/tests/producer-collateral.test.cjs +++ b/packages/flow-producer-registration/tests/producer-collateral.test.cjs @@ -7,7 +7,7 @@ const { Report, Steps, WireCollateralTool, - WireReserveTool, + Constants: DepotConstants, privateKeyFromNativeString } = require("@wireio/cluster-tool") const { @@ -39,7 +39,7 @@ test("producer policy requires exactly 2.0 of BOTH depot shadows", () => { Assert.deepEqual( defaults.requiredProducerCollateral, ["LIQSOL", "LIQETH"].map(token => ({ - chainCode: WireReserveTool.WireChainCode, + chainCode: DepotConstants.WireChainCode, tokenCode: SlugName.from(token), minimumBond: 2_000_000_000 })) @@ -94,10 +94,7 @@ test("bond-only steps use depot collateral without WIRE funding", async () => { token ) Assert.equal(step.runner, WireCollateralTool.runDeposit) - Assert.equal( - step.input.collateral.chain_code, - WireReserveTool.WireChainCode - ) + Assert.equal(step.input.collateral.chain_code, DepotConstants.WireChainCode) await run(step, context({})) Assert.deepEqual(calls.at(-1).data, { account, diff --git a/packages/flow-reserve-lifecycle/package.json b/packages/flow-reserve-lifecycle/package.json deleted file mode 100644 index c5922de1b..000000000 --- a/packages/flow-reserve-lifecycle/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "@wireio/test-flow-reserve-lifecycle", - "version": "0.1.18", - "private": true, - "type": "commonjs", - "description": "Flow: gated reserve create→match lifecycle + private-reserve exclusions", - "scripts": { - "build": "tsc -b tsconfig.json", - "test:disabled": "node lib/index.js", - "clean": "rm -rf lib" - }, - "dependencies": { - "@wireio/cluster-tool": "workspace:*", - "@wireio/debugging-shared": "workspace:*", - "@wireio/sdk-core": "^1.0.92", - "@wireio/opp-typescript-models": "^1.0.50", - "ethers": "^6.14.0" - }, - "devDependencies": { - "@types/node": "25.5.0" - } -} diff --git a/packages/flow-reserve-lifecycle/src/ReserveLifecycleScenario.ts b/packages/flow-reserve-lifecycle/src/ReserveLifecycleScenario.ts deleted file mode 100644 index 791f65f3d..000000000 --- a/packages/flow-reserve-lifecycle/src/ReserveLifecycleScenario.ts +++ /dev/null @@ -1,646 +0,0 @@ -import Assert from "node:assert" -import { SlugName, SysioContracts } from "@wireio/sdk-core" -import { oppDebuggingPath } from "@wireio/debugging-shared" -import { - ClusterBuildPhase, - EthereumLocalReserveStatus, - FlowScenario, - ProtocolTiming, - Report, - SwapUserIdentities, - containsSwapRevert, - matchesProtoEnum, - pollUntil, - slugValue, - swapUserOutputKey, - verifyStep, - type ClusterBuild, - type ClusterBuildContext, - type ClusterBuildOptions -} from "@wireio/cluster-tool" -import { ReserveLifecycleScenarioConstants as Constants } from "./ReserveLifecycleScenarioConstants.js" -import { ReserveLifecycleScenarioOwnerSteps as OwnerSteps } from "./steps/ReserveLifecycleScenarioOwnerSteps.js" -import { ReserveLifecycleScenarioReserveSteps as ReserveSteps } from "./steps/ReserveLifecycleScenarioReserveSteps.js" - -const { - SysioContractAccount, - SysioContractName, - SysioReservReservestatus, - SysioUwritChainkind -} = SysioContracts -const { Actor } = Report - -/** The depot reserve row for a `(chain, token, reserve)` slug triple (a read). */ -async function readReserveRow( - ctx: ClusterBuildContext, - chainCode: number, - tokenCode: number, - reserveCode: number -): Promise { - const { rows } = await ctx.wire - .getSysioContract(SysioContractName.reserv) - .tables.reserves.query({ limit: 100 }) - return rows.find( - row => - slugValue(row.chain_code) === chainCode && - slugValue(row.token_code) === tokenCode && - slugValue(row.reserve_code) === reserveCode - ) -} - -/** The forbidden UWREQ — anything sourcing the private ETH reserve (a read). */ -async function readPrivateSourcedUwreq( - ctx: ClusterBuildContext -): Promise { - const { rows } = await ctx.wire - .getSysioContract(SysioContractName.uwrit) - .tables.uwreqs.query({ limit: 100 }) - return rows.find( - request => - slugValue(request.src_chain_code) === Constants.EthereumChainCode && - slugValue(request.src_reserve_code) === Constants.PrivateReserveCode - ) -} - -/** - * Inverted poll — passes only when `predicate` stays FALSE for the whole - * window. `pollUntil`'s deadline expiry IS the pass; the predicate turning - * true (the forbidden condition appearing) fails, and any other error - * propagates unchanged. - * - * @param label - What must NOT appear (for the failure message). - * @param predicate - Async check; `true` means the forbidden condition appeared. - * @param windowMs - The observation window (ms). - * @param intervalMs - Delay between checks (ms). - * @throws When the forbidden condition appears inside the window. - */ -async function assertNeverWithinWindow( - label: string, - predicate: () => Promise, - windowMs: number, - intervalMs: number -): Promise { - await pollUntil(label, predicate, windowMs, intervalMs).then( - () => { - throw new Error( - `Forbidden condition observed within ${windowMs}ms: ${label}` - ) - }, - (error: unknown) => { - const message = error instanceof Error ? error.message : String(error) - if (!message.includes(Constants.PollTimeoutMessageFragment)) throw error - } - ) -} - -/** - * Gated reserve create→match lifecycle + private-reserve exclusions. - * - * The depot (`sysio.reserv`) gates post-bootstrap reserve creation on the - * creator's authex link and activation on a REAL WIRE match: - * - * - An outpost `create_reserve` ships `isPrivate` + the creator's pubkey - * (33-byte compressed secp256k1 on ETH, contract-verified to derive to - * `msg.sender`). The depot inserts a PENDING row when the creator is - * authex-linked; an unlinked creator gets a CANCELLED row + a - * RESERVE_CREATE_CANCELLED round-trip that refunds the outpost escrow. - * - `matchreserve` requires the matcher to be THE WIRE account authex-linked - * to the creator's key, escrows `wire_amount` REAL WIRE (matcher → - * sysio.reserv custody), flips the row ACTIVE, records `owner = matcher`, - * and queues RESERVE_READY back to the outpost. - * - Private reserves are excluded from everything except same-owner private - * pairs: a private↔public pairing draws a SWAP_REVERT (no UWREQ row is - * ever created) and WIRE-endpoint swaps touching one are rejected - * (`swapfromwire` asserts at push). - * - * ETH-side only — the SOL create path is exercised by - * `flow-swap-private-reserves`. - */ -export class ReserveLifecycleScenario extends FlowScenario { - readonly name = "flow-reserve-lifecycle" - readonly description = - "Gated reserve create→match lifecycle + private-reserve exclusions (ETH-side)" - - override readonly defaults: ClusterBuildOptions = { - // Seed the mock (chain, token) PRIMARY reserves this flow reads — `regreserve` - // is epoch-0-gated by the depot, so it must ride the bootstrap, not a flow phase. - enableMockReserves: true, - // The launch build of the Solana outpost withholds the reserve/swap - // bootstrap calls; this flow exercises them, so it opts in (never in plan()). - enableLaunchWithheldOperations: true, - epochDurationSec: Constants.EpochDurationSec, - requiredUnderwriterCollateral: [ - { - chainCode: Constants.EthereumChainCode, - tokenCode: Constants.EthereumTokenCode, - minimumBond: Constants.UnderwriterMinimumBond - }, - { - chainCode: Constants.SolanaChainCode, - tokenCode: Constants.SolanaTokenCode, - minimumBond: Constants.UnderwriterMinimumBond - } - ] - } - - plan(cluster: ClusterBuild): void { - const relayStepOptions = { - timeoutMs: Constants.RelayDeadlineMs + ProtocolTiming.PollDeadlineBufferMs - }, - readyStepOptions = { - timeoutMs: Constants.ReadyDeadlineMs + ProtocolTiming.PollDeadlineBufferMs - }, - windowStepOptions = { - timeoutMs: Constants.NoUwreqWindowMs + ProtocolTiming.PollDeadlineBufferMs - }, - ethereumWriteOptions = { - timeoutMs: Constants.EthereumWriteStepTimeoutMs - }, - wireWriteOptions = { timeoutMs: Constants.WireWriteStepTimeoutMs } - - // ── 0. Substrate health (the old suite's phase-0 checks) ── - ClusterBuildPhase.create( - cluster, - "VerifySubstrate", - "The chain produces blocks; the public counterpart is seeded" - ).push( - verifyStep( - Actor.Sysio, - "wire-chain-producing", - "WIRE chain is producing blocks", - async ctx => { - const info = await ctx.wire.getInfo() - Assert.ok( - Number(info.head_block_num) > 0, - "head_block_num must be > 0" - ) - } - ), - verifyStep( - Actor.Sysio, - "public-counterpart-seeded", - "bootstrap seeded the SOLANA/SOL/PRIMARY public counterpart", - async ctx => { - // The swap probe pairs the private reserve against this public - // reserve — it must exist so the rejection is attributable to - // privacy, not absence. - const row = await readReserveRow( - ctx, - Constants.SolanaChainCode, - Constants.SolanaTokenCode, - Constants.PublicReserveCode - ) - Assert.ok(row != null, "SOLANA/SOL/PRIMARY reserve row must exist") - } - ) - ) - - // ── 1. The reserve creator's paired ETH + SOL identity (+ SOL airdrop) ── - SwapUserIdentities.planIdentityProvisioning( - cluster, - "ProvisionCreatorIdentity", - "Provision the reserve creator's ETH + SOL identity", - {} - ) - - // ── 2. The WIRE matcher accounts + the creator-key authex link ── - ClusterBuildPhase.create( - cluster, - "ProvisionOwner", - "Provision the matcher accounts and authex-link the matcher to the creator key" - ).push( - OwnerSteps.planProvisionUser( - Actor.User, - "provision-matcher", - `provision + fund ${Constants.MatcherAccount} (the legitimate matcher)`, - wireWriteOptions, - Constants.MatcherAccount, - Constants.MatcherFunding - ), - // The wrong matcher is funded identically so ONLY the missing authex - // link explains its rejection. - OwnerSteps.planProvisionUser( - Actor.User, - "provision-wrong-matcher", - `provision + fund ${Constants.WrongMatcherAccount} (never authex-linked)`, - wireWriteOptions, - Constants.WrongMatcherAccount, - Constants.WrongMatcherFunding - ), - OwnerSteps.planCreateLink( - Actor.User, - "authex-link-matcher", - `authex-link ${Constants.MatcherAccount} to the creator wallet's secp256k1 key`, - wireWriteOptions, - Constants.MatcherAccount - ) - ) - - // ── 3. CreatePrivateLinked — linked creator's create lands PENDING ── - ClusterBuildPhase.create( - cluster, - "CreatePrivateLinked", - "The linked creator's private create_reserve lands a PENDING depot row" - ).push( - ReserveSteps.planCreateReserve( - Actor.User, - "create-private-reserve", - `create_reserve(PRIVRES, isPrivate) escrowing ${Constants.PrivateEscrowWei} wei`, - ethereumWriteOptions, - { - tokenCode: BigInt(Constants.EthereumTokenCode), - reserveCode: BigInt(Constants.PrivateReserveCode), - externalTokenAmount: Constants.PrivateEscrowWei, - requestedWireAmount: Constants.RequestedWireAmount, - connectorWeightBps: Constants.ConnectorWeightBps, - name: Constants.PrivateReserveName, - description: Constants.PrivateReserveDescription, - isPrivate: true - } - ), - verifyStep( - Actor.Sysio, - "depot-row-pending", - "PRIVRES depot row lands PENDING carrying the privacy flag + creator pubkey", - async ctx => { - await pollUntil( - "PRIVRES depot row status=PENDING", - async () => { - const row = await readReserveRow( - ctx, - Constants.EthereumChainCode, - Constants.EthereumTokenCode, - Constants.PrivateReserveCode - ) - return ( - row != null && - matchesProtoEnum( - row.status, - SysioReservReservestatus, - SysioReservReservestatus.RESERVE_STATUS_PENDING - ) - ) - }, - Constants.RelayDeadlineMs, - Constants.PollIntervalMs - ) - // The privacy flag + the normalized creator key both ride the - // create attestation and land verbatim on the depot row. - const row = await readReserveRow( - ctx, - Constants.EthereumChainCode, - Constants.EthereumTokenCode, - Constants.PrivateReserveCode - ) - Assert.ok( - row.is_private === true || Number(row.is_private) === 1, - "PRIVRES depot row must carry is_private" - ) - Assert.ok( - typeof row.creator_pub_key === "string" && - row.creator_pub_key.length > 0, - "PRIVRES depot row must carry the creator pubkey" - ) - }, - relayStepOptions - ) - ) - - // ── 4. CreatePrivateUnlinked — unlinked creator is cancelled back + refunded ── - ClusterBuildPhase.create( - cluster, - "CreatePrivateUnlinked", - "An unlinked creator's create is cancelled back and its escrow refunded" - ).push( - ReserveSteps.planFundUnlinkedCreator( - Actor.User, - "fund-unlinked-creator", - `seed the never-linked creator wallet with ${Constants.NoLinkWalletFundingWei} wei`, - ethereumWriteOptions, - Constants.NoLinkWalletFundingWei - ), - ReserveSteps.planCreateReserveUnlinked( - Actor.User, - "create-unlinked-reserve", - `create_reserve(NOLINKRS) escrowing ${Constants.NoLinkEscrowWei} wei from the unlinked creator`, - ethereumWriteOptions, - { - tokenCode: BigInt(Constants.EthereumTokenCode), - reserveCode: BigInt(Constants.NoLinkReserveCode), - externalTokenAmount: Constants.NoLinkEscrowWei, - requestedWireAmount: Constants.RequestedWireAmount, - connectorWeightBps: Constants.ConnectorWeightBps, - name: Constants.NoLinkReserveName, - description: Constants.NoLinkReserveDescription, - isPrivate: false - } - ), - verifyStep( - Actor.Sysio, - "depot-row-cancelled", - "the depot rejects by inserting a CANCELLED audit row", - async ctx => { - await pollUntil( - "NOLINKRS depot row status=CANCELLED", - async () => { - const row = await readReserveRow( - ctx, - Constants.EthereumChainCode, - Constants.EthereumTokenCode, - Constants.NoLinkReserveCode - ) - return ( - row != null && - matchesProtoEnum( - row.status, - SysioReservReservestatus, - SysioReservReservestatus.RESERVE_STATUS_CANCELLED - ) - ) - }, - Constants.RelayDeadlineMs, - Constants.PollIntervalMs - ) - }, - relayStepOptions - ), - verifyStep( - Actor.EthereumOutpost, - "escrow-refunded", - "the RESERVE_CREATE_CANCELLED round-trip refunds the creator's escrow (gas-only spend)", - async ctx => { - const preCreateBalance = ctx.outputs.assert( - ReserveSteps.unlinkedCreatorPreCreateBalanceKey() - ) - const refundFloor = preCreateBalance - Constants.RefundGasAllowanceWei - await pollUntil( - "NOLINKRS creator escrow refunded", - async () => - (await ctx.ethereum.getBalance( - ReserveSteps.unlinkedCreatorWallet(ctx).address - )) > refundFloor, - Constants.ReadyDeadlineMs, - Constants.PollIntervalMs - ) - }, - readyStepOptions - ), - verifyStep( - Actor.EthereumOutpost, - "outpost-record-cancelled", - "the outpost-local NOLINKRS record flips CANCELLED in the same dispatch", - async ctx => { - await pollUntil( - "outpost-local NOLINKRS record CANCELLED", - async () => - (await ReserveSteps.readOutpostReserveStatus( - ctx, - BigInt(Constants.EthereumTokenCode), - BigInt(Constants.NoLinkReserveCode) - )) === EthereumLocalReserveStatus.CANCELLED, - Constants.ReadyDeadlineMs, - Constants.PollIntervalMs - ) - }, - readyStepOptions - ) - ) - - // ── 5. MatcherMustBeLinked — the match gate rejects unlinked, accepts linked ── - ClusterBuildPhase.create( - cluster, - "MatcherMustBeLinked", - "matchreserve gates on the matcher's authex link; the linked matcher activates with exact WIRE escrow" - ).push( - verifyStep( - Actor.Sysio, - "unlinked-matcher-rejected", - "matchreserve from a non-linked account is rejected", - async ctx => { - // wrongmatchr passes every pre-check (row PENDING, exact - // wire_amount) so the failure is attributable to the missing - // authex link alone. - await Assert.rejects( - ctx.wire - .getSysioContract(SysioContractName.reserv) - .actions.matchreserve.invoke( - { - chain_code: SlugName.toString(Constants.EthereumChainCode), - token_code: SlugName.toString(Constants.EthereumTokenCode), - reserve_code: SlugName.toString(Constants.PrivateReserveCode), - matcher: Constants.WrongMatcherAccount, - wire_amount: Number(Constants.RequestedWireAmount) - }, - { - authorization: [ - { - actor: Constants.WrongMatcherAccount, - permission: "active" - } - ], - skipWait: true - } - ), - Constants.MatcherNotLinkedPattern - ) - }, - wireWriteOptions - ), - ReserveSteps.planMatchReserve( - Actor.User, - "match-reserve", - `${Constants.MatcherAccount} escrows ${Constants.RequestedWireAmount} raw WIRE via matchreserve`, - wireWriteOptions, - Constants.EthereumChainCode, - Constants.EthereumTokenCode, - Constants.PrivateReserveCode, - Constants.MatcherAccount, - Constants.RequestedWireAmount - ), - verifyStep( - Actor.Sysio, - "depot-row-active", - "the match flips PRIVRES ACTIVE recording owner + wire book", - async ctx => { - await pollUntil( - "PRIVRES depot row status=ACTIVE", - async () => { - const row = await readReserveRow( - ctx, - Constants.EthereumChainCode, - Constants.EthereumTokenCode, - Constants.PrivateReserveCode - ) - return ( - row != null && - matchesProtoEnum( - row.status, - SysioReservReservestatus, - SysioReservReservestatus.RESERVE_STATUS_ACTIVE - ) - ) - }, - Constants.ReadyDeadlineMs, - Constants.PollIntervalMs - ) - const row = await readReserveRow( - ctx, - Constants.EthereumChainCode, - Constants.EthereumTokenCode, - Constants.PrivateReserveCode - ) - Assert.strictEqual( - row.owner, - Constants.MatcherAccount, - "owner must be the matcher" - ) - Assert.strictEqual( - BigInt(row.reserve_wire_amount), - Constants.RequestedWireAmount, - "reserve_wire_amount must equal the requested amount" - ) - }, - readyStepOptions - ), - verifyStep( - Actor.Sysio, - "wire-custody-exact", - "the match IS a WIRE deposit — custody up, matcher down, exactly", - async ctx => { - const snapshot = ctx.outputs.assert( - ReserveSteps.wireCustodySnapshotKey() - ) - const custodyAfter = await ctx.wire.getWireBalance( - SysioContractAccount[SysioContractName.reserv] - ) - const matcherAfter = await ctx.wire.getWireBalance( - Constants.MatcherAccount - ) - Assert.strictEqual( - custodyAfter, - snapshot.custody + Constants.RequestedWireAmount, - "sysio.reserv custody must increase by exactly the match amount" - ) - Assert.strictEqual( - matcherAfter, - snapshot.matcher - Constants.RequestedWireAmount, - "the matcher must decrease by exactly the match amount" - ) - } - ), - verifyStep( - Actor.EthereumOutpost, - "outpost-record-active", - "RESERVE_READY flips the outpost-local record ACTIVE", - async ctx => { - await pollUntil( - "outpost-local PRIVRES record ACTIVE", - async () => - (await ReserveSteps.readOutpostReserveStatus( - ctx, - BigInt(Constants.EthereumTokenCode), - BigInt(Constants.PrivateReserveCode) - )) === EthereumLocalReserveStatus.ACTIVE, - Constants.ReadyDeadlineMs, - Constants.PollIntervalMs - ) - }, - readyStepOptions - ) - ) - - // ── 6. PrivateRejectPublic — the privacy gate rejects cross-owner pairings ── - ClusterBuildPhase.create( - cluster, - "PrivateRejectPublic", - "A private reserve cannot pair with a public counterpart, in either direction" - ).push( - ReserveSteps.planRequestSwapProbe( - Actor.User, - "swap-private-to-public", - "requestSwap sourcing PRIVRES against the public SOLANA/SOL/PRIMARY reserve", - ethereumWriteOptions, - { - sourceTokenCode: BigInt(Constants.EthereumTokenCode), - sourceReserveCode: BigInt(Constants.PrivateReserveCode), - sourceAmountWei: Constants.SwapProbeSourceEthereumWei, - targetChainCode: BigInt(Constants.SolanaChainCode), - targetTokenCode: BigInt(Constants.SolanaTokenCode), - targetReserveCode: BigInt(Constants.PublicReserveCode), - targetAmount: Constants.SwapProbeTargetAmount, - targetToleranceBps: Constants.SwapProbeToleranceBps - } - ), - verifyStep( - Actor.Sysio, - "no-private-uwreq", - "no UWREQ sourcing the private reserve is ever created", - async ctx => { - // The privacy gate precedes the variance check on the depot, so - // the sentinel target never reaches the quote comparison — the - // swap is rejected and NO uwreq row appears inside the window. - await assertNeverWithinWindow( - "forbidden PRIVRES-sourced UWREQ", - async () => (await readPrivateSourcedUwreq(ctx)) != null, - Constants.NoUwreqWindowMs, - Constants.PollIntervalMs - ) - }, - windowStepOptions - ), - verifyStep( - Actor.Sysio, - "swap-revert-circulated", - "a SWAP_REVERT attestation circulates back to the Ethereum outpost", - async ctx => { - await pollUntil( - "SWAP_REVERT envelope on DEPOT→ETHEREUM", - async () => - containsSwapRevert(oppDebuggingPath(ctx.config.clusterPath)), - Constants.ReadyDeadlineMs, - Constants.PollIntervalMs - ) - }, - readyStepOptions - ), - verifyStep( - Actor.Sysio, - "swapfromwire-rejected", - "swapfromwire targeting a private reserve is rejected at submit", - async ctx => { - // WIRE-endpoint exclusion is symmetric: an outbound from-WIRE swap - // aimed AT a private reserve asserts at push (the reserve is - // ACTIVE, so the privacy check is the one that fires). - const swapUser = ctx.outputs.assert(swapUserOutputKey()) - await Assert.rejects( - ctx.wire - .getSysioContract(SysioContractName.uwrit) - .actions.swapfromwire.invoke( - { - user: Constants.MatcherAccount, - wire_amount: Number(Constants.FromWireProbeWireAmount), - dst_chain_code: SlugName.toString(Constants.EthereumChainCode), - dst_token_code: SlugName.toString(Constants.EthereumTokenCode), - dst_reserve_code: SlugName.toString(Constants.PrivateReserveCode), - target_amount: Number(Constants.FromWireProbeTargetAmount), - target_tolerance_bps: Constants.FromWireProbeToleranceBps, - recipient_kind: SysioUwritChainkind.CHAIN_KIND_EVM, - recipient_addr: Buffer.from( - swapUser.ethereumAddressBytes - ).toString("hex") - }, - { - authorization: [ - { actor: Constants.MatcherAccount, permission: "active" } - ], - skipWait: true - } - ), - Constants.PrivateFromWireExcludedPattern - ) - }, - wireWriteOptions - ) - ) - } -} diff --git a/packages/flow-reserve-lifecycle/src/ReserveLifecycleScenarioConstants.ts b/packages/flow-reserve-lifecycle/src/ReserveLifecycleScenarioConstants.ts deleted file mode 100644 index 6a1bd6dd5..000000000 --- a/packages/flow-reserve-lifecycle/src/ReserveLifecycleScenarioConstants.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { SlugName } from "@wireio/sdk-core" -import { ProtocolTiming, SwapUserIdentities } from "@wireio/cluster-tool" - -/** - * Constants for the reserve-lifecycle flow. Amounts and reserve identities - * carry over from the previously-validated jest run; protocol waits derive - * from the {@link ProtocolTiming} envelope. A linked creator opens a PRIVATE - * ETH reserve (PENDING → matched → ACTIVE with exact WIRE custody), an - * unlinked creator's reserve is cancelled back + refunded, and the private - * reserve is excluded from public pairings and WIRE-endpoint swaps. - */ -export namespace ReserveLifecycleScenarioConstants { - // ── timing (60s epochs per `epoch-stall-is-fatal.md`) ────────────────────── - - /** Epoch duration (s) — the `sysio.epoch::setconfig` floor is 60. */ - export const EpochDurationSec = 60 - /** - * Outpost RESERVE_CREATE → OPP relay → depot `oncrtreserve` row insert — a - * single outpost→depot hop. - */ - export const RelayDeadlineMs = ProtocolTiming.SingleHopBudgetMs - /** - * Depot RESERVE_READY / RESERVE_CREATE_CANCELLED back to the outpost (local - * record flip + refund both ride inbound dispatch) — a single - * depot→outpost hop. - */ - export const ReadyDeadlineMs = ProtocolTiming.SingleHopBudgetMs - /** - * Window in which a FORBIDDEN UWREQ must NOT appear. ~1.5 epochs — long - * enough for the SWAP_REQUEST to have relayed and been rejected, short - * enough to keep the inverted poll cheap. - */ - export const NoUwreqWindowMs = 90_000 - /** Interval for long-running chain-state polls (ms). */ - export const PollIntervalMs = 3_000 - /** Step timeout for single Ethereum-outpost writes (tx submit + 1 confirmation). */ - export const EthereumWriteStepTimeoutMs = 60_000 - /** Step timeout for WIRE depot writes (each waits to irreversibility). */ - export const WireWriteStepTimeoutMs = 120_000 - - // ── reserve identities ───────────────────────────────────────────────────── - - /** Registered chain slug codes (must match the bootstrap registry seed). */ - export const EthereumChainCode = SlugName.from("ETHEREUM") - export const SolanaChainCode = SlugName.from("SOLANA") - /** Registered token slug codes. */ - export const EthereumTokenCode = SlugName.from("ETH") - export const SolanaTokenCode = SlugName.from("SOL") - /** The gated + PRIVATE reserve — linked creator, real WIRE match. */ - export const PrivateReserveCode = SlugName.from("PRIVRES") - /** - * The unlinked-creator reserve — depot cancels it back and the outpost - * refunds. `NOLINKRS` (not `NOLINKRES`) because slug_names cap at 8 chars. - */ - export const NoLinkReserveCode = SlugName.from("NOLINKRS") - /** - * Bootstrap-seeded public counterpart proving private↔public pairing is - * rejected — the (f) probe pairs the private reserve against this one, so - * it must exist for the rejection to be attributable to privacy. - */ - export const PublicReserveCode = SlugName.from("PRIMARY") - - // ── create_reserve parameters ────────────────────────────────────────────── - - /** 0.05 ETH escrowed into PRIVRES (msg.value — must equal the arg). */ - export const PrivateEscrowWei = 50_000_000_000_000_000n - /** 0.02 ETH escrowed into NOLINKRS — refunded on the cancel round-trip. */ - export const NoLinkEscrowWei = 20_000_000_000_000_000n - /** - * WIRE (raw 9-dp base units) the matcher must escrow — `matchreserve` - * requires `wire_amount == requested_wire_amount` EXACTLY, so the negative - * and positive match pushes both pass this verbatim. - */ - export const RequestedWireAmount = 1_000_000n - /** 50% Bancor connector weight = pure constant product. */ - export const ConnectorWeightBps = 5000 - /** Display metadata forwarded verbatim to the depot row. */ - export const PrivateReserveName = "ETHEREUM-ETH/WIRE private reserve" - export const PrivateReserveDescription = "flow-reserve-lifecycle gated+private reserve" - export const NoLinkReserveName = "ETHEREUM-ETH/WIRE unlinked-creator reserve" - export const NoLinkReserveDescription = "flow-reserve-lifecycle cancelled-back reserve" - - // ── private↔public swap probe (the depot rejects on privacy, pre-variance) ─ - - /** 0.01 ETH source deposit (refunded by the SWAP_REVERT). */ - export const SwapProbeSourceEthereumWei = 10_000_000_000_000_000n - /** Positive sentinel — never reaches the variance comparison. */ - export const SwapProbeTargetAmount = 1n - /** 5% — irrelevant to the privacy gate; matches template defaults. */ - export const SwapProbeToleranceBps = 500 - - // ── `swapfromwire` private-exclusion probe ───────────────────────────────── - - /** - * 0.1 WIRE — meets the dev-cluster `min_fromwire_amount` floor exactly, so - * the privacy gate (not the escrow floor) is what rejects the push. Never - * escrowed; the push asserts before the transfer. - */ - export const FromWireProbeWireAmount = 100_000_000n - /** Positive sentinel — the privacy assert fires first. */ - export const FromWireProbeTargetAmount = 1n - export const FromWireProbeToleranceBps = 500 - - // ── WIRE accounts provisioned by this flow ───────────────────────────────── - - /** The authex-linked matcher that legitimately activates PRIVRES. */ - export const MatcherAccount = "rsrvmatcher" - /** Funded the same as the matcher but NEVER authex-linked. */ - export const WrongMatcherAccount = "wrongmatchr" - /** 1 WIRE — covers the 0.001 WIRE match escrow with generous slack. */ - export const MatcherFunding = 1_000_000_000n - /** Same funding so only the missing link differentiates the rejection. */ - export const WrongMatcherFunding = 1_000_000_000n - - // ── ETH-side allowances for the unlinked-creator refund assertion ────────── - - /** 0.1 ETH seeded onto the new unlinked wallet from anvil signer 0. */ - export const NoLinkWalletFundingWei = 100_000_000_000_000_000n - /** - * 0.005 ETH gas allowance — after the refund lands, the creator's balance - * must exceed `preCreate - this` (create_reserve gas is the only - * unrecovered spend). - */ - export const RefundGasAllowanceWei = 5_000_000_000_000_000n - - /** - * Anvil HD-derivation index of the unlinked creator wallet — one past the - * shared swap-user slot so it can never collide with an operator or the - * linked creator wallet. - */ - export const NoLinkCreatorHdIndex = SwapUserIdentities.DefaultEthereumHdIndex + 1 - - // ── cluster option defaults ──────────────────────────────────────────────── - - /** Per-chain underwriter collateral minimum (raw outpost units). */ - export const UnderwriterMinimumBond = 1_000_000_000 - - // ── depot assert-message patterns (the negative gates match on these) ────── - - /** `sysio.reserv::matchreserve` rejection for a matcher with no authex link. */ - export const MatcherNotLinkedPattern = /matcher has no authex link/ - /** `sysio.uwrit::swapfromwire` rejection for a private destination reserve. */ - export const PrivateFromWireExcludedPattern = /private reserves are excluded from WIRE-endpoint swaps/ - - // ── misc ─────────────────────────────────────────────────────────────────── - - /** The `pollUntil` deadline-expiry message fragment (an expiry IS the inverted-poll pass). */ - export const PollTimeoutMessageFragment = "Timed out waiting for" - /** Outpost contract name for artifact/address resolution. */ - export const ReserveManagerContractName = "ReserveManager" -} diff --git a/packages/flow-reserve-lifecycle/src/index.ts b/packages/flow-reserve-lifecycle/src/index.ts deleted file mode 100644 index 8e1cf89e6..000000000 --- a/packages/flow-reserve-lifecycle/src/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { FlowCLI } from "@wireio/cluster-tool" -import { ReserveLifecycleScenario } from "./ReserveLifecycleScenario.js" - -/** Run the reserve-lifecycle flow as an executable — exit code = report success. */ -async function main(): Promise { - const report = await FlowCLI.create(ReserveLifecycleScenario).run() - process.exit(report.succeeded ? 0 : 1) -} - -void main() diff --git a/packages/flow-reserve-lifecycle/src/steps/ReserveLifecycleScenarioOwnerSteps.ts b/packages/flow-reserve-lifecycle/src/steps/ReserveLifecycleScenarioOwnerSteps.ts deleted file mode 100644 index e2af78311..000000000 --- a/packages/flow-reserve-lifecycle/src/steps/ReserveLifecycleScenarioOwnerSteps.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { ChainKind } from "@wireio/opp-typescript-models" -import { - AuthExLinkTool, - ClusterBuildStep, - Report, - ethereumPrivateKeyFromWallet, - provisionWireUser, - swapUserOutputKey, - type ClusterBuildContext, - type ClusterBuildStepOptions, - type StepInput -} from "@wireio/cluster-tool" - -/** - * Flow-local Step factories for the reserve OWNER provisioning writes: the - * WIRE matcher accounts (create + policy + funding via the harness's - * `provisionWireUser`) and the `sysio.authex::createlink` binding the matcher - * to the reserve creator's Ethereum key. Every on-chain WRITE is its own - * {@link ClusterBuildStep} so the `Report` records it. - */ -export namespace ReserveLifecycleScenarioOwnerSteps { - // ── Step: provision a WIRE user (create + policy + optional funding) ────── - - /** Input for {@link planProvisionUser} — one WIRE-user provisioning write set. */ - export interface ProvisionUserInput extends StepInput { - readonly kind: "ReserveLifecycleScenarioOwnerSteps.ProvisionUserInput" - /** WIRE account name to provision (1..12 chars, base32 alphabet). */ - readonly account: string - /** Raw 9-decimal WIRE base units transferred from the `sysio` treasury. */ - readonly fundWireAmount: bigint - } - - /** - * Provision one WIRE user account via the harness's `provisionWireUser` - * (idempotent create under the dev key + resource policy + treasury funding). - * - * @param actor - The narrative subject. - * @param name - Step name (report row). - * @param description - One-line description. - * @param options - Per-step tuning (e.g. `timeoutMs`). - * @param account - WIRE account name to provision. - * @param fundWireAmount - Raw WIRE base units to fund the account with. - * @returns The definition step. - */ - export function planProvisionUser< - C extends ClusterBuildContext = ClusterBuildContext - >( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - account: string, - fundWireAmount: bigint - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { - kind: "ReserveLifecycleScenarioOwnerSteps.ProvisionUserInput", - account, - fundWireAmount - }, - runProvisionUser - ) - } - - /** Named runner — provision the account through the harness helper. */ - export async function runProvisionUser( - ctx: C, - input: ProvisionUserInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - await provisionWireUser(ctx.wire, input.account, { - fundWireAmount: input.fundWireAmount - }) - } - - // ── Step: authex link (matcher ↔ the creator wallet's secp256k1 key) ────── - - /** Input for {@link planCreateLink} — one `sysio.authex::createlink` write. */ - export interface CreateLinkInput extends StepInput { - readonly kind: "ReserveLifecycleScenarioOwnerSteps.CreateLinkInput" - /** WIRE account the creator key is linked TO. */ - readonly account: string - } - - /** - * Bind `account` to the swap-user (reserve creator) wallet's secp256k1 key - * via `sysio.authex::createlink`. The depot's `oncrtreserve` accepts the - * create because the CREATOR key has a link; `matchreserve` later requires - * the MATCHER's link key to equal the creator key — this single link - * satisfies both sides. - * - * @param actor - The narrative subject. - * @param name - Step name (report row). - * @param description - One-line description. - * @param options - Per-step tuning (e.g. `timeoutMs`). - * @param account - WIRE account to link the creator key to. - * @returns The definition step. - */ - export function planCreateLink< - C extends ClusterBuildContext = ClusterBuildContext - >( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - account: string - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { kind: "ReserveLifecycleScenarioOwnerSteps.CreateLinkInput", account }, - runCreateLink - ) - } - - /** Named runner — ONE EVM `createlink` write, signed with the creator wallet key. */ - export async function runCreateLink( - ctx: C, - input: CreateLinkInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - const swapUser = ctx.outputs.assert(swapUserOutputKey()) - await AuthExLinkTool.createLink(ctx.wire, { - chainKind: ChainKind.EVM, - account: input.account, - privateKey: ethereumPrivateKeyFromWallet(swapUser.ethereumWallet), - ethereumWallet: swapUser.ethereumWallet - }) - } -} diff --git a/packages/flow-reserve-lifecycle/src/steps/ReserveLifecycleScenarioReserveSteps.ts b/packages/flow-reserve-lifecycle/src/steps/ReserveLifecycleScenarioReserveSteps.ts deleted file mode 100644 index 6bc15d2e3..000000000 --- a/packages/flow-reserve-lifecycle/src/steps/ReserveLifecycleScenarioReserveSteps.ts +++ /dev/null @@ -1,551 +0,0 @@ -import Assert from "node:assert" -import { ethers } from "ethers" -import { SlugName, SysioContracts } from "@wireio/sdk-core" -import { - ClusterBuildStep, - EthereumCollateralTool, - EthereumOutpostBootstrapper, - Report, - outputKey, - requestEthereumSwap, - contractView, - resolveLatestNonce, - swapUserOutputKey, - type ClusterBuildContext, - type ClusterBuildStepOptions, - type EthereumLocalReserveRecord, - type EthereumPayableOverrides, - type EthereumReserveCreateArgs, - type EthereumSwapRequest, - type OutputKey, - type ReserveManagerRequestSwapContract, - type StepInput, - ClusterConfigProvider -} from "@wireio/cluster-tool" -import { ReserveLifecycleScenarioConstants as Constants } from "../ReserveLifecycleScenarioConstants.js" - -const { SysioContractAccount, SysioContractName } = SysioContracts - -/** - * Flow-local Step factories for the reserve-lifecycle WRITES: the two native - * `ReserveManager.create_reserve` submissions (linked + unlinked creators), - * the unlinked-creator gas funding, the depot `sysio.reserv::matchreserve` - * escrow write, and the private↔public `requestSwap` probe. Every on-chain - * WRITE is its own {@link ClusterBuildStep}; the ReserveManager binding + - * wallet derivation are pure value helpers used INSIDE the runners, and - * {@link readOutpostReserveStatus} is a free READ for verify steps. - */ -export namespace ReserveLifecycleScenarioReserveSteps { - /** - * The native reserve-create args — the ERC-20 struct from - * `EthereumSwapTool` minus `creatorPubKey`, which each runner injects from - * its signing wallet at run time (the contract verifies the compressed key - * derives to `msg.sender`). - */ - export type NativeReserveCreateArgs = Omit< - EthereumReserveCreateArgs, - "creatorPubKey" - > - - /** The swap-probe args — `EthereumSwapRequest` minus the runtime recipient. */ - export type SwapProbeRequest = Omit - - /** - * Structural surface of the `ReserveManager` members this flow binds: the - * NATIVE positional `create_reserve` (the ERC-20 struct variants live on - * `EthereumSwapTool`) and the `getReserve` local-record read. - */ - export interface ReserveManagerNativeContract extends ethers.BaseContract { - create_reserve: ( - tokenCode: bigint, - reserveCode: bigint, - externalTokenAmount: bigint, - requestedWireAmount: bigint, - connectorWeightBps: number, - name: string, - description: string, - isPrivate: boolean, - creatorPubKey: string, - overrides: EthereumPayableOverrides - ) => Promise - getReserve: ( - tokenCode: bigint, - reserveCode: bigint - ) => Promise - } - - // ── cross-step output keys ───────────────────────────────────────────────── - - /** WIRE balances snapshotted immediately before the `matchreserve` push. */ - export interface WireCustodySnapshot { - /** `sysio.reserv` custody balance (raw 9-dp base units). */ - readonly custody: bigint - /** The matcher's balance (raw 9-dp base units). */ - readonly matcher: bigint - } - - /** - * Typed cross-step output key for the pre-match WIRE balance snapshot — the - * custody-exact verify reads it back after the row flips ACTIVE. - * - * @returns A typed `OutputKey` for `ctx.outputs`. - */ - export function wireCustodySnapshotKey(): OutputKey { - return outputKey( - "reserveLifecycleWireCustodySnapshot", - "WIRE custody + matcher balances before matchreserve" - ) - } - - /** - * Typed cross-step output key for the unlinked creator's ETH balance right - * before its `create_reserve` — the refund verify computes the floor - * (`preCreate - gas allowance`) from it. - * - * @returns A typed `OutputKey` for `ctx.outputs`. - */ - export function unlinkedCreatorPreCreateBalanceKey(): OutputKey { - return outputKey( - "reserveLifecycleUnlinkedCreatorPreCreateBalance", - "unlinked creator's wei balance before create_reserve" - ) - } - - // ── value helpers (derivation / binding / reads — used INSIDE runners) ───── - - /** - * The deterministic unlinked-creator wallet — one anvil HD slot past the - * shared swap-user slot, funded for gas + escrow but NEVER authex-linked. - * - * @param ctx - The build context (supplies the Ethereum provider). - * @returns The connected HD wallet. - */ - export function unlinkedCreatorWallet( - ctx: C - ): ethers.HDNodeWallet { - return ethers.HDNodeWallet.fromMnemonic( - ethers.Mnemonic.fromPhrase(EthereumOutpostBootstrapper.AnvilMnemonic), - `${EthereumOutpostBootstrapper.DerivationPath}${Constants.NoLinkCreatorHdIndex}` - ).connect(ctx.ethereum.provider) - } - - /** - * Resolve the `ReserveManager` contract from the run's deploy artifacts, - * bound to `signer`. Address + ABI via `EthereumCollateralTool`'s outpost - * artifact loaders. - * - * @param ctx - The build context (supplies `config.ethereumPath`). - * @param signer - The signer to bind (any signer works for reads). - * @returns The bound contract, typed to the surfaces this flow uses. - */ - export function loadReserveManager( - ctx: C, - signer: ethers.Signer - ): ReserveManagerNativeContract & ReserveManagerRequestSwapContract { - const address = EthereumCollateralTool.loadOutpostAddresses( - ClusterConfigProvider.ethereumDeploymentsPath(ctx.config) - )[Constants.ReserveManagerContractName] - Assert.ok( - address != null && /^0x[0-9a-fA-F]{40}$/.test(address), - `ReserveLifecycleScenarioReserveSteps: ReserveManager not in outpost-addrs.json (got ${address})` - ) - const abi = EthereumCollateralTool.loadOutpostAbi( - ctx.config.ethereumPath, - Constants.ReserveManagerContractName - ) - return contractView< - ReserveManagerNativeContract & ReserveManagerRequestSwapContract - >(address, abi, signer) - } - - /** - * READ the outpost-LOCAL reserve record status (the `ReserveManager` - * storage mirror, `EthereumLocalReserveStatus`-valued). A read — executes - * freely inside verify steps. - * - * @param ctx - The build context. - * @param tokenCode - The reserve's token slug (uint64 as bigint). - * @param reserveCode - The reserve's own slug (uint64 as bigint). - * @returns The record's numeric local status. - */ - export async function readOutpostReserveStatus( - ctx: C, - tokenCode: bigint, - reserveCode: bigint - ): Promise { - const reserveManager = loadReserveManager(ctx, ctx.ethereum.wallet.signer) - const record = await reserveManager.getReserve(tokenCode, reserveCode) - return Number(record.status) - } - - /** ONE native `create_reserve` write from `wallet`, escrow riding `msg.value`. */ - async function sendCreateReserve( - ctx: C, - wallet: ethers.HDNodeWallet, - create: EthereumReserveCreateArgs - ): Promise { - Assert.ok( - create.externalTokenAmount > 0n, - "ReserveLifecycleScenarioReserveSteps: externalTokenAmount must be > 0" - ) - const reserveManager = loadReserveManager(ctx, wallet) - const nonce = await resolveLatestNonce(reserveManager) - const response = await reserveManager.create_reserve( - create.tokenCode, - create.reserveCode, - create.externalTokenAmount, - create.requestedWireAmount, - create.connectorWeightBps, - create.name, - create.description, - create.isPrivate, - create.creatorPubKey, - { value: create.externalTokenAmount, nonce } - ) - const receipt = await response.wait(1) - Assert.ok( - receipt?.status === 1, - `ReserveLifecycleScenarioReserveSteps: create_reserve reverted (status=${receipt?.status ?? "null"})` - ) - } - - // ── Step: create_reserve from the LINKED creator (the swap-user wallet) ─── - - /** Input for {@link planCreateReserve} — one linked-creator `create_reserve` write. */ - export interface CreateReserveInput extends StepInput { - readonly kind: "ReserveLifecycleScenarioReserveSteps.CreateReserveInput" - /** The create args; the runner injects the creator's compressed pubkey. */ - readonly create: NativeReserveCreateArgs - } - - /** - * A single native `create_reserve` write signed by the AUTHEX-LINKED - * creator (the swap-user wallet from `ctx.outputs`). The contract escrows - * `externalTokenAmount` wei and queues the RESERVE_CREATE attestation. - * - * @param actor - The narrative subject. - * @param name - Step name (report row). - * @param description - One-line description. - * @param options - Per-step tuning (e.g. `timeoutMs`). - * @param create - The create args (pubkey injected at run time). - * @returns The definition step. - */ - export function planCreateReserve< - C extends ClusterBuildContext = ClusterBuildContext - >( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - create: NativeReserveCreateArgs - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { - kind: "ReserveLifecycleScenarioReserveSteps.CreateReserveInput", - create - }, - runCreateReserve - ) - } - - /** Named runner — ONE `create_reserve` write from the linked swap-user wallet. */ - export async function runCreateReserve( - ctx: C, - input: CreateReserveInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - const wallet = ctx.outputs.assert(swapUserOutputKey()).ethereumWallet - await sendCreateReserve(ctx, wallet, { - ...input.create, - creatorPubKey: wallet.signingKey.compressedPublicKey - }) - } - - // ── Step: create_reserve from the UNLINKED creator (new HD wallet) ────── - - /** Input for {@link planCreateReserveUnlinked} — one unlinked-creator create write. */ - export interface CreateReserveUnlinkedInput extends StepInput { - readonly kind: "ReserveLifecycleScenarioReserveSteps.CreateReserveUnlinkedInput" - /** The create args; the runner injects the creator's compressed pubkey. */ - readonly create: NativeReserveCreateArgs - } - - /** - * A single native `create_reserve` write signed by the NEVER-LINKED creator - * wallet ({@link unlinkedCreatorWallet}). The runner snapshots the wallet's - * pre-create balance into `ctx.outputs` - * ({@link unlinkedCreatorPreCreateBalanceKey}) so the refund verify can - * compute its floor. The depot cancels the create back - * (RESERVE_CREATE_CANCELLED) because the creator key has no authex link. - * - * @param actor - The narrative subject. - * @param name - Step name (report row). - * @param description - One-line description. - * @param options - Per-step tuning (e.g. `timeoutMs`). - * @param create - The create args (pubkey injected at run time). - * @returns The definition step. - */ - export function planCreateReserveUnlinked< - C extends ClusterBuildContext = ClusterBuildContext - >( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - create: NativeReserveCreateArgs - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { - kind: "ReserveLifecycleScenarioReserveSteps.CreateReserveUnlinkedInput", - create - }, - runCreateReserveUnlinked - ) - } - - /** Named runner — snapshot the pre-create balance, then ONE `create_reserve` write. */ - export async function runCreateReserveUnlinked( - ctx: C, - input: CreateReserveUnlinkedInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - const wallet = unlinkedCreatorWallet(ctx) - const preCreateBalance = await ctx.ethereum.getBalance(wallet.address) - ctx.outputs.set(unlinkedCreatorPreCreateBalanceKey(), preCreateBalance) - await sendCreateReserve(ctx, wallet, { - ...input.create, - creatorPubKey: wallet.signingKey.compressedPublicKey - }) - } - - // ── Step: fund the unlinked creator wallet (gas + escrow headroom) ──────── - - /** Input for {@link planFundUnlinkedCreator} — one wei transfer write. */ - export interface FundUnlinkedCreatorInput extends StepInput { - readonly kind: "ReserveLifecycleScenarioReserveSteps.FundUnlinkedCreatorInput" - /** Wei sent from anvil signer 0 to the unlinked creator wallet. */ - readonly amountWei: bigint - } - - /** - * A single wei transfer from the harness's default anvil signer to the - * unlinked creator wallet, covering its create escrow + gas. - * - * @param actor - The narrative subject. - * @param name - Step name (report row). - * @param description - One-line description. - * @param options - Per-step tuning (e.g. `timeoutMs`). - * @param amountWei - Wei to seed the wallet with. - * @returns The definition step. - */ - export function planFundUnlinkedCreator< - C extends ClusterBuildContext = ClusterBuildContext - >( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - amountWei: bigint - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { - kind: "ReserveLifecycleScenarioReserveSteps.FundUnlinkedCreatorInput", - amountWei - }, - runFundUnlinkedCreator - ) - } - - /** Named runner — ONE `sendTransaction` write from anvil signer 0. */ - export async function runFundUnlinkedCreator( - ctx: C, - input: FundUnlinkedCreatorInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - Assert.ok( - input.amountWei > 0n, - "ReserveLifecycleScenarioReserveSteps: funding amountWei must be > 0" - ) - const response = await ctx.ethereum.wallet.signer.sendTransaction({ - to: unlinkedCreatorWallet(ctx).address, - value: input.amountWei - }) - const receipt = await response.wait(1) - Assert.ok( - receipt?.status === 1, - `ReserveLifecycleScenarioReserveSteps: funding tx reverted (status=${receipt?.status ?? "null"})` - ) - } - - // ── Step: matchreserve (the depot escrow write that activates the row) ──── - - /** Input for {@link planMatchReserve} — one `sysio.reserv::matchreserve` write. */ - export interface MatchReserveInput extends StepInput { - readonly kind: "ReserveLifecycleScenarioReserveSteps.MatchReserveInput" - /** The reserve's chain slug value. */ - readonly chainCode: number - /** The reserve's token slug value. */ - readonly tokenCode: number - /** The reserve's own slug value. */ - readonly reserveCode: number - /** The WIRE account escrowing the match (must be authex-linked to the creator key). */ - readonly matcher: string - /** Raw WIRE base units — must equal the row's `requested_wire_amount` exactly. */ - readonly wireAmount: bigint - } - - /** - * A single `sysio.reserv::matchreserve` write authorized by the matcher. - * The runner snapshots the `sysio.reserv` custody + matcher WIRE balances - * into `ctx.outputs` ({@link wireCustodySnapshotKey}) immediately before - * the push so the custody-exact verify can assert the transfer amounts. - * - * @param actor - The narrative subject. - * @param name - Step name (report row). - * @param description - One-line description. - * @param options - Per-step tuning (e.g. `timeoutMs`). - * @param chainCode - The reserve's chain slug value. - * @param tokenCode - The reserve's token slug value. - * @param reserveCode - The reserve's own slug value. - * @param matcher - The authex-linked WIRE matcher account. - * @param wireAmount - Raw WIRE base units to escrow (exact match required). - * @returns The definition step. - */ - export function planMatchReserve< - C extends ClusterBuildContext = ClusterBuildContext - >( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - chainCode: number, - tokenCode: number, - reserveCode: number, - matcher: string, - wireAmount: bigint - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { - kind: "ReserveLifecycleScenarioReserveSteps.MatchReserveInput", - chainCode, - tokenCode, - reserveCode, - matcher, - wireAmount - }, - runMatchReserve - ) - } - - /** Named runner — snapshot balances, then ONE `matchreserve` write. */ - export async function runMatchReserve( - ctx: C, - input: MatchReserveInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - const custody = await ctx.wire.getWireBalance( - SysioContractAccount[SysioContractName.reserv] - ) - const matcher = await ctx.wire.getWireBalance(input.matcher) - Assert.ok( - matcher >= input.wireAmount, - `ReserveLifecycleScenarioReserveSteps: matcher ${input.matcher} holds ${matcher}, needs ${input.wireAmount}` - ) - ctx.outputs.set(wireCustodySnapshotKey(), { custody, matcher }) - await ctx.wire - .getSysioContract(SysioContractName.reserv) - .actions.matchreserve.invoke( - { - chain_code: SlugName.toString(input.chainCode), - token_code: SlugName.toString(input.tokenCode), - reserve_code: SlugName.toString(input.reserveCode), - matcher: input.matcher, - wire_amount: Number(input.wireAmount) - }, - { authorization: [{ actor: input.matcher, permission: "active" }] } - ) - } - - // ── Step: the private↔public swap probe (rejected by the privacy gate) ──── - - /** Input for {@link planRequestSwapProbe} — one `requestSwap` write. */ - export interface RequestSwapProbeInput extends StepInput { - readonly kind: "ReserveLifecycleScenarioReserveSteps.RequestSwapProbeInput" - /** The swap args; the runner injects the swap user's SOL recipient bytes. */ - readonly request: SwapProbeRequest - } - - /** - * A single `ReserveManager.requestSwap` write from the swap-user wallet, - * sourcing the PRIVATE reserve against a PUBLIC counterpart. The outpost - * accepts the tx (escrow + SWAP_REQUEST attestation); the DEPOT's privacy - * gate then rejects the pairing with a SWAP_REVERT and never opens a UWREQ. - * - * @param actor - The narrative subject. - * @param name - Step name (report row). - * @param description - One-line description. - * @param options - Per-step tuning (e.g. `timeoutMs`). - * @param request - The swap args (recipient injected at run time). - * @returns The definition step. - */ - export function planRequestSwapProbe< - C extends ClusterBuildContext = ClusterBuildContext - >( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - request: SwapProbeRequest - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { - kind: "ReserveLifecycleScenarioReserveSteps.RequestSwapProbeInput", - request - }, - runRequestSwapProbe - ) - } - - /** Named runner — ONE `requestSwap` write via the harness's `requestEthereumSwap`. */ - export async function runRequestSwapProbe( - ctx: C, - input: RequestSwapProbeInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - const swapUser = ctx.outputs.assert(swapUserOutputKey()) - const reserveManager = loadReserveManager(ctx, swapUser.ethereumWallet) - const result = await requestEthereumSwap(reserveManager, { - ...input.request, - targetRecipient: swapUser.solanaPublicKeyBytes - }) - Assert.ok( - result.transactionHash.length > 0, - "ReserveLifecycleScenarioReserveSteps: requestSwap returned no transaction hash" - ) - } -} diff --git a/packages/flow-reserve-lifecycle/tsconfig.json b/packages/flow-reserve-lifecycle/tsconfig.json deleted file mode 100644 index 82b017421..000000000 --- a/packages/flow-reserve-lifecycle/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-reserve-lifecycle/tsconfig.src.json b/packages/flow-reserve-lifecycle/tsconfig.src.json deleted file mode 100644 index e67621b57..000000000 --- a/packages/flow-reserve-lifecycle/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/flow-swap-from-wire/package.json b/packages/flow-swap-from-wire/package.json deleted file mode 100644 index b761ad178..000000000 --- a/packages/flow-swap-from-wire/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "@wireio/test-flow-swap-from-wire", - "version": "0.1.18", - "private": true, - "type": "commonjs", - "description": "Flow: Swap FROM WIRE (WIRE depot → Solana, queued escrow + single-leg underwriting)", - "scripts": { - "build": "tsc -b tsconfig.json", - "test:disabled": "node lib/index.js", - "clean": "rm -rf lib" - }, - "dependencies": { - "@wireio/cluster-tool": "workspace:*", - "@wireio/sdk-core": "^1.0.92", - "@wireio/shared": "^1.0.92" - }, - "devDependencies": { - "@types/node": "25.5.0" - } -} diff --git a/packages/flow-swap-from-wire/src/SwapFromWireScenario.ts b/packages/flow-swap-from-wire/src/SwapFromWireScenario.ts deleted file mode 100644 index 19af86df7..000000000 --- a/packages/flow-swap-from-wire/src/SwapFromWireScenario.ts +++ /dev/null @@ -1,578 +0,0 @@ -import Assert from "node:assert" -import type { ClusterConfig } from "@wireio/cluster-tool-shared" -import { SysioContracts } from "@wireio/sdk-core" -import { getLogger } from "@wireio/shared" -import { - ClusterBuildPhase, - Constants as HarnessConstants, - FlowScenario, - Report, - Steps, - SwapScenarioContext, - SwapUserIdentities, - WireReserveTool, - WireUnderwriterTool, - matchesProtoEnum, - pollUntil, - slugValue, - swapUserOutputKey, - verifyStep, - type ClusterBuild, - type ClusterBuildOptions, - type Logger -} from "@wireio/cluster-tool" -import { SwapFromWireScenarioConstants as Constants } from "./SwapFromWireScenarioConstants.js" -import { SwapFromWireScenarioOutputs as Outputs } from "./SwapFromWireScenarioOutputs.js" -import { SwapFromWireScenarioUwritSteps } from "./steps/index.js" - -const { - SysioContractName, - SysioContractAccount, - SysioOpregOperatorstatus, - SysioUwritUnderwriterequeststatus -} = SysioContracts -const { Actor } = Report - -const log = getLogger(__filename) - -/** The account holding escrowed swap WIRE — `sysio.reserv`'s token balance. */ -const ReserveCustodyAccount = SysioContractAccount[SysioContractName.reserv] - -/** - * The WIRE-leg fee split for `wireAmount` at the LIVE cluster fee rate — a - * thin composition of {@link WireReserveTool.readFeeBps} (the live - * `sysio.uwrit::uwconfig.fee_bps` the bootstrap's `setconfig` seeded) into - * {@link WireReserveTool.splitWireFee}, so the flow's book math always matches - * the depot's, whatever rate the harness configures (a read). - * - * @param ctx - The scenario context. - * @param wireAmount - The gross WIRE leg to split. - * @returns The {@link WireReserveTool.WireFee} decomposition at the live rate. - */ -async function readWireLegFee( - ctx: SwapScenarioContext, - wireAmount: bigint -): Promise { - return WireReserveTool.splitWireFee( - wireAmount, - await WireReserveTool.readFeeBps(ctx.wire) - ) -} - -/** The from-WIRE uwreq row (src=WIRE, dst=SOLANA), or nothing yet (a read). */ -function readFromWireUwreq(ctx: SwapScenarioContext) { - return ctx.uwreq(Constants.WireChainCode, Constants.SolanaChainCode) -} - -/** Whether every `labels` entry is `OPERATOR_STATUS_ACTIVE` on `sysio.opreg` (a read). */ -async function readAllOperatorsActive( - ctx: SwapScenarioContext, - labels: string[] -): Promise { - const { rows } = await ctx.wire - .getSysioContract(SysioContractName.opreg) - .tables.operators.query({ limit: 100 }) - return labels.every(label => { - const account = ctx.keyStore.assertOperator(label).account, - operator = rows.find(row => row.account === account) - return ( - operator != null && - matchesProtoEnum( - operator.status, - SysioOpregOperatorstatus, - SysioOpregOperatorstatus.OPERATOR_STATUS_ACTIVE - ) - ) - }) -} - -/** - * Swap FROM WIRE — the WIRE depot itself is the source chain, driven - * end-to-end through OPP against the Solana outpost: - * - * 1. **SubstrateHealth** — the chain produces blocks; the bootstrap seeded the - * SOLANA/SOL/PRIMARY reserve. - * 2. **UnderwriterCollateral** / **UnderwriterActivation** — the bootstrap - * underwriters bond the default collateral on both outposts and flip - * `OPERATOR_STATUS_ACTIVE` once the deposits credit. - * 3. **ProvisionSwapUser** / **ProvisionDepositor** — the Solana recipient - * identity + the WIRE depositor (funded from the treasury). - * 4. **QuoteAndEscrow** — quote `cp_output(dst.wire, dst.chain, wire_in)` off - * the destination reserve, snapshot balances, push - * `sysio.uwrit::swapfromwire`: the WIRE escrows NOW (depositor −, custody +, - * EXACT) and NO uwreq exists yet. - * 5. **QueueDrain** — the next `sysio.epoch::advance` drains the queue into a - * PENDING uwreq with `src = WIRE`, the escrowed `src_amount`, and the - * depot-origin bit-63 id tag. - * 6. **RaceTargetLeg** — underwriters race the TARGET leg only: CONFIRMED with - * exactly ONE lock (on SOLANA); at emit time the escrow became dst-reserve - * WIRE liquidity net of the WIRE-leg fee (#414) and the chain side was - * debited by the target. - * 7. **RemitAndReceive** — a normal SWAP_REMIT pays the recipient on Solana. - * 8. **DrainRewards** — custody settles at `before + source − fee` once - * `payepoch → sysio.reserv::drainrewards` drains the rewards bucket (#425); - * the target-leg lock persists through the challenge window. - */ -export class SwapFromWireScenario extends FlowScenario { - readonly name = "flow-swap-from-wire" - readonly description = - "Swap FROM WIRE (WIRE depot → Solana): queued escrow, single-leg underwriting, SOL payout, rewards drain" - - override readonly defaults: ClusterBuildOptions = { - // Seed the mock (chain, token) PRIMARY reserves this flow reads — `regreserve` - // is epoch-0-gated by the depot, so it must ride the bootstrap, not a flow phase. - enableMockReserves: true, - // The launch build of the Solana outpost withholds the reserve/swap - // bootstrap calls; this flow exercises them, so it opts in (never in plan()). - enableLaunchWithheldOperations: true, - epochDurationSec: Constants.EpochDurationSec, - // ACTIVE gates on real bonds on EVERY registered outpost chain, so the - // flow's underwriter-activation assertion is meaningful. The default - // collateral plan also funds and bonds WIRE on the depot; that bond is not - // one of these requirements. - requiredUnderwriterCollateral: [ - { - chainCode: Constants.EthereumChainCode, - tokenCode: Constants.EthereumTokenCode, - minimumBond: Number(Constants.UnderwriterMinimumBond) - }, - { - chainCode: Constants.SolanaChainCode, - tokenCode: Constants.SolanaTokenCode, - minimumBond: Number(Constants.UnderwriterMinimumBond) - } - ] - } - - /** The swap flows share the {@link SwapScenarioContext} query surface. */ - override createContext( - config: ClusterConfig, - log: Logger - ): SwapScenarioContext { - return new SwapScenarioContext(config, log) - } - - plan(cluster: ClusterBuild): void { - const config = cluster.context.config, - underwriterLabels = Array.from( - { length: config.underwriterCount }, - (_, index) => HarnessConstants.underwriterLabel(index) - ), - writeStepOptions = { timeoutMs: 60_000 }, - relayStepOptions = { - timeoutMs: Constants.relayDeadlineMs() + Constants.PollDeadlineBufferMs - }, - drainStepOptions = { - timeoutMs: Constants.DrainDeadlineMs + Constants.PollDeadlineBufferMs - }, - raceStepOptions = { - timeoutMs: Constants.RaceDeadlineMs + Constants.PollDeadlineBufferMs - }, - remitStepOptions = { - timeoutMs: Constants.RemitDeadlineMs + Constants.PollDeadlineBufferMs - } - - // ── 1. Substrate health — blocks + the bootstrap-seeded reserve ── - ClusterBuildPhase.create( - cluster, - "SubstrateHealth", - "The chain produces blocks; the SOL reserve is seeded" - ).push( - verifyStep( - Actor.Sysio, - "chain-producing", - "WIRE chain is producing blocks", - async ctx => { - const info = await ctx.wire.getInfo() - Assert.ok( - Number(info.head_block_num) > 0, - "head_block_num must be positive on a live chain" - ) - } - ), - verifyStep( - Actor.Sysio, - "solana-reserve-seeded", - "bootstrap seeded the SOLANA/SOL/PRIMARY reserve", - async ctx => { - // reserveBook asserts the row exists (throws when absent). - await ctx.reserveBook( - Constants.SolanaChainCode, - Constants.SolanaTokenCode, - Constants.PrimaryReserveCode - ) - } - ) - ) - - // ── 2. Underwriter bonds on both outposts → ACTIVE (deposits credit) ── - // The default plan also funds the underwriter with WIRE from `sysio` - // and bonds it on the depot through `sysio.opreg::deposit`. - WireUnderwriterTool.planCollateralDeposit( - cluster, - "UnderwriterCollateral", - "Bond the default underwriter collateral: WIRE on the depot, ETH and SOL on the outposts", - writeStepOptions, - underwriterLabels, - WireUnderwriterTool.load(null, config.underwriterCount) - ) - ClusterBuildPhase.create( - cluster, - "UnderwriterActivation", - "Underwriters flip ACTIVE once the bonds credit" - ).push( - verifyStep( - Actor.Sysio, - "underwriters-active", - `underwriters (${underwriterLabels.join(", ")}) become OPERATOR_STATUS_ACTIVE`, - async ctx => { - await pollUntil( - "every underwriter ACTIVE on sysio.opreg", - () => readAllOperatorsActive(ctx, underwriterLabels), - Constants.relayDeadlineMs(), - Constants.PollIntervalMs - ) - }, - relayStepOptions - ) - ) - - // ── 3. The Solana recipient + the WIRE depositor ── - SwapUserIdentities.planIdentityProvisioning( - cluster, - "ProvisionSwapUser", - "Provision the swap recipient's Ethereum + Solana identity", - {} - ) - ClusterBuildPhase.create( - cluster, - "ProvisionDepositor", - "Provision the WIRE depositor, funded from the treasury" - ).push( - Steps.user.planProvisionWire( - Actor.User, - "provision-depositor", - `provision ${Constants.DepositorAccount} + fund ${Constants.DepositorFunding} WIRE base units from the treasury`, - writeStepOptions, - Constants.DepositorAccount, - Constants.DepositorFunding - ) - ) - - // ── 4. Quote off the destination curve; escrow is immediate and REAL ── - ClusterBuildPhase.create( - cluster, - "QuoteAndEscrow", - "Quote the from-WIRE target; swapfromwire escrows NOW — no uwreq yet" - ).push( - verifyStep( - Actor.User, - "quote-target", - "compute the from-WIRE target from the destination reserve curve", - async ctx => { - // src == WIRE quotes against the DESTINATION reserve only. The - // WIRE-leg fee comes off the escrowed input FIRST, so the curve - // converts only the post-fee net: cp_output(dst.wire, dst.chain, - // split_wire_fee(wire_in).net) — `WireReserveTool.quoteSwap` mirrors - // `opp::amm::quote_swap`, which is what `drainfwq` stores as - // `dst_amount` and what `applyfromwire` bounds the debit by. - const book = await ctx.reserveBook( - Constants.SolanaChainCode, - Constants.SolanaTokenCode, - Constants.PrimaryReserveCode - ) - const targetSolanaAmount = WireReserveTool.quoteSwap( - null, - book, - Constants.SourceWireUnits, - await WireReserveTool.readFeeBps(ctx.wire) - ) - Assert.ok( - targetSolanaAmount > 0n, - "constant-product target must be positive" - ) - ctx.outputs - .set(Outputs.solanaReserveBefore, book) - .set(Outputs.targetSolanaAmount, targetSolanaAmount) - log.info(`[FromWire] curve target = ${targetSolanaAmount} lamports`) - } - ), - verifyStep( - Actor.User, - "snapshot-balances", - "snapshot depositor WIRE / sysio.reserv custody / recipient lamports baselines", - async ctx => { - const depositorWireBefore = await ctx.wire.getWireBalance( - Constants.DepositorAccount - ), - reserveCustodyBefore = await ctx.wire.getWireBalance( - ReserveCustodyAccount - ), - swapUser = ctx.outputs.assert(swapUserOutputKey()), - recipientLamportsBefore = await ctx.solana.getLamports( - swapUser.solanaKeypair.publicKey - ) - Assert.ok( - depositorWireBefore >= Constants.SourceWireUnits, - `depositor must hold at least ${Constants.SourceWireUnits} WIRE base units to escrow` - ) - ctx.outputs - .set(Outputs.depositorWireBefore, depositorWireBefore) - .set(Outputs.reserveCustodyBefore, reserveCustodyBefore) - .set(Outputs.recipientLamportsBefore, recipientLamportsBefore) - } - ), - SwapFromWireScenarioUwritSteps.planSwapfromwire( - Actor.User, - "push-swapfromwire", - `${Constants.DepositorAccount} pushes sysio.uwrit::swapfromwire for ${Constants.SourceWireUnits} WIRE base units`, - writeStepOptions, - Constants.DepositorAccount, - Constants.SourceWireUnits, - Constants.SolanaChainCode, - Constants.SolanaTokenCode, - Constants.PrimaryReserveCode, - Constants.VarianceToleranceBps - ), - verifyStep( - Actor.User, - "escrow-exact", - "escrow is immediate and REAL: depositor WIRE down, sysio.reserv custody up (EXACT)", - async ctx => { - const depositorWireBefore = ctx.outputs.assert( - Outputs.depositorWireBefore - ), - reserveCustodyBefore = ctx.outputs.assert( - Outputs.reserveCustodyBefore - ) - Assert.strictEqual( - await ctx.wire.getWireBalance(Constants.DepositorAccount), - depositorWireBefore - Constants.SourceWireUnits, - "depositor WIRE must decrement by exactly the escrowed source units" - ) - Assert.strictEqual( - await ctx.wire.getWireBalance(ReserveCustodyAccount), - reserveCustodyBefore + Constants.SourceWireUnits, - "sysio.reserv custody must increment by exactly the escrowed source units" - ) - } - ) - ) - - // ── 5. Next epoch advance drains the queue into the PENDING uwreq ── - ClusterBuildPhase.create( - cluster, - "QueueDrain", - "The next epoch advance drains the from-WIRE queue into a PENDING uwreq" - ).push( - verifyStep( - Actor.Sysio, - "queue-drained", - "a src=WIRE uwreq appears with the escrowed amount and the depot-origin id tag", - async ctx => { - await pollUntil( - "from-WIRE UWREQ row appears", - async () => (await readFromWireUwreq(ctx)) != null, - Constants.DrainDeadlineMs, - Constants.PollIntervalMs - ) - const request = await readFromWireUwreq(ctx) - Assert.strictEqual( - slugValue(request.src_token_code), - Constants.WireTokenCode, - "the drained uwreq's source token must be WIRE" - ) - Assert.strictEqual( - Number(request.src_amount), - Number(Constants.SourceWireUnits), - "the drained uwreq must carry the escrowed source amount" - ) - // Depot-origin id space: bit 63 tags queued from-WIRE requests. - Assert.strictEqual( - BigInt(request.id) & Constants.DepotOriginIdBit, - Constants.DepotOriginIdBit, - "the drained uwreq id must carry the depot-origin bit-63 tag" - ) - }, - drainStepOptions - ) - ) - - // ── 6. Single-leg race: CONFIRMED, ONE lock on SOLANA, emit-time books ── - ClusterBuildPhase.create( - cluster, - "RaceTargetLeg", - "Underwriters race the target leg only; the depot applies the books at emit time" - ).push( - verifyStep( - Actor.Underwriter, - "confirmed-single-lock", - "uwreq flips CONFIRMED with exactly ONE lock, on the SOLANA target leg", - async ctx => { - await pollUntil( - "from-WIRE UWREQ status=CONFIRMED", - async () => { - const request = await readFromWireUwreq(ctx) - return ( - request != null && - matchesProtoEnum( - request.status, - SysioUwritUnderwriterequeststatus, - SysioUwritUnderwriterequeststatus.UNDERWRITE_REQUEST_STATUS_CONFIRMED - ) - ) - }, - Constants.RaceDeadlineMs, - Constants.PollIntervalMs - ) - // The WIRE source leg carries no bond — only the SOL target leg locks. - const request = await readFromWireUwreq(ctx) - const locks = await ctx.locksForUwreq(Number(request.id)) - Assert.strictEqual( - locks.length, - 1, - "the WIRE source leg carries no bond — exactly one lock expected" - ) - Assert.strictEqual( - slugValue(locks[0].chain_code), - Constants.SolanaChainCode, - "the single lock must sit on the SOLANA target leg" - ) - }, - raceStepOptions - ), - verifyStep( - Actor.Underwriter, - "emit-time-books", - "the escrow became dst-reserve WIRE liquidity (post-fee net) and the chain side was debited", - async ctx => { - // Emit-time books: #414's `applyfromwire` skims the WIRE-leg fee off - // the escrowed input, so the reserve's WIRE side grows by the - // post-fee NET, not the gross escrow. - const booksBefore = ctx.outputs.assert(Outputs.solanaReserveBefore), - targetSolanaAmount = ctx.outputs.assert(Outputs.targetSolanaAmount), - fromWireFee = await readWireLegFee(ctx, Constants.SourceWireUnits), - reserve = await ctx.reserveBook( - Constants.SolanaChainCode, - Constants.SolanaTokenCode, - Constants.PrimaryReserveCode - ) - Assert.strictEqual( - reserve.wire, - booksBefore.wire + fromWireFee.net, - "the reserve's WIRE side must grow by the post-fee net escrow" - ) - Assert.strictEqual( - reserve.chain, - booksBefore.chain - targetSolanaAmount, - "the reserve's chain side must be debited by the target amount" - ) - } - ) - ) - - // ── 7. SWAP_REMIT pays the recipient on Solana ── - ClusterBuildPhase.create( - cluster, - "RemitAndReceive", - "SWAP_REMIT lands on the SOL outpost; the recipient is paid" - ).push( - verifyStep( - Actor.SolanaOutpost, - "recipient-receives-solana", - "recipient's SOL balance bumps by ~targetAmount (within variance tolerance)", - async ctx => { - const swapUser = ctx.outputs.assert(swapUserOutputKey()), - recipientLamportsBefore = ctx.outputs.assert( - Outputs.recipientLamportsBefore - ), - targetSolanaAmount = ctx.outputs.assert(Outputs.targetSolanaAmount), - drift = WireReserveTool.varianceDrift( - targetSolanaAmount, - Constants.VarianceToleranceBps - ) - await pollUntil( - "from-WIRE recipient receives SOL", - async () => { - const current = await ctx.solana.getLamports( - swapUser.solanaKeypair.publicKey - ) - return ( - current >= - recipientLamportsBefore + Number(targetSolanaAmount - drift) - ) - }, - Constants.RemitDeadlineMs, - Constants.PollIntervalMs - ) - const finalLamports = await ctx.solana.getLamports( - swapUser.solanaKeypair.publicKey - ) - log.info( - `[FromWire] recipient received ${finalLamports - recipientLamportsBefore} lamports` - ) - Assert.ok( - finalLamports - recipientLamportsBefore > 0, - "the recipient's lamport balance must have increased" - ) - }, - remitStepOptions - ) - ) - - // ── 8. Custody settles post rewards drain; the lock persists ── - ClusterBuildPhase.create( - cluster, - "DrainRewards", - "Custody settles once payepoch drains the rewards bucket; the lock persists" - ).push( - verifyStep( - Actor.Sysio, - "custody-settled", - "custody holds the escrow minus the drained rewards half; the underwriter half and the target-leg lock persist", - async ctx => { - // FROM-WIRE never pays the escrow back out — it became reserve - // liquidity. Of the WIRE-leg fee, ONLY the batch-operator rewards half - // ever leaves this custody, and only when payepoch drains the rewards - // bucket (sysio.reserv::drainrewards). The winning underwriter's half - // stays put in `sysio.reserv::uwfees` until that account claims it — - // this flow never calls `claimuwfee`, so it is still here. - // - // The drain can land just after the SOL-recipient poll succeeds, so - // poll until custody settles at the drained value rather than - // snapshotting mid-race. - const reserveCustodyBefore = ctx.outputs.assert( - Outputs.reserveCustodyBefore - ), - fromWireFee = await readWireLegFee(ctx, Constants.SourceWireUnits), - expectedCustody = - reserveCustodyBefore + - Constants.SourceWireUnits - - fromWireFee.rewardShare - await pollUntil( - "rewards bucket drained from sysio.reserv custody", - async () => - (await ctx.wire.getWireBalance(ReserveCustodyAccount)) === - expectedCustody, - Constants.DrainDeadlineMs, - Constants.PollIntervalMs - ) - Assert.strictEqual( - await ctx.wire.getWireBalance(ReserveCustodyAccount), - expectedCustody, - "custody must settle at baseline + escrow − the drained rewards half of the WIRE-leg fee" - ) - // Challenge window: the target-leg lock persists after delivery. - const request = await readFromWireUwreq(ctx) - Assert.ok(request != null, "the from-WIRE uwreq must still exist") - Assert.strictEqual( - (await ctx.locksForUwreq(Number(request.id))).length, - 1, - "the target-leg lock must persist through the challenge window" - ) - }, - drainStepOptions - ) - ) - } -} diff --git a/packages/flow-swap-from-wire/src/SwapFromWireScenarioConstants.ts b/packages/flow-swap-from-wire/src/SwapFromWireScenarioConstants.ts deleted file mode 100644 index eb6ed5283..000000000 --- a/packages/flow-swap-from-wire/src/SwapFromWireScenarioConstants.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { SlugName } from "@wireio/sdk-core" -import { ProtocolTiming } from "@wireio/cluster-tool" - -/** - * Constants for the swap-from-WIRE flow. Protocol waits derive from the - * {@link ProtocolTiming} envelope; reserve slug codes, the escrow amount, and - * the depositor account carry over from the previously-validated suite: the - * depositor escrows 0.1 WIRE against the bootstrap-seeded SOLANA/SOL/PRIMARY - * reserve and the payout lands on a provisioned Solana recipient. - */ -export namespace SwapFromWireScenarioConstants { - /** Swap-from-WIRE depositor — a plain WIRE user funded from the treasury. */ - export const DepositorAccount = "wirefromusr" - /** Treasury funding (raw 9-dec WIRE): 10× the escrow so re-runs in one cluster still work. */ - export const DepositorFunding = 1_000_000_000n - - /** Epoch duration (s) — the `sysio.epoch::setconfig` floor is 60. */ - export const EpochDurationSec = 60 - /** Epochs budgeted for the depot-internal `drainfwq` queue drain. */ - export const DrainEpochBudget = 3 - /** 1 s in ms — multiplies epoch counts into ms deadlines. */ - export const MsPerSecond = 1_000 - - /** - * `swapfromwire` is QUEUED — the PENDING uwreq materialises at the next - * `sysio.epoch::advance` (drainfwq). Depot-internal, so the budget is - * extension-inclusive epochs rather than a hop class. Also bounds the #425 - * rewards-bucket custody settlement poll. - */ - export const DrainDeadlineMs = - ProtocolTiming.effectiveEpochSec(EpochDurationSec) * - DrainEpochBudget * - MsPerSecond - /** Single-leg underwriter race (target commit only) — a single hop: the - * winning commit lands on the SOL outpost and relays back to the depot. */ - export const RaceDeadlineMs = ProtocolTiming.SingleHopBudgetMs - /** SWAP_REMIT delivery to the SOL outpost + recipient credit — the tail of - * the depot→outpost path, budgeted as the double hop. */ - export const RemitDeadlineMs = ProtocolTiming.DoubleHopBudgetMs - /** Interval for long-running chain-state polls (ms). */ - export const PollIntervalMs = 3_000 - /** Buffer added on top of each poll deadline for the enclosing step timeout (ms). */ - export const PollDeadlineBufferMs = 30_000 - - /** Epochs budgeted for the underwriter bonds to relay + credit + flip ACTIVE — - * above the envelope's 4–6 minute collateral class. */ - export const RelayEpochBudget = 9 - - /** Registered chain slug codes (must match the bootstrap registry seed). */ - export const WireChainCode = SlugName.from("WIRE") - export const EthereumChainCode = SlugName.from("ETHEREUM") - export const SolanaChainCode = SlugName.from("SOLANA") - /** Registered token slug codes. */ - export const WireTokenCode = SlugName.from("WIRE") - export const EthereumTokenCode = SlugName.from("ETH") - export const SolanaTokenCode = SlugName.from("SOL") - /** The bootstrap-seeded reserve's own slug code. */ - export const PrimaryReserveCode = SlugName.from("PRIMARY") - - /** - * 0.1 WIRE = 1e8 base units escrowed by the depositor — ~1% of the seeded SOL - * reserve, comfortably inside variance tolerance. WIRE and SOL (lamports) - * share the 9-decimal frame, so no precision scaling. - */ - export const SourceWireUnits = 100_000_000n - /** Variance tolerance carried on the swap request (bps). */ - export const VarianceToleranceBps = 500 - - /** - * Per-(chain, token) `req_uw_collat` minimum gating `OPERATOR_STATUS_ACTIVE` — - * equals {@link WireUnderwriterTool.DefaultAmount} so the default deposit plan - * satisfies it exactly on both outposts. The plan also funds and bonds that - * amount of WIRE on the depot, which no requirement here names. - */ - export const UnderwriterMinimumBond = 1_000_000_000n - - /** Depot-origin id space: bit 63 tags uwreqs queued by `swapfromwire`. */ - export const DepotOriginIdBit = 1n << 63n - - /** Deadline for the underwriter deposit → depot credit → ACTIVE relay. */ - export function relayDeadlineMs(): number { - return ( - ProtocolTiming.effectiveEpochSec(EpochDurationSec) * - RelayEpochBudget * - MsPerSecond - ) - } -} diff --git a/packages/flow-swap-from-wire/src/SwapFromWireScenarioOutputs.ts b/packages/flow-swap-from-wire/src/SwapFromWireScenarioOutputs.ts deleted file mode 100644 index 6f80dcc46..000000000 --- a/packages/flow-swap-from-wire/src/SwapFromWireScenarioOutputs.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { outputKey, type ReserveBook } from "@wireio/cluster-tool" - -/** - * Typed cross-step outputs for the swap-from-WIRE scenario — every baseline and - * computed target rides `ctx.outputs` under these keys (never a shared mutable - * closure). The QuoteAndEscrow phase writes them; the race / remit / drain - * verifications read them back. - */ -export namespace SwapFromWireScenarioOutputs { - /** Constant-product target (lamports) quoted off the destination reserve curve. */ - export const targetSolanaAmount = outputKey( - "fromWire.targetSolanaAmount", - "quoted SOL target amount (lamports)" - ) - - /** The SOLANA/SOL/PRIMARY reserve `(chain, wire)` book before the swap. */ - export const solanaReserveBefore = outputKey( - "fromWire.solanaReserveBefore", - "destination reserve book baseline" - ) - - /** The depositor's real WIRE balance before the `swapfromwire` escrow. */ - export const depositorWireBefore = outputKey( - "fromWire.depositorWireBefore", - "depositor WIRE balance baseline" - ) - - /** `sysio.reserv`'s real WIRE custody balance before the escrow. */ - export const reserveCustodyBefore = outputKey( - "fromWire.reserveCustodyBefore", - "sysio.reserv WIRE custody baseline" - ) - - /** The Solana recipient's lamport balance before the swap. */ - export const recipientLamportsBefore = outputKey( - "fromWire.recipientLamportsBefore", - "recipient lamport balance baseline" - ) -} diff --git a/packages/flow-swap-from-wire/src/index.ts b/packages/flow-swap-from-wire/src/index.ts deleted file mode 100644 index b8d920f72..000000000 --- a/packages/flow-swap-from-wire/src/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { FlowCLI } from "@wireio/cluster-tool" -import { SwapFromWireScenario } from "./SwapFromWireScenario.js" - -/** Run the swap-from-WIRE flow as an executable — exit code = report success. */ -async function main(): Promise { - const report = await FlowCLI.create(SwapFromWireScenario).run() - process.exit(report.succeeded ? 0 : 1) -} - -void main() diff --git a/packages/flow-swap-from-wire/src/steps/SwapFromWireScenarioUwritSteps.ts b/packages/flow-swap-from-wire/src/steps/SwapFromWireScenarioUwritSteps.ts deleted file mode 100644 index 440d4d2fc..000000000 --- a/packages/flow-swap-from-wire/src/steps/SwapFromWireScenarioUwritSteps.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { SlugName, SysioContracts } from "@wireio/sdk-core" -import { - ClusterBuildStep, - swapUserOutputKey, - type ClusterBuildContext, - type ClusterBuildStepOptions, - type Report, - type StepInput -} from "@wireio/cluster-tool" -import { SwapFromWireScenarioOutputs as Outputs } from "../SwapFromWireScenarioOutputs.js" - -const { SysioContractName, SysioUwritChainkind } = SysioContracts - -/** - * Flow-local `sysio.uwrit` Steps — the queued from-WIRE swap entry point. The - * shared palette (`Steps.contracts.sysio.uwrit`) carries no `swapfromwire` - * factory yet, so the flow lifts the typed action invoke into its own - * Report-validated write Step here. - */ -export namespace SwapFromWireScenarioUwritSteps { - /** Input for {@link planSwapfromwire} — the swap's static scalars; the quoted - * target and the recipient identity resolve from `ctx.outputs` at run time. */ - export interface SwapfromwireInput extends StepInput { - readonly kind: "SwapFromWireScenarioUwritSteps.SwapfromwireInput" - /** The escrowing WIRE user (also the action authorizer). */ - readonly user: string - /** Gross WIRE escrowed (raw 9-dec base units). */ - readonly wireAmount: bigint - /** Destination chain slug value. */ - readonly destinationChainCode: number - /** Destination token slug value. */ - readonly destinationTokenCode: number - /** Destination reserve slug value. */ - readonly destinationReserveCode: number - /** Variance tolerance carried on the request (bps). */ - readonly targetToleranceBps: number - } - - /** - * `sysio.uwrit::swapfromwire` — escrow the user's REAL WIRE into - * `sysio.reserv` custody and enqueue the from-WIRE queue row (NO uwreq exists - * until the next `sysio.epoch::advance` drains the queue). - * - * @param actor - The narrative subject. - * @param name - Step name (report row). - * @param description - One-line description. - * @param options - Step option overrides. - * @param user - The escrowing WIRE account (authorizes the action). - * @param wireAmount - Gross WIRE escrowed (raw base units). - * @param destinationChainCode - Destination chain slug value. - * @param destinationTokenCode - Destination token slug value. - * @param destinationReserveCode - Destination reserve slug value. - * @param targetToleranceBps - Variance tolerance (bps). - * @returns The definition step. - */ - export function planSwapfromwire< - C extends ClusterBuildContext = ClusterBuildContext - >( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - user: string, - wireAmount: bigint, - destinationChainCode: number, - destinationTokenCode: number, - destinationReserveCode: number, - targetToleranceBps: number - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { - kind: "SwapFromWireScenarioUwritSteps.SwapfromwireInput", - user, - wireAmount, - destinationChainCode, - destinationTokenCode, - destinationReserveCode, - targetToleranceBps - }, - runSwapfromwire - ) - } - - /** - * Named runner — one `sysio.uwrit::swapfromwire` write, authorized by the - * depositor. The target amount comes from the quote step's output; the - * recipient is the provisioned swap user's Solana pubkey (this flow targets - * the SVM outpost, hence `CHAIN_KIND_SVM` + the 32-byte ed25519 hex address). - */ - export async function runSwapfromwire( - ctx: C, - input: SwapfromwireInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - const targetAmount = ctx.outputs.assert(Outputs.targetSolanaAmount), - swapUser = ctx.outputs.assert(swapUserOutputKey()), - data: SysioContracts.SysioUwritSwapfromwireAction = { - user: input.user, - wire_amount: Number(input.wireAmount), - dst_chain_code: SlugName.toString(input.destinationChainCode), - dst_token_code: SlugName.toString(input.destinationTokenCode), - dst_reserve_code: SlugName.toString(input.destinationReserveCode), - target_amount: Number(targetAmount), - target_tolerance_bps: input.targetToleranceBps, - recipient_kind: SysioUwritChainkind.CHAIN_KIND_SVM, - recipient_addr: Buffer.from(swapUser.solanaPublicKeyBytes).toString( - "hex" - ) - } - await ctx.wire - .getSysioContract(SysioContractName.uwrit) - .actions.swapfromwire.invoke(data, { - authorization: [{ actor: input.user, permission: "active" }] - }) - } -} diff --git a/packages/flow-swap-from-wire/src/steps/index.ts b/packages/flow-swap-from-wire/src/steps/index.ts deleted file mode 100644 index 9c8459fe5..000000000 --- a/packages/flow-swap-from-wire/src/steps/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./SwapFromWireScenarioUwritSteps.js" diff --git a/packages/flow-swap-from-wire/tsconfig.json b/packages/flow-swap-from-wire/tsconfig.json deleted file mode 100644 index 82b017421..000000000 --- a/packages/flow-swap-from-wire/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-swap-from-wire/tsconfig.src.json b/packages/flow-swap-from-wire/tsconfig.src.json deleted file mode 100644 index b8bf202f6..000000000 --- a/packages/flow-swap-from-wire/tsconfig.src.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "extends": "../../etc/tsconfig/tsconfig.base.jest.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "lib", - "paths": { - "@wireio/cluster-tool": [ - "../cluster-tool/src" - ], - "@wireio/cluster-tool-shared": [ - "../cluster-tool-shared/src" - ] - } - }, - "include": [ - "src" - ], - "references": [ - { - "path": "../cluster-tool-shared/tsconfig.json" - }, - { - "path": "../cluster-tool/tsconfig.json" - } - ] -} diff --git a/packages/flow-swap-non-native-tokens/package.json b/packages/flow-swap-non-native-tokens/package.json deleted file mode 100644 index 43d173ba4..000000000 --- a/packages/flow-swap-non-native-tokens/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "@wireio/test-flow-swap-non-native-tokens", - "version": "0.1.18", - "private": true, - "type": "commonjs", - "description": "Flow: SWAP with non-native tokens (USDC / USDT / LIQ on ETH + SOL)", - "scripts": { - "build": "tsc -b tsconfig.json", - "test:disabled": "node lib/index.js", - "clean": "rm -rf lib" - }, - "dependencies": { - "@solana/spl-token": "^0.4.9", - "@solana/web3.js": "^1.98.0", - "@wireio/cluster-tool": "workspace:*", - "@wireio/sdk-core": "^1.0.92", - "@wireio/opp-typescript-models": "^1.0.50", - "@wireio/shared": "^1.0.92", - "ethers": "^6.14.0", - "ts-pattern": "^5.9.0" - }, - "devDependencies": { - "@types/node": "25.5.0" - } -} diff --git a/packages/flow-swap-non-native-tokens/src/SwapNonNativeScenario.ts b/packages/flow-swap-non-native-tokens/src/SwapNonNativeScenario.ts deleted file mode 100644 index 82552545e..000000000 --- a/packages/flow-swap-non-native-tokens/src/SwapNonNativeScenario.ts +++ /dev/null @@ -1,624 +0,0 @@ -import Assert from "node:assert" -import type { ClusterConfig } from "@wireio/cluster-tool-shared" -import { TokenAmount } from "@wireio/opp-typescript-models" -import { SysioContracts } from "@wireio/sdk-core" -import { - ClusterBuildPhase, - ClusterBuildPhaseGroup, - Constants as HarnessConstants, - FlowScenario, - Report, - SwapScenarioContext, - SwapUserIdentities, - WireUnderwriterTool, - slugValue, - verifyStep, - type ClusterBuild, - type ClusterBuildOptions, - type ClusterBuildStep, - type ClusterBuildStepOptions, - type Logger -} from "@wireio/cluster-tool" -import { SwapNonNativeScenarioConstants as Constants } from "./SwapNonNativeScenarioConstants.js" -import { - SwapDestinationKind, - SwapNonNativeScenarioTokenSteps as TokenSteps, - type SwapCell, - type UnderwriterCollateralMatrix -} from "./steps/SwapNonNativeScenarioTokenSteps.js" - -const { SysioContractName } = SysioContracts -const { Actor } = Report -const { Reserves, SwapAmounts, Timing, TokenDecimals } = Constants - -// ── Step option presets (poll deadline + buffer per the timing budgets) ───── - -/** On-chain write steps (mint / approve / permit / requestSwap transactions). */ -const WriteStepOptions: ClusterBuildStepOptions = { - timeoutMs: Timing.WriteTimeoutMs -} -/** Verify steps polling to the UWREQ-creation / lock deadline. */ -const UwreqStepOptions: ClusterBuildStepOptions = { - timeoutMs: Timing.UwreqDeadlineMs + Timing.PollDeadlineBufferMs -} -/** Verify steps polling to the underwriter-race deadline. */ -const RaceStepOptions: ClusterBuildStepOptions = { - timeoutMs: Timing.RaceDeadlineMs + Timing.PollDeadlineBufferMs -} -/** Verify steps polling to the remit / relay deadline. */ -const RemitStepOptions: ClusterBuildStepOptions = { - timeoutMs: Timing.RemitDeadlineMs + Timing.PollDeadlineBufferMs -} - -// ── The swap matrix (one SwapCell per old jest test) ──────────────────────── - -/** USDC (ETH) → native SOL: permit custody + cross-chain payout (old test 1). */ -const UsdcPermitToSolanaNativeCell: SwapCell = { - name: "usdc-permit-to-solana-native", - sourceChainCode: Reserves.Ethereum.ChainCode, - sourceTokenCode: Reserves.Ethereum.USDC, - sourceAmount: SwapAmounts.SourceErc20Stable, - sourceDecimals: TokenDecimals.Erc20Stable, - targetChainCode: Reserves.Solana.ChainCode, - targetTokenCode: Reserves.Solana.SOL, - // Native SOL is 9-dec — the depot-frame target IS lamports. - destinationDecimals: TokenDecimals.SolanaNative, - destination: SwapDestinationKind.solanaNative -} - -/** USDT (ETH) → native SOL: pre-set-allowance custody + payout (old test 2). */ -const UsdtApprovalToSolanaNativeCell: SwapCell = { - name: "usdt-approval-to-solana-native", - sourceChainCode: Reserves.Ethereum.ChainCode, - sourceTokenCode: Reserves.Ethereum.USDT, - sourceAmount: SwapAmounts.SourceErc20Stable, - sourceDecimals: TokenDecimals.Erc20Stable, - targetChainCode: Reserves.Solana.ChainCode, - targetTokenCode: Reserves.Solana.SOL, - destinationDecimals: TokenDecimals.SolanaNative, - destination: SwapDestinationKind.solanaNative -} - -/** USDCSOL (SPL) → native ETH: signed `request_swap_spl` custody (old test 3). */ -const UsdcSolanaToEthereumNativeCell: SwapCell = { - name: "usdcsol-spl-to-ethereum-native", - sourceChainCode: Reserves.Solana.ChainCode, - sourceTokenCode: Reserves.Solana.USDCSOL, - sourceAmount: SwapAmounts.SourceSplStable, - sourceDecimals: TokenDecimals.SplStable, - targetChainCode: Reserves.Ethereum.ChainCode, - targetTokenCode: Reserves.Ethereum.ETH, - // The target publishes in depot 9-dec units; the outpost pays wei (×1e9). - destinationDecimals: TokenDecimals.EthereumNative, - destination: SwapDestinationKind.ethereumNative -} - -/** USDC (ETH) → USDT-on-SOL: cross-chain ERC-20 → SPL stablecoin swap (old test 4). */ -const UsdcPermitToUsdtSolanaCell: SwapCell = { - name: "usdc-permit-to-usdtsol", - sourceChainCode: Reserves.Ethereum.ChainCode, - sourceTokenCode: Reserves.Ethereum.USDC, - sourceAmount: SwapAmounts.SourceErc20Stable, - sourceDecimals: TokenDecimals.Erc20Stable, - targetChainCode: Reserves.Solana.ChainCode, - targetTokenCode: Reserves.Solana.USDTSOL, - // 6-dec SPL destination — at/below the cap, `fromDepot(target)` is identity. - destinationDecimals: TokenDecimals.SplStable, - destination: SwapDestinationKind.solanaSplToken -} - -/** USDC (ETH) → USDC-on-SOL: cross-chain same-asset bridging (old test 5). */ -const UsdcPermitToUsdcSolanaCell: SwapCell = { - name: "usdc-permit-to-usdcsol", - sourceChainCode: Reserves.Ethereum.ChainCode, - sourceTokenCode: Reserves.Ethereum.USDC, - sourceAmount: SwapAmounts.SourceErc20Stable, - sourceDecimals: TokenDecimals.Erc20Stable, - targetChainCode: Reserves.Solana.ChainCode, - targetTokenCode: Reserves.Solana.USDCSOL, - destinationDecimals: TokenDecimals.SplStable, - destination: SwapDestinationKind.solanaSplToken -} - -// ── Flow-local composition helpers ────────────────────────────────────────── - -/** The (chain, token) legs every underwriter bonds — the full swap matrix set. */ -const UnderwriterCollateralLegPairs: ReadonlyArray< - [chainCode: number, tokenCode: number] -> = [ - [Reserves.Ethereum.ChainCode, Reserves.Ethereum.ETH], - [Reserves.Ethereum.ChainCode, Reserves.Ethereum.USDC], - [Reserves.Ethereum.ChainCode, Reserves.Ethereum.USDT], - [Reserves.Solana.ChainCode, Reserves.Solana.SOL], - [Reserves.Solana.ChainCode, Reserves.Solana.USDCSOL], - [Reserves.Solana.ChainCode, Reserves.Solana.USDTSOL] -] - -/** - * Build the single-underwriter collateral plan: one - * {@link Constants.UnderwriterCollateralAmount} bond per swap-matrix leg. - * - * @returns The per-underwriter entry list (the defaults wrap it in the outer - * per-underwriter array). - */ -function underwriterCollateralLegs(): UnderwriterCollateralMatrix[number] { - return UnderwriterCollateralLegPairs.map(([chainCode, tokenCode]) => ({ - chain_code: chainCode, - amount: TokenAmount.create({ - tokenCode: BigInt(tokenCode), - amount: Constants.UnderwriterCollateralAmount - }) - })) -} - -/** Reserve token codes the bootstrap must seed on the Ethereum side (old phase 0). */ -const EthereumReserveTokenCodes = [ - Reserves.Ethereum.ETH, - Reserves.Ethereum.LIQETH, - Reserves.Ethereum.USDC, - Reserves.Ethereum.USDT -] - -/** Reserve token codes the bootstrap must seed on the Solana side (old phase 0). */ -const SolanaReserveTokenCodes = [ - Reserves.Solana.SOL, - Reserves.Solana.LIQSOL, - Reserves.Solana.USDCSOL, - Reserves.Solana.USDTSOL -] - -/** - * READ the seeded reserve token codes for `chainCode` from - * `sysio.reserv::reserves` (typed table accessor). - * - * @param ctx - The scenario context. - * @param chainCode - The chain slug value to filter on. - * @returns The token slug values of every seeded reserve on the chain. - */ -async function readReserveTokenCodes( - ctx: SwapScenarioContext, - chainCode: number -): Promise { - const { rows } = await ctx.wire - .getSysioContract(SysioContractName.reserv) - .tables.reserves.query({ limit: 50 }) - return rows - .filter(row => slugValue(row.chain_code) === chainCode) - .map(row => slugValue(row.token_code)) -} - -/** - * Assert every expected reserve token code was seeded on a chain. - * - * @param seeded - Token codes read back from the depot. - * @param expected - Token codes the bootstrap must have seeded. - * @param chainName - Chain label for the failure message. - */ -function assertReserveTokenCodes( - seeded: number[], - expected: number[], - chainName: string -): void { - expected.forEach(code => - Assert.ok( - seeded.includes(code), - `reserve for token code ${code} missing on ${chainName} (seeded: ${seeded.join(", ")})` - ) - ) -} - -/** - * The verify tail every swap cell shares: the depot opens a NEW uwreq row, the - * underwriter race resolves it to CONFIRMED, both legs lock, and the user's - * destination balance bumps by the variance-adjusted target. - * - * @param cell - The swap cell under verification. - * @returns The four verify steps, in lifecycle order. - */ -function planSwapVerifySteps( - cell: SwapCell -): ClusterBuildStep.Any[] { - const destinationActor = - cell.destination === SwapDestinationKind.ethereumNative - ? Actor.EthereumOutpost - : Actor.SolanaOutpost - return [ - TokenSteps.planVerifyUwreqCreated( - Actor.Sysio, - "uwreq-created", - "depot opens a new UWREQ row for the swap", - UwreqStepOptions, - cell - ), - TokenSteps.planVerifyUwreqConfirmed( - Actor.Underwriter, - "uwreq-confirmed", - "the underwriter race resolves the UWREQ to CONFIRMED", - RaceStepOptions, - cell - ), - TokenSteps.planVerifyUwreqLocks( - Actor.Sysio, - "uwreq-locks", - `both legs locked (${Constants.LocksPerSwap} persistent locks)`, - UwreqStepOptions, - cell - ), - TokenSteps.planVerifyDestinationPayout( - destinationActor, - "payout", - "the user's destination balance bumps by the variance-adjusted target", - RemitStepOptions, - cell - ) - ] -} - -/** - * Flow: SWAP_REQUEST → underwriter race → SWAP_REMIT for **non-native** source - * tokens on both outposts. Extends the bidirectional native swap proven in - * `flow-swap-with-underwriting` to ERC-20 source custody on Ethereum - * (USDC via EIP-2612 permit, USDT via pre-set allowance) and SPL source - * custody on Solana (USDCSOL via signed `request_swap_spl`), plus the mixed - * ERC-20 → SPL combinations (USDC → USDTSOL, USDC → USDCSOL bridging). - * - * Scenario phases on top of the bootstrap: - * - * 1. **UnderwriterCollateral** — bond every (chain, token) leg the swap matrix - * touches (from the `underwriterCollateral` defaults matrix). - * 2. **SwapUser** — provision the dual-chain swap user identity. - * 3. **SetupTokens** — mint mock stablecoin balances to the user (one write - * step per mint on each chain). - * 4. **ClusterHealth** — chain liveness + the bootstrap-seeded non-native - * reserves on both chains (old phase-0 assertions). - * 5. **UnderwriterBondsRelayed** — gate: every bond credited depot-side. - * 6. **SwapEthereumToSolanaErc20** — the four ERC-20-source cells (permit / - * approval / mixed SPL destinations). - * 7. **SwapSolanaToEthereumSpl** — the SPL-source inverse direction. - * - * **Canonical proof** for every cell: the destination balance bumps by the - * variance-adjusted target — only achievable if every layer (source custody, - * OPP envelope round-trip, underwriter race, depot variance check, destination - * payout) worked. - */ -export class SwapNonNativeScenario extends FlowScenario { - readonly name = "flow-swap-non-native-tokens" - readonly description = - "SWAP with non-native tokens (USDC / USDT / USDCSOL / USDTSOL) across both outposts" - - override readonly defaults: ClusterBuildOptions = { - // Seed the mock (chain, token) PRIMARY reserves this flow reads — `regreserve` - // is epoch-0-gated by the depot, so it must ride the bootstrap, not a flow phase. - enableMockReserves: true, - // The launch build of the Solana outpost withholds the reserve/swap - // bootstrap calls; this flow exercises them, so it opts in (never in plan()). - enableLaunchWithheldOperations: true, - epochDurationSec: Timing.EpochDurationSec, - // The underwriter must bond on every (chain, token) leg this flow's swap - // matrix touches — `sysio.uwrit::createuwreq` re-checks `meets_role_min` - // for BOTH legs, and the underwriter plugin's `select_coverable` requires - // a non-zero credit line per (chain, token). - requiredUnderwriterCollateral: [ - { - chainCode: Reserves.Ethereum.ChainCode, - tokenCode: Reserves.Ethereum.ETH, - minimumBond: Constants.UnderwriterMinimumBond - }, - { - chainCode: Reserves.Solana.ChainCode, - tokenCode: Reserves.Solana.SOL, - minimumBond: Constants.UnderwriterMinimumBond - } - ], - underwriterCollateral: [underwriterCollateralLegs()] - } - - override createContext( - config: ClusterConfig, - log: Logger - ): SwapScenarioContext { - return new SwapScenarioContext(config, log) - } - - plan(cluster: ClusterBuild): void { - const config = cluster.context.config - const underwriterLabels = Array.from( - { length: config.underwriterCount }, - (_, index) => HarnessConstants.underwriterLabel(index) - ) - const collateral = config.underwriterCollateral - Assert.ok( - collateral != null && collateral.length === underwriterLabels.length, - `flow-swap-non-native-tokens: expected a collateral matrix for ` + - `${underwriterLabels.length} underwriter(s), got ${collateral?.length ?? 0}` - ) - - // ── 1. Bond every (chain, token) leg the swap matrix touches ── - WireUnderwriterTool.planCollateralDeposit( - cluster, - "UnderwriterCollateral", - "Bond the per-leg underwriter collateral for the swap matrix", - WriteStepOptions, - underwriterLabels, - collateral - ) - - // ── 2. The dual-chain swap end-user ── - SwapUserIdentities.planIdentityProvisioning( - cluster, - "SwapUser", - "Provision the dual-chain swap user identity", - WriteStepOptions - ) - - // ── 3. Mock stablecoin balances for the user's source legs ── - ClusterBuildPhase.create( - cluster, - "SetupTokens", - "Mint mock stablecoin balances to the swap user on both chains" - ).push( - TokenSteps.planMintErc20ToSwapUser( - Actor.User, - "mint-usdc", - `mint ${Constants.Erc20FundingAmount} USDC base units to the swap user`, - WriteStepOptions, - Reserves.Ethereum.USDC, - Constants.Erc20FundingAmount - ), - TokenSteps.planMintErc20ToSwapUser( - Actor.User, - "mint-usdt", - `mint ${Constants.Erc20FundingAmount} USDT base units to the swap user`, - WriteStepOptions, - Reserves.Ethereum.USDT, - Constants.Erc20FundingAmount - ), - TokenSteps.planMintSplToSwapUser( - Actor.User, - "mint-usdcsol", - `mint ${Constants.SplFundingAmount} USDCSOL base units into the swap user's ATA`, - WriteStepOptions, - Reserves.Solana.USDCSOL, - Constants.SplFundingAmount - ), - TokenSteps.planMintSplToSwapUser( - Actor.User, - "mint-usdtsol", - `mint ${Constants.SplFundingAmount} USDTSOL base units into the swap user's ATA`, - WriteStepOptions, - Reserves.Solana.USDTSOL, - Constants.SplFundingAmount - ) - ) - - // ── 4. Cluster health + non-native reserve presence (old phase 0) ── - ClusterBuildPhase.create( - cluster, - "ClusterHealth", - "Chain liveness + bootstrap-seeded non-native reserves" - ).push( - verifyStep( - Actor.Sysio, - "chain-producing", - "WIRE chain is producing blocks", - async ctx => { - const info = await ctx.wire.getInfo() - Assert.ok( - Number(info.head_block_num) > 0, - "WIRE chain reported head_block_num 0" - ) - } - ), - verifyStep( - Actor.Sysio, - "ethereum-reserves-seeded", - "bootstrap seeded the ETH / LIQETH / USDC / USDT reserves on ETHEREUM", - async ctx => { - assertReserveTokenCodes( - await readReserveTokenCodes(ctx, Reserves.Ethereum.ChainCode), - EthereumReserveTokenCodes, - "ETHEREUM" - ) - } - ), - verifyStep( - Actor.Sysio, - "solana-reserves-seeded", - "bootstrap seeded the SOL / LIQSOL / USDCSOL / USDTSOL reserves on SOLANA", - async ctx => { - assertReserveTokenCodes( - await readReserveTokenCodes(ctx, Reserves.Solana.ChainCode), - SolanaReserveTokenCodes, - "SOLANA" - ) - } - ) - ) - - // ── 5. Gate: outpost bonds credited depot-side before the first swap ── - ClusterBuildPhase.create( - cluster, - "UnderwriterBondsRelayed", - "Every underwriter bond relayed + credited on sysio.opreg" - ).push( - TokenSteps.planVerifyUnderwriterBondsRelayed( - Actor.Sysio, - "bonds-relayed", - "sysio.opreg balance rows exist for every collateral leg", - RemitStepOptions, - underwriterLabels, - collateral - ) - ) - - // ── 6. ERC-20 source custody on Ethereum → Solana payouts ── - const ethereumToSolana = ClusterBuildPhaseGroup.create( - cluster, - "SwapEthereumToSolanaErc20", - "ERC-20 source custody on Ethereum → Solana payouts (permit + approval paths)" - ) - ClusterBuildPhase.create( - ethereumToSolana, - "UsdcPermitToSolanaNative", - "USDC (ETH) → native SOL: permit custody + cross-chain payout" - ).push( - TokenSteps.planQuoteTarget( - Actor.Sysio, - "quote-target", - "quote the swap live against the depot reserve books", - WriteStepOptions, - UsdcPermitToSolanaNativeCell - ), - TokenSteps.planSignPermit( - Actor.User, - "sign-permit", - "user signs the EIP-2612 permit for the swap amount", - WriteStepOptions, - UsdcPermitToSolanaNativeCell - ), - TokenSteps.planRequestSwapErc20WithPermit( - Actor.User, - "request-swap", - `swap ${SwapAmounts.SourceErc20Stable} USDC base units → SOL via inline permit`, - WriteStepOptions, - UsdcPermitToSolanaNativeCell - ), - TokenSteps.planVerifyErc20Custody( - Actor.EthereumOutpost, - "custody-escrowed", - "ReserveManager's USDC balance bumps by exactly the source amount", - WriteStepOptions, - UsdcPermitToSolanaNativeCell - ), - ...planSwapVerifySteps(UsdcPermitToSolanaNativeCell) - ) - ClusterBuildPhase.create( - ethereumToSolana, - "UsdtApprovalToSolanaNative", - "USDT (ETH) → native SOL: pre-set-allowance custody + cross-chain payout" - ).push( - TokenSteps.planQuoteTarget( - Actor.Sysio, - "quote-target", - "quote the swap live against the depot reserve books", - WriteStepOptions, - UsdtApprovalToSolanaNativeCell - ), - TokenSteps.planApproveErc20Spend( - Actor.User, - "approve-spend", - "user pre-approves ReserveManager for the swap amount (no-permit path)", - WriteStepOptions, - UsdtApprovalToSolanaNativeCell - ), - TokenSteps.planRequestSwapErc20WithApproval( - Actor.User, - "request-swap", - `swap ${SwapAmounts.SourceErc20Stable} USDT base units → SOL via pre-set allowance`, - WriteStepOptions, - UsdtApprovalToSolanaNativeCell - ), - TokenSteps.planVerifyErc20Custody( - Actor.EthereumOutpost, - "custody-escrowed", - "ReserveManager's USDT balance bumps by exactly the source amount", - WriteStepOptions, - UsdtApprovalToSolanaNativeCell - ), - ...planSwapVerifySteps(UsdtApprovalToSolanaNativeCell) - ) - ClusterBuildPhase.create( - ethereumToSolana, - "UsdcPermitToUsdtSolana", - "USDC (ETH) → USDT (SOL): cross-chain ERC-20 → SPL stablecoin swap" - ).push( - TokenSteps.planQuoteTarget( - Actor.Sysio, - "quote-target", - "quote the swap live against the depot reserve books", - WriteStepOptions, - UsdcPermitToUsdtSolanaCell - ), - TokenSteps.planSignPermit( - Actor.User, - "sign-permit", - "user signs the EIP-2612 permit for the swap amount", - WriteStepOptions, - UsdcPermitToUsdtSolanaCell - ), - TokenSteps.planRequestSwapErc20WithPermit( - Actor.User, - "request-swap", - `swap ${SwapAmounts.SourceErc20Stable} USDC base units → USDTSOL via inline permit`, - WriteStepOptions, - UsdcPermitToUsdtSolanaCell - ), - TokenSteps.planVerifyErc20Custody( - Actor.EthereumOutpost, - "custody-escrowed", - "ReserveManager's USDC balance bumps by exactly the source amount", - WriteStepOptions, - UsdcPermitToUsdtSolanaCell - ), - ...planSwapVerifySteps(UsdcPermitToUsdtSolanaCell) - ) - ClusterBuildPhase.create( - ethereumToSolana, - "UsdcPermitToUsdcSolana", - "USDC (ETH) → USDCSOL: cross-chain same-asset bridging" - ).push( - TokenSteps.planQuoteTarget( - Actor.Sysio, - "quote-target", - "quote the swap live against the depot reserve books", - WriteStepOptions, - UsdcPermitToUsdcSolanaCell - ), - TokenSteps.planSignPermit( - Actor.User, - "sign-permit", - "user signs the EIP-2612 permit for the swap amount", - WriteStepOptions, - UsdcPermitToUsdcSolanaCell - ), - TokenSteps.planRequestSwapErc20WithPermit( - Actor.User, - "request-swap", - `swap ${SwapAmounts.SourceErc20Stable} USDC base units → USDCSOL via inline permit`, - WriteStepOptions, - UsdcPermitToUsdcSolanaCell - ), - TokenSteps.planVerifyErc20Custody( - Actor.EthereumOutpost, - "custody-escrowed", - "ReserveManager's USDC balance bumps by exactly the source amount", - WriteStepOptions, - UsdcPermitToUsdcSolanaCell - ), - ...planSwapVerifySteps(UsdcPermitToUsdcSolanaCell) - ) - - // ── 7. SPL source custody on Solana → native ETH payout ── - ClusterBuildPhase.create( - cluster, - "SwapSolanaToEthereumSpl", - "SPL source custody on Solana → native ETH payout" - ).push( - TokenSteps.planQuoteTarget( - Actor.Sysio, - "quote-target", - "quote the swap live against the depot reserve books", - WriteStepOptions, - UsdcSolanaToEthereumNativeCell - ), - TokenSteps.planRequestSwapSpl( - Actor.User, - "request-swap", - `swap ${SwapAmounts.SourceSplStable} USDCSOL base units → ETH via signed request_swap_spl`, - WriteStepOptions, - UsdcSolanaToEthereumNativeCell - ), - ...planSwapVerifySteps(UsdcSolanaToEthereumNativeCell) - ) - } -} diff --git a/packages/flow-swap-non-native-tokens/src/SwapNonNativeScenarioConstants.ts b/packages/flow-swap-non-native-tokens/src/SwapNonNativeScenarioConstants.ts deleted file mode 100644 index 096f83231..000000000 --- a/packages/flow-swap-non-native-tokens/src/SwapNonNativeScenarioConstants.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { SlugName } from "@wireio/sdk-core" -import { ProtocolTiming } from "@wireio/cluster-tool" - -/** - * Constants for the non-native-token swap flow. Token codes, swap amounts, - * variance tolerance, and timing budgets carry over from the previously- - * validated jest run (tests/constants.ts): every swap draws ~1% of the seeded - * reserves so the two-leg constant-product math stays inside the 200 bps - * tolerance window, and every poll deadline scales from the 60 s minimum epoch. - */ -export namespace SwapNonNativeScenarioConstants { - /** - * Per-leg underwriter bond posted for EVERY (chain, token) the swap matrix - * touches. The depot's `sysio.uwrit::createuwreq` re-checks `meets_role_min` - * for BOTH legs of every swap, and the underwriter plugin's - * `select_coverable` requires the leg's credit line (the depot balance — - * raw native deposit units — minus persistent locks) to cover the FULL - * leg amount in the token's depot frame. Sized with heavy margin over the - * summed live-quoted leg exposures across all five sequential cells (locks - * persist for the challenge window, so per-leg exposure accumulates). - */ - export const UnderwriterCollateralAmount = 15_000_000_000n - - /** Native-leg minimum bond mirrored into `sysio.opreg::setconfig req_uw_collat`. */ - export const UnderwriterMinimumBond = 1_000_000_000 - - // ── Reserve identity (slug codes) ────────────────────────────────────── - - /** Slug codes for every chain / token / reserve the swap matrix touches. */ - export namespace Reserves { - /** Every leg pairs against the PRIMARY reserve. */ - export const ReserveCode = SlugName.from("PRIMARY") - - export namespace Ethereum { - export const ChainCode = SlugName.from("ETHEREUM") - export const ETH = SlugName.from("ETH") - export const USDC = SlugName.from("USDC") - export const USDT = SlugName.from("USDT") - export const LIQETH = SlugName.from("LIQETH") - } - - export namespace Solana { - export const ChainCode = SlugName.from("SOLANA") - export const SOL = SlugName.from("SOL") - export const USDCSOL = SlugName.from("USDCSOL") - export const USDTSOL = SlugName.from("USDTSOL") - export const LIQSOL = SlugName.from("LIQSOL") - } - } - - /** Source amounts per swap cell (source-token NATIVE base units). */ - export namespace SwapAmounts { - /** ERC-20 stablecoin source draw (6-dec base units — 0.1 USDC / USDT). */ - export const SourceErc20Stable = 100_000n - /** SPL stablecoin source draw (6-dec base units — 0.1 USDCSOL). */ - export const SourceSplStable = 100_000n - } - - /** - * Chain-native decimals per token class — drives the native ↔ depot-frame - * conversions (`WireReserveTool.toDepot`/`fromDepot`, per-token precision - * `min(decimals, 9)`) each cell's quote and payout-floor math must mirror. - */ - export namespace TokenDecimals { - /** Mock USDC / USDT ERC-20s (`MockUsdc.decimals() == 6`). */ - export const Erc20Stable = 6 - /** Mock USDCSOL / USDTSOL SPL mints (created with 6 decimals). */ - export const SplStable = 6 - /** Native ETH (wei). */ - export const EthereumNative = 18 - /** Native SOL (lamports). */ - export const SolanaNative = 9 - } - - /** Variance-tolerance knobs every SwapRequest publishes. */ - export namespace Variance { - /** Published tolerance window (bps) — covers cp remainder + swap fee. */ - export const ToleranceBps = 200 - } - - /** Timing budgets — protocol waits derive from the {@link ProtocolTiming} envelope. */ - export namespace Timing { - export const EpochDurationSec = 60 - /** Per on-chain write step budget. */ - export const WriteTimeoutMs = 60_000 - /** - * Relay → depot uwreq appearance — a single outpost→depot hop. A request - * rides the outpost's NEXT outbound emit (0–2 min of phase alignment, - * verified on-chain 2026-07-03) + delivery + the depot's createuwreq - * (~1–1.5 min), inside the envelope's 5–7 minute single-hop class. - */ - export const UwreqDeadlineMs = ProtocolTiming.SingleHopBudgetMs - /** Underwriter race resolution — the winning commit lands on the - * destination outpost and relays back to the depot: a single hop. */ - export const RaceDeadlineMs = ProtocolTiming.SingleHopBudgetMs - /** SWAP_REMIT + destination payout — the tail of the full - * outpost→depot→outpost path, budgeted as the double hop. */ - export const RemitDeadlineMs = ProtocolTiming.DoubleHopBudgetMs - /** Poll cadence for table / balance probes. */ - export const LongPollIntervalMs = 3_000 - /** Slack added to phase timeouts over their poll deadlines. */ - export const PollDeadlineBufferMs = 60_000 - } - - /** The swap user is funded `FundingMultiple ×` each source amount per token. */ - export const FundingMultiple = 100n - /** Mock ERC-20 units minted to the swap user per stablecoin (10 USDC / 10 USDT). */ - export const Erc20FundingAmount = FundingMultiple * SwapAmounts.SourceErc20Stable - /** Mock SPL units minted to the swap user per stablecoin (10 USDCSOL / 10 USDTSOL). */ - export const SplFundingAmount = FundingMultiple * SwapAmounts.SourceSplStable - - /** EIP-2612 permit validity window (s) — one hour from signing. */ - export const PermitDeadlineWindowSec = 3_600 - - /** - * Locks the depot pins per settled swap — one per (source, destination) leg. - * They form in the same transaction that resolves the underwriter race and - * PERSIST for the wall-clock challenge window (never released by delivery). - */ - export const LocksPerSwap = 2 - - /** - * Mock-SPL-mint manifest the Solana outpost bootstrapper persists into the - * cluster data dir (`Array<{ code, mint, decimals }>`). - */ - export const SolanaMockMintsFilename = "sol-mock-mints.json" - - /** - * `outpost-addrs.json` keys this flow binds contracts from (the hardhat - * artifact names match, so these double as ABI artifact names). - */ - export enum OutpostAddressKey { - ReserveManager = "ReserveManager", - MockUsdc = "MockUsdc", - MockUsdt = "MockUsdt" - } - - /** `outpost-addrs.json` key per mock ERC-20 token code (ETH-side stablecoins). */ - export const MockErc20AddressKeyByTokenCode: ReadonlyMap = - new Map([ - [Reserves.Ethereum.USDC, OutpostAddressKey.MockUsdc], - [Reserves.Ethereum.USDT, OutpostAddressKey.MockUsdt] - ]) -} diff --git a/packages/flow-swap-non-native-tokens/src/index.ts b/packages/flow-swap-non-native-tokens/src/index.ts deleted file mode 100644 index 36eb7c4eb..000000000 --- a/packages/flow-swap-non-native-tokens/src/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { FlowCLI } from "@wireio/cluster-tool" -import { SwapNonNativeScenario } from "./SwapNonNativeScenario.js" - -/** Run the non-native-token swap flow as an executable — exit code = report success. */ -async function main(): Promise { - const report = await FlowCLI.create(SwapNonNativeScenario).run() - process.exit(report.succeeded ? 0 : 1) -} - -void main() diff --git a/packages/flow-swap-non-native-tokens/src/steps/SwapNonNativeScenarioTokenSteps.ts b/packages/flow-swap-non-native-tokens/src/steps/SwapNonNativeScenarioTokenSteps.ts deleted file mode 100644 index b12184eb3..000000000 --- a/packages/flow-swap-non-native-tokens/src/steps/SwapNonNativeScenarioTokenSteps.ts +++ /dev/null @@ -1,1185 +0,0 @@ -/** - * SwapNonNativeScenarioTokenSteps — flow-local Step factories for the - * non-native swap matrix. One factory per on-chain WRITE (each mint, each - * approve, each permit-signing setup, each requestSwap) plus one verify factory - * per old-jest assertion, so the `Report` narrates every custody move and every - * depot-side effect per swap cell. Cross-step runtime values (permit - * signatures, pre-swap balances, uwreq id baselines) ride `ctx.outputs` under - * per-cell typed {@link OutputKey}s; the {@link SwapCell} descriptors are pure - * build-time data composed from the scenario constants. - */ - -import Assert from "node:assert" -import Fs from "node:fs" -import Path from "node:path" -import { ethers } from "ethers" -import { PublicKey } from "@solana/web3.js" -import { getAssociatedTokenAddressSync } from "@solana/spl-token" -import { match } from "ts-pattern" -import { getLogger } from "@wireio/shared" -import { SysioContracts } from "@wireio/sdk-core" -import { - ClusterBuildStep, - EthereumCollateralTool, - Report, - SolanaCollateralTool, - SolanaFundingTool, - SwapScenarioContext, - WireReserveTool, - matchesProtoEnum, - mintMockErc20ToUser, - outputKey, - pollUntil, - requestEthereumSwapErc20WithApproval, - requestEthereumSwapErc20WithPermit, - requestSolanaSwapSpl, - contractView, - resolveLatestNonce, - signErc20Permit, - slugValue, - swapUserOutputKey, - verifyStep, - type ClusterBuildOptions, - type ClusterBuildStepOptions, - type Erc20ApprovableContract, - type Erc20PermitTarget, - type EthereumSwapArgs, - type MintableErc20, - type OutputKey, - type PermitSignature, - type ReserveManagerErc20SwapContract, - type StepInput, - type SwapUserOutput, - ClusterConfigProvider -} from "@wireio/cluster-tool" -import { SwapNonNativeScenarioConstants as Constants } from "../SwapNonNativeScenarioConstants.js" - -const log = getLogger(__filename) - -const { SysioContractName, SysioUwritUnderwriterequeststatus } = SysioContracts -const { Timing } = Constants - -/** The per-underwriter collateral matrix shape carried on the cluster options. */ -export type UnderwriterCollateralMatrix = NonNullable< - ClusterBuildOptions["underwriterCollateral"] -> - -/** How a swap cell's destination-side payout balance is observed. */ -export enum SwapDestinationKind { - solanaNative = "solanaNative", - solanaSplToken = "solanaSplToken", - ethereumNative = "ethereumNative" -} - -/** - * One cell of the non-native swap matrix — pure build-time data (composed from - * the scenario constants) shared by the cell's write step and its verify steps. - * Runtime values (balances, uwreq ids) ride `ctx.outputs` keyed by `name`. - */ -export interface SwapCell { - /** Unique kebab-case cell id — prefixes every per-cell output key. */ - readonly name: string - /** Source chain slug value (the outpost the custody lands on). */ - readonly sourceChainCode: number - /** Source token slug value (a non-native mock stablecoin). */ - readonly sourceTokenCode: number - /** Source amount in chain-native base units (6-dec for the mock stables). */ - readonly sourceAmount: bigint - /** - * Source token's chain-native decimals. The source outpost stamps the - * outbound `SwapRequest.source_amount` as `toDepot(sourceAmount, this)` — - * per-token depot precision `min(decimals, 9)`: identity for the 6-dec - * stables, ÷1e9 for 18-dec wei — so - * {@link SwapNonNativeScenarioTokenSteps.quoteTarget} must quote with the - * SAME depot-frame amount the depot re-quotes with at ingestion. The - * target itself is NEVER static — the depot re-quotes the live curve at - * ingestion and variance-reverts anything outside tolerance. - */ - readonly sourceDecimals: number - /** Target chain slug value. */ - readonly targetChainCode: number - /** Target token slug value. */ - readonly targetTokenCode: number - /** - * Destination token's chain-native decimals. The published target rides - * the attestation in the DESTINATION token's depot frame - * (`min(decimals, 9)` precision); the destination outpost pays out - * `fromDepot(target, this)` native units, which is what the payout-floor - * assertion must expect. - */ - readonly destinationDecimals: number - /** How the destination payout balance is read for the floor assertion. */ - readonly destination: SwapDestinationKind -} - -/** - * Flow-local Step factories + typed per-cell output keys for the non-native - * swap matrix. Write factories return one {@link ClusterBuildStep} per on-chain - * transaction; verify factories lift each old-jest assertion into a - * {@link verifyStep}. - */ -export namespace SwapNonNativeScenarioTokenSteps { - // ── Typed per-cell output keys (cross-step values ride ctx.outputs) ────── - - /** The cell's signed EIP-2612 permit (set by {@link planSignPermit}). */ - export function permitSignatureOutputKey( - cellName: string - ): OutputKey { - return outputKey( - `${cellName}.permitSignature`, - `signed EIP-2612 permit for ${cellName}` - ) - } - - /** ReserveManager's source-token balance snapshotted before the swap write. */ - export function custodyBeforeOutputKey(cellName: string): OutputKey { - return outputKey( - `${cellName}.custodyBefore`, - `pre-swap ReserveManager source-token balance for ${cellName}` - ) - } - - /** The user's destination balance snapshotted before the swap write. */ - export function destinationBeforeOutputKey( - cellName: string - ): OutputKey { - return outputKey( - `${cellName}.destinationBefore`, - `pre-swap destination balance for ${cellName}` - ) - } - - /** The LIVE depot quote for the cell (set by {@link planQuoteTarget}). */ - export function liveTargetOutputKey(cellName: string): OutputKey { - return outputKey( - `${cellName}.liveTarget`, - `live-quoted depot-frame target for ${cellName}` - ) - } - - /** Highest pre-existing uwreq id for the cell's chain pair (-1 when none). */ - export function uwreqBaselineIdOutputKey( - cellName: string - ): OutputKey { - return outputKey( - `${cellName}.uwreqBaselineId`, - `pre-swap max uwreq id for ${cellName}` - ) - } - - /** The cell's own uwreq row id (set once {@link planVerifyUwreqCreated} sees it). */ - export function uwreqIdOutputKey(cellName: string): OutputKey { - return outputKey( - `${cellName}.uwreqId`, - `uwreq row id for ${cellName}` - ) - } - - // ── Step: mint mock ERC-20 to the swap user (write) ────────────────────── - - /** Input for {@link planMintErc20ToSwapUser}. */ - export interface MintErc20ToSwapUserInput extends StepInput { - readonly kind: "SwapNonNativeScenarioTokenSteps.MintErc20ToSwapUserInput" - /** Mock ERC-20 token slug value (resolves the deployed mock's address). */ - readonly tokenCode: number - /** Token base units to mint to the swap user. */ - readonly amount: bigint - } - - /** - * ONE deployer-signed `mint(user, amount)` on the mock ERC-20 for - * `tokenCode`. The mocks expose `mint` ungated, and the deployer signer - * (anvil HD 0) sidesteps the intermittent user-wallet nonce races the old - * jest run hit — the swap steps still sign source custody with the USER - * wallet, so the user-signed path stays exercised. - */ - export function planMintErc20ToSwapUser( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - tokenCode: number, - amount: bigint - ): ClusterBuildStep { - return ClusterBuildStep.create< - SwapScenarioContext, - MintErc20ToSwapUserInput - >( - actor, - name, - description, - options, - { - kind: "SwapNonNativeScenarioTokenSteps.MintErc20ToSwapUserInput", - tokenCode, - amount - }, - runMintErc20ToSwapUser - ) - } - - /** Named runner — ONE mock-ERC-20 `mint(...)` write to the swap user. */ - export async function runMintErc20ToSwapUser( - ctx: SwapScenarioContext, - input: MintErc20ToSwapUserInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - Assert.ok(input.amount > 0n, "mintErc20ToSwapUser: amount must be positive") - const swapUser = ctx.outputs.assert(swapUserOutputKey()) - const token = mockErc20Contract(ctx, input.tokenCode) - const transactionHash = await mintMockErc20ToUser( - token, - swapUser.ethereumWallet.address, - input.amount - ) - log.info( - `[swap-non-native] minted ${input.amount} of token ${input.tokenCode} to ${swapUser.ethereumWallet.address} (${transactionHash})` - ) - } - - // ── Step: mint mock SPL to the swap user's ATA (write) ─────────────────── - - /** Input for {@link planMintSplToSwapUser}. */ - export interface MintSplToSwapUserInput extends StepInput { - readonly kind: "SwapNonNativeScenarioTokenSteps.MintSplToSwapUserInput" - /** Mock SPL token slug value (resolves the persisted mint pubkey). */ - readonly tokenCode: number - /** Token base units to mint into the swap user's ATA. */ - readonly amount: bigint - } - - /** - * ONE deployer-signed mock-SPL mint into the swap user's ATA (creating the - * ATA on demand). The mint pubkey comes from the bootstrap-persisted - * `sol-mock-mints.json`; the deployer keypair is the mint authority. - */ - export function planMintSplToSwapUser( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - tokenCode: number, - amount: bigint - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { - kind: "SwapNonNativeScenarioTokenSteps.MintSplToSwapUserInput", - tokenCode, - amount - }, - runMintSplToSwapUser - ) - } - - /** Named runner — ONE `mintMockSplToUser(...)` write into the swap user's ATA. */ - export async function runMintSplToSwapUser( - ctx: SwapScenarioContext, - input: MintSplToSwapUserInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - Assert.ok(input.amount > 0n, "mintSplToSwapUser: amount must be positive") - const swapUser = ctx.outputs.assert(swapUserOutputKey()) - const associatedTokenAddress = await SolanaFundingTool.mintMockSplToUser( - ctx.solana.connection, - SolanaFundingTool.loadDeployerKeypair(ctx.config.dataPath), - resolveSolanaMockMint(ctx.config.dataPath, input.tokenCode), - swapUser.solanaKeypair.publicKey, - input.amount - ) - log.info( - `[swap-non-native] minted ${input.amount} of token ${input.tokenCode} into ATA ${associatedTokenAddress.toBase58()}` - ) - } - - // ── Step: sign the cell's EIP-2612 permit (setup — stores the output) ──── - - /** Input for {@link planSignPermit}. */ - export interface SignPermitInput extends StepInput { - readonly kind: "SwapNonNativeScenarioTokenSteps.SignPermitInput" - readonly cell: SwapCell - } - - /** - * The user signs an EIP-2612 permit granting ReserveManager the cell's - * source amount, stored under {@link permitSignatureOutputKey} for the - * {@link planRequestSwapErc20WithPermit} write. Off-chain typed-data signing — - * no transaction leaves this step. - */ - /** - * Quote the cell's swap LIVE against the depot's reserve books and store - * the depot-frame target under {@link liveTargetOutputKey}. The depot - * re-quotes at ingestion and reverts anything outside tolerance, so the - * published target must come from the same curve — never a static constant. - */ - export function planQuoteTarget( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - cell: SwapCell - ): ClusterBuildStep { - return verifyStep( - actor, - name, - description, - async ctx => { - // Quote with the DEPOT-frame source — the amount the source outpost - // stamps into SwapRequest.source_amount and the depot re-quotes with. - const depotSourceAmount = WireReserveTool.toDepot( - cell.sourceAmount, - cell.sourceDecimals - ) - const target = await WireReserveTool.swapquote(ctx.wire, { - from: { - chainCode: cell.sourceChainCode, - tokenCode: cell.sourceTokenCode, - reserveCode: Constants.Reserves.ReserveCode - }, - fromAmount: depotSourceAmount, - to: { - chainCode: cell.targetChainCode, - tokenCode: cell.targetTokenCode, - reserveCode: Constants.Reserves.ReserveCode - } - }) - Assert.ok( - target > 0n, - `${cell.name}: live swapquote returned 0 — required reserves missing/inactive` - ) - ctx.outputs.set(liveTargetOutputKey(cell.name), target) - ctx.log.info( - `[swap-non-native] ${cell.name}: live target ${target} (depot frame) for depot source ${depotSourceAmount} (native ${cell.sourceAmount})` - ) - }, - options - ) - } - - export function planSignPermit( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - cell: SwapCell - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { kind: "SwapNonNativeScenarioTokenSteps.SignPermitInput", cell }, - runSignPermit - ) - } - - /** Named runner — sign the permit typed-data payload, store the signature. */ - export async function runSignPermit( - ctx: SwapScenarioContext, - input: SignPermitInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - const { cell } = input - const swapUser = ctx.outputs.assert(swapUserOutputKey()) - const token = mockErc20Contract( - ctx, - cell.sourceTokenCode - ) - const deadline = BigInt( - Math.floor(Date.now() / 1_000) + Constants.PermitDeadlineWindowSec - ) - const signature = await signErc20Permit( - swapUser.ethereumWallet, - token, - assertReserveManagerAddress(ctx), - cell.sourceAmount, - deadline - ) - ctx.outputs.set(permitSignatureOutputKey(cell.name), signature) - } - - // ── Step: pre-set ERC-20 allowance for the approval path (write) ───────── - - /** Input for {@link planApproveErc20Spend}. */ - export interface ApproveErc20SpendInput extends StepInput { - readonly kind: "SwapNonNativeScenarioTokenSteps.ApproveErc20SpendInput" - readonly cell: SwapCell - } - - /** - * ONE user-signed `approve(ReserveManager, sourceAmount)` on the cell's - * source ERC-20. Mainnet USDT does not implement EIP-2612, so this pre-set - * allowance is the production codepath for those tokens. - */ - export function planApproveErc20Spend( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - cell: SwapCell - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { kind: "SwapNonNativeScenarioTokenSteps.ApproveErc20SpendInput", cell }, - runApproveErc20Spend - ) - } - - /** Named runner — ONE user-signed ERC-20 `approve(...)` write. */ - export async function runApproveErc20Spend( - ctx: SwapScenarioContext, - input: ApproveErc20SpendInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - const { cell } = input - const swapUser = ctx.outputs.assert(swapUserOutputKey()) - const token = mockErc20Contract( - ctx, - cell.sourceTokenCode, - swapUser.ethereumWallet - ) - const nonce = await resolveLatestNonce(token) - const response = await token.approve( - assertReserveManagerAddress(ctx), - cell.sourceAmount, - { nonce } - ) - const receipt = await response.wait(1) - Assert.ok( - receipt?.status === 1, - `approveErc20Spend: approve reverted (status=${receipt?.status ?? "null"})` - ) - } - - // ── Step: ERC-20 swap via inline permit (write) ────────────────────────── - - /** Input for {@link planRequestSwapErc20WithPermit}. */ - export interface RequestSwapErc20WithPermitInput extends StepInput { - readonly kind: "SwapNonNativeScenarioTokenSteps.RequestSwapErc20WithPermitInput" - readonly cell: SwapCell - } - - /** - * ONE user-signed `ReserveManager.requestSwapErc20WithPermit(...)` write — - * permit consumption + transferFrom + fee-on-transfer guard + SWAP_REQUEST - * queue, all atomic. Snapshots the custody / destination / uwreq baselines - * (reads) immediately before submitting. - */ - export function planRequestSwapErc20WithPermit( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - cell: SwapCell - ): ClusterBuildStep { - return ClusterBuildStep.create< - SwapScenarioContext, - RequestSwapErc20WithPermitInput - >( - actor, - name, - description, - options, - { - kind: "SwapNonNativeScenarioTokenSteps.RequestSwapErc20WithPermitInput", - cell - }, - runRequestSwapErc20WithPermit - ) - } - - /** Named runner — snapshot baselines, then ONE permit-path swap write. */ - export async function runRequestSwapErc20WithPermit( - ctx: SwapScenarioContext, - input: RequestSwapErc20WithPermitInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - const { cell } = input - const swapUser = ctx.outputs.assert(swapUserOutputKey()) - const permitSignature = ctx.outputs.assert( - permitSignatureOutputKey(cell.name) - ) - await snapshotErc20Custody(ctx, cell) - await snapshotSwapBaselines(ctx, swapUser, cell) - const result = await requestEthereumSwapErc20WithPermit( - reserveManagerForSwapUser(ctx, swapUser.ethereumWallet), - erc20SwapArgs( - swapUser, - cell, - ctx.outputs.assert(liveTargetOutputKey(cell.name)) - ), - permitSignature - ) - Assert.ok(result.transactionHash, "requestSwapErc20WithPermit: no tx hash") - log.info( - `[swap-non-native] ${cell.name}: swap requested (${result.transactionHash})` - ) - } - - // ── Step: ERC-20 swap via pre-set allowance (write) ────────────────────── - - /** Input for {@link planRequestSwapErc20WithApproval}. */ - export interface RequestSwapErc20WithApprovalInput extends StepInput { - readonly kind: "SwapNonNativeScenarioTokenSteps.RequestSwapErc20WithApprovalInput" - readonly cell: SwapCell - } - - /** - * ONE user-signed `ReserveManager.requestSwapErc20WithApproval(...)` write. - * The allowance MUST be pre-set by an {@link planApproveErc20Spend} Step earlier - * in the same phase. - */ - export function planRequestSwapErc20WithApproval( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - cell: SwapCell - ): ClusterBuildStep { - return ClusterBuildStep.create< - SwapScenarioContext, - RequestSwapErc20WithApprovalInput - >( - actor, - name, - description, - options, - { - kind: "SwapNonNativeScenarioTokenSteps.RequestSwapErc20WithApprovalInput", - cell - }, - runRequestSwapErc20WithApproval - ) - } - - /** Named runner — snapshot baselines, then ONE approval-path swap write. */ - export async function runRequestSwapErc20WithApproval( - ctx: SwapScenarioContext, - input: RequestSwapErc20WithApprovalInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - const { cell } = input - const swapUser = ctx.outputs.assert(swapUserOutputKey()) - await snapshotErc20Custody(ctx, cell) - await snapshotSwapBaselines(ctx, swapUser, cell) - const result = await requestEthereumSwapErc20WithApproval( - reserveManagerForSwapUser(ctx, swapUser.ethereumWallet), - erc20SwapArgs( - swapUser, - cell, - ctx.outputs.assert(liveTargetOutputKey(cell.name)) - ) - ) - Assert.ok( - result.transactionHash, - "requestSwapErc20WithApproval: no tx hash" - ) - log.info( - `[swap-non-native] ${cell.name}: swap requested (${result.transactionHash})` - ) - } - - // ── Step: SPL swap via signed request_swap_spl IX (write) ──────────────── - - /** Input for {@link planRequestSwapSpl}. */ - export interface RequestSwapSplInput extends StepInput { - readonly kind: "SwapNonNativeScenarioTokenSteps.RequestSwapSplInput" - readonly cell: SwapCell - } - - /** - * ONE user-signed `opp_outpost::request_swap_spl` instruction — transfers - * the source amount from the user's ATA into the reserve vault PDA and - * queues the SWAP_REQUEST attestation. Snapshots the destination / uwreq - * baselines (reads) immediately before submitting. - */ - export function planRequestSwapSpl( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - cell: SwapCell - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { kind: "SwapNonNativeScenarioTokenSteps.RequestSwapSplInput", cell }, - runRequestSwapSpl - ) - } - - /** Named runner — snapshot baselines, then ONE `request_swap_spl` write. */ - export async function runRequestSwapSpl( - ctx: SwapScenarioContext, - input: RequestSwapSplInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - const { cell } = input - const swapUser = ctx.outputs.assert(swapUserOutputKey()) - await snapshotSwapBaselines(ctx, swapUser, cell) - const signature = await requestSolanaSwapSpl( - ctx.solana.connection, - SolanaCollateralTool.loadOppOutpostProgram(ctx, swapUser.solanaKeypair), - swapUser.solanaKeypair, - { - sourceTokenCode: BigInt(cell.sourceTokenCode), - sourceReserveCode: BigInt(Constants.Reserves.ReserveCode), - sourceAmount: cell.sourceAmount, - sourceMint: resolveSolanaMockMint( - ctx.config.dataPath, - cell.sourceTokenCode - ), - targetChainCode: BigInt(cell.targetChainCode), - targetTokenCode: BigInt(cell.targetTokenCode), - targetReserveCode: BigInt(Constants.Reserves.ReserveCode), - targetRecipient: targetRecipient(swapUser, cell), - targetAmount: ctx.outputs.assert(liveTargetOutputKey(cell.name)), - targetToleranceBps: Constants.Variance.ToleranceBps - } - ) - Assert.ok(signature, "requestSwapSpl: no transaction signature") - log.info(`[swap-non-native] ${cell.name}: swap requested (${signature})`) - } - - // ── Verify factories (each old-jest assertion → one verify step) ───────── - - /** - * Source side: ReserveManager's source-token balance bumped by EXACTLY - * `sourceAmount` (proves the fee-on-transfer guard didn't reject and - * custody landed). Read-once — the swap write already awaited its receipt. - */ - export function planVerifyErc20Custody( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - cell: SwapCell - ): ClusterBuildStep { - return verifyStep( - actor, - name, - description, - async ctx => { - const custodyBefore = ctx.outputs.assert( - custodyBeforeOutputKey(cell.name) - ) - const custodyAfter = await readMockErc20Balance( - ctx, - cell.sourceTokenCode, - assertReserveManagerAddress(ctx) - ) - Assert.strictEqual( - custodyAfter, - custodyBefore + cell.sourceAmount, - `${cell.name}: ReserveManager custody must bump by exactly ${cell.sourceAmount} ` + - `(before=${custodyBefore}, after=${custodyAfter})` - ) - }, - options - ) - } - - /** - * The depot opens a NEW uwreq row for the cell's chain pair (id above the - * pre-swap baseline); the row id is stored under {@link uwreqIdOutputKey} - * for the confirm / lock verifies. - */ - export function planVerifyUwreqCreated( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - cell: SwapCell - ): ClusterBuildStep { - return verifyStep( - actor, - name, - description, - async ctx => { - const baselineId = ctx.outputs.assert( - uwreqBaselineIdOutputKey(cell.name) - ) - await pollUntil( - `${cell.name}: new UWREQ row (id > ${baselineId})`, - async () => { - const created = ( - await readUwreqRowsForPair( - ctx, - cell.sourceChainCode, - cell.targetChainCode - ) - ).filter(row => Number(row.id) > baselineId) - if (created.length === 0) return false - const newest = created.reduce((left, right) => - Number(left.id) >= Number(right.id) ? left : right - ) - ctx.outputs.set(uwreqIdOutputKey(cell.name), Number(newest.id)) - return true - }, - Timing.UwreqDeadlineMs, - Timing.LongPollIntervalMs - ) - }, - options - ) - } - - /** - * The underwriter race resolves the cell's uwreq to CONFIRMED. COMPLETED is - * also accepted — a poll tick can land after settlement already advanced the - * row, and reaching COMPLETED implies CONFIRMED happened. - */ - export function planVerifyUwreqConfirmed( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - cell: SwapCell - ): ClusterBuildStep { - return verifyStep( - actor, - name, - description, - async ctx => { - const uwreqId = ctx.outputs.assert(uwreqIdOutputKey(cell.name)) - await pollUntil( - `${cell.name}: UWREQ ${uwreqId} CONFIRMED`, - async () => { - const row = ( - await readUwreqRowsForPair( - ctx, - cell.sourceChainCode, - cell.targetChainCode - ) - ).find(candidate => Number(candidate.id) === uwreqId) - return ( - row != null && - (matchesProtoEnum( - row.status, - SysioUwritUnderwriterequeststatus, - SysioUwritUnderwriterequeststatus.UNDERWRITE_REQUEST_STATUS_CONFIRMED - ) || - matchesProtoEnum( - row.status, - SysioUwritUnderwriterequeststatus, - SysioUwritUnderwriterequeststatus.UNDERWRITE_REQUEST_STATUS_COMPLETED - )) - ) - }, - Timing.RaceDeadlineMs, - Timing.LongPollIntervalMs - ) - }, - options - ) - } - - /** - * Both legs locked: exactly {@link Constants.LocksPerSwap} `sysio.uwrit::locks` - * rows reference the cell's uwreq (the locks form in the race-resolving - * transaction and persist for the challenge window). - */ - export function planVerifyUwreqLocks( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - cell: SwapCell - ): ClusterBuildStep { - return verifyStep( - actor, - name, - description, - async ctx => { - const uwreqId = ctx.outputs.assert(uwreqIdOutputKey(cell.name)) - await pollUntil( - `${cell.name}: ${Constants.LocksPerSwap} locks on UWREQ ${uwreqId}`, - async () => - (await ctx.locksForUwreq(uwreqId)).length === - Constants.LocksPerSwap, - Timing.UwreqDeadlineMs, - Timing.LongPollIntervalMs - ) - }, - options - ) - } - - /** - * Canonical proof: the user's destination balance reaches - * `before + (destinationTargetAmount − drift)` — only achievable if source - * custody, the OPP round-trip, the underwriter race, the depot variance - * check, and the destination payout all worked. - */ - export function planVerifyDestinationPayout( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - cell: SwapCell - ): ClusterBuildStep { - return verifyStep( - actor, - name, - description, - async ctx => { - const swapUser = ctx.outputs.assert(swapUserOutputKey()) - const destinationBefore = ctx.outputs.assert( - destinationBeforeOutputKey(cell.name) - ) - const liveTarget = ctx.outputs.assert(liveTargetOutputKey(cell.name)) - const floor = destinationPayoutFloor( - destinationBefore, - cell, - liveTarget - ) - await pollUntil( - `${cell.name}: destination balance ≥ ${floor}`, - async () => - (await readDestinationBalance(ctx, swapUser, cell)) >= floor, - Timing.RemitDeadlineMs, - Timing.LongPollIntervalMs - ) - }, - options - ) - } - - /** - * Gate: every underwriter's outpost bonds have relayed to the depot — one - * `sysio.opreg` balance row per (chain, token) collateral entry, at or above - * the deposited amount. Without this the first SWAP_REQUEST can reach - * `sysio.uwrit::createuwreq` before the bonds exist depot-side and revert - * with "insufficient bond on one or both legs". - */ - export function planVerifyUnderwriterBondsRelayed( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - underwriterLabels: string[], - collateral: UnderwriterCollateralMatrix - ): ClusterBuildStep { - collateral.forEach(entries => - entries.forEach(entry => - Assert.ok( - entry.amount, - "verifyUnderwriterBondsRelayed: every collateral entry needs an amount" - ) - ) - ) - return verifyStep( - actor, - name, - description, - async ctx => { - await pollUntil( - "every underwriter bond credited on sysio.opreg", - async () => { - const { rows } = await ctx.wire - .getSysioContract(SysioContractName.opreg) - .tables.operators.query({ limit: 100 }) - return underwriterLabels.every((label, index) => { - const account = ctx.keyStore.assertOperator(label).account, - operator = rows.find(row => row.account === account) - if (operator == null) return false - return collateral[index].every(entry => - operator.balances.some( - balance => - slugValue(balance.chain_code) === entry.chain_code && - slugValue(balance.token_code) === - Number(entry.amount.tokenCode) && - BigInt(balance.balance) >= entry.amount.amount - ) - ) - }) - }, - Timing.RemitDeadlineMs, - Timing.LongPollIntervalMs - ) - }, - options - ) - } -} - -// ── Module-internal reads / value helpers (executed INSIDE runners) ───────── - -/** One row of the bootstrap-persisted `sol-mock-mints.json` manifest. */ -interface SolanaMockMint { - code: number - mint: string - decimals: number -} - -/** Minimal structural surface for the ERC-20 `balanceOf` read. */ -interface Erc20BalanceReadContract extends ethers.BaseContract { - balanceOf: (owner: string) => Promise -} - -/** A `sysio.uwrit::uwreqs` row (generated table-row type). */ -type UwreqRow = SysioContracts.SysioUwritUwRequestTType - -/** Baseline uwreq id when the pair has no pre-existing rows. */ -const NoUwreqBaselineId = -1 - -/** - * Load a test-mock ERC-20 ABI from the hardhat artifacts. The mocks live under - * `contracts/test/outpost/` — separate from the production `contracts/outpost/` - * tree that `EthereumCollateralTool.loadOutpostAbi` reads. - */ -function loadTestErc20Abi( - ethereumPath: string, - contractName: string -): ethers.InterfaceAbi { - const artifactPath = Path.join( - ethereumPath, - "artifacts", - "contracts", - "test", - "outpost", - `${contractName}.sol`, - `${contractName}.json` - ) - Assert.ok( - Fs.existsSync(artifactPath), - `SwapNonNativeScenarioTokenSteps: mock ERC-20 artifact not found at ${artifactPath}` - ) - return JSON.parse(Fs.readFileSync(artifactPath, "utf8")).abi -} - -/** - * Bind the deployed mock ERC-20 for `tokenCode`. Deployer-signed (the cached - * `ctx.ethereum` client wallet — anvil HD 0) unless a `signer` is supplied for - * user-signed writes (approve). - */ -function mockErc20Contract( - ctx: SwapScenarioContext, - tokenCode: number, - signer?: ethers.Signer -): View & ethers.BaseContract { - const addressKey = Constants.MockErc20AddressKeyByTokenCode.get(tokenCode) - Assert.ok( - addressKey != null, - `SwapNonNativeScenarioTokenSteps: no mock ERC-20 mapping for token code ${tokenCode}` - ) - const address = EthereumCollateralTool.loadOutpostAddresses( - ClusterConfigProvider.ethereumDeploymentsPath(ctx.config) - )[addressKey] - Assert.ok( - address != null && ethers.isAddress(address), - `SwapNonNativeScenarioTokenSteps: ${addressKey} not in outpost-addrs.json (got ${address})` - ) - const abi = loadTestErc20Abi(ctx.config.ethereumPath, addressKey) - return ( - signer == null - ? ctx.ethereum.getContract(addressKey, address, abi) - : contractView(address, abi, signer) - ) as View & ethers.BaseContract -} - -/** The deployed ReserveManager address from `outpost-addrs.json` (asserted). */ -function assertReserveManagerAddress(ctx: SwapScenarioContext): string { - const address = EthereumCollateralTool.loadOutpostAddresses( - ClusterConfigProvider.ethereumDeploymentsPath(ctx.config) - )[Constants.OutpostAddressKey.ReserveManager] - Assert.ok( - address != null && ethers.isAddress(address), - `SwapNonNativeScenarioTokenSteps: ReserveManager not in outpost-addrs.json (got ${address})` - ) - return address -} - -/** Bind ReserveManager's ERC-20 swap surface to the swap user's wallet. */ -function reserveManagerForSwapUser( - ctx: SwapScenarioContext, - wallet: ethers.Signer -): ReserveManagerErc20SwapContract { - const abi = EthereumCollateralTool.loadOutpostAbi( - ctx.config.ethereumPath, - Constants.OutpostAddressKey.ReserveManager - ) - return contractView( - assertReserveManagerAddress(ctx), - abi, - wallet - ) -} - -/** - * Resolve the bootstrap-persisted mock SPL mint pubkey for `tokenCode` from - * `/sol-mock-mints.json`. - */ -function resolveSolanaMockMint(dataPath: string, tokenCode: number): PublicKey { - const mintsFile = Path.join(dataPath, Constants.SolanaMockMintsFilename) - Assert.ok( - Fs.existsSync(mintsFile), - `SwapNonNativeScenarioTokenSteps: mock SPL mints not found at ${mintsFile}` - ) - const mints = JSON.parse( - Fs.readFileSync(mintsFile, "utf8") - ) as SolanaMockMint[] - const found = mints.find(entry => entry.code === tokenCode) - Assert.ok( - found, - `SwapNonNativeScenarioTokenSteps: no mock SPL mint persisted for token code ${tokenCode}` - ) - return new PublicKey(found.mint) -} - -/** READ the mock ERC-20 balance of `owner` for `tokenCode`. */ -function readMockErc20Balance( - ctx: SwapScenarioContext, - tokenCode: number, - owner: string -): Promise { - return mockErc20Contract(ctx, tokenCode).balanceOf( - owner - ) -} - -/** The cell's raw recipient bytes on the target chain (20-byte EVM / 32-byte SVM). */ -function targetRecipient(swapUser: SwapUserOutput, cell: SwapCell): Uint8Array { - return cell.destination === SwapDestinationKind.ethereumNative - ? swapUser.ethereumAddressBytes - : swapUser.solanaPublicKeyBytes -} - -/** READ the user's destination-side balance per the cell's destination kind. */ -function readDestinationBalance( - ctx: SwapScenarioContext, - swapUser: SwapUserOutput, - cell: SwapCell -): Promise { - return match(cell.destination) - .with(SwapDestinationKind.solanaNative, async () => - BigInt(await ctx.solana.getLamports(swapUser.solanaKeypair.publicKey)) - ) - .with(SwapDestinationKind.ethereumNative, () => - ctx.ethereum.getBalance(swapUser.ethereumWallet.address) - ) - .with(SwapDestinationKind.solanaSplToken, () => - ctx.solana.getSplBalance( - getAssociatedTokenAddressSync( - resolveSolanaMockMint(ctx.config.dataPath, cell.targetTokenCode), - swapUser.solanaKeypair.publicKey - ) - ) - ) - .exhaustive() -} - -/** - * The payout floor: `before + fromDepot(liveTarget − variance drift)`. Drift is - * applied in the depot frame (where the depot's variance gate evaluates it), - * then the net converts to destination-native units exactly as the outpost's - * `fromDepot` payout does. - */ -function destinationPayoutFloor( - destinationBefore: bigint, - cell: SwapCell, - liveTarget: bigint -): bigint { - const depotNet = - liveTarget - - WireReserveTool.varianceDrift(liveTarget, Constants.Variance.ToleranceBps) - return ( - destinationBefore + - WireReserveTool.fromDepot(depotNet, cell.destinationDecimals) - ) -} - -/** The calldata-facing `SwapArgs` struct for the cell (ERC-20 source paths). */ -function erc20SwapArgs( - swapUser: SwapUserOutput, - cell: SwapCell, - targetAmount: bigint -): EthereumSwapArgs { - return { - sourceTokenCode: BigInt(cell.sourceTokenCode), - sourceReserveCode: BigInt(Constants.Reserves.ReserveCode), - sourceAmount: cell.sourceAmount, - targetChainCode: BigInt(cell.targetChainCode), - targetTokenCode: BigInt(cell.targetTokenCode), - targetReserveCode: BigInt(Constants.Reserves.ReserveCode), - targetRecipient: targetRecipient(swapUser, cell), - targetAmount, - targetToleranceBps: Constants.Variance.ToleranceBps - } -} - -/** READ the `sysio.uwrit::uwreqs` rows for a (source chain, target chain) pair. */ -async function readUwreqRowsForPair( - ctx: SwapScenarioContext, - sourceChainCode: number, - targetChainCode: number -): Promise { - const { rows } = await ctx.wire - .getSysioContract(SysioContractName.uwrit) - .tables.uwreqs.query() - return rows.filter( - row => - slugValue(row.src_chain_code) === sourceChainCode && - slugValue(row.dst_chain_code) === targetChainCode - ) -} - -/** READ the highest existing uwreq id for the pair ({@link NoUwreqBaselineId} when none). */ -async function maxUwreqIdForPair( - ctx: SwapScenarioContext, - sourceChainCode: number, - targetChainCode: number -): Promise { - return ( - await readUwreqRowsForPair(ctx, sourceChainCode, targetChainCode) - ).reduce((max, row) => Math.max(max, Number(row.id)), NoUwreqBaselineId) -} - -/** Snapshot the ReserveManager's source-token custody before an ERC-20 swap write. */ -async function snapshotErc20Custody( - ctx: SwapScenarioContext, - cell: SwapCell -): Promise { - ctx.outputs.set( - SwapNonNativeScenarioTokenSteps.custodyBeforeOutputKey(cell.name), - await readMockErc20Balance( - ctx, - cell.sourceTokenCode, - assertReserveManagerAddress(ctx) - ) - ) -} - -/** - * Snapshot the destination balance + the uwreq id baseline immediately before - * the swap write (reads) — the verify steps compare against these outputs. - */ -async function snapshotSwapBaselines( - ctx: SwapScenarioContext, - swapUser: SwapUserOutput, - cell: SwapCell -): Promise { - ctx.outputs.set( - SwapNonNativeScenarioTokenSteps.destinationBeforeOutputKey(cell.name), - await readDestinationBalance(ctx, swapUser, cell) - ) - ctx.outputs.set( - SwapNonNativeScenarioTokenSteps.uwreqBaselineIdOutputKey(cell.name), - await maxUwreqIdForPair(ctx, cell.sourceChainCode, cell.targetChainCode) - ) -} diff --git a/packages/flow-swap-non-native-tokens/tsconfig.json b/packages/flow-swap-non-native-tokens/tsconfig.json deleted file mode 100644 index 82b017421..000000000 --- a/packages/flow-swap-non-native-tokens/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-swap-non-native-tokens/tsconfig.src.json b/packages/flow-swap-non-native-tokens/tsconfig.src.json deleted file mode 100644 index b8bf202f6..000000000 --- a/packages/flow-swap-non-native-tokens/tsconfig.src.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "extends": "../../etc/tsconfig/tsconfig.base.jest.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "lib", - "paths": { - "@wireio/cluster-tool": [ - "../cluster-tool/src" - ], - "@wireio/cluster-tool-shared": [ - "../cluster-tool-shared/src" - ] - } - }, - "include": [ - "src" - ], - "references": [ - { - "path": "../cluster-tool-shared/tsconfig.json" - }, - { - "path": "../cluster-tool/tsconfig.json" - } - ] -} diff --git a/packages/flow-swap-private-reserves/package.json b/packages/flow-swap-private-reserves/package.json deleted file mode 100644 index 51c6e9de1..000000000 --- a/packages/flow-swap-private-reserves/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "@wireio/test-flow-swap-private-reserves", - "version": "0.1.18", - "private": true, - "type": "commonjs", - "description": "Flow: bidirectional swaps through a same-owner PRIVATE reserve pair (native × non-native) + private→WIRE exclusion", - "scripts": { - "build": "tsc -b tsconfig.json", - "test:disabled": "node lib/index.js", - "clean": "rm -rf lib" - }, - "dependencies": { - "@coral-xyz/anchor": "^0.31.0", - "@solana/spl-token": "^0.4.9", - "@solana/web3.js": "^1.98.0", - "@wireio/cluster-tool": "workspace:*", - "@wireio/debugging-shared": "workspace:*", - "@wireio/sdk-core": "^1.0.92", - "@wireio/opp-typescript-models": "^1.0.50", - "@wireio/shared": "^1.0.92", - "ethers": "^6.14.0" - }, - "devDependencies": { - "@types/node": "25.5.0" - } -} diff --git a/packages/flow-swap-private-reserves/src/SwapPrivateReservesScenario.ts b/packages/flow-swap-private-reserves/src/SwapPrivateReservesScenario.ts deleted file mode 100644 index 1c48687fe..000000000 --- a/packages/flow-swap-private-reserves/src/SwapPrivateReservesScenario.ts +++ /dev/null @@ -1,1359 +0,0 @@ -import Assert from "node:assert" -import type { ClusterConfig } from "@wireio/cluster-tool-shared" -import { getAssociatedTokenAddressSync } from "@solana/spl-token" -import { ChainKind, TokenAmount } from "@wireio/opp-typescript-models" -import { SlugName, SysioContracts } from "@wireio/sdk-core" -import { getLogger } from "@wireio/shared" -import { oppDebuggingPath } from "@wireio/debugging-shared" -import { - ClusterBuildPhase, - Constants as HarnessConstants, - Report, - Steps, - SwapScenarioContext, - SwapUserIdentities, - WireReserveTool, - WireUnderwriterTool, - FlowScenario, - containsSwapRevert, - matchesProtoEnum, - pollUntil, - slugValue, - swapUserOutputKey, - verifyStep, - type ClusterBuild, - type ClusterBuildOptions, - type Logger -} from "@wireio/cluster-tool" -import { SwapPrivateReservesScenarioArtifacts as Artifacts } from "./SwapPrivateReservesScenarioArtifacts.js" -import { SwapPrivateReservesScenarioConstants as Constants } from "./SwapPrivateReservesScenarioConstants.js" -import { SwapPrivateReservesScenarioOutputs as Outputs } from "./SwapPrivateReservesScenarioOutputs.js" -import { SwapPrivateReservesScenarioOwnerSteps as OwnerSteps } from "./steps/SwapPrivateReservesScenarioOwnerSteps.js" -import { SwapPrivateReservesScenarioReserveSteps as ReserveSteps } from "./steps/SwapPrivateReservesScenarioReserveSteps.js" -import { SwapPrivateReservesScenarioSwapSteps as SwapSteps } from "./steps/SwapPrivateReservesScenarioSwapSteps.js" - -const { - SysioContractName, - SysioOpregOperatorstatus, - SysioReservReservestatus, - SysioUwritUnderwriterequeststatus -} = SysioContracts -const { Actor } = Report - -const log = getLogger(__filename) - -/** The scenario context — the shared swap-flow depot query surface. */ -type Context = SwapScenarioContext - -// ── Depot read helpers (reads execute freely inside verify runners) ───────── - -/** The depot's PRIVATE reserve row for `(chainCode, tokenCode)`, or nothing yet. */ -async function readPrivateReserveRow( - ctx: Context, - chainCode: number, - tokenCode: number -): Promise { - const { rows } = await ctx.wire - .getSysioContract(SysioContractName.reserv) - .tables.reserves.query() - return rows.find( - row => - slugValue(row.chain_code) === chainCode && - slugValue(row.token_code) === tokenCode && - slugValue(row.reserve_code) === Constants.Reserves.PrivateReserveCode - ) -} - -/** - * The UWREQ sourcing the PRIVATE reserve on `sourceChainCode` toward - * `destinationChainCode`, or nothing yet (the `src_reserve_code` filter is the - * old suite's triple match — the WIRE-probe negative depends on it excluding - * the swap phases' requests by destination). - */ -async function readPrivatePairUwreq( - ctx: Context, - sourceChainCode: number, - destinationChainCode: number -): Promise { - const { rows } = await ctx.wire - .getSysioContract(SysioContractName.uwrit) - .tables.uwreqs.query() - return rows.find( - request => - slugValue(request.src_chain_code) === sourceChainCode && - slugValue(request.src_reserve_code) === - Constants.Reserves.PrivateReserveCode && - slugValue(request.dst_chain_code) === destinationChainCode - ) -} - -/** True when the UWREQ row exists and reports CONFIRMED (either wire shape). */ -function uwreqConfirmed(row: SysioContracts.SysioUwritUwRequestTType): boolean { - return ( - row != null && - matchesProtoEnum( - row.status, - SysioUwritUnderwriterequeststatus, - SysioUwritUnderwriterequeststatus.UNDERWRITE_REQUEST_STATUS_CONFIRMED - ) - ) -} - -/** The user's USDCSOL ATA balance in base units (a read). */ -async function readUserUsdcSolBalance(ctx: Context): Promise { - const swapUser = ctx.outputs.assert(swapUserOutputKey()) - return ctx.solana.getSplBalance( - getAssociatedTokenAddressSync( - Artifacts.loadUsdcSolMint(ctx), - swapUser.solanaKeypair.publicKey - ) - ) -} - -/** - * Flow: bidirectional swaps through a same-owner PRIVATE reserve pair - * (native × non-native) + private→WIRE exclusion. - * - * The FINAL verification flow for the gated-reserve feature: both private - * reserves are stood up via the REAL handshake — outpost `create_reserve` - * (ETH native / SOL USDCSOL SPL, `isPrivate=true`) → depot PENDING row → - * `matchreserve` by the single authex-linked owner (`privowner`, linked on - * BOTH chains) escrowing real WIRE → ACTIVE with `owner = privowner` → - * RESERVE_READY flips both outpost-local records. - * - * Because both reserves share one non-empty owner, the depot's privacy gate - * ADMITS the pair: Phase A swaps native ETH → USDCSOL (SPL payout to the - * user's ATA) and Phase B swaps USDCSOL → native ETH, each through the - * standard UWREQ race (two legs ⇒ two locks) with the emit-time four-leg - * constant-product books asserted with exact integers. The WIRE-endpoint - * exclusion still binds: a swap sourcing the private ETH reserve toward WIRE - * draws a SWAP_REVERT and never creates a UWREQ. - */ -export class SwapPrivateReservesScenario extends FlowScenario { - readonly name = "flow-swap-private-reserves" - readonly description = - "Bidirectional swaps through a same-owner PRIVATE reserve pair (native × non-native) + private→WIRE exclusion" - - override readonly defaults: ClusterBuildOptions = { - // The launch build of the Solana outpost withholds the reserve/swap - // bootstrap calls; this flow exercises them, so it opts in (never in plan()). - enableLaunchWithheldOperations: true, - epochDurationSec: Constants.Timing.EpochDurationSec, - // `createuwreq` re-checks `meets_role_min` for BOTH legs of every swap — - // the per-(chain, token) minimums the underwriter's deposits must clear. - requiredUnderwriterCollateral: [ - { - chainCode: Constants.Reserves.Ethereum.ChainCode, - tokenCode: Constants.Reserves.Ethereum.TokenCode, - minimumBond: Constants.Underwriting.MinimumBond - }, - { - chainCode: Constants.Reserves.Solana.ChainCode, - tokenCode: Constants.Reserves.Solana.NativeTokenCode, - minimumBond: Constants.Underwriting.MinimumBond - } - ], - // The underwriter bonds on every leg this flow's matrix touches: native - // ETH, native SOL, and the non-native USDCSOL leg (the underwriter - // plugin's `select_coverable` requires a credit line per (chain, token)). - underwriterCollateral: [ - [ - { - chain_code: Constants.Reserves.Ethereum.ChainCode, - amount: TokenAmount.create({ - tokenCode: BigInt(Constants.Reserves.Ethereum.TokenCode), - amount: Constants.Underwriting.CollateralAmount - }) - }, - { - chain_code: Constants.Reserves.Solana.ChainCode, - amount: TokenAmount.create({ - tokenCode: BigInt(Constants.Reserves.Solana.NativeTokenCode), - amount: Constants.Underwriting.CollateralAmount - }) - }, - { - chain_code: Constants.Reserves.Solana.ChainCode, - amount: TokenAmount.create({ - tokenCode: BigInt(Constants.Reserves.Solana.TokenCode), - amount: Constants.Underwriting.CollateralAmount - }) - } - ] - ] - } - - override createContext(config: ClusterConfig, contextLog: Logger): Context { - return new SwapScenarioContext(config, contextLog) - } - - plan(cluster: ClusterBuild): void { - const relayOptions = { - timeoutMs: - Constants.Timing.RelayDeadlineMs + - Constants.Timing.PollDeadlineBufferMs - }, - readyOptions = { - timeoutMs: - Constants.Timing.ReadyDeadlineMs + - Constants.Timing.PollDeadlineBufferMs - }, - uwreqOptions = { - timeoutMs: - Constants.Timing.UwreqDeadlineMs + - Constants.Timing.PollDeadlineBufferMs - }, - raceOptions = { - timeoutMs: - Constants.Timing.RaceDeadlineMs + - Constants.Timing.PollDeadlineBufferMs - }, - remitOptions = { - timeoutMs: - Constants.Timing.RemitDeadlineMs + - Constants.Timing.PollDeadlineBufferMs - }, - noUwreqOptions = { - timeoutMs: - Constants.Timing.NoUwreqWindowMs + - Constants.Timing.PollDeadlineBufferMs - }, - // Write / snapshot ceiling — covers the SOL confirm loop's own 60s - // deadline plus tx build headroom (the old suite ran writes under a - // 720s beforeAll umbrella with no per-write ceiling). - writeOptions = { timeoutMs: 90_000 } - - // ── 1. Underwriter collateral (the old harness's bootstrap deposits) ── - // One Phase per underwriter, one Step per (chain, token) bond, from the - // resolved config plan (this scenario's defaults: ETH + SOL + USDCSOL). - const underwriterLabels = Array.from( - { length: cluster.config.underwriterCount }, - (_, index) => HarnessConstants.underwriterLabel(index) - ) - WireUnderwriterTool.planCollateralDeposit( - cluster, - "UnderwriterCollateral", - "Underwriters bond collateral on every leg the swap matrix touches", - writeOptions, - underwriterLabels, - cluster.config.underwriterCollateral ?? - WireUnderwriterTool.load(null, cluster.config.underwriterCount) - ) - - // ── 2. Substrate health (old Phase 0 tests) ── - ClusterBuildPhase.create( - cluster, - "SubstrateHealth", - "The WIRE chain produces blocks and the underwriter's deposits credit" - ).push( - verifyStep( - Actor.Sysio, - "wire-chain-producing", - "WIRE chain is producing blocks", - runVerifyWireChainProducing, - writeOptions - ), - verifyStep( - Actor.Underwriter, - "underwriter-active", - `${underwriterLabels[0]} becomes ACTIVE (deposits credit)`, - runVerifyUnderwriterActive, - uwreqOptions - ) - ) - - // ── 3. The swap user's dual-chain identity (creator on BOTH chains) ── - SwapUserIdentities.planIdentityProvisioning( - cluster, - "SwapUserIdentity", - "Provision the dual-chain swap user (reserve creator + swap actor)", - writeOptions - ) - - // ── 4. The single owner — WIRE account + authex links + SPL funding ── - ClusterBuildPhase.create( - cluster, - "OwnerProvisioning", - `Provision ${Constants.Accounts.Owner} (WIRE account, BOTH-chain authex links, creator USDCSOL funding)` - ).push( - OwnerSteps.planProvisionOwner( - Actor.User, - "provision-owner", - `create + fund the ${Constants.Accounts.Owner} WIRE account`, - writeOptions - ), - OwnerSteps.planLinkOwner( - Actor.User, - "link-owner-ethereum", - `authex-link ${Constants.Accounts.Owner} to the ETH creator wallet's secp256k1 key`, - writeOptions, - ChainKind.EVM - ), - OwnerSteps.planLinkOwner( - Actor.User, - "link-owner-solana", - `authex-link ${Constants.Accounts.Owner} to the SOL creator keypair's ed25519 key`, - writeOptions, - ChainKind.SVM - ), - OwnerSteps.planMintCreatorUsdcSol( - Actor.SolanaOutpost, - "mint-creator-usdcsol", - `mint ${Constants.SplFunding.CreatorMintAmount} USDCSOL base units to the creator's ATA`, - writeOptions - ) - ) - - // ── 5. ETH-side gated handshake: create → PENDING → match → ACTIVE ── - ClusterBuildPhase.create( - cluster, - "CreateMatchEthereumPrivate", - "ETH outpost create_reserve(isPrivate) → depot PENDING → matchreserve → ACTIVE + RESERVE_READY" - ).push( - ReserveSteps.planCreateEthereumReserve( - Actor.User, - "create-ethereum-private-reserve", - `create_reserve(ETH/PRIVATE) escrowing ${Constants.CreateParams.EthereumEscrowWei} wei`, - writeOptions - ), - verifyStep( - Actor.Sysio, - "ethereum-depot-row-pending", - "the ETH-side private depot row appears with status=PENDING", - runVerifyEthereumDepotRowPending, - relayOptions - ), - ReserveSteps.planMatchReserve( - Actor.User, - "match-ethereum-private-reserve", - `${Constants.Accounts.Owner} matches the ETH private reserve with ${Constants.CreateParams.EthereumRequestedWire} WIRE`, - writeOptions, - Constants.Reserves.Ethereum.ChainCode, - Constants.Reserves.Ethereum.TokenCode, - Constants.CreateParams.EthereumRequestedWire - ), - verifyStep( - Actor.Sysio, - "ethereum-depot-row-active", - `the ETH depot row is ACTIVE with owner=${Constants.Accounts.Owner}, is_private, and exact custody`, - runVerifyEthereumDepotRowActive, - writeOptions - ), - verifyStep( - Actor.EthereumOutpost, - "ethereum-local-record-active", - "RESERVE_READY flips the ETH outpost-local private record ACTIVE", - runVerifyEthereumLocalReserveActive, - readyOptions - ) - ) - - // ── 6. SOL-side gated handshake (mirror) ── - ClusterBuildPhase.create( - cluster, - "CreateMatchSolanaPrivate", - "SOL outpost create_reserve(isPrivate, SPL) → depot PENDING → matchreserve → ACTIVE + RESERVE_READY" - ).push( - ReserveSteps.planCreateSolanaReserve( - Actor.User, - "create-solana-private-reserve", - `create_reserve(USDCSOL/PRIVATE) escrowing ${Constants.CreateParams.SolanaEscrowChainUnits} base units`, - writeOptions - ), - verifyStep( - Actor.Sysio, - "solana-depot-row-pending", - "the SOL-side private depot row appears with status=PENDING", - runVerifySolanaDepotRowPending, - relayOptions - ), - ReserveSteps.planMatchReserve( - Actor.User, - "match-solana-private-reserve", - `${Constants.Accounts.Owner} matches the SOL private reserve with ${Constants.CreateParams.SolanaRequestedWire} WIRE`, - writeOptions, - Constants.Reserves.Solana.ChainCode, - Constants.Reserves.Solana.TokenCode, - Constants.CreateParams.SolanaRequestedWire - ), - verifyStep( - Actor.Sysio, - "solana-depot-row-active", - `the SOL depot row is ACTIVE with owner=${Constants.Accounts.Owner}, is_private, and exact custody`, - runVerifySolanaDepotRowActive, - writeOptions - ), - verifyStep( - Actor.SolanaOutpost, - "solana-local-record-active", - "RESERVE_READY flips the SOL outpost's private Reserve PDA Active (gates Phase B's request_swap_spl)", - runVerifySolanaLocalReserveActive, - readyOptions - ) - ) - - // ── 6b. The owner prices BOTH reserves before any swap runs ── - // Both rows are ACTIVE and owned by now, which is what `setrsvfee` requires - // (an owner-less reserve can never charge). One write per step. - ClusterBuildPhase.create( - cluster, - "ReserveOwnerFees", - "The reserve owner sets an owner fee on each private reserve" - ).push( - Steps.contracts.sysio.reserv.planSetrsvfee( - Actor.User, - "set-ethereum-reserve-fee", - `owner charges ${Constants.ReserveOwnerFees.EthereumBps} bps on the private ETH reserve`, - writeOptions, - { - chain_code: SlugName.toString(Constants.Reserves.Ethereum.ChainCode), - token_code: SlugName.toString(Constants.Reserves.Ethereum.TokenCode), - reserve_code: SlugName.toString(Constants.Reserves.PrivateReserveCode), - owner_fee_bps: Constants.ReserveOwnerFees.EthereumBps - }, - Constants.Accounts.Owner - ), - Steps.contracts.sysio.reserv.planSetrsvfee( - Actor.User, - "set-solana-reserve-fee", - `owner charges ${Constants.ReserveOwnerFees.SolanaBps} bps on the private SOL reserve`, - writeOptions, - { - chain_code: SlugName.toString(Constants.Reserves.Solana.ChainCode), - token_code: SlugName.toString(Constants.Reserves.Solana.TokenCode), - reserve_code: SlugName.toString(Constants.Reserves.PrivateReserveCode), - owner_fee_bps: Constants.ReserveOwnerFees.SolanaBps - }, - Constants.Accounts.Owner - ), - verifyStep( - Actor.User, - "reserve-fees-recorded", - "both private reserves carry the owner's fee rate and have earned nothing yet", - runVerifyReserveFeesSet, - writeOptions - ) - ) - - // ── 7. Phase A: ETH (native) → USDCSOL (SPL) through the private pair ── - ClusterBuildPhase.create( - cluster, - "PhaseAEthereumToSolana", - "ETH (native) → USDCSOL (SPL) through the private pair: quote → request → UWREQ race → books → payout" - ).push( - verifyStep( - Actor.User, - "phase-a-quote", - "compute the private-pair quote (two-hop constant product) + snapshot baselines", - runPhaseAQuote, - writeOptions - ), - SwapSteps.planRequestSwapEthereumToSolana( - Actor.User, - "phase-a-request-swap", - "user calls ReserveManager.requestSwap sourcing the private ETH reserve", - writeOptions - ), - verifyStep( - Actor.Sysio, - "phase-a-uwreq-created", - "depot creates the PENDING private-pair UWREQ (same-owner pairing admitted)", - runPhaseAUwreqCreated, - uwreqOptions - ), - verifyStep( - Actor.Underwriter, - "phase-a-uwreq-confirmed", - "UWREQ resolves CONFIRMED with TWO locks (one per leg)", - runPhaseAUwreqConfirmed, - raceOptions - ), - verifyStep( - Actor.Sysio, - "phase-a-four-leg-books", - "emit-time four-leg books move on the two private rows (Σwire drops by the WIRE-leg fee)", - runPhaseAFourLegBooks, - writeOptions - ), - verifyStep( - Actor.SolanaOutpost, - "phase-a-user-payout", - "user's USDCSOL ATA bumps by ~target", - runPhaseAUserPayout, - remitOptions - ) - ) - - // ── 8. Phase B: USDCSOL (SPL) → ETH (native) through the private pair ── - ClusterBuildPhase.create( - cluster, - "PhaseBSolanaToEthereum", - "USDCSOL (SPL) → ETH (native) through the private pair: inverse quote → request → UWREQ race → books → payout" - ).push( - verifyStep( - Actor.User, - "phase-b-quote", - "compute the inverse private-pair quote from the live (post-Phase-A) books", - runPhaseBQuote, - writeOptions - ), - SwapSteps.planRequestSwapSolanaToEthereum( - Actor.User, - "phase-b-request-swap", - "user calls opp_outpost::request_swap_spl sourcing the private USDCSOL reserve", - writeOptions - ), - verifyStep( - Actor.Sysio, - "phase-b-uwreq-created", - "depot creates the PENDING inverse UWREQ (src_amount in the token's depot frame)", - runPhaseBUwreqCreated, - uwreqOptions - ), - verifyStep( - Actor.Underwriter, - "phase-b-uwreq-confirmed", - "UWREQ resolves CONFIRMED with TWO locks (one per leg)", - runPhaseBUwreqConfirmed, - raceOptions - ), - verifyStep( - Actor.Sysio, - "phase-b-four-leg-books", - "emit-time four-leg books move on the two private rows (inverted)", - runPhaseBFourLegBooks, - writeOptions - ), - verifyStep( - Actor.EthereumOutpost, - "phase-b-user-payout", - "user's ETH balance bumps by ~target", - runPhaseBUserPayout, - remitOptions - ) - ) - - // ── 9. WIRE-endpoint exclusion still binds for the owned private pair ── - ClusterBuildPhase.create( - cluster, - "PrivateToWireExcluded", - "A swap sourcing the private ETH reserve toward WIRE draws a SWAP_REVERT and never creates a UWREQ" - ).push( - SwapSteps.planRequestSwapPrivateToWire( - Actor.User, - "request-private-to-wire", - "user requests private-ETH → WIRE (the privacy gate must reject it)", - writeOptions - ), - verifyStep( - Actor.EthereumOutpost, - "swap-revert-circulated", - "a SWAP_REVERT attestation circulates back toward the ETH outpost", - runVerifySwapRevertCirculated, - // Remit-class budget: request → outpost outbound (up to 2 epochs) → - // depot reject → SWAP_REVERT rides the NEXT depot outbound (~4-5 - // epochs total). The 3-epoch uwreq budget missed by seconds - // (2026-07-02: E>D envelope 13 carried the request at t+176s). - remitOptions - ), - verifyStep( - Actor.Sysio, - "no-private-to-wire-uwreq", - `no (src=PRIVATE, dst=WIRE) UWREQ appears within ${Constants.Timing.NoUwreqWindowMs}ms`, - runVerifyNoPrivateToWireUwreq, - noUwreqOptions - ) - ) - - // ── 10. The owner claims what both reserves earned across A and B ── - ClusterBuildPhase.create( - cluster, - "ClaimReserveOwnerFees", - "The reserve owner claims each private reserve's accrued owner fee as real WIRE" - ).push( - verifyStep( - Actor.User, - "snapshot-owner-balance", - "record the owner's WIRE balance before either claim", - runSnapshotOwnerBalance, - writeOptions - ), - Steps.contracts.sysio.reserv.planClaimrsvfee( - Actor.User, - "claim-ethereum-reserve-fee", - "owner claims the private ETH reserve's accrued owner fee", - writeOptions, - { - chain_code: SlugName.toString(Constants.Reserves.Ethereum.ChainCode), - token_code: SlugName.toString(Constants.Reserves.Ethereum.TokenCode), - reserve_code: SlugName.toString(Constants.Reserves.PrivateReserveCode) - }, - Constants.Accounts.Owner - ), - Steps.contracts.sysio.reserv.planClaimrsvfee( - Actor.User, - "claim-solana-reserve-fee", - "owner claims the private SOL reserve's accrued owner fee", - writeOptions, - { - chain_code: SlugName.toString(Constants.Reserves.Solana.ChainCode), - token_code: SlugName.toString(Constants.Reserves.Solana.TokenCode), - reserve_code: SlugName.toString(Constants.Reserves.PrivateReserveCode) - }, - Constants.Accounts.Owner - ), - verifyStep( - Actor.User, - "owner-fees-paid-out", - "both accruals are swept to zero and the owner holds exactly what the pair earned", - runVerifyOwnerFeesClaimed, - writeOptions - ) - ) - } -} - -/** Baseline the owner's WIRE balance before either `claimrsvfee` fires. */ -async function runSnapshotOwnerBalance(ctx: Context): Promise { - ctx.outputs.set( - Outputs.ownerBalanceBeforeClaims, - await ctx.wire.getWireBalance(Constants.Accounts.Owner) - ) -} - -// ── Substrate-health verify runners ───────────────────────────────────────── - -/** Old "WIRE chain is producing blocks" — `get_info` head block is past genesis. */ -async function runVerifyWireChainProducing(ctx: Context): Promise { - const info = await ctx.wire.getInfo() - Assert.ok( - Number(info.head_block_num) > 0, - `WIRE chain is not producing blocks (head_block_num=${info.head_block_num})` - ) -} - -/** Old "uwrit.a becomes ACTIVE (deposits credit)" — poll the operator row. */ -async function runVerifyUnderwriterActive(ctx: Context): Promise { - const label = HarnessConstants.underwriterLabel(0), - account = ctx.keyStore.assertOperator(label).account - await pollUntil( - `${label} (${account}) ACTIVE`, - async () => { - const { rows } = await ctx.wire - .getSysioContract(SysioContractName.opreg) - .tables.operators.query() - const underwriter = rows.find(row => row.account === account) - return ( - underwriter != null && - matchesProtoEnum( - underwriter.status, - SysioOpregOperatorstatus, - SysioOpregOperatorstatus.OPERATOR_STATUS_ACTIVE - ) - ) - }, - Constants.Timing.UwreqDeadlineMs, - Constants.Timing.LongPollIntervalMs - ) -} - -// ── Gated-handshake verify runners ────────────────────────────────────────── - -/** Assert one depot row is ACTIVE with the owner, privacy flag, and EXACT custody. */ -function assertPrivateRowActive( - row: SysioContracts.SysioReservReserveRowType, - label: string, - requestedWireAmount: bigint, - escrowDepotUnits: bigint -): void { - Assert.ok(row != null, `${label}: private depot row not found`) - Assert.ok( - matchesProtoEnum( - row.status, - SysioReservReservestatus, - SysioReservReservestatus.RESERVE_STATUS_ACTIVE - ), - `${label}: expected status ACTIVE, got ${row.status}` - ) - Assert.ok( - row.owner === Constants.Accounts.Owner, - `${label}: expected owner ${Constants.Accounts.Owner}, got ${row.owner}` - ) - Assert.ok( - row.is_private === true || Number(row.is_private) === 1, - `${label}: expected is_private=true, got ${row.is_private}` - ) - // The match escrowed the requested WIRE verbatim; the chain side seeded at - // the escrow's DEPOT-FRAME conversion (`ReserveCreate.external_amount` — - // `toDepot(amount, nativeDecimals)`: ÷1e9 for 18-dec wei, identity for the - // 6-dec USDCSOL escrow). - Assert.strictEqual( - BigInt(row.reserve_wire_amount), - requestedWireAmount, - `${label}: reserve_wire_amount != requested WIRE` - ) - Assert.strictEqual( - BigInt(row.reserve_chain_amount), - escrowDepotUnits, - `${label}: reserve_chain_amount != depot-frame escrow` - ) -} - -/** ETH-side depot row appears PENDING (RESERVE_CREATE relayed). */ -async function runVerifyEthereumDepotRowPending(ctx: Context): Promise { - await pollUntil( - "ETH private depot row status=PENDING", - async () => { - const row = await readPrivateReserveRow( - ctx, - Constants.Reserves.Ethereum.ChainCode, - Constants.Reserves.Ethereum.TokenCode - ) - return ( - row != null && - matchesProtoEnum( - row.status, - SysioReservReservestatus, - SysioReservReservestatus.RESERVE_STATUS_PENDING - ) - ) - }, - Constants.Timing.RelayDeadlineMs, - Constants.Timing.LongPollIntervalMs - ) -} - -/** SOL-side depot row appears PENDING (RESERVE_CREATE relayed). */ -async function runVerifySolanaDepotRowPending(ctx: Context): Promise { - await pollUntil( - "SOL private depot row status=PENDING", - async () => { - const row = await readPrivateReserveRow( - ctx, - Constants.Reserves.Solana.ChainCode, - Constants.Reserves.Solana.TokenCode - ) - return ( - row != null && - matchesProtoEnum( - row.status, - SysioReservReservestatus, - SysioReservReservestatus.RESERVE_STATUS_PENDING - ) - ) - }, - Constants.Timing.RelayDeadlineMs, - Constants.Timing.LongPollIntervalMs - ) -} - -/** ETH depot row ACTIVE + owner + is_private + exact custody (old test #4, ETH half). */ -async function runVerifyEthereumDepotRowActive(ctx: Context): Promise { - assertPrivateRowActive( - await readPrivateReserveRow( - ctx, - Constants.Reserves.Ethereum.ChainCode, - Constants.Reserves.Ethereum.TokenCode - ), - "ETH private reserve", - Constants.CreateParams.EthereumRequestedWire, - Constants.CreateParams.EthereumEscrowDepotUnits - ) -} - -/** SOL depot row ACTIVE + owner + is_private + exact custody (old test #4, SOL half). */ -async function runVerifySolanaDepotRowActive(ctx: Context): Promise { - assertPrivateRowActive( - await readPrivateReserveRow( - ctx, - Constants.Reserves.Solana.ChainCode, - Constants.Reserves.Solana.TokenCode - ), - "SOL private reserve", - Constants.CreateParams.SolanaRequestedWire, - Constants.CreateParams.SolanaEscrowDepotUnits - ) -} - -/** RESERVE_READY landed on the ETH outpost (local record ACTIVE). */ -async function runVerifyEthereumLocalReserveActive( - ctx: Context -): Promise { - await pollUntil( - "ETH outpost-local private record ACTIVE", - () => ReserveSteps.readEthereumLocalReserveActive(ctx), - Constants.Timing.ReadyDeadlineMs, - Constants.Timing.LongPollIntervalMs - ) - log.info("[PrivatePair] ETH private reserve ACTIVE end-to-end") -} - -/** RESERVE_READY landed on the SOL outpost (Reserve PDA `Active`). */ -async function runVerifySolanaLocalReserveActive(ctx: Context): Promise { - await pollUntil( - "SOL outpost-local private record ACTIVE", - () => ReserveSteps.readSolanaLocalReserveActive(ctx), - Constants.Timing.ReadyDeadlineMs, - Constants.Timing.LongPollIntervalMs - ) - log.info("[PrivatePair] SOL private reserve ACTIVE end-to-end") -} - -// ── Phase A verify runners ────────────────────────────────────────────────── - -/** - * Mirror the depot's `swap_quote` / `applyswap` math exactly from the live - * pre-swap rows: `w = cp(src.chain, src.wire, amount)` gross, then - * `target = cp(dst.wire, dst.chain, split_wire_fee(w).net)`. The fee is charged - * between the hops — the source gives up the gross `w`, the destination - * receives only the net and pays the curve output for THAT. Same integers in == - * same integers out, so the variance check sees no drift at all and the books - * assertions below can demand exact equality. - */ -async function runPhaseAQuote(ctx: Context): Promise { - const ethereumBook = await ctx.reserveBook( - Constants.Reserves.Ethereum.ChainCode, - Constants.Reserves.Ethereum.TokenCode, - Constants.Reserves.PrivateReserveCode - ), - solanaBook = await ctx.reserveBook( - Constants.Reserves.Solana.ChainCode, - Constants.Reserves.Solana.TokenCode, - Constants.Reserves.PrivateReserveCode - ), - wireIntermediate = WireReserveTool.tokenToWire( - ethereumBook.chain, - ethereumBook.wire, - ethereumBook.connectorWeightBps, - Constants.SwapAmounts.PhaseASourceDepotUnits - ), - target = WireReserveTool.quoteSwap( - ethereumBook, - solanaBook, - Constants.SwapAmounts.PhaseASourceDepotUnits, - await WireReserveTool.readFeeBps(ctx.wire) - ) - Assert.ok(wireIntermediate > 0n, "PhaseA: WIRE intermediate must be positive") - Assert.ok(target > 0n, "PhaseA: target must be positive") - ctx.outputs - .set(Outputs.phaseABooksBefore, { src: ethereumBook, dst: solanaBook }) - .set(Outputs.phaseAWireIntermediate, wireIntermediate) - .set(Outputs.phaseATarget, target) - .set(Outputs.phaseAUserAtaBefore, await readUserUsdcSolBalance(ctx)) - log.info(`[PhaseA] w=${wireIntermediate} target=${target} (depot units)`) -} - -/** The PENDING private-pair UWREQ appears with the expected destination + source amount. */ -async function runPhaseAUwreqCreated(ctx: Context): Promise { - await pollUntil( - "PhaseA private-pair UWREQ row appears", - async () => - (await readPrivatePairUwreq( - ctx, - Constants.Reserves.Ethereum.ChainCode, - Constants.Reserves.Solana.ChainCode - )) != null, - Constants.Timing.UwreqDeadlineMs, - Constants.Timing.LongPollIntervalMs - ) - const row = await readPrivatePairUwreq( - ctx, - Constants.Reserves.Ethereum.ChainCode, - Constants.Reserves.Solana.ChainCode - ) - // Same-owner pairing admitted: the privacy gate let the request through to - // a real UWREQ instead of a SWAP_REVERT. - Assert.strictEqual( - slugValue(row.dst_token_code), - Constants.Reserves.Solana.TokenCode, - "PhaseA: UWREQ dst_token_code != USDCSOL" - ) - Assert.strictEqual( - BigInt(row.src_amount), - Constants.SwapAmounts.PhaseASourceDepotUnits, - "PhaseA: UWREQ src_amount != depot-frame source" - ) -} - -/** The UWREQ resolves CONFIRMED backed by exactly TWO locks (one per leg). */ -async function runPhaseAUwreqConfirmed(ctx: Context): Promise { - await pollUntil( - "PhaseA UWREQ status=CONFIRMED", - async () => - uwreqConfirmed( - await readPrivatePairUwreq( - ctx, - Constants.Reserves.Ethereum.ChainCode, - Constants.Reserves.Solana.ChainCode - ) - ), - Constants.Timing.RaceDeadlineMs, - Constants.Timing.LongPollIntervalMs - ) - const row = await readPrivatePairUwreq( - ctx, - Constants.Reserves.Ethereum.ChainCode, - Constants.Reserves.Solana.ChainCode - ) - await assertTwoLegLocks(ctx, Number(row.id), "PhaseA") -} - -/** Exactly two locks — one per swap leg chain. */ -async function assertTwoLegLocks( - ctx: Context, - uwreqId: number, - label: string -): Promise { - const locks = await ctx.locksForUwreq(uwreqId) - Assert.strictEqual( - locks.length, - 2, - `${label}: expected TWO locks (one per leg)` - ) - const lockChains = locks - .map(lock => slugValue(lock.chain_code)) - .sort((a, b) => a - b), - expectedChains = [ - Constants.Reserves.Ethereum.ChainCode, - Constants.Reserves.Solana.ChainCode - ].sort((a, b) => a - b) - Assert.deepStrictEqual( - lockChains, - expectedChains, - `${label}: lock chains != [ETHEREUM, SOLANA]` - ) -} - -/** - * `applyswap` fires inline with the race win: src.chain += src, src.wire -= w - * (gross), dst.wire += net, dst.chain -= target. The WIRE-leg fee is skimmed - * inside the hop, so the destination gains the post-fee net and the pair's - * Σwire drops by exactly the fee (no longer conserved). - */ -async function runPhaseAFourLegBooks(ctx: Context): Promise { - const booksBefore = ctx.outputs.assert(Outputs.phaseABooksBefore), - wireIntermediate = ctx.outputs.assert(Outputs.phaseAWireIntermediate), - target = ctx.outputs.assert(Outputs.phaseATarget), - // THREE fees ride this leg: the network fee plus BOTH reserves' owner fees - // (WIRE-281 — "per reserve"). `.fee` is their total, which is what leaves - // the pair's Σwire. Phase A runs ETH → SOL, so ETH is the source side. - split = WireReserveTool.splitWireFee( - wireIntermediate, - await WireReserveTool.readFeeBps(ctx.wire), - WireReserveTool.FeeUnderwriterShareBps, - WireReserveTool.FeeEmissionsShareBps, - Constants.ReserveOwnerFees.EthereumBps, - Constants.ReserveOwnerFees.SolanaBps - ), - fee = split.fee, - ethereumBook = await ctx.reserveBook( - Constants.Reserves.Ethereum.ChainCode, - Constants.Reserves.Ethereum.TokenCode, - Constants.Reserves.PrivateReserveCode - ), - solanaBook = await ctx.reserveBook( - Constants.Reserves.Solana.ChainCode, - Constants.Reserves.Solana.TokenCode, - Constants.Reserves.PrivateReserveCode - ) - Assert.strictEqual( - ethereumBook.chain, - booksBefore.src.chain + Constants.SwapAmounts.PhaseASourceDepotUnits, - "PhaseA books: src.chain must gain the source deposit" - ) - Assert.strictEqual( - ethereumBook.wire, - booksBefore.src.wire - wireIntermediate, - "PhaseA books: src.wire must give up the gross WIRE intermediate" - ) - Assert.strictEqual( - solanaBook.wire, - booksBefore.dst.wire + wireIntermediate - fee, - "PhaseA books: dst.wire must gain the post-fee net" - ) - Assert.strictEqual( - solanaBook.chain, - booksBefore.dst.chain - target, - "PhaseA books: dst.chain must give up the target" - ) - // Σ reserve_wire_amount over the pair drops by the WIRE-leg fee. - Assert.strictEqual( - ethereumBook.wire + solanaBook.wire, - booksBefore.src.wire + booksBefore.dst.wire - fee, - "PhaseA books: Σwire must drop by exactly the WIRE-leg fee" - ) - - // Per-reserve attribution: BOTH reserves earned their own owner fee off the - // same leg, so the single owner is paid twice for one swap. The amounts leave - // the reserve BOOKS (asserted above) and land as claimable accruals. - const ethereumFee = await ctx.reserveOwnerFee( - Constants.Reserves.Ethereum.ChainCode, - Constants.Reserves.Ethereum.TokenCode, - Constants.Reserves.PrivateReserveCode - ), - solanaFee = await ctx.reserveOwnerFee( - Constants.Reserves.Solana.ChainCode, - Constants.Reserves.Solana.TokenCode, - Constants.Reserves.PrivateReserveCode - ) - Assert.ok( - split.srcReserveShare > 0n && split.dstReserveShare > 0n, - "PhaseA fees: both reserve owner fees must be non-zero at these amounts" - ) - Assert.strictEqual( - ethereumFee.accrued, - split.srcReserveShare, - "PhaseA fees: the source (ETH) reserve accrued its own owner fee" - ) - Assert.strictEqual( - solanaFee.accrued, - split.dstReserveShare, - "PhaseA fees: the destination (SOL) reserve accrued its own owner fee" - ) - Assert.strictEqual( - ethereumFee.lifetime, - ethereumFee.accrued, - "PhaseA fees: nothing claimed yet, so lifetime equals the accrual" - ) - Assert.strictEqual( - solanaFee.lifetime, - solanaFee.accrued, - "PhaseA fees: nothing claimed yet, so lifetime equals the accrual" - ) -} - -/** - * Both private reserves carry the owner's rate before any swap runs, and - * neither has earned anything yet — the baseline the Phase A accrual assertions - * are measured against. - */ -async function runVerifyReserveFeesSet(ctx: Context): Promise { - const ethereum = await ctx.reserveOwnerFee( - Constants.Reserves.Ethereum.ChainCode, - Constants.Reserves.Ethereum.TokenCode, - Constants.Reserves.PrivateReserveCode - ), - solana = await ctx.reserveOwnerFee( - Constants.Reserves.Solana.ChainCode, - Constants.Reserves.Solana.TokenCode, - Constants.Reserves.PrivateReserveCode - ) - Assert.strictEqual( - ethereum.feeBps, - Constants.ReserveOwnerFees.EthereumBps, - "the ETH private reserve must carry the owner's configured fee" - ) - Assert.strictEqual( - solana.feeBps, - Constants.ReserveOwnerFees.SolanaBps, - "the SOL private reserve must carry the owner's configured fee" - ) - Assert.strictEqual(ethereum.owner, Constants.Accounts.Owner, "ETH reserve owner") - Assert.strictEqual(solana.owner, Constants.Accounts.Owner, "SOL reserve owner") - Assert.strictEqual(ethereum.accrued, 0n, "no ETH reserve fee earned before any swap") - Assert.strictEqual(solana.accrued, 0n, "no SOL reserve fee earned before any swap") -} - -/** - * The owner sweeps both reserves' accrued fees into their own WIRE balance — - * the payout half of WIRE-281. Each claim zeroes that reserve's accrual while - * its lifetime audit total survives. - */ -async function runVerifyOwnerFeesClaimed(ctx: Context): Promise { - const ethereum = await ctx.reserveOwnerFee( - Constants.Reserves.Ethereum.ChainCode, - Constants.Reserves.Ethereum.TokenCode, - Constants.Reserves.PrivateReserveCode - ), - solana = await ctx.reserveOwnerFee( - Constants.Reserves.Solana.ChainCode, - Constants.Reserves.Solana.TokenCode, - Constants.Reserves.PrivateReserveCode - ), - claimed = ctx.outputs.assert(Outputs.ownerBalanceBeforeClaims) - Assert.strictEqual(ethereum.accrued, 0n, "the ETH reserve's accrual is swept to zero") - Assert.strictEqual(solana.accrued, 0n, "the SOL reserve's accrual is swept to zero") - Assert.ok( - ethereum.lifetime > 0n && solana.lifetime > 0n, - "each reserve's lifetime audit total survives the claim" - ) - // Both claims paid REAL WIRE to the owner. - const balance = await ctx.wire.getWireBalance(Constants.Accounts.Owner) - Assert.strictEqual( - balance, - claimed + ethereum.lifetime + solana.lifetime, - "the owner received exactly both reserves' earned fees" - ) -} - -/** - * The target rides the attestation in USDCSOL's depot frame — its native - * 6-dec units (at/below the per-token cap) — so the SOL outpost pays the - * user's ATA `from_depot(target, 6)` = target, identity. - */ -async function runPhaseAUserPayout(ctx: Context): Promise { - const target = ctx.outputs.assert(Outputs.phaseATarget), - balanceBefore = ctx.outputs.assert(Outputs.phaseAUserAtaBefore), - drift = WireReserveTool.varianceDrift( - target, - Constants.Variance.ToleranceBps - ), - floor = - balanceBefore + - WireReserveTool.fromDepot( - target - drift, - Constants.SwapAmounts.UsdcSolDecimals - ) - await pollUntil( - "PhaseA user USDCSOL ATA bump", - async () => (await readUserUsdcSolBalance(ctx)) >= floor, - Constants.Timing.RemitDeadlineMs, - Constants.Timing.LongPollIntervalMs - ) - const balanceAfter = await readUserUsdcSolBalance(ctx) - log.info( - `[PhaseA] user received ${balanceAfter - balanceBefore} USDCSOL base units` - ) - Assert.ok( - balanceAfter - balanceBefore > 0n, - "PhaseA: user's USDCSOL ATA must increase" - ) -} - -// ── Phase B verify runners ────────────────────────────────────────────────── - -/** - * Same two-hop math, inverted: the SOL reserve is the source leg. Rows are - * read LIVE so Phase A's book movement is the new baseline. - */ -async function runPhaseBQuote(ctx: Context): Promise { - const swapUser = ctx.outputs.assert(swapUserOutputKey()), - ethereumBook = await ctx.reserveBook( - Constants.Reserves.Ethereum.ChainCode, - Constants.Reserves.Ethereum.TokenCode, - Constants.Reserves.PrivateReserveCode - ), - solanaBook = await ctx.reserveBook( - Constants.Reserves.Solana.ChainCode, - Constants.Reserves.Solana.TokenCode, - Constants.Reserves.PrivateReserveCode - ), - wireIntermediate = WireReserveTool.tokenToWire( - solanaBook.chain, - solanaBook.wire, - solanaBook.connectorWeightBps, - Constants.SwapAmounts.PhaseBSourceDepotUnits - ), - target = WireReserveTool.quoteSwap( - solanaBook, - ethereumBook, - Constants.SwapAmounts.PhaseBSourceDepotUnits, - await WireReserveTool.readFeeBps(ctx.wire) - ) - Assert.ok(wireIntermediate > 0n, "PhaseB: WIRE intermediate must be positive") - Assert.ok(target > 0n, "PhaseB: target must be positive") - ctx.outputs - .set(Outputs.phaseBBooksBefore, { src: solanaBook, dst: ethereumBook }) - .set(Outputs.phaseBWireIntermediate, wireIntermediate) - .set(Outputs.phaseBTarget, target) - .set( - Outputs.phaseBEthereumBalanceBefore, - await ctx.ethereum.getBalance(swapUser.ethereumWallet.address) - ) - log.info(`[PhaseB] w=${wireIntermediate} target=${target} (depot units)`) -} - -/** The PENDING inverse UWREQ appears carrying the depot-frame source amount. */ -async function runPhaseBUwreqCreated(ctx: Context): Promise { - await pollUntil( - "PhaseB private-pair UWREQ row appears", - async () => - (await readPrivatePairUwreq( - ctx, - Constants.Reserves.Solana.ChainCode, - Constants.Reserves.Ethereum.ChainCode - )) != null, - Constants.Timing.UwreqDeadlineMs, - Constants.Timing.LongPollIntervalMs - ) - const row = await readPrivatePairUwreq( - ctx, - Constants.Reserves.Solana.ChainCode, - Constants.Reserves.Ethereum.ChainCode - ) - // `request_swap_spl` rescales the 6-dec source into the depot frame before - // the attestation, so the UWREQ carries the depot units. - Assert.strictEqual( - BigInt(row.src_amount), - Constants.SwapAmounts.PhaseBSourceDepotUnits, - "PhaseB: UWREQ src_amount != depot-frame source" - ) -} - -/** The inverse UWREQ resolves CONFIRMED backed by exactly TWO locks. */ -async function runPhaseBUwreqConfirmed(ctx: Context): Promise { - await pollUntil( - "PhaseB UWREQ status=CONFIRMED", - async () => - uwreqConfirmed( - await readPrivatePairUwreq( - ctx, - Constants.Reserves.Solana.ChainCode, - Constants.Reserves.Ethereum.ChainCode - ) - ), - Constants.Timing.RaceDeadlineMs, - Constants.Timing.LongPollIntervalMs - ) - const row = await readPrivatePairUwreq( - ctx, - Constants.Reserves.Solana.ChainCode, - Constants.Reserves.Ethereum.ChainCode - ) - await assertTwoLegLocks(ctx, Number(row.id), "PhaseB") -} - -/** - * Inverted hop: SOL is the source (gives up gross wireIntermediate), ETH the - * destination (gains the post-fee net); Σwire drops by the fee. - */ -async function runPhaseBFourLegBooks(ctx: Context): Promise { - const booksBefore = ctx.outputs.assert(Outputs.phaseBBooksBefore), - wireIntermediate = ctx.outputs.assert(Outputs.phaseBWireIntermediate), - target = ctx.outputs.assert(Outputs.phaseBTarget), - // Phase B is the INVERSE direction (SOL → ETH), so the SOL reserve is the - // source side and the ETH reserve the destination — the same two rates, - // swapped into their Phase B roles. - fee = WireReserveTool.splitWireFee( - wireIntermediate, - await WireReserveTool.readFeeBps(ctx.wire), - WireReserveTool.FeeUnderwriterShareBps, - WireReserveTool.FeeEmissionsShareBps, - Constants.ReserveOwnerFees.SolanaBps, - Constants.ReserveOwnerFees.EthereumBps - ).fee, - ethereumBook = await ctx.reserveBook( - Constants.Reserves.Ethereum.ChainCode, - Constants.Reserves.Ethereum.TokenCode, - Constants.Reserves.PrivateReserveCode - ), - solanaBook = await ctx.reserveBook( - Constants.Reserves.Solana.ChainCode, - Constants.Reserves.Solana.TokenCode, - Constants.Reserves.PrivateReserveCode - ) - Assert.strictEqual( - solanaBook.chain, - booksBefore.src.chain + Constants.SwapAmounts.PhaseBSourceDepotUnits, - "PhaseB books: src.chain must gain the source deposit" - ) - Assert.strictEqual( - solanaBook.wire, - booksBefore.src.wire - wireIntermediate, - "PhaseB books: src.wire must give up the gross WIRE intermediate" - ) - Assert.strictEqual( - ethereumBook.wire, - booksBefore.dst.wire + wireIntermediate - fee, - "PhaseB books: dst.wire must gain the post-fee net" - ) - Assert.strictEqual( - ethereumBook.chain, - booksBefore.dst.chain - target, - "PhaseB books: dst.chain must give up the target" - ) - Assert.strictEqual( - ethereumBook.wire + solanaBook.wire, - booksBefore.src.wire + booksBefore.dst.wire - fee, - "PhaseB books: Σwire must drop by exactly the WIRE-leg fee" - ) -} - -/** - * target is depot 9-dec; native ETH is 18-dec so the ETH outpost pays - * `fromDepot(target, 18)` wei (×1e9) from its custody balance. - */ -async function runPhaseBUserPayout(ctx: Context): Promise { - const swapUser = ctx.outputs.assert(swapUserOutputKey()), - target = ctx.outputs.assert(Outputs.phaseBTarget), - balanceBefore = ctx.outputs.assert(Outputs.phaseBEthereumBalanceBefore), - targetWei = WireReserveTool.fromDepot( - target, - Constants.SwapAmounts.EthereumNativeDecimals - ), - driftWei = WireReserveTool.varianceDrift( - targetWei, - Constants.Variance.ToleranceBps - ), - floor = balanceBefore + (targetWei - driftWei) - await pollUntil( - "PhaseB user receives ETH", - async () => - (await ctx.ethereum.getBalance(swapUser.ethereumWallet.address)) >= floor, - Constants.Timing.RemitDeadlineMs, - Constants.Timing.LongPollIntervalMs - ) - const balanceAfter = await ctx.ethereum.getBalance( - swapUser.ethereumWallet.address - ) - log.info( - `[PhaseB] user received ${balanceAfter - balanceBefore} wei (targetWei=${targetWei})` - ) - Assert.ok( - balanceAfter - balanceBefore > 0n, - "PhaseB: user's ETH balance must increase" - ) -} - -// ── Private→WIRE exclusion verify runners ─────────────────────────────────── - -/** - * The privacy gate's WIRE-endpoint branch rejects the probe on the depot and - * routes a SWAP_REVERT back toward the requesting (ETH) outpost — scan the - * cluster's opp-debugging artifacts for the attestation-type byte tag. - */ -async function runVerifySwapRevertCirculated(ctx: Context): Promise { - const oppDebuggingDirectory = oppDebuggingPath(ctx.config.clusterPath) - await pollUntil( - "SWAP_REVERT attestation circulated toward the ETH outpost", - async () => containsSwapRevert(oppDebuggingDirectory), - // Remit-class round trip (see the step registration) — NOT the 3-epoch - // uwreq budget. - Constants.Timing.RemitDeadlineMs, - Constants.Timing.LongPollIntervalMs - ) -} - -/** - * Inverted poll: the forbidden (src=PRIVATE, dst=WIRE) UWREQ must NEVER - * appear — `pollUntil` throwing its deadline error IS the pass. The privacy - * gate's WIRE-endpoint branch fires before the variance check ever sees the - * sentinel target. (The Phase A uwreq — dst=SOLANA — is excluded by the - * destination filter.) - */ -async function runVerifyNoPrivateToWireUwreq(ctx: Context): Promise { - await pollUntil( - "forbidden private→WIRE UWREQ", - async () => - (await readPrivatePairUwreq( - ctx, - Constants.Reserves.Ethereum.ChainCode, - Constants.Reserves.Wire.ChainCode - )) != null, - Constants.Timing.NoUwreqWindowMs, - Constants.Timing.LongPollIntervalMs - ).then( - () => { - throw new Error( - "forbidden private→WIRE UWREQ was created — the privacy gate failed to exclude the WIRE endpoint" - ) - }, - (error: unknown) => { - const message = error instanceof Error ? error.message : String(error) - Assert.ok( - message.includes("Timed out"), - `no-uwreq window failed for an unexpected reason: ${message}` - ) - } - ) -} diff --git a/packages/flow-swap-private-reserves/src/SwapPrivateReservesScenarioArtifacts.ts b/packages/flow-swap-private-reserves/src/SwapPrivateReservesScenarioArtifacts.ts deleted file mode 100644 index 541570644..000000000 --- a/packages/flow-swap-private-reserves/src/SwapPrivateReservesScenarioArtifacts.ts +++ /dev/null @@ -1,114 +0,0 @@ -import Assert from "node:assert" -import Fs from "node:fs" -import Path from "node:path" -import { ethers } from "ethers" -import { PublicKey } from "@solana/web3.js" -import { - contractView, - EthereumCollateralTool, - SolanaOutpostBootstrapper, - type ClusterBuildContext, - type EthereumLocalReserveRecord, - type EthereumPayableOverrides, - ClusterConfigProvider -} from "@wireio/cluster-tool" -import { SwapPrivateReservesScenarioConstants as Constants } from "./SwapPrivateReservesScenarioConstants.js" - -/** - * Deploy-artifact resolution shared by the flow's step runners and verify - * steps — pure VALUE helpers (reads), never steps themselves: the - * `ReserveManager` contract binding (address from `outpost-addrs.json`, ABI - * from the hardhat artifact) and the USDCSOL mock mint persisted by the - * Solana outpost bootstrap. - */ -export namespace SwapPrivateReservesScenarioArtifacts { - /** The `ReserveManager` entry name in `outpost-addrs.json` / the artifact tree. */ - export const ReserveManagerContractName = "ReserveManager" - - /** - * Structural surface of the `ReserveManager` members this flow binds beyond - * the harness's swap surface: the payable native `create_reserve` write and - * the `getReserve` local-record read. (Following the harness's - * `OperatorRegistryContract` precedent — typechain types live in - * `wire-ethereum` and are not consumable here.) - */ - export interface ReserveManagerPrivateReserveContract - extends ethers.BaseContract { - create_reserve: ( - tokenCode: bigint, - reserveCode: bigint, - externalTokenAmount: bigint, - requestedWireAmount: bigint, - connectorWeightBps: number, - name: string, - description: string, - isPrivate: boolean, - creatorPubKey: string, - overrides: EthereumPayableOverrides - ) => Promise - getReserve: ( - tokenCode: bigint, - reserveCode: bigint - ) => Promise - } - - /** - * Bind the deployed `ReserveManager` to `signer` — address from - * the cluster deployments dir (`ClusterConfigProvider.ethereumDeploymentsPath`), ABI from the - * hardhat artifact (both via the harness's exported artifact readers). - * - * @param ctx - The build context (carries `config.ethereumPath`). - * @param signer - The wallet the writes are signed by. - * @returns The signer-connected contract. - */ - export function loadReserveManager< - View extends object, - C extends ClusterBuildContext = ClusterBuildContext - >(ctx: C, signer: ethers.Signer): View & ethers.BaseContract { - const address = EthereumCollateralTool.loadOutpostAddresses( - ClusterConfigProvider.ethereumDeploymentsPath(ctx.config) - )[ReserveManagerContractName] - Assert.ok( - address != null && /^0x[0-9a-fA-F]{40}$/.test(address), - `SwapPrivateReservesScenario: ${ReserveManagerContractName} not in outpost-addrs.json (got ${address})` - ) - const abi = EthereumCollateralTool.loadOutpostAbi( - ctx.config.ethereumPath, - ReserveManagerContractName - ) - return contractView(address, abi, signer) - } - - /** - * The USDCSOL mock mint persisted by the Solana outpost bootstrap - * (`/sol-mock-mints.json`, rows of - * {@link SolanaOutpostBootstrapper.PersistedSplMint}). - * - * @param ctx - The build context (carries `config.dataPath`). - * @returns The mint pubkey. - * @throws When the manifest or the USDCSOL entry is missing. - */ - export function loadUsdcSolMint( - ctx: C - ): PublicKey { - const mintsFile = Path.join( - ctx.config.dataPath, - Constants.SolanaMockMintsFilename - ) - Assert.ok( - Fs.existsSync(mintsFile), - `SwapPrivateReservesScenario: mock SPL mints not found at ${mintsFile}` - ) - const mints = JSON.parse( - Fs.readFileSync(mintsFile, "utf8") - ) as SolanaOutpostBootstrapper.PersistedSplMint[] - const usdcSolEntry = mints.find( - entry => entry.code === Constants.Reserves.Solana.TokenCode - ) - Assert.ok( - usdcSolEntry != null, - "SwapPrivateReservesScenario: bootstrap did not persist the USDCSOL SPL mint" - ) - return new PublicKey(usdcSolEntry.mint) - } -} diff --git a/packages/flow-swap-private-reserves/src/SwapPrivateReservesScenarioConstants.ts b/packages/flow-swap-private-reserves/src/SwapPrivateReservesScenarioConstants.ts deleted file mode 100644 index 29eebe424..000000000 --- a/packages/flow-swap-private-reserves/src/SwapPrivateReservesScenarioConstants.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { SlugName } from "@wireio/sdk-core" -import { ProtocolTiming, WireReserveTool } from "@wireio/cluster-tool" - -/** - * Constants for the private-reserve swap flow: the same-owner PRIVATE reserve - * pair sizing, the per-phase ~1% swap draws, and the private→WIRE - * exclusion-probe parameters. Protocol waits derive from the - * {@link ProtocolTiming} envelope. - */ -export namespace SwapPrivateReservesScenarioConstants { - /** - * Timing budgets — protocol waits pinned to their {@link ProtocolTiming} - * class. Each poll deadline is also the enclosing step's timeout minus - * {@link Timing.PollDeadlineBufferMs}. - */ - export namespace Timing { - /** Epoch duration (s) — the `sysio.epoch::setconfig` floor is 60. */ - export const EpochDurationSec = 60 - /** Outpost RESERVE_CREATE → depot PENDING row (per chain) — a single hop. */ - export const RelayDeadlineMs = ProtocolTiming.SingleHopBudgetMs - /** RESERVE_READY → outpost-local record ACTIVE — a single depot→outpost hop. */ - export const ReadyDeadlineMs = ProtocolTiming.SingleHopBudgetMs - /** - * SWAP_REQUEST relay + UWREQ insert (also the uwrit.a ACTIVE budget) — a - * single outpost→depot hop: the request rides the outpost's NEXT outbound - * emit (0–2 min of phase alignment) + delivery + the depot's createuwreq. - */ - export const UwreqDeadlineMs = ProtocolTiming.SingleHopBudgetMs - /** Two-leg underwriter race resolution — a single hop back to the depot. */ - export const RaceDeadlineMs = ProtocolTiming.SingleHopBudgetMs - /** SWAP_REMIT delivery + destination payout — the tail of the full - * outpost→depot→outpost path, budgeted as the double hop. */ - export const RemitDeadlineMs = ProtocolTiming.DoubleHopBudgetMs - /** Window in which the forbidden private→WIRE UWREQ must NOT appear. */ - export const NoUwreqWindowMs = 90_000 - /** Interval for long-running chain-state polls (ms). */ - export const LongPollIntervalMs = 3_000 - /** Buffer added on top of each poll deadline for the enclosing step timeout (ms). */ - export const PollDeadlineBufferMs = 30_000 - } - - /** - * The same-owner PRIVATE reserve pair this flow creates via the real gated - * handshake (outpost create → depot PENDING → matchreserve → ACTIVE), plus - * the WIRE endpoint identity for the exclusion probe. - */ - export namespace Reserves { - /** Shared reserve discriminator for both private reserves. */ - export const PrivateReserveCode = SlugName.from("PRIVATE") - - export namespace Ethereum { - export const ChainCode = SlugName.from("ETHEREUM") - /** Native ETH — the pair's native side. */ - export const TokenCode = SlugName.from("ETH") - } - export namespace Solana { - export const ChainCode = SlugName.from("SOLANA") - /** USDC-on-SOL mock SPL (6 decimals) — the pair's non-native side. */ - export const TokenCode = SlugName.from("USDCSOL") - /** Native SOL — only used for underwriter bond configuration. */ - export const NativeTokenCode = SlugName.from("SOL") - } - export namespace Wire { - export const ChainCode = SlugName.from("WIRE") - export const TokenCode = SlugName.from("WIRE") - /** - * Non-zero sentinel the outpost requires on a to-WIRE SwapRequest; - * the depot never quotes or debits a WIRE-side reserve. - */ - export const SentinelReserveCode = SlugName.from("PRIMARY") - } - } - - /** - * create_reserve parameters for the private pair. - * - * Sizing note: a token's depot precision is `min(nativeDecimals, 9)` — - * each outpost converts at its boundary (`PrecisionLib.toDepot` on ETH - * downscales 18-dec wei ÷1e9; 6-dec USDCSOL is at the cap or below, so - * `to_depot` on SOL is identity) before stamping the RESERVE_CREATE - * attestation's `ReserveAmount`. The ETH escrow lands as 1e10 depot units - * and the USDCSOL escrow as 1e7 (its native 6-dec base units); each seeds - * against 1e10 matched WIRE, and the ~1% Phase draws below keep the - * constant-product math well-conditioned on both books. - */ - export namespace CreateParams { - /** ETH escrow — 10 ETH in wei; `toDepot(·, 18)` → 1e10 depot units. */ - export const EthereumEscrowWei = 10_000_000_000_000_000_000n - /** Depot-frame seed the ETH escrow lands as on the depot row. */ - export const EthereumEscrowDepotUnits = WireReserveTool.toDepot(EthereumEscrowWei, 18) - /** WIRE (raw 9-dp) the owner matches against the ETH reserve. */ - export const EthereumRequestedWire = 10_000_000_000n - /** USDCSOL escrow — 10 USDC in 6-dec units; at/below the cap → identity. */ - export const SolanaEscrowChainUnits = 10_000_000n - /** Depot-frame seed the USDCSOL escrow lands as on the depot row (1e7). */ - export const SolanaEscrowDepotUnits = WireReserveTool.toDepot(SolanaEscrowChainUnits, 6) - /** WIRE (raw 9-dp) the owner matches against the SOL reserve. */ - export const SolanaRequestedWire = 10_000_000_000n - /** 50% Bancor connector weight = pure constant product. */ - export const ConnectorWeightBps = 5000 - /** Display metadata forwarded verbatim to the depot rows. */ - export const EthereumName = "ETHEREUM-ETH/WIRE private reserve" - export const EthereumDescription = - "flow-swap-private-reserves ETH-side private reserve" - export const SolanaName = "SOLANA-USDCSOL/WIRE private reserve" - export const SolanaDescription = - "flow-swap-private-reserves SOL-side private reserve" - } - - /** - * Per-phase swap source amounts. Each phase draws ~1% of its source - * reserve (Phase A: 1e8 of the 1e10-depot-unit ETH seed; Phase B: 1e5 of - * the 1e7-unit USDCSOL seed) so slippage stays well inside the tolerance. - */ - export namespace SwapAmounts { - /** Phase A: 0.1 ETH in wei; `toDepot(·, 18)` → 1e8 depot units. */ - export const PhaseASourceWei = 100_000_000_000_000_000n - export const PhaseASourceDepotUnits = WireReserveTool.toDepot(PhaseASourceWei, 18) - /** Phase B: 0.1 USDCSOL in 6-dec chain units; at/below the cap → identity (1e5). */ - export const PhaseBSourceSplUnits = 100_000n - export const PhaseBSourceDepotUnits = WireReserveTool.toDepot(PhaseBSourceSplUnits, 6) - /** USDCSOL chain-native decimals — at/below the cap, so `fromDepot(target, 6)` is identity. */ - export const UsdcSolDecimals = 6 - /** Native-ETH chain decimals — payouts convert `fromDepot(target, 18)` (×1e9). */ - export const EthereumNativeDecimals = 18 - } - - export namespace Variance { - /** 5% — generous so quote drift between poll and race never reverts. */ - export const ToleranceBps = 500 - } - - /** - * The owner fee each private reserve charges on its own WIRE leg — the - * per-reserve fee that is INDEPENDENT of the network `fee_bps` (WIRE-281). - * - * Because one account owns both reserves, a swap through the pair charges - * BOTH rates on the same leg, so the owner earns twice from a single swap — - * exactly the "per reserve" attribution under test. Two DIFFERENT rates are - * used so a bug that swaps or double-counts a side can't hide behind equal - * numbers. Both sit well inside the contract's `[1, 9900]` band. - */ - export namespace ReserveOwnerFees { - /** ETH-side private reserve: 1%. */ - export const EthereumBps = 100 - /** SOL-side private reserve: 2%. */ - export const SolanaBps = 200 - } - - /** WIRE accounts provisioned by this flow. */ - export namespace Accounts { - /** The single WIRE account that matches (and therefore owns) BOTH - * private reserves — the pair's same-owner predicate. */ - export const Owner = "privowner" - /** 2× the two requested WIRE amounts so both matches clear with slack. */ - export const OwnerFunding = - 2n * - (CreateParams.EthereumRequestedWire + CreateParams.SolanaRequestedWire) - } - - /** SPL funding for the SOL-side create + Phase B source custody. */ - export namespace SplFunding { - /** Escrow + Phase B source + generous headroom on the creator's ATA. */ - export const CreatorMintAmount = CreateParams.SolanaEscrowChainUnits * 2n - } - - /** Parameters for the private→WIRE exclusion probe (final phase). */ - export namespace WireProbe { - /** 0.01 ETH source deposit (refunded by the SWAP_REVERT). */ - export const SourceEthereumWei = 10_000_000_000_000_000n - /** Positive sentinel — the privacy gate precedes the variance check. */ - export const TargetAmount = 1n - export const ToleranceBps = 500 - /** 8-char lowercase WIRE recipient name riding the request as bytes. */ - export const RecipientName = "privrcpt" - } - - /** - * Underwriter provisioning: the depot's `createuwreq` re-checks - * `meets_role_min` for BOTH legs of every swap, and the underwriter - * plugin's `select_coverable` requires a non-zero credit-line bucket per - * (chain, token) — so the underwriter must bond on every leg this flow's - * matrix touches: native ETH, native SOL, and the non-native USDCSOL leg. - */ - export namespace Underwriting { - /** Per-(chain, token) `req_uw_collat` minimum bond (raw base units). */ - export const MinimumBond = 1_000_000_000 - /** Collateral bonded per leg by the underwriter (raw base units). */ - export const CollateralAmount = 1_000_000_000n - } - - /** Mock-SPL-mint manifest the Solana outpost bootstrap persists in the cluster data dir. */ - export const SolanaMockMintsFilename = "sol-mock-mints.json" -} diff --git a/packages/flow-swap-private-reserves/src/SwapPrivateReservesScenarioOutputs.ts b/packages/flow-swap-private-reserves/src/SwapPrivateReservesScenarioOutputs.ts deleted file mode 100644 index d8ae91bbd..000000000 --- a/packages/flow-swap-private-reserves/src/SwapPrivateReservesScenarioOutputs.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { outputKey, type Books } from "@wireio/cluster-tool" - -/** - * Typed cross-step output keys for the private-reserve swap flow. Each swap - * phase's quote step SNAPSHOTS the pre-swap state (the two reserve books, the - * computed WIRE intermediate + target, and the user's destination-side balance - * baseline); the request/verify steps read the same values back — no shared - * mutable closures (cross-step values ride `ctx.outputs`). - */ -export namespace SwapPrivateReservesScenarioOutputs { - /** Phase A (ETH → USDCSOL) pre-swap `(src=ETH, dst=SOL)` reserve books. */ - export const phaseABooksBefore = outputKey( - "swapPrivateReserves.phaseA.booksBefore", - "phase A pre-swap private reserve books (src=ETHEREUM, dst=SOLANA)" - ) - /** Phase A gross WIRE intermediate `w = cp(eth.chain, eth.wire, source)`. */ - export const phaseAWireIntermediate = outputKey( - "swapPrivateReserves.phaseA.wireIntermediate", - "phase A gross WIRE intermediate (depot units)" - ) - /** Phase A user target `cp(sol.wire, sol.chain, w)` (depot units). */ - export const phaseATarget = outputKey( - "swapPrivateReserves.phaseA.target", - "phase A target amount (depot units)" - ) - /** Phase A pre-swap USDCSOL balance of the user's ATA (SPL base units). */ - export const phaseAUserAtaBefore = outputKey( - "swapPrivateReserves.phaseA.userAtaBefore", - "phase A user USDCSOL ATA baseline (base units)" - ) - - /** Phase B (USDCSOL → ETH) pre-swap `(src=SOL, dst=ETH)` reserve books. */ - export const phaseBBooksBefore = outputKey( - "swapPrivateReserves.phaseB.booksBefore", - "phase B pre-swap private reserve books (src=SOLANA, dst=ETHEREUM)" - ) - /** Phase B gross WIRE intermediate `w = cp(sol.chain, sol.wire, source)`. */ - export const phaseBWireIntermediate = outputKey( - "swapPrivateReserves.phaseB.wireIntermediate", - "phase B gross WIRE intermediate (depot units)" - ) - /** Phase B user target `cp(eth.wire, eth.chain, w)` (depot units). */ - export const phaseBTarget = outputKey( - "swapPrivateReserves.phaseB.target", - "phase B target amount (depot units)" - ) - /** Phase B pre-swap native-ETH balance of the user's wallet (wei). */ - export const phaseBEthereumBalanceBefore = outputKey( - "swapPrivateReserves.phaseB.ethereumBalanceBefore", - "phase B user ETH balance baseline (wei)" - ) - /** - * The reserve owner's WIRE balance immediately before they claim either - * reserve's accrued owner fee — the baseline the claim payout is measured - * against (depot units). - */ - export const ownerBalanceBeforeClaims = outputKey( - "swapPrivateReserves.ownerFees.balanceBeforeClaims", - "reserve owner WIRE balance before claiming owner fees (depot units)" - ) -} diff --git a/packages/flow-swap-private-reserves/src/index.ts b/packages/flow-swap-private-reserves/src/index.ts deleted file mode 100644 index a0a797897..000000000 --- a/packages/flow-swap-private-reserves/src/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { FlowCLI } from "@wireio/cluster-tool" -import { SwapPrivateReservesScenario } from "./SwapPrivateReservesScenario.js" - -/** Run the private-reserve swap flow as an executable — exit code = report success. */ -async function main(): Promise { - const report = await FlowCLI.create(SwapPrivateReservesScenario).run() - process.exit(report.succeeded ? 0 : 1) -} - -void main() diff --git a/packages/flow-swap-private-reserves/src/steps/SwapPrivateReservesScenarioOwnerSteps.ts b/packages/flow-swap-private-reserves/src/steps/SwapPrivateReservesScenarioOwnerSteps.ts deleted file mode 100644 index bd62da9c4..000000000 --- a/packages/flow-swap-private-reserves/src/steps/SwapPrivateReservesScenarioOwnerSteps.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { Bytes, KeyType, PrivateKey } from "@wireio/sdk-core" -import { ChainKind } from "@wireio/opp-typescript-models" -import { - AuthExLinkTool, - ClusterBuildStep, - Report, - SolanaFundingTool, - ethereumPrivateKeyFromWallet, - provisionWireUser, - swapUserOutputKey, - type ClusterBuildContext, - type ClusterBuildStepOptions, - type StepInput -} from "@wireio/cluster-tool" -import { SwapPrivateReservesScenarioArtifacts as Artifacts } from "../SwapPrivateReservesScenarioArtifacts.js" -import { SwapPrivateReservesScenarioConstants as Constants } from "../SwapPrivateReservesScenarioConstants.js" - -/** - * Owner-provisioning write Steps: the single WIRE account (`privowner`) that - * matches — and therefore owns — BOTH private reserves, its authex links on - * BOTH chains (each link key must equal the matching reserve's creator key for - * `matchreserve` to admit it), and the creator-side USDCSOL funding for the - * SOL escrow + Phase B source. - */ -export namespace SwapPrivateReservesScenarioOwnerSteps { - // ── Step: provision the owner WIRE account (writes) ────────────────────── - - /** Input for {@link planProvisionOwner}. */ - export interface ProvisionOwnerInput extends StepInput { - readonly kind: "SwapPrivateReservesScenarioOwnerSteps.ProvisionOwnerInput" - /** The owner WIRE account name. */ - readonly account: string - /** Raw 9-dec WIRE funding (covers both real-WIRE match escrows). */ - readonly fundWireAmount: bigint - } - - /** - * Provision the owner WIRE account — create it under the dev key, attach the - * standard resource policy, and fund it with enough WIRE to escrow both - * `matchreserve` amounts (via the harness's `provisionWireUser`). - */ - export function planProvisionOwner< - C extends ClusterBuildContext = ClusterBuildContext - >( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { - kind: "SwapPrivateReservesScenarioOwnerSteps.ProvisionOwnerInput", - account: Constants.Accounts.Owner, - fundWireAmount: Constants.Accounts.OwnerFunding - }, - runProvisionOwner - ) - } - - /** Named runner — account create + resource policy + WIRE funding. */ - export async function runProvisionOwner( - ctx: C, - input: ProvisionOwnerInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - await provisionWireUser(ctx.wire, input.account, { - fundWireAmount: input.fundWireAmount - }) - } - - // ── Step: authex-link the owner to a creator chain key (write) ─────────── - - /** Input for {@link planLinkOwner}. */ - export interface LinkOwnerInput extends StepInput { - readonly kind: "SwapPrivateReservesScenarioOwnerSteps.LinkOwnerInput" - /** The owner WIRE account name. */ - readonly account: string - /** The chain family whose creator key the owner links to. */ - readonly chainKind: ChainKind - } - - /** - * A single `sysio.authex::createlink` write binding the owner to the swap - * user's key on one chain (EVM: the ETH creator wallet's secp256k1 key; - * SVM: the SOL creator keypair's ed25519 key). `matchreserve` gates on the - * matcher's link key for the reserve's chain equalling the creator key, so - * BOTH links are required before either match. - */ - export function planLinkOwner< - C extends ClusterBuildContext = ClusterBuildContext - >( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - chainKind: ChainKind - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { - kind: "SwapPrivateReservesScenarioOwnerSteps.LinkOwnerInput", - account: Constants.Accounts.Owner, - chainKind - }, - runLinkOwner - ) - } - - /** - * Named runner — ONE `createlink` write for the owner on `input.chainKind` - * (same EVM-first / SVM-passthrough shape as the palette's own - * `WireOperatorProvisioningTool.runAuthexLink`, keyed to the swap user's - * live wallet/keypair instead of a stored operator). - */ - export async function runLinkOwner( - ctx: C, - input: LinkOwnerInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - const swapUser = ctx.outputs.assert(swapUserOutputKey()) - if (input.chainKind === ChainKind.EVM) { - await AuthExLinkTool.createLink(ctx.wire, { - chainKind: ChainKind.EVM, - account: input.account, - privateKey: ethereumPrivateKeyFromWallet(swapUser.ethereumWallet), - ethereumWallet: swapUser.ethereumWallet - }) - return - } - // WIRE PrivateKey stores the full 64-byte secretKey (seed + pubkey - // concat — the same shape as `Keypair.secretKey`), so the SVM link - // regenerates from the keypair's full secret verbatim. - await AuthExLinkTool.createLink(ctx.wire, { - chainKind: input.chainKind, - account: input.account, - privateKey: PrivateKey.regenerate( - KeyType.ED, - Bytes.fromString( - Buffer.from(swapUser.solanaKeypair.secretKey).toString("hex"), - "hex" - ) - ) - }) - } - - // ── Step: fund the creator's USDCSOL ATA (write) ───────────────────────── - - /** Input for {@link planMintCreatorUsdcSol}. */ - export interface MintCreatorUsdcSolInput extends StepInput { - readonly kind: "SwapPrivateReservesScenarioOwnerSteps.MintCreatorUsdcSolInput" - /** USDCSOL base units minted into the creator's ATA. */ - readonly amount: bigint - } - - /** - * A single mock-SPL mint into the SOL creator's ATA (create escrow + - * Phase B source + headroom), signed by the persisted deployer keypair - * (the mint authority). - */ - export function planMintCreatorUsdcSol< - C extends ClusterBuildContext = ClusterBuildContext - >( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { - kind: "SwapPrivateReservesScenarioOwnerSteps.MintCreatorUsdcSolInput", - amount: Constants.SplFunding.CreatorMintAmount - }, - runMintCreatorUsdcSol - ) - } - - /** Named runner — ONE `mintMockSplToUser` into the swap user's USDCSOL ATA. */ - export async function runMintCreatorUsdcSol( - ctx: C, - input: MintCreatorUsdcSolInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - const swapUser = ctx.outputs.assert(swapUserOutputKey()) - await SolanaFundingTool.mintMockSplToUser( - ctx.solana.connection, - SolanaFundingTool.loadDeployerKeypair(ctx.config.dataPath), - Artifacts.loadUsdcSolMint(ctx), - swapUser.solanaKeypair.publicKey, - input.amount - ) - } -} diff --git a/packages/flow-swap-private-reserves/src/steps/SwapPrivateReservesScenarioReserveSteps.ts b/packages/flow-swap-private-reserves/src/steps/SwapPrivateReservesScenarioReserveSteps.ts deleted file mode 100644 index a7d0dbceb..000000000 --- a/packages/flow-swap-private-reserves/src/steps/SwapPrivateReservesScenarioReserveSteps.ts +++ /dev/null @@ -1,353 +0,0 @@ -import Assert from "node:assert" -import * as anchor from "@coral-xyz/anchor" -import { PublicKey, SystemProgram } from "@solana/web3.js" -import { - TOKEN_PROGRAM_ID, - getAssociatedTokenAddressSync -} from "@solana/spl-token" -import { SlugName, SysioContracts } from "@wireio/sdk-core" -import { - ClusterBuildStep, - EthereumLocalReserveStatus, - Report, - SolanaCollateralTool, - SolanaOutpostBootstrapper, - confirmSignature, - resolveLatestNonce, - swapUserOutputKey, - slugNameToLittleEndianBuffer, - type AnchorEnumVariant, - type ClusterBuildContext, - type ClusterBuildStepOptions, - type StepInput -} from "@wireio/cluster-tool" -import { SwapPrivateReservesScenarioArtifacts as Artifacts } from "../SwapPrivateReservesScenarioArtifacts.js" -import { SwapPrivateReservesScenarioConstants as Constants } from "../SwapPrivateReservesScenarioConstants.js" - -const { SysioContractName } = SysioContracts -const { PdaSeed } = SolanaOutpostBootstrapper - -/** - * The gated private-reserve handshake writes: the outpost `create_reserve` - * submissions (`isPrivate=true`, creator key riding the attestation) and the - * depot-side `sysio.reserv::matchreserve` by the authex-linked owner — plus - * the outpost-local record reads the RESERVE_READY verifies poll. - */ -export namespace SwapPrivateReservesScenarioReserveSteps { - // ── Step: ETH-native create_reserve (write) ────────────────────────────── - - /** Input for {@link planCreateEthereumReserve}. */ - export interface CreateEthereumReserveInput extends StepInput { - readonly kind: "SwapPrivateReservesScenarioReserveSteps.CreateEthereumReserveInput" - readonly tokenCode: number - readonly reserveCode: number - /** Wei escrowed as `msg.value`. */ - readonly escrowWei: bigint - /** WIRE (raw 9-dp) the depot row requests from the matcher. */ - readonly requestedWireAmount: bigint - } - - /** - * Submit the ETH-native `create_reserve` for the private ETH reserve — - * escrows `msg.value` wei and ships the creator's compressed secp256k1 key - * (contract-verified to derive to the caller). - */ - export function planCreateEthereumReserve< - C extends ClusterBuildContext = ClusterBuildContext - >( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { - kind: "SwapPrivateReservesScenarioReserveSteps.CreateEthereumReserveInput", - tokenCode: Constants.Reserves.Ethereum.TokenCode, - reserveCode: Constants.Reserves.PrivateReserveCode, - escrowWei: Constants.CreateParams.EthereumEscrowWei, - requestedWireAmount: Constants.CreateParams.EthereumRequestedWire - }, - runCreateEthereumReserve - ) - } - - /** Named runner — ONE payable `ReserveManager.create_reserve(...)` write. */ - export async function runCreateEthereumReserve( - ctx: C, - input: CreateEthereumReserveInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - const swapUser = ctx.outputs.assert(swapUserOutputKey()) - const reserveManager = - Artifacts.loadReserveManager( - ctx, - swapUser.ethereumWallet - ) - const nonce = await resolveLatestNonce(reserveManager) - const response = await reserveManager.create_reserve( - BigInt(input.tokenCode), - BigInt(input.reserveCode), - input.escrowWei, - input.requestedWireAmount, - Constants.CreateParams.ConnectorWeightBps, - Constants.CreateParams.EthereumName, - Constants.CreateParams.EthereumDescription, - true, - swapUser.ethereumWallet.signingKey.compressedPublicKey, - { value: input.escrowWei, nonce } - ) - const receipt = await response.wait(1) - Assert.ok( - receipt?.status === 1, - `create_reserve(ETH/PRIVATE) reverted (status=${receipt?.status ?? "null"})` - ) - } - - // ── Step: SOL SPL-branch create_reserve (write) ────────────────────────── - - /** Input for {@link planCreateSolanaReserve}. */ - export interface CreateSolanaReserveInput extends StepInput { - readonly kind: "SwapPrivateReservesScenarioReserveSteps.CreateSolanaReserveInput" - readonly tokenCode: number - readonly reserveCode: number - /** USDCSOL base units escrowed from the creator's ATA. */ - readonly escrowChainUnits: bigint - /** WIRE (raw 9-dp) the depot row requests from the matcher. */ - readonly requestedWireAmount: bigint - } - - /** - * Submit the permissionless SPL-branch `create_reserve` IX for the private - * USDCSOL reserve. Accounts mirror the program's `CreateReserve` struct - * (`create_reserve.rs`): the per-reserve PDA + vault are `init`-allocated - * here, the escrow transfers from the creator's ATA into the vault, and the - * signer's ed25519 key rides the attestation as `creator_pub_key` - * automatically. - */ - export function planCreateSolanaReserve< - C extends ClusterBuildContext = ClusterBuildContext - >( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { - kind: "SwapPrivateReservesScenarioReserveSteps.CreateSolanaReserveInput", - tokenCode: Constants.Reserves.Solana.TokenCode, - reserveCode: Constants.Reserves.PrivateReserveCode, - escrowChainUnits: Constants.CreateParams.SolanaEscrowChainUnits, - requestedWireAmount: Constants.CreateParams.SolanaRequestedWire - }, - runCreateSolanaReserve - ) - } - - /** Named runner — ONE `opp_outpost::create_reserve` IX (SPL branch). */ - export async function runCreateSolanaReserve( - ctx: C, - input: CreateSolanaReserveInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - const swapUser = ctx.outputs.assert(swapUserOutputKey()) - const program = SolanaCollateralTool.loadOppOutpostProgram( - ctx, - swapUser.solanaKeypair - ) - const programId = program.programId - const [configPda] = PublicKey.findProgramAddressSync( - [Buffer.from(PdaSeed.OutpostConfig)], - programId - ) - const [outboundMessageBufferPda] = PublicKey.findProgramAddressSync( - [Buffer.from(PdaSeed.OutboundMessageBuffer)], - programId - ) - const tokenCodeLE = slugNameToLittleEndianBuffer(input.tokenCode) - const reserveCodeLE = slugNameToLittleEndianBuffer(input.reserveCode) - const [reservePda] = PublicKey.findProgramAddressSync( - [Buffer.from(PdaSeed.Reserve), tokenCodeLE, reserveCodeLE], - programId - ) - const [reserveVaultPda] = PublicKey.findProgramAddressSync( - [Buffer.from(PdaSeed.ReserveVault), tokenCodeLE, reserveCodeLE], - programId - ) - const usdcSolMint = Artifacts.loadUsdcSolMint(ctx) - - const transaction = await program.methods - .createReserve( - new anchor.BN(input.tokenCode), - new anchor.BN(input.reserveCode), - new anchor.BN(input.escrowChainUnits.toString()), - new anchor.BN(input.requestedWireAmount.toString()), - Constants.CreateParams.ConnectorWeightBps, - Constants.CreateParams.SolanaName, - Constants.CreateParams.SolanaDescription, - true - ) - .accounts({ - creator: swapUser.solanaKeypair.publicKey, - config: configPda, - reserve: reservePda, - reserveVault: reserveVaultPda, - mint: usdcSolMint, - creatorAta: getAssociatedTokenAddressSync( - usdcSolMint, - swapUser.solanaKeypair.publicKey - ), - outboundMessageBuffer: outboundMessageBufferPda, - tokenProgram: TOKEN_PROGRAM_ID, - systemProgram: SystemProgram.programId, - rent: anchor.web3.SYSVAR_RENT_PUBKEY - }) - .signers([swapUser.solanaKeypair]) - .transaction() - const signature = await ctx.solana.connection.sendTransaction( - transaction, - [swapUser.solanaKeypair], - { skipPreflight: false } - ) - await confirmSignature( - ctx.solana.connection, - signature, - "create_reserve(USDCSOL/PRIVATE)" - ) - } - - // ── Step: depot matchreserve (write) ───────────────────────────────────── - - /** Input for {@link planMatchReserve}. */ - export interface MatchReserveInput extends StepInput { - readonly kind: "SwapPrivateReservesScenarioReserveSteps.MatchReserveInput" - readonly chainCode: number - readonly tokenCode: number - readonly reserveCode: number - /** The matcher WIRE account (must be authex-linked to the creator key). */ - readonly matcher: string - /** Must equal the row's `requested_wire_amount` exactly. */ - readonly wireAmount: bigint - } - - /** - * Push `sysio.reserv::matchreserve` as the owner for one `(chain, token, - * reserve)` triple — escrows the requested WIRE, flips the depot row ACTIVE - * synchronously, and queues RESERVE_READY back to the outpost. - */ - export function planMatchReserve< - C extends ClusterBuildContext = ClusterBuildContext - >( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - chainCode: number, - tokenCode: number, - wireAmount: bigint - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { - kind: "SwapPrivateReservesScenarioReserveSteps.MatchReserveInput", - chainCode, - tokenCode, - reserveCode: Constants.Reserves.PrivateReserveCode, - matcher: Constants.Accounts.Owner, - wireAmount - }, - runMatchReserve - ) - } - - /** Named runner — ONE typed `matchreserve` invoke, authorized by the owner. */ - export async function runMatchReserve( - ctx: C, - input: MatchReserveInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - await ctx.wire - .getSysioContract(SysioContractName.reserv) - .actions.matchreserve.invoke( - { - chain_code: SlugName.toString(input.chainCode), - token_code: SlugName.toString(input.tokenCode), - reserve_code: SlugName.toString(input.reserveCode), - matcher: input.matcher, - wire_amount: Number(input.wireAmount) - }, - { authorization: [{ actor: input.matcher, permission: "active" }] } - ) - } - - // ── Outpost-local record reads (polled by the RESERVE_READY verifies) ──── - - /** True once the ETH outpost's local PRIVATE record reports ACTIVE (a read). */ - export async function readEthereumLocalReserveActive< - C extends ClusterBuildContext - >(ctx: C): Promise { - const reserveManager = - Artifacts.loadReserveManager( - ctx, - ctx.ethereum.wallet.signer - ) - const record = await reserveManager.getReserve( - BigInt(Constants.Reserves.Ethereum.TokenCode), - BigInt(Constants.Reserves.PrivateReserveCode) - ) - return Number(record.status) === EthereumLocalReserveStatus.ACTIVE - } - - /** - * The `opp-outpost` Reserve PDA fields this flow reads. Anchor's IDL-generic - * `AccountClient` hands back an untyped record, so only the anchor-encoded - * `status` variant tag is modeled. - */ - interface SolanaReserveAccountView { - /** Anchor variant tag for the PDA's `ReserveStatus` (e.g. `{ active: {} }`). */ - status?: AnchorEnumVariant - } - - /** - * True once the SOL outpost's PRIVATE Reserve PDA reports `Active` (a read). - * Required before Phase B — `request_swap_spl` constraint-gates on the local - * status, so the RESERVE_READY round-trip must have landed. - */ - export async function readSolanaLocalReserveActive< - C extends ClusterBuildContext - >(ctx: C): Promise { - const swapUser = ctx.outputs.assert(swapUserOutputKey()) - const program = SolanaCollateralTool.loadOppOutpostProgram( - ctx, - swapUser.solanaKeypair - ) - const [reservePda] = PublicKey.findProgramAddressSync( - [ - Buffer.from(PdaSeed.Reserve), - slugNameToLittleEndianBuffer(Constants.Reserves.Solana.TokenCode), - slugNameToLittleEndianBuffer(Constants.Reserves.PrivateReserveCode) - ], - program.programId - ) - const account = await ( - program.account as Record> - ).reserve.fetch(reservePda) - const status = (account as SolanaReserveAccountView).status - return typeof status === "object" && status !== null && "active" in status - } -} diff --git a/packages/flow-swap-private-reserves/src/steps/SwapPrivateReservesScenarioSwapSteps.ts b/packages/flow-swap-private-reserves/src/steps/SwapPrivateReservesScenarioSwapSteps.ts deleted file mode 100644 index 560fb141d..000000000 --- a/packages/flow-swap-private-reserves/src/steps/SwapPrivateReservesScenarioSwapSteps.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { - ClusterBuildStep, - Report, - SolanaCollateralTool, - requestEthereumSwap, - requestSolanaSwapSpl, - swapUserOutputKey, - type ClusterBuildContext, - type ClusterBuildStepOptions, - type ReserveManagerRequestSwapContract, - type StepInput -} from "@wireio/cluster-tool" -import { SwapPrivateReservesScenarioArtifacts as Artifacts } from "../SwapPrivateReservesScenarioArtifacts.js" -import { SwapPrivateReservesScenarioConstants as Constants } from "../SwapPrivateReservesScenarioConstants.js" -import { SwapPrivateReservesScenarioOutputs as Outputs } from "../SwapPrivateReservesScenarioOutputs.js" - -/** - * The user-side SWAP_REQUEST writes of the private-pair flow: Phase A's - * ETH-native `ReserveManager.requestSwap` sourcing the private ETH reserve, - * Phase B's `opp_outpost::request_swap_spl` sourcing the private USDCSOL - * reserve, and the private→WIRE exclusion probe. Each runner reads the swap - * user identity + the phase's computed target from `ctx.outputs` and performs - * exactly ONE on-chain write. - */ -export namespace SwapPrivateReservesScenarioSwapSteps { - // ── Step: Phase A — ETH (native) → USDCSOL (SPL) request (write) ───────── - - /** Input for {@link planRequestSwapEthereumToSolana}. */ - export interface RequestSwapEthereumToSolanaInput extends StepInput { - readonly kind: "SwapPrivateReservesScenarioSwapSteps.RequestSwapEthereumToSolanaInput" - /** Wei escrowed into the private ETH reserve as the swap input. */ - readonly sourceWei: bigint - readonly targetToleranceBps: number - } - - /** - * The user calls `ReserveManager.requestSwap` sourcing the private ETH - * reserve toward the private USDCSOL reserve. The swap USER signs on-chain - * with the same wallet that created the reserves — but ownership lives with - * the WIRE account (`privowner`); the user is not the owner of anything here. - * The target amount comes from the quote step via - * {@link Outputs.phaseATarget}; SPL recipients ride as the WALLET pubkey — - * the outpost pays the recipient's ATA (pre-existing via the creator mint). - */ - export function planRequestSwapEthereumToSolana< - C extends ClusterBuildContext = ClusterBuildContext - >( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { - kind: "SwapPrivateReservesScenarioSwapSteps.RequestSwapEthereumToSolanaInput", - sourceWei: Constants.SwapAmounts.PhaseASourceWei, - targetToleranceBps: Constants.Variance.ToleranceBps - }, - runRequestSwapEthereumToSolana - ) - } - - /** Named runner — ONE `requestSwap` write against the private pair (A leg). */ - export async function runRequestSwapEthereumToSolana< - C extends ClusterBuildContext - >( - ctx: C, - input: RequestSwapEthereumToSolanaInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - const swapUser = ctx.outputs.assert(swapUserOutputKey()) - const target = ctx.outputs.assert(Outputs.phaseATarget) - const reserveManager = - Artifacts.loadReserveManager( - ctx, - swapUser.ethereumWallet - ) - await requestEthereumSwap(reserveManager, { - sourceTokenCode: BigInt(Constants.Reserves.Ethereum.TokenCode), - sourceReserveCode: BigInt(Constants.Reserves.PrivateReserveCode), - sourceAmountWei: input.sourceWei, - targetChainCode: BigInt(Constants.Reserves.Solana.ChainCode), - targetTokenCode: BigInt(Constants.Reserves.Solana.TokenCode), - targetReserveCode: BigInt(Constants.Reserves.PrivateReserveCode), - targetRecipient: swapUser.solanaPublicKeyBytes, - targetAmount: target, - targetToleranceBps: input.targetToleranceBps - }) - } - - // ── Step: Phase B — USDCSOL (SPL) → ETH (native) request (write) ───────── - - /** Input for {@link planRequestSwapSolanaToEthereum}. */ - export interface RequestSwapSolanaToEthereumInput extends StepInput { - readonly kind: "SwapPrivateReservesScenarioSwapSteps.RequestSwapSolanaToEthereumInput" - /** USDCSOL base units drawn from the creator's ATA as the swap input. */ - readonly sourceSplUnits: bigint - readonly targetToleranceBps: number - } - - /** - * The user calls `opp_outpost::request_swap_spl` sourcing the private - * USDCSOL reserve toward the private ETH reserve. The target amount comes - * from the inverse quote step via {@link Outputs.phaseBTarget}; the payout - * recipient is the user's raw 20-byte ETH address. - */ - export function planRequestSwapSolanaToEthereum< - C extends ClusterBuildContext = ClusterBuildContext - >( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { - kind: "SwapPrivateReservesScenarioSwapSteps.RequestSwapSolanaToEthereumInput", - sourceSplUnits: Constants.SwapAmounts.PhaseBSourceSplUnits, - targetToleranceBps: Constants.Variance.ToleranceBps - }, - runRequestSwapSolanaToEthereum - ) - } - - /** Named runner — ONE `request_swap_spl` write against the private pair (B leg). */ - export async function runRequestSwapSolanaToEthereum< - C extends ClusterBuildContext - >( - ctx: C, - input: RequestSwapSolanaToEthereumInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - const swapUser = ctx.outputs.assert(swapUserOutputKey()) - const target = ctx.outputs.assert(Outputs.phaseBTarget) - await requestSolanaSwapSpl( - ctx.solana.connection, - SolanaCollateralTool.loadOppOutpostProgram(ctx, swapUser.solanaKeypair), - swapUser.solanaKeypair, - { - sourceTokenCode: BigInt(Constants.Reserves.Solana.TokenCode), - sourceReserveCode: BigInt(Constants.Reserves.PrivateReserveCode), - sourceAmount: input.sourceSplUnits, - sourceMint: Artifacts.loadUsdcSolMint(ctx), - targetChainCode: BigInt(Constants.Reserves.Ethereum.ChainCode), - targetTokenCode: BigInt(Constants.Reserves.Ethereum.TokenCode), - targetReserveCode: BigInt(Constants.Reserves.PrivateReserveCode), - targetRecipient: swapUser.ethereumAddressBytes, - targetAmount: target, - targetToleranceBps: input.targetToleranceBps - } - ) - } - - // ── Step: private → WIRE exclusion probe request (write) ───────────────── - - /** Input for {@link planRequestSwapPrivateToWire}. */ - export interface RequestSwapPrivateToWireInput extends StepInput { - readonly kind: "SwapPrivateReservesScenarioSwapSteps.RequestSwapPrivateToWireInput" - /** Wei escrowed as the probe's source deposit (refunded by the SWAP_REVERT). */ - readonly sourceWei: bigint - /** The WIRE recipient account name riding the request as bytes. */ - readonly recipientName: string - /** Positive sentinel — the privacy gate precedes the variance check. */ - readonly targetAmount: bigint - readonly targetToleranceBps: number - } - - /** - * The user requests a swap sourcing the private ETH reserve toward the WIRE - * endpoint. The depot's privacy gate MUST reject it (SWAP_REVERT, no UWREQ) - * — the request itself still lands on the outpost, which is why this is a - * write step; the negative assertions live in the phase's verify steps. - */ - export function planRequestSwapPrivateToWire< - C extends ClusterBuildContext = ClusterBuildContext - >( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { - kind: "SwapPrivateReservesScenarioSwapSteps.RequestSwapPrivateToWireInput", - sourceWei: Constants.WireProbe.SourceEthereumWei, - recipientName: Constants.WireProbe.RecipientName, - targetAmount: Constants.WireProbe.TargetAmount, - targetToleranceBps: Constants.WireProbe.ToleranceBps - }, - runRequestSwapPrivateToWire - ) - } - - /** Named runner — ONE `requestSwap` write targeting the WIRE endpoint. */ - export async function runRequestSwapPrivateToWire< - C extends ClusterBuildContext - >( - ctx: C, - input: RequestSwapPrivateToWireInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - const swapUser = ctx.outputs.assert(swapUserOutputKey()) - const reserveManager = - Artifacts.loadReserveManager( - ctx, - swapUser.ethereumWallet - ) - await requestEthereumSwap(reserveManager, { - sourceTokenCode: BigInt(Constants.Reserves.Ethereum.TokenCode), - sourceReserveCode: BigInt(Constants.Reserves.PrivateReserveCode), - sourceAmountWei: input.sourceWei, - targetChainCode: BigInt(Constants.Reserves.Wire.ChainCode), - targetTokenCode: BigInt(Constants.Reserves.Wire.TokenCode), - targetReserveCode: BigInt(Constants.Reserves.Wire.SentinelReserveCode), - targetRecipient: new TextEncoder().encode(input.recipientName), - targetAmount: input.targetAmount, - targetToleranceBps: input.targetToleranceBps - }) - } -} diff --git a/packages/flow-swap-private-reserves/tsconfig.json b/packages/flow-swap-private-reserves/tsconfig.json deleted file mode 100644 index 82b017421..000000000 --- a/packages/flow-swap-private-reserves/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-swap-private-reserves/tsconfig.src.json b/packages/flow-swap-private-reserves/tsconfig.src.json deleted file mode 100644 index 60177986b..000000000 --- a/packages/flow-swap-private-reserves/tsconfig.src.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "extends": "../../etc/tsconfig/tsconfig.base.jest.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "lib", - "paths": { - "@wireio/cluster-tool": [ - "../cluster-tool/src" - ], - "@wireio/cluster-tool-shared": [ - "../cluster-tool-shared/src" - ], - "@wireio/debugging-shared": [ - "../debugging-shared/src" - ] - } - }, - "include": [ - "src" - ], - "references": [ - { - "path": "../cluster-tool-shared/tsconfig.json" - }, - { - "path": "../cluster-tool/tsconfig.json" - }, - { - "path": "../debugging-shared/tsconfig.json" - } - ] -} diff --git a/packages/flow-swap-to-wire/package.json b/packages/flow-swap-to-wire/package.json deleted file mode 100644 index 00485c622..000000000 --- a/packages/flow-swap-to-wire/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "@wireio/test-flow-swap-to-wire", - "version": "0.1.18", - "private": true, - "type": "commonjs", - "description": "Flow: Swap TO WIRE (Ethereum → WIRE depot, single-leg underwriting + direct WIRE payout)", - "scripts": { - "build": "tsc -b tsconfig.json", - "test:disabled": "node lib/index.js", - "clean": "rm -rf lib" - }, - "dependencies": { - "@wireio/cluster-tool": "workspace:*", - "@wireio/sdk-core": "^1.0.92", - "ethers": "^6.14.0" - }, - "devDependencies": { - "@types/node": "25.5.0" - } -} diff --git a/packages/flow-swap-to-wire/src/SwapToWireScenario.ts b/packages/flow-swap-to-wire/src/SwapToWireScenario.ts deleted file mode 100644 index cb33d4cc3..000000000 --- a/packages/flow-swap-to-wire/src/SwapToWireScenario.ts +++ /dev/null @@ -1,801 +0,0 @@ -import Assert from "node:assert" -import type { ClusterConfig } from "@wireio/cluster-tool-shared" -import { SysioContracts } from "@wireio/sdk-core" -import { - contractView, - ClusterBuildPhase, - ClusterBuildStep, - Constants as HarnessConstants, - EthereumCollateralTool, - FlowScenario, - Report, - SwapScenarioContext, - SwapUserIdentities, - WireReserveTool, - WireUnderwriterTool, - getLogger, - isNotEmpty, - matchesProtoEnum, - outputKey, - pollUntil, - provisionWireUser, - requestEthereumSwap, - slugValue, - swapUserOutputKey, - verifyStep, - type ClusterBuild, - type ClusterBuildOptions, - type ClusterBuildStepOptions, - type Logger, - type ReserveBook, - type ReserveManagerRequestSwapContract, - type StepInput, - type WireUser, - ClusterConfigProvider -} from "@wireio/cluster-tool" -import { SwapToWireScenarioConstants as Constants } from "./SwapToWireScenarioConstants.js" - -const { - SysioContractName, - SysioMsgchAttestationtype, - SysioOpregOperatorstatus, - SysioUwritUnderwriterequeststatus -} = SysioContracts -const { Actor } = Report - -const log = getLogger(__filename) - -// ── Reads (execute freely inside verify-step runners) ─────────────────────── - -/** - * Whether every `labels` entry has an `sysio.opreg::operators` row in - * `OPERATOR_STATUS_ACTIVE` (a read). - * - * @param ctx - The scenario context. - * @param labels - The underwriters' durable harness `label` handles to check. - * @returns Whether the whole roster is ACTIVE. - */ -async function underwritersActive( - ctx: SwapScenarioContext, - labels: string[] -): Promise { - const { rows } = await ctx.wire - .getSysioContract(SysioContractName.opreg) - .tables.operators.query({ limit: Constants.OperatorTableRowLimit }) - return labels.every(label => { - const account = ctx.keyStore.assertOperator(label).account, - operator = rows.find(row => row.account === account) - return ( - operator != null && - matchesProtoEnum( - operator.status, - SysioOpregOperatorstatus, - SysioOpregOperatorstatus.OPERATOR_STATUS_ACTIVE - ) - ) - }) -} - -/** - * Predicate factory — whether `account`'s `sysio.reserv::wireclaims` credit has - * reached `target` (a read). - * - * `paywire` credits a claimable balance rather than transferring, so the - * recipient's token balance stays flat until it pulls with `claimwire` — this - * reads the claim ledger, not the balance. - * - * @param ctx - The scenario context. - * @param account - The payout recipient's WIRE account. - * @param target - The post-fee WIRE amount the credit must reach. - * @returns A poll predicate for whether the credit is at or above `target`. - */ -function claimableReachedPredicate( - ctx: SwapScenarioContext, - account: string, - target: bigint -): () => Promise { - return () => - ctx.wire.getWireClaimable(account).then(amount => amount >= target) -} - -/** - * The to-WIRE uwreq row (src=ETHEREUM, dst=WIRE) — throws when the depot has - * not created it (a read). - * - * @param ctx - The scenario context. - * @returns The matching `sysio.uwrit::uwreqs` row. - * @throws When no to-WIRE uwreq row exists yet. - */ -async function assertToWireUwreq( - ctx: SwapScenarioContext -): Promise { - const request = await ctx.uwreq( - Constants.EthereumChainCode, - Constants.WireChainCode - ) - Assert.ok( - request != null, - "to-WIRE uwreq row (src=ETHEREUM, dst=WIRE) not found" - ) - return request -} - -// ── Flow-local write Steps (each on-chain WRITE is its own Step) ───────────── - -/** Input for {@link planProvisionRecipient}. */ -interface ProvisionRecipientInput extends StepInput { - readonly kind: "SwapToWireScenario.ProvisionRecipientInput" - /** WIRE account name to provision (unfunded — the payout is the flow's proof). */ - readonly account: string -} - -/** - * Step factory — provision the WIRE recipient account (create + resource - * policy, via the shared WIRE-user tool). The recipient only needs to EXIST: - * it starts at zero WIRE so the direct payout is unambiguous. - * - * @param actor - The narrative subject. - * @param name - Step name (report row). - * @param description - One-line description. - * @param options - Per-step tuning. - * @param account - The WIRE account name to provision. - * @returns The definition step. - */ -function planProvisionRecipient( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - account: string -): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { kind: "SwapToWireScenario.ProvisionRecipientInput", account }, - runProvisionRecipient - ) -} - -/** Named runner — provision the recipient and store its {@link WireUser} output. */ -async function runProvisionRecipient( - ctx: SwapScenarioContext, - input: ProvisionRecipientInput, - signal: AbortSignal -): Promise { - signal.throwIfAborted() - const recipient = await provisionWireUser(ctx.wire, input.account) - ctx.outputs.set(SwapToWireScenario.Output.recipient, recipient) -} - -/** Input for {@link planRequestSwap}. */ -interface RequestSwapInput extends StepInput { - readonly kind: "SwapToWireScenario.RequestSwapInput" - /** Wei escrowed into the source reserve as the swap input. */ - readonly sourceAmountWei: bigint - /** Acceptable variance (bps) on the WIRE target. */ - readonly toleranceBps: number -} - -/** - * Step factory — the ONE `ReserveManager.requestSwap` write with the WIRE - * target: targetChain/Token = WIRE, the non-zero sentinel reserve code (the - * outpost guards `reserveCode != 0`; the depot never quotes or debits a - * WIRE-side reserve), and the recipient's account-name bytes. - * - * @param actor - The narrative subject. - * @param name - Step name (report row). - * @param description - One-line description. - * @param options - Per-step tuning. - * @param sourceAmountWei - Wei to escrow on the source outpost. - * @param toleranceBps - Variance tolerance riding the request. - * @returns The definition step. - */ -function planRequestSwap( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - sourceAmountWei: bigint, - toleranceBps: number -): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { - kind: "SwapToWireScenario.RequestSwapInput", - sourceAmountWei, - toleranceBps - }, - runRequestSwap - ) -} - -/** - * Named runner — bind `ReserveManager` to the swap user's wallet and submit - * the ONE `requestSwap` write carrying the curve target from `ctx.outputs`. - */ -async function runRequestSwap( - ctx: SwapScenarioContext, - input: RequestSwapInput, - signal: AbortSignal -): Promise { - signal.throwIfAborted() - const swapUser = ctx.outputs.assert(swapUserOutputKey()), - recipient = ctx.outputs.assert(SwapToWireScenario.Output.recipient), - target = ctx.outputs.assert(SwapToWireScenario.Output.target) - const addresses = EthereumCollateralTool.loadOutpostAddresses( - ClusterConfigProvider.ethereumDeploymentsPath(ctx.config) - ), - reserveManagerAddress = addresses[Constants.ReserveManagerContractName] - Assert.ok( - reserveManagerAddress != null, - `SwapToWireScenario: ${Constants.ReserveManagerContractName} missing from outpost-addrs.json` - ) - const reserveManager = contractView( - reserveManagerAddress, - EthereumCollateralTool.loadOutpostAbi( - ctx.config.ethereumPath, - Constants.ReserveManagerContractName - ), - swapUser.ethereumWallet - ) - const result = await requestEthereumSwap(reserveManager, { - sourceTokenCode: BigInt(Constants.EthereumTokenCode), - sourceReserveCode: BigInt(Constants.EthereumReserveCode), - sourceAmountWei: input.sourceAmountWei, - targetChainCode: BigInt(Constants.WireChainCode), - targetTokenCode: BigInt(Constants.WireTokenCode), - targetReserveCode: BigInt(Constants.WireSentinelReserveCode), - targetRecipient: recipient.accountBytes, - targetAmount: target, - targetToleranceBps: input.toleranceBps - }) - Assert.ok( - isNotEmpty(result.transactionHash), - "SwapToWireScenario: requestSwap must return a mined transaction hash" - ) - log.info( - `[SwapToWire] requestSwap mined: ${result.transactionHash} (block ${result.blockNumber})` - ) -} - -/** - * Flow: Swap TO WIRE — Ethereum → the WIRE depot itself. - * - * The single-leg shape: the user deposits native ETH on the source outpost - * exactly like a normal swap, but the target is the WIRE token on the depot. - * Only the SOURCE leg is underwritten (one UIC, one bond, one lock); at race - * resolution the depot books the source reserve (`chain += src, wire -= dst`) - * and pays the recipient REAL WIRE from `sysio.reserv` custody in the same - * transaction. No destination outpost, no SWAP_REMIT, no ack — and the - * underwriter's collateral lock PERSISTS for its wall-clock challenge window - * (it is never released by delivery). - * - * Scenario phases (on top of the bootstrap): - * 1. **SubstrateHealth** — blocks produce; the ETHEREUM/ETH/PRIMARY reserve is - * seeded with real WIRE custody behind it. - * 2. **SwapUser** / **Recipient** — the paired ETH+SOL swap identity and the - * (unfunded) WIRE recipient. - * 3. **UnderwriterCollateral** / **UnderwriterActive** — default bonds on both - * outposts; the roster flips ACTIVE once the deposits credit over OPP. - * 4. **QuoteAndRequest** — single-reserve `cp_output(src.chain, src.wire, - * amount)` gross leg, less the WIRE-leg fee, is the target; the ONE - * `requestSwap` write with the WIRE target. - * 5. **CreateUwreq** — the PENDING to-WIRE UWREQ appears (dst token = WIRE). - * 6. **RaceSourceLeg** — CONFIRMED with exactly ONE lock, on the SOURCE chain. - * 7. **PayWireDirectly** — recipient paid the EXACT post-fee target; source - * book moves `chain += src, wire -= target + fee` (i.e. the gross leg); - * custody drains to `before − target − fee`. - * 8. **ChallengeWindow** — the lock persists (still CONFIRMED) and NO outbound - * SWAP_REMIT references the uwreq (the depot itself was the payer). - */ -export class SwapToWireScenario extends FlowScenario { - readonly name = "flow-swap-to-wire" - readonly description = - "Single-leg swap TO WIRE: ETH escrow on the source outpost, direct WIRE payout from sysio.reserv custody" - - override readonly defaults: ClusterBuildOptions = { - // Seed the mock (chain, token) PRIMARY reserves this flow reads — `regreserve` - // is epoch-0-gated by the depot, so it must ride the bootstrap, not a flow phase. - enableMockReserves: true, - // The launch build of the Solana outpost withholds the reserve/swap - // bootstrap calls; this flow exercises them, so it opts in (never in plan()). - enableLaunchWithheldOperations: true, - epochDurationSec: Constants.EpochDurationSec, - // The underwriter ACTIVE gate: minimums on BOTH outpost chains, matched - // exactly by the default bond plan the scenario deposits. - requiredUnderwriterCollateral: [ - { - chainCode: Constants.EthereumChainCode, - tokenCode: Constants.EthereumTokenCode, - minimumBond: Constants.UnderwriterMinimumBond - }, - { - chainCode: Constants.SolanaChainCode, - tokenCode: Constants.SolanaTokenCode, - minimumBond: Constants.UnderwriterMinimumBond - } - ] - } - - /** The swap flows share the {@link SwapScenarioContext} query surface. */ - override createContext( - config: ClusterConfig, - log: Logger - ): SwapScenarioContext { - return new SwapScenarioContext(config, log) - } - - plan(cluster: ClusterBuild): void { - const config = cluster.context.config, - underwriterLabels = Array.from( - { length: config.underwriterCount }, - (_, index) => HarnessConstants.underwriterLabel(index) - ), - writeOptions = { timeoutMs: Constants.WriteTimeoutMs }, - activeStepOptions = { - timeoutMs: Constants.relayDeadlineMs() + Constants.PollDeadlineBufferMs - }, - uwreqStepOptions = { - timeoutMs: Constants.UwreqDeadlineMs + Constants.PollDeadlineBufferMs - }, - raceStepOptions = { - timeoutMs: Constants.RaceDeadlineMs + Constants.PollDeadlineBufferMs - }, - payoutStepOptions = { - timeoutMs: Constants.PayoutDeadlineMs + Constants.PollDeadlineBufferMs - } - - // ── 1. Substrate health — blocks + the seeded, custody-backed reserve ── - ClusterBuildPhase.create( - cluster, - "SubstrateHealth", - "The chain produces blocks and the source reserve is seeded" - ).push( - verifyStep( - Actor.Sysio, - "chain-producing", - "WIRE chain is producing blocks", - async ctx => { - const info = await ctx.wire.getInfo() - Assert.ok( - Number(info.head_block_num) > 0, - `head_block_num must be positive (got ${info.head_block_num})` - ) - } - ), - verifyStep( - Actor.Sysio, - "reserve-custody-seeded", - "bootstrap seeded ETHEREUM/ETH/PRIMARY with real WIRE custody behind it", - async ctx => { - // Custody prerequisite for the direct payout: sysio.reserv's REAL - // WIRE balance backs the reserve rows (regreserve treasury drain). - const book = await ctx.reserveBook( - Constants.EthereumChainCode, - Constants.EthereumTokenCode, - Constants.EthereumReserveCode - ) - const custody = await ctx.wire.getWireBalance( - Constants.ReserveCustodyAccount - ) - Assert.ok( - custody >= book.wire, - `sysio.reserv custody (${custody}) must back the reserve's WIRE book (${book.wire})` - ) - } - ) - ) - - // ── 2. The swap user's paired ETH + SOL identity (+ SOL airdrop) ── - SwapUserIdentities.planIdentityProvisioning( - cluster, - "SwapUser", - "Provision the swap user's paired Ethereum + Solana identity", - {} - ) - - // ── 3. The WIRE recipient — exists, holds no WIRE until the payout ── - ClusterBuildPhase.create( - cluster, - "Recipient", - "Provision the (unfunded) WIRE recipient" - ).push( - planProvisionRecipient( - Actor.User, - "provision-recipient", - `provision ${Constants.RecipientAccount} (zero WIRE until the payout)`, - writeOptions, - Constants.RecipientAccount - ) - ) - - // ── 4. Underwriter bonds on both outposts → roster flips ACTIVE; the default - // plan also funds and bonds WIRE on the depot ── - // (The old harness bonded these during bootstrap; the scenario owns them now.) - WireUnderwriterTool.planCollateralDeposit( - cluster, - "UnderwriterCollateral", - "Bond default underwriter collateral: WIRE on the depot, ETH and SOL on the outposts", - writeOptions, - underwriterLabels, - WireUnderwriterTool.load(null, config.underwriterCount) - ) - ClusterBuildPhase.create( - cluster, - "UnderwriterActive", - "Underwriter bonds credit over OPP; the roster flips ACTIVE" - ).push( - verifyStep( - Actor.Underwriter, - "underwriters-active", - "every underwriter reaches OPERATOR_STATUS_ACTIVE (deposits credit)", - async ctx => { - await pollUntil( - "every underwriter OPERATOR_STATUS_ACTIVE", - () => underwritersActive(ctx, underwriterLabels), - Constants.relayDeadlineMs(), - Constants.LongPollIntervalMs - ) - }, - activeStepOptions - ) - ) - - // ── 5. QuoteAndRequest — curve target + the ONE requestSwap write ── - ClusterBuildPhase.create( - cluster, - "QuoteAndRequest", - "Compute the to-WIRE curve target and submit requestSwap" - ).push( - verifyStep( - Actor.User, - "compute-target", - "compute the to-WIRE target from the source reserve curve (single-reserve cp_output)", - async ctx => { - // A WIRE destination has no destination reserve, so the curve is the - // single-reserve branch `cp_output(src.chain, src.wire, amount)` — - // and that produces the GROSS WIRE leg. The fee comes out of it: the - // recipient receives `split_wire_fee(gross).net` (what `paywire` - // pays, and what `quoteSwap(book, null, ...)` returns), while the - // reserve gives up `net + fee == gross`. Both halves are snapshotted - // so the settlement assertions can spend them separately. - const book = await ctx.reserveBook( - Constants.EthereumChainCode, - Constants.EthereumTokenCode, - Constants.EthereumReserveCode - ) - const custody = await ctx.wire.getWireBalance( - Constants.ReserveCustodyAccount - ) - const feeBps = await WireReserveTool.readFeeBps(ctx.wire) - const grossWireLeg = WireReserveTool.tokenToWire( - book.chain, - book.wire, - book.connectorWeightBps, - Constants.SourceDepotUnits - ) - // ONE split of the GROSS leg supplies both halves: `net` is what the - // recipient receives and every share is a slice of that same gross. - // Splitting `target` instead would charge the fee twice. - const wireLegFee = WireReserveTool.splitWireFee(grossWireLeg, feeBps), - target = wireLegFee.net - Assert.ok(target > 0n, "post-fee curve target must be positive") - ctx.outputs - .set(SwapToWireScenario.Output.bookBefore, book) - .set(SwapToWireScenario.Output.custodyBefore, custody) - .set(SwapToWireScenario.Output.target, target) - .set(SwapToWireScenario.Output.wireLegFee, wireLegFee) - log.info( - `[SwapToWire] gross curve leg = ${grossWireLeg}, post-fee target = ${target} WIRE base units (fee_bps=${feeBps})` - ) - } - ), - planRequestSwap( - Actor.User, - "request-swap", - `requestSwap: ${Constants.SourceEthereumWei} wei ETH → WIRE (sentinel reserve, recipient account bytes)`, - writeOptions, - Constants.SourceEthereumWei, - Constants.ToleranceBps - ) - ) - - // ── 6. CreateUwreq — the PENDING to-WIRE UWREQ appears ── - ClusterBuildPhase.create( - cluster, - "CreateUwreq", - "The depot creates the PENDING to-WIRE UWREQ" - ).push( - verifyStep( - Actor.Sysio, - "uwreq-appears", - "the to-WIRE UWREQ row appears with dst token = WIRE", - async ctx => { - await pollUntil( - "to-WIRE UWREQ row appears", - async () => - (await ctx.uwreq( - Constants.EthereumChainCode, - Constants.WireChainCode - )) != null, - Constants.UwreqDeadlineMs, - Constants.LongPollIntervalMs - ) - const request = await assertToWireUwreq(ctx) - Assert.ok( - slugValue(request.dst_token_code) === Constants.WireTokenCode, - `uwreq dst_token_code must be WIRE (got ${slugValue(request.dst_token_code)})` - ) - }, - uwreqStepOptions - ) - ) - - // ── 7. RaceSourceLeg — CONFIRMED with exactly ONE lock (source leg) ── - ClusterBuildPhase.create( - cluster, - "RaceSourceLeg", - "The single-leg race resolves with one source-chain lock" - ).push( - verifyStep( - Actor.Underwriter, - "race-confirms-single-lock", - "uwreq flips CONFIRMED with exactly one lock, on the SOURCE chain", - async ctx => { - await pollUntil( - "to-WIRE UWREQ status=CONFIRMED", - async () => { - const request = await ctx.uwreq( - Constants.EthereumChainCode, - Constants.WireChainCode - ) - return ( - request != null && - matchesProtoEnum( - request.status, - SysioUwritUnderwriterequeststatus, - SysioUwritUnderwriterequeststatus.UNDERWRITE_REQUEST_STATUS_CONFIRMED - ) - ) - }, - Constants.RaceDeadlineMs, - Constants.LongPollIntervalMs - ) - // The WIRE leg carries no bond — only the ETH source leg is locked. - const request = await assertToWireUwreq(ctx) - const locks = await ctx.locksForUwreq(Number(request.id)) - Assert.ok( - locks.length === 1, - `the WIRE leg carries no bond — expected exactly 1 lock, got ${locks.length}` - ) - Assert.ok( - slugValue(locks[0].chain_code) === Constants.EthereumChainCode, - `the single lock must sit on the SOURCE chain (got chain_code=${slugValue(locks[0].chain_code)})` - ) - }, - raceStepOptions - ) - ) - - // ── 8. PayWireDirectly — exact payout, book movement, custody drain ── - ClusterBuildPhase.create( - cluster, - "PayWireDirectly", - "The depot pays the recipient REAL WIRE inline and books the source reserve" - ).push( - verifyStep( - Actor.Sysio, - "recipient-paid-exact", - "recipient receives EXACTLY the post-fee target (the fee comes out of the WIRE leg)", - async ctx => { - const recipient = ctx.outputs.assert( - SwapToWireScenario.Output.recipient - ), - target = ctx.outputs.assert(SwapToWireScenario.Output.target) - // paywire CREDITS the recipient rather than transferring: it settles inside the - // never-throw consensus dispatch chain, where a pushed `sysio.token::transfer` would let - // the recipient's notify handler abort the delivery. So wait for the claimable balance, - // not the token balance — the latter stays at zero until the recipient pulls it. - await pollUntil( - "recipient claimable WIRE reaches the target", - claimableReachedPredicate(ctx, recipient.account, target), - Constants.PayoutDeadlineMs, - Constants.LongPollIntervalMs - ) - // Bridging into WIRE is a two-step flow now: settle, then claim. - await ctx.wire.claimWire(recipient.account) - // paywire pays `dst_amount` exactly, and since #550 `dst_amount` is - // the depot's own quote — `split_wire_fee(gross).net` — not the - // caller's `target_amount`. The fee is borne by the swapper, not by - // the reserve: the recipient gets the post-fee WIRE. - const received = await ctx.wire.getWireBalance(recipient.account) - Assert.ok( - received === target, - `paywire pays the post-fee quote exactly — expected ${target}, got ${received}` - ) - }, - payoutStepOptions - ), - verifyStep( - Actor.Sysio, - "source-book-moved", - "source reserve books moved at emit: chain += src, wire -= target + fee", - async ctx => { - // Applied in the SAME transaction as the race resolution (emit-time - // settlement). #414 charges the WIRE-leg fee on the gross - // intermediate ON TOP of the recipient's payout, so the source - // reserve's WIRE side gives up target + fee. - const before = ctx.outputs.assert( - SwapToWireScenario.Output.bookBefore - ), - target = ctx.outputs.assert(SwapToWireScenario.Output.target), - { fee } = ctx.outputs.assert(SwapToWireScenario.Output.wireLegFee) - const book = await ctx.reserveBook( - Constants.EthereumChainCode, - Constants.EthereumTokenCode, - Constants.EthereumReserveCode - ) - Assert.ok( - book.chain === before.chain + Constants.SourceDepotUnits, - `source reserve chain side must gain the escrow — expected ${before.chain + Constants.SourceDepotUnits}, got ${book.chain}` - ) - Assert.ok( - book.wire === before.wire - target - fee, - `source reserve WIRE side must give up target + fee — expected ${before.wire - target - fee}, got ${book.wire}` - ) - } - ), - verifyStep( - Actor.Sysio, - "custody-settled", - "sysio.reserv custody drains to before − target − the rewards half of the fee", - async ctx => { - // The recipient's payout leaves `sysio.reserv`. Of the WIRE-leg fee, - // only the batch-operator rewards half follows it out — drained each - // epoch by payepoch (sysio.reserv::drainrewards). The winning - // underwriter's half stays in custody as a `uwfees` accrual until that - // account calls `claimuwfee`, which this flow never does. The drain - // can land just after emit, so poll until custody settles. - const custodyBefore = ctx.outputs.assert( - SwapToWireScenario.Output.custodyBefore - ), - target = ctx.outputs.assert(SwapToWireScenario.Output.target), - // The payout leg only leaves custody once the recipient claims it, - // which the recipient-paid-exact step above already did. - { rewardShare } = ctx.outputs.assert( - SwapToWireScenario.Output.wireLegFee - ), - expectedCustody = custodyBefore - target - rewardShare - await pollUntil( - "rewards bucket drained from sysio.reserv custody", - async () => - (await ctx.wire.getWireBalance( - Constants.ReserveCustodyAccount - )) === expectedCustody, - Constants.PayoutDeadlineMs, - Constants.LongPollIntervalMs - ) - const custody = await ctx.wire.getWireBalance( - Constants.ReserveCustodyAccount - ) - Assert.ok( - custody === expectedCustody, - `sysio.reserv custody must settle at ${expectedCustody}, got ${custody}` - ) - }, - payoutStepOptions - ) - ) - - // ── 9. ChallengeWindow — the lock persists; nothing rides OPP outbound ── - ClusterBuildPhase.create( - cluster, - "ChallengeWindow", - "The source-leg lock persists and no SWAP_REMIT was queued" - ).push( - verifyStep( - Actor.Underwriter, - "lock-persists", - "the source-leg lock PERSISTS after payout (wall-clock challenge window)", - async ctx => { - // Locks are a wall-clock challenge window — delivery does NOT release - // them (chklocks sweeps them after collateral_lock_duration_ms). - const request = await assertToWireUwreq(ctx) - const locks = await ctx.locksForUwreq(Number(request.id)) - Assert.ok( - locks.length === 1, - `the source-leg lock must persist through the challenge window — expected 1, got ${locks.length}` - ) - // And the uwreq stays CONFIRMED until that window elapses. - Assert.ok( - matchesProtoEnum( - request.status, - SysioUwritUnderwriterequeststatus, - SysioUwritUnderwriterequeststatus.UNDERWRITE_REQUEST_STATUS_CONFIRMED - ), - `uwreq must stay CONFIRMED until the lock window elapses (status=${request.status})` - ) - } - ), - verifyStep( - Actor.Sysio, - "no-swap-remit", - "no outbound SWAP_REMIT was queued for the to-WIRE uwreq (the depot itself is the payer)", - async ctx => { - const request = await assertToWireUwreq(ctx) - const { rows } = await ctx.wire - .getSysioContract(SysioContractName.msgch) - .tables.attestations.query({ - limit: Constants.AttestationScanRowLimit - }) - // SwapRemit.original_message_id low 8 bytes = uwreq id (LE) — the - // id's low byte leads the hex-encoded attestation data. - const requestIdHexLittleEndian = Number(request.id) - .toString(16) - .padStart(Constants.UwreqIdHexByteWidth, "0") - const remitForRequest = rows.find( - attestation => - matchesProtoEnum( - attestation.type, - SysioMsgchAttestationtype, - SysioMsgchAttestationtype.ATTESTATION_TYPE_SWAP_REMIT - ) && - typeof attestation.data === "string" && - attestation.data.length > 0 && - attestation.data.includes(requestIdHexLittleEndian) - ) - Assert.ok( - remitForRequest == null, - `the depot pays the WIRE leg inline — no outbound SWAP_REMIT may reference uwreq ${request.id}` - ) - } - ) - ) - } -} - -export namespace SwapToWireScenario { - /** Typed cross-step outputs — no raw string keys, no shared mutable closures. */ - export namespace Output { - /** The provisioned WIRE recipient (account + ChainAddress byte encoding). */ - export const recipient = outputKey( - "swapToWire.recipient", - "the provisioned WIRE recipient" - ) - /** - * The POST-FEE WIRE target the request carries — `split_wire_fee(gross).net` - * over the single-reserve curve leg. `paywire` pays this exactly. - */ - export const target = outputKey( - "swapToWire.target", - "post-fee single-reserve cp_output WIRE target" - ) - /** - * The WIRE-leg fee decomposition charged on the GROSS curve leg (at the - * live uwconfig `fee_bps`). `target + fee` is the gross — what the source - * reserve gives up and what custody drains by. The whole `fee` leaves the - * source reserve's WIRE side; only `rewardShare` ever leaves `sysio.reserv` - * CUSTODY (at the payepoch drain) — the underwriter half stays until that - * account claims it. - */ - export const wireLegFee = outputKey( - "swapToWire.wireLegFee", - "WIRE-leg fee decomposition charged on the gross curve leg" - ) - /** The ETHEREUM/ETH/PRIMARY book snapshot taken at quote time. */ - export const bookBefore = outputKey( - "swapToWire.bookBefore", - "source reserve (chain, wire) baseline" - ) - /** The `sysio.reserv` custody balance snapshot taken at quote time. */ - export const custodyBefore = outputKey( - "swapToWire.custodyBefore", - "sysio.reserv WIRE custody baseline" - ) - } -} diff --git a/packages/flow-swap-to-wire/src/SwapToWireScenarioConstants.ts b/packages/flow-swap-to-wire/src/SwapToWireScenarioConstants.ts deleted file mode 100644 index 4c01704ca..000000000 --- a/packages/flow-swap-to-wire/src/SwapToWireScenarioConstants.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { SlugName } from "@wireio/sdk-core" -import { ProtocolTiming } from "@wireio/cluster-tool" - -/** - * Constants for the swap-to-WIRE flow — amounts, slug codes, and timing - * budgets. Protocol waits derive from the {@link ProtocolTiming} envelope. The - * flow is single-leg: the user escrows native ETH on the source outpost, the - * depot books the source reserve and pays the recipient REAL WIRE from - * `sysio.reserv` custody inline — no destination outpost, no SWAP_REMIT. - */ -export namespace SwapToWireScenarioConstants { - // ── Timing budgets (60s epochs per `epoch-stall-is-fatal.md`) ──────────── - - /** Epoch duration (s) — the `sysio.epoch::setconfig` floor is 60. */ - export const EpochDurationSec = 60 - /** SWAP_REQUEST relay + UWREQ insert — a single outpost→depot hop. */ - export const UwreqDeadlineMs = ProtocolTiming.SingleHopBudgetMs - /** Single-leg underwriter race (source commit only) — a single hop. */ - export const RaceDeadlineMs = ProtocolTiming.SingleHopBudgetMs - /** - * Direct WIRE payout window. The depot pays in the SAME transaction that - * resolves the race — no outpost hop — so this stays a depot-local window - * rather than an envelope class. - */ - export const PayoutDeadlineMs = 120_000 - /** Interval for long-running chain-state polls (ms). */ - export const LongPollIntervalMs = 3_000 - /** Buffer added on top of each poll deadline for the enclosing step timeout (ms). */ - export const PollDeadlineBufferMs = 30_000 - /** Timeout ceiling for the flow's on-chain WRITE steps (ms). */ - export const WriteTimeoutMs = 60_000 - /** - * Epochs budgeted for the underwriter DEPOSIT_REQUESTs to relay + credit on - * the depot and flip the underwriters ACTIVE — above the envelope's - * 4–6 minute collateral class. - */ - export const RelayEpochBudget = 9 - /** 1 s in ms — multiplies epoch counts into ms deadlines. */ - export const MsPerSecond = 1_000 - - /** Deadline for the underwriter bonds to credit over OPP and flip ACTIVE — - * extension-inclusive epochs so consecutive extended epochs still fit. */ - export function relayDeadlineMs(): number { - return ( - ProtocolTiming.effectiveEpochSec(EpochDurationSec) * - RelayEpochBudget * - MsPerSecond - ) - } - - // ── Bootstrap-seeded reserve identity (source leg) ─────────────────────── - - /** Registered chain slug codes (must match the bootstrap registry seed). */ - export const EthereumChainCode = SlugName.from("ETHEREUM") - export const SolanaChainCode = SlugName.from("SOLANA") - /** Registered token slug codes. */ - export const EthereumTokenCode = SlugName.from("ETH") - export const SolanaTokenCode = SlugName.from("SOL") - /** The bootstrap-seeded ETHEREUM/ETH reserve consumed as the source leg. */ - export const EthereumReserveCode = SlugName.from("PRIMARY") - - // ── The WIRE target identity ────────────────────────────────────────────── - - /** The depot chain/token slug codes the swap targets. */ - export const WireChainCode = SlugName.from("WIRE") - export const WireTokenCode = SlugName.from("WIRE") - /** - * The depot pays WIRE directly — there is no WIRE-side reserve — but the - * outposts require a NON-ZERO targetReserveCode, so the PRIMARY sentinel - * rides the SwapRequest and is never quoted or debited. - */ - export const WireSentinelReserveCode = SlugName.from("PRIMARY") - - // ── Swap amounts ───────────────────────────────────────────────────────── - - /** - * 0.1 ETH = 1e17 wei → 1e8 depot units (18-dec wei is above the per-token - * depot precision cap of 9, so the ETH outpost divides by 1e9). ~1% of the - * seeded reserve, so slippage stays well inside the tolerance. - */ - export const SourceEthereumWei = 100_000_000_000_000_000n - /** The escrowed source amount in the depot's 9-decimal frame. */ - export const SourceDepotUnits = SourceEthereumWei / 10n ** 9n - /** 5% — generous so quote drift between poll and race never reverts. */ - export const ToleranceBps = 500 - - // ── Accounts ───────────────────────────────────────────────────────────── - - /** Swap-to-WIRE recipient — exists, holds no WIRE until the payout. */ - export const RecipientAccount = "wirercpt" - /** The depot's reserve-custody account (real WIRE backing every reserve row). */ - export const ReserveCustodyAccount = "sysio.reserv" - /** - * Per-(chain, token) collateral minimum gating underwriter ACTIVE status — - * matches {@link WireUnderwriterTool.DefaultAmount} so the default bond meets - * the minimum exactly on both outpost chains. The default plan also funds and - * bonds that amount of WIRE on the depot, which no requirement here names. - */ - export const UnderwriterMinimumBond = 1_000_000_000 - - // ── Ethereum outpost artifacts ─────────────────────────────────────────── - - /** The outpost contract holding reserves + `requestSwap` (deploy-artifact key). */ - export const ReserveManagerContractName = "ReserveManager" - - // ── Table-read limits ──────────────────────────────────────────────────── - - /** Row ceiling for the `sysio.opreg::operators` roster read. */ - export const OperatorTableRowLimit = 100 - /** - * Row ceiling for the `sysio.msgch::attestations` negative scan — generous - * versus the handful of attestation rows a short flow run accumulates, so a - * queued SWAP_REMIT cannot hide past the read window. - */ - export const AttestationScanRowLimit = 500 - - /** - * Hex characters encoding the uwreq id's low byte — `SwapRemit. - * original_message_id` carries the uwreq id in its low 8 bytes (LE), so the - * id's low byte leads the attestation's hex-encoded data payload. - */ - export const UwreqIdHexByteWidth = 2 -} diff --git a/packages/flow-swap-to-wire/src/index.ts b/packages/flow-swap-to-wire/src/index.ts deleted file mode 100644 index 6f4577705..000000000 --- a/packages/flow-swap-to-wire/src/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { FlowCLI } from "@wireio/cluster-tool" -import { SwapToWireScenario } from "./SwapToWireScenario.js" - -/** Run the swap-to-WIRE flow as an executable — exit code = report success. */ -async function main(): Promise { - const report = await FlowCLI.create(SwapToWireScenario).run() - process.exit(report.succeeded ? 0 : 1) -} - -void main() diff --git a/packages/flow-swap-to-wire/tsconfig.json b/packages/flow-swap-to-wire/tsconfig.json deleted file mode 100644 index 82b017421..000000000 --- a/packages/flow-swap-to-wire/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-swap-to-wire/tsconfig.src.json b/packages/flow-swap-to-wire/tsconfig.src.json deleted file mode 100644 index b8bf202f6..000000000 --- a/packages/flow-swap-to-wire/tsconfig.src.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "extends": "../../etc/tsconfig/tsconfig.base.jest.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "lib", - "paths": { - "@wireio/cluster-tool": [ - "../cluster-tool/src" - ], - "@wireio/cluster-tool-shared": [ - "../cluster-tool-shared/src" - ] - } - }, - "include": [ - "src" - ], - "references": [ - { - "path": "../cluster-tool-shared/tsconfig.json" - }, - { - "path": "../cluster-tool/tsconfig.json" - } - ] -} diff --git a/packages/flow-swap-variance-revert/package.json b/packages/flow-swap-variance-revert/package.json deleted file mode 100644 index 7533315ed..000000000 --- a/packages/flow-swap-variance-revert/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "@wireio/test-flow-swap-variance-revert", - "version": "0.1.17", - "private": true, - "type": "commonjs", - "description": "Flow: Swap Variance-Tolerance Revert", - "scripts": { - "build": "tsc -b tsconfig.json", - "test:disabled": "node lib/index.js", - "clean": "rm -rf lib" - }, - "dependencies": { - "@wireio/cluster-tool": "workspace:*", - "@wireio/debugging-shared": "workspace:*", - "@wireio/sdk-core": "^1.0.92", - "@wireio/opp-typescript-models": "^1.0.50", - "@wireio/shared": "^1.0.92", - "ethers": "^6.14.0" - }, - "devDependencies": { - "@types/node": "25.5.0" - } -} diff --git a/packages/flow-swap-variance-revert/src/SwapVarianceRevertScenario.ts b/packages/flow-swap-variance-revert/src/SwapVarianceRevertScenario.ts deleted file mode 100644 index 995914377..000000000 --- a/packages/flow-swap-variance-revert/src/SwapVarianceRevertScenario.ts +++ /dev/null @@ -1,445 +0,0 @@ -import Assert from "node:assert" -import type { ClusterConfig } from "@wireio/cluster-tool-shared" -import { ethers } from "ethers" -import { DebugOutpostEndpointsType } from "@wireio/opp-typescript-models" -import { oppDebuggingPath } from "@wireio/debugging-shared" -import { - contractView, - ClusterBuildPhase, - ClusterBuildStep, - EthereumCollateralTool, - FlowScenario, - Report, - SwapScenarioContext, - SwapUserIdentities, - WireReserveTool, - containsSwapRevert, - getLogger, - outputKey, - pollUntil, - requestEthereumSwap, - sleep, - swapUserOutputKey, - verifyStep, - type ClusterBuild, - type ClusterBuildOptions, - type ClusterBuildStepOptions, - type Logger, - type ReserveManagerRequestSwapContract, - type StepInput, - ClusterConfigProvider -} from "@wireio/cluster-tool" -import { SwapVarianceRevertScenarioConstants as Constants } from "./SwapVarianceRevertScenarioConstants.js" - -const log = getLogger(__filename) -const { Actor } = Report - -// ── typed cross-step outputs ───────────────────────────────────────────────── - -/** The deliberately-inflated `target_amount` (live quote × RevertMultiplier). */ -const InflatedTargetAmountKey = outputKey( - "swapVarianceRevert.inflatedTargetAmount", - "inflated swap target amount (destination-chain base units)" -) - -/** The swap user's ETH balance (wei) snapshotted before the SwapRequest write. */ -const EthereumBalanceBeforeKey = outputKey( - "swapVarianceRevert.ethereumBalanceBefore", - "swap user's pre-swap ETH balance in wei" -) - -// ── value helpers (reads / pure math — executed INSIDE runners) ────────────── - -/** - * Bind the Ethereum outpost's `ReserveManager` to `wallet` from the run's - * deploy artifacts (address from `outpost-addrs.json`, ABI from the hardhat - * artifact) — a pure value helper used inside the submit runner. - * - * @param ctx - The scenario context (carries `config.ethereumPath`). - * @param wallet - The signer the contract binds to (the swap user's wallet). - * @returns The `requestSwap`-capable contract surface. - */ -function loadReserveManager( - ctx: SwapScenarioContext, - wallet: ethers.Signer -): ReserveManagerRequestSwapContract { - const address = EthereumCollateralTool.loadOutpostAddresses( - ClusterConfigProvider.ethereumDeploymentsPath(ctx.config) - )[Constants.ReserveManagerContractName] - Assert.ok( - ethers.isAddress(address), - `SwapVarianceRevertScenario: ${Constants.ReserveManagerContractName} not in outpost-addrs.json (got ${address})` - ) - const abi = EthereumCollateralTool.loadOutpostAbi( - ctx.config.ethereumPath, - Constants.ReserveManagerContractName - ) - return contractView(address, abi, wallet) -} - -// ── Step: compute the live quote and inflate the target (setup) ────────────── - -/** Input for the quote-and-inflate setup step. */ -interface ComputeQuoteAndInflateInput extends StepInput { - readonly kind: "SwapVarianceRevertScenario.ComputeQuoteAndInflateInput" - /** The swap input in the depot's 9-decimal frame (the quote basis). */ - readonly sourceDepotAmount: bigint - /** Multiplier applied to the live quote (drives the target past tolerance). */ - readonly revertMultiplier: bigint -} - -/** - * Named runner — quote the swap live via {@link WireReserveTool.swapquote} - * (the same two-hop `src → WIRE → dst` curve with the WIRE-leg fee charged - * between the hops that the depot runs when the SwapRequest is dispatched — a - * read), assert the quote is non-zero, inflate it by `revertMultiplier`, and - * store the target in `ctx.outputs`. - */ -async function runComputeQuoteAndInflate( - ctx: SwapScenarioContext, - input: ComputeQuoteAndInflateInput, - signal: AbortSignal -): Promise { - signal.throwIfAborted() - const liveQuote = await WireReserveTool.swapquote(ctx.wire, { - from: { - chainCode: Constants.EthereumChainCode, - tokenCode: Constants.EthereumTokenCode, - reserveCode: Constants.PrimaryReserveCode - }, - fromAmount: input.sourceDepotAmount, - to: { - chainCode: Constants.SolanaChainCode, - tokenCode: Constants.SolanaTokenCode, - reserveCode: Constants.PrimaryReserveCode - } - }) - Assert.ok( - liveQuote > 0n, - `SwapVarianceRevertScenario: live swap quote must be > 0 (got ${liveQuote})` - ) - // Inflate by RevertMultiplier — far past the 50 bps tolerance. Doubling the - // target gives a 10_000 bps drift (100% off), guaranteeing the depot's - // variance check rejects on first inspection. - const inflatedTargetAmount = liveQuote * input.revertMultiplier - ctx.outputs.set(InflatedTargetAmountKey, inflatedTargetAmount) - log.info( - `[VarianceRevert] liveQuote=${liveQuote} inflatedTarget=${inflatedTargetAmount} tolerance_bps=${Constants.ToleranceBps}` - ) -} - -/** - * Named runner — snapshot the swap user's pre-swap ETH balance (a read) into - * `ctx.outputs` so the refund step can assert against it. - */ -async function runSnapshotEthereumBalance( - ctx: SwapScenarioContext, - _input: null, - signal: AbortSignal -): Promise { - signal.throwIfAborted() - const swapUser = ctx.outputs.assert(swapUserOutputKey()) - const balance = await ctx.ethereum.provider.getBalance( - swapUser.ethereumWallet.address - ) - ctx.outputs.set(EthereumBalanceBeforeKey, balance) -} - -// ── Step: the out-of-tolerance SwapRequest (the flow's ONE write) ──────────── - -/** Input for the out-of-tolerance `requestSwap` write step. */ -interface RequestSwapOutOfToleranceInput extends StepInput { - readonly kind: "SwapVarianceRevertScenario.RequestSwapOutOfToleranceInput" - /** Wei escrowed as the swap's source deposit. */ - readonly sourceAmountWei: bigint - /** Acceptable variance (bps) — far below the inflated target's drift. */ - readonly targetToleranceBps: number -} - -/** - * Named runner — ONE `ReserveManager.requestSwap(...)` write carrying the - * inflated `target_amount`, signed by the swap user's wallet. - */ -async function runRequestSwapOutOfTolerance( - ctx: SwapScenarioContext, - input: RequestSwapOutOfToleranceInput, - signal: AbortSignal -): Promise { - signal.throwIfAborted() - const swapUser = ctx.outputs.assert(swapUserOutputKey()) - const inflatedTargetAmount = ctx.outputs.assert(InflatedTargetAmountKey) - const reserveManager = loadReserveManager(ctx, swapUser.ethereumWallet) - const result = await requestEthereumSwap(reserveManager, { - sourceTokenCode: BigInt(Constants.EthereumTokenCode), - sourceReserveCode: BigInt(Constants.PrimaryReserveCode), - sourceAmountWei: input.sourceAmountWei, - targetChainCode: BigInt(Constants.SolanaChainCode), - targetTokenCode: BigInt(Constants.SolanaTokenCode), - targetReserveCode: BigInt(Constants.PrimaryReserveCode), - targetRecipient: swapUser.solanaPublicKeyBytes, - targetAmount: inflatedTargetAmount, - targetToleranceBps: input.targetToleranceBps - }) - Assert.ok( - result.transactionHash, - "SwapVarianceRevertScenario: requestSwap must return a mined transaction hash" - ) -} - -/** - * Swap Variance-Tolerance Revert — exercises `sysio.uwrit::createuwreq`'s - * variance guard end-to-end through OPP. The depot computes a live `swap_quote` - * for the (src, dst) reserves at the moment the SwapRequest is dispatched; if - * the user's `target_amount` deviates from the live quote by more than - * `target_tolerance_bps`, the depot: - * - * 1. Skips the UWREQ row entirely (no `reqs.emplace` runs). - * 2. Queues a `SWAP_REVERT` attestation back to the source outpost. - * 3. The source outpost refunds the user's source-side deposit. - * - * Canonical proof: - * - A `DEPOT_OUTPOST_ETHEREUM` envelope carrying an - * `ATTESTATION_TYPE_SWAP_REVERT` entry ({@link containsSwapRevert}). - * - The user's ETH balance returns to (initial − gas) within - * {@link SwapVarianceRevertScenarioConstants.RevertDeadlineMs}. - * - * The user passes a deliberately-inflated `target_amount` (live quote × - * {@link SwapVarianceRevertScenarioConstants.RevertMultiplier}) so the guard - * fires every time without needing to drift the reserve. - */ -export class SwapVarianceRevertScenario extends FlowScenario { - readonly name = "flow-swap-variance-revert" - readonly description = - "Out-of-tolerance SwapRequest trips the depot's variance guard: no UWREQ, SWAP_REVERT refunds the user on the ETH outpost" - - override readonly defaults: ClusterBuildOptions = { - // Seed the mock (chain, token) PRIMARY reserves this flow reads — `regreserve` - // is epoch-0-gated by the depot, so it must ride the bootstrap, not a flow phase. - enableMockReserves: true, - // The launch build of the Solana outpost withholds the reserve/swap - // bootstrap calls; this flow exercises them, so it opts in (never in plan()). - enableLaunchWithheldOperations: true, - epochDurationSec: Constants.EpochDurationSec, - requiredUnderwriterCollateral: [ - { - chainCode: Constants.EthereumChainCode, - tokenCode: Constants.EthereumTokenCode, - minimumBond: Constants.UnderwriterMinimumBond - }, - { - chainCode: Constants.SolanaChainCode, - tokenCode: Constants.SolanaTokenCode, - minimumBond: Constants.UnderwriterMinimumBond - } - ] - } - - override createContext( - config: ClusterConfig, - log: Logger - ): SwapScenarioContext { - return new SwapScenarioContext(config, log) - } - - plan(cluster: ClusterBuild): void { - const defaultStepOptions: ClusterBuildStepOptions = { - timeoutMs: Constants.DefaultStepTimeoutMs - }, - negativeAssertStepOptions: ClusterBuildStepOptions = { - timeoutMs: - Constants.UwreqNegativeAssertMs + Constants.PollDeadlineBufferMs - }, - revertStepOptions: ClusterBuildStepOptions = { - timeoutMs: Constants.RevertDeadlineMs + Constants.PollDeadlineBufferMs - } - - // ── 1. Provision the paired ETH + SOL swap end-user identity ── - SwapUserIdentities.planIdentityProvisioning( - cluster, - "ProvisionSwapUser", - "Provision the paired ETH + SOL swap end-user identity", - {} - ) - - // ── 2. Chain health + bootstrap-seeded reserves ── - ClusterBuildPhase.create( - cluster, - "ChainHealth", - "Chain liveness + bootstrap-seeded primary reserves" - ).push( - verifyStep( - Actor.Sysio, - "wire-producing-blocks", - "WIRE chain is producing blocks", - async ctx => { - const info = await ctx.wire.getInfo() - Assert.ok( - Number(info.head_block_num) > 0, - `WIRE head_block_num must be > 0 (got ${info.head_block_num})` - ) - }, - defaultStepOptions - ), - verifyStep( - Actor.Sysio, - "bootstrap-reserves-seeded", - "bootstrap seeded ETHEREUM/ETH/PRIMARY + SOLANA/SOL/PRIMARY reserves", - async ctx => { - // `reserveBook` throws when the row is absent — both reads - // succeeding IS the assertion. - await ctx.reserveBook( - Constants.EthereumChainCode, - Constants.EthereumTokenCode, - Constants.PrimaryReserveCode - ) - await ctx.reserveBook( - Constants.SolanaChainCode, - Constants.SolanaTokenCode, - Constants.PrimaryReserveCode - ) - }, - defaultStepOptions - ) - ) - - // ── 3. Quote the swap live; inflate the target past tolerance ── - ClusterBuildPhase.create( - cluster, - "ComputeQuoteAndInflate", - "Quote the swap live, inflate the user's target past tolerance, snapshot the pre-swap balance" - ).push( - ClusterBuildStep.create( - Actor.User, - "compute-quote-and-inflate", - "compute live swapquote, then inflate the user's target to exceed tolerance", - defaultStepOptions, - { - kind: "SwapVarianceRevertScenario.ComputeQuoteAndInflateInput", - sourceDepotAmount: Constants.SourceDepotAmount, - revertMultiplier: Constants.RevertMultiplier - }, - runComputeQuoteAndInflate - ), - ClusterBuildStep.create( - Actor.User, - "snapshot-ethereum-balance", - "snapshot the swap user's pre-swap ETH balance", - defaultStepOptions, - null, - runSnapshotEthereumBalance - ) - ) - - // ── 4. The out-of-tolerance SwapRequest write ── - ClusterBuildPhase.create( - cluster, - "SubmitOutOfTolerance", - "User calls ReserveManager.requestSwap with the inflated target_amount" - ).push( - ClusterBuildStep.create( - Actor.User, - "request-swap-out-of-tolerance", - `request a ${Constants.SourceEthereumWei} wei ETH → SOL swap carrying the inflated target`, - defaultStepOptions, - { - kind: "SwapVarianceRevertScenario.RequestSwapOutOfToleranceInput", - sourceAmountWei: Constants.SourceEthereumWei, - targetToleranceBps: Constants.ToleranceBps - }, - runRequestSwapOutOfTolerance - ) - ) - - // ── 5. The depot's variance guard fires: no UWREQ + SWAP_REVERT outbound ── - ClusterBuildPhase.create( - cluster, - "VarianceGuardFires", - "The depot rejects the swap without opening a UWREQ and queues SWAP_REVERT" - ).push( - verifyStep( - Actor.Sysio, - "no-uwreq-row", - "depot does NOT create a UWREQ row (createuwreq variance branch fires)", - async ctx => { - // Give the SwapRequest envelope time to reach the depot. If the - // depot's variance branch DIDN'T fire, a UWREQ row would appear - // within ~1 epoch; we wait a generous window to be sure. - await sleep(Constants.UwreqNegativeAssertMs) - const inflatedTargetAmount = ctx.outputs.assert( - InflatedTargetAmountKey - ) - const request = await ctx.uwreq( - Constants.EthereumChainCode, - Constants.SolanaChainCode - ) - Assert.ok( - request == null || - BigInt(request.dst_amount ?? 0) !== inflatedTargetAmount, - `UWREQ row ${request?.id} matches the rejected ETHEREUM→SOLANA swap (dst_amount=${request?.dst_amount}) — the variance guard did not fire` - ) - }, - negativeAssertStepOptions - ), - verifyStep( - Actor.EthereumOutpost, - "swap-revert-envelope", - "SWAP_REVERT envelope queued outbound to the ETHEREUM outpost", - async ctx => { - await pollUntil( - "SWAP_REVERT envelope appears for ETHEREUM outpost", - async () => - containsSwapRevert( - oppDebuggingPath(ctx.config.clusterPath), - DebugOutpostEndpointsType.DEPOT_OUTPOST_ETHEREUM - ), - Constants.RevertDeadlineMs, - Constants.LongPollIntervalMs - ) - }, - revertStepOptions - ) - ) - - // ── 6. The ETH outpost refunds the user's source-side deposit ── - ClusterBuildPhase.create( - cluster, - "RefundOnOutpost", - "The ETH outpost processes SWAP_REVERT; the user's source deposit is refunded" - ).push( - verifyStep( - Actor.EthereumOutpost, - "refund-restores-balance", - "user's ETH balance returns to ~initial (source-side refund landed)", - async ctx => { - const swapUser = ctx.outputs.assert(swapUserOutputKey()) - const balanceBefore = ctx.outputs.assert(EthereumBalanceBeforeKey) - // After the SwapRequest deducted sourceAmountWei + gas, the refund - // should restore sourceAmountWei. Tolerate up to MaxGasReservedWei - // for the request tx gas. - const refundFloor = balanceBefore - Constants.MaxGasReservedWei - await pollUntil( - "user ETH balance back to (initial − gas)", - async () => - (await ctx.ethereum.provider.getBalance( - swapUser.ethereumWallet.address - )) >= refundFloor, - Constants.RevertDeadlineMs, - Constants.LongPollIntervalMs - ) - const finalBalance = await ctx.ethereum.provider.getBalance( - swapUser.ethereumWallet.address - ) - const spent = balanceBefore - finalBalance - log.info( - `[VarianceRevert] user spent ${spent} wei (= gas only; source deposit refunded)` - ) - Assert.ok( - spent < Constants.MaxGasReservedWei, - `user spent ${spent} wei — exceeds the ${Constants.MaxGasReservedWei} wei gas ceiling, so the refund did not land` - ) - }, - revertStepOptions - ) - ) - } -} diff --git a/packages/flow-swap-variance-revert/src/SwapVarianceRevertScenarioConstants.ts b/packages/flow-swap-variance-revert/src/SwapVarianceRevertScenarioConstants.ts deleted file mode 100644 index a4f8de40b..000000000 --- a/packages/flow-swap-variance-revert/src/SwapVarianceRevertScenarioConstants.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { SlugName } from "@wireio/sdk-core" -import { ProtocolTiming } from "@wireio/cluster-tool" - -/** - * Constants for the swap variance-tolerance revert flow. Amounts mirror - * `flow-swap-with-underwriting` so both flows run against the same bootstrap - * reserve seed; protocol waits derive from the {@link ProtocolTiming} - * envelope. The user requests an ETHEREUM/ETH → SOLANA/SOL swap whose - * `target_amount` is deliberately inflated past the variance tolerance, so the - * depot's `sysio.uwrit::createuwreq` guard rejects it and refunds the source - * deposit via SWAP_REVERT. - */ -export namespace SwapVarianceRevertScenarioConstants { - // ── epoch / timing ───────────────────────────────────────────────────────── - - /** Epoch duration (s) — the `sysio.epoch::setconfig` floor is 60. */ - export const EpochDurationSec = 60 - - /** - * Negative-assertion wait (ms): how long the flow sleeps before checking that - * NO UWREQ row was created. Needs to be long enough that the SwapRequest - * envelope has reached the depot and `createuwreq` has dispatched — one epoch - * (60s) plus the natural batch-op relay latency. Deliberately NOT an envelope - * budget: this is a fixed sleep, so widening it adds wall clock to every run, - * and an early check can only weaken the assertion, never flake it. - */ - export const UwreqNegativeAssertMs = 90_000 - - /** - * Time (ms) the SWAP_REVERT outbound has to reach `DEPOT_OUTPOST_ETHEREUM` - * AND for the ETH outpost to credit the user's refund. The request travels - * outpost → depot, the rejection rides the depot's NEXT outbound back to the - * outpost — a true double hop. - */ - export const RevertDeadlineMs = ProtocolTiming.DoubleHopBudgetMs - - /** Sleep between long-running chain-state polls (ms). */ - export const LongPollIntervalMs = 3_000 - - /** Buffer added on top of each poll/sleep deadline for the enclosing step timeout (ms). */ - export const PollDeadlineBufferMs = 30_000 - - /** - * Ceiling (ms) for the flow's quick read/compute/write steps — carries the - * old jest per-test default so a hung RPC fails the step instead of the run. - */ - export const DefaultStepTimeoutMs = 120_000 - - // ── registered chain / token / reserve slug codes ────────────────────────── - // Must match the bootstrap registry + reserve seed. - - /** Chain slug of the Ethereum outpost. */ - export const EthereumChainCode = SlugName.from("ETHEREUM") - /** Token slug of native ETH on the Ethereum outpost. */ - export const EthereumTokenCode = SlugName.from("ETH") - /** Chain slug of the Solana outpost. */ - export const SolanaChainCode = SlugName.from("SOLANA") - /** Token slug of native SOL on the Solana outpost. */ - export const SolanaTokenCode = SlugName.from("SOL") - /** Reserve slug shared by both bootstrap-seeded primary reserves. */ - export const PrimaryReserveCode = SlugName.from("PRIMARY") - - // ── collateral ───────────────────────────────────────────────────────────── - - /** - * Per-(chain, token) minimum underwriter bond the depot config requires — - * mirrors `flow-swap-with-underwriting` so both flows share one cluster shape - * (no underwriting actually settles here; the guard rejects pre-UWREQ). - */ - export const UnderwriterMinimumBond = 1_000_000_000 - - // ── swap amounts ─────────────────────────────────────────────────────────── - - /** - * Source wei (1e18) escrowed into the swap — 0.05 ETH. Converted through - * {@link WeiPerDepotUnit} so the quote math operates on the same 9-decimal - * magnitude as the reserve seed. - */ - export const SourceEthereumWei = 50_000_000_000_000_000n - - /** Wei per depot 9-decimal unit — converts wei (1e18) into the depot's frame. */ - export const WeiPerDepotUnit = 1_000_000_000n - - /** The swap's source amount expressed in the depot's 9-decimal frame (the quote input). */ - export const SourceDepotAmount = SourceEthereumWei / WeiPerDepotUnit - - /** - * Maximum gas (in wei) the variance-revert tx is allowed to consume. - * `ReserveManager.requestSwap` on anvil settles in ~120k gas at baseFee - * ~1 gwei; 5e15 wei (0.005 ETH) is well above the ceiling and stays under any - * realistic anvil-default balance allowance. Used to assert "user spent only - * gas" after the refund lands. - */ - export const MaxGasReservedWei = 5_000_000_000_000_000n - - // ── variance thresholds ──────────────────────────────────────────────────── - - /** Acceptable variance in basis points (0.5%). */ - export const ToleranceBps = 50 - - /** - * Multiplier applied to the live quote when constructing the deliberately- - * inflated `target_amount`. 2× the live quote gives a 10000 bps drift — 200× - * past the 50 bps tolerance, so the variance branch fires unambiguously - * regardless of small quote movement between the flow's quote read and the - * depot's `createuwreq` dispatch. - */ - export const RevertMultiplier = 2n - - // ── deploy artifacts ─────────────────────────────────────────────────────── - - /** The Ethereum outpost contract holding `requestSwap` (deploy-artifact name). */ - export const ReserveManagerContractName = "ReserveManager" -} diff --git a/packages/flow-swap-variance-revert/src/index.ts b/packages/flow-swap-variance-revert/src/index.ts deleted file mode 100644 index 3a69ad974..000000000 --- a/packages/flow-swap-variance-revert/src/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { FlowCLI } from "@wireio/cluster-tool" -import { SwapVarianceRevertScenario } from "./SwapVarianceRevertScenario.js" - -/** Run the swap variance-revert flow as an executable — exit code = report success. */ -async function main(): Promise { - const report = await FlowCLI.create(SwapVarianceRevertScenario).run() - process.exit(report.succeeded ? 0 : 1) -} - -void main() diff --git a/packages/flow-swap-variance-revert/tsconfig.json b/packages/flow-swap-variance-revert/tsconfig.json deleted file mode 100644 index 82b017421..000000000 --- a/packages/flow-swap-variance-revert/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-swap-variance-revert/tsconfig.src.json b/packages/flow-swap-variance-revert/tsconfig.src.json deleted file mode 100644 index b8bf202f6..000000000 --- a/packages/flow-swap-variance-revert/tsconfig.src.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "extends": "../../etc/tsconfig/tsconfig.base.jest.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "lib", - "paths": { - "@wireio/cluster-tool": [ - "../cluster-tool/src" - ], - "@wireio/cluster-tool-shared": [ - "../cluster-tool-shared/src" - ] - } - }, - "include": [ - "src" - ], - "references": [ - { - "path": "../cluster-tool-shared/tsconfig.json" - }, - { - "path": "../cluster-tool/tsconfig.json" - } - ] -} diff --git a/packages/flow-swap-with-underwriting/package.json b/packages/flow-swap-with-underwriting/package.json deleted file mode 100644 index 23cac99cb..000000000 --- a/packages/flow-swap-with-underwriting/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "@wireio/test-flow-swap-with-underwriting", - "version": "0.1.18", - "private": true, - "type": "commonjs", - "description": "Flow: Bidirectional SWAP (Ethereum ↔ Solana) with Underwriting", - "scripts": { - "build": "tsc -b tsconfig.json", - "test:disabled": "node lib/index.js", - "clean": "rm -rf lib" - }, - "dependencies": { - "@wireio/cluster-tool": "workspace:*", - "@wireio/sdk-core": "^1.0.92", - "@wireio/shared": "^1.0.92", - "ethers": "^6.14.0" - }, - "devDependencies": { - "@types/node": "25.5.0" - } -} diff --git a/packages/flow-swap-with-underwriting/src/SwapWithUnderwritingScenario.ts b/packages/flow-swap-with-underwriting/src/SwapWithUnderwritingScenario.ts deleted file mode 100644 index 8a7479fb4..000000000 --- a/packages/flow-swap-with-underwriting/src/SwapWithUnderwritingScenario.ts +++ /dev/null @@ -1,603 +0,0 @@ -import Assert from "node:assert" -import type { ClusterConfig } from "@wireio/cluster-tool-shared" -import { SysioContracts } from "@wireio/sdk-core" -import { getLogger, type Logger } from "@wireio/shared" -import { - ClusterBuildPhase, - Constants as ClusterConstants, - FlowScenario, - Report, - SwapScenarioContext, - SwapUserIdentities, - WireReserveTool, - WireUnderwriterTool, - matchesProtoEnum, - pollUntil, - swapUserOutputKey, - verifyStep, - type Books, - type ClusterBuild, - type ClusterBuildContext, - type ClusterBuildOptions -} from "@wireio/cluster-tool" -import { SwapWithUnderwritingScenarioConstants as Constants } from "./SwapWithUnderwritingScenarioConstants.js" -import { SwapWithUnderwritingScenarioSwapSteps as SwapSteps } from "./steps/index.js" - -const { - SysioContractName, - SysioOpregOperatorstatus, - SysioUwritUnderwriterequeststatus -} = SysioContracts -const { Actor } = Report -const log = getLogger(__filename) - -/** An operator's row on `sysio.opreg::operators` by durable `label` handle (a read). */ -async function readUnderwriterRow( - ctx: ClusterBuildContext, - label: string -): Promise { - const account = ctx.keyStore.assertOperator(label).account, - { rows } = await ctx.wire - .getSysioContract(SysioContractName.opreg) - .tables.operators.query({ limit: 100 }) - return rows.find(row => row.account === account) -} - -/** - * Full-hop (token → WIRE → token) swap quote over a pre-request book - * snapshot — {@link WireReserveTool.quoteSwap} against the SAME `Books` the - * four-sided assertion later replays, so quote and book math share one - * baseline rather than re-reading the table via - * {@link WireReserveTool.swapquote}. - * - * The WIRE-leg fee is charged BETWEEN the hops: the source side gives up the - * gross intermediate, the destination side receives only the net and pays out - * the curve output for that net. Quoting the hops fee-free would overstate the - * target by roughly `feeBps` — which a loose variance tolerance would still - * admit, so the flow would pass a request the depot settles short of. - * - * @param books - The source + destination reserve books (depot 9-decimal frame). - * @param sourceAmount - Source amount in depot 9-decimal units. - * @param feeBps - The live WIRE-leg fee in bps ({@link WireReserveTool.readFeeBps}). - * @returns The destination amount in depot units, or `0n` when either hop has - * no liquidity — the on-chain "no quote available" convention. - */ -function swapquote(books: Books, sourceAmount: bigint, feeBps: number): bigint { - return WireReserveTool.quoteSwap(books.src, books.dst, sourceAmount, feeBps) -} - -/** - * Flow: SWAP_REQUEST → underwriter race → SWAP_REMIT (bidirectional - * Ethereum ↔ Solana) — end-to-end through real outposts. - * - * On top of the bootstrap: bond every underwriter's collateral on both - * outposts (the OPP DEPOSIT_REQUEST round-trip flips `uwrit.a` ACTIVE), - * provision the swap end-user, then: - * - * **Phase A — Ethereum → Solana.** User calls `ReserveManager.requestSwap` - * with native ETH; batch operators relay the SWAP_REQUEST envelope to the - * depot; underwriter daemons commit on both outposts; depot resolves the race - * + settles the four-sided reserve books + emits SWAP_REMIT; the SOL outpost's - * `handle_swap_remit` drains lamports from the Reserve PDA to the user. - * - * **Phase B — Solana → Ethereum.** Same flow inverted via the SOL outpost's - * `request_swap` ix; the ETH outpost's `_handleSwapRemit` settles ETH to the - * user's address (depot-frame 9-decimal target × 1e9 → wei). - * - * The canonical proof in each direction is the **destination user balance - * bump** — which only happens if every protocol surface (six per direction, - * twelve total) works end-to-end. - */ -export class SwapWithUnderwritingScenario extends FlowScenario { - readonly name = "flow-swap-with-underwriting" - readonly description = - "Bidirectional Ethereum ↔ Solana swap settled by an underwriter race (Phase A + inverse Phase B)" - - override readonly defaults: ClusterBuildOptions = { - // Seed the mock (chain, token) PRIMARY reserves this flow reads — `regreserve` - // is epoch-0-gated by the depot, so it must ride the bootstrap, not a flow phase. - enableMockReserves: true, - // The launch build of the Solana outpost withholds the reserve/swap - // bootstrap calls; this flow exercises them, so it opts in (never in plan()). - enableLaunchWithheldOperations: true, - epochDurationSec: Constants.EpochDurationSec, - // The depot's `meets_role_min` rejects non-bootstrapped underwriters when - // the config is empty — `uwrit.a` must flip ACTIVE for the race to land - // any commits. The UnderwriterCollateral phase bonds - // `WireUnderwriterTool.DefaultAmount` on both outpost chains, so configuring - // the requirement at the same threshold lets `reevaluate_eligibility` call - // `processuw` to set status=ACTIVE on the second deposit round-trip. The - // phase also funds and bonds the same amount of WIRE on the depot; that bond - // is not one of these requirements. - requiredUnderwriterCollateral: [ - { - chainCode: Constants.EthereumChainCode, - tokenCode: Constants.EthereumTokenCode, - minimumBond: Constants.UnderwriterMinimumBond - }, - { - chainCode: Constants.SolanaChainCode, - tokenCode: Constants.SolanaTokenCode, - minimumBond: Constants.UnderwriterMinimumBond - } - ] - } - - override createContext( - config: ClusterConfig, - log: Logger - ): SwapScenarioContext { - return new SwapScenarioContext(config, log) - } - - plan(cluster: ClusterBuild): void { - const config = cluster.context.config, - firstUnderwriter = ClusterConstants.underwriterLabel(0), - underwriterLabels = Array.from( - { length: config.underwriterCount }, - (_, index) => ClusterConstants.underwriterLabel(index) - ), - requestStepOptions = { timeoutMs: Constants.RequestStepTimeoutMs }, - underwriterGateOptions = { - timeoutMs: - Constants.underwriterActiveDeadlineMs() + - Constants.PollDeadlineBufferMs - }, - uwreqStepOptions = { - timeoutMs: Constants.UwreqDeadlineMs + Constants.PollDeadlineBufferMs - }, - raceStepOptions = { - timeoutMs: Constants.RaceDeadlineMs + Constants.PollDeadlineBufferMs - }, - remitStepOptions = { - timeoutMs: Constants.RemitDeadlineMs + Constants.PollDeadlineBufferMs - } - - // ── 1. Bond underwriter collateral on both outposts. The bootstrap - // provisions the underwriter accounts but leaves the bonds to the - // flow; each per-(chain, token) deposit is its own Step. ── - WireUnderwriterTool.planCollateralDeposit( - cluster, - "UnderwriterCollateral", - "Bond every underwriter's collateral: WIRE on the depot, ETH and SOL on the outposts", - requestStepOptions, - underwriterLabels, - config.underwriterCollateral ?? - WireUnderwriterTool.load(null, config.underwriterCount) - ) - - // ── 2. The swap end-user's paired ETH + SOL identity (+ SOL airdrop) ── - SwapUserIdentities.planIdentityProvisioning( - cluster, - "SwapUser", - "Provision the swap end-user's Ethereum + Solana identities", - {} - ) - - // ── 3. Bootstrap state (chain health + seeded reserves) ── - ClusterBuildPhase.create( - cluster, - "BootstrapState", - "WIRE chain is live and the bootstrap seeded both PRIMARY reserves" - ).push( - verifyStep( - Actor.Sysio, - "wire-chain-producing", - "WIRE chain is producing blocks", - async (ctx: SwapScenarioContext) => { - const info = await ctx.wire.getInfo() - Assert.ok( - Number(info.head_block_num) > 0, - `head_block_num must be positive, got ${info.head_block_num}` - ) - } - ), - verifyStep( - Actor.Sysio, - "reserves-seeded", - "bootstrap seeded ETHEREUM/ETH/PRIMARY + SOLANA/SOL/PRIMARY reserves", - async (ctx: SwapScenarioContext) => { - // `reserveBook` throws when the row is absent — presence IS the check. - await ctx.reserveBook( - Constants.EthereumChainCode, - Constants.EthereumTokenCode, - Constants.PrimaryReserveCode - ) - await ctx.reserveBook( - Constants.SolanaChainCode, - Constants.SolanaTokenCode, - Constants.PrimaryReserveCode - ) - } - ) - ) - - // ── Phase A: Ethereum → Solana ── - ClusterBuildPhase.create( - cluster, - "PhaseA", - "Ethereum → Solana swap settled by the underwriter race" - ).push( - // The collateral DEPOSIT_REQUESTs must complete their OPP round-trip - // (outpost → depot `depositinle`) before the depot marks the underwriter - // ACTIVE. Without an ACTIVE underwriter no commits land for SWAP_REQUEST - // and the race never resolves. - verifyStep( - Actor.Underwriter, - "underwriter-active", - `${firstUnderwriter} flips OPERATOR_STATUS_ACTIVE once its deposits credit`, - async (ctx: SwapScenarioContext) => { - await pollUntil( - `${firstUnderwriter} ACTIVE`, - async () => { - const operator = await readUnderwriterRow(ctx, firstUnderwriter) - return ( - operator != null && - matchesProtoEnum( - operator.status, - SysioOpregOperatorstatus, - SysioOpregOperatorstatus.OPERATOR_STATUS_ACTIVE - ) - ) - }, - Constants.underwriterActiveDeadlineMs(), - Constants.LongPollIntervalMs - ) - }, - underwriterGateOptions - ), - verifyStep( - Actor.Sysio, - "swapquote-ethereum-to-solana", - "compute the ETH→SOL swapquote + snapshot books and the user's SOL balance", - async (ctx: SwapScenarioContext) => { - const swapUser = ctx.outputs.assert(swapUserOutputKey()), - booksBefore: Books = { - src: await ctx.reserveBook( - Constants.EthereumChainCode, - Constants.EthereumTokenCode, - Constants.PrimaryReserveCode - ), - dst: await ctx.reserveBook( - Constants.SolanaChainCode, - Constants.SolanaTokenCode, - Constants.PrimaryReserveCode - ) - } - // Scale source wei (1e18) → depot 9-decimal units so the - // constant-product math operates on the reserve books' magnitude; - // for SOL the depot unit IS the lamport, so the quote needs no - // outbound scaling. - const quote = swapquote( - booksBefore, - Constants.SourceEthereumWei / Constants.WeiPerDepotUnit, - await WireReserveTool.readFeeBps(ctx.wire) - ) - Assert.ok(quote > 0n, "ETH→SOL swapquote returned no quote") - ctx.outputs - .set(Constants.PhaseATargetAmountKey, quote) - .set(Constants.PhaseABooksBeforeKey, booksBefore) - .set( - Constants.PhaseASolanaBalanceBeforeKey, - await ctx.solana.getLamports(swapUser.solanaKeypair.publicKey) - ) - log.info( - `[PhaseA] swapquote = ${quote} → targetAmount = ${quote} lamports` - ) - } - ), - SwapSteps.planRequestSwapEthereum( - Actor.User, - "request-swap-ethereum", - `user calls ReserveManager.requestSwap (${Constants.SourceEthereumWei} wei ETH → SOL)`, - requestStepOptions, - { - sourceTokenCode: BigInt(Constants.EthereumTokenCode), - sourceReserveCode: BigInt(Constants.PrimaryReserveCode), - sourceAmountWei: Constants.SourceEthereumWei, - targetChainCode: BigInt(Constants.SolanaChainCode), - targetTokenCode: BigInt(Constants.SolanaTokenCode), - targetReserveCode: BigInt(Constants.PrimaryReserveCode), - targetToleranceBps: Constants.ToleranceBps - } - ), - verifyStep( - Actor.Sysio, - "uwreq-pending", - "depot creates the ETH→SOL UWREQ row", - async (ctx: SwapScenarioContext) => { - await pollUntil( - "PhaseA UWREQ row appears", - async () => - (await ctx.uwreq( - Constants.EthereumChainCode, - Constants.SolanaChainCode - )) != null, - Constants.UwreqDeadlineMs, - Constants.LongPollIntervalMs - ) - }, - uwreqStepOptions - ), - verifyStep( - Actor.Underwriter, - "uwreq-confirmed", - "UWREQ transitions to CONFIRMED with a winning underwriter", - async (ctx: SwapScenarioContext) => { - await pollUntil( - "PhaseA UWREQ status=CONFIRMED", - async () => { - const request = await ctx.uwreq( - Constants.EthereumChainCode, - Constants.SolanaChainCode - ) - return ( - request != null && - matchesProtoEnum( - request.status, - SysioUwritUnderwriterequeststatus, - SysioUwritUnderwriterequeststatus.UNDERWRITE_REQUEST_STATUS_CONFIRMED - ) - ) - }, - Constants.RaceDeadlineMs, - Constants.LongPollIntervalMs - ) - }, - raceStepOptions - ), - verifyStep( - Actor.Sysio, - "four-sided-books", - "emit-time four-sided reserve accounting + two persistent locks", - async (ctx: SwapScenarioContext) => { - // The reserve books move in the SAME transaction that resolves the - // race — before the SWAP_REMIT ever leaves the depot — so they are - // already final here, ahead of the destination payout: - // src: chain += src_amount, wire -= w - // dst: wire += w − fee, chain -= dst_amount - // with w = cp_output(src.chain, src.wire, src_amount) on the - // pre-swap source row, and the WIRE-leg fee skimmed in the hop - // (routed to rewards custody + emissions out) at the LIVE - // `sysio.uwrit::uwconfig.fee_bps` rate the bootstrap seeded. - const booksBefore = ctx.outputs.assert( - Constants.PhaseABooksBeforeKey - ), - targetAmount = ctx.outputs.assert(Constants.PhaseATargetAmountKey), - sourceAmountDepot = - Constants.SourceEthereumWei / Constants.WeiPerDepotUnit, - wireIntermediate = WireReserveTool.tokenToWire( - booksBefore.src.chain, - booksBefore.src.wire, - booksBefore.src.connectorWeightBps, - sourceAmountDepot - ), - phaseAFee = WireReserveTool.splitWireFee( - wireIntermediate, - await WireReserveTool.readFeeBps(ctx.wire) - ) - - const src = await ctx.reserveBook( - Constants.EthereumChainCode, - Constants.EthereumTokenCode, - Constants.PrimaryReserveCode - ), - dst = await ctx.reserveBook( - Constants.SolanaChainCode, - Constants.SolanaTokenCode, - Constants.PrimaryReserveCode - ) - Assert.strictEqual( - src.chain, - booksBefore.src.chain + sourceAmountDepot, - "source reserve chain side gains the escrowed amount" - ) - Assert.strictEqual( - src.wire, - booksBefore.src.wire - wireIntermediate, - "source reserve wire side gives up the full gross intermediate" - ) - Assert.strictEqual( - dst.wire, - booksBefore.dst.wire + phaseAFee.net, - "destination reserve wire side gains the post-fee net" - ) - Assert.strictEqual( - dst.chain, - booksBefore.dst.chain - targetAmount, - "destination reserve chain side pays out the curve output for the post-fee net" - ) - // The w hop is internal, but the fee is skimmed inside it — so - // Σ reserve_wire_amount across the pair drops by exactly the fee. - // The fee itself stays in `sysio.reserv` custody, split between the - // winning underwriter's `uwfees` accrual and the rewards bucket; it - // leaves the reserve ROWS, not the contract's balance. - Assert.strictEqual( - src.wire + dst.wire, - booksBefore.src.wire + booksBefore.dst.wire - phaseAFee.fee, - "Σ wire across the pair drops by exactly the WIRE-leg fee" - ) - - // Both legs locked, and the locks PERSIST (wall-clock challenge - // window — never released by delivery). - const request = await ctx.uwreq( - Constants.EthereumChainCode, - Constants.SolanaChainCode - ) - Assert.ok(request, "PhaseA UWREQ row must exist") - const locks = await ctx.locksForUwreq(Number(request.id)) - Assert.strictEqual( - locks.length, - 2, - "exactly two persistent locks back the UWREQ" - ) - - // The winner is PAID for the assurance: half the WIRE-leg fee accrues - // to their `sysio.reserv::uwfees` row at settlement, claimable on - // their own schedule via `claimuwfee`. PhaseA is this cluster's FIRST - // settled swap, so the whole balance is this swap's share. - const { winner } = request - Assert.ok(winner, "the confirmed UWREQ must name a winning underwriter") - const accrual = await ctx.underwriterFees(winner) - Assert.ok( - accrual, - `the winning underwriter ${winner} must have a uwfees accrual row` - ) - Assert.strictEqual( - BigInt(accrual.balance), - phaseAFee.underwriterShare, - "the winner's unclaimed accrual must equal the underwriter half of the WIRE-leg fee" - ) - Assert.strictEqual( - BigInt(accrual.lifetime_claimed), - 0n, - "nothing is claimed until the underwriter calls claimuwfee" - ) - } - ), - verifyStep( - Actor.SolanaOutpost, - "solana-payout", - "user's SOL balance bumps by ~targetAmount", - async (ctx: SwapScenarioContext) => { - const swapUser = ctx.outputs.assert(swapUserOutputKey()), - balanceBefore = ctx.outputs.assert( - Constants.PhaseASolanaBalanceBeforeKey - ), - targetAmount = ctx.outputs.assert(Constants.PhaseATargetAmountKey) - await pollUntil( - "PhaseA user receives SOL", - async () => { - const current = await ctx.solana.getLamports( - swapUser.solanaKeypair.publicKey - ), - drift = WireReserveTool.varianceDrift( - targetAmount, - Constants.ToleranceBps - ) - return current >= balanceBefore + Number(targetAmount - drift) - }, - Constants.RemitDeadlineMs, - Constants.LongPollIntervalMs - ) - const finalBalance = await ctx.solana.getLamports( - swapUser.solanaKeypair.publicKey - ), - received = BigInt(finalBalance - balanceBefore) - Assert.ok( - received > 0n, - `expected a positive SOL payout, received ${received}` - ) - log.info( - `[PhaseA] user received ${received} lamports (target=${targetAmount})` - ) - }, - remitStepOptions - ) - ) - - // ── Phase B: Solana → Ethereum (inverse) ── - ClusterBuildPhase.create( - cluster, - "PhaseB", - "Solana → Ethereum swap (inverse direction)" - ).push( - verifyStep( - Actor.Sysio, - "swapquote-solana-to-ethereum", - "compute the SOL→ETH swapquote + snapshot the user's ETH balance", - async (ctx: SwapScenarioContext) => { - const swapUser = ctx.outputs.assert(swapUserOutputKey()), - books: Books = { - src: await ctx.reserveBook( - Constants.SolanaChainCode, - Constants.SolanaTokenCode, - Constants.PrimaryReserveCode - ), - dst: await ctx.reserveBook( - Constants.EthereumChainCode, - Constants.EthereumTokenCode, - Constants.PrimaryReserveCode - ) - } - // Lamports are already depot 9-decimal units — no inbound scaling. - // The quote is the depot-frame target riding the OPP envelope; the - // ETH outpost scales it × 1e9 → wei when settling the SWAP_REMIT. - const quote = swapquote( - books, - Constants.SourceSolanaLamports, - await WireReserveTool.readFeeBps(ctx.wire) - ) - Assert.ok(quote > 0n, "SOL→ETH swapquote returned no quote") - ctx.outputs - .set(Constants.PhaseBTargetAmountDepotKey, quote) - .set( - Constants.PhaseBEthereumBalanceBeforeKey, - await ctx.ethereum.provider.getBalance( - swapUser.ethereumWallet.address - ) - ) - log.info( - `[PhaseB] swapquote = ${quote} → depotUnits=${quote} wei=${quote * Constants.WeiPerDepotUnit}` - ) - } - ), - SwapSteps.planRequestSwapSolana( - Actor.User, - "request-swap-solana", - `user calls opp_outpost::request_swap (${Constants.SourceSolanaLamports} lamports SOL → ETH)`, - requestStepOptions, - { - sourceTokenCode: BigInt(Constants.SolanaTokenCode), - sourceReserveCode: BigInt(Constants.PrimaryReserveCode), - sourceAmount: Constants.SourceSolanaLamports, - targetChainCode: BigInt(Constants.EthereumChainCode), - targetTokenCode: BigInt(Constants.EthereumTokenCode), - targetReserveCode: BigInt(Constants.PrimaryReserveCode), - targetToleranceBps: Constants.ToleranceBps - } - ), - verifyStep( - Actor.EthereumOutpost, - "ethereum-payout", - "user's ETH balance bumps by ~targetAmount", - async (ctx: SwapScenarioContext) => { - const swapUser = ctx.outputs.assert(swapUserOutputKey()), - balanceBefore = ctx.outputs.assert( - Constants.PhaseBEthereumBalanceBeforeKey - ), - targetAmountWei = - ctx.outputs.assert(Constants.PhaseBTargetAmountDepotKey) * - Constants.WeiPerDepotUnit - await pollUntil( - "PhaseB user receives ETH", - async () => { - const current = await ctx.ethereum.provider.getBalance( - swapUser.ethereumWallet.address - ), - drift = WireReserveTool.varianceDrift( - targetAmountWei, - Constants.ToleranceBps - ) - return current >= balanceBefore + (targetAmountWei - drift) - }, - Constants.RemitDeadlineMs, - Constants.LongPollIntervalMs - ) - const finalBalance = await ctx.ethereum.provider.getBalance( - swapUser.ethereumWallet.address - ), - received = finalBalance - balanceBefore - Assert.ok( - received > 0n, - `expected a positive ETH payout, received ${received}` - ) - log.info( - `[PhaseB] user received ${received} wei (targetWei=${targetAmountWei})` - ) - }, - remitStepOptions - ) - ) - } -} diff --git a/packages/flow-swap-with-underwriting/src/SwapWithUnderwritingScenarioConstants.ts b/packages/flow-swap-with-underwriting/src/SwapWithUnderwritingScenarioConstants.ts deleted file mode 100644 index 4970b1ace..000000000 --- a/packages/flow-swap-with-underwriting/src/SwapWithUnderwritingScenarioConstants.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { SlugName } from "@wireio/sdk-core" -import { - outputKey, - ProtocolTiming, - type Books, - type OutputKey -} from "@wireio/cluster-tool" - -/** - * Constants for the bidirectional swap-with-underwriting flow. Every - * protocol-wait budget derives from the {@link ProtocolTiming} envelope - * (collateral 4–6 min, single hop 5–7 min, double hop 10–14 min at the 60s - * minimum epoch); each direction's source amount draws 1% of the bootstrap - * reserve seed. - */ -export namespace SwapWithUnderwritingScenarioConstants { - // ── Timing ──────────────────────────────────────────────────────────────── - - /** Epoch duration (s) — the `sysio.epoch::setconfig` floor is 60. */ - export const EpochDurationSec = 60 - /** UWREQ row on the depot after the source outpost emits SWAP_REQUEST — a - * single outpost→depot hop. */ - export const UwreqDeadlineMs = ProtocolTiming.SingleHopBudgetMs - /** Underwriter race resolution (CONFIRMED): the winning commit lands on the - * destination outpost and relays back to the depot — a single hop. */ - export const RaceDeadlineMs = ProtocolTiming.SingleHopBudgetMs - /** SWAP_REMIT on the destination outpost AND the user credited — the tail of - * the full outpost→depot→outpost path, budgeted as the double hop. */ - export const RemitDeadlineMs = ProtocolTiming.DoubleHopBudgetMs - /** Sleep between long-running chain-state polls. */ - export const LongPollIntervalMs = 3_000 - /** Buffer added on top of each poll deadline for the enclosing step timeout (ms). */ - export const PollDeadlineBufferMs = 30_000 - /** Hard ceiling on each on-chain request write step (submit + confirm). */ - export const RequestStepTimeoutMs = 60_000 - /** 1 s in ms — multiplies epoch counts into ms deadlines. */ - export const MsPerSecond = 1_000 - - /** - * Epochs budgeted for the underwriter collateral DEPOSIT_REQUESTs to relay to - * the depot and flip `uwrit.a` ACTIVE — the same 9-epoch budget - * `flow-operator-collateral-deposit` validated green, comfortably above the - * envelope's 4–6 minute collateral class. - */ - export const UnderwriterActiveEpochBudget = 9 - - /** Deadline for the underwriter deposit relay + ACTIVE eligibility flip — - * extension-inclusive epochs so consecutive extended epochs still fit. */ - export function underwriterActiveDeadlineMs(): number { - return ( - ProtocolTiming.effectiveEpochSec(EpochDurationSec) * - UnderwriterActiveEpochBudget * - MsPerSecond - ) - } - - // ── Registry slugs (must match the bootstrap registry seed) ────────────── - - /** Registered chain slug codes. */ - export const EthereumChainCode = SlugName.from("ETHEREUM") - export const SolanaChainCode = SlugName.from("SOLANA") - /** Registered token slug codes. */ - export const EthereumTokenCode = SlugName.from("ETH") - export const SolanaTokenCode = SlugName.from("SOL") - /** The bootstrap-seeded reserve slug both directions swap through. */ - export const PrimaryReserveCode = SlugName.from("PRIMARY") - - /** Ethereum outpost contract the swap user calls `requestSwap` on. */ - export const ReserveManagerContractName = "ReserveManager" - - // ── Reserves & amounts ──────────────────────────────────────────────────── - - /** - * `chain_amount` + `wire_amount` the bootstrap seeds into each PRIMARY - * reserve (`RegistrySteps.planMockReserves`, depot 9-decimal frame). The - * source amounts below draw 1% of it so the constant-product quote stays deep - * in the linear region. - */ - export const InitialReserveAmount = 10_000_000_000n - - /** - * Wei per depot base unit: a token's depot precision is - * `min(nativeDecimals, 9)`, and native ETH is 18-decimal — above the cap — - * so the ETH outpost divides wei by 1e9 inbound (`PrecisionLib.toDepot`) - * and multiplies by 1e9 when settling a SWAP_REMIT. - */ - export const WeiPerDepotUnit = 10n ** 9n - - /** - * Phase A source: 0.1 ETH = 1e17 wei. The outpost converts to 1e8 depot - * 9-decimal units — 1% of {@link InitialReserveAmount}. - */ - export const SourceEthereumWei = 100_000_000_000_000_000n - - /** - * Phase B source: 0.1 SOL = 1e8 lamports. Lamports are already 9-decimal, - * so this passes through as 1e8 depot units — 1% of - * {@link InitialReserveAmount}. - */ - export const SourceSolanaLamports = 100_000_000n - - /** - * Variance tolerance the user attaches to each SwapRequest — 50 basis - * points (0.5%). Generous so quote drift between request-build and - * depot-resolve (including the wire-leg fee skim) doesn't trip the depot's - * variance check during a long E2E run. - */ - export const ToleranceBps = 50 - - /** - * Per-(chain, token) minimum bond configured via - * `requiredUnderwriterCollateral` → `sysio.opreg::setconfig(req_uw_collat)`. - * The depot's `meets_role_min` rejects non-bootstrapped underwriters when the - * config is empty, and `uwrit.a` must flip ACTIVE for the race to land any - * commits. Matches `WireUnderwriterTool.DefaultAmount` (the per-chain deposit - * this flow's UnderwriterCollateral phase bonds), so `reevaluate_eligibility` - * flips status=ACTIVE once both DEPOSIT_REQUESTs credit. The phase also funds - * and bonds that amount of WIRE on the depot, which no requirement here names. - */ - export const UnderwriterMinimumBond = 1_000_000_000 - - // ── Cross-step output keys (values ride `ctx.outputs`) ─────────────────── - - /** Phase A lamport-scale target amount computed by the swapquote step. */ - export const PhaseATargetAmountKey: OutputKey = outputKey( - "swapWithUnderwriting.phaseATargetAmount", - "Phase A target amount (lamports) from the ETH→SOL swapquote" - ) - - /** Phase A pre-request source + destination reserve books. */ - export const PhaseABooksBeforeKey: OutputKey = outputKey( - "swapWithUnderwriting.phaseABooksBefore", - "Phase A pre-request (src=ETH, dst=SOL) reserve books" - ) - - /** Phase A pre-request swap-user SOL balance (lamports). */ - export const PhaseASolanaBalanceBeforeKey: OutputKey = outputKey( - "swapWithUnderwriting.phaseASolanaBalanceBefore", - "Phase A pre-request swap-user SOL balance (lamports)" - ) - - /** Phase B depot-frame (9-decimal) target amount — rides the OPP envelope. */ - export const PhaseBTargetAmountDepotKey: OutputKey = outputKey( - "swapWithUnderwriting.phaseBTargetAmountDepot", - "Phase B target amount (depot 9-decimal units) from the SOL→ETH swapquote" - ) - - /** Phase B pre-request swap-user ETH balance (wei). */ - export const PhaseBEthereumBalanceBeforeKey: OutputKey = outputKey( - "swapWithUnderwriting.phaseBEthereumBalanceBefore", - "Phase B pre-request swap-user ETH balance (wei)" - ) -} diff --git a/packages/flow-swap-with-underwriting/src/index.ts b/packages/flow-swap-with-underwriting/src/index.ts deleted file mode 100644 index 04d2c3582..000000000 --- a/packages/flow-swap-with-underwriting/src/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { FlowCLI } from "@wireio/cluster-tool" -import { SwapWithUnderwritingScenario } from "./SwapWithUnderwritingScenario.js" - -/** Run the swap-with-underwriting flow as an executable — exit code = report success. */ -async function main(): Promise { - const report = await FlowCLI.create(SwapWithUnderwritingScenario).run() - process.exit(report.succeeded ? 0 : 1) -} - -void main() diff --git a/packages/flow-swap-with-underwriting/src/steps/SwapWithUnderwritingScenarioSwapSteps.ts b/packages/flow-swap-with-underwriting/src/steps/SwapWithUnderwritingScenarioSwapSteps.ts deleted file mode 100644 index a94475a0b..000000000 --- a/packages/flow-swap-with-underwriting/src/steps/SwapWithUnderwritingScenarioSwapSteps.ts +++ /dev/null @@ -1,260 +0,0 @@ -/** - * SwapWithUnderwritingScenarioSwapSteps — the flow-local Step factories for the - * two SWAP_REQUEST writes this scenario submits. Every on-chain WRITE is its - * own {@link ClusterBuildStep} so the `Report` records it: - * {@link SwapWithUnderwritingScenarioSwapSteps.requestSwapEthereum} (one - * `ReserveManager.requestSwap` tx) and - * {@link SwapWithUnderwritingScenarioSwapSteps.requestSwapSolana} (one - * `opp_outpost::request_swap` ix). Each runner resolves the swap user's - * identity + the quote-computed target amount from `ctx.outputs` and performs - * exactly ONE write via the harness swap tools. - */ - -import Assert from "node:assert" -import { ethers } from "ethers" -import { getLogger } from "@wireio/shared" -import { - contractView, - ClusterBuildStep, - EthereumCollateralTool, - SolanaCollateralTool, - requestEthereumSwap, - requestSolanaSwap, - swapUserOutputKey, - type ClusterBuildContext, - type ClusterBuildStepOptions, - type Report, - type ReserveManagerRequestSwapContract, - type StepInput, - ClusterConfigProvider -} from "@wireio/cluster-tool" -import { SwapWithUnderwritingScenarioConstants as Constants } from "../SwapWithUnderwritingScenarioConstants.js" - -const log = getLogger(__filename) - -export namespace SwapWithUnderwritingScenarioSwapSteps { - // ── Step: Phase A SWAP_REQUEST (`ReserveManager.requestSwap`) ───────────── - - /** - * Input for {@link planRequestSwapEthereum} — the static SWAP_REQUEST parameters. - * The target amount rides {@link SwapWithUnderwritingScenarioConstants.PhaseATargetAmountKey} - * and the recipient is the swap user's SOL pubkey (both resolved from - * `ctx.outputs` at run time). - */ - export interface RequestSwapEthereumInput extends StepInput { - readonly kind: "SwapWithUnderwritingScenarioSwapSteps.RequestSwapEthereumInput" - /** slug_name of the source token on the Ethereum outpost (native `ETH`). */ - readonly sourceTokenCode: bigint - /** slug_name of the source reserve. */ - readonly sourceReserveCode: bigint - /** Wei to escrow into the source reserve (`msg.value`). */ - readonly sourceAmountWei: bigint - /** slug_name of the target chain (`SOLANA`). */ - readonly targetChainCode: bigint - /** slug_name of the target token (`SOL`). */ - readonly targetTokenCode: bigint - /** slug_name of the target reserve. */ - readonly targetReserveCode: bigint - /** Acceptable variance in basis points. */ - readonly targetToleranceBps: number - } - - /** - * A single `ReserveManager.requestSwap(...)` write, signed by the swap - * user's Ethereum wallet — the Phase A (Ethereum → Solana) SWAP_REQUEST - * emission. - * - * @param actor - The narrative subject (the swap user). - * @param name - Step name (report row). - * @param description - One-line description. - * @param options - Per-step tuning (e.g. `timeoutMs`). - * @param request - The static swap parameters ({@link RequestSwapEthereumInput} minus `kind`). - * @returns The definition step. - */ - export function planRequestSwapEthereum< - C extends ClusterBuildContext = ClusterBuildContext - >( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - request: Omit - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { - kind: "SwapWithUnderwritingScenarioSwapSteps.RequestSwapEthereumInput", - ...request - }, - runRequestSwapEthereum - ) - } - - /** - * Named runner — bind `ReserveManager` to the swap user's wallet and perform - * ONE `requestSwap(...)` write. The receipt assertion (mined, status=1) lives - * in {@link requestEthereumSwap}. - */ - export async function runRequestSwapEthereum( - ctx: C, - input: RequestSwapEthereumInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - const swapUser = ctx.outputs.assert(swapUserOutputKey()) - const targetAmount = ctx.outputs.assert(Constants.PhaseATargetAmountKey) - const reserveManager = loadReserveManager(ctx, swapUser.ethereumWallet) - const result = await requestEthereumSwap(reserveManager, { - sourceTokenCode: input.sourceTokenCode, - sourceReserveCode: input.sourceReserveCode, - sourceAmountWei: input.sourceAmountWei, - targetChainCode: input.targetChainCode, - targetTokenCode: input.targetTokenCode, - targetReserveCode: input.targetReserveCode, - targetRecipient: swapUser.solanaPublicKeyBytes, - targetAmount, - targetToleranceBps: input.targetToleranceBps - }) - Assert.ok( - result.transactionHash, - "SwapWithUnderwritingScenarioSwapSteps.requestSwapEthereum: no transaction hash" - ) - log.info( - `[PhaseA] requestSwap tx=${result.transactionHash} block=${result.blockNumber} target=${targetAmount}` - ) - } - - // ── Step: Phase B SWAP_REQUEST (`opp_outpost::request_swap`) ────────────── - - /** - * Input for {@link planRequestSwapSolana} — the static SWAP_REQUEST parameters. - * The depot-frame target amount rides - * {@link SwapWithUnderwritingScenarioConstants.PhaseBTargetAmountDepotKey} - * and the recipient is the swap user's ETH address bytes (both resolved from - * `ctx.outputs` at run time). - */ - export interface RequestSwapSolanaInput extends StepInput { - readonly kind: "SwapWithUnderwritingScenarioSwapSteps.RequestSwapSolanaInput" - /** slug_name of the source token on the Solana outpost (native `SOL`). */ - readonly sourceTokenCode: bigint - /** slug_name of the source reserve. */ - readonly sourceReserveCode: bigint - /** Lamports to escrow into the source reserve. */ - readonly sourceAmount: bigint - /** slug_name of the target chain (`ETHEREUM`). */ - readonly targetChainCode: bigint - /** slug_name of the target token (`ETH`). */ - readonly targetTokenCode: bigint - /** slug_name of the target reserve. */ - readonly targetReserveCode: bigint - /** Acceptable variance in basis points. */ - readonly targetToleranceBps: number - } - - /** - * A single `opp_outpost::request_swap` ix, signed by the swap user's Solana - * keypair — the Phase B (Solana → Ethereum) SWAP_REQUEST emission. - * - * @param actor - The narrative subject (the swap user). - * @param name - Step name (report row). - * @param description - One-line description. - * @param options - Per-step tuning (e.g. `timeoutMs`). - * @param request - The static swap parameters ({@link RequestSwapSolanaInput} minus `kind`). - * @returns The definition step. - */ - export function planRequestSwapSolana< - C extends ClusterBuildContext = ClusterBuildContext - >( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - request: Omit - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { - kind: "SwapWithUnderwritingScenarioSwapSteps.RequestSwapSolanaInput", - ...request - }, - runRequestSwapSolana - ) - } - - /** - * Named runner — bind the `opp_outpost` program to the swap user's keypair - * and perform ONE `request_swap` write. Signature confirmation lives in - * {@link requestSolanaSwap}. - */ - export async function runRequestSwapSolana( - ctx: C, - input: RequestSwapSolanaInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - const swapUser = ctx.outputs.assert(swapUserOutputKey()) - const targetAmount = ctx.outputs.assert( - Constants.PhaseBTargetAmountDepotKey - ) - const program = SolanaCollateralTool.loadOppOutpostProgram( - ctx, - swapUser.solanaKeypair - ) - const signature = await requestSolanaSwap( - ctx.solana.connection, - program, - swapUser.solanaKeypair, - { - sourceTokenCode: input.sourceTokenCode, - sourceReserveCode: input.sourceReserveCode, - sourceAmount: input.sourceAmount, - targetChainCode: input.targetChainCode, - targetTokenCode: input.targetTokenCode, - targetReserveCode: input.targetReserveCode, - targetRecipient: swapUser.ethereumAddressBytes, - targetAmount, - targetToleranceBps: input.targetToleranceBps - } - ) - Assert.ok( - signature, - "SwapWithUnderwritingScenarioSwapSteps.requestSwapSolana: no transaction signature" - ) - log.info(`[PhaseB] request_swap sig=${signature} target=${targetAmount}`) - } - - // ── value helpers (artifact loads — executed INSIDE runners) ────────────── - - /** - * Resolve the `ReserveManager` contract from the run's deploy artifacts, - * bound to `wallet` — address from `outpost-addrs.json`, ABI from the - * hardhat artifact (both via {@link EthereumCollateralTool}'s loaders). - * - * @param ctx - The build context (supplies `config.ethereumPath`). - * @param wallet - The signer to bind (the swap user's wallet). - * @returns The bound `requestSwap` contract surface. - */ - export function loadReserveManager( - ctx: C, - wallet: ethers.Signer - ): ReserveManagerRequestSwapContract { - const address = EthereumCollateralTool.loadOutpostAddresses( - ClusterConfigProvider.ethereumDeploymentsPath(ctx.config) - )[Constants.ReserveManagerContractName] - Assert.ok( - address != null && /^0x[0-9a-fA-F]{40}$/.test(address), - `SwapWithUnderwritingScenarioSwapSteps: ${Constants.ReserveManagerContractName} not in outpost-addrs.json (got ${address})` - ) - const abi = EthereumCollateralTool.loadOutpostAbi( - ctx.config.ethereumPath, - Constants.ReserveManagerContractName - ) - return contractView(address, abi, wallet) - } -} diff --git a/packages/flow-swap-with-underwriting/src/steps/index.ts b/packages/flow-swap-with-underwriting/src/steps/index.ts deleted file mode 100644 index 97aa197ec..000000000 --- a/packages/flow-swap-with-underwriting/src/steps/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./SwapWithUnderwritingScenarioSwapSteps.js" diff --git a/packages/flow-swap-with-underwriting/tsconfig.json b/packages/flow-swap-with-underwriting/tsconfig.json deleted file mode 100644 index 82b017421..000000000 --- a/packages/flow-swap-with-underwriting/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-swap-with-underwriting/tsconfig.src.json b/packages/flow-swap-with-underwriting/tsconfig.src.json deleted file mode 100644 index b8bf202f6..000000000 --- a/packages/flow-swap-with-underwriting/tsconfig.src.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "extends": "../../etc/tsconfig/tsconfig.base.jest.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "lib", - "paths": { - "@wireio/cluster-tool": [ - "../cluster-tool/src" - ], - "@wireio/cluster-tool-shared": [ - "../cluster-tool-shared/src" - ] - } - }, - "include": [ - "src" - ], - "references": [ - { - "path": "../cluster-tool-shared/tsconfig.json" - }, - { - "path": "../cluster-tool/tsconfig.json" - } - ] -} diff --git a/packages/flow-syndication-challenge/src/SyndicationChallengeScenario.ts b/packages/flow-syndication-challenge/src/SyndicationChallengeScenario.ts index ee6fd1ddb..c8680166a 100644 --- a/packages/flow-syndication-challenge/src/SyndicationChallengeScenario.ts +++ b/packages/flow-syndication-challenge/src/SyndicationChallengeScenario.ts @@ -40,7 +40,7 @@ export class SyndicationChallengeScenario extends SyndicationScenario { epochDurationSec: SyndicationScenario.EpochDurationSec, producerCount: 3, batchOperatorCount: 3, - underwriterCount: 1 + underwriterCount: 0 } plan(cluster: ClusterBuild): void { diff --git a/packages/flow-syndication-rate-limit/src/SyndicationRateLimitScenario.ts b/packages/flow-syndication-rate-limit/src/SyndicationRateLimitScenario.ts index fa025a638..d79c2ac68 100644 --- a/packages/flow-syndication-rate-limit/src/SyndicationRateLimitScenario.ts +++ b/packages/flow-syndication-rate-limit/src/SyndicationRateLimitScenario.ts @@ -63,7 +63,7 @@ export class SyndicationRateLimitScenario extends SyndicationScenario { epochDurationSec: SyndicationScenario.EpochDurationSec, producerCount: 3, batchOperatorCount: 3, - underwriterCount: 1 + underwriterCount: 0 } plan(cluster: ClusterBuild): void { diff --git a/packages/flow-syndication-underwriting/src/SyndicationUnderwritingScenario.ts b/packages/flow-syndication-underwriting/src/SyndicationUnderwritingScenario.ts index 4f57f488f..b955e8ecb 100644 --- a/packages/flow-syndication-underwriting/src/SyndicationUnderwritingScenario.ts +++ b/packages/flow-syndication-underwriting/src/SyndicationUnderwritingScenario.ts @@ -40,7 +40,7 @@ export class SyndicationUnderwritingScenario extends SyndicationScenario { epochDurationSec: SyndicationScenario.EpochDurationSec, producerCount: 3, batchOperatorCount: 3, - underwriterCount: 1 + underwriterCount: 0 } plan(cluster: ClusterBuild): void { diff --git a/packages/flow-underwriter-slashing/package.json b/packages/flow-underwriter-slashing/package.json deleted file mode 100644 index bc320aba7..000000000 --- a/packages/flow-underwriter-slashing/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "@wireio/test-flow-underwriter-slashing", - "version": "0.1.2", - "private": true, - "type": "commonjs", - "description": "Flow: Underwriter slashing via the bonded Tier-1 challenge vote (WIRE-297)", - "scripts": { - "build": "tsc -b tsconfig.json", - "test:disabled": "node lib/index.js", - "clean": "rm -rf lib" - }, - "dependencies": { - "@wireio/cluster-tool": "workspace:*", - "@wireio/sdk-core": "^1.0.92", - "@wireio/shared": "^1.0.92", - "ethers": "^6.14.0" - }, - "devDependencies": { - "@types/node": "25.5.0" - } -} diff --git a/packages/flow-underwriter-slashing/src/UnderwriterSlashingScenario.ts b/packages/flow-underwriter-slashing/src/UnderwriterSlashingScenario.ts deleted file mode 100644 index 946231f7b..000000000 --- a/packages/flow-underwriter-slashing/src/UnderwriterSlashingScenario.ts +++ /dev/null @@ -1,722 +0,0 @@ -import Assert from "node:assert" -import type { ClusterConfig } from "@wireio/cluster-tool-shared" -import { SysioContracts } from "@wireio/sdk-core" -import { getLogger, type Logger } from "@wireio/shared" -import { - ClusterBuildPhase, - Constants as ClusterConstants, - FlowScenario, - Report, - Steps, - SwapScenarioContext, - SwapUserIdentities, - WireReserveTool, - WireUnderwriterTool, - matchesProtoEnum, - pollUntil, - verifyStep, - type Books, - type ClusterBuild, - type ClusterBuildOptions, - type ClusterBuildStepOptions, - type OutputKey -} from "@wireio/cluster-tool" -import { UnderwriterSlashingScenarioConstants as Constants } from "./UnderwriterSlashingScenarioConstants.js" -import { UnderwriterSlashingScenarioOutputs as Outputs } from "./UnderwriterSlashingScenarioOutputs.js" -import { - UnderwriterSlashingScenarioChallengeSteps as ChallengeSteps, - UnderwriterSlashingScenarioSwapSteps as SwapSteps -} from "./steps/index.js" - -const { - SysioContractAccount, - SysioContractName, - SysioChalgUwchalBallot, - SysioChalgUwchalVerdict, - SysioOpregOperatorstatus, - SysioUwritUnderwriterequeststatus -} = SysioContracts -const { Actor } = Report -const log = getLogger(__filename) - -/** - * Full-hop (token → WIRE → token) swap quote over a pre-request book - * snapshot — the same thin {@link WireReserveTool.cpOutput} composition the - * other swap flows quote with (both hops fee-less constant products; the fee - * only appears in the settled books, inside the user's variance tolerance). - */ -function swapquote(books: Books, sourceAmount: bigint): bigint { - const wireIntermediate = WireReserveTool.cpOutput( - books.src.chain, - books.src.wire, - sourceAmount - ) - if (wireIntermediate === 0n) return 0n - return WireReserveTool.cpOutput( - books.dst.wire, - books.dst.chain, - wireIntermediate - ) -} - -/** - * Flow: the bonded, council-adjudicated underwriter challenge (WIRE-297) — - * end-to-end against a live cluster. The in-repo C++ suite covers the chalg - * actions in isolation; this flow covers the trigger + slash economics on the - * REAL pipeline: real swaps, a real underwriter daemon winning real races, - * real locks, and the deferred-slash collateral debit. - * - * Substrate: two identical ETH→SOL swaps (flow-swap-with-underwriting's - * Phase A, twice), yielding two CONFIRMED commitments by the same winning - * underwriter — proving challenges are per-commitment, not per-underwriter. - * - * Order matters: BOTH swaps confirm before ANY challenge, because the upheld - * challenge slashes the cluster's only underwriter (a SLASHED underwriter can - * win no further races). The REJECTED challenge therefore runs first: - * - * 1. **ChallengeRejected (commitment B).** The challenger files + escrows the - * bond; all three Tier-1 voters ballot REJECT_FORFEIT; `chkuwchal` resolves - * REJECTED_FORFEIT — the bond is CREDITED to the wrongly-challenged - * underwriter, the lock holds clear, the operator stays ACTIVE, and the - * collateral is untouched. The underwriter then pulls the forfeit via - * `claimbond`. - * 2. **ChallengeUpheld (commitment A).** Same filing; three UPHOLD ballots; - * `chkuwchal` resolves UPHELD — the underwriter flips SLASHED, the locks - * sweep through `releaselock`'s deferred-slash branch (locked collateral - * debited, outbound SLASH attestations queued), the uwreq completes, and the - * bond is credited back to the challenger, who pulls it via `claimbond`. - * Only then does `sysio.chalg` reach zero WIRE custody. - * - * Bonds are CREDITED at resolution and moved only by `claimbond`, so each - * verdict is asserted twice: once that resolution moved no WIRE (the credit - * exists, balances are unchanged), and once that the pull delivered it. That - * split is the point — `chkuwchal` can run inline under the epoch tick, where - * `sysio.token::transfer`'s `require_recipient(to)` would run the recipient's - * own code and let it abort epoch advancement. - */ -export class UnderwriterSlashingScenario extends FlowScenario { - readonly name = "flow-underwriter-slashing" - readonly description = - "Two underwritten swaps; a rejected challenge forfeits its bond to the underwriter, an upheld challenge slashes the winner via the Tier-1 vote" - - override readonly defaults: ClusterBuildOptions = { - // The swaps ride the bootstrap-seeded mock PRIMARY reserves — `regreserve` - // is epoch-0-gated by the depot, so seeding must ride the bootstrap. - enableMockReserves: true, - // The launch build of the Solana outpost withholds the reserve/swap - // bootstrap calls; this flow exercises them, so it opts in (never in plan()). - enableLaunchWithheldOperations: true, - epochDurationSec: Constants.EpochDurationSec, - // The depot's `meets_role_min` rejects non-bootstrapped underwriters when - // the config is empty — the underwriter must flip ACTIVE for the races to - // land commits (see flow-swap-with-underwriting for the full rationale). - requiredUnderwriterCollateral: [ - { - chainCode: Constants.EthereumChainCode, - tokenCode: Constants.EthereumTokenCode, - minimumBond: Constants.UnderwriterMinimumBond - }, - { - chainCode: Constants.SolanaChainCode, - tokenCode: Constants.SolanaTokenCode, - minimumBond: Constants.UnderwriterMinimumBond - } - ] - } - - override createContext( - config: ClusterConfig, - log: Logger - ): SwapScenarioContext { - return new SwapScenarioContext(config, log) - } - - plan(cluster: ClusterBuild): void { - const config = cluster.context.config, - firstUnderwriter = ClusterConstants.underwriterLabel(0), - underwriterLabels = Array.from( - { length: config.underwriterCount }, - (_, index) => ClusterConstants.underwriterLabel(index) - ), - requestStepOptions = { timeoutMs: Constants.RequestStepTimeoutMs }, - underwriterGateOptions = { - timeoutMs: - Constants.underwriterActiveDeadlineMs() + - Constants.PollDeadlineBufferMs - }, - raceStepOptions = { - timeoutMs: - Constants.UwreqDeadlineMs + - Constants.RaceDeadlineMs + - Constants.PollDeadlineBufferMs - }, - resolveStepOptions = { - timeoutMs: Constants.ResolveDeadlineMs + Constants.PollDeadlineBufferMs - } - - // ── 1. Underwriter collateral on both outposts (flips the winner ACTIVE), - // plus the WIRE the default plan funds and bonds on the depot ── - WireUnderwriterTool.planCollateralDeposit( - cluster, - "UnderwriterCollateral", - "Bond every underwriter's collateral: WIRE on the depot, ETH and SOL on the outposts", - requestStepOptions, - underwriterLabels, - config.underwriterCollateral ?? - WireUnderwriterTool.load(null, config.underwriterCount) - ) - - // ── 2. The swap end-user's paired ETH + SOL identity ── - SwapUserIdentities.planIdentityProvisioning( - cluster, - "SwapUser", - "Provision the swap end-user's Ethereum + Solana identities", - {} - ) - - // ── 3. Bootstrap state (chain health + seeded reserves) ── - ClusterBuildPhase.create( - cluster, - "BootstrapState", - "WIRE chain is live and the bootstrap seeded both PRIMARY reserves" - ).push( - verifyStep( - Actor.Sysio, - "wire-chain-producing", - "WIRE chain is producing blocks", - async (ctx: SwapScenarioContext) => { - const info = await ctx.wire.getInfo() - Assert.ok( - Number(info.head_block_num) > 0, - `head_block_num must be positive, got ${info.head_block_num}` - ) - } - ), - verifyStep( - Actor.Sysio, - "reserves-seeded", - "bootstrap seeded ETHEREUM/ETH/PRIMARY + SOLANA/SOL/PRIMARY reserves", - async (ctx: SwapScenarioContext) => { - // `reserveBook` throws when the row is absent — presence IS the check. - await ctx.reserveBook( - Constants.EthereumChainCode, - Constants.EthereumTokenCode, - Constants.PrimaryReserveCode - ) - await ctx.reserveBook( - Constants.SolanaChainCode, - Constants.SolanaTokenCode, - Constants.PrimaryReserveCode - ) - } - ) - ) - - // ── 4. The challenge cast: Tier-1 electorate + funded challenger ── - ClusterBuildPhase.create( - cluster, - "ProvisionChallengeCast", - "Create + register the 3 Tier-1 voters; create + fund the challenger" - ).push( - ...Constants.Tier1VoterNames.flatMap(voter => [ - Steps.account.planCreateKeyed( - Actor.User, - `create-${voter}`, - `create Tier-1 voter account ${voter} (shared dev key)`, - {}, - voter, - ClusterConstants.DEV_K1_PUBLIC_KEY - ), - ChallengeSteps.planForcereg( - Actor.Sysio, - `register-${voter}`, - `register ${voter} Tier-1 via roa::forcereg (the challenge electorate)`, - {}, - { owner: voter, tier: Constants.Tier1 } - ) - ]), - Steps.account.planCreateKeyed( - Actor.User, - "create-challenger", - `create the challenger account ${Constants.ChallengerAccount}`, - {}, - Constants.ChallengerAccount, - ClusterConstants.DEV_K1_PUBLIC_KEY - ), - Steps.contracts.sysio.token.planTransfer( - Actor.Sysio, - "fund-challenger", - "treasury funds the challenger's bond budget", - {}, - { - from: Constants.TreasuryAccount, - to: Constants.ChallengerAccount, - quantity: Constants.ChallengerFundingQuantity, - memo: "underwriter-challenge bond budget" - } - ) - ) - - // ── 5. Swap A + capture its CONFIRMED commitment ── - this.planSwap( - cluster, - "SwapA", - underwriterGateOptions, - raceStepOptions, - requestStepOptions, - firstUnderwriter, - Outputs.swapATargetAmount, - Outputs.commitmentA, - [] - ) - - // ── 6. Swap B + capture (excluding A's uwreq id) ── - this.planSwap( - cluster, - "SwapB", - underwriterGateOptions, - raceStepOptions, - requestStepOptions, - firstUnderwriter, - Outputs.swapBTargetAmount, - Outputs.commitmentB, - [Outputs.commitmentA] - ) - - // ── 7. The REJECTED challenge (commitment B) — runs FIRST so the - // underwriter is still healthy when it wins nothing further. ── - const rejectKeys: ChallengeSteps.ChallengeKeys = { - commitment: Outputs.commitmentB, - chalId: Outputs.challengeBId, - bond: Outputs.challengeBBond, - challengerBalanceBefore: Outputs.challengerBalanceBeforeB - } - ClusterBuildPhase.create( - cluster, - "ChallengeRejected", - "Commitment B is challenged; the council rejects with forfeit — bond to the underwriter, collateral untouched" - ).push( - verifyStep( - Actor.Sysio, - "snapshot-underwriter-balance", - "snapshot the winner's WIRE balance (the forfeit lands exactly on top)", - async (ctx: SwapScenarioContext) => { - const commitment = ctx.outputs.assert(Outputs.commitmentB) - ctx.outputs.set( - Outputs.underwriterBalanceBeforeB, - await ctx.wire.getWireBalance(commitment.underwriterAccount) - ) - } - ), - ChallengeSteps.planOpenuwchal( - Actor.User, - "open-challenge-b", - "challenger files against commitment B and escrows the bond", - requestStepOptions, - rejectKeys, - Constants.ChallengeReason, - "flow: baseless allegation — the council rejects this one" - ), - ...Constants.Tier1VoterNames.map(voter => - ChallengeSteps.planVoteuwchal( - Actor.User, - `vote-reject-${voter}`, - `${voter} ballots REJECT_FORFEIT (frivolous challenge)`, - {}, - Outputs.challengeBId, - voter, - SysioChalgUwchalBallot.REJECT_FORFEIT - ) - ), - ChallengeSteps.planChkuwchal( - Actor.User, - "crank-challenge-b", - "crank the tally — three REJECT_FORFEIT ballots clear the quorum", - requestStepOptions, - Outputs.challengeBId - ), - verifyStep( - Actor.Sysio, - "challenge-b-rejected", - "verdict REJECTED_FORFEIT: bond CREDITED to the underwriter (no transfer); holds clear; operator ACTIVE; uwreq still CONFIRMED", - async (ctx: SwapScenarioContext) => { - const chalId = ctx.outputs.assert(Outputs.challengeBId) - await ChallengeSteps.awaitVerdict( - ctx, - chalId, - SysioChalgUwchalVerdict.REJECTED_FORFEIT - ) - - const commitment = ctx.outputs.assert(Outputs.commitmentB), - bond = ctx.outputs.assert(Outputs.challengeBBond), - underwriterBefore = ctx.outputs.assert( - Outputs.underwriterBalanceBeforeB - ) - // Forfeiture is explicit council judgment — the bond is credited to - // the wrongly-challenged underwriter, to the unit. Resolution moves - // no WIRE: the crank can run under the epoch tick, where a transfer - // would execute the recipient's notification handler and let it - // abort epoch advancement. Custody stays with chalg until the pull. - Assert.strictEqual( - await ChallengeSteps.readBondCredit( - ctx, - commitment.underwriterAccount - ), - bond, - "a rejected-with-forfeit challenge credits the underwriter the whole bond" - ) - Assert.strictEqual( - await ctx.wire.getWireBalance(commitment.underwriterAccount), - underwriterBefore, - "resolution alone must not move WIRE — the credit is claimed, not pushed" - ) - // No fault: the operator stands, the locks persist unheld to their - // natural expiry, the commitment stays CONFIRMED. - const operator = await ChallengeSteps.readOperatorRow( - ctx, - commitment.underwriterAccount - ) - Assert.ok( - matchesProtoEnum( - operator.status, - SysioOpregOperatorstatus, - SysioOpregOperatorstatus.OPERATOR_STATUS_ACTIVE - ), - "a rejected challenge must leave the underwriter ACTIVE" - ) - const locks = await ctx.locksForUwreq(commitment.uwreqId) - Assert.strictEqual( - locks.length, - 2, - "both locks persist after a rejected challenge" - ) - locks.forEach(lock => - Assert.strictEqual( - Number(lock.challenge_id), - 0, - "a rejected challenge clears the lock hold" - ) - ) - log.info( - `[uwchal] challenge ${chalId} REJECTED_FORFEIT — bond ${bond} credited to ${commitment.underwriterAccount}` - ) - }, - resolveStepOptions - ), - ChallengeSteps.planClaimbond( - Actor.User, - "claim-forfeited-bond", - "the wrongly-challenged underwriter pulls its forfeited bond out of chalg custody", - requestStepOptions, - Outputs.commitmentB - ), - verifyStep( - Actor.Sysio, - "challenge-b-forfeit-paid", - "the forfeited bond lands on the underwriter exactly, once claimed", - async (ctx: SwapScenarioContext) => { - const commitment = ctx.outputs.assert(Outputs.commitmentB), - bond = ctx.outputs.assert(Outputs.challengeBBond), - underwriterBefore = ctx.outputs.assert( - Outputs.underwriterBalanceBeforeB - ) - Assert.strictEqual( - await ctx.wire.getWireBalance(commitment.underwriterAccount), - underwriterBefore + bond, - "the claimed forfeit lands on the wrongly-challenged underwriter exactly" - ) - }, - resolveStepOptions - ) - ) - - // ── 8. The UPHELD challenge (commitment A) — slash, sweep, refund. ── - const upholdKeys: ChallengeSteps.ChallengeKeys = { - commitment: Outputs.commitmentA, - chalId: Outputs.challengeAId, - bond: Outputs.challengeABond, - challengerBalanceBefore: Outputs.challengerBalanceBeforeA - } - ClusterBuildPhase.create( - cluster, - "ChallengeUpheld", - "Commitment A is challenged; the council upholds — SLASHED, locked collateral debited, bond refunded" - ).push( - ChallengeSteps.planOpenuwchal( - Actor.User, - "open-challenge-a", - "challenger files against commitment A and escrows the bond", - requestStepOptions, - upholdKeys, - Constants.ChallengeReason, - "flow: the committed source deposit does not exist" - ), - ...Constants.Tier1VoterNames.map(voter => - ChallengeSteps.planVoteuwchal( - Actor.User, - `vote-uphold-${voter}`, - `${voter} ballots UPHOLD (fault proven)`, - {}, - Outputs.challengeAId, - voter, - SysioChalgUwchalBallot.UPHOLD - ) - ), - ChallengeSteps.planChkuwchal( - Actor.User, - "crank-challenge-a", - "crank the tally — three UPHOLD ballots clear the quorum", - requestStepOptions, - Outputs.challengeAId - ), - verifyStep( - Actor.Sysio, - "challenge-a-upheld", - "verdict UPHELD: operator SLASHED; locks swept via deferred-slash; uwreq COMPLETED; bond refunded; chalg custody zero", - async (ctx: SwapScenarioContext) => { - const chalId = ctx.outputs.assert(Outputs.challengeAId) - await ChallengeSteps.awaitVerdict( - ctx, - chalId, - SysioChalgUwchalVerdict.UPHELD - ) - - const commitment = ctx.outputs.assert(Outputs.commitmentA), - bond = ctx.outputs.assert(Outputs.challengeABond), - challengerBefore = ctx.outputs.assert( - Outputs.challengerBalanceBeforeA - ) - // Slash: the verdict flips the winner's operator row SLASHED in the - // same transaction that records it. - const operator = await ChallengeSteps.readOperatorRow( - ctx, - commitment.underwriterAccount - ) - Assert.ok( - matchesProtoEnum( - operator.status, - SysioOpregOperatorstatus, - SysioOpregOperatorstatus.OPERATOR_STATUS_SLASHED - ), - "an upheld challenge must flip the underwriter SLASHED" - ) - // Sweep: releaselock's deferred-slash branch consumes both held - // locks (locked collateral debited, outbound SLASH attestations - // queued) — no lock rows survive. - const locks = await ctx.locksForUwreq(commitment.uwreqId) - Assert.strictEqual( - locks.length, - 0, - "the upheld challenge sweeps both locks via the deferred slash" - ) - // The commitment finalizes COMPLETED — never re-underwritable. - const request = await ChallengeSteps.readUwreq( - ctx, - commitment.uwreqId - ) - Assert.ok( - request != null, - "the challenged uwreq row must survive resolution" - ) - Assert.ok( - matchesProtoEnum( - request.status, - SysioUwritUnderwriterequeststatus, - SysioUwritUnderwriterequeststatus.UNDERWRITE_REQUEST_STATUS_COMPLETED - ), - "an upheld challenge finalizes the uwreq COMPLETED" - ) - // Refund: an upheld challenge credits the bond back. As with the - // forfeit, resolution moves no WIRE — the challenger is still down - // the escrow until it pulls. - Assert.strictEqual( - await ChallengeSteps.readBondCredit( - ctx, - Constants.ChallengerAccount - ), - bond, - "an upheld challenge credits the whole bond back to the challenger" - ) - Assert.strictEqual( - await ctx.wire.getWireBalance(Constants.ChallengerAccount), - challengerBefore - bond, - "resolution alone must not move WIRE — the refund is claimed, not pushed" - ) - log.info( - `[uwchal] challenge ${chalId} UPHELD — ${commitment.underwriterAccount} SLASHED, bond ${bond} credited back` - ) - }, - resolveStepOptions - ), - ChallengeSteps.planClaimbond( - Actor.User, - "claim-refunded-bond", - "the challenger pulls its refunded bond out of chalg custody", - requestStepOptions, - Constants.ChallengerAccount - ), - verifyStep( - Actor.Sysio, - "challenge-a-refund-paid", - "the refund restores the challenger exactly; both bonds settled, chalg custody zero", - async (ctx: SwapScenarioContext) => { - const challengerBefore = ctx.outputs.assert( - Outputs.challengerBalanceBeforeA - ) - Assert.strictEqual( - await ctx.wire.getWireBalance(Constants.ChallengerAccount), - challengerBefore, - "the claimed refund makes the challenger whole" - ) - // Custody: both bonds resolved AND claimed (one forfeited, one - // refunded) — sysio.chalg escrows nothing at rest. - Assert.strictEqual( - await ctx.wire.getWireBalance( - SysioContractAccount[SysioContractName.chalg] - ), - 0n, - "sysio.chalg ends the flow with zero WIRE custody" - ) - }, - resolveStepOptions - ) - ) - } - - /** - * One ETH→SOL swap phase: gate on the underwriter being ACTIVE, quote the - * live books, submit `ReserveManager.requestSwap`, then capture the - * CONFIRMED commitment (uwreq id + winner) and assert its two persistent - * locks. Commitments already captured under `excludeCommitmentKeys` are - * excluded from the capture read, keeping the two same-direction swaps - * distinguishable. - */ - private planSwap( - cluster: ClusterBuild, - phaseName: string, - underwriterGateOptions: ClusterBuildStepOptions, - raceStepOptions: ClusterBuildStepOptions, - requestStepOptions: ClusterBuildStepOptions, - underwriterLabel: string, - targetAmountKey: OutputKey, - commitmentKey: OutputKey, - excludeCommitmentKeys: ReadonlyArray< - OutputKey - > - ): void { - ClusterBuildPhase.create( - cluster, - phaseName, - "ETH→SOL swap — request, underwriter race, and capture of the CONFIRMED commitment" - ).push( - // The collateral DEPOSIT_REQUESTs must complete their OPP round-trip - // before the depot marks the underwriter ACTIVE; without it no commits - // land and the race never resolves. (Already-ACTIVE resolves instantly.) - verifyStep( - Actor.Underwriter, - "underwriter-active", - `${underwriterLabel} is OPERATOR_STATUS_ACTIVE (deposits credited)`, - async (ctx: SwapScenarioContext) => { - const account = ctx.keyStore.assertOperator(underwriterLabel).account - await pollUntil( - `${underwriterLabel} ACTIVE`, - async () => { - const operator = await ChallengeSteps.readOperatorRow( - ctx, - account - ) - return ( - operator != null && - matchesProtoEnum( - operator.status, - SysioOpregOperatorstatus, - SysioOpregOperatorstatus.OPERATOR_STATUS_ACTIVE - ) - ) - }, - Constants.underwriterActiveDeadlineMs(), - Constants.LongPollIntervalMs - ) - }, - underwriterGateOptions - ), - verifyStep( - Actor.Sysio, - "swapquote", - "compute the ETH→SOL swapquote over the live books", - async (ctx: SwapScenarioContext) => { - const books: Books = { - src: await ctx.reserveBook( - Constants.EthereumChainCode, - Constants.EthereumTokenCode, - Constants.PrimaryReserveCode - ), - dst: await ctx.reserveBook( - Constants.SolanaChainCode, - Constants.SolanaTokenCode, - Constants.PrimaryReserveCode - ) - } - // Scale source wei (1e18) → depot 9-decimal units; for SOL the - // depot unit IS the lamport, so the quote needs no outbound scaling. - const quote = swapquote( - books, - Constants.SourceEthereumWei / Constants.WeiPerDepotUnit - ) - Assert.ok( - quote > 0n, - `${phaseName} ETH→SOL swapquote returned no quote` - ) - ctx.outputs.set(targetAmountKey, quote) - log.info(`[${phaseName}] swapquote = ${quote} lamports`) - } - ), - SwapSteps.planRequestSwapEthereum( - Actor.User, - "request-swap", - `user calls ReserveManager.requestSwap (${Constants.SourceEthereumWei} wei ETH → SOL)`, - requestStepOptions, - targetAmountKey, - { - sourceAmountWei: Constants.SourceEthereumWei, - targetToleranceBps: Constants.ToleranceBps - } - ), - verifyStep( - Actor.Underwriter, - "capture-commitment", - "the race resolves CONFIRMED — capture (uwreq id, winner) + assert both locks", - async (ctx: SwapScenarioContext) => { - const excludeUwreqIds = excludeCommitmentKeys.map( - key => ctx.outputs.assert(key).uwreqId - ) - await pollUntil( - `${phaseName} commitment CONFIRMED`, - async () => - (await ChallengeSteps.findConfirmedCommitment( - ctx, - excludeUwreqIds - )) != null, - Constants.UwreqDeadlineMs + Constants.RaceDeadlineMs, - Constants.LongPollIntervalMs - ) - const commitment = await ChallengeSteps.findConfirmedCommitment( - ctx, - excludeUwreqIds - ) - ctx.outputs.set(commitmentKey, commitment) - const locks = await ctx.locksForUwreq(commitment.uwreqId) - Assert.strictEqual( - locks.length, - 2, - "exactly two persistent locks back the CONFIRMED commitment" - ) - log.info( - `[${phaseName}] CONFIRMED uwreq ${commitment.uwreqId} won by ${commitment.underwriterAccount}` - ) - }, - raceStepOptions - ) - ) - } -} diff --git a/packages/flow-underwriter-slashing/src/UnderwriterSlashingScenarioConstants.ts b/packages/flow-underwriter-slashing/src/UnderwriterSlashingScenarioConstants.ts deleted file mode 100644 index b626800dc..000000000 --- a/packages/flow-underwriter-slashing/src/UnderwriterSlashingScenarioConstants.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { SlugName, SysioContracts } from "@wireio/sdk-core" -import { ProtocolTiming } from "@wireio/cluster-tool" - -/** - * Constants for the underwriter-slashing flow (WIRE-297). The swap substrate - * (chains, amounts, tolerances, timing budgets) mirrors - * `flow-swap-with-underwriting`'s Phase A exactly — the same ETH→SOL swap, - * run twice so the flow holds two independently challengeable commitments. - * Every protocol-wait budget derives from the {@link ProtocolTiming} envelope. - */ -export namespace UnderwriterSlashingScenarioConstants { - // ── Timing ──────────────────────────────────────────────────────────────── - - /** Epoch duration (s) — the `sysio.epoch::setconfig` floor is 60. */ - export const EpochDurationSec = 60 - /** UWREQ row on the depot after the source outpost emits SWAP_REQUEST — a - * single outpost→depot hop. */ - export const UwreqDeadlineMs = ProtocolTiming.SingleHopBudgetMs - /** Underwriter race resolution (CONFIRMED): the winning commit lands on the - * destination outpost and relays back to the depot — a single hop. */ - export const RaceDeadlineMs = ProtocolTiming.SingleHopBudgetMs - /** Sleep between long-running chain-state polls. */ - export const LongPollIntervalMs = 3_000 - /** Buffer added on top of each poll deadline for the enclosing step timeout (ms). */ - export const PollDeadlineBufferMs = 30_000 - /** Hard ceiling on each on-chain write step (submit + confirm). */ - export const RequestStepTimeoutMs = 60_000 - /** 1 s in ms — multiplies epoch counts into ms deadlines. */ - export const MsPerSecond = 1_000 - - /** - * Epochs budgeted for the underwriter collateral DEPOSIT_REQUESTs to relay to - * the depot and flip the underwriter ACTIVE — the same 9-epoch budget the - * other underwriting flows validated green. - */ - export const UnderwriterActiveEpochBudget = 9 - - /** Deadline for the underwriter deposit relay + ACTIVE eligibility flip — - * extension-inclusive epochs so consecutive extended epochs still fit. */ - export function underwriterActiveDeadlineMs(): number { - return ( - ProtocolTiming.effectiveEpochSec(EpochDurationSec) * - UnderwriterActiveEpochBudget * - MsPerSecond - ) - } - - /** - * Deadline for a challenge resolution to take effect on the depot - * (verdict + slash + sweep, or verdict + hold-clear — all inline in one - * `chkuwchal` transaction, so a single-hop budget is generous). - */ - export const ResolveDeadlineMs = ProtocolTiming.SingleHopBudgetMs - - // ── Registry slugs (must match the bootstrap registry seed) ────────────── - - /** Registered chain slug codes. */ - export const EthereumChainCode = SlugName.from("ETHEREUM") - export const SolanaChainCode = SlugName.from("SOLANA") - /** Registered token slug codes. */ - export const EthereumTokenCode = SlugName.from("ETH") - export const SolanaTokenCode = SlugName.from("SOL") - /** The bootstrap-seeded reserve slug both swaps ride. */ - export const PrimaryReserveCode = SlugName.from("PRIMARY") - - /** Ethereum outpost contract the swap user calls `requestSwap` on. */ - export const ReserveManagerContractName = "ReserveManager" - - // ── Reserves & amounts (mirror flow-swap-with-underwriting Phase A) ────── - - /** Wei per depot base unit (native ETH is 18-decimal; the depot frame is 9). */ - export const WeiPerDepotUnit = 10n ** 9n - /** Each swap's source: 0.1 ETH = 1e17 wei → 1e8 depot units (1% of the seed). */ - export const SourceEthereumWei = 100_000_000_000_000_000n - /** Variance tolerance attached to each SwapRequest (0.5%). */ - export const ToleranceBps = 50 - /** Per-(chain, token) minimum bond for `req_uw_collat` (see the - * swap-with-underwriting constants for the full rationale). */ - export const UnderwriterMinimumBond = 1_000_000_000 - - // ── The challenge cast ──────────────────────────────────────────────────── - - /** - * The three Tier-1 voters — the flow-provisioned electorate. With the ONE - * bootstrap Tier-1 owner (`wireno`) the snapshot is N = 4, Q = ⌊4/2⌋+1 = 3, - * so all three voting the same way is exactly quorum. The scenario asserts - * the snapshotted quorum ≤ 3 after each open, so a bootstrap-roster change - * fails loudly instead of deadlocking the vote. - */ - export const Tier1VoterNames = ["uwvotera", "uwvoterb", "uwvoterc"] as const - - /** Tier value `roa::forcereg` registers the voters at (Tier-1 electorate). */ - export const Tier1 = 1 - - /** - * The bond-posting challenger. A plain keyed account (shared dev K1 key, so - * the flow signs its actions), funded from the WIRE treasury below — - * deliberately NOT a voter: filing and adjudicating stay separate parties. - */ - export const ChallengerAccount = "uwchallenger" - - /** - * WIRE the treasury grants the challenger (9-decimal units = 10 WIRE). Each - * challenge bond is the WIRE value of two ~1e8-unit legs (~0.2 WIRE against - * the seeded books), so this covers both challenges with margin — and the - * flow proves every bond comes back (refund or forfeit, never burned). - */ - export const ChallengerFundingAmount = 10_000_000_000n - /** The funding transfer's `quantity` string (asset form of the above). */ - export const ChallengerFundingQuantity = "10.000000000 WIRE" - /** The WIRE treasury account the funding transfer draws from. */ - export const TreasuryAccount = "sysio" - - /** The fault class each challenge alleges (what a real challenger would - * claim when the committed source deposit cannot be found). */ - export const ChallengeReason = - SysioContracts.SysioChalgUnderwriteFaultReason.SOURCE_DEPOSIT_MISSING -} diff --git a/packages/flow-underwriter-slashing/src/UnderwriterSlashingScenarioOutputs.ts b/packages/flow-underwriter-slashing/src/UnderwriterSlashingScenarioOutputs.ts deleted file mode 100644 index d0967ad74..000000000 --- a/packages/flow-underwriter-slashing/src/UnderwriterSlashingScenarioOutputs.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { outputKey, type OutputKey } from "@wireio/cluster-tool" - -/** - * Typed cross-step output keys for the underwriter-slashing flow. Each swap's - * quote step snapshots its target amount; each capture step records the - * CONFIRMED commitment (uwreq id + winning underwriter account); each - * challenge phase records its chalg row id, the escrowed bond, and the - * balances its resolution is measured against. Cross-step values ride - * `ctx.outputs` — never shared mutable closures. - */ -export namespace UnderwriterSlashingScenarioOutputs { - /** - * A CONFIRMED underwrite commitment under (potential) challenge: the uwreq - * row id and its winning underwriter's ON-CHAIN account (the `winner` - * field — already an account name, not a harness label). - */ - export interface ChallengedCommitment { - readonly uwreqId: number - readonly underwriterAccount: string - } - - /** Swap A's quote-computed target amount (lamports). */ - export const swapATargetAmount: OutputKey = outputKey( - "underwriterSlashing.swapA.targetAmount", - "swap A target amount (lamports) from the ETH→SOL swapquote" - ) - /** Swap B's quote-computed target amount (lamports). */ - export const swapBTargetAmount: OutputKey = outputKey( - "underwriterSlashing.swapB.targetAmount", - "swap B target amount (lamports) from the ETH→SOL swapquote" - ) - - /** Swap A's CONFIRMED commitment — the UPHELD challenge's target. */ - export const commitmentA: OutputKey = outputKey( - "underwriterSlashing.commitmentA", - "swap A's CONFIRMED (uwreq id, winner) — challenged and UPHELD" - ) - /** Swap B's CONFIRMED commitment — the REJECTED challenge's target. */ - export const commitmentB: OutputKey = outputKey( - "underwriterSlashing.commitmentB", - "swap B's CONFIRMED (uwreq id, winner) — challenged and REJECTED" - ) - - /** The uphold challenge's `sysio.chalg::uwchals` row id. */ - export const challengeAId: OutputKey = outputKey( - "underwriterSlashing.challengeA.id", - "chalg uwchals row id of the UPHELD challenge (commitment A)" - ) - /** The reject challenge's `sysio.chalg::uwchals` row id. */ - export const challengeBId: OutputKey = outputKey( - "underwriterSlashing.challengeB.id", - "chalg uwchals row id of the REJECTED challenge (commitment B)" - ) - - /** The WIRE bond the uphold challenge escrowed (9-decimal units). */ - export const challengeABond: OutputKey = outputKey( - "underwriterSlashing.challengeA.bond", - "WIRE bond escrowed by the UPHELD challenge" - ) - /** The WIRE bond the reject challenge escrowed (9-decimal units). */ - export const challengeBBond: OutputKey = outputKey( - "underwriterSlashing.challengeB.bond", - "WIRE bond escrowed by the REJECTED challenge" - ) - - /** Challenger WIRE balance immediately BEFORE filing the uphold challenge. */ - export const challengerBalanceBeforeA: OutputKey = outputKey( - "underwriterSlashing.challengeA.challengerBalanceBefore", - "challenger WIRE balance before the UPHELD challenge's escrow" - ) - /** Challenger WIRE balance immediately BEFORE filing the reject challenge. */ - export const challengerBalanceBeforeB: OutputKey = outputKey( - "underwriterSlashing.challengeB.challengerBalanceBefore", - "challenger WIRE balance before the REJECTED challenge's escrow" - ) - /** The wrongly-challenged underwriter's WIRE balance before the reject - * challenge — the forfeited bond must land exactly on top of it. */ - export const underwriterBalanceBeforeB: OutputKey = outputKey( - "underwriterSlashing.challengeB.underwriterBalanceBefore", - "winner's WIRE balance before the REJECTED challenge (forfeit baseline)" - ) -} diff --git a/packages/flow-underwriter-slashing/src/index.ts b/packages/flow-underwriter-slashing/src/index.ts deleted file mode 100644 index 873c2861a..000000000 --- a/packages/flow-underwriter-slashing/src/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { FlowCLI } from "@wireio/cluster-tool" -import { UnderwriterSlashingScenario } from "./UnderwriterSlashingScenario.js" - -/** Run the underwriter-slashing flow as an executable — exit code = report success. */ -async function main(): Promise { - const report = await FlowCLI.create(UnderwriterSlashingScenario).run() - process.exit(report.succeeded ? 0 : 1) -} - -void main() diff --git a/packages/flow-underwriter-slashing/src/steps/UnderwriterSlashingScenarioChallengeSteps.ts b/packages/flow-underwriter-slashing/src/steps/UnderwriterSlashingScenarioChallengeSteps.ts deleted file mode 100644 index 485e6b103..000000000 --- a/packages/flow-underwriter-slashing/src/steps/UnderwriterSlashingScenarioChallengeSteps.ts +++ /dev/null @@ -1,527 +0,0 @@ -/** - * UnderwriterSlashingScenarioChallengeSteps — the flow-local Step factories for - * every `sysio.chalg` underwriter-challenge WRITE this scenario submits - * (`openuwchal` / `voteuwchal` / `chkuwchal`), plus the plain read helpers the - * verify steps poll with. Every on-chain write is its own - * {@link ClusterBuildStep} so the `Report` records it; cross-step values - * (commitments, challenge ids, bonds, balance baselines) ride `ctx.outputs` - * under {@link UnderwriterSlashingScenarioOutputs}'s typed keys. - * - * Flow-local by the same precedent as `flow-batch-operator-slashing`'s dispute - * steps: these actions are this scenario's content, not shared harness - * surface. - */ - -import Assert from "node:assert" -import { getLogger } from "@wireio/shared" -import { SysioContracts } from "@wireio/sdk-core" -import { - ClusterBuildStep, - matchesProtoEnum, - pollUntil, - slugValue, - type ClusterBuildStepOptions, - type OutputKey, - type Report, - type StepInput, - type SwapScenarioContext -} from "@wireio/cluster-tool" -import { UnderwriterSlashingScenarioConstants as Constants } from "../UnderwriterSlashingScenarioConstants.js" -import { UnderwriterSlashingScenarioOutputs as Outputs } from "../UnderwriterSlashingScenarioOutputs.js" - -const { - SysioContractAccount, - SysioContractName, - SysioChalgUwchalBallot, - SysioChalgUwchalVerdict, - SysioUwritUnderwriterequeststatus -} = SysioContracts - -const log = getLogger(__filename) - -/** The standard `active` permission every flow-signed action authorizes with. */ -const ActivePermission = "active" - -export namespace UnderwriterSlashingScenarioChallengeSteps { - // ── read helpers (plain functions — reads execute freely inside runners) ── - - /** - * The newest CONFIRMED ETH→SOL uwreq NOT in `excludeUwreqIds` — how the two - * same-direction swaps are told apart (`ctx.uwreq` matches on direction - * alone, which is ambiguous once swap B lands). - * - * @param ctx - The build context. - * @param excludeUwreqIds - Uwreq ids already captured (swap A's, for swap B). - * @returns The commitment, or null while the race is still unresolved. - */ - export async function findConfirmedCommitment( - ctx: SwapScenarioContext, - excludeUwreqIds: readonly number[] - ): Promise { - const { rows } = await ctx.wire - .getSysioContract(SysioContractName.uwrit) - .tables.uwreqs.query() - const row = rows.find( - request => - slugValue(request.src_chain_code) === Constants.EthereumChainCode && - slugValue(request.dst_chain_code) === Constants.SolanaChainCode && - matchesProtoEnum( - request.status, - SysioUwritUnderwriterequeststatus, - SysioUwritUnderwriterequeststatus.UNDERWRITE_REQUEST_STATUS_CONFIRMED - ) && - !excludeUwreqIds.includes(Number(request.id)) - ) - return row == null - ? null - : { uwreqId: Number(row.id), underwriterAccount: String(row.winner) } - } - - /** The `sysio.uwrit::uwreqs` row by id. */ - export async function readUwreq( - ctx: SwapScenarioContext, - uwreqId: number - ): Promise { - const { rows } = await ctx.wire - .getSysioContract(SysioContractName.uwrit) - .tables.uwreqs.query() - return rows.find(request => Number(request.id) === uwreqId) - } - - /** - * The `sysio.chalg::uwchals` row for one challenged commitment (there is at - * most one, ever — the contract's uniqueness rule). - */ - export async function readUwchalByCommitment( - ctx: SwapScenarioContext, - commitment: Outputs.ChallengedCommitment - ): Promise { - const { rows } = await ctx.wire - .getSysioContract(SysioContractName.chalg) - .tables.uwchals.query() - return rows.find( - challenge => - Number(challenge.uwreq_id) === commitment.uwreqId && - challenge.underwriter === commitment.underwriterAccount - ) - } - - /** The `sysio.chalg::uwchals` row by id. */ - export async function readUwchal( - ctx: SwapScenarioContext, - chalId: number - ): Promise { - const { rows } = await ctx.wire - .getSysioContract(SysioContractName.chalg) - .tables.uwchals.query() - return rows.find(challenge => Number(challenge.id) === chalId) - } - - /** - * The WIRE `sysio.chalg` still owes `account` from resolved challenges — - * its `bondcredits` row, or 0n when none exists. - * - * Resolution CREDITS the bond rather than transferring it: `chkuwchal` can - * run inline under the epoch tick, where `sysio.token::transfer`'s - * `require_recipient(to)` would execute the recipient's own code and let it - * abort epoch advancement. The WIRE moves only in `claimbond`. - */ - export async function readBondCredit( - ctx: SwapScenarioContext, - account: string - ): Promise { - const { rows } = await ctx.wire - .getSysioContract(SysioContractName.chalg) - .tables.bondcredits.query() - const row = rows.find(credit => credit.account === account) - return row == null ? 0n : BigInt(row.amount) - } - - /** The `sysio.opreg::operators` row by ON-CHAIN account name. */ - export async function readOperatorRow( - ctx: SwapScenarioContext, - account: string - ): Promise { - const { rows } = await ctx.wire - .getSysioContract(SysioContractName.opreg) - .tables.operators.query() - return rows.find(operator => operator.account === account) - } - - /** - * Poll until the challenge reaches `verdict`. The tally, slash, sweep (or - * hold-clear), row compaction and bond CREDIT all ride ONE `chkuwchal` - * transaction, so once the verdict lands every consequence asserted - * afterwards is final — except the bond's delivery, which is a separate pull - * ({@link planClaimbond}). The - * verdict cell rides the ABI `enums` extension, so the RPC serializes it as - * the NAME spelling — match via {@link matchesProtoEnum}, never `Number()`. - */ - export async function awaitVerdict( - ctx: SwapScenarioContext, - chalId: number, - verdict: SysioContracts.SysioChalgUwchalVerdict - ): Promise { - await pollUntil( - `challenge ${chalId} reaches verdict ${SysioChalgUwchalVerdict[verdict]}`, - async () => { - const challenge = await readUwchal(ctx, chalId) - return ( - challenge != null && - matchesProtoEnum(challenge.verdict, SysioChalgUwchalVerdict, verdict) - ) - }, - Constants.ResolveDeadlineMs, - Constants.LongPollIntervalMs - ) - } - - // ── Step: sysio.roa::forcereg (write) ───────────────────────────────────── - - /** Input for {@link planForcereg} — the generated `roa::forcereg` data. */ - export interface ForceregInput extends StepInput { - readonly kind: "UnderwriterSlashingScenarioChallengeSteps.ForceregInput" - readonly data: SysioContracts.SysioRoaForceregAction - } - - /** - * `sysio.roa::forcereg` — register one Tier-1 voter into the electorate the - * challenge snapshots. The direct-registration path (no NFT claim): the - * batch-op slashing flow exercises the full claim registration; this flow's - * electorate is infrastructure, not the thing under test. - */ - export function planForcereg( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - data: SysioContracts.SysioRoaForceregAction - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { kind: "UnderwriterSlashingScenarioChallengeSteps.ForceregInput", data }, - runForcereg - ) - } - - /** Named runner — one `roa::forcereg` write under `sysio.roa@active`. */ - export async function runForcereg( - ctx: SwapScenarioContext, - input: ForceregInput, - signal: AbortSignal - ): Promise { - signal.throwIfAborted() - await ctx.wire - .getSysioContract(SysioContractName.roa) - .actions.forcereg.invoke(input.data, { - authorization: [ - { - actor: SysioContractAccount[SysioContractName.roa], - permission: ActivePermission - } - ] - }) - } - - // ── Step: sysio.chalg::openuwchal (write) ───────────────────────────────── - - /** Input for {@link planOpenuwchal} — the allegation's static parameters. */ - export interface OpenuwchalInput extends StepInput { - readonly kind: "UnderwriterSlashingScenarioChallengeSteps.OpenuwchalInput" - /** Numeric `underwrite_fault_reason` the challenger alleges. */ - readonly reason: SysioContracts.SysioChalgUnderwriteFaultReason - /** Free-text evidence context recorded on the challenge row. */ - readonly detail: string - } - - /** The output keys one challenge phase writes/reads — bundled so both - * challenge phases reuse the same factories with their own key set. */ - export interface ChallengeKeys { - /** The commitment under challenge (set by the capture step). */ - readonly commitment: OutputKey - /** Where the opened challenge's row id lands. */ - readonly chalId: OutputKey - /** Where the escrowed bond amount lands. */ - readonly bond: OutputKey - /** Where the challenger's pre-escrow balance lands. */ - readonly challengerBalanceBefore: OutputKey - } - - /** - * `sysio.chalg::openuwchal` — file the challenge against the captured - * commitment. ONE write; the runner then reads back the challenge row it - * created (id + bond) into `keys`, and asserts the bond actually left the - * challenger (escrow is an inline transfer inside the same transaction) and - * that the snapshotted quorum is reachable by the flow's three voters. - */ - export function planOpenuwchal( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - keys: ChallengeKeys, - reason: SysioContracts.SysioChalgUnderwriteFaultReason, - detail: string - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { - kind: "UnderwriterSlashingScenarioChallengeSteps.OpenuwchalInput", - reason, - detail - }, - async (ctx, input, signal) => runOpenuwchal(ctx, input, signal, keys) - ) - } - - /** Named runner body for {@link planOpenuwchal} (keys bound by the factory). */ - export async function runOpenuwchal( - ctx: SwapScenarioContext, - input: OpenuwchalInput, - signal: AbortSignal, - keys: ChallengeKeys - ): Promise { - signal.throwIfAborted() - const commitment = ctx.outputs.assert(keys.commitment) - const balanceBefore = await ctx.wire.getWireBalance( - Constants.ChallengerAccount - ) - ctx.outputs.set(keys.challengerBalanceBefore, balanceBefore) - - await ctx.wire - .getSysioContract(SysioContractName.chalg) - .actions.openuwchal.invoke( - { - challenger: Constants.ChallengerAccount, - uwreq_id: commitment.uwreqId, - underwriter: commitment.underwriterAccount, - reason: input.reason, - detail: input.detail - }, - { - authorization: [ - { actor: Constants.ChallengerAccount, permission: ActivePermission } - ] - } - ) - - const challenge = await readUwchalByCommitment(ctx, commitment) - Assert.ok( - challenge != null, - `openuwchal landed but no uwchals row exists for uwreq ${commitment.uwreqId}` - ) - const bond = BigInt(challenge.bond_amount) - Assert.ok(bond > 0n, "the escrowed challenge bond must be positive") - Assert.strictEqual( - await ctx.wire.getWireBalance(Constants.ChallengerAccount), - balanceBefore - bond, - "the challenger's WIRE drops by exactly the escrowed bond" - ) - // The flow casts Tier1VoterNames.length identical ballots; a quorum above - // that means the bootstrap Tier-1 roster grew — fail loudly here instead - // of deadlocking the vote downstream. - Assert.ok( - Number(challenge.quorum) <= Constants.Tier1VoterNames.length, - `snapshotted quorum ${challenge.quorum} exceeds the flow's ${Constants.Tier1VoterNames.length} voters` - ) - ctx.outputs.set(keys.chalId, Number(challenge.id)).set(keys.bond, bond) - log.info( - `[uwchal] opened challenge ${challenge.id} on uwreq ${commitment.uwreqId} (bond ${bond})` - ) - } - - // ── Step: sysio.chalg::voteuwchal (write) ───────────────────────────────── - - /** Input for {@link planVoteuwchal}. */ - export interface VoteuwchalInput extends StepInput { - readonly kind: "UnderwriterSlashingScenarioChallengeSteps.VoteuwchalInput" - /** The Tier-1 owner casting the ballot. */ - readonly voter: string - /** Numeric `uwchal_ballot` value. */ - readonly ballot: SysioContracts.SysioChalgUwchalBallot - } - - /** - * `sysio.chalg::voteuwchal` — one Tier-1 owner's ballot (record-only on - * chain; `chkuwchal` tallies). The challenge id is resolved from - * `ctx.outputs` at run time — it does not exist when the plan is built. - */ - export function planVoteuwchal( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - chalIdKey: OutputKey, - voter: string, - ballot: SysioContracts.SysioChalgUwchalBallot - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { - kind: "UnderwriterSlashingScenarioChallengeSteps.VoteuwchalInput", - voter, - ballot - }, - async (ctx, input, signal) => runVoteuwchal(ctx, input, signal, chalIdKey) - ) - } - - /** Named runner body for {@link planVoteuwchal}. */ - export async function runVoteuwchal( - ctx: SwapScenarioContext, - input: VoteuwchalInput, - signal: AbortSignal, - chalIdKey: OutputKey - ): Promise { - signal.throwIfAborted() - const chalId = ctx.outputs.assert(chalIdKey) - await ctx.wire - .getSysioContract(SysioContractName.chalg) - .actions.voteuwchal.invoke( - { owner: input.voter, chal_id: chalId, ballot: input.ballot }, - { - authorization: [{ actor: input.voter, permission: ActivePermission }] - } - ) - log.info( - `[uwchal] ${input.voter} voted ${SysioChalgUwchalBallot[input.ballot]} on challenge ${chalId}` - ) - } - - // ── Step: sysio.chalg::chkuwchal (write — the permissionless tally) ────── - - /** Input for {@link planChkuwchal}. */ - export interface ChkuwchalInput extends StepInput { - readonly kind: "UnderwriterSlashingScenarioChallengeSteps.ChkuwchalInput" - } - - /** - * `sysio.chalg::chkuwchal` — crank the tally right after the deciding vote - * (production needs no caller: `chklocks` pokes it from the epoch tick; the - * flow cranks manually for a deterministic, faster resolution — the same - * relationship `chkdispute` has to its voters). - */ - export function planChkuwchal( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - chalIdKey: OutputKey - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { kind: "UnderwriterSlashingScenarioChallengeSteps.ChkuwchalInput" }, - async (ctx, input, signal) => runChkuwchal(ctx, input, signal, chalIdKey) - ) - } - - /** Named runner body for {@link planChkuwchal}. */ - export async function runChkuwchal( - ctx: SwapScenarioContext, - input: ChkuwchalInput, - signal: AbortSignal, - chalIdKey: OutputKey - ): Promise { - signal.throwIfAborted() - const chalId = ctx.outputs.assert(chalIdKey) - await ctx.wire - .getSysioContract(SysioContractName.chalg) - .actions.chkuwchal.invoke( - { chal_id: chalId }, - { - authorization: [ - { actor: Constants.ChallengerAccount, permission: ActivePermission } - ] - } - ) - } - - // ── Step: sysio.chalg::claimbond (write — the recipient's own pull) ─────── - - /** Input for {@link planClaimbond}. */ - export interface ClaimbondInput extends StepInput { - readonly kind: "UnderwriterSlashingScenarioChallengeSteps.ClaimbondInput" - } - - /** - * `sysio.chalg::claimbond` — the recipient pulls its resolved bond out of - * chalg custody. This is the ONLY step in the flow where a resolved bond - * actually moves: `chkuwchal` credits a claimable balance instead of - * transferring, because the tally can run inline under the epoch tick - * (`sysio.epoch::advance` -> `sysio.uwrit::chklocks` -> `chkuwchal`) where - * `sysio.token::transfer`'s `require_recipient(to)` would run the - * recipient's own code and hand it a lever on epoch advancement. - * - * `recipient` is either a literal account (the challenger, known when the - * plan is built) or the commitment whose winning underwriter is owed the - * forfeit (resolved from `ctx.outputs` at run time, like the challenge id). - */ - export function planClaimbond( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - recipient: string | OutputKey - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { kind: "UnderwriterSlashingScenarioChallengeSteps.ClaimbondInput" }, - async (ctx, input, signal) => runClaimbond(ctx, input, signal, recipient) - ) - } - - /** Named runner body for {@link planClaimbond}. */ - export async function runClaimbond( - ctx: SwapScenarioContext, - input: ClaimbondInput, - signal: AbortSignal, - recipient: string | OutputKey - ): Promise { - signal.throwIfAborted() - // `OutputKey` is an object, so the literal-vs-key discrimination is total. - const account = - typeof recipient === "string" - ? recipient - : ctx.outputs.assert(recipient).underwriterAccount - const credit = await readBondCredit(ctx, account), - balanceBefore = await ctx.wire.getWireBalance(account) - Assert.ok( - credit > 0n, - `claimbond: ${account} has no claimable bond credit to pull` - ) - - await ctx.wire - .getSysioContract(SysioContractName.chalg) - .actions.claimbond.invoke( - { account }, - { authorization: [{ actor: account, permission: ActivePermission }] } - ) - - Assert.strictEqual( - await ctx.wire.getWireBalance(account), - balanceBefore + credit, - "claimbond pays out the whole credited balance" - ) - Assert.strictEqual( - await readBondCredit(ctx, account), - 0n, - "claimbond erases the credit row it paid out" - ) - log.info(`[uwchal] ${account} claimed bond credit ${credit}`) - } -} diff --git a/packages/flow-underwriter-slashing/src/steps/UnderwriterSlashingScenarioSwapSteps.ts b/packages/flow-underwriter-slashing/src/steps/UnderwriterSlashingScenarioSwapSteps.ts deleted file mode 100644 index 4332402f9..000000000 --- a/packages/flow-underwriter-slashing/src/steps/UnderwriterSlashingScenarioSwapSteps.ts +++ /dev/null @@ -1,124 +0,0 @@ -/** - * UnderwriterSlashingScenarioSwapSteps — the flow-local Step factory for the - * ETH→SOL `ReserveManager.requestSwap` writes this scenario submits (one per - * challengeable commitment). The heavy lifting is the harness's - * `requestEthereumSwap` + `EthereumCollateralTool` loaders; this file is the - * per-flow glue every swap flow declares for itself. The target amount is - * keyed per swap so ONE factory serves both. - */ - -import Assert from "node:assert" -import { ethers } from "ethers" -import { getLogger } from "@wireio/shared" -import { - contractView, - ClusterBuildStep, - ClusterConfigProvider, - EthereumCollateralTool, - requestEthereumSwap, - swapUserOutputKey, - type ClusterBuildStepOptions, - type OutputKey, - type Report, - type ReserveManagerRequestSwapContract, - type StepInput, - type SwapScenarioContext -} from "@wireio/cluster-tool" -import { UnderwriterSlashingScenarioConstants as Constants } from "../UnderwriterSlashingScenarioConstants.js" - -const log = getLogger(__filename) - -export namespace UnderwriterSlashingScenarioSwapSteps { - /** Input for {@link planRequestSwapEthereum} — the static SWAP_REQUEST - * parameters (the quote-computed target rides `targetAmountKey`). */ - export interface RequestSwapEthereumInput extends StepInput { - readonly kind: "UnderwriterSlashingScenarioSwapSteps.RequestSwapEthereumInput" - /** Wei to escrow into the source reserve (`msg.value`). */ - readonly sourceAmountWei: bigint - /** Acceptable variance in basis points. */ - readonly targetToleranceBps: number - } - - /** - * A single `ReserveManager.requestSwap(...)` write signed by the swap user — - * one ETH→SOL SWAP_REQUEST emission (the same leg shape as - * flow-swap-with-underwriting's Phase A; chain/token/reserve codes are the - * flow constants). - */ - export function planRequestSwapEthereum( - actor: Report.Actor, - name: string, - description: string, - options: ClusterBuildStepOptions, - targetAmountKey: OutputKey, - request: Omit - ): ClusterBuildStep { - return ClusterBuildStep.create( - actor, - name, - description, - options, - { - kind: "UnderwriterSlashingScenarioSwapSteps.RequestSwapEthereumInput", - ...request - }, - async (ctx, input, signal) => - runRequestSwapEthereum(ctx, input, signal, targetAmountKey) - ) - } - - /** Named runner body — bind `ReserveManager` to the swap user's wallet and - * perform ONE `requestSwap(...)` write. */ - export async function runRequestSwapEthereum( - ctx: SwapScenarioContext, - input: RequestSwapEthereumInput, - signal: AbortSignal, - targetAmountKey: OutputKey - ): Promise { - signal.throwIfAborted() - const swapUser = ctx.outputs.assert(swapUserOutputKey()) - const targetAmount = ctx.outputs.assert(targetAmountKey) - const reserveManager = loadReserveManager(ctx, swapUser.ethereumWallet) - const result = await requestEthereumSwap(reserveManager, { - sourceTokenCode: BigInt(Constants.EthereumTokenCode), - sourceReserveCode: BigInt(Constants.PrimaryReserveCode), - sourceAmountWei: input.sourceAmountWei, - targetChainCode: BigInt(Constants.SolanaChainCode), - targetTokenCode: BigInt(Constants.SolanaTokenCode), - targetReserveCode: BigInt(Constants.PrimaryReserveCode), - targetRecipient: swapUser.solanaPublicKeyBytes, - targetAmount, - targetToleranceBps: input.targetToleranceBps - }) - Assert.ok( - result.transactionHash, - "UnderwriterSlashingScenarioSwapSteps.requestSwapEthereum: no transaction hash" - ) - log.info( - `[uwchal-swap] requestSwap tx=${result.transactionHash} block=${result.blockNumber} target=${targetAmount}` - ) - } - - /** - * Resolve the `ReserveManager` contract from the run's deploy artifacts, - * bound to `wallet` — address from `outpost-addrs.json`, ABI from the - * hardhat artifact (both via {@link EthereumCollateralTool}'s loaders). - */ - export function loadReserveManager( - ctx: SwapScenarioContext, - wallet: ethers.Signer - ): ReserveManagerRequestSwapContract { - const address = EthereumCollateralTool.loadOutpostAddresses( - ClusterConfigProvider.ethereumDeploymentsPath(ctx.config) - )[Constants.ReserveManagerContractName] - Assert.ok( - address != null && /^0x[0-9a-fA-F]{40}$/.test(address), - `UnderwriterSlashingScenarioSwapSteps: ${Constants.ReserveManagerContractName} not in outpost-addrs.json (got ${address})` - ) - const abi = EthereumCollateralTool.loadOutpostAbi( - ctx.config.ethereumPath, - Constants.ReserveManagerContractName - ) - return contractView(address, abi, wallet) - } -} diff --git a/packages/flow-underwriter-slashing/src/steps/index.ts b/packages/flow-underwriter-slashing/src/steps/index.ts deleted file mode 100644 index 4a976d785..000000000 --- a/packages/flow-underwriter-slashing/src/steps/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./UnderwriterSlashingScenarioChallengeSteps.js" -export * from "./UnderwriterSlashingScenarioSwapSteps.js" diff --git a/packages/flow-underwriter-slashing/tsconfig.json b/packages/flow-underwriter-slashing/tsconfig.json deleted file mode 100644 index 82b017421..000000000 --- a/packages/flow-underwriter-slashing/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-underwriter-slashing/tsconfig.src.json b/packages/flow-underwriter-slashing/tsconfig.src.json deleted file mode 100644 index b8bf202f6..000000000 --- a/packages/flow-underwriter-slashing/tsconfig.src.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "extends": "../../etc/tsconfig/tsconfig.base.jest.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "lib", - "paths": { - "@wireio/cluster-tool": [ - "../cluster-tool/src" - ], - "@wireio/cluster-tool-shared": [ - "../cluster-tool-shared/src" - ] - } - }, - "include": [ - "src" - ], - "references": [ - { - "path": "../cluster-tool-shared/tsconfig.json" - }, - { - "path": "../cluster-tool/tsconfig.json" - } - ] -} diff --git a/packages/flow-yield-distribution/src/YieldDistributionScenarioConstants.ts b/packages/flow-yield-distribution/src/YieldDistributionScenarioConstants.ts index 4ffeaa048..7d9619278 100644 --- a/packages/flow-yield-distribution/src/YieldDistributionScenarioConstants.ts +++ b/packages/flow-yield-distribution/src/YieldDistributionScenarioConstants.ts @@ -16,7 +16,7 @@ export namespace YieldDistributionScenarioConstants { /** 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 = 1 + export const UnderwriterCount = 0 /** The AuthEx-linked staker's WIRE account — its reward lands in `sysio.dclaim::pclaims`. */ export const LinkedStakerAccount = "yield.lnk" @@ -58,7 +58,8 @@ export namespace YieldDistributionScenarioConstants { * 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 + export const ReplayRejectionPattern = + /externalEpochRef not monotonic|reverted/i /** Settle window slept before asserting the replayed emission credited nothing. */ export function dedupeSettleMs(): number { diff --git a/packages/flow-yield-distribution/src/steps/YieldDistributionScenarioEmitSteps.ts b/packages/flow-yield-distribution/src/steps/YieldDistributionScenarioEmitSteps.ts index 778647fc3..6bb7cbd55 100644 --- a/packages/flow-yield-distribution/src/steps/YieldDistributionScenarioEmitSteps.ts +++ b/packages/flow-yield-distribution/src/steps/YieldDistributionScenarioEmitSteps.ts @@ -14,10 +14,25 @@ * 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 { - EthereumCollateralTool, + OperatorDaemonArtifactsKey, Report, emitYieldBatch, loadMockYieldEmitter, @@ -26,8 +41,7 @@ import { type ClusterBuildStepOptions, type MockYieldEmitterContract, type OutputKey, - type StepInput, - ClusterConfigProvider + type StepInput } from "@wireio/cluster-tool" import { YieldDistributionScenarioConstants as Constants } from "../YieldDistributionScenarioConstants.js" @@ -240,9 +254,7 @@ export namespace YieldDistributionScenarioEmitSteps { ): MockYieldEmitterContract { return loadMockYieldEmitter( ctx.config.ethereumPath, - EthereumCollateralTool.loadOutpostAddresses( - ClusterConfigProvider.ethereumDeploymentsPath(ctx.config) - ), + ctx.outputs.assert(OperatorDaemonArtifactsKey).ethereumAddresses, ctx.ethereum.wallet.signer ) } diff --git a/tsconfig.json b/tsconfig.json index 49a851db5..a71e1fd4d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,15 +17,8 @@ { "path": "./packages/cluster-tool/tsconfig.json" }, { "path": "./packages/flow-operator-collateral-deposit/tsconfig.json" }, { "path": "./packages/flow-producer-registration/tsconfig.json" }, - { "path": "./packages/flow-swap-with-underwriting/tsconfig.json" }, { "path": "./packages/flow-batch-operator-termination/tsconfig.json" }, { "path": "./packages/flow-batch-operator-slashing/tsconfig.json" }, - { "path": "./packages/flow-swap-variance-revert/tsconfig.json" }, - { "path": "./packages/flow-swap-non-native-tokens/tsconfig.json" }, - { "path": "./packages/flow-swap-to-wire/tsconfig.json" }, - { "path": "./packages/flow-swap-from-wire/tsconfig.json" }, - { "path": "./packages/flow-reserve-lifecycle/tsconfig.json" }, - { "path": "./packages/flow-swap-private-reserves/tsconfig.json" }, { "path": "./packages/flow-node-owner-nft/tsconfig.json" }, { "path": "./packages/flow-emissions-soak/tsconfig.json" }, { "path": "./packages/flow-yield-distribution/tsconfig.json" }, @@ -35,7 +28,6 @@ { "path": "./packages/flow-emergency-stop/tsconfig.json" }, { "path": "./packages/flow-syndication-challenge/tsconfig.json" }, { "path": "./packages/flow-syndication-rate-limit/tsconfig.json" }, - { "path": "./packages/flow-underwriter-slashing/tsconfig.json" }, { "path": "./packages/debugging-shared/tsconfig.json" }, { "path": "./packages/debugging-server/tsconfig.json" }, { "path": "./packages/debugging-client-shared/tsconfig.json" }, From e46fb06a19721f910b421a2d9091479678e4df88 Mon Sep 17 00:00:00 2001 From: Jonathan Glanz Date: Tue, 6 Oct 2026 13:00:36 -0400 Subject: [PATCH 3/4] docs: align with the syndication feature set and current cluster tooling Docs now describe the current cross-chain feature set: liq syndication (SYNDICATE_LIQ / LIQ_YIELD / DESYNDICATE_LIQ) with sysio.bond underwriting, node-owner registration, depot-native operator collateral and the depot-local sysio.swap AMM. Outpost swaps, reserves, underwriter commits and outpost collateral are described only as removed. Co-Authored-By: Claude Opus 5.5 (1M context) --- README.md | 7 +- STYLE.md | 8 +- docs/local-setup.md | 463 ++++++++++++--------------- docs/production-bootstrap.md | 253 +++++++++------ docs/setup-external-cluster-guide.md | 7 +- packages/cluster-tool/README.md | 35 +- 6 files changed, 386 insertions(+), 387 deletions(-) diff --git a/README.md b/README.md index 8adbcfd20..0380b35d3 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,10 @@ that OPP envelopes circulate and that on-chain state stays consistent end to end The OPP message flow spans three chains: - **WIRE depot** (`nodeop` + `kiod`) — system contracts `sysio.epoch`, `sysio.msgch`, - `sysio.opreg`, `sysio.uwrit`, `sysio.reserv`, `sysio.chalg`, … -- **Ethereum outpost** (`anvil`) — `OPP.sol`, `OPPInbound.sol`, `OperatorRegistry.sol`, - `ReserveManager.sol`, `StakingManager.sol` (+ `liqEth`). + `sysio.opreg`, `sysio.chalg`, `sysio.synd`, `sysio.bond`, `sysio.liq`, `sysio.swap` + (a depot-local AMM with no outpost participation), `sysio.andon`, `sysio.dclaim`, … +- **Ethereum outpost** (`anvil`) — `OPP.sol`, `OPPInbound.sol`, `OutpostManager.sol`, + `SyndicationPool.sol`, `BAR.sol`, `StakingManager.sol` (an inert placeholder) (+ `liqEth`). - **Solana outpost** (`solana-test-validator`) — all four wire-solana Anchor programs loaded at genesis (`liqsol_core`, which hosts the OPP outpost interface, plus `liqsol_token` / `transfer_hook` / `validator_leaderboard`), with the liqsol diff --git a/STYLE.md b/STYLE.md index 22843e8cb..45a5b531c 100644 --- a/STYLE.md +++ b/STYLE.md @@ -529,8 +529,8 @@ const { Notes: async defaults are fine (`const { port = await BindConfigProvider.findAvailable(…) } = options`); a fallback to ANOTHER member destructures both then coalesces the locals -(`const { batchOperatorAccount, underwriterAccount } = node` → -`batchOperatorAccount ?? underwriterAccount`); optional chains and computed +(`const { primary, fallback } = options` → +`primary ?? fallback`); optional chains and computed members (`arr[0] ?? x`) stay accessor-form. ### Module-level joined declarations @@ -966,8 +966,8 @@ define protocol latency): |---|---|---| | `EpochExtensionMaxSec` (30s) | via `effectiveEpochSec()` | every N-epoch deadline: `N × (epochDurationSec + 30) × MsPerSecond` | | `CollateralVerifyBudgetMs` (6 min) | deposit → depot verification gates | -| `SingleHopBudgetMs` (7 min) | act on an outpost, verify on the depot — or the reverse (uwreq appears, race confirms, reserve relay/READY, attestation propagation) | -| `DoubleHopBudgetMs` (14 min) | outpost → depot → outpost tails (remit/payout, SWAP_REVERT round-trips) | +| `SingleHopBudgetMs` (7 min) | act on an outpost, verify on the depot — or the reverse (a `SYNDICATE_LIQ` envelope reaches `REQUESTED`, a `DESYNDICATE_LIQ` payout reaches the outpost wallet, attestation propagation) | +| `DoubleHopBudgetMs` (14 min) | outpost → depot → outpost tails (an outpost syndication bonded and released on the depot, then redeemed back to the outpost wallet via `DESYNDICATE_LIQ`) | Principles: diff --git a/docs/local-setup.md b/docs/local-setup.md index 6ecafcbd9..9c45a69eb 100644 --- a/docs/local-setup.md +++ b/docs/local-setup.md @@ -1,12 +1,15 @@ # wire-cluster-tool: Local Setup (Non-Docker) -End-to-end instructions for building Wire's `wire-cluster-tool` CLI from source on the host (Ubuntu 24.04 / WSL2), **without** the Docker image. Based on the five repos' own README / CLAUDE.md files; the reference Dockerfile is out of date in a few places (see "Differences from the Dockerfile" at the bottom). +End-to-end instructions for building Wire's `wire-cluster-tool` CLI (alias `wtc`) and running the `flow-*` +suites from source on the host (Ubuntu 24.04 / WSL2), **without** a container image. Each repo's own +README / CLAUDE.md stays authoritative for its build; this page strings them together in dependency order. +The container route is described in `wire-devcontainer` (see §5). -> **Status:** built and smoke-tested on Ubuntu 24.04 WSL2, 2026-04-24. -> End-to-end `create` + `run` both succeed — cluster spins up 1 bios + 5 -> producer + 3 batchop + 1 uwrit `nodeop` processes, `anvil` with OPP -> contracts, and `solana-test-validator` with the `opp_outpost` BPF -> program preloaded. +**What `create` brings up:** one bios `nodeop`, the producer nodes, one `nodeop` per batch operator (3 by +default, each running the OPP relay daemon), optional API nodes, `kiod`, `anvil` with the Ethereum outpost +contracts deployed, and `solana-test-validator` with all four wire-solana programs loaded at genesis +(`liqsol_core`, which hosts the Solana OPP outpost, plus `liqsol_token`, `transfer_hook` and +`validator_leaderboard`). --- @@ -24,18 +27,23 @@ sudo apt-get install -y \ doxygen fish golang \ libbz2-dev libcurl4-openssl-dev libgmp-dev liblzma-dev \ libncurses5-dev libusb-1.0-0-dev libzstd-dev zlib1g-dev \ + openjdk-21-jre-headless \ python3 python3-pip python3-venv python3-dev \ sudo tar unzip vim zip ``` +`openjdk-21-jre-headless` runs the ANTLR parser generator of wire-sysio's query engine plugin at build time. + ### 1.2 Toolchains (one-time) -Install only what's missing. Versions below match CI: +Install only what's missing. The Solana / Anchor / Rust versions are pinned by `wire-solana` +(`Anchor.toml` → `anchor_version = "0.31.0"`, `solana_version = "4.2.0"`; `rust-toolchain.toml` → `1.86.0`) — +a mismatched validator or Anchor CLI produces program-load failures that look like flow bugs. ```bash -# -- Rust (stable) -- -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable -export PATH="$HOME/.cargo/bin:$PATH" +# -- Rust -- +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +. "$HOME/.cargo/env" # -- Foundry (anvil) -- curl -L https://foundry.paradigm.xyz | bash @@ -46,17 +54,21 @@ export PATH="$HOME/.foundry/bin:$PATH" sh -c "$(curl -sSfL https://release.anza.xyz/v4.2.0/install)" export PATH="$HOME/.local/share/solana/install/active_release/bin:$PATH" -# -- Node.js 24+ -- -# wire-libraries-ts and wire-e2e-tests roots declare engines.node >=22, -# but the bundled CLI packages (protoc-gen-solidity, protoc-gen-solana, -# protobuf-bundler) declare engines.node >=24 and `@yao-pkg/pkg` fetches -# a v24 runtime to bundle into their binaries. Use 24. CI also runs 24. -# (Verified: Node 20.x does NOT satisfy the engine check.) +# -- Anchor via avm -- +cargo install --git https://github.com/coral-xyz/anchor avm --force +avm install 0.31.0 +avm use 0.31.0 + +# -- Node.js >= 24.9 -- +# wire-tools-ts declares engines.node >=24.9. Below it, require(ESM) on yargs fails and +# every jest suite that reaches the @wireio/cluster-tool barrel dies at import. +# wire-sysio's OPP bundle generation also requires node >= 24. nvm install 24 && nvm alias default 24 # -- pnpm (corepack) -- corepack enable corepack prepare pnpm@10.32.1 --activate +pnpm setup # creates the pnpm global bin dir; start a new shell afterwards (see §6.1) ``` Verify: @@ -64,9 +76,10 @@ Verify: ```bash rustc --version; cargo --version anvil --version -solana --version -node --version # must be >= 24 -pnpm --version # must be 10.x +solana --version # 4.2.0 ... Agave +anchor --version # anchor-cli 0.31.0 +node --version # must be >= 24.9 +pnpm --version # 10.x clang-18 --version cmake --version ninja --version @@ -74,48 +87,32 @@ ninja --version ### 1.3 Repo layout -All five repos must sit in the **same parent directory** so sibling-based -package resolution (`.pnpmfile.cjs`, `generate-opp-bundles.fish`) works: +The repos must sit in the **same parent directory** — `wire-tools-ts/.pnpmfile.cjs` resolves `@wireio/*` +from its siblings (`../wire-libraries-ts/packages/*` and `../wire-sysio/build/opp/typescript`): ``` / ├── wire-cdt/ -├── wire-sysio/ +├── wire-sysio/ # build// holds the binaries; build/opp/ holds the generated OPP bundles ├── wire-libraries-ts/ ├── wire-ethereum/ ├── wire-solana/ -├── wire-e2e-tests/ -├── wire-opp/ # GENERATED by generate-opp-bundles.fish, not cloned -└── wire-install/ # GENERATED install prefix for wire-cdt + wire-sysio +├── wire-tools-ts/ +└── wire-install/ # install prefix for wire-cdt (consumed by wire-sysio) ``` -Clone each (`git clone --recursive`) if not already present. +The platform checks these out together with Google's `repo` tool from `wire-platform-manifest`; cloning each +repo by hand (`git clone --recursive`) into one parent directory works too. -The `wire-opp/` directory **must exist** before the wire-sysio build runs — `generate-opp-bundles.fish` only writes there when the directory is already present (it's the local-dev hand-off path detected by `.pnpmfile.cjs` in `wire-ethereum`/`wire-e2e-tests`). It can start empty: +### 1.4 Branches -```bash -mkdir -p "$WIRE_ROOT/wire-opp" -``` +Use the branches the manifest declares (`wire-platform-manifest/default.xml`): the default revision +`master` for `wire-cdt`, `wire-sysio`, `wire-libraries-ts` and `wire-tools-ts`, and `next` for +`wire-ethereum` and `wire-solana`. -### 1.4 Branches required for the OPP cross-chain integration - -`master` isn't enough — the `wire-cluster-tool` harness expects artifacts -(contracts, scripts, Solana program, protos) that only exist on the OPP -feature branches: - -| Repo | Branch | What's on it | -|---|---|---| -| `wire-sysio` | any branch that carries the OPP protos — `feature/protobuf-support-for-opp` or a branch merged from it (e.g. `feature/intrinsic-span-cleanup`) | `libraries/opp/proto/sysio/opp/**/*.proto`, `sysio.{chalg,epoch,msgch,uwrit,opreg}` contracts, `cdt::protoc-gen-zpp` wiring | -| `wire-cdt` | `feature/kv-secondary-primary-id` (current) | `protoc-gen-zpp` tool + install target | -| `wire-libraries-ts` | `master` | `@wireio/shared*`, `@wireio/sdk-core`, `protoc-gen-*` + `wire-protobuf-bundler` CLIs | -| `wire-ethereum` | `feature/protobufs-for-opp` | `src/scripts/deployLocal.ts` (called by harness ETHBootstrapper), `contracts/outpost/*.sol` consuming `@wireio/opp-solidity-models` | -| `wire-solana` | `feature/opp-solana-outpost-integration` | `programs/opp-outpost` Anchor program + `wallets/opp-outpost-keypair.json` | -| `wire-e2e-tests` | `master` | the harness + flow tests | - -Switch each repo before building: ```bash -(cd wire-ethereum && git fetch && git checkout feature/protobufs-for-opp) -(cd wire-solana && git fetch && git checkout feature/opp-solana-outpost-integration) +(cd wire-ethereum && git fetch && git checkout next) +(cd wire-solana && git fetch && git checkout next) ``` --- @@ -136,64 +133,42 @@ export NUM_JOBS=$(( $(nproc) - 2 )) ## 3. Build steps -> Each step assumes `cd "$WIRE_ROOT/"` unless stated otherwise. - -### 3.1 `wire-libraries-ts` — shared JS/TS packages + protoc plugins - -This produces the `@wireio/*` packages and the three protoc CLI binaries -(`wire-protobuf-bundler`, `protoc-gen-solidity`, `protoc-gen-solana`) that -`generate-opp-bundles.fish` needs on PATH. - -```bash -cd "$WIRE_ROOT/wire-libraries-ts" -pnpm install # postinstall runs pnpm dist for protoc-gen-* (slow first time) -pnpm build - -# Put the three CLI binaries on PATH for the OPP generation step: -for p in protoc-gen-solidity protoc-gen-solana protobuf-bundler; do - (cd "packages/$p" && pnpm run dist && pnpm link --global) -done - -# Verify: -command -v wire-protobuf-bundler protoc-gen-solidity protoc-gen-solana -``` - -**Note on `pnpm link --global`:** Only the three CLI packages need this, and -only so the `.fish` script finds the binaries on PATH. `@wireio/shared`, -`@wireio/shared-node`, `@wireio/sdk-core`, etc. are resolved automatically by -`wire-e2e-tests/.pnpmfile.cjs` — **do not** manually link them. +> Each step assumes `cd "$WIRE_ROOT/"` unless stated otherwise. Build order: +> wire-cdt → wire-sysio → wire-libraries-ts → wire-ethereum → wire-solana → wire-tools-ts. -### 3.2 `wire-cdt` — Contract Development Toolkit (WASM/ABI compilers) +### 3.1 `wire-cdt` — Contract Development Toolkit (WASM/ABI compilers) -Required by `wire-sysio` so `BUILD_SYSTEM_CONTRACTS=ON` can build the `.wasm` -contracts. Installs into a local prefix we point `wire-sysio` at. +Required by `wire-sysio` so `BUILD_SYSTEM_CONTRACTS=ON` can build the `.wasm` contracts. Installs into a +local prefix that `wire-sysio` is pointed at. ```bash cd "$WIRE_ROOT/wire-cdt" ./vcpkg/bootstrap-vcpkg.sh # one-time -cmake -S . -B build/debug -G Ninja \ - -DCMAKE_BUILD_TYPE=Debug \ +cmake -S . -B build/release -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ -DENABLE_CCACHE=ON \ - -DENABLE_TESTS=ON \ -DCMAKE_C_COMPILER="$CC" \ -DCMAKE_CXX_COMPILER="$CXX" \ -DCMAKE_TOOLCHAIN_FILE="$PWD/vcpkg/scripts/buildsystems/vcpkg.cmake" \ -DCMAKE_INSTALL_PREFIX="$WIRE_ROOT/wire-install" \ -DCMAKE_PREFIX_PATH="$WIRE_ROOT/wire-install" -cmake --build build/debug -j"$NUM_JOBS" -cmake --install build/debug +cmake --build build/release -j"$NUM_JOBS" +cmake --install build/release ``` -### 3.3 `wire-sysio` — node software (`nodeop`, `kiod`, `clio`, `sys-util`) +Rebuild wire-cdt whenever you update it — wire-sysio's system contracts compile against its headers, and a +stale CDT shows up as contract compile errors in wire-sysio. + +### 3.2 `wire-sysio` — node software, system contracts and the OPP bundles -> **Constraint from `CLAUDE.md`:** the build directory MUST be under -> `wire-sysio/build/` (e.g. `build/claude`, `build/debug-clang-clion`). -> Other locations break `configure_file` for test reference data. +> **Constraint from `wire-sysio/CLAUDE.md`:** the build directory MUST be under `wire-sysio/build/` +> (e.g. `build/debug`, `build/release`). Other locations break `configure_file` for test reference data. ```bash cd "$WIRE_ROOT/wire-sysio" +git submodule update --init --recursive vcpkg libraries/appbase ./vcpkg/bootstrap-vcpkg.sh # one-time export BUILD_DIR="$PWD/build/debug" @@ -206,94 +181,81 @@ cmake -S . -B "$BUILD_DIR" -G Ninja \ -DCMAKE_C_COMPILER="$CC" \ -DCMAKE_CXX_COMPILER="$CXX" \ -DCMAKE_TOOLCHAIN_FILE="$PWD/vcpkg/scripts/buildsystems/vcpkg.cmake" \ - -DCMAKE_INSTALL_PREFIX="$WIRE_ROOT/wire-install" \ - -DCMAKE_PREFIX_PATH="$WIRE_ROOT/wire-install/cdt" + -DCMAKE_PREFIX_PATH="$WIRE_ROOT/wire-install" -# Make sure the protoc CLIs are on PATH — there's a POST_BUILD step on -# libopp.a that invokes generate-opp-bundles.fish (see §6.5 below). -export PATH="$HOME/.local/share/pnpm:$PATH" cmake --build "$BUILD_DIR" -j"$NUM_JOBS" # Some compilation units need ~4GB RAM; reduce -j if you see OOMs. ``` -> **Note on the zpp-generated contract headers.** The `contracts_project` -> ExternalProject depends on `opp_cdt_models.protos` (which runs -> `cdt::protoc-gen-zpp` to produce `sysio/opp/**/*.pb.hpp` under -> `build//libraries/opp/generated-cdt/`). wire-sysio commit -> `1015c98d3` (Apr 2026) wired `add_dependencies(contracts_project -> opp_cdt_models.protos)` so a single `cmake --build` should now produce -> the headers before the contracts compile. If you still hit -> `fatal error: 'sysio/opp/types/types.pb.hpp' file not found` on a cold -> build, run `ninja -C "$BUILD_DIR" opp_cdt_models.protos` once and -> resume the full build. +This produces `$BUILD_DIR/bin/{nodeop,kiod,clio,sys-util}` and the system-contract `.wasm`/`.abi`. + +**OPP bundles.** `BUILD_OPP_BUNDLES` is `ON` by default: after the protobuf generation, a POST_BUILD step +runs `libraries/opp/tools/scripts/generate-opp-bundles.sh`, which builds the repo-local protoc plugins +(`protoc-gen-solidity`, `protoc-gen-solana`, `wire-protobuf-bundler` under `libraries/opp/tools/`) and writes +the TypeScript, Solidity and Solana model packages to `wire-sysio/build/opp/{typescript,solidity,solana}/`. +It requires `bash`, `npm`, `node >= 24` and `pnpm >= 10` at configure time; when any is missing CMake prints +`BUILD_OPP_BUNDLES is ON, but requirements are not met` and the bundles are not generated (see §6.2). Never +run `protoc` by hand — this build step is the only producer of those packages. -### 3.4 Generate the `wire-opp/` sibling directory +### 3.3 `wire-libraries-ts` — shared JS/TS packages -`wire-opp` is **not** a repo — it's generated from the proto definitions in -`wire-sysio/libraries/opp/proto/` via a fish script, and its output is -consumed by `wire-ethereum` and `wire-e2e-tests` through their -`.pnpmfile.cjs` hooks. +Produces `@wireio/shared`, `@wireio/shared-node`, `@wireio/sdk-core`, … which `wire-tools-ts` links from +source via its `.pnpmfile.cjs` (no `pnpm link` needed). ```bash -cd "$WIRE_ROOT/wire-sysio/libraries/opp/tools" -./generate-opp-bundles.fish -# Creates $WIRE_ROOT/wire-opp/{typescript,solidity,solana}/ +cd "$WIRE_ROOT/wire-libraries-ts" +pnpm install +pnpm build ``` -Prerequisites: the three CLIs from step 3.1 must be on PATH. The script checks and aborts with a clear error otherwise. - -### 3.5 `wire-ethereum` — Solidity contracts (Hardhat) +### 3.4 `wire-ethereum` — Ethereum outpost contracts (Hardhat) -wire-ethereum uses **npm**, not pnpm — the `.pnpmfile.cjs` auto-linking does -not apply here. Link the generated `@wireio/opp-*` packages globally into -npm's global bin first, then link them into the project: +wire-ethereum uses **npm** (it ships a `package-lock.json`), so `.pnpmfile.cjs` auto-linking does not apply. +To build against the freshly generated Solidity models instead of the published +`@wireio/opp-solidity-models`, link the wire-sysio bundle first: ```bash -# One-time: register the wire-opp/* packages as globally-linkable. -# Both npm and pnpm globals are needed — npm for wire-ethereum below, -# pnpm so any pnpm-based consumer (or sibling wire-e2e-tests workspace -# falling back to a global resolve) finds them. -(cd "$WIRE_ROOT/wire-opp/solidity" && npm link && pnpm link --global) -(cd "$WIRE_ROOT/wire-opp/typescript" && npm link && pnpm link --global) +(cd "$WIRE_ROOT/wire-sysio/build/opp/solidity" && npm link) cd "$WIRE_ROOT/wire-ethereum" -git checkout feature/protobufs-for-opp # see §1.4 npm install -npm link @wireio/opp-solidity-models @wireio/opp-typescript-models -npx hardhat compile +npm link @wireio/opp-solidity-models +npm run build # npx hardhat compile ``` -### 3.5b Anchor — build `opp-outpost` in `wire-solana` +### 3.5 `wire-solana` — the four Anchor programs -The harness loads this Solana program onto `solana-test-validator` via -`--bpf-program`, and reads its IDL for CPI. Requires `anchor-cli 0.31.0`. +The harness loads `liqsol_core`, `liqsol_token`, `transfer_hook` and `validator_leaderboard` onto +`solana-test-validator` at genesis and runs wire-solana's own `anchor run init-*` scripts during the +bootstrap. The Solana OPP outpost lives inside `liqsol_core` (`programs/liqsol-core/src/instructions/opp/`); +there is no separate outpost program. ```bash -# One-time: install anchor-cli (solana-cli already installed in §1.2) -cargo install --git https://github.com/coral-xyz/anchor --tag v0.31.0 anchor-cli --locked -# (or via avm: cargo install --git https://github.com/coral-xyz/anchor avm --locked && avm install 0.31.0 && avm use 0.31.0) - cd "$WIRE_ROOT/wire-solana" -git checkout feature/opp-solana-outpost-integration # see §1.4 -anchor build -p opp-outpost -# Produces target/deploy/opp_outpost.so and target/idl/opp_outpost.json +npm install +npm run build:programs # the build the platform gate runs and the harness deploys +# Produces target/deploy/{liqsol_core,liqsol_token,transfer_hook,validator_leaderboard}.so (+ IDLs) ``` -### 3.6 `wire-e2e-tests` — harness monorepo + `wire-cluster-tool` CLI +A bare `anchor build` is not equivalent to `build:programs`. + +### 3.6 `wire-tools-ts` — harness monorepo + `wire-cluster-tool` CLI ```bash -cd "$WIRE_ROOT/wire-e2e-tests" -pnpm install # .pnpmfile.cjs auto-links @wireio/* from ../wire-libraries-ts and ../wire-opp -pnpm build # tsc -b composite build across flow-a…flow-d + harness +cd "$WIRE_ROOT/wire-tools-ts" +pnpm install # .pnpmfile.cjs links @wireio/* from ../wire-libraries-ts and ../wire-sysio/build/opp +pnpm build # tsc -b across packages/cluster-tool, the flow-* packages and the debugging packages -# Expose wire-cluster-tool on PATH — the harness package.json declares -# "bin": { "wire-cluster-tool": "lib/cjs/cli.js" }, so a plain link -# picks up the post-tsc output. No pkg/standalone binary step. -(cd packages/harness && pnpm link --global) +# The CLI ships in packages/cluster-tool (bins `wire-cluster-tool` and `wtc`, both +# bin/wire-cluster-tool, which runs the built lib/cjs/cli/index.js). Put it on PATH: +export PATH="$WIRE_ROOT/wire-tools-ts/packages/cluster-tool/bin:$PATH" command -v wire-cluster-tool ``` +`pnpm install` prints a `Linked …` line per sibling package it resolved; if `@wireio/opp-typescript-models` +is not among them, `wire-sysio/build/opp/typescript` does not exist yet (see §3.2). + --- ## 4. Running the cluster @@ -306,93 +268,101 @@ killall nodeop kiod anvil solana-test-validator 2>/dev/null || true export CHAIN_DIR="$HOME/ext/wire-chains/dev-001" -wire-cluster-tool \ +wire-cluster-tool create \ --cluster-path="$CHAIN_DIR" \ --force \ - create \ - --build-path="$WIRE_ROOT/wire-sysio/build/debug" \ - --ethereum-path="$WIRE_ROOT/wire-ethereum" \ - --solana-path="$WIRE_ROOT/wire-solana" \ - --prod-count=5 \ - --pnodes=1 \ - --batch-operators=3 \ - --underwriters=1 \ - --epoch-duration=60 - -wire-cluster-tool --cluster-path="$CHAIN_DIR" run -# Ctrl+C to stop. To wipe: wire-cluster-tool --cluster-path="$CHAIN_DIR" destroy + --build-path="$WIRE_ROOT/wire-sysio/build/debug" \ + --ethereum-path="$WIRE_ROOT/wire-ethereum" \ + --solana-path="$WIRE_ROOT/wire-solana" \ + --producer-count=5 \ + --node-count=1 \ + --batch-operator-count=3 \ + --epoch-duration-sec=60 + +wire-cluster-tool run --cluster-path="$CHAIN_DIR" +# Ctrl+C to stop. To wipe: wire-cluster-tool destroy --cluster-path="$CHAIN_DIR" ``` > Note: the harness CLI looks for `nodeop`/`kiod`/`clio`/`sys-util` at > `/bin/`. A full successful `cmake --build` populates > that directory automatically via the `copy_bin` macro. If `build/bin/` > is missing the binaries, the build didn't actually finish; check the -> ninja log for earlier failures (e.g. the OPP POST_BUILD fish gotcha in -> §6.5). +> ninja log for earlier failures. + +The command comes first and every option follows it. Commonly used `create` +flags (the full reference, including the mock-data, API-node and external-outpost +options, is the `create` table in the repo root `README.md`): + +| Flag | Notes | +|---|---| +| `--cluster-path` / `-d` | Directory for chain data, config, logs (required) | +| `--force` | Replace an existing cluster directory | +| `--build-path` | wire-sysio build directory (required) | +| `--ethereum-path` | wire-ethereum root; bootstraps anvil + the outpost contracts (required) | +| `--solana-path` | wire-solana root; bootstraps solana-test-validator with the wire-solana programs (required) | +| `--node-count` / `-n` | Producer node processes (default 1) | +| `--producer-count` / `-p` | Producer accounts to register (default 1) | +| `--batch-operator-count` / `-b` | Batch operators (default 3) | +| `--epoch-duration-sec` | Minimum epoch duration in seconds (default 60) | +| `--enable-mock-liq-pools` | Seed the mock LIQETH/LIQSOL yield pools during epoch zero | +| `--enable-mock-syndication-import` | Import the mock bonder's LIQ positions during epoch zero | + +Swap-underwriter daemons have been removed: `--underwriter-count` still parses, +but any nonzero value is rejected. + +### 4.1 Running a flow + +Each `flow-*` package is a standalone scenario that bootstraps its own cluster. There are 13: + +| Flow | Flow | Flow | +|---|---|---| +| `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-producer-registration` | | | -Flag reference (from `wire-e2e-tests/packages/harness/src/cli.ts`): +Launch one with `scripts/run-flow.mjs` and watch it with `scripts/flow-heartbeat-monitor.mjs` (one monitor +per run, pointed at the same cluster path): -| Flag | Scope | Notes | -|---|---|---| -| `--cluster-path` | global, required | Directory for chain data, config, logs | -| `--force` | global | Remove existing cluster-path before `create` | -| `--build-path` | create, required | wire-sysio build directory | -| `--ethereum-path` | create | wire-ethereum root; bootstraps anvil + OPP contracts | -| `--solana-path` | create | wire-solana root; bootstraps solana-test-validator | -| `-p/--pnodes` | create | Producer node count | -| `-n/--nodes` | create | Non-producer node count | -| `--prod-count` | create | Producers to register (default 21) | -| `--batch-operators` | create | 3–21 | -| `--underwriters` | create | 1–100 | -| `--epoch-duration` | create | Seconds | +```bash +cd "$WIRE_ROOT/wire-tools-ts" ---- +./scripts/run-flow.mjs flow-operator-collateral-deposit \ + --cluster-path /tmp/wire-flow \ + --wire-build-path "$WIRE_ROOT/wire-sysio/build/debug" \ + --ethereum-path "$WIRE_ROOT/wire-ethereum" \ + --solana-path "$WIRE_ROOT/wire-solana" + +# In a second shell: +node scripts/flow-heartbeat-monitor.mjs --cluster-path /tmp/wire-flow +``` -## 5. Differences from the reference Dockerfile - -Noted because the Dockerfile in circulation is partly stale: - -- **`wire-opp` layout.** Dockerfile uses `${WIRE_ROOT}/wire-opp` and assumes - it appears as a side-effect of the `wire-sysio` build. It does not — you - must run `generate-opp-bundles.fish` explicitly. -- **`pnpm link --global` for shared/sdk-core.** Dockerfile globally-links - `shared`, `shared-node`, `shared-web`, `sdk-core`. Not needed locally — - `wire-e2e-tests/.pnpmfile.cjs` hooks resolve them from - `../wire-libraries-ts/packages/` automatically. The packages that still - need a global link are the three CLI binaries (so `.fish` finds them on - PATH) and the generated `wire-opp/{solidity,typescript}` packages (so - `wire-ethereum`'s npm install and any pnpm-based consumer can `link` - them — see §3.5). -- **wire-ethereum `npm i` of pinned opp-models.** Dockerfile does - `npm i && npm link @wireio/opp-solidity-models @wireio/opp-typescript-models` - to work around a `package-lock.json` that pins 1.0.9 (missing types). Still - applies locally. -- **A line in the Dockerfile is a bug:** - `RUN test -d ${WIRE_OPP_ROOT}/typescript && ${WIRE_OPP_ROOT}/solidity && …` - executes `${WIRE_OPP_ROOT}/solidity` as a command instead of checking the - directory. It would silently fail; we don't replicate this. +`run-flow.mjs` accepts the full name, the short form (`operator-collateral-deposit`) or a regex; with no +argument it opens an interactive picker. Each path option falls back to an env var — `WIRE_BUILD_PATH`, +`WIRE_ETH_PATH`, `WIRE_SOLANA_PATH`, `WIRE_CLUSTER_PATH` — and `--cluster-path` is optional (a fresh temp +cluster path is generated per run when omitted). It validates that the build path contains `bin/nodeop` and +the Ethereum path contains `hardhat.config.ts`. A flow exits `0` iff every Report step succeeded; the +Report lands under `/reports/`. Give parallel runs disjoint `--cluster-path` directories. --- -## 6. Troubleshooting / gotchas hit during setup +## 5. Container route -### 6.1 `@yao-pkg/pkg-fetch` race during `pnpm install` in `wire-libraries-ts` +`wire-devcontainer` builds the same toolchain into an image (`Dockerfile`, `e2e-build.Dockerfile`, driven by +`scripts/wire-local-setup.bash`); its `LOCAL_DOCKER_E2E_CLUSTER_GUIDE.md` and `LOCAL_SETUP_GUIDE.md` cover +that path. Where an image's pinned branches or build steps disagree with this page, the repos' own +README / CLAUDE.md files win. -Two `protoc-gen-*` postinstall scripts run `pnpm dist` in parallel, both -calling `pkg-fetch` which writes to the same `~/.pkg-cache/v3.5/` — one wins, -the other fails with `ENOENT: ... fetched-v24.-linux-x64.downloading`. -**Fix:** pre-populate the cache once before `pnpm install`: +--- -```bash -npx --yes @yao-pkg/pkg-fetch node24 linux x64 # fills ~/.pkg-cache/v3.5/ -``` +## 6. Troubleshooting / gotchas -### 6.2 `pnpm link --global` errors with `ERR_PNPM_NO_GLOBAL_BIN_DIR` +### 6.1 `pnpm add --global` / `pnpm link --global` errors with `ERR_PNPM_NO_GLOBAL_BIN_DIR` -Fresh pnpm installs don't have a global bin dir wired. Run `pnpm setup` -once — it picks `$HOME/.local/share/pnpm`, writes `PNPM_HOME` and the -PATH export into your shell rc, and configures `global-bin-dir` / -`global-dir`: +`generate-opp-bundles.sh` registers its CLIs with `pnpm add --global`, which needs a global bin dir. Fresh +pnpm installs don't have one wired. Run `pnpm setup` once — it picks `$HOME/.local/share/pnpm`, writes +`PNPM_HOME` and the PATH export into your shell rc, and configures `global-bin-dir` / `global-dir`: ```bash pnpm setup @@ -400,72 +370,33 @@ pnpm setup # so PNPM_HOME and PATH pick up. ``` -### 6.3 `protoc-gen-ts not found` during `generate-opp-bundles.fish` +### 6.2 `wire-sysio/build/opp/` is missing after a successful build -`wire-protobuf-bundler` calls `protoc-gen-ts` from `@protobuf-ts/plugin` -(not the 3 CLIs we built from `wire-libraries-ts`). Install globally: +The OPP bundle step only attaches when CMake finds `bash`, `npm`, `pnpm` and `node` at configure time and +`node >= 24` / `pnpm >= 10`. Otherwise configure prints +`BUILD_OPP_BUNDLES is ON, but requirements are not met: … OPP bundles (TypeScript, Solidity, Solana) will NOT +be generated.` and the build succeeds without them. Fix the toolchain (§1.2), re-run the `cmake -S . -B +"$BUILD_DIR" …` configure so the check re-evaluates, and rebuild. -```bash -pnpm add -g @protobuf-ts/plugin # provides protoc-gen-ts on PATH -``` +### 6.3 Cold-build error: `'sysio/opp/types/types.pb.hpp' file not found` -### 6.4 Cold-build error: `'sysio/opp/types/types.pb.hpp' file not found` - -The `contracts_project` ExternalProject needs the zpp-generated `.pb.hpp` -headers before it compiles. wire-sysio commit `1015c98d3` (Apr 2026) wired -`add_dependencies(contracts_project opp_cdt_models.protos)`, so a single -`cmake --build` should now produce the headers in order. If you still see -the error on a cold build, run the protos target explicitly first: +The `contracts_project` ExternalProject needs the zpp-generated `.pb.hpp` headers before it compiles; the +CMake wiring adds that dependency, so a single `cmake --build` produces the headers in order. If you still +see the error on a cold build, run the protos target explicitly first: ```bash ninja -C build/debug opp_cdt_models.protos cmake --build build/debug -j${NUM_JOBS} ``` -### 6.5 Build fails at `libopp.a` with `required tools are not found on PATH` - -`libraries/opp/CMakeLists.txt` runs `generate-opp-bundles.fish` as a -POST_BUILD step on `libopp.a`. If `wire-protobuf-bundler`, -`protoc-gen-solidity`, or `protoc-gen-solana` aren't on `PATH` when ninja -runs that step, the link fails with: - -``` -Error: The following required tools are not found on PATH: - - wire-protobuf-bundler - - protoc-gen-solidity - - protoc-gen-solana -``` - -(wire-sysio commit `b66eea7243` fixed an earlier bug where the CMake -"tools missing → skip" branch never fired due to `find_program` cache -reuse — now that branch works, and CMake will warn-and-skip when the CLIs -are absent. Older builds without that fix attached the POST_BUILD step -unconditionally.) - -Fix: install the three CLIs via §3.1 and ensure `$PNPM_HOME` (typically -`$HOME/.local/share/pnpm`) is on `PATH` before `cmake --build`. - -### 6.6 `wire-cluster-tool create` crashes with `paths[0] must be string` when `--solana-path` is omitted - -Harness `cli.ts:233` calls `Path.resolve(argv.solanaPath)` unconditionally. -Until fixed in harness, always pass `--solana-path=` even -if the Solana program isn't deployed — `SOLBootstrap` will warn and skip -program init when the expected `/wallets/opp-outpost-keypair.json` -is absent. - -### 6.7 `wire-solana` master is the liqSol repo — use the OPP branch - -`wire-solana` on `master` is the Capital Staking / liqSol program and does -not contain `opp-outpost`. Checkout -`feature/opp-solana-outpost-integration`, which adds `programs/opp-outpost/`, -`wallets/opp-outpost-keypair.json`, and the Anchor.toml entry. Then -`anchor build -p opp-outpost` produces the `.so` + IDL the harness expects. +### 6.4 `pkill -f ""` self-terminates your session -### 6.8 `pkill -f ""` self-terminates your session +`pkill -f "solana-test-validator|anvil|kiod|nodeop"` matches the regex against the full argv (including the +pattern string itself), so `pkill` kills its parent shell — the Bash/zsh invocation exits 144 and +`wire-cluster-tool` never runs. Use `killall ` instead. -The Dockerfile's run instructions suggest -`pkill -f "solana-test-validator|anvil|kiod|nodeop"`. Because that regex is -matched against the full argv (including the pattern string itself), `pkill` -kills its parent shell — the Bash/zsh invocation exits 144 and `wire-cluster-tool` -never runs. Use `killall ` instead. +### 6.5 Workstation disk usage above 90% +`nodeop`'s resource monitor shuts a node down when its data volume passes the 90% threshold. Local clusters +set `resource-monitor-not-shutdown-on-threshold-exceeded = true`, but stale cluster directories still +consume space quickly — clear old `--cluster-path` directories before starting several clusters. diff --git a/docs/production-bootstrap.md b/docs/production-bootstrap.md index 5f1a21e10..f5d8b00b5 100644 --- a/docs/production-bootstrap.md +++ b/docs/production-bootstrap.md @@ -12,8 +12,8 @@ defaults*. They split into two kinds: - **Structural / fixed** — core symbol, chain/token codes, ROA byte price, operator caps, authority shapes, the `setemitcfg` split ratios. Production keeps these. - **Deployment-tunable / dev stand-in** — keys, the finalizer set, supplies, epoch duration, collateral - amounts and lock windows, the external (ETH/SOL) chain ids, mock token contract addresses, reserve seed - sizes, operator/producer counts. Production substitutes real keys, the real finalizer set, real external + amounts and lock windows, the external (ETH/SOL) chain ids, mock token contract addresses, syndication + bucket sizes and challenge window, operator/producer counts. Production substitutes real keys, the real finalizer set, real external chain ids/addresses, and final economic policy. Each such value is flagged **(cluster; production: …)**. ## Conventions @@ -27,7 +27,7 @@ defaults*. They split into two kinds: `sysio` (giftram self-reference). `code` = raw wasm bytes (hex); `abi` = PACKED `abi_def` bytes; `vmtype = vmversion = 0`. Both actions are signed `[sysio@active]`. - **slug(X)** — the `slug_name` codename for string `X`, packed into the `{ value: }` shape the - regenerated ABI emits. Chain/token/reserve codes are slug names. + regenerated ABI emits. Chain/token codes are slug names. ## RAM model — no unlimited accounts Every account's RAM is **finite** and **gifted from the `sysio` pool** as a conserving transfer (never minted): @@ -49,8 +49,7 @@ Every account's RAM is **finite** and **gifted from the `sysio` pool** as a cons code/abi bytes from the pool. `giftram` REJECTS a non-finite target (`"giftram target must have a finite RAM limit"`), so the account must already be finite. - **System-contract table rows** — config/state/registration/log rows on the separate-account system - contracts (`sysio.token` plus the OPP set: chains, tokens, epoch, opreg, msgch, uwrit, reserv, chalg, - dclaim) — are billed directly to `sysio` via a per-contract `ram_payer = "sysio"_n` (privileged-contract + contracts (`sysio.token` plus the OPP set, Stage 8) — are billed directly to `sysio` via a per-contract `ram_payer = "sysio"_n` (privileged-contract model). `setsyscode`'s `giftram` covers only code/abi, so a self-billed row would overflow the contract's exact limit; routing rows to the pool keeps each contract account finite at code/abi size. `bios`/`system` code lives on `sysio` and likewise consumes the pool directly. @@ -63,7 +62,7 @@ Every account's RAM is **finite** and **gifted from the `sysio` pool** as a cons - **All `sysio.*` accounts: owner = active = `sysio@active`** — an account-authority delegating to `sysio`: `{threshold:1, keys:[], accounts:[{permission:{actor:"sysio",permission:"active"},weight:1}], waits:[]}`; no standalone key. Governance (`sysio`, msig-backed in production) controls every system account and signs every - `[sysio.X@active]` step. Stage 8 only ADDs `@sysio.code` weights on the nine OPP accounts' **owner** + `[sysio.X@active]` step. Stage 8 only ADDs `@sysio.code` weights on the eleven OPP accounts' **owner** authorities on top of that `sysio@active` base (never removing it). No other authority is rewritten during the bootstrap. - The root **`sysio`** account is the one exception: its own `active` authority carries a **standalone key** @@ -93,11 +92,11 @@ DEV_BLS = BLS.regenerate(SHA256("wire")) # BLS12-381 finalizer key | `DEV_BLS_PUBLIC_KEY` | Genesis `initial_finalizer_key` (bios finalizer). | `DEV_BLS.publicKey` (`PUB_BLS_*` spelling) | | `DEV_BLS_PROOF_OF_POSSESSION` | PoP for `DEV_BLS_PUBLIC_KEY`. | `DEV_BLS.proofOfPossession` (`SIG_BLS_*` spelling) | | Per-node K1/BLS keys | Producer nodes' own block-signing (K1) + finalizer (BLS) keys, distinct from `DEV_*`; used by `setprodkeys` / `setfinalizer` AND as the producer accounts' owner/active keys. | generated at runtime (`clio create key --k1`, `sys-util bls create key`) | -| Per-operator K1/EM/ED keys | Each batch operator / underwriter's UNIQUE identity: a WIRE account key (K1, imported into kiod so `@active` signs), an ETH key (EM, anvil-mnemonic HD-derived), and a SOL key (ED25519). The chain ACCOUNT is node-owner-generated (`wireno.` via `roa::newuser`); the deterministic labels (`batchop.a`, `uwrit.a`) are the newuser sponsor nonces + the tooling's keystore keys. | generated at runtime (`KeyGenerator`) | +| Per-operator K1/EM/ED keys | Each batch operator's UNIQUE identity: a WIRE account key (K1, imported into kiod so `@active` signs), an ETH key (EM, anvil-mnemonic HD-derived), and a SOL key (ED25519). The chain ACCOUNT is node-owner-generated (`wireno.` via `roa::newuser`); the deterministic labels (`batchop.a`, …) are the newuser sponsor nonces + the tooling's keystore keys. | generated at runtime (`KeyGenerator`) | | `BOOTSTRAP_NODE_OWNER` | Bootstrap tier-1 node owner (2–6 chars to satisfy `valid_name_for_tier`); its tier-1 reserve is what post-bootstrap resource policies are issued from. | `wireno` | | `DEFAULT_WALLET_NAME` | kiod wallet the bootstrap creates and every helper re-opens. | `default` | -¹ **`DEV_K1` derivation & governance scope.** `DEV_K1_PUBLIC_KEY` is `K1` regenerated from `SHA256("nathan")` (the well-known dev key, SYS-prefixed). It is the genesis block-signing key, the standalone key on `sysio`'s own `active`, and the owner/active key of node owner `wireno` — nothing else. Producer accounts are keyed by their **hosting node's generated K1**, operators (`batchop.*` / `uwrit.*`) by their **own per-operator generated K1**, and the `sysio.*` system accounts (and `dev.owner1`) by `sysio@active` — production substitutes real keys / msig throughout. +¹ **`DEV_K1` derivation & governance scope.** `DEV_K1_PUBLIC_KEY` is `K1` regenerated from `SHA256("nathan")` (the well-known dev key, SYS-prefixed). It is the genesis block-signing key, the standalone key on `sysio`'s own `active`, and the owner/active key of node owner `wireno` — nothing else. Producer accounts are keyed by their **hosting node's generated K1**, operators (`batchop.*`) by their **own per-operator generated K1**, and the `sysio.*` system accounts (and `dev.owner1`) by `sysio@active` — production substitutes real keys / msig throughout. ### Core symbol, tokens & supplies | Constant | Value | @@ -114,8 +113,9 @@ DEV_BLS = BLS.regenerate(SHA256("wire")) # BLS12-381 finalizer key | `ROA_BYTES_PER_UNIT` | `104` (fixed) | ### Resource-policy weights (`sysio.roa::addpolicy`, issued as `wireno`) — NOT part of the bootstrap -The bootstrap registers `wireno` (Stage 10) so its tier-1 reserve can issue these policies, but issues none -itself — flows/tools provision users and non-bootstrapped operators with them post-bootstrap: +The bootstrap registers `wireno` (Stage 10) so its tier-1 reserve can issue these policies. It issues exactly +one itself — to the `sysio.andon` panic account (Stage 10b); flows/tools provision users and non-bootstrapped +operators with them post-bootstrap: | Field | Value | |---|---| @@ -130,7 +130,7 @@ itself — flows/tools provision users and non-bootstrapped operators with them | `producerCount` | `21` (`MAX_PRODUCERS`) | accounts `defproducera … defproduceru` | | `nodeCount` | `1` **(cluster)** | producer nodes hosting the producers | | `batchOperatorCount` | `3` **(cluster)** | labels `batchop.a/b/c` (chain accounts are `wireno.`, node-owner-generated) | -| `underwriterCount` | `1` **(cluster)** | label `uwrit.a` (chain account likewise generated) | +| `underwriterCount` | `0` | swap-underwriter daemons have been removed; any nonzero value is rejected | | `apiCount` | `0` **(cluster)** | non-producing API nodes serving the chain API + `/v1/query/execute` | | `epochDurationSec` | `90` **(cluster; production: real cadence)** | | | `EnvelopeLogRetentionEpochs` | `10` | `sysio.epoch::setconfig.epoch_retention_envelope_log_count` | @@ -144,14 +144,19 @@ itself — flows/tools provision users and non-bootstrapped operators with them effectively active from genesis — wire-sysio whitelists the `preactivate_feature` intrinsic in its genesis intrinsic set — so the tooling simply skips its digest; no producer-API scheduling step exists. "already activated" errors are benign and ignored. -3. `sysio::setfinalizer({finalizer_policy:{threshold, finalizers:[…]}})` — `[sysio@active]`: - - `threshold = floor(N*2/3) + 1`, where `N` = number of producer nodes (cluster default `N = 1` ⇒ - `threshold = 1`). - - each finalizer `= {description:"finalizer-", weight:1, public_key:, pop:}`. These are the **producer nodes' own generated BLS - keys**, not `DEV_BLS` (which is only the genesis/bios finalizer). - The chain finalizes on these node BLS keys and keeps producing on the genesis `sysio` producer until the - Stage 5 handoff — no early `setprods`/`setprodkeys` (producer accounts don't exist pre-pool). +3. `sysio::setfinalizer({finalizer_policy:{threshold, finalizers:[…]}})` — `[sysio@active]` — a bios-ABI + action, so it runs before `system` replaces `bios` in Stage 3: + - `threshold = floor(N*2/3) + 1`, where `N` = number of producer **accounts** (`producerCount = 21` ⇒ + `threshold = 15`). + - each finalizer `= {description:, weight:1, public_key:, pop:}`. Every producer account owns a BLS finalizer key minted for + it before any node starts (its block-signing K1 stays shared with its hosting node); these are not + `DEV_BLS` (which is only the genesis/bios finalizer). The policy is keyed on accounts, not nodes, + because `sysio.system` rebuilds the finalizer policy from the keys accounts registered via `regfinkey` + (step 23) the first time producer ranking publishes; a node-keyed genesis policy would be replaced by + one the running nodes hold no key for. + The chain finalizes on these keys and keeps producing on the genesis `sysio` producer until the Stage 5 + handoff — no early `setprods`/`setprodkeys` (producer accounts don't exist on chain pre-pool). ## Stage 2 — Bring-up-essential accounts only (native `newaccount`, pre-ROA) 4. `sysio::newaccount({creator:"sysio", name, owner:sysio@active, active:sysio@active})` — `[sysio@active]` — @@ -189,7 +194,7 @@ unlimited. | Group | Accounts | |---|---| | System / authority | `sysio.noop`, `sysio.bpay`, `sysio.msig`, `sysio.names`, `sysio.token`, `sysio.vpay`, `sysio.wrap`, `sysio.authex` | -| OPP set | `sysio.chains`, `sysio.tokens`, `sysio.epoch`, `sysio.opreg`, `sysio.msgch`, `sysio.uwrit`, `sysio.reserv`, `sysio.chalg`, `sysio.dclaim` | +| OPP set | `sysio.chains`, `sysio.tokens`, `sysio.epoch`, `sysio.opreg`, `sysio.msgch`, `sysio.chalg`, `sysio.dclaim`, `sysio.swap`, `sysio.liq`, `sysio.andon`, `sysio.bond`, `sysio.synd` | | T5 buckets | `sysio.gov` (governance), `sysio.ops` (capex/ops) | | Dev-only (cluster) | `dev.owner1` | 11. `sysio::setprodkeys({schedule:[{producer_name, block_signing_key}…]})` — `[sysio@active]` — one row per @@ -216,13 +221,17 @@ standalone key, and `sysio.authex` keeps the plain `sysio@active` owner/active i ## Stage 8 — OPP contracts + owner `sysio.code` grants 18. deploy the OPP set — **sys** — `[sysio@active]`, in order: `sysio.chains`, `sysio.tokens`, `sysio.epoch`, - `sysio.opreg`, `sysio.msgch`, `sysio.uwrit`, `sysio.reserv`, `sysio.chalg`, `sysio.dclaim`. + `sysio.opreg`, `sysio.msgch`, `sysio.chalg`, `sysio.dclaim`, `sysio.andon`, `sysio.swap`, `sysio.liq`, + `sysio.bond`, `sysio.synd`. `sysio.andon` precedes the four contracts that read its cord (swap, liq, bond, + synd), `sysio.bond` precedes `sysio.synd`, and `sysio.synd` precedes the first liq-token registration + (Stage 11). 19. `sysio::updateauth` on **each OPP account's owner** (`grantSysioCode`) — `[@owner]` — owner ← `{threshold:1, keys:[], accounts:[{sysio@active,1},{@sysio.code,1}], waits:[]}` (sorted by name - value; `sysio` sorts first). (9 calls.) Lets each contract inline-send its own actions (epoch `advance`, - `evalcons`, `dispatch`, …) while staying governed by `sysio@active`. + value; `sysio` sorts first). (11 calls: every OPP account except `sysio.andon`, which sends no inline + actions.) Lets each contract inline-send its own actions (epoch `advance`, `evalcons`, `dispatch`, …) + while staying governed by `sysio@active`. -These nine owner grants are the ONLY authority rewrites in the bootstrap. The former cross-contract +These eleven owner grants are the ONLY authority rewrites in the bootstrap. The former cross-contract active-permission delegations (`@sysio.code` weights for `sysio.msgch` on opreg/roa and for `sysio.roa` on authex) are no longer configured. @@ -235,7 +244,7 @@ uses `sysio.roa::setsyscode`, which grants privilege as part of deployment, so t `setpriv` action—is the production precondition. This bootstrap satisfies it: DClaim is deployed in Stage 8 before the first node-owner link in Stage 10 and operator `createlink` calls in Stage 12. -## Stage 9 — OPP / application configuration (epoch, opreg, emissions, dclaim) +## Stage 9 — OPP / application configuration (epoch, opreg, genesis producer registration, emissions, dclaim) 20. `sysio.epoch::setconfig({epoch_duration_sec:90, operators_per_epoch:1, batch_operator_minimum_active:3, batch_op_groups:3, epoch_retention_envelope_log_count:10})` — `[sysio.epoch@active]`. Sizing is computed from the operator counts: @@ -249,7 +258,7 @@ before the first node-owner link in Stage 10 and operator `createlink` calls in |---|---|---| | `max_available_producers` | `21` | | | `max_available_batch_ops` | `63` | | -| `max_available_underwriters` | `21` | | +| `max_available_underwriters` | `21` | still part of the `opreg` config; the bootstrap registers no underwriter operators | | `terminate_prune_delay_ms` | `600000` | 10 min **(cluster; production: larger)** | | `terminate_max_consecutive_misses` | `5` | | | `terminate_max_pct_misses_24h` | `5` | | @@ -258,7 +267,27 @@ before the first node-owner link in Stage 10 and operator `createlink` calls in | `req_batchop_collat` | `[]` | empty by default | | `req_uw_collat` | `[]` | empty by default | -22. **WIRE token + emissions** — `sysio.token` is reused for a separate `9,WIRE` token the emissions contract +22. **Genesis producer operators** — `sysio.opreg::regoperator({account:, + type:OPERATOR_TYPE_PRODUCER, is_bootstrapped:true})` — `[sysio.opreg@active]` — one call per producer + account (`defproducera … defproduceru`). Registered here rather than in Stage 5 because `sysio.opreg` is + only deployed (Stage 8) and configured (step 21) by now. Bootstrapped by fiat: genesis producers post no + collateral, `req_prod_collat` ships empty, and a non-bootstrapped registration against an empty + requirement vector would never become eligible. As bootstrapped operators they are also exempt from + `termcheck`. +23. **Genesis producer registration** — per producer account, in this order: + - `sysio::regproducer({producer:, producer_key:, url:"", location:0})` — + `[@active]`. `sysio.system` admits it only for an ACTIVE PRODUCER operator, so it follows + step 22. Registration is what makes a producer rankable: the ranked schedule is built only from + producers with an active `producers` row. + - `sysio::regfinkey({finalizer_name:, finalizer_key:, + proof_of_possession:})` — `[@active]`. Requires the `producers` row written just + above; a producer's first finalizer key is activated by `regfinkey` itself, so no `actfinkey` follows. + The key is the same per-account BLS key the Stage 1 finalizer policy was built from. + + The producer, finalizer-key and finalizer rows are billed to `sysio.system`; producers receive no RAM + grant for them. The bootstrap makes no producer collateral deposit. + +24. **WIRE token + emissions** — `sysio.token` is reused for a separate `9,WIRE` token the emissions contract reads from `sysio`'s balance. Five actions, in order: - `sysio.token::create({issuer:"sysio", maximum_supply:"1000000000.000000000 WIRE"})` — `[sysio.token@active]`. - `sysio.token::issue({to:"sysio", quantity:"1000000000.000000000 WIRE", memo:"initial WIRE for emissions"})` — `[sysio@active]`. @@ -304,17 +333,17 @@ before the first node-owner link in Stage 10 and operator `createlink` calls in | `epoch_log_retention_count` | `8640` | emissions pay-log retention, in epochs | | `pay_cadence_epochs` | `1` | fire `payepoch` every epoch **(cluster; production: higher)** | -23. `sysio.dclaim::setconfig({})` — `[sysio.dclaim@active]` — idempotent; creates the `cap_config` singleton +25. `sysio.dclaim::setconfig({})` — `[sysio.dclaim@active]` — idempotent; creates the `cap_config` singleton with the contract's default 180-day claim window. (No `setclmwindow`/`importseed`/`importdone` in the bootstrap — those are external/operational tools, not part of the sequence.) ## Stage 10 — Register the bootstrap node owner (real `nodeownreg` flow; NO `forcereg`) Drives the two `sysio.roa` actions the OPP NFT-claim depot (`sysio.msgch`) would inline-send for a real claim: -24. `sysio.roa::newnameduser({account:"wireno", pubkey:DEV_K1_PUBLIC_KEY, tier:1})` — `[sysio.roa@active]` — +26. `sysio.roa::newnameduser({account:"wireno", pubkey:DEV_K1_PUBLIC_KEY, tier:1})` — `[sysio.roa@active]` — creates `wireno` (owner = active = `DEV_K1`) with a finite pool-gifted RAM allocation. `tier:1` = T1 (Validator); `NodeOwnerTier` = `{T1:1, T2:2, T3:3}`. -25. `sysio.roa::nodeownreg({owner:"wireno", tier:1, eth_pub_key:, wire_pub_key:DEV_K1_PUBLIC_KEY, eth_address:<20-byte ETH address>})` — +27. `sysio.roa::nodeownreg({owner:"wireno", tier:1, eth_pub_key:, wire_pub_key:DEV_K1_PUBLIC_KEY, eth_address:<20-byte ETH address>})` — `[sysio.roa@active]` — records the depositor ETH key as a `sysio.authex` link (inline `recordlink`) and allocates the tier-1 reserve post-bootstrap resource policies are issued from. `eth_pub_key` is a **fresh random `PUB_EM_*` secp256k1 key (cluster throwaway; production: the NFT depositor's key)**; its @@ -323,15 +352,35 @@ Drives the two `sysio.roa` actions the OPP NFT-claim depot (`sysio.msgch`) would aborting the transaction, so the tooling follows with a verify that the `nodeowners` row exists (surfacing the audit rejection if not). -## Stage 11 — Outpost deploys, then registry seeding + underwriter config +## Stage 10b — Emergency stop (`sysio.andon`) +`setpanic` needs an existing account, so the panic account is created first, through the ordinary user path. + +28. **Panic account** — `sysio::newaccount({creator:"sysio", name:"andon.panic", owner:DEV_K1_PUBLIC_KEY, + active:DEV_K1_PUBLIC_KEY})` — `[sysio@active]` — then `sysio.roa::addpolicy({owner:"andon.panic", + issuer:"wireno", net_weight:"25.0000 SYS", cpu_weight:"25.0000 SYS", ram_weight:"25.0000 SYS", + time_block:0, network_gen:0})` — `[wireno@active]`. No WIRE is transferred to it **(cluster dev key; + production: the governance-held panic key)**. +29. `sysio.andon::setpanic({account:"andon.panic"})` — `[sysio.andon@active]` — the account that may pull and + clear the cord. +30. `sysio.andon::addpuller({contract:"sysio.synd"})` — `[sysio.andon@active]` — registers `sysio.synd` as a + puller, so a custody shortfall it detects pulls the cord. The cord is armed before any cord reader carries + traffic. + +## Stage 11 — Outpost deploys, then registry + syndication configuration The ETH and SOL outposts deploy here (chain-side, not depot actions): anvil starts (instamine), the Ethereum -outpost contracts deploy + seed, anvil switches to interval mining; solana-test-validator starts with -`liqsol_core` (the OPP outpost), then PDAs init + SPL reserves provision. These deploys produce the artifact -files (`outpost-addrs.json`, `liqeth-addrs.json`, `sol-mock-mints.json`) the registry rows below read their -chain-side addresses from; production registers the canonical contract/mint addresses via the same shapes. - -26. **Chains** — `sysio.chains::regchain({kind, code, external_chain_id, name, description})` — - `[sysio.chains@active]`, one per chain (registered ACTIVE; there is no separate `activchain`): +outpost contracts deploy (`OPP`, `OPPInbound`, `OutpostManager`, `SyndicationPool`, `BAR`, the inert +`StakingManager`, plus the `liqEth` suite) and the tooling verifies the `SyndicationPool` configuration and +the panic account's pause role, then anvil switches to interval mining; solana-test-validator starts with all +four wire-solana programs at genesis (`liqsol_core` hosts the OPP outpost), the liqsol surface is stood up by +wire-solana's own `init-*` scripts, and the OPP PDAs are initialized with the native-SOL token binding and +precision. These deploys produce the artifact files (`outpost-addrs.json`, `liqeth-addrs.json`) the registry +rows below read their chain-side addresses from; in external-outpost mode the operator-supplied artifacts +(including `sol-mock-mints.json`) are copied into place instead. Production registers the canonical +contract/mint addresses via the same shapes. + +31. **Chains** — `sysio.chains::regchain({kind, code, external_chain_id, name, description, outpost})` — + `[sysio.chains@active]`, one per chain (registered ACTIVE; there is no separate `activchain`). `outpost` + is left empty here and filled by `setoutpost` in Stage 12: | `kind` | `code` | `external_chain_id` | `name` | `description` | |---|---|---|---|---| @@ -339,92 +388,110 @@ chain-side addresses from; production registers the canonical contract/mint addr | `CHAIN_KIND_EVM` | `slug("ETHEREUM")` | `31337` **(cluster anvil; production: real EVM id)** | `Ethereum (anvil)` | local anvil EVM chain | | `CHAIN_KIND_SVM` | `slug("SOLANA")` | `0` | `Solana (test-validator)` | local solana-test-validator | -27. **Tokens** — `sysio.tokens::regtoken({kind, code, symbol_name, description, precision, address})` — +32. **Tokens** — `sysio.tokens::regtoken({kind, code, symbol_name, description, precision, address})` — `[sysio.tokens@active]`, one per token (registered ACTIVE; no separate `activtoken`). `precision` is `9` for NATIVE/LIQ tokens and `6` for the ERC-20/SPL stablecoins (their chain-native decimals). `address = {kind, address}`; NATIVE leaves `address` empty; non-native carries the chain-side contract bytes (hex, - `0x` stripped): + `0x` stripped) when the deploy artifact provides them, and is empty otherwise: | `kind` | `code` | `symbol_name` | `precision` | `address` source | |---|---|---|---|---| | `TOKEN_KIND_NATIVE` | `slug("WIRE")` | `Wire` | `9` | empty | | `TOKEN_KIND_NATIVE` | `slug("ETH")` | `Ether` | `9` | empty | | `TOKEN_KIND_LIQ` | `slug("LIQETH")` | `Liquid ETH` | `9` | deployed LiqETH EVM address **(runtime)** | -| `TOKEN_KIND_ERC20` | `slug("USDC")` | `USD Coin` | `6` | mock USDC EVM address **(runtime)** | -| `TOKEN_KIND_ERC20` | `slug("USDT")` | `Tether USD` | `6` | mock USDT EVM address **(runtime)** | +| `TOKEN_KIND_ERC20` | `slug("USDC")` | `USD Coin` | `6` | `MockUsdc` from `outpost-addrs.json` when present (the local deploy no longer ships one, so empty) | +| `TOKEN_KIND_ERC20` | `slug("USDT")` | `Tether USD` | `6` | `MockUsdt` from `outpost-addrs.json` when present (likewise empty locally) | | `TOKEN_KIND_NATIVE` | `slug("SOL")` | `Sol` | `9` | empty | -| `TOKEN_KIND_LIQ` | `slug("LIQSOL")` | `Liquid SOL` | `9` | mock LIQSOL SPL mint **(runtime)** | -| `TOKEN_KIND_SPL` | `slug("USDCSOL")` | `USDC (Solana)` | `6` | mock USDC SPL mint **(runtime)** | -| `TOKEN_KIND_SPL` | `slug("USDTSOL")` | `USDT (Solana)` | `6` | mock USDT SPL mint **(runtime)** | +| `TOKEN_KIND_LIQ` | `slug("LIQSOL")` | `Liquid SOL` | `9` | LIQSOL mint from `sol-mock-mints.json` when present | +| `TOKEN_KIND_SPL` | `slug("USDCSOL")` | `USDC (Solana)` | `6` | USDC mint from `sol-mock-mints.json` when present | +| `TOKEN_KIND_SPL` | `slug("USDTSOL")` | `USDT (Solana)` | `6` | USDT mint from `sol-mock-mints.json` when present | -28. **Chain-token bindings** — `sysio.tokens::regctok({chain_code, token_code, contract_addr, is_native})` — +33. **Chain-token bindings** — `sysio.tokens::regctok({chain_code, token_code, contract_addr, is_native})` — `[sysio.tokens@active]`, one per binding (no separate `activctok`). Exactly one `is_native:true` per chain; non-native bindings carry the same chain-side address bytes as their token row (empty when unavailable): `(WIRE,WIRE,native)`, `(ETHEREUM,ETH,native)`, `(ETHEREUM,LIQETH)`, `(ETHEREUM,USDC)`, `(ETHEREUM,USDT)`, `(SOLANA,SOL,native)`, `(SOLANA,LIQSOL)`, `(SOLANA,USDCSOL)`, `(SOLANA,USDTSOL)`. -29. **Reserves** — `sysio.reserv::regreserve({chain_code, token_code, reserve_code:slug("PRIMARY"), name, - description, initial_chain_amount, initial_wire_amount:10000000000, source_token_precision, - connector_weight_bps:5000, is_private:false, owner:""})` — `[sysio.reserv@active]`, one PRIMARY reserve - per external chain-token (registered ACTIVE). Eight reserves: `ETHEREUM×{ETH, LIQETH, USDC, USDT}` and - `SOLANA×{SOL, LIQSOL, USDCSOL, USDTSOL}`. Every reserve seeds a 10-token notional on each leg **(cluster - devnet sizing; production: real seeds)**, 50% Bancor connector weight, public, no owner. The depot frames - each chain leg at `min(native, 9)` decimals, recorded per-reserve as `source_token_precision`: - - non-stable rows: `initial_chain_amount = 10,000,000,000` (9-dec frame), `source_token_precision = 9`; - - stablecoin rows (`USDC`/`USDT`/`USDCSOL`/`USDTSOL`): `initial_chain_amount = 10,000,000` (the same 10 - tokens in their 6-dec native frame), `source_token_precision = 6`. - -30. `sysio.uwrit::setconfig({fee_bps:30, collateral_lock_duration_ms:600000, min_fromwire_amount:100000000, - fromwire_revert_fee_bps:10})` — `[sysio.uwrit@active]`: - - `fee_bps = 30` — the per-spoke swap fee, taken out of the WIRE leg of every swap; `sysio.reserv` routes - the collected fee 50/50 to its rewards bucket and the `sysio` emissions treasury - (`FEE_REWARD_SHARE_BPS`) **(cluster 30; the contract default is 10)**. - - `collateral_lock_duration_ms = 600000` — the **wall-clock** challenge window: locks are never released - by delivery; they expire this many ms after creation and are swept by `chklocks` at epoch advance - **(cluster 10 min; the contract default is 12 h = 43,200,000 ms — production uses that)**. - - `min_fromwire_amount = 100000000` — minimum `swapfromwire` escrow (9-dec base units) = 0.1 WIRE - **(cluster, matching the flow's escrow exactly; the contract default is 5 WIRE = 5,000,000,000)**. - - `fromwire_revert_fee_bps = 10` — fee on caller-fault drain-time reverts of queued `swapfromwire` rows - (zero quote / missed variance at `drainfwq`), routed like the settlement fee (mirrors the contract - default; happy-path flows never pay it and system-caused reverts refund in full). +34. `sysio.swap::setconfig({fee_authority:"sysio", system_token:{sym:"9,WIRE", contract:"sysio.token"}})` — + `[sysio.swap@active]` — the depot-local AMM's fee authority and system token. `sysio.swap` trades on the + depot only; it has no outpost participation and emits no OPP attestations. +35. **Shadow liq symbols** — `sysio.liq::create({sym, chain_code, token_code})` — `[sysio.liq@active]`, one per + 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). + - `sysio.synd::setconfig({...})` — `[sysio.synd@active]`, one per shadow pair (`ETHEREUM/LIQETH`, + `SOLANA/LIQSOL`). A pair with no row releases no syndication and accepts no desyndication: + +| Field | Value | Notes | +|---|---|---| +| `synd_fee_bps` / `desynd_fee_bps` | `0` / `0` | **(cluster; production: real fees)** | +| `synd_burst` / `synd_refill` | `1000000000000000` / same | one million tokens at 9 decimals per bucket and per-epoch refill **(cluster)** | +| `desynd_burst` / `desynd_refill` | `1000000000000000` / same | likewise | +| `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`) | + +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` +drops every `SYNDICATE_LIQ` of the pair). +38. **Mock data — opt-in only, never production.** Both run inside the epoch-0 window: + - `--enable-mock-liq-pools`: `sysio.liq::regliqpool` × 2 — `[sysio.liq@active]` — a `LIQETHP` / `LIQSOLP` + yield pool per shadow on `sysio.swap`, each seeded with `10000000000` shadow and `10000000000` WIRE base + units, fee `30`, 30 s conversion horizon, 3000 bps depth cap, clip floor `1000`. + - `--enable-mock-syndication-import`: `sysio.synd::importsynd` × 2 — one parked `100000000000` (100-token) + position each of LIQSOL (ED key) and LIQETH (EM key) for an unlinked mock bonder — then + `sysio.synd::importdone`, and a verify that both are parked with no custody mismatch. + - With either flag, outpost custody is then funded (Solana pool ATA, Ethereum `SyndicationPool`) to back + all outstanding mock shadow. ## Stage 12 — Operator provisioning + first epoch The genesis-replacing real producer schedule is already live. NOTHING here uses `forcereg`. -31. **Operator accounts (node-owner-created)** — `sysio.roa::newuser({creator:"wireno", nonce: