@@ -857,6 +857,290 @@ export class ShowRewardInfo extends Command {
857857 }
858858}
859859
860+ export class CheckAllPendingRoots extends Command {
861+ static paths = [ [ "reward" , "check-all-pending" ] ] ;
862+ dir = Option . String ( "--dir" , "chains" ) ;
863+
864+ async execute ( ) {
865+ const rewards = await loadAllData ( this . dir , "rewards" ) ;
866+ console . log ( "🔍 Checking validity of all pending roots...\n" ) ;
867+
868+ let foundPending = false ;
869+ let validCount = 0 ;
870+ let invalidCount = 0 ;
871+ let errorCount = 0 ;
872+ let readyToAccept : string [ ] = [ ] ;
873+
874+ for ( const reward of rewards ) {
875+ try {
876+ const { chain, publicClient } = getWalletInfo ( reward . chainId ) ;
877+
878+ if ( ! ( "urdFactory" in chain . morpho ) ) {
879+ console . log ( `⚠️ Skipping ${ reward . id } : No urdFactory for chain ${ chain . id } ` ) ;
880+ continue ;
881+ }
882+
883+ const pendingRoot = await getPendingRoot (
884+ publicClient ,
885+ getAddress ( reward . urdAddress ) ,
886+ ) ;
887+
888+ if ( pendingRoot !== zeroHash ) {
889+ foundPending = true ;
890+ console . log ( `\n${ '=' . repeat ( 60 ) } ` ) ;
891+ console . log ( `🔄 CHECKING: ${ reward . id } (${ reward . name || "no name" } )` ) ;
892+ console . log ( `${ '=' . repeat ( 60 ) } ` ) ;
893+
894+ let isValid = true ;
895+ let hasErrors = false ;
896+
897+ // Get detailed pending root info and timelock
898+ const pendingRootData = await getPendingRootWithTimestamp (
899+ publicClient ,
900+ getAddress ( reward . urdAddress ) ,
901+ ) ;
902+
903+ const timelockPeriod = await getTimelock (
904+ publicClient ,
905+ getAddress ( reward . urdAddress ) ,
906+ ) ;
907+
908+ console . log ( `📋 Chain: ${ reward . chainId } ` ) ;
909+ console . log ( `📋 URD Address: ${ reward . urdAddress } ` ) ;
910+ console . log ( `📋 On-chain Pending Root: ${ pendingRoot } ` ) ;
911+
912+ // Check endpoint validation
913+ try {
914+ const endpointUrl = `https://sap.icarus.tools/blue?method=getPendingTreeForCampaign¶ms=[%22${ encodeURIComponent ( reward . id ) } %22]` ;
915+ const response = await fetch ( endpointUrl ) ;
916+
917+ if ( ! response . ok ) {
918+ throw new Error ( `HTTP error! status: ${ response . status } ` ) ;
919+ }
920+
921+ const endpointData = await response . json ( ) ;
922+
923+ if ( endpointData && endpointData . result && endpointData . result . root ) {
924+ const campaignTree = endpointData . result ;
925+ const endpointRoot = campaignTree . root ;
926+
927+ console . log ( `📋 Endpoint Root: ${ endpointRoot } ` ) ;
928+
929+ if ( pendingRoot . toLowerCase ( ) === endpointRoot . toLowerCase ( ) ) {
930+ console . log ( "✅ Root Match: On-chain pending root matches endpoint root" ) ;
931+ } else {
932+ console . log ( "❌ Root Match: On-chain pending root does NOT match endpoint root" ) ;
933+ isValid = false ;
934+ }
935+
936+ // Validate campaign progress
937+ const progressValid = this . validateCampaignProgressQuick ( reward , campaignTree ) ;
938+ if ( ! progressValid ) {
939+ isValid = false ;
940+ }
941+
942+ // Validate blacklist
943+ const blacklistValid = this . validateBlacklistQuick ( campaignTree , reward . chainId ) ;
944+ if ( ! blacklistValid ) {
945+ isValid = false ;
946+ }
947+
948+ } else {
949+ console . log ( "❌ Endpoint: Did not return a valid campaign tree" ) ;
950+ isValid = false ;
951+ }
952+ } catch ( error ) {
953+ console . log ( `❌ Endpoint Error: ${ error instanceof Error ? error . message : error } ` ) ;
954+ hasErrors = true ;
955+ }
956+
957+ // Display timelock information
958+ this . displayTimelockInfoQuick ( pendingRootData , timelockPeriod ) ;
959+
960+ // Final status
961+ const validAtTimestamp = Number ( pendingRootData . timestamp ) ;
962+ const now = Math . floor ( Date . now ( ) / 1000 ) ;
963+ const isReady = now >= validAtTimestamp ;
964+
965+ if ( hasErrors ) {
966+ console . log ( `\n🔴 FINAL STATUS: ERROR - Could not fully validate ${ reward . id } ` ) ;
967+ errorCount ++ ;
968+ } else if ( ! isValid ) {
969+ console . log ( `\n🔴 FINAL STATUS: INVALID - ${ reward . id } has validation issues` ) ;
970+ invalidCount ++ ;
971+ } else if ( ! isReady ) {
972+ console . log ( `\n🟡 FINAL STATUS: VALID BUT LOCKED - ${ reward . id } is valid but still in timelock` ) ;
973+ validCount ++ ;
974+ } else {
975+ console . log ( `\n🟢 FINAL STATUS: VALID AND READY - ${ reward . id } is ready to accept` ) ;
976+ validCount ++ ;
977+ readyToAccept . push ( reward . id ) ;
978+ }
979+ }
980+ } catch ( error ) {
981+ console . log ( `❌ Error checking ${ reward . id } : ${ error instanceof Error ? error . message : error } ` ) ;
982+ errorCount ++ ;
983+ }
984+ }
985+
986+ // Summary
987+ console . log ( `\n${ '=' . repeat ( 60 ) } ` ) ;
988+ console . log ( `📊 VALIDATION SUMMARY` ) ;
989+ console . log ( `${ '=' . repeat ( 60 ) } ` ) ;
990+
991+ if ( ! foundPending ) {
992+ console . log ( "✨ No rewards have pending roots." ) ;
993+ } else {
994+ console . log ( `🟢 Valid: ${ validCount } ` ) ;
995+ console . log ( `🔴 Invalid: ${ invalidCount } ` ) ;
996+ console . log ( `⚠️ Errors: ${ errorCount } ` ) ;
997+ console . log ( `📊 Total Checked: ${ validCount + invalidCount + errorCount } ` ) ;
998+
999+ if ( validCount > 0 ) {
1000+ console . log ( `\n✅ ${ validCount } campaign(s) ready for acceptance` ) ;
1001+ }
1002+ if ( invalidCount > 0 ) {
1003+ console . log ( `\n❌ ${ invalidCount } campaign(s) have validation issues - DO NOT ACCEPT` ) ;
1004+ }
1005+ if ( errorCount > 0 ) {
1006+ console . log ( `\n⚠️ ${ errorCount } campaign(s) had validation errors - manual review required` ) ;
1007+ }
1008+
1009+ // Print acceptance commands for ready campaigns
1010+ if ( readyToAccept . length > 0 ) {
1011+ console . log ( `\n${ '=' . repeat ( 60 ) } ` ) ;
1012+ console . log ( `🚀 READY TO ACCEPT - RUN THESE COMMANDS:` ) ;
1013+ console . log ( `${ '=' . repeat ( 60 ) } ` ) ;
1014+
1015+ for ( const campaignId of readyToAccept ) {
1016+ console . log ( `./cli reward accept ${ campaignId } ` ) ;
1017+ }
1018+
1019+ console . log ( `\n💡 Or accept all at once:` ) ;
1020+ const acceptAllCommand = readyToAccept . map ( id => `./cli reward accept ${ id } ` ) . join ( ' && ' ) ;
1021+ console . log ( acceptAllCommand ) ;
1022+ }
1023+ }
1024+ }
1025+
1026+ private validateCampaignProgressQuick ( reward : any , campaignTree : any ) : boolean {
1027+ try {
1028+ const referenceTime = this . getMostRecentFriday9amPDT ( ) ;
1029+ const startTime = campaignTree . metadata ?. start_timestamp || reward . start_timestamp ;
1030+ const endTime = campaignTree . metadata ?. end_timestamp || reward . end_timestamp ;
1031+ const totalRewardAmount = BigInt ( reward . reward_amount ) ;
1032+
1033+ let completionPercentage = 0 ;
1034+ if ( referenceTime < startTime ) {
1035+ completionPercentage = 0 ;
1036+ } else if ( referenceTime >= endTime ) {
1037+ completionPercentage = 100 ;
1038+ } else {
1039+ const elapsed = referenceTime - startTime ;
1040+ const total = endTime - startTime ;
1041+ completionPercentage = ( elapsed / total ) * 100 ;
1042+ }
1043+
1044+ if ( campaignTree . tree && Array . isArray ( campaignTree . tree ) ) {
1045+ const totalClaimable = campaignTree . tree . reduce ( ( sum : bigint , entry : any ) => {
1046+ return sum + BigInt ( entry . claimable || entry . amount || 0 ) ;
1047+ } , BigInt ( 0 ) ) ;
1048+
1049+ const expectedMax = ( totalRewardAmount * BigInt ( Math . ceil ( completionPercentage ) ) ) / BigInt ( 100 ) ;
1050+ const tolerance = BigInt ( 5 ) ;
1051+ const maxAllowed = ( totalRewardAmount * ( BigInt ( Math . ceil ( completionPercentage ) ) + tolerance ) ) / BigInt ( 100 ) ;
1052+
1053+ if ( totalClaimable <= maxAllowed ) {
1054+ console . log ( "✅ Progress: Total claimable amount is within expected range" ) ;
1055+ return true ;
1056+ } else {
1057+ console . log ( "❌ Progress: Total claimable amount exceeds expected range" ) ;
1058+ return false ;
1059+ }
1060+ }
1061+ return true ;
1062+ } catch ( error ) {
1063+ console . log ( `⚠️ Progress: Could not validate campaign progress: ${ error } ` ) ;
1064+ return false ;
1065+ }
1066+ }
1067+
1068+ private validateBlacklistQuick ( campaignTree : any , chainId : number ) : boolean {
1069+ try {
1070+ const blacklist = loadBlacklist ( this . dir , chainId ) ;
1071+
1072+ if ( blacklist . length === 0 ) {
1073+ console . log ( "ℹ️ Blacklist: No blacklist found for this chain" ) ;
1074+ return true ;
1075+ }
1076+
1077+ if ( ! campaignTree . tree || ! Array . isArray ( campaignTree . tree ) ) {
1078+ console . log ( "⚠️ Blacklist: No tree data available for validation" ) ;
1079+ return false ;
1080+ }
1081+
1082+ for ( const entry of campaignTree . tree ) {
1083+ const userAddress = ( entry . account || entry . user || entry . address || '' ) . toLowerCase ( ) ;
1084+ if ( blacklist . includes ( userAddress ) ) {
1085+ console . log ( "🔥 CRITICAL: BLACKLISTED ADDRESS DETECTED - DO NOT ACCEPT" ) ;
1086+ return false ;
1087+ }
1088+ }
1089+
1090+ console . log ( "✅ Blacklist: No blacklisted addresses found" ) ;
1091+ return true ;
1092+ } catch ( error ) {
1093+ console . log ( `⚠️ Blacklist: Could not validate blacklist: ${ error } ` ) ;
1094+ return false ;
1095+ }
1096+ }
1097+
1098+ private getMostRecentFriday9amPDT ( ) : number {
1099+ const now = new Date ( ) ;
1100+ const pdtOffset = 7 * 60 ;
1101+ const nowPDT = new Date ( now . getTime ( ) - pdtOffset * 60 * 1000 ) ;
1102+ const currentDay = nowPDT . getDay ( ) ;
1103+
1104+ let daysToSubtract = 0 ;
1105+ if ( currentDay === 5 ) {
1106+ daysToSubtract = 0 ;
1107+ } else if ( currentDay === 6 ) {
1108+ daysToSubtract = 1 ;
1109+ } else if ( currentDay === 0 ) {
1110+ daysToSubtract = 2 ;
1111+ } else {
1112+ daysToSubtract = currentDay + 2 ;
1113+ }
1114+
1115+ const targetFriday = new Date ( nowPDT ) ;
1116+ targetFriday . setDate ( targetFriday . getDate ( ) - daysToSubtract ) ;
1117+ targetFriday . setHours ( 9 , 0 , 0 , 0 ) ;
1118+ targetFriday . setHours ( targetFriday . getHours ( ) - 12 ) ;
1119+
1120+ const fridayUTC = new Date ( targetFriday . getTime ( ) + pdtOffset * 60 * 1000 ) ;
1121+ return Math . floor ( fridayUTC . getTime ( ) / 1000 ) ;
1122+ }
1123+
1124+ private displayTimelockInfoQuick ( pendingRootData : any , timelockPeriod : bigint ) {
1125+ if ( pendingRootData . root === zeroHash ) {
1126+ console . log ( `ℹ️ Timelock: No pending root to timelock` ) ;
1127+ return ;
1128+ }
1129+
1130+ const validAtTimestamp = Number ( pendingRootData . timestamp ) ;
1131+ const now = Math . floor ( Date . now ( ) / 1000 ) ;
1132+
1133+ if ( now >= validAtTimestamp ) {
1134+ console . log ( `✅ Timelock: Ready to accept (timelock expired)` ) ;
1135+ } else {
1136+ const timeRemaining = validAtTimestamp - now ;
1137+ const hoursRemaining = Math . floor ( timeRemaining / 3600 ) ;
1138+ const minutesRemaining = Math . floor ( ( timeRemaining % 3600 ) / 60 ) ;
1139+ console . log ( `⏳ Timelock: Still locked (${ hoursRemaining } h ${ minutesRemaining } m remaining)` ) ;
1140+ }
1141+ }
1142+ }
1143+
8601144export class TransferOwner extends Command {
8611145 static paths = [ [ "reward" , "transfer-owner" ] ] ;
8621146
0 commit comments