test(builder): registry tier-1 + gated lifecycle E2E; shared sign helper; legacy suite goes keyless
Adds the three remaining integration layers for spec PR 7 (agent-registry keyless): a tier-1 suite that exercises the five build_* tools against the live testnet registry with zero secrets (build-only, nothing signed or submitted), a gated full lifecycle E2E (register -> update -> deregister) that actually submits, and a shared signWithMnemonic() helper extracted from keyless-e2e.test.ts so registry-e2e.test.ts and run.test.ts's registry section don't each re-inline the same CML signing sequence. run.test.ts's "Agent Network Tools" section is rewritten: each of the five old custodial calls is now build_* -> signWithMnemonic -> submit, via a new buildSignSubmit() helper that reuses the file's existing assertSuccessOrKnownError convention at both the build and submit legs. Found and fixed one load-bearing bug while proving this live, not just in review: the inline signing block moved verbatim per the brief's Step 1 built the signed tx's witness set from scratch, which is lossless only for a script-less payment (the one case it had been proven against). The registry family's build_* tools attach a minting policy and/or spending validator, so their unsigned CBOR already carries scripts/redeemers that the from-scratch witness set silently dropped - the ledger rejected the first live E2E attempt with MissingScriptWitnessesUTXOW / PPViewHashesDontMatch. Verified zero funds were lost (balance identical before/after the rejected submission - a mempool-level rejection never reaches a block) before fixing it by merging into the tx's existing witness set via CML's add_all_witnesses instead of discarding it. Re-run clean: all 3 E2E transactions confirmed on-chain, plus all 5 of the legacy suite's registry operations submitted successfully too (independent confirmation across both the mint and spend redeemer paths). Also widened one tier-1 regex that didn't tolerate the live tool's Markdown-bold label formatting. Live evidence (testnet, funded wallet, 8 real transactions total across the gated E2E and the legacy suite): balance 986.689510 -> 982.280960 AP3X, fully reconciled against transaction fees plus one message delivery to an owner-derived enterprise address. All tx hashes, explorer links, and the full reconciliation are in task-5-report.md. mnemonic.txt and the two temporary evidence-capture scripts were removed and confirmed absent before this commit. Gates: typecheck 0, unit 350/350, smoke 26/26, auth 12/12, boundary 4/4 - all re-verified after cleanup. E2E skip-path (VECTOR_E2E_SUBMIT unset) exits 0 with tests 0 for both keyless-e2e.test.ts and registry-e2e.test.ts, confirmed with mnemonic.txt genuinely absent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
// packages/builder/test/integration/keyless-e2e.test.ts
|
||||
import { describe, test, before, after } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { CML, walletFromSeed } from '@lucid-evolution/lucid';
|
||||
import { walletFromSeed } from '@lucid-evolution/lucid';
|
||||
import { startServer, stopServer, callTool, getMnemonic, ServerContext } from '../setup.ts';
|
||||
import { signWithMnemonic } from './sign-helper.ts';
|
||||
|
||||
// Full non-custodial pipeline against the live testnet:
|
||||
// keyless build (hosted tool) -> inline CML sign (this process, standing in
|
||||
@@ -31,7 +32,8 @@ describe('keyless E2E: build -> sign -> submit -> await', { skip: !RUN }, () =>
|
||||
after(async () => { await stopServer(ctx); });
|
||||
|
||||
test('the four-call non-custodial flow lands a self-send on-chain', { timeout: 300_000 }, async () => {
|
||||
const { address, paymentKey } = walletFromSeed(getMnemonic(), { network: 'Mainnet', accountIndex: 0 });
|
||||
const mnemonic = getMnemonic();
|
||||
const { address } = walletFromSeed(mnemonic, { network: 'Mainnet', accountIndex: 0 });
|
||||
|
||||
// 1. keyless build (hosted)
|
||||
const buildText = await callTool(ctx.client, 'vector_build_send_apex', {
|
||||
@@ -40,21 +42,12 @@ describe('keyless E2E: build -> sign -> submit -> await', { skip: !RUN }, () =>
|
||||
const unsignedCbor = extractCbor(buildText);
|
||||
|
||||
// 2. sign locally (stand-in for vector_signer_sign)
|
||||
const tx = CML.Transaction.from_cbor_hex(unsignedCbor);
|
||||
const txHash = CML.hash_transaction(tx.body());
|
||||
const sk = CML.PrivateKey.from_bech32(paymentKey);
|
||||
const vkeyWitness = CML.make_vkey_witness(txHash, sk);
|
||||
const witnessSet = CML.TransactionWitnessSet.new();
|
||||
const vkeys = CML.VkeywitnessList.new();
|
||||
vkeys.add(vkeyWitness);
|
||||
witnessSet.set_vkeywitnesses(vkeys);
|
||||
const signed = CML.Transaction.new(tx.body(), witnessSet, tx.is_valid(), tx.auxiliary_data());
|
||||
const signedCbor = signed.to_cbor_hex();
|
||||
const { signedCborHex: signedCbor, txHash } = signWithMnemonic(unsignedCbor, mnemonic);
|
||||
|
||||
// 3. submit (hosted)
|
||||
const submitText = await callTool(ctx.client, 'vector_submit_transaction', { signedTxCbor: signedCbor });
|
||||
const submittedHash = extractTxHash(submitText);
|
||||
assert.equal(submittedHash, txHash.to_hex(), 'submitted hash differs from the locally computed body hash');
|
||||
assert.equal(submittedHash, txHash, 'submitted hash differs from the locally computed body hash');
|
||||
|
||||
// 4. await confirmation (hosted)
|
||||
const awaitText = await callTool(ctx.client, 'vector_await_transaction', {
|
||||
|
||||
83
packages/builder/test/integration/registry-e2e.test.ts
Normal file
83
packages/builder/test/integration/registry-e2e.test.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { describe, test, before, after } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { startServer, stopServer, callTool, getMnemonic, wait, ServerContext } from '../setup.ts';
|
||||
import { signWithMnemonic } from './sign-helper.ts';
|
||||
import { walletFromSeed } from '@lucid-evolution/lucid';
|
||||
|
||||
// FULL REGISTRY LIFECYCLE ON LIVE TESTNET - gated. Registers a real agent,
|
||||
// updates it, deregisters it. Net cost ~= 3 tx fees (~0.6 AP3X); the 10 AP3X
|
||||
// deposit round-trips. Requires packages/builder/mnemonic.txt (never committed).
|
||||
// VECTOR_E2E_SUBMIT=1 node --import tsx --test packages/builder/test/integration/registry-e2e.test.ts
|
||||
const GATE = process.env.VECTOR_E2E_SUBMIT === '1';
|
||||
|
||||
function extractCbor(text: string): string {
|
||||
const m = text.match(/```\n([0-9a-f]+)\n```/);
|
||||
assert.ok(m, `no CBOR block:\n${text.slice(0, 500)}`);
|
||||
return m![1];
|
||||
}
|
||||
function extractTxHash(text: string): string {
|
||||
const m = text.match(/Transaction Hash:\s*([0-9a-f]{64})/);
|
||||
assert.ok(m, `no tx hash:\n${text.slice(0, 500)}`);
|
||||
return m![1];
|
||||
}
|
||||
|
||||
(GATE ? describe : describe.skip)('registry lifecycle E2E (live testnet, SUBMITS)', () => {
|
||||
let ctx: ServerContext;
|
||||
let mnemonic: string;
|
||||
let walletAddress: string;
|
||||
let agentDid: string;
|
||||
|
||||
before(async () => {
|
||||
ctx = await startServer();
|
||||
mnemonic = getMnemonic();
|
||||
walletAddress = walletFromSeed(mnemonic, { network: 'Mainnet', accountIndex: 0 }).address;
|
||||
});
|
||||
after(async () => { await stopServer(ctx); });
|
||||
|
||||
async function signSubmitAwait(buildText: string): Promise<string> {
|
||||
const { signedCborHex, txHash } = signWithMnemonic(extractCbor(buildText), mnemonic);
|
||||
const submitText = await callTool(ctx.client, 'vector_submit_transaction', { signedTxCbor: signedCborHex });
|
||||
const submittedHash = extractTxHash(submitText);
|
||||
assert.equal(submittedHash, txHash, 'submitted hash != locally computed hash');
|
||||
const awaitText = await callTool(ctx.client, 'vector_await_transaction', { txHash: submittedHash, timeoutSeconds: 240 });
|
||||
assert.match(awaitText, /Transaction Confirmed/);
|
||||
return submittedHash;
|
||||
}
|
||||
|
||||
test('register -> confirm -> profile readable', { timeout: 420_000 }, async () => {
|
||||
const buildText = await callTool(ctx.client, 'vector_build_register_agent', {
|
||||
changeAddress: walletAddress, name: `E2EAgent-${Date.now()}`,
|
||||
description: 'registry e2e lifecycle agent', capabilities: ['testing', 'e2e'], framework: 'custom', endpoint: '',
|
||||
});
|
||||
const didMatch = buildText.match(/(did:vector:agent:[a-f0-9]+:[a-f0-9]+)/);
|
||||
assert.ok(didMatch, 'no DID in register build');
|
||||
agentDid = didMatch![1];
|
||||
await signSubmitAwait(buildText);
|
||||
await wait(5);
|
||||
const profile = await callTool(ctx.client, 'vector_get_agent_profile', { agent_id: agentDid });
|
||||
assert.match(profile, /registry e2e lifecycle agent/);
|
||||
});
|
||||
|
||||
test('update -> confirm -> profile shows the change', { timeout: 420_000 }, async () => {
|
||||
await wait(10); // let register's UTxOs settle
|
||||
const buildText = await callTool(ctx.client, 'vector_build_update_agent', {
|
||||
changeAddress: walletAddress, agent_id: agentDid, description: 'updated by registry e2e',
|
||||
});
|
||||
await signSubmitAwait(buildText);
|
||||
await wait(5);
|
||||
const profile = await callTool(ctx.client, 'vector_get_agent_profile', { agent_id: agentDid });
|
||||
assert.match(profile, /updated by registry e2e/);
|
||||
});
|
||||
|
||||
test('deregister -> confirm -> agent gone', { timeout: 420_000 }, async () => {
|
||||
await wait(10);
|
||||
const buildText = await callTool(ctx.client, 'vector_build_deregister_agent', {
|
||||
changeAddress: walletAddress, agent_id: agentDid,
|
||||
});
|
||||
assert.match(buildText, /Deposit returned on confirmation/);
|
||||
await signSubmitAwait(buildText);
|
||||
await wait(5);
|
||||
const profile = await callTool(ctx.client, 'vector_get_agent_profile', { agent_id: agentDid });
|
||||
assert.match(profile, /Agent not found|Failed to get agent profile/);
|
||||
});
|
||||
});
|
||||
90
packages/builder/test/integration/registry-keyless.test.ts
Normal file
90
packages/builder/test/integration/registry-keyless.test.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { describe, test, before, after } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { CML } from '@lucid-evolution/lucid';
|
||||
import { startServer, stopServer, callTool, ServerContext } from '../setup.ts';
|
||||
|
||||
// Tier-1 registry integration: LIVE testnet, ZERO secrets. Build-only - nothing
|
||||
// is signed, nothing is submitted, no funds move. The register build runs the
|
||||
// REAL registry validator server-side during .complete().
|
||||
const OWN_ADDRESS = 'addr1qylasu4y34ccwt8hv55tkswa9fthtck9xtppvc4y03kwhaztd0jlxdzk72zj8mgy6x4269v9gytkgffh3k8d3l8987yqu5c35z';
|
||||
|
||||
let ctx: ServerContext;
|
||||
before(async () => { ctx = await startServer(); });
|
||||
after(async () => { await stopServer(ctx); });
|
||||
|
||||
function extractCbor(text: string): string {
|
||||
const m = text.match(/```\n([0-9a-f]+)\n```/);
|
||||
assert.ok(m, `no CBOR block in response:\n${text.slice(0, 500)}`);
|
||||
return m![1];
|
||||
}
|
||||
function assertBuildNotSubmitted(text: string) {
|
||||
assert.doesNotMatch(text, /(?<!Not )Submitted\b|Transaction Submitted/);
|
||||
assert.match(text, /Unsigned|NOT been submitted|Not Submitted/i);
|
||||
}
|
||||
|
||||
describe('registry keyless tier-1 (live testnet, no secrets)', () => {
|
||||
let liveDid: string | null = null;
|
||||
|
||||
test('discover returns live agents and yields a DID to work with', { timeout: 120_000 }, async () => {
|
||||
const text = await callTool(ctx.client, 'vector_discover_agents', { limit: 3 });
|
||||
const m = text.match(/(did:vector:agent:[a-f0-9]+:[a-f0-9]+)/);
|
||||
assert.ok(m, 'no DID found in discovery output - is the testnet registry empty?');
|
||||
liveDid = m![1];
|
||||
});
|
||||
|
||||
test('build_register_agent builds an evaluable unsigned registration', { timeout: 120_000 }, async () => {
|
||||
const text = await callTool(ctx.client, 'vector_build_register_agent', {
|
||||
changeAddress: OWN_ADDRESS, name: `Tier1Agent-${process.pid}`,
|
||||
description: 'tier-1 build-only agent', capabilities: ['testing'], framework: 'custom', endpoint: '',
|
||||
});
|
||||
assertBuildNotSubmitted(text);
|
||||
// Brief specified /Agent DID:\s*did:vector:agent:/ - the live response is
|
||||
// Markdown ("**Agent DID:** did:vector:agent:..."), so two literal '*'
|
||||
// sit between the label and the value that \s* alone doesn't cover.
|
||||
// Verified against the actual tier-1 live response (register-agent build
|
||||
// succeeded; only this assertion's regex was too strict) before widening
|
||||
// it, per Task 5's task-5-report.md self-review.
|
||||
assert.match(text, /Agent DID:\*{0,2}\s*did:vector:agent:/);
|
||||
const tx = CML.Transaction.from_cbor_hex(extractCbor(text));
|
||||
const vkeys = tx.witness_set().vkeywitnesses();
|
||||
assert.ok(vkeys === undefined || vkeys.len() === 0, 'register build must be unsigned');
|
||||
assert.ok(tx.body().mint(), 'register tx has no mint field');
|
||||
});
|
||||
|
||||
test('build_message_agent builds against a live agent', { timeout: 120_000 }, async () => {
|
||||
assert.ok(liveDid, 'needs the DID from discovery');
|
||||
const text = await callTool(ctx.client, 'vector_build_message_agent', {
|
||||
changeAddress: OWN_ADDRESS, agent_id: liveDid!, message_type: 'inquiry', payload: 'tier-1 build-only ping',
|
||||
});
|
||||
assertBuildNotSubmitted(text);
|
||||
const tx = CML.Transaction.from_cbor_hex(extractCbor(text));
|
||||
const aux = tx.auxiliary_data();
|
||||
assert.ok(aux && aux.metadata() && aux.metadata()!.get(674n), 'label-674 metadata missing');
|
||||
});
|
||||
|
||||
test('get_agent_profile resolves the live agent', { timeout: 120_000 }, async () => {
|
||||
assert.ok(liveDid);
|
||||
const text = await callTool(ctx.client, 'vector_get_agent_profile', { agent_id: liveDid! });
|
||||
assert.match(text, /Agent Profile/);
|
||||
assert.match(text, /Registry UTxO:/);
|
||||
});
|
||||
|
||||
test('a bogus changeAddress fails fast with a clear validation error', { timeout: 30_000 }, async () => {
|
||||
const t0 = Date.now();
|
||||
const text = await callTool(ctx.client, 'vector_build_register_agent', {
|
||||
changeAddress: 'addr1garbage', name: 'X', description: '', capabilities: [], framework: 'c', endpoint: '',
|
||||
});
|
||||
assert.match(text, /Invalid change address/);
|
||||
assert.ok(Date.now() - t0 < 5_000, 'validation should not have hit the network');
|
||||
});
|
||||
|
||||
test('non-owner update is refused at build time', { timeout: 120_000 }, async () => {
|
||||
assert.ok(liveDid);
|
||||
const text = await callTool(ctx.client, 'vector_build_update_agent', {
|
||||
changeAddress: OWN_ADDRESS, agent_id: liveDid!, description: 'not mine',
|
||||
});
|
||||
// The discovered agent belongs to another wallet (verified during planning:
|
||||
// live datum owners differ from OWN_ADDRESS's key). Expect the ownership fail-fast.
|
||||
assert.match(text, /Ownership check failed|Failed to build agent update/);
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import { describe, test, before, after } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { walletFromSeed } from '@lucid-evolution/lucid';
|
||||
import { startServer, stopServer, callTool, getMnemonic, wait, ServerContext } from '../setup.ts';
|
||||
import { signWithMnemonic } from './sign-helper.ts';
|
||||
|
||||
// Always-succeeds PlutusV2 validator (accepts any datum/redeemer/context, returns True)
|
||||
const ALWAYS_SUCCEEDS_V2 = '49480100002221200101';
|
||||
@@ -222,27 +223,69 @@ describe('Smart Contract Tools', () => {
|
||||
});
|
||||
|
||||
// ─── Agent Network Tools ────────────────────────────────────────────────────
|
||||
// Keyless since spec PR 7: each old custodial call (mnemonic in, submitted
|
||||
// on-chain out, all inside one tool call) is now build_* (hosted, unsigned)
|
||||
// -> signWithMnemonic (local, stands in for the local signer) ->
|
||||
// vector_submit_transaction (hosted) - the same non-custodial flow the
|
||||
// Transaction Tools tests above and keyless-e2e.test.ts already exercise.
|
||||
// vector_discover_agents/vector_get_agent_profile are read-only and were
|
||||
// never custodial - both unchanged (tool name and call shape). walletHasAda
|
||||
// gating, the settle waits, and the assertSuccessOrKnownError convention are
|
||||
// unchanged from the pre-keyless version of this section.
|
||||
|
||||
// Extracts the CBOR block a build_* tool response embeds ("```\n<hex>\n```"),
|
||||
// or null if the build didn't produce one (a known-error response, e.g. an
|
||||
// unfunded wallet) - lets callers skip signing/submitting instead of crashing
|
||||
// on a missing match.
|
||||
function extractCborOrNull(text: string): string | null {
|
||||
const m = text.match(/```\n([0-9a-f]+)\n```/);
|
||||
return m ? m[1] : null;
|
||||
}
|
||||
|
||||
// build_* (hosted, unsigned) -> signWithMnemonic (local) ->
|
||||
// vector_submit_transaction (hosted). Reuses assertSuccessOrKnownError at
|
||||
// both legs: the build may legitimately fail a known way on an unfunded
|
||||
// wallet (no sign/submit attempted then), and submit is only attempted once
|
||||
// the build actually produced CBOR to sign. Returns the build response text
|
||||
// (callers that need e.g. the registered DID parse it from there) plus
|
||||
// whether the transaction was actually submitted, so callers only chain
|
||||
// follow-on state (agentDid) off a real submission, not just a successful build.
|
||||
async function buildSignSubmit(
|
||||
toolName: string, args: Record<string, unknown>, buildSuccessPattern: RegExp,
|
||||
): Promise<{ buildText: string; submitted: boolean }> {
|
||||
const buildText = await callTool(ctx.client, toolName, args);
|
||||
console.log(buildText);
|
||||
assertSuccessOrKnownError(buildText, buildSuccessPattern, toolName);
|
||||
const cbor = extractCborOrNull(buildText);
|
||||
if (!buildSuccessPattern.test(buildText) || !cbor) {
|
||||
console.log(` ⚠ ${toolName}: build did not succeed - skipping sign/submit`);
|
||||
return { buildText, submitted: false };
|
||||
}
|
||||
const { signedCborHex } = signWithMnemonic(cbor, mnemonic);
|
||||
const submitText = await callTool(ctx.client, 'vector_submit_transaction', { signedTxCbor: signedCborHex });
|
||||
console.log(submitText);
|
||||
assertSuccessOrKnownError(submitText, /Transaction Submitted/, `${toolName} (submit)`);
|
||||
return { buildText, submitted: /Transaction Submitted/.test(submitText) };
|
||||
}
|
||||
|
||||
describe('Agent Network Tools', () => {
|
||||
test('vector_register_agent', { timeout: 120_000 }, async () => {
|
||||
test('vector_build_register_agent -> sign -> submit', { timeout: 120_000 }, async () => {
|
||||
if (walletHasAda) {
|
||||
console.log('Waiting 10s for UTxOs to settle before register...');
|
||||
await wait(10);
|
||||
}
|
||||
const timestamp = Date.now();
|
||||
const text = await callTool(ctx.client, 'vector_register_agent', {
|
||||
mnemonic,
|
||||
const { buildText, submitted } = await buildSignSubmit('vector_build_register_agent', {
|
||||
changeAddress: walletAddress,
|
||||
name: `TestAgent-${timestamp}`,
|
||||
description: 'Integration test agent',
|
||||
capabilities: ['testing'],
|
||||
framework: 'custom',
|
||||
endpoint: '',
|
||||
});
|
||||
console.log(text);
|
||||
assertSuccessOrKnownError(text, /Agent DID:/, 'vector_register_agent');
|
||||
}, /Agent DID:/);
|
||||
|
||||
const didMatch = text.match(/(did:vector:agent:[a-f0-9]+:[a-f0-9]+)/);
|
||||
if (didMatch) {
|
||||
const didMatch = buildText.match(/(did:vector:agent:[a-f0-9]+:[a-f0-9]+)/);
|
||||
if (submitted && didMatch) {
|
||||
agentDid = didMatch[1];
|
||||
console.log(`Registered agent DID: ${agentDid}`);
|
||||
}
|
||||
@@ -266,7 +309,7 @@ describe('Agent Network Tools', () => {
|
||||
assertSuccessOrKnownError(text, /Agent Profile/, 'vector_get_agent_profile');
|
||||
});
|
||||
|
||||
test('vector_update_agent', { timeout: 120_000 }, async () => {
|
||||
test('vector_build_update_agent -> sign -> submit', { timeout: 120_000 }, async () => {
|
||||
if (!agentDid) {
|
||||
console.log('Skipping update — no agent registered');
|
||||
return;
|
||||
@@ -275,16 +318,14 @@ describe('Agent Network Tools', () => {
|
||||
console.log('Waiting 10s for UTxOs to settle before update...');
|
||||
await wait(10);
|
||||
}
|
||||
const text = await callTool(ctx.client, 'vector_update_agent', {
|
||||
mnemonic,
|
||||
await buildSignSubmit('vector_build_update_agent', {
|
||||
changeAddress: walletAddress,
|
||||
agent_id: agentDid,
|
||||
description: 'Updated integration test agent',
|
||||
});
|
||||
console.log(text);
|
||||
assertSuccessOrKnownError(text, /Agent Updated|Updated fields/, 'vector_update_agent');
|
||||
}, /Agent DID:/);
|
||||
});
|
||||
|
||||
test('vector_transfer_agent (to self)', { timeout: 120_000 }, async () => {
|
||||
test('vector_build_transfer_agent (to self) -> sign -> submit', { timeout: 120_000 }, async () => {
|
||||
if (!agentDid || !walletAddress) {
|
||||
console.log('Skipping transfer — no agent registered or no wallet address');
|
||||
return;
|
||||
@@ -293,33 +334,29 @@ describe('Agent Network Tools', () => {
|
||||
console.log('Waiting 10s for UTxOs to settle before transfer...');
|
||||
await wait(10);
|
||||
}
|
||||
const text = await callTool(ctx.client, 'vector_transfer_agent', {
|
||||
mnemonic,
|
||||
await buildSignSubmit('vector_build_transfer_agent', {
|
||||
changeAddress: walletAddress,
|
||||
agent_id: agentDid,
|
||||
new_owner_address: walletAddress,
|
||||
});
|
||||
console.log(text);
|
||||
assertSuccessOrKnownError(text, /Agent Transferred|Transfer/, 'vector_transfer_agent');
|
||||
}, /Agent DID:/);
|
||||
});
|
||||
|
||||
test('vector_message_agent', { timeout: 120_000 }, async () => {
|
||||
test('vector_build_message_agent -> sign -> submit', { timeout: 120_000 }, async () => {
|
||||
// Use a known DID format even if registration failed, to test the tool responds
|
||||
const testDid = agentDid || `did:vector:agent:${ALWAYS_SUCCEEDS_V2}:${'a'.repeat(64)}`;
|
||||
if (walletHasAda && agentDid) {
|
||||
console.log('Waiting 10s for UTxOs to settle before message...');
|
||||
await wait(10);
|
||||
}
|
||||
const text = await callTool(ctx.client, 'vector_message_agent', {
|
||||
await buildSignSubmit('vector_build_message_agent', {
|
||||
changeAddress: walletAddress,
|
||||
agent_id: testDid,
|
||||
message_type: 'inquiry',
|
||||
payload: 'integration test ping',
|
||||
mnemonic,
|
||||
});
|
||||
console.log(text);
|
||||
assertSuccessOrKnownError(text, /Message Sent/, 'vector_message_agent');
|
||||
}, /Unsigned Message Built/);
|
||||
});
|
||||
|
||||
test('vector_deregister_agent', { timeout: 120_000 }, async () => {
|
||||
test('vector_build_deregister_agent -> sign -> submit', { timeout: 120_000 }, async () => {
|
||||
if (!agentDid) {
|
||||
console.log('Skipping deregister — no agent registered');
|
||||
return;
|
||||
@@ -328,11 +365,9 @@ describe('Agent Network Tools', () => {
|
||||
console.log('Waiting 10s for UTxOs to settle before deregister...');
|
||||
await wait(10);
|
||||
}
|
||||
const text = await callTool(ctx.client, 'vector_deregister_agent', {
|
||||
mnemonic,
|
||||
await buildSignSubmit('vector_build_deregister_agent', {
|
||||
changeAddress: walletAddress,
|
||||
agent_id: agentDid,
|
||||
});
|
||||
console.log(text);
|
||||
assertSuccessOrKnownError(text, /Agent Deregistered|deposit returned/, 'vector_deregister_agent');
|
||||
}, /Deposit returned on confirmation/);
|
||||
});
|
||||
});
|
||||
|
||||
54
packages/builder/test/integration/sign-helper.ts
Normal file
54
packages/builder/test/integration/sign-helper.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
// packages/builder/test/integration/sign-helper.ts
|
||||
//
|
||||
// Shared local-signing helper for the integration suites. Stands in for the
|
||||
// in-tree local signer (packages/signer) — same CML primitives (hash ->
|
||||
// make_vkey_witness -> witness-set attach), without the signer's own
|
||||
// decode/policy/audit path. Extracted verbatim (Task 5, spec PR 7) from the
|
||||
// inline signing block keyless-e2e.test.ts proved against the live testnet in
|
||||
// PR #9's E2E, so registry-e2e.test.ts and run.test.ts's registry section can
|
||||
// share it instead of re-inlining the same sequence.
|
||||
import { CML, walletFromSeed } from '@lucid-evolution/lucid';
|
||||
|
||||
/**
|
||||
* Sign an unsigned transaction (CBOR hex) with the payment key derived from a
|
||||
* mnemonic. Takes the mnemonic directly (not a pre-derived key) so every
|
||||
* caller can go straight from `getMnemonic()` to a signed transaction in one
|
||||
* call.
|
||||
*
|
||||
* CORRECTION vs. the original inline block this was extracted from: that
|
||||
* block built the signed tx's witness set from scratch
|
||||
* (`CML.TransactionWitnessSet.new()`, only ever given the new vkey witness),
|
||||
* which is lossless only when the unsigned tx's own witness set is empty -
|
||||
* true for a plain payment (keyless-e2e.test.ts's proven case: no script, no
|
||||
* redeemer). The agent-registry build_* tools attach a minting policy and/or
|
||||
* spending validator, so their unsigned CBOR already carries
|
||||
* plutus_v3_scripts/redeemers in `tx.witness_set()`, matched to a
|
||||
* script_data_hash baked into the tx BODY at build time. Replacing the
|
||||
* witness set outright silently drops that content; the ledger then rejects
|
||||
* the resigned tx with `MissingScriptWitnessesUTXOW` (script witness gone)
|
||||
* and `PPViewHashesDontMatch` (the body's script_data_hash no longer matches
|
||||
* a witness set the ledger now sees as script-free) - reproduced live via
|
||||
* registry-e2e.test.ts's register step, see task-5-report.md. Fixed by
|
||||
* merging the new vkey witness into the tx's EXISTING witness set with
|
||||
* `add_all_witnesses` (CML's documented primitive for exactly this "add a
|
||||
* signature to an already-witnessed tx" composition) instead of discarding
|
||||
* it. Strictly more correct and a no-op behavior change for the script-less
|
||||
* case, since there's nothing else to preserve there.
|
||||
*/
|
||||
export function signWithMnemonic(unsignedCborHex: string, mnemonic: string): { signedCborHex: string; txHash: string } {
|
||||
const { paymentKey } = walletFromSeed(mnemonic, { network: 'Mainnet', accountIndex: 0 });
|
||||
const tx = CML.Transaction.from_cbor_hex(unsignedCborHex);
|
||||
const txHash = CML.hash_transaction(tx.body());
|
||||
const sk = CML.PrivateKey.from_bech32(paymentKey);
|
||||
const vkeyWitness = CML.make_vkey_witness(txHash, sk);
|
||||
|
||||
const witnessSet = tx.witness_set();
|
||||
const newWitnesses = CML.TransactionWitnessSet.new();
|
||||
const vkeys = CML.VkeywitnessList.new();
|
||||
vkeys.add(vkeyWitness);
|
||||
newWitnesses.set_vkeywitnesses(vkeys);
|
||||
witnessSet.add_all_witnesses(newWitnesses);
|
||||
|
||||
const signed = CML.Transaction.new(tx.body(), witnessSet, tx.is_valid(), tx.auxiliary_data());
|
||||
return { signedCborHex: signed.to_cbor_hex(), txHash: txHash.to_hex() };
|
||||
}
|
||||
Reference in New Issue
Block a user