wip, going pretty well

trevor/read-txs-nov-8
Trevor Porter 1 month ago
parent ec667cb8e8
commit 0914a2d35e
  1. 1
      typescript/infra/scripts/safes/example-data.json
  2. 16
      typescript/infra/scripts/safes/parse-multicall.ts
  3. 205
      typescript/infra/src/tx/transaction-reader.ts

File diff suppressed because one or more lines are too long

@ -1,8 +1,8 @@
import { stringifyObject, strip0x } from '@hyperlane-xyz/utils'; import { stringifyObject, strip0x } from '@hyperlane-xyz/utils';
import { GnosisMultisendReader } from '../../src/tx/transaction-reader.js'; import { TransactionReader } from '../../src/tx/transaction-reader.js';
import { getArgs } from '../agent-utils.js'; import { getArgs } from '../agent-utils.js';
import { getEnvironmentConfig } from '../core-utils.js'; import { getEnvironmentConfig, getHyperlaneCore } from '../core-utils.js';
import tx from './example-data.json'; import tx from './example-data.json';
@ -10,9 +10,17 @@ async function main() {
const { environment } = await getArgs().argv; const { environment } = await getArgs().argv;
const config = getEnvironmentConfig(environment); const config = getEnvironmentConfig(environment);
const multiProvider = await config.getMultiProvider(); const multiProvider = await config.getMultiProvider();
const { chainAddresses } = await getHyperlaneCore(environment, multiProvider);
const multisendReader = new GnosisMultisendReader(multiProvider); const reader = new TransactionReader(
// const results = await multisendReader.read(tx); environment,
multiProvider,
'ethereum',
chainAddresses,
);
const results = await reader.read('ethereum', tx);
console.log('results', results);
console.log(stringifyObject(results, 'yaml', 2)); console.log(stringifyObject(results, 'yaml', 2));
} }

@ -1,4 +1,10 @@
import { Result } from '@ethersproject/abi';
import { decodeMultiSendData } from '@safe-global/protocol-kit/dist/src/utils/index.js'; import { decodeMultiSendData } from '@safe-global/protocol-kit/dist/src/utils/index.js';
import {
MetaTransactionData,
OperationType,
} from '@safe-global/safe-core-sdk-types';
import { BigNumber, ethers } from 'ethers';
import { import {
AnnotatedEV5Transaction, AnnotatedEV5Transaction,
@ -6,63 +12,214 @@ import {
ChainName, ChainName,
HyperlaneReader, HyperlaneReader,
MultiProvider, MultiProvider,
interchainAccountFactories,
} from '@hyperlane-xyz/sdk'; } from '@hyperlane-xyz/sdk';
import { eqAddress } from '@hyperlane-xyz/utils'; import {
addressToBytes32,
bytes32ToAddress,
eqAddress,
} from '@hyperlane-xyz/utils';
import { getHyperlaneCore } from '../../scripts/core-utils.js';
import { DeployEnvironment } from '../config/environment.js'; import { DeployEnvironment } from '../config/environment.js';
import { getSafeAndService } from '../utils/safe.js';
export abstract class TransactionReader { // export abstract class TransactionReader {
async read(chain: ChainName, tx: any): Promise<any> { // async read(chain: ChainName, tx: any): Promise<any> {
throw new Error('Not implemented'); // throw new Error('Not implemented');
} // }
} // }
export class GnosisMultisendReader extends TransactionReader { // export class GnosisMultisendReader extends TransactionReader {
constructor(multiProvider: MultiProvider) { // constructor(multiProvider: MultiProvider) {
super(); // super();
} // }
async read(chain: ChainName, tx: AnnotatedEV5Transaction): Promise<any> { // async read(chain: ChainName, tx: AnnotatedEV5Transaction): Promise<any> {
if (!tx.data) { // if (!tx.data) {
return undefined; // return undefined;
} // }
const multisends = decodeMultiSendData(tx.data); // const multisends = decodeMultiSendData(tx.data);
return multisends; // return multisends;
} // }
} // }
export class GenericTransactionReader extends HyperlaneReader { export class TransactionReader extends HyperlaneReader {
constructor( constructor(
readonly environment: DeployEnvironment, readonly environment: DeployEnvironment,
readonly multiProvider: MultiProvider, readonly multiProvider: MultiProvider,
readonly chain: ChainName,
readonly chainAddresses: ChainMap<Record<string, string>>, readonly chainAddresses: ChainMap<Record<string, string>>,
) { ) {
super(); super(multiProvider, chain);
} }
async read(chain: ChainName, tx: AnnotatedEV5Transaction): Promise<any> { async read(chain: ChainName, tx: AnnotatedEV5Transaction): Promise<any> {
try {
return await this.doRead(chain, tx);
} catch (e) {
console.error('Error reading transaction', e, chain, tx);
throw e;
}
}
async doRead(chain: ChainName, tx: AnnotatedEV5Transaction): Promise<any> {
// If it's an ICA // If it's an ICA
if (this.isIcaTransaction(chain, tx)) { if (this.isIcaTransaction(chain, tx)) {
return this.readIcaTransaction(chain, tx); return this.readIcaTransaction(chain, tx);
} }
if (await this.isMultisendTransaction(chain, tx)) {
return this.readMultisendTransaction(chain, tx);
}
return {};
} }
private async readIcaTransaction( private async readIcaTransaction(
chain: ChainName, chain: ChainName,
tx: AnnotatedEV5Transaction, tx: AnnotatedEV5Transaction,
): Promise<any> { ): Promise<any> {
// TODO if (!tx.data) {
console.log('No data in ICA transaction');
return undefined;
}
const { symbol } = await this.multiProvider.getNativeToken(chain);
const decoded =
interchainAccountFactories.interchainAccountRouter.interface.parseTransaction(
{
data: tx.data,
value: tx.value,
},
);
const args = formatFunctionFragmentArgs(
decoded.args,
decoded.functionFragment,
);
let prettyArgs = args;
if (decoded.functionFragment.name === 'enrollRemoteRouters') {
prettyArgs = await this.formatRouterEnrollments(
chain,
'interchainAccountRouter',
args,
);
}
return {
value: `${ethers.utils.formatEther(decoded.value)} ${symbol}`,
signature: decoded.signature,
args: prettyArgs,
};
} }
private isIcaTransaction( private async formatRouterEnrollments(
chain: ChainName,
routerName: string,
args: Record<string, any>,
): Promise<any> {
const { _domains: domains, _addresses: addresses } = args;
return domains.map((domain: number, index: number) => {
const remoteChainName = this.multiProvider.getChainName(domain);
const expectedRouter = this.chainAddresses[remoteChainName][routerName];
const routerToBeEnrolled = addresses[index];
const matchesExpectedRouter =
eqAddress(expectedRouter, bytes32ToAddress(routerToBeEnrolled)) &&
// Poor man's check that the 12 byte padding is all zeroes
addressToBytes32(bytes32ToAddress(routerToBeEnrolled)) ===
routerToBeEnrolled;
return {
domain: domain,
chainName: remoteChainName,
router: routerToBeEnrolled,
'good?': matchesExpectedRouter
? '✅ matches expected router from artifacts'
: `❌ fatal mismatch, expected ${expectedRouter}`,
};
});
}
private async readMultisendTransaction(
chain: ChainName, chain: ChainName,
tx: AnnotatedEV5Transaction, tx: AnnotatedEV5Transaction,
): boolean { ): Promise<any> {
if (!tx.data) {
console.log('No data in multisend transaction');
return undefined;
}
const multisends = decodeMultiSendData(tx.data);
const { symbol } = await this.multiProvider.getNativeToken(chain);
return Promise.all(
multisends.map(async (multisend, index) => {
const decoded = await this.read(
chain,
metaTransactionDataToEV5Transaction(multisend),
);
return {
index,
value: `${ethers.utils.formatEther(multisend.value)} ${symbol}`,
operation: formatOperationType(multisend.operation),
decoded,
};
}),
);
}
isIcaTransaction(chain: ChainName, tx: AnnotatedEV5Transaction): boolean {
return ( return (
tx.to !== undefined && tx.to !== undefined &&
eqAddress(tx.to, this.chainAddresses[chain].interchainAccountRouter) eqAddress(tx.to, this.chainAddresses[chain].interchainAccountRouter)
); );
} }
async isMultisendTransaction(
chain: ChainName,
tx: AnnotatedEV5Transaction,
): Promise<boolean> {
if (tx.to === undefined) {
return false;
}
const { safeSdk } = await getSafeAndService(
this.chain,
this.multiProvider,
'0x3965AC3D295641E452E0ea896a086A9cD7C6C5b6',
);
// why call only? if we do delegatecall
return eqAddress(safeSdk.getMultiSendCallOnlyAddress(), tx.to);
}
}
function metaTransactionDataToEV5Transaction(
metaTransactionData: MetaTransactionData,
): AnnotatedEV5Transaction {
return {
to: metaTransactionData.to,
value: BigNumber.from(metaTransactionData.value),
data: metaTransactionData.data,
};
}
function formatFunctionFragmentArgs(
args: Result,
fragment: ethers.utils.FunctionFragment,
): Record<string, string> {
const accumulator: Record<string, string> = {};
return fragment.inputs.reduce((acc, input, index) => {
acc[input.name] = args[index];
return acc;
}, accumulator);
}
function formatOperationType(operation: OperationType | undefined): string {
switch (operation) {
case OperationType.Call:
return 'Call';
case OperationType.DelegateCall:
return 'Delegate Call';
default:
return '⚠ Unknown ⚠';
}
} }

Loading…
Cancel
Save