diff --git a/packages/chain-adapters/src/evm/SecondClassEvmAdapter.ts b/packages/chain-adapters/src/evm/SecondClassEvmAdapter.ts index a2c19aea666..27ceb3b5dfa 100644 --- a/packages/chain-adapters/src/evm/SecondClassEvmAdapter.ts +++ b/packages/chain-adapters/src/evm/SecondClassEvmAdapter.ts @@ -1,5 +1,5 @@ import type { AssetId, ChainId } from '@shapeshiftoss/caip' -import { ASSET_NAMESPACE, toAssetId } from '@shapeshiftoss/caip' +import { ASSET_NAMESPACE, hyperEvmChainId, toAssetId } from '@shapeshiftoss/caip' import type { evm } from '@shapeshiftoss/common-api' import { MULTICALL3_CONTRACT, viemClientByChainId } from '@shapeshiftoss/contracts' import type { EvmChainId, RootBip44Params } from '@shapeshiftoss/types' @@ -340,6 +340,46 @@ export abstract class SecondClassEvmAdapter extends EvmBas }) } + private async fetchInternalTransactions( + txHash: string, + ): Promise<{ from: string; to: string; value: string }[]> { + if (this.chainId === hyperEvmChainId) { + return [] + } + + try { + const trace = await this.requestQueue.add(() => + this.provider.send('debug_traceTransaction', [txHash, { tracer: 'callTracer' }]), + ) + + const internalTxs: { from: string; to: string; value: string }[] = [] + + const extractCalls = (call: any) => { + if (call.value && call.value !== '0x0' && call.value !== '0x') { + internalTxs.push({ + from: call.from, + to: call.to, + value: BigInt(call.value).toString(), + }) + } + + if (call.calls && Array.isArray(call.calls)) { + for (const subcall of call.calls) { + extractCalls(subcall) + } + } + } + + if (trace) { + extractCalls(trace) + } + + return internalTxs + } catch (error) { + return [] + } + } + async parseTx(txHash: unknown, pubkey: string): Promise { const hash = txHash as Hex const viemClient = viemClientByChainId[this.chainId] @@ -349,9 +389,10 @@ export abstract class SecondClassEvmAdapter extends EvmBas } try { - const [transaction, receipt] = await Promise.all([ + const [transaction, receipt, internalTxs] = await Promise.all([ viemClient.getTransaction({ hash }), viemClient.getTransactionReceipt({ hash }), + this.fetchInternalTransactions(hash), ]) if (!transaction || !receipt) { @@ -414,6 +455,7 @@ export abstract class SecondClassEvmAdapter extends EvmBas gasPrice: receipt.effectiveGasPrice.toString(), inputData: transaction.input, tokenTransfers, + internalTxs: internalTxs.length > 0 ? internalTxs : undefined, } return this.parse(parsedTx, pubkey) @@ -451,6 +493,35 @@ export abstract class SecondClassEvmAdapter extends EvmBas }) } + if (tx.internalTxs) { + for (const internalTx of tx.internalTxs) { + if (bn(internalTx.value).lte(0)) continue + + const internalFrom = getAddress(internalTx.from) + const internalTo = getAddress(internalTx.to) + + if (isAddressEqual(address, internalFrom)) { + nativeTransfers.push({ + assetId: this.assetId, + from: [internalTx.from], + to: [internalTx.to], + type: TransferType.Send, + value: internalTx.value, + }) + } + + if (isAddressEqual(address, internalTo)) { + nativeTransfers.push({ + assetId: this.assetId, + from: [internalTx.from], + to: [internalTx.to], + type: TransferType.Receive, + value: internalTx.value, + }) + } + } + } + const tokenTransfers = tx.tokenTransfers?.flatMap(transfer => { const transferFrom = getAddress(transfer.from) diff --git a/packages/swapper/src/utils/tenderly/simulate.ts b/packages/swapper/src/utils/tenderly/simulate.ts index 166ced6549f..5330e6147b7 100644 --- a/packages/swapper/src/utils/tenderly/simulate.ts +++ b/packages/swapper/src/utils/tenderly/simulate.ts @@ -17,9 +17,11 @@ import { import type { TenderlyConfig, TenderlyErrorResponse, + TenderlyInternalTransaction, TenderlySimulationRequest, TenderlySimulationResponse, TenderlyStateOverrides, + TenderlyTransactionResponse, } from './types' export type SimulationResult = { @@ -113,6 +115,34 @@ export const simulateWithStateOverrides = async ( } } +export const fetchInternalTransactions = async ( + params: { + chainId: ChainId + txHash: string + }, + config: TenderlyConfig, +): Promise => { + const { chainId, txHash } = params + + try { + const evmNetworkId = Number(fromChainId(chainId).chainReference) + + const url = `https://api.tenderly.co/api/v1/account/${config.accountSlug}/project/${config.projectSlug}/transactions/by-hash/${evmNetworkId}/${txHash}` + + const response = await axios.get(url, { + headers: { + 'Content-Type': 'application/json', + 'X-Access-Key': config.apiKey, + }, + timeout: 10000, + }) + + return response.data.transaction.internal_transactions ?? [] + } catch (error) { + return [] + } +} + const buildStateOverrides = (params: { from: Address spender: Address diff --git a/packages/swapper/src/utils/tenderly/types.ts b/packages/swapper/src/utils/tenderly/types.ts index 1973c3534bc..bbf3123806f 100644 --- a/packages/swapper/src/utils/tenderly/types.ts +++ b/packages/swapper/src/utils/tenderly/types.ts @@ -59,3 +59,26 @@ export type TenderlyConfig = { accountSlug: string projectSlug: string } + +export type TenderlyInternalTransaction = { + from: Address + to: Address + value: string +} + +export type TenderlyTransactionResponse = { + transaction: { + hash: string + block_number: number + from: Address + to: Address + input: string + value: string + gas: number + gas_used: number + gas_price: string + status: boolean + error_message?: string + internal_transactions?: TenderlyInternalTransaction[] + } +}