Repository: skalenetwork/IMA (language: python)

Issue:
Ticket 1620/resolve speed issues of ima event scanner
IMA event log scanner moved into external module.
No new tests needed.
Legacy network test must be fast.


Discussion / hints:

Fix patch:
diff --git a/VERSION b/VERSION
index 227cea215..38f77a65b 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-2.0.0
+2.0.1
diff --git a/agent/bls.mjs b/agent/bls.mjs
index 45c1a1212..c5588d155 100644
--- a/agent/bls.mjs
+++ b/agent/bls.mjs
@@ -1271,7 +1271,7 @@ async function gatherSigningStartImpl( optsSignOperation ) {
                         "\n";
                     if( log.verboseGet() >= log.verboseReversed().error ) {
                         optsSignOperation.details.write( strErrorMessage );
-                        if( log.id != details.id )
+                        if( log.id != optsSignOperation.details.id )
                             log.write( strErrorMessage );
                     }
                 }
diff --git a/agent/clpTools.mjs b/agent/clpTools.mjs
index 811d02cfb..8bb30473c 100644
--- a/agent/clpTools.mjs
+++ b/agent/clpTools.mjs
@@ -1314,14 +1314,21 @@ export function commandLineTaskTransferM2S() {
                 idxChainKnownForS2S: 0,
                 cntChainsKnownForS2S: 0
             };
+            const optsChainPair = {
+                "strDirection": "M2S",
+                "chainSrc": imaState.chainProperties.mn,
+                "chainDst": imaState.chainProperties.sc
+            };
             return await IMA.doTransfer( // main-net --> s-chain
                 "M2S",
                 joRuntimeOpts,
                 imaState.chainProperties.mn.ethersProvider,
                 imaState.joMessageProxyMainNet,
+                imaState.chainProperties.mn.joAbiIMA.message_proxy_mainnet_abi,
                 imaState.chainProperties.mn.joAccount,
                 imaState.chainProperties.sc.ethersProvider,
                 imaState.joMessageProxySChain,
+                imaState.chainProperties.sc.joAbiIMA.message_proxy_chain_abi,
                 imaState.chainProperties.sc.joAccount,
                 imaState.chainProperties.mn.strChainName,
                 imaState.chainProperties.sc.strChainName,
@@ -1336,7 +1343,8 @@ export function commandLineTaskTransferM2S() {
                 imaState.nBlockAgeM2S,
                 imaBLS.doSignMessagesM2S,
                 null,
-                imaState.chainProperties.sc.transactionCustomizer
+                imaState.chainProperties.sc.transactionCustomizer,
+                optsChainPair
             );
         }
     } );
@@ -1354,14 +1362,21 @@ export function commandLineTaskTransferS2M() {
                 idxChainKnownForS2S: 0,
                 cntChainsKnownForS2S: 0
             };
+            const optsChainPair = {
+                "strDirection": "S2M",
+                "chainSrc": imaState.chainProperties.sc,
+                "chainDst": imaState.chainProperties.mn
+            };
             return await IMA.doTransfer( // s-chain --> main-net
                 "S2M",
                 joRuntimeOpts,
                 imaState.chainProperties.sc.ethersProvider,
                 imaState.joMessageProxySChain,
+                imaState.chainProperties.sc.joAbiIMA.message_proxy_chain_abi,
                 imaState.chainProperties.sc.joAccount,
                 imaState.chainProperties.mn.ethersProvider,
                 imaState.joMessageProxyMainNet,
+                imaState.chainProperties.sc.joAbiIMA.message_proxy_mainnet_abi,
                 imaState.chainProperties.mn.joAccount,
                 imaState.chainProperties.sc.strChainName,
                 imaState.chainProperties.mn.strChainName,
@@ -1376,7 +1391,8 @@ export function commandLineTaskTransferS2M() {
                 imaState.nBlockAgeS2M,
                 imaBLS.doSignMessagesS2M,
                 null,
-                imaState.chainProperties.mn.transactionCustomizer
+                imaState.chainProperties.mn.transactionCustomizer,
+                optsChainPair
             );
         }
     } );
