Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 77 additions & 2 deletions packages/bitbadgesjs-sdk/src/api-indexer/verify-standards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -993,6 +993,79 @@ function verifyVault(value: any): StandardViolation[] {
return violations;
}

// ============================================================
// Agent Vault Validator
// ============================================================

function verifyAgentVault(value: any): StandardViolation[] {
const violations: StandardViolation[] = [];
const std = 'Agent Vault';
const standards: string[] = value?.standards ?? [];
const approvals = getApprovals(value);
const invariants = getInvariants(value);
const isTrue = (v: any) => v === true || v === 'true';

// Agent Vaults are Smart Tokens — the base tag must be present too.
if (!standards.includes('Smart Token')) {
violations.push({ standard: std, field: 'standards', message: 'Agent Vault collections MUST also carry the "Smart Token" standard.' });
}

// Must have the IBC backing path.
if (!invariants.cosmosCoinBackedPath) {
violations.push({ standard: std, field: 'invariants.cosmosCoinBackedPath', message: 'Agent Vault collections MUST have a cosmosCoinBackedPath defining the IBC backing.' });
}

// validTokenIds must be exactly [{1,1}] (kept in lockstep with the consumer
// validator in core/agent-vaults.ts).
const vt = value?.validTokenIds ?? [];
const vtOk = vt.length === 1 && String(vt[0]?.start) === '1' && String(vt[0]?.end) === '1';
if (!vtOk) {
violations.push({ standard: std, field: 'validTokenIds', message: 'Agent Vault validTokenIds MUST be exactly [{start: 1, end: 1}].' });
}

// Must have BOTH a deposit and a withdraw approval — a deposit-only vault is
// a fund trap (you can mint but never release the backing coin). Match by id
// substring, the same heuristic as findDepositApproval/findWithdrawApproval.
const idOf = (a: any) => String(a.approvalId ?? '').toLowerCase();
const hasDeposit = approvals.some((a: any) => idOf(a).includes('deposit') || (idOf(a).includes('back') && !idOf(a).includes('unback')));
const hasWithdraw = approvals.some((a: any) => idOf(a).includes('withdraw') || idOf(a).includes('unback'));
if (!hasDeposit) {
violations.push({ standard: std, field: 'collectionApprovals', message: 'Agent Vault MUST have a deposit approval (approvalId containing "deposit" or "back").' });
}
if (!hasWithdraw) {
violations.push({ standard: std, field: 'collectionApprovals', message: 'Agent Vault MUST have a withdraw approval (approvalId containing "withdraw" or "unback").' });
}

// Backing approvals (allowBackedMinting) must be mustPrioritize'd.
const backingApprovals = approvals.filter((a: any) => isTrue(a.approvalCriteria?.allowBackedMinting));
if (backingApprovals.length === 0) {
violations.push({ standard: std, field: 'collectionApprovals', message: 'Agent Vault MUST have at least one approval with allowBackedMinting: true.' });
}
for (const ba of backingApprovals) {
if (!isTrue(ba.approvalCriteria?.mustPrioritize)) {
violations.push({ standard: std, field: `collectionApprovals[${ba.approvalId}].mustPrioritize`, message: `Agent Vault backing approval "${ba.approvalId}" MUST have mustPrioritize: true.` });
}
}

// Admin kill-switch consistency: any forceful approval (overridesFrom/To with
// a non-Mint source) requires noForcefulPostMintTransfers === false (else the
// chain rejects creation) AND must be admin-scoped — never initiatedBy "All"
// (that would let anyone forcibly seize vault tokens).
const forceful = approvals.filter(
(a: any) => isTrue(a.approvalCriteria?.overridesFromOutgoingApprovals) || isTrue(a.approvalCriteria?.overridesToIncomingApprovals)
);
if (forceful.length > 0 && isTrue(invariants.noForcefulPostMintTransfers)) {
violations.push({ standard: std, field: 'invariants.noForcefulPostMintTransfers', message: 'Agent Vault has a forceful (override) approval but noForcefulPostMintTransfers is true — the chain will reject this; set it to false.' });
}
for (const fa of forceful) {
if (String(fa.initiatedByListId ?? '') === 'All') {
violations.push({ standard: std, field: `collectionApprovals[${fa.approvalId}].initiatedByListId`, message: `Agent Vault forceful approval "${fa.approvalId}" MUST be admin-scoped (initiatedByListId cannot be "All").` });
}
}

return violations;
}

