Skip to content
Merged
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
10 changes: 5 additions & 5 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"name": "@toreva/cli",
"version": "0.1.1",
"description": "Non-custodial execution primitives for Solana. Best-execution routing across Jupiter Perps, Pacifica, Drift, and Flash Trade. 1 bps to open. Everything else is free.",
"version": "0.2.0",
"description": "Toreva CLI — MCP-native Solana primitive platform. R1: earn-compare and token-receive read-only commands callable against mcp.toreva.com. See toreva.com/operational-matrix.",
"keywords": [
"solana", "defi", "perps", "perpetual-futures", "yield",
"non-custodial", "jupiter", "drift", "execution-primitives",
"cli"
"solana", "defi", "kamino", "marginfi", "spl-token",
"non-custodial", "mcp", "agent-execution", "agent-skills",
"earn", "lending", "apy", "cli"
],
"repository": {
"type": "git",
Expand Down
114 changes: 114 additions & 0 deletions packages/cli/src/__tests__/earn-compare.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { describe, it, expect, vi, afterEach } from 'vitest';
import {
parseEarnCompareArgs,
formatEarnCompare,
runEarnCompareCommand,
} from '../commands/earn-compare.js';
import type { EarnCompareResult } from '@toreva/sdk';

const baseResult: EarnCompareResult = {
ok: true,
venue: 'kamino',
asset: 'USDC',
apyPct: 4.3626,
apyBasePct: 4.3626,
apyRewardPct: null,
tvlUsd: 7_799_705,
underlyingTokens: ['EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'],
chain: 'Solana',
project: 'kamino-lend',
poolId: 'd2141a59-c199-4be7-8d4b-c8223954836b',
source: 'defillama',
sourceUrl: 'https://yields.llama.fi/pools',
fetchedAt: '2026-05-10T12:20:09.082Z',
evidenceRef: {
readEvidenceId: 'NETSOL-READ-kamino-earn-compare-2026-05-08-slot-418488624',
venueIntelligenceReceiptId: 'VS-kamino-earn-compare-2026-05-08',
sentinelReviewReceiptId: 'SENT-REVIEW-kamino-earn-compare-2026-05-08',
},
tool: 'toreva_earn_compare_kamino',
familyId: 'earn_lending',
primitiveId: 'exec.earn_compare',
operation: 'compare',
latencyMs: 1263,
};

vi.mock('@toreva/sdk', async () => {
const actual = await vi.importActual<typeof import('@toreva/sdk')>('@toreva/sdk');
return {
...actual,
earnCompare: vi.fn(async () => baseResult),
};
});

afterEach(() => {
vi.clearAllMocks();
});

describe('parseEarnCompareArgs', () => {
it('accepts --asset=USDC --venue=kamino', () => {
const r = parseEarnCompareArgs(['--asset=USDC', '--venue=kamino']);
expect(r).toEqual({ asset: 'USDC', venue: 'kamino' });
});

it('accepts space-separated --asset USDC --venue marginfi', () => {
const r = parseEarnCompareArgs(['--asset', 'USDC', '--venue', 'marginfi']);
expect(r).toEqual({ asset: 'USDC', venue: 'marginfi' });
});

it('throws on missing --asset', () => {
expect(() => parseEarnCompareArgs(['--venue=kamino'])).toThrow(/asset/);
});

it('throws on missing --venue', () => {
expect(() => parseEarnCompareArgs(['--asset=USDC'])).toThrow(/venue/);
});

it('rejects unsupported asset', () => {
expect(() => parseEarnCompareArgs(['--asset=SOL', '--venue=kamino'])).toThrow(/SOL/);
});

it('rejects unsupported venue', () => {
expect(() => parseEarnCompareArgs(['--asset=USDC', '--venue=save'])).toThrow(/save/);
});
});

describe('formatEarnCompare', () => {
it('renders the canonical fields and the evidence triple', () => {
const out = formatEarnCompare(baseResult);
expect(out).toMatch(/Venue\s+kamino/);
expect(out).toMatch(/Asset\s+USDC/);
expect(out).toMatch(/APY\s+4\.3626%/);
expect(out).toMatch(/Source\s+defillama/);
expect(out).toMatch(/Pool ID\s+d2141a59/);
expect(out).toMatch(/sentinel\s+SENT-REVIEW/);
expect(out).toMatch(/venue intel\s+VS-/);
expect(out).toMatch(/read\s+NETSOL-READ/);
});

it('omits null reward APY line', () => {
const out = formatEarnCompare(baseResult);
expect(out).not.toMatch(/APY \(reward\)/);
});

it('renders TVL with thousands separator', () => {
const out = formatEarnCompare(baseResult);
expect(out).toMatch(/TVL \(USD\)\s+\$7,799,705/);
});
});

describe('runEarnCompareCommand', () => {
it('calls earnCompare with parsed args and prints the table', async () => {
const sdk = await import('@toreva/sdk');
const mockedEarnCompare = vi.mocked(sdk.earnCompare);
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);

await runEarnCompareCommand(['--asset=USDC', '--venue=kamino']);

expect(mockedEarnCompare).toHaveBeenCalledWith({ asset: 'USDC', venue: 'kamino' });
const printed = logSpy.mock.calls.flat().join('\n');
expect(printed).toMatch(/kamino/);
expect(printed).toMatch(/SENT-REVIEW/);
logSpy.mockRestore();
});
});
127 changes: 127 additions & 0 deletions packages/cli/src/__tests__/token-receive.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { describe, it, expect, vi, afterEach } from 'vitest';
import {
parseTokenReceiveArgs,
formatTokenReceive,
runTokenReceiveCommand,
} from '../commands/token-receive.js';
import type { TokenReceiveResult } from '@toreva/sdk';

