Skip to content

Commit ed05058

Browse files
authored
feat(infra): fetch pending squads txs (#7035)
1 parent 32479e1 commit ed05058

7 files changed

Lines changed: 799 additions & 3 deletions

File tree

typescript/infra/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"@hyperlane-xyz/tsconfig": "workspace:^",
4242
"@nomiclabs/hardhat-ethers": "^2.2.3",
4343
"@nomiclabs/hardhat-waffle": "^2.0.6",
44+
"@sqds/multisig": "2.1.4",
4445
"@types/chai": "^4.2.21",
4546
"@types/json-stable-stringify": "^1.0.36",
4647
"@types/mocha": "^10.0.1",
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import chalk from 'chalk';
2+
import yargs from 'yargs';
3+
4+
import {
5+
LogFormat,
6+
LogLevel,
7+
configureRootLogger,
8+
rootLogger,
9+
} from '@hyperlane-xyz/utils';
10+
11+
import { squadsConfigs } from '../../src/config/squads.js';
12+
import { logTable } from '../../src/utils/log.js';
13+
import { getPendingProposalsForChains } from '../../src/utils/squads.js';
14+
import { withChains } from '../agent-utils.js';
15+
import { getEnvironmentConfig } from '../core-utils.js';
16+
17+
const environment = 'mainnet3';
18+
19+
async function main() {
20+
configureRootLogger(LogFormat.Pretty, LogLevel.Info);
21+
22+
const { chains } = await withChains(
23+
yargs(process.argv.slice(2)),
24+
Object.keys(squadsConfigs),
25+
).argv;
26+
27+
const squadChains = Object.keys(squadsConfigs);
28+
const chainsToCheck = chains || squadChains;
29+
30+
if (chainsToCheck.length === 0) {
31+
rootLogger.error('No chains provided');
32+
process.exit(1);
33+
}
34+
35+
rootLogger.info(chalk.blue.bold('🔍 Squads Proposal Status Monitor'));
36+
rootLogger.info(
37+
chalk.blue(
38+
`Checking squads proposals on chains: ${chainsToCheck.join(', ')}`,
39+
),
40+
);
41+
42+
const envConfig = getEnvironmentConfig(environment);
43+
const mpp = await envConfig.getMultiProtocolProvider();
44+
45+
const pendingProposals = await getPendingProposalsForChains(
46+
chainsToCheck,
47+
mpp,
48+
);
49+
50+
if (pendingProposals.length === 0) {
51+
rootLogger.info(chalk.green('No pending proposals found!'));
52+
process.exit(0);
53+
}
54+
55+
logTable(pendingProposals, [
56+
'chain',
57+
'nonce',
58+
'submissionDate',
59+
'fullTxHash',
60+
'approvals',
61+
'threshold',
62+
'status',
63+
'balance',
64+
]);
65+
66+
process.exit(0);
67+
}
68+
69+
main()
70+
.then()
71+
.catch((e) => {
72+
rootLogger.error(e);
73+
process.exit(1);
74+
});
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
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+
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { PublicKey } from '@solana/web3.js';
2+
3+
import { ChainMap, ChainName } from '@hyperlane-xyz/sdk';
4+
import { Address } from '@hyperlane-xyz/utils';
5+
6+
export type SquadConfig = {
7+
programId: Address;
8+
multisigPda: Address;
9+
vault: Address;
10+
};
11+
12+
export type SquadsKeys = Record<keyof SquadConfig, PublicKey>;
13+
14+
export const squadsConfigs: ChainMap<SquadConfig> = {
15+
solanamainnet: {
16+
programId: 'SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf',
17+
multisigPda: 'EvptYJrjGUB3FXDoW8w8LTpwg1TTS4W1f628c1BnscB4',
18+
vault: '3oocunLfAgATEqoRyW7A5zirsQuHJh6YjD4kReiVVKLa',
19+
},
20+
soon: {
21+
programId: 'Hz8Zg8JYFshThnKHXSZV9XJFbyYUUKBb5NJUrxDvF8PB',
22+
multisigPda: '3tQm2hkauvqoRsfJg6NmUA6eMEWqFdvbiJUZUBFHXD6A',
23+
vault: '7Y6WDpMfNeb1b4YYbyUkF41z1DuPhvDDuWWJCHPRNa9Y',
24+
},
25+
eclipsemainnet: {
26+
programId: 'eSQDSMLf3qxwHVHeTr9amVAGmZbRLY2rFdSURandt6f',
27+
multisigPda: 'CSnrKeqrrLm6v9NvChYKT58mfRGYnMk8MeLGWhKvBdbk',
28+
vault: 'D742EWw9wpV47jRAvEenG1oWHfMmpiQNJLjHTBfXhuRm',
29+
},
30+
sonicsvm: {
31+
programId: 'sqdsFBUUwbsuoLUhoWdw343Je6mvn7dGVVRYCa4wtqJ',
32+
multisigPda: 'BsdNMofu1a4ncHFJSNZWuTcZae9yt4ZGDuaneN5am5m6',
33+
vault: '8ECSwp5yo2EeZkozSrpPnMj5Rmcwa4VBYCETE9LHmc9y',
34+
},
35+
solaxy: {
36+
programId: '222DRw2LbM7xztYq1efxcbfBePi6xnv27o7QBGm9bpts',
37+
multisigPda: 'XgeE3uXEy5bKPbgYv3D9pWovhu3PWrxt3RR5bdp9RkW',
38+
vault: '4chV16Dea6CW6xyQcHj9RPwBZitfxYgpafkSoZgzy4G8',
39+
},
40+
// svmbnb: {
41+
// programId: 'Hz8Zg8JYFshThnKHXSZV9XJFbyYUUKBb5NJUrxDvF8PB',
42+
// multisigPda: '9eQpT28rq83sc2wtsGK7TYirXJ4sL1QmQENSMz2TbHEv',
43+
// vault: '3JiYeSX1rN2nsh78Xqypg87vQd2Y5Y9h5w9ns6Hjii5B',
44+
// },
45+
};
46+
47+
export function getSquadsKeys(chainName: ChainName): SquadsKeys {
48+
if (!squadsConfigs[chainName]) {
49+
throw new Error(`Squads config not found on chain ${chainName}`);
50+
}
51+
return {
52+
multisigPda: new PublicKey(squadsConfigs[chainName].multisigPda),
53+
programId: new PublicKey(squadsConfigs[chainName].programId),
54+
vault: new PublicKey(squadsConfigs[chainName].vault),
55+
};
56+
}

0 commit comments

Comments
 (0)