Skip to content

Commit

Permalink
feat: infra safe tx parser auto-infers txs to parse (#5183)
Browse files Browse the repository at this point in the history
### 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]>
  • Loading branch information
paulbalaji authored Jan 21, 2025
1 parent 7275828 commit b98902f
Show file tree
Hide file tree
Showing 8 changed files with 383 additions and 193 deletions.
9 changes: 0 additions & 9 deletions typescript/infra/scripts/agent-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,15 +286,6 @@ export function withRpcUrls<T>(args: Argv<T>) {
.alias('r', 'rpcUrls');
}

export function withTxHashes<T>(args: Argv<T>) {
return args
.describe('txHashes', 'transaction hash')
.string('txHashes')
.array('txHashes')
.demandOption('txHashes')
.alias('t', 'txHashes');
}

// Interactively gets a single warp route ID
export async function getWarpRouteIdInteractive() {
const choices = Object.values(WarpRouteIds).map((id) => ({
Expand Down
138 changes: 25 additions & 113 deletions typescript/infra/scripts/safes/get-pending-txs.ts
Original file line number Diff line number Diff line change
@@ -1,118 +1,25 @@
import { confirm } from '@inquirer/prompts';
import chalk from 'chalk';
import { formatUnits } from 'ethers/lib/utils.js';
import yargs from 'yargs';

import { MultiProvider } from '@hyperlane-xyz/sdk';
import { LogFormat, LogLevel, configureRootLogger } from '@hyperlane-xyz/utils';
import {
LogFormat,
LogLevel,
configureRootLogger,
rootLogger,
} from '@hyperlane-xyz/utils';

import { Contexts } from '../../config/contexts.js';
import { safes } from '../../config/environments/mainnet3/owners.js';
import { Role } from '../../src/roles.js';
import { executeTx, getSafeAndService } from '../../src/utils/safe.js';
import {
SafeTxStatus,
executeTx,
getPendingTxsForChains,
} from '../../src/utils/safe.js';
import { withChains } from '../agent-utils.js';
import { getEnvironmentConfig } from '../core-utils.js';

export enum SafeTxStatus {
NO_CONFIRMATIONS = '🔴',
PENDING = '🟡',
ONE_AWAY = '🔵',
READY_TO_EXECUTE = '🟢',
}

type SafeStatus = {
chain: string;
nonce: number;
submissionDate: string;
shortTxHash: string;
fullTxHash: string;
confs: number;
threshold: number;
status: string;
balance: string;
};

export async function getPendingTxsForChains(
chains: string[],
multiProvider: MultiProvider,
): Promise<SafeStatus[]> {
const txs: SafeStatus[] = [];
await Promise.all(
chains.map(async (chain) => {
if (!safes[chain]) {
console.error(chalk.red.bold(`No safe found for ${chain}`));
return;
}

if (chain === 'endurance') {
console.info(
chalk.gray.italic(
`Skipping chain ${chain} as it does not have a functional safe API`,
),
);
return;
}

let safeSdk, safeService;
try {
({ safeSdk, safeService } = await getSafeAndService(
chain,
multiProvider,
safes[chain],
));
} catch (error) {
console.warn(
chalk.yellow(
`Skipping chain ${chain} as there was an error getting the safe service: ${error}`,
),
);
return;
}

const threshold = await safeSdk.getThreshold();
const pendingTxs = await safeService.getPendingTransactions(safes[chain]);
if (pendingTxs.results.length === 0) {
return;
}

const balance = await safeSdk.getBalance();
const nativeToken = await multiProvider.getNativeToken(chain);
const formattedBalance = formatUnits(balance, nativeToken.decimals);

pendingTxs.results.forEach(
({ nonce, submissionDate, safeTxHash, confirmations }) => {
const confs = confirmations?.length ?? 0;
const status =
confs >= threshold
? SafeTxStatus.READY_TO_EXECUTE
: confs === 0
? SafeTxStatus.NO_CONFIRMATIONS
: threshold - confs
? SafeTxStatus.ONE_AWAY
: SafeTxStatus.PENDING;

txs.push({
chain,
nonce,
submissionDate: new Date(submissionDate).toDateString(),
shortTxHash: `${safeTxHash.slice(0, 6)}...${safeTxHash.slice(-4)}`,
fullTxHash: safeTxHash,
confs,
threshold,
status,
balance: `${Number(formattedBalance).toFixed(5)} ${
nativeToken.symbol
}`,
});
},
);
}),
);
return txs.sort(
(a, b) => a.chain.localeCompare(b.chain) || a.nonce - b.nonce,
);
}

async function main() {
const safeChains = Object.keys(safes);
configureRootLogger(LogFormat.Pretty, LogLevel.Info);
Expand All @@ -129,7 +36,7 @@ async function main() {

const chainsToCheck = chains || safeChains;
if (chainsToCheck.length === 0) {
console.error('No chains provided');
rootLogger.error('No chains provided');
process.exit(1);
}

Expand All @@ -141,11 +48,16 @@ async function main() {
chainsToCheck,
);

const pendingTxs = await getPendingTxsForChains(chainsToCheck, multiProvider);
const pendingTxs = await getPendingTxsForChains(
chainsToCheck,
multiProvider,
safes,
);
if (pendingTxs.length === 0) {
console.info(chalk.green('No pending transactions found!'));
rootLogger.info(chalk.green('No pending transactions found!'));
process.exit(0);
}
// eslint-disable-next-line no-console
console.table(pendingTxs, [
'chain',
'nonce',
Expand All @@ -161,7 +73,7 @@ async function main() {
(tx) => tx.status === SafeTxStatus.READY_TO_EXECUTE,
);
if (executableTxs.length === 0) {
console.info(chalk.green('No transactions to execute!'));
rootLogger.info(chalk.green('No transactions to execute!'));
process.exit(0);
}

Expand All @@ -171,23 +83,23 @@ async function main() {
});

if (!shouldExecute) {
console.info(
rootLogger.info(
chalk.blue(
`${executableTxs.length} transactions available for execution`,
),
);
process.exit(0);
}

console.info(chalk.blueBright('Executing transactions...'));
rootLogger.info(chalk.blueBright('Executing transactions...'));

for (const tx of executableTxs) {
const confirmExecuteTx = await confirm({
message: `Execute transaction ${tx.shortTxHash} on chain ${tx.chain}?`,
default: false,
});
if (confirmExecuteTx) {
console.log(
rootLogger.info(
`Executing transaction ${tx.shortTxHash} on chain ${tx.chain}`,
);
try {
Expand All @@ -198,7 +110,7 @@ async function main() {
tx.fullTxHash,
);
} catch (error) {
console.error(chalk.red(`Error executing transaction: ${error}`));
rootLogger.error(chalk.red(`Error executing transaction: ${error}`));
return;
}
}
Expand All @@ -210,6 +122,6 @@ async function main() {
main()
.then()
.catch((e) => {
console.error(e);
rootLogger.error(e);
process.exit(1);
});
69 changes: 49 additions & 20 deletions typescript/infra/scripts/safes/parse-txs.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
import chalk from 'chalk';
import { BigNumber } from 'ethers';
import yargs from 'yargs';

import { AnnotatedEV5Transaction } from '@hyperlane-xyz/sdk';
import {
LogFormat,
LogLevel,
configureRootLogger,
rootLogger,
stringifyObject,
} from '@hyperlane-xyz/utils';

import { safes } from '../../config/environments/mainnet3/owners.js';
import { GovernTransactionReader } from '../../src/tx/govern-transaction-reader.js';
import { getSafeTx } from '../../src/utils/safe.js';
import { getArgs, withChainsRequired, withTxHashes } from '../agent-utils.js';
import { getPendingTxsForChains, getSafeTx } from '../../src/utils/safe.js';
import { writeYamlAtPath } from '../../src/utils/utils.js';
import { withChains } from '../agent-utils.js';
import { getEnvironmentConfig, getHyperlaneCore } from '../core-utils.js';

async function main() {
const { environment, chains, txHashes } = await withTxHashes(
withChainsRequired(getArgs()),
).argv;
const environment = 'mainnet3';
const safeChains = Object.keys(safes);

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

const config = getEnvironmentConfig(environment);
Expand All @@ -35,11 +41,31 @@ async function main() {
warpRoutes,
);

const pendingTxs = await getPendingTxsForChains(
!chains || chains.length === 0 ? safeChains : chains,
multiProvider,
safes,
);
if (pendingTxs.length === 0) {
rootLogger.info(chalk.green('No pending transactions found!'));
process.exit(0);
}
// eslint-disable-next-line no-console
console.table(pendingTxs, [
'chain',
'nonce',
'submissionDate',
'fullTxHash',
'confs',
'threshold',
'status',
'balance',
]);

const chainResultEntries = await Promise.all(
chains.map(async (chain, chainIndex) => {
const txHash = txHashes[chainIndex];
console.log(`Reading tx ${txHash} on ${chain}`);
const safeTx = await getSafeTx(chain, multiProvider, txHash);
pendingTxs.map(async ({ chain, fullTxHash }) => {
rootLogger.info(`Reading tx ${fullTxHash} on ${chain}`);
const safeTx = await getSafeTx(chain, multiProvider, fullTxHash);
const tx: AnnotatedEV5Transaction = {
to: safeTx.to,
data: safeTx.data,
Expand All @@ -48,27 +74,30 @@ async function main() {

try {
const results = await reader.read(chain, tx);
console.log(`Finished reading tx ${txHash} on ${chain}`);
return [chain, results];
rootLogger.info(`Finished reading tx ${fullTxHash} on ${chain}`);
return [`${chain}-${fullTxHash}`, results];
} catch (err) {
console.error('Error reading transaction', err, chain, tx);
rootLogger.error('Error reading transaction', err, chain, tx);
process.exit(1);
}
}),
);

const chainResults = Object.fromEntries(chainResultEntries);
console.log(stringifyObject(chainResults, 'yaml', 2));

if (reader.errors.length) {
console.error('❌❌❌❌❌ Encountered fatal errors ❌❌❌❌❌');
console.log(stringifyObject(reader.errors, 'yaml', 2));
console.error('❌❌❌❌❌ Encountered fatal errors ❌❌❌❌❌');
rootLogger.error('❌❌❌❌❌ Encountered fatal errors ❌❌❌❌❌');
rootLogger.info(stringifyObject(reader.errors, 'yaml', 2));
rootLogger.error('❌❌❌❌❌ Encountered fatal errors ❌❌❌❌❌');
process.exit(1);
} else {
rootLogger.info('✅✅✅✅✅ No fatal errors ✅✅✅✅✅');
const chainResults = Object.fromEntries(chainResultEntries);
const resultsPath = `safe-tx-results-${Date.now()}.yaml`;
writeYamlAtPath(resultsPath, chainResults);
rootLogger.info(`Results written to ${resultsPath}`);
}
}

main().catch((err) => {
console.error('Error:', err);
rootLogger.error('Error:', err);
process.exit(1);
});
Loading

0 comments on commit b98902f

Please sign in to comment.