// ============================================================
// Standard → Validator Map
// ============================================================
Expand All @@ -1014,7 +1087,8 @@ const STANDARD_VALIDATORS: Record<string, (value: any) => StandardViolation[]> =
Auction: verifyAuction,
Products: verifyProducts,
'Prediction Market': verifyPredictionMarket,
Vault: verifyVault
Vault: verifyVault,
'Agent Vault': verifyAgentVault
};

// Also match common alternative names
Expand Down Expand Up @@ -1045,7 +1119,8 @@ const STANDARD_ALIASES: Record<string, string> = {
Products: 'Products',
'Product Catalog': 'Products',
'Prediction Market': 'Prediction Market',
Vault: 'Vault'
Vault: 'Vault',
'Agent Vault': 'Agent Vault'
};

// ============================================================
Expand Down
50 changes: 50 additions & 0 deletions packages/bitbadgesjs-sdk/src/cli/commands/agent-vaults.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Command-tree shape tests for agent-vaults.ts. Helpers/builders are exercised
* in core/agent-vaults.spec.ts + core/builders/agent-vault.spec.ts; this spec
* guards the CLI surface against accidental flag/subcommand drift.
*
* Note: the `build` alias is wired in cli/index.ts (makeBuildAlias), not in this
* command file, so it is intentionally absent here.
*/

import { agentVaultsCommand } from './agent-vaults.js';

describe('agentVaultsCommand shape', () => {
it('exposes the documented subcommand verbs', () => {
const names = agentVaultsCommand.commands.map((c) => c.name()).sort();
expect(names).toEqual(['deposit', 'list', 'pay', 'recover', 'show', 'status', 'vote', 'withdraw']);
});

it('recover requires --creator, --from, --amount', () => {
const cmd = agentVaultsCommand.commands.find((c) => c.name() === 'recover')!;
const required = (cmd as any).options.filter((o: any) => o.required).map((o: any) => o.long);
for (const f of ['--creator', '--from', '--amount']) expect(required).toContain(f);
});

it('deposit + withdraw require --creator and --amount', () => {
for (const verb of ['deposit', 'withdraw']) {
const cmd = agentVaultsCommand.commands.find((c) => c.name() === verb)!;
const required = (cmd as any).options.filter((o: any) => o.required).map((o: any) => o.long);
for (const f of ['--creator', '--amount']) expect(required).toContain(f);
}
});

it('pay requires --creator, --amount, --to', () => {
const cmd = agentVaultsCommand.commands.find((c) => c.name() === 'pay')!;
const required = (cmd as any).options.filter((o: any) => o.required).map((o: any) => o.long);
for (const f of ['--creator', '--amount', '--to']) expect(required).toContain(f);
});

it('vote requires --creator', () => {
const cmd = agentVaultsCommand.commands.find((c) => c.name() === 'vote')!;
const required = (cmd as any).options.filter((o: any) => o.required).map((o: any) => o.long);
expect(required).toContain('--creator');
});

it('every subcommand takes <collection-id> as the first positional', () => {
for (const verb of ['show', 'status', 'deposit', 'withdraw', 'pay', 'recover', 'vote']) {
const c = agentVaultsCommand.commands.find((cmd) => cmd.name() === verb)!;
expect((c as any)._args[0].name()).toBe('collection-id');
}
});
});
Loading
Loading