const event = {
signature: '4xF2abcd9876sig',
mint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
amount: 100,
decimals: 6,
fromWallet: 'Sender11111111111111111111111111111111111111',
blockTime: '2026-05-10T11:50:00.000Z',
slot: 418_488_700,
};
const baseResult: TokenReceiveResult = {
ok: true,
walletAddress: 'TestWallet1111111111111111111111111111111111',
wallet: 'TestWallet1111111111111111111111111111111111',
inspectedSignatureCount: 1,
totalSignatureCountReturned: 1,
count: 1,
events: [event],
receives: [event],
source: 'solana-rpc',
fetchedAt: '2026-05-10T12:20:09.082Z',
evidenceRef: {
readEvidenceId: 'NETSOL-READ-spl-token-receive-2026-05-08',
venueIntelligenceReceiptId: 'VS-spl-token-receive-2026-05-08',
sentinelReviewReceiptId: 'SENT-REVIEW-spl-token-receive-2026-05-08',
},
tool: 'toreva_token_receive_scan',
familyId: 'token_ops',
primitiveId: 'exec.receive',
operation: 'scan',
latencyMs: 600,
};

vi.mock('@toreva/sdk', async () => {
const actual = await vi.importActual<typeof import('@toreva/sdk')>('@toreva/sdk');
return {
...actual,
tokenReceive: vi.fn(async () => baseResult),
};
});

afterEach(() => {
vi.clearAllMocks();
});

describe('parseTokenReceiveArgs', () => {
it('accepts --wallet=<addr>', () => {
const r = parseTokenReceiveArgs(['--wallet=ABC123']);
expect(r).toEqual({ wallet: 'ABC123' });
});

it('accepts --wallet <addr> --limit 25', () => {
const r = parseTokenReceiveArgs(['--wallet', 'ABC123', '--limit', '25']);
expect(r).toEqual({ wallet: 'ABC123', limit: 25 });
});

it('throws on missing --wallet', () => {
expect(() => parseTokenReceiveArgs([])).toThrow(/wallet/);
});

it('rejects out-of-range limit (low)', () => {
expect(() => parseTokenReceiveArgs(['--wallet=ABC', '--limit=0'])).toThrow(/limit/);
});

it('rejects out-of-range limit (high — capped at 25 by live MCP)', () => {
expect(() => parseTokenReceiveArgs(['--wallet=ABC', '--limit=9999'])).toThrow(/limit/);
});

it('rejects non-numeric limit', () => {
expect(() => parseTokenReceiveArgs(['--wallet=ABC', '--limit=abc'])).toThrow(/limit/);
});
});

describe('formatTokenReceive', () => {
it('renders the wallet, count, source, and evidence triple', () => {
const out = formatTokenReceive(baseResult);
expect(out).toMatch(/Wallet\s+TestWallet/);
expect(out).toMatch(/Receives\s+1/);
expect(out).toMatch(/Source\s+solana-rpc/);
expect(out).toMatch(/sentinel\s+SENT-REVIEW/);
expect(out).toMatch(/venue intel\s+VS-/);
expect(out).toMatch(/read\s+NETSOL-READ/);
});

it('renders a row per receive entry with truncated mint and signature', () => {
const out = formatTokenReceive(baseResult);
expect(out).toMatch(/2026-05-10T11:50:00\.000Z/);
expect(out).toMatch(/amount=100/);
expect(out).toMatch(/mint=EPjFWdd5/);
expect(out).toMatch(/sig=4xF2abcd/);
});

it('renders a friendly empty-state message when no receives', () => {
const empty: TokenReceiveResult = {
...baseResult,
receives: [],
events: [],
count: 0,
totalSignatureCountReturned: 0,
};
const out = formatTokenReceive(empty);
expect(out).toMatch(/no recent inbound SPL token transfers/);
});
});

describe('runTokenReceiveCommand', () => {
it('calls tokenReceive with parsed args and prints the table', async () => {
const sdk = await import('@toreva/sdk');
const mockedTokenReceive = vi.mocked(sdk.tokenReceive);
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);

await runTokenReceiveCommand(['--wallet=ABC123', '--limit=10']);

expect(mockedTokenReceive).toHaveBeenCalledWith({ wallet: 'ABC123', limit: 10 });
const printed = logSpy.mock.calls.flat().join('\n');
expect(printed).toMatch(/TestWallet/);
expect(printed).toMatch(/SENT-REVIEW/);
logSpy.mockRestore();
});
});
90 changes: 90 additions & 0 deletions packages/cli/src/commands/earn-compare.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { earnCompare, type EarnCompareVenue } from '@toreva/sdk';

export interface EarnCompareCliArgs {
asset: 'USDC';
venue: EarnCompareVenue;
}

/**
* Parse `toreva earn-compare --asset <asset> --venue <kamino|marginfi>`.
*
* Throws on missing or invalid args. Mirrors `init.ts` parser style.
*/
export function parseEarnCompareArgs(args: string[]): EarnCompareCliArgs {
let asset: string | undefined;
let venue: string | undefined;

for (const arg of args) {
if (arg.startsWith('--asset=')) {
asset = arg.slice('--asset='.length);
} else if (arg === '--asset') {
const idx = args.indexOf('--asset');
asset = args[idx + 1];
} else if (arg.startsWith('--venue=')) {
venue = arg.slice('--venue='.length);
} else if (arg === '--venue') {
const idx = args.indexOf('--venue');
venue = args[idx + 1];
}
}

if (!asset) {
throw new Error('Missing required --asset flag (only USDC supported in R1)');
}
if (asset !== 'USDC') {
throw new Error(`Unsupported asset: ${asset}. R1 supports USDC only.`);
}
if (!venue) {
throw new Error('Missing required --venue flag (kamino|marginfi)');
}
if (venue !== 'kamino' && venue !== 'marginfi') {
throw new Error(`Unsupported venue: ${venue}. R1 supports: kamino, marginfi.`);
}

return { asset: 'USDC', venue: venue as EarnCompareVenue };
}

/**
* Format the earn-compare result as a human-readable table.
*/
export function formatEarnCompare(r: Awaited<ReturnType<typeof earnCompare>>): string {
const lines: string[] = [];
lines.push('');
lines.push(` Venue ${r.venue}`);
lines.push(` Asset ${r.asset}`);
if (r.apyPct !== null && r.apyPct !== undefined) {
lines.push(` APY ${r.apyPct.toFixed(4)}%`);
} else {
lines.push(` APY (unavailable)`);
if (r.apyNote) lines.push(` Note ${r.apyNote}`);
}
if (r.apyBasePct !== undefined && r.apyBasePct !== null) {
lines.push(` APY (base) ${r.apyBasePct.toFixed(4)}%`);
}
if (r.apyRewardPct !== undefined && r.apyRewardPct !== null) {
lines.push(` APY (reward) ${r.apyRewardPct.toFixed(4)}%`);
}
if (r.tvlUsd !== undefined && r.tvlUsd !== null) {
lines.push(` TVL (USD) $${r.tvlUsd.toLocaleString('en-US')}`);
}
if (r.project) lines.push(` Project ${r.project}`);
if (r.poolId) lines.push(` Pool ID ${r.poolId}`);
lines.push(` Source ${r.source}`);
if (r.fetchedAt) lines.push(` Fetched at ${r.fetchedAt}`);
lines.push('');
lines.push(' Evidence:');
lines.push(` read ${r.evidenceRef.readEvidenceId}`);
lines.push(` venue intel ${r.evidenceRef.venueIntelligenceReceiptId}`);
lines.push(` sentinel ${r.evidenceRef.sentinelReviewReceiptId}`);
lines.push('');
return lines.join('\n');
}

/**
* Run the CLI earn-compare command.
*/
export async function runEarnCompareCommand(args: string[]): Promise<void> {
const parsed = parseEarnCompareArgs(args);
const result = await earnCompare(parsed);
console.log(formatEarnCompare(result));
}
Loading
Loading