diff --git a/typescript/infra/scripts/agent-utils.ts b/typescript/infra/scripts/agent-utils.ts index 2b0a788b2b..260c51956b 100644 --- a/typescript/infra/scripts/agent-utils.ts +++ b/typescript/infra/scripts/agent-utils.ts @@ -286,15 +286,6 @@ export function withRpcUrls(args: Argv) { .alias('r', 'rpcUrls'); } -export function withTxHashes(args: Argv) { - 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) => ({ diff --git a/typescript/infra/scripts/safes/get-pending-txs.ts b/typescript/infra/scripts/safes/get-pending-txs.ts index 13d8c15d88..ead1b69c74 100644 --- a/typescript/infra/scripts/safes/get-pending-txs.ts +++ b/typescript/infra/scripts/safes/get-pending-txs.ts @@ -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 { - 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); @@ -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); } @@ -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', @@ -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); } @@ -171,7 +83,7 @@ async function main() { }); if (!shouldExecute) { - console.info( + rootLogger.info( chalk.blue( `${executableTxs.length} transactions available for execution`, ), @@ -179,7 +91,7 @@ async function main() { process.exit(0); } - console.info(chalk.blueBright('Executing transactions...')); + rootLogger.info(chalk.blueBright('Executing transactions...')); for (const tx of executableTxs) { const confirmExecuteTx = await confirm({ @@ -187,7 +99,7 @@ async function main() { default: false, }); if (confirmExecuteTx) { - console.log( + rootLogger.info( `Executing transaction ${tx.shortTxHash} on chain ${tx.chain}`, ); try { @@ -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; } } @@ -210,6 +122,6 @@ async function main() { main() .then() .catch((e) => { - console.error(e); + rootLogger.error(e); process.exit(1); }); diff --git a/typescript/infra/scripts/safes/parse-txs.ts b/typescript/infra/scripts/safes/parse-txs.ts index 21e8f87eed..6df936817d 100644 --- a/typescript/infra/scripts/safes/parse-txs.ts +++ b/typescript/infra/scripts/safes/parse-txs.ts @@ -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); @@ -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, @@ -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); }); diff --git a/typescript/infra/src/tx/govern-transaction-reader.ts b/typescript/infra/src/tx/govern-transaction-reader.ts index ef82ab1e1a..7e21335a17 100644 --- a/typescript/infra/src/tx/govern-transaction-reader.ts +++ b/typescript/infra/src/tx/govern-transaction-reader.ts @@ -8,7 +8,12 @@ import assert from 'assert'; import chalk from 'chalk'; import { BigNumber, ethers } from 'ethers'; -import { ProxyAdmin__factory, TokenRouter__factory } from '@hyperlane-xyz/core'; +import { + Ownable__factory, + ProxyAdmin__factory, + TimelockController__factory, + TokenRouter__factory, +} from '@hyperlane-xyz/core'; import { AnnotatedEV5Transaction, ChainMap, @@ -35,6 +40,7 @@ import { icaOwnerChain, icas, safes, + timelocks, } from '../../config/environments/mainnet3/owners.js'; import { DeployEnvironment } from '../config/environment.js'; import { getSafeAndService } from '../utils/safe.js'; @@ -128,6 +134,11 @@ export class GovernTransactionReader { return this.readProxyAdminTransaction(chain, tx); } + // If it's to a TimelockController + if (this.isTimelockControllerTransaction(chain, tx)) { + return this.readTimelockControllerTransaction(chain, tx); + } + // If it's a Multisend if (await this.isMultisendTransaction(chain, tx)) { return this.readMultisendTransaction(chain, tx); @@ -138,6 +149,11 @@ export class GovernTransactionReader { return this.readWarpModuleTransaction(chain, tx); } + // If it's an Ownable transaction + if (await this.isOwnableTransaction(chain, tx)) { + return this.readOwnableTransaction(chain, tx); + } + const insight = '⚠️ Unknown transaction type'; // If we get here, it's an unknown transaction this.errors.push({ @@ -153,6 +169,73 @@ export class GovernTransactionReader { }; } + private isTimelockControllerTransaction( + chain: ChainName, + tx: AnnotatedEV5Transaction, + ): boolean { + return ( + tx.to !== undefined && + timelocks[chain] !== undefined && + eqAddress(tx.to!, timelocks[chain]!) + ); + } + + private async readTimelockControllerTransaction( + chain: ChainName, + tx: AnnotatedEV5Transaction, + ): Promise { + if (!tx.data) { + throw new Error('No data in TimelockController transaction'); + } + + const timelockControllerInterface = + TimelockController__factory.createInterface(); + const decoded = timelockControllerInterface.parseTransaction({ + data: tx.data, + value: tx.value, + }); + + let insight; + if ( + decoded.functionFragment.name === + timelockControllerInterface.functions[ + 'schedule(address,uint256,bytes,bytes32,bytes32,uint256)' + ].name + ) { + const [target, value, data, eta, executor, delay] = decoded.args; + insight = `Schedule ${target} to be executed at ${eta} with ${value} ${data}. Executor: ${executor}, Delay: ${delay}`; + } + + if ( + decoded.functionFragment.name === + timelockControllerInterface.functions[ + 'execute(address,uint256,bytes,bytes32,bytes32)' + ].name + ) { + const [target, value, data, executor] = decoded.args; + insight = `Execute ${target} with ${value} ${data}. Executor: ${executor}`; + } + + if ( + decoded.functionFragment.name === + timelockControllerInterface.functions['cancel(bytes32)'].name + ) { + const [id] = decoded.args; + insight = `Cancel scheduled transaction ${id}`; + } + + const args = formatFunctionFragmentArgs( + decoded.args, + decoded.functionFragment, + ); + + return { + chain, + to: `Timelock Controller (${chain} ${tx.to})`, + ...(insight ? { insight } : { args }), + }; + } + private isWarpModuleTransaction( chain: ChainName, tx: AnnotatedEV5Transaction, @@ -180,13 +263,7 @@ export class GovernTransactionReader { value: tx.value, }); - const args = formatFunctionFragmentArgs( - decoded.args, - decoded.functionFragment, - ); - - let insight = ''; - + let insight; if ( decoded.functionFragment.name === tokenRouterInterface.functions['setHook(address)'].name @@ -255,16 +332,22 @@ export class GovernTransactionReader { insight = `Unenroll remote routers for ${insights.join(', ')}`; } + let ownableTx = {}; + if (!insight) { + ownableTx = await this.readOwnableTransaction(chain, tx); + } + assert(tx.to, 'Warp Module transaction must have a to address'); - const token = this.warpRouteIndex[chain][tx.to.toLowerCase()]; + const tokenAddress = tx.to.toLowerCase(); + const token = this.warpRouteIndex[chain][tokenAddress]; return { + ...ownableTx, chain, - to: `${token.symbol} (${token.name}, ${token.standard})`, + to: `${token.symbol} (${token.name}, ${token.standard}, ${tokenAddress})`, insight, value: `${ethers.utils.formatEther(decoded.value)} ${symbol}`, signature: decoded.signature, - args, }; } @@ -401,26 +484,11 @@ export class GovernTransactionReader { value: tx.value, }); - const args = formatFunctionFragmentArgs( - decoded.args, - decoded.functionFragment, - ); - - let insight; - if ( - decoded.functionFragment.name === - proxyAdminInterface.functions['transferOwnership(address)'].name - ) { - const [newOwner] = decoded.args; - insight = `Transfer ownership to ${newOwner}`; - } - + const ownableTx = await this.readOwnableTransaction(chain, tx); return { - chain, + ...ownableTx, to: `Proxy Admin (${chain} ${this.chainAddresses[chain].proxyAdmin})`, - insight, signature: decoded.signature, - args, }; } @@ -629,6 +697,49 @@ export class GovernTransactionReader { }; } + private async readOwnableTransaction( + chain: ChainName, + tx: AnnotatedEV5Transaction, + ): Promise { + if (!tx.data) { + throw new Error('⚠️ No data in Ownable transaction'); + } + + const ownableInterface = Ownable__factory.createInterface(); + const decoded = ownableInterface.parseTransaction({ + data: tx.data, + value: tx.value, + }); + + let insight; + if ( + decoded.functionFragment.name === + ownableInterface.functions['renounceOwnership()'].name + ) { + insight = `Renounce ownership`; + } + + if ( + decoded.functionFragment.name === + ownableInterface.functions['transferOwnership(address)'].name + ) { + const [newOwner] = decoded.args; + insight = `Transfer ownership to ${newOwner}`; + } + + const args = formatFunctionFragmentArgs( + decoded.args, + decoded.functionFragment, + ); + + return { + chain, + to: `Ownable (${chain} ${tx.to})`, + ...(insight ? { insight } : { args }), + signature: decoded.signature, + }; + } + isIcaTransaction(chain: ChainName, tx: AnnotatedEV5Transaction): boolean { return ( tx.to !== undefined && @@ -670,6 +781,23 @@ export class GovernTransactionReader { return eqAddress(multiSendCallOnlyAddress, tx.to); } + async isOwnableTransaction( + chain: ChainName, + tx: AnnotatedEV5Transaction, + ): Promise { + if (!tx.to) return false; + try { + const account = Ownable__factory.connect( + tx.to, + this.multiProvider.getProvider(chain), + ); + await account.owner(); + return true; + } catch { + return false; + } + } + private multiSendCallOnlyAddressCache: ChainMap = {}; async getMultiSendCallOnlyAddress( diff --git a/typescript/infra/src/utils/safe.ts b/typescript/infra/src/utils/safe.ts index 95e27c50e3..c85a4f6043 100644 --- a/typescript/infra/src/utils/safe.ts +++ b/typescript/infra/src/utils/safe.ts @@ -7,6 +7,7 @@ import { } from '@safe-global/safe-core-sdk-types'; import chalk from 'chalk'; import { BigNumber, ethers } from 'ethers'; +import { formatUnits } from 'ethers/lib/utils.js'; import { ChainNameOrId, @@ -14,9 +15,16 @@ import { getSafe, getSafeService, } from '@hyperlane-xyz/sdk'; -import { Address, CallData, eqAddress, retryAsync } from '@hyperlane-xyz/utils'; +import { + Address, + CallData, + eqAddress, + retryAsync, + rootLogger, +} from '@hyperlane-xyz/utils'; import safeSigners from '../../config/environments/mainnet3/safe/safeSigners.json' assert { type: 'json' }; +// eslint-disable-next-line import/no-cycle import { AnnotatedCallData } from '../govern/HyperlaneAppGovernor.js'; export async function getSafeAndService( @@ -82,7 +90,7 @@ export async function executeTx( await safeSdk.executeTransaction(safeTransaction); - console.log( + rootLogger.info( chalk.green.bold(`Executed transaction ${safeTxHash} on ${chain}`), ); } @@ -122,7 +130,7 @@ export async function proposeSafeTransaction( senderSignature: senderSignature.data, }); - console.log( + rootLogger.info( chalk.green(`Proposed transaction on ${chain} with hash ${safeTxHash}`), ); } @@ -143,7 +151,7 @@ export async function deleteAllPendingSafeTxs( }); if (!pendingTxsResponse.ok) { - console.error( + rootLogger.error( chalk.red(`Failed to fetch pending transactions for ${safeAddress}`), ); return; @@ -156,7 +164,7 @@ export async function deleteAllPendingSafeTxs( await deleteSafeTx(chain, multiProvider, safeAddress, tx.safeTxHash); } - console.log( + rootLogger.info( `Deleted all pending transactions on ${chain} for ${safeAddress}\n`, ); } @@ -177,7 +185,7 @@ export async function getSafeTx( }); if (!txDetailsResponse.ok) { - console.error( + rootLogger.error( chalk.red(`Failed to fetch transaction details for ${safeTxHash}`), ); return; @@ -205,7 +213,7 @@ export async function deleteSafeTx( }); if (!txDetailsResponse.ok) { - console.error( + rootLogger.error( chalk.red(`Failed to fetch transaction details for ${safeTxHash}`), ); return; @@ -215,21 +223,23 @@ export async function deleteSafeTx( const proposer = txDetails.proposer; if (!proposer) { - console.error(chalk.red(`No proposer found for transaction ${safeTxHash}`)); + rootLogger.error( + chalk.red(`No proposer found for transaction ${safeTxHash}`), + ); return; } // Compare proposer to signer const signerAddress = await signer.getAddress(); - if (proposer !== signerAddress) { - console.log( + if (!eqAddress(proposer, signerAddress)) { + rootLogger.info( chalk.italic( `Skipping deletion of transaction ${safeTxHash} proposed by ${proposer}`, ), ); return; } - console.log(`Deleting transaction ${safeTxHash} proposed by ${proposer}`); + rootLogger.info(`Deleting transaction ${safeTxHash} proposed by ${proposer}`); try { // Generate the EIP-712 signature @@ -275,7 +285,7 @@ export async function deleteSafeTx( }); if (res.status === 204) { - console.log( + rootLogger.info( chalk.green( `Successfully deleted transaction ${safeTxHash} on ${chain}`, ), @@ -284,13 +294,13 @@ export async function deleteSafeTx( } const errorBody = await res.text(); - console.error( + rootLogger.error( chalk.red( `Failed to delete transaction ${safeTxHash} on ${chain}: Status ${res.status} ${res.statusText}. Response body: ${errorBody}`, ), ); } catch (error) { - console.error( + rootLogger.error( chalk.red(`Failed to delete transaction ${safeTxHash} on ${chain}:`), error, ); @@ -310,8 +320,8 @@ export async function updateSafeOwner( (newOwner) => !owners.some((owner) => eqAddress(newOwner, owner)), ); - console.log(chalk.magentaBright('Owners to remove:', ownersToRemove)); - console.log(chalk.magentaBright('Owners to add:', ownersToAdd)); + rootLogger.info(chalk.magentaBright('Owners to remove:', ownersToRemove)); + rootLogger.info(chalk.magentaBright('Owners to add:', ownersToAdd)); const transactions: AnnotatedCallData[] = []; @@ -343,3 +353,104 @@ export async function updateSafeOwner( return transactions; } + +type SafeStatus = { + chain: string; + nonce: number; + submissionDate: string; + shortTxHash: string; + fullTxHash: string; + confs: number; + threshold: number; + status: string; + balance: string; +}; + +export enum SafeTxStatus { + NO_CONFIRMATIONS = '🔴', + PENDING = '🟡', + ONE_AWAY = '🔵', + READY_TO_EXECUTE = '🟢', +} + +export async function getPendingTxsForChains( + chains: string[], + multiProvider: MultiProvider, + safes: Record, +): Promise { + const txs: SafeStatus[] = []; + await Promise.all( + chains.map(async (chain) => { + if (!safes[chain]) { + rootLogger.error(chalk.red.bold(`No safe found for ${chain}`)); + return; + } + + if (chain === 'endurance') { + rootLogger.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) { + rootLogger.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, + ); +} diff --git a/typescript/sdk/package.json b/typescript/sdk/package.json index 9c0b130684..aef1b75a55 100644 --- a/typescript/sdk/package.json +++ b/typescript/sdk/package.json @@ -12,7 +12,7 @@ "@hyperlane-xyz/utils": "8.2.0", "@safe-global/api-kit": "1.3.0", "@safe-global/protocol-kit": "1.3.0", - "@safe-global/safe-deployments": "1.37.8", + "@safe-global/safe-deployments": "1.37.23", "@solana/spl-token": "^0.4.9", "@solana/web3.js": "^1.95.4", "bignumber.js": "^9.1.1", diff --git a/typescript/sdk/src/utils/gnosisSafe.js b/typescript/sdk/src/utils/gnosisSafe.js index 51235e2121..90f360821a 100644 --- a/typescript/sdk/src/utils/gnosisSafe.js +++ b/typescript/sdk/src/utils/gnosisSafe.js @@ -43,6 +43,16 @@ const safeDeploymentsVersions = { }, }; +// Override for chains that haven't yet been published in the safe-deployments package. +// Temporary until PR to safe-deployments package is merged and SDK dependency is updated. +const chainOverrides = { + // zeronetwork + 543210: { + multiSend: '0x0dFcccB95225ffB03c6FBB2559B530C2B7C8A912', + multiSendCallOnly: '0xf220D3b4DFb23C4ade8C88E526C1353AbAcbC38F', + }, +}; + export async function getSafe(chain, multiProvider, safeAddress) { // Create Ethers Adapter const signer = multiProvider.getSigner(chain); @@ -61,7 +71,16 @@ export async function getSafe(chain, multiProvider, safeAddress) { // Get the multiSend and multiSendCallOnly deployments for the given chain let multiSend, multiSendCallOnly; - if (safeDeploymentsVersions[safeVersion]) { + if (chainOverrides[chainId]) { + multiSend = { + networkAddresses: { [chainId]: chainOverrides[chainId].multiSend }, + }; + multiSendCallOnly = { + networkAddresses: { + [chainId]: chainOverrides[chainId].multiSendCallOnly, + }, + }; + } else if (safeDeploymentsVersions[safeVersion]) { const { multiSendVersion, multiSendCallOnlyVersion } = safeDeploymentsVersions[safeVersion]; multiSend = getMultiSendDeployment({ diff --git a/yarn.lock b/yarn.lock index cf72bf2284..5b8d3be79a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7563,7 +7563,7 @@ __metadata: "@nomiclabs/hardhat-waffle": "npm:^2.0.6" "@safe-global/api-kit": "npm:1.3.0" "@safe-global/protocol-kit": "npm:1.3.0" - "@safe-global/safe-deployments": "npm:1.37.8" + "@safe-global/safe-deployments": "npm:1.37.23" "@solana/spl-token": "npm:^0.4.9" "@solana/web3.js": "npm:^1.95.4" "@types/mocha": "npm:^10.0.1" @@ -12754,12 +12754,12 @@ __metadata: languageName: node linkType: hard -"@safe-global/safe-deployments@npm:1.37.8": - version: 1.37.8 - resolution: "@safe-global/safe-deployments@npm:1.37.8" +"@safe-global/safe-deployments@npm:1.37.23": + version: 1.37.23 + resolution: "@safe-global/safe-deployments@npm:1.37.23" dependencies: semver: "npm:^7.6.2" - checksum: 10/bc8fce2c4d557e547a6cceebb611f9584d998dfb459cd50cf338409de986bed247ebca9425b0984a6e1a6accab42c7c4d1c68811e09cc981756183ba50a5e5a9 + checksum: 10/90fca085c94fdeed7d2112699dfe58e1b1178358ccaf98049fd1fdd52be78de261753d2abc7351d6a9f977e400706d960ca6ad3f66413847a7b268999be0eff0 languageName: node linkType: hard