Skip to content

Commit

Permalink
feat: script to get+execute pending safe txs
Browse files Browse the repository at this point in the history
  • Loading branch information
paulbalaji committed Oct 17, 2024
1 parent b4d26dd commit d0773c5
Show file tree
Hide file tree
Showing 3 changed files with 223 additions and 9 deletions.
20 changes: 11 additions & 9 deletions typescript/infra/scripts/agent-utils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import path, { join } from 'path';
import yargs, { Argv } from 'yargs';

import {
ChainAddresses,
IRegistry,
warpConfigToWarpAddresses,
} from '@hyperlane-xyz/registry';
import { ChainAddresses, IRegistry } from '@hyperlane-xyz/registry';
import {
ChainMap,
ChainMetadata,
Expand Down Expand Up @@ -157,20 +153,26 @@ export function withChain<T>(args: Argv<T>) {
.alias('c', 'chain');
}

export function withChains<T>(args: Argv<T>) {
export function withChains<T>(args: Argv<T>, chainOptions?: ChainName[]) {
return (
args
.describe('chains', 'Set of chains to perform actions on.')
.array('chains')
.choices('chains', getChains())
.choices(
'chains',
!chainOptions || chainOptions.length === 0 ? getChains() : chainOptions,
)
// Ensure chains are unique
.coerce('chains', (chains: string[]) => Array.from(new Set(chains)))
.alias('c', 'chains')
);
}

export function withChainsRequired<T>(args: Argv<T>) {
return withChains(args).demandOption('chains');
export function withChainsRequired<T>(
args: Argv<T>,
chainOptions?: ChainName[],
) {
return withChains(args, chainOptions).demandOption('chains');
}

export function withWarpRouteId<T>(args: Argv<T>) {
Expand Down
178 changes: 178 additions & 0 deletions typescript/infra/scripts/safes/get-pending-txs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { confirm } from '@inquirer/prompts';
import chalk from 'chalk';
import yargs from 'yargs';

import { MultiProvider } from '@hyperlane-xyz/sdk';

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 { 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;
};

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;
}

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;
}

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,
});
},
);
}),
);
return txs.sort(
(a, b) => a.chain.localeCompare(b.chain) || a.nonce - b.nonce,
);
}

async function main() {
const safeChains = Object.keys(safes);
const { chains, fullTxHash, execute } = await withChains(
yargs(process.argv.slice(2)),
safeChains,
)
.describe(
'fullTxHash',
'If enabled, include the full tx hash in the output',
)
.boolean('fullTxHash')
.default('fullTxHash', false)
.describe(
'execute',
'If enabled, execute transactions that have enough confirmations',
)
.boolean('execute')
.default('execute', false).argv;

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

const envConfig = getEnvironmentConfig('mainnet3');
const multiProvider = await envConfig.getMultiProvider(
Contexts.Hyperlane,
Role.Deployer,
true,
chainsToCheck,
);

const pendingTxs = await getPendingTxsForChains(chainsToCheck, multiProvider);
console.table(pendingTxs, [
'chain',
'nonce',
'submissionDate',
fullTxHash ? 'fullTxHash' : 'shortTxHash',
'confs',
'threshold',
'status',
]);

const executableTxs = pendingTxs.filter(
(tx) => tx.status === SafeTxStatus.READY_TO_EXECUTE,
);
if (
executableTxs.length === 0 ||
!execute ||
!(await confirm({
message: 'Execute transactions?',
default: execute,
}))
) {
process.exit(0);
} else {
console.log(chalk.blueBright('Executing transactions'));
}

for (const tx of executableTxs) {
const confirmExecuteTx = await confirm({
message: `Execute transaction ${tx.shortTxHash} on chain ${tx.chain}?`,
default: execute,
});
if (confirmExecuteTx) {
console.log(
`Executing transaction ${tx.shortTxHash} on chain ${tx.chain}`,
);
await executeTx(tx.chain, multiProvider, safes[tx.chain], tx.fullTxHash);
}
}

process.exit(0);
}

main()
.then()
.catch((e) => {
console.error(e);
process.exit(1);
});
34 changes: 34 additions & 0 deletions typescript/infra/src/utils/safe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,40 @@ export function createSafeTransactionData(call: CallData): MetaTransactionData {
};
}

export async function executeTx(
chain: ChainNameOrId,
multiProvider: MultiProvider,
safeAddress: Address,
safeTxHash: string,
): Promise<void> {
const { safeSdk, safeService } = await getSafeAndService(
chain,
multiProvider,
safeAddress,
);
let safeTransaction;
try {
safeTransaction = await safeService.getTransaction(safeTxHash);
if (!safeTransaction) {
throw new Error(`Failed to fetch transaction details for ${safeTxHash}`);
}
} catch (error) {
console.error(chalk.red(`Error fetching transaction: ${error}`));
return;
}

try {
await safeSdk.executeTransaction(safeTransaction);
} catch (error) {
console.error(chalk.red(`Error executing transaction: ${error}`));
return;
}

console.log(
chalk.green.bold(`Executed transaction ${safeTxHash} on ${chain}`),
);
}

export async function createSafeTransaction(
safeSdk: Safe.default,
safeService: SafeApiKit.default,
Expand Down

0 comments on commit d0773c5

Please sign in to comment.