From 86896ee476e713443568f4e3bdbd59b621e7c9d0 Mon Sep 17 00:00:00 2001 From: Subham Satapathy <72144128+Subham-Satapathy@users.noreply.github.com> Date: Sat, 8 Feb 2025 08:06:04 +0530 Subject: [PATCH] Cowswap protocol parser (#40) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial commit * feat: implement Cowswap protocol parser and add transaction handling * feat: add Cowswap protocol support with transaction parsing and event handling * feat: update recipient address parsing in Cowswap protocol and adjust test data * feat: update identifier and name for Oneinch protocol in configuration * feat: remove Oneinch protocol support and related files * feat: improve token address handling and recipient parsing in Cowswap protocol * feat: update Cowswap tests to parse v2 transactions and remove Oneinch protocol tests --------- Co-authored-by: “Subham-Satapathy” <“subhamsatapathy515@gmail.com”> --- src/config/protocols.ts | 9 +- src/lib/Cowswap/abis/GPV2Settlement.json | 410 +++++++++++++++++++++++ src/lib/Cowswap/contracts.ts | 43 +++ src/lib/Cowswap/index.ts | 40 +++ src/lib/Cowswap/parser.ts | 43 +++ src/lib/index.ts | 4 +- tests/lib/Cowswap/data.ts | 123 +++++++ tests/lib/Cowswap/index.test.ts | 32 ++ 8 files changed, 702 insertions(+), 2 deletions(-) create mode 100644 src/lib/Cowswap/abis/GPV2Settlement.json create mode 100644 src/lib/Cowswap/contracts.ts create mode 100644 src/lib/Cowswap/index.ts create mode 100644 src/lib/Cowswap/parser.ts create mode 100644 tests/lib/Cowswap/data.ts create mode 100644 tests/lib/Cowswap/index.test.ts diff --git a/src/config/protocols.ts b/src/config/protocols.ts index 5743360..4fd7b34 100644 --- a/src/config/protocols.ts +++ b/src/config/protocols.ts @@ -56,5 +56,12 @@ export const protocols: IProtocolDefinitionMap = { twitter: "https://x.com/PancakeSwap", logo: "https://storage-mercle-prod.s3.amazonaws.com/public/protocol-logos/pancakeswap.png", website: "https://pancakeswap.finance/", - } + }, + cowswap: { + identifier: "cowswap", + name: "Cowswap", + twitter: "https://x.com/CoWSwap", + logo: "https://storage-mercle-prod.s3.amazonaws.com/public/protocol-logos/cowswap.png", + website: "https://swap.cow.fi/", + }, }; diff --git a/src/lib/Cowswap/abis/GPV2Settlement.json b/src/lib/Cowswap/abis/GPV2Settlement.json new file mode 100644 index 0000000..18397cf --- /dev/null +++ b/src/lib/Cowswap/abis/GPV2Settlement.json @@ -0,0 +1,410 @@ +[ + { + "inputs": [ + { + "internalType": "contract GPv2Authentication", + "name": "authenticator_", + "type": "address" + }, + { "internalType": "contract IVault", "name": "vault_", "type": "address" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes4", + "name": "selector", + "type": "bytes4" + } + ], + "name": "Interaction", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "orderUid", + "type": "bytes" + } + ], + "name": "OrderInvalidated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "orderUid", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "bool", + "name": "signed", + "type": "bool" + } + ], + "name": "PreSignature", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "solver", + "type": "address" + } + ], + "name": "Settlement", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "contract IERC20", + "name": "sellToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "contract IERC20", + "name": "buyToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "sellAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "buyAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "feeAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "orderUid", + "type": "bytes" + } + ], + "name": "Trade", + "type": "event" + }, + { + "inputs": [], + "name": "authenticator", + "outputs": [ + { + "internalType": "contract GPv2Authentication", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "domainSeparator", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }], + "name": "filledAmount", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes[]", "name": "orderUids", "type": "bytes[]" } + ], + "name": "freeFilledAmountStorage", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes[]", "name": "orderUids", "type": "bytes[]" } + ], + "name": "freePreSignatureStorage", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "offset", "type": "uint256" }, + { "internalType": "uint256", "name": "length", "type": "uint256" } + ], + "name": "getStorageAt", + "outputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes", "name": "orderUid", "type": "bytes" } + ], + "name": "invalidateOrder", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }], + "name": "preSignature", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes", "name": "orderUid", "type": "bytes" }, + { "internalType": "bool", "name": "signed", "type": "bool" } + ], + "name": "setPreSignature", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20[]", + "name": "tokens", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "clearingPrices", + "type": "uint256[]" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "sellTokenIndex", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "buyTokenIndex", + "type": "uint256" + }, + { "internalType": "address", "name": "receiver", "type": "address" }, + { + "internalType": "uint256", + "name": "sellAmount", + "type": "uint256" + }, + { "internalType": "uint256", "name": "buyAmount", "type": "uint256" }, + { "internalType": "uint32", "name": "validTo", "type": "uint32" }, + { "internalType": "bytes32", "name": "appData", "type": "bytes32" }, + { "internalType": "uint256", "name": "feeAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "flags", "type": "uint256" }, + { + "internalType": "uint256", + "name": "executedAmount", + "type": "uint256" + }, + { "internalType": "bytes", "name": "signature", "type": "bytes" } + ], + "internalType": "struct GPv2Trade.Data[]", + "name": "trades", + "type": "tuple[]" + }, + { + "components": [ + { "internalType": "address", "name": "target", "type": "address" }, + { "internalType": "uint256", "name": "value", "type": "uint256" }, + { "internalType": "bytes", "name": "callData", "type": "bytes" } + ], + "internalType": "struct GPv2Interaction.Data[][3]", + "name": "interactions", + "type": "tuple[][3]" + } + ], + "name": "settle", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "targetContract", + "type": "address" + }, + { "internalType": "bytes", "name": "calldataPayload", "type": "bytes" } + ], + "name": "simulateDelegatecall", + "outputs": [ + { "internalType": "bytes", "name": "response", "type": "bytes" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "targetContract", + "type": "address" + }, + { "internalType": "bytes", "name": "calldataPayload", "type": "bytes" } + ], + "name": "simulateDelegatecallInternal", + "outputs": [ + { "internalType": "bytes", "name": "response", "type": "bytes" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "bytes32", "name": "poolId", "type": "bytes32" }, + { + "internalType": "uint256", + "name": "assetInIndex", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "assetOutIndex", + "type": "uint256" + }, + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "internalType": "bytes", "name": "userData", "type": "bytes" } + ], + "internalType": "struct IVault.BatchSwapStep[]", + "name": "swaps", + "type": "tuple[]" + }, + { + "internalType": "contract IERC20[]", + "name": "tokens", + "type": "address[]" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "sellTokenIndex", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "buyTokenIndex", + "type": "uint256" + }, + { "internalType": "address", "name": "receiver", "type": "address" }, + { + "internalType": "uint256", + "name": "sellAmount", + "type": "uint256" + }, + { "internalType": "uint256", "name": "buyAmount", "type": "uint256" }, + { "internalType": "uint32", "name": "validTo", "type": "uint32" }, + { "internalType": "bytes32", "name": "appData", "type": "bytes32" }, + { "internalType": "uint256", "name": "feeAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "flags", "type": "uint256" }, + { + "internalType": "uint256", + "name": "executedAmount", + "type": "uint256" + }, + { "internalType": "bytes", "name": "signature", "type": "bytes" } + ], + "internalType": "struct GPv2Trade.Data", + "name": "trade", + "type": "tuple" + } + ], + "name": "swap", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "vault", + "outputs": [ + { "internalType": "contract IVault", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "vaultRelayer", + "outputs": [ + { + "internalType": "contract GPv2VaultRelayer", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { "stateMutability": "payable", "type": "receive" } +] diff --git a/src/lib/Cowswap/contracts.ts b/src/lib/Cowswap/contracts.ts new file mode 100644 index 0000000..f4dfc90 --- /dev/null +++ b/src/lib/Cowswap/contracts.ts @@ -0,0 +1,43 @@ +import { ethers } from "ethers"; +import { CHAIN_ID, LISTEN_FOR_TRANSACTIONS } from "../../enums"; +import { IProtocolContractDefinitions } from "../../types"; +import GPV2SettlementAbi from "./abis/GPV2Settlement.json"; + +enum CONTRACT_ENUM { + GP_V2_SETTLEMENT = "GP_V2_SETTLEMENT", +} +enum EVENT_ENUM { + TRADE = "0xa07a543ab8a018198e99ca0184c93fe9050a79400a0a723441f84de1d972cc17" +} +const contracts: IProtocolContractDefinitions = { + [CONTRACT_ENUM.GP_V2_SETTLEMENT]: { + interface: new ethers.Interface(GPV2SettlementAbi), + deployments: { + [CHAIN_ID.BASE]: { + address: "0x9008D19f58AAbD9eD0D60971565AA8510560ab41", + listenForTransactions: [LISTEN_FOR_TRANSACTIONS.INCOMING], + }, + [CHAIN_ID.ARBITRUM]: { + address: "0x9008D19f58AAbD9eD0D60971565AA8510560ab41", + listenForTransactions: [LISTEN_FOR_TRANSACTIONS.INCOMING], + }, + [CHAIN_ID.ETHEREUM]: { + address: "0x9008D19f58AAbD9eD0D60971565AA8510560ab41", + listenForTransactions: [LISTEN_FOR_TRANSACTIONS.INCOMING], + }, + [CHAIN_ID.POLYGON]: { + address: "0x9008D19f58AAbD9eD0D60971565AA8510560ab41", + listenForTransactions: [LISTEN_FOR_TRANSACTIONS.INCOMING], + } + }, + events: { + [EVENT_ENUM.TRADE]: { + abi: new ethers.Interface([ + "event Trade(address indexed owner, address sellToken, address buyToken, uint256 sellAmount, uint256 buyAmount, uint256 feeAmount, bytes orderUid)", + ]), + }, + }, + }, +}; + +export { CONTRACT_ENUM, contracts, EVENT_ENUM }; diff --git a/src/lib/Cowswap/index.ts b/src/lib/Cowswap/index.ts new file mode 100644 index 0000000..c994392 --- /dev/null +++ b/src/lib/Cowswap/index.ts @@ -0,0 +1,40 @@ +import { protocols } from "../../config"; +import { ProtocolHelper } from "../../helpers"; +import { + IProtocolContractDefinitions, + IProtocolParserExport, + ITransaction, + ITransactionAction, +} from "../../types"; +import { CONTRACT_ENUM, contracts } from "./contracts"; +import { CowswapContractParser } from "./parser"; + +export default class Cowswap implements IProtocolParserExport { + public readonly protocolIdentifier: string; + + constructor() { + this.protocolIdentifier = protocols.cowswap.identifier; + } + + public async parseTransaction( + transaction: ITransaction + ): Promise { + const actions: ITransactionAction[] = []; + + if ( + ProtocolHelper.txnToIsListenerContract( + transaction, + CONTRACT_ENUM.GP_V2_SETTLEMENT, + contracts + ) + ) { + const action = CowswapContractParser.parseTransaction(transaction); + actions.push(...action); + } + return actions; + } + + public getProtocolContracts(): IProtocolContractDefinitions { + return contracts; + } +} diff --git a/src/lib/Cowswap/parser.ts b/src/lib/Cowswap/parser.ts new file mode 100644 index 0000000..f3b776f --- /dev/null +++ b/src/lib/Cowswap/parser.ts @@ -0,0 +1,43 @@ +import { ACTION_ENUM } from "../../enums"; +import { ProtocolHelper } from "../../helpers"; +import { + ISingleSwapAction, + ITransaction, + ITransactionAction, + ITransactionLog, +} from "../../types"; +import { CONTRACT_ENUM, contracts, EVENT_ENUM } from "./contracts"; + +export class CowswapContractParser { + public static parseTransaction( + transaction: ITransaction + ): ITransactionAction[] { + const actions: ITransactionAction[] = []; + + const matchedSwapOrderLog = transaction.logs.find( + (log) => log.topics[0] === EVENT_ENUM.TRADE + ); + if (matchedSwapOrderLog) { + actions.push(this.parseSwapOrder(matchedSwapOrderLog, transaction)); + } + + return actions; + } + + private static parseSwapOrder(log: ITransactionLog, transaction:ITransaction): ISingleSwapAction { + const parsedLog = ProtocolHelper.parseLog( + log, + contracts.GP_V2_SETTLEMENT.events[EVENT_ENUM.TRADE] + ); + + return { + type: ACTION_ENUM.SINGLE_SWAP, + fromToken: parsedLog.args.sellToken, + toToken: parsedLog.args.buyToken, + fromAmount: parsedLog.args.sellAmount.toString(), + toAmount: parsedLog.args.buyAmount.toString(), + sender: transaction.from, + recipient: parsedLog.args.owner ?? `0x${parsedLog.args.orderUid.slice(66,106)}` + }; + } +} diff --git a/src/lib/index.ts b/src/lib/index.ts index c712c86..c62fb1d 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -7,6 +7,7 @@ import Across from "./Across"; import Debridge from "./Debridge"; import Aerodrome from "./Aerodrome"; import Pancakeswap from "./Pancakeswap"; +import Cowswap from "./Cowswap"; export const parsers = { [protocols.rhinofi.identifier]: new RhinoFi(), @@ -16,5 +17,6 @@ export const parsers = { [protocols.across.identifier]: new Across(), [protocols.debridge.identifier]: new Debridge(), [protocols.aerodrome.identifier]: new Aerodrome(), - [protocols.pancakeswap.identifier]: new Pancakeswap() + [protocols.pancakeswap.identifier]: new Pancakeswap(), + [protocols.cowswap.identifier]: new Cowswap() }; diff --git a/tests/lib/Cowswap/data.ts b/tests/lib/Cowswap/data.ts new file mode 100644 index 0000000..e1de0bc --- /dev/null +++ b/tests/lib/Cowswap/data.ts @@ -0,0 +1,123 @@ +import { ACTION_ENUM, CHAIN_ID } from "../../../src"; +import { IProtocolTestingData } from "../../../src/types"; + +export enum COWSWAP_VERSIONS { + V2 = "v2", +} + +export const cowswapSwapData: IProtocolTestingData = { + [COWSWAP_VERSIONS.V2]: [ + { + txnHash: + "0x5f22915f8bd90dd55e63f2a4cafc5688501649cad090502cf663b97873567e1f", + chainId: CHAIN_ID.ETHEREUM, + emittedActions: [ + { + type: ACTION_ENUM.SINGLE_SWAP, + fromToken: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + toToken: "0x9Ce84F6A69986a83d92C324df10bC8E64771030f", + fromAmount: "20000000000", + toAmount: "42310150156594486438480", + sender: "0x34717040928d7fd8154d4612f3228eff14521023", + recipient: "0x6fE3314087acE6B2b54021980a690c02ACdd0098" + }, + ], + }, + { + txnHash: + "0x1619b2d1ca449f7b98abdb69a4e3389291120cd5ff930dbe3477f7992e710595", + chainId: CHAIN_ID.ETHEREUM, + emittedActions: [ + { + type: ACTION_ENUM.SINGLE_SWAP, + fromToken: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + toToken: "0x44108f0223A3C3028F5Fe7AEC7f9bb2E66beF82F", + fromAmount: "8299499821", + toAmount: "20000000000000000000000", + sender: "0x00806daa2cfe49715ea05243ff259deb195760fc", + recipient: "0x1405F9bEAE7B74EcE74F6734D1a1302E1873211C" + }, + ], + }, + { + txnHash: + "0x84f1e17d076f9f7e6ad964e6d23a26362b716f92686d8e641eb0b306575f9b8b", + chainId: CHAIN_ID.ETHEREUM, + emittedActions: [ + { + type: ACTION_ENUM.SINGLE_SWAP, + fromToken: "0x808507121B80c02388fAd14726482e061B8da827", + toToken: "0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E", + fromAmount: "236830943687003687961", + toAmount: "804205943233010446709", + sender: "0x8f70a86c1309d8b1f5befc58948e7386fd495875", + recipient: "0x402904E954aebEE2E78b7B09595393cf05571333" + }, + ], + }, + { + txnHash: + "0x37548d13aa41fc4ba410ff54735a6534e00e87da4552ae2f973755b34e8e2556", + chainId: CHAIN_ID.ETHEREUM, + emittedActions: [ + { + type: ACTION_ENUM.SINGLE_SWAP, + fromToken: "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", + toToken: "0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB", + fromAmount: "244783248237207583", + toAmount: "1660276181802241186294", + sender: "0x00806daa2cfe49715ea05243ff259deb195760fc", + recipient: "0x9bd702E05B9c97E4A4a3E47Df1e0fe7A0C26d2F1" + }, + ], + }, + { + txnHash: + "0x3603aba3537f3ec185c8c3bd4151a8fae20a04cc6c22c1a0ec398116edcdb8fd", + chainId: CHAIN_ID.ETHEREUM, + emittedActions: [ + { + type: ACTION_ENUM.SINGLE_SWAP, + fromToken: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + toToken: "0x0000000000c5dc95539589fbD24BE07c6C14eCa4", + fromAmount: "5000000000000000000", + toAmount: "6576007078316234267229248", + sender: "0x008300082c3000009e63680088f8c7f4d3ff2e87", + recipient: "0x40A50cf069e992AA4536211B23F286eF88752187" + }, + ], + }, + { + txnHash: + "0xb4efb21514531ca5bf369a97a5854bb394d3f92d000cbe899e1db00cb4fd2e2a", + chainId: CHAIN_ID.ARBITRUM, + emittedActions: [ + { + type: ACTION_ENUM.SINGLE_SWAP, + fromToken: "0xaa6b1798A97505b36D9c4a3736C2Aa48674Aeb97", + toToken: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", + fromAmount: "18564027758822375816", + toAmount: "2177656738445500", + sender: "0x6bf97aFe2D2C790999cDEd2a8523009eB8a0823f", + recipient: "0xF9Fa05eE5D7C9E9eeA2e73e6767e6b59563fdF5d" + }, + ], + }, + { + txnHash: + "0x839acebbd00ebea1eca464799cae88fccd522a10799803d2aa692b4258c93dd2", + chainId: CHAIN_ID.ETHEREUM, + emittedActions: [ + { + type: ACTION_ENUM.SINGLE_SWAP, + fromToken: "0xdAC17F958D2ee523a2206206994597C13D831ec7", + toToken: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + fromAmount: "83703525974", + toAmount: "83753136176", + sender: "0x6bf97aFe2D2C790999cDEd2a8523009eB8a0823f", + recipient: "0xF4DC338B1b1184F84a461f0bb2f974fC90A81456" + }, + ], + } + ], +}; diff --git a/tests/lib/Cowswap/index.test.ts b/tests/lib/Cowswap/index.test.ts new file mode 100644 index 0000000..1f519d3 --- /dev/null +++ b/tests/lib/Cowswap/index.test.ts @@ -0,0 +1,32 @@ +import chalk from 'chalk'; +import { protocols } from '../../../src'; +import { ProtocolParserUtils } from '../../index'; +import { COWSWAP_VERSIONS, cowswapSwapData } from './data'; + +describe('CowswapParser', () => { + let utils: ProtocolParserUtils; + + beforeAll(async () => { + utils = new ProtocolParserUtils(protocols.cowswap.identifier); + await utils.initialize(); + }); + + it('is correctly defined', () => { + utils.isValidProtocol(); + }); + + it('should parse v2 transactions correctly', async () => { + const v2Transactions = cowswapSwapData[COWSWAP_VERSIONS.V2]; + + for (const transaction of v2Transactions) { + const actions = await utils.fetchAndParseTestTxn(transaction); + utils.assertTestTransactionForData(transaction, actions); + + console.log( + chalk.green('Successfully parsed swap transaction with actions:', actions.map((action) => action.type).join(',')), + 'and hash:', + transaction.txnHash + ); + } + }); +}); \ No newline at end of file