|
| 1 | +import chalk from 'chalk'; |
| 2 | +import yargs, { Argv } from 'yargs'; |
| 3 | + |
| 4 | +import { |
| 5 | + LogFormat, |
| 6 | + LogLevel, |
| 7 | + configureRootLogger, |
| 8 | + rootLogger, |
| 9 | + stringifyObject, |
| 10 | +} from '@hyperlane-xyz/utils'; |
| 11 | + |
| 12 | +import { getSquadsKeys, squadsConfigs } from '../../src/config/squads.js'; |
| 13 | +import { getSquadProposal } from '../../src/utils/squads.js'; |
| 14 | +import { withChain } from '../agent-utils.js'; |
| 15 | +import { getEnvironmentConfig } from '../core-utils.js'; |
| 16 | + |
| 17 | +const environment = 'mainnet3'; |
| 18 | + |
| 19 | +function withTransactionIndex<T>(args: Argv<T>) { |
| 20 | + return args |
| 21 | + .describe('transactionIndex', 'Transaction index of the proposal to read') |
| 22 | + .number('transactionIndex') |
| 23 | + .demandOption('transactionIndex') |
| 24 | + .alias('t', 'transactionIndex'); |
| 25 | +} |
| 26 | + |
| 27 | +function withVerbose<T>(args: Argv<T>) { |
| 28 | + return args |
| 29 | + .describe('verbose', 'Show verbose output including raw API data') |
| 30 | + .boolean('verbose') |
| 31 | + .default('verbose', false) |
| 32 | + .alias('v', 'verbose'); |
| 33 | +} |
| 34 | + |
| 35 | +async function main() { |
| 36 | + configureRootLogger(LogFormat.Pretty, LogLevel.Info); |
| 37 | + |
| 38 | + const { chain, transactionIndex, verbose } = await withChain( |
| 39 | + withTransactionIndex(withVerbose(yargs(process.argv.slice(2)))), |
| 40 | + ) |
| 41 | + .choices('chain', Object.keys(squadsConfigs)) |
| 42 | + .demandOption('chain').argv; |
| 43 | + |
| 44 | + if (!squadsConfigs[chain]) { |
| 45 | + rootLogger.error( |
| 46 | + chalk.red.bold(`No squads config found for chain: ${chain}`), |
| 47 | + ); |
| 48 | + rootLogger.info( |
| 49 | + chalk.gray('Available chains:'), |
| 50 | + Object.keys(squadsConfigs).join(', '), |
| 51 | + ); |
| 52 | + process.exit(1); |
| 53 | + } |
| 54 | + |
| 55 | + rootLogger.info(chalk.blue.bold('🔍 Squads Proposal Reader')); |
| 56 | + rootLogger.info( |
| 57 | + chalk.blue(`Reading proposal ${transactionIndex} on chain: ${chain}`), |
| 58 | + ); |
| 59 | + |
| 60 | + try { |
| 61 | + const envConfig = getEnvironmentConfig(environment); |
| 62 | + const mpp = await envConfig.getMultiProtocolProvider(); |
| 63 | + |
| 64 | + rootLogger.info(chalk.gray('Fetching proposal data...')); |
| 65 | + |
| 66 | + const proposalData = await getSquadProposal(chain, mpp, transactionIndex); |
| 67 | + |
| 68 | + if (!proposalData) { |
| 69 | + rootLogger.error( |
| 70 | + chalk.red.bold(`Proposal ${transactionIndex} not found on ${chain}`), |
| 71 | + ); |
| 72 | + process.exit(1); |
| 73 | + } |
| 74 | + |
| 75 | + const { proposal, multisig, proposalPda } = proposalData; |
| 76 | + |
| 77 | + // Display basic proposal information |
| 78 | + rootLogger.info(chalk.green.bold('\n📋 Proposal Information:')); |
| 79 | + rootLogger.info(chalk.white(` Chain: ${chain}`)); |
| 80 | + rootLogger.info(chalk.white(` Transaction Index: ${transactionIndex}`)); |
| 81 | + rootLogger.info(chalk.white(` Proposal PDA: ${proposalPda.toBase58()}`)); |
| 82 | + rootLogger.info( |
| 83 | + chalk.white(` Multisig PDA: ${proposal.multisig.toBase58()}`), |
| 84 | + ); |
| 85 | + |
| 86 | + // Coerce all numeric fields to consistent types for safe comparison |
| 87 | + const threshold = Number(multisig.threshold); |
| 88 | + const staleTransactionIndex = Number(multisig.staleTransactionIndex); |
| 89 | + const currentTransactionIndex = Number(multisig.transactionIndex); |
| 90 | + const timeLock = Number(multisig.timeLock); |
| 91 | + |
| 92 | + // Display proposal status |
| 93 | + rootLogger.info(chalk.green.bold('\n📊 Status Information:')); |
| 94 | + rootLogger.info( |
| 95 | + chalk.white( |
| 96 | + ` Status: ${transactionIndex < staleTransactionIndex ? 'Stale' : proposal.status.__kind}`, |
| 97 | + ), |
| 98 | + ); |
| 99 | + if ('timestamp' in proposal.status && proposal.status.timestamp) { |
| 100 | + const timestamp = Number(proposal.status.timestamp); |
| 101 | + const date = new Date(timestamp * 1000); |
| 102 | + rootLogger.info( |
| 103 | + chalk.white( |
| 104 | + ` Timestamp: ${date.toISOString()} (${date.toLocaleString()})`, |
| 105 | + ), |
| 106 | + ); |
| 107 | + } |
| 108 | + |
| 109 | + // Display voting information |
| 110 | + rootLogger.info(chalk.green.bold('\n🗳️ Voting Information:')); |
| 111 | + rootLogger.info(chalk.white(` Approvals: ${proposal.approved.length}`)); |
| 112 | + rootLogger.info(chalk.white(` Rejections: ${proposal.rejected.length}`)); |
| 113 | + rootLogger.info( |
| 114 | + chalk.white(` Cancellations: ${proposal.cancelled.length}`), |
| 115 | + ); |
| 116 | + rootLogger.info(chalk.white(` Threshold: ${threshold}`)); |
| 117 | + |
| 118 | + const status = proposal.status.__kind; |
| 119 | + const approvals = proposal.approved.length; |
| 120 | + |
| 121 | + if (status === 'Active' && approvals >= threshold) { |
| 122 | + rootLogger.info( |
| 123 | + chalk.green( |
| 124 | + ` Status: Ready to execute (${approvals}/${threshold} approvals)`, |
| 125 | + ), |
| 126 | + ); |
| 127 | + } else if (status === 'Active') { |
| 128 | + if (transactionIndex < staleTransactionIndex) { |
| 129 | + rootLogger.info(chalk.red(` Status: Stale`)); |
| 130 | + } else { |
| 131 | + rootLogger.info( |
| 132 | + chalk.yellow( |
| 133 | + ` Status: Pending (${approvals}/${threshold} approvals)`, |
| 134 | + ), |
| 135 | + ); |
| 136 | + } |
| 137 | + } else { |
| 138 | + rootLogger.info(chalk.blue(` Status: ${status}`)); |
| 139 | + } |
| 140 | + |
| 141 | + // Display approvers |
| 142 | + if (proposal.approved.length > 0) { |
| 143 | + rootLogger.info(chalk.green.bold('\n✅ Approvers:')); |
| 144 | + proposal.approved.forEach((approver, index) => { |
| 145 | + rootLogger.info(chalk.white(` ${index + 1}. ${approver.toBase58()}`)); |
| 146 | + }); |
| 147 | + } |
| 148 | + |
| 149 | + // Display rejectors |
| 150 | + if (proposal.rejected.length > 0) { |
| 151 | + rootLogger.info(chalk.red.bold('\n❌ Rejectors:')); |
| 152 | + proposal.rejected.forEach((rejector, index) => { |
| 153 | + rootLogger.info(chalk.white(` ${index + 1}. ${rejector.toBase58()}`)); |
| 154 | + }); |
| 155 | + } |
| 156 | + |
| 157 | + // Display cancellers |
| 158 | + if (proposal.cancelled.length > 0) { |
| 159 | + rootLogger.info(chalk.gray.bold('\n🚫 Cancellers:')); |
| 160 | + proposal.cancelled.forEach((canceller, index) => { |
| 161 | + rootLogger.info(chalk.white(` ${index + 1}. ${canceller.toBase58()}`)); |
| 162 | + }); |
| 163 | + } |
| 164 | + |
| 165 | + // Display transaction details |
| 166 | + rootLogger.info(chalk.green.bold('\n💼 Transaction Details:')); |
| 167 | + rootLogger.info( |
| 168 | + chalk.white(` Transaction Index: ${Number(proposal.transactionIndex)}`), |
| 169 | + ); |
| 170 | + rootLogger.info(chalk.white(` Bump: ${Number(proposal.bump)}`)); |
| 171 | + |
| 172 | + // Display vault information |
| 173 | + const { vault } = getSquadsKeys(chain); |
| 174 | + const vaultBalance = await mpp |
| 175 | + .getSolanaWeb3Provider(chain) |
| 176 | + .getBalance(vault); |
| 177 | + const decimals = mpp.getChainMetadata(chain).nativeToken?.decimals; |
| 178 | + if (!decimals) { |
| 179 | + rootLogger.error(chalk.red.bold(`No decimals found for ${chain}`)); |
| 180 | + process.exit(1); |
| 181 | + } |
| 182 | + const balanceFormatted = (vaultBalance / 10 ** decimals).toFixed(5); |
| 183 | + rootLogger.info(chalk.green.bold('\n💰 Vault Information:')); |
| 184 | + rootLogger.info(chalk.white(` Vault Address: ${vault.toBase58()}`)); |
| 185 | + rootLogger.info(chalk.white(` Balance: ${balanceFormatted} SOL`)); |
| 186 | + |
| 187 | + // Display multisig information |
| 188 | + rootLogger.info(chalk.green.bold('\n🏛️ Multisig Information:')); |
| 189 | + rootLogger.info(chalk.white(` Threshold: ${threshold}`)); |
| 190 | + rootLogger.info(chalk.white(` Members: ${multisig.members.length}`)); |
| 191 | + rootLogger.info( |
| 192 | + chalk.white(` Current Transaction Index: ${currentTransactionIndex}`), |
| 193 | + ); |
| 194 | + rootLogger.info(chalk.white(` Time Lock: ${timeLock}`)); |
| 195 | + rootLogger.info( |
| 196 | + chalk.white(` Stale Transaction Index: ${staleTransactionIndex}`), |
| 197 | + ); |
| 198 | + rootLogger.info( |
| 199 | + chalk.white(` Create Key: ${multisig.createKey.toBase58()}`), |
| 200 | + ); |
| 201 | + rootLogger.info( |
| 202 | + chalk.white(` Config Authority: ${multisig.configAuthority.toBase58()}`), |
| 203 | + ); |
| 204 | + |
| 205 | + // Display members |
| 206 | + rootLogger.info(chalk.green.bold('\n👥 Multisig Members:')); |
| 207 | + multisig.members.forEach((member, index) => { |
| 208 | + rootLogger.info(chalk.white(` ${index + 1}. ${member.key.toBase58()}`)); |
| 209 | + }); |
| 210 | + |
| 211 | + // Verbose output with raw data |
| 212 | + if (verbose) { |
| 213 | + rootLogger.info(chalk.green.bold('\n🔍 Raw Proposal Data:')); |
| 214 | + rootLogger.info(chalk.gray(stringifyObject(proposal))); |
| 215 | + |
| 216 | + rootLogger.info(chalk.green.bold('\n🔍 Raw Multisig Data:')); |
| 217 | + rootLogger.info( |
| 218 | + chalk.gray( |
| 219 | + stringifyObject({ |
| 220 | + createKey: multisig.createKey.toBase58(), |
| 221 | + configAuthority: multisig.configAuthority.toBase58(), |
| 222 | + threshold: threshold, |
| 223 | + timeLock: timeLock, |
| 224 | + transactionIndex: currentTransactionIndex, |
| 225 | + staleTransactionIndex: staleTransactionIndex, |
| 226 | + rentCollector: multisig.rentCollector?.toBase58() || 'null', |
| 227 | + bump: multisig.bump, |
| 228 | + members: multisig.members.map((m) => ({ |
| 229 | + key: m.key.toBase58(), |
| 230 | + permissions: m.permissions, |
| 231 | + })), |
| 232 | + }), |
| 233 | + ), |
| 234 | + ); |
| 235 | + } |
| 236 | + |
| 237 | + rootLogger.info( |
| 238 | + chalk.green.bold('\n✅ Proposal data retrieved successfully!'), |
| 239 | + ); |
| 240 | + } catch (error) { |
| 241 | + rootLogger.error(chalk.red.bold('❌ Error reading proposal:')); |
| 242 | + rootLogger.error(chalk.red(error)); |
| 243 | + process.exit(1); |
| 244 | + } |
| 245 | +} |
| 246 | + |
| 247 | +main() |
| 248 | + .then() |
| 249 | + .catch((e) => { |
| 250 | + rootLogger.error(e); |
| 251 | + process.exit(1); |
| 252 | + }); |
0 commit comments