fix(builder): disarm remaining seed-vocab landmine, tighten tier-1 amount assertion

Opus review round 2 on Task 6:

- agent-network.ts: seedUtxo -> oneShotUtxo (5 occurrences). This is
  one-shot-minting-policy vocabulary that survives PR 7's keyless rewrite,
  but the boundary test's FORBIDDEN regex is an unanchored case-insensitive
  substring match on "seed" - left as-is it would have false-red PR 7's
  guard the moment agent-network.ts drops off the custodial allowlist. Only
  selectWallet.fromSeed (genuine key-material vocabulary, deleted by PR 7
  anyway) still matches `seed` in this file. File is @ts-nocheck, so this
  was verified mechanically, not just visually: line-match counts before
  (seedUtxo=5) and after (seedUtxo=0, oneShotUtxo=5) line up exactly, and a
  fresh `import()` of the module still loads error-free.
- keyless-build.test.ts: the first tier-1 test summed every output
  (`paid >= 3_000_000n`), but a self-send's change output lands at the same
  address as the payment, so the sum floor could never actually detect a
  dropped or wrong payment amount - change alone clears it. Added an
  exact-count check (outputs at exactly 3_000_000n lovelace, >=1) alongside
  the existing sum/fee/unsigned assertions, matching the pattern already
  used by the build_transaction test in the same file. Also added the
  missing assertBuildNotSubmitted check on the dry-run test's own build
  response.
- run.test.ts: deleted walletBalanceAda - write-only since the UTxO
  Consolidation block (its only reader) was removed earlier in this task.

Re-verified full gate plus one live re-run of the tightened tier-1 suite
(build-only, no funds spent, no E2E resubmission).
This commit is contained in:
FilipBl4gojevic
2026-07-29 13:56:33 +02:00
parent 9799cbd8ad
commit dde221fdba
3 changed files with 16 additions and 8 deletions

View File

@@ -306,21 +306,21 @@ ${capList}
if (!vkeyHash) throw new Error('Cannot derive payment key hash from wallet address');
const utxos = await lucid.utxosAt(await lucid.wallet().address());
const seedUtxo = utxos.find(u => {
const oneShotUtxo = utxos.find(u => {
const keys = Object.keys(u.assets);
return keys.length === 1 && keys[0] === 'lovelace' && u.assets['lovelace'] >= MIN_AP3X_DEPOSIT + 2_000_000n;
}) || utxos[0];
if (!seedUtxo) throw new Error('No UTxOs in wallet. Please fund the wallet first.');
if (!oneShotUtxo) throw new Error('No UTxOs in wallet. Please fund the wallet first.');
const nftAssetName = deriveNftAssetName(seedUtxo.txHash, seedUtxo.outputIndex);
const nftAssetName = deriveNftAssetName(oneShotUtxo.txHash, oneShotUtxo.outputIndex);
const nftUnit = `${REGISTRY_POLICY_ID}${nftAssetName}`;
const datum = buildAgentDatum(vkeyHash, name, description, capabilities, framework, endpoint);
const registryScript = { type: 'PlutusV3' as const, script: REGISTRY_SCRIPT_CBOR };
const registryAddress = validatorToAddress('Mainnet', registryScript);
const registerRedeemer = Data.to(new Constr(0, [new Constr(0, [seedUtxo.txHash, BigInt(seedUtxo.outputIndex)])]));
const registerRedeemer = Data.to(new Constr(0, [new Constr(0, [oneShotUtxo.txHash, BigInt(oneShotUtxo.outputIndex)])]));
const tx = await lucid.newTx()
.collectFrom([seedUtxo])
.collectFrom([oneShotUtxo])
.mintAssets({ [nftUnit]: 1n }, registerRedeemer)
.attach.MintingPolicy(registryScript)
.pay.ToAddressWithData(registryAddress, { kind: "inline", value: datum }, { lovelace: MIN_AP3X_DEPOSIT, [nftUnit]: 1n })

View File

@@ -45,11 +45,20 @@ describe('keyless build against live testnet', () => {
const tx = CML.Transaction.from_cbor_hex(cbor);
const outs = tx.body().outputs();
let paid = 0n;
// A self-send means the payment output and the change output land at the
// same address, so summing every output can't tell a correct 3 AP3X
// payment apart from a dropped/wrong one (change alone dwarfs the floor).
// Count outputs at exactly the requested lovelace amount instead - the
// same exact-count pattern the build_transaction test below uses.
let paymentOutputs = 0;
for (let i = 0; i < outs.len(); i++) {
const o = outs.get(i);
assert.equal(o.address().to_bech32(), OWN_ADDRESS, 'self-send must only pay the own address');
paid += BigInt(o.amount().coin().toString());
const lovelace = BigInt(o.amount().coin().toString());
paid += lovelace;
if (lovelace === 3_000_000n) paymentOutputs++;
}
assert.ok(paymentOutputs >= 1, `expected a 3 AP3X payment output, found ${paymentOutputs} (total paid: ${paid})`);
const fee = BigInt(tx.body().fee().toString());
assert.ok(fee >= 156_253n && fee <= 500_000n, `fee out of sane range: ${fee}`);
const vkeys = tx.witness_set().vkeywitnesses();
@@ -62,6 +71,7 @@ describe('keyless build against live testnet', () => {
const buildText = await callTool(ctx.client, 'vector_build_send_apex', {
changeAddress: OWN_ADDRESS, recipientAddress: OWN_ADDRESS, amount: 3,
});
assertBuildNotSubmitted(buildText);
const cbor = extractCbor(buildText);
const text = await callTool(ctx.client, 'vector_dry_run', { txCbor: cbor });
assert.match(text, /Valid: Yes/);

View File

@@ -10,7 +10,6 @@ let ctx: ServerContext;
let mnemonic: string;
let walletAddress: string;
let walletHasAda = false;
let walletBalanceAda = 0;
let agentDid: string | null = null;
before(async () => {
@@ -66,7 +65,6 @@ describe('Wallet Tools', () => {
const balanceMatch = text.match(/AP3X Balance:\s*([\d.]+)/);
if (balanceMatch && parseFloat(balanceMatch[1]) > 0) {
walletHasAda = true;
walletBalanceAda = parseFloat(balanceMatch[1]);
console.log(`Wallet funded: ${balanceMatch[1]} AP3X`);
} else {
console.log('Wallet has 0 AP3X - transaction tests will verify error handling');