Files
agents-marketplace/tests/unit/tx-assert-collateral.test.ts
david 972c0f9b89 wallet: tx:consolidate-wallet CLI + collateral pre-check across script spends
Adds a one-shot operational tx that re-shapes any operator wallet to
{5 AP3X collateral candidate, working remainder} so subsequent Plutus
script-spends (Claim/Submit/Accept/Reclaim/RetireAdvert) find a UTxO
that satisfies lucid's setCollateral=5_000_000n default coin selector.

Symptom this addresses: open-webui mainnet supplier wallet held 8 UTxOs
max 2.40 AP3X, every Accept tx failed at lucid .complete() with the
"wallet must hold a pure-ADA UTxO ≥ 5 ADA" error path. Same fragmentation
risk on the other 4 mainnet operator wallets (one big UTxO surrounded by
dust — first script-spend consumes the big one and leaves nothing usable
as collateral).

Phase 1 — new tx:consolidate-wallet CLI (supplier + buyer):
  - planConsolidate.ts — pure planner; supports consolidate (N>1 → 2),
    split (1 → 2), and already-healthy skip.
  - consolidateWallet.ts — live builder; presetWalletInputs pins all
    UTxOs, two pay.ToAddress outputs (collateral + working).
  - runConsolidateWallet.ts — orchestration (build + submit + awaitTx).
  - supplier/src/cli/consolidate-wallet.ts + buyer mirror.
  - monitor-wallets.ts — add supplier-tts@vector-marketplace (was
    misclassified as 3rd-party).

Phase 2 — defensive pre-check in the 5 script-spend builders:
  - assertCollateralCandidate(utxos) fires at the top of
    buildSpendOpenOrClaimedTx (Claim/Submit), buildLiveTxForAccept,
    buildLiveTxForReclaim, buildLiveTxForRetireAdvert. Throws a
    TxConstructionError("collateral_required") pointing at
    tx:consolidate-wallet instead of waiting for lucid's deep failure.

Tests: 12 new passing (planConsolidate edge cases + assert helper).
Pre-existing lucid 0.4.30 WASM "unreachable" failures in tx-*-live.test.ts
fixtures are unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 10:59:11 +02:00

74 lines
2.6 KiB
TypeScript

/**
* tx-assert-collateral.test.ts — unit tests for the assertCollateralCandidate
* pre-check that fires from each script-spend builder.
*/
import { describe, it, expect } from "vitest";
import { assertCollateralCandidate } from "../../packages/shared/src/tx/internal/liveCbor.js";
import { TxConstructionError } from "../../packages/shared/src/tx/types.js";
type FakeLucidUtxo = { assets: Record<string, bigint> };
function lov(n: bigint): FakeLucidUtxo {
return { assets: { lovelace: n } };
}
describe("assertCollateralCandidate", () => {
it("passes when one UTxO is ≥ 5 AP3X pure-AP3X", () => {
const utxos = [lov(1_000_000n), lov(5_000_000n), lov(2_400_000n)];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect(() => assertCollateralCandidate(utxos as any)).not.toThrow();
});
it("passes when one UTxO is much larger than 5 AP3X", () => {
const utxos = [lov(1_000_000_000n)];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect(() => assertCollateralCandidate(utxos as any)).not.toThrow();
});
it("throws when the wallet has no UTxO ≥ 5 AP3X", () => {
const utxos = [
lov(2_400_750n),
lov(1_679_301n),
lov(1_679_301n),
lov(1_679_301n),
];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let caught: unknown;
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
assertCollateralCandidate(utxos as any);
} catch (e) {
caught = e;
}
expect(caught).toBeInstanceOf(TxConstructionError);
expect((caught as TxConstructionError).reason).toBe("collateral_required");
expect((caught as TxConstructionError).message).toMatch(
/tx:consolidate-wallet/,
);
expect((caught as TxConstructionError).message).toMatch(/4 UTxO/);
expect((caught as TxConstructionError).message).toMatch(/2400750/);
});
it("throws when the wallet is empty", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect(() => assertCollateralCandidate([] as any)).toThrow(
TxConstructionError,
);
});
it("skips UTxOs that carry native assets even when ≥ 5 AP3X", () => {
// On Vector L2 the operator wallets are pure-AP3X, but the safety net
// catches a hypothetical UTxO with native tokens — lucid's collateral
// selector rejects those.
const utxos = [
{ assets: { lovelace: 10_000_000n, ["d3.tok"]: 1n } },
lov(1_000_000n),
];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect(() => assertCollateralCandidate(utxos as any)).toThrow(
/collateral/i,
);
});
});