refactor(builder): registry builds derive the change address from the wallet - mismatch now unrepresentable
This commit is contained in:
@@ -159,7 +159,7 @@ ${capList}
|
||||
if (rateLimited) return rateLimited;
|
||||
try {
|
||||
const lucid = await lucidForAddress(newProvider(), changeAddress);
|
||||
const r = await buildRegisterAgent(lucid, { changeAddress, name, description, capabilities, framework, endpoint });
|
||||
const r = await buildRegisterAgent(lucid, { name, description, capabilities, framework, endpoint });
|
||||
return {
|
||||
content: [{
|
||||
type: "text" as const,
|
||||
@@ -215,7 +215,7 @@ The DID above is bound to the exact wallet UTxO this build consumed - if that UT
|
||||
try {
|
||||
const provider = newProvider();
|
||||
const lucid = await lucidForAddress(provider, changeAddress);
|
||||
const r = await buildUpdateAgent(lucid, provider, { changeAddress, agentId: agent_id, name, description, capabilities, framework, endpoint });
|
||||
const r = await buildUpdateAgent(lucid, provider, { agentId: agent_id, name, description, capabilities, framework, endpoint });
|
||||
return {
|
||||
content: [{
|
||||
type: "text" as const,
|
||||
@@ -264,7 +264,7 @@ The identity NFT and deposit are unchanged by this update. ${SIGN_AND_SUBMIT_COP
|
||||
try {
|
||||
const provider = newProvider();
|
||||
const lucid = await lucidForAddress(provider, changeAddress);
|
||||
const r = await buildTransferAgent(lucid, provider, { changeAddress, agentId: agent_id, newOwnerAddress: new_owner_address });
|
||||
const r = await buildTransferAgent(lucid, provider, { agentId: agent_id, newOwnerAddress: new_owner_address });
|
||||
return {
|
||||
content: [{
|
||||
type: "text" as const,
|
||||
@@ -314,7 +314,7 @@ After this transaction confirms, only the new owner can update, transfer, or der
|
||||
try {
|
||||
const provider = newProvider();
|
||||
const lucid = await lucidForAddress(provider, changeAddress);
|
||||
const r = await buildDeregisterAgent(lucid, provider, { changeAddress, agentId: agent_id });
|
||||
const r = await buildDeregisterAgent(lucid, provider, { agentId: agent_id });
|
||||
return {
|
||||
content: [{
|
||||
type: "text" as const,
|
||||
@@ -366,7 +366,7 @@ The identity NFT is burned by this transaction; the deposit returns to the chang
|
||||
try {
|
||||
const provider = newProvider();
|
||||
const lucid = await lucidForAddress(provider, changeAddress);
|
||||
const r = await buildMessageAgent(lucid, provider, { changeAddress, agentId: agent_id, messageType: message_type, payload });
|
||||
const r = await buildMessageAgent(lucid, provider, { agentId: agent_id, messageType: message_type, payload });
|
||||
return {
|
||||
content: [{
|
||||
type: "text" as const,
|
||||
|
||||
@@ -157,12 +157,13 @@ export function verifyOwnership(profile: RegistryAgentProfile, walletVkeyHash: s
|
||||
}
|
||||
|
||||
export async function buildRegisterAgent(lucid: LucidEvolution, p: {
|
||||
changeAddress: string; name: string; description: string;
|
||||
name: string; description: string;
|
||||
capabilities: string[]; framework: string; endpoint: string;
|
||||
}): Promise<VectorBuildRegisterResult> {
|
||||
const changeAddress = await lucid.wallet().address();
|
||||
validateEndpoint(p.endpoint);
|
||||
validateCapabilities(p.capabilities);
|
||||
const vkeyHash = paymentKeyHashOf(p.changeAddress);
|
||||
const vkeyHash = paymentKeyHashOf(changeAddress);
|
||||
const utxos = await lucid.wallet().getUtxos();
|
||||
// Same selection heuristic as the old custodial tool (behavior parity):
|
||||
// prefer a pure-lovelace UTxO covering deposit + fee headroom, else the first.
|
||||
@@ -180,7 +181,7 @@ export async function buildRegisterAgent(lucid: LucidEvolution, p: {
|
||||
.mintAssets({ [nftUnit]: 1n }, registerRedeemer)
|
||||
.attach.MintingPolicy(registryScript())
|
||||
.pay.ToAddressWithData(getRegistryAddress(), { kind: 'inline', value: datum }, { lovelace: MIN_AP3X_DEPOSIT, [nftUnit]: 1n })
|
||||
.addSigner(p.changeAddress)
|
||||
.addSigner(changeAddress)
|
||||
.complete();
|
||||
return {
|
||||
...toBuildResult(completed),
|
||||
@@ -195,17 +196,18 @@ export async function buildUpdateAgent(
|
||||
lucid: LucidEvolution,
|
||||
provider: Pick<OgmiosProvider, 'getUtxos' | 'getUtxoByUnit'>,
|
||||
p: {
|
||||
changeAddress: string; agentId: string; name?: string; description?: string;
|
||||
agentId: string; name?: string; description?: string;
|
||||
capabilities?: string[]; framework?: string; endpoint?: string;
|
||||
},
|
||||
): Promise<VectorBuildAgentOpResult> {
|
||||
const changeAddress = await lucid.wallet().address();
|
||||
if (p.name === undefined && p.description === undefined && p.capabilities === undefined
|
||||
&& p.framework === undefined && p.endpoint === undefined) {
|
||||
throw new Error('At least one field must be provided to update (name, description, capabilities, framework, or endpoint).');
|
||||
}
|
||||
if (p.endpoint !== undefined) validateEndpoint(p.endpoint);
|
||||
if (p.capabilities !== undefined) validateCapabilities(p.capabilities);
|
||||
const vkeyHash = paymentKeyHashOf(p.changeAddress);
|
||||
const vkeyHash = paymentKeyHashOf(changeAddress);
|
||||
const { profile, utxo } = await resolveAgentUtxo(provider, p.agentId);
|
||||
verifyOwnership(profile, vkeyHash);
|
||||
// Owner is written from profile.ownerVkeyHash, not the caller's vkeyHash:
|
||||
@@ -232,7 +234,7 @@ export async function buildUpdateAgent(
|
||||
.collectFrom([utxo], spendRedeemer)
|
||||
.attach.SpendingValidator(registryScript())
|
||||
.pay.ToAddressWithData(getRegistryAddress(), { kind: 'inline', value: newDatum }, utxo.assets)
|
||||
.addSigner(p.changeAddress)
|
||||
.addSigner(changeAddress)
|
||||
.complete();
|
||||
const updatedFields: string[] = [];
|
||||
if (p.name !== undefined) updatedFields.push('name');
|
||||
@@ -249,9 +251,10 @@ export async function buildUpdateAgent(
|
||||
export async function buildTransferAgent(
|
||||
lucid: LucidEvolution,
|
||||
provider: Pick<OgmiosProvider, 'getUtxos' | 'getUtxoByUnit'>,
|
||||
p: { changeAddress: string; agentId: string; newOwnerAddress: string },
|
||||
p: { agentId: string; newOwnerAddress: string },
|
||||
): Promise<VectorBuildAgentOpResult> {
|
||||
const vkeyHash = paymentKeyHashOf(p.changeAddress);
|
||||
const changeAddress = await lucid.wallet().address();
|
||||
const vkeyHash = paymentKeyHashOf(changeAddress);
|
||||
let newOwnerHash: string;
|
||||
try {
|
||||
newOwnerHash = paymentKeyHashOf(p.newOwnerAddress);
|
||||
@@ -272,7 +275,7 @@ export async function buildTransferAgent(
|
||||
.collectFrom([utxo], spendRedeemer)
|
||||
.attach.SpendingValidator(registryScript())
|
||||
.pay.ToAddressWithData(getRegistryAddress(), { kind: 'inline', value: newDatum }, utxo.assets)
|
||||
.addSigner(p.changeAddress)
|
||||
.addSigner(changeAddress)
|
||||
.complete();
|
||||
return {
|
||||
...toBuildResult(completed),
|
||||
@@ -283,9 +286,10 @@ export async function buildTransferAgent(
|
||||
export async function buildDeregisterAgent(
|
||||
lucid: LucidEvolution,
|
||||
provider: Pick<OgmiosProvider, 'getUtxos' | 'getUtxoByUnit'>,
|
||||
p: { changeAddress: string; agentId: string },
|
||||
p: { agentId: string },
|
||||
): Promise<VectorBuildAgentOpResult> {
|
||||
const vkeyHash = paymentKeyHashOf(p.changeAddress);
|
||||
const changeAddress = await lucid.wallet().address();
|
||||
const vkeyHash = paymentKeyHashOf(changeAddress);
|
||||
const { profile, utxo, nftUnit } = await resolveAgentUtxo(provider, p.agentId);
|
||||
verifyOwnership(profile, vkeyHash);
|
||||
const spendRedeemer = Data.to(new Constr(1, [])); // Deregister
|
||||
@@ -321,7 +325,7 @@ export async function buildDeregisterAgent(
|
||||
.collectFrom([feeUtxo])
|
||||
.mintAssets({ [nftUnit]: -1n }, mintRedeemer)
|
||||
.attach.MintingPolicy(registryScript())
|
||||
.addSigner(p.changeAddress)
|
||||
.addSigner(changeAddress)
|
||||
.complete();
|
||||
return {
|
||||
...toBuildResult(completed),
|
||||
@@ -332,9 +336,10 @@ export async function buildDeregisterAgent(
|
||||
export async function buildMessageAgent(
|
||||
lucid: LucidEvolution,
|
||||
provider: Pick<OgmiosProvider, 'getUtxos' | 'getUtxoByUnit'>,
|
||||
p: { changeAddress: string; agentId: string; messageType: string; payload: string },
|
||||
p: { agentId: string; messageType: string; payload: string },
|
||||
): Promise<VectorBuildMessageResult> {
|
||||
paymentKeyHashOf(p.changeAddress); // validate before any resolution work
|
||||
const changeAddress = await lucid.wallet().address();
|
||||
paymentKeyHashOf(changeAddress); // validate before any resolution work
|
||||
const { profile } = await resolveAgentUtxo(provider, p.agentId);
|
||||
if (!profile.ownerVkeyHash) throw new Error('Could not parse agent owner from registry datum');
|
||||
const recipientAddress = credentialToAddress('Mainnet', { type: 'Key', hash: profile.ownerVkeyHash });
|
||||
@@ -342,7 +347,7 @@ export async function buildMessageAgent(
|
||||
.pay.ToAddress(recipientAddress, { lovelace: 2_000_000n })
|
||||
.attachMetadata(AGENT_MESSAGE_LABEL, {
|
||||
msg: ['a2a'],
|
||||
from: metadataStr(p.changeAddress),
|
||||
from: metadataStr(changeAddress),
|
||||
to: metadataStr(p.agentId),
|
||||
type: p.messageType,
|
||||
payload: metadataStr(p.payload),
|
||||
|
||||
@@ -121,7 +121,7 @@ describe('buildRegisterAgent (real validator, offline)', () => {
|
||||
|
||||
test('builds an evaluable register tx: mint +1, deposit output, inline datum, owner signer', async () => {
|
||||
const r = await buildRegisterAgent(lucid, {
|
||||
changeAddress: OWN_ADDRESS, name: 'UnitAgent', description: 'unit test agent',
|
||||
name: 'UnitAgent', description: 'unit test agent',
|
||||
capabilities: ['testing', 'ci'], framework: 'custom', endpoint: '',
|
||||
});
|
||||
const tx = decodeTx(r.txCbor);
|
||||
@@ -172,16 +172,23 @@ describe('buildRegisterAgent (real validator, offline)', () => {
|
||||
|
||||
test('rejects invalid endpoint and empty capability before building', async () => {
|
||||
await assert.rejects(buildRegisterAgent(lucid, {
|
||||
changeAddress: OWN_ADDRESS, name: 'X', description: '', capabilities: [], framework: 'c', endpoint: 'not a url',
|
||||
name: 'X', description: '', capabilities: [], framework: 'c', endpoint: 'not a url',
|
||||
}), /Invalid endpoint URL/);
|
||||
await assert.rejects(buildRegisterAgent(lucid, {
|
||||
changeAddress: OWN_ADDRESS, name: 'X', description: '', capabilities: ['ok', ' '], framework: 'c', endpoint: '',
|
||||
name: 'X', description: '', capabilities: ['ok', ' '], framework: 'c', endpoint: '',
|
||||
}), /capability/i);
|
||||
});
|
||||
|
||||
test('rejects a script-credential changeAddress with a clear error', async () => {
|
||||
await assert.rejects(buildRegisterAgent(lucid, {
|
||||
changeAddress: getRegistryAddress(), name: 'X', description: '', capabilities: [], framework: 'c', endpoint: '',
|
||||
// Pre-refactor this test passed a script-address changeAddress param that
|
||||
// disagreed with `lucid`'s own wallet (OWN_ADDRESS) - exactly the
|
||||
// mismatch class this refactor makes unrepresentable. Now it must
|
||||
// construct a lucid instance FOR the script address itself, so the
|
||||
// derived changeAddress (== lucid.wallet().address()) is the one that
|
||||
// paymentKeyHashOf rejects.
|
||||
const scriptLucid = await lucidForAddress(new FixtureProvider([REGISTRY_UTXO]), getRegistryAddress());
|
||||
await assert.rejects(buildRegisterAgent(scriptLucid, {
|
||||
name: 'X', description: '', capabilities: [], framework: 'c', endpoint: '',
|
||||
}), /verification key|payment key/i);
|
||||
});
|
||||
});
|
||||
@@ -191,7 +198,7 @@ describe('buildMessageAgent (resolves via fixture registry UTxO)', () => {
|
||||
const provider = new FixtureProvider([...FIXTURE_UTXOS, REGISTRY_UTXO]);
|
||||
const lucid = await lucidForAddress(provider, OWN_ADDRESS);
|
||||
const r = await buildMessageAgent(lucid, provider, {
|
||||
changeAddress: OWN_ADDRESS, agentId: AGENT_DID, messageType: 'inquiry', payload: 'unit ping',
|
||||
agentId: AGENT_DID, messageType: 'inquiry', payload: 'unit ping',
|
||||
});
|
||||
assert.equal(r.agentName, 'FixtureAgent');
|
||||
const tx = decodeTx(r.txCbor);
|
||||
@@ -219,7 +226,7 @@ describe('buildMessageAgent (resolves via fixture registry UTxO)', () => {
|
||||
const provider = new FixtureProvider(FIXTURE_UTXOS); // no registry UTxO
|
||||
const lucid = await lucidForAddress(provider, OWN_ADDRESS);
|
||||
await assert.rejects(buildMessageAgent(lucid, provider, {
|
||||
changeAddress: OWN_ADDRESS, agentId: AGENT_DID, messageType: 'inquiry', payload: 'x',
|
||||
agentId: AGENT_DID, messageType: 'inquiry', payload: 'x',
|
||||
}), /Agent not found/);
|
||||
});
|
||||
});
|
||||
@@ -254,7 +261,7 @@ describe('registry owner ops (real validator, offline)', () => {
|
||||
const provider = ownedProvider();
|
||||
const lucid = await lucidForAddress(provider, OWN_ADDRESS);
|
||||
const r = await buildUpdateAgent(lucid, provider, {
|
||||
changeAddress: OWN_ADDRESS, agentId: AGENT_DID, description: 'updated by unit test',
|
||||
agentId: AGENT_DID, description: 'updated by unit test',
|
||||
});
|
||||
assert.equal(r.op, 'update');
|
||||
assert.equal(r.detail, 'description');
|
||||
@@ -281,7 +288,7 @@ describe('registry owner ops (real validator, offline)', () => {
|
||||
const provider = ownedProvider();
|
||||
const lucid = await lucidForAddress(provider, OWN_ADDRESS);
|
||||
await assert.rejects(buildUpdateAgent(lucid, provider, {
|
||||
changeAddress: OWN_ADDRESS, agentId: AGENT_DID,
|
||||
agentId: AGENT_DID,
|
||||
}), /At least one field/);
|
||||
});
|
||||
|
||||
@@ -299,7 +306,7 @@ describe('registry owner ops (real validator, offline)', () => {
|
||||
]);
|
||||
const strangerLucid = await lucidForAddress(strangerProvider, stranger);
|
||||
await assert.rejects(buildUpdateAgent(strangerLucid, strangerProvider, {
|
||||
changeAddress: stranger, agentId: AGENT_DID, description: 'hijack attempt',
|
||||
agentId: AGENT_DID, description: 'hijack attempt',
|
||||
}), /Ownership check failed/);
|
||||
});
|
||||
|
||||
@@ -309,7 +316,7 @@ describe('registry owner ops (real validator, offline)', () => {
|
||||
const newOwnerHash = '11'.repeat(28);
|
||||
const newOwner = credentialToAddress('Mainnet', { type: 'Key', hash: newOwnerHash });
|
||||
const r = await buildTransferAgent(lucid, provider, {
|
||||
changeAddress: OWN_ADDRESS, agentId: AGENT_DID, newOwnerAddress: newOwner,
|
||||
agentId: AGENT_DID, newOwnerAddress: newOwner,
|
||||
});
|
||||
assert.equal(r.op, 'transfer');
|
||||
const tx = decodeTx(r.txCbor);
|
||||
@@ -324,7 +331,7 @@ describe('registry owner ops (real validator, offline)', () => {
|
||||
assert.equal(p.name, 'FixtureAgent'); // everything else preserved
|
||||
// script-credential new owner refused
|
||||
await assert.rejects(buildTransferAgent(lucid, provider, {
|
||||
changeAddress: OWN_ADDRESS, agentId: AGENT_DID, newOwnerAddress: getRegistryAddress(),
|
||||
agentId: AGENT_DID, newOwnerAddress: getRegistryAddress(),
|
||||
}), /verification key/i);
|
||||
});
|
||||
|
||||
@@ -332,7 +339,7 @@ describe('registry owner ops (real validator, offline)', () => {
|
||||
const provider = ownedProvider();
|
||||
const lucid = await lucidForAddress(provider, OWN_ADDRESS);
|
||||
const r = await buildDeregisterAgent(lucid, provider, {
|
||||
changeAddress: OWN_ADDRESS, agentId: AGENT_DID,
|
||||
agentId: AGENT_DID,
|
||||
});
|
||||
assert.equal(r.op, 'deregister');
|
||||
assert.equal(r.agentName, 'FixtureAgent');
|
||||
@@ -353,7 +360,7 @@ describe('registry owner ops (real validator, offline)', () => {
|
||||
// small listed FIRST so first-match selection would pick it
|
||||
const provider = new FixtureProvider([small, large, REGISTRY_UTXO]);
|
||||
const lucid = await lucidForAddress(provider, OWN_ADDRESS);
|
||||
const r = await buildDeregisterAgent(lucid, provider, { changeAddress: OWN_ADDRESS, agentId: AGENT_DID });
|
||||
const r = await buildDeregisterAgent(lucid, provider, { agentId: AGENT_DID });
|
||||
const tx = CML.Transaction.from_cbor_hex(r.txCbor);
|
||||
const ins = tx.body().inputs();
|
||||
let sawLarge = false, sawSmall = false;
|
||||
@@ -387,7 +394,7 @@ describe('registry owner ops - value preservation & deregister floor (correction
|
||||
const provider = new FixtureProvider([...FIXTURE_UTXOS, overfunded]);
|
||||
const lucid = await lucidForAddress(provider, OWN_ADDRESS);
|
||||
const r = await buildUpdateAgent(lucid, provider, {
|
||||
changeAddress: OWN_ADDRESS, agentId: AGENT_DID, description: 'overfunded update',
|
||||
agentId: AGENT_DID, description: 'overfunded update',
|
||||
});
|
||||
assert.equal(r.op, 'update');
|
||||
const tx = decodeTx(r.txCbor);
|
||||
@@ -407,7 +414,7 @@ describe('registry owner ops - value preservation & deregister floor (correction
|
||||
const newOwnerHash = '22'.repeat(28);
|
||||
const newOwner = credentialToAddress('Mainnet', { type: 'Key', hash: newOwnerHash });
|
||||
const r = await buildTransferAgent(lucid, provider, {
|
||||
changeAddress: OWN_ADDRESS, agentId: AGENT_DID, newOwnerAddress: newOwner,
|
||||
agentId: AGENT_DID, newOwnerAddress: newOwner,
|
||||
});
|
||||
assert.equal(r.op, 'transfer');
|
||||
const tx = decodeTx(r.txCbor);
|
||||
@@ -449,7 +456,7 @@ describe('registry owner ops - value preservation & deregister floor (correction
|
||||
// build above), which is safe here because the provider also carries
|
||||
// FIXTURE_UTXOS, so the extra fee UTxO that path unconditionally pulls in
|
||||
// is available.
|
||||
const r = await buildDeregisterAgent(lucid, provider, { changeAddress: OWN_ADDRESS, agentId: AGENT_DID });
|
||||
const r = await buildDeregisterAgent(lucid, provider, { agentId: AGENT_DID });
|
||||
assert.equal(r.detail, '25000000', 'detail must be the spent registry UTxO lovelace, not the constant');
|
||||
});
|
||||
|
||||
@@ -461,7 +468,7 @@ describe('registry owner ops - value preservation & deregister floor (correction
|
||||
const provider = new FixtureProvider([tokenOnlyUtxo, REGISTRY_UTXO]);
|
||||
const lucid = await lucidForAddress(provider, OWN_ADDRESS);
|
||||
await assert.rejects(buildDeregisterAgent(lucid, provider, {
|
||||
changeAddress: OWN_ADDRESS, agentId: AGENT_DID,
|
||||
agentId: AGENT_DID,
|
||||
}), /AP3X-only UTxO/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user