wip, going pretty well

trevor/read-txs-nov-8
Trevor Porter 4 weeks 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 { GnosisMultisendReader } from '../../src/tx/transaction-reader.js';
import { TransactionReader } from '../../src/tx/transaction-reader.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';
@ -10,9 +10,17 @@ async function main() {
const { environment } = await getArgs().argv;
const config = getEnvironmentConfig(environment);
const multiProvider = await config.getMultiProvider();
const { chainAddresses } = await getHyperlaneCore(environment, multiProvider);
const multisendReader = new GnosisMultisendReader(multiProvider);
// const results = await multisendReader.read(tx);
const reader = new TransactionReader(
environment,
multiProvider,
'ethereum',
chainAddresses,
);
const results = await reader.read('ethereum', tx);
console.log('results', results);
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 {
MetaTransactionData,
OperationType,
} from '@safe-global/safe-core-sdk-types';
import { BigNumber, ethers } from 'ethers';
import {
AnnotatedEV5Transaction,
@ -6,63 +12,214 @@ import {
ChainName,
HyperlaneReader,
MultiProvider,
interchainAccountFactories,
} 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 { getSafeAndService } from '../utils/safe.js';
export abstract class TransactionReader {
async read(chain: ChainName, tx: any): Promise<any> {
throw new Error('Not implemented');
}
}
// export abstract class TransactionReader {
// async read(chain: ChainName, tx: any): Promise<any> {
// throw new Error('Not implemented');
// }
// }
export class GnosisMultisendReader extends TransactionReader {
constructor(multiProvider: MultiProvider) {
super();
}
// export class GnosisMultisendReader extends TransactionReader {
// constructor(multiProvider: MultiProvider) {
// super();
// }
async read(chain: ChainName, tx: AnnotatedEV5Transaction): Promise<any> {
if (!tx.data) {
return undefined;
}
const multisends = decodeMultiSendData(tx.data);
// async read(chain: ChainName, tx: AnnotatedEV5Transaction): Promise<any> {
// if (!tx.data) {
// return undefined;
// }
// const multisends = decodeMultiSendData(tx.data);
return multisends;
}
}
// return multisends;
// }
// }
export class GenericTransactionReader extends HyperlaneReader {
export class TransactionReader extends HyperlaneReader {
constructor(
readonly environment: DeployEnvironment,
readonly multiProvider: MultiProvider,
readonly chain: ChainName,
readonly chainAddresses: ChainMap<Record<string, string>>,
) {
super();
super(multiProvider, chain);
}
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 (this.isIcaTransaction(chain, tx)) {
return this.readIcaTransaction(chain, tx);
}
if (await this.isMultisendTransaction(chain, tx)) {
return this.readMultisendTransaction(chain, tx);
}
return {};
}
private async readIcaTransaction(
chain: ChainName,
tx: AnnotatedEV5Transaction,
): 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 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 isIcaTransaction(
private async readMultisendTransaction(
chain: ChainName,
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 (
tx.to !== undefined &&
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