Skip to content

Commit b98902f

Browse files
authored
feat: infra safe tx parser auto-infers txs to parse (#5183)
### Description - feat: infra safe tx parser infers txs to parse - users are no longer required to provide the exact txs to parse - now supports multiple txs per chain - still allows someone to provide `--chains`/`-c` arg if they wish to specify exactly which chains to check - support timelock txs - write the yaml output to file instead of requiring users to `| tee file.txt` - add catch-all case for ownership transfers on ownables ### Drive-by changes - rootLogger instead of console logs (make eslint happy in editor) - remove `withTxHashes` helper as it's no longer used anywhere - fix dependency issue by moving getPendingTxs helper to utils - fixes ### Related issues no longer need #5160 ### Backward compatibility <!-- Are these changes backward compatible? Are there any infrastructure implications, e.g. changes that would prohibit deploying older commits using this infra tooling? Yes/No --> ### Testing <!-- What kind of testing have these changes undergone? None/Manual/Unit Tests --> --------- Signed-off-by: pbio <[email protected]>
1 parent 7275828 commit b98902f

File tree

8 files changed

+383
-193
lines changed

8 files changed

+383
-193
lines changed

typescript/infra/scripts/agent-utils.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -286,15 +286,6 @@ export function withRpcUrls<T>(args: Argv<T>) {
286286
.alias('r', 'rpcUrls');
287287
}
288288

289-
export function withTxHashes<T>(args: Argv<T>) {
290-
return args
291-
.describe('txHashes', 'transaction hash')
292-
.string('txHashes')
293-
.array('txHashes')
294-
.demandOption('txHashes')
295-
.alias('t', 'txHashes');
296-
}
297-
298289
// Interactively gets a single warp route ID
299290
export async function getWarpRouteIdInteractive() {
300291
const choices = Object.values(WarpRouteIds).map((id) => ({
Lines changed: 25 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,118 +1,25 @@
11
import { confirm } from '@inquirer/prompts';
22
import chalk from 'chalk';
3-
import { formatUnits } from 'ethers/lib/utils.js';
43
import yargs from 'yargs';
54

6-
import { MultiProvider } from '@hyperlane-xyz/sdk';
7-
import { LogFormat, LogLevel, configureRootLogger } from '@hyperlane-xyz/utils';
5+
import {
6+
LogFormat,
7+
LogLevel,
8+
configureRootLogger,
9+
rootLogger,
10+
} from '@hyperlane-xyz/utils';
811

912
import { Contexts } from '../../config/contexts.js';
1013
import { safes } from '../../config/environments/mainnet3/owners.js';
1114
import { Role } from '../../src/roles.js';
12-
import { executeTx, getSafeAndService } from '../../src/utils/safe.js';
15+
import {
16+
SafeTxStatus,
17+
executeTx,
18+
getPendingTxsForChains,
19+
} from '../../src/utils/safe.js';
1320
import { withChains } from '../agent-utils.js';
1421
import { getEnvironmentConfig } from '../core-utils.js';
1522

16-
export enum SafeTxStatus {
17-
NO_CONFIRMATIONS = '🔴',
18-
PENDING = '🟡',
19-
ONE_AWAY = '🔵',
20-
READY_TO_EXECUTE = '🟢',
21-
}
22-
23-
type SafeStatus = {
24-
chain: string;
25-
nonce: number;
26-
submissionDate: string;
27-
shortTxHash: string;
28-
fullTxHash: string;
29-
confs: number;
30-
threshold: number;
31-
status: string;
32-
balance: string;
33-
};
34-
35-
export async function getPendingTxsForChains(
36-
chains: string[],
37-
multiProvider: MultiProvider,
38-
): Promise<SafeStatus[]> {
39-
const txs: SafeStatus[] = [];
40-
await Promise.all(
41-
chains.map(async (chain) => {
42-
if (!safes[chain]) {
43-
console.error(chalk.red.bold(`No safe found for ${chain}`));
44-
return;
45-
}
46-
47-
if (chain === 'endurance') {
48-
console.info(
49-
chalk.gray.italic(
50-
`Skipping chain ${chain} as it does not have a functional safe API`,
51-
),
52-
);
53-
return;
54-
}
55-
56-
let safeSdk, safeService;
57-
try {
58-
({ safeSdk, safeService } = await getSafeAndService(
59-
chain,
60-
multiProvider,
61-
safes[chain],
62-
));
63-
} catch (error) {
64-
console.warn(
65-
chalk.yellow(
66-
`Skipping chain ${chain} as there was an error getting the safe service: ${error}`,
67-
),
68-
);
69-
return;
70-
}
71-
72-
const threshold = await safeSdk.getThreshold();
73-
const pendingTxs = await safeService.getPendingTransactions(safes[chain]);
74-
if (pendingTxs.results.length === 0) {
75-
return;
76-
}
77-
78-
const balance = await safeSdk.getBalance();
79-
const nativeToken = await multiProvider.getNativeToken(chain);
80-
const formattedBalance = formatUnits(balance, nativeToken.decimals);
81-
82-
pendingTxs.results.forEach(
83-
({ nonce, submissionDate, safeTxHash, confirmations }) => {
84-
const confs = confirmations?.length ?? 0;
85-
const status =
86-
confs >= threshold
87-
? SafeTxStatus.READY_TO_EXECUTE
88-
: confs === 0
89-
? SafeTxStatus.NO_CONFIRMATIONS
90-
: threshold - confs
91-
? SafeTxStatus.ONE_AWAY
92-
: SafeTxStatus.PENDING;
93-
94-
txs.push({
95-
chain,
96-
nonce,
97-
submissionDate: new Date(submissionDate).toDateString(),
98-
shortTxHash: `${safeTxHash.slice(0, 6)}...${safeTxHash.slice(-4)}`,
99-
fullTxHash: safeTxHash,
100-
confs,
101-
threshold,
102-
status,
103-
balance: `${Number(formattedBalance).toFixed(5)} ${
104-
nativeToken.symbol
105-
}`,
106-
});
107-
},
108-
);
109-
}),
110-
);
111-
return txs.sort(
112-
(a, b) => a.chain.localeCompare(b.chain) || a.nonce - b.nonce,
113-
);
114-
}
115-
11623
async function main() {
11724
const safeChains = Object.keys(safes);
11825
configureRootLogger(LogFormat.Pretty, LogLevel.Info);
@@ -129,7 +36,7 @@ async function main() {
12936

13037
const chainsToCheck = chains || safeChains;
13138
if (chainsToCheck.length === 0) {
132-
console.error('No chains provided');
39+
rootLogger.error('No chains provided');
13340
process.exit(1);
13441
}
13542

@@ -141,11 +48,16 @@ async function main() {
14148
chainsToCheck,
14249
);
14350

144-
const pendingTxs = await getPendingTxsForChains(chainsToCheck, multiProvider);
51+
const pendingTxs = await getPendingTxsForChains(
52+
chainsToCheck,
53+
multiProvider,
54+
safes,
55+
);
14556
if (pendingTxs.length === 0) {
146-
console.info(chalk.green('No pending transactions found!'));
57+
rootLogger.info(chalk.green('No pending transactions found!'));
14758
process.exit(0);
14859
}
60+
// eslint-disable-next-line no-console
14961
console.table(pendingTxs, [
15062
'chain',
15163
'nonce',
@@ -161,7 +73,7 @@ async function main() {
16173
(tx) => tx.status === SafeTxStatus.READY_TO_EXECUTE,
16274
);
16375
if (executableTxs.length === 0) {
164-
console.info(chalk.green('No transactions to execute!'));
76+
rootLogger.info(chalk.green('No transactions to execute!'));
16577
process.exit(0);
16678
}
16779

@@ -171,23 +83,23 @@ async function main() {
17183
});
17284

17385
if (!shouldExecute) {
174-
console.info(
86+
rootLogger.info(
17587
chalk.blue(
17688
`${executableTxs.length} transactions available for execution`,
17789
),
17890
);
17991
process.exit(0);
18092
}
18193

182-
console.info(chalk.blueBright('Executing transactions...'));
94+
rootLogger.info(chalk.blueBright('Executing transactions...'));
18395

18496
for (const tx of executableTxs) {
18597
const confirmExecuteTx = await confirm({
18698
message: `Execute transaction ${tx.shortTxHash} on chain ${tx.chain}?`,
18799
default: false,
188100
});
189101
if (confirmExecuteTx) {
190-
console.log(
102+
rootLogger.info(
191103
`Executing transaction ${tx.shortTxHash} on chain ${tx.chain}`,
192104
);
193105
try {
@@ -198,7 +110,7 @@ async function main() {
198110
tx.fullTxHash,
199111
);
200112
} catch (error) {
201-
console.error(chalk.red(`Error executing transaction: ${error}`));
113+
rootLogger.error(chalk.red(`Error executing transaction: ${error}`));
202114
return;
203115
}
204116
}
@@ -210,6 +122,6 @@ async function main() {
210122
main()
211123
.then()
212124
.catch((e) => {
213-
console.error(e);
125+
rootLogger.error(e);
214126
process.exit(1);
215127
});
Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,29 @@
1+
import chalk from 'chalk';
12
import { BigNumber } from 'ethers';
3+
import yargs from 'yargs';
24

35
import { AnnotatedEV5Transaction } from '@hyperlane-xyz/sdk';
46
import {
57
LogFormat,
68
LogLevel,
79
configureRootLogger,
10+
rootLogger,
811
stringifyObject,
912
} from '@hyperlane-xyz/utils';
1013

14+
import { safes } from '../../config/environments/mainnet3/owners.js';
1115
import { GovernTransactionReader } from '../../src/tx/govern-transaction-reader.js';
12-
import { getSafeTx } from '../../src/utils/safe.js';
13-
import { getArgs, withChainsRequired, withTxHashes } from '../agent-utils.js';
16+
import { getPendingTxsForChains, getSafeTx } from '../../src/utils/safe.js';
17+
import { writeYamlAtPath } from '../../src/utils/utils.js';
18+
import { withChains } from '../agent-utils.js';
1419
import { getEnvironmentConfig, getHyperlaneCore } from '../core-utils.js';
1520

16-
async function main() {
17-
const { environment, chains, txHashes } = await withTxHashes(
18-
withChainsRequired(getArgs()),
19-
).argv;
21+
const environment = 'mainnet3';
22+
const safeChains = Object.keys(safes);
2023

24+
async function main() {
25+
const { chains } = await withChains(yargs(process.argv.slice(2)), safeChains)
26+
.argv;
2127
configureRootLogger(LogFormat.Pretty, LogLevel.Info);
2228

2329
const config = getEnvironmentConfig(environment);
@@ -35,11 +41,31 @@ async function main() {
3541
warpRoutes,
3642
);
3743

44+
const pendingTxs = await getPendingTxsForChains(
45+
!chains || chains.length === 0 ? safeChains : chains,
46+
multiProvider,
47+
safes,
48+
);
49+
if (pendingTxs.length === 0) {
50+
rootLogger.info(chalk.green('No pending transactions found!'));
51+
process.exit(0);
52+
}
53+
// eslint-disable-next-line no-console
54+
console.table(pendingTxs, [
55+
'chain',
56+
'nonce',
57+
'submissionDate',
58+
'fullTxHash',
59+
'confs',
60+
'threshold',
61+
'status',
62+
'balance',
63+
]);
64+
3865
const chainResultEntries = await Promise.all(
39-
chains.map(async (chain, chainIndex) => {
40-
const txHash = txHashes[chainIndex];
41-
console.log(`Reading tx ${txHash} on ${chain}`);
42-
const safeTx = await getSafeTx(chain, multiProvider, txHash);
66+
pendingTxs.map(async ({ chain, fullTxHash }) => {
67+
rootLogger.info(`Reading tx ${fullTxHash} on ${chain}`);
68+
const safeTx = await getSafeTx(chain, multiProvider, fullTxHash);
4369
const tx: AnnotatedEV5Transaction = {
4470
to: safeTx.to,
4571
data: safeTx.data,
@@ -48,27 +74,30 @@ async function main() {
4874

4975
try {
5076
const results = await reader.read(chain, tx);
51-
console.log(`Finished reading tx ${txHash} on ${chain}`);
52-
return [chain, results];
77+
rootLogger.info(`Finished reading tx ${fullTxHash} on ${chain}`);
78+
return [`${chain}-${fullTxHash}`, results];
5379
} catch (err) {
54-
console.error('Error reading transaction', err, chain, tx);
80+
rootLogger.error('Error reading transaction', err, chain, tx);
5581
process.exit(1);
5682
}
5783
}),
5884
);
5985

60-
const chainResults = Object.fromEntries(chainResultEntries);
61-
console.log(stringifyObject(chainResults, 'yaml', 2));
62-
6386
if (reader.errors.length) {
64-
console.error('❌❌❌❌❌ Encountered fatal errors ❌❌❌❌❌');
65-
console.log(stringifyObject(reader.errors, 'yaml', 2));
66-
console.error('❌❌❌❌❌ Encountered fatal errors ❌❌❌❌❌');
87+
rootLogger.error('❌❌❌❌❌ Encountered fatal errors ❌❌❌❌❌');
88+
rootLogger.info(stringifyObject(reader.errors, 'yaml', 2));
89+
rootLogger.error('❌❌❌❌❌ Encountered fatal errors ❌❌❌❌❌');
6790
process.exit(1);
91+
} else {
92+
rootLogger.info('✅✅✅✅✅ No fatal errors ✅✅✅✅✅');
93+
const chainResults = Object.fromEntries(chainResultEntries);
94+
const resultsPath = `safe-tx-results-${Date.now()}.yaml`;
95+
writeYamlAtPath(resultsPath, chainResults);
96+
rootLogger.info(`Results written to ${resultsPath}`);
6897
}
6998
}
7099

71100
main().catch((err) => {
72-
console.error('Error:', err);
101+
rootLogger.error('Error:', err);
73102
process.exit(1);
74103
});

0 commit comments

Comments
 (0)