@@ -6,7 +6,7 @@ import { TOKENS, type SupportedChainName } from '@azeth/common';
66import { PaymentAgreementModuleAbi } from '@azeth/common/abis' ;
77import { createClient , resolveChain } from '../utils/client.js' ;
88import { resolveAddress , resolveSmartAccount } from '../utils/resolve.js' ;
9- import { success , error , handleError , formatUSD , guardianRequiredError } from '../utils/response.js' ;
9+ import { success , error , handleError , formatUSD , guardianRequiredError , safeIso } from '../utils/response.js' ;
1010
1111// ──────────────────────────────────────────────
1212// Shared formatting utilities
@@ -481,8 +481,6 @@ export function registerAgreementTools(server: McpServer): void {
481481 const chain = resolveChain ( args . chain ) ;
482482
483483 // Resolve to the caller's OWN smart account (not arbitrary addresses).
484- // Only the payer can cancel their own agreements — resolveSmartAccount
485- // restricts resolution to accounts owned by the caller's private key.
486484 let account : `0x${string } `;
487485 if ( args . smartAccount ) {
488486 try {
@@ -499,6 +497,19 @@ export function registerAgreementTools(server: McpServer): void {
499497 account = await client . resolveSmartAccount ( ) ;
500498 }
501499
500+ // OWNERSHIP GATE: resolveSmartAccount passes raw addresses through unchecked,
501+ // so a foreign account address would reach the signing path and die with a
502+ // misleading guardian-co-signature error (the chain rejects it regardless —
503+ // this check exists for error quality, not security). Only the payer can cancel.
504+ const ownedAccounts = await client . getSmartAccounts ( ) ;
505+ if ( ! ownedAccounts . some ( ( a ) => a . toLowerCase ( ) === account . toLowerCase ( ) ) ) {
506+ return error (
507+ 'UNAUTHORIZED' ,
508+ `Smart account ${ account } is not owned by your key — only the payer (agreement creator) can cancel an agreement.` ,
509+ 'Use azeth_accounts to list your own accounts. To stop receiving payments as a payee, contact the payer or simply stop providing the service.' ,
510+ ) ;
511+ }
512+
502513 const agreementId = BigInt ( args . agreementId ) ;
503514
504515 // Pre-flight: check agreement exists and is active
@@ -620,7 +631,7 @@ export function registerAgreementTools(server: McpServer): void {
620631 // schedules from creation), so lastExecuted is never 0n even before any execution.
621632 const lastExecutedAt = agreement . executionCount === 0n
622633 ? null
623- : new Date ( Number ( agreement . lastExecuted ) * 1000 ) . toISOString ( ) ;
634+ : safeIso ( agreement . lastExecuted ) ;
624635
625636 let nextExecutionTime : string ;
626637 let nextExecutionIn : string ;
@@ -632,7 +643,9 @@ export function registerAgreementTools(server: McpServer): void {
632643 nextExecutionIn = `N/A (${ status } )` ;
633644 isDue = false ;
634645 } else {
635- nextExecutionTime = new Date ( Number ( nextExecTime ) * 1000 ) . toISOString ( ) ;
646+ // safeIso: read path of ON-CHAIN data — an agreement created by a non-MCP client
647+ // can hold a uint256 timestamp beyond the JS Date range; reporting must not throw.
648+ nextExecutionTime = safeIso ( nextExecTime ) ;
636649 const nowSecs = Math . floor ( Date . now ( ) / 1000 ) ;
637650 const diff = Number ( nextExecTime ) - nowSecs ;
638651 if ( diff <= 0 ) {
@@ -697,7 +710,7 @@ export function registerAgreementTools(server: McpServer): void {
697710 lastExecutedAt,
698711 nextExecutionTime,
699712 nextExecutionIn,
700- expiresAt : agreement . endTime === 0n ? 'never' : new Date ( Number ( agreement . endTime ) * 1000 ) . toISOString ( ) ,
713+ expiresAt : agreement . endTime === 0n ? 'never' : safeIso ( agreement . endTime ) ,
701714 // Checks
702715 isDue,
703716 canExecute,
0 commit comments