@@ -1403,6 +1419,7 @@ export function commandLineTaskTransferS2S() {
                 skaleObserver,
                 imaState.chainProperties.sc.ethersProvider,
                 imaState.joMessageProxySChain,
+                imaState.chainProperties.sc.joAbiIMA.message_proxy_chain_abi,
                 imaState.chainProperties.sc.joAccount,
                 imaState.chainProperties.sc.strChainName,
                 imaState.chainProperties.sc.chainId,
diff --git a/agent/loop.mjs b/agent/loop.mjs
index 71a1170a6..ae8a64cc2 100644
--- a/agent/loop.mjs
+++ b/agent/loop.mjs
@@ -220,16 +220,21 @@ async function singleTransferLoopPartM2S( optsLoop, strLogPrefix ) {
                 if( checkTimeFraming( null, "m2s", optsLoop.joRuntimeOpts ) ) {
                     imaState.loopState.m2s.isInProgress = true;
                     await pwa.notifyOnLoopStart( imaState, "m2s" );
+                    const optsChainPair = {
+                        "strDirection": "M2S",
+                        "chainSrc": imaState.chainProperties.mn,
+                        "chainDst": imaState.chainProperties.sc
+                    };
                     b1 = await IMA.doTransfer( // main-net --> s-chain
                         "M2S",
                         optsLoop.joRuntimeOpts,
-
                         imaState.chainProperties.mn.ethersProvider,
                         imaState.joMessageProxyMainNet,
+                        imaState.chainProperties.sc.joAbiIMA.message_proxy_mainnet_abi,
                         imaState.chainProperties.mn.joAccount,
                         imaState.chainProperties.sc.ethersProvider,
                         imaState.joMessageProxySChain,
-
+                        imaState.chainProperties.sc.joAbiIMA.message_proxy_chain_abi,
                         imaState.chainProperties.sc.joAccount,
                         imaState.chainProperties.mn.strChainName,
                         imaState.chainProperties.sc.strChainName,
@@ -244,7 +249,8 @@ async function singleTransferLoopPartM2S( optsLoop, strLogPrefix ) {
                         imaState.nBlockAgeM2S,
                         imaBLS.doSignMessagesM2S,
                         null,
-                        imaState.chainProperties.sc.transactionCustomizer
+                        imaState.chainProperties.sc.transactionCustomizer,
+                        optsChainPair
                     );
                     imaState.loopState.m2s.isInProgress = false;
                     await pwa.notifyOnLoopEnd( imaState, "m2s" );
@@ -300,16 +306,21 @@ async function singleTransferLoopPartS2M( optsLoop, strLogPrefix ) {
                 if( checkTimeFraming( null, "s2m", optsLoop.joRuntimeOpts ) ) {
                     imaState.loopState.s2m.isInProgress = true;
                     await pwa.notifyOnLoopStart( imaState, "s2m" );
+                    const optsChainPair = {
+                        "strDirection": "S2M",
+                        "chainSrc": imaState.chainProperties.sc,
+                        "chainDst": imaState.chainProperties.mn
+                    };
                     b2 = await IMA.doTransfer( // s-chain --> main-net
                         "S2M",
                         optsLoop.joRuntimeOpts,
-
                         imaState.chainProperties.sc.ethersProvider,
                         imaState.joMessageProxySChain,
+                        imaState.chainProperties.sc.joAbiIMA.message_proxy_chain_abi,
                         imaState.chainProperties.sc.joAccount,
                         imaState.chainProperties.mn.ethersProvider,
                         imaState.joMessageProxyMainNet,
-
+                        imaState.chainProperties.sc.joAbiIMA.message_proxy_mainnet_abi,
                         imaState.chainProperties.mn.joAccount,
                         imaState.chainProperties.sc.strChainName,
                         imaState.chainProperties.mn.strChainName,
@@ -324,7 +335,8 @@ async function singleTransferLoopPartS2M( optsLoop, strLogPrefix ) {
                         imaState.nBlockAgeS2M,
                         imaBLS.doSignMessagesS2M,
                         null,
-                        imaState.chainProperties.mn.transactionCustomizer
+                        imaState.chainProperties.mn.transactionCustomizer,
+                        optsChainPair
                     );
                     imaState.loopState.s2m.isInProgress = false;
                     await pwa.notifyOnLoopEnd( imaState, "s2m" );
@@ -373,6 +385,7 @@ async function singleTransferLoopPartS2S( optsLoop, strLogPrefix ) {
                 skaleObserver,
                 imaState.chainProperties.sc.ethersProvider,
                 imaState.joMessageProxySChain,
+                imaState.chainProperties.sc.joAbiIMA.message_proxy_chain_abi,
                 imaState.chainProperties.sc.joAccount,
                 imaState.chainProperties.sc.strChainName,
                 imaState.chainProperties.sc.chainId,
@@ -836,7 +849,7 @@ export async function ensureHaveWorkers( opts ) {
         };
         while( ! aClient.logicalInitComplete ) {
             if( log.verboseGet() >= log.verboseReversed().info )
-                log.write( "LOOP server is not inited yet...\n" );
+                log.write( "LOOP server is not initialized yet...\n" );
             await threadInfo.sleep( 1000 );
             aClient.send( jo );
         }
diff --git a/npms/skale-ima/imaEventLogScan.mjs b/npms/skale-ima/imaEventLogScan.mjs
index 5b22f155d..bf7da1fc9 100644
--- a/npms/skale-ima/imaEventLogScan.mjs
+++ b/npms/skale-ima/imaEventLogScan.mjs
@@ -31,6 +31,12 @@ import * as rpcCall from "../../agent/rpcCall.mjs";
 import * as imaHelperAPIs from "./imaHelperAPIs.mjs";
 import * as imaTransferErrorHandling from "./imaTransferErrorHandling.mjs";
 
+import * as childProcessModule from "child_process";
+import * as path from "path";
+import * as url from "url";
+
+const __dirname = path.dirname( url.fileURLToPath( import.meta.url ) );
+
 export function createProgressiveEventsScanPlan( details, nLatestBlockNumber ) {
     // assume Main Net mines 6 blocks per minute
     const blocksInOneMinute = 6;
@@ -86,9 +92,87 @@ export function createProgressiveEventsScanPlan( details, nLatestBlockNumber ) {
     return arrProgressiveEventsScanPlan;
 }
 
+export function extractEventArg( arg ) {
+    if( arg && typeof arg == "object" && "type" in arg && typeof arg.type == "string" &&
+        arg.type == "BigNumber" && "hex" in arg && typeof arg.hex == "string" )
+        return owaspUtils.toBN( arg.hex );
+    return arg;
+}
+
+function generateWhileTransferringLogMessageSuffix( optsChainPair ) {
+    if( ! optsChainPair )
+        return "";
+    if( ! optsChainPair.strDirection )
+        return "";
+    if( optsChainPair.strDirection == "S2S" ) {
+        return cc.debug( " (while performing " ) + cc.attention( optsChainPair.strDirection ) +
+            cc.debug( " transfer with external S-Chain " ) +
+            cc.info( optsChainPair.optsSpecificS2S.joSChain.data.name ) + cc.debug( " / " ) +
+            cc.notice( optsChainPair.optsSpecificS2S.joSChain.data.computed.chainId ) +
+            cc.debug( " node " ) + cc.info( optsChainPair.optsSpecificS2S.idxNode ) +
+            cc.debug( ")" );
+    }
+    return cc.debug( " (while performing " ) + cc.attention( optsChainPair.strDirection ) +
+        cc.debug( " transfer)" );
+}
+
+export async function safeGetPastEventsProgressiveExternal(
+    details, strLogPrefix, ethersProvider, attempts,
+    joContract, joABI, strEventName,
+    nBlockFrom, nBlockTo, joFilter, optsChainPair
+) {
+    if( joABI && typeof joABI == "object" ) {
+        const escapeShell = function( cmd ) {
+            return "\"" + cmd.replace( /(["'$`\\])/g,"\\$1" ) + "\"";
+        };
+        const joArg = {
+            "url": owaspUtils.ethersProviderToUrl( ethersProvider ),
+            "attempts": attempts,
+            "strEventName": strEventName,
+            "nBlockFrom": nBlockFrom,
+            "nBlockTo": nBlockTo,
+            "joFilter": joFilter,
+            "address": joContract.address,
+            "abi": joABI
+        };
+        const cmd = "node " + path.join( __dirname, "imaExternalLogScan.mjs" ) + " " +
+            escapeShell( JSON.stringify( joArg ) );
+        if( log.verboseGet() >= log.verboseReversed().trace ) {
+            details.write( strLogPrefix +
+                cc.debug( "Will run external command to search logs for event " ) +
+                cc.j( strEventName ) + cc.debug( " via URL " ) + cc.u( joArg.url ) +
+                generateWhileTransferringLogMessageSuffix( optsChainPair ) +
+                cc.debug( "..." ) + "\n" );
+        }
+        const res = childProcessModule.execSync( cmd );
+        if( "error" in res && res.error ) {
+            if( log.verboseGet() >= log.verboseReversed().error ) {
+                details.write( strLogPrefix +
+                    cc.error( "Got error from external command to search logs for event " ) +
+                    cc.j( strEventName ) + cc.error( " via URL " ) + cc.u( joArg.url ) +
+                    generateWhileTransferringLogMessageSuffix( optsChainPair ) +
+                    cc.error( ":" ) + cc.warning( owaspUtils.extractErrorMessage( err ) ) + "\n" );
+            }
+            throw new Error( res.error );
+        }
+        if( log.verboseGet() >= log.verboseReversed().trace ) {
+            details.write( strLogPrefix +
+                cc.debug( "Done running external command to search logs for event " ) +
+                cc.j( strEventName ) + cc.debug( " via URL " ) + cc.u( joArg.url ) +
+                generateWhileTransferringLogMessageSuffix( optsChainPair ) +
+                cc.debug( "." ) + "\n" );
+        }
+        return JSON.parse( res ).result;
+    }
+    return await safeGetPastEventsProgressive(
+        details, strLogPrefix, ethersProvider, attempts,
+        joContract, strEventName,
+        nBlockFrom, nBlockTo, joFilter );
+}
+
 export async function safeGetPastEventsProgressive(
-    details, strLogPrefix,
-    ethersProvider, attempts, joContract, strEventName,
+    details, strLogPrefix, ethersProvider, attempts,
+    joContract, strEventName,
     nBlockFrom, nBlockTo, joFilter
 ) {
     if( ! imaTransferErrorHandling.getEnabledProgressiveEventsScan() ) {
diff --git a/npms/skale-ima/imaExternalLogScan.mjs b/npms/skale-ima/imaExternalLogScan.mjs
new file mode 100644
index 000000000..6c740d497
--- /dev/null
+++ b/npms/skale-ima/imaExternalLogScan.mjs
@@ -0,0 +1,88 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+
+/**
+ * @license
+ * SKALE IMA
+ *
+ * SKALE IMA is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * SKALE IMA is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with SKALE IMA.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @file imaExternalLogScan.mjs
+ * @copyright SKALE Labs 2019-Present
+ */
+
+import * as cc from "../skale-cc/cc.mjs";
+import * as log from "../skale-log/log.mjs";
+import * as owaspUtils from "../skale-owasp/owaspUtils.mjs";
+import * as imaEventLogScan from "./imaEventLogScan.mjs";
+
+const gIsDebugLogging = false; // development option only, must be always false
+cc.enable( false );
+log.addStdout();
+
+// allow self-signed wss and https
+process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0;
+
+function finalizeOutput( jo ) {
+    if( ! jo )
+        return;
+    cc.enable( false );
+    process.stdout.write( cc.j( jo ) );
+}
+
+async function run() {
+    const details = log.createMemoryStream();
+    try {
+        if( gIsDebugLogging ) {
+            log.write( cc.debug( "Process startup arguments array is " ) +
+                cc.j( process.argv ) + "\n" );
+        }
+        if( process.argv.length != 3 )
+            throw new Error( "Wrong number of command line arguments" );
+
+        if( gIsDebugLogging ) {
+            log.write( cc.debug( "Main argument text is " ) +
+                cc.j( process.argv[2] ) + "\n" );
+        }
+        const joArg = JSON.parse( process.argv[2] );
+        if( gIsDebugLogging ) {
+            log.write( cc.debug( "Main argument JSON is " ) +
+                cc.j( joArg ) + "\n" );
+        }
+
+        const ethersProvider = owaspUtils.getEthersProviderFromURL( joArg.url );
+        const joContract = new owaspUtils.ethersMod.ethers.Contract(
+            joArg.address, joArg.abi, ethersProvider );
+
+        const arrLogRecordReferencesWalk = await imaEventLogScan.safeGetPastEventsProgressive(
+            details, "", ethersProvider, joArg.attempts,
+            joContract, joArg.strEventName,
+            joArg.nBlockFrom, joArg.nBlockTo, joArg.joFilter );
+
+        finalizeOutput( { "result": arrLogRecordReferencesWalk, "error": null } );
+        process.exit( 0 );
+    } catch ( err ) {
+        if( gIsDebugLogging ) {
+            log.write( cc.error( "Failed to create RPC call: " ) +
+        cc.j( err ) + "\n" );
+        }
+        finalizeOutput( {
+            "error": owaspUtils.extractErrorMessage( err ),
+            "output": details.toString()
+        } );
+        process.exit( 1 );
+    }
+}
+run();
diff --git a/npms/skale-ima/imaSgxExternalSigner.mjs b/npms/skale-ima/imaSgxExternalSigner.mjs
index 87715dcc4..60fe93b9b 100644
--- a/npms/skale-ima/imaSgxExternalSigner.mjs
+++ b/npms/skale-ima/imaSgxExternalSigner.mjs
@@ -6,7 +6,7 @@ import * as rpcCall from "../../agent/rpcCall.mjs";
 
 const gIsDebugLogging = false; // development option only, must be always false
 const isColors = owaspUtils.toBoolean( process.argv[2] );
-cc.enable( true );
+cc.enable( false );
 log.addStdout();
 
 // allow self-signed wss and https
diff --git a/npms/skale-ima/index.mjs b/npms/skale-ima/index.mjs
index 6666a1cbf..93b9ff712 100644
--- a/npms/skale-ima/index.mjs
+++ b/npms/skale-ima/index.mjs
@@ -44,16 +44,14 @@ const perMessageGasForTransfer = 1000000;
 const additionalS2MTransferOverhead = 200000;
 
 async function findOutReferenceLogRecord(
-    details, strLogPrefix,
-    ethersProvider, joMessageProxy,
-    bnBlockId, nMessageNumberToFind, isVerbose
+    details, strLogPrefix, ethersProvider, joMessageProxy, joABI,
+    bnBlockId, nMessageNumberToFind, isVerbose, optsChainPair
 ) {
     const bnMessageNumberToFind = owaspUtils.toBN( nMessageNumberToFind.toString() );
     const strEventName = "PreviousMessageReference";
-    const arrLogRecords = await imaEventLogScan.safeGetPastEventsProgressive(
-        details, strLogPrefix,
-        ethersProvider, 10, joMessageProxy, strEventName,
-        bnBlockId, bnBlockId, joMessageProxy.filters[strEventName]()
+    const arrLogRecords = await imaEventLogScan.safeGetPastEventsProgressiveExternal(
+        details, strLogPrefix, ethersProvider, 10, joMessageProxy, joABI, strEventName,
+        bnBlockId, bnBlockId, joMessageProxy.filters[strEventName](), optsChainPair
     );
     const cntLogRecord = arrLogRecords.length;
     if( isVerbose ) {
@@ -66,8 +64,8 @@ async function findOutReferenceLogRecord(
     for( let idxLogRecord = 0; idxLogRecord < cntLogRecord; ++ idxLogRecord ) {
         const joEvent = arrLogRecords[idxLogRecord];
         const eventValuesByName = {
-            "currentMessage": joEvent.args[0],
-            "previousOutgoingMessageBlockId": joEvent.args[1]
+            "currentMessage": imaEventLogScan.extractEventArg( joEvent.args[0] ),
+            "previousOutgoingMessageBlockId": imaEventLogScan.extractEventArg( joEvent.args[1] )
         };
         const joReferenceLogRecord = {
             "currentMessage": eventValuesByName.currentMessage,
@@ -98,9 +96,8 @@ async function findOutReferenceLogRecord(
 }
 
 async function findOutAllReferenceLogRecords(
-    details, strLogPrefix,
-    ethersProvider, joMessageProxy,
-    bnBlockId, nIncMsgCnt, nOutMsgCnt, isVerbose
+    details, strLogPrefix, ethersProvider, joMessageProxy, joABI,
+    bnBlockId, nIncMsgCnt, nOutMsgCnt, isVerbose, optsChainPair
 ) {
     if( isVerbose ) {
         if( log.verboseGet() >= log.verboseReversed().debug ) {
@@ -128,12 +125,9 @@ async function findOutAllReferenceLogRecords(
     let nWalkMsgNumber = nOutMsgCnt - 1;
     let nWalkBlockId = bnBlockId;
     for( ; nWalkMsgNumber >= nIncMsgCnt; -- nWalkMsgNumber ) {
-        const joReferenceLogRecord =
-            await findOutReferenceLogRecord(
-                details, strLogPrefix,
-                ethersProvider, joMessageProxy,
-                nWalkBlockId, nWalkMsgNumber, isVerbose
-            );
+        const joReferenceLogRecord = await findOutReferenceLogRecord(
+            details, strLogPrefix, ethersProvider, joMessageProxy, joABI,
+            nWalkBlockId, nWalkMsgNumber, isVerbose, optsChainPair );
         if( joReferenceLogRecord == null )
             break;
         nWalkBlockId = owaspUtils.toBN( joReferenceLogRecord.previousOutgoingMessageBlockId );
@@ -287,11 +281,11 @@ async function doQueryOutgoingMessageCounter( optsTransfer ) {
             optsTransfer.strActionName =
                 "in-getOutgoingMessagesCounter()--findOutAllReferenceLogRecords()";
             optsTransfer.arrLogRecordReferences =
-                await findOutAllReferenceLogRecords(
-                    optsTransfer.details, optsTransfer.strLogPrefixShort,
-                    optsTransfer.ethersProviderSrc, optsTransfer.joMessageProxySrc,
-                    bnBlockId, optsTransfer.nIncMsgCnt, optsTransfer.nOutMsgCnt, true
-                );
+                await findOutAllReferenceLogRecords( optsTransfer.details,
+                    optsTransfer.strLogPrefixShort, optsTransfer.ethersProviderSrc,
+                    optsTransfer.joMessageProxySrc, optsTransfer.joMessageProxySrcABI,
+                    bnBlockId, optsTransfer.nIncMsgCnt, optsTransfer.nOutMsgCnt, true,
+                    optsTransfer.optsChainPair );
             return true; // success, finish at this point
         } catch ( err ) {
             optsTransfer.arrLogRecordReferences = [];
@@ -326,15 +320,14 @@ async function doQueryOutgoingMessageCounter( optsTransfer ) {
         ++ nWalkMsgNumber
     ) {
         const joFilter = optsTransfer.joMessageProxySrc.filters[strEventName](
-            owaspUtils.ethersMod.ethers.utils.id( optsTransfer.chainIdDst ), // dstChainHash
+            owaspUtils.ethersMod.ethers.utils.id( optsTransfer.chainIdDst ),
             owaspUtils.toBN( nWalkMsgNumber )
         );
-        const arrLogRecordReferencesWalk = await imaEventLogScan.safeGetPastEventsProgressive(
-            optsTransfer.details, optsTransfer.strLogPrefixShort,
-            optsTransfer.ethersProviderSrc, attempts, optsTransfer.joMessageProxySrc,
-            strEventName,
-            nBlockFrom, nBlockTo, joFilter
-        );
+        const arrLogRecordReferencesWalk = await imaEventLogScan
+            .safeGetPastEventsProgressiveExternal( optsTransfer.details,
+                optsTransfer.strLogPrefixShort, optsTransfer.ethersProviderSrc, attempts,
+                optsTransfer.joMessageProxySrc, optsTransfer.joMessageProxySrcABI, strEventName,
+                nBlockFrom, nBlockTo, joFilter, optsTransfer.optsChainPair );
         optsTransfer.arrLogRecordReferences =
             optsTransfer.arrLogRecordReferences.concat( arrLogRecordReferencesWalk );
     }
@@ -361,11 +354,11 @@ async function analyzeGatheredRecords( optsTransfer, r ) {
                 cc.debug( " with data " ) + cc.j( joEvent ) + "\n" );
         }
         const eventValuesByName = {
-            "dstChainHash": joEvent.args[0],
-            "msgCounter": joEvent.args[1],
-            "srcContract": joEvent.args[2],
-            "dstContract": joEvent.args[3],
-            "data": joEvent.args[4]
+            "dstChainHash": imaEventLogScan.extractEventArg( joEvent.args[0] ),
+            "msgCounter": imaEventLogScan.extractEventArg( joEvent.args[1] ),
+            "srcContract": imaEventLogScan.extractEventArg( joEvent.args[2] ),
+            "dstContract": imaEventLogScan.extractEventArg( joEvent.args[3] ),
+            "data": imaEventLogScan.extractEventArg( joEvent.args[4] )
         };
         if( eventValuesByName.dstChainHash == strChainHashWeAreLookingFor ) {
             joValues = eventValuesByName;
@@ -432,13 +425,13 @@ async function gatherMessages( optsTransfer ) {
                 cc.notice( optsTransfer.strActionName ) + cc.debug( " for " ) +
                 cc.info( strEventName ) + cc.debug( " event..." ) + "\n" );
         }
-        r = await imaEventLogScan.safeGetPastEventsProgressive(
-            optsTransfer.details, optsTransfer.strLogPrefixShort, optsTransfer.ethersProviderSrc,
-            10, optsTransfer.joMessageProxySrc, strEventName, nBlockFrom, nBlockTo,
-            optsTransfer.joMessageProxySrc.filters[strEventName](
-                owaspUtils.ethersMod.ethers.utils.id( optsTransfer.chainNameDst ), // dstChainHash
+        r = await imaEventLogScan.safeGetPastEventsProgressiveExternal( optsTransfer.details,
+            optsTransfer.strLogPrefixShort, optsTransfer.ethersProviderSrc, 10,
+            optsTransfer.joMessageProxySrc, optsTransfer.joMessageProxySrcABI, strEventName,
+            nBlockFrom, nBlockTo, optsTransfer.joMessageProxySrc.filters[strEventName](
+                owaspUtils.ethersMod.ethers.utils.id( optsTransfer.chainNameDst ),
                 owaspUtils.toBN( optsTransfer.nIdxCurrentMsg )
-            ) );
+            ), optsTransfer.optsChainPair );
         const joValues = await analyzeGatheredRecords( optsTransfer, r );
         if( joValues == null )
             return false;
@@ -450,8 +443,8 @@ async function gatherMessages( optsTransfer ) {
                 const transactionHash = r[0].transactionHash;
                 if( log.verboseGet() >= log.verboseReversed().debug ) {
                     optsTransfer.details.write( optsTransfer.strLogPrefix +
-                        cc.debug( "Event transactionHash is " ) + cc.info( transactionHash ) +
-                        "\n" );
+                        cc.debug( "Event transactionHash is " ) +
+                        cc.info( transactionHash ) + "\n" );
                 }
                 const blockNumber = r[0].blockNumber;
                 optsTransfer.details.write( optsTransfer.strLogPrefix +
@@ -477,10 +470,10 @@ async function gatherMessages( optsTransfer ) {
                 if( log.verboseGet() >= log.verboseReversed().critical ) {
                     const strError = owaspUtils.extractErrorMessage( err );
                     const s = optsTransfer.strLogPrefix + cc.fatal( "CRITICAL ERROR:" ) +
-                        cc.error( " Exception(evaluate block depth) while " +
-                            "getting transaction hash and block number during " +
-                        optsTransfer.strActionName + ": " ) + cc.error( strError ) +
-                        cc.error( ", stack is: " ) + "\n" + cc.stack( err.stack ) + "\n";
+                        cc.error( " Exception(evaluate block depth) while getting transaction " +
+                        "hash and block number during " + optsTransfer.strActionName + ": " ) +
+                        cc.error( strError ) + cc.error( ", stack is: " ) + "\n" +
+                        cc.stack( err.stack ) + "\n";
                     optsTransfer.details.write( s );
                     if( log.id != optsTransfer.details.id )
                         log.write( s );
@@ -585,9 +578,7 @@ async function gatherMessages( optsTransfer ) {
                 cc.info( optsTransfer.nIdxCurrentMsg ) + cc.success( " and " ) +
                 cc.notice( "dstChain" ) + cc.success( " set to " ) +
                 cc.info( optsTransfer.chainNameDst ) + cc.success( ", event description: " ) +
-                cc.j( joValues ) +
-                // + cc.j(evs) +
-                "\n" );
+                cc.j( joValues ) + "\n" );
             optsTransfer.details.write( optsTransfer.strLogPrefix +
                 cc.debug( "Will process message counter value " ) +
                 cc.info( optsTransfer.nIdxCurrentMsg ) + "\n" );
@@ -898,6 +889,7 @@ async function handleAllMessagesSigning( optsTransfer ) {
 }
 
 async function checkOutgoingMessageEvent( optsTransfer, joSChain ) {
+    const sc = optsTransfer.imaState.chainProperties.sc;
     const cntNodes = joSChain.data.computed.nodes.length;
     const cntMessages = optsTransfer.jarrMessages.length;
     for( let idxMessage = 0; idxMessage < cntMessages; ++ idxMessage ) {
@@ -930,25 +922,20 @@ async function checkOutgoingMessageEvent( optsTransfer, joSChain ) {
                     // eslint-disable-next-line dot-notation
                     const ethersProviderNode =
                         owaspUtils.getEthersProviderFromURL( strUrlHttp );
-                    const joMessageProxyNode =
-                        new owaspUtils.ethersMod.ethers.Contract(
-                            optsTransfer.imaState.chainProperties.sc
-                                .joAbiIMA.message_proxy_chain_address,
-                            optsTransfer.imaState.chainProperties.sc
-                                .joAbiIMA.message_proxy_chain_abi,
-                            ethersProviderNode
-                        );
+                    const joMessageProxyNode = new owaspUtils.ethersMod.ethers.Contract(
+                        sc.joAbiIMA.message_proxy_chain_address,
+                        sc.joAbiIMA.message_proxy_chain_abi,
+                        ethersProviderNode );
+                    const joMessageProxyNodeABI = sc.joAbiIMA.message_proxy_chain_abi;
                     const strEventName = "OutgoingMessage";
-                    const node_r = await imaEventLogScan.safeGetPastEventsProgressive(
-                        optsTransfer.details, optsTransfer.strLogPrefixShort,
-                        ethersProviderNode, 10, joMessageProxyNode, strEventName,
+                    const node_r = await imaEventLogScan.safeGetPastEventsProgressiveExternal(
+                        optsTransfer.details, optsTransfer.strLogPrefixShort, ethersProviderNode,
+                        10, joMessageProxyNode, joMessageProxyNodeABI, strEventName,
                         joMessage.savedBlockNumberForOptimizations,
                         joMessage.savedBlockNumberForOptimizations,
                         joMessageProxyNode.filters[strEventName](
                             owaspUtils.ethersMod.ethers.utils.id( optsTransfer.chainNameDst ),
-                            owaspUtils.toBN( idxImaMessage )
-                        )
-                    );
+                            owaspUtils.toBN( idxImaMessage ) ), optsTransfer.optsChainPair );
                     const cntEvents = node_r.length;
                     if( log.verboseGet() >= log.verboseReversed().trace ) {
                         optsTransfer.details.write( optsTransfer.strLogPrefix + cc.debug( "Got " ) +
@@ -960,11 +947,11 @@ async function checkOutgoingMessageEvent( optsTransfer, joSChain ) {
                     for( let idxEvent = 0; idxEvent < cntEvents; ++ idxEvent ) {
                         const joEvent = node_r[idxEvent];
                         const eventValuesByName = {
-                            "dstChainHash": joEvent.args[0],
-                            "msgCounter": joEvent.args[1],
-                            "srcContract": joEvent.args[2],
-                            "dstContract": joEvent.args[3],
-                            "data": joEvent.args[4]
+                            "dstChainHash": imaEventLogScan.extractEventArg( joEvent.args[0] ),
+                            "msgCounter": imaEventLogScan.extractEventArg( joEvent.args[1] ),
+                            "srcContract": imaEventLogScan.extractEventArg( joEvent.args[2] ),
+                            "dstContract": imaEventLogScan.extractEventArg( joEvent.args[3] ),
+                            "data": imaEventLogScan.extractEventArg( joEvent.args[4] )
                         };
                         if( owaspUtils.ensureStartsWith0x(
                             joMessage.sender ).toLowerCase() ==
@@ -1276,23 +1263,26 @@ let gIsOneTransferInProgressInThisThread = false;
 
 export async function doTransfer(
     strDirection, joRuntimeOpts,
-    ethersProviderSrc, joMessageProxySrc, joAccountSrc,
-    ethersProviderDst, joMessageProxyDst, joAccountDst,
+    ethersProviderSrc, joMessageProxySrc, joMessageProxySrcABI, joAccountSrc,
+    ethersProviderDst, joMessageProxyDst, joMessageProxyDstABI, joAccountDst,
     chainNameSrc, chainNameDst, chainIdSrc, chainIdDst,
     joDepositBoxMainNet, // for logs validation on mainnet
     joTokenManagerSChain, // for logs validation on s-chain
     nTransactionsCountInBlock,
     nTransferSteps, nMaxTransactionsCount, nBlockAwaitDepth, nBlockAge,
-    fnSignMessages, joExtraSignOpts, transactionCustomizerDst
+    fnSignMessages, joExtraSignOpts, transactionCustomizerDst, optsChainPair
 ) {
     const optsTransfer = {
         strDirection: strDirection,
         joRuntimeOpts: joRuntimeOpts,
+        optsChainPair: optsChainPair,
         ethersProviderSrc: ethersProviderSrc,
         joMessageProxySrc: joMessageProxySrc,
+        joMessageProxySrcABI: joMessageProxySrcABI,
         joAccountSrc: joAccountSrc,
         ethersProviderDst: ethersProviderDst,
         joMessageProxyDst: joMessageProxyDst,
+        joMessageProxyDstABI: joMessageProxyDstABI,
         joAccountDst: joAccountDst,
         chainNameSrc: chainNameSrc,
         chainNameDst: chainNameDst,
@@ -1471,6 +1461,7 @@ export async function doAllS2S( // s-chain --> s-chain
     skaleObserver,
     ethersProviderDst,
     joMessageProxyDst,
+    joMessageProxyDstABI,
     joAccountDst,
     chainNameDst,
     chainIdDst,
@@ -1495,7 +1486,8 @@ export async function doAllS2S( // s-chain --> s-chain
     }
     for( let idxSChain = 0; idxSChain < cntSChains; ++ idxSChain ) {
         const joSChain = arrSChainsCached[idxSChain];
-        const urlSrc = skaleObserver.pickRandomSChainUrl( joSChain );
+        const joPickResult = skaleObserver.pickRandomSChainIndexAndNodeAndUrl( joSChain );
+        const urlSrc = joPickResult.strURL;
         const ethersProviderSrc = owaspUtils.getEthersProviderFromURL( urlSrc );
         const joAccountSrc = joAccountDst; // ???
         const chainNameSrc = "" + joSChain.data.name;
@@ -1524,6 +1516,8 @@ export async function doAllS2S( // s-chain --> s-chain
                             imaState.chainProperties.sc.joAbiIMA.message_proxy_chain_abi,
                             ethersProviderSrc
                         );
+                    const joMessageProxySrcABI =
+                        imaState.chainProperties.sc.joAbiIMA.message_proxy_chain_abi;
                     const joDepositBoxSrc =
                         new owaspUtils.ethersMod.ethers.Contract(
                             imaState.chainProperties.sc.joAbiIMA.message_proxy_chain_address,
@@ -1548,15 +1542,28 @@ export async function doAllS2S( // s-chain --> s-chain
                     imaState.loopState.s2s.isInProgress = true;
                     await pwa.notifyOnLoopStart( imaState, "s2s", nIndexS2S );
 
-                    bOK =
-                    await doTransfer(
+                    const optsChainPair = {
+                        "strDirection": strDirection,
+                        "chainSrc": null,
+                        "chainDst": imaState.chainProperties.sc,
+                        "optsSpecificS2S": {
+                            "joSChain": joSChain,
+                            "idxSChain": idxSChain,
+                            "urlSrc": urlSrc,
+                            "idxNode": joPickResult.idxNode,
+                            "joNode": joPickResult.joNode
+                        }
+                    };
+                    bOK = await doTransfer(
                         strDirection,
                         joRuntimeOpts,
                         ethersProviderSrc,
                         joMessageProxySrc,
+                        joMessageProxySrcABI,
                         joAccountSrc,
                         ethersProviderDst,
                         joMessageProxyDst,
+                        joMessageProxyDstABI,
                         joAccountDst,
                         chainNameSrc,
                         chainNameDst,
@@ -1571,7 +1578,8 @@ export async function doAllS2S( // s-chain --> s-chain
                         nBlockAge,
                         fnSignMessages,
                         joExtraSignOpts,
-                        transactionCustomizerDst
+                        transactionCustomizerDst,
+                        optsChainPair
                     );
                     imaState.loopState.s2s.isInProgress = false;
                     await pwa.notifyOnLoopEnd( imaState, "s2s", nIndexS2S );
diff --git a/npms/skale-observer/observer.mjs b/npms/skale-observer/observer.mjs
index 47364448e..c9146cf9c 100644
--- a/npms/skale-observer/observer.mjs
+++ b/npms/skale-observer/observer.mjs
@@ -1333,7 +1333,7 @@ export async function ensureHaveWorker( opts ) {
     };
     while( ! gClient.logicalInitComplete ) {
         if( log.verboseGet() >= log.verboseReversed().info )
-            log.write( "SNB server is not inited yet...\n" );
+            log.write( "SNB server is not initialized yet...\n" );
 
         await threadInfo.sleep( 1000 );
         gClient.send( jo );
@@ -1513,6 +1513,19 @@ export function pickRandomSChainUrl( joSChain ) {
     return "" + joNode["http_endpoint_ip"];
 }
 
+export function pickRandomSChainIndexAndNodeAndUrl( joSChain ) {
+    const idxNode = pickRandomSChainNodeIndex( joSChain );
+    const joNode = joSChain.data.computed.nodes[idxNode];
+    // eslint-disable-next-line dot-notation
+    const strURL = "" + joNode["http_endpoint_ip"];
+    const joPickResult = {
+        "strURL": strURL,
+        "joNode": joNode,
+        "idxNode": idxNode
+    };
+    return joPickResult;
+}
+
 export async function discoverChainId( strURL ) {
     let ret = null;
     const rpcCallOpts = null;


Test patch:
diff --git a/agent/test/agentUnitTests.js b/agent/test/agentUnitTests.js
index 00158e3bd..d142e5487 100644
--- a/agent/test/agentUnitTests.js
+++ b/agent/test/agentUnitTests.js
@@ -490,6 +490,8 @@ describe( "tests for `npms/skale-ima` 3", function() {
 
     it( "should return `false` invoke `doTransfer`", async function() {
         let joMessageProxySrc; // for `false` output
+        const joMessageProxySrcABI = null;
+        const joMessageProxyDstABI = null;
         const chainNameSrc = "test";
         const chainNameDst = "test";
         const nTransactionsCountInBlock = 4;
@@ -508,9 +510,11 @@ describe( "tests for `npms/skale-ima` 3", function() {
             joRuntimeOpts,
             ethersProviderSrc,
             joMessageProxySrc,
+            joMessageProxySrcABI,
             joAccountSrc,
             ethersProviderDst,
             joMessageProxyDst,
+            joMessageProxyDstABI,
             joAccountDst,
             chainNameSrc,
             chainNameDst,
@@ -542,15 +546,19 @@ describe( "tests for `npms/skale-ima` 3", function() {
             idxChainKnownForS2S: 0,
             cntChainsKnownForS2S: 0
         };
+        const joMessageProxySrcABI = null;
+        const joMessageProxyDstABI = null;
         // eslint-disable-next-line no-unused-expressions
         expect( await IMA.doTransfer(
             "M2S",
             joRuntimeOpts,
             ethersProviderSrc,
             joMessageProxySrc,
+            joMessageProxySrcABI,
             joAccountSrc,
             ethersProviderDst,
             joMessageProxyDst,
+            joMessageProxyDstABI,
             joAccountDst,
             chainNameSrc,
             chainNameDst,