Repository: skalenetwork/IMA (language: python)

Issue:
Send a small amount of sFuel to a receiver of IMA transfer



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml
new file mode 100644
index 000000000..66b7580a8
--- /dev/null
+++ b/.github/workflows/cla.yml
@@ -0,0 +1,37 @@
+name: "cla"
+on:
+  issue_comment:
+    types: [created]
+  pull_request_target:
+    types: [opened, closed, synchronize]
+
+jobs:
+  cla:
+    runs-on: ubuntu-latest
+    steps:
+      - name: "Get Team Members"
+        id: team
+        # github-script, v6.1.0
+        uses: actions/github-script@7a5c598405937d486b0331594b5da2b14db670da
+        with:
+          github-token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
+          result-encoding: string
+          script: |
+            const members = await github.paginate(
+              github.rest.orgs.listMembers,
+              { org: "skalenetwork" },
+            );
+            return members.map(m => m.login).join(",");
+      - name: "CLA Assistant"
+        if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
+        # Beta Release, v2.1.3-beta
+        uses: cla-assistant/github-action@ba066dbae3769e2ce93ec8cfc4fdc51b9db628ba
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }}
+        with:
+          path-to-signatures: 'signatures/version1/cla.json'
+          path-to-document: 'https://skale.network/cla.txt'
+          remote-organization-name: 'skalenetwork'
+          remote-repository-name: cla-sigs
+          allowlist: '${{ steps.team.outputs.result }},*[bot]'
diff --git a/.gitignore b/.gitignore
index 7b0773a12..d6174759c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -63,6 +63,7 @@ npms/skale-ima/node_modules
 npms/skale-log/node_modules
 npms/skale-observer/node_modules
 npms/skale-owasp/node_modules
+npms/skale-cool-socket/node_modules
 
 # TypeScript v1 declaration files
 typings/
diff --git a/VERSION b/VERSION
index d0149fef7..80e78df68 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-1.3.4
+1.3.5
diff --git a/agent/CLI.md b/agent/CLI.md
new file mode 100644
index 000000000..4a8f572b7
--- /dev/null
+++ b/agent/CLI.md
@@ -0,0 +1,293 @@
+<!-- SPDX-License-Identifier: (AGPL-3.0-only OR CC-BY-4.0) -->
+
+# IMA Command Line Interface
+
+**IMA** runs message transfer loop between **Ethereum(Main Net)** and **S-Chain**, also between assigned **S-Chain** and all connected to it other **S-Chains**. IMA also offers set of test tasks in its CLI.
+
+IMA operates with 3 types of chains:
+
+- **Ethereum(Main Net)**
+- **S-Chain**, source **S-Chain**
+- **T-Chain**, target **S-Chain**
+
+In the most of use cases only **Ethereum(Main Net)** and source **S-Chain** are needed.
+
+**IMA** supports the following groups of operations:
+
+- Run message loop.
+- Perform **S-Chain** registration and initialization.
+- Confiure and change gas reimbursement.
+- Do **ETH**, **ERC20**, **ERC721**, **ERC1155**, batch **ERC1155** payments between chains.
+- View amount of **ETH** can be received on Main Net.
+- Mint **ERC20**, **ERC721**, **ERC1155** tokens.
+- Show **ETH**, **ERC20**, **ERC721**, **ERC1155** balances.
+- Browse **SKALE network**.
+- Download source **S-Chain** information.
+- Discover set of **S-Chains** connected to specifed **S-Chains**.
+- Discover chain ID of specified chain with `--discover-cid`.
+- Run monitoring service and expose its JSON RPC, the `--monitoring-port=number` option turns on monitoring web socket RPC server on specified port. By default monitoring server is disabled.
+
+Here is list of options running operations described above:
+
+```
+--show-config...................Show configuration values and exit.
+--show-balance..................Show ETH and/or token balances on Main-net and/or S-Chain and exit.
+--m2s-payment...................Do one payment from Main-net user account to S-chain user account.
+--s2m-payment...................Do one payment from S-chain user account to Main-net user account.
+--s2m-receive...................Receive one payment from S-chain user account to Main-net user account(ETH only, receives all the ETH pending in transfer).
+--s2m-view......................View money amount user can receive as payment from S-chain user account to Main-net user account(ETH only, receives all the ETH pending in transfer).
+--s2s-payment...................Do one payment from S-chain user account to other S-chain user account.
+--s2s-forward...................Indicates S<->S transfer direction is forward. I.e. source S-chain is token minter and instantiator. This is default mode.
+--s2s-reverse...................Indicates S<->S transfer direction is reverse. I.e. destination S-chain is token minter and instantiator.
+--m2s-transfer..................Do single money transfer loop from Main-net to S-chain.
+--s2m-transfer..................Do single money transfer loop from S-chain to Main-net.
+--s2s-transfer..................Do single money transfer loop from S-chain to S-chain.
+--with-metadata.................Makes ERC721 transfer using special version of Token Manager to transfer token metadata.
+--transfer......................Run single M<->S and, optionally, S->S transfer loop iteration.
+--loop..........................Run M<->S and, optionally, S->S transfer loop.
+--browse-s-chain................Download own S-Chain network information.
+--browse-skale-network..........Download entire SKALE network description.
+--browse-connected-schains......Download S-Chains conected to S-Chain with name specified in id-s-chain command line parameter.
+--mint-erc20....................Mint ERC20 tokens.
+--mint-erc721...................Mint ERC721 tokens.
+--mint-erc1155..................Mint ERC1155 tokens.
+--burn-erc20....................Burn ERC20 tokens.
+--burn-erc721...................Burn ERC721 tokens.
+--burn-erc1155..................Burn ERC1155 tokens.
+```
+
+Please notice, token testing commands require `--tm-url-t-chain`, `cid-t-chain`, `erc20-t-chain` or `erc721-t-chain` or `erc1155-t-chain`, account information (like private key `key-t-chain`) command line arguments specified. Token amounts are specified via amount command line arguments specified. Token IDs are specified via tid or tids command line arguments.
+
+One or more of the following URL, chain name and chain ID parameters are needed for most of **IMA** operations:
+
+```
+--url-main-net=URL..............Main-net URL. Value is automatically loaded from the URL_W3_ETHEREUM environment variable if not specified.
+--url-s-chain=URL...............S-chain URL. Value is automatically loaded from the URL_W3_S_CHAIN environment variable if not specified.
+--url-t-chain=URL...............S<->S Target S-chain URL. Value is automatically loaded from the URL_W3_S_CHAIN_TARGET environment variable if 
+--id-main-net=number............Main-net Ethereum network name.. Value is automatically loaded from the CHAIN_NAME_ETHEREUM environment variable if not specified. Default value is "Mainnet".
+--id-s-chain=number.............S-chain Ethereum network name.. Value is automatically loaded from the CHAIN_NAME_SCHAIN environment variable if not specified. Default value is "id-S-chain".
+--id-t-chain=number.............S<->S Target S-chain Ethereum network name.. Value is automatically loaded from the CHAIN_NAME_SCHAIN_TARET environment variable if not specified. Default value is "id-T-chain".
+--cid-main-net=number...........Main-net Ethereum chain ID. Value is automatically loaded from the CID_ETHEREUM environment variable if not specified. Default value is -4.
+--cid-s-chain=number............S-chain Ethereum chain ID. Value is automatically loaded from the CID_SCHAIN environment variable if not specified. Default value is -4.
+--cid-t-chain=number............S<->S Target S-chain Ethereum chain ID. Value is automatically loaded from the CID_SCHAIN_TARGET environment variable if not specified. Default value is -4.
+```
+
+For most of operatons, **IMA** needs ABIs of **Skale Manager**, **Ethereum(Main Net)**, **S-Chain(s)**:
+
+```
+--abi-skale-manager=path........Path to JSON file containing Skale Manager ABI. Optional parameter. It's needed for S-Chain to S-Chain transfers.
+--abi-main-net=path.............Path to JSON file containing IMA ABI for Main-net.
+--abi-s-chain=path..............Path to JSON file containing IMA ABI for S-chain.
+--abi-t-chain=path..............Path to JSON file containing IMA ABI for S<->S Target S-chain.
+```
+
+Token transfer commands require token APIs on approproate chains.
+
+**ERC20** options:
+
+```
+--erc20-main-net=path...........Path to JSON file containing ERC20 ABI for Main-net.
+--erc20-s-chain=path............Path to JSON file containing ERC20 ABI for S-chain.
+--addr-erc20-s-chain=address....Explicit ERC20 address in S-chain.
+--erc20-t-chain=path............Path to JSON file containing ERC20 ABI for S<->S Target S-chain.
+--addr-erc20-t-chain=address....Explicit ERC20 address in S<->S Target S-chain.
+```
+
+**ERC721** options:
+
+```
+--erc721-main-net=path..........Path to JSON file containing ERC721 ABI for Main-net.
+--erc721-s-chain=path...........Path to JSON file containing ERC721 ABI for S-chain.
+--addr-erc721-s-chain=address...Explicit ERC721 address in S-chain.
+--erc721-t-chain=path...........Path to JSON file containing ERC721 ABI for S<->S S-chain.
+--addr-erc721-t-chain=address...Explicit ERC721 address in S<->S S-chain.
+```
+
+**ERC1155** options:
+
+```
+--erc1155-main-net=path.........Path to JSON file containing ERC1155 ABI for Main-net.
+--erc1155-s-chain=path..........Path to JSON file containing ERC1155 ABI for S-chain.
+--addr-erc1155-s-chain=address..Explicit ERC1155 address in S-chain.
+--erc1155-t-chain=path..........Path to JSON file containing ERC1155 ABI for S<->S S-chain.
+--addr-erc1155-t-chain=address..Explicit ERC1155 address in S<->S S-chain.
+```
+
+**IMA** can sign transactions using one of following ways:
+
+- Using **Transaction Manager** JSON RPC
+- Using **SGX wallet** JSON RPC
+- Using explicitly specified private key
+- Using wallet address, for read only operations only
+
+The folllowing parameters needed to use **Transaction Manager**:
+
+```
+--tm-url-main-net=URL...........Transaction Manager server URL for Main-net. Value is automatically loaded from the TRANSACTION_MANAGER_URL_ETHEREUM environment variable if not specified. Example: redis://@127.0.0.1:6379
+--tm-url-s-chain=URL............Transaction Manager server URL for S-chain. Value is automatically loaded from the TRANSACTION_MANAGER_URL_S_CHAIN environment variable if not specified.
+--tm-url-t-chain=URL............Transaction Manager server URL for S<->S Target S-chain. Value is automatically loaded from the TRANSACTION_MANAGER_URL_S_CHAIN_TARGET environment variable if not specified.
+--tm-priority-main-net=URL......Transaction Manager priority for Main-net. Value is automatically loaded from the TRANSACTION_MANAGER_PRIORITY_ETHEREUM environment variable if not specified. Default is 5.
+--tm-priority-s-chain=URL.......Transaction Manager priority for S-chain. Value is automatically loaded from the TRANSACTION_MANAGER_PRIORITY_S_CHAIN environment variable if not specified. Default is 5.
+--tm-priority-t-chain=URL.......Transaction Manager priority for S<->S Target S-chain. Value is automatically loaded from the TRANSACTION_MANAGER_PRIORITY_S_CHAIN_TARGET environment variable if not specified. Default is 5.
+```
+
+The folllowing parameters needed to use **SGX wallet**:
+
+```
+--sgx-url-main-net=URL..........SGX server URL for Main-net. Value is automatically loaded from the SGX_URL_ETHEREUM environment variable if not specified.
+--sgx-url-s-chain=URL...........SGX server URL for S-chain. Value is automatically loaded from the SGX_URL_S_CHAIN environment variable if not specified.
+--sgx-url-t-chain=URL...........SGX server URL for S<->S Target S-chain. Value is automatically loaded from the SGX_URL_S_CHAIN_TARGET environment variable if not specified.
+--sgx-ecdsa-key-main-net=name...SGX/ECDSA key name for Main-net. Value is automatically loaded from the SGX_KEY_ETHEREUM environment variable if not specified.
+--sgx-ecdsa-key-s-chain=name....SGX/ECDSA key name for S-chain. Value is automatically loaded from the SGX_KEY_S_CHAIN environment variable if not specified.
+--sgx-ecdsa-key-t-chain=name....SGX/ECDSA key name for S<->S Target S-chain. Value is automatically loaded from the SGX_KEY_S_CHAIN_TARGET environment variable if not specified.
+--sgx-ssl-key-main-net=path.....Path to SSL key file for SGX wallet of Main-net. Value is automatically loaded from the SGX_SSL_KEY_FILE_ETHEREUM environment variable if not specified.
+--sgx-ssl-key-s-chain=path......Path to SSL key file for SGX wallet of S-chain. Value is automatically loaded from the SGX_SSL_KEY_FILE_S_CHAIN environment variable if not specified.
+--sgx-ssl-key-t-chain=path......Path to SSL key file for SGX wallet of S<->S Target S-chain. Value is automatically loaded from the SGX_SSL_KEY_FILE_S_CHAIN_TARGET environment variable if not specified.
+--sgx-ssl-cert-main-net=path....Path to SSL certificate file for SGX wallet of Main-net. Value is automatically loaded from the SGX_SSL_CERT_FILE_ETHEREUM environment variable if not specified.
+--sgx-ssl-cert-s-chain=path.....Path to SSL certificate file for SGX wallet of S-chain. Value is automatically loaded from the SGX_SSL_CERT_FILE_S_CHAIN environment variable if not specified.
+--sgx-ssl-cert-t-chain=path.....Path to SSL certificate file for SGX wallet of S<->S Target S-chain. Value is automatically loaded from the SGX_SSL_CERT_FILE_S_CHAIN_TARGET environment variable if not specified.
+```
+
+Using explicitly specified private key:
+
+```
+--address-main-net=value........Main-net user account address. Value is automatically loaded from the ACCOUNT_FOR_ETHEREUM environment variable if not specified.
+--address-s-chain=value.........S-chain user account address. Value is automatically loaded from the ACCOUNT_FOR_SCHAIN environment variable if not specified.
+--address-t-chain=value.........S<->S Target S-chain user account address. Value is automatically loaded from the ACCOUNT_FOR_SCHAIN_TARGET environment variable if not specified.
+```
+
+For read only operations, only wallet address can be specified:
+
+```
+--key-main-net=value............Private key for Main-net user account address. Value is automatically loaded from the PRIVATE_KEY_FOR_ETHEREUM environment variable if not specified.
+--key-s-chain=value.............Private key for S-Chain user account address. Value is automatically loaded from the PRIVATE_KEY_FOR_SCHAIN environment variable if not specified.
+--key-t-chain=value.............Private key for S<->S Target S-Chain user account address. Value is automatically loaded from the PRIVATE_KEY_FOR_SCHAIN_TARGET environment variable if not specified.
+```
+
+Please notice, **IMA** prefer to use transaction manager to sign blockchain transactions if `--tm-url-main-net`/`--tm-url-s-chain` command line values or `TRANSACTION_MANAGER_URL_ETHEREUM`/`TRANSACTION_MANAGER_URL_S_CHAIN` shell variables were specified. Next preferred option is **SGX wallet** which is used if `--sgx-url-main-net`/`--sgx-url-s-chain` command line values or `SGX_URL_ETHEREUM`/`SGX_URL_S_CHAIN` shell variables were specified. SGX signing also needs key name, key and certificate files. Finally, **IMA** attempts to use explicitly provided private key to sign blockchain transactions if `--key-main-net`/`--key-s-chain` command line values or `PRIVATE_KEY_FOR_ETHEREUM`/`PRIVATE_KEY_FOR_SCHAIN` shell variables were specified. 
+
+**ETH** transfers operations require amout of **ETH** to be specied with one of the following options:
+
+```
+--value=numberunitName..........Amount of unitName to transfer, where unitName is well known Ethereum unit name like ether or wei.
+--wei=number....................Amount of wei to transfer.
+--babbage=number................Amount of babbage(wei*1000) to transfer.
+--lovelace=number...............Amount of lovelace(wei*1000*1000) to transfer.
+--shannon=number................Amount of shannon(wei*1000*1000*1000) to transfer.
+--szabo=number..................Amount of szabo(wei*1000*1000*1000*1000) to transfer.
+--finney=number.................Amount of finney(wei*1000*1000*1000*1000*1000) to transfer.
+--ether=number..................Amount of ether(wei*1000*1000*1000*1000*1000*1000) to transfer.
+```
+
+Token transfer operations require token amounts and/or token IDs:
+
+```
+--amount=number.................Amount of tokens to transfer.
+--tid=number....................ERC721 or ERC1155 token id to transfer.
+--amounts=array of numbers......ERC1155 token id to transfer in batch.
+--tids=array of numbers.........ERC1155 token amount to transfer in batch.
+--sleep-between-tx=number.......Sleep time (in milliseconds) between transactions during complex operations.
+--wait-next-block...............Wait for next block between transactions during complex operations.
+```
+
+**Gas reumbursement** can be configure with the following options:
+
+```
+--reimbursement-chain=name......Specifies chain name.
+--reimbursement-recharge=vu.....Recharge user wallet with specified value v, unit name u is well known Ethereum unit name like ether or wei.
+--reimbursement-withdraw=vu.....Withdraw user wallet with specified value v, unit name u is well known Ethereum unit name like ether or wei.
+--reimbursement-balance.........Show wallet balance.
+--reimbursement-range=number....Sets minimal time interval between transfers from S-Chain to Main Net.
+```
+
+**Gas reumbursement** can be **Oracle**-based if the following options are specified:
+
+--enable-oracle.................Enable call to Oracle to compute gas price for gas reimbursement. Default mode.
+--disable-oracle................Disable call to Oracle to compute gas price for gas reimbursement.
+
+**IMA** must be initialized and its **S-Chain** must be registered once after creation with the following options:
+
+```
+--register......................Register(perform all steps).
+--check-registration............Perform registration status check(perform all steps).
+```
+
+**S-Chain** to **S-Chain** transfers must be turned on and require periodic **SKALE network re-discovery**:
+
+```
+--s2s-enable....................Enables S-Chain to S-Chain transfers. Default mode. The abi-skale-manager path must be provided.
+--s2s-disable...................Disables S-Chain to S-Chain transfers.
+--net-rediscover=number.........SKALE NETWORK re-discovery interval(in seconds). Default is 3600 seconds or 1 hour, specify 0 to disable SKALE NETWORK re-discovery.
+```
+
+**IMA** loop can optionally use dry run, group **IMA** messages and supports varuous customizations:
+
+```
+--no-wait-s-chain...............Do not wait until S-Chain is started.
+--max-wait-attempts=value.......Max number of S-Chain call attempts to do while it became alive and sane.
+--skip-dry-run..................Skip dry run contract method calls.
+--ignore-dry-run................Ignore result of dry run contract method calls and continue execute.
+--dry-run.......................Use error results of dry run contract method calls as actual errors and stop execute.
+--m2s-transfer-block-size.......Number of transactions in one block to use in money transfer loop from Main-net to S-chain.
+--s2m-transfer-block-size.......Number of transactions in one block to use in money transfer loop from S-chain to Main-net.
+--s2s-transfer-block-size.......Number of transactions in one block to use in money transfer loop from S-chain to S-chain.
+--transfer-block-size...........Number of transactions in one block to use in both money transfer loops.
+--m2s-max-transactions..........Maximal number of transactions to do in money transfer loop from Main-net to S-chain(0 is unlimited).
+--s2m-max-transactions..........Maximal number of transactions to do in money transfer loop from S-chain to Main-net(0 is unlimited).
+--s2s-max-transactions..........Maximal number of transactions to do in money transfer loop from S-chain to S-chain(0 is unlimited).
+--max-transactions..............Maximal number of transactions to do in both money transfer loops(0 is unlimited).
+--m2s-await-blocks..............Maximal number of blocks to wait to appear in blockchain before transaction from Main-net to S-chain(0 is no wait).
+--s2m-await-blocks..............Maximal number of blocks to wait to appear in blockchain before transaction from S-chain to Main-net(0 is no wait).
+--s2s-await-blocks..............Maximal number of blocks to wait to appear in blockchain before transaction from S-chain to S-chain(0 is no wait).
+--await-blocks..................Maximal number of blocks to wait to appear in blockchain before transaction between both S-chain and Main-net(0 is no wait).
+--m2s-await-time................Minimal age of transaction message(in seconds) before it will be transferred from Main-net to S-chain(0 is no wait).
+--s2m-await-time................Minimal age of transaction message(in seconds) before it will be transferred from S-chain to Main-net(0 is no wait).
+--s2s-await-time................Minimal age of transaction message(in seconds) before it will be transferred from S-chain to S-chain(0 is no wait).
+--await-time....................Minimal age of transaction message(in seconds) before it will be transferred between both S-chain and Main-net(0 is no wait).
+--period........................Transfer loop period(in seconds).
+--node-number=value.............S-Chain node number(0-based).
+--nodes-count=value.............S-Chain nodes count.
+--time-framing=value............Specifies period(in seconds) for time framing(0 to disable time framing).
+--time-gap=value................Specifies gap(in seconds) before next time frame.
+```
+
+**IMA** transfer loop must **BLS**-sign messages and needs paths to **BLS** command line utilities:
+
+```
+--sign-messages.................Sign transferred messages.
+--bls-glue=path.................Specifies path to bls_glue application.
+--hash-g1=path..................Specifies path to hash_g1 application.
+--bls-verify=path...............Optional parameter, specifies path to verify_bls application.
+```
+
+**IMA** transfer loop needs to scan **IMA smart contract** events, scanning can be customized with the following options:
+
+```
+--bs-step-size=number...........Specifies step block range size to search iterative past events step by step. 0 to disable iterative search.
+--bs-max-all-range=number.......Specifies max number of steps to allow to search as [0...latest] range. 0 to disable iterative search.
+--bs-progressive-enable.........Enables progressive block scan to search past events.
+--bs-progressive-disable........Disables progressive block scan to search past events.
+```
+
+Like any command line application, **IMA** produces various command line output and supports logging. Logging can be customized with the following options:
+
+```
+--expose........................Expose low-level log details after successful operations. By default details exposed only on errors.
+--no-expose.....................Expose low-level log details only after errors. Default expose mode.
+--verbose=value.................Set level of output details.
+--verbose-list..................List available verbose levels and exit.
+--log=path......................Write program output to specified log file(multiple files can be specified).
+--log-size=value................Max size(in bytes) of one log file(affects to log log rotation).
+--log-files=value...............Maximum number of log files for log rotation.
+--gathered......................Print details of gathering data from command line arguments. Default mode.
+--no-gathered...................Do not print details of gathering data from command line arguments.
+--expose-security-info..........Expose security-related values in log output. This mode is needed for debugging purposes only.
+--no-expose-security-info.......Do not expose security-related values in log output. Default mode.
+```
+
+Command line output and loggging can be plain or ANSI-colorized:
+
+```
+--colors........................Use ANSI-colorized logging.
+--no-colors.....................Use monochrome logging.
+```
\ No newline at end of file
diff --git a/agent/bls.js b/agent/bls.js
index 95371c189..4442863d8 100644
--- a/agent/bls.js
+++ b/agent/bls.js
@@ -30,6 +30,8 @@
 const child_process = require( "child_process" );
 const shell = require( "shelljs" );
 const { Keccak } = require( "sha3" );
+const { cc } = require( "./utils" );
+const owaspUtils = require( "../npms/skale-owasp/owasp-util" );
 
 function init() {
     owaspUtils.owaspAddUsageRef();
@@ -44,7 +46,7 @@ async function with_timeout( strDescription, promise, seconds ) {
     let result_err = null, isComplete = false;
     promise.catch( function( err ) {
         isComplete = true;
-        result_err = new Error( strDescription + "error: " + err.toString() );
+        result_err = new Error( strDescription + "error: " + owaspUtils.extract_error_message( err ) );
     } ).finally( function() {
         isComplete = true;
     } );
@@ -61,6 +63,8 @@ async function with_timeout( strDescription, promise, seconds ) {
 
 function discover_bls_threshold( joSChainNetworkInfo ) {
     joSChainNetworkInfo = joSChainNetworkInfo || imaState.joSChainNetworkInfo;
+    if( ! joSChainNetworkInfo )
+        return -1;
     const jarrNodes = joSChainNetworkInfo.network;
     for( let i = 0; i < jarrNodes.length; ++i ) {
         const joNode = jarrNodes[i];
@@ -75,6 +79,8 @@ function discover_bls_threshold( joSChainNetworkInfo ) {
 
 function discover_bls_participants( joSChainNetworkInfo ) {
     joSChainNetworkInfo = joSChainNetworkInfo || imaState.joSChainNetworkInfo;
+    if( ! joSChainNetworkInfo )
+        return -1;
     const jarrNodes = joSChainNetworkInfo.network;
     for( let i = 0; i < jarrNodes.length; ++i ) {
         const joNode = jarrNodes[i];
@@ -361,7 +367,7 @@ function perform_bls_glue(
         //
         fnShellRestore();
     } catch ( err ) {
-        const s1 = strLogPrefix + cc.fatal( "BLS glue CRITICAL ERROR:" ) + cc.error( " error description is: " ) + cc.warning( err.toString() ) + "\n";
+        const s1 = strLogPrefix + cc.fatal( "BLS glue CRITICAL ERROR:" ) + cc.error( " error description is: " ) + cc.warning( owaspUtils.extract_error_message( err ) ) + "\n";
         const s2 = strLogPrefix + cc.error( "BLS glue output is:\n" ) + cc.notice( strOutput ) + "\n";
         log.write( s1 );
         details.write( s1 );
@@ -460,7 +466,7 @@ function perform_bls_glue_u256( details, u256, arrSignResults ) {
         //
         fnShellRestore();
     } catch ( err ) {
-        const s1 = strLogPrefix + cc.fatal( "BLS glue CRITICAL ERROR:" ) + cc.error( " error description is: " ) + cc.warning( err.toString() ) + "\n";
+        const s1 = strLogPrefix + cc.fatal( "BLS glue CRITICAL ERROR:" ) + cc.error( " error description is: " ) + cc.warning( owaspUtils.extract_error_message( err ) ) + "\n";
         const s2 = strLogPrefix + cc.error( "BLS glue output is:\n" ) + cc.notice( strOutput ) + "\n";
         log.write( s1 );
         details.write( s1 );
@@ -524,7 +530,7 @@ function perform_bls_verify_i(
         fnShellRestore();
         return true;
     } catch ( err ) {
-        const s1 = strLogPrefix + cc.fatal( "CRITICAL ERROR:" ) + cc.error( " BLS node " ) + cc.notice( "#" ) + cc.info( nZeroBasedNodeIndex ) + cc.error( " verify error:" ) + cc.normal( " error description is: " ) + cc.warning( err.toString() ) + "\n";
+        const s1 = strLogPrefix + cc.fatal( "CRITICAL ERROR:" ) + cc.error( " BLS node " ) + cc.notice( "#" ) + cc.info( nZeroBasedNodeIndex ) + cc.error( " verify error:" ) + cc.normal( " error description is: " ) + cc.warning( owaspUtils.extract_error_message( err ) ) + "\n";
         const s2 = strLogPrefix + cc.error( "CRITICAL ERROR:" ) + cc.error( " BLS node " ) + cc.notice( "#" ) + cc.info( nZeroBasedNodeIndex ) + cc.error( " verify output is:\n" ) + cc.notice( strOutput ) + "\n";
         log.write( s1 );
         details.write( s1 );
@@ -550,6 +556,7 @@ function perform_bls_verify_i_u256( details, nZeroBasedNodeIndex, joResultFromNo
     let strOutput = "";
     try {
         shell.cd( strActionDir );
+        //
         const joMsg = { message: keccak256_u256( u256, true ) };
         details.write( strLogPrefix + cc.debug( "BLS u256 node " ) + cc.notice( "#" ) + cc.info( nZeroBasedNodeIndex ) + cc.debug( " verify message " ) + cc.j( joMsg ) + cc.debug( " composed from " ) + cc.j( u256 ) + cc.debug( " using glue " ) + cc.j( joResultFromNode ) + cc.debug( " and public key " ) + cc.j( joPublicKey ) + "\n" );
         const strSignResultFileName = strActionDir + "/sign-result" + nZeroBasedNodeIndex + ".json";
@@ -573,7 +580,7 @@ function perform_bls_verify_i_u256( details, nZeroBasedNodeIndex, joResultFromNo
         fnShellRestore();
         return true;
     } catch ( err ) {
-        const s1 = strLogPrefix + cc.fatal( "CRITICAL ERROR:" ) + cc.error( " BLS u256 node " ) + cc.notice( "#" ) + cc.info( nZeroBasedNodeIndex ) + cc.error( " verify error:" ) + cc.normal( " error description is: " ) + cc.warning( err.toString() ) + "\n";
+        const s1 = strLogPrefix + cc.fatal( "CRITICAL ERROR:" ) + cc.error( " BLS u256 node " ) + cc.notice( "#" ) + cc.info( nZeroBasedNodeIndex ) + cc.error( " verify error:" ) + cc.normal( " error description is: " ) + cc.warning( owaspUtils.extract_error_message( err ) ) + "\n";
         const s2 = strLogPrefix + cc.error( "CRITICAL ERROR:" ) + cc.error( " BLS u256 node " ) + cc.notice( "#" ) + cc.info( nZeroBasedNodeIndex ) + cc.error( " verify output is:\n" ) + cc.notice( strOutput ) + "\n";
         log.write( s1 );
         details.write( s1 );
@@ -642,7 +649,7 @@ function perform_bls_verify(
         fnShellRestore();
         return true;
     } catch ( err ) {
-        const s1 = strLogPrefix + cc.fatal( "BLS/summary verify CRITICAL ERROR:" ) + cc.normal( " error description is: " ) + cc.warning( err.toString() ) + "\n";
+        const s1 = strLogPrefix + cc.fatal( "BLS/summary verify CRITICAL ERROR:" ) + cc.normal( " error description is: " ) + cc.warning( owaspUtils.extract_error_message( err ) ) + "\n";
         const s2 = strLogPrefix + cc.error( "BLS/summary verify output is:\n" ) + cc.notice( strOutput ) + "\n";
         log.write( s1 );
         details.write( s1 );
@@ -700,7 +707,7 @@ function perform_bls_verify_u256( details, joGlueResult, u256, joCommonPublicKey
         fnShellRestore();
         return true;
     } catch ( err ) {
-        const s1 = strLogPrefix + cc.fatal( "BLS u256/summary verify CRITICAL ERROR:" ) + cc.normal( " error description is: " ) + cc.warning( err.toString() ) + "\n";
+        const s1 = strLogPrefix + cc.fatal( "BLS u256/summary verify CRITICAL ERROR:" ) + cc.normal( " error description is: " ) + cc.warning( owaspUtils.extract_error_message( err ) ) + "\n";
         const s2 = strLogPrefix + cc.error( "BLS u256/summary verify output is:\n" ) + cc.notice( strOutput ) + "\n";
         log.write( s1 );
         details.write( s1 );
@@ -784,7 +791,7 @@ async function check_correctness_of_messages_to_sign( details, strLogPrefix, str
                     cc.error( " Correctness validation failed for message " ) + cc.info( idxMessage ) +
                     cc.error( " sent to " ) + cc.info( joChainName ) +
                     cc.error( ", message is: " ) + cc.j( joMessage ) +
-                    cc.error( ", error information: " ) + cc.warning( err.toString() ) +
+                    cc.error( ", error information: " ) + cc.warning( owaspUtils.extract_error_message( err ) ) +
                     "\n";
                 log.write( s );
                 details.write( s );
@@ -918,18 +925,20 @@ async function do_sign_messages_impl(
                 cc.normal( "(" ) + cc.bright( i ) + cc.normal( "/" ) + cc.bright( jarrNodes.length ) + cc.normal( ", ID " ) + cc.info( joNode.nodeID ) + cc.normal( ")" ) +
                 cc.normal( ", " ) + cc.notice( "sequence ID" ) + cc.normal( " is " ) + cc.attention( sequence_id );
             const rpcCallOpts = null;
-            await rpcCall.create( strNodeURL, rpcCallOpts, async function( joCall, err ) {
+            /*await*/ rpcCall.create( strNodeURL, rpcCallOpts, async function( joCall, err ) {
                 if( err ) {
                     ++joGatheringTracker.nCountReceived; // including errors
                     ++joGatheringTracker.nCountErrors;
                     const strErrorMessage =
                         strLogPrefix + cc.fatal( "CRITICAL ERROR:" ) +
                         cc.error( " JSON RPC call to S-Chain node " ) + strNodeDescColorized +
-                        cc.error( " failed, RPC call was not created, error: " ) + cc.warning( err ) +
+                        cc.error( " failed, RPC call was not created, error is: " ) + cc.warning( owaspUtils.extract_error_message( err ) ) +
                         cc.error( ", " ) + cc.notice( "sequence ID" ) + cc.error( " is " ) + cc.attention( sequence_id ) +
                         "\n";
                     log.write( strErrorMessage );
                     details.write( strErrorMessage );
+                    if( joCall )
+                        await joCall.disconnect();
                     return;
                 }
                 let targetChainName = "";
@@ -951,8 +960,10 @@ async function do_sign_messages_impl(
                     fromChainName = "" + joExtraSignOpts.chain_id_src;
                     // targetChainURL = owaspUtils.w3_2_url( joExtraSignOpts.w3_dst );
                     // fromChainURL = owaspUtils.w3_2_url( joExtraSignOpts.w3_src );
-                } else
+                } else {
+                    await joCall.disconnect();
                     throw new Error( "CRITICAL ERROR: Failed do_sign_messages_impl() with unknown directon \"" + strDirection + "\"" );
+                }
 
                 const joParams = {
                     direction: "" + strDirection,
@@ -986,11 +997,12 @@ async function do_sign_messages_impl(
                         const strErrorMessage =
                             strLogPrefix + cc.fatal( "CRITICAL ERROR:" ) +
                             cc.error( " JSON RPC call to S-Chain node " ) + strNodeDescColorized +
-                            cc.error( " failed, RPC call reported error: " ) + cc.warning( err ) +
+                            cc.error( " failed, RPC call reported error: " ) + cc.warning( owaspUtils.extract_error_message( err ) ) +
                             cc.error( ", " ) + cc.notice( "sequence ID" ) + cc.error( " is " ) + cc.attention( sequence_id ) +
                             "\n";
                         log.write( strErrorMessage );
                         details.write( strErrorMessage );
+                        await joCall.disconnect();
                         return;
                     }
                     details.write(
@@ -1004,25 +1016,16 @@ async function do_sign_messages_impl(
                         "\n" );
                     if( joOut.result == null || joOut.result == undefined || ( !typeof joOut.result == "object" ) ) {
                         ++joGatheringTracker.nCountErrors;
-                        if( "error" in joOut && "message" in joOut.error ) {
-                            const strErrorMessage =
-                                strLogPrefix + cc.fatal( "Wallet CRITICAL ERROR:" ) + " " +
-                                cc.error( "S-Chain node " ) + strNodeDescColorized +
-                                cc.error( " reported wallet error: " ) + cc.warning( joOut.error.message ) +
-                                cc.error( ", " ) + cc.notice( "sequence ID" ) + cc.error( " is " ) + cc.attention( sequence_id ) +
-                                "\n";
-                            log.write( strErrorMessage );
-                            details.write( strErrorMessage );
-                        } else {
-                            const strErrorMessage =
-                                strLogPrefix + cc.fatal( "Wallet CRITICAL ERROR:" ) + " " +
-                                cc.error( "JSON RPC call to S-Chain node " ) + strNodeDescColorized +
-                                cc.error( " failed with " ) + cc.warning( "unknown wallet error" ) +
-                                cc.error( ", " ) + cc.notice( "sequence ID" ) + cc.error( " is " ) + cc.attention( sequence_id ) +
-                                "\n";
-                            log.write( strErrorMessage );
-                            details.write( strErrorMessage );
-                        }
+                        const strErrorMessage =
+                            strLogPrefix + cc.fatal( "Wallet CRITICAL ERROR:" ) + " " +
+                            cc.error( "S-Chain node " ) + strNodeDescColorized +
+                            cc.error( " reported wallet error: " ) +
+                            cc.warning( owaspUtils.extract_error_message( joOut, "unknown wallet error(1)" ) ) +
+                            cc.error( ", " ) + cc.notice( "sequence ID" ) + cc.error( " is " ) + cc.attention( sequence_id ) +
+                            "\n";
+                        log.write( strErrorMessage );
+                        details.write( strErrorMessage );
+                        await joCall.disconnect();
                         return;
                     }
                     details.write( strLogPrefix + cc.normal( "Node " ) + cc.info( joNode.nodeID ) + cc.normal( " sign result: " ) + cc.j( joOut.result ? joOut.result : null ) + "\n" );
@@ -1043,6 +1046,7 @@ async function do_sign_messages_impl(
                                         cc.debug( " because " ) + cc.info( nThreshold ) + cc.debug( "/" ) + cc.info( nCountOfBlsPartsToCollect ) +
                                         cc.debug( " threshold number of BLS signature parts already gathered" ) +
                                         "\n" );
+                                    await joCall.disconnect();
                                     return;
                                 }
                                 const arrTmp = joOut.result.signResult.signatureShare.split( ":" );
@@ -1065,15 +1069,13 @@ async function do_sign_messages_impl(
                                 ) ) {
                                     details.write( strLogPrefixA + cc.success( "Got successful BLS verification result for node " ) + cc.info( joNode.nodeID ) + cc.success( " with index " ) + cc.info( nZeroBasedNodeIndex ) + "\n" );
                                     bNodeSignatureOKay = true; // node verification passed
-                                } else {
-                                    const strError = "BLS verify failed";
-                                    details.write( strLogPrefixA + cc.fatal( "CRITICAL ERROR:" ) + " " + cc.error( strError ) + "\n" );
-                                }
+                                } else
+                                    details.write( strLogPrefixA + cc.fatal( "CRITICAL ERROR:" ) + " " + cc.error( "BLS verification failed" ) + "\n" );
                             } catch ( err ) {
                                 const strErrorMessage =
                                     strLogPrefixA + cc.error( "S-Chain node " ) + strNodeDescColorized + cc.error( " sign " ) +
                                     cc.error( " CRITICAL ERROR:" ) + cc.error( " partial signature fail from with index " ) + cc.info( nZeroBasedNodeIndex ) +
-                                    cc.error( ", error is " ) + cc.warning( err.toString() ) +
+                                    cc.error( ", error is " ) + cc.warning( owaspUtils.extract_error_message( err ) ) +
                                     cc.error( ", " ) + cc.notice( "sequence ID" ) + cc.error( " is " ) + cc.attention( sequence_id ) +
                                     "\n";
                                 log.write( strErrorMessage );
@@ -1100,16 +1102,17 @@ async function do_sign_messages_impl(
                                 ++joGatheringTracker.nCountErrors;
                         }
                     } catch ( err ) {
-                        ++nCountErrors;
+                        ++joGatheringTracker.nCountErrors;
                         const strErrorMessage =
                             strLogPrefix + cc.error( "S-Chain node " ) + strNodeDescColorized + " " + cc.fatal( "CRITICAL ERROR:" ) +
                             cc.error( " signature fail from node " ) + cc.info( joNode.nodeID ) +
-                            cc.error( ", error is " ) + cc.warning( err.toString() ) +
+                            cc.error( ", error is " ) + cc.warning( owaspUtils.extract_error_message( err ) ) +
                             cc.error( ", " ) + cc.notice( "sequence ID" ) + cc.error( " is " ) + cc.attention( sequence_id ) +
                             "\n";
                         log.write( strErrorMessage );
                         details.write( strErrorMessage );
                     }
+                    await joCall.disconnect();
                 } ); // joCall.call ...
             } ); // rpcCall.create ...
         } // for( let i = 0; i < jarrNodes.length; ++i )
@@ -1118,7 +1121,7 @@ async function do_sign_messages_impl(
         details.write( strLogPrefix + cc.debug( "Waiting for BLS glue result " ) + "\n" );
         let errGathering = null;
         const promise_gathering_complete = new Promise( ( resolve, reject ) => {
-            const iv = setInterval( async function() {
+            const iv = setInterval( function() {
                 ++ joGatheringTracker.nWaitIntervalStepsDone;
                 cntSuccess = joGatheringTracker.nCountReceived - joGatheringTracker.nCountErrors;
                 if( cntSuccess >= nCountOfBlsPartsToCollect ) {
@@ -1146,7 +1149,7 @@ async function do_sign_messages_impl(
                                 strSuccessfulResultDescription = "Got successful summary BLS verification result";
                                 details.write( strLogPrefixB + cc.success( strSuccessfulResultDescription ) + "\n" );
                             } else {
-                                strError = "BLS verify failed";
+                                strError = "BLS verification failed";
                                 log.write( strLogPrefixB + cc.fatal( "CRITICAL ERROR:" ) + cc.error( strError ) + "\n" );
                                 details.write( strLogPrefixB + cc.fatal( "CRITICAL ERROR:" ) + cc.error( strError ) + "\n" );
                             }
@@ -1161,10 +1164,10 @@ async function do_sign_messages_impl(
                     log.write( cc.debug( "Will call sending function (fn)" ) + "\n" );
                     details.write( cc.debug( "Will call sending function (fn) for " ) + "\n" );
                     /*await*/ fn( strError, jarrMessages, joGlueResult ).catch( ( err ) => {
-                        const strErrorMessage = cc.error( "Problem(2) in BLS sign result handler: " ) + cc.warning( err.toString() ) + "\n";
+                        const strErrorMessage = cc.error( "Problem(2) in BLS sign result handler: " ) + cc.warning( owaspUtils.extract_error_message( err ) ) + "\n";
                         log.write( strErrorMessage );
                         details.write( strErrorMessage );
-                        errGathering = "Problem(2) in BLS sign result handler: " + err.toString();
+                        errGathering = "Problem(2) in BLS sign result handler: " + owaspUtils.extract_error_message( err );
                         return;
                     } );
                     bHaveResultReportCalled = true;
@@ -1177,16 +1180,16 @@ async function do_sign_messages_impl(
                 }
                 if( joGatheringTracker.nCountReceived >= jarrNodes.length ) {
                     clearInterval( iv );
-                    await fn( "signature error(2), got " + joGatheringTracker.nCountErrors + " errors(s) for " + jarrNodes.length + " node(s)", jarrMessages, null ).catch( ( err ) => {
+                    /*await*/ fn( "signature error(2), got " + joGatheringTracker.nCountErrors + " errors(s) for " + jarrNodes.length + " node(s)", jarrMessages, null ).catch( ( err ) => {
                         const strErrorMessage =
                             cc.error( "Problem(3) in BLS sign result handler, not enough successful BLS signature parts(" ) +
-                            cc.info( cntSuccess ) + cc.error( " when all attempts done, error details: " ) + cc.warning( err.toString() ) +
+                            cc.info( cntSuccess ) + cc.error( " when all attempts done, error details: " ) + cc.warning( owaspUtils.extract_error_message( err ) ) +
                             "\n";
                         log.write( strErrorMessage );
                         details.write( strErrorMessage );
                         errGathering =
                             "Problem(3) in BLS sign result handler, not enough successful BLS signature parts(" +
-                            cntSuccess + " when all attempts done, error details: " + err.toString();
+                            cntSuccess + " when all attempts done, error details: " + owaspUtils.extract_error_message( err );
                         reject( new Error( errGathering ) );
                     } );
                     bHaveResultReportCalled = true;
@@ -1194,16 +1197,16 @@ async function do_sign_messages_impl(
                 }
                 if( joGatheringTracker.nWaitIntervalStepsDone >= joGatheringTracker.nWaitIntervalMaxSteps ) {
                     clearInterval( iv );
-                    await fn( "signature error(3), got " + joGatheringTracker.nCountErrors + " errors(s) for " + jarrNodes.length + " node(s)", jarrMessages, null ).catch( ( err ) => {
+                    /*await*/ fn( "signature error(3), got " + joGatheringTracker.nCountErrors + " errors(s) for " + jarrNodes.length + " node(s)", jarrMessages, null ).catch( ( err ) => {
                         const strErrorMessage =
                             cc.error( "Problem(4) in BLS sign result handler, not enough successful BLS signature parts(" ) +
                             cc.info( cntSuccess ) + cc.error( ") and timeout reached, error details: " ) +
-                            cc.warning( err.toString() ) + "\n";
+                            cc.warning( owaspUtils.extract_error_message( err ) ) + "\n";
                         log.write( strErrorMessage );
                         details.write( strErrorMessage );
                         errGathering =
                             "Problem(4) in BLS sign result handler, not enough successful BLS signature parts(" +
-                            cntSuccess + ") and timeout reached, error details: " + err.toString();
+                            cntSuccess + ") and timeout reached, error details: " + owaspUtils.extract_error_message( err );
                         reject( new Error( errGathering ) );
                     } );
                     bHaveResultReportCalled = true;
@@ -1217,7 +1220,7 @@ async function do_sign_messages_impl(
             details.write( cc.success( "BLS verification and sending promise awaited." ) + "\n" );
             log.write( cc.success( "BLS verification and sending promise awaited." ) + "\n" );
         } ).catch( err => {
-            const strErrorMessage = cc.error( "Failed to verify BLS and send message : " ) + cc.warning( err.toString() ) + "\n";
+            const strErrorMessage = cc.error( "Failed to verify BLS and send message : " ) + cc.warning( owaspUtils.extract_error_message( err ) ) + "\n";
             log.write( strErrorMessage );
             details.write( strErrorMessage );
         } );
@@ -1229,14 +1232,14 @@ async function do_sign_messages_impl(
                 bHaveResultReportCalled = true;
                 await fn(
                     "Failed to gather BLS signatures in " + jarrNodes.length + " node(s), trakcer data is: " +
-                        JSON.stringify( joGatheringTracker ) + ", error: " + errGathering.toString(),
+                        JSON.stringify( joGatheringTracker ) + ", error is: " + errGathering.toString(),
                     jarrMessages,
                     null
                 ).catch( ( err ) => {
                     const strErrorMessage =
                         cc.error( "Problem(5) in BLS sign result handler, not enough successful BLS signature parts(" ) +
                         cc.info( cntSuccess ) + cc.error( ") and timeout reached, error details: " ) +
-                        cc.warning( err.toString() ) + "\n";
+                        cc.warning( owaspUtils.extract_error_message( err ) ) + "\n";
                     log.write( strErrorMessage );
                     details.write( strErrorMessage );
                     details.exposeDetailsTo( log, strGatheredDetailsName, false );
@@ -1247,7 +1250,9 @@ async function do_sign_messages_impl(
             return;
         }
         if( ! bHaveResultReportCalled ) {
-            const strErrorMessage = cc.error( "Failed BLS sign result awaiting(2): " ) + cc.warning( err.toString() ) + "\n";
+            const strErrorMessage = cc.error( "Failed BLS sign result awaiting(2): " ) +
+                cc.warning( "No reports were arrived" ) + // cc.warning( owaspUtils.extract_error_message( err ) )
+                + "\n";
             log.write( strErrorMessage );
             details.write( strErrorMessage );
             bHaveResultReportCalled = true;
@@ -1255,7 +1260,7 @@ async function do_sign_messages_impl(
                 const strErrorMessage =
                     cc.error( "Problem(6) in BLS sign result handler, not enough successful BLS signature parts(" ) +
                     cc.info( cntSuccess ) + cc.error( ") and timeout reached, error details: " ) +
-                    cc.warning( err ) + "\n";
+                    cc.warning( owaspUtils.extract_error_message( err ) ) + "\n";
                 log.write( strErrorMessage );
                 details.write( strErrorMessage );
                 details.exposeDetailsTo( log, strGatheredDetailsName, false );
@@ -1264,14 +1269,15 @@ async function do_sign_messages_impl(
             } );
         }
     } catch ( err ) {
-        const strErrorMessage = cc.error( "Failed BLS sign due to generic flow exception: " ) + cc.warning( err.toString() ) + "\n";
+        const strErrorMessage =
+            cc.error( "Failed BLS sign due to generic flow exception: " ) + cc.warning( owaspUtils.extract_error_message( err ) ) + "\n";
         log.write( strErrorMessage );
         if( details )
             details.write( strErrorMessage );
         if( ! bHaveResultReportCalled ) {
             bHaveResultReportCalled = true;
-            await fn( "Failed BLS sign due to exception: " + err.toString(), jarrMessages, null ).catch( ( err ) => {
-                const strErrorMessage = cc.error( "Failed BLS sign due to error-erporting callback exception: " ) + cc.warning( err.toString() ) + "\n";
+            await fn( "Failed BLS sign due to exception: " + owaspUtils.extract_error_message( err ), jarrMessages, null ).catch( ( err ) => {
+                const strErrorMessage = cc.error( "Failed BLS sign due to error-erporting callback exception: " ) + cc.warning( owaspUtils.extract_error_message( err ) ) + "\n";
                 log.write( strErrorMessage );
                 if( details ) {
                     details.write( strErrorMessage );
@@ -1397,9 +1403,11 @@ async function do_sign_u256( u256, details, fn ) {
                 const strErrorMessage =
                     strLogPrefix + cc.fatal( "CRITICAL ERROR:" ) +
                     cc.error( " JSON RPC call to S-Chain node " ) + strNodeDescColorized +
-                    cc.error( " failed, RPC call was not created, error: " ) + cc.warning( err ) + "\n";
+                    cc.error( " failed, RPC call was not created, error is: " ) + cc.warning( owaspUtils.extract_error_message( err ) ) + "\n";
                 log.write( strErrorMessage );
                 details.write( strErrorMessage );
+                if( joCall )
+                    await joCall.disconnect();
                 return;
             }
             details.write(
@@ -1409,7 +1417,7 @@ async function do_sign_u256( u256, details, fn ) {
             await joCall.call( {
                 method: "skale_imaBSU256",
                 params: {
-                    valueToSign: u256
+                    valueToSign: u256 // must be 0x string, came from outside 0x string
                 }
             }, async function( joIn, joOut, err ) {
                 ++joGatheringTracker.nCountReceived; // including errors
@@ -1418,9 +1426,10 @@ async function do_sign_u256( u256, details, fn ) {
                     const strErrorMessage =
                         strLogPrefix + cc.fatal( "CRITICAL ERROR:" ) +
                         cc.error( " JSON RPC call to S-Chain node " ) + strNodeDescColorized +
-                        cc.error( " failed, RPC call reported error: " ) + cc.warning( err ) + "\n";
+                        cc.error( " failed, RPC call reported error: " ) + cc.warning( owaspUtils.extract_error_message( err ) ) + "\n";
                     log.write( strErrorMessage );
                     details.write( strErrorMessage );
+                    await joCall.disconnect();
                     return;
                 }
                 details.write(
@@ -1430,21 +1439,15 @@ async function do_sign_u256( u256, details, fn ) {
                     "\n" );
                 if( joOut.result == null || joOut.result == undefined || ( !typeof joOut.result == "object" ) ) {
                     ++joGatheringTracker.nCountErrors;
-                    if( "error" in joOut && "message" in joOut.error ) {
-                        const strErrorMessage =
-                            strLogPrefix + cc.fatal( "Wallet CRITICAL ERROR:" ) + " " +
-                            cc.error( "S-Chain node " ) + strNodeDescColorized +
-                            cc.error( " reported wallet error: " ) + cc.warning( joOut.error.message ) + "\n";
-                        log.write( strErrorMessage );
-                        details.write( strErrorMessage );
-                    } else {
-                        const strErrorMessage =
-                            strLogPrefix + cc.fatal( "Wallet CRITICAL ERROR:" ) + " " +
-                            cc.error( "JSON RPC call to S-Chain node " ) + strNodeDescColorized +
-                            cc.error( " failed with " ) + cc.warning( "unknown wallet error" ) + "\n";
-                        log.write( strErrorMessage );
-                        details.write( strErrorMessage );
-                    }
+                    const strErrorMessage =
+                        strLogPrefix + cc.fatal( "Wallet CRITICAL ERROR:" ) + " " +
+                        cc.error( "S-Chain node " ) + strNodeDescColorized +
+                        cc.error( " reported wallet error: " ) +
+                        cc.warning( owaspUtils.extract_error_message( joOut, "unknown wallet error(2)" ) ) +
+                        "\n";
+                    log.write( strErrorMessage );
+                    details.write( strErrorMessage );
+                    await joCall.disconnect();
                     return;
                 }
                 details.write( strLogPrefix + cc.normal( "Node " ) + cc.info( joNode.nodeID ) + cc.normal( " sign result: " ) + cc.j( joOut.result ? joOut.result : null ) + "\n" );
@@ -1488,7 +1491,7 @@ async function do_sign_u256( u256, details, fn ) {
                             const strErrorMessage =
                                 strLogPrefixA + cc.error( "S-Chain node " ) + strNodeDescColorized + cc.error( " sign " ) +
                                 cc.error( " CRITICAL ERROR:" ) + cc.error( " partial signature fail from with index " ) + cc.info( nZeroBasedNodeIndex ) +
-                                cc.error( ", error is " ) + cc.warning( err.toString() ) + "\n";
+                                cc.error( ", error is " ) + cc.warning( owaspUtils.extract_error_message( err ) ) + "\n";
                             log.write( strErrorMessage );
                             details.write( strErrorMessage );
                         }
@@ -1513,14 +1516,15 @@ async function do_sign_u256( u256, details, fn ) {
                             ++joGatheringTracker.nCountErrors;
                     }
                 } catch ( err ) {
-                    ++nCountErrors;
+                    ++joGatheringTracker.nCountErrors;
                     const strErrorMessage =
                         strLogPrefix + cc.error( "S-Chain node " ) + strNodeDescColorized + " " + cc.fatal( "CRITICAL ERROR:" ) +
                         cc.error( " signature fail from node " ) + cc.info( joNode.nodeID ) +
-                        cc.error( ", error is " ) + cc.warning( err.toString() ) + "\n";
+                        cc.error( ", error is " ) + cc.warning( owaspUtils.extract_error_message( err ) ) + "\n";
                     log.write( strErrorMessage );
                     details.write( strErrorMessage );
                 }
+                await joCall.disconnect();
             } ); // joCall.call ...
         } ); // rpcCall.create ...
     }
@@ -1529,7 +1533,7 @@ async function do_sign_u256( u256, details, fn ) {
     details.write( strLogPrefix + cc.debug( "Waiting for BLS glue result " ) + "\n" );
     errGathering = null;
     const promise_gathering_complete = new Promise( ( resolve, reject ) => {
-        const iv = setInterval( async function() {
+        const iv = setInterval( function() {
             ++ joGatheringTracker.nWaitIntervalStepsDone;
             const cntSuccess = joGatheringTracker.nCountReceived - joGatheringTracker.nCountErrors;
             if( cntSuccess >= nCountOfBlsPartsToCollect ) {
@@ -1546,7 +1550,7 @@ async function do_sign_u256( u256, details, fn ) {
                             strSuccessfulResultDescription = "Got successful summary BLS u256 verification result";
                             details.write( strLogPrefixB + cc.success( strSuccessfulResultDescription ) + "\n" );
                         } else {
-                            strError = "BLS verify failed";
+                            strError = "BLS verification failed";
                             log.write( strLogPrefixB + cc.fatal( "CRITICAL ERROR:" ) + cc.error( strError ) + "\n" );
                             details.write( strLogPrefixB + cc.fatal( "CRITICAL ERROR:" ) + cc.error( strError ) + "\n" );
                         }
@@ -1560,11 +1564,11 @@ async function do_sign_u256( u256, details, fn ) {
                 }
                 log.write( cc.debug( "Will call sending function (fn)" ) + "\n" );
                 details.write( cc.debug( "Will call sending function (fn) for " ) + "\n" );
-                await fn( strError, u256, joGlueResult ).catch( ( err ) => {
-                    const strErrorMessage = cc.error( "Problem(2) in BLS u256 sign result handler: " ) + cc.warning( err.toString() ) + "\n";
+                /*await*/ fn( strError, u256, joGlueResult ).catch( ( err ) => {
+                    const strErrorMessage = cc.error( "Problem(2) in BLS u256 sign result handler: " ) + cc.warning( owaspUtils.extract_error_message( err ) ) + "\n";
                     log.write( strErrorMessage );
                     details.write( strErrorMessage );
-                    errGathering = "Problem(2) in BLS u256 sign result handler: " + err.toString();
+                    errGathering = "Problem(2) in BLS u256 sign result handler: " + owaspUtils.extract_error_message( err );
                 } );
                 if( strError ) {
                     errGathering = strError;
@@ -1575,32 +1579,32 @@ async function do_sign_u256( u256, details, fn ) {
             }
             if( joGatheringTracker.nCountReceived >= jarrNodes.length ) {
                 clearInterval( iv );
-                await fn( "signature error(2, u256), got " + joGatheringTracker.nCountErrors + " errors(s) for " + jarrNodes.length + " node(s)", u256, null ).catch( ( err ) => {
+                /*await*/ fn( "signature error(2, u256), got " + joGatheringTracker.nCountErrors + " errors(s) for " + jarrNodes.length + " node(s)", u256, null ).catch( ( err ) => {
                     const strErrorMessage =
                         cc.error( "Problem(3) in BLS u256 sign result handler, not enough successful BLS signature parts(" ) +
-                        cc.info( cntSuccess ) + cc.error( " when all attempts done, error details: " ) + cc.warning( err.toString() ) +
+                        cc.info( cntSuccess ) + cc.error( " when all attempts done, error details: " ) + cc.warning( owaspUtils.extract_error_message( err ) ) +
                         "\n";
                     log.write( strErrorMessage );
                     details.write( strErrorMessage );
                     errGathering =
                         "Problem(3) in BLS u256 sign result handler, not enough successful BLS signature parts(" +
-                        cntSuccess + " when all attempts done, error details: " + err.toString();
+                        cntSuccess + " when all attempts done, error details: " + owaspUtils.extract_error_message( err );
                     reject( new Error( errGathering ) );
                 } );
                 return;
             }
             if( joGatheringTracker.nWaitIntervalStepsDone >= joGatheringTracker.nWaitIntervalMaxSteps ) {
                 clearInterval( iv );
-                await fn( "signature error(3, u256), got " + joGatheringTracker.nCountErrors + " errors(s) for " + jarrNodes.length + " node(s)", u256, null ).catch( ( err ) => {
+                /*await*/ fn( "signature error(3, u256), got " + joGatheringTracker.nCountErrors + " errors(s) for " + jarrNodes.length + " node(s)", u256, null ).catch( ( err ) => {
                     const strErrorMessage =
                         cc.error( "Problem(4) in BLS u256 sign result handler, not enough successful BLS signature parts(" ) +
                         cc.info( cntSuccess ) + cc.error( ") and timeout reached, error details: " ) +
-                        cc.warning( err.toString() ) + "\n";
+                        cc.warning( owaspUtils.extract_error_message( err ) ) + "\n";
                     log.write( strErrorMessage );
                     details.write( strErrorMessage );
                     errGathering =
                         "Problem(4) in BLS u256 sign result handler, not enough successful BLS signature parts(" +
-                        cntSuccess + ") and timeout reached, error details: " + err.toString();
+                        cntSuccess + ") and timeout reached, error details: " + owaspUtils.extract_error_message( err );
                     reject( new Error( errGathering ) );
                 } );
                 return;
@@ -1613,7 +1617,7 @@ async function do_sign_u256( u256, details, fn ) {
         details.write( cc.info( "BLS u256 sign promise awaited." ) + "\n" );
         log.write( cc.info( "BLS u256 sign promise awaited." ) + "\n" );
     } ).catch( err => {
-        const strErrorMessage = cc.error( "Failed to verify BLS and send message : " ) + cc.warning( err.toString() ) + "\n";
+        const strErrorMessage = cc.error( "Failed to verify BLS and send message : " ) + cc.warning( owaspUtils.extract_error_message( err ) ) + "\n";
         log.write( strErrorMessage );
         details.write( strErrorMessage );
     } );
diff --git a/agent/cli.js b/agent/cli.js
index a8fb69514..9188a5173 100644
--- a/agent/cli.js
+++ b/agent/cli.js
@@ -110,22 +110,32 @@ function ensure_have_chain_credentials( strFriendlyChainName, joAccount, isExitI
         if( isExitIfEmpty )
             process.exit( 126 );
     }
-    if( "strTransactionManagerURL" in joAccount && typeof joAccount.strTransactionManagerURL == "string" && joAccount.strTransactionManagerURL.length > 0 )
+    let cntAccountVariantsSpecified = 0;
+    if( "strTransactionManagerURL" in joAccount && typeof joAccount.strTransactionManagerURL == "string" && joAccount.strTransactionManagerURL.length > 0 ) {
+        ++ cntAccountVariantsSpecified;
         ensure_have_value( "" + strFriendlyChainName + "/TM/URL", joAccount.strTransactionManagerURL, isExitIfEmpty, isPrintValue );
-    else if( "strSgxURL" in joAccount && typeof joAccount.strSgxURL == "string" && joAccount.strSgxURL.length > 0 &&
-        "strSgxKeyName" in joAccount && typeof joAccount.strSgxKeyName == "string" && joAccount.strSgxKeyName.length > 0
-    ) {
+    }
+    if( "strSgxURL" in joAccount && typeof joAccount.strSgxURL == "string" && joAccount.strSgxURL.length > 0 ) {
+        ++ cntAccountVariantsSpecified;
         ensure_have_value( "" + strFriendlyChainName + "/SGX/URL", joAccount.strSgxURL, isExitIfEmpty, isPrintValue );
-        ensure_have_value( "" + strFriendlyChainName + "/SGX/keyName", joAccount.strSgxKeyName, isExitIfEmpty, isPrintValue );
         if( "strPathSslKey" in joAccount && typeof joAccount.strPathSslKey == "string" && joAccount.strPathSslKey.length > 0 )
             ensure_have_value( "" + strFriendlyChainName + "/SGX/SSL/keyPath", joAccount.strPathSslKey, isExitIfEmpty, isPrintValue );
         if( "strPathSslCert" in joAccount && typeof joAccount.strPathSslCert == "string" && joAccount.strPathSslCert.length > 0 )
             ensure_have_value( "" + strFriendlyChainName + "/SGX/SSL/certPath", joAccount.strPathSslCert, isExitIfEmpty, isPrintValue );
-    } else if( "privateKey" in joAccount && typeof joAccount.privateKey == "string" && joAccount.privateKey.length > 0 )
+    }
+    if( "strSgxKeyName" in joAccount && typeof joAccount.strSgxKeyName == "string" && joAccount.strSgxKeyName.length > 0 ) {
+        ++ cntAccountVariantsSpecified;
+        ensure_have_value( "" + strFriendlyChainName + "/SGX/keyName", joAccount.strSgxKeyName, isExitIfEmpty, isPrintValue );
+    }
+    if( "privateKey" in joAccount && typeof joAccount.privateKey == "string" && joAccount.privateKey.length > 0 ) {
+        ++ cntAccountVariantsSpecified;
         ensure_have_value( "" + strFriendlyChainName + "/privateKey", joAccount.privateKey, isExitIfEmpty, isPrintValue );
-    else if( "address_" in joAccount && typeof joAccount.address_ == "string" && joAccount.address_.length > 0 )
+    }
+    if( "address_" in joAccount && typeof joAccount.address_ == "string" && joAccount.address_.length > 0 ) {
+        ++ cntAccountVariantsSpecified;
         ensure_have_value( "" + strFriendlyChainName + "/walletAddress", joAccount.address_, isExitIfEmpty, isPrintValue );
-    else {
+    }
+    if( cntAccountVariantsSpecified == 0 ) {
         log.write( cc.error( "ARGUMENTS VALIDATION WARNING:" ) +
             cc.warning( " bad credentials information specified for " ) + cc.info( strFriendlyChainName ) +
             cc.warning( " chain, no explicit SGX, no explicit private key, no wallet address found" ) + "\n"
@@ -390,7 +400,7 @@ function parse( joExternalHandlers, argv ) {
             console.log( soi + cc.debug( "--" ) + cc.bright( "bls-verify" ) + cc.sunny( "=" ) + cc.note( "path" ) + cc.debug( "..............." ) + cc.debug( "Optional parameter, specifies path to " ) + cc.note( "verify_bls" ) + cc.debug( " application." ) );
             //
             console.log( cc.sunny( "MONITORING" ) + cc.info( " options:" ) );
-            console.log( soi + cc.debug( "--" ) + cc.bright( "monitoring-port" ) + cc.sunny( "=" ) + cc.note( "number" ) + cc.debug( "........" ) + cc.notice( "Run " ) + cc.note( "monitoring web socket RPC server" ) + cc.notice( " on specified port. " ) + cc.debug( "By default monitoring server is " ) + cc.error( "disabled" ) + cc.notice( "." ) );
+            console.log( soi + cc.debug( "--" ) + cc.bright( "monitoring-port" ) + cc.sunny( "=" ) + cc.note( "number" ) + cc.debug( "........" ) + cc.notice( "Run " ) + cc.note( "monitoring web socket RPC server" ) + cc.notice( " on specified port. " ) + cc.debug( "Specify " ) + cc.sunny( "0" ) + cc.debug( " to " ) + cc.error( "disable" ) + cc.notice( "." ) + cc.debug( " By default monitoring server is " ) + cc.error( "disabled" ) + cc.notice( "." ) );
             //
             console.log( cc.sunny( "GAS REIMBURSEMENT" ) + cc.info( " options:" ) );
             console.log( soi + cc.debug( "--" ) + cc.bright( "reimbursement-chain" ) + cc.sunny( "=" ) + cc.note( "name" ) + cc.debug( "......" ) + cc.notice( "Specifies chain name." ) );
@@ -482,6 +492,11 @@ function parse( joExternalHandlers, argv ) {
             imaState.strChainName_s_chain = joArg.value;
             continue;
         }
+        if( joArg.name == "id-origin-chain" ) {
+            owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+            imaState.strChainName_origin_chain = joArg.value;
+            continue;
+        }
         if( joArg.name == "id-t-chain" ) {
             owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
             imaState.strChainName_t_chain = joArg.value;
@@ -1130,7 +1145,7 @@ function parse( joExternalHandlers, argv ) {
             continue;
         }
         if( joArg.name == "monitoring-port" ) {
-            owaspUtils.verifyArgumentIsIntegerIpPortNumber( joArg );
+            owaspUtils.verifyArgumentIsIntegerIpPortNumber( joArg, true );
             imaState.nMonitoringPort = owaspUtils.toInteger( joArg.value );
             continue;
         }
@@ -1230,8 +1245,9 @@ function parse( joExternalHandlers, argv ) {
     return 0;
 }
 
-function getWeb3FromURL( strURL ) {
+function getWeb3FromURL( strURL, log ) {
     let w3 = null;
+    log = log || { write: console.log };
     try {
         const u = cc.safeURL( strURL );
         const strProtocol = u.protocol.trim().toLowerCase().replace( ":", "" ).replace( "/", "" );
@@ -1261,7 +1277,7 @@ function getWeb3FromURL( strURL ) {
     } catch ( err ) {
         log.write( cc.fatal( "CRITICAL ERROR:" ) + cc.error( " Failed to create " ) +
             cc.attention( "Web3" ) + cc.error( " connection to " ) + cc.info( strURL ) +
-            cc.error( ": " ) + cc.warning( err.toString() ) + "\n" );
+            cc.error( ": " ) + cc.warning( owaspUtils.extract_error_message( err ) ) + "\n" );
         w3 = null;
     }
     return w3;
@@ -1282,7 +1298,7 @@ async function async_check_url_at_startup( u, name ) {
     } catch ( err ) {
         details.write(
             cc.fatal( "ERROR:" ) + cc.error( " Failed to check URL " ) +
-            cc.u( u ) + cc.error( " connectivity for " ) + cc.info( name ) + cc.error( " at start-up, error is: " ) + cc.warning( err.toString() ) +
+            cc.u( u ) + cc.error( " connectivity for " ) + cc.info( name ) + cc.error( " at start-up, error is: " ) + cc.warning( owaspUtils.extract_error_message( err ) ) +
             "\n" );
     }
     // details.exposeDetailsTo( log, "async_check_url_at_startup( \"" + u + "\", \"" + name + "\" )", true );
@@ -1483,7 +1499,7 @@ function ima_common_init() {
     if( imaState.strURL_main_net && typeof imaState.strURL_main_net == "string" && imaState.strURL_main_net.length > 0 ) {
         const u = imaState.strURL_main_net;
         async_check_url_at_startup( u, "Main-net" );
-        imaState.w3_main_net = getWeb3FromURL( u );
+        imaState.w3_main_net = getWeb3FromURL( u, log );
     } else {
         log.write(
             cc.error( "WARNING:" ) + cc.warning( " No " ) + cc.note( "Main-net" ) +
@@ -1495,7 +1511,7 @@ function ima_common_init() {
     if( imaState.strURL_s_chain && typeof imaState.strURL_s_chain == "string" && imaState.strURL_s_chain.length > 0 ) {
         const u = imaState.strURL_s_chain;
         async_check_url_at_startup( u, "S-Chain" );
-        imaState.w3_s_chain = getWeb3FromURL( u );
+        imaState.w3_s_chain = getWeb3FromURL( u, log );
     } else {
         log.write(
             cc.error( "WARNING:" ) + cc.warning( " No " ) + cc.note( "S-Chain" ) +
@@ -1507,7 +1523,7 @@ function ima_common_init() {
     if( imaState.strURL_t_chain && typeof imaState.strURL_t_chain == "string" && imaState.strURL_t_chain.length > 0 ) {
         const u = imaState.strURL_t_chain;
         async_check_url_at_startup( u, "S<->S Target S-Chain" );
-        imaState.w3_t_chain = getWeb3FromURL( u );
+        imaState.w3_t_chain = getWeb3FromURL( u, log );
     } else {
         log.write(
             cc.error( "WARNING:" ) + cc.warning( " No " ) + cc.note( "S<->S Target S-Chain" ) +
diff --git a/agent/main.js b/agent/main.js
index d1f9bc425..63b93918b 100644
--- a/agent/main.js
+++ b/agent/main.js
@@ -320,21 +320,24 @@ const fnInitActionSkaleNetworkScanForS2S = function() {
                 "secondsToReDiscoverSkaleNetwork": imaState.s2s_opts.secondsToReDiscoverSkaleNetwork
             };
             const addressFrom = imaState.joAccount_main_net.address( imaState.w3_main_net );
-            const strError = await skale_observer.cache_schains(
-                imaState.strChainName_s_chain, // strChainNameConnectedTo
-                imaState.w3_main_net,
-                addressFrom,
-                opts
-            );
-            if( strError ) {
-                log.write( strLogPrefix + cc.error( "Failed to get " ) + cc.info( "SKALE NETWORK" ) + cc.error( " information: " ) + cc.warning( strError ) + "\n" );
-                return true;
-            }
-            const arr_schains = skale_observer.get_last_cached_schains();
-            log.write( strLogPrefix + cc.normal( "Got " ) + cc.info( "SKALE NETWORK" ) + cc.normal( " information: " ) + cc.j( arr_schains ) + "\n" );
+            // const strError = await skale_observer.cache_schains(
+            //     imaState.strChainName_s_chain, // strChainNameConnectedTo
+            //     imaState.w3_main_net,
+            //     imaState.w3_s_chain,
+            //     addressFrom,
+            //     opts
+            // );
+            // if( strError ) {
+            //     log.write( strLogPrefix + cc.error( "Failed to get " ) + cc.info( "SKALE NETWORK" ) + cc.error( " information: " ) + cc.warning( strError ) + "\n" );
+            //     return true;
+            // }
+            // const arr_schains = skale_observer.get_last_cached_schains();
+            // log.write( strLogPrefix + cc.normal( "Got " ) + cc.info( "SKALE NETWORK" ) + cc.normal( " information: " ) + cc.j( arr_schains ) + "\n" );
+            log.write( strLogPrefix + cc.debug( "Will start periodic S-Chains caching..." ) + "\n" );
             await skale_observer.periodic_caching_start(
                 imaState.strChainName_s_chain, // strChainNameConnectedTo
                 imaState.w3_main_net,
+                imaState.w3_s_chain,
                 addressFrom,
                 opts
             );
@@ -1385,6 +1388,8 @@ imaCLI.parse( {
                 await rpcCall.create( imaState.strURL_s_chain, rpcCallOpts, async function( joCall, err ) {
                     if( err ) {
                         console.log( cc.fatal( "CRITICAL ERROR:" ) + cc.error( " JSON RPC call to S-Chain failed" ) );
+                        if( joCall )
+                            await joCall.disconnect();
                         process.exit( 156 );
                     }
                     await joCall.call( {
@@ -1395,6 +1400,7 @@ imaCLI.parse( {
                     }, async function( joIn, joOut, err ) {
                         if( err ) {
                             console.log( cc.fatal( "CRITICAL ERROR:" ) + cc.error( " JSON RPC call to S-Chain failed, error: " ) + cc.warning( err ) );
+                            await joCall.disconnect();
                             process.exit( 157 );
                         }
                         log.write( strLogPrefix + cc.normal( "S-Chain network information: " ) + cc.j( joOut.result ) + "\n" );
@@ -1413,6 +1419,7 @@ imaCLI.parse( {
                             await rpcCall.create( strNodeURL, rpcCallOpts, async function( joCall, err ) {
                                 if( err ) {
                                     console.log( cc.fatal( "CRITICAL ERROR:" ) + cc.error( " JSON RPC call to S-Chain failed" ) );
+                                    await joCall.disconnect();
                                     process.exit( 158 );
                                 }
                                 await joCall.call( {
@@ -1428,6 +1435,7 @@ imaCLI.parse( {
                                     }
                                     log.write( strLogPrefix + cc.normal( "Node " ) + cc.info( joNode.nodeID ) + cc.normal( " IMA information: " ) + cc.j( joOut.result ) + "\n" );
                                     //process.exit( 0 );
+                                    await joCall.disconnect();
                                 } );
                             } );
                         }
@@ -1438,6 +1446,7 @@ imaCLI.parse( {
                                 process.exit( 0 );
                             }
                         }, 100 );
+                        await joCall.disconnect();
                     } );
                 } );
                 return true;
@@ -1462,7 +1471,8 @@ imaCLI.parse( {
                 };
                 const addressFrom = imaState.joAccount_main_net.address( imaState.w3_main_net );
                 const arr_schains = await skale_observer.load_schains( imaState.w3_main_net, addressFrom, opts );
-                log.write( strLogPrefix + cc.normal( "Got " ) + cc.info( "SKALE NETWORK" ) + cc.normal( " information: " ) + cc.j( arr_schains ) + "\n" );
+                const cnt = arr_schains.length;
+                log.write( strLogPrefix + cc.normal( "Got " ) + cc.info( cnt ) + cc.normal( " S-Chains(s) in SKALE NETWORK information: " ) + cc.j( arr_schains ) + "\n" );
                 return true;
             }
         } );
@@ -1485,18 +1495,17 @@ imaCLI.parse( {
                     "bStopNeeded": false
                 };
                 const addressFrom = imaState.joAccount_main_net.address( imaState.w3_main_net );
-                const arr_schains = await skale_observer.load_schains( imaState.w3_main_net, addressFrom, opts );
-                await skale_observer.check_connected_schains(
+
+                const arr_schains_cached = await skale_observer.load_schains_connected_only(
+                    imaState.w3_main_net,
+                    imaState.w3_s_chain,
                     imaState.strChainName_s_chain, // strChainNameConnectedTo
-                    arr_schains,
                     addressFrom,
                     opts
                 );
-                const arr_schains_cached = await skale_observer.filter_schains_marked_as_connected(
-                    arr_schains,
-                    opts
-                );
-                log.write( strLogPrefix + cc.normal( "Got " ) + cc.info( "connected S-Chains" ) + cc.normal( " information: " ) + cc.j( arr_schains_cached ) + "\n" );
+
+                const cnt = arr_schains_cached.length;
+                log.write( strLogPrefix + cc.normal( "Got " ) + cc.info( cnt ) + cc.normal( " onnected S-Chain(s): " ) + cc.j( arr_schains_cached ) + "\n" );
                 return true;
             }
         } );
@@ -1751,8 +1760,11 @@ async function continue_schain_discovery_in_background_if_needed( isSilent ) {
         return;
     if( imaState.joSChainDiscovery.repeatIntervalMilliseconds <= 0 )
         return; // no S-Chain re-discovery (for debugging only)
-    g_timer_s_chain_discovery = setInterval( async function() {
+    const fn_async_handler = async function() {
+        if( g_b_in_s_chain_discovery )
+            return;
         if( g_b_in_s_chain_discovery ) {
+            isInsideAsyncHandler = false;
             if( IMA.verbose_get() >= IMA.RV_VERBOSE.information )
                 log.write( cc.warning( "Notice: long S-Chain discovery is in progress" ) + "\n" );
             return;
@@ -1819,6 +1831,11 @@ async function continue_schain_discovery_in_background_if_needed( isSilent ) {
             } );
         } catch ( err ) { }
         g_b_in_s_chain_discovery = false;
+    };
+    g_timer_s_chain_discovery = setInterval( function() {
+        if( g_b_in_s_chain_discovery )
+            return;
+        fn_async_handler();
     }, imaState.joSChainDiscovery.repeatIntervalMilliseconds );
 }
 
@@ -1842,6 +1859,8 @@ async function discover_s_chain_network( fnAfter, isSilent, joPrevSChainNetworkI
                     );
                 }
                 fnAfter( err, null );
+                if( joCall )
+                    await joCall.disconnect();
                 return;
             }
             await joCall.call( {
@@ -1859,6 +1878,7 @@ async function discover_s_chain_network( fnAfter, isSilent, joPrevSChainNetworkI
                         );
                     }
                     fnAfter( err, null );
+                    await joCall.disconnect();
                     return;
                 }
                 if( ( !isSilent ) && IMA.verbose_get() >= IMA.RV_VERBOSE.trace )
@@ -1877,6 +1897,7 @@ async function discover_s_chain_network( fnAfter, isSilent, joPrevSChainNetworkI
                         );
                     }
                     fnAfter( err2, null );
+                    await joCall.disconnect();
                     return;
                 }
                 const jarrNodes = joSChainNetworkInfo.network;
@@ -1932,6 +1953,8 @@ async function discover_s_chain_network( fnAfter, isSilent, joPrevSChainNetworkI
                                 }
                                 // fnAfter( err, null );
                                 ++ cntFailed;
+                                if( joCall )
+                                    await joCall.disconnect();
                                 return;
                             }
                             joCall.call( {
@@ -2062,6 +2085,7 @@ async function discover_s_chain_network( fnAfter, isSilent, joPrevSChainNetworkI
                         );
                     }
                 }, nWaitStepMilliseconds );
+                await joCall.disconnect();
             } );
         } );
     } catch ( err ) {
@@ -2084,7 +2108,7 @@ async function discover_s_chain_network( fnAfter, isSilent, joPrevSChainNetworkI
 let g_ws_server_monitoring = null;
 
 if( imaState.nMonitoringPort > 0 ) {
-    const strLogPrefix = cc.attention( "Monitoring" ) + " " + cc.sunny( ">>" ) + " ";
+    const strLogPrefix = cc.attention( "Monitoring:" ) + " ";
     if( IMA.verbose_get() >= IMA.RV_VERBOSE.trace )
         log.write( strLogPrefix + cc.normal( "Will start monitoring WS server on port " ) + cc.info( imaState.nMonitoringPort ) + "\n" );
     g_ws_server_monitoring = new ws.Server( { port: 0 + imaState.nMonitoringPort } );
@@ -2101,7 +2125,7 @@ if( imaState.nMonitoringPort > 0 ) {
             try {
                 const joMessage = JSON.parse( message );
                 if( IMA.verbose_get() >= IMA.RV_VERBOSE.trace )
-                    log.write( strLogPrefix + cc.normal( "Message from " ) + cc.info( ip ) + cc.normal( ": " ) + cc.j( joMessage ) + "\n" );
+                    log.write( strLogPrefix + cc.sunny( "<<<" ) + " " + cc.normal( "message from " ) + cc.info( ip ) + cc.normal( ": " ) + cc.j( joMessage ) + "\n" );
                 if( ! ( "method" in joMessage ) )
                     throw new Error( "\"method\" field was not specified" );
                 joAnswer.method = joMessage.method;
@@ -2175,8 +2199,8 @@ if( imaState.nMonitoringPort > 0 ) {
                     } break;
                 case "get_last_transfer_errors":
                     // call:   { "id": 1, "method": "get_last_transfer_errors" }
-                    // answer: { "id": 1, "method": "get_last_transfer_errors", "error": null, "last_transfer_errors": [ { ts: ..., textLog: ... }, ... ] }
-                    joAnswer.last_transfer_errors = IMA.get_last_transfer_errors();
+                    // answer: { "id": 1, "method": "get_last_transfer_errors", "isIncludeTextLog": true, "error": null, "last_transfer_errors": [ { ts: ..., textLog: ... }, ... ] }
+                    joAnswer.last_transfer_errors = IMA.get_last_transfer_errors( ( ( "isIncludeTextLog" in joMessage ) && joMessage.isIncludeTextLog ) ? true : false );
                     joAnswer.last_error_categories = IMA.get_last_error_categories();
                     break;
                 default:
@@ -2192,7 +2216,7 @@ if( imaState.nMonitoringPort > 0 ) {
             }
             try {
                 if( IMA.verbose_get() >= IMA.RV_VERBOSE.trace )
-                    log.write( strLogPrefix + cc.normal( "Answer to " ) + cc.info( ip ) + cc.normal( ": " ) + cc.j( joAnswer ) + "\n" );
+                    log.write( strLogPrefix + cc.sunny( ">>>" ) + " " + cc.normal( "answer to " ) + cc.info( ip ) + cc.normal( ": " ) + cc.j( joAnswer ) + "\n" );
                 ws_peer.send( JSON.stringify( joAnswer ) );
             } catch ( err ) {
                 if( IMA.verbose_get() >= IMA.RV_VERBOSE.error ) {
diff --git a/agent/package.json b/agent/package.json
index a6ce83b34..401151436 100644
--- a/agent/package.json
+++ b/agent/package.json
@@ -7,12 +7,14 @@
         "postinstall": "echo \"----- installing in OWASP\n\" && cd ../npms/skale-owasp && yarn install && cd ../../agent && echo \"----- installing in IMA CORE\n\" && cd ../npms/skale-ima && yarn install && cd ../../agent && echo \"----- installing in IMA OBSERVER\n\" && cd ../npms/skale-observer && yarn install && cd ../../agent"
     },
     "dependencies": {
+        "js-sha3": "^0.8.0",
+        "number-to-bn": "^1.7.0",
         "ethereumjs-tx": "2.1.2",
         "ethereumjs-wallet": "^1.0.2",
-        "ethereumjs-util": "^7.1.3",
+        "ethereumjs-util": "^7.1.4",
         "web3": "^1.6.1",
         "uuid": "8.3.2",
-        "ws": "^8.4.0",
+        "ws": "^8.6.0",
         "urllib": "2.38.0",
         "sha3": "2.1.4",
         "shelljs": "^0.8.5"
diff --git a/agent/rpc-call.js b/agent/rpc-call.js
index f6bcfe98d..dd1aa81d1 100644
--- a/agent/rpc-call.js
+++ b/agent/rpc-call.js
@@ -29,9 +29,17 @@ const net = require( "net" );
 
 const g_nConnectionTimeoutSeconds = 60;
 
+function rpc_call_init() {
+    owaspUtils.owaspAddUsageRef();
+}
+
+function validateURL( x ) {
+    return owaspUtils.validateURL( x );
+}
+
 function is_http_url( strURL ) {
     try {
-        if( !owaspUtils.validateURL( strURL ) )
+        if( !validateURL( strURL ) )
             return false;
         const u = new URL( strURL );
         if( u.protocol == "http:" || u.protocol == "https:" )
@@ -43,7 +51,7 @@ function is_http_url( strURL ) {
 
 function is_ws_url( strURL ) {
     try {
-        if( !owaspUtils.validateURL( strURL ) )
+        if( !validateURL( strURL ) )
             return false;
         const u = new URL( strURL );
         if( u.protocol == "ws:" || u.protocol == "wss:" )
@@ -53,16 +61,17 @@ function is_ws_url( strURL ) {
     return false;
 }
 
-function rpc_call_init() {
-    owaspUtils.owaspAddUsageRef();
-}
-
 async function wait_web_socket_is_open( socket, fnDone, fnStep ) {
     fnDone = fnDone || async function( nStep ) {};
     fnDone = fnStep || async function( nStep ) { return true; };
-    const nStep = 0;
+    let nStep = 0;
     const promiseComplete = new Promise( function( resolve, reject ) {
-        const iv = setInterval( async function() {
+        let isInsideAsyncHandler = false;
+        const fn_async_handler = async function() {
+            if( isInsideAsyncHandler )
+                return;
+            isInsideAsyncHandler = true;
+            ++ nStep;
             if( socket.readyState === 1 ) {
                 // console.log( "Connection is made" )
                 clearInterval( iv );
@@ -74,6 +83,15 @@ async function wait_web_socket_is_open( socket, fnDone, fnStep ) {
                     reject( new Error( "web socket wait timout by callback on step " + nStep ) );
                 }
             }
+            isInsideAsyncHandler = false;
+        };
+        const iv = setInterval( function() {
+            if( isInsideAsyncHandler )
+                return;
+            fn_async_handler()
+                .then( () => {
+                } ).catch( () => {
+                } );
         }, 1000 ); // 1 second
     } );
     await Promise.all( [ promiseComplete ] );
@@ -82,7 +100,7 @@ async function wait_web_socket_is_open( socket, fnDone, fnStep ) {
 async function do_connect( joCall, opts, fn ) {
     try {
         fn = fn || async function() {};
-        if( !owaspUtils.validateURL( joCall.url ) )
+        if( !validateURL( joCall.url ) )
             throw new Error( "JSON RPC CALLER cannot connect web socket to invalid URL: " + joCall.url );
         if( is_ws_url( joCall.url ) ) {
             let strWsError = null;
@@ -92,15 +110,23 @@ async function do_connect( joCall, opts, fn ) {
             } );
             joCall.wsConn.on( "close", async function() {
                 strWsError = "web socket was closed, please check provided URL is valid and accessible";
-                joCall.wsConn = 0;
+                joCall.wsConn = null;
             } );
             joCall.wsConn.on( "error", async function( err ) {
                 strWsError = err.toString() || "internal web socket error";
                 log.write( cc.u( joCall.url ) + cc.error( " web socket error: " ) + cc.warning( err.toString() ) + "\n" );
+                const wsConn = joCall.wsConn;
+                joCall.wsConn = null;
+                wsConn.close();
+                do_reconnect_ws_step( joCall, opts );
             } );
             joCall.wsConn.on( "fail", async function( err ) {
                 strWsError = err.toString() || "internal web socket failure";
                 log.write( cc.u( joCall.url ) + cc.error( " web socket fail: " ) + cc.warning( err.toString() ) + "\n" );
+                const wsConn = joCall.wsConn;
+                joCall.wsConn = null;
+                wsConn.close();
+                do_reconnect_ws_step( joCall, opts );
             } );
             joCall.wsConn.on( "message", async function incoming( data ) {
                 // log.write( cc.info( "WS message " ) + cc.attention( data ) + "\n" );
@@ -108,6 +134,10 @@ async function do_connect( joCall, opts, fn ) {
                 if( joOut.id in joCall.mapPendingByCallID ) {
                     const entry = joCall.mapPendingByCallID[joOut.id];
                     delete joCall.mapPendingByCallID[joOut.id];
+                    if( entry.iv ) {
+                        clearTimeout( entry.iv );
+                        entry.iv = null;
+                    }
                     clearTimeout( entry.out );
                     await entry.fn( entry.joIn, joOut, null );
                 }
@@ -123,6 +153,10 @@ async function do_connect( joCall, opts, fn ) {
                     if( nStep >= g_nConnectionTimeoutSeconds ) {
                         strWsError = "wait timeout, web socket is connecting too long";
                         log.write( cc.u( joCall.url ) + cc.error( " web socket wait timeout detected" ) + "\n" );
+                        const wsConn = joCall.wsConn;
+                        joCall.wsConn = null;
+                        wsConn.close();
+                        do_reconnect_ws_step( joCall, opts );
                         return false; // stop waiting
                     }
                     return true; // continue waiting
@@ -138,12 +172,13 @@ async function do_connect( joCall, opts, fn ) {
         joCall.wsConn = null;
         await fn( joCall, err );
     }
+    return joCall;
 }
 
 async function do_connect_if_needed( joCall, opts, fn ) {
     try {
         fn = fn || async function() {};
-        if( !owaspUtils.validateURL( joCall.url ) )
+        if( !validateURL( joCall.url ) )
             throw new Error( "JSON RPC CALLER cannot connect web socket to invalid URL: " + joCall.url );
         if( is_ws_url( joCall.url ) && ( !joCall.wsConn ) ) {
             await joCall.reconnect( fn );
@@ -153,9 +188,43 @@ async function do_connect_if_needed( joCall, opts, fn ) {
     } catch ( err ) {
         await fn( joCall, err );
     }
+    return joCall;
 }
 
-const impl_sleep = ( milliseconds ) => { return new Promise( resolve => setTimeout( resolve, milliseconds ) ); };
+async function do_reconnect_ws_step( joCall, opts, fn ) {
+    if( ! joCall.isAutoReconnect )
+        return;
+    if( joCall.isDisconnectMode )
+        return;
+    fn = fn || async function() {};
+    do_connect( joCall, opts, async function( joCall, err ) {
+        if( err ) {
+            do_reconnect_ws_step( joCall, opts );
+            return;
+        }
+        await fn( joCall, null );
+    } );
+}
+
+async function do_disconnect( joCall, fn ) {
+    fn = fn || async function() {};
+    try {
+        joCall.isDisconnectMode = true;
+        const wsConn = joCall.wsConn ? joCall.wsConn : null;
+        joCall.wsConn = null;
+        if( wsConn )
+            wsConn.close();
+        joCall.isDisconnectMode = false;
+        try {
+            await fn( joCall, null );
+        } catch ( err ) {
+        }
+    } catch ( err ) {
+        await await fn( joCall, err );
+    }
+}
+
+const do_sleep = ( milliseconds ) => { return new Promise( resolve => setTimeout( resolve, milliseconds ) ); };
 
 async function do_call( joCall, joIn, fn ) {
     // console.log( "--- --- --- initial joIn is", joIn );
@@ -169,13 +238,15 @@ async function do_call( joCall, joIn, fn ) {
             out: null
         };
         joCall.mapPendingByCallID[joIn.id] = entry;
-        entry.out = setTimeout( function() {
+        entry.iv = setTimeout( function() {
+            clearTimeout( entry.iv );
+            entry.iv = null;
             delete joCall.mapPendingByCallID[joIn.id];
-        }, 20 * 1000 );
+        }, 200 * 1000 );
         joCall.wsConn.send( JSON.stringify( joIn ) );
     } else {
         // console.log( "--- --- --- call URL is", joCall.url );
-        if( !owaspUtils.validateURL( joCall.url ) ) {
+        if( !validateURL( joCall.url ) ) {
             // throw new Error( "JSON RPC CALLER cannot do query post to invalid URL: " + joCall.url );
             await fn( joIn, null, "JSON RPC CALLER cannot do query post to invalid URL: " + joCall.url );
             return;
@@ -223,17 +294,17 @@ async function do_call( joCall, joIn, fn ) {
         } );
         for( let idxWait = 0; ! bCompleteFlag; ++ idxWait ) {
             if( idxWait < 50 )
-                await impl_sleep( 5 );
+                await do_sleep( 5 );
             else if( idxWait < 100 )
-                await impl_sleep( 10 );
+                await do_sleep( 10 );
             else if( idxWait < 1000 )
-                await impl_sleep( 100 );
+                await do_sleep( 100 );
             else {
                 const nLastWaitPeriod = 200;
                 if( ( idxWait - 1000 ) * nLastWaitPeriod > g_nConnectionTimeoutSeconds )
                     bCompleteFlag = true;
                 else
-                    await impl_sleep( nLastWaitPeriod );
+                    await do_sleep( nLastWaitPeriod );
             }
         } // for( let idxWait = 0; ! bCompleteFlag; ++ idxWait )
         try {
@@ -244,7 +315,7 @@ async function do_call( joCall, joIn, fn ) {
 }
 
 async function rpc_call_create( strURL, opts, fn ) {
-    if( !owaspUtils.validateURL( strURL ) )
+    if( !validateURL( strURL ) )
         throw new Error( "JSON RPC CALLER cannot create a call object invalid URL: " + strURL );
     fn = fn || async function() {};
     if( !( strURL && typeof strURL == "string" && strURL.length > 0 ) )
@@ -254,6 +325,8 @@ async function rpc_call_create( strURL, opts, fn ) {
         "joRpcOptions": opts ? opts : null,
         "mapPendingByCallID": { },
         "wsConn": null,
+        "isAutoReconnect": ( opts && "isAutoReconnect" in opts && opts.isAutoReconnect ) ? true : false,
+        "isDisconnectMode": false,
         "reconnect": async function( fnAfter ) {
             await do_connect( joCall, fnAfter );
         },
@@ -269,9 +342,13 @@ async function rpc_call_create( strURL, opts, fn ) {
                 }
                 await do_call( joCall, joIn, fnAfter );
             } );
+        },
+        "disconnect": async function( fnAfter ) {
+            await do_disconnect( joCall, fnAfter );
         }
     };
     await do_connect( joCall, opts, fn );
+    return joCall;
 }
 
 function generate_random_integer_in_range( min, max ) {
@@ -426,5 +503,6 @@ module.exports = {
     get_valid_host_and_port: get_valid_host_and_port,
     check_tcp_promise: check_tcp_promise,
     check_tcp: check_tcp,
-    check_url: check_url
+    check_url: check_url,
+    sleep: do_sleep
 }; // module.exports
diff --git a/agent/run.sh b/agent/run.sh
index 77646efc8..205af234d 100644
--- a/agent/run.sh
+++ b/agent/run.sh
@@ -78,7 +78,6 @@ BASE_OPTIONS="--gas-price-multiplier=$GAS_PRICE_MULTIPLIER \
 --cid-s-chain=$CID_SCHAIN \
 --abi-main-net=$MAINNET_PROXY_PATH \
 --abi-s-chain=$SCHAIN_PROXY_PATH \
---state-file=$STATE_FILE \
 --sgx-url-s-chain=$SGX_URL \
 --sgx-ecdsa-key-s-chain=$ECDSA_KEY_NAME \
 --sgx-ssl-key-s-chain=$SGX_SSL_KEY_PATH \
@@ -86,7 +85,9 @@ BASE_OPTIONS="--gas-price-multiplier=$GAS_PRICE_MULTIPLIER \
 --address-main-net=$NODE_ADDRESS \
 --address-s-chain=$NODE_ADDRESS \
 --sign-messages \
+--gathered \
 --expose \
+--no-expose-security-info \
 --skip-dry-run \
 --bls-glue=/ima/bls_binaries/bls_glue \
 --hash-g1=/ima/bls_binaries/hash_g1 \
diff --git a/agent/yarn.lock b/agent/yarn.lock
index 0e786c078..756364277 100644
--- a/agent/yarn.lock
+++ b/agent/yarn.lock
@@ -1192,6 +1192,17 @@ ethereumjs-util@^7.1.0, ethereumjs-util@^7.1.2, ethereumjs-util@^7.1.3:
     ethereum-cryptography "^0.1.3"
     rlp "^2.2.4"
 
+ethereumjs-util@^7.1.4:
+  version "7.1.5"
+  resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-7.1.5.tgz#9ecf04861e4fbbeed7465ece5f23317ad1129181"
+  integrity sha512-SDl5kKrQAudFBUe5OJM9Ac6WmMyYmXX/6sTmLZ3ffG2eY6ZIGBes3pEDxNN6V72WyOw4CPD5RomKdsa8DAAwLg==
+  dependencies:
+    "@types/bn.js" "^5.1.0"
+    bn.js "^5.1.2"
+    create-hash "^1.1.2"
+    ethereum-cryptography "^0.1.3"
+    rlp "^2.2.4"
+
 ethereumjs-wallet@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/ethereumjs-wallet/-/ethereumjs-wallet-1.0.2.tgz#2c000504b4c71e8f3782dabe1113d192522e99b6"
@@ -2396,10 +2407,10 @@ number-is-nan@^1.0.0:
   resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
   integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
 
-number-to-bn@1.7.0:
+number-to-bn@1.7.0, number-to-bn@^1.7.0:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/number-to-bn/-/number-to-bn-1.7.0.tgz#bb3623592f7e5f9e0030b1977bd41a0c53fe1ea0"
-  integrity sha1-uzYjWS9+X54AMLGXe9QaDFP+HqA=
+  integrity sha512-wsJ9gfSz1/s4ZsJN01lyonwuxA1tml6X1yBDnfpMglypcBRFZZkus26EdPSlqS5GJfYddVZa22p3VNb3z5m5Ig==
   dependencies:
     bn.js "4.11.6"
     strip-hex-prefix "1.0.0"
@@ -3232,7 +3243,7 @@ thenify-all@^1.0.0:
 through@~2.3:
   version "2.3.8"
   resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
-  integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
+  integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==
 
 timed-out@^4.0.0, timed-out@^4.0.1:
   version "4.0.1"
@@ -3792,10 +3803,10 @@ ws@^3.0.0:
     safe-buffer "~5.1.0"
     ultron "~1.1.0"
 
-ws@^8.4.0:
-  version "8.4.0"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-8.4.0.tgz#f05e982a0a88c604080e8581576e2a063802bed6"
-  integrity sha512-IHVsKe2pjajSUIl4KYMQOdlyliovpEPquKkqbwswulszzI7r0SfQrxnXdWAEqOlDCLrVSJzo+O1hAwdog2sKSQ==
+ws@^8.6.0:
+  version "8.9.0"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-8.9.0.tgz#2a994bb67144be1b53fe2d23c53c028adeb7f45e"
+  integrity sha512-Ja7nszREasGaYUYCI2k4lCKIRTt+y7XuqVoHR44YpI49TtryyqbqvDMn5eqfW7e6HzTukDRIsXqzVHScqRcafg==
 
 xhr-request-promise@^0.1.2:
   version "0.1.3"
diff --git a/npms/skale-cc/cc.js b/npms/skale-cc/cc.js
index 10f5dc410..0c86e20a2 100644
--- a/npms/skale-cc/cc.js
+++ b/npms/skale-cc/cc.js
@@ -31,6 +31,63 @@ function replaceAll( str, find, replace ) {
     return str.replace( new RegExp( find, "g" ), replace );
 }
 
+function validateRadix( value, radix ) {
+    value = "" + ( value ? value.toString() : "10" );
+    value = value.trim();
+    radix = ( radix == null || radix == undefined )
+        ? ( ( value.length > 2 && value[0] == "0" && ( value[1] == "x" || value[1] == "X" ) ) ? 16 : 10 )
+        : parseInt( radix, 10 );
+    return radix;
+}
+
+function validateInteger( value, radix ) {
+    try {
+        value = "" + value;
+        value = value.trim();
+        if( value.length < 1 )
+            return false;
+        radix = validateRadix( value, radix );
+        if( ( !isNaN( value ) ) &&
+            ( parseInt( Number( value ), radix ) == value || radix !== 10 ) &&
+            ( !isNaN( parseInt( value, radix ) ) )
+        )
+            return true;
+    } catch ( err ) {
+    }
+    return false;
+}
+
+function toInteger( value, radix ) {
+    try {
+        radix = validateRadix( value, radix );
+        if( !validateInteger( value, radix ) )
+            return NaN;
+        return parseInt( value, radix );
+    } catch ( err ) {
+    }
+    return false;
+}
+
+function validateFloat( value ) {
+    try {
+        const f = parseFloat( value );
+        if( isNaN( f ) )
+            return false;
+        return true;
+    } catch ( err ) {
+    }
+    return false;
+}
+
+function toFloat( value ) {
+    try {
+        const f = parseFloat( value );
+        return f;
+    } catch ( err ) {
+    }
+    return false;
+}
+
 function toBoolean( value ) {
     let b = false;
     try {
diff --git a/npms/skale-cool-socket/.eslintrc.js b/npms/skale-cool-socket/.eslintrc.js
new file mode 100644
index 000000000..a2a8d690f
--- /dev/null
+++ b/npms/skale-cool-socket/.eslintrc.js
@@ -0,0 +1,102 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+
+/**
+ * @license
+ * SKALE COOL SOCKET
+ *
+ * This program 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.
+ *
+ * This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @file .eslintrc.js
+ * @copyright SKALE Labs 2019-Present
+ */
+
+module.exports = {
+    "env": {
+        "browser": true,
+        "es6": true
+    },
+    "extends": "standard",
+    "globals": {
+        "Atomics": "readonly",
+        "SharedArrayBuffer": "readonly"
+    },
+    "parserOptions": {
+        "ecmaVersion": 2018,
+        "sourceType": "module"
+    },
+    "rules": {
+        "indent": [ "error", 4 ],
+        "linebreak-style": [ "error", "unix" ],
+        "quotes": [ "error", "double" ],
+        "semi": [ "error", "always" ],
+        "camelcase": "off",
+        // "no-unused-vars": "off",
+        "eqeqeq": "off",
+        "comma-dangle": [ "error", "never" ],
+        "comma-style": [ "error", "last" ],
+        "comma-spacing": "off",
+        "space-before-function-paren": [ "error", "never" ],
+        "space-in-parens": [ "error", "always" ],
+        "keyword-spacing": [ "error", {
+            "overrides": {
+                "if": {
+                    "before": false,
+                    "after": false
+                },
+                "else": {
+                    "before": true,
+                    "after": true
+                },
+                "for": {
+                    "before": false,
+                    "after": false
+                },
+                "while": {
+                    "before": false,
+                    "after": false
+                }
+            }
+        } ],
+        "space-before-blocks": [ "error", "always" ],
+        "array-bracket-spacing": [ "error", "always" ],
+        "object-curly-spacing": [ "error", "always" ],
+        "space-unary-ops": "off",
+        "spaced-comment": "off",
+        "curly": [ "error", "multi-or-nest" ],
+        "nonblock-statement-body-position": [ "error", "below" ],
+        "one-var": "off",
+        "no-unneeded-ternary": "off",
+        "no-cond-assign": [ "error", "always" ],
+        "no-console": "off",
+        "new-cap": "off",
+        "no-tabs": "off",
+        "no-mixed-spaces-and-tabs": "off",
+        "no-prototype-builtins": "off",
+        "quote-props": "off",
+        "no-undef": "off",
+        "no-useless-return": "off",
+        "no-new": "off",
+        "no-useless-constructor": "off",
+        "no-lone-blocks": "off",
+        "no-fallthrough": "off",
+        "no-useless-catch": "off",
+        "padded-blocks": "off",
+        "no-use-before-define": "off", // [ "error", { "variables": false,  "functions": false } ],
+        "lines-between-class-members": [ "error", "never" ],
+        "no-var": "error",
+        "no-unused-vars": "error"
+    }
+};
diff --git a/npms/skale-cool-socket/event_dispatcher.js b/npms/skale-cool-socket/event_dispatcher.js
new file mode 100644
index 000000000..43e9e5f30
--- /dev/null
+++ b/npms/skale-cool-socket/event_dispatcher.js
@@ -0,0 +1,115 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+
+/**
+ * @license
+ * SKALE COOL SOCKET
+ *
+ * This program 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.
+ *
+ * This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @file event_dispatcher.js
+ * @copyright SKALE Labs 2019-Present
+ */
+
+class UniversalDispatcherEvent {
+    constructor( type, jo ) {
+        this.type = type;
+        for( const [ key, value ] of Object.entries( jo ) ) {
+            if( key in this ) {
+                console.warn( "UniversalDispatcherEvent will skip", key, "data field" );
+                continue;
+            }
+            this[key] = value;
+        }
+    }
+};
+
+class EventDispatcher {
+    // see https://stackoverflow.com/questions/36675693/eventtarget-interface-in-safari
+    constructor() {
+        this._listeners = [];
+        this.isDisposed = false;
+        this.isDisposing = false;
+    }
+    dispose() {
+        if( this.isDisposed )
+            return;
+        this.isDisposing = true;
+        this.isDisposed = true;
+        this.dispatchEvent( new UniversalDispatcherEvent( "dispose", { detail: { ref: this } } ) );
+        this.removeAllEventListeners();
+    }
+    hasEventListener( type, listener ) {
+        return this._listeners.some( item => item.type === type && item.listener === listener );
+    }
+    addEventListener( type, listener ) {
+        if( ! this.hasEventListener( type, listener ) ) {
+            this._listeners.push( {
+                type,
+                listener,
+                options: { once: false }
+            } );
+        }
+        // console.log( `${this}-listeners:`,this._listeners );
+        return this;
+    }
+    removeEventListener( type, listener ) {
+        while( true ) {
+            const index = ( listener != undefined )
+                ? this._listeners.findIndex( item => item.type === type && item.listener === listener )
+                : this._listeners.findIndex( item => item.type === type );
+            if( index >= 0 ) {
+                this._listeners.splice( index, 1 );
+                continue;
+            }
+            break;
+        }
+        // console.log( `${this}-listeners:`, this._listeners );
+        return this;
+    }
+    removeAllEventListeners() {
+        this._listeners = [];
+        return this;
+    }
+    on( type, listener ) {
+        return this.addEventListener( type, listener );
+    }
+    off( type, listener ) {
+        return this.removeEventListener( type, listener );
+    }
+    offAll() {
+        return this.removeAllEventListeners();
+    }
+    dispatchEvent( evt ) {
+        const a = this._listeners.filter( item => item.type === evt.type );
+        for( const item of a ) {
+            const {
+                type,
+                listener,
+                options: { once }
+            } = item;
+            listener.call( this, evt );
+            if( once === true )
+                this.removeEventListener( type, listener );
+        }
+        // console.log( `${this}-listeners:`,this._listeners );
+        return this;
+    }
+};
+
+module.exports = {
+    UniversalDispatcherEvent: UniversalDispatcherEvent,
+    EventDispatcher: EventDispatcher
+};
diff --git a/npms/skale-cool-socket/package.json b/npms/skale-cool-socket/package.json
new file mode 100644
index 000000000..e05e2f54a
--- /dev/null
+++ b/npms/skale-cool-socket/package.json
@@ -0,0 +1,35 @@
+{
+    "name": "skale-cool-socket",
+    "version": "1.2.3",
+    "description": "Cool socket implementation for direct, local, worker thread, web socket, web RTC connections",
+    "main": "test.js",
+    "license": "AGPL-3.0",
+    "author": "SKALE Labs and contributors",
+    "scripts": {
+        "lint-check": "eslint *.js",
+        "lint-fix": "eslint ./*.js --fix",
+        "test": "echo \"Error: no test specified\" && exit 1"
+    },
+    "keywords": [
+        "cool",
+        "socket",
+        "worker",
+        "thread",
+        "ws",
+        "wss",
+        "webrtc"
+    ],
+    "dependencies": {
+        "connect": "^3.7.0",
+        "serve-static": "^1.14.1",
+        "ws": "^8.6.0"
+    },
+    "devDependencies": {
+        "eslint": "^6.8.0",
+        "eslint-config-standard": "^14.1.1",
+        "eslint-plugin-import": "^2.20.2",
+        "eslint-plugin-node": "^11.1.0",
+        "eslint-plugin-promise": "^4.2.1",
+        "eslint-plugin-standard": "^4.0.1"
+    }
+}
diff --git a/npms/skale-cool-socket/server.js b/npms/skale-cool-socket/server.js
new file mode 100644
index 000000000..d4335a472
--- /dev/null
+++ b/npms/skale-cool-socket/server.js
@@ -0,0 +1,141 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+
+/**
+ * @license
+ * SKALE COOL SOCKET
+ *
+ * This program 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.
+ *
+ * This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @file server.js
+ * @copyright SKALE Labs 2019-Present
+ */
+
+const { EventDispatcher, UniversalDispatcherEvent } = require( "./event_dispatcher.js" );
+const utils = require( "./utils.js" );
+
+class Server extends EventDispatcher {
+    constructor( acceptor ) {
+        super();
+        if( acceptor == null || acceptor == undefined || typeof acceptor != "object" )
+            throw new Error( "Cannot create server on bad acceptor" );
+        const self = this;
+        self.log = console.log;
+        self.acceptor = acceptor;
+        self.mapApiHandlers = {};
+        self.mapAcceptedPipes = { };
+        self.isLogAcceptedSocket = false;
+        self.isLogSocketErrors = true;
+        self.isLogSocketTraffic = false;
+        self.isLogSocketTrafficRaw = false;
+        acceptor.on( "connection", function( eventData ) {
+            const socket = eventData.socket;
+            if( ( ! ( "remoteAddress" in eventData ) ) || eventData.remoteAddress == null || eventData.remoteAddress == undefined )
+                socket.strSavedRemoteAddress = socket.constructor.name;
+            else
+                socket.strSavedRemoteAddress = "" + eventData.remoteAddress;
+            if( self.isLogAcceptedSocket )
+                self.log( "New server connection \"" + socket.strSavedRemoteAddress + "\"" );
+            self.mapAcceptedPipes[socket] = { };
+            let _offAllPipeEventListeners = null;
+            let _onPipeClose = function() {
+                if( self.isLogAcceptedSocket )
+                    self.log( "Accepted socket closed \"" + socket.strSavedRemoteAddress + "\"" );
+                if( _offAllPipeEventListeners ) {
+                    _offAllPipeEventListeners();
+                    _offAllPipeEventListeners = null;
+                }
+                delete self.mapAcceptedPipes[socket];
+            };
+            let _onPipeError = function( eventData ) {
+                if( self.isLogSocketErrors )
+                    self.log( "Socket error \"" + socket.strSavedRemoteAddress + "\"" );
+                if( _offAllPipeEventListeners ) {
+                    _offAllPipeEventListeners();
+                    _offAllPipeEventListeners = null;
+                }
+                delete self.mapAcceptedPipes[socket];
+            };
+            let _onPipeMessage = function( eventData ) {
+                if( self.isLogSocketTrafficRaw )
+                    self.log( "Accepted socket \"" + socket.strSavedRemoteAddress + "\" raw message", eventData );
+                const joMessage = eventData.message;
+                if( self.isLogAcceptedSocket )
+                    self.log( "Accepted socket \"" + socket.strSavedRemoteAddress + "\" message", joMessage );
+                let joAnswer = null;
+                let isFlush = false;
+                try {
+                    if( joMessage.method in self.mapApiHandlers ) {
+                        joAnswer = utils.prepareAnswerJSON( joMessage );
+                        joAnswer = self.mapApiHandlers[joMessage.method]( joMessage, joAnswer, eventData, socket );
+                        if( joAnswer )
+                            isFlush = true;
+                    } else {
+                        joAnswer = utils.prepareAnswerJSON( joMessage );
+                        joAnswer.error = "Unhandled message";
+                        joAnswer.joMessage = joMessage; // send it back ))
+                        if( self.isLogSocketTraffic )
+                            self.log( "Accepted socket \"" + socket.strSavedRemoteAddress + "\" unhandled message", joMessage );
+                        isFlush = true;
+                    }
+                } catch ( err ) {
+                    if( self.isLogSocketErrors )
+                        self.log( "Server method", joMessage.method, "RPC exception:", err );
+                    joAnswer = utils.prepareAnswerJSON( joMessage );
+                    joAnswer.error = "" + err.toString();
+                }
+                //
+                //
+                if( joAnswer != null && joAnswer != undefined ) {
+                    if( typeof joAnswer.error == "string" && joAnswer.error.length > 0 ) {
+                        if( self.isLogSocketErrors )
+                            self.log( "Accepted socket \"" + socket.strSavedRemoteAddress + "\" error answer", joAnswer );
+                    } else {
+                        if( self.isLogSocketTraffic )
+                            self.log( "Accepted socket \"" + socket.strSavedRemoteAddress + " answer", joAnswer );
+                    }
+                    socket.send( joAnswer, isFlush );
+                }
+            };
+            _offAllPipeEventListeners = function() {
+                if( _onPipeClose ) {
+                    socket.off( "close", _onPipeClose );
+                    _onPipeClose = null;
+                }
+                if( _onPipeError ) {
+                    socket.off( "error", _onPipeError );
+                    _onPipeError = null;
+                }
+                if( _onPipeMessage ) {
+                    socket.off( "message", _onPipeMessage );
+                    _onPipeMessage = null;
+                }
+                socket.disposeImpersonatedEntries();
+            };
+            socket.on( "close", _onPipeClose );
+            socket.on( "error", _onPipeError );
+            socket.on( "message", _onPipeMessage );
+        } );
+        this.dispatchEvent( new UniversalDispatcherEvent( "initialized", { detail: { ref: this } } ) );
+    }
+    dispose() {
+        this.isDisposing = true;
+        super.dispose();
+    }
+};
+
+module.exports = {
+    Server: Server
+};
diff --git a/npms/skale-cool-socket/settings.js b/npms/skale-cool-socket/settings.js
new file mode 100644
index 000000000..ca1e6215c
--- /dev/null
+++ b/npms/skale-cool-socket/settings.js
@@ -0,0 +1,197 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+
+/**
+ * @license
+ * SKALE COOL SOCKET
+ *
+ * This program 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.
+ *
+ * This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @file settings.js
+ * @copyright SKALE Labs 2019-Present
+ */
+
+const settings = {
+    rtcSpace: {
+        defaultSpaceName: "default space",
+        defaultSpaceCategory: "default category"
+    },
+    net: {
+        hostname: "localhost",
+        secure: false,
+        ports: {
+            http: 8080, // 80, 443, 8080
+            ws: 17171,
+            signaling: 17172
+        },
+        pipe: {
+            maxAccumulatedMessagesCount: 30
+        },
+        ws: {
+            client: {
+                reconnectAfterMilliseconds: 100
+            }
+        },
+        rtc: {
+            arrKnownIceServers: [
+                // see: https://gist.github.com/mondain/b0ec1cf5f60ae726202e and https://gist.github.com/zziuni/3741933
+                // see: https://stackoverflow.com/questions/20068944/webrtc-stun-stun-l-google-com19302
+                // see: https://gist.github.com/yetithefoot/7592580
+                // even more to see: https://gist.github.com/sagivo/3a4b2f2c7ac6e1b5267c2f1f59ac6c6b
+                "stun:stun.1.google.com:19302",
+                "stun:stun.2.google.com:19302",
+                "stun:stun.3.google.com:19302",
+                "stun:stun.4.google.com:19302",
+                "stun:stun.5.google.com:19302", // where is the end?
+                "stun:stun.l.google.com:19302",
+                "stun:stun1.l.google.com:19302",
+                "stun:stun2.l.google.com:19302",
+                "stun:stun3.l.google.com:19302",
+                "stun:stun4.l.google.com:19302",
+                "stun:stun5.l.google.com:19302", // where is the end?
+                "stun:stun.gmx.net",
+                "stun:stun.sipgate.net",
+                "stun:stun.sipgate.net:10000",
+                "stun:stun.phoneserve.com",
+                "stun:stun.counterpath.net",
+                "stun:stun.12connect.com:3478",
+                "stun:stun.xten.com",
+                "stun:stun01.sipphone.com",
+                "stun:stun.ekiga.net",
+                "stun:stun.fwdnet.net",
+                "stun:stun.ideasip.com",
+                "stun:stun.iptel.org",
+                "stun:stun.schlund.de",
+                "stun:stun.voiparound.com",
+                "stun:stun.voipbuster.com",
+                "stun:stun.voipstunt.com",
+                "stun:stun.voxgratia.org"
+            ] ,
+            peerConfiguration: {
+                iceServers: [
+                    // { urls: "stun:192.168.88.220:3478", username: "webrtc", credential: "qwerty" }
+                    // { urls: "turn:192.168.88.220:3478", username: "webrtc", credential: "qwerty" }
+                    { urls: "stun:127.0.0.1:3478", username: "webrtc", credential: "qwerty" }
+                    // { urls: "turn:127.0.0.1:3478", username: "webrtc", credential: "qwerty" }
+                ]
+                // , iceTransportPolicy: "all"
+                // , iceCandidatePoolSize: "0" // efault value is 0 (meaning no candidate pre-fetching will occur).
+            },
+            peerAdditionalOptions: {
+                optional: [ { DtlsSrtpKeyAgreement: true } ]
+            },
+            dataChannel: {
+                label: "genericDataChannel",
+                opts: { reliable: true, ordered: true }
+            },
+            maxActiveOfferCount: 10,
+            isAutoCloseSignalingPipeOnDataChannelOpen: true, // network_layer.WebRTCClientPipe only
+            timeToPublishMilliseconds: 0, // 0 - no timeout, 300000 = 5 minutes, 60000 = 1 minute
+            timeToSignalingNegotiationMilliseconds: 0, // 0 - no timeout, 10000 = 10 seconds to identify by WebRTC
+            offerDiscovery: {
+                periodMilliseconds: 1000,
+                stepCount: 20
+            },
+            fastPublishMode: {
+                serverPeer: true,
+                joiner: true
+            }
+        }
+    },
+    logging: {
+        net: {
+            socket: {
+                flush: false,
+                flushOne: false,
+                flushBlock: false,
+                flushCount: false,
+                flushMethodStats: false,
+                accumulate: false,
+                send: false,
+                receive: false,
+                receiveBlock: false,
+                receiveCount: false,
+                receiveMethodStats: false
+            },
+            signaling: {
+                generic: false,
+                connect: true,
+                disconnect: true,
+                error: true,
+                rawMessage: false,
+                message: false,
+                impersonate: false,
+                publishOffer: false,
+                objectLifetime: true,
+                offer: false,
+                answer: false,
+                localDescription: false,
+                remoteDescription: false,
+                candidate: false,
+                candidateWalk: false,
+                publishTimeout: true,
+                signalingNegotiationTimeout: true,
+                offerDiscoveryStepFail: true,
+                offerRegister: false,
+                offerUnregister: false,
+                offerSkipPublishedAnswer: false,
+                creatorImpersonationComplete: false
+            },
+            rtc: {
+                generic: false,
+                error: true,
+                closePeer: true,
+                closeDataChannel: true,
+                iceConnectionStateChange: false,
+                iceConnectionStateName: false,
+                iceGatheringStateChange: false,
+                iceGatheringStateName: false,
+                iceIceIdentifyResult: false,
+                iceSignalingStateChange: false,
+                iceNegotiationNeeded: false
+            },
+            server: {
+                connect: true,
+                disconnect: true,
+                error: true,
+                rawMessage: false,
+                message: false,
+                impersonate: true,
+                weaponShot: false,
+                collisionDetection: true
+            },
+            client: {
+                space: { attach: true, detach: true },
+                connect: true,
+                disconnect: true,
+                error: true,
+                rawMessage: false,
+                message: false,
+                impersonate: true
+            },
+            relay: {
+                connect: true,
+                disconnect: true,
+                error: true,
+                rawMessage: false,
+                message: false
+            }
+        }
+    }
+};
+
+module.exports = {
+    settings: settings
+};
diff --git a/npms/skale-cool-socket/socket.js b/npms/skale-cool-socket/socket.js
new file mode 100644
index 000000000..6b2df9dd7
--- /dev/null
+++ b/npms/skale-cool-socket/socket.js
@@ -0,0 +1,2703 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+
+/**
+ * @license
+ * SKALE COOL SOCKET
+ *
+ * This program 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.
+ *
+ * This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @file socket.js
+ * @copyright SKALE Labs 2019-Present
+ */
+
+const { UniversalDispatcherEvent, EventDispatcher } = require( "./event_dispatcher.js" );
+const { settings } = require( "./settings.js" );
+const utils = require( "./utils.js" );
+
+let https_mod = null; // server side only
+let ws_mod = null; // server side only
+let wrtc_mod = null; // server side only
+
+// if( typeof window == "undefined" ) {
+//     try {
+//         https_mod = require( "https" );
+//         console.log( "Using pre-loaded HTTPS API in socket.js" );
+//     } catch ( err ) {
+//     }
+//     try {
+//         ws_mod = WebSocket;
+//         console.log( "Using pre-loaded WebSocket API in socket.js" );
+//     } catch ( err ) {
+//     }
+// } else {
+//     try {
+//         wrtc_mod = window;
+//         console.log( "Using pre-loaded WebSocket API in socket.js" );
+//     } catch ( err ) {
+//     }
+// }
+
+// needed to init from outside: import * as https_loaded_mod from "https";
+// const https_mod = https_loaded_mod.default;
+// needed to init from outside: import * as ws_loaded_mod from "ws";
+// const ws_mod = ws_loaded_mod.default;
+
+function set_https_mod( mod ) {
+    https_mod = mod ? mod : null;
+}
+function set_ws_mod( mod ) {
+    ws_mod = mod ? mod : null;
+}
+function set_wrtc_mod( mod ) {
+    wrtc_mod = mod ? mod : null;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+const g_mapLocalServers = { }; // used both for local and in-worker servers
+
+const socket_sent_data_marshall = function( data ) {
+    const s = data
+        ? ( ( typeof data == "string" )
+            ? data
+            : ( ( typeof data == "object" ) ? JSON.stringify( data ) : data.toString() )
+        )
+        : "";
+    return s;
+};
+const socket_received_data_reverse_marshall = function( data ) {
+    try {
+        const jo = data
+            ? ( ( typeof data == "object" )
+                ? data
+                : ( ( typeof data == "string" ) ? JSON.parse( data ) : data )
+            )
+            : { };
+        return jo;
+    } catch ( err ) {
+        return {
+            error: true,
+            message: "data un-marshal error",
+            data: data
+        };
+    }
+};
+
+const update_socket_data_stats_for_message = function( joMessage, joStats ) {
+    let strMethod = "_N/A_";
+    if( "method" in joMessage &&
+        joMessage.method &&
+        typeof joMessage.method == "string"
+    )
+        strMethod = "" + joMessage.method;
+    if( strMethod in joStats )
+        joStats[strMethod] ++;
+    else
+        joStats[strMethod] = 1;
+};
+const generate_socket_data_stats_JSON = function( jo ) {
+    const joStats = {};
+    //let cnt = 1;
+    if( "arr_packed_messages" in jo &&
+        jo.arr_packed_messages &&
+        typeof jo.arr_packed_messages == "object"
+    ) {
+        //cnt = jo.arr_packed_messages.length;
+        for( const joMessage of jo.arr_packed_messages )
+            update_socket_data_stats_for_message( joMessage, joStats );
+
+    } else
+        update_socket_data_stats_for_message( jo, joStats );
+    //joStats["_cnt_"] = cnt;
+    return joStats;
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class BasicServerAcceptor extends EventDispatcher {
+    constructor() {
+        super();
+        this.socketType = "BasicAcceptor";
+        this.socketSubtype = "acceptor";
+        this.isListening = false;
+        this.strEndPoint = null;
+        this.nextClientNumber = 1;
+        this.mapClients = { };
+    }
+    dispose() {
+        if( this.isDisposed )
+            return;
+        this.isDisposing = true;
+        this.nextClientNumber = 1;
+        this.isListening = false;
+        this.disposeNotifyClients();
+        super.dispose();
+    }
+    disposeNotifyClients() {
+        for( const [ /*key*/, entry ] of Object.entries( this.mapClients ) ) {
+            if( ( "serverPipe" in entry ) && ( "clientPipe" in entry ) ) {
+                const pair = entry;
+                pair.serverPipe.handleServerDisposed();
+                pair.clientPipe.handleServerDisposed();
+                pair.serverPipe = null;
+                pair.clientPipe = null;
+            } else {
+                const pipe = entry;
+                pipe.handleServerDisposed();
+            }
+        }
+        this.mapClients = { };
+    }
+    unregisterClientByKey( key ) {
+        if( key in this.mapClients ) {
+            const entry = this.mapClients[key];
+            if( entry ) {
+                if( ( "serverPipe" in entry ) && ( "clientPipe" in entry ) ) {
+                    const pair = entry;
+                    pair.serverPipe = null;
+                    pair.clientPipe = null;
+                }
+                delete this.mapClients[key];
+            }
+        }
+    }
+    flush() {
+        if( this.isDisposing || this.isDisposed )
+            return;
+        for( const [ /*key*/, entry ] of Object.entries( this.mapClients ) ) {
+            if( ( "serverPipe" in entry ) && ( "clientPipe" in entry ) ) {
+                const pair = entry;
+                pair.serverPipe.flush();
+            } else {
+                const pipe = entry;
+                pipe.flush();
+            }
+        }
+    }
+    newDirectConnection() {
+        if( this.isDisposing || this.isDisposed )
+            return null;
+        if( !this.isListening )
+            return null;
+        const clientPipe = new DirectPipe( null, false );
+        const serverPipe = new DirectPipe( clientPipe, false );
+        serverPipe.acceptor = this;
+        this.mapClients["" + serverPipe.clientPort] = serverPipe;
+        const self = this;
+        const iv = setTimeout( function() {
+            clearTimeout( iv );
+            serverPipe.dispatchEvent( new UniversalDispatcherEvent( "open", { socket: serverPipe } ) );
+            self.dispatchEvent( new UniversalDispatcherEvent( "connection", { socket: serverPipe, remoteAddress: "" + self.url } ) );
+            clientPipe.dispatchEvent( new UniversalDispatcherEvent( "open", { socket: clientPipe } ) );
+        }, 0 );
+        return clientPipe;
+    }
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class BasicSocketPipe extends EventDispatcher {
+    constructor() {
+        super();
+        this.socketType = "N/A";
+        this.socketSubtype = "N/A";
+        this.url = "N/A";
+        this.isConnected = true;
+        this.arr_accumulated_messages = [];
+        this.maxAccumulatedMessagesCount = 0 + settings.net.pipe.maxAccumulatedMessagesCount;
+        this.relayClientSocket = null; // for relay only
+        this.mapImpersonatedEntries = { }; // for external in-app usage only
+    }
+    dispose() {
+        if( this.relayClientSocket ) {
+            this.relayClientSocket.dispose();
+            this.relayClientSocket = null;
+        }
+        this.disposeImpersonatedEntries(); // for external in-app usage only
+        this.disconnect();
+        this.arr_accumulated_messages = [];
+        super.dispose();
+    }
+    disposeImpersonatedEntries() { // for external in-app usage only
+        for( const [ /*key*/, entry ] of Object.entries( this.mapImpersonatedEntries ) ) {
+            try {
+                if( entry && "dispose" in entry && typeof entry.dispose == "function" )
+                    entry.dispose();
+            } catch ( err ) {
+            }
+        }
+        this.mapImpersonatedEntries = { }; // for app usage
+    }
+    impl_send( data ) {
+        throw new Error( "BasicSocketPipe.impl_send() must be overridden but calling it was attempted" );
+    }
+    is_auto_flush() {
+        if( this.maxAccumulatedMessagesCount <= 1 )
+            return true;
+        const cnt = this.arr_accumulated_messages.length;
+        if( cnt == 0 || cnt < this.maxAccumulatedMessagesCount )
+            return false;
+        return true;
+    }
+    socketDescription() {
+        return "" +
+            // + this.socketType
+            // + " "
+            // + this.socketSubtype
+            // + " "
+            this.url;
+    }
+    socketLoggingTextPrefix( strLogEventName ) {
+        return "" + strLogEventName + " " + this.socketDescription() + " -";
+    }
+    send( data, isFlush ) {
+        if( this.isDisposed || ( !this.isConnected ) )
+            return;
+        if( this.is_auto_flush() ) {
+            if( settings.logging.net.socket.send || settings.logging.net.socket.flush )
+                console.log( this.socketLoggingTextPrefix( "send+flush" ), data );
+            this.impl_send( data );
+            return;
+        }
+        isFlush = ( isFlush == undefined || isFlush == null ) ? true : ( isFlush ? true : false );
+        const jo = socket_received_data_reverse_marshall( data );
+        if( settings.logging.net.socket.accumulate )
+            console.log( this.socketLoggingTextPrefix( "accumulate" ), data );
+        this.arr_accumulated_messages.push( jo );
+        if( isFlush )
+            this.flush();
+    }
+    flush() {
+        if( this.isDisposed || ( !this.isConnected ) )
+            return;
+        const cnt = this.arr_accumulated_messages.length;
+        if( cnt == 0 )
+            return;
+        if( settings.logging.net.socket.flushCount )
+            console.log( this.socketLoggingTextPrefix( "flush-count(" + cnt + ")" ) );
+        let joSend = null;
+        if( cnt == 1 ) {
+            joSend = this.arr_accumulated_messages[0];
+            if( settings.logging.net.socket.flushOne || settings.logging.net.socket.flush )
+                console.log( this.socketLoggingTextPrefix( "flush-one" ), joSend );
+        } else {
+            joSend = { arr_packed_messages: this.arr_accumulated_messages };
+            if( settings.logging.net.socket.flushBlock || settings.logging.net.socket.flush )
+                console.log( this.socketLoggingTextPrefix( "flush-block(" + cnt + ")" ), joSend );
+        }
+        if( settings.logging.net.socket.flushMethodStats )
+            console.log( this.socketLoggingTextPrefix( "flush-method-stats(" + cnt + ")" ), generate_socket_data_stats_JSON( joSend ) );
+        this.impl_send( joSend );
+        this.arr_accumulated_messages = [];
+        if( this.relayClientSocket )
+            this.relayClientSocket.flush();
+    }
+    impl_receive( data ) {
+        const jo = socket_received_data_reverse_marshall( data );
+        this.dispatchEvent( new UniversalDispatcherEvent( "message", { socket: this, message: jo } ) );
+    }
+    receive( data ) {
+        if( settings.logging.net.socket.receiveBlock )
+            console.log( this.socketLoggingTextPrefix( "receive-block" ), data );
+        const jo = socket_received_data_reverse_marshall( data );
+        if( "arr_packed_messages" in jo &&
+            jo.arr_packed_messages &&
+            typeof jo.arr_packed_messages == "object"
+        ) {
+            const cnt = jo.arr_packed_messages.length;
+            if( settings.logging.net.socket.receiveCount )
+                console.log( this.socketLoggingTextPrefix( "receive-count(" + cnt + ")" ) );
+            if( settings.logging.net.socket.receiveMethodStats )
+                console.log( this.socketLoggingTextPrefix( "receive-method-stats(" + cnt + ")" ), generate_socket_data_stats_JSON( jo ) );
+            for( let i = 0; i < cnt; ++ i ) {
+                const joMessage = jo.arr_packed_messages[i];
+                if( settings.logging.net.socket.receive )
+                    console.log( this.socketLoggingTextPrefix( "receive" ), joMessage );
+                this.impl_receive( joMessage );
+            }
+            return;
+        }
+        if( settings.logging.net.socket.receiveCount )
+            console.log( this.socketLoggingTextPrefix( "receive-count(" + 1 + ")" ) );
+        if( settings.logging.net.socket.receiveMethodStats )
+            console.log( this.socketLoggingTextPrefix( "receive-method-stats(" + 1 + ")" ), generate_socket_data_stats_JSON( jo ) );
+        if( settings.logging.net.socket.receive )
+            console.log( this.socketLoggingTextPrefix( "receive" ), jo );
+        this.impl_receive( jo );
+    }
+    disconnect() {
+        this.isConnected = false;
+        // if( this.relayClientSocket ) {
+        //     this.relayClientSocket.disconnect();
+        //     this.relayClientSocket = null;
+        // }
+    }
+    reconnect() {
+    }
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class NullSocketPipe extends BasicSocketPipe {
+    constructor() {
+        super();
+        this.socketType = "NULL";
+        this.socketSubtype = "pipe";
+        this.url = "NullUrl";
+        this.isConnected = true;
+    }
+    dispose() {
+        this.isConnected = false;
+        super.dispose();
+    }
+    impl_send( data ) {
+    }
+    impl_receive( data ) {
+    }
+    send( data ) {
+    }
+    receive( data ) {
+    }
+    flush() {
+    }
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+const is_running_in_worker = function() {
+    if( self.document === undefined )
+        return true;
+    return false;
+};
+
+const g_map_awaiting_in_worker_clients = { }; // in-worker clients in connecting state
+const g_map_connected_in_worker_clients = { }; // in-worker clients in connecting state
+
+const out_of_worker_apis = {
+    on_message: function( worker, data ) {
+        const jo = socket_received_data_reverse_marshall( data );
+        if( ! ( "worker_message_type" in jo ) || typeof jo.worker_message_type != "string" || jo.worker_message_type.length == 0 )
+            return false; // not a socket message
+        if( ! ( "worker_endpoint" in jo ) || typeof jo.worker_endpoint != "string" || jo.worker_endpoint.length == 0 )
+            return false; // TO-DO: send error answer and return true
+        if( ! ( "worker_uuid" in jo ) || typeof jo.worker_uuid != "string" || jo.worker_uuid.length == 0 )
+            return false; // TO-DO: send error answer and return true
+        switch ( jo.worker_message_type ) {
+        case "in_worker_connect": {
+            if( !( jo.worker_uuid in g_map_awaiting_in_worker_clients ) )
+                return false;
+            const pipe = g_map_awaiting_in_worker_clients[jo.worker_uuid];
+            pipe.performSuccessfulConnection();
+        } return true;
+        case "in_worker_disconnect": {
+            if( !( jo.worker_uuid in g_map_connected_in_worker_clients ) )
+                return false;
+            const pipe = g_map_connected_in_worker_clients[jo.worker_uuid];
+            pipe.performDisconnect();
+        } return true;
+        case "in_worker_message": {
+            if( !( jo.worker_uuid in g_map_connected_in_worker_clients ) )
+                return false;
+            const pipe = g_map_connected_in_worker_clients[jo.worker_uuid];
+            pipe.receive( jo.data );
+        } return true;
+        default:
+            return false; // TO-DO: send error answer and return true
+        } // switch( jo.worker_message_type )
+    },
+    on_send_message: function( worker, type, endpoint, worker_uuid, data ) {
+        const jo = socket_received_data_reverse_marshall( data );
+        const joSend = {
+            worker_message_type: ( type && typeof type == "string" && type.length > 0 ) ? type : "in_worker_message",
+            worker_endpoint: endpoint,
+            worker_uuid: worker_uuid,
+            data: jo
+        };
+        //worker.postMessage( socket_received_data_reverse_marshall( joSend ) );
+        worker.postMessage( socket_sent_data_marshall( joSend ) );
+    }
+};
+const in_worker_apis = {
+    on_message: function( data ) {
+        const jo = socket_received_data_reverse_marshall( data );
+        if( ! ( "worker_message_type" in jo ) || typeof jo.worker_message_type != "string" || jo.worker_message_type.length == 0 )
+            return false; // not a socket message
+        if( ! ( "worker_endpoint" in jo ) || typeof jo.worker_endpoint != "string" || jo.worker_endpoint.length == 0 )
+            return false; // TO-DO: send error answer and return true
+        if( ! ( "worker_uuid" in jo ) || typeof jo.worker_uuid != "string" || jo.worker_uuid.length == 0 )
+            return false; // TO-DO: send error answer and return true
+        if( ! ( jo.worker_endpoint in g_mapLocalServers ) )
+            return false; // TO-DO: send error answer and return true
+        const acceptor = g_mapLocalServers[jo.worker_endpoint];
+        switch ( jo.worker_message_type ) {
+        case "in_worker_connect":
+            return acceptor.performAccept( jo );
+        case "in_worker_disconnect":
+            return acceptor.performDisconnect( jo );
+        case "in_worker_message":
+            return acceptor.receiveForClientPort( jo.worker_uuid, jo.data );
+        default:
+            return false; // TO-DO: send error answer and return true
+        } // switch( jo.worker_message_type )
+    },
+    on_send_message: function( type, endpoint, worker_uuid, data ) {
+        const jo = socket_received_data_reverse_marshall( data );
+        const joSend = {
+            worker_message_type: ( type && typeof type == "string" && type.length > 0 ) ? type : "in_worker_message",
+            worker_endpoint: endpoint,
+            worker_uuid: worker_uuid,
+            data: jo
+        };
+        //postMessage( socket_received_data_reverse_marshall( joSend ) );
+        postMessage( socket_sent_data_marshall( joSend ) );
+    }
+};
+
+class InWorkerServerPipe extends BasicSocketPipe {
+    constructor( acceptor, clientPort, fnSend ) {
+        super();
+        this.socketType = "InWorker";
+        this.socketSubtype = "server";
+        this.isConnected = true;
+        this.acceptor = acceptor;
+        this.clientPort = "" + clientPort;
+        this.fnSend = fnSend || in_worker_apis.on_send_message;
+        this.url = "in_worker_server_pipe://" + acceptor.strEndPoint + ":" + clientPort;
+        this.acceptor.mapClients[this.clientPort] = this;
+        this.fnSend( "in_worker_connect", this.acceptor.strEndPoint, this.clientPort, {} );
+        const self = this;
+        const iv = setTimeout( function() {
+            clearTimeout( iv );
+            self.dispatchEvent( new UniversalDispatcherEvent( "open", { socket: self } ) );
+            self.acceptor.dispatchEvent( new UniversalDispatcherEvent( "connection", { socket: self, remoteAddress: "" + self.url } ) );
+        }, 0 );
+    }
+    dispose() {
+        this.performDisconnect();
+        super.dispose();
+    }
+    handleServerDisposed() {
+        this.performDisconnect();
+        this.isConnected = false;
+        this.dispatchEvent( new UniversalDispatcherEvent( "close", { socket: this } ) );
+        this.acceptor = null;
+        this.fnSend = null;
+        this.url = "";
+        this.dispose();
+    }
+    performDisconnect() {
+        if( ! this.isConnected )
+            return;
+        this.fnSend( "in_worker_disconnect", this.acceptor.strEndPoint, this.clientPort, {} );
+        this.isConnected = false;
+        if( this.acceptor )
+            this.acceptor.unregisterClientByKey( this.clientPort );
+        this.dispatchEvent( new UniversalDispatcherEvent( "close", { socket: this } ) );
+        this.acceptor = null;
+        this.fnSend = null;
+        this.url = "";
+    }
+    impl_send( data ) {
+        if( ( !this.isConnected ) || ( !this.fnSend ) || typeof this.fnSend != "function" ) {
+            const s = "Cannot send messages to disconnected in-worker server pipe";
+            this.dispatchEvent( new UniversalDispatcherEvent( "error", { socket: this, message: "" + s } ) );
+            throw new Error( s );
+        }
+        const jo = socket_received_data_reverse_marshall( data );
+        this.fnSend( "in_worker_message", this.acceptor.strEndPoint, this.clientPort, jo );
+    }
+    disconnect() {
+        this.performDisconnect();
+        super.disconnect();
+    }
+};
+
+class InWorkerSocketServerAcceptor extends BasicServerAcceptor {
+    constructor( strEndPoint, fnSend ) {
+        super();
+        this.socketType = "InWorker";
+        this.strEndPoint = ( strEndPoint && typeof strEndPoint == "string" && strEndPoint.length > 0 ) ? strEndPoint : "default_local_endpoint";
+        if( this.strEndPoint in g_mapLocalServers ) {
+            const s = "Cannot start in-worker socket server on already listening \"" + this.strEndPoint + "\" endpoint";
+            this.dispatchEvent( new UniversalDispatcherEvent( "error", { socket: this, message: "" + s } ) );
+            throw new Error( s );
+        }
+        g_mapLocalServers[this.strEndPoint] = this;
+        this.fnSend = fnSend || in_worker_apis.on_send_message;
+        this.isListening = true;
+        const self = this;
+        const iv = setTimeout( function() {
+            clearTimeout( iv );
+            self.dispatchEvent( new UniversalDispatcherEvent( "open", { socket: self } ) );
+        }, 0 );
+    }
+    dispose() {
+        if( this.isDisposed )
+            return;
+        this.isDisposing = true;
+        this.disposeNotifyClients();
+        if( this.strEndPoint && typeof this.strEndPoint == "string" && this.strEndPoint.length > 0 ) {
+            if( this.strEndPoint in g_mapLocalServers )
+                delete g_mapLocalServers[this.strEndPoint];
+        }
+        super.dispose();
+    }
+    performAccept( jo ) {
+        if( jo.worker_uuid in this.mapClients )
+            return false; // TO-DO: send error answer and return true
+        // const pipe =
+        new InWorkerServerPipe( this, "" + jo.worker_uuid, this.fnSend );
+        return true;
+    }
+    performDisconnect( jo ) {
+        if( ! ( jo.worker_uuid in this.mapClients ) )
+            return false; // TO-DO: send error answer and return true
+        const pipe = this.mapClients[jo.worker_uuid];
+        pipe.performDisconnect();
+        return true;
+    }
+    receiveForClientPort( clientPort, jo ) {
+        if( ! ( clientPort in this.mapClients ) )
+            return false; // TO-DO: send error answer and return true
+        const pipe = this.mapClients[clientPort];
+        pipe.receive( jo );
+        return true;
+    }
+};
+
+class OutOfWorkerSocketClientPipe extends BasicSocketPipe {
+    constructor( strEndPoint, worker, fnSend ) {
+        super();
+        this.socketType = "InWorker";
+        this.socketSubtype = "client";
+        this.isConnected = false;
+        this.worker = worker;
+        this.clientPort = utils.uuid_v4();
+        this.strEndPoint = ( strEndPoint && typeof strEndPoint == "string" && strEndPoint.length > 0 ) ? strEndPoint : "default_in_worker_endpoint";
+        this.url = "out_of_worker_client_pipe://" + this.strEndPoint + ":" + this.clientPort;
+        this.fnSend = fnSend || out_of_worker_apis.on_send_message;
+        this.fnSend( this.worker, "in_worker_connect", this.strEndPoint, this.clientPort, {} );
+        g_map_awaiting_in_worker_clients["" + this.clientPort] = this;
+    }
+    dispose() {
+        if( this.isDisposed )
+            return;
+        this.isDisposing = true;
+        this.performDisconnect();
+        if( this.clientPort in g_map_awaiting_in_worker_clients )
+            delete g_map_awaiting_in_worker_clients[this.clientPort];
+        super.dispose();
+    }
+    performDisconnect() {
+        if( ! this.isConnected )
+            return;
+        this.isConnected = false;
+        delete g_map_connected_in_worker_clients["" + this.clientPort];
+        this.fnSend( this.worker, "in_worker_disconnect", this.strEndPoint, this.clientPort, {} );
+        this.dispatchEvent( new UniversalDispatcherEvent( "close", { socket: this } ) );
+        this.worker = null;
+        this.clientPort = "";
+        this.strEndPoint = "";
+        this.url = "";
+    }
+    performSuccessfulConnection() {
+        delete g_map_awaiting_in_worker_clients[this.clientPort];
+        g_map_connected_in_worker_clients["" + this.clientPort] = this;
+        this.isConnected = true;
+        this.dispatchEvent( new UniversalDispatcherEvent( "open", { socket: this } ) );
+    }
+    impl_send( data ) {
+        if( ( !this.isConnected ) || ( !this.worker ) || ( !this.fnSend ) || typeof this.fnSend != "function" ) {
+            const s = "Cannot send messages to disconnected in-worker client pipe";
+            this.dispatchEvent( new UniversalDispatcherEvent( "error", { socket: this, message: "" + s } ) );
+            throw new Error( s );
+        }
+        const jo = socket_received_data_reverse_marshall( data );
+        this.fnSend( this.worker, "in_worker_message", this.strEndPoint, this.clientPort, jo );
+    }
+    disconnect() {
+        this.performDisconnect();
+        super.disconnect();
+    }
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class OutOfWorkerRelay extends EventDispatcher {
+    constructor( strRelayName, acceptor, fnCreateClient, isAutoFlushIncoming, isAutoFlushOutgoing ) {
+        super();
+        const self = this;
+        self.strRelayName = ( strRelayName != null && strRelayName != undefined && typeof strRelayName == "string" && strRelayName.length > 0 ) ? ( "" + strRelayName ) : "unnamed";
+        self.isAutoFlushIncoming = ( isAutoFlushIncoming == null || isAutoFlushIncoming == undefined ) ? true : ( isAutoFlushIncoming ? true : false );
+        self.isAutoFlushOutgoing = ( isAutoFlushOutgoing == null || isAutoFlushOutgoing == undefined ) ? true : ( isAutoFlushOutgoing ? true : false );
+        if( ! acceptor )
+            throw new Error( "OutOfWorkerRelay \"" + self.strRelayName + "\" needs acceptor for normal functionality" );
+        if( typeof fnCreateClient != "function" )
+            throw new Error( "OutOfWorkerRelay \"" + self.strRelayName + "\" needs callback to create connections to target server" );
+        self.acceptor = acceptor;
+        self.fnCreateClient = fnCreateClient;
+        self.onConnection_ = function( eventData ) {
+            const pipeIncoming = eventData.socket;
+            let pipeOutgoing = null;
+            if( ( ! ( "remoteAddress" in eventData ) ) || eventData.remoteAddress == null || eventData.remoteAddress == undefined )
+                pipeIncoming.strSavedRemoteAddress = pipeIncoming.constructor.name;
+            else
+                pipeIncoming.strSavedRemoteAddress = "" + eventData.remoteAddress;
+            if( settings.logging.net.relay.connect )
+                console.log( "Relay \"" + self.strRelayName + "\" got new external-client connection \"" + pipeIncoming.strSavedRemoteAddress + "\"" );
+            self.dispatchEvent( new UniversalDispatcherEvent( "connection", { relay: self, socket: pipeIncoming, remoteAddress: "" + pipeIncoming.strSavedRemoteAddress } ) );
+            //
+            // 1) configure incoming pipe
+            //
+            let _offAllPipeEventListeners = null;
+            let _onExternalPipeClose = function() {
+                if( settings.logging.net.relay.disconnect )
+                    console.warn( "Relay \"" + self.strRelayName + "\" external-client socket closed \"" + pipeIncoming.strSavedRemoteAddress + "\"" );
+                self.dispatchEvent( new UniversalDispatcherEvent( "close", { relay: self, socket: pipeIncoming, remoteAddress: "" + pipeIncoming.strSavedRemoteAddress, isExternalSocket: true } ) );
+                if( _offAllPipeEventListeners ) {
+                    _offAllPipeEventListeners();
+                    _offAllPipeEventListeners = null;
+                }
+            };
+            let _onRelayPipeClose = function() {
+                if( settings.logging.net.relay.disconnect )
+                    console.warn( "Relay \"" + self.strRelayName + "\" relay-client socket closed \"" + pipeIncoming.strSavedRemoteAddress + "\"" );
+                self.dispatchEvent( new UniversalDispatcherEvent( "close", { relay: self, socket: pipeIncoming, remoteAddress: "" + pipeIncoming.strSavedRemoteAddress, isExternalSocket: false } ) );
+                if( _offAllPipeEventListeners ) {
+                    _offAllPipeEventListeners();
+                    _offAllPipeEventListeners = null;
+                }
+            };
+            let _onExternalPipeError = function( eventData ) {
+                if( settings.logging.net.relay.error )
+                    console.warn( "Relay client  \"" + self.strRelayName + "\" external-client socket error \"" + pipeIncoming.strSavedRemoteAddress + "\"" );
+                self.dispatchEvent( new UniversalDispatcherEvent( "error", { relay: self, socket: pipeIncoming, remoteAddress: "" + pipeIncoming.strSavedRemoteAddress, isExternalSocket: true } ) );
+                if( _offAllPipeEventListeners ) {
+                    _offAllPipeEventListeners();
+                    _offAllPipeEventListeners = null;
+                }
+            };
+            let _onRelayPipeError = function( eventData ) {
+                if( settings.logging.net.relay.error )
+                    console.warn( "Relay client  \"" + self.strRelayName + "\" relay-client socket error \"" + pipeIncoming.strSavedRemoteAddress + "\"" );
+                self.dispatchEvent( new UniversalDispatcherEvent( "error", { relay: self, socket: pipeIncoming, remoteAddress: "" + pipeIncoming.strSavedRemoteAddress, isExternalSocket: false } ) );
+                if( _offAllPipeEventListeners ) {
+                    _offAllPipeEventListeners();
+                    _offAllPipeEventListeners = null;
+                }
+            };
+            let _onExternalPipeMessage = function( eventData ) {
+                if( settings.logging.net.relay.rawMessage )
+                    console.log( "Relay \"" + self.strRelayName + "\" external-client socket \"" + eventData.strSavedRemoteAddress + "\" raw message", eventData );
+                const joMessage = eventData.message;
+                if( settings.logging.net.relay.message )
+                    console.log( "Relay \"" + self.strRelayName + "\" external-client socket \"" + pipeIncoming.strSavedRemoteAddress + "\" message ", joMessage );
+                if( ! pipeOutgoing )
+                    throw new Error( "Relay \"" + self.strRelayName + "\" is not completely initialized and cannot transfer messages" );
+                self.dispatchEvent( new UniversalDispatcherEvent( "message", { relay: self, socket: pipeIncoming, remoteAddress: "" + pipeIncoming.strSavedRemoteAddress, isExternalSocket: true, message: joMessage } ) );
+                pipeOutgoing.send( joMessage );
+                if( self.isAutoFlushIncoming )
+                    pipeOutgoing.flush();
+            };
+            let _onRelayPipeMessage = function( eventData ) {
+                if( settings.logging.net.relay.rawMessage )
+                    console.log( "Relay \"" + self.strRelayName + "\" relay-client socket \"" + eventData.strSavedRemoteAddress + "\" raw message", eventData );
+                const joMessage = eventData.message;
+                if( settings.logging.net.relay.message )
+                    console.log( "Relay \"" + self.strRelayName + "\" relay-client socket \"" + pipeIncoming.strSavedRemoteAddress + "\" message ", joMessage );
+                if( ! pipeOutgoing )
+                    throw new Error( "Relay \"" + self.strRelayName + "\" is not completely initialized and cannot transfer messages" );
+                self.dispatchEvent( new UniversalDispatcherEvent( "message", { relay: self, socket: pipeIncoming, remoteAddress: "" + pipeIncoming.strSavedRemoteAddress, isExternalSocket: false, message: joMessage } ) );
+                pipeOutgoing.send( joMessage );
+                if( self.isAutoFlushOutgoing )
+                    pipeOutgoing.flush();
+            };
+            _offAllPipeEventListeners = function() {
+                if( _onExternalPipeClose ) {
+                    pipeIncoming.off( "close", _onExternalPipeClose );
+                    _onExternalPipeClose = null;
+                }
+                if( _onExternalPipeError ) {
+                    pipeIncoming.off( "error", _onExternalPipeError );
+                    _onExternalPipeError = null;
+                }
+                if( _onExternalPipeMessage ) {
+                    pipeIncoming.off( "message", _onExternalPipeMessage );
+                    _onExternalPipeMessage = null;
+                }
+                if( pipeOutgoing.relayClientSocket ) {
+                    if( _onRelayPipeClose ) {
+                        pipeOutgoing.off( "close", _onRelayPipeClose );
+                        _onRelayPipeClose = null;
+                    }
+                    if( _onRelayPipeError ) {
+                        pipeOutgoing.off( "error", _onRelayPipeError );
+                        _onRelayPipeError = null;
+                    }
+                    if( _onRelayPipeMessage ) {
+                        pipeOutgoing.off( "message", _onRelayPipeMessage );
+                        _onRelayPipeMessage = null;
+                    }
+                    pipeOutgoing.disconnect();
+                    pipeOutgoing.dispose();
+                }
+                pipeIncoming.disconnect();
+                pipeIncoming.dispose();
+            };
+            pipeIncoming.on( "close", _onExternalPipeClose );
+            pipeIncoming.on( "error", _onExternalPipeError );
+            pipeIncoming.on( "message", _onExternalPipeMessage );
+            //
+            // 2) configure outgoing relay client pipe
+            //
+            pipeOutgoing = pipeIncoming.relayClientSocket = self.fnCreateClient();
+            if( ! pipeOutgoing ) {
+                pipeIncoming.dispose();
+                throw new Error( "Relay \"" + self.strRelayName + "\" failed to initialize relay-client socket to target server" );
+            }
+            pipeOutgoing.on( "close", _onRelayPipeClose );
+            pipeOutgoing.on( "error", _onRelayPipeError );
+            pipeOutgoing.on( "message", _onRelayPipeMessage );
+        };
+        self.acceptor.on( "connection", self.onConnection_ );
+    }
+    dispose() {
+        this.isDisposing = true;
+        if( this.acceptor )
+            this.acceptor.off( "connection", this.onConnection_ );
+        this.onConnection_ = null;
+        super.dispose();
+    }
+    flush() {
+        if( this.acceptor )
+            this.acceptor.flush();
+    }
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class OneToOneRelay extends EventDispatcher {
+    constructor( strRelayName, pipeIncoming, pipeOutgoing, isAutoFlushIncoming, isAutoFlushOutgoing ) {
+        super();
+        const self = this;
+        self.strRelayName = ( strRelayName != null && strRelayName != undefined && typeof strRelayName == "string" && strRelayName.length > 0 ) ? ( "" + strRelayName ) : "unnamed";
+        self.isAutoFlushIncoming = ( isAutoFlushIncoming == null || isAutoFlushIncoming == undefined ) ? true : ( isAutoFlushIncoming ? true : false );
+        self.isAutoFlushOutgoing = ( isAutoFlushOutgoing == null || isAutoFlushOutgoing == undefined ) ? true : ( isAutoFlushOutgoing ? true : false );
+        self.pipeIncoming = pipeIncoming;
+        self.pipeOutgoing = pipeOutgoing;
+        if( ( !( "strSavedRemoteAddress" in pipeIncoming ) ) || pipeIncoming.strSavedRemoteAddress == null || pipeIncoming.strSavedRemoteAddress == undefined )
+            pipeIncoming.strSavedRemoteAddress = "" + pipeIncoming.constructor.name;
+        if( ( !( "strSavedRemoteAddress" in pipeOutgoing ) ) || pipeOutgoing.strSavedRemoteAddress == null || pipeOutgoing.strSavedRemoteAddress == undefined )
+            pipeOutgoing.strSavedRemoteAddress = "" + pipeOutgoing.constructor.name;
+        //
+        // 1) configure incoming pipe
+        //
+        let _offAllPipeEventListeners = null;
+        let _onIncomingPipeClose = function() {
+            if( settings.logging.net.relay.disconnect )
+                console.warn( "Relay \"" + self.strRelayName + "\" incoming-client socket closed \"" + pipeIncoming.strSavedRemoteAddress + "\"" );
+            self.dispatchEvent( new UniversalDispatcherEvent( "close", { relay: self, socket: pipeIncoming, remoteAddress: "" + pipeIncoming.strSavedRemoteAddress, isExternalSocket: true } ) );
+            if( _offAllPipeEventListeners ) {
+                _offAllPipeEventListeners();
+                _offAllPipeEventListeners = null;
+            }
+        };
+        let _onOutgoingPipeClose = function() {
+            if( settings.logging.net.relay.disconnect )
+                console.warn( "Relay \"" + self.strRelayName + "\" outgoing-client socket closed \"" + pipeIncoming.strSavedRemoteAddress + "\"" );
+            self.dispatchEvent( new UniversalDispatcherEvent( "close", { relay: self, socket: pipeIncoming, remoteAddress: "" + pipeIncoming.strSavedRemoteAddress, isExternalSocket: false } ) );
+            if( _offAllPipeEventListeners ) {
+                _offAllPipeEventListeners();
+                _offAllPipeEventListeners = null;
+            }
+        };
+        let _onIncomingPipeError = function( eventData ) {
+            if( settings.logging.net.relay.error )
+                console.warn( "Relay client  \"" + self.strRelayName + "\" incoming-client socket error \"" + pipeIncoming.strSavedRemoteAddress + "\"" );
+            self.dispatchEvent( new UniversalDispatcherEvent( "error", { relay: self, socket: pipeIncoming, remoteAddress: "" + pipeIncoming.strSavedRemoteAddress, isExternalSocket: true } ) );
+            if( _offAllPipeEventListeners ) {
+                _offAllPipeEventListeners();
+                _offAllPipeEventListeners = null;
+            }
+        };
+        let _onOutgoingPipeError = function( eventData ) {
+            if( settings.logging.net.relay.error )
+                console.warn( "Relay client  \"" + self.strRelayName + "\" outgoing-client socket error \"" + pipeIncoming.strSavedRemoteAddress + "\"" );
+            self.dispatchEvent( new UniversalDispatcherEvent( "error", { relay: self, socket: pipeIncoming, remoteAddress: "" + pipeIncoming.strSavedRemoteAddress, isExternalSocket: false } ) );
+            if( _offAllPipeEventListeners ) {
+                _offAllPipeEventListeners();
+                _offAllPipeEventListeners = null;
+            }
+        };
+        let _onIncomingPipeMessage = function( eventData ) {
+            if( settings.logging.net.relay.rawMessage )
+                console.log( "Relay \"" + self.strRelayName + "\" incoming-client socket \"" + eventData.strSavedRemoteAddress + "\" raw message", eventData );
+            const joMessage = eventData.message;
+            if( settings.logging.net.relay.message )
+                console.log( "Relay \"" + self.strRelayName + "\" incoming-client socket \"" + pipeIncoming.strSavedRemoteAddress + "\" message ", joMessage );
+            if( ! pipeOutgoing )
+                throw new Error( "Relay \"" + self.strRelayName + "\" is not completely initialized and cannot transfer messages" );
+            self.dispatchEvent( new UniversalDispatcherEvent( "message", { relay: self, socket: pipeIncoming, remoteAddress: "" + pipeIncoming.strSavedRemoteAddress, isExternalSocket: true, message: joMessage } ) );
+            pipeOutgoing.send( joMessage );
+            if( self.isAutoFlushIncoming )
+                pipeOutgoing.flush();
+        };
+        let _onOutgoingPipeMessage = function( eventData ) {
+            if( settings.logging.net.relay.rawMessage )
+                console.log( "Relay \"" + self.strRelayName + "\" outgoing-client socket \"" + eventData.strSavedRemoteAddress + "\" raw message", eventData );
+            const joMessage = eventData.message;
+            if( settings.logging.net.relay.message )
+                console.log( "Relay \"" + self.strRelayName + "\" outgoing-client socket \"" + pipeIncoming.strSavedRemoteAddress + "\" message ", joMessage );
+            if( ! pipeOutgoing )
+                throw new Error( "Relay \"" + self.strRelayName + "\" is not completely initialized and cannot transfer messages" );
+            self.dispatchEvent( new UniversalDispatcherEvent( "message", { relay: self, socket: pipeIncoming, remoteAddress: "" + pipeIncoming.strSavedRemoteAddress, isExternalSocket: false, message: joMessage } ) );
+            pipeIncoming.send( joMessage );
+            if( self.isAutoFlushOutgoing )
+                pipeIncoming.flush();
+        };
+        _offAllPipeEventListeners = function() {
+            if( _onIncomingPipeClose ) {
+                pipeIncoming.off( "close", _onIncomingPipeClose );
+                _onIncomingPipeClose = null;
+            }
+            if( _onIncomingPipeError ) {
+                pipeIncoming.off( "error", _onIncomingPipeError );
+                _onIncomingPipeError = null;
+            }
+            if( _onIncomingPipeMessage ) {
+                pipeIncoming.off( "message", _onIncomingPipeMessage );
+                _onIncomingPipeMessage = null;
+            }
+            if( pipeOutgoing.relayClientSocket ) {
+                if( _onOutgoingPipeClose ) {
+                    pipeOutgoing.off( "close", _onOutgoingPipeClose );
+                    _onOutgoingPipeClose = null;
+                }
+                if( _onOutgoingPipeError ) {
+                    pipeOutgoing.off( "error", _onOutgoingPipeError );
+                    _onOutgoingPipeError = null;
+                }
+                if( _onOutgoingPipeMessage ) {
+                    pipeOutgoing.off( "message", _onOutgoingPipeMessage );
+                    _onOutgoingPipeMessage = null;
+                }
+                pipeOutgoing.disconnect();
+                pipeOutgoing.dispose();
+            }
+            pipeIncoming.disconnect();
+            pipeIncoming.dispose();
+        };
+        pipeIncoming.on( "close", _onIncomingPipeClose );
+        pipeIncoming.on( "error", _onIncomingPipeError );
+        pipeIncoming.on( "message", _onIncomingPipeMessage );
+        //
+        // 2) configure outgoing relay client pipe
+        //
+        pipeOutgoing.on( "close", _onOutgoingPipeClose );
+        pipeOutgoing.on( "error", _onOutgoingPipeError );
+        pipeOutgoing.on( "message", _onOutgoingPipeMessage );
+    }
+    dispose() {
+        this.isDisposing = true;
+        super.dispose();
+    }
+    flush() {
+        if( this.pipeIncoming )
+            this.pipeIncoming.flush();
+        if( this.pipeOutgoing )
+            this.pipeOutgoing.flush();
+    }
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class DirectPipe extends BasicSocketPipe {
+    constructor( counterPipe, isBroadcastOpenEvents ) {
+        super();
+        isBroadcastOpenEvents = isBroadcastOpenEvents ? true : false;
+        this.socketType = "Direct";
+        this.socketSubtype = "direct.not.initialized.yet";
+        this.isConnected = false;
+        this.acceptor = null;
+        this.counterPipe = ( counterPipe != null && counterPipe != undefined ) ? counterPipe : null; // set outside after this constructor call
+        this.strEndPoint = this.counterPipe ? ( "2-" + this.counterPipe.strEndPoint ) : ( "1-" + utils.randomDirectPipeID() );
+        this.clientPort = this.counterPipe ? 2 : 1;
+        this.socketSubtype = "direct." + this.clientPort;
+        this.url = "direct_pipe://" + this.strEndPoint + ":" + this.clientPort;
+        if( this.counterPipe ) {
+            this.counterPipe.counterPipe = this;
+            this.isConnected = true;
+            this.counterPipe.isConnected = true;
+            if( isBroadcastOpenEvents ) {
+                const self = this;
+                const iv = setTimeout( function() {
+                    clearTimeout( iv );
+                    self.dispatchEvent( new UniversalDispatcherEvent( "open", { socket: self } ) );
+                    // self.acceptor.dispatchEvent( new UniversalDispatcherEvent( "connection", { socket: serverPipe, remoteAddress: "" + self.url } ) );
+                    self.counterPipe.dispatchEvent( new UniversalDispatcherEvent( "open", { socket: self.counterPipe } ) );
+                }, 0 );
+            }
+        }
+    }
+    dispose() {
+        this.performDisconnect();
+        super.dispose();
+    }
+    handleServerDisposed() { // this method is for using in local client/server pipe pairs
+        this.performDisconnect();
+        this.isConnected = false;
+        this.dispatchEvent( new UniversalDispatcherEvent( "close", { socket: this } ) );
+        this.acceptor = null;
+        this.counterPipe = null;
+        this.clientPort = 0;
+        this.url = "";
+        this.dispose();
+    }
+    performDisconnect() {
+        if( ! this.isConnected )
+            return;
+        this.isConnected = false;
+        if( this.acceptor )
+            this.acceptor.unregisterClientByKey( this.clientPort );
+        this.dispatchEvent( new UniversalDispatcherEvent( "close", { socket: this } ) );
+        this.counterPipe.performDisconnect();
+        this.acceptor = null;
+        this.counterPipe = null;
+        this.clientPort = 0;
+        this.url = "";
+    }
+    impl_send( data ) {
+        if( ( !this.isConnected ) || ( !this.counterPipe ) || ( !this.counterPipe.isConnected ) ) {
+            const s = "Cannot send messages to disconnected local server pipe";
+            this.dispatchEvent( new UniversalDispatcherEvent( "error", { socket: this, message: "" + s } ) );
+            throw new Error( s );
+        }
+        const s = socket_sent_data_marshall( data );
+        const jo = socket_received_data_reverse_marshall( s );
+        this.counterPipe.receive( jo );
+    }
+    disconnect() {
+        this.performDisconnect();
+        super.disconnect();
+    }
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class LocalSocketServerPipe extends DirectPipe {
+    constructor( counterPipe, acceptor, clientPort ) {
+        super( counterPipe, false );
+        this.socketType = "Local";
+        this.socketSubtype = "server";
+        this.isConnected = true;
+        this.acceptor = acceptor;
+        this.clientPort = 0 + parseInt( clientPort, 10 );
+        this.url = "local_server_pipe://" + acceptor.strEndPoint + ":" + clientPort;
+        this.acceptor.mapClients["" + clientPort] = this;
+        const self = this;
+        const iv = setTimeout( function() {
+            clearTimeout( iv );
+            self.dispatchEvent( new UniversalDispatcherEvent( "open", { socket: self } ) );
+        }, 0 );
+    }
+    dispose() {
+        super.dispose();
+    }
+};
+
+class LocalSocketServerAcceptor extends BasicServerAcceptor {
+    constructor( strEndPoint ) {
+        super();
+        this.socketType = "Local";
+        this.nextClientPort = 1;
+        this.strEndPoint = ( strEndPoint && typeof strEndPoint == "string" && strEndPoint.length > 0 ) ? strEndPoint : "default_local_endpoint";
+        if( this.strEndPoint in g_mapLocalServers ) {
+            const s = "Cannot start local socket server on already listening \"" + this.strEndPoint + "\" endpoint";
+            this.dispatchEvent( new UniversalDispatcherEvent( "error", { socket: this, message: "" + s } ) );
+            throw new Error( s );
+        }
+        g_mapLocalServers[this.strEndPoint] = this;
+        this.isListening = true;
+        const self = this;
+        const iv = setTimeout( function() {
+            clearTimeout( iv );
+            self.dispatchEvent( new UniversalDispatcherEvent( "open", { socket: self } ) );
+        }, 0 );
+    }
+    dispose() {
+        if( this.isDisposed )
+            return;
+        this.isDisposing = true;
+        this.disposeNotifyClients();
+        if( this.strEndPoint && typeof this.strEndPoint == "string" && this.strEndPoint.length > 0 ) {
+            if( this.strEndPoint in g_mapLocalServers )
+                delete g_mapLocalServers[this.strEndPoint];
+        }
+        super.dispose();
+    }
+};
+
+class LocalSocketClientPipe extends DirectPipe {
+    constructor( strEndPoint ) {
+        super( null, false );
+        this.socketType = "Local";
+        this.socketSubtype = "client";
+        this.isConnected = false;
+        this.clientPort = 0;
+        this.acceptor = null;
+        this.counterPipe = null;
+        this.strEndPoint = ( strEndPoint && typeof strEndPoint == "string" && strEndPoint.length > 0 ) ? strEndPoint : "default_local_endpoint";
+        if( !( this.strEndPoint in g_mapLocalServers ) ) {
+            const s = "Cannot connect to local socket server \"" + this.strEndPoint + "\" endpoint, no such server";
+            this.dispatchEvent( new UniversalDispatcherEvent( "error", { socket: this, message: "" + s } ) );
+            throw new Error( s );
+        }
+        this.acceptor = g_mapLocalServers[this.strEndPoint];
+        this.clientPort = 0 + this.acceptor.nextClientPort;
+        ++ this.acceptor.nextClientPort;
+        this.url = "local_client_pipe://" + this.strEndPoint + ":" + this.clientPort;
+        this.isConnected = true;
+        const serverPipe = new LocalSocketServerPipe( this, this.acceptor, 0 + this.clientPort );
+        serverPipe.counterPipe = this;
+        this.counterPipe = serverPipe;
+        this.acceptor.mapClients[0 + this.clientPort] = {
+            serverPipe: serverPipe,
+            clientPipe: this
+        };
+        const self = this;
+        const iv = setTimeout( function() {
+            clearTimeout( iv );
+            self.dispatchEvent( new UniversalDispatcherEvent( "open", { socket: self } ) );
+            self.acceptor.dispatchEvent( new UniversalDispatcherEvent( "connection", { socket: serverPipe, remoteAddress: "" + self.url } ) );
+        }, 0 );
+    }
+    dispose() {
+        super.dispose();
+    }
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class WebSocketServerPipe extends BasicSocketPipe {
+    constructor( acceptor, ws_conn, remoteAddress ) {
+        super();
+        this.socketType = "WS";
+        this.socketSubtype = "server";
+        const self = this;
+        this.isConnected = true;
+        this.acceptor = acceptor;
+        this.clientNumber = 0 + acceptor.nextClientNumber;
+        this.clientPort = 0 + this.clientNumber;
+        ++ acceptor.nextClientNumber;
+        this.ws_conn = ws_conn;
+        this.remoteAddress = "" + remoteAddress;
+        this.url = "ws_server_pipe(" + this.clientNumber + ")://" + remoteAddress;
+        this._onWsClose = function() {
+            self.dispatchEvent( new UniversalDispatcherEvent( "close", { socket: self } ) );
+        };
+        this._onWsError = function( event ) {
+            self.dispatchEvent( new UniversalDispatcherEvent( "error", { socket: self, message: event } ) );
+        };
+        this._onWsMessage = function( event ) {
+            self.receive( event.data );
+        };
+        this._removeWsEventListeners = function() {
+            if( self._onWsClose ) {
+                ws_conn.removeEventListener( "close", self._onWsClose );
+                self._onWsClose = null;
+            }
+            if( self._onWsError ) {
+                ws_conn.removeEventListener( "error", self._onWsError );
+                self._onWsError = null;
+            }
+            if( self._onWsMessage ) {
+                ws_conn.removeEventListener( "message", self._onWsMessage );
+                self._onWsMessage = null;
+            }
+        };
+        ws_conn.addEventListener( "close", this._onWsClose );
+        ws_conn.addEventListener( "error", this._onWsError );
+        ws_conn.addEventListener( "message", this._onWsMessage );
+        this.acceptor.mapClients["" + this.clientPort] = this;
+        const iv = setTimeout( function() {
+            clearTimeout( iv );
+            self.dispatchEvent( new UniversalDispatcherEvent( "open", { socket: self } ) );
+            self.acceptor.dispatchEvent( new UniversalDispatcherEvent( "connection", { socket: self, remoteAddress: "" + remoteAddress } ) );
+        }, 0 );
+    }
+    dispose() {
+        this.performDisconnect();
+        super.dispose();
+    }
+    handleServerDisposed() {
+        this.isConnected = false;
+        this.clientNumber = 0;
+        this.acceptor = null;
+        this.ws_conn = null;
+        this.url = "";
+        this.remoteAddress = "";
+        this.dispose();
+    }
+    performDisconnect() {
+        if( ! this.isConnected )
+            return;
+        this.isConnected = false;
+        if( this._removeWsEventListeners ) {
+            this._removeWsEventListeners();
+            this._removeWsEventListeners = null;
+        }
+        if( this.ws_conn ) {
+            try {
+                this.ws_conn.terminate();
+            } catch ( err ) {
+                console.warn( "Web socket server pipe termination error", err );
+            }
+            this.ws_conn = null;
+        }
+        if( this.acceptor )
+            this.acceptor.unregisterClientByKey( this.clientPort );
+        this.clientNumber = 0;
+        this.acceptor = null;
+        this.url = "";
+        this.remoteAddress = "";
+    }
+    impl_send( data ) {
+        if( ( !this.isConnected ) || ( !this.ws_conn ) ) {
+            const s = "Cannot send messages to disconnected web socket server pipe";
+            this.dispatchEvent( new UniversalDispatcherEvent( "error", { socket: this, message: "" + s } ) );
+            throw new Error( s );
+        }
+        const s = socket_sent_data_marshall( data );
+        this.ws_conn.send( s );
+    }
+    disconnect() {
+        this.performDisconnect();
+        super.disconnect();
+    }
+    impl_receive( data ) {
+        const jo = socket_received_data_reverse_marshall( data );
+        this.dispatchEvent( new UniversalDispatcherEvent( "message", { socket: this, message: jo } ) );
+    }
+};
+
+class WebSocketServerAcceptor extends BasicServerAcceptor {
+    constructor( nTcpPort, key, cert ) {
+        super();
+        this.socketType = "WS";
+        this.ws_srv = null;
+        if( key != null && key != undefined && typeof key == "string" && key.length > 0 &&
+            cert != null && cert != undefined && typeof cert == "string" && cert.length > 0
+        ) {
+            const server = https_mod.createServer( {
+                key: "" + key,
+                cert: "" + cert
+                // , ca: ...
+            } );
+            server.listen( nTcpPort );
+            this.ws_srv = new ws_mod.Server( { server } );
+        } else
+            this.ws_srv = new ws_mod.Server( { port: nTcpPort } );
+
+        const self = this;
+        self.ws_srv.on( "connection", function( ws_conn, req ) {
+            ws_conn.strSavedRemoteAddress = "" + req.connection.remoteAddress;
+            ws_conn.serverPipe = new WebSocketServerPipe( self, ws_conn, req.connection.remoteAddress );
+        } );
+        this.isListening = true;
+        const iv = setTimeout( function() {
+            clearTimeout( iv );
+            self.dispatchEvent( new UniversalDispatcherEvent( "open", { socket: self } ) );
+        }, 0 );
+    }
+    dispose() {
+        if( this.isDisposed )
+            return;
+        this.isDisposing = true;
+        this.disposeNotifyClients();
+        super.dispose();
+    }
+};
+
+class WebSocketClientPipe extends BasicSocketPipe {
+    constructor( url ) {
+        super();
+        this.socketType = "WS";
+        this.socketSubtype = "client";
+        this.isConnected = false;
+        this.ws_conn = null;
+        this._onWsOpen = null;
+        this._onWsClose = null;
+        this._onWsError = null;
+        this._onWsMessage = null;
+        this.ws_url = "" + ( ( url != null && url != undefined && typeof url == "string" ) ? url : "" );
+        this.url = "ws_client_pipe-" + this.ws_url;
+        this.reconnect();
+    }
+    dispose() {
+        if( this.isDisposed )
+            return;
+        this.isDisposing = true;
+        this.performDisconnect();
+        this.ws_url = null;
+        super.dispose();
+    }
+    impl_send( data ) {
+        if( ( !this.isConnected ) || ( !this.ws_conn ) ) {
+            const s = "Cannot send messages to disconnected web socket client pipe";
+            this.dispatchEvent( new UniversalDispatcherEvent( "error", { socket: this, message: "" + s } ) );
+            throw new Error( s );
+        }
+        const s = socket_sent_data_marshall( data );
+        this.ws_conn.send( s );
+    }
+    reconnect() {
+        this.performDisconnect();
+        this.ws_connect( "" + this.ws_url );
+    }
+    disconnect() {
+        this.performDisconnect();
+        super.disconnect();
+    }
+    performDisconnect() {
+        if( ! this.isConnected )
+            return;
+        this.ws_disconnect();
+    }
+    ws_connect_attempt( url, reconnectAfterMilliseconds, iv ) {
+        const self = this;
+        try {
+            if( this.isConnected || this.ws_conn )
+                this.ws_disconnect();
+            this.ws_conn = ws_mod
+                ? new ws_mod( url, { tlsOptions: { rejectUnauthorized: false } } ) // server side
+                : new WebSocket( url ); // client side
+            this.url = "" + url;
+            this._onWsOpen = function() {
+                self.isConnected = true;
+                self.dispatchEvent( new UniversalDispatcherEvent( "open", { socket: self } ) );
+            };
+            this._onWsClose = function( event ) {
+                // alert( JSON.stringify( event ) );
+                self.isConnected = false;
+                self.dispatchEvent( new UniversalDispatcherEvent( "close", { socket: self, message: event } ) );
+            };
+            this._onWsError = function( event ) {
+                // alert( JSON.stringify( event ) );
+                self.isConnected = false;
+                self.dispatchEvent( new UniversalDispatcherEvent( "error", { socket: self, message: event } ) );
+            };
+            this._onWsMessage = function( event ) {
+                self.receive( event.data );
+            };
+            this._removeWsEventListeners = function() {
+                if( self._onWsOpen ) {
+                    self.ws_conn.removeEventListener( "open", self._onWsOpen );
+                    self._onWsOpen = null;
+                }
+                if( self._onWsClose ) {
+                    self.ws_conn.removeEventListener( "close", self._onWsClose );
+                    self._onWsClose = null;
+                }
+                if( self._onWsError ) {
+                    self.ws_conn.removeEventListener( "error", self._onWsError );
+                    self._onWsError = null;
+                }
+                if( self._onWsMessage ) {
+                    self.ws_conn.removeEventListener( "message", self._onWsMessage );
+                    self._onWsMessage = null;
+                }
+            };
+            this.ws_conn.addEventListener( "open", this._onWsOpen );
+            this.ws_conn.addEventListener( "close", this._onWsClose );
+            this.ws_conn.addEventListener( "error", this._onWsError );
+            this.ws_conn.addEventListener( "message", this._onWsMessage );
+            if( iv )
+                clearTimeout( iv );
+            return true;
+        } catch ( err ) {
+            console.warn( "WS client connect error:", err );
+        }
+        if( reconnectAfterMilliseconds != null && reconnectAfterMilliseconds != undefined ) {
+            reconnectAfterMilliseconds = parseInt( reconnectAfterMilliseconds, 10 );
+            if( reconnectAfterMilliseconds > 0 && ( !iv ) ) {
+                const iv = setTimeout( function() {
+                    try {
+                        if( self.ws_connect_attempt( url, reconnectAfterMilliseconds, iv ) )
+                            clearTimeout( iv );
+                    } catch ( err ) {
+                    }
+                }, reconnectAfterMilliseconds );
+            }
+        }
+        return false;
+    }
+    ws_connect( url ) {
+        if( url.length == 0 ) {
+            const s = "Cannot connect web socket server \"" + url + "\", bad url";
+            this.dispatchEvent( new UniversalDispatcherEvent( "error", { socket: this, message: "" + s } ) );
+            throw new Error( s );
+        }
+        this.ws_connect_attempt( url, settings.net.ws.client.reconnectAfterMilliseconds, null );
+    }
+    ws_disconnect() {
+        if( this._removeWsEventListeners ) {
+            this._removeWsEventListeners();
+            this._removeWsEventListeners = null;
+        }
+        if( this.ws_conn ) {
+            //try { this.ws_conn.close( 1000, "Good bye!" ); } catch( err ) { } // see codes here: https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes
+            let bPass = false, anyError = null;
+            try {
+                this.ws_conn.close();
+                bPass = true;
+            } catch ( err ) {
+                anyError = err;
+            }
+            if( ! bPass ) {
+                try {
+                    this.ws_conn.terminate();
+                    bPass = true;
+                } catch ( err ) {
+                    anyError = err;
+                }
+            }
+            if( ! bPass )
+                console.warn( "Web socket client pipe termination error", anyError );
+            this.ws_conn = null;
+        }
+        this.isConnected = false;
+        this.url = "";
+    }
+    impl_receive( data ) {
+        const jo = socket_received_data_reverse_marshall( data );
+        this.dispatchEvent( new UniversalDispatcherEvent( "message", { socket: this, message: jo } ) );
+    }
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class RTCConnection extends EventDispatcher {
+    constructor( strSignalingServerURL, idRtcParticipant ) {
+        super();
+        this.strSignalingServerURL = utils.makeValidSignalingServerURL( strSignalingServerURL );
+        this.idRtcParticipant = "" + ( ( idRtcParticipant != null && idRtcParticipant != undefined && typeof idRtcParticipant == "string" && idRtcParticipant.length > 0 ) ? idRtcParticipant : utils.uuid_v4() );
+        this.wasIdentified = false;
+        this.iceComplete = false;
+        this.pc = null;
+        this.dc = null;
+    }
+    dispose() {
+        if( this.isDisposed )
+            return;
+        this.isDisposing = true;
+        this.closeDataChannel();
+        this.closePeer();
+        this.dc = null;
+        this.wasIdentified = false;
+        this.iceComplete = false;
+        this.idRtcParticipant = null;
+        super.dispose();
+    }
+    describe( strInstanceType, arrAdditionalProps ) {
+        let strInstanceDescription = ( strInstanceType == null || strInstanceType == undefined || ( typeof strInstanceType != "string" ) || strInstanceType.length == 0 )
+            ? "participant"
+            : ( "" + strInstanceType );
+        if( typeof this.idRtcParticipant == "string" && this.idRtcParticipant.length > 0 )
+            strInstanceDescription += " " + this.idRtcParticipant;
+        const arrProps = [];
+        if( this.isDisposed )
+            arrProps.push( "disposed" );
+        if( this.wasIdentified )
+            arrProps.push( "identified" );
+        if( this.pc )
+            arrProps.push( "pc" );
+        if( this.dc )
+            arrProps.push( "dc" );
+        if( arrAdditionalProps != null && arrAdditionalProps != undefined && arrAdditionalProps.length > 0 ) {
+            for( let i = 0; i < arrAdditionalProps.length; ++ i )
+                arrProps.push( arrAdditionalProps[i] );
+        }
+        if( arrProps.length > 0 )
+            strInstanceDescription += "(" + arrProps.join( ", " ) + ")";
+        return strInstanceDescription;
+    }
+    closeDataChannel() {
+        if( this.dc ) {
+            try {
+                this.dc.ondatachannel = null;
+                this.dc.close();
+                if( settings.logging.net.rtc.closeDataChannel )
+                    console.warn( this.describe() + " did closed RTC data channel" );
+            } catch ( err ) {
+                if( settings.logging.net.rtc.error )
+                    console.warn( this.describe() + " error closing RTC data channel:", err );
+            }
+            this.dc = null;
+            this.dispatchEvent( new UniversalDispatcherEvent( "dataChannelClose", { detail: { actor: this } } ) );
+        }
+    }
+    closePeer() {
+        if( this.pc ) {
+            try {
+                this.pc.onicecandidate = null;
+                this.pc.oniceconnectionstatechange = null;
+                this.pc.close();
+                if( settings.logging.net.rtc.closePeer )
+                    console.warn( this.describe() + " did closed RTC peer" );
+            } catch ( err ) {
+                if( settings.logging.net.rtc.error )
+                    console.warn( this.describe() + " error closing RTC peer:", err );
+            }
+            this.dispatchEvent( new UniversalDispatcherEvent( "peerClose", { detail: { actor: this } } ) );
+            this.pc = null;
+        }
+    }
+    onError( err ) {
+        this.dispatchEvent( new UniversalDispatcherEvent( "rtcParticipantError", { detail: { actor: this, error: err } } ) );
+        if( settings.logging.net.rtc.error )
+            console.warn( " !!! " + this.describe() + " error:", err );
+        this.closeDataChannel();
+        this.closePeer();
+    }
+    send( data ) {
+        const s = socket_sent_data_marshall( data );
+        if( ! this.dc ) {
+            this.onError( "Attempt to send message to uninitialized RTC data channel: " + s );
+            return;
+        }
+        try {
+            this.dc.send( s );
+        } catch ( err ) {
+            this.onError( "Failed to send message to RTC data channel: " + err.toString() );
+        }
+    }
+    onDataChannelOpen( event ) {
+        this.dispatchEvent( new UniversalDispatcherEvent( "dataChannelOpen", { detail: { actor: this } } ) );
+    }
+    onDataChannelClose( event ) {
+        this.dispatchEvent( new UniversalDispatcherEvent( "dataChannelClose", { detail: { actor: this } } ) );
+        // this.onError( "Data channel closed" );
+    }
+    onDataChannelError( event ) {
+        this.dispatchEvent( new UniversalDispatcherEvent( "dataChannelError", { detail: { actor: this } } ) );
+        this.onError( "Data channel error " + event.toString() );
+    }
+    onDataChannelMessage( event ) {
+        if( event.data.size ) {
+            // fileReceiver.receive( event.data, { } );
+            if( settings.logging.net.rtc.error )
+                console.warn( this.describe() + " will ignore file transfer message of size", event.data.size );
+        } else {
+            if( event.data.charCodeAt( 0 ) == 2 )
+                return;
+            const data = JSON.parse( event.data );
+            if( data.type === "file" ) {
+                // fileReceiver.receive( event.data, { } );
+                if( settings.logging.net.rtc.error )
+                    console.warn( this.describe() + " will ignore file transfer message" );
+            } else
+                this.dispatchEvent( new UniversalDispatcherEvent( "dataChannelMessage", { detail: { actor: this, data: data } } ) );
+        }
+    }
+    onIceComplete( event ) {
+    }
+    onIceConnectionStateChange( event ) { // handler for self.pc.oniceconnectionstatechange, see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/oniceconnectionstatechange
+        if( settings.logging.net.rtc.iceConnectionStateChange )
+            console.log( "Participant \"" + this.idRtcParticipant + "\" ICE connection state changed to \"" + this.pc.iceConnectionState + "\", event is:", event );
+        else if( settings.logging.net.rtc.iceConnectionStateName ) // similar to previous but prints only connection state name
+            console.log( "Participant \"" + this.idRtcParticipant + "\" ICE connection state changed to \"" + this.pc.iceConnectionState + "\"" );
+        if( this.pc.iceConnectionState === "failed" ||
+            this.pc.iceConnectionState === "closed" ||
+            this.pc.iceConnectionState === "disconnected"
+        )
+            this.onError( "ICE connection state(oniceconnectionstatechange) changed to " + this.pc.iceConnectionState );
+    }
+    onIceGatheringStateChange( event ) { // handler for self.pc.onicegatheringstatechange - this is recommended to handle in a same way as oniceconnectionstatechange, see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/onicegatheringstatechange
+        if( ! this.pc ) {
+            console.log( "WARNING: Participant \"" + this.idRtcParticipant + "\" ICE gathering state changed event with no pc\", event is:", event );
+            return;
+        }
+        if( settings.logging.net.rtc.iceGatheringStateChange )
+            console.log( "Participant \"" + this.idRtcParticipant + "\" ICE gathering state changed to \"" + this.pc.iceGatheringState + "\", event is:", event );
+        else if( settings.logging.net.rtc.iceGatheringStateName ) // similar to previous but prints only gathering state name
+            console.log( "Participant \"" + this.idRtcParticipant + "\" ICE gathering state changed to \"" + this.pc.iceGatheringState + "\"" );
+        if( this.pc.iceConnectionState === "failed" ||
+            this.pc.iceConnectionState === "closed" ||
+            this.pc.iceConnectionState === "disconnected"
+        )
+            this.onError( "ICE connection state(onicegatheringstatechange) changed to " + this.pc.iceConnectionState );
+    }
+    onIceIdentifyResult( event ) { // handler for self.pc.onidentityresult, see https://developer.mozilla.org/en-US/docs/Web/API/RTCIdentityEvent
+        if( settings.logging.net.rtc.iceIceIdentifyResult ) {
+            if( "assertion" in event )
+                console.warn( "Participant \"" + this.idRtcParticipant + "\" ICE identify result event with new identity assertion (blob: '" + event.assertion + "') has been generated." );
+            else
+                console.warn( "Participant \"" + this.idRtcParticipant + "\" ICE identify result event is:", event );
+        }
+    }
+    onIceSignalingStateChange( event ) { // handler for self.pc.onsignalingstatechange, see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/onsignalingstatechange
+        if( settings.logging.net.rtc.iceSignalingStateChange ) {
+            console.log( "Participant \"" + this.idRtcParticipant + "\" ICE signaling state changed to \"" +
+                ( ( this.pc && "signalingState" in this.pc ) ? this.pc.signalingState : "N/A" ) +
+                "\", event is:", event );
+        }
+    }
+    onIceNegotiationNeeded( event ) { // handler for self.pc.onnegotiationneeded, see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/onnegotiationneeded
+        // TO-DO: improve this
+        if( settings.logging.net.rtc.iceNegotiationNeeded )
+            console.log( "Participant \"" + this.idRtcParticipant + "\" ICE negotiation needed event is:", event );
+    }
+};
+
+class RTCActor extends RTCConnection {
+    constructor( strSignalingServerURL, idRtcParticipant, offerOptions, signalingOptions ) {
+        super( strSignalingServerURL, idRtcParticipant );
+        this.isDisposed = false;
+        this.idSomebodyCreator = null;
+        this.bWasImpersonated = false;
+        this.isCreator = false;
+        this.isJoiner = false;
+        //
+        this.offerOptions = {
+            optional: [],
+            offerToReceiveAudio: false, // offer to the remote peer the opportunity to try to send audio
+            offerToReceiveVideo: false, // offer to the remote peer the opportunity to try to send video
+            voiceActivityDetection: false,
+            iceRestart: false
+        };
+        if( offerOptions ) {
+            this.offerOptions.offerToReceiveAudio = ( "offerToReceiveAudio" in offerOptions && offerOptions.offerToReceiveAudio ) ? true : false;
+            this.offerOptions.offerToReceiveVideo = ( "offerToReceiveVideo" in offerOptions && offerOptions.offerToReceiveVideo ) ? true : false;
+            this.offerOptions.voiceActivityDetection = ( "voiceActivityDetection" in offerOptions && offerOptions.voiceActivityDetection ) ? true : false;
+            this.offerOptions.iceRestart = ( "iceRestart" in offerOptions && offerOptions.iceRestart ) ? true : false;
+        }
+        //
+        this.signalingOptions = {
+            idCategory: "" + settings.rtcSpace.defaultSpaceCategory,
+            idSpace: "" + settings.rtcSpace.defaultSpaceName
+        };
+        if( signalingOptions ) {
+            if( "idCategory" in signalingOptions && typeof signalingOptions.idCategory == "string" && signalingOptions.idCategory.length > 0 )
+                this.signalingOptions.idCategory = "" + signalingOptions.idCategory;
+            if( "idSpace" in signalingOptions && typeof signalingOptions.idSpace == "string" && signalingOptions.idSpace.length > 0 )
+                this.signalingOptions.idSpace = "" + signalingOptions.idSpace;
+        }
+    }
+    dispose() {
+        if( this.isDisposed )
+            return;
+        this.isDisposing = true;
+        this.signalingPipeClose();
+        this.idSomebodyCreator = null;
+        this.strSignalingServerURL = null;
+        this.bWasImpersonated = false;
+        super.dispose();
+    }
+    describe( strInstanceType, arrAdditionalProps ) {
+        strInstanceType = ( strInstanceType == null || strInstanceType == undefined || ( typeof strInstanceType != "string" ) || strInstanceType.length == 0 )
+            ? ( this.isCreator ? "creator" : ( this.isJoiner ? "joiner" : "actor" ) )
+            : strInstanceType;
+        // arrAdditionalProps = arrAdditionalProps || [];
+        return super.describe( strInstanceType, arrAdditionalProps );
+    }
+    onError( err ) {
+        super.onError( err );
+    }
+    signalingPipeOpen() {
+        try {
+            const self = this;
+            self.signalingPipeClose();
+            self.dispatchEvent( new UniversalDispatcherEvent( "signalingWillStart", { detail: { actor: this } } ) );
+            self.signalingPipe = new WebSocketClientPipe( self.strSignalingServerURL );
+            self.signalingPipe.on( "open", function( eventData ) { self.signalingPipeOnOpen( eventData ); } );
+            self.signalingPipe.on( "close", function( eventData ) { self.signalingPipeOnClose( eventData ); } );
+            self.signalingPipe.on( "error", function( eventData ) { self.signalingPipeOnError( eventData ); } );
+            self.signalingPipe.on( "message", function( eventData ) { self.signalingPipeOnRawMessage( eventData ); } );
+            self.dispatchEvent( new UniversalDispatcherEvent( "signalingDidStarted", { detail: { actor: this } } ) );
+        } catch ( err ) {
+            if( settings.logging.net.signaling.error )
+                console.warn( this.describe() + " error starting signaling pipe:", err );
+            this.onError( err );
+        }
+    }
+    signalingPipeClose() {
+        if( this.signalingPipe ) {
+            try {
+                if( settings.logging.net.signaling.disconnect )
+                    console.warn( this.describe() + " will close signaling pipe" );
+                this.signalingPipe.offAll();
+                this.signalingPipe.disconnect();
+                if( settings.logging.net.signaling.disconnect )
+                    console.warn( this.describe() + " did closed signaling pipe" );
+            } catch ( err ) {
+                if( settings.logging.net.signaling.error )
+                    console.warn( this.describe() + " error closing signaling pipe:", err );
+            }
+            this.signalingPipe = null;
+            this.dispatchEvent( new UniversalDispatcherEvent( "signalingClosed", { detail: { actor: this } } ) );
+        }
+    }
+    signalingPipeOnOpen( eventData ) {
+        try {
+            this.dispatchEvent( new UniversalDispatcherEvent( "signalingOpened", { detail: { actor: this } } ) );
+            if( settings.logging.net.signaling.connect )
+                console.log( "+++ " + this.describe() + " did connected to " + this.strSignalingServerURL );
+            const joImpersonateMessage = {
+                id: utils.randomCallID(),
+                method: "signalingImpersonate",
+                idCategory: "" + this.signalingOptions.idCategory,
+                idSpace: "" + this.signalingOptions.idSpace,
+                idRtcParticipant: "" + this.idRtcParticipant,
+                role: this.isCreator ? "creator" : "joiner"
+            };
+            if( settings.logging.net.signaling.message )
+                console.log( " <<< " + this.describe() + " message out", joImpersonateMessage );
+            this.signalingPipe.send( joImpersonateMessage );
+        } catch ( err ) {
+            if( settings.logging.net.signaling.error )
+                console.warn( this.describe() + " error sending impersonation to signaling pipe:", err );
+            this.onError( err );
+        }
+    }
+    signalingPipeOnClose( eventData ) {
+        this.dispatchEvent( new UniversalDispatcherEvent( "signalingPipeClose", { detail: { actor: this } } ) );
+        if( settings.logging.net.signaling.disconnect )
+            console.warn( " !!! " + this.describe() + " signaling pipe closed for " + this.strSignalingServerURL );
+        this.signalingPipeClose();
+    }
+    signalingPipeOnError( eventData ) {
+        // alert( JSON.stringify( eventData ) );
+        this.dispatchEvent( new UniversalDispatcherEvent( "signalingPipeError", { detail: { actor: this, error: eventData } } ) );
+        if( settings.logging.net.signaling.error )
+            console.warn( " !!! " + this.describe() + " signaling pipe error for " + this.strSignalingServerURL + ", error is:", eventData );
+        this.onError( eventData );
+        this.signalingPipeClose();
+    }
+    signalingPipeOnRawMessage( eventData ) {
+        try {
+            if( settings.logging.net.signaling.rawMessage )
+                console.log( " >>> " + this.describe() + " raw signaling message received", eventData );
+            const joMessage = eventData.message;
+            if( settings.logging.net.signaling.message )
+                console.log( " >>> " + this.describe() + " signaling message received", joMessage );
+            this.signalingPipeOnMessage( joMessage );
+        } catch ( err ) {
+            if( settings.logging.net.signaling.error )
+                console.warn( "Error handling raw message in " + this.describe() + ":", err );
+            this.onError( err );
+        }
+    }
+    signalingPipeOnMessage( joMessage ) {
+        switch ( joMessage.method ) {
+        case "signalingImpersonate": {
+            if( joMessage.error == null ) {
+                // OKay, impersonated
+                this.bWasImpersonated = true;
+                if( settings.logging.net.signaling.generic )
+                    console.log( "Success, " + this.describe() + " impersonated on signaling server" );
+                this.dispatchEvent( new UniversalDispatcherEvent( "signalingPassedImpersonation", { detail: { actor: this } } ) );
+                this.onImpersonationComplete();
+            } else {
+                if( settings.logging.net.signaling.error )
+                    console.warn( " >>> " + this.describe() + " signaling impersonation error", joMessage.error );
+                this.dispatchEvent( new UniversalDispatcherEvent( "signalingFailedImpersonation", { detail: { actor: this, error: joMessage.error } } ) );
+                this.onError( joMessage.error );
+            }
+        } break;
+        default:
+            if( settings.logging.net.signaling.error )
+                console.warn( " >>> " + this.describe() + " unhandled signaling message", joMessage );
+            break;
+        } // switch( joMessage.method )
+    }
+    onImpersonationComplete() { }
+    onOtherSideIdentified( idSomebodyOtherSide, idOffer ) { } // generic implementation should never be called
+};
+
+class RTCServerPeer extends RTCConnection {
+    constructor( rtcCreator, timeToPublishMilliseconds, timeToSignalingNegotiationMilliseconds, peerConfiguration, peerAdditionalOptions, localMediaStream ) {
+        super();
+        this.rtcCreator = rtcCreator;
+        this.idSomebodyOtherSide = null;
+        this.idOffer = this.rtcCreator.idOfferNext ++;
+        this.tsOfferCreated = null;
+        if( settings.logging.net.signaling.offerRegister )
+            console.log( "Register offer", this.idOffer, "(RTCServerPeer constructor)" );
+        this.rtcCreator.map_server_offers[0 + this.idOffer] = this;
+        this.isPublishing = false;
+        this.isSignalingNegotiation = false;
+        this.isPublishTimeout = false;
+        this.isSignalingNegotiationTimeout = false;
+        this.timerPublishing = null;
+        this.timerSignalingNegotiation = null;
+        this.timeToPublishMilliseconds = timeToPublishMilliseconds
+            ? parseInt( timeToPublishMilliseconds, 10 )
+            : settings.net.rtc.timeToPublishMilliseconds;
+        this.timeToSignalingNegotiationMilliseconds = timeToSignalingNegotiationMilliseconds
+            ? parseInt( timeToSignalingNegotiationMilliseconds, 10 )
+            : settings.net.rtc.timeToSignalingNegotiationMilliseconds;
+        this.peerConfiguration = ( peerConfiguration && typeof peerConfiguration == "object" ) ? peerConfiguration : settings.net.rtc.peerConfiguration;
+        this.peerAdditionalOptions = ( peerAdditionalOptions && typeof peerAdditionalOptions == "object" ) ? peerAdditionalOptions : settings.net.rtc.peerAdditionalOptions;
+        this.localMediaStream = ( localMediaStream != null && localMediaStream != undefined && typeof localMediaStream == "object" ) ? localMediaStream : null;
+        this.isOfferPublishedOnSignalingServer = false;
+        this.initPeer();
+        this.publish();
+    }
+    dispose() {
+        if( this.isDisposed )
+            return;
+        this.isDisposing = true;
+        this.publishCancel();
+        this.signalingNegotiationCancel();
+        if( this.rtcCreator ) {
+            if( this.idOffer in this.rtcCreator.map_server_offers ) {
+                if( settings.logging.net.signaling.offerUnregister )
+                    console.log( "Unregister offer", this.idOffer, "(RTCServerPeer dispose)" );
+                delete this.rtcCreator.map_server_offers[this.idOffer];
+            }
+            this.idOffer = 0;
+        }
+        this.idOffer = 0;
+        if( this.idSomebodyOtherSide != null ) {
+            if( this.idSomebodyOtherSide in this.rtcCreator.map_server_peers )
+                delete this.rtcCreator.map_server_peers[this.idSomebodyOtherSide];
+
+            this.idSomebodyOtherSide = null;
+        }
+        this.rtcCreator = null;
+        this.tsOfferCreated = null;
+        super.dispose();
+    }
+    describe( strInstanceType, arrAdditionalProps ) {
+        strInstanceType = ( strInstanceType == null || strInstanceType == undefined || ( typeof strInstanceType != "string" ) || strInstanceType.length == 0 )
+            ? "server-peer"
+            : strInstanceType;
+        // arrAdditionalProps = arrAdditionalProps || [];
+        return super.describe( strInstanceType, arrAdditionalProps );
+    }
+    initPeer() {
+        if( this.isDisposed )
+            return;
+        const self = this;
+        if( self.pc )
+            return;
+        self.pc = new wrtc_mod.RTCPeerConnection( self.peerConfiguration, self.peerAdditionalOptions );
+        if( self.localMediaStream ) {
+            for( const track of self.localMediaStream.getTracks() )
+                self.pc.addTrack( track, self.localMediaStream );
+        } else {
+            self.dc = self.pc.createDataChannel( settings.net.rtc.dataChannel.label, settings.net.rtc.dataChannel.opts );
+            self.dc.addEventListener( "open", function( event ) { self.onDataChannelOpen( event ); } );
+            self.dc.addEventListener( "close", function( event ) { self.onDataChannelClose( event ); } );
+            self.dc.addEventListener( "error", function( event ) { self.onDataChannelError( event ); } );
+            self.dc.addEventListener( "message", function( event ) { self.onDataChannelMessage( event ); } );
+        }
+    }
+    publishCancel() {
+        if( ! this.isPublishing )
+            return;
+        this.isOfferPublishedOnSignalingServer = false;
+        this.isPublishing = false;
+        if( this.timerPublishing ) {
+            clearTimeout( this.timerPublishing );
+            this.timerPublishing = null;
+        }
+        this.signalingNegotiationCancel(); // mutual cancel
+    }
+    signalingNegotiationCancel() {
+        if( ! this.isSignalingNegotiation )
+            return;
+        this.isSignalingNegotiation = false;
+        if( this.timerSignalingNegotiation ) {
+            clearTimeout( this.timerSignalingNegotiation );
+            this.timerSignalingNegotiation = null;
+        }
+        this.publishCancel(); // mutual cancel
+    }
+    publish() {
+        if( this.isDisposed || this.isPublishing || this.isSignalingNegotiation || ( !this.rtcCreator ) || ( !this.rtcCreator.signalingPipe ) )
+            return;
+        const self = this;
+        self.isPublishing = true;
+        if( self.timeToPublishMilliseconds > 0 ) {
+            self.isSignalingNegotiation = false;
+            self.timerPublishing = setTimeout( function() {
+                self.publishCancel();
+                self.signalingNegotiationCancel();
+                self.isPublishTimeout = true;
+                if( settings.logging.net.signaling.publishTimeout )
+                    console.warn( " !!! " + self.describe() + " offer publish timeout " + self.timeToPublishMilliseconds + " milliseconds reached" );
+                self.dispatchEvent( new UniversalDispatcherEvent( "publishTimeout", { detail: { participant: self } } ) );
+                if( self.rtcCreator )
+                    self.rtcCreator.dispatchEvent( new UniversalDispatcherEvent( "publishTimeout", { detail: { participant: self } } ) );
+            }, self.timeToPublishMilliseconds );
+        } // if( self.timeToPublishMilliseconds > 0 )
+        self.dispatchEvent( new UniversalDispatcherEvent( "publishStart", { detail: { participant: self } } ) );
+        self.pc.oniceconnectionstatechange = function( event ) { self.onIceConnectionStateChange( event ); };
+        self.pc.onicegatheringstatechange = function( event ) { self.onIceGatheringStateChange( event ); };
+        self.pc.onidentityresult = function( event ) { self.onIceIdentifyResult( event ); };
+        self.pc.onsignalingstatechange = function( event ) { self.onIceSignalingStateChange( event ); };
+        self.pc.onnegotiationneeded = function( event ) { self.onIceNegotiationNeeded( event ); };
+        self.pc.createOffer( self.offerOptions ).then(
+            function( offerDescription ) {
+                // success
+                self.tsOfferCreated = new Date();
+                if( settings.logging.net.signaling.offer )
+                    console.log( " <<< " + self.describe() + " offer created at " + utils.format_date_time( self.tsOfferCreated ) + " with description:", offerDescription );
+                self.dispatchEvent( new UniversalDispatcherEvent( "offerCreated", { detail: { participant: self } } ) );
+                self.pc.setLocalDescription( offerDescription ).then(
+                    function() {
+                        // success
+                        if( settings.logging.net.signaling.localDescription )
+                            console.log( " <<< " + self.describe() + " local description set:", offerDescription );
+                        self.dispatchEvent( new UniversalDispatcherEvent( "localDescriptionSet", { detail: { participant: self } } ) );
+                        self.pc.onicecandidate = function( event ) {
+                            self.iceComplete = true;
+                            self.onIceComplete( event );
+                        }; // onicecandidate
+                    }, function( err ) {
+                        // error of setLocalDescription
+                        self.publishCancel();
+                        self.signalingNegotiationCancel();
+                        self.onError( "Failed to set local description: " + err.toString() );
+                    } );
+            }, function() {
+                self.publishCancel();
+                self.signalingNegotiationCancel();
+                // error of createOffer
+                self.onError( "Failed to create offer:" + err.toString() );
+            } );
+    }
+    onOtherSideIdentified( idSomebodyOtherSide ) {
+        this.publishCancel();
+        this.signalingNegotiationCancel();
+        this.idSomebodyOtherSide = "" + idSomebodyOtherSide;
+        this.wasIdentified = true;
+        this.dispatchEvent( new UniversalDispatcherEvent( "identified", { detail: { participant: this, idSomebodyOtherSide: "" + idSomebodyOtherSide } } ) );
+    }
+    onError( err ) {
+        if( this.rtcCreator ) {
+            this.rtcCreator.onRtcPeerError( this, err );
+            if( this.idOffer in this.rtcCreator.map_server_offers ) {
+                if( settings.logging.net.signaling.offerUnregister )
+                    console.log( "Unregister offer", this.idOffer, "due to RTCServerPeer error:".err );
+                delete this.rtcCreator.map_server_offers[this.idOffer];
+            }
+            this.idOffer = 0;
+        }
+        if( this.idSomebodyOtherSide != null ) {
+            if( this.idSomebodyOtherSide in this.rtcCreator.map_server_peers )
+                delete this.rtcCreator.map_server_peers[this.idSomebodyOtherSide];
+            this.idSomebodyOtherSide = null;
+        }
+        super.onError( err );
+    }
+    onImpersonationCompleteForCreator() { // specific for server peer
+        if( settings.logging.net.signaling.creatorImpersonationComplete )
+            console.log( "Creator impersonation complete" );
+    }
+    publishOfferOnSignalingServer() {
+        const self = this;
+        try {
+            if( settings.logging.net.signaling.candidate )
+                console.log( " <<< " + self.describe() + " got candidate", event );
+            if( settings.logging.net.signaling.candidate )
+                console.log( " <<< " + self.describe() + " got candidate", event );
+            if( ! self.rtcCreator.signalingPipe )
+                throw new Error( "no connection to signaling server" );
+            const joPublishOfferMessage = {
+                id: utils.randomCallID(),
+                method: "signalingPublishOffer",
+                offer: self.pc.localDescription,
+                idSomebodyCreator: "" + self.rtcCreator.idRtcParticipant,
+                idOffer: 0 + self.idOffer
+            };
+            if( settings.logging.net.signaling.message )
+                console.log( " <<< " + self.describe() + " signaling message out", joPublishOfferMessage );
+            self.rtcCreator.signalingPipe.send( joPublishOfferMessage );
+            self.publishCancel();
+            self.dispatchEvent( new UniversalDispatcherEvent( "signalingNegotiationStart", { detail: { participant: self } } ) );
+            if( self.timeToSignalingNegotiationMilliseconds > 0 ) {
+                self.isSignalingNegotiation = true;
+                self.timerSignalingNegotiation = setTimeout( function() {
+                    self.publishCancel();
+                    self.signalingNegotiationCancel();
+                    self.isSignalingNegotiationTimeout = true;
+                    if( settings.logging.net.signaling.signalingNegotiationTimeout )
+                        console.warn( " !!! " + self.describe() + " signaling negotiation timeout " + self.timeToSignalingNegotiationMilliseconds + " milliseconds reached" );
+                    self.dispatchEvent( new UniversalDispatcherEvent( "signalingNegotiationTimeout", { detail: { participant: self } } ) );
+                    if( self.rtcCreator )
+                        self.rtcCreator.dispatchEvent( new UniversalDispatcherEvent( "signalingNegotiationTimeout", { detail: { participant: self } } ) );
+                }, self.timeToSignalingNegotiationMilliseconds );
+            } // if( self.timeToSignalingNegotiationMilliseconds > 0 )
+        } catch ( err ) {
+            throw err;
+        }
+    }
+    onIceComplete( event ) {
+        super.onIceComplete( event );
+        const self = this;
+        try {
+            if( event.candidate == null || settings.net.rtc.fastPublishMode.serverPeer ) {
+                if( ! self.isOfferPublishedOnSignalingServer ) {
+                    self.isOfferPublishedOnSignalingServer = true;
+                    self.publishOfferOnSignalingServer();
+                }
+            }
+            if( event.candidate != null ) {
+                if( settings.logging.net.signaling.candidateWalk )
+                    console.log( " <<< " + self.describe() + " got candidate", event );
+            }
+        } catch ( err ) {
+            self.publishCancel();
+            self.signalingNegotiationCancel();
+            self.onError( "Failed to process ICE candidate: " + err.toString() );
+        }
+    }
+};
+
+class RTCCreator extends RTCActor {
+    constructor( strSignalingServerURL, idRtcParticipant, offerOptions, signalingOptions ) {
+        super( strSignalingServerURL, idRtcParticipant, offerOptions, signalingOptions );
+        const self = this;
+        self.idOfferNext = 1;
+        self.isCreator = true;
+        self.map_server_offers = { }; // idOffer -> RTCServerPeer
+        self.map_server_peers = { }; // idSomebodyOtherSide -> RTCServerPeer
+        self.signalingPipeOpen();
+    }
+    dispose() {
+        if( this.isDisposed )
+            return;
+        this.isDisposing = true;
+        for( const [ idOffer, rtcPeer ] of Object.entries( this.map_server_offers ) ) {
+            if( settings.logging.net.signaling.offerUnregister )
+                console.log( "Unregister offer", idOffer, "(one of all, RTCCreator dispose)" );
+            rtcPeer.dispose();
+        }
+        for( const [ /*idSomebodyOtherSide*/, rtcPeer ] of Object.entries( this.map_server_peers ) )
+            rtcPeer.dispose();
+        this.map_server_offers = { };
+        // self.idOfferNext = 0;
+        super.dispose();
+    }
+    describe( strInstanceType, arrAdditionalProps ) {
+        strInstanceType = ( strInstanceType == null || strInstanceType == undefined || ( typeof strInstanceType != "string" ) || strInstanceType.length == 0 )
+            ? "rtc-creator"
+            : strInstanceType;
+        // arrAdditionalProps = arrAdditionalProps || [];
+        return super.describe( strInstanceType, arrAdditionalProps );
+    }
+    onOtherSideIdentified( idSomebodyOtherSide, idOffer ) { // server peer got result
+        if( settings.logging.net.signaling.impersonate )
+            console.log( this.describe() + " did identified other side RTC joiner \"" + idSomebodyOtherSide + "\" via offer ID " + idOffer.toString() );
+        if( ! ( idOffer in this.map_server_offers ) ) {
+            const strError = "not a registered pending offer(onOtherSideIdentified)";
+            if( settings.logging.net.signaling.error )
+                console.warn( " >>> " + this.describe() + " came across with incorrect other side identification for *somebody*", idSomebodyOtherSide, "and offer ID", idOffer, ":".strError );
+            this.onError( strError );
+            return;
+        }
+        const rtcPeer = this.map_server_offers[idOffer];
+        if( settings.logging.net.signaling.offerUnregister )
+            console.log( "Unregister offer", idOffer, "(onOtherSideIdentified in RTCCreator)" );
+        delete this.map_server_offers[idOffer];
+        this.map_server_peers["" + idSomebodyOtherSide] = rtcPeer;
+        rtcPeer.onOtherSideIdentified( "" + idSomebodyOtherSide );
+    }
+    onRtcPeerError( rtcPeer, err ) {
+        if( settings.logging.net.rtc.error )
+            console.warn( " !!! " + this.describe() + " rtc peer error", err );
+        this.dispatchEvent( new UniversalDispatcherEvent( "rtcPeerError", { detail: { actor: this, peer: rtcPeer, error: err } } ) );
+    }
+    signalingPipeOnMessage( joMessage ) {
+        const self = this;
+        switch ( joMessage.method ) {
+        case "signalingPublishOffer": {
+            if( joMessage.error == null ) {
+                // OKay, creator offer published
+                if( settings.logging.net.signaling.offer )
+                    console.log( "Success, " + this.describe() + " offer published (step 1)" );
+                this.dispatchEvent( new UniversalDispatcherEvent( "signalingPassedOfferPublish", { detail: { actor: this } } ) );
+            } else {
+                if( settings.logging.net.signaling.error )
+                    console.warn( " !!! " + this.describe() + " signaling offer publishing (step 1) error", joMessage.error );
+                this.dispatchEvent( new UniversalDispatcherEvent( "signalingFailedOfferPublish", { detail: { actor: this, error: joMessage.error } } ) );
+                this.onError( joMessage.error );
+            }
+        } break;
+        case "signalingPublishAnswer": { // server peer got result
+            if( joMessage.error == null ) {
+                const idSomebodyOtherSide = "" + joMessage.idSomebody_joiner;
+                const idOffer = 0 + joMessage.idOffer;
+                if( ! ( idOffer in this.map_server_offers ) ) {
+                    const strError = "not a registered pending offer(signalingPublishAnswer)";
+                    if( settings.logging.net.signaling.error )
+                        console.warn( " !!! " + this.describe() + " came across with incorrect signalingPublishAnswer message for *somebody*", idSomebodyOtherSide, "and offer ID", idOffer, ":", strError );
+                    this.onError( strError );
+                    return;
+                }
+                const rtcPeer = this.map_server_offers[idOffer];
+                // OKay, finally got answer from candida
+                if( settings.logging.net.signaling.generic )
+                    console.log( "Success, " + this.describe() + " got answer from candidate (step 3)" );
+                this.dispatchEvent( new UniversalDispatcherEvent( "signalingPassedPublishAnswer", { detail: { actor: this, idSomebodyOtherSide: "" + idSomebodyOtherSide, idOffer: 0 + idOffer } } ) );
+                const answer = joMessage.answer;
+                if( settings.logging.net.signaling.offer )
+                    console.log( " >>> " + self.describe() + " got answer:", answer );
+                const answerDescription = new wrtc_mod.RTCSessionDescription( answer );
+                if( settings.logging.net.signaling.offer )
+                    console.log( " >>> " + self.describe() + " got answer description:", answerDescription );
+                // console.log( "----- will set remote desc when in state", rtcPeer.pc.signalingState );
+                if( rtcPeer.pc.signalingState != "have-local-offer" ) {
+                    if( settings.logging.net.signaling.offerSkipPublishedAnswer )
+                        console.warn( " >>> " + self.describe() + " in \"" + rtcPeer.pc.signalingState + "\" state will skip setting remote description from answer", answerDescription );
+                    return;
+                }
+                rtcPeer.pc.setRemoteDescription( answerDescription ).then(
+                    function() {
+                        // success
+                        if( settings.logging.net.signaling.remoteDescription )
+                            console.log( " >>> " + self.describe() + "did set remote description:", answerDescription );
+                        self.dispatchEvent( new UniversalDispatcherEvent( "remoteDescriptionSet", { detail: { participant: self } } ) );
+                        self.onOtherSideIdentified( idSomebodyOtherSide, idOffer ); // server peer got result
+                    }, function( err ) {
+                        // error
+                        self.onError( "Failed to set remote description: " + err.toString() );
+                    } );
+            } else {
+                if( settings.logging.net.signaling.error )
+                    console.warn( " !!! " + this.describe() + " error getting candidate answer (step 1) error", joMessage.error );
+                this.dispatchEvent( new UniversalDispatcherEvent( "signalingFailedPublishAnswer", { detail: { actor: this, error: joMessage.error } } ) );
+                this.onError( joMessage.error );
+            }
+        } break;
+        default:
+            super.signalingPipeOnMessage( joMessage );
+            break;
+        } // switch( joMessage.method )
+    }
+    send( data ) { // implementation in RTCCreator does send to all
+        try {
+            const s = socket_sent_data_marshall( data );
+            for( const [ /*idSomebodyOtherSide*/, rtcPeer ] of Object.entries( this.map_server_peers ) ) {
+                try {
+                    rtcPeer.send( s );
+                } catch ( err ) {
+                    this.onRtcPeerError( rtcPeer, err );
+                }
+            }
+        } catch ( err ) {
+            this.onError( err );
+        }
+    }
+    onImpersonationComplete() {
+        super.onImpersonationComplete();
+        for( const [ /*idOffer*/, rtcPeer ] of Object.entries( this.map_server_offers ) )
+            rtcPeer.onImpersonationCompleteForCreator();
+        for( const [ /*idSomebodyOtherSide*/, rtcPeer ] of Object.entries( this.map_server_peers ) )
+            rtcPeer.onImpersonationCompleteForCreator();
+    }
+};
+
+class RTCJoiner extends RTCActor {
+    constructor( strSignalingServerURL, idRtcParticipant, offerOptions, signalingOptions, peerConfiguration, peerAdditionalOptions ) {
+        super( strSignalingServerURL, idRtcParticipant, offerOptions, signalingOptions );
+        this.idSomebodyOtherSide = null;
+        this.idOffer = 0;
+        this.isJoiner = true;
+        this.tsAnswerCreated = null;
+        this.isAnswerPublishedOnSignalingServer = false;
+        this.signalingPipeOpen();
+        this.peerConfiguration = ( peerConfiguration && typeof peerConfiguration == "object" ) ? peerConfiguration : settings.net.rtc.peerConfiguration;
+        this.peerAdditionalOptions = ( peerAdditionalOptions && typeof peerAdditionalOptions == "object" ) ? peerAdditionalOptions : settings.net.rtc.peerAdditionalOptions;
+        // this.initPeer();
+    }
+    dispose() {
+        if( this.isDisposed )
+            return;
+        this.isDisposing = true;
+        this.idSomebodyOtherSide = null;
+        this.idOffer = 0;
+        this.tsAnswerCreated = null;
+        this.isAnswerPublishedOnSignalingServer = false;
+        super.dispose();
+    }
+    describe( strInstanceType, arrAdditionalProps ) {
+        strInstanceType = ( strInstanceType == null || strInstanceType == undefined || ( typeof strInstanceType != "string" ) || strInstanceType.length == 0 )
+            ? "rtc-joiner"
+            : strInstanceType;
+        // arrAdditionalProps = arrAdditionalProps || [];
+        return super.describe( strInstanceType, arrAdditionalProps );
+    }
+    initPeer() {
+        if( this.isDisposed )
+            return;
+        const self = this;
+        if( self.pc )
+            return;
+        self.pc = new wrtc_mod.RTCPeerConnection( self.peerConfiguration, self.peerAdditionalOptions );
+        self.pc.addEventListener( "track", function( event ) {
+            self.dispatchEvent( new UniversalDispatcherEvent( "trackAvailable", { detail: { participant: self, event: event } } ) );
+        } );
+        self.pc.oniceconnectionstatechange = function( event ) { self.onIceConnectionStateChange( event ); };
+        self.pc.onicegatheringstatechange = function( event ) { self.onIceGatheringStateChange( event ); };
+        self.pc.onidentityresult = function( event ) { self.onIceIdentifyResult( event ); };
+        self.pc.onsignalingstatechange = function( event ) { self.onIceSignalingStateChange( event ); };
+        self.pc.onnegotiationneeded = function( event ) { self.onIceNegotiationNeeded( event ); };
+        self.pc.ondatachannel = function( event ) {
+            self.dispatchEvent( new UniversalDispatcherEvent( "dataChannelAvailable", { detail: { participant: self, event: event } } ) );
+            const dataChannel = event.channel || event;
+            self.dc = dataChannel;
+            self.dc.addEventListener( "open", function( event ) { self.onDataChannelOpen( event ); } );
+            self.dc.addEventListener( "close", function( event ) { self.onDataChannelClose( event ); } );
+            self.dc.addEventListener( "error", function( event ) { self.onDataChannelError( event ); } );
+            self.dc.addEventListener( "message", function( event ) { self.onDataChannelMessage( event ); } );
+        };
+        self.pc.onicecandidate = function( event ) {
+            self.iceComplete = true;
+            self.onIceComplete( event );
+            try {
+                if( ! self.signalingPipe ) {
+                    if( self.dc )
+                        return; // already connected, ignore (Firefox fix)
+                    // if( self.pc && self.pc.onicecandidate )
+                    //     self.pc.onicecandidate = null;
+                    throw new Error( "no connection to signaling server" );
+                }
+                if( ! self.isAnswerPublishedOnSignalingServer ) {
+                    self.publishSignalingAnswer( event );
+                    self.iAnswerPublishedOnSignalingServer = true;
+                }
+                if( event.candidate != null ) {
+                    if( settings.logging.net.signaling.candidateWalk )
+                        console.log( " <<< " + self.describe() + " got candidate", event );
+                }
+            } catch ( err ) {
+                // self.publishCancel();
+                // self.signalingNegotiationCancel();
+                self.onError( "Failed to process ICE candidate: " + err.toString() );
+            }
+        }; // onicecandidate
+    }
+    publishSignalingAnswer( event ) {
+        const self = this;
+        try {
+            if( event.candidate == null || settings.net.rtc.fastPublishMode.joiner ) {
+                if( settings.logging.net.signaling.candidate )
+                    console.log( " <<< " + self.describe() + " got candidate", event );
+                if( ! self.signalingPipe )
+                    throw new Error( "no connection to signaling server" );
+                const joPublishAnswerMessage = {
+                    id: utils.randomCallID(),
+                    method: "signalingPublishAnswer",
+                    answer: self.pc.localDescription,
+                    idRtcParticipant: "" + self.idRtcParticipant,
+                    idSomebodyCreator: "" + self.idSomebodyCreator,
+                    idOffer: 0 + self.idOffer
+                };
+                if( settings.logging.net.signaling.message )
+                    console.log( " <<< " + self.describe() + " signaling client message out", joPublishAnswerMessage );
+                self.signalingPipe.send( joPublishAnswerMessage );
+            }
+        } catch ( err ) {
+            throw err;
+        }
+    }
+    delayedInitPeer() {
+        if( this.bWasImpersonated )
+            this.initPeer();
+    }
+    onImpersonationComplete() {
+        super.onImpersonationComplete();
+        const joFetchOfferMessage = {
+            id: utils.randomCallID(),
+            method: "signalingFetchOffer"
+        };
+        if( settings.logging.net.signaling.message )
+            console.log( " <<< " + this.describe() + " signaling client message out", joFetchOfferMessage );
+        this.signalingPipe.send( joFetchOfferMessage );
+    }
+    onIceComplete( event ) {
+        super.onIceComplete( event );
+    }
+    onOtherSideIdentified( idSomebodyOtherSide, idOffer ) { // client peer got result
+        if( settings.logging.net.signaling.impersonate )
+            console.log( this.describe() + " did identified other side RTC creator \"" + idSomebodyOtherSide + "\" via offer ID " + idOffer.toString() );
+        this.idSomebodyOtherSide = "" + idSomebodyOtherSide;
+        this.idOffer = 0 + idOffer;
+        this.wasIdentified = true;
+        this.dispatchEvent( new UniversalDispatcherEvent( "identified", { detail: { participant: this, idSomebodyOtherSide: "" + idSomebodyOtherSide } } ) );
+    }
+    signalingPipeOnMessage( joMessage ) {
+        const self = this;
+        switch ( joMessage.method ) {
+        case "signalingFetchOffer": {
+            if( joMessage.error == null ) {
+                // OKay, fetched offer from creator
+                this.delayedInitPeer();
+                this.idSomebodyCreator = "" + joMessage.idSomebodyCreator;
+                const idSomebodyOtherSide = "" + joMessage.idSomebodyCreator;
+                const idOffer = 0 + joMessage.idOffer;
+                if( settings.logging.net.signaling.generic )
+                    console.log( "Success, " + this.describe() + " fetched offer from creator (step 2)" );
+                this.dispatchEvent( new UniversalDispatcherEvent( "signalingPassedFetchOffer", { detail: { actor: this, idSomebodyOtherSide: "" + idSomebodyOtherSide, idOffer: 0 + idOffer } } ) );
+                const offer = joMessage.offer;
+                if( settings.logging.net.signaling.offer )
+                    console.log( " <<< " + self.describe() + " got offer:", offer );
+                const offerDescription = new wrtc_mod.RTCSessionDescription( offer );
+                if( settings.logging.net.signaling.offer )
+                    console.log( " <<< " + self.describe() + " got offer description:", offerDescription );
+                this.pc.setRemoteDescription( offerDescription ).then(
+                    function() {
+                        // success
+                        if( settings.logging.net.signaling.remoteDescription )
+                            console.log( " <<< " + self.describe() + "did set remote description:", offerDescription );
+                        self.dispatchEvent( new UniversalDispatcherEvent( "remoteDescriptionSet", { detail: { participant: self } } ) );
+                        self.pc.createAnswer( self.offerOptions ).then(
+                            function( answerDescription ) {
+                                // success
+                                self.tsAnswerCreated = new Date();
+                                if( settings.logging.net.signaling.answer )
+                                    console.log( " <<< " + self.describe() + "did created answer at " + utils.format_date_time( self.tsAnswerCreated ) + " with description:", answerDescription );
+                                self.dispatchEvent( new UniversalDispatcherEvent( "answerCreated", { detail: { participant: self } } ) );
+                                self.pc.setLocalDescription( answerDescription ).then(
+                                    function() {
+                                        // success
+                                        if( settings.logging.net.signaling.localDescription )
+                                            console.log( " <<< " + self.describe() + " local description set:", answerDescription );
+                                        self.dispatchEvent( new UniversalDispatcherEvent( "localDescriptionSet", { detail: { participant: self } } ) );
+                                        self.onOtherSideIdentified( idSomebodyOtherSide, idOffer ); // client peer got result
+                                    }, function( err ) {
+                                        // error of setLocalDescription
+                                        self.onError( "Failed to set local description (while fetching offer for \"" + idSomebodyOtherSide + "\"): " + err.toString() );
+                                    } );
+                            }, function( err ) {
+                                // error of createAnswer
+                                self.onError( "Failed to create answer (while fetching offer for \"" + idSomebodyOtherSide + "\"): " + err.toString() );
+                            } );
+                    }, function( err ) {
+                        // error of setLocalDescription
+                        self.onError( "Failed to set remote description: (while fetching offer for \"" + idSomebodyOtherSide + "\"): " + err.toString() );
+                    } );
+            } else {
+                if( settings.logging.net.signaling.error )
+                    console.warn( " !!! " + this.describe() + " signaling offer publishing (step 1) error", joMessage.error );
+                this.dispatchEvent( new UniversalDispatcherEvent( "signalingFailedFetchOffer", { detail: { actor: this, error: joMessage.error } } ) );
+                this.onError( joMessage.error );
+            }
+        } break;
+        default:
+            super.signalingPipeOnMessage( joMessage );
+            break;
+        } // switch( joMessage.method )
+    }
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class WebRTCServerPipe extends BasicSocketPipe {
+    constructor( acceptor, rtcPeer, strSignalingServerURL ) {
+        super();
+        const self = this;
+        self.socketType = "WebRTC";
+        self.socketSubtype = "server";
+        self.isConnected = true;
+        self.acceptor = acceptor;
+        self.clientNumber = 0 + acceptor.nextClientNumber;
+        self.clientPort = 0 + self.clientNumber;
+        ++ acceptor.nextClientNumber;
+        self.rtcPeer = rtcPeer;
+        self.strSignalingServerURL = utils.makeValidSignalingServerURL( strSignalingServerURL );
+        self.url = "rtc_server_pipe(" + self.clientNumber + ")://" + strSignalingServerURL;
+        // self.rtcPeer.on( "identified", function( event ) {
+        //     if( settings.logging.net.signaling.generic )
+        //         console.log( self.describe() + " is now identified peer", event.detail.idSomebodyOtherSide );
+        // } );
+        self.rtcPeer.on( "dataChannelOpen", function( jo ) {
+            self.isConnected = true;
+            self.acceptor.mapClients["" + self.clientPort] = self;
+            self.dispatchEvent( new UniversalDispatcherEvent( "open", { socket: self } ) );
+            self.acceptor.dispatchEvent( new UniversalDispatcherEvent( "connection", { socket: self, strSignalingServerURL: "" + strSignalingServerURL } ) );
+        } );
+        self.rtcPeer.on( "dataChannelMessage", function( jo ) {
+            self.receive( jo.detail.data );
+        } );
+        self.rtcPeer.on( "rtcParticipantError", function( jo ) {
+            self.isConnected = false;
+            self.dispatchEvent( new UniversalDispatcherEvent( "error", { socket: self, message: jo } ) );
+        } );
+        self.rtcPeer.on( "dataChannelError", function( jo ) {
+            self.isConnected = false;
+            self.dispatchEvent( new UniversalDispatcherEvent( "error", { socket: self, message: jo } ) );
+        } );
+        self.rtcPeer.on( "dataChannelClose", function( jo ) {
+            self.isConnected = false;
+            self.dispatchEvent( new UniversalDispatcherEvent( "close", { socket: self, message: jo } ) );
+        } );
+        self.rtcPeer.on( "peerClose", function( jo ) {
+            self.isConnected = false;
+            self.dispatchEvent( new UniversalDispatcherEvent( "close", { socket: self, message: jo } ) );
+        } );
+    }
+    dispose() {
+        this.performDisconnect();
+        super.dispose();
+    }
+    handleServerDisposed() {
+        this.performDisconnect();
+        this.isConnected = false;
+        this.clientNumber = 0;
+        this.acceptor = null;
+        this.rtcPeer = null;
+        this.url = "";
+        this.strSignalingServerURL = "";
+        super.handleServerDisposed();
+        this.dispose();
+    }
+    performDisconnect() {
+        if( ! this.isConnected )
+            return;
+        this.isConnected = false;
+        if( this.acceptor )
+            this.acceptor.unregisterClientByKey( this.clientPort );
+        if( this.rtcPeer ) {
+            this.rtcPeer.offAll();
+            this.rtcPeer = null;
+        }
+        this.clientNumber = 0;
+        this.acceptor = null;
+        this.url = "";
+        this.strSignalingServerURL = "";
+    }
+    impl_send( data ) {
+        if( ( !this.isConnected ) || ( !this.rtcPeer ) ) {
+            const err = "Cannot send messages to disconnected WebRTC socket server pipe";
+            this.onError( err );
+            throw err;
+        }
+        const s = socket_sent_data_marshall( data );
+        this.rtcPeer.send( s );
+    }
+    disconnect() {
+        this.performDisconnect();
+        super.disconnect();
+    }
+    impl_receive( data ) {
+        const jo = socket_received_data_reverse_marshall( data );
+        this.dispatchEvent( new UniversalDispatcherEvent( "message", { socket: this, message: jo } ) );
+    }
+};
+
+class WebRTCServerAcceptor extends BasicServerAcceptor {
+    constructor( strSignalingServerURL, idRtcParticipant, offerOptions, signalingOptions, maxActiveOfferCount, timeToPublishMilliseconds, timeToSignalingNegotiationMilliseconds, peerConfiguration, peerAdditionalOptions ) {
+        super();
+        this.strSignalingServerURL = utils.makeValidSignalingServerURL( strSignalingServerURL );
+        this.idRtcParticipant = "" + ( ( idRtcParticipant != null && idRtcParticipant != undefined && typeof idRtcParticipant == "string" && idRtcParticipant.length > 0 ) ? idRtcParticipant : utils.uuid_v4() );
+        this.offerOptions = offerOptions ? offerOptions : null;
+        this.signalingOptions = signalingOptions ? signalingOptions : null;
+        this.peerConfiguration = ( peerConfiguration && typeof peerConfiguration == "object" ) ? peerConfiguration : settings.net.rtc.peerConfiguration;
+        this.peerAdditionalOptions = ( peerAdditionalOptions && typeof peerAdditionalOptions == "object" ) ? peerAdditionalOptions : settings.net.rtc.peerAdditionalOptions;
+        this.socketType = "WebRTC";
+        this.maxActiveOfferCount = ( maxActiveOfferCount != null && maxActiveOfferCount != undefined ) ? parseInt( maxActiveOfferCount, 10 ) : settings.net.rtc.maxActiveOfferCount;
+        if( this.maxActiveOfferCount < 1 )
+            this.maxActiveOfferCount = 1;
+        this.mapPendingOffers = { }; // idOffer -> RTCServerPeer
+        this.timeToPublishMilliseconds = timeToPublishMilliseconds
+            ? parseInt( timeToPublishMilliseconds, 10 )
+            : settings.net.rtc.timeToPublishMilliseconds;
+        this.timeToSignalingNegotiationMilliseconds = timeToSignalingNegotiationMilliseconds
+            ? parseInt( timeToSignalingNegotiationMilliseconds, 10 )
+            : settings.net.rtc.timeToSignalingNegotiationMilliseconds;
+        this.rtcCreator = new RTCCreator( "" + this.strSignalingServerURL, "" + this.idRtcParticipant, this.offerOptions, this.signalingOptions );
+        this.isListening = true;
+        const self = this;
+        this.rtcCreator.on( "signalingPassedImpersonation", function( eventData ) {
+            self.updateAllPendingOffers();
+            self.dispatchEvent( new UniversalDispatcherEvent( "signalingPassedImpersonation", { detail: { acceptor: self } } ) );
+        } );
+        this.rtcCreator.on( "signalingFailedImpersonation", function( eventData ) {
+            self.dispatchEvent( new UniversalDispatcherEvent( "signalingFailedImpersonation", { detail: { acceptor: self } } ) );
+        } );
+        this.rtcCreator.on( "error", function( eventData ) {
+            self.dispatchEvent( new UniversalDispatcherEvent( "error", { detail: { acceptor: self, eventData: eventData, errorType: "rtcCreatorError" } } ) );
+        } );
+        this.rtcCreator.on( "close", function( eventData ) {
+            self.dispatchEvent( new UniversalDispatcherEvent( "close", { detail: { acceptor: self, eventData: eventData } } ) );
+        } );
+        self.rtcCreator.on( "signalingPipeError", function( jo ) {
+            self.isConnected = false;
+            self.dispatchEvent( new UniversalDispatcherEvent( "error", { socket: self, message: jo, errorType: "signalingPipeError" } ) );
+        } );
+    }
+    dispose() {
+        if( this.isDisposed )
+            return;
+        this.isDisposing = true;
+        this.removeAllPendingOffers();
+        if( this.rtcCreator ) {
+            this.rtcCreator.dispose();
+            this.rtcCreator = null;
+        }
+        this.disposeNotifyClients();
+        super.dispose();
+    }
+    // flush() {
+    //     if( this.isDisposed )
+    //         return;
+    //     if( this.rtcCreator ) {
+    //         for( const [ /*idSomebodyOtherSide*/, rtcPeer ] of Object.entries( this.rtcCreator.map_server_peers ) ) {
+    //             const serverPipe = rtcPeer.serverPipe;
+    //             serverPipe.flush();
+    //         }
+    //     }
+    // }
+    addPendingOffer() {
+        if( this.isDisposed )
+            return;
+        const rtcPeer = new RTCServerPeer( this.rtcCreator, this.timeToPublishMilliseconds, this.timeToSignalingNegotiationMilliseconds, this.peerConfiguration, this.peerAdditionalOptions );
+        const self = this;
+        rtcPeer.on( "identified", function( event ) {
+            if( rtcPeer.isDisposing || rtcPeer.isDisposed )
+                return;
+            if( settings.logging.net.signaling.generic )
+                console.log( self.rtcCreator.describe() + " is now identified peer", event.detail.idSomebodyOtherSide );
+            rtcPeer.serverPipe = new WebRTCServerPipe( self, rtcPeer, self.strSignalingServerURL );
+            self.detachPendingOffer( rtcPeer.idOffer );
+            self.dispatchEvent( new UniversalDispatcherEvent( "identified", { detail: { peer: rtcPeer } } ) );
+            self.updateAllPendingOffers();
+        } );
+        rtcPeer.on( "localDescriptionSet", function( event ) {
+            self.dispatchEvent( new UniversalDispatcherEvent( "peerLocalDescriptionSet", { detail: { acceptor: self, peerEvent: event } } ) );
+        } );
+        const onTimeoutHandler = function() {
+            self.disposePendingOffer( rtcPeer.idOffer );
+            self.updateAllPendingOffers();
+        };
+        rtcPeer.on( "publishTimeout", onTimeoutHandler );
+        rtcPeer.on( "signalingNegotiationTimeout", onTimeoutHandler );
+        rtcPeer.on( "signalingNegotiationStart", function() {
+            self.updateAllPendingOffers();
+        } );
+
+        const retranslateError = function( eventData ) {
+            self.dispatchEvent( new UniversalDispatcherEvent( "error", { detail: { acceptor: self, rtcPeer: rtcPeer, eventData: eventData, errorType: "rtcPeerError" } } ) );
+        };
+        rtcPeer.on( "error", retranslateError );
+        rtcPeer.on( "rtcPeerError", retranslateError );
+
+        this.mapPendingOffers[rtcPeer.idOffer] = rtcPeer;
+    }
+    detachPendingOffer( idOffer ) {
+        if( idOffer in this.mapPendingOffers )
+            delete this.mapPendingOffers[idOffer];
+    }
+    disposePendingOffer( idOffer ) {
+        if( idOffer in this.mapPendingOffers ) {
+            const rtcPeer = this.mapPendingOffers[idOffer];
+            rtcPeer.dispose();
+            delete this.mapPendingOffers[idOffer];
+        }
+    }
+    removeAllPendingOffers() {
+        for( const [ /*idOffer*/, rtcPeer ] of Object.entries( this.rtcCreator.map_server_peers ) ) {
+            const serverPipe = rtcPeer.serverPipe;
+            serverPipe.dispose();
+        }
+        this.rtcCreator.map_server_peers = { };
+        for( const [ /*idOffer*/, rtcPeer ] of Object.entries( this.rtcCreator.mapPendingOffers ) )
+            rtcPeer.dispose();
+
+        this.mapPendingOffers = { };
+    }
+    updateAllPendingOffers() {
+        if( this.isDisposed )
+            return;
+        for( let n = Object.keys( this.mapPendingOffers ); n < this.maxActiveOfferCount; ++ n )
+            this.addPendingOffer();
+    }
+};
+
+class WebRTCClientPipe extends BasicSocketPipe {
+    constructor( strSignalingServerURL, idRtcParticipant, offerOptions, signalingOptions, peerConfiguration, peerAdditionalOptions ) {
+        super();
+        this.strSignalingServerURL = utils.makeValidSignalingServerURL( strSignalingServerURL );
+        this.idRtcParticipant = "" + ( ( idRtcParticipant != null && idRtcParticipant != undefined && typeof idRtcParticipant == "string" && idRtcParticipant.length > 0 ) ? idRtcParticipant : utils.uuid_v4() );
+        this.offerOptions = offerOptions ? offerOptions : null;
+        this.signalingOptions = signalingOptions ? signalingOptions : null;
+        this.peerConfiguration = ( peerConfiguration && typeof peerConfiguration == "object" ) ? peerConfiguration : settings.net.rtc.peerConfiguration;
+        this.peerAdditionalOptions = ( peerAdditionalOptions && typeof peerAdditionalOptions == "object" ) ? peerAdditionalOptions : settings.net.rtc.peerAdditionalOptions;
+        this.socketType = "WebRTC";
+        this.socketSubtype = "client";
+        this.isConnected = false;
+        this.rtcPeer = null;
+        this.isAutoCloseSignalingPipeOnDataChannelOpen = settings.net.rtc.isAutoCloseSignalingPipeOnDataChannelOpen ? true : false;
+        this.url = "rtc_client_pipe-" + this.strSignalingServerURL;
+        this.reconnect();
+    }
+    dispose() {
+        if( this.isDisposed )
+            return;
+        this.isDisposing = true;
+        this.performDisconnect();
+        this.strSignalingServerURL = null;
+        super.dispose();
+    }
+    impl_send( data ) {
+        if( ( !this.isConnected ) || ( !this.rtcPeer ) ) {
+            const s = "Cannot send messages to disconnected WebRTC socket client pipe";
+            this.dispatchEvent( new UniversalDispatcherEvent( "error", { socket: this, message: "" + s, errorType: "dataSendError" } ) );
+            throw new Error( s );
+        }
+        const s = socket_sent_data_marshall( data );
+        this.rtcPeer.send( s );
+    }
+    reconnect() {
+        this.performDisconnect();
+        this.rtc_connect( "" + this.strSignalingServerURL );
+    }
+    disconnect() {
+        this.performDisconnect();
+        super.disconnect();
+    }
+    performDisconnect() {
+        if( ! this.isConnected )
+            return;
+        this.rtc_disconnect();
+    }
+    rtc_connect( strSignalingServerURL ) {
+        if( strSignalingServerURL.length == 0 ) {
+            const s = "Cannot connect signaling server \"" + strSignalingServerURL + "\", bad url";
+            this.dispatchEvent( new UniversalDispatcherEvent( "error", { socket: this, message: "" + s, errorType: "badSignalingServerURL" } ) );
+            throw new Error( s );
+        }
+        const self = this;
+        while( true ) {
+            try {
+                if( self.isConnected || self.rtcPeer )
+                    self.rtc_disconnect();
+                self.rtcPeer = new RTCJoiner( "" + strSignalingServerURL, "" + self.idRtcParticipant, self.offerOptions, self.signalingOptions, self.peerConfiguration, self.peerAdditionalOptions ); // client side
+                self.strSignalingServerURL = utils.makeValidSignalingServerURL( strSignalingServerURL );
+                self.rtcPeer.on( "identified", function( event ) {
+                    if( settings.logging.net.signaling.generic )
+                        console.log( self.rtcPeer.describe() + " is now identified peer", event.detail.idSomebodyOtherSide );
+                } );
+                self.rtcPeer.on( "dataChannelOpen", function( jo ) {
+                    self.isConnected = true;
+                    self.dispatchEvent( new UniversalDispatcherEvent( "open", { socket: self } ) );
+                    if( self.isAutoCloseSignalingPipeOnDataChannelOpen ) {
+                        if( settings.logging.net.signaling.disconnect )
+                            console.warn( self.rtcPeer.describe() + " will auto-close signaling pipe(inside socket \"dataChannelOpen\" handler)" );
+                        self.rtcPeer.signalingPipeClose();
+                    }
+                } );
+                self.rtcPeer.on( "dataChannelMessage", function( jo ) {
+                    self.receive( jo.detail.data );
+                } );
+                self.rtcPeer.on( "rtcParticipantError", function( jo ) {
+                    self.isConnected = false;
+                    self.dispatchEvent( new UniversalDispatcherEvent( "error", { socket: self, message: jo, errorType: "rtcParticipantError" } ) );
+                } );
+                self.rtcPeer.on( "dataChannelError", function( jo ) {
+                    self.isConnected = false;
+                    self.dispatchEvent( new UniversalDispatcherEvent( "error", { socket: self, message: jo, errorType: "dataChannelError" } ) );
+                } );
+                self.rtcPeer.on( "dataChannelClose", function( jo ) {
+                    self.isConnected = false;
+                    self.dispatchEvent( new UniversalDispatcherEvent( "close", { socket: self, message: jo } ) );
+                } );
+                self.rtcPeer.on( "signalingPipeError", function( jo ) {
+                    self.isConnected = false;
+                    self.dispatchEvent( new UniversalDispatcherEvent( "error", { socket: self, message: jo, errorType: "signalingPipeError" } ) );
+                } );
+                return;
+            } catch ( err ) {
+                console.warn( "WebRTC client connect error:", err );
+                continue;
+            }
+        } // while( true )
+    }
+    rtc_disconnect() {
+        if( this.rtcPeer ) {
+            this.rtcPeer.offAll();
+            this.rtcPeer.dispose();
+            this.rtcPeer = null;
+        }
+        this.isConnected = false;
+        this.url = "";
+    }
+    impl_receive( data ) {
+        const jo = socket_received_data_reverse_marshall( data );
+        this.dispatchEvent( new UniversalDispatcherEvent( "message", { socket: this, message: jo } ) );
+    }
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+module.exports = {
+    https_mod: https_mod,
+    ws_mod: ws_mod,
+    wrtc_mod: wrtc_mod,
+    set_https_mod: set_https_mod,
+    set_ws_mod: set_ws_mod,
+    set_wrtc_mod: set_wrtc_mod,
+    g_mapLocalServers: g_mapLocalServers,
+    socket_sent_data_marshall: socket_sent_data_marshall,
+    socket_received_data_reverse_marshall: socket_received_data_reverse_marshall,
+    update_socket_data_stats_for_message: update_socket_data_stats_for_message,
+    generate_socket_data_stats_JSON: generate_socket_data_stats_JSON,
+    BasicServerAcceptor: BasicServerAcceptor,
+    BasicSocketPipe: BasicSocketPipe,
+    NullSocketPipe: NullSocketPipe,
+    is_running_in_worker: is_running_in_worker,
+    g_map_awaiting_in_worker_clients: g_map_awaiting_in_worker_clients,
+    g_map_connected_in_worker_clients: g_map_connected_in_worker_clients,
+    out_of_worker_apis: out_of_worker_apis,
+    in_worker_apis: in_worker_apis,
+    InWorkerServerPipe: InWorkerServerPipe,
+    InWorkerSocketServerAcceptor: InWorkerSocketServerAcceptor,
+    OutOfWorkerSocketClientPipe: OutOfWorkerSocketClientPipe,
+    OutOfWorkerRelay: OutOfWorkerRelay,
+    OneToOneRelay: OneToOneRelay,
+    DirectPipe: DirectPipe,
+    LocalSocketServerPipe: LocalSocketServerPipe,
+    LocalSocketServerAcceptor: LocalSocketServerAcceptor,
+    LocalSocketClientPipe: LocalSocketClientPipe,
+    WebSocketServerPipe: WebSocketServerPipe,
+    WebSocketServerAcceptor: WebSocketServerAcceptor,
+    WebSocketClientPipe: WebSocketClientPipe,
+    RTCConnection: RTCConnection,
+    RTCActor: RTCActor,
+    RTCServerPeer: RTCServerPeer,
+    RTCCreator: RTCCreator,
+    RTCJoiner: RTCJoiner,
+    WebRTCServerPipe: WebRTCServerPipe,
+    WebRTCServerAcceptor: WebRTCServerAcceptor,
+    WebRTCClientPipe: WebRTCClientPipe
+};
diff --git a/npms/skale-cool-socket/utils.js b/npms/skale-cool-socket/utils.js
new file mode 100644
index 000000000..270b57c68
--- /dev/null
+++ b/npms/skale-cool-socket/utils.js
@@ -0,0 +1,253 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+
+/**
+ * @license
+ * SKALE COOL SOCKET
+ *
+ * This program 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.
+ *
+ * This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @file utils.js
+ * @copyright SKALE Labs 2019-Present
+ */
+
+const { settings } = require( "./settings.js" );
+
+const uuid_v4 = function() {
+    return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace( /[xy]/g, function( c ) {
+        const r = Math.random() * 16 | 0, v = c == "x" ? r : ( r & 0x3 | 0x8 );
+        return v.toString( 16 );
+    } );
+};
+
+const getRandomInt = function( nMax ) {
+    return parseInt( Math.floor( Math.random() * Math.floor( nMax ) ), 10 );
+};
+
+const randomFixedInteger = function( length ) {
+    return Math.floor( Math.pow( 10, length - 1 ) + Math.random() * ( Math.pow( 10, length ) - Math.pow( 10, length - 1 ) - 1 ) );
+};
+
+const randomStringABC = function( length, arrCharacters ) {
+    length = parseInt( length, 10 );
+    if( length <= 0 || arrCharacters.length == 0 )
+        return "";
+    let s = "";
+    for( let i = 0; i < length; ++i )
+        s += arrCharacters.charAt( Math.floor( Math.random() * arrCharacters.length ) );
+    return s;
+};
+
+const randomString = function( length, isABC, isDigits, isSpecChr, isPunctuation ) { // by default only isABC=true
+    length = parseInt( length, 10 );
+    if( length <= 0 )
+        return "";
+    isABC = ( isABC == null || isABC == undefined ) ? true : ( isABC ? true : false );
+    isDigits = ( isDigits == null || isDigits == undefined ) ? false : ( isDigits ? true : false );
+    isSpecChr = ( isSpecChr == null || isSpecChr == undefined ) ? false : ( isSpecChr ? true : false );
+    isPunctuation = ( isPunctuation == null || isPunctuation == undefined ) ? false : ( isPunctuation ? true : false );
+    let arrCharacters = "";
+    if( isABC )
+        arrCharacters += "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+    if( isDigits )
+        arrCharacters += "0123456789";
+    if( isSpecChr )
+        arrCharacters += "(){}[]~!?@#$%^&*_+-='\"/\\";
+    if( isPunctuation )
+        arrCharacters += ",.:;";
+    if( arrCharacters.length == 0 )
+        return "";
+    return randomStringABC( length, arrCharacters );
+};
+
+const randomHexString = function( length ) { // length in characters, not bytes, each byte is 2 characters
+    const arrCharacters = "0123456789abcdef";
+    return randomStringABC( length, arrCharacters );
+};
+
+const replaceAll = function( str, find, replace ) {
+    return str.replace( new RegExp( find, "g" ), replace );
+};
+
+const simpleEscapeString = function( s ) {
+    if( s == null || s == undefined || typeof s != "string" )
+        return s;
+    s = replaceAll( s, "&", "&amp;" );
+    s = replaceAll( s, "<", "&lt;" );
+    s = replaceAll( s, ">", "&gt;" );
+    s = replaceAll( s, " ", "&nbsp;" );
+    return s;
+};
+
+const abstractUniqueID = function() {
+    const id = replaceAll( uuid_v4(), "-", "" ).toLowerCase();
+    return id;
+};
+
+const isEven = function( n ) {
+    return n % 2 == 0;
+};
+const isOdd = function( n ) {
+    return Math.abs( n % 2 ) == 1;
+};
+
+const g_nCallIdDigits = 10;
+const randomCallID = function() {
+    const id = randomHexString( g_nCallIdDigits );
+    return id;
+};
+
+const g_nDirectPipeIdDigits = 10;
+const randomDirectPipeID = function() {
+    const id = randomHexString( g_nDirectPipeIdDigits );
+    return id;
+};
+
+const bind_scope_to_function = function( scope, fn ) {
+    return function() {
+        fn.apply( scope, arguments );
+    };
+};
+
+const prepareAnswerJSON = function( joMessage ) {
+    const joAnswer = {
+        id: "" + ( ( joMessage != null && joMessage != undefined && typeof joMessage.id == "string" ) ? joMessage.id : randomCallID() ),
+        method: "" + ( ( joMessage != null && joMessage != undefined && typeof joMessage.method == "string" ) ? joMessage.method : "" ),
+        error: null
+    };
+    return joAnswer;
+};
+
+const makeValidSignalingServerURL = function( strSignalingServerURL ) {
+    const proto = settings.net.secure ? "wss" : "ws";
+    return "" + ( ( strSignalingServerURL != null && strSignalingServerURL != undefined && typeof strSignalingServerURL == "string" && strSignalingServerURL.length > 0 )
+        ? "" + strSignalingServerURL
+        : "" + proto + "://" + settings.net.hostname + ":" + settings.net.ports.signaling
+    );
+};
+
+const zero_padding_left = function( val, cntCharsNeeded ) {
+    if( val == null || val == undefined )
+        return val;
+    let s = "" + val;
+    while( s.length < cntCharsNeeded )
+        s = "0" + s;
+    return s;
+};
+const zero_padding_right = function( val, cntCharsNeeded ) {
+    if( val == null || val == undefined )
+        return val;
+    let s = "" + val;
+    while( s.length < cntCharsNeeded )
+        s = s + "0";
+    return s;
+};
+
+const parse_date_time = function( ts ) {
+    if( ts === null || ts === undefined )
+        return ts;
+    if( typeof ts != "string" )
+        return null;
+    // example:
+    //  0----|----1----|----2----|----
+    //  012345678901234567890123456789
+    // "2020/03/19-19:42:55.663"
+    const year = parseInt( ts.substring( 0, 4 ), 10 );
+    const month = parseInt( ts.substring( 5, 7 ), 10 ) + 1;
+    const day = parseInt( ts.substring( 8, 10 ), 10 );
+    const hour = parseInt( ts.substring( 11, 13 ), 10 );
+    const minute = parseInt( ts.substring( 14, 16 ), 10 );
+    const second = parseInt( ts.substring( 17, 19 ), 10 );
+    let millisecond = ts.substring( 20 );
+    if( millisecond.length > 3 )
+        millisecond = millisecond.substring( 0, 3 );
+    else {
+        while( millisecond.length < 3 )
+            millisecond = "0" + millisecond;
+    }
+    millisecond = parseInt( millisecond, 10 );
+    const u = Date.UTC( year, month, day, hour, minute, second, millisecond );
+    const d = new Date( u );
+    d.setMilliseconds( millisecond );
+    return d;
+};
+const format_date_time = function( dt, isDate, isTime, isMilliseconds, sepDate, sepTime, sepBetween, sepMilliseconds ) {
+    if( dt === null )
+        return "null-date-time";
+    if( dt === undefined )
+        return "undefined-date-time";
+    if( ! ( dt instanceof Date ) )
+        return "not-a-date-time";
+    isDate = ( isDate == null || isDate == undefined ) ? true : ( isDate ? true : false );
+    isTime = ( isTime == null || isTime == undefined ) ? true : ( isTime ? true : false );
+    if( ( !isDate ) && ( !isTime ) )
+        return "";
+    let s = "";
+    if( isDate ) {
+        sepDate = ( sepDate == null || sepDate == undefined || ( typeof sepDate != "string" ) ) ? "/" : sepDate;
+        const strDate = "" +
+            zero_padding_left( dt.getFullYear(), 4 ) +
+            sepDate +
+            zero_padding_left( dt.getMonth() + 1, 2 ) +
+            sepDate +
+            zero_padding_left( dt.getDate(), 2 );
+        s += strDate;
+    }
+    if( isTime ) {
+        sepTime = ( sepTime == null || sepTime == undefined || ( typeof sepTime != "string" ) ) ? ":" : sepTime;
+        if( isDate ) {
+            sepBetween = ( sepBetween == null || sepBetween == undefined || ( typeof sepBetween != "string" ) ) ? "-" : sepBetween;
+            s += sepBetween;
+        }
+        let strTime = "" +
+            zero_padding_left( dt.getHours(), 2 ) +
+            sepDate +
+            zero_padding_left( dt.getMinutes(), 2 ) +
+            sepDate +
+            zero_padding_left( dt.getSeconds(), 2 );
+        isMilliseconds = ( isMilliseconds == null || isMilliseconds == undefined ) ? true : ( isMilliseconds ? true : false );
+        if( isMilliseconds ) {
+            sepMilliseconds = ( sepMilliseconds == null || sepMilliseconds == undefined || ( typeof sepMilliseconds != "string" ) ) ? "." : sepMilliseconds;
+            strTime += sepMilliseconds + zero_padding_right( dt.getMilliseconds(), 3 );
+        }
+        s += strTime;
+    }
+    return s;
+};
+
+module.exports = {
+    uuid_v4: uuid_v4,
+    getRandomInt: getRandomInt,
+    randomFixedInteger: randomFixedInteger,
+    randomStringABC: randomStringABC,
+    randomString: randomString,
+    randomHexString: randomHexString,
+    replaceAll: replaceAll,
+    simpleEscapeString: simpleEscapeString,
+    abstractUniqueID: abstractUniqueID,
+    isEven: isEven,
+    isOdd: isOdd,
+    g_nCallIdDigits: g_nCallIdDigits,
+    randomCallID: randomCallID,
+    g_nDirectPipeIdDigits: g_nDirectPipeIdDigits,
+    randomDirectPipeID: randomDirectPipeID,
+    bind_scope_to_function: bind_scope_to_function,
+    prepareAnswerJSON: prepareAnswerJSON,
+    makeValidSignalingServerURL: makeValidSignalingServerURL,
+    zero_padding_left: zero_padding_left,
+    zero_padding_right: zero_padding_right,
+    parse_date_time: parse_date_time,
+    format_date_time: format_date_time
+};
diff --git a/npms/skale-cool-socket/yarn.lock b/npms/skale-cool-socket/yarn.lock
new file mode 100644
index 000000000..71b02d8b2
--- /dev/null
+++ b/npms/skale-cool-socket/yarn.lock
@@ -0,0 +1,1510 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@babel/code-frame@^7.0.0":
+  version "7.16.7"
+  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789"
+  integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==
+  dependencies:
+    "@babel/highlight" "^7.16.7"
+
+"@babel/helper-validator-identifier@^7.16.7":
+  version "7.16.7"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad"
+  integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==
+
+"@babel/highlight@^7.16.7":
+  version "7.17.9"
+  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.17.9.tgz#61b2ee7f32ea0454612def4fccdae0de232b73e3"
+  integrity sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.16.7"
+    chalk "^2.0.0"
+    js-tokens "^4.0.0"
+
+"@types/json5@^0.0.29":
+  version "0.0.29"
+  resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
+  integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
+
+acorn-jsx@^5.2.0:
+  version "5.3.2"
+  resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
+  integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
+
+acorn@^7.1.1:
+  version "7.4.1"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
+  integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
+
+ajv@^6.10.0, ajv@^6.10.2:
+  version "6.12.6"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
+  integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
+  dependencies:
+    fast-deep-equal "^3.1.1"
+    fast-json-stable-stringify "^2.0.0"
+    json-schema-traverse "^0.4.1"
+    uri-js "^4.2.2"
+
+ansi-escapes@^4.2.1:
+  version "4.3.2"
+  resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
+  integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==
+  dependencies:
+    type-fest "^0.21.3"
+
+ansi-regex@^4.1.0:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed"
+  integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==
+
+ansi-regex@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
+  integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
+
+ansi-styles@^3.2.0, ansi-styles@^3.2.1:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
+  integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
+  dependencies:
+    color-convert "^1.9.0"
+
+ansi-styles@^4.1.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
+  integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
+  dependencies:
+    color-convert "^2.0.1"
+
+argparse@^1.0.7:
+  version "1.0.10"
+  resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
+  integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
+  dependencies:
+    sprintf-js "~1.0.2"
+
+array-includes@^3.1.4:
+  version "3.1.4"
+  resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.4.tgz#f5b493162c760f3539631f005ba2bb46acb45ba9"
+  integrity sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw==
+  dependencies:
+    call-bind "^1.0.2"
+    define-properties "^1.1.3"
+    es-abstract "^1.19.1"
+    get-intrinsic "^1.1.1"
+    is-string "^1.0.7"
+
+array.prototype.flat@^1.2.5:
+  version "1.2.5"
+  resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz#07e0975d84bbc7c48cd1879d609e682598d33e13"
+  integrity sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg==
+  dependencies:
+    call-bind "^1.0.2"
+    define-properties "^1.1.3"
+    es-abstract "^1.19.0"
+
+astral-regex@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
+  integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==
+
+balanced-match@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
+  integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
+
+brace-expansion@^1.1.7:
+  version "1.1.11"
+  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+  integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+  dependencies:
+    balanced-match "^1.0.0"
+    concat-map "0.0.1"
+
+call-bind@^1.0.0, call-bind@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
+  integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==
+  dependencies:
+    function-bind "^1.1.1"
+    get-intrinsic "^1.0.2"
+
+callsites@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
+  integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
+
+chalk@^2.0.0, chalk@^2.1.0:
+  version "2.4.2"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
+  integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
+  dependencies:
+    ansi-styles "^3.2.1"
+    escape-string-regexp "^1.0.5"
+    supports-color "^5.3.0"
+
+chalk@^4.1.0:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
+  integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
+  dependencies:
+    ansi-styles "^4.1.0"
+    supports-color "^7.1.0"
+
+chardet@^0.7.0:
+  version "0.7.0"
+  resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
+  integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
+
+cli-cursor@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307"
+  integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==
+  dependencies:
+    restore-cursor "^3.1.0"
+
+cli-width@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6"
+  integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==
+
+color-convert@^1.9.0:
+  version "1.9.3"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
+  integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
+  dependencies:
+    color-name "1.1.3"
+
+color-convert@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
+  integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
+  dependencies:
+    color-name "~1.1.4"
+
+color-name@1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+  integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
+
+color-name@~1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+  integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+concat-map@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+  integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
+
+connect@^3.7.0:
+  version "3.7.0"
+  resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8"
+  integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==
+  dependencies:
+    debug "2.6.9"
+    finalhandler "1.1.2"
+    parseurl "~1.3.3"
+    utils-merge "1.0.1"
+
+cross-spawn@^6.0.5:
+  version "6.0.5"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
+  integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
+  dependencies:
+    nice-try "^1.0.4"
+    path-key "^2.0.1"
+    semver "^5.5.0"
+    shebang-command "^1.2.0"
+    which "^1.2.9"
+
+debug@2.6.9, debug@^2.6.9:
+  version "2.6.9"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+  integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
+  dependencies:
+    ms "2.0.0"
+
+debug@^3.2.7:
+  version "3.2.7"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
+  integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
+  dependencies:
+    ms "^2.1.1"
+
+debug@^4.0.1:
+  version "4.3.4"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
+  integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
+  dependencies:
+    ms "2.1.2"
+
+deep-is@~0.1.3:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
+  integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
+
+define-properties@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
+  integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
+  dependencies:
+    object-keys "^1.0.12"
+
+depd@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
+  integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
+
+destroy@1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
+  integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
+
+doctrine@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
+  integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==
+  dependencies:
+    esutils "^2.0.2"
+
+doctrine@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
+  integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
+  dependencies:
+    esutils "^2.0.2"
+
+ee-first@1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
+  integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
+
+emoji-regex@^7.0.1:
+  version "7.0.3"
+  resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
+  integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
+
+emoji-regex@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
+  integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
+
+encodeurl@~1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
+  integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
+
+es-abstract@^1.19.0, es-abstract@^1.19.1:
+  version "1.19.2"
+  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.2.tgz#8f7b696d8f15b167ae3640b4060670f3d054143f"
+  integrity sha512-gfSBJoZdlL2xRiOCy0g8gLMryhoe1TlimjzU99L/31Z8QEGIhVQI+EWwt5lT+AuU9SnorVupXFqqOGqGfsyO6w==
+  dependencies:
+    call-bind "^1.0.2"
+    es-to-primitive "^1.2.1"
+    function-bind "^1.1.1"
+    get-intrinsic "^1.1.1"
+    get-symbol-description "^1.0.0"
+    has "^1.0.3"
+    has-symbols "^1.0.3"
+    internal-slot "^1.0.3"
+    is-callable "^1.2.4"
+    is-negative-zero "^2.0.2"
+    is-regex "^1.1.4"
+    is-shared-array-buffer "^1.0.1"
+    is-string "^1.0.7"
+    is-weakref "^1.0.2"
+    object-inspect "^1.12.0"
+    object-keys "^1.1.1"
+    object.assign "^4.1.2"
+    string.prototype.trimend "^1.0.4"
+    string.prototype.trimstart "^1.0.4"
+    unbox-primitive "^1.0.1"
+
+es-to-primitive@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
+  integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==
+  dependencies:
+    is-callable "^1.1.4"
+    is-date-object "^1.0.1"
+    is-symbol "^1.0.2"
+
+escape-html@~1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
+  integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
+
+escape-string-regexp@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+  integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
+
+eslint-config-standard@^14.1.1:
+  version "14.1.1"
+  resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-14.1.1.tgz#830a8e44e7aef7de67464979ad06b406026c56ea"
+  integrity sha512-Z9B+VR+JIXRxz21udPTL9HpFMyoMUEeX1G251EQ6e05WD9aPVtVBn09XUmZ259wCMlCDmYDSZG62Hhm+ZTJcUg==
+
+eslint-import-resolver-node@^0.3.6:
+  version "0.3.6"
+  resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz#4048b958395da89668252001dbd9eca6b83bacbd"
+  integrity sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==
+  dependencies:
+    debug "^3.2.7"
+    resolve "^1.20.0"
+
+eslint-module-utils@^2.7.3:
+  version "2.7.3"
+  resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz#ad7e3a10552fdd0642e1e55292781bd6e34876ee"
+  integrity sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==
+  dependencies:
+    debug "^3.2.7"
+    find-up "^2.1.0"
+
+eslint-plugin-es@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz#75a7cdfdccddc0589934aeeb384175f221c57893"
+  integrity sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==
+  dependencies:
+    eslint-utils "^2.0.0"
+    regexpp "^3.0.0"
+
+eslint-plugin-import@^2.20.2:
+  version "2.26.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz#f812dc47be4f2b72b478a021605a59fc6fe8b88b"
+  integrity sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==
+  dependencies:
+    array-includes "^3.1.4"
+    array.prototype.flat "^1.2.5"
+    debug "^2.6.9"
+    doctrine "^2.1.0"
+    eslint-import-resolver-node "^0.3.6"
+    eslint-module-utils "^2.7.3"
+    has "^1.0.3"
+    is-core-module "^2.8.1"
+    is-glob "^4.0.3"
+    minimatch "^3.1.2"
+    object.values "^1.1.5"
+    resolve "^1.22.0"
+    tsconfig-paths "^3.14.1"
+
+eslint-plugin-node@^11.1.0:
+  version "11.1.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz#c95544416ee4ada26740a30474eefc5402dc671d"
+  integrity sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==
+  dependencies:
+    eslint-plugin-es "^3.0.0"
+    eslint-utils "^2.0.0"
+    ignore "^5.1.1"
+    minimatch "^3.0.4"
+    resolve "^1.10.1"
+    semver "^6.1.0"
+
+eslint-plugin-promise@^4.2.1:
+  version "4.3.1"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.3.1.tgz#61485df2a359e03149fdafc0a68b0e030ad2ac45"
+  integrity sha512-bY2sGqyptzFBDLh/GMbAxfdJC+b0f23ME63FOE4+Jao0oZ3E1LEwFtWJX/1pGMJLiTtrSSern2CRM/g+dfc0eQ==
+
+eslint-plugin-standard@^4.0.1:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-4.1.0.tgz#0c3bf3a67e853f8bbbc580fb4945fbf16f41b7c5"
+  integrity sha512-ZL7+QRixjTR6/528YNGyDotyffm5OQst/sGxKDwGb9Uqs4In5Egi4+jbobhqJoyoCM6/7v/1A5fhQ7ScMtDjaQ==
+
+eslint-scope@^5.0.0:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
+  integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
+  dependencies:
+    esrecurse "^4.3.0"
+    estraverse "^4.1.1"
+
+eslint-utils@^1.4.3:
+  version "1.4.3"
+  resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f"
+  integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==
+  dependencies:
+    eslint-visitor-keys "^1.1.0"
+
+eslint-utils@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27"
+  integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==
+  dependencies:
+    eslint-visitor-keys "^1.1.0"
+
+eslint-visitor-keys@^1.1.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e"
+  integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==
+
+eslint@^6.8.0:
+  version "6.8.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb"
+  integrity sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==
+  dependencies:
+    "@babel/code-frame" "^7.0.0"
+    ajv "^6.10.0"
+    chalk "^2.1.0"
+    cross-spawn "^6.0.5"
+    debug "^4.0.1"
+    doctrine "^3.0.0"
+    eslint-scope "^5.0.0"
+    eslint-utils "^1.4.3"
+    eslint-visitor-keys "^1.1.0"
+    espree "^6.1.2"
+    esquery "^1.0.1"
+    esutils "^2.0.2"
+    file-entry-cache "^5.0.1"
+    functional-red-black-tree "^1.0.1"
+    glob-parent "^5.0.0"
+    globals "^12.1.0"
+    ignore "^4.0.6"
+    import-fresh "^3.0.0"
+    imurmurhash "^0.1.4"
+    inquirer "^7.0.0"
+    is-glob "^4.0.0"
+    js-yaml "^3.13.1"
+    json-stable-stringify-without-jsonify "^1.0.1"
+    levn "^0.3.0"
+    lodash "^4.17.14"
+    minimatch "^3.0.4"
+    mkdirp "^0.5.1"
+    natural-compare "^1.4.0"
+    optionator "^0.8.3"
+    progress "^2.0.0"
+    regexpp "^2.0.1"
+    semver "^6.1.2"
+    strip-ansi "^5.2.0"
+    strip-json-comments "^3.0.1"
+    table "^5.2.3"
+    text-table "^0.2.0"
+    v8-compile-cache "^2.0.3"
+
+espree@^6.1.2:
+  version "6.2.1"
+  resolved "https://registry.yarnpkg.com/espree/-/espree-6.2.1.tgz#77fc72e1fd744a2052c20f38a5b575832e82734a"
+  integrity sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==
+  dependencies:
+    acorn "^7.1.1"
+    acorn-jsx "^5.2.0"
+    eslint-visitor-keys "^1.1.0"
+
+esprima@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
+  integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
+
+esquery@^1.0.1:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5"
+  integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==
+  dependencies:
+    estraverse "^5.1.0"
+
+esrecurse@^4.3.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
+  integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==
+  dependencies:
+    estraverse "^5.2.0"
+
+estraverse@^4.1.1:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
+  integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
+
+estraverse@^5.1.0, estraverse@^5.2.0:
+  version "5.3.0"
+  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
+  integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
+
+esutils@^2.0.2:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
+  integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
+
+etag@~1.8.1:
+  version "1.8.1"
+  resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
+  integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
+
+external-editor@^3.0.3:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495"
+  integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==
+  dependencies:
+    chardet "^0.7.0"
+    iconv-lite "^0.4.24"
+    tmp "^0.0.33"
+
+fast-deep-equal@^3.1.1:
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
+  integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+
+fast-json-stable-stringify@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
+  integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
+
+fast-levenshtein@~2.0.6:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
+  integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
+
+figures@^3.0.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
+  integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==
+  dependencies:
+    escape-string-regexp "^1.0.5"
+
+file-entry-cache@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c"
+  integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==
+  dependencies:
+    flat-cache "^2.0.1"
+
+finalhandler@1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
+  integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
+  dependencies:
+    debug "2.6.9"
+    encodeurl "~1.0.2"
+    escape-html "~1.0.3"
+    on-finished "~2.3.0"
+    parseurl "~1.3.3"
+    statuses "~1.5.0"
+    unpipe "~1.0.0"
+
+find-up@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
+  integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c=
+  dependencies:
+    locate-path "^2.0.0"
+
+flat-cache@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0"
+  integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==
+  dependencies:
+    flatted "^2.0.0"
+    rimraf "2.6.3"
+    write "1.0.3"
+
+flatted@^2.0.0:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138"
+  integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==
+
+fresh@0.5.2:
+  version "0.5.2"
+  resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
+  integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
+
+fs.realpath@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+  integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
+
+function-bind@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+  integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+
+functional-red-black-tree@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
+  integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
+
+get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6"
+  integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==
+  dependencies:
+    function-bind "^1.1.1"
+    has "^1.0.3"
+    has-symbols "^1.0.1"
+
+get-symbol-description@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6"
+  integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==
+  dependencies:
+    call-bind "^1.0.2"
+    get-intrinsic "^1.1.1"
+
+glob-parent@^5.0.0:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+  integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+  dependencies:
+    is-glob "^4.0.1"
+
+glob@^7.1.3:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
+  integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
+  dependencies:
+    fs.realpath "^1.0.0"
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "^3.0.4"
+    once "^1.3.0"
+    path-is-absolute "^1.0.0"
+
+globals@^12.1.0:
+  version "12.4.0"
+  resolved "https://registry.yarnpkg.com/globals/-/globals-12.4.0.tgz#a18813576a41b00a24a97e7f815918c2e19925f8"
+  integrity sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==
+  dependencies:
+    type-fest "^0.8.1"
+
+has-bigints@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
+  integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==
+
+has-flag@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+  integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
+
+has-flag@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
+  integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+
+has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
+  integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
+
+has-tostringtag@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25"
+  integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==
+  dependencies:
+    has-symbols "^1.0.2"
+
+has@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+  integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+  dependencies:
+    function-bind "^1.1.1"
+
+http-errors@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3"
+  integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==
+  dependencies:
+    depd "2.0.0"
+    inherits "2.0.4"
+    setprototypeof "1.2.0"
+    statuses "2.0.1"
+    toidentifier "1.0.1"
+
+iconv-lite@^0.4.24:
+  version "0.4.24"
+  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
+  integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
+  dependencies:
+    safer-buffer ">= 2.1.2 < 3"
+
+ignore@^4.0.6:
+  version "4.0.6"
+  resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
+  integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
+
+ignore@^5.1.1:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a"
+  integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==
+
+import-fresh@^3.0.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
+  integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
+  dependencies:
+    parent-module "^1.0.0"
+    resolve-from "^4.0.0"
+
+imurmurhash@^0.1.4:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
+  integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
+
+inflight@^1.0.4:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+  integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
+  dependencies:
+    once "^1.3.0"
+    wrappy "1"
+
+inherits@2, inherits@2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+  integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+inquirer@^7.0.0:
+  version "7.3.3"
+  resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003"
+  integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==
+  dependencies:
+    ansi-escapes "^4.2.1"
+    chalk "^4.1.0"
+    cli-cursor "^3.1.0"
+    cli-width "^3.0.0"
+    external-editor "^3.0.3"
+    figures "^3.0.0"
+    lodash "^4.17.19"
+    mute-stream "0.0.8"
+    run-async "^2.4.0"
+    rxjs "^6.6.0"
+    string-width "^4.1.0"
+    strip-ansi "^6.0.0"
+    through "^2.3.6"
+
+internal-slot@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c"
+  integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==
+  dependencies:
+    get-intrinsic "^1.1.0"
+    has "^1.0.3"
+    side-channel "^1.0.4"
+
+is-bigint@^1.0.1:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3"
+  integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==
+  dependencies:
+    has-bigints "^1.0.1"
+
+is-boolean-object@^1.1.0:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719"
+  integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==
+  dependencies:
+    call-bind "^1.0.2"
+    has-tostringtag "^1.0.0"
+
+is-callable@^1.1.4, is-callable@^1.2.4:
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945"
+  integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==
+
+is-core-module@^2.8.1:
+  version "2.8.1"
+  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211"
+  integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==
+  dependencies:
+    has "^1.0.3"
+
+is-date-object@^1.0.1:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f"
+  integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==
+  dependencies:
+    has-tostringtag "^1.0.0"
+
+is-extglob@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+  integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
+
+is-fullwidth-code-point@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
+  integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
+
+is-fullwidth-code-point@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
+  integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
+
+is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+  integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+  dependencies:
+    is-extglob "^2.1.1"
+
+is-negative-zero@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150"
+  integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==
+
+is-number-object@^1.0.4:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc"
+  integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==
+  dependencies:
+    has-tostringtag "^1.0.0"
+
+is-regex@^1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958"
+  integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==
+  dependencies:
+    call-bind "^1.0.2"
+    has-tostringtag "^1.0.0"
+
+is-shared-array-buffer@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79"
+  integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==
+  dependencies:
+    call-bind "^1.0.2"
+
+is-string@^1.0.5, is-string@^1.0.7:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd"
+  integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==
+  dependencies:
+    has-tostringtag "^1.0.0"
+
+is-symbol@^1.0.2, is-symbol@^1.0.3:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c"
+  integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==
+  dependencies:
+    has-symbols "^1.0.2"
+
+is-weakref@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2"
+  integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==
+  dependencies:
+    call-bind "^1.0.2"
+
+isexe@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+  integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
+
+js-tokens@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+  integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+
+js-yaml@^3.13.1:
+  version "3.14.1"
+  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
+  integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
+  dependencies:
+    argparse "^1.0.7"
+    esprima "^4.0.0"
+
+json-schema-traverse@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
+  integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
+
+json-stable-stringify-without-jsonify@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
+  integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=
+
+json5@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
+  integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==
+  dependencies:
+    minimist "^1.2.0"
+
+levn@^0.3.0, levn@~0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
+  integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=
+  dependencies:
+    prelude-ls "~1.1.2"
+    type-check "~0.3.2"
+
+locate-path@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
+  integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=
+  dependencies:
+    p-locate "^2.0.0"
+    path-exists "^3.0.0"
+
+lodash@^4.17.14, lodash@^4.17.19:
+  version "4.17.21"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+  integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+
+mime@1.6.0:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
+  integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
+
+mimic-fn@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
+  integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
+
+minimatch@^3.0.4, minimatch@^3.1.2:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
+  integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
+  dependencies:
+    brace-expansion "^1.1.7"
+
+minimist@^1.2.0, minimist@^1.2.6:
+  version "1.2.6"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
+  integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
+
+mkdirp@^0.5.1:
+  version "0.5.6"
+  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
+  integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
+  dependencies:
+    minimist "^1.2.6"
+
+ms@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+  integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
+
+ms@2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
+  integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+
+ms@2.1.3, ms@^2.1.1:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
+  integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+
+mute-stream@0.0.8:
+  version "0.0.8"
+  resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
+  integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
+
+natural-compare@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
+  integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
+
+nice-try@^1.0.4:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
+  integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
+
+object-inspect@^1.12.0, object-inspect@^1.9.0:
+  version "1.12.0"
+  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0"
+  integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==
+
+object-keys@^1.0.12, object-keys@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
+  integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
+
+object.assign@^4.1.2:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940"
+  integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==
+  dependencies:
+    call-bind "^1.0.0"
+    define-properties "^1.1.3"
+    has-symbols "^1.0.1"
+    object-keys "^1.1.1"
+
+object.values@^1.1.5:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.5.tgz#959f63e3ce9ef108720333082131e4a459b716ac"
+  integrity sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==
+  dependencies:
+    call-bind "^1.0.2"
+    define-properties "^1.1.3"
+    es-abstract "^1.19.1"
+
+on-finished@2.4.1:
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
+  integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==
+  dependencies:
+    ee-first "1.1.1"
+
+on-finished@~2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
+  integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
+  dependencies:
+    ee-first "1.1.1"
+
+once@^1.3.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+  integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
+  dependencies:
+    wrappy "1"
+
+onetime@^5.1.0:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
+  integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
+  dependencies:
+    mimic-fn "^2.1.0"
+
+optionator@^0.8.3:
+  version "0.8.3"
+  resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
+  integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==
+  dependencies:
+    deep-is "~0.1.3"
+    fast-levenshtein "~2.0.6"
+    levn "~0.3.0"
+    prelude-ls "~1.1.2"
+    type-check "~0.3.2"
+    word-wrap "~1.2.3"
+
+os-tmpdir@~1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
+  integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
+
+p-limit@^1.1.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8"
+  integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==
+  dependencies:
+    p-try "^1.0.0"
+
+p-locate@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
+  integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=
+  dependencies:
+    p-limit "^1.1.0"
+
+p-try@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
+  integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=
+
+parent-module@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
+  integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
+  dependencies:
+    callsites "^3.0.0"
+
+parseurl@~1.3.3:
+  version "1.3.3"
+  resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
+  integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
+
+path-exists@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
+  integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
+
+path-is-absolute@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+  integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
+
+path-key@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
+  integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
+
+path-parse@^1.0.7:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
+  integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+
+prelude-ls@~1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
+  integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
+
+progress@^2.0.0:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
+  integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
+
+punycode@^2.1.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
+  integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
+
+range-parser@~1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
+  integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
+
+regexpp@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f"
+  integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==
+
+regexpp@^3.0.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
+  integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==
+
+resolve-from@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
+  integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
+
+resolve@^1.10.1, resolve@^1.20.0, resolve@^1.22.0:
+  version "1.22.0"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198"
+  integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==
+  dependencies:
+    is-core-module "^2.8.1"
+    path-parse "^1.0.7"
+    supports-preserve-symlinks-flag "^1.0.0"
+
+restore-cursor@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e"
+  integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==
+  dependencies:
+    onetime "^5.1.0"
+    signal-exit "^3.0.2"
+
+rimraf@2.6.3:
+  version "2.6.3"
+  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
+  integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
+  dependencies:
+    glob "^7.1.3"
+
+run-async@^2.4.0:
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
+  integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==
+
+rxjs@^6.6.0:
+  version "6.6.7"
+  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"
+  integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==
+  dependencies:
+    tslib "^1.9.0"
+
+"safer-buffer@>= 2.1.2 < 3":
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+  integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+
+semver@^5.5.0:
+  version "5.7.1"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
+  integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
+
+semver@^6.1.0, semver@^6.1.2:
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
+  integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
+
+send@0.18.0:
+  version "0.18.0"
+  resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be"
+  integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==
+  dependencies:
+    debug "2.6.9"
+    depd "2.0.0"
+    destroy "1.2.0"
+    encodeurl "~1.0.2"
+    escape-html "~1.0.3"
+    etag "~1.8.1"
+    fresh "0.5.2"
+    http-errors "2.0.0"
+    mime "1.6.0"
+    ms "2.1.3"
+    on-finished "2.4.1"
+    range-parser "~1.2.1"
+    statuses "2.0.1"
+
+serve-static@^1.14.1:
+  version "1.15.0"
+  resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540"
+  integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==
+  dependencies:
+    encodeurl "~1.0.2"
+    escape-html "~1.0.3"
+    parseurl "~1.3.3"
+    send "0.18.0"
+
+setprototypeof@1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
+  integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
+
+shebang-command@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
+  integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=
+  dependencies:
+    shebang-regex "^1.0.0"
+
+shebang-regex@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
+  integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
+
+side-channel@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
+  integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==
+  dependencies:
+    call-bind "^1.0.0"
+    get-intrinsic "^1.0.2"
+    object-inspect "^1.9.0"
+
+signal-exit@^3.0.2:
+  version "3.0.7"
+  resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
+  integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
+
+slice-ansi@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636"
+  integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==
+  dependencies:
+    ansi-styles "^3.2.0"
+    astral-regex "^1.0.0"
+    is-fullwidth-code-point "^2.0.0"
+
+sprintf-js@~1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
+  integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
+
+statuses@2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
+  integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
+
+statuses@~1.5.0:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
+  integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
+
+string-width@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
+  integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==
+  dependencies:
+    emoji-regex "^7.0.1"
+    is-fullwidth-code-point "^2.0.0"
+    strip-ansi "^5.1.0"
+
+string-width@^4.1.0:
+  version "4.2.3"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
+  integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
+  dependencies:
+    emoji-regex "^8.0.0"
+    is-fullwidth-code-point "^3.0.0"
+    strip-ansi "^6.0.1"
+
+string.prototype.trimend@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80"
+  integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==
+  dependencies:
+    call-bind "^1.0.2"
+    define-properties "^1.1.3"
+
+string.prototype.trimstart@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed"
+  integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==
+  dependencies:
+    call-bind "^1.0.2"
+    define-properties "^1.1.3"
+
+strip-ansi@^5.1.0, strip-ansi@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
+  integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
+  dependencies:
+    ansi-regex "^4.1.0"
+
+strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+  integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+  dependencies:
+    ansi-regex "^5.0.1"
+
+strip-bom@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
+  integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=
+
+strip-json-comments@^3.0.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
+  integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
+
+supports-color@^5.3.0:
+  version "5.5.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
+  integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
+  dependencies:
+    has-flag "^3.0.0"
+
+supports-color@^7.1.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
+  integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
+  dependencies:
+    has-flag "^4.0.0"
+
+supports-preserve-symlinks-flag@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
+  integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
+
+table@^5.2.3:
+  version "5.4.6"
+  resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"
+  integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==
+  dependencies:
+    ajv "^6.10.2"
+    lodash "^4.17.14"
+    slice-ansi "^2.1.0"
+    string-width "^3.0.0"
+
+text-table@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
+  integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
+
+through@^2.3.6:
+  version "2.3.8"
+  resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
+  integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
+
+tmp@^0.0.33:
+  version "0.0.33"
+  resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
+  integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==
+  dependencies:
+    os-tmpdir "~1.0.2"
+
+toidentifier@1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
+  integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
+
+tsconfig-paths@^3.14.1:
+  version "3.14.1"
+  resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a"
+  integrity sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==
+  dependencies:
+    "@types/json5" "^0.0.29"
+    json5 "^1.0.1"
+    minimist "^1.2.6"
+    strip-bom "^3.0.0"
+
+tslib@^1.9.0:
+  version "1.14.1"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
+  integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
+
+type-check@~0.3.2:
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
+  integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=
+  dependencies:
+    prelude-ls "~1.1.2"
+
+type-fest@^0.21.3:
+  version "0.21.3"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
+  integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
+
+type-fest@^0.8.1:
+  version "0.8.1"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
+  integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
+
+unbox-primitive@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471"
+  integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==
+  dependencies:
+    function-bind "^1.1.1"
+    has-bigints "^1.0.1"
+    has-symbols "^1.0.2"
+    which-boxed-primitive "^1.0.2"
+
+unpipe@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
+  integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
+
+uri-js@^4.2.2:
+  version "4.4.1"
+  resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
+  integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
+  dependencies:
+    punycode "^2.1.0"
+
+utils-merge@1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
+  integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
+
+v8-compile-cache@^2.0.3:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
+  integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
+
+which-boxed-primitive@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"
+  integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==
+  dependencies:
+    is-bigint "^1.0.1"
+    is-boolean-object "^1.1.0"
+    is-number-object "^1.0.4"
+    is-string "^1.0.5"
+    is-symbol "^1.0.3"
+
+which@^1.2.9:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
+  integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
+  dependencies:
+    isexe "^2.0.0"
+
+word-wrap@~1.2.3:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
+  integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
+
+wrappy@1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+  integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
+
+write@1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3"
+  integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==
+  dependencies:
+    mkdirp "^0.5.1"
+
+ws@^8.6.0:
+  version "8.6.0"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-8.6.0.tgz#e5e9f1d9e7ff88083d0c0dd8281ea662a42c9c23"
+  integrity sha512-AzmM3aH3gk0aX7/rZLYvjdvZooofDu3fFOzGqcSnQ1tOcTWwhM/o+q++E8mAyVVIyUdajrkzWUGftaVSDLn1bw==
diff --git a/npms/skale-ima/index.js b/npms/skale-ima/index.js
index ba0f56434..ef70478aa 100644
--- a/npms/skale-ima/index.js
+++ b/npms/skale-ima/index.js
@@ -192,7 +192,7 @@ async function get_web3_blockNumber( details, cntAttempts, w3, retValOnFail, thr
             cc.error( "Failed call attempt " ) + cc.info( idxAttempt ) +
             cc.error( " to " ) + cc.note( strFnName + "()" ) +
             cc.error( " via " ) + cc.u( u ) +
-            cc.error( ", error is: " ) + cc.warning( err.toString() ) +
+            cc.error( ", error is: " ) + cc.warning( owaspUtils.extract_error_message( err ) ) +
             "\n" );
     }
     ++ idxAttempt;
@@ -222,7 +222,7 @@ async function get_web3_blockNumber( details, cntAttempts, w3, retValOnFail, thr
                 cc.error( "Failed call attempt " ) + cc.info( idxAttempt ) +
                 cc.error( " to " ) + cc.note( strFnName + "()" ) +
                 cc.error( " via " ) + cc.u( u ) +
-                cc.error( ", error is: " ) + cc.warning( err.toString() ) +
+                cc.error( ", error is: " ) + cc.warning( owaspUtils.extract_error_message( err ) ) +
                 "\n" );
         }
         ++ idxAttempt;
@@ -257,7 +257,7 @@ async function get_web3_transactionCount( details, cntAttempts, w3, address, par
             cc.error( "Failed call attempt " ) + cc.info( idxAttempt ) +
             cc.error( " to " ) + cc.note( strFnName + "()" ) +
             cc.error( " via " ) + cc.u( u ) +
-            cc.error( ", error is: " ) + cc.warning( err.toString() ) +
+            cc.error( ", error is: " ) + cc.warning( owaspUtils.extract_error_message( err ) ) +
             "\n" );
     }
     ++ idxAttempt;
@@ -287,7 +287,7 @@ async function get_web3_transactionCount( details, cntAttempts, w3, address, par
                 cc.error( "Failed call attempt " ) + cc.info( idxAttempt ) +
                 cc.error( " to " ) + cc.note( strFnName + "()" ) +
                 cc.error( " via " ) + cc.u( u ) +
-                cc.error( ", error is: " ) + cc.warning( err.toString() ) +
+                cc.error( ", error is: " ) + cc.warning( owaspUtils.extract_error_message( err ) ) +
                 "\n" );
         }
         ++ idxAttempt;
@@ -322,7 +322,7 @@ async function get_web3_transactionReceipt( details, cntAttempts, w3, txHash, re
             cc.error( "Failed call attempt " ) + cc.info( idxAttempt ) +
             cc.error( " to " ) + cc.note( strFnName + "()" ) +
             cc.error( " via " ) + cc.u( u ) +
-            cc.error( ", error is: " ) + cc.warning( err.toString() ) +
+            cc.error( ", error is: " ) + cc.warning( owaspUtils.extract_error_message( err ) ) +
             "\n" );
     }
     ++ idxAttempt;
@@ -352,7 +352,7 @@ async function get_web3_transactionReceipt( details, cntAttempts, w3, txHash, re
                 cc.error( "Failed call attempt " ) + cc.info( idxAttempt ) +
                 cc.error( " to " ) + cc.note( strFnName + "()" ) +
                 cc.error( " via " ) + cc.u( u ) +
-                cc.error( ", error is: " ) + cc.warning( err.toString() ) +
+                cc.error( ", error is: " ) + cc.warning( owaspUtils.extract_error_message( err ) ) +
                 "\n" );
         }
         ++ idxAttempt;
@@ -393,9 +393,9 @@ async function get_web3_pastEvents( details, w3, cntAttempts, joContract, strEve
             cc.error( " to " ) + cc.note( strFnName + "()" ) +
             cc.error( " via " ) + cc.u( u ) +
             cc.error( ", from block " ) + cc.warning( nBlockFrom ) + cc.error( ", to block " ) + cc.warning( nBlockTo ) +
-            cc.error( ", error is: " ) + cc.warning( err.toString() ) +
+            cc.error( ", error is: " ) + cc.warning( owaspUtils.extract_error_message( err ) ) +
             "\n" );
-        if( err.toString().indexOf( strErrorTextAboutNotExistingEvent ) >= 0 ) {
+        if( owaspUtils.extract_error_message( err ).indexOf( strErrorTextAboutNotExistingEvent ) >= 0 ) {
             details.write(
                 cc.error( "Did stopped calls to " ) + cc.note( strFnName + "()" ) +
                 cc.error( " because event " ) + cc.notice( strEventName ) +
@@ -439,9 +439,9 @@ async function get_web3_pastEvents( details, w3, cntAttempts, joContract, strEve
                 cc.error( " to " ) + cc.note( strFnName + "()" ) +
                 cc.error( " via " ) + cc.u( u ) +
                 cc.error( ", from block " ) + cc.warning( nBlockFrom ) + cc.error( ", to block " ) + cc.warning( nBlockTo ) +
-                cc.error( ", error is: " ) + cc.warning( err.toString() ) +
+                cc.error( ", error is: " ) + cc.warning( owaspUtils.extract_error_message( err ) ) +
                 "\n" );
-            if( err.toString().indexOf( strErrorTextAboutNotExistingEvent ) >= 0 ) {
+            if( owaspUtils.extract_error_message( err ).indexOf( strErrorTextAboutNotExistingEvent ) >= 0 ) {
                 details.write(
                     cc.error( "Did stopped calls to " ) + cc.note( strFnName + "()" ) +
                     cc.error( " because event " ) + cc.notice( strEventName ) +
@@ -552,7 +552,7 @@ async function get_web3_pastEventsIterative( details, w3, attempts, joContract,
                 cc.error( "Got scan error during interative scan of " ) +
                 cc.info( idxBlockSubRangeFrom ) + cc.error( "/" ) + cc.info( idxBlockSubRangeTo ) +
                 cc.error( " block sub-range in " ) + cc.info( nBlockFrom ) + cc.error( "/" ) + cc.info( nBlockTo ) +
-                cc.error( " block range, error is: " ) + cc.warning( err.toString() ) + "\n"
+                cc.error( " block range, error is: " ) + cc.warning( owaspUtils.extract_error_message( err ) ) + "\n"
             );
         }
         idxBlockSubRangeFrom = idxBlockSubRangeTo;
@@ -599,8 +599,18 @@ function save_transfer_success_all() {
     g_mapTransferErrorCategories = { }; // clear all transfer error categories, out of time frame
 }
 
-function get_last_transfer_errors() {
-    return JSON.parse( JSON.stringify( g_arrLastTransferErrors ) );
+function get_last_transfer_errors( isIncludeTextLog ) {
+    if( typeof isIncludeTextLog == "undefined" )
+        isIncludeTextLog = true;
+    const jarr = JSON.parse( JSON.stringify( g_arrLastTransferErrors ) );
+    if( ! isIncludeTextLog ) {
+        for( let i = 0; i < jarr.length; ++ i ) {
+            const jo = jarr[i];
+            if( "textLog" in jo )
+                delete jo.textLog;
+        }
+    } // if( ! isIncludeTextLog )
+    return jarr;
 }
 
 function get_last_error_categories() {
@@ -1056,8 +1066,8 @@ async function dry_run_call( details, w3, methodWithArguments, joAccount, strDRC
             details.write(
                 strLogPrefix + cc.fatal( "CRITICAL DRY RUN FAIL" ) + " " +
                 cc.error( " invoking the " ) + cc.info( strMethodName ) + cc.error( " method: " ) +
-                cc.warning( err.toString() ) + "\n" );
-            return "CRITICAL DRY RUN FAIL invoking the \"" + strMethodName + "\" method: " + err.toString();
+                cc.warning( owaspUtils.extract_error_message( err ) ) + "\n" );
+            return "CRITICAL DRY RUN FAIL invoking the \"" + strMethodName + "\" method: " + owaspUtils.extract_error_message( err );
         }
     }
     return null;
@@ -1238,7 +1248,7 @@ async function safe_sign_transaction_with_account( details, w3, tx, rawTx, joAcc
     const strPrefixLog = cc.debug( "(immediate log)" ) + " ";
     const sendingCnt = loopTmSendingCnt++;
     let strMsg =
-        cc.debug( "Sending transaction with account(" ) + cc.notice( "sending counter" ) + cc.debug( " is " ) +
+        cc.debug( "Signing(and later, sending) transaction with account(" ) + cc.notice( "sending counter" ) + cc.debug( " is " ) +
         cc.info( sendingCnt ) + cc.debug( "), raw TX object is " ) + cc.j( rawTx );
     details.write( strPrefixDetails + strMsg + "\n" );
     log.write( strPrefixLog + strMsg + "\n" );
@@ -1247,6 +1257,9 @@ async function safe_sign_transaction_with_account( details, w3, tx, rawTx, joAcc
         tx: null,
         txHashSent: null
     };
+    strMsg = cc.debug( "Signing(and later, sending) transaction with backend type: " ) + cc.bright( joSR.joACI.strType );
+    details.write( strPrefixDetails + strMsg + "\n" );
+    log.write( strPrefixLog + strMsg + "\n" );
     switch ( joSR.joACI.strType ) {
     case "tm": {
         /*
@@ -1269,6 +1282,8 @@ async function safe_sign_transaction_with_account( details, w3, tx, rawTx, joAcc
                 if( verbose_get() >= RV_VERBOSE.error )
                     log.write( s );
                 details.write( s );
+                if( joCall )
+                    await joCall.disconnect();
                 return;
             }
             const txAdjusted = JSON.parse( JSON.stringify( rawTx ) ); // tx // rawTx
@@ -1290,6 +1305,7 @@ async function safe_sign_transaction_with_account( details, w3, tx, rawTx, joAcc
                         if( verbose_get() >= RV_VERBOSE.error )
                             log.write( s );
                         details.write( s );
+                        await joCall.disconnect();
                         return;
                     }
                     details.write( cc.debug( "Transaction Manager sign-and-send result is: " ) + cc.j( joOut ) + "\n" );
@@ -1302,6 +1318,7 @@ async function safe_sign_transaction_with_account( details, w3, tx, rawTx, joAcc
                         details.write( s );
                         return;
                     }
+                    await joCall.disconnect();
                 } );
         } );
         await sleep( 5000 );
@@ -1325,26 +1342,37 @@ async function safe_sign_transaction_with_account( details, w3, tx, rawTx, joAcc
             redis = new Redis( joAccount.strTransactionManagerURL );
         const priority = joAccount.tm_priority || 5;
         try {
+            strMsg = cc.debug( "Will TM-ensure transaction " ) + cc.j( txAdjusted ) + cc.debug( "..." );
+            details.write( strPrefixDetails + strMsg + "\n" );
+            log.write( strPrefixLog + strMsg + "\n" );
             const [ tx_id, joReceipt ] = await tm_ensure_transaction( details, w3, priority, txAdjusted );
+            strMsg = cc.success( "Done TM-ensure transaction, got ID " ) + cc.notice( tx_id ) + cc.success( " and receipt " ) + cc.j( joReceipt );
+            details.write( strPrefixDetails + strMsg + "\n" );
+            log.write( strPrefixLog + strMsg + "\n" );
             joSR.txHashSent = "" + joReceipt.transactionHash;
             joSR.joReceipt = joReceipt;
             joSR.tm_tx_id = tx_id;
+            strMsg = cc.success( "Done, TX was signed with Transaction Manager" );
+            details.write( strPrefixDetails + strMsg + "\n" );
+            log.write( strPrefixLog + strMsg + "\n" );
         } catch ( err ) {
             strMsg =
                 cc.fatal( "BAD ERROR:" ) + " " +
                 cc.error( "TM - transaction was not sent, underlying error is: " ) +
-                cc.warning( err.toString() );
+                cc.warning( owaspUtils.extract_error_message( err ) );
             details.write( strPrefixDetails + strMsg + "\n" );
             log.write( strPrefixLog + strMsg + "\n" );
             // throw err;
         }
     } break;
     case "sgx": {
-        details.write(
-            cc.debug( "Will sign with SGX wallet, transaction is " ) + cc.j( tx ) +
-            cc.debug( ", raw transaction is " ) + cc.j( rawTx ) + "\n"
-            // + cc.debug( " using account " ) + cc.j( joAccount ) + "\n"
-        );
+        strMsg =
+            cc.debug( "Will sign with SGX Wallet, transaction is " ) + cc.j( tx ) +
+            cc.debug( ", raw transaction is " ) + cc.j( rawTx )
+            // + cc.debug( " using account " ) + cc.j( joAccount )
+        ;
+        details.write( strPrefixDetails + strMsg + "\n" );
+        log.write( strPrefixLog + strMsg + "\n" );
         let rpcCallOpts = null;
         if( "strPathSslKey" in joAccount && typeof joAccount.strPathSslKey == "string" && joAccount.strPathSslKey.length > 0 &&
             "strPathSslCert" in joAccount && typeof joAccount.strPathSslCert == "string" && joAccount.strPathSslCert.length > 0
@@ -1361,6 +1389,8 @@ async function safe_sign_transaction_with_account( details, w3, tx, rawTx, joAcc
                 if( verbose_get() >= RV_VERBOSE.error )
                     log.write( s );
                 details.write( s );
+                if( joCall )
+                    await joCall.disconnect();
                 return;
             }
             const msgHash = tx.hash( false );
@@ -1374,16 +1404,20 @@ async function safe_sign_transaction_with_account( details, w3, tx, rawTx, joAcc
                     "base": 16 // 10
                 }
             };
-            details.write( cc.debug( "Calling SGX to sign using ECDSA key with " ) + cc.info( joIn.method ) + cc.debug( "..." ) + "\n" );
+            strMsg = cc.debug( "Calling SGX to sign using ECDSA key with " ) + cc.info( joIn.method ) + cc.debug( "..." );
+            details.write( strPrefixDetails + strMsg + "\n" );
+            log.write( strPrefixLog + strMsg + "\n" );
             await joCall.call( joIn, async function( joIn, joOut, err ) {
                 if( err ) {
-                    const s = cc.fatal( "CRITICAL TRANSACTION SIGNING ERROR:" ) + cc.error( " JSON RPC call to SGX wallet failed, error: " ) + cc.warning( err ) + "\n";
-                    if( verbose_get() >= RV_VERBOSE.error )
-                        log.write( s );
-                    details.write( s );
+                    strMsg = cc.fatal( "CRITICAL TRANSACTION SIGNING ERROR:" ) + cc.error( " JSON RPC call to SGX wallet failed, error: " ) + cc.warning( err );
+                    details.write( strPrefixDetails + strMsg + "\n" );
+                    log.write( strPrefixLog + strMsg + "\n" );
+                    await joCall.disconnect();
                     return;
                 }
-                details.write( cc.debug( "SGX wallet ECDSA sign result is: " ) + cc.j( joOut ) + "\n" );
+                strMsg = cc.debug( "SGX wallet ECDSA sign result is: " ) + cc.j( joOut );
+                details.write( strPrefixDetails + strMsg + "\n" );
+                log.write( strPrefixLog + strMsg + "\n" );
                 const joNeededResult = {
                     // "v": Buffer.from( parseIntOrHex( joOut.result.signature_v ).toString( "hex" ), "utf8" ),
                     // "r": Buffer.from( "" + joOut.result.signature_r, "utf8" ),
@@ -1392,7 +1426,9 @@ async function safe_sign_transaction_with_account( details, w3, tx, rawTx, joAcc
                     "r": "" + joOut.result.signature_r,
                     "s": "" + joOut.result.signature_s
                 };
-                details.write( cc.debug( "Sign result to assign into transaction is: " ) + cc.j( joNeededResult ) + "\n" );
+                strMsg = cc.debug( "SGX Wallet sign result to assign into transaction is: " ) + cc.j( joNeededResult );
+                details.write( strPrefixDetails + strMsg + "\n" );
+                log.write( strPrefixLog + strMsg + "\n" );
                 //
                 // if( "_chainId" in tx && tx._chainId != null && tx._chainId != undefined )
                 //     tx.v += tx._chainId * 2 + 8;
@@ -1418,27 +1454,38 @@ async function safe_sign_transaction_with_account( details, w3, tx, rawTx, joAcc
                 tx.v = joNeededResult.v;
                 tx.r = joNeededResult.r;
                 tx.s = joNeededResult.s;
-                details.write( cc.debug( "Resulting adjusted transaction is: " ) + cc.j( tx ) + "\n" );
+                strMsg = cc.debug( "Resulting adjusted transaction is: " ) + cc.j( tx );
+                details.write( strPrefixDetails + strMsg + "\n" );
+                log.write( strPrefixLog + strMsg + "\n" );
+                await joCall.disconnect();
+                strMsg = cc.success( "Done, TX was signed with SGX Wallet" );
+                details.write( strPrefixDetails + strMsg + "\n" );
+                log.write( strPrefixLog + strMsg + "\n" );
             } );
         } );
         await sleep( 3000 );
     } break;
     case "direct": {
-        details.write(
+        strMsg =
             cc.debug( "Will sign with private key, transaction is " ) + cc.notice( JSON.stringify( tx ) ) +
-            cc.debug( ", raw transaction is " ) + cc.notice( JSON.stringify( rawTx ) ) + "\n"
-            // + cc.debug( " using account " ) + cc.j( joAccount ) + "\n"
-        );
+            cc.debug( ", raw transaction is " ) + cc.notice( JSON.stringify( rawTx ) )
+            // + cc.debug( " using account " ) + cc.j( joAccount )
+        ;
+        details.write( strPrefixDetails + strMsg + "\n" );
+        log.write( strPrefixLog + strMsg + "\n" );
         const key = Buffer.from( joAccount.privateKey, "hex" ); // convert private key to buffer
         tx.sign( key ); // arg is privateKey as buffer
+        strMsg = cc.success( "Done, TX was signed with private key" );
+        details.write( strPrefixDetails + strMsg + "\n" );
+        log.write( strPrefixLog + strMsg + "\n" );
     } break;
     default: {
-        const s = cc.fatal( "CRITICAL TRANSACTION SIGNING ERROR:" ) +
-            cc.error( " bad credentials information specified, no explicit SGX and no explicit private key found" ) +
+        strMsg = cc.fatal( "CRITICAL TRANSACTION SIGNING ERROR:" ) +
+            cc.error( " bad credentials information specified, no explicit SGX and no explicit private key found" )
             // + cc.error( ", account is: " ) + cc.j( joAccount )
-            "\n";
-        details.write( s );
-        log.write( s );
+        ;
+        details.write( strPrefixDetails + strMsg + "\n" );
+        log.write( strPrefixLog + strMsg + "\n" );
         if( isExitIfEmpty ) {
             details.exposeDetailsTo( log, "safe_sign_transaction_with_account", false );
             details.close();
@@ -1465,7 +1512,7 @@ async function safe_send_signed_transaction( details, w3, serializedTx, strActio
     details.write( strPrefixDetails + strMsg + "\n" );
     log.write( strPrefixLog + strMsg + "\n" );
     const strTX = "0x" + serializedTx.toString( "hex" ); // strTX is string starting from "0x"
-    details.write( strLogPrefix + cc.debug( "....signed raw TX is " ) + cc.j( strTX ) + "\n" );
+    details.write( strLogPrefix + cc.debug( "....signed raw TX is " ) + cc.attention( strTX ) + "\n" );
     let joReceipt = null;
     let bHaveReceipt = false;
     try {
@@ -5323,7 +5370,6 @@ async function do_transfer(
                     savedBlockNumberForOptimizations: joValues.savedBlockNumberForOptimizations
                 };
                 jarrMessages.push( joMessage );
-
             } // for( let idxInBlock = 0; nIdxCurrentMsg < nOutMsgCnt && idxInBlock < nTransactionsCountInBlock; ++ nIdxCurrentMsg, ++ idxInBlock, ++cntAccumulatedForBlock )
             if( cntAccumulatedForBlock == 0 )
                 break;
@@ -5506,14 +5552,15 @@ async function do_transfer(
                                 cc.debug( " using URL " ) + cc.info( jo_node.http_endpoint_ip ) +
                                 cc.debug( "..." ) + "\n" );
                             try {
-                                const w3_node = getWeb3FromURL( jo_node.http_endpoint_ip );
+                                const w3_node = getWeb3FromURL( jo_node.http_endpoint_ip, details );
                                 const jo_message_proxy_node = new w3_node.eth.Contract( imaState.joAbiPublishResult_s_chain.message_proxy_chain_abi, imaState.joAbiPublishResult_s_chain.message_proxy_chain_address );
+                                const strEventName = "OutgoingMessage";
                                 const node_r = await get_web3_pastEventsProgressive(
                                     details,
                                     w3_node,
                                     10,
                                     jo_message_proxy_node,
-                                    "OutgoingMessage",
+                                    strEventName,
                                     joMessage.savedBlockNumberForOptimizations, // 0, // nBlockFrom
                                     joMessage.savedBlockNumberForOptimizations, // "latest", // nBlockTo
                                     {
@@ -5523,7 +5570,7 @@ async function do_transfer(
                                 );
                                 const cntEvents = node_r.length;
                                 details.write( strLogPrefix +
-                                    cc.debug( "Got " ) + cc.info( cntEvents ) + cc.debug( " events on node " ) +
+                                    cc.debug( "Got " ) + cc.info( cntEvents ) + cc.debug( " event(s) (" ) + cc.info( strEventName ) + cc.debug( ") on node " ) +
                                     cc.info( jo_node.name ) + cc.debug( " with data: " ) + cc.j( node_r ) + "\n" );
                                 for( let idxEvent = 0; idxEvent < cntEvents; ++ idxEvent ) {
                                     const joEvent = node_r[idxEvent];
@@ -5540,7 +5587,7 @@ async function do_transfer(
                                 ++ cntFailedNodes;
                                 const strError = strLogPrefix + cc.fatal( strDirection + " message analysis error:" ) + " " +
                                     cc.error( "Failed to scan events on node " ) + cc.info( jo_node.name ) +
-                                    cc.error( ", error is: " ) + cc.warning( err.toString() ) +
+                                    cc.error( ", error is: " ) + cc.warning( owaspUtils.extract_error_message( err ) ) +
                                     cc.error( ", detailed node description is: " ) + cc.j( jo_node ) +
                                     "\n";
                                 details.write( strError );
@@ -5583,7 +5630,7 @@ async function do_transfer(
                             cc.error( "Failed to process events for " ) + cc.sunny( strDirection ) + cc.error( " message " ) +
                             cc.info( idxMessage + 1 ) + cc.error( " on node " ) + cc.info( jo_node.name ) +
                             cc.success( " using URL " ) + cc.info( jo_node.http_endpoint_ip ) +
-                            cc.debug( ", error is: " ) + cc.warning( err.toString() ) + "\n";
+                            cc.debug( ", error is: " ) + cc.warning( owaspUtils.extract_error_message( err ) ) + "\n";
                         details.write( strError );
                         if( verbose_get() >= RV_VERBOSE.fatal )
                             log.write( strError );
@@ -5915,7 +5962,7 @@ async function do_s2s_all( // s-chain --> s-chain
     for( let idxSChain = 0; idxSChain < cntSChains; ++ idxSChain ) {
         const jo_schain = arr_schains_cached[idxSChain];
         const url_src = skale_observer.pick_random_schain_w3_url( jo_schain );
-        const w3_src = getWeb3FromURL( url_src );
+        const w3_src = getWeb3FromURL( url_src, log );
         const joAccountSrc = joAccountDst; // ???
         const chain_id_src = "" + jo_schain.data.name;
         const cid_src = "" + jo_schain.data.computed.chainId;
diff --git a/npms/skale-ima/package.json b/npms/skale-ima/package.json
index 05aacded4..f425aa5d4 100644
--- a/npms/skale-ima/package.json
+++ b/npms/skale-ima/package.json
@@ -33,7 +33,7 @@
     "dependencies": {
         "ethereumjs-tx": "1.3.7",
         "ethereumjs-wallet": "^1.0.2",
-        "ethereumjs-util": "^7.1.3",
+        "ethereumjs-util": "^7.1.4",
         "web3": "^1.6.1",
         "ioredis": "^4.28.2"
     },
diff --git a/npms/skale-ima/yarn.lock b/npms/skale-ima/yarn.lock
index 053271b48..677be220b 100644
--- a/npms/skale-ima/yarn.lock
+++ b/npms/skale-ima/yarn.lock
@@ -1087,6 +1087,17 @@ ethereumjs-util@^7.1.0, ethereumjs-util@^7.1.2, ethereumjs-util@^7.1.3:
     ethereum-cryptography "^0.1.3"
     rlp "^2.2.4"
 
+ethereumjs-util@^7.1.4:
+  version "7.1.5"
+  resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-7.1.5.tgz#9ecf04861e4fbbeed7465ece5f23317ad1129181"
+  integrity sha512-SDl5kKrQAudFBUe5OJM9Ac6WmMyYmXX/6sTmLZ3ffG2eY6ZIGBes3pEDxNN6V72WyOw4CPD5RomKdsa8DAAwLg==
+  dependencies:
+    "@types/bn.js" "^5.1.0"
+    bn.js "^5.1.2"
+    create-hash "^1.1.2"
+    ethereum-cryptography "^0.1.3"
+    rlp "^2.2.4"
+
 ethereumjs-wallet@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/ethereumjs-wallet/-/ethereumjs-wallet-1.0.2.tgz#2c000504b4c71e8f3782dabe1113d192522e99b6"
diff --git a/npms/skale-observer/README.md b/npms/skale-observer/README.md
index 19c60cc3a..8d3b81db8 100644
--- a/npms/skale-observer/README.md
+++ b/npms/skale-observer/README.md
@@ -12,7 +12,7 @@ First SKALE network scan is performed by **SNB** on IMA startup. Next network de
 
 ## Implementation details
 
-The `SchainsInternal.numberOfSchains` contract call returns number of created S-Chains to load from **SKALE Mananger**. For each of S-Chains we need to get its hash by index what is done `SchainsInternal.schainsAtSystem` contract call. Then contract call to `SchainsInternal.schains` returns basic S-Chain description by hash. Obtained basic S-Chain description does not describe nodes and they must be loaded via set of contract calls. The `SchainsInternal.getNodesInGroup` contract call returns array of node identifiers for all S-Chain nodes. The `Nodes.nodes` contract call returns node description by node id. Returned node description includes IP adderess, domain name and, base port of a node, maintanance state flag. Then call to `SchainsInternal.getSchainIdsForNode` contract call allows to find effective node base port and compute per-protocol ports (`http`, `https`, `ws`, `wss`).
+The `SchainsInternal.numberOfSchains` contract call returns number of created S-Chains to load from **SKALE Mananger**. For each of S-Chains we need to get its hash by index what is done `SchainsInternal.schainsAtSystem` contract call. Then contract call to `SchainsInternal.schains` returns basic S-Chain description by hash. Obtained basic S-Chain description does not describe nodes and they must be loaded via set of contract calls. The `SchainsInternal.getNodesInGroup` contract call returns array of node identifiers for all S-Chain nodes. The `Nodes.nodes` contract call returns node description by node id. Returned node description includes IP adderess, domain name and, base port of a node, maintanance state flag. Then call to `SchainsInternal.getSchainHashesForNode` contract call allows to find effective node base port and compute per-protocol ports (`http`, `https`, `ws`, `wss`).
 
 Cache of S-Chain descriptions is result of download process described above. When new S-Chain descriptions are downloaded, they replace old ones. By default this is performed once in an hour.
 
diff --git a/npms/skale-observer/observer.js b/npms/skale-observer/observer.js
index d22107216..59e6cc27a 100644
--- a/npms/skale-observer/observer.js
+++ b/npms/skale-observer/observer.js
@@ -24,12 +24,58 @@
  * @copyright SKALE Labs 2019-Present
  */
 
+const network_layer = require( "../skale-cool-socket/socket.js" );
+const { Worker } = require( "worker_threads" );
+const path = require( "path" );
+
 const owaspUtils = require( "../skale-owasp/owasp-util.js" );
-const { getWeb3FromURL } = require( "../../agent/cli.js" );
 const cc = owaspUtils.cc;
 
+const w3mod = require( "web3" );
+// const ethereumjs_tx = require( "ethereumjs-tx" );
+// const ethereumjs_wallet = require( "ethereumjs-wallet" );
+// const ethereumjs_util = require( "ethereumjs-util" );
+
 const PORTS_PER_SCHAIN = 64;
 
+function getWeb3FromURL( strURL, log ) {
+    let w3 = null;
+    log = log || { write: console.log };
+    try {
+        const u = cc.safeURL( strURL );
+        const strProtocol = u.protocol.trim().toLowerCase().replace( ":", "" ).replace( "/", "" );
+        if( strProtocol == "ws" || strProtocol == "wss" ) {
+            const w3ws = new w3mod.providers.WebsocketProvider( strURL, {
+                // see: https://github.com/ChainSafe/web3.js/tree/1.x/packages/web3-providers-ws#usage
+                clientConfig: {
+                    // // if requests are large:
+                    // maxReceivedFrameSize: 100000000,   // bytes - default: 1MiB
+                    // maxReceivedMessageSize: 100000000, // bytes - default: 8MiB
+                    // keep a connection alive
+                    keepalive: true,
+                    keepaliveInterval: 200000 // ms
+                },
+                reconnect: { // enable auto reconnection
+                    auto: true,
+                    delay: 5000, // ms
+                    maxAttempts: 10000000, // 10 million times
+                    onTimeout: false
+                }
+            } );
+            w3 = new w3mod( w3ws );
+        } else {
+            const w3http = new w3mod.providers.HttpProvider( strURL );
+            w3 = new w3mod( w3http );
+        }
+    } catch ( err ) {
+        log.write( cc.fatal( "CRITICAL ERROR:" ) + cc.error( " Failed to create " ) +
+            cc.attention( "Web3" ) + cc.error( " connection to " ) + cc.info( strURL ) +
+            cc.error( ": " ) + cc.warning( owaspUtils.extract_error_message( err ) ) );
+        w3 = null;
+    }
+    return w3;
+}
+
 function get_schain_index_in_node( schain_id, schains_ids_on_node ) {
     let i = 0;
     for( const schain_id_on_node of schains_ids_on_node ) {
@@ -55,6 +101,7 @@ function compose_endpoints( jo_schain, node_dict, endpoint_type ) {
     node_dict["ws_endpoint_" + endpoint_type] = "ws://" + node_dict[endpoint_type] + ":" + jo_schain.data.computed.ports.wsRpcPort;
     node_dict["wss_endpoint_" + endpoint_type] = "wss://" + node_dict[endpoint_type] + ":" + jo_schain.data.computed.ports.wssRpcPort;
     node_dict["info_http_endpoint_" + endpoint_type] = "http://" + node_dict[endpoint_type] + ":" + jo_schain.data.computed.ports.infoHttpRpcPort;
+    node_dict["ima_agent_endpoint_" + endpoint_type] = "http://" + node_dict[endpoint_type] + ":" + jo_schain.data.computed.ports.imaAgentRpcPort;
 }
 
 const SkaledPorts = {
@@ -67,7 +114,8 @@ const SkaledPorts = {
     IMA_MONITORING: 6,
     WSS_JSON: 7,
     HTTPS_JSON: 8,
-    INFO_HTTP_JSON: 9
+    INFO_HTTP_JSON: 9,
+    IMA_AGENT_JSON: 10
 };
 
 function calc_ports( jo_schain, schain_base_port ) {
@@ -77,7 +125,8 @@ function calc_ports( jo_schain, schain_base_port ) {
         httpsRpcPort: schain_base_port + SkaledPorts.HTTPS_JSON,
         wsRpcPort: schain_base_port + SkaledPorts.WS_JSON,
         wssRpcPort: schain_base_port + SkaledPorts.WSS_JSON,
-        infoHttpRpcPort: schain_base_port + SkaledPorts.INFO_HTTP_JSON
+        infoHttpRpcPort: schain_base_port + SkaledPorts.INFO_HTTP_JSON,
+        imaAgentRpcPort: schain_base_port + SkaledPorts.IMA_AGENT_JSON
     };
 }
 
@@ -137,12 +186,12 @@ function remove_schain_desc_data_num_keys( jo_schain ) {
     }
 }
 
-async function load_schain( w3, addressFrom, idxSChain, cntSChains, opts ) {
+async function load_schain( w3, addressFrom, idxSChain, hash, cntSChains, opts ) {
     if( ! opts.imaState )
         throw new Error( "Cannot load S-Chain description in observer, no imaState is provided" );
     if( opts && opts.details )
         opts.details.write( cc.debug( "Loading S-Chain " ) + cc.notice( "#" ) + cc.info( idxSChain + 1 ) + cc.debug( " of " ) + cc.info( cntSChains ) + cc.debug( "..." ) + "\n" );
-    const hash = await opts.imaState.jo_schains_internal.methods.schainsAtSystem( idxSChain ).call( { from: addressFrom } );
+    hash = hash || await opts.imaState.jo_schains_internal.methods.schainsAtSystem( idxSChain ).call( { from: addressFrom } );
     if( opts && opts.details )
         opts.details.write( cc.debug( "    Hash " ) + cc.attention( hash ) + "\n" );
     if( opts && opts.bStopNeeded )
@@ -172,7 +221,7 @@ async function load_schains( w3, addressFrom, opts ) {
     for( let idxSChain = 0; idxSChain < cntSChains; ++ idxSChain ) {
         if( opts && opts.bStopNeeded )
             break;
-        const jo_schain = await load_schain( w3, addressFrom, idxSChain, cntSChains, opts );
+        const jo_schain = await load_schain( w3, addressFrom, idxSChain, null, cntSChains, opts );
         if( ! jo_schain )
             break;
         arr_schains.push( jo_schain );
@@ -186,6 +235,87 @@ async function load_schains( w3, addressFrom, opts ) {
     return arr_schains;
 }
 
+async function load_cached_schains_simplified( w3, addressFrom, opts ) {
+    if( ! opts.imaState )
+        throw new Error( "Cannot load S-Chains in observer, no imaState is provided" );
+    if( opts && opts.details )
+        opts.details.write( cc.debug( "Will request all S-Chain(s) hashes..." ) + "\n" );
+    const arrSChainHashes = await opts.imaState.jo_schains_internal.methods.getSchains().call( { from: addressFrom } );
+    const cntSChains = arrSChainHashes.length;
+    if( opts && opts.details )
+        opts.details.write( cc.debug( "Have all " ) + cc.info( cntSChains ) + cc.debug( " S-Chain(s) hashes: " ) + cc.j( arrSChainHashes ) + "\n" );
+    const arr_schains = [];
+    for( let idxSChain = 0; idxSChain < cntSChains; ++ idxSChain ) {
+        if( opts && opts.bStopNeeded )
+            break;
+        const strSChainHash = arrSChainHashes[idxSChain];
+        const strSChainName = await opts.imaState.jo_schains_internal.methods.getSchainName( strSChainHash ).call( { from: addressFrom } );
+        if( opts && opts.details )
+            opts.details.write( cc.debug( "S-Chain " ) + cc.notice( idxSChain ) + cc.debug( " hash " ) + cc.notice( strSChainHash ) + cc.debug( " corresponds to S-Chain name " ) + cc.notice( strSChainName ) + "\n" );
+        if( opts && opts.bStopNeeded )
+            break;
+        const jo_schain = await load_schain( w3, addressFrom, idxSChain, strSChainHash, cntSChains, opts );
+        if( ! jo_schain )
+            break;
+        arr_schains.push( jo_schain );
+    }
+    return arr_schains;
+}
+
+async function load_schains_connected_only( w3_main_net, w3_s_chain, strChainNameConnectedTo, addressFrom, opts ) {
+    if( ! opts.imaState )
+        throw new Error( "Cannot load S-Chains in observer, no imaState is provided" );
+    if( opts && opts.details )
+        opts.details.write( cc.debug( "Will request all S-Chain(s) hashes..." ) + "\n" );
+    const arrSChainHashes = await opts.imaState.jo_schains_internal.methods.getSchains().call( { from: addressFrom } );
+    const cntSChains = arrSChainHashes.length;
+    if( opts && opts.details )
+        opts.details.write( cc.debug( "Have all " ) + cc.info( cntSChains ) + cc.debug( " S-Chain(s) hashes: " ) + cc.j( arrSChainHashes ) + "\n" );
+    const jo_message_proxy_s_chain =
+        new w3_s_chain.eth.Contract(
+            opts.imaState.joAbiPublishResult_s_chain.message_proxy_chain_abi,
+            opts.imaState.joAbiPublishResult_s_chain.message_proxy_chain_address
+        );
+    const arr_schains = [];
+    for( let idxSChain = 0; idxSChain < cntSChains; ++ idxSChain ) {
+        try {
+            if( opts && opts.bStopNeeded )
+                break;
+            const strSChainHash = arrSChainHashes[idxSChain];
+            const strSChainName = await opts.imaState.jo_schains_internal.methods.getSchainName( strSChainHash ).call( { from: addressFrom } );
+            if( opts && opts.details )
+                opts.details.write( cc.debug( "S-Chain " ) + cc.notice( idxSChain ) + cc.debug( " hash " ) + cc.notice( strSChainHash ) + cc.debug( " corresponds to S-Chain name " ) + cc.notice( strSChainName ) + "\n" );
+            if( opts && opts.bStopNeeded )
+                break;
+            //
+            if( strChainNameConnectedTo == strSChainName ) {
+                if( opts && opts.details )
+                    opts.details.write( cc.debug( "Skip this S-Chain " ) + cc.info( strSChainName ) + cc.debug( " connected status check" ) + "\n" );
+                continue;
+            }
+            if( opts && opts.details ) {
+                opts.details.write(
+                    cc.debug( "Querying connected status between S-Chain " ) + cc.info( strSChainName ) + cc.debug( " and S-Chain " ) +
+                    cc.info( strChainNameConnectedTo ) + cc.debug( "..." ) + "\n" );
+            }
+            const isConnected = await jo_message_proxy_s_chain.methods.isConnectedChain( strSChainName ).call( { from: addressFrom } );
+            if( opts && opts.details )
+                opts.details.write( cc.debug( "Got S-Chain " ) + cc.info( strSChainName ) + cc.debug( " connected status: " ) + cc.yn( isConnected ) + "\n" );
+            if( ! isConnected )
+                continue;
+            const jo_schain = await load_schain( w3_main_net, addressFrom, idxSChain, strSChainHash, cntSChains, opts );
+            if( ! jo_schain )
+                break;
+            jo_schain.isConnected = true;
+            arr_schains.push( jo_schain );
+        } catch ( err ) {
+            if( opts && opts.details )
+                opts.details.write( cc.error( "Got error: " ) + cc.warning( owaspUtils.extract_error_message( err ) ) + "\n" );
+        }
+    }
+    return arr_schains;
+}
+
 async function check_connected_schains( strChainNameConnectedTo, arr_schains, addressFrom, opts ) {
     if( ! opts.imaState )
         throw new Error( "Cannot load S-Chains in observer, no imaState is provided" );
@@ -198,7 +328,6 @@ async function check_connected_schains( strChainNameConnectedTo, arr_schains, ad
         if( jo_schain.data.name == strChainNameConnectedTo )
             continue;
         try {
-            //jo_schain.isConnected = await opts.imaState.jo_message_proxy_s_chain.methods.isConnectedChain( strChainNameConnectedTo ).call( { from: addressFrom } );
             const url = pick_random_schain_w3_url( jo_schain );
             if( opts && opts.details ) {
                 opts.details.write(
@@ -206,8 +335,8 @@ async function check_connected_schains( strChainNameConnectedTo, arr_schains, ad
                     cc.info( jo_schain.data.name ) + cc.debug( " whether it's connected to S-Chain " ) +
                     cc.info( strChainNameConnectedTo ) + cc.debug( "..." ) + "\n" );
             }
-            const w3 = getWeb3FromURL( url );
-            const jo_message_proxy_s_chain = new w3.eth.Contract( imaState.joAbiPublishResult_s_chain.message_proxy_chain_abi, imaState.joAbiPublishResult_s_chain.message_proxy_chain_address );
+            const w3 = getWeb3FromURL( url, opts.details );
+            const jo_message_proxy_s_chain = new w3.eth.Contract( opts.imaState.joAbiPublishResult_s_chain.message_proxy_chain_abi, opts.imaState.joAbiPublishResult_s_chain.message_proxy_chain_address );
             jo_schain.isConnected = await jo_message_proxy_s_chain.methods.isConnectedChain( strChainNameConnectedTo ).call( { from: addressFrom } );
             if( opts && opts.details ) {
                 opts.details.write(
@@ -215,7 +344,7 @@ async function check_connected_schains( strChainNameConnectedTo, arr_schains, ad
             }
         } catch ( err ) {
             if( opts && opts.details )
-                opts.details.write( cc.error( "Got error: " ) + cc.warning( err.toString() ) + "\n" );
+                opts.details.write( cc.error( "Got error: " ) + cc.warning( owaspUtils.extract_error_message( err ) ) + "\n" );
         }
     }
     return arr_schains;
@@ -298,40 +427,41 @@ function merge_schains_array_from_to( arrSrc, arrDst, arrNew, arrOld, opts ) {
         opts.details.write( cc.success( "Finally, have " ) + cc.info( arrDst.length ) + cc.success( " S-Chain(s)" ) + "\n" );
 }
 
-let g_arr_schains_cached = null;
+let g_arr_schains_cached = [];
 
-async function cache_schains( strChainNameConnectedTo, w3, addressFrom, opts ) {
+async function cache_schains( strChainNameConnectedTo, w3_main_net, w3_s_chain, addressFrom, opts ) {
     let strError = null;
     try {
-        const arr_schains = await load_schains( w3, addressFrom, opts );
+        let arr_schains = [];
         if( strChainNameConnectedTo && ( typeof strChainNameConnectedTo == "string" ) && strChainNameConnectedTo.length > 0 ) {
-            await check_connected_schains(
+            arr_schains = await load_schains_connected_only(
+                w3_main_net,
+                w3_s_chain,
                 strChainNameConnectedTo,
-                arr_schains,
                 addressFrom,
                 opts
             );
-            g_arr_schains_cached = await filter_schains_marked_as_connected(
-                arr_schains,
-                opts
-            );
         } else
-            g_arr_schains_cached = arr_schains;
+            arr_schains = await load_schains( w3_main_net, addressFrom, opts );
+
+        g_arr_schains_cached = arr_schains;
+
         if( opts && opts.details ) {
             opts.details.write(
-                cc.debug( "Connected " ) + cc.attention( "S-Chains" ) + cc.debug( " cache was updated: " ) +
+                cc.debug( "Connected " ) + cc.attention( "S-Chains" ) + cc.debug( " cache was updated in this thread: " ) +
                 cc.j( g_arr_schains_cached ) + "\n" );
         }
         if( opts.fn_chache_changed )
             opts.fn_chache_changed( g_arr_schains_cached, null ); // null - no error
     } catch ( err ) {
-        strError = err.toString();
+        strError = owaspUtils.extract_error_message( err );
         if( ! strError )
             strError = "unknown exception during S-Chains download";
         if( opts.fn_chache_changed )
             opts.fn_chache_changed( g_arr_schains_cached, strError );
         if( opts && opts.details )
             opts.details.write( cc.fatal( "ERROR:" ) + cc.error( " Failed to cache: " ) + cc.error( err ) + "\n" );
+
     }
     return strError; // null on success
 }
@@ -340,33 +470,123 @@ function get_last_cached_schains() {
     return JSON.parse( JSON.stringify( g_arr_schains_cached ) );
 }
 
-let g_intervalPeriodicSchainsCaching = null;
-let g_bIsPeriodicCachingStepInProgress = false;
-
-async function periodic_caching_start( strChainNameConnectedTo, w3, addressFrom, opts ) {
-    await periodic_caching_stop();
-    if( ! ( "secondsToReDiscoverSkaleNetwork" in opts ) )
-        return false;
-    const secondsToReDiscoverSkaleNetwork = parseInt( opts.secondsToReDiscoverSkaleNetwork );
-    if( secondsToReDiscoverSkaleNetwork <= 0 )
-        return false;
-    g_intervalPeriodicSchainsCaching = setInterval( async function() {
-        if( g_bIsPeriodicCachingStepInProgress )
+const impl_sleep = ( milliseconds ) => { return new Promise( resolve => setTimeout( resolve, milliseconds ) ); };
+
+let g_worker = null;
+let g_client = null;
+
+async function ensure_have_worker( opts ) {
+    if( g_worker )
+        return g_worker;
+    const url = "skale_observer_worker_server";
+    g_worker = new Worker( path.join( __dirname, "observer_worker.js" ) );
+    // console.log( "Will connect to " + url );
+    g_worker.on( "message", jo => {
+        if( network_layer.out_of_worker_apis.on_message( g_worker, jo ) )
             return;
-        g_bIsPeriodicCachingStepInProgress = true;
-        // const strError =
-        await cache_schains( strChainNameConnectedTo, w3, addressFrom, opts );
-        g_bIsPeriodicCachingStepInProgress = false;
-    }, secondsToReDiscoverSkaleNetwork * 1000 );
-    return true;
+    } );
+    g_client = new network_layer.OutOfWorkerSocketClientPipe( url, g_worker );
+    g_client.on( "message", function( eventData ) {
+        const joMessage = eventData.message;
+        // console.log( "CLIENT <<<", JSON.stringify( joMessage ) );
+        switch ( joMessage.method ) {
+        case "periodic_caching_do_now":
+            g_arr_schains_cached = joMessage.message;
+            if( opts && opts.details ) {
+                opts.details.write(
+                    cc.debug( "Connected " ) + cc.attention( "S-Chains" ) +
+                    cc.debug( " cache was updated using data arrived from SNB worker: " ) +
+                    cc.j( g_arr_schains_cached ) + "\n" );
+            }
+            break;
+        case "log":
+            log.write( cc.attention( "SNB WORKER" ) + " " + joMessage.message );
+            break;
+        } // switch ( joMessage.method )
+    } );
+    await impl_sleep( 1000 );
+    const jo = {
+        method: "init",
+        message: {
+            opts: {
+                imaState: {
+                    "joAbiPublishResult_skale_manager": opts.imaState.joAbiPublishResult_skale_manager,
+                    "joAbiPublishResult_main_net": opts.imaState.joAbiPublishResult_main_net,
+                    "joAbiPublishResult_s_chain": opts.imaState.joAbiPublishResult_s_chain,
+                    "bHaveSkaleManagerABI": opts.imaState.bHaveSkaleManagerABI,
+                    "bHaveImaAbiMainNet": opts.imaState.bHaveImaAbiMainNet,
+                    "bNoWaitSChainStarted": opts.imaState.bNoWaitSChainStarted,
+                    "nMaxWaitSChainAttempts": opts.imaState.nMaxWaitSChainAttempts,
+                    "strURL_main_net": opts.imaState.strURL_main_net,
+                    "strChainName_main_net": opts.imaState.strChainName_main_net,
+                    "cid_main_net": opts.imaState.cid_main_net,
+                    "strURL_s_chain": opts.imaState.strURL_s_chain,
+                    "strChainName_s_chain": opts.imaState.strChainName_s_chain,
+                    "cid_s_chain": opts.imaState.cid_s_chain,
+                    "nNodeNumber": opts.imaState.nNodeNumber, // S-Chain node number(zero based)
+                    "nNodesCount": opts.imaState.nNodesCount,
+                    "nTimeFrameSeconds": opts.imaState.nTimeFrameSeconds, // 0-disable, 60-recommended
+                    "nNextFrameGap": opts.imaState.nNextFrameGap,
+                    "joAccount_main_net": {
+                        "privateKey": opts.imaState.joAccount_main_net.privateKey,
+                        // "address": IMA.owaspUtils.fn_address_impl_,
+                        "strTransactionManagerURL": opts.imaState.joAccount_main_net.strTransactionManagerURL,
+                        "tm_priority": opts.imaState.joAccount_main_net.tm_priority,
+                        "strSgxURL": opts.imaState.joAccount_main_net.strSgxURL,
+                        "strSgxKeyName": opts.imaState.joAccount_main_net.strSgxKeyName,
+                        "strPathSslKey": opts.imaState.joAccount_main_net.strPathSslKey,
+                        "strPathSslCert": opts.imaState.joAccount_main_net.strPathSslCert,
+                        "strBlsKeyName": opts.imaState.joAccount_main_net.strBlsKeyName
+                    },
+                    "joAccount_s_chain": {
+                        "privateKey": opts.imaState.joAccount_s_chain.privateKey,
+                        // "address": IMA.owaspUtils.fn_address_impl_,
+                        "strTransactionManagerURL": opts.imaState.joAccount_s_chain.strTransactionManagerURL,
+                        "tm_priority": opts.imaState.joAccount_s_chain.tm_priority,
+                        "strSgxURL": opts.imaState.joAccount_s_chain.strSgxURL,
+                        "strSgxKeyName": opts.imaState.joAccount_s_chain.strSgxKeyName,
+                        "strPathSslKey": opts.imaState.joAccount_s_chain.strPathSslKey,
+                        "strPathSslCert": opts.imaState.joAccount_s_chain.strPathSslCert,
+                        "strBlsKeyName": opts.imaState.joAccount_s_chain.strBlsKeyName
+                    },
+                    // "tc_main_net": IMA.tc_main_net,
+                    // "tc_s_chain": IMA.tc_s_chain,
+                    // "doEnableDryRun": function( isEnable ) { return IMA.dry_run_enable( isEnable ); },
+                    // "doIgnoreDryRun": function( isIgnore ) { return IMA.dry_run_ignore( isIgnore ); },
+                    "joSChainDiscovery": {
+                        "isSilentReDiscovery": opts.imaState.joSChainDiscovery.isSilentReDiscovery,
+                        "repeatIntervalMilliseconds": opts.imaState.joSChainDiscovery.repeatIntervalMilliseconds // zero to disable (for debugging only)
+                    }
+                }
+            },
+            "cc": {
+                "isEnabled": cc.isEnabled()
+            }
+        }
+    };
+    g_client.send( jo );
+}
+
+async function periodic_caching_start( strChainNameConnectedTo, w3_main_net, w3_s_chain, addressFrom, opts ) {
+    await ensure_have_worker( opts );
+    const jo = {
+        method: "periodic_caching_start",
+        message: {
+            secondsToReDiscoverSkaleNetwork: parseInt( opts.secondsToReDiscoverSkaleNetwork ),
+            strChainNameConnectedTo: strChainNameConnectedTo,
+            addressFrom: addressFrom
+        }
+    };
+    g_client.send( jo );
 }
 async function periodic_caching_stop() {
-    if( ! g_intervalPeriodicSchainsCaching )
-        return false;
-    clearInterval( g_intervalPeriodicSchainsCaching );
-    g_intervalPeriodicSchainsCaching = null;
-    g_bIsPeriodicCachingStepInProgress = false;
-    return true;
+    await ensure_have_worker( opts );
+    const jo = {
+        method: "periodic_caching_stop",
+        message: {
+        }
+    };
+    g_client.send( jo );
 }
 
 function pick_random_schain_node_index( jo_schain ) {
@@ -391,7 +611,9 @@ async function discover_chain_id( strURL ) {
     const rpcCallOpts = null;
     await rpcCall.create( strURL, rpcCallOpts, async function( joCall, err ) {
         if( err ) {
-            //ret = "Failed to create RPC (" + strURL + ") call: " + err.toString();
+            //ret = "Failed to create RPC (" + strURL + ") call: " + owaspUtils.extract_error_message( err );
+            if( joCall )
+                await joCall.disconnect();
             return;
         }
         await joCall.call( {
@@ -399,24 +621,31 @@ async function discover_chain_id( strURL ) {
             "params": []
         }, async function( joIn, joOut, err ) {
             if( err ) {
-                //ret = "Failed to query RPC (" + strURL + ") for chain ID: " + err.toString();
+                //ret = "Failed to query RPC (" + strURL + ") for chain ID: " + owaspUtils.extract_error_message( err );
+                await joCall.disconnect();
                 return;
             }
             if( ! ( "result" in joOut && joOut.result ) ) {
                 //ret = "Failed to query RPC (" + strURL + ") for chain ID, got bad result: " + JSON.stringify( joOut );
+                await joCall.disconnect();
                 return;
             }
             ret = joOut.result;
+            await joCall.disconnect();
         } ); // joCall.call ...
     } ); // rpcCall.create ...
     return ret;
 }
 
+module.exports.w3mod = w3mod;
+module.exports.getWeb3FromURL = getWeb3FromURL;
 module.exports.owaspUtils = owaspUtils;
 module.exports.cc = cc;
 module.exports.get_schains_count = get_schains_count;
 module.exports.load_schain = load_schain;
 module.exports.load_schains = load_schains;
+module.exports.load_cached_schains_simplified = load_cached_schains_simplified;
+module.exports.load_schains_connected_only = load_schains_connected_only;
 module.exports.check_connected_schains = check_connected_schains;
 module.exports.filter_schains_marked_as_connected = filter_schains_marked_as_connected;
 module.exports.find_schain_index_in_array_by_name = find_schain_index_in_array_by_name;
diff --git a/npms/skale-observer/observer_worker.js b/npms/skale-observer/observer_worker.js
new file mode 100644
index 000000000..3180fc071
--- /dev/null
+++ b/npms/skale-observer/observer_worker.js
@@ -0,0 +1,210 @@
+
+// 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 observer_worker.js
+ * @copyright SKALE Labs 2019-Present
+ */
+
+const {
+    parentPort
+    //, workerData
+} = require( "worker_threads" );
+const network_layer = require( "../skale-cool-socket/socket.js" );
+const { Server } = require( "../skale-cool-socket/server.js" );
+const owaspUtils = require( "../skale-owasp/owasp-util.js" );
+const cc = owaspUtils.cc;
+
+const skale_observer = require( "./observer.js" );
+
+const g_url = "skale_observer_worker_server";
+
+parentPort.on( "message", jo => {
+    if( network_layer.in_worker_apis.on_message( jo ) )
+        return;
+} );
+
+function doSendMessage( type, endpoint, worker_uuid, data ) {
+    const jo = network_layer.socket_received_data_reverse_marshall( data );
+    const joSend = {
+        worker_message_type: ( type && typeof type == "string" && type.length > 0 ) ? type : "in_worker_message",
+        worker_endpoint: endpoint,
+        worker_uuid: worker_uuid,
+        data: jo
+    };
+    parentPort.postMessage( network_layer.socket_sent_data_marshall( joSend ) );
+}
+
+class ObserverServer extends Server {
+    constructor( acceptor ) {
+        super( acceptor );
+        const self = this;
+        self.opts = null;
+        self.intervalPeriodicSchainsCaching = null;
+        self.bIsPeriodicCachingStepInProgress = false;
+        self.mapApiHandlers.init = function( joMessage, joAnswer, eventData, socket ) {
+            self.log = function() {
+                const args = Array.prototype.slice.call( arguments );
+                const jo = {
+                    method: "log",
+                    error: null,
+                    message: args.join( " " )
+                };
+                const isFlush = true;
+                socket.send( jo, isFlush );
+            };
+            self.opts = JSON.parse( JSON.stringify( joMessage.message.opts ) );
+            self.opts.details = {
+                write: self.log
+            };
+            // self.log( cc.debug( "Initialized in-worker options:" ) + " " + cc.j( self.opts ) + "\n" );
+            //
+            self.opts.imaState.joAccount_main_net.address = owaspUtils.fn_address_impl_;
+            self.opts.imaState.joAccount_s_chain.address = owaspUtils.fn_address_impl_;
+            //
+            if( self.opts.imaState.strURL_main_net && typeof self.opts.imaState.strURL_main_net == "string" && self.opts.imaState.strURL_main_net.length > 0 ) {
+                const u = self.opts.imaState.strURL_main_net;
+                self.opts.imaState.w3_main_net = skale_observer.getWeb3FromURL( u, self.log );
+            } else {
+                self.log(
+                    cc.error( "WARNING:" ) + cc.warning( " No " ) + cc.note( "Main-net" ) +
+                    cc.warning( " URL specified in command line arguments" ) +
+                    cc.debug( "(needed for particular operations only)" ) +
+                    "\n" );
+            }
+            //
+            if( self.opts.imaState.strURL_s_chain && typeof self.opts.imaState.strURL_s_chain == "string" && self.opts.imaState.strURL_s_chain.length > 0 ) {
+                const u = self.opts.imaState.strURL_s_chain;
+                self.opts.imaState.w3_s_chain = skale_observer.getWeb3FromURL( u, self.log );
+            } else {
+                self.log(
+                    cc.error( "WARNING:" ) + cc.warning( " No " ) + cc.note( "Main-net" ) +
+                    cc.warning( " URL specified in command line arguments" ) +
+                    cc.debug( "(needed for particular operations only)" ) +
+                    "\n" );
+            }
+            //
+            self.opts.imaState.jo_nodes = new self.opts.imaState.w3_main_net.eth.Contract( self.opts.imaState.joAbiPublishResult_skale_manager.nodes_abi, self.opts.imaState.joAbiPublishResult_skale_manager.nodes_address );
+            self.opts.imaState.jo_schains = new self.opts.imaState.w3_main_net.eth.Contract( self.opts.imaState.joAbiPublishResult_skale_manager.schains_abi, self.opts.imaState.joAbiPublishResult_skale_manager.schains_address );
+            self.opts.imaState.jo_schains_internal = new self.opts.imaState.w3_main_net.eth.Contract( self.opts.imaState.joAbiPublishResult_skale_manager.schains_internal_abi, self.opts.imaState.joAbiPublishResult_skale_manager.schains_internal_address );
+            //
+            self.opts.imaState.jo_message_proxy_s_chain = new self.opts.imaState.w3_s_chain.eth.Contract( self.opts.imaState.joAbiPublishResult_s_chain.message_proxy_chain_abi, self.opts.imaState.joAbiPublishResult_s_chain.message_proxy_chain_address );
+            //
+            cc.enable( joMessage.message.cc.isEnabled );
+            joAnswer.message = {
+                method: "" + joMessage.method,
+                error: null
+            };
+            self.log( cc.debug( "Full init compete for in-worker SNB server" ) + " " + cc.notice( g_url ) + "\n" );
+            return joAnswer;
+        };
+        self.mapApiHandlers.periodic_caching_start = function( joMessage, joAnswer, eventData, socket ) {
+            self.periodic_caching_start(
+                socket,
+                joMessage.message.secondsToReDiscoverSkaleNetwork,
+                joMessage.message.strChainNameConnectedTo,
+                joMessage.message.addressFrom
+            );
+            joAnswer.message = {
+                method: "" + joMessage.method,
+                error: null
+            };
+            return joAnswer;
+        };
+        self.mapApiHandlers.periodic_caching_stop = function( joMessage, joAnswer, eventData, socket ) {
+            self.periodic_caching_stop();
+            joAnswer.message = {
+                method: "" + joMessage.method,
+                error: null
+            };
+            return joAnswer;
+        };
+        self.log( cc.debug( "Initialized in-worker SNB server" ) + " " + cc.notice( g_url ) + "\n" );
+    }
+    dispose() {
+        const self = this;
+        self.isDisposing = true;
+        if( self.intervalPeriodicSchainsCaching ) {
+            clearInterval( self.intervalPeriodicSchainsCaching );
+            self.intervalPeriodicSchainsCaching = null;
+        }
+        super.dispose();
+    }
+    async periodic_caching_do_now( socket, secondsToReDiscoverSkaleNetwork, strChainNameConnectedTo, addressFrom ) {
+        const self = this;
+        if( self.bIsPeriodicCachingStepInProgress )
+            return;
+        self.bIsPeriodicCachingStepInProgress = true;
+        // const strError =
+        await skale_observer.cache_schains(
+            strChainNameConnectedTo,
+            self.opts.imaState.w3_main_net,
+            self.opts.imaState.w3_s_chain,
+            addressFrom,
+            self.opts
+        );
+        self.bIsPeriodicCachingStepInProgress = false;
+        const arr_schains = skale_observer.get_last_cached_schains();
+        // self.log( cc.normal( "Got " ) + cc.info( "SKALE NETWORK" ) + cc.normal( " information in worker: " ) + cc.j( arr_schains ) + "\n" );
+        const jo = {
+            method: "periodic_caching_do_now",
+            error: null,
+            message: arr_schains
+        };
+        const isFlush = true;
+        socket.send( jo, isFlush );
+    }
+    async periodic_caching_start( socket, secondsToReDiscoverSkaleNetwork, strChainNameConnectedTo, addressFrom ) {
+        const self = this;
+        await self.periodic_caching_stop();
+        if( secondsToReDiscoverSkaleNetwork <= 0 )
+            return false;
+        const fn_async_handler = async function() {
+            await self.periodic_caching_do_now( socket, secondsToReDiscoverSkaleNetwork, strChainNameConnectedTo, addressFrom );
+        };
+        self.intervalPeriodicSchainsCaching = setInterval( function() {
+            if( self.bIsPeriodicCachingStepInProgress )
+                return;
+            fn_async_handler()
+                .then( () => {
+                } ).catch( () => {
+                } );
+        }, secondsToReDiscoverSkaleNetwork * 1000 );
+        fn_async_handler(); // initial async call
+        return true;
+    }
+    async periodic_caching_stop() {
+        const self = this;
+        if( ! self.intervalPeriodicSchainsCaching )
+            return false;
+        clearInterval( self.intervalPeriodicSchainsCaching );
+        self.intervalPeriodicSchainsCaching = null;
+        self.bIsPeriodicCachingStepInProgress = false;
+        return true;
+    }
+};
+
+const acceptor = new network_layer.InWorkerSocketServerAcceptor( g_url, doSendMessage );
+const server = new ObserverServer( acceptor );
+server.on( "dispose", function() {
+    const self = server;
+    self.log( cc.debug( "Disposed in-worker SNB server" ) + " " + cc.notice( g_url ) + "\n" );
+} );
diff --git a/npms/skale-observer/package.json b/npms/skale-observer/package.json
index c3575b4e1..6aff75501 100644
--- a/npms/skale-observer/package.json
+++ b/npms/skale-observer/package.json
@@ -5,9 +5,9 @@
     "author": "SKALE Labs and contributors",
     "scripts": {},
     "dependencies": {
-        "ethereumjs-tx": "1.3.7",
-        "ethereumjs-wallet": "0.6.3",
-        "ethereumjs-util": "6.1.0",
+        "ethereumjs-tx": "2.1.2",
+        "ethereumjs-wallet": "^1.0.2",
+        "ethereumjs-util": "^7.1.4",
         "web3": "^1.6.1",
         "uuid": "3.3.3",
         "sha3": "2.1.4"
diff --git a/npms/skale-observer/yarn.lock b/npms/skale-observer/yarn.lock
index a0ba805e4..e04aa5e45 100644
--- a/npms/skale-observer/yarn.lock
+++ b/npms/skale-observer/yarn.lock
@@ -257,7 +257,7 @@ accepts@~1.3.7:
     mime-types "~2.1.24"
     negotiator "0.6.2"
 
-aes-js@^3.1.1:
+aes-js@^3.1.2:
   version "3.1.2"
   resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.1.2.tgz#db9aabde85d5caabbfc0d4f2a4446960f627146a"
   integrity sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ==
@@ -376,20 +376,6 @@ bignumber.js@^9.0.0:
   resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.2.tgz#71c6c6bed38de64e24a65ebe16cfcf23ae693673"
   integrity sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw==
 
-bindings@^1.2.1, bindings@^1.5.0:
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
-  integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==
-  dependencies:
-    file-uri-to-path "1.0.0"
-
-bip66@^1.1.5:
-  version "1.1.5"
-  resolved "https://registry.yarnpkg.com/bip66/-/bip66-1.1.5.tgz#01fa8748785ca70955d5011217d1b3139969ca22"
-  integrity sha1-AfqHSHhcpwlV1QESF9GzE5lpyiI=
-  dependencies:
-    safe-buffer "^5.0.1"
-
 blakejs@^1.1.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/blakejs/-/blakejs-1.1.1.tgz#bf313053978b2cd4c444a48795710be05c785702"
@@ -412,7 +398,7 @@ bn.js@4.11.6:
   resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.6.tgz#53344adb14617a13f6e8dd2ce28905d1c0ba3215"
   integrity sha1-UzRK2xRhehP26N0s4okF0cC6MhU=
 
-bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.0, bn.js@^4.11.6, bn.js@^4.11.8, bn.js@^4.11.9:
+bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.0, bn.js@^4.11.6, bn.js@^4.11.9:
   version "4.12.0"
   resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
   integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
@@ -451,7 +437,7 @@ brorand@^1.0.1, brorand@^1.1.0:
   resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
   integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
 
-browserify-aes@^1.0.0, browserify-aes@^1.0.4, browserify-aes@^1.0.6, browserify-aes@^1.2.0:
+browserify-aes@^1.0.0, browserify-aes@^1.0.4, browserify-aes@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48"
   integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==
@@ -858,15 +844,6 @@ dom-walk@^0.1.0:
   resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84"
   integrity sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==
 
-drbg.js@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/drbg.js/-/drbg.js-1.0.1.tgz#3e36b6c42b37043823cdbc332d58f31e2445480b"
-  integrity sha1-Pja2xCs3BDgjzbwzLVjzHiRFSAs=
-  dependencies:
-    browserify-aes "^1.0.6"
-    create-hash "^1.1.2"
-    create-hmac "^1.1.4"
-
 duplexer3@^0.1.4:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
@@ -1022,11 +999,6 @@ ethereum-bloom-filters@^1.0.6:
   dependencies:
     js-sha3 "^0.8.0"
 
-ethereum-common@^0.0.18:
-  version "0.0.18"
-  resolved "https://registry.yarnpkg.com/ethereum-common/-/ethereum-common-0.0.18.tgz#2fdc3576f232903358976eb39da783213ff9523f"
-  integrity sha1-L9w1dvIykDNYl26znaeDIT/5Uj8=
-
 ethereum-cryptography@^0.1.3:
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-0.1.3.tgz#8d6143cfc3d74bf79bbd8edecdf29e4ae20dd191"
@@ -1048,39 +1020,18 @@ ethereum-cryptography@^0.1.3:
     secp256k1 "^4.0.1"
     setimmediate "^1.0.5"
 
-ethereumjs-tx@1.3.7:
-  version "1.3.7"
-  resolved "https://registry.yarnpkg.com/ethereumjs-tx/-/ethereumjs-tx-1.3.7.tgz#88323a2d875b10549b8347e09f4862b546f3d89a"
-  integrity sha512-wvLMxzt1RPhAQ9Yi3/HKZTn0FZYpnsmQdbKYfUUpi4j1SEIcbkd9tndVjcPrufY3V7j2IebOpC00Zp2P/Ay2kA==
-  dependencies:
-    ethereum-common "^0.0.18"
-    ethereumjs-util "^5.0.0"
-
-ethereumjs-util@6.1.0:
-  version "6.1.0"
-  resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-6.1.0.tgz#e9c51e5549e8ebd757a339cc00f5380507e799c8"
-  integrity sha512-URESKMFbDeJxnAxPppnk2fN6Y3BIatn9fwn76Lm8bQlt+s52TpG8dN9M66MLPuRAiAOIqL3dfwqWJf0sd0fL0Q==
-  dependencies:
-    bn.js "^4.11.0"
-    create-hash "^1.1.2"
-    ethjs-util "0.1.6"
-    keccak "^1.0.2"
-    rlp "^2.0.0"
-    safe-buffer "^5.1.1"
-    secp256k1 "^3.0.1"
+ethereumjs-common@^1.5.0:
+  version "1.5.2"
+  resolved "https://registry.yarnpkg.com/ethereumjs-common/-/ethereumjs-common-1.5.2.tgz#2065dbe9214e850f2e955a80e650cb6999066979"
+  integrity sha512-hTfZjwGX52GS2jcVO6E2sx4YuFnf0Fhp5ylo4pEPhEffNln7vS59Hr5sLnp3/QCazFLluuBZ+FZ6J5HTp0EqCA==
 
-ethereumjs-util@^5.0.0:
-  version "5.2.1"
-  resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-5.2.1.tgz#a833f0e5fca7e5b361384dc76301a721f537bf65"
-  integrity sha512-v3kT+7zdyCm1HIqWlLNrHGqHGLpGYIhjeHxQjnDXjLT2FyGJDsd3LWMYUo7pAFRrk86CR3nUJfhC81CCoJNNGQ==
+ethereumjs-tx@2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/ethereumjs-tx/-/ethereumjs-tx-2.1.2.tgz#5dfe7688bf177b45c9a23f86cf9104d47ea35fed"
+  integrity sha512-zZEK1onCeiORb0wyCXUvg94Ve5It/K6GD1K+26KfFKodiBiS6d9lfCXlUKGBBdQ+bv7Day+JK0tj1K+BeNFRAw==
   dependencies:
-    bn.js "^4.11.0"
-    create-hash "^1.1.2"
-    elliptic "^6.5.2"
-    ethereum-cryptography "^0.1.3"
-    ethjs-util "^0.1.3"
-    rlp "^2.0.0"
-    safe-buffer "^5.1.1"
+    ethereumjs-common "^1.5.0"
+    ethereumjs-util "^6.0.0"
 
 ethereumjs-util@^6.0.0:
   version "6.2.1"
@@ -1106,20 +1057,30 @@ ethereumjs-util@^7.0.10, ethereumjs-util@^7.1.0, ethereumjs-util@^7.1.3:
     ethereum-cryptography "^0.1.3"
     rlp "^2.2.4"
 
-ethereumjs-wallet@0.6.3:
-  version "0.6.3"
-  resolved "https://registry.yarnpkg.com/ethereumjs-wallet/-/ethereumjs-wallet-0.6.3.tgz#b0eae6f327637c2aeb9ccb9047b982ac542e6ab1"
-  integrity sha512-qiXPiZOsStem+Dj/CQHbn5qex+FVkuPmGH7SvSnA9F3tdRDt8dLMyvIj3+U05QzVZNPYh4HXEdnzoYI4dZkr9w==
+ethereumjs-util@^7.1.2, ethereumjs-util@^7.1.4:
+  version "7.1.5"
+  resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-7.1.5.tgz#9ecf04861e4fbbeed7465ece5f23317ad1129181"
+  integrity sha512-SDl5kKrQAudFBUe5OJM9Ac6WmMyYmXX/6sTmLZ3ffG2eY6ZIGBes3pEDxNN6V72WyOw4CPD5RomKdsa8DAAwLg==
+  dependencies:
+    "@types/bn.js" "^5.1.0"
+    bn.js "^5.1.2"
+    create-hash "^1.1.2"
+    ethereum-cryptography "^0.1.3"
+    rlp "^2.2.4"
+
+ethereumjs-wallet@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/ethereumjs-wallet/-/ethereumjs-wallet-1.0.2.tgz#2c000504b4c71e8f3782dabe1113d192522e99b6"
+  integrity sha512-CCWV4RESJgRdHIvFciVQFnCHfqyhXWchTPlkfp28Qc53ufs+doi5I/cV2+xeK9+qEo25XCWfP9MiL+WEPAZfdA==
   dependencies:
-    aes-js "^3.1.1"
+    aes-js "^3.1.2"
     bs58check "^2.1.2"
-    ethereumjs-util "^6.0.0"
-    hdkey "^1.1.0"
-    randombytes "^2.0.6"
-    safe-buffer "^5.1.2"
-    scrypt.js "^0.3.0"
+    ethereum-cryptography "^0.1.3"
+    ethereumjs-util "^7.1.2"
+    randombytes "^2.1.0"
+    scrypt-js "^3.0.1"
     utf8 "^3.0.0"
-    uuid "^3.3.2"
+    uuid "^8.3.2"
 
 ethjs-unit@0.1.6:
   version "0.1.6"
@@ -1129,7 +1090,7 @@ ethjs-unit@0.1.6:
     bn.js "4.11.6"
     number-to-bn "1.7.0"
 
-ethjs-util@0.1.6, ethjs-util@^0.1.3:
+ethjs-util@0.1.6:
   version "0.1.6"
   resolved "https://registry.yarnpkg.com/ethjs-util/-/ethjs-util-0.1.6.tgz#f308b62f185f9fe6237132fb2a9818866a5cd536"
   integrity sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w==
@@ -1223,11 +1184,6 @@ fast-json-stable-stringify@^2.0.0:
   resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
   integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
 
-file-uri-to-path@1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
-  integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
-
 finalhandler@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
@@ -1496,15 +1452,6 @@ hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7:
     inherits "^2.0.3"
     minimalistic-assert "^1.0.1"
 
-hdkey@^1.1.0:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/hdkey/-/hdkey-1.1.2.tgz#c60f9cf6f90fbf24a8a52ea06893f36a0108cd3e"
-  integrity sha512-PTQ4VKu0oRnCrYfLp04iQZ7T2Cxz0UsEXYauk2j8eh6PJXCpbXuCFhOmtIFtbET0i3PMWmHN9J11gU8LEgUljQ==
-  dependencies:
-    bs58check "^2.1.2"
-    safe-buffer "^5.1.1"
-    secp256k1 "^3.0.1"
-
 hmac-drbg@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
@@ -1823,16 +1770,6 @@ jsprim@^1.2.2:
     json-schema "0.4.0"
     verror "1.10.0"
 
-keccak@^1.0.2:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/keccak/-/keccak-1.4.0.tgz#572f8a6dbee8e7b3aa421550f9e6408ca2186f80"
-  integrity sha512-eZVaCpblK5formjPjeTBik7TAg+pqnDrMHIffSvi9Lh7PQgM1+hSzakUeZFCk9DVVG0dacZJuaz2ntwlzZUIBw==
-  dependencies:
-    bindings "^1.2.1"
-    inherits "^2.0.3"
-    nan "^2.2.1"
-    safe-buffer "^5.1.0"
-
 keccak@^3.0.0:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.2.tgz#4c2c6e8c54e04f2670ee49fa734eb9da152206e0"
@@ -2031,7 +1968,7 @@ multihashes@^0.4.15, multihashes@~0.4.15:
     multibase "^0.7.0"
     varint "^5.0.0"
 
-nan@^2.14.0, nan@^2.2.1:
+nan@^2.14.0:
   version "2.15.0"
   resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee"
   integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==
@@ -2398,7 +2335,7 @@ query-string@^5.0.1:
     object-assign "^4.1.0"
     strict-uri-encode "^1.0.0"
 
-randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.0.6, randombytes@^2.1.0:
+randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
   integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
@@ -2508,7 +2445,7 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
     hash-base "^3.0.0"
     inherits "^2.0.1"
 
-rlp@^2.0.0, rlp@^2.2.3, rlp@^2.2.4:
+rlp@^2.2.3, rlp@^2.2.4:
   version "2.2.7"
   resolved "https://registry.yarnpkg.com/rlp/-/rlp-2.2.7.tgz#33f31c4afac81124ac4b283e2bd4d9720b30beaf"
   integrity sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ==
@@ -2540,41 +2477,11 @@ scrypt-js@^3.0.0, scrypt-js@^3.0.1:
   resolved "https://registry.yarnpkg.com/scrypt-js/-/scrypt-js-3.0.1.tgz#d314a57c2aef69d1ad98a138a21fe9eafa9ee312"
   integrity sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==
 
-scrypt.js@^0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/scrypt.js/-/scrypt.js-0.3.0.tgz#6c62d61728ad533c8c376a2e5e3e86d41a95c4c0"
-  integrity sha512-42LTc1nyFsyv/o0gcHtDztrn+aqpkaCNt5Qh7ATBZfhEZU7IC/0oT/qbBH+uRNoAPvs2fwiOId68FDEoSRA8/A==
-  dependencies:
-    scryptsy "^1.2.1"
-  optionalDependencies:
-    scrypt "^6.0.2"
-
-scrypt@^6.0.2, "scrypt@file:../scrypt/node-scrypt":
+"scrypt@file:../scrypt/node-scrypt":
   version "6.0.3"
   dependencies:
     nan "^2.14.0"
 
-scryptsy@^1.2.1:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/scryptsy/-/scryptsy-1.2.1.tgz#a3225fa4b2524f802700761e2855bdf3b2d92163"
-  integrity sha1-oyJfpLJST4AnAHYeKFW987LZIWM=
-  dependencies:
-    pbkdf2 "^3.0.3"
-
-secp256k1@^3.0.1:
-  version "3.8.0"
-  resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-3.8.0.tgz#28f59f4b01dbee9575f56a47034b7d2e3b3b352d"
-  integrity sha512-k5ke5avRZbtl9Tqx/SA7CbY3NF6Ro+Sj9cZxezFzuBlLDmyqPiL8hJJ+EmzD8Ig4LUDByHJ3/iPOVoRixs/hmw==
-  dependencies:
-    bindings "^1.5.0"
-    bip66 "^1.1.5"
-    bn.js "^4.11.8"
-    create-hash "^1.2.0"
-    drbg.js "^1.0.1"
-    elliptic "^6.5.2"
-    nan "^2.14.0"
-    safe-buffer "^5.1.2"
-
 secp256k1@^4.0.1:
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-4.0.2.tgz#15dd57d0f0b9fdb54ac1fa1694f40e5e9a54f4a1"
@@ -3005,6 +2912,11 @@ uuid@^3.3.2:
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
   integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
 
+uuid@^8.3.2:
+  version "8.3.2"
+  resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
+  integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
+
 varint@^5.0.0:
   version "5.0.2"
   resolved "https://registry.yarnpkg.com/varint/-/varint-5.0.2.tgz#5b47f8a947eb668b848e034dcfa87d0ff8a7f7a4"
diff --git a/npms/skale-owasp/owasp-util.js b/npms/skale-owasp/owasp-util.js
index c7e51d941..439adfc9d 100644
--- a/npms/skale-owasp/owasp-util.js
+++ b/npms/skale-owasp/owasp-util.js
@@ -285,12 +285,12 @@ function verifyArgumentIsInteger( joArg ) {
     }
 }
 
-function verifyArgumentIsIntegerIpPortNumber( joArg ) {
+function verifyArgumentIsIntegerIpPortNumber( joArg, isEnableZero ) {
     try {
         verifyArgumentIsInteger( joArg );
         if( joArg.value < 0 )
             throw new Error( "Port number " + joArg.value + " cannot be negative" );
-        if( joArg.value < 1 )
+        if( ( !isEnableZero ) && joArg.value < 1 )
             throw new Error( "Port number " + joArg.value + " too small" );
         if( joArg.value > 65535 )
             throw new Error( "Port number " + joArg.value + " too big" );
@@ -625,6 +625,39 @@ function w3_2_url( w3 ) {
 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
 
+function extract_error_message( jo, strDefaultErrorText ) {
+    strDefaultErrorText = strDefaultErrorText || "unknown error or error without a description";
+    try {
+        if( ! jo )
+            return strDefaultErrorText;
+        if( typeof jo != "object" )
+            return strDefaultErrorText;
+        let strStack = "";
+        if( "stack" in jo && jo.stack && typeof jo.stack == "object" && "length" in jo.stack && jo.stack.length > 0 ) {
+            strStack += "\nCall stack from error object:";
+            for( let i = 0; i < jo.stack.length; ++ i )
+                strStack += "\n" + jo.stack[i].toString();
+        }
+        if( "error" in jo ) {
+            jo = jo.error;
+            if( typeof jo == "string" )
+                return jo;
+            if( typeof jo != "object" )
+                return strDefaultErrorText + "(" + jo.toString() + ")" + strStack;
+        }
+        if( "message" in jo ) {
+            jo = jo.message;
+            if( typeof jo == "string" )
+                return jo + strStack;
+        }
+        strDefaultErrorText += "(" + jo.toString() + ")" + strStack;
+    } catch ( err ) {
+    }
+    return strDefaultErrorText;
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
 module.exports = {
     cc: cc,
     w3mod: w3mod,
@@ -667,5 +700,6 @@ module.exports = {
     fn_address_impl_: fn_address_impl_,
     compute_chain_id_from_schain_name: compute_chain_id_from_schain_name,
     w3provider_2_url: w3provider_2_url,
-    w3_2_url: w3_2_url
+    w3_2_url: w3_2_url,
+    extract_error_message: extract_error_message
 }; // module.exports
diff --git a/proxy/contracts/MessageProxy.sol b/proxy/contracts/MessageProxy.sol
index 36c8ba12a..1a67e3133 100644
--- a/proxy/contracts/MessageProxy.sol
+++ b/proxy/contracts/MessageProxy.sol
@@ -45,6 +45,7 @@ abstract contract MessageProxy is AccessControlEnumerableUpgradeable, IMessagePr
         uint256 incomingMessageCounter;
         uint256 outgoingMessageCounter;
         bool inited;
+        uint256 lastOutgoingMessageBlockId;
     }
 
     bytes32 public constant MAINNET_HASH = keccak256(abi.encodePacked("Mainnet"));
@@ -57,8 +58,7 @@ abstract contract MessageProxy is AccessControlEnumerableUpgradeable, IMessagePr
     //   schainHash => ConnectedChainInfo
     mapping(bytes32 => ConnectedChainInfo) public connectedChains;
     //   schainHash => contract address => allowed
-    // solhint-disable-next-line private-vars-leading-underscore
-    mapping(bytes32 => mapping(address => bool)) internal deprecatedRegistryContracts;
+    mapping(bytes32 => mapping(address => bool)) private _deprecatedRegistryContracts;
 
     uint256 public gasLimit;
 
@@ -111,6 +111,14 @@ abstract contract MessageProxy is AccessControlEnumerableUpgradeable, IMessagePr
         address contractAddress
     );
 
+    /**
+     * @dev Emitted when referenced to previous outgoing message.
+     */
+    event PreviousMessageReference (
+        uint256 currentMessage,
+        uint256 previousOutgoingMessageBlockId
+    );
+
     /**
      * @dev Modifier to make a function callable only if caller is granted with {CHAIN_CONNECTOR_ROLE}.
      */
@@ -133,13 +141,21 @@ abstract contract MessageProxy is AccessControlEnumerableUpgradeable, IMessagePr
     modifier onlyConstantSetter() {
         require(hasRole(CONSTANT_SETTER_ROLE, msg.sender), "Not enough permissions to set constant");
         _;
-    }    
+    }
+
+    /**
+     * @dev Modifier to make a function callable only if caller is granted with {DEFAULT_ADMIN_ROLE}.
+     */
+    modifier onlyOwner() {
+        require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "DEFAULT_ADMIN_ROLE is required");
+        _;
+    }
 
     /**
      * @dev Sets gasLimit to a new value.
-     * 
+     *
      * Requirements:
-     * 
+     *
      * - `msg.sender` must be granted CONSTANT_SETTER_ROLE.
      */
     function setNewGasLimit(uint256 newGasLimit) external override onlyConstantSetter {
@@ -163,9 +179,9 @@ abstract contract MessageProxy is AccessControlEnumerableUpgradeable, IMessagePr
     /**
      * @dev Allows `msg.sender` to register extra contract for all schains
      * for being able to transfer messages from custom contracts.
-     * 
+     *
      * Requirements:
-     * 
+     *
      * - `msg.sender` must be granted as EXTRA_CONTRACT_REGISTRAR_ROLE.
      * - Passed address should be contract.
      * - Extra contract must not be registered.
@@ -180,9 +196,9 @@ abstract contract MessageProxy is AccessControlEnumerableUpgradeable, IMessagePr
     /**
      * @dev Allows `msg.sender` to remove extra contract for all schains.
      * Extra contract will no longer be able to send messages through MessageProxy.
-     * 
+     *
      * Requirements:
-     * 
+     *
      * - `msg.sender` must be granted as EXTRA_CONTRACT_REGISTRAR_ROLE.
      */
     function removeExtraContractForAll(address extraContract) external override onlyExtraContractRegistrar {
@@ -200,7 +216,7 @@ abstract contract MessageProxy is AccessControlEnumerableUpgradeable, IMessagePr
 
     /**
      * @dev Should return a range of contracts registered by schainHash.
-     * 
+     *
      * Requirements:
      * range should be less or equal 10 contracts
      */
@@ -226,9 +242,9 @@ abstract contract MessageProxy is AccessControlEnumerableUpgradeable, IMessagePr
 
     /**
      * @dev Returns number of outgoing messages.
-     * 
+     *
      * Requirements:
-     * 
+     *
      * - Target schain  must be initialized.
      */
     function getOutgoingMessagesCounter(string calldata targetSchainName)
@@ -242,11 +258,21 @@ abstract contract MessageProxy is AccessControlEnumerableUpgradeable, IMessagePr
         return connectedChains[dstChainHash].outgoingMessageCounter;
     }
 
+
+    /**
+     * @dev Should return block number of the last message transferred to schain
+     */
+    function getLastOutgoingMessageBlockId(string memory targetSchainName) external view override returns (uint) {
+        bytes32 dstChainHash = keccak256(abi.encodePacked(targetSchainName));
+        require(connectedChains[dstChainHash].inited, "Destination chain is not initialized");
+        return connectedChains[dstChainHash].lastOutgoingMessageBlockId;
+    }
+
     /**
      * @dev Returns number of incoming messages.
-     * 
+     *
      * Requirements:
-     * 
+     *
      * - Source schain must be initialized.
      */
     function getIncomingMessagesCounter(string calldata fromSchainName)
@@ -272,11 +298,11 @@ abstract contract MessageProxy is AccessControlEnumerableUpgradeable, IMessagePr
     /**
      * @dev Posts message from this contract to `targetChainHash` MessageProxy contract.
      * This is called by a smart contract to make a cross-chain call.
-     * 
+     *
      * Emits an {OutgoingMessage} event.
      *
      * Requirements:
-     * 
+     *
      * - Target chain must be initialized.
      * - Target chain must be registered as external contract.
      */
@@ -291,23 +317,28 @@ abstract contract MessageProxy is AccessControlEnumerableUpgradeable, IMessagePr
     {
         require(connectedChains[targetChainHash].inited, "Destination chain is not initialized");
         _authorizeOutgoingMessageSender(targetChainHash);
-        
+
+        uint outgoingMessageCounter = connectedChains[targetChainHash].outgoingMessageCounter;
         emit OutgoingMessage(
             targetChainHash,
-            connectedChains[targetChainHash].outgoingMessageCounter,
+            outgoingMessageCounter,
             msg.sender,
             targetContract,
             data
         );
-
-        connectedChains[targetChainHash].outgoingMessageCounter += 1;
+        emit PreviousMessageReference(
+            outgoingMessageCounter,
+            connectedChains[targetChainHash].lastOutgoingMessageBlockId
+        );
+        connectedChains[targetChainHash].outgoingMessageCounter++;
+        connectedChains[targetChainHash].lastOutgoingMessageBlockId = block.number;
     }
 
     /**
      * @dev Allows CHAIN_CONNECTOR_ROLE to remove connected chain from this contract.
-     * 
+     *
      * Requirements:
-     * 
+     *
      * - `msg.sender` must be granted CHAIN_CONNECTOR_ROLE.
      * - `schainName` must be initialized.
      */
@@ -315,7 +346,7 @@ abstract contract MessageProxy is AccessControlEnumerableUpgradeable, IMessagePr
         bytes32 schainHash = keccak256(abi.encodePacked(schainName));
         require(connectedChains[schainHash].inited, "Chain is not initialized");
         delete connectedChains[schainHash];
-    }    
+    }
 
     /**
      * @dev Checks whether chain is currently connected.
@@ -349,9 +380,9 @@ abstract contract MessageProxy is AccessControlEnumerableUpgradeable, IMessagePr
 
     /**
      * @dev Allows MessageProxy to register extra contract for being able to transfer messages from custom contracts.
-     * 
+     *
      * Requirements:
-     * 
+     *
      * - Extra contract address must be contract.
      * - Extra contract must not be registered.
      * - Extra contract must not be registered for all chains.
@@ -361,14 +392,14 @@ abstract contract MessageProxy is AccessControlEnumerableUpgradeable, IMessagePr
         address extraContract
     )
         internal
-    {      
+    {
         require(extraContract.isContract(), "Given address is not a contract");
         require(!_getRegistryContracts()[chainHash].contains(extraContract), "Extra contract is already registered");
         require(
             !_getRegistryContracts()[bytes32(0)].contains(extraContract),
             "Extra contract is already registered for all chains"
         );
-        
+
         _getRegistryContracts()[chainHash].add(extraContract);
         emit ExtraContractRegistered(chainHash, extraContract);
     }
@@ -376,9 +407,9 @@ abstract contract MessageProxy is AccessControlEnumerableUpgradeable, IMessagePr
     /**
      * @dev Allows MessageProxy to remove extra contract,
      * thus `extraContract` will no longer be available to transfer messages from mainnet to schain.
-     * 
+     *
      * Requirements:
-     * 
+     *
      * - Extra contract must be registered.
      */
     function _removeExtraContract(
@@ -394,9 +425,9 @@ abstract contract MessageProxy is AccessControlEnumerableUpgradeable, IMessagePr
 
     /**
      * @dev Allows MessageProxy to connect schain with MessageProxyOnMainnet for transferring messages.
-     * 
+     *
      * Requirements:
-     * 
+     *
      * - `msg.sender` must be granted CHAIN_CONNECTOR_ROLE.
      * - SKALE chain must not be connected.
      */
@@ -405,7 +436,8 @@ abstract contract MessageProxy is AccessControlEnumerableUpgradeable, IMessagePr
         connectedChains[schainHash] = ConnectedChainInfo({
             incomingMessageCounter: 0,
             outgoingMessageCounter: 0,
-            inited: true
+            inited: true,
+            lastOutgoingMessageBlockId: 0
         });
     }
 
@@ -496,7 +528,7 @@ abstract contract MessageProxy is AccessControlEnumerableUpgradeable, IMessagePr
         require(
             isContractRegistered(bytes32(0), msg.sender) || isContractRegistered(targetChainHash, msg.sender),
             "Sender contract is not registered"
-        );        
+        );
     }
 
     /**
@@ -543,6 +575,6 @@ abstract contract MessageProxy is AccessControlEnumerableUpgradeable, IMessagePr
         for(uint i = 0; i < slicedEnd; i++){
             sliced[i] = text[i];
         }
-        return sliced;    
+        return sliced;
     }
 }
diff --git a/proxy/contracts/mainnet/CommunityPool.sol b/proxy/contracts/mainnet/CommunityPool.sol
index 1dd726872..ddcbb7a36 100644
--- a/proxy/contracts/mainnet/CommunityPool.sol
+++ b/proxy/contracts/mainnet/CommunityPool.sol
@@ -118,7 +118,7 @@ contract CommunityPool is Twin, ICommunityPool {
             activeUsers[user][schainHash] = false;
             messageProxy.postOutgoingMessage(
                 schainHash,
-                schainLinks[schainHash],
+                getSchainContract(schainHash),
                 Messages.encodeLockUserMessage(user)
             );
         }
@@ -167,7 +167,7 @@ contract CommunityPool is Twin, ICommunityPool {
             activeUsers[user][schainHash] = true;
             messageProxy.postOutgoingMessage(
                 schainHash,
-                schainLinks[schainHash],
+                getSchainContract(schainHash),
                 Messages.encodeActivateUserMessage(user)
             );
         }
@@ -194,7 +194,7 @@ contract CommunityPool is Twin, ICommunityPool {
             activeUsers[msg.sender][schainHash] = false;
             messageProxy.postOutgoingMessage(
                 schainHash,
-                schainLinks[schainHash],
+                getSchainContract(schainHash),
                 Messages.encodeLockUserMessage(msg.sender)
             );
         }
diff --git a/proxy/contracts/mainnet/MessageProxyForMainnet.sol b/proxy/contracts/mainnet/MessageProxyForMainnet.sol
index 1e9f05f90..ea05fa80a 100644
--- a/proxy/contracts/mainnet/MessageProxyForMainnet.sol
+++ b/proxy/contracts/mainnet/MessageProxyForMainnet.sol
@@ -84,6 +84,9 @@ contract MessageProxyForMainnet is SkaleManagerClient, MessageProxy, IMessagePro
     // schainHash   => Pause structure
     mapping(bytes32 => Pause) public pauseInfo;
 
+    //   schainHash => Set of addresses of reimbursed contracts
+    mapping(bytes32 => EnumerableSetUpgradeable.AddressSet) private _reimbursedContracts;
+
     /**
      * @dev Emitted when gas cost for message header was changed.
      */
@@ -114,6 +117,22 @@ contract MessageProxyForMainnet is SkaleManagerClient, MessageProxy, IMessagePro
         bytes32 indexed schainHash
     );
 
+   /**
+     * @dev Emitted when reimbursed contract was added
+     */
+    event ReimbursedContractAdded(
+        bytes32 indexed schainHash,
+        address contractAddress
+    );
+
+   /**
+     * @dev Emitted when reimbursed contract was removed
+     */
+    event ReimbursedContractRemoved(
+        bytes32 indexed schainHash,
+        address contractAddress
+    );
+
     /**
      * @dev Reentrancy guard for postIncomingMessages.
      */
@@ -124,6 +143,9 @@ contract MessageProxyForMainnet is SkaleManagerClient, MessageProxy, IMessagePro
         messageInProgress = false;
     }
 
+    /**
+     * @dev Modifier to make a function callable only when IMA is active.
+     */
     modifier whenNotPaused(bytes32 schainHash) {
         require(!isPaused(schainHash), "IMA is paused");
         _;
@@ -152,8 +174,7 @@ contract MessageProxyForMainnet is SkaleManagerClient, MessageProxy, IMessagePro
      * - `msg.sender` must be granted as DEFAULT_ADMIN_ROLE.
      * - Address of CommunityPool contract must not be null.
      */
-    function setCommunityPool(ICommunityPool newCommunityPoolAddress) external override {
-        require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "Not authorized caller");
+    function setCommunityPool(ICommunityPool newCommunityPoolAddress) external override onlyOwner {
         require(address(newCommunityPoolAddress) != address(0), "CommunityPool address has to be set");
         communityPool = newCommunityPoolAddress;
     }
@@ -195,6 +216,56 @@ contract MessageProxyForMainnet is SkaleManagerClient, MessageProxy, IMessagePro
         );
         require(schainHash != MAINNET_HASH, "Schain hash can not be equal Mainnet");
         _removeExtraContract(schainHash, extraContract);
+        if (_reimbursedContracts[schainHash].contains(extraContract)) {
+            _removeReimbursedContract(schainHash, extraContract);
+        }
+    }
+
+    /**
+     * @dev Allows `msg.sender` to add reimbursed contract for being able to reimburse gas amount from CommunityPool
+     * during message transfers from custom contracts.
+     * 
+     * Requirements:
+     * 
+     * - `msg.sender` must be granted as EXTRA_CONTRACT_REGISTRAR_ROLE or owner of given `schainName`.
+     * - Schain name must not be `Mainnet`.
+     * - `reimbursedContract` should be registered as extra contract
+     */
+    function addReimbursedContract(string memory schainName, address reimbursedContract) external override {
+        bytes32 schainHash = keccak256(abi.encodePacked(schainName));
+        require(schainHash != MAINNET_HASH, "Schain hash can not be equal Mainnet");        
+        require(
+            hasRole(EXTRA_CONTRACT_REGISTRAR_ROLE, msg.sender) ||
+            isSchainOwner(msg.sender, schainHash),
+            "Not enough permissions to add reimbursed contract"
+        );
+        require(reimbursedContract.isContract(), "Given address is not a contract");
+        require(isContractRegistered(schainHash, reimbursedContract), "Contract is not registered");
+        require(!_reimbursedContracts[schainHash].contains(reimbursedContract), "Reimbursed contract is already added");
+        _reimbursedContracts[schainHash].add(reimbursedContract);
+        emit ReimbursedContractAdded(schainHash, reimbursedContract);
+    }
+
+    /**
+     * @dev Allows `msg.sender` to remove reimbursed contract,
+     * thus `reimbursedContract` will no longer be available to reimburse gas amount from CommunityPool during
+     * message transfers from mainnet to schain.
+     * 
+     * Requirements:
+     * 
+     * - `msg.sender` must be granted as EXTRA_CONTRACT_REGISTRAR_ROLE or owner of given `schainName`.
+     * - Schain name must not be `Mainnet`.
+     */
+    function removeReimbursedContract(string memory schainName, address reimbursedContract) external override {
+        bytes32 schainHash = keccak256(abi.encodePacked(schainName));
+        require(schainHash != MAINNET_HASH, "Schain hash can not be equal Mainnet");
+        require(
+            hasRole(EXTRA_CONTRACT_REGISTRAR_ROLE, msg.sender) ||
+            isSchainOwner(msg.sender, schainHash),
+            "Not enough permissions to remove reimbursed contract"
+        );
+        require(_reimbursedContracts[schainHash].contains(reimbursedContract), "Reimbursed contract is not added");
+        _removeReimbursedContract(schainHash, reimbursedContract);
     }
 
     /**
@@ -238,7 +309,7 @@ contract MessageProxyForMainnet is SkaleManagerClient, MessageProxy, IMessagePro
         connectedChains[fromSchainHash].incomingMessageCounter += messages.length;
         for (uint256 i = 0; i < messages.length; i++) {
             gasTotal = gasleft();
-            if (isContractRegistered(bytes32(0), messages[i].destinationContract)) {
+            if (isReimbursedContract(fromSchainHash, messages[i].destinationContract)) {
                 address receiver = _getGasPayer(fromSchainHash, messages[i], startingCounter + i);
                 _callReceiverContract(fromSchainHash, messages[i], startingCounter + i);
                 notReimbursedGas += communityPool.refundGasByUser(
@@ -286,21 +357,18 @@ contract MessageProxyForMainnet is SkaleManagerClient, MessageProxy, IMessagePro
      *
      * - `msg.sender` must be granted DEFAULT_ADMIN_ROLE.
      */
-    function setVersion(string calldata newVersion) external override {
-        require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "DEFAULT_ADMIN_ROLE is required");
+    function setVersion(string calldata newVersion) external override onlyOwner {
         emit VersionUpdated(version, newVersion);
         version = newVersion;
     }
 
     /**
      * @dev Allows PAUSABLE_ROLE to pause IMA bridge unlimited
-     * or DEFAULT_ADMIN_ROLE to pause for 4 hours
-     * or schain owner to pause unlimited after DEFAULT_ADMIN_ROLE pause it
      *
      * Requirements:
      *
      * - IMA bridge to current schain was not paused
-     * - Sender should be PAUSABLE_ROLE, DEFAULT_ADMIN_ROLE or schain owner
+     * - Sender should be PAUSABLE_ROLE
      */
     function pause(string calldata schainName) external override {
         bytes32 schainHash = keccak256(abi.encodePacked(schainName));
@@ -326,6 +394,39 @@ contract MessageProxyForMainnet is SkaleManagerClient, MessageProxy, IMessagePro
         emit SchainResumed(schainHash);
     }
 
+    /**
+     * @dev Should return length of reimbursed contracts by schainHash.
+     */
+    function getReimbursedContractsLength(bytes32 schainHash) external view override returns (uint256) {
+        return _reimbursedContracts[schainHash].length();
+    }
+
+    /**
+     * @dev Should return a range of reimbursed contracts by schainHash.
+     * 
+     * Requirements:
+     * range should be less or equal 10 contracts
+     */
+    function getReimbursedContractsRange(
+        bytes32 schainHash,
+        uint256 from,
+        uint256 to
+    )
+        external
+        view
+        override
+        returns (address[] memory contractsInRange)
+    {
+        require(
+            from < to && to - from <= 10 && to <= _reimbursedContracts[schainHash].length(),
+            "Range is incorrect"
+        );
+        contractsInRange = new address[](to - from);
+        for (uint256 i = from; i < to; i++) {
+            contractsInRange[i - from] = _reimbursedContracts[schainHash].at(i);
+        }
+    }
+
     /**
      * @dev Creates a new MessageProxyForMainnet contract.
      */
@@ -380,6 +481,15 @@ contract MessageProxyForMainnet is SkaleManagerClient, MessageProxy, IMessagePro
         return pauseInfo[schainHash].paused;
     }
 
+    /**
+     * @dev Returns true if message to the contract should be reimbursed from CommunityPool.
+     */
+    function isReimbursedContract(bytes32 schainHash, address contractAddress) public view override returns (bool) {
+        return
+            isContractRegistered(bytes32(0), contractAddress) ||
+            _reimbursedContracts[schainHash].contains(contractAddress);
+    }
+
     // private
 
     function _authorizeOutgoingMessageSender(bytes32 targetChainHash) internal view override {
@@ -438,4 +548,10 @@ contract MessageProxyForMainnet is SkaleManagerClient, MessageProxy, IMessagePro
     {
         return _registryContracts;
     }
+
+
+    function _removeReimbursedContract(bytes32 schainHash, address reimbursedContract) private {
+        _reimbursedContracts[schainHash].remove(reimbursedContract);
+        emit ReimbursedContractRemoved(schainHash, reimbursedContract);
+    }
 }
diff --git a/proxy/contracts/mainnet/Twin.sol b/proxy/contracts/mainnet/Twin.sol
index 45236f4b3..4a90507ac 100644
--- a/proxy/contracts/mainnet/Twin.sol
+++ b/proxy/contracts/mainnet/Twin.sol
@@ -103,4 +103,12 @@ abstract contract Twin is SkaleManagerClient, ITwin {
         SkaleManagerClient.initialize(contractManagerOfSkaleManagerValue);
         messageProxy = newMessageProxy;
     }
+
+    function getSchainContract(bytes32 schainHash) public override view returns (address) {
+        require(
+            schainLinks[schainHash] != address(0),
+            "Destination contract must be defined"
+        );
+        return schainLinks[schainHash];
+    }
 }
diff --git a/proxy/contracts/schain/CommunityLocker.sol b/proxy/contracts/schain/CommunityLocker.sol
index b853ed679..61a74af3b 100644
--- a/proxy/contracts/schain/CommunityLocker.sol
+++ b/proxy/contracts/schain/CommunityLocker.sol
@@ -28,13 +28,17 @@ import "@skalenetwork/ima-interfaces/schain/ICommunityLocker.sol";
 
 import "../Messages.sol";
 
+interface ICommunityLockerInitializer is ICommunityLocker {
+    function initializeTimestamp() external;
+}
+
 
 /**
  * @title CommunityLocker
  * @dev Contract contains logic to perform automatic reimbursement
  * of gas fees for sent messages
  */
-contract CommunityLocker is ICommunityLocker, AccessControlEnumerableUpgradeable {
+contract CommunityLocker is ICommunityLockerInitializer, AccessControlEnumerableUpgradeable {
 
     /**
      * @dev Mainnet identifier.
@@ -71,11 +75,11 @@ contract CommunityLocker is ICommunityLocker, AccessControlEnumerableUpgradeable
      */
     bytes32 public schainHash;
 
-    /**
-     * @dev Amount of seconds after message sending
-     * when next message cannot be sent.
-     */
-    uint public timeLimitPerMessage;
+    // Disable slither check due to variable depreciation
+    // and unavailability of making it constant because
+    // it breaks upgradeability pattern.
+    // slither-disable-next-line constable-states
+    uint private _deprecatedTimeLimitPerMessage;
 
     /**
      * @dev Mapping of users who are allowed to send a message.
@@ -89,10 +93,30 @@ contract CommunityLocker is ICommunityLocker, AccessControlEnumerableUpgradeable
     // user address => timestamp of last message
     mapping(address => uint) public lastMessageTimeStamp;
 
+    /**
+     * @dev mainnet gas price(baseFee) value
+     */
     uint256 public mainnetGasPrice;
 
+    /**
+     * @dev Timestamp of previous set of mainnet gas price
+     */
     uint256 public gasPriceTimestamp;
 
+    /**
+     * @dev Amount of seconds after message sending
+     * when next message cannot be sent.
+     */
+    // schainHash   => time limit
+    mapping(bytes32 => uint) public timeLimitPerMessage;
+
+    /**
+     * @dev Timestamp of previous sent message by user during
+     * schain to schain transfers
+     */
+    // schainHash   =>           user  => timestamp
+    mapping(bytes32 => mapping(address => uint)) public lastMessageTimeStampToSchain;
+
     /**
      * @dev Emitted when a user becomes active.
      */
@@ -118,6 +142,19 @@ contract CommunityLocker is ICommunityLocker, AccessControlEnumerableUpgradeable
         uint newValue
     );
 
+    modifier checkUserBeforeTransfer(bytes32 chainHash, address user) {
+        uint256 lastTimestamp = lastMessageTimeStampToSchain[chainHash][user];
+        if (chainHash == MAINNET_HASH) {
+            require(activeUsers[user], "Recipient must be active");
+            lastTimestamp = lastMessageTimeStamp[user];
+        }
+        require(
+            lastTimestamp + timeLimitPerMessage[chainHash] < block.timestamp,
+            "Exceeded message rate limit"
+        );
+        _;
+    }
+
     /**
      * @dev Allows MessageProxy to post operational message from mainnet
      * or SKALE chains.
@@ -163,21 +200,24 @@ contract CommunityLocker is ICommunityLocker, AccessControlEnumerableUpgradeable
      * - Previous message sent by {receiver} must be sent earlier then {timeLimitPerMessage} seconds before current time
      * or there are no messages sent by {receiver}.
      */
-    function checkAllowedToSendMessage(address receiver) external override {
+    function checkAllowedToSendMessage(bytes32 chainHash, address receiver)
+        external
+        checkUserBeforeTransfer(chainHash, receiver)
+        override
+    {
         require(
-            tokenManagerLinker.hasTokenManager(ITokenManager(msg.sender)),
+            tokenManagerLinker.hasTokenManager(msg.sender),
             "Sender is not registered token manager"
         );
-        require(activeUsers[receiver], "Recipient must be active");
-        require(
-            lastMessageTimeStamp[receiver] + timeLimitPerMessage < block.timestamp,
-            "Trying to send messages too often"
-        );
-        lastMessageTimeStamp[receiver] = block.timestamp;
+        if (chainHash == MAINNET_HASH) {
+            lastMessageTimeStamp[receiver] = block.timestamp;
+        } else {
+            lastMessageTimeStampToSchain[chainHash][receiver] = block.timestamp;
+        }
     }
 
     /**
-     * @dev Set value of {timeLimitPerMessage}.
+     * @dev Set value of {timeLimitPerMessage} of given chain.
      *
      * Requirements:
      * 
@@ -185,14 +225,16 @@ contract CommunityLocker is ICommunityLocker, AccessControlEnumerableUpgradeable
      * 
      * Emits a {ConstantUpdated} event.
      */
-    function setTimeLimitPerMessage(uint newTimeLimitPerMessage) external override {
+    function setTimeLimitPerMessage(string memory chainName, uint newTimeLimitPerMessage) external override {
         require(hasRole(CONSTANT_SETTER_ROLE, msg.sender), "Not enough permissions to set constant");
+        bytes32 chainHash = keccak256(abi.encodePacked(chainName));
+        require(chainHash != schainHash, "Incorrect chain");
         emit ConstantUpdated(
             keccak256(abi.encodePacked("TimeLimitPerMessage")),
-            timeLimitPerMessage,
+            timeLimitPerMessage[chainHash],
             newTimeLimitPerMessage
         );
-        timeLimitPerMessage = newTimeLimitPerMessage;
+        timeLimitPerMessage[chainHash] = newTimeLimitPerMessage;
     }
 
     /**
@@ -247,7 +289,21 @@ contract CommunityLocker is ICommunityLocker, AccessControlEnumerableUpgradeable
         messageProxy = newMessageProxy;
         tokenManagerLinker = newTokenManagerLinker;
         schainHash = keccak256(abi.encodePacked(newSchainName));
-        timeLimitPerMessage = 5 minutes;
+        timeLimitPerMessage[MAINNET_HASH] = 5 minutes;
         communityPool = newCommunityPool;
     }
+
+    /**
+     * @dev Initialize timestamp after upgrade and should be removed after upgrade
+     *
+     * Requirements:
+     * Should be called only by address which hold DEFAULT_ADMIN_ROLE role
+     */
+    function initializeTimestamp() external override {
+        require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "Incorrect sender");
+        // Disable slither check due to moving data to the new data structure
+        // slither-disable-next-line uninitialized-state
+        timeLimitPerMessage[MAINNET_HASH] = _deprecatedTimeLimitPerMessage;
+        delete _deprecatedTimeLimitPerMessage;
+    }
 }
diff --git a/proxy/contracts/schain/DefaultAddresses.sol b/proxy/contracts/schain/DefaultAddresses.sol
new file mode 100644
index 000000000..e467be99d
--- /dev/null
+++ b/proxy/contracts/schain/DefaultAddresses.sol
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+
+/**
+ *   DefaultAddresses.sol - SKALE Interchain Messaging Agent
+ *   Copyright (C) 2022-Present SKALE Labs
+ *   @author Dmytro Stebaiev
+ *
+ *   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/>.
+ */
+
+ pragma solidity 0.8.16;
+
+library DefaultAddresses {
+                                                          // 17 0x00 bytes
+    address public constant ETHERBASE = address(0xd2bA3e << (17 * 8));
+                                                                       // 16 0x00 bytes
+    address public constant TOKEN_MANAGER_LINKER = address(0xD2aAA008 << (16 * 8));
+}
\ No newline at end of file
diff --git a/proxy/contracts/schain/MessageProxyForSchain.sol b/proxy/contracts/schain/MessageProxyForSchain.sol
index 4ce9bad84..625ccf1c4 100644
--- a/proxy/contracts/schain/MessageProxyForSchain.sol
+++ b/proxy/contracts/schain/MessageProxyForSchain.sol
@@ -27,15 +27,17 @@ import "@skalenetwork/etherbase-interfaces/IEtherbaseUpgradeable.sol";
 
 import "../MessageProxy.sol";
 import "./bls/SkaleVerifier.sol";
+import "./DefaultAddresses.sol";
+import "./TokenManagerLinker.sol";
 
 
 /**
  * @title MessageProxyForSchain
  * @dev Entry point for messages that come from mainnet or other SKALE chains
  * and contract that emits messages for mainnet or other SKALE chains.
- * 
+ *
  * Messages are submitted by IMA-agent and secured with threshold signature.
- * 
+ *
  * IMA-agent monitors events of {MessageProxyForSchain} and sends messages to other chains.
 
  * NOTE: 16 Agents
@@ -55,7 +57,7 @@ contract MessageProxyForSchain is MessageProxy, IMessageProxyForSchain {
     using EnumerableSetUpgradeable for EnumerableSetUpgradeable.AddressSet;
 
     IEtherbaseUpgradeable public constant ETHERBASE = IEtherbaseUpgradeable(
-        payable(0xd2bA3e0000000000000000000000000000000000)
+        payable(DefaultAddresses.ETHERBASE)
     );
     uint public constant MINIMUM_BALANCE = 1 ether;
 
@@ -99,6 +101,20 @@ contract MessageProxyForSchain is MessageProxy, IMessageProxyForSchain {
     string public version;
     bool public override messageInProgress;
 
+    /**
+     * @dev if receiver has no sFuil it's balance is topupped from etherbase for the value
+     * if the value is 0 MINIMUM_BALANCE is used
+     */
+    uint256 public minimumReceiverBalance;
+
+    /**
+     * @dev the event is emitted when value of receiver's minimum balance is changed
+     */
+    event MinimumReceiverBalanceChanged (
+        uint256 oldValue,
+        uint256 newValue
+    );
+
     /**
      * @dev Reentrancy guard for postIncomingMessages.
      */
@@ -111,9 +127,9 @@ contract MessageProxyForSchain is MessageProxy, IMessageProxyForSchain {
 
     /**
      * @dev Allows MessageProxy to register extra contract for being able to transfer messages from custom contracts.
-     * 
+     *
      * Requirements:
-     * 
+     *
      * - Function caller has to be granted with {EXTRA_CONTRACT_REGISTRAR_ROLE}.
      * - Destination chain hash cannot be equal to itself
      */
@@ -133,9 +149,9 @@ contract MessageProxyForSchain is MessageProxy, IMessageProxyForSchain {
     /**
      * @dev Allows MessageProxy to remove extra contract,
      * thus `extraContract` will no longer be available to transfer messages from chain to chain.
-     * 
+     *
      * Requirements:
-     * 
+     *
      * - Function caller has to be granted with {EXTRA_CONTRACT_REGISTRAR_ROLE}.
      * - Destination chain hash cannot be equal to itself
      */
@@ -156,9 +172,9 @@ contract MessageProxyForSchain is MessageProxy, IMessageProxyForSchain {
      * @dev Link external chain.
      *
      * NOTE: Mainnet is linked automatically.
-     * 
+     *
      * Requirements:
-     * 
+     *
      * - Function caller has to be granted with {CHAIN_CONNECTOR_ROLE}.
      * - Target chain must be different from the current.
      */
@@ -166,14 +182,14 @@ contract MessageProxyForSchain is MessageProxy, IMessageProxyForSchain {
         bytes32 chainHash = keccak256(abi.encodePacked(chainName));
         require(chainHash != schainHash, "Schain cannot connect itself");
         _addConnectedChain(chainHash);
-    }    
+    }
 
     /**
      * @dev Entry point for incoming messages.
      * This function is called by IMA-agent to deliver incoming messages from external chains.
-     * 
+     *
      * Requirements:
-     * 
+     *
      * - Origin chain has to be registered.
      * - Amount of messages must be no more than {MESSAGES_LENGTH}.
      * - Messages batch has to be signed with threshold signature.
@@ -184,7 +200,7 @@ contract MessageProxyForSchain is MessageProxy, IMessageProxyForSchain {
         string calldata fromChainName,
         uint256 startingCounter,
         Message[] calldata messages,
-        Signature calldata signature 
+        Signature calldata signature
     )
         external
         override(IMessageProxy, MessageProxy)
@@ -203,22 +219,42 @@ contract MessageProxyForSchain is MessageProxy, IMessageProxyForSchain {
         for (uint256 i = 0; i < messages.length; i++) {
             _callReceiverContract(fromChainHash, messages[i], startingCounter + 1);
         }
-        _topUpBalance();
+        _topUpSenderBalance();
     }
 
     /**
      * @dev Sets new version of contracts on schain
-     * 
+     *
      * Requirements:
-     * 
+     *
      * - `msg.sender` must be granted DEFAULT_ADMIN_ROLE.
      */
-    function setVersion(string calldata newVersion) external override {
-        require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "DEFAULT_ADMIN_ROLE is required");
+    function setVersion(string calldata newVersion) external override onlyOwner {
         emit VersionUpdated(version, newVersion);
         version = newVersion;
     }
 
+    /**
+     * @dev Sets a minimum balance of a receiver.
+     * If the balance is lower IMA tries to send sFuel to top up it.
+     */
+    function setMinimumReceiverBalance(uint256 balance) external override onlyConstantSetter {
+        emit MinimumReceiverBalanceChanged(minimumReceiverBalance, balance);
+        minimumReceiverBalance = balance;
+    }
+
+    /**
+     * @dev Sends sFuel to the `receiver` address to satisfy a minimum balance
+     */
+    function topUpReceiverBalance(address payable receiver) external override {
+        require(isContractRegistered(bytes32(0), msg.sender), "Sender is not registered");
+        uint256 balance = receiver.balance;
+        uint256 threshold = minimumReceiverBalance;
+        if (balance < threshold) {
+            _transferFromEtherbase(receiver, threshold - balance);
+        }
+    }
+
     /**
      * @dev Verify if the message metadata is valid.
      */
@@ -263,7 +299,8 @@ contract MessageProxyForSchain is MessageProxy, IMessageProxyForSchain {
         ] = ConnectedChainInfo(
             0,
             0,
-            true
+            true,
+            0
         );
 	    schainHash = keccak256(abi.encodePacked(schainName));
 
@@ -273,9 +310,9 @@ contract MessageProxyForSchain is MessageProxy, IMessageProxyForSchain {
 
     /**
      * @dev Unlink external SKALE chain.
-     * 
+     *
      * Requirements:
-     * 
+     *
      * - Function caller has to be granted with {CHAIN_CONNECTOR_ROLE}.
      * - Target chain must be different from Mainnet.
      */
@@ -294,9 +331,9 @@ contract MessageProxyForSchain is MessageProxy, IMessageProxyForSchain {
     /**
      * @dev This function is called by a smart contract
      * that wants to make a cross-chain call.
-     * 
+     *
      * Requirements:
-     * 
+     *
      * - Destination chain has to be registered.
      * - Sender contract has to be registered.
      */
@@ -372,20 +409,27 @@ contract MessageProxyForSchain is MessageProxy, IMessageProxyForSchain {
     }
 
     /**
-     * @dev Move skETH from Etherbase if the balance is too low
+     * @dev Move SFuel from Etherbase if the sender balance is too low
      */
-    function _topUpBalance() private {
+    function _topUpSenderBalance() private {
         uint balance = msg.sender.balance + gasleft() * tx.gasprice;
+        if (balance < MINIMUM_BALANCE) {
+            _transferFromEtherbase(payable(msg.sender), MINIMUM_BALANCE - balance);
+        }
+    }
+
+    /**
+     * @dev Move SFuel from Etherbase to `target` address
+     */
+    function _transferFromEtherbase(address payable target, uint256 value) private {
         IEtherbaseUpgradeable etherbase = _getEtherbase();
         if (address(etherbase).isContract()
-            && etherbase.hasRole(etherbase.ETHER_MANAGER_ROLE(), address(this)) 
-            && balance < MINIMUM_BALANCE
+            && etherbase.hasRole(etherbase.ETHER_MANAGER_ROLE(), address(this))
         ) {
-            uint missingAmount = MINIMUM_BALANCE - balance;
-            if (missingAmount < address(etherbase).balance) {
-                etherbase.partiallyRetrieve(payable(msg.sender), missingAmount);
+            if (value < address(etherbase).balance) {
+                etherbase.partiallyRetrieve(target, value);
             } else {
-                etherbase.retrieve(payable(msg.sender));
+                etherbase.retrieve(target);
             }
         }
     }
@@ -402,5 +446,5 @@ contract MessageProxyForSchain is MessageProxy, IMessageProxyForSchain {
             message.data
         );
         return keccak256(data);
-    }    
+    }
 }
diff --git a/proxy/contracts/schain/TokenManagerLinker.sol b/proxy/contracts/schain/TokenManagerLinker.sol
index ac9501c2a..048dae3e2 100644
--- a/proxy/contracts/schain/TokenManagerLinker.sol
+++ b/proxy/contracts/schain/TokenManagerLinker.sol
@@ -172,11 +172,12 @@ contract TokenManagerLinker is ITokenManagerLinker, AccessControlEnumerableUpgra
     /**
      * @dev Check if {tokenManager} is registered in IMA.
      */
-    function hasTokenManager(ITokenManager tokenManager) external view override returns (bool) {
+    function hasTokenManager(address tokenManager) external view override returns (bool) {
+        // TODO: optimize algorithm to save gas
         uint index;
         uint length = tokenManagers.length;
         for (index = 0; index < length; index++) {
-            if (tokenManagers[index] == tokenManager) {
+            if (address(tokenManagers[index]) == tokenManager) {
                 return true;
             }
         }
diff --git a/proxy/contracts/schain/TokenManagers/TokenManagerERC1155.sol b/proxy/contracts/schain/TokenManagers/TokenManagerERC1155.sol
index dc463d18b..05aaab524 100644
--- a/proxy/contracts/schain/TokenManagers/TokenManagerERC1155.sol
+++ b/proxy/contracts/schain/TokenManagers/TokenManagerERC1155.sol
@@ -111,7 +111,7 @@ contract TokenManagerERC1155 is
         external
         override
     {
-        communityLocker.checkAllowedToSendMessage(msg.sender);
+        communityLocker.checkAllowedToSendMessage(MAINNET_HASH, msg.sender);
         _exit(MAINNET_HASH, depositBox, contractOnMainnet, msg.sender, id, amount);
     }
 
@@ -128,7 +128,7 @@ contract TokenManagerERC1155 is
         external
         override
     {
-        communityLocker.checkAllowedToSendMessage(msg.sender);
+        communityLocker.checkAllowedToSendMessage(MAINNET_HASH, msg.sender);
         _exitBatch(MAINNET_HASH, depositBox, contractOnMainnet, msg.sender, ids, amounts);
     }
 
@@ -149,6 +149,7 @@ contract TokenManagerERC1155 is
         rightTransaction(targetSchainName, msg.sender)
     {
         bytes32 targetSchainHash = keccak256(abi.encodePacked(targetSchainName));
+        communityLocker.checkAllowedToSendMessage(targetSchainHash, msg.sender);
         _exit(targetSchainHash, tokenManagers[targetSchainHash], contractOnMainnet, msg.sender, id, amount);
     }
 
@@ -169,6 +170,7 @@ contract TokenManagerERC1155 is
         rightTransaction(targetSchainName, msg.sender)
     {
         bytes32 targetSchainHash = keccak256(abi.encodePacked(targetSchainName));
+        communityLocker.checkAllowedToSendMessage(targetSchainHash, msg.sender);
         _exitBatch(targetSchainHash, tokenManagers[targetSchainHash], contractOnMainnet, msg.sender, ids, amounts);
     }
 
@@ -355,6 +357,7 @@ contract TokenManagerERC1155 is
             _asSingletonArray(id),
             _asSingletonArray(amount)
         );
+        messageProxy.topUpReceiverBalance(payable(receiver));
         return receiver;
     }
 
@@ -405,6 +408,7 @@ contract TokenManagerERC1155 is
             contractOnSchain.mintBatch(receiver, ids, amounts, "");
         }
         emit ERC1155TokenReceived(fromChainHash, token, address(contractOnSchain), ids, amounts);
+        messageProxy.topUpReceiverBalance(payable(receiver));
         return receiver;
     }
 
diff --git a/proxy/contracts/schain/TokenManagers/TokenManagerERC20.sol b/proxy/contracts/schain/TokenManagers/TokenManagerERC20.sol
index 6d72b1fdb..55c0cbbd0 100644
--- a/proxy/contracts/schain/TokenManagers/TokenManagerERC20.sol
+++ b/proxy/contracts/schain/TokenManagers/TokenManagerERC20.sol
@@ -99,7 +99,7 @@ contract TokenManagerERC20 is TokenManager, ITokenManagerERC20 {
         external
         override
     {
-        communityLocker.checkAllowedToSendMessage(msg.sender);
+        communityLocker.checkAllowedToSendMessage(MAINNET_HASH, msg.sender);
         _exit(MAINNET_HASH, depositBox, contractOnMainnet, msg.sender, amount);
     }
 
@@ -119,6 +119,7 @@ contract TokenManagerERC20 is TokenManager, ITokenManagerERC20 {
         rightTransaction(targetSchainName, msg.sender)
     {
         bytes32 targetSchainHash = keccak256(abi.encodePacked(targetSchainName));
+        communityLocker.checkAllowedToSendMessage(targetSchainHash, msg.sender);
         _exit(targetSchainHash, tokenManagers[targetSchainHash], contractOnMainnet, msg.sender, amount);
     }
 
@@ -253,6 +254,7 @@ contract TokenManagerERC20 is TokenManager, ITokenManagerERC20 {
             );
         }
         emit ERC20TokenReceived(fromChainHash, token, address(contractOnSchain), amount);
+        messageProxy.topUpReceiverBalance(payable(receiver));
         return receiver;
     }
 
diff --git a/proxy/contracts/schain/TokenManagers/TokenManagerERC721.sol b/proxy/contracts/schain/TokenManagers/TokenManagerERC721.sol
index 3f815e833..0315bfc6d 100644
--- a/proxy/contracts/schain/TokenManagers/TokenManagerERC721.sol
+++ b/proxy/contracts/schain/TokenManagers/TokenManagerERC721.sol
@@ -98,7 +98,7 @@ contract TokenManagerERC721 is TokenManager, ITokenManagerERC721 {
         external
         override
     {
-        communityLocker.checkAllowedToSendMessage(msg.sender);
+        communityLocker.checkAllowedToSendMessage(MAINNET_HASH, msg.sender);
         _exit(MAINNET_HASH, depositBox, contractOnMainnet, msg.sender, tokenId);
     }
 
@@ -118,6 +118,7 @@ contract TokenManagerERC721 is TokenManager, ITokenManagerERC721 {
         rightTransaction(targetSchainName, msg.sender)
     {
         bytes32 targetSchainHash = keccak256(abi.encodePacked(targetSchainName));
+        communityLocker.checkAllowedToSendMessage(targetSchainHash, msg.sender);
         _exit(targetSchainHash, tokenManagers[targetSchainHash], contractOnMainnet, msg.sender, tokenId);
     }
 
@@ -245,6 +246,7 @@ contract TokenManagerERC721 is TokenManager, ITokenManagerERC721 {
             contractOnSchain.mint(receiver, tokenId);
         }
         emit ERC721TokenReceived(fromChainHash, token, address(contractOnSchain), tokenId);
+        messageProxy.topUpReceiverBalance(payable(receiver));
         return receiver;
     }
 
diff --git a/proxy/contracts/schain/TokenManagers/TokenManagerERC721WithMetadata.sol b/proxy/contracts/schain/TokenManagers/TokenManagerERC721WithMetadata.sol
index 51807eeef..076322482 100644
--- a/proxy/contracts/schain/TokenManagers/TokenManagerERC721WithMetadata.sol
+++ b/proxy/contracts/schain/TokenManagers/TokenManagerERC721WithMetadata.sol
@@ -120,6 +120,7 @@ contract TokenManagerERC721WithMetadata is TokenManagerERC721 {
             contractOnSchain.setTokenURI(tokenId, tokenURI);
         }
         emit ERC721TokenReceived(fromChainHash, token, address(contractOnSchain), tokenId);
+        messageProxy.topUpReceiverBalance(payable(receiver));
         return receiver;
     }
 
diff --git a/proxy/contracts/schain/TokenManagers/TokenManagerEth.sol b/proxy/contracts/schain/TokenManagers/TokenManagerEth.sol
index 28f3aaf03..2a339bd2c 100644
--- a/proxy/contracts/schain/TokenManagers/TokenManagerEth.sol
+++ b/proxy/contracts/schain/TokenManagers/TokenManagerEth.sol
@@ -54,7 +54,7 @@ contract TokenManagerEth is TokenManager, ITokenManagerEth {
      * EthErc20 tokens are burned on schain and ETH are unlocked on mainnet for {to} address.
      */
     function exitToMain(uint256 amount) external override {
-        communityLocker.checkAllowedToSendMessage(msg.sender);
+        communityLocker.checkAllowedToSendMessage(MAINNET_HASH, msg.sender);
         _exit(MAINNET_HASH, depositBox, msg.sender, amount);
     }
 
@@ -81,6 +81,7 @@ contract TokenManagerEth is TokenManager, ITokenManagerEth {
         address receiver = decodedMessage.receiver;
         require(receiver != address(0), "Incorrect receiver");
         ethErc20.mint(receiver, decodedMessage.amount);
+        messageProxy.topUpReceiverBalance(payable(receiver));
     }
 
     /**
diff --git a/proxy/gas/calculateGas.ts b/proxy/gas/calculateGas.ts
index f621e9ac9..29cfba43e 100644
--- a/proxy/gas/calculateGas.ts
+++ b/proxy/gas/calculateGas.ts
@@ -86,7 +86,7 @@ import { deployTokenManagerERC1155 } from "../test/utils/deploy/schain/tokenMana
 import { deployMessageProxyForSchain } from "../test/utils/deploy/schain/messageProxyForSchain";
 import { deployMessages } from "../test/utils/deploy/messages";
 
-import { randomString, stringValue, getPublicKey } from "../test/utils/helper";
+import { stringValue, getPublicKey } from "../test/utils/helper";
 
 import { ethers, web3 } from "hardhat";
 import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address";
diff --git a/proxy/migrations/deployMainnet.ts b/proxy/migrations/deployMainnet.ts
index f9cf83f33..46238ae28 100644
--- a/proxy/migrations/deployMainnet.ts
+++ b/proxy/migrations/deployMainnet.ts
@@ -100,9 +100,8 @@ export const contracts = [
     "DepositBoxEth",
     "DepositBoxERC20",
     "DepositBoxERC721",
-    "DepositBoxERC1155"
-
-    // "DepositBoxERC721WithMetadata"
+    "DepositBoxERC1155",
+    "DepositBoxERC721WithMetadata"
 ]
 
 async function main() {
diff --git a/proxy/migrations/deploySchain.ts b/proxy/migrations/deploySchain.ts
index c73d15c21..014d89236 100644
--- a/proxy/migrations/deploySchain.ts
+++ b/proxy/migrations/deploySchain.ts
@@ -40,7 +40,8 @@ import {
     TokenManagerERC721,
     TokenManagerEth,
     TokenManagerLinker,
-    TokenManagerERC721WithMetadata
+    TokenManagerERC721WithMetadata,
+    MessageProxyForSchainWithoutSignature
 } from '../typechain';
 import { TokenManagerERC1155 } from '../typechain/TokenManagerERC1155';
 import { getVersion } from './tools/version';
@@ -154,17 +155,19 @@ async function main() {
     deployed.set( "KeyStorage", { address: keyStorage.address, interface: keyStorage.interface } );
     console.log("Contract KeyStorage deployed to", keyStorage.address);
 
-    let messageProxy: Contract;
+    let messageProxy: MessageProxyForSchain | MessageProxyForSchainWithoutSignature;
     if( process.env.NO_SIGNATURES === "true" ) {
         console.log( "Deploy IMA without signature verification" );
         console.log("Deploy MessageProxyForSchainWithoutSignature");
-        messageProxy = await (await ethers.getContractFactory("MessageProxyForSchainWithoutSignature")).deploy(schainName);
+        messageProxy = await
+            (await ethers.getContractFactory("MessageProxyForSchainWithoutSignature"))
+            .deploy(schainName) as MessageProxyForSchainWithoutSignature;
     } else {
         console.log("Deploy MessageProxyForSchain");
         messageProxy = await upgrades.deployProxy(
             await ethers.getContractFactory("MessageProxyForSchain"),
             [keyStorage.address, schainName]
-        );
+        ) as MessageProxyForSchain;
     }
     await messageProxy.deployTransaction.wait();
     deployed.set( "MessageProxyForSchain", { address: messageProxy.address, interface: messageProxy.interface } );
@@ -172,7 +175,7 @@ async function main() {
 
     try {
         console.log(`Set version ${version}`)
-        await (await (messageProxy as MessageProxyForSchain).setVersion(version)).wait();
+        await (await messageProxy.setVersion(version)).wait();
     } catch {
         console.log("Failed to set ima version on schain");
     }
diff --git a/proxy/migrations/upgradeSchain.ts b/proxy/migrations/upgradeSchain.ts
index b1cbbd7d8..72ac293fc 100644
--- a/proxy/migrations/upgradeSchain.ts
+++ b/proxy/migrations/upgradeSchain.ts
@@ -1,6 +1,11 @@
-import { contracts } from "./deploySchain";
+import { getManifestAdmin } from "@openzeppelin/hardhat-upgrades/dist/admin";
+import { CommunityLocker } from "../typechain/CommunityLocker";
+import { contracts, getContractKeyInAbiFile } from "./deploySchain";
 import { manifestSetup } from "./generateManifest";
+import { encodeTransaction } from "./tools/multiSend";
 import { upgrade } from "./upgrade";
+import chalk from "chalk";
+import { ethers } from "hardhat";
 
 function stringValue(value: string | undefined) {
     if (value) {
@@ -20,7 +25,24 @@ async function main() {
             // deploying of new contracts
         },
         async( safeTransactions, abi ) => {
-            // initialization
+            const communityLockerName = "CommunityLocker";
+            const communityLockerFactory = await ethers.getContractFactory(communityLockerName);
+            const communityLockerAddress = abi[getContractKeyInAbiFile(communityLockerName) + "_address"];
+            let communityLocker;
+            if (communityLockerAddress) {
+                communityLocker = communityLockerFactory.attach(communityLockerAddress) as CommunityLocker;
+                console.log(chalk.yellow("Prepare transaction to initialize timestamp"));
+                safeTransactions.push(encodeTransaction(
+                    0,
+                    communityLockerAddress,
+                    0,
+                    communityLocker.interface.encodeFunctionData("initializeTimestamp")
+                ));
+            } else {
+                console.log(chalk.red("CommunityLocker was not found!"));
+                console.log(chalk.red("Check your abi!!!"));
+                process.exit(1);
+            }
         },
         "proxySchain"
     );
diff --git a/proxy/package.json b/proxy/package.json
index 37a01e933..6287c248b 100644
--- a/proxy/package.json
+++ b/proxy/package.json
@@ -26,7 +26,7 @@
         "@openzeppelin/contracts-upgradeable": "^4.7.1",
         "@openzeppelin/hardhat-upgrades": "^1.9.0",
         "@skalenetwork/etherbase-interfaces": "^0.0.1-develop.20",
-        "@skalenetwork/ima-interfaces": "^1.0.0-withdrawals-blocking.1",
+        "@skalenetwork/ima-interfaces": "^1.1.0-onboarding.0",
         "@skalenetwork/skale-manager-interfaces": "1.0.0-develop.1",
         "axios": "^0.21.4",
         "dotenv": "^10.0.0",
diff --git a/proxy/predeployed/src/ima_predeployed/contracts/community_locker.py b/proxy/predeployed/src/ima_predeployed/contracts/community_locker.py
index 850d40944..9dff37b2c 100644
--- a/proxy/predeployed/src/ima_predeployed/contracts/community_locker.py
+++ b/proxy/predeployed/src/ima_predeployed/contracts/community_locker.py
@@ -15,6 +15,7 @@ class CommunityLockerGenerator(AccessControlEnumerableGenerator):
     META_FILENAME = 'CommunityLocker.meta.json'
     DEFAULT_ADMIN_ROLE = (0).to_bytes(32, 'big')
     DEFAULT_TIME_LIMIT_SEC = 5 * 60
+    MAINNET_HASH = Web3.solidityKeccak(['string'], ['Mainnet'])
 
     # ---------- storage ----------
     # --------Initializable--------
@@ -42,9 +43,13 @@ class CommunityLockerGenerator(AccessControlEnumerableGenerator):
     # 202:  tokenManagerLinker
     # 203:  communityPool
     # 204:  schainHash
-    # 205:  timeLimitPerMessage
-    # 206:  _unfrozenUsers
-    # 207:  _lastMessageTimeStamp
+    # 205:  _deprecatedTimeLimitPerMessage
+    # 206:  activeUsers
+    # 207:  lastMessageTimeStamp
+    # 208:  mainnetGasPrice
+    # 209:  gasPriceTimestamp
+    # 210:  timeLimitPerMessage
+    # 211:  lastMessageTimeStampToSchain
 
     INITIALIZED_SLOT = 0
     ROLES_SLOT = 101
@@ -53,7 +58,7 @@ class CommunityLockerGenerator(AccessControlEnumerableGenerator):
     TOKEN_MANAGER_LINKER_SLOT = AccessControlEnumerableGenerator.next_slot(MESSAGE_PROXY_SLOT)
     COMMUNITY_POOL_SLOT = AccessControlEnumerableGenerator.next_slot(TOKEN_MANAGER_LINKER_SLOT)
     SCHAIN_HASH_SLOT = AccessControlEnumerableGenerator.next_slot(COMMUNITY_POOL_SLOT)
-    TIME_LIMIT_PER_MESSAGE_SLOT = AccessControlEnumerableGenerator.next_slot(SCHAIN_HASH_SLOT)
+    TIME_LIMIT_PER_MESSAGE_SLOT = 210
 
     def __init__(self):
         generator = CommunityLockerGenerator.from_hardhat_artifact(
@@ -77,7 +82,9 @@ def generate_storage(cls, **kwargs) -> Dict[str, str]:
         cls._write_address(storage, cls.COMMUNITY_POOL_SLOT, community_pool_address)
 
         cls._write_bytes32(storage, cls.SCHAIN_HASH_SLOT, Web3.solidityKeccak(['string'], [schain_name]))
-        cls._write_uint256(storage, cls.TIME_LIMIT_PER_MESSAGE_SLOT, cls.DEFAULT_TIME_LIMIT_SEC)
+        time_limit_per_message_slot = AccessControlEnumerableGenerator.calculate_mapping_value_slot(
+            cls.TIME_LIMIT_PER_MESSAGE_SLOT, cls.MAINNET_HASH, 'bytes32')
+        cls._write_uint256(storage, time_limit_per_message_slot, cls.DEFAULT_TIME_LIMIT_SEC)
 
         return storage
 
diff --git a/proxy/yarn.lock b/proxy/yarn.lock
index e82a8e181..034fb1cd3 100644
--- a/proxy/yarn.lock
+++ b/proxy/yarn.lock
@@ -757,10 +757,10 @@
   resolved "https://registry.yarnpkg.com/@skalenetwork/etherbase-interfaces/-/etherbase-interfaces-0.0.1-develop.20.tgz#33f61e18d695fd47063aa39dce4df335d26b9528"
   integrity sha512-j3xnuQtOtjvjAoUMJgSUFxRa9/Egkg1RyA8r6PjcEb33VksE4LWLBy0PNFUFehLZv48595JROTcViGeXXwg5HQ==
 
-"@skalenetwork/ima-interfaces@^1.0.0-withdrawals-blocking.1":
-  version "1.0.0-withdrawals-blocking.1"
-  resolved "https://registry.yarnpkg.com/@skalenetwork/ima-interfaces/-/ima-interfaces-1.0.0-withdrawals-blocking.1.tgz#f973c63a74fa0bca3ee72a967d7e2aa80680fa4a"
-  integrity sha512-pETNThx9NDGdmbdUVXmrf58Fw883SglWak1cmo+FIhKzDrQm+cTxZhX9xdrBoWy2WDmNk37ZmseMx0v454ozCg==
+"@skalenetwork/ima-interfaces@^1.1.0-onboarding.0":
+  version "1.1.0-onboarding.0"
+  resolved "https://registry.yarnpkg.com/@skalenetwork/ima-interfaces/-/ima-interfaces-1.1.0-onboarding.0.tgz#1885bf4f32e8b3d476ed65a9a1e4504116bdef4e"
+  integrity sha512-vlucoXUlVZoISB3aXsn+yVxkfTncqqfxhVeG5g74C5jhl0pU8Ys5zaIE/YuiTmj4Noq57MfGeh+PQ+yzjxNihA==
   dependencies:
     "@skalenetwork/skale-manager-interfaces" "^0.1.2"
 


Test patch:
diff --git a/npms/skale-cool-socket/test.js b/npms/skale-cool-socket/test.js
new file mode 100644
index 000000000..dd9183dd2
--- /dev/null
+++ b/npms/skale-cool-socket/test.js
@@ -0,0 +1,164 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+
+/**
+ * @license
+ * SKALE COOL SOCKET
+ *
+ * This program 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.
+ *
+ * This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @file test.js
+ * @copyright SKALE Labs 2019-Present
+ */
+
+const network_layer = require( "./socket.js" );
+const { TestServer } = require( "./test_server.js" );
+const { Worker } = require( "worker_threads" );
+const { settings } = require( "./settings.js" );
+const ws = require( "ws" );
+// const wrtc = require( "wrtc" );
+
+const joTestMessage = { "method": "echo", "message": "Please echo this message!" };
+
+const sleep = ( milliseconds ) => { return new Promise( resolve => setTimeout( resolve, milliseconds ) ); };
+
+async function test_local() {
+    console.log( "Local test" );
+    const strEndPoint = "local_endpoint";
+    const acceptor = new network_layer.LocalSocketServerAcceptor( strEndPoint );
+    const server = new TestServer( acceptor );
+    const client = new network_layer.LocalSocketClientPipe( strEndPoint );
+    client.on( "message", function( eventData ) {
+        const joMessage = eventData.message;
+        console.log( "CLIENT <<<", JSON.stringify( joMessage ) );
+        client.disconnect();
+        console.log( " " );
+    } );
+    await sleep( 1 );
+    console.log( "CLIENT >>>", JSON.stringify( joTestMessage ) );
+    client.send( joTestMessage );
+    await sleep( 100 );
+    const joReturnValue = {
+        server: server,
+        client: client
+    };
+    return joReturnValue;
+}
+
+async function test_worker() {
+    console.log( "Worker test" );
+    const url = "local_worker_server";
+    const worker = new Worker( "./test_worker.js" );
+    console.log( "Will connect to " + url );
+    worker.on( "message", jo => {
+        if( network_layer.out_of_worker_apis.on_message( worker, jo ) )
+            return;
+    } );
+    const client = new network_layer.OutOfWorkerSocketClientPipe( url, worker );
+    client.on( "message", function( eventData ) {
+        const joMessage = eventData.message;
+        console.log( "CLIENT <<<", JSON.stringify( joMessage ) );
+        client.disconnect();
+        worker.terminate();
+        console.log( " " );
+    } );
+    await sleep( 100 );
+    console.log( "CLIENT >>>", JSON.stringify( joTestMessage ) );
+    client.send( joTestMessage );
+    await sleep( 100 );
+    const joReturnValue = {
+        worker: worker,
+        client: client
+    };
+    return joReturnValue;
+}
+
+async function test_web_socket() {
+    console.log( "Web socket test" );
+    network_layer.set_ws_mod( ws );
+    const nPort = 33123;
+    const url = ( settings.net.secure ? "wss" : "ws" ) + "://127.0.0.1:" + nPort;
+    const key = settings.net.secure ? fs.readFileSync( "./self-signed/self-signed-key.pem", "utf8" ) : null;
+    const cert = settings.net.secure ? fs.readFileSync( "./self-signed/self-signed-cert.pem", "utf8" ) : null;
+    const acceptor = new network_layer.WebSocketServerAcceptor( nPort, key, cert );
+    const server = new TestServer( acceptor );
+    const client = new network_layer.WebSocketClientPipe( url );
+    client.on( "message", function( eventData ) {
+        const joMessage = eventData.message;
+        console.log( "CLIENT <<<", JSON.stringify( joMessage ) );
+        client.disconnect();
+        console.log( " " );
+    } );
+    await sleep( 100 );
+    console.log( "CLIENT >>>", JSON.stringify( joTestMessage ) );
+    client.send( joTestMessage );
+    await sleep( 100 );
+    const joReturnValue = {
+        server: server,
+        client: client
+    };
+    return joReturnValue;
+}
+
+// async function test_wrtc() {
+//     //
+//     // Needed in package.json: "wrtc": "^0.4.6",
+//     //
+//     // ## Coturn - own STUN and/or TURN server
+//     //
+//     // See <https://github.com/coturn/coturn> and see <https://www.allerstorfer.at/install-coturn-on-ubuntu/>
+//     // Explanation for Mac OSX is <https://medium.com/@ittipon.bay/how-to-setup-coturn-for-mac-a3c4a6ba4db8>
+//     //
+//     // Install via **sudo apt-get install coturn** or **brew install coturn**.
+//     // Run **sudo nano /etc/turnserver.conf** and specify described in <https://www.allerstorfer.at/install-coturn-on-ubuntu/>,
+//     // realm **realm=origin/realm**, **no-tls** and **no-dtls**. Also set **listening-ip** to **0.0.0.0**
+//     //
+//     // **STUN** config entry is **{ urls: "stun:127.0.0.1:3478", username: "webrtc", credential: "qwerty" }**.
+//     // **TURN** config entry is **{ urls: "turn:127.0.0.1:3478", username: "webrtc", credential: "qwerty" }**.
+//     //
+//     console.log( "Web RTC test" );
+//     network_layer.set_ws_mod( ws );
+//     network_layer.set_wrtc_mod( wrtc );
+//     const url = null; // null here means url will be got from settings
+//     const acceptor = new network_layer.WebRTCServerAcceptor( url );
+//     const server = new TestServer( acceptor );
+//     server.on( "dispose", function() { console.log( "disposed", url ); } );
+//     console.log( "Will connect to " + url );
+//     const client = new network_layer.WebRTCClientPipe( url );
+//     client.on( "message", function( eventData ) {
+//         const joMessage = eventData.message;
+//         console.log( "CLIENT <<<", JSON.stringify( joMessage ) );
+//         client.disconnect();
+//         console.log( " " );
+//     } );
+//     await sleep( 1000 );
+//     console.log( "CLIENT >>>", JSON.stringify( joTestMessage ) );
+//     client.send( joTestMessage );
+//     await sleep( 1000 );
+//     const joReturnValue = {
+//         server: server,
+//         client: client
+//     };
+//     return joReturnValue;
+// }
+
+async function test() {
+    await test_local();
+    await test_worker();
+    await test_web_socket();
+    // await test_wrtc();
+    process.exit( 0 );
+}
+test();
diff --git a/npms/skale-cool-socket/test_server.js b/npms/skale-cool-socket/test_server.js
new file mode 100644
index 000000000..32283918d
--- /dev/null
+++ b/npms/skale-cool-socket/test_server.js
@@ -0,0 +1,47 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+
+/**
+ * @license
+ * SKALE COOL SOCKET
+ *
+ * This program 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.
+ *
+ * This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @file test_server.js
+ * @copyright SKALE Labs 2019-Present
+ */
+
+const { Server } = require( "./server.js" );
+
+class TestServer extends Server {
+    constructor( acceptor ) {
+        super( acceptor );
+        const self = this;
+        self.mapApiHandlers.echo = function( joMessage, joAnswer, eventData, socket ) {
+            console.log( "SERVER <<<", JSON.stringify( joMessage ) );
+            joAnswer.message = joMessage.message ? joMessage.message : "<no message echo>";
+            console.log( "SERVER >>>", JSON.stringify( joAnswer ) );
+            return joAnswer;
+        };
+    }
+    dispose() {
+        this.isDisposing = true;
+        super.dispose();
+    }
+};
+
+module.exports = {
+    TestServer: TestServer
+};
diff --git a/npms/skale-cool-socket/test_signaling_server.js b/npms/skale-cool-socket/test_signaling_server.js
new file mode 100644
index 000000000..736839c19
--- /dev/null
+++ b/npms/skale-cool-socket/test_signaling_server.js
@@ -0,0 +1,734 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+
+/**
+ * @license
+ * SKALE COOL SOCKET
+ *
+ * This program 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.
+ *
+ * This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @file test_signaling_server.js
+ * @copyright SKALE Labs 2019-Present
+ */
+
+const fs = require( "fs" );
+
+const https_loaded_mod = require( "https" );
+const ws_loaded_mod = require( "ws" );
+const wrtc_loaded_mod = require( "wrtc" );
+
+const network_layer = require( "./socket.js" );
+const { settings } = require( "./settings.js" );
+const { UniversalDispatcherEvent, EventDispatcher } = require( "./event_dispatcher.js" );
+const utils = require( "./utils.js" );
+
+// const connect = connect_loaded_mod; // .default;
+// const serveStatic = serveStatic_loaded_mod; // .default;
+const https_mod = https_loaded_mod; // .default;
+const ws_mod = ws_loaded_mod; // .default;
+const wrtc_mod = wrtc_loaded_mod; // .default;
+
+network_layer.set_https_mod( https_mod );
+network_layer.set_ws_mod( ws_mod );
+network_layer.set_wrtc_mod( wrtc_mod );
+
+console.log( "Test signaling server application..." );
+process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class SignalingClient extends EventDispatcher {
+    constructor( idRtcParticipant, strRole, signalingSpace, socket ) {
+        super();
+        this.isDisposed = false;
+        this.idRtcParticipant = "" + ( ( idRtcParticipant && typeof idRtcParticipant == "string" && idRtcParticipant.length > 0 ) ? idRtcParticipant : "" );
+        this.isCreator = ( strRole == "creator" ) ? true : false;
+        this.isJoiner = ( strRole == "joiner" ) ? true : false;
+        this.signalingSpace = signalingSpace;
+        this.socket = socket;
+        socket.signalingClient = this;
+        if( this.isCreator )
+            this.signalingSpace.idSomebodyCreator = "" + this.idRtcParticipant;
+        this.signalingSpace.map_clients[this.idRtcParticipant] = this;
+        this.idSpace = "" + this.signalingSpace.idSpace;
+        this.idCategory = "" + this.signalingSpace.signalingCategory.idCategory;
+        this.isFetchingOffer = false;
+        this.timerFetchingOffer = null;
+        this.fetchingOfferStepNumber = 0;
+        if( settings.logging.net.signaling.objectLifetime )
+            console.log( "New signaling client \"" + this.idRtcParticipant + "\" in signaling space \"" + this.idSpace + "\" in signaling category \"" + this.idCategory + "\" using socket " + this.socket.strSavedRemoteAddress );
+        this.signalingSpace.dispatchEvent( new UniversalDispatcherEvent( "clientAdded", { detail: { signalingClient: this } } ) );
+    }
+    dispose() {
+        if( this.isDisposed )
+            return;
+        this.isDisposing = true;
+        if( settings.logging.net.signaling.objectLifetime )
+            console.log( "Disposing signaling client \"" + this.idRtcParticipant + "\" in signaling space \"" + this.idSpace + "\" in signaling category \"" + this.idCategory + "\"" );
+        this.disconnect();
+        if( this.idRtcParticipant ) {
+            if( this.signalingSpace ) {
+                this.signalingSpace.dispatchEvent( new UniversalDispatcherEvent( "clientRemoved", { detail: { signalingClient: this } } ) );
+                delete this.signalingSpace.map_clients[this.idRtcParticipant];
+            }
+            this.idRtcParticipant = null;
+        }
+        if( this.isCreator ) {
+            if( this.signalingSpace )
+                this.signalingSpace.idSomebodyCreator = "";
+        }
+        this.isCreator = false;
+        this.isJoiner = false;
+        this.idSpace = null;
+        this.idCategory = null;
+        this.signalingSpace.autoDispose();
+        this.signalingSpace = null;
+        super.dispose();
+    }
+    disconnect() {
+        if( this.isDisposed || ( !this.socket ) )
+            return;
+        this.offerDiscoveryStop();
+        let bPass = false, anyError = null;
+        try {
+            this.socket.disconnect();
+            bPass = true;
+        } catch ( err ) {
+            anyError = err;
+        }
+        if( ! bPass ) {
+            if( settings.logging.net.signaling.error )
+                console.warn( "Signaling client \"" + this.idRtcParticipant + "\" in signaling space \"" + this.idSpace + "\" in signaling category \"" + this.idCategory + "\" - web socket signaling pipe termination error", anyError );
+        }
+        //
+        this.socket.signalingClient = null;
+        this.socket = null;
+        if( settings.logging.net.signaling.disconnect )
+            console.warn( "Disconnected/force signaling client \"" + this.idRtcParticipant + "\" in signaling space \"" + this.idSpace + "\" in signaling category \"" + this.idCategory + "\"" );
+    }
+    onPipeClose( socket ) {
+        if( this.isDisposed )
+            return;
+        if( settings.logging.net.signaling.disconnect )
+            console.warn( "Disconnected/pipe signaling client \"" + this.idRtcParticipant + "\" in signaling space \"" + this.idSpace + "\" in signaling category \"" + this.idCategory + "\"" );
+        this.offerDiscoveryStop();
+        this.dispose();
+    }
+    onPipeError( socket ) {
+        if( this.isDisposed )
+            return;
+        if( settings.logging.net.signaling.error )
+            console.warn( "Disconnected/error signaling client \"" + this.idRtcParticipant + "\" in signaling space \"" + this.idSpace + "\" in signaling category \"" + this.idCategory + "\"" );
+        this.offerDiscoveryStop();
+        this.dispose();
+    }
+    offerDiscoveryStop() {
+        if( ! this.isFetchingOffer )
+            return;
+        if( this.timerFetchingOffer ) {
+            clearInterval( this.timerFetchingOffer );
+            this.timerFetchingOffer = null;
+        }
+        this.isFetchingOffer = false;
+        this.fetchingOfferStepNumber = 0;
+    }
+    offerDiscoveryStart( joMessage ) {
+        if( this.isFetchingOffer )
+            return;
+        this.isFetchingOffer = true;
+        this.offerDiscoveryStep( joMessage );
+    }
+    offerDiscoveryStep( joMessage ) {
+        if( ! this.isFetchingOffer )
+            return;
+        let joAnswer = null;
+        ++ this.fetchingOfferStepNumber;
+        try {
+            // let signalingCategory = null;
+            const signalingSpace = this.signalingSpace;
+            // if( signalingSpace )
+            //     signalingCategory = signalingSpace.signalingCategory;
+            const joOfferInfo = signalingSpace.fetchPublishedOffer();
+            if( ! joOfferInfo ) {
+                if( settings.logging.net.signaling.offerDiscoveryStepFail )
+                    console.warn( "Signaling client socket \"" + this.socket.strSavedRemoteAddress + "\" did not found offer at step", this.fetchingOfferStepNumber, "of", settings.net.rtc.offerDiscovery.stepCount );
+                if( this.fetchingOfferStepNumber >= settings.net.rtc.offerDiscovery.stepCount ) {
+                    this.offerDiscoveryStop();
+                    throw new Error( "no offer found" );
+                }
+                if( ! this.timerFetchingOffer ) {
+                    const self = this;
+                    this.timerFetchingOffer = setInterval( function() {
+                        self.offerDiscoveryStep( joMessage );
+                    }, settings.net.rtc.offerDiscovery.periodMilliseconds );
+                }
+                return;
+            }
+            if( settings.logging.net.signaling.impersonate ) {
+                console.log( "Signaling client socket \"" + this.socket.strSavedRemoteAddress + "\" impersonated as \"" + this.idRtcParticipant +
+                    "\" in signaling space \"" + signalingSpace.idSpace + "\" did fetched published offer:", joOfferInfo );
+            }
+            joAnswer = utils.prepareAnswerJSON( joMessage ); // successful answer
+            joAnswer.offer = joOfferInfo.offer;
+            joAnswer.idOffer = 0 + joOfferInfo.idOffer;
+            joAnswer.idSomebodyCreator = joOfferInfo.idSomebodyCreator; // server holder
+        } catch ( err ) {
+            if( settings.logging.net.signaling.error )
+                console.warn( "Server method", joMessage.method, "RPC exception:", err );
+            joAnswer = utils.prepareAnswerJSON( joMessage );
+            joAnswer.error = "" + err.toString();
+        }
+        if( typeof joAnswer.error == "string" && joAnswer.error.length > 0 ) {
+            if( settings.logging.net.signaling.error )
+                console.warn( "Signaling client socket \"" + this.socket.strSavedRemoteAddress + "\" error answer", joAnswer );
+        } else if( settings.logging.net.signaling.message )
+            console.log( "Signaling client socket \"" + this.socket.strSavedRemoteAddress + " answer", joAnswer );
+        this.socket.send( joAnswer, true ); // isFlush=true always in signaling server
+        this.offerDiscoveryStop();
+    }
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class SignalingSpace extends EventDispatcher {
+    constructor( idSpace, signalingCategory ) {
+        super();
+        this.isDisposed = false;
+        this.idSpace = "" + ( ( idSpace && typeof idSpace == "string" && idSpace.length > 0 ) ? idSpace : "" );
+        this.idSomebodyCreator = "";
+        this.arr_published_offers = [];
+        this.signalingCategory = signalingCategory;
+        this.map_clients = {};
+        this.signalingCategory.map_spaces[this.idSpace] = this;
+        this.idCategory = "" + this.signalingCategory.idCategory;
+        if( settings.logging.net.signaling.objectLifetime )
+            console.log( "New signaling space \"" + this.idSpace + "\" in signaling category \"" + this.idCategory + "\"" );
+        this.signalingCategory.dispatchEvent( new UniversalDispatcherEvent( "spaceAdded", { detail: { signalingSpace: this } } ) );
+    }
+    dispose() {
+        if( this.isDisposed )
+            return;
+        this.isDisposing = true;
+        if( settings.logging.net.signaling.objectLifetime )
+            console.log( "Disposing signaling space \"" + this.idSpace + "\" in signaling category \"" + this.idCategory + "\"" );
+        for( const [ /*idRtcParticipant*/, signalingClient ] of Object.entries( this.map_clients ) )
+            signalingClient.dispose();
+        if( this.idSpace ) {
+            if( this.signalingCategory ) {
+                this.signalingCategory.dispatchEvent( new UniversalDispatcherEvent( "spaceRemoved", { detail: { signalingSpace: this } } ) );
+                delete this.signalingCategory.map_spaces[this.idSpace];
+            }
+            this.idSpace = null;
+        }
+        this.idCategory = null;
+        this.signalingCategory.autoDispose();
+        this.signalingCategory = null;
+        this.arr_published_offers = [];
+        super.dispose();
+    }
+    autoDispose() {
+        if( this.isDisposed )
+            return;
+        if( this.allSomebodyIDs().length > 0 )
+            return;
+        if( settings.logging.net.signaling.objectLifetime )
+            console.log( "Auto-dispose signaling space \"" + this.idSpace + "\" in signaling category \"" + this.idCategory + "\"" );
+        this.dispose();
+    }
+    allSomebodyIDs() {
+        if( this.isDisposed )
+            return [];
+        return Object.keys( this.map_clients );
+    }
+    clientGet( idRtcParticipant ) {
+        if( this.isDisposed )
+            return null;
+        const signalingClient = this.map_clients[idRtcParticipant];
+        return signalingClient ? signalingClient : null;
+    }
+    clientRemove( idRtcParticipant ) {
+        if( this.isDisposed )
+            return false;
+        idRtcParticipant = "" + ( idRtcParticipant ? idRtcParticipant.toString() : "" );
+        if( idRtcParticipant in this.map_clients ) {
+            const signalingClient = this.map_clients[idRtcParticipant];
+            signalingClient.dispose();
+            this.autoDispose();
+            return true;
+        }
+        return false;
+    }
+    fetchPublishedOffer() {
+        if( this.isDisposed || this.isDisposing ) {
+            if( settings.logging.net.signaling.offerDiscoveryStepFail )
+                console.warn( "Attempt to fetch offer in destroyed signaling space" );
+            return null;
+        }
+        if( this.idSomebodyCreator == undefined || this.idSomebodyCreator == null || this.idSomebodyCreator == "" ) {
+            if( settings.logging.net.signaling.offerDiscoveryStepFail )
+                console.warn( "Attempt to fetch offer in malformed signaling space" );
+            return null;
+        }
+        if( this.arr_published_offers.length == 0 ) {
+            if( settings.logging.net.signaling.offerDiscoveryStepFail )
+                console.warn( "Attempt to fetch offer in  signaling space with no offers" );
+            return null;
+        }
+        const joOfferInfo = this.arr_published_offers[0];
+        this.arr_published_offers.splice( 0, 1 );
+        joOfferInfo.idSomebodyCreator = "" + this.idSomebodyCreator;
+        return joOfferInfo;
+    }
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class SignalingCategory extends EventDispatcher {
+    constructor( idCategory, signalingManager ) {
+        super();
+        this.isDisposed = false;
+        this.idCategory = "" + ( ( idCategory && typeof idCategory == "string" && idCategory.length > 0 ) ? idCategory : "" );
+        this.signalingManager = signalingManager;
+        this.map_spaces = {};
+        this.signalingManager.map_categories[this.idCategory] = this;
+        if( settings.logging.net.signaling.objectLifetime )
+            console.log( "New signaling category \"" + this.idCategory + "\"" );
+        this.signalingManager.dispatchEvent( new UniversalDispatcherEvent( "categoryAdded", { detail: { signalingCategory: this } } ) );
+    }
+    dispose() {
+        if( this.isDisposed )
+            return;
+        this.isDisposing = true;
+        if( settings.logging.net.signaling.objectLifetime )
+            console.log( "Disposing signaling category \"" + this.idCategory + "\"" );
+        for( const [ /*idSpace*/, signalingSpace ] of Object.entries( this.map_spaces ) )
+            signalingSpace.dispose();
+        if( this.signalingManager ) {
+            delete this.signalingManager.map_categories[this.idCategory];
+            this.signalingManager.dispatchEvent( new UniversalDispatcherEvent( "categoryRemoved", { detail: { signalingCategory: this } } ) );
+            this.signalingManager = null;
+        }
+        this.map_spaces = {};
+        this.idCategory = null;
+        super.dispose();
+    }
+    autoDispose() {
+        if( this.isDisposed )
+            return;
+        if( this.allCSpaceIDs().length > 0 )
+            return;
+        if( settings.logging.net.signaling.objectLifetime )
+            console.log( "Auto-dispose signaling category \"" + this.idCategory + "\"" );
+        this.dispose();
+    }
+    allCSpaceIDs() {
+        if( this.isDisposed )
+            return [];
+        return Object.keys( this.map_spaces );
+    }
+    spaceGet( idSpace, isAutoAlloc ) {
+        if( this.isDisposed )
+            return null;
+        try {
+            idSpace = "" + ( idSpace ? idSpace.toString() : settings.rtcSpace.defaultSpaceName );
+            isAutoAlloc = ( isAutoAlloc == null || isAutoAlloc == undefined ) ? true : ( isAutoAlloc ? true : false );
+            let signalingSpace = null;
+            if( idSpace in this.map_spaces )
+                signalingSpace = this.map_spaces[idSpace];
+            else if( isAutoAlloc )
+                this.map_spaces["" + idSpace] = signalingSpace = new SignalingSpace( "" + idSpace, this );
+            return signalingSpace;
+        } catch ( err ) {
+            if( settings.logging.net.signaling.error )
+                console.warn( "Signaling space retrieval error:", err );
+            return null;
+        }
+    }
+    spaceRemove( idSpace ) {
+        if( this.isDisposed )
+            return false;
+        idSpace = "" + ( idSpace ? idSpace.toString() : idSpace.rtcSpace.defaultSpaceName );
+        if( idSpace in this.map_spaces ) {
+            const signalingSpace = this.map_spaces[idSpace];
+            signalingSpace.dispose();
+            this.autoDispose();
+            return true;
+        }
+        return false;
+    }
+    fetchPublishedOffer() {
+        if( this.isDisposed )
+            return null;
+        for( const [ /*idSpace*/, signalingSpace ] of Object.entries( this.map_spaces ) ) {
+            const joOfferInfo = signalingSpace.fetchPublishedOffer();
+            if( joOfferInfo )
+                return joOfferInfo;
+        }
+        return null;
+    }
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class SignalingManager extends EventDispatcher {
+    constructor() {
+        super();
+        this.isDisposed = false;
+        this.map_categories = {};
+        if( settings.logging.net.signaling.objectLifetime )
+            console.log( "New signaling manager" );
+    }
+    dispose() {
+        if( this.isDisposed )
+            return;
+        this.isDisposing = true;
+        if( settings.logging.net.signaling.objectLifetime )
+            console.log( "Disposing signaling manager" );
+        for( const [ /*idCategory*/, signalingCategory ] of Object.entries( this.map_categories ) )
+            signalingCategory.dispose();
+        this.map_categories = {};
+        super.dispose();
+    }
+    allCategoryIDs() {
+        return Object.keys( this.map_categories );
+    }
+    categoryGet( idCategory, isAutoAlloc ) {
+        try {
+            idCategory = "" + ( idCategory ? idCategory.toString() : settings.rtcSpace.defaultSpaceCategory );
+            isAutoAlloc = ( isAutoAlloc == null || isAutoAlloc == undefined ) ? true : ( isAutoAlloc ? true : false );
+            let signalingCategory = null;
+            if( idCategory in this.map_categories )
+                signalingCategory = this.map_categories[idCategory];
+            else if( isAutoAlloc )
+                this.map_categories["" + idCategory] = signalingCategory = new SignalingCategory( "" + idCategory, this );
+            return signalingCategory;
+        } catch ( err ) {
+            if( settings.logging.net.signaling.error )
+                console.warn( "Signaling category retrieval error:", err );
+            return null;
+        }
+    }
+    categoryRemove( idCategory ) {
+        idCategory = "" + ( idCategory ? idCategory.toString() : settings.rtcSpace.defaultSpaceName );
+        if( idCategory in this.map_categories ) {
+            const signalingCategory = this.map_categories[idCategory];
+            signalingCategory.dispose();
+            return true;
+        }
+        return false;
+    }
+};
+
+const g_default_signaling_manager = new SignalingManager();
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class SignalingServer extends EventDispatcher {
+    constructor( acceptor, signalingManager ) {
+        super();
+        if( acceptor == null || acceptor == undefined || typeof acceptor != "object" )
+            throw new Error( "Cannot create test server on bad acceptor" );
+        this.acceptor = acceptor;
+        this.signalingManager = signalingManager || g_default_signaling_manager;
+        const self = this;
+        acceptor.on( "connection", function( eventData ) {
+            const socket = eventData.socket;
+            if( ( ! ( "remoteAddress" in eventData ) ) || eventData.remoteAddress == null || eventData.remoteAddress == undefined )
+                socket.strSavedRemoteAddress = socket.constructor.name;
+            else
+                socket.strSavedRemoteAddress = "" + eventData.remoteAddress;
+            socket.signalingClient = null; // not impersonated yet
+            if( settings.logging.net.signaling.connect )
+                console.log( "New signaling server connection \"" + socket.strSavedRemoteAddress + "\"" );
+            socket.signalingAuthInfo = {
+                isAuthorized: false,
+                idCategory: null,
+                idSpaceSpace: null,
+                idRtcParticipant: null
+            };
+            let _offAllPipeEventListeners = null;
+            let _onPipeClose = function() {
+                if( settings.logging.net.signaling.disconnect )
+                    console.warn( "Signaling client socket closed \"" + socket.strSavedRemoteAddress + "\"" );
+                if( _offAllPipeEventListeners ) {
+                    _offAllPipeEventListeners();
+                    _offAllPipeEventListeners = null;
+                }
+                if( socket.signalingClient ) {
+                    socket.signalingClient.onPipeClose( socket );
+                    socket.signalingClient = null;
+                }
+            };
+            let _onPipeError = function( eventData ) {
+                if( settings.logging.net.signaling.error )
+                    console.warn( "Socket error \"" + socket.strSavedRemoteAddress + "\"" );
+                if( _offAllPipeEventListeners ) {
+                    _offAllPipeEventListeners();
+                    _offAllPipeEventListeners = null;
+                }
+                if( socket.signalingClient ) {
+                    socket.signalingClient.onPipeError( socket );
+                    socket.signalingClient = null;
+                }
+            };
+            let _onPipeMessage = function( eventData ) {
+                if( settings.logging.net.signaling.rawMessage )
+                    console.log( "Signaling client socket \"" + eventData.strSavedRemoteAddress + "\" raw message", eventData );
+                const joMessage = eventData.message;
+                if( settings.logging.net.signaling.message )
+                    console.log( "Signaling client socket \"" + socket.strSavedRemoteAddress + "\" message ", joMessage );
+                let signalingCategory = null;
+                let signalingSpace = null;
+                let signalingClient = socket.signalingClient;
+                if( signalingClient ) {
+                    signalingSpace = signalingClient.signalingSpace;
+                    if( signalingSpace )
+                        signalingCategory = signalingSpace.signalingCategory;
+                }
+                let joAnswer = null;
+                let isForceDisconnect = false;
+                try {
+                    switch ( joMessage.method ) {
+                    case "signalingListSpaces": {
+                        joAnswer = utils.prepareAnswerJSON( joMessage );
+                        joAnswer.arrSpaces = self.gamingSpaceManager.allNames();
+                    } break;
+                    case "signalingImpersonate": {
+                        const idRtcParticipant = joMessage.idRtcParticipant;
+                        if( ( !idRtcParticipant ) || typeof idRtcParticipant != "string" || idRtcParticipant.length <= 0 ) {
+                            isForceDisconnect = true;
+                            throw new Error( "Bad impersonate call data, no valid signaling *somebody* ID provided" );
+                        }
+                        //
+                        const strRole = joMessage.role;
+                        if( ( !strRole ) || typeof strRole != "string" || strRole.length <= 0 || ( ! ( strRole == "creator" || strRole == "joiner" ) ) ) {
+                            isForceDisconnect = true;
+                            throw new Error( "Bad impersonate call data, no valid signaling *somebody* role provided" );
+                        }
+                        //
+                        const idCategory = joMessage.idCategory;
+                        if( ( !idCategory ) || typeof idCategory != "string" || idCategory.length <= 0 ) {
+                            isForceDisconnect = true;
+                            throw new Error( "Bad impersonate call data, no valid signaling space category provided" );
+                        }
+                        signalingCategory = self.signalingManager.categoryGet( idCategory, true );
+                        if( ! signalingCategory ) {
+                            isForceDisconnect = true;
+                            throw new Error( "Bad impersonate call data, cannot get/alloc signaling category with \"" + idCategory + "\" name" );
+                        }
+                        //
+                        const idSpace = joMessage.idSpace;
+                        if( ( !idSpace ) || typeof idSpace != "string" || idSpace.length <= 0 ) {
+                            isForceDisconnect = true;
+                            throw new Error( "Bad impersonate call data, no valid signaling space name provided" );
+                        }
+                        signalingSpace = signalingCategory.spaceGet( idSpace, true );
+                        if( ! signalingSpace ) {
+                            isForceDisconnect = true;
+                            throw new Error( "Bad impersonate call data, cannot get/alloc signaling space with \"" + idSpace + "\" name" );
+                        }
+                        //
+                        if( signalingSpace.clientGet( idRtcParticipant ) != null ) {
+                            isForceDisconnect = true;
+                            throw new Error( "*Somebody* \"" + idRtcParticipant + "\" is already in \"" + idSpace + "\" signaling space" );
+                        }
+                        //
+                        if( strRole == "creator" && signalingSpace.idSomebodyCreator != "" && signalingSpace.idSomebodyCreator != idRtcParticipant )
+                            throw new Error( "*Somebody* \"" + idRtcParticipant + "\" is already in \"" + idSpace + "\" attempted to impersonate as creator while other creator already exist" );
+                        //
+                        signalingClient = new SignalingClient( "" + idRtcParticipant, "" + strRole, signalingSpace, socket );
+                        if( settings.logging.net.signaling.impersonate ) {
+                            isForceDisconnect = true;
+                            console.log( "Signaling client socket \"" + socket.strSavedRemoteAddress + "\" was impersonated as \"" + idRtcParticipant +
+                                "\" in signaling space \"" + signalingSpace.idSpace + "\"" );
+                        }
+                        // if( (!( "fnFlushNetwork" in signalingSpace )) || (!signalingSpace.fnFlushNetwork) )
+                        //     //console.log( "Setting up network data flushing in acceptor" );
+                        //     signalingSpace.fnFlushNetwork = function() {
+                        //         // TO-DO: improve this
+                        //         //console.log( "Flushing data to network via acceptor" );
+                        //         acceptor.flush(); // network
+                        //     };
+                        socket.signalingClient = signalingClient;
+                        socket.signalingAuthInfo.isAuthorized = true;
+                        socket.signalingAuthInfo.idCategory = "" + idCategory;
+                        socket.signalingAuthInfo.idSpaceSpace = "" + idSpace;
+                        socket.signalingAuthInfo.idRtcParticipant = "" + idRtcParticipant;
+                        joAnswer = utils.prepareAnswerJSON( joMessage ); // successful answer
+                        joAnswer.signalingAuthInfo = JSON.parse( JSON.stringify( socket.signalingAuthInfo ) );
+                    } break;
+                    case "signalingPublishOffer": {
+                        if( ! ( signalingClient && signalingSpace && signalingCategory ) )
+                            throw new Error( "only connected signaling clients can publish offers" );
+                        if( ! ( signalingClient.isCreator ) )
+                            throw new Error( "only creator can publish offers" );
+                        const joOfferInfo = {
+                            // ts: some time stamp
+                            offer: joMessage.offer,
+                            idOffer: 0 + joMessage.idOffer
+                        };
+                        signalingSpace.arr_published_offers.push( joOfferInfo );
+                        if( settings.logging.net.signaling.publishOffer ) {
+                            console.log( "Signaling client socket \"" + socket.strSavedRemoteAddress + "\" impersonated as \"" + signalingClient.idRtcParticipant +
+                                "\" in signaling space \"" + signalingSpace.idSpace + "\" did published creator offer:", joOfferInfo );
+                        }
+                        joAnswer = utils.prepareAnswerJSON( joMessage ); // successful answer
+                    } break;
+                    case "signalingFetchOffer": {
+                        if( ! ( signalingClient && signalingSpace && signalingCategory ) )
+                            throw new Error( "only connected signaling clients can fetch published offers" );
+                        signalingClient.offerDiscoveryStart( joMessage );
+                    } break;
+                    case "signalingPublishAnswer": {
+                        if( ! ( signalingClient && signalingSpace && signalingCategory ) )
+                            throw new Error( "only connected signaling clients can publish offer answers" );
+                        const connectedServerCreator = signalingSpace.clientGet( joMessage.idSomebodyCreator );
+                        if( ! connectedServerCreator )
+                            throw new Error( "answer published with invalid server holder reference: " + joMessage.idSomebodyCreator );
+                        const joForwardMessage = JSON.parse( JSON.stringify( joMessage ) );
+                        joForwardMessage.idSomebody_joiner = "" + signalingClient.idRtcParticipant;
+                        connectedServerCreator.socket.send( joForwardMessage ); // re-send it to server holder, joiner *somebody* ID is added
+                        // no answer so far((
+                    } break;
+                    default: {
+                        joAnswer = utils.prepareAnswerJSON( joMessage );
+                        joAnswer.error = "Unhandled message";
+                        joAnswer.joMessage = joMessage; // send it back ))
+                        if( settings.logging.net.signaling.error )
+                            console.warn( "Signaling client socket \"" + socket.strSavedRemoteAddress + "\" unhandled message", joMessage );
+                    } break;
+                    } // switch( joMessage.method )
+                } catch ( err ) {
+                    if( settings.logging.net.signaling.error )
+                        console.warn( "Server method", joMessage.method, "RPC exception:", err );
+                    joAnswer = utils.prepareAnswerJSON( joMessage );
+                    joAnswer.error = "" + err.toString();
+                }
+                if( joAnswer != null && joAnswer != undefined ) {
+                    if( typeof joAnswer.error == "string" && joAnswer.error.length > 0 ) {
+                        if( settings.logging.net.signaling.error )
+                            console.warn( "Signaling client socket \"" + socket.strSavedRemoteAddress + "\" error answer", joAnswer );
+                    } else if( settings.logging.net.signaling.message )
+                        console.log( "Signaling client socket \"" + socket.strSavedRemoteAddress + " answer", joAnswer );
+                    socket.send( joAnswer, true ); // isFlush=true always in signaling server
+                    if( isForceDisconnect )
+                        socket.disconnect();
+                }
+                if( signalingSpace )
+                    signalingSpace.autoDispose();
+                if( signalingCategory )
+                    signalingCategory.autoDispose();
+            };
+            _offAllPipeEventListeners = function() {
+                if( _onPipeClose ) {
+                    socket.off( "close", _onPipeClose );
+                    _onPipeClose = null;
+                }
+                if( _onPipeError ) {
+                    socket.off( "error", _onPipeError );
+                    _onPipeError = null;
+                }
+                if( _onPipeMessage ) {
+                    socket.off( "message", _onPipeMessage );
+                    _onPipeMessage = null;
+                }
+                let signalingSpace = null;
+                const signalingClient = socket.signalingClient;
+                if( signalingClient ) {
+                    signalingSpace = signalingClient.signalingSpace;
+                    if( settings.logging.net.signaling.disconnect )
+                        console.log( "Handling connection close for signaling client \"" + signalingClient.idRtcParticipant + "\"" );
+                    if( signalingSpace )
+                        signalingSpace.clientRemove( signalingClient.idRtcParticipant );
+
+                    signalingClient.dispose();
+                }
+                socket.signalingClient = null;
+            };
+            socket.on( "close", _onPipeClose );
+            socket.on( "error", _onPipeError );
+            socket.on( "message", _onPipeMessage );
+        } );
+        this.dispatchEvent( new UniversalDispatcherEvent( "initialized", { detail: { ref: this } } ) );
+    }
+    dispose() {
+        this.isDisposing = true;
+        super.dispose();
+    }
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+const protoName = settings.net.secure ? "WSS" : "WS";
+if( settings.logging.net.signaling.generic )
+    console.log( protoName + " signaling server will start" );
+
+const key = settings.net.secure ? fs.readFileSync( "./self-signed/self-signed-key.pem", "utf8" ) : null;
+const cert = settings.net.secure ? fs.readFileSync( "./self-signed/self-signed-cert.pem", "utf8" ) : null;
+let acceptor = new network_layer.WebSocketServerAcceptor( settings.net.ports.signaling, key, cert );
+let signalingServer = new SignalingServer( acceptor );
+signalingServer.on( "initialized", function() {
+    if( settings.logging.net.signaling.generic )
+        console.log( protoName + " signaling server did started" );
+} );
+signalingServer.on( "dispose", function() {
+    if( settings.logging.net.signaling.generic )
+        console.log( protoName + " signaling server did stopped" );
+} );
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+let g_bShouldExit = false, g_bProcessExitRequested = false;
+function exit_if_needed() {
+    if( ! g_bShouldExit )
+        return;
+    if( g_bProcessExitRequested )
+        return;
+    // ensure components stopped here, if needed
+    g_bProcessExitRequested = true;
+    console.log( "App will exit" );
+    process.exit( 0 );
+}
+
+process.on( "SIGINT", function() {
+    console.warn( "\nApp did caught interrupt signal " );
+    // stop components here, if needed
+    if( signalingServer ) {
+        console.log( "Will stop signaling server" );
+        signalingServer.dispose();
+        signalingServer = null;
+        console.log( "Did stopped signaling server" );
+    }
+    if( acceptor ) {
+        console.log( "Will stop signaling acceptor" );
+        acceptor.dispose();
+        acceptor = null;
+        console.log( "Did stopped signaling acceptor" );
+    }
+    g_bShouldExit = true;
+    //
+    exit_if_needed();
+    g_bShouldExit = true;
+} );
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/npms/skale-cool-socket/test_worker.js b/npms/skale-cool-socket/test_worker.js
new file mode 100644
index 000000000..a315cf581
--- /dev/null
+++ b/npms/skale-cool-socket/test_worker.js
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+
+/**
+ * @license
+ * SKALE COOL SOCKET
+ *
+ * This program 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.
+ *
+ * This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @file test_worker.js
+ * @copyright SKALE Labs 2019-Present
+ */
+
+const network_layer = require( "./socket.js" );
+const { TestServer } = require( "./test_server.js" );
+const {
+    parentPort
+    //, workerData
+} = require( "worker_threads" );
+
+parentPort.on( "message", jo => {
+    if( network_layer.in_worker_apis.on_message( jo ) )
+        return;
+} );
+
+function doSendMessage( type, endpoint, worker_uuid, data ) {
+    const jo = network_layer.socket_received_data_reverse_marshall( data );
+    const joSend = {
+        worker_message_type: ( type && typeof type == "string" && type.length > 0 ) ? type : "in_worker_message",
+        worker_endpoint: endpoint,
+        worker_uuid: worker_uuid,
+        data: jo
+    };
+    parentPort.postMessage( network_layer.socket_sent_data_marshall( joSend ) );
+}
+
+const url = "local_worker_server";
+const acceptor = new network_layer.InWorkerSocketServerAcceptor( url, doSendMessage );
+const server = new TestServer( acceptor );
+server.on( "dispose", function() { console.log( "disposed in-worker server" ); } );
diff --git a/proxy/contracts/test/MessageProxyForSchainWithoutSignature.sol b/proxy/contracts/test/MessageProxyForSchainWithoutSignature.sol
index 47e63829b..c287cf1d0 100644
--- a/proxy/contracts/test/MessageProxyForSchainWithoutSignature.sol
+++ b/proxy/contracts/test/MessageProxyForSchainWithoutSignature.sol
@@ -25,7 +25,8 @@ import "./MessageProxyForSchainTester.sol";
 
 contract MessageProxyForSchainWithoutSignature is MessageProxyForSchainTester {
 
-    constructor(string memory schainName) MessageProxyForSchainTester(IKeyStorage(address(0)), schainName)
+    constructor(string memory schainName)
+        MessageProxyForSchainTester(IKeyStorage(address(0)), schainName)
     // solhint-disable-next-line no-empty-blocks
     {}
 
diff --git a/proxy/predeployed/test/contracts/community_locker.py b/proxy/predeployed/test/contracts/community_locker.py
index e5a53eb86..a1707e98b 100644
--- a/proxy/predeployed/test/contracts/community_locker.py
+++ b/proxy/predeployed/test/contracts/community_locker.py
@@ -14,4 +14,4 @@ def check_community_locker(deployer_address: str, schain_name: str, community_po
     if not community_locker.functions.tokenManagerLinker().call() == TOKEN_MANAGER_LINKER_ADDRESS: raise AssertionError
     if not community_locker.functions.communityPool().call() == community_pool_address: raise AssertionError
     if not community_locker.functions.schainHash().call() == w3.solidityKeccak(['string'], [schain_name]): raise AssertionError
-    if not community_locker.functions.timeLimitPerMessage().call() == CommunityLockerGenerator.DEFAULT_TIME_LIMIT_SEC: raise AssertionError
+    if not community_locker.functions.timeLimitPerMessage(CommunityLockerGenerator.MAINNET_HASH).call() == CommunityLockerGenerator.DEFAULT_TIME_LIMIT_SEC: raise AssertionError
diff --git a/proxy/test/CommunityLocker.ts b/proxy/test/CommunityLocker.ts
index 9d75f5913..23d3f62d4 100644
--- a/proxy/test/CommunityLocker.ts
+++ b/proxy/test/CommunityLocker.ts
@@ -107,11 +107,25 @@ describe("CommunityLocker", () => {
     });
 
     it("should set time limit per message", async () => {
-        await communityLocker.setTimeLimitPerMessage(0)
+        expect(await communityLocker.timeLimitPerMessage(mainnetHash)).to.be.equal(300);
+        await communityLocker.setTimeLimitPerMessage("Mainnet", 0)
             .should.be.eventually.rejectedWith("Not enough permissions to set constant");
         await communityLocker.grantRole(await communityLocker.CONSTANT_SETTER_ROLE(), deployer.address);
-        await communityLocker.setTimeLimitPerMessage(0);
-        expect(BigNumber.from(await communityLocker.timeLimitPerMessage()).toString()).to.be.equal(BigNumber.from(0).toString());
+        await communityLocker.setTimeLimitPerMessage("Mainnet", 0);
+        expect(await communityLocker.timeLimitPerMessage(mainnetHash)).to.be.equal(0);
+    });
+
+    it("should set time limit per message for schain", async () => {
+        const anotherSchainName = "Schain Sierra";
+        const schainHash = ethers.utils.id(anotherSchainName);
+        expect(await communityLocker.timeLimitPerMessage(schainHash)).to.be.equal(0);
+        await communityLocker.setTimeLimitPerMessage(anotherSchainName, 1200)
+            .should.be.eventually.rejectedWith("Not enough permissions to set constant");
+        await communityLocker.grantRole(await communityLocker.CONSTANT_SETTER_ROLE(), deployer.address);
+        await communityLocker.setTimeLimitPerMessage(schainName, 1200)
+            .should.be.eventually.rejectedWith("Incorrect chain");
+        await communityLocker.setTimeLimitPerMessage(anotherSchainName, 1200);
+        expect(await communityLocker.timeLimitPerMessage(schainHash)).to.be.equal(1200);
     });
 
     it("should set gasprice", async () => {
@@ -128,8 +142,8 @@ describe("CommunityLocker", () => {
         const time = await currentTime();
         await communityLocker.setGasPrice(100, time + 200, sign).should.be.eventually.rejectedWith("Timestamp should not be in the future");
         await communityLocker.setGasPrice(100, time, sign);
-        expect(BigNumber.from(await communityLocker.mainnetGasPrice()).toString()).to.be.equal(BigNumber.from(100).toString());
-        expect(BigNumber.from(await communityLocker.gasPriceTimestamp()).toString()).to.be.equal(BigNumber.from(time).toString());
+        expect(await communityLocker.mainnetGasPrice()).to.be.equal(100);
+        expect(await communityLocker.gasPriceTimestamp()).to.be.equal(time);
 
         skipTime(60);
 
@@ -137,7 +151,7 @@ describe("CommunityLocker", () => {
         await communityLocker.setGasPrice(101, time, sign).should.be.eventually.rejectedWith("Gas price timestamp already updated");
         await communityLocker.setGasPrice(101, time + 70, sign).should.be.eventually.rejectedWith("Timestamp should not be in the future");
         await communityLocker.setGasPrice(101, time + 40, sign);
-        expect(BigNumber.from(await communityLocker.mainnetGasPrice()).toString()).to.be.equal(BigNumber.from(101).toString());
-        expect(BigNumber.from(await communityLocker.gasPriceTimestamp()).toString()).to.be.equal(BigNumber.from(time + 40).toString());
+        expect(await communityLocker.mainnetGasPrice()).to.be.equal(101);
+        expect(await communityLocker.gasPriceTimestamp()).to.be.equal(time + 40);
     });
 });
diff --git a/proxy/test/CommunityPool.ts b/proxy/test/CommunityPool.ts
index 2a265451f..2fafeb9b1 100644
--- a/proxy/test/CommunityPool.ts
+++ b/proxy/test/CommunityPool.ts
@@ -4,7 +4,8 @@ import {
     Linker,
     MessageProxyForMainnet,
     MessageProxyForMainnetTester,
-    CommunityPool
+    CommunityPool,
+    MessagesTester
 } from "../typechain";
 
 import { stringValue } from "./utils/helper";
@@ -30,6 +31,7 @@ import { ethers, web3 } from "hardhat";
 import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
 import { BigNumber } from "ethers";
 import { assert, expect } from "chai";
+import { deployMessages } from "./utils/deploy/messages";
 
 async function getBalance(address: string) {
     return parseFloat(web3.utils.fromWei(await web3.eth.getBalance(address)));
@@ -44,8 +46,10 @@ describe("CommunityPool", () => {
     let messageProxy: MessageProxyForMainnet;
     let linker: Linker;
     let communityPool: CommunityPool;
+    let messages: MessagesTester;
     const contractManagerAddress = "0x0000000000000000000000000000000000000000";
     const schainName = "Schain";
+    const schainName2 = "Schain2";
     let minTransactionGas: any;
 
     before(async () => {
@@ -58,6 +62,7 @@ describe("CommunityPool", () => {
         linker = await deployLinker(contractManager, messageProxy);
         communityPool = await deployCommunityPool(contractManager, linker, messageProxy);
         minTransactionGas = await communityPool.minTransactionGas();
+        messages = await deployMessages();
 
         const CHAIN_CONNECTOR_ROLE = await messageProxy.CHAIN_CONNECTOR_ROLE();
         await messageProxy.grantRole(CHAIN_CONNECTOR_ROLE, deployer.address);
@@ -65,96 +70,14 @@ describe("CommunityPool", () => {
         await messageProxy.grantRole(EXTRA_CONTRACT_REGISTRAR_ROLE, deployer.address);
     });
 
-    it("should not allow to withdraw from user wallet if CommunityPool is not registered for all chains", async () => {
-        const extraContractRegistrarRole = await messageProxy.EXTRA_CONTRACT_REGISTRAR_ROLE();
-        await messageProxy.grantRole(extraContractRegistrarRole, deployer.address);
-        await messageProxy.registerExtraContractForAll(communityPool.address);
-        const tx = await messageProxy.addConnectedChain(schainName);
-        const wei = minTransactionGas.mul(tx.gasPrice);
-        await communityPool.connect(user).rechargeUserWallet(schainName, user.address, { value: wei.toString() });
-        await messageProxy.removeExtraContractForAll(communityPool.address);
-        await communityPool.connect(user).withdrawFunds(schainName, wei.toString())
-            .should.be.eventually.rejectedWith("Sender contract is not registered");
-    });
-
-    describe("when chain connected and contract registered", async () => {
-        let gasPrice: BigNumber;
-        beforeEach(async () => {
-            await messageProxy.registerExtraContract(schainName, communityPool.address);
-            gasPrice = ((await messageProxy.addConnectedChain(schainName)).gasPrice) as BigNumber;
-        });
-
-        it("should not allow to withdraw from user wallet if CommunityPool is not registered", async () => {
-            const amount = minTransactionGas.mul(gasPrice);
-            await communityPool.connect(user).rechargeUserWallet(schainName, user.address, { value: amount.toString() });
-            await messageProxy.removeExtraContract(schainName, communityPool.address);
-            await communityPool.connect(user).withdrawFunds(schainName, amount.toString())
-                .should.be.eventually.rejectedWith("Sender contract is not registered");
-        });
-
-        it("should revert if user recharged not enough money for most costly transaction", async () => {
-            const amount = minTransactionGas.mul(gasPrice).sub(1);
-            await communityPool.connect(user).rechargeUserWallet(schainName, user.address, { value: amount.toString(), gasPrice })
-                .should.be.eventually.rejectedWith("Not enough ETH for transaction");
-        });
-
-        it("should recharge wallet if user passed enough money", async () => {
-            const amount = minTransactionGas.mul(gasPrice);
-            await communityPool.connect(user).rechargeUserWallet(schainName, user.address, { value: amount.toString(), gasPrice });
-            let userBalance = await communityPool.getBalance(user.address, schainName);
-            userBalance.should.be.deep.equal(amount);
-            await communityPool.connect(user).rechargeUserWallet(schainName, user.address, { value: amount.toString(), gasPrice });
-            userBalance = await communityPool.getBalance(user.address, schainName);
-            userBalance.should.be.deep.equal(amount.mul(2));
-            expect(BigNumber.from(await messageProxy.getOutgoingMessagesCounter(schainName)).toString()).to.be.equal(BigNumber.from(1).toString());
-        });
-
-        it("should recharge wallet if user passed enough money with basefee", async () => {
-            const blockNumber = await ethers.provider.getBlockNumber();
-            const baseFeePerGas = (await ethers.provider.getBlock(blockNumber)).baseFeePerGas;
-            const baseFeeCandidate = (baseFeePerGas ? baseFeePerGas : new BigNumber(1, "hex"));
-            const basefee = baseFeeCandidate.toNumber() === 0 ? BigNumber.from(1) : baseFeeCandidate;
-            const amount = minTransactionGas.mul(basefee);
-            //
-            // comment this check until issue https://github.com/NomicFoundation/hardhat/issues/1688 would be fixed
-            //
-            // const amount = await communityPool.getRecommendedRechargeAmount(ethers.utils.id(schainName), user.address));
-            //
-            await communityPool.connect(user).rechargeUserWallet(schainName, user.address, { value: amount.sub(1).toString(), gasPrice: basefee.toNumber()}).should.be.eventually.rejectedWith("Not enough ETH for transaction");
-            await communityPool.connect(user).rechargeUserWallet(schainName, user.address, { value: amount.toString(), gasPrice: basefee.toNumber()});
-            let userBalance = await communityPool.getBalance(user.address, schainName);
-            userBalance.should.be.deep.equal(amount);
-            await communityPool.connect(user).rechargeUserWallet(schainName, user.address, { value: amount.toString(), gasPrice: basefee.toNumber()});
-            userBalance = await communityPool.getBalance(user.address, schainName);
-            userBalance.should.be.deep.equal(amount.mul(2));
-            expect(BigNumber.from(await messageProxy.getOutgoingMessagesCounter(schainName)).toString()).to.be.equal(BigNumber.from(1).toString());
-        });
-
-        it("should allow to withdraw money", async () => {
-            const amount = minTransactionGas.mul(gasPrice).toNumber();
-            await communityPool.connect(user).rechargeUserWallet(schainName, user.address, { value: (amount + 1).toString(), gasPrice });
-            expect(BigNumber.from(await messageProxy.getOutgoingMessagesCounter(schainName)).toString()).to.be.equal(BigNumber.from(1).toString());
-
-            await communityPool.connect(user).withdrawFunds(schainName, amount + 2 )
-                .should.be.eventually.rejectedWith("Balance is too low");
-
-            const balanceBefore = await getBalance(user.address);
-            const tx = await (await communityPool.connect(user).withdrawFunds(schainName, amount, { gasPrice })).wait();
-            const balanceAfter = await getBalance(user.address);
-            const transactionFee = tx.gasUsed.mul(gasPrice).toNumber();
-            (balanceAfter + transactionFee / 1e18).should.be.almost(balanceBefore + amount / 1e18);
-
-            await communityPool.connect(user).withdrawFunds(schainName, 1, { gasPrice });
-
-            expect(BigNumber.from(await messageProxy.getOutgoingMessagesCounter(schainName)).toString()).deep.equal(BigNumber.from(2).toString());
-
-        });
-
-    });
 
     it("should add link to contract on schain", async () => {
         const fakeContractOnSchain = user.address;
         const nullAddress = "0x0000000000000000000000000000000000000000";
+        const schainHash = stringValue(web3.utils.soliditySha3(schainName));
+
+        await communityPool.getSchainContract(schainHash)
+            .should.be.eventually.rejectedWith("Destination contract must be defined");
 
         await communityPool.addSchainContract(schainName, fakeContractOnSchain)
             .should.be.eventually.rejectedWith("Not authorized caller");
@@ -172,7 +95,7 @@ describe("CommunityPool", () => {
 
     it("should remove link to contract on schain", async () => {
         const fakeContractOnSchain = user.address;
-        await initializeSchain(contractManager, schainName, user.address,  1, 1);
+        await initializeSchain(contractManager, schainName, user.address, 1, 1);
         await communityPool.connect(user).addSchainContract(schainName, fakeContractOnSchain);
         expect(await communityPool.hasSchainContract(schainName)).to.be.true;
         await communityPool.removeSchainContract(schainName)
@@ -186,50 +109,6 @@ describe("CommunityPool", () => {
             .should.be.eventually.rejectedWith("SKALE chain is not set");
     });
 
-    it("should recharge wallet for couple chains", async () => {
-        await messageProxy.registerExtraContract(schainName, communityPool.address);
-        await messageProxy.registerExtraContract("schainName2", communityPool.address);
-        const tx = await messageProxy.addConnectedChain(schainName);
-        await messageProxy.addConnectedChain("schainName2");
-        const gasPrice = tx.gasPrice;
-        const wei = minTransactionGas.mul(gasPrice);
-        const wei2 = minTransactionGas.mul(gasPrice).mul(2);
-        const res1 = await (await communityPool.connect(user).rechargeUserWallet(schainName, user.address, { value: wei.toString(), gasPrice })).wait();
-        const res2 = await (await communityPool.connect(user).rechargeUserWallet("schainName2", user.address, { value: wei2.toString(), gasPrice })).wait();
-        const userBalance = await communityPool.getBalance(user.address, schainName);
-        userBalance.should.be.deep.equal(wei);
-        const userBalance2 = await communityPool.getBalance(user.address, "schainName2");
-        userBalance2.should.be.deep.equal(wei2);
-
-        if (!res1.events) {
-            assert("No events were emitted");
-        } else {
-            const last = res1.events.length - 1;
-            expect(res1.events[last]?.topics[0]).to.equal(stringValue(web3.utils.soliditySha3("OutgoingMessage(bytes32,uint256,address,address,bytes)")));
-            expect(res1.events[last]?.topics[1]).to.equal(stringValue(web3.utils.soliditySha3(schainName)));
-            expect(BigNumber.from(res1.events[last]?.topics[2]).toString()).to.equal("0");
-        }
-
-        if (!res2.events) {
-            assert("No events were emitted");
-        } else {
-            const last = res2.events.length - 1;
-            expect(res2.events[last]?.topics[0]).to.equal(stringValue(web3.utils.soliditySha3("OutgoingMessage(bytes32,uint256,address,address,bytes)")));
-            expect(res2.events[last]?.topics[1]).to.equal(stringValue(web3.utils.soliditySha3("schainName2")));
-            expect(BigNumber.from(res2.events[last]?.topics[2]).toString()).to.equal("0");
-        }
-
-        const res3 = await (await communityPool.connect(user).rechargeUserWallet(schainName, user.address, { value: wei.toString(), gasPrice })).wait();
-        expect(res3.events).to.be.empty;
-    });
-
-    it("should allow to withdraw money", async () => {
-        await messageProxy.registerExtraContract(schainName, communityPool.address);
-        const gasPrice = (await messageProxy.addConnectedChain(schainName)).gasPrice;
-        const wei = minTransactionGas.mul(gasPrice).toNumber();
-        await communityPool.connect(user).rechargeUserWallet(schainName, user.address, { value: wei.toString(), gasPrice });
-    });
-
     it("should add and remove link to contract on schain as LINKER_ROLE", async () => {
         const fakeContractOnSchain = user.address;
         const LINKER_ROLE = await communityPool.LINKER_ROLE();
@@ -242,18 +121,153 @@ describe("CommunityPool", () => {
             .should.be.eventually.rejectedWith("SKALE chain is not set");
     });
 
-    it("should set new minimal transaction gas", async () => {
-        const newMinTransactionGas = BigNumber.from(100);
-        const CONSTANT_SETTER_ROLE  = await communityPool.CONSTANT_SETTER_ROLE();
-        await communityPool.grantRole(CONSTANT_SETTER_ROLE, deployer.address);
-        expect(BigNumber.from(await communityPool.minTransactionGas()).toString()).to.be.equal(BigNumber.from(1000000).toString());
-        await communityPool.connect(user).setMinTransactionGas(newMinTransactionGas)
-            .should.be.eventually.rejectedWith("CONSTANT_SETTER_ROLE is required");
-        await communityPool.setMinTransactionGas(newMinTransactionGas);
-        expect(BigNumber.from(await communityPool.minTransactionGas()).toString()).to.be.equal(newMinTransactionGas.toString());
-    });
+    describe("when CommunityPool linked to mocking CommunityLocker", async () => {
+        let mockContractOnSchain: string;
 
-    it("should set new multiplier", async () => {
+        beforeEach(async () => {
+            mockContractOnSchain = node.address;
+            await communityPool.grantRole(await communityPool.LINKER_ROLE(), deployer.address);
+            await communityPool.addSchainContract(schainName, mockContractOnSchain);
+        });
+
+        it("should not allow to withdraw from user wallet if CommunityPool is not registered for all chains", async () => {
+            const extraContractRegistrarRole = await messageProxy.EXTRA_CONTRACT_REGISTRAR_ROLE();
+            await messageProxy.grantRole(extraContractRegistrarRole, deployer.address);
+            await messageProxy.registerExtraContractForAll(communityPool.address);
+            const tx = await messageProxy.addConnectedChain(schainName);
+            const wei = minTransactionGas.mul(tx.gasPrice);
+            await communityPool.connect(user).rechargeUserWallet(schainName, user.address, { value: wei.toString() });
+            await messageProxy.removeExtraContractForAll(communityPool.address);
+            await communityPool.connect(user).withdrawFunds(schainName, wei.toString())
+                .should.be.eventually.rejectedWith("Sender contract is not registered");
+        });
+
+        describe("when chain connected and contract registered", async () => {
+            let gasPrice: BigNumber;
+            beforeEach(async () => {
+                await messageProxy.registerExtraContract(schainName, communityPool.address);
+                gasPrice = ((await messageProxy.addConnectedChain(schainName)).gasPrice) as BigNumber;
+            });
+
+            it("should not allow to withdraw from user wallet if CommunityPool is not registered", async () => {
+                const amount = minTransactionGas.mul(gasPrice);
+                await communityPool.connect(user).rechargeUserWallet(schainName, user.address, { value: amount.toString() });
+                await messageProxy.removeExtraContract(schainName, communityPool.address);
+                await communityPool.connect(user).withdrawFunds(schainName, amount.toString())
+                    .should.be.eventually.rejectedWith("Sender contract is not registered");
+            });
+
+            it("should revert if user recharged not enough money for most costly transaction", async () => {
+                const amount = minTransactionGas.mul(gasPrice).sub(1);
+                await communityPool.connect(user).rechargeUserWallet(schainName, user.address, { value: amount.toString(), gasPrice })
+                    .should.be.eventually.rejectedWith("Not enough ETH for transaction");
+            });
+
+            it("should recharge wallet if user passed enough money", async () => {
+                const amount = minTransactionGas.mul(gasPrice);
+                await communityPool.connect(user).rechargeUserWallet(schainName, user.address, { value: amount.toString(), gasPrice });
+                let userBalance = await communityPool.getBalance(user.address, schainName);
+                userBalance.should.be.deep.equal(amount);
+                await communityPool.connect(user).rechargeUserWallet(schainName, user.address, { value: amount.toString(), gasPrice });
+                userBalance = await communityPool.getBalance(user.address, schainName);
+                userBalance.should.be.deep.equal(amount.mul(2));
+                expect(BigNumber.from(await messageProxy.getOutgoingMessagesCounter(schainName)).toString()).to.be.equal(BigNumber.from(1).toString());
+            });
+
+        it("should recharge wallet if user passed enough money with basefee", async () => {
+            const blockNumber = await ethers.provider.getBlockNumber();
+            const baseFeePerGas = (await ethers.provider.getBlock(blockNumber)).baseFeePerGas;
+            const baseFeeCandidate = (baseFeePerGas ? baseFeePerGas : new BigNumber(1, "hex"));
+            const basefee = baseFeeCandidate.toNumber() === 0 ? BigNumber.from(1) : baseFeeCandidate;
+            const amount = minTransactionGas.mul(basefee);
+            //
+            // comment this check until issue https://github.com/NomicFoundation/hardhat/issues/1688 would be fixed
+            //
+            // const amount = await communityPool.getRecommendedRechargeAmount(ethers.utils.id(schainName), user.address));
+            //
+            await communityPool.connect(user).rechargeUserWallet(schainName, user.address, { value: amount.sub(1).toString(), gasPrice: basefee.toNumber()}).should.be.eventually.rejectedWith("Not enough ETH for transaction");
+            await communityPool.connect(user).rechargeUserWallet(schainName, user.address, { value: amount.toString(), gasPrice: basefee.toNumber()});
+            let userBalance = await communityPool.getBalance(user.address, schainName);
+            userBalance.should.be.deep.equal(amount);
+            await communityPool.connect(user).rechargeUserWallet(schainName, user.address, { value: amount.toString(), gasPrice: basefee.toNumber()});
+            userBalance = await communityPool.getBalance(user.address, schainName);
+            userBalance.should.be.deep.equal(amount.mul(2));
+            expect(BigNumber.from(await messageProxy.getOutgoingMessagesCounter(schainName)).toString()).to.be.equal(BigNumber.from(1).toString());
+        });
+
+            it("should allow to withdraw money", async () => {
+                const amount = minTransactionGas.mul(gasPrice).toNumber();
+                await communityPool.connect(user).rechargeUserWallet(schainName, user.address, { value: (amount + 1).toString(), gasPrice });
+                expect(BigNumber.from(await messageProxy.getOutgoingMessagesCounter(schainName)).toString()).to.be.equal(BigNumber.from(1).toString());
+
+                await communityPool.connect(user).withdrawFunds(schainName, amount + 2)
+                    .should.be.eventually.rejectedWith("Balance is too low");
+
+                const balanceBefore = await getBalance(user.address);
+                const tx = await (await communityPool.connect(user).withdrawFunds(schainName, amount, { gasPrice })).wait();
+                const balanceAfter = await getBalance(user.address);
+                const transactionFee = tx.gasUsed.mul(gasPrice).toNumber();
+                (balanceAfter + transactionFee / 1e18).should.be.almost(balanceBefore + amount / 1e18);
+
+                await communityPool.connect(user).withdrawFunds(schainName, 1, { gasPrice });
+
+                expect(BigNumber.from(await messageProxy.getOutgoingMessagesCounter(schainName)).toString()).deep.equal(BigNumber.from(2).toString());
+            });
+        });
+
+        it("should recharge wallet for couple chains", async () => {
+            const schainHash = stringValue(web3.utils.soliditySha3(schainName));
+            const schainHash2 = stringValue(web3.utils.soliditySha3(schainName2));
+            const activateUserData = await messages.encodeActivateUserMessage(user.address);
+
+            await communityPool.addSchainContract(schainName2, mockContractOnSchain);
+            for (const schain of [schainName, schainName2]) {
+                await messageProxy.registerExtraContract(schain, communityPool.address);
+                await messageProxy.addConnectedChain(schain);
+
+            }
+            const gasPrice = BigNumber.from(1e9);
+            const wei = minTransactionGas.mul(gasPrice);
+            const wei2 = minTransactionGas.mul(gasPrice).mul(2);
+            const res1 = await communityPool.connect(user).rechargeUserWallet(schainName, user.address, { value: wei.toString(), gasPrice });
+            const res2 = await communityPool.connect(user).rechargeUserWallet(schainName2, user.address, { value: wei2.toString(), gasPrice });
+            const userBalance = await communityPool.getBalance(user.address, schainName);
+            userBalance.should.be.deep.equal(wei);
+            const userBalance2 = await communityPool.getBalance(user.address, schainName2);
+            userBalance2.should.be.deep.equal(wei2);
+
+            await expect(res1)
+                .to.emit(messageProxy, "OutgoingMessage")
+                .withArgs(schainHash, 0, communityPool.address, mockContractOnSchain, activateUserData);
+
+
+            await expect(res2)
+                .to.emit(messageProxy, "OutgoingMessage")
+                .withArgs(schainHash2, 0, communityPool.address, mockContractOnSchain, activateUserData);
+
+            const res3 = await (await communityPool.connect(user).rechargeUserWallet(schainName, user.address, { value: wei.toString(), gasPrice })).wait();
+            expect(res3.events).to.be.empty;
+        });
+
+        it("should allow to withdraw money", async () => {
+            await messageProxy.registerExtraContract(schainName, communityPool.address);
+            const gasPrice = (await messageProxy.addConnectedChain(schainName)).gasPrice;
+            const wei = minTransactionGas.mul(gasPrice).toNumber();
+            await communityPool.connect(user).rechargeUserWallet(schainName, user.address, { value: wei.toString(), gasPrice });
+        });
+
+        it("should set new minimal transaction gas", async () => {
+            const newMinTransactionGas = BigNumber.from(100);
+            const CONSTANT_SETTER_ROLE = await communityPool.CONSTANT_SETTER_ROLE();
+            await communityPool.grantRole(CONSTANT_SETTER_ROLE, deployer.address);
+            expect(BigNumber.from(await communityPool.minTransactionGas()).toString()).to.be.equal(BigNumber.from(1000000).toString());
+            await communityPool.connect(user).setMinTransactionGas(newMinTransactionGas)
+                .should.be.eventually.rejectedWith("CONSTANT_SETTER_ROLE is required");
+            await communityPool.setMinTransactionGas(newMinTransactionGas);
+            expect(BigNumber.from(await communityPool.minTransactionGas()).toString()).to.be.equal(newMinTransactionGas.toString());
+        });
+
+        it("should set new multiplier", async () => {
         const newMultipliermultiplierNumeratorr = BigNumber.from(5);
         const newMultiplierDivider = BigNumber.from(4);
         const CONSTANT_SETTER_ROLE  = await communityPool.CONSTANT_SETTER_ROLE();
@@ -269,15 +283,23 @@ describe("CommunityPool", () => {
     });
 
     it("should set rejected when call refundGasByUser not from messageProxy contract", async () => {
-        const schainHash = stringValue(web3.utils.soliditySha3("Schain"));
-        await communityPool.connect(deployer).refundGasByUser(schainHash, node.address, user.address, 0)
-            .should.be.eventually.rejectedWith("Sender is not a MessageProxy");
+            const schainHash = stringValue(web3.utils.soliditySha3("Schain"));
+            await communityPool.connect(deployer).refundGasByUser(schainHash, node.address, user.address, 0)
+                .should.be.eventually.rejectedWith("Sender is not a MessageProxy");
+        });
+
+        it("should set rejected when call refundGasBySchainWallet not from messageProxy contract", async () => {
+            const schainHash = stringValue(web3.utils.soliditySha3("Schain"));
+            await communityPool.connect(deployer).refundGasBySchainWallet(schainHash, node.address, 0)
+                .should.be.eventually.rejectedWith("Sender is not a MessageProxy");
+        });
     });
 
     describe("tests for refundGasByUser", async () => {
         let messageProxyTester: MessageProxyForMainnetTester;
         let linkerTester: Linker;
         let communityPoolTester: CommunityPool;
+        let mockContractOnSchain: string;
         const schainNameRGBU = "SchainRGBU";
         const schainHashRGBU = stringValue(web3.utils.soliditySha3("SchainRGBU"));
 
@@ -285,6 +307,9 @@ describe("CommunityPool", () => {
             messageProxyTester = await deployMessageProxyForMainnetTester(contractManager);
             linkerTester = await deployLinker(contractManager, messageProxyTester);
             communityPoolTester = await deployCommunityPoolTester(contractManager, linkerTester, messageProxyTester);
+            mockContractOnSchain = node.address;
+            await communityPoolTester.grantRole(await communityPool.LINKER_ROLE(), deployer.address);
+            await communityPoolTester.addSchainContract(schainNameRGBU, mockContractOnSchain);
         });
 
         it("should be rejected with Node address must be set", async () => {
@@ -292,7 +317,7 @@ describe("CommunityPool", () => {
             await messageProxyTester.registerExtraContract(schainNameRGBU, communityPoolTester.address);
             const gasPrice = tx.gasPrice;
             const wei = minTransactionGas.mul(gasPrice).mul(2);
-            await communityPoolTester.connect(user).rechargeUserWallet(schainNameRGBU, user.address, { value: wei.toString()});
+            await communityPoolTester.connect(user).rechargeUserWallet(schainNameRGBU, user.address, { value: wei.toString() });
             await messageProxyTester.connect(deployer).refundGasByUser(schainHashRGBU, "0x0000000000000000000000000000000000000000", user.address, 0)
                 .should.be.eventually.rejectedWith("Node address must be set");
         });
@@ -303,8 +328,8 @@ describe("CommunityPool", () => {
             await messageProxyTester.registerExtraContract(schainNameRGBU, communityPoolTester.address);
             const gasPrice = tx.gasPrice;
             const wei = minTransactionGas.mul(gasPrice).mul(2);
-            await communityPoolTester.connect(user).rechargeUserWallet(schainNameRGBU, user.address, { value: wei.toString()});
-            await messageProxyTester.connect(deployer).refundGasByUser(schainHashRGBU, node.address, user.address, 1000000, {gasPrice});
+            await communityPoolTester.connect(user).rechargeUserWallet(schainNameRGBU, user.address, { value: wei.toString() });
+            await messageProxyTester.connect(deployer).refundGasByUser(schainHashRGBU, node.address, user.address, 1000000, { gasPrice });
             const balanceAfter = await getBalance(node.address);
             (balanceAfter).should.be.almost(balanceBefore + (1000000 * BigNumber.from(gasPrice).toNumber()) / 1e18);
         });
@@ -315,9 +340,9 @@ describe("CommunityPool", () => {
             const gasPrice = tx.gasPrice;
             const wei = minTransactionGas.mul(gasPrice);
             expect(await communityPoolTester.activeUsers(user.address, schainHashRGBU)).to.be.false;
-            await communityPoolTester.connect(user).rechargeUserWallet(schainNameRGBU, user.address, { value: wei.toString()});
+            await communityPoolTester.connect(user).rechargeUserWallet(schainNameRGBU, user.address, { value: wei.toString() });
             expect(await communityPoolTester.activeUsers(user.address, schainHashRGBU)).to.be.true;
-            await messageProxyTester.connect(deployer).refundGasByUser(schainHashRGBU, node.address, user.address, 1000000, {gasPrice});
+            await messageProxyTester.connect(deployer).refundGasByUser(schainHashRGBU, node.address, user.address, 1000000, { gasPrice });
             expect(await communityPoolTester.activeUsers(user.address, schainHashRGBU)).to.be.false;
         });
 
@@ -328,19 +353,10 @@ describe("CommunityPool", () => {
             const wei = minTransactionGas.mul(gasPrice);
             gasPrice = gasPrice?.mul(2);
             expect(await communityPoolTester.activeUsers(user.address, schainHashRGBU)).to.be.false;
-            await communityPoolTester.connect(user).rechargeUserWallet(schainNameRGBU, user.address, { value: wei.toString()});
+            await communityPoolTester.connect(user).rechargeUserWallet(schainNameRGBU, user.address, { value: wei.toString() });
             expect(await communityPoolTester.activeUsers(user.address, schainHashRGBU)).to.be.true;
-            await messageProxyTester.connect(deployer).refundGasByUser(schainHashRGBU, node.address, user.address, 1000000, {gasPrice});
+            await messageProxyTester.connect(deployer).refundGasByUser(schainHashRGBU, node.address, user.address, 1000000, { gasPrice });
             expect(await communityPoolTester.activeUsers(user.address, schainHashRGBU)).to.be.false;
         });
     });
-
-
-
-    it("should set rejected when call refundGasBySchainWallet not from messageProxy contract", async () => {
-        const schainHash = stringValue(web3.utils.soliditySha3("Schain"));
-        await communityPool.connect(deployer).refundGasBySchainWallet(schainHash, node.address, 0)
-            .should.be.eventually.rejectedWith("Sender is not a MessageProxy");
-    });
-
 });
diff --git a/proxy/test/DepositBoxERC1155.ts b/proxy/test/DepositBoxERC1155.ts
index 10860cc29..a5627f629 100644
--- a/proxy/test/DepositBoxERC1155.ts
+++ b/proxy/test/DepositBoxERC1155.ts
@@ -34,7 +34,7 @@ import {
     MessagesTester,
     CommunityPool
 } from "../typechain";
-import { randomString, stringFromHex, stringValue, getPublicKey } from "./utils/helper";
+import { stringFromHex, stringValue, getPublicKey } from "./utils/helper";
 
 import chai = require("chai");
 import chaiAlmost = require("chai-almost");
diff --git a/proxy/test/DepositBoxERC721.ts b/proxy/test/DepositBoxERC721.ts
index d7108957a..9088cb31d 100644
--- a/proxy/test/DepositBoxERC721.ts
+++ b/proxy/test/DepositBoxERC721.ts
@@ -34,7 +34,7 @@ import {
     MessagesTester,
     CommunityPool
 } from "../typechain";
-import { randomString, stringFromHex, stringValue, getPublicKey } from "./utils/helper";
+import { stringFromHex, stringValue, getPublicKey } from "./utils/helper";
 
 import chai = require("chai");
 import chaiAlmost = require("chai-almost");
diff --git a/proxy/test/DepositBoxERC721WithMetadata.ts b/proxy/test/DepositBoxERC721WithMetadata.ts
index 98e80989b..1c8119d93 100644
--- a/proxy/test/DepositBoxERC721WithMetadata.ts
+++ b/proxy/test/DepositBoxERC721WithMetadata.ts
@@ -34,7 +34,7 @@ import {
     MessagesTester,
     CommunityPool
 } from "../typechain";
-import { randomString, stringFromHex, stringValue, getPublicKey } from "./utils/helper";
+import { stringFromHex, stringValue, getPublicKey } from "./utils/helper";
 
 import chai = require("chai");
 import chaiAlmost = require("chai-almost");
diff --git a/proxy/test/DepositBoxEth.ts b/proxy/test/DepositBoxEth.ts
index c94071b76..9c491f6a7 100644
--- a/proxy/test/DepositBoxEth.ts
+++ b/proxy/test/DepositBoxEth.ts
@@ -37,7 +37,7 @@ import {
     ERC20OnChain,
     CommunityPool
 } from "../typechain";
-import { randomString, stringFromHex, stringValue, getPublicKey } from "./utils/helper";
+import { stringFromHex, stringValue, getPublicKey } from "./utils/helper";
 
 import chai = require("chai");
 import chaiAlmost = require("chai-almost");
@@ -277,6 +277,7 @@ describe("DepositBoxEth", () => {
             await messageProxy.addConnectedChain(schainName);
             await initializeSchain(contractManager, schainName, deployer.address, 1, 1);
             await setCommonPublicKey(contractManager, schainName);
+            await communityPool.addSchainContract(schainName, deployer.address);
             await communityPool
                 .connect(user)
                 .rechargeUserWallet(schainName, user.address, { value: wei });
diff --git a/proxy/test/Linker.ts b/proxy/test/Linker.ts
index 105098eec..af63d296a 100644
--- a/proxy/test/Linker.ts
+++ b/proxy/test/Linker.ts
@@ -33,7 +33,7 @@ import {
     Linker,
     MessageProxyForMainnet,
 } from "../typechain";
-import { randomString, stringValue } from "./utils/helper";
+import { stringValue } from "./utils/helper";
 
 
 chai.should();
diff --git a/proxy/test/MessageProxy.ts b/proxy/test/MessageProxy.ts
index 0937901a6..2ac1670b4 100644
--- a/proxy/test/MessageProxy.ts
+++ b/proxy/test/MessageProxy.ts
@@ -40,8 +40,7 @@ import {
     EtherbaseMock,
     SchainsInternal
 } from "../typechain/";
-import { randomString, stringToHex, getPublicKey } from "./utils/helper";
-import ABIReceiverMock = require("../artifacts/contracts/test/ReceiverMock.sol/ReceiverMock.json");
+import { stringToHex, getPublicKey } from "./utils/helper";
 import { deployLinker } from "./utils/deploy/mainnet/linker";
 import { deployMessageProxyForMainnet } from "./utils/deploy/mainnet/messageProxyForMainnet";
 import { deployDepositBoxEth } from "./utils/deploy/mainnet/depositBoxEth";
@@ -52,15 +51,14 @@ import { setCommonPublicKey } from "./utils/skale-manager-utils/keyStorage";
 import { deployMessageProxyCaller } from "./utils/deploy/test/messageProxyCaller";
 import { deployMessages } from "./utils/deploy/messages";
 import { deployKeyStorageMock } from "./utils/deploy/test/keyStorageMock";
-import { ethers, web3 } from "hardhat";
+import { ethers } from "hardhat";
 import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address";
 import { BigNumber, Wallet } from "ethers";
-import { assert, expect } from "chai";
+import { expect } from "chai";
 import { MessageProxyForSchainTester } from "../typechain/MessageProxyForSchainTester";
 import { deployMessageProxyForSchainTester } from "./utils/deploy/test/messageProxyForSchainTester";
 import { deployCommunityPool } from "./utils/deploy/mainnet/communityPool";
 import { createNode } from "./utils/skale-manager-utils/nodes";
-import { skipTime } from "./utils/time";
 
 chai.should();
 chai.use((chaiAsPromised));
@@ -173,7 +171,7 @@ describe("MessageProxy", () => {
         });
 
         it("should detect registration state by `isConnectedChain` function", async () => {
-            const newSchainName = randomString(10);
+            const newSchainName = "NewSchainName";
             const isConnectedChain = await messageProxyForMainnet.isConnectedChain(newSchainName);
             isConnectedChain.should.be.deep.equal(Boolean(false));
             await messageProxyForMainnet.connect(deployer).addConnectedChain(newSchainName);
@@ -214,7 +212,7 @@ describe("MessageProxy", () => {
             notConnectedChain.should.be.deep.equal(Boolean(false));
         });
 
-        it("should post outgoing message", async () => {
+        it("should post outgoing message twice", async () => {
             const contractAddress = messageProxyForMainnet.address;
             const amount = 4;
             const bytesData = await messages.encodeTransferEthMessage(user.address, amount);
@@ -224,11 +222,29 @@ describe("MessageProxy", () => {
                 .should.be.rejectedWith("Destination chain is not initialized");
 
             await messageProxyForMainnet.connect(deployer).addConnectedChain(schainName);
-            await caller
-                .postOutgoingMessageTester(messageProxyForMainnet.address, schainHash, contractAddress, bytesData);
-            const outgoingMessagesCounter = BigNumber.from(
-                await messageProxyForMainnet.getOutgoingMessagesCounter(schainName));
+            const message1 = caller.postOutgoingMessageTester(messageProxyForMainnet.address, schainHash, contractAddress, bytesData);
+
+            await expect(message1)
+                .to.emit(messageProxyForMainnet, 'PreviousMessageReference')
+                .withArgs(0, 0);
+
+            let outgoingMessagesCounter = BigNumber.from(
+                await messageProxyForMainnet.getOutgoingMessagesCounter(schainName)
+            );
             outgoingMessagesCounter.should.be.deep.equal(BigNumber.from(1));
+            const lastOutgoingMessageBlockId = BigNumber.from(
+                await messageProxyForMainnet.getLastOutgoingMessageBlockId(schainName)
+            );
+
+            const message2 = caller.postOutgoingMessageTester(messageProxyForMainnet.address, schainHash, contractAddress, bytesData);
+            await expect(message2)
+                .to.emit(messageProxyForMainnet, 'PreviousMessageReference')
+                .withArgs(1, lastOutgoingMessageBlockId);
+
+            outgoingMessagesCounter = BigNumber.from(
+                await messageProxyForMainnet.getOutgoingMessagesCounter(schainName)
+            );
+            outgoingMessagesCounter.should.be.deep.equal(BigNumber.from(2));
         });
 
         it("should pause with a role and unpause", async () => {
@@ -363,6 +379,7 @@ describe("MessageProxy", () => {
             await setCommonPublicKey(contractManager, schainName);
             await messageProxyForMainnet.registerExtraContract(schainName, communityPool.address);
             await depositBox.addSchainContract(schainName, deployer.address);
+            await communityPool.addSchainContract(schainName, deployer.address);
             const minTransactionGas = await communityPool.minTransactionGas();
             const amountWei = minTransactionGas.mul(gasPrice);
 
@@ -433,7 +450,7 @@ describe("MessageProxy", () => {
             incomingMessagesCounter.should.be.deep.equal(BigNumber.from(4));
         });
 
-        it("should not post incoming messages when IMA bridge is paused", async () => {
+        it("should post incoming message and reimburse from CommunityPool", async () => {
             const startingCounter = 0;
             await initializeSchain(contractManager, schainName, deployer.address, 1, 1);
             const nodeCreationParams = {
@@ -452,6 +469,97 @@ describe("MessageProxy", () => {
             await messageProxyForMainnet.registerExtraContract(schainName, communityPool.address);
             await depositBox.addSchainContract(schainName, deployer.address);
             const minTransactionGas = await communityPool.minTransactionGas();
+            const amountWei = minTransactionGas.mul(gasPrice).mul(2);
+
+            await messageProxyForMainnet.registerExtraContract(schainName, depositBox.address);
+
+            const message1 = {
+                destinationContract: depositBox.address,
+                sender: deployer.address,
+                data: await messages.encodeTransferEthMessage(client.address, 1),
+            };
+
+            const outgoingMessages = [message1];
+            const sign = {
+                blsSignature: BlsSignature,
+                counter: Counter,
+                hashA: HashA,
+                hashB: HashB,
+            };
+
+            await messageProxyForMainnet.connect(deployer).addConnectedChain(schainName);
+            await communityPool.connect(deployer).addSchainContract(schainName, communityPool.address);
+
+            await communityPool.connect(client).rechargeUserWallet(schainName, client.address, {value: amountWei.toString()});
+
+            const testWalletsFactory = await ethers.getContractFactory("Wallets");
+            const testWallets = testWalletsFactory.attach(await contractManager.getContract("Wallets"));
+
+            let balance = await testWallets.getSchainBalance(schainHash);
+            let userBalance = await communityPool.getBalance(client.address, schainName);
+
+            const overrides = {
+                gasPrice
+            }
+
+            await messageProxyForMainnet
+                .connect(nodeAddress)
+                .postIncomingMessages(
+                    schainName,
+                    startingCounter,
+                    outgoingMessages,
+                    sign,
+                    overrides
+                );
+
+            let newBalance = await testWallets.getSchainBalance(schainHash);
+            let newUserBalance = await communityPool.getBalance(client.address, schainName);
+
+            newBalance.should.be.lt(balance);
+            newUserBalance.should.be.deep.equal(userBalance);
+
+            await messageProxyForMainnet.addReimbursedContract(schainName, depositBox.address);
+
+            balance = newBalance;
+            userBalance = newUserBalance;
+
+            await messageProxyForMainnet
+                .connect(nodeAddress)
+                .postIncomingMessages(
+                    schainName,
+                    startingCounter + 1,
+                    outgoingMessages,
+                    sign,
+                    overrides
+                );
+
+            newBalance = await testWallets.getSchainBalance(schainHash);
+            newUserBalance = await communityPool.getBalance(client.address, schainName);
+
+            newBalance.should.be.deep.equal(balance);
+            newUserBalance.toNumber().should.be.lessThan(userBalance.toNumber());
+        });
+
+        it("should not post incoming messages when IMA bridge is paused", async () => {
+            const startingCounter = 0;
+            await initializeSchain(contractManager, schainName, deployer.address, 1, 1);
+            const nodeCreationParams = {
+                port: 1337,
+                nonce: 1337,
+                ip: "0x12345678",
+                publicIp: "0x12345678",
+                publicKey: getPublicKey(nodeAddress),
+                name: "GasCalculationNode",
+                domainName: "gascalculationnode.com"
+            };
+            await createNode(contractManager, nodeAddress.address, nodeCreationParams);
+            await addNodesToSchain(contractManager, schainName, [0]);
+            await setCommonPublicKey(contractManager, schainName);
+            await messageProxyForMainnet.registerExtraContract(schainName, communityPool.address);
+            await depositBox.addSchainContract(schainName, deployer.address);
+            await communityPool.addSchainContract(schainName, deployer.address);
+            await rechargeSchainWallet(contractManager, schainName, deployer.address, "1000000000000000000");
+            const minTransactionGas = await communityPool.minTransactionGas();
             const amountWei = minTransactionGas.mul(gasPrice);
 
             const message1 = {
@@ -569,6 +677,7 @@ describe("MessageProxy", () => {
             await setCommonPublicKey(contractManager, schainName);
             await messageProxyForMainnet.registerExtraContract(schainName, communityPool.address);
             await depositBox.addSchainContract(schainName, deployer.address);
+            await communityPool.addSchainContract(schainName, deployer.address);
             const minTransactionGas = await communityPool.minTransactionGas();
             const amountWei = minTransactionGas.mul(gasPrice);
 
@@ -1032,6 +1141,30 @@ describe("MessageProxy", () => {
                     .should.be.eventually.rejectedWith("Extra contract is already registered");
             });
 
+            it("should register reimbursed contract", async () => {
+                const fakeContractOnSchain = deployer.address;
+                await messageProxyForMainnet.connect(user).addReimbursedContract(schainName,  depositBox.address)
+                    .should.be.eventually.rejectedWith("Not enough permissions to add reimbursed contract");
+                await messageProxyForMainnet.addReimbursedContract(schainName, fakeContractOnSchain)
+                    .should.be.eventually.rejectedWith("Given address is not a contract");
+                await messageProxyForMainnet.addReimbursedContract(schainName, depositBox.address)
+                    .should.be.eventually.rejectedWith("Contract is not registered");
+
+                expect((await messageProxyForMainnet.getReimbursedContractsLength(schainHash)).toString()).to.be.equal("0");
+                expect(await messageProxyForMainnet.isReimbursedContract(schainHash, depositBox.address)).to.be.equal(false);
+                await messageProxyForMainnet.registerExtraContract(schainName, depositBox.address);
+                await messageProxyForMainnet.addReimbursedContract(schainName, depositBox.address);
+                expect(await messageProxyForMainnet.isReimbursedContract(schainHash, depositBox.address)).to.be.equal(true);
+                expect((await messageProxyForMainnet.getReimbursedContractsLength(schainHash)).toString()).to.be.equal("1");
+                expect((await messageProxyForMainnet.getReimbursedContractsRange(schainHash, 0, 1)).length).to.be.equal(1);
+                expect((await messageProxyForMainnet.getReimbursedContractsRange(schainHash, 0, 1))[0]).to.be.equal(depositBox.address);
+                await messageProxyForMainnet.getReimbursedContractsRange(schainHash, 0, 11).should.be.eventually.rejectedWith("Range is incorrect");
+                await messageProxyForMainnet.getReimbursedContractsRange(schainHash, 1, 0).should.be.eventually.rejectedWith("Range is incorrect");;
+
+                await messageProxyForMainnet.addReimbursedContract(schainName, depositBox.address)
+                    .should.be.eventually.rejectedWith("Reimbursed contract is already added");
+            });
+
             it("should remove extra contract", async () => {
                 const fakeContractOnSchain = deployer.address;
                 await messageProxyForMainnet.connect(user).removeExtraContract(schainName,  depositBox.address)
@@ -1086,6 +1219,64 @@ describe("MessageProxy", () => {
                 await messageProxyForMainnet.removeExtraContractForAll(depositBox.address)
                     .should.be.eventually.rejectedWith("Extra contract is not registered");
             });
+
+            it("should remove reimbursed contract", async () => {
+                const fakeContractOnSchain = deployer.address;
+                await messageProxyForMainnet.connect(user).removeReimbursedContract(schainName,  depositBox.address)
+                    .should.be.eventually.rejectedWith("Not enough permissions to remove reimbursed contract");
+                await messageProxyForMainnet.removeReimbursedContract(schainName, fakeContractOnSchain)
+                    .should.be.eventually.rejectedWith("Reimbursed contract is not added");
+
+                expect((await messageProxyForMainnet.getReimbursedContractsLength(schainHash)).toString()).to.be.equal("0");
+                await messageProxyForMainnet.registerExtraContract(schainName, depositBox.address);
+                await expect(
+                    messageProxyForMainnet.addReimbursedContract(schainName, depositBox.address)
+                ).to.emit(
+                    messageProxyForMainnet,
+                    "ReimbursedContractAdded"
+                ).withArgs(schainHash, depositBox.address);
+                expect((await messageProxyForMainnet.getReimbursedContractsLength(schainHash)).toString()).to.be.equal("1");
+                await expect(
+                    messageProxyForMainnet.removeReimbursedContract(schainName, depositBox.address)
+                ).to.emit(
+                    messageProxyForMainnet,
+                    "ReimbursedContractRemoved"
+                ).withArgs(schainHash, depositBox.address);
+                expect((await messageProxyForMainnet.getReimbursedContractsLength(schainHash)).toString()).to.be.equal("0");
+
+                await messageProxyForMainnet.removeReimbursedContract(schainName, depositBox.address)
+                    .should.be.eventually.rejectedWith("Reimbursed contract is not added");
+                expect(await messageProxyForMainnet.isReimbursedContract(schainHash, depositBox.address)).to.be.equal(false);
+            });
+
+            it("should remove reimbursed contract when remove extra contract", async () => {
+                const fakeContractOnSchain = deployer.address;
+                await messageProxyForMainnet.connect(user).removeReimbursedContract(schainName,  depositBox.address)
+                    .should.be.eventually.rejectedWith("Not enough permissions to remove reimbursed contract");
+                await messageProxyForMainnet.removeReimbursedContract(schainName, fakeContractOnSchain)
+                    .should.be.eventually.rejectedWith("Reimbursed contract is not added");
+
+                expect((await messageProxyForMainnet.getReimbursedContractsLength(schainHash)).toString()).to.be.equal("0");
+                await messageProxyForMainnet.registerExtraContract(schainName, depositBox.address);
+                await expect(
+                    messageProxyForMainnet.addReimbursedContract(schainName, depositBox.address)
+                ).to.emit(
+                    messageProxyForMainnet,
+                    "ReimbursedContractAdded"
+                ).withArgs(schainHash, depositBox.address);
+                expect((await messageProxyForMainnet.getReimbursedContractsLength(schainHash)).toString()).to.be.equal("1");
+                await expect(
+                    messageProxyForMainnet.removeExtraContract(schainName, depositBox.address)
+                ).to.emit(
+                    messageProxyForMainnet,
+                    "ReimbursedContractRemoved"
+                ).withArgs(schainHash, depositBox.address);
+                expect((await messageProxyForMainnet.getReimbursedContractsLength(schainHash)).toString()).to.be.equal("0");
+
+                await messageProxyForMainnet.removeReimbursedContract(schainName, depositBox.address)
+                    .should.be.eventually.rejectedWith("Reimbursed contract is not added");
+                expect(await messageProxyForMainnet.isReimbursedContract(schainHash, depositBox.address)).to.be.equal(false);
+            });
         });
 
     });
@@ -1475,6 +1666,11 @@ describe("MessageProxy", () => {
             });
         });
 
+        it("should not allow anyone to top up balance with sFuel", async () => {
+            await messageProxyForSchain.connect(user).topUpReceiverBalance(user.address)
+                .should.be.rejectedWith("Sender is not registered");
+        });
+
         describe("register and remove extra contracts", async () => {
             it("should register extra contract", async () => {
                 const fakeContractOnSchain = deployer.address;
diff --git a/proxy/test/TokenManagerERC1155.ts b/proxy/test/TokenManagerERC1155.ts
index 18e46ca82..e634d620c 100644
--- a/proxy/test/TokenManagerERC1155.ts
+++ b/proxy/test/TokenManagerERC1155.ts
@@ -34,7 +34,8 @@ import {
     CommunityLocker
 } from "../typechain";
 
-import { randomString, stringValue } from "./utils/helper";
+import { stringValue } from "./utils/helper";
+import { skipTime } from "./utils/time";
 
 chai.should();
 chai.use((chaiAsPromised as any));
@@ -215,6 +216,7 @@ describe("TokenManagerERC1155", () => {
             tokenManagerERC11552 = await deployTokenManagerERC1155(newSchainName, messageProxyForSchain2.address, tokenManagerLinker2, communityLocker2, fakeDepositBox);
             await erc1155OnTargetChain.connect(deployer).grantRole(await erc1155OnTargetChain.MINTER_ROLE(), tokenManagerERC11552.address);
             await tokenManagerLinker2.registerTokenManager(tokenManagerERC11552.address);
+            await messageProxyForSchain2.registerExtraContractForAll(tokenManagerERC11552.address);
         });
 
         it("should invoke `transferToSchainERC1155` without mistakes", async () => {
@@ -248,6 +250,93 @@ describe("TokenManagerERC1155", () => {
             outgoingMessagesCounter.should.be.deep.equal(BigNumber.from(1));
         });
 
+        it("should reject `transferToSchainERC1155` when executing earlier then allowed", async () => {
+            // add connected chain:
+            await messageProxyForSchain.connect(deployer).grantRole(await messageProxyForSchain.CHAIN_CONNECTOR_ROLE(), deployer.address);
+            await messageProxyForSchain.connect(deployer).addConnectedChain(newSchainName);
+
+            await erc1155OnOriginChain.connect(deployer).mint(user.address, id, 5, "0x");
+            await erc1155OnOriginChain.connect(user).setApprovalForAll(tokenManagerERC1155.address, true);
+
+            await tokenManagerERC1155
+                .connect(user)
+                .transferToSchainERC1155(newSchainName, erc1155OnOriginChain.address, id, 1)
+                .should.be.eventually.rejectedWith("Incorrect Token Manager address");
+
+            await tokenManagerERC1155
+                .connect(user)
+                .transferToSchainERC1155("Mainnet", erc1155OnOriginChain.address, id, 1)
+                .should.be.eventually.rejectedWith("This function is not for transferring to Mainnet");
+
+            await tokenManagerERC1155.addTokenManager(newSchainName, tokenManagerERC11552.address);
+
+            await tokenManagerERC1155
+                .connect(user)
+                .transferToSchainERC1155(newSchainName, erc1155OnOriginChain.address, id, 1);
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(1));
+
+
+            await tokenManagerERC1155
+                .connect(user)
+                .transferToSchainERC1155(newSchainName, erc1155OnOriginChain.address, id, 1);
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(2));
+
+            await communityLocker.grantRole(await communityLocker.CONSTANT_SETTER_ROLE(), deployer.address);
+
+            await communityLocker.setTimeLimitPerMessage(newSchainName, 100);
+
+            await tokenManagerERC1155
+                .connect(user)
+                .transferToSchainERC1155(newSchainName, erc1155OnOriginChain.address, id, 1)
+                .should.be.eventually.rejectedWith("Exceeded message rate limit");
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(2));
+
+            await skipTime(90);
+
+            await tokenManagerERC1155
+                .connect(user)
+                .transferToSchainERC1155(newSchainName, erc1155OnOriginChain.address, id, 1)
+                .should.be.eventually.rejectedWith("Exceeded message rate limit");
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(2));
+
+            await skipTime(20);
+
+            await tokenManagerERC1155
+                .connect(user)
+                .transferToSchainERC1155(newSchainName, erc1155OnOriginChain.address, id, 1);
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(3));
+
+            await communityLocker.setTimeLimitPerMessage(newSchainName, 0);
+
+            await tokenManagerERC1155
+                .connect(user)
+                .transferToSchainERC1155(newSchainName, erc1155OnOriginChain.address, id, 1);
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(4));
+
+            await communityLocker.setTimeLimitPerMessage(newSchainName, 100);
+
+            await tokenManagerERC1155
+                .connect(user)
+                .transferToSchainERC1155(newSchainName, erc1155OnOriginChain.address, id, 1)
+                .should.be.eventually.rejectedWith("Exceeded message rate limit");
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(4));
+
+            await skipTime(110);
+
+            await tokenManagerERC1155
+                .connect(user)
+                .transferToSchainERC1155(newSchainName, erc1155OnOriginChain.address, id, 1);
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(5));
+        });
+
         it("should invoke `transferToSchainERC1155` and receive tokens without mistakes", async () => {
             // add connected chain:
             await messageProxyForSchain.connect(deployer).grantRole(await messageProxyForSchain.CHAIN_CONNECTOR_ROLE(), deployer.address);
@@ -536,13 +625,6 @@ describe("TokenManagerERC1155", () => {
 
             await targetErc1155OnChain.connect(user).setApprovalForAll(tokenManagerERC11552.address, true);
 
-            await tokenManagerERC11552
-                .connect(user)
-                .transferToSchainERC1155(schainName, erc1155OnOriginChain.address, id, amount)
-                .should.be.eventually.rejectedWith("Sender contract is not registered");
-
-            await messageProxyForSchain2.registerExtraContract(schainName, tokenManagerERC11552.address);
-
             await tokenManagerERC11552
                 .connect(user)
                 .transferToSchainERC1155(schainName, erc1155OnOriginChain.address, id, amount);
@@ -628,13 +710,6 @@ describe("TokenManagerERC1155", () => {
 
             await erc1155OnTargetChain.connect(user).setApprovalForAll(tokenManagerERC11552.address, true);
 
-            await tokenManagerERC11552
-                .connect(user)
-                .transferToSchainERC1155(schainName, erc1155OnOriginChain.address, id, amount)
-                .should.be.eventually.rejectedWith("Sender contract is not registered");
-
-            await messageProxyForSchain2.registerExtraContract(schainName, tokenManagerERC11552.address);
-
             await tokenManagerERC11552
                 .connect(user)
                 .transferToSchainERC1155(schainName, erc1155OnOriginChain.address, id, amount);
@@ -725,13 +800,6 @@ describe("TokenManagerERC1155", () => {
 
             await targetErc1155OnChain.connect(user).setApprovalForAll(tokenManagerERC11552.address, true);
 
-            await tokenManagerERC11552
-                .connect(user)
-                .transferToSchainERC1155(schainName, erc1155OnOriginChain.address, id, amount)
-                .should.be.eventually.rejectedWith("Sender contract is not registered");
-
-            await messageProxyForSchain2.registerExtraContract(schainName, tokenManagerERC11552.address);
-
             await tokenManagerERC11552
                 .connect(user)
                 .transferToSchainERC1155(schainName, erc1155OnOriginChain.address, id, amount);
@@ -885,13 +953,6 @@ describe("TokenManagerERC1155", () => {
 
             await erc1155OnTargetChain.connect(user).setApprovalForAll(tokenManagerERC11552.address, true);
 
-            await tokenManagerERC11552
-                .connect(user)
-                .transferToSchainERC1155(schainName, erc1155OnOriginChain.address, id, amount)
-                .should.be.eventually.rejectedWith("Sender contract is not registered");
-
-            await messageProxyForSchain2.registerExtraContract(schainName, tokenManagerERC11552.address);
-
             await tokenManagerERC11552
                 .connect(user)
                 .transferToSchainERC1155(schainName, erc1155OnOriginChain.address, id, amount);
@@ -978,8 +1039,6 @@ describe("TokenManagerERC1155", () => {
         });
 
         it("should not be able to transfer X->Y->Z", async () => {
-            // await messageProxyForSchain.registerExtraContract(newSchainName, tokenManagerERC1155.address);
-
             // add connected chain:
             await messageProxyForSchain.connect(deployer).grantRole(await messageProxyForSchain.CHAIN_CONNECTOR_ROLE(), deployer.address);
             await messageProxyForSchain.connect(deployer).addConnectedChain(newSchainName);
@@ -1041,7 +1100,6 @@ describe("TokenManagerERC1155", () => {
             await messageProxyForSchain2.connect(deployer).grantRole(await messageProxyForSchain2.CHAIN_CONNECTOR_ROLE(), deployer.address);
             await messageProxyForSchain2.connect(deployer).addConnectedChain(newSchainNameZ);
 
-            await messageProxyForSchain2.registerExtraContract(newSchainNameZ, tokenManagerERC11552.address);
             await tokenManagerERC11552.addTokenManager(newSchainNameZ, tokenManagerERC1155Z.address);
 
             await erc1155OnTargetChain.connect(user).setApprovalForAll(tokenManagerERC11552.address, true);
@@ -1058,8 +1116,6 @@ describe("TokenManagerERC1155", () => {
         });
 
         it("should not be able to transfer main chain token or clone to mainnet", async () => {
-            // await messageProxyForSchain.registerExtraContract(newSchainName, tokenManagerERC1155.address);
-
             // add connected chain:
             await messageProxyForSchain.connect(deployer).grantRole(await messageProxyForSchain.CHAIN_CONNECTOR_ROLE(), deployer.address);
             await messageProxyForSchain.connect(deployer).addConnectedChain(newSchainName);
@@ -1117,8 +1173,6 @@ describe("TokenManagerERC1155", () => {
                 .exitToMainERC1155(erc1155OnTargetChain.address, id, amount)
                 .should.be.eventually.rejectedWith("Incorrect main chain token");
 
-            await messageProxyForSchain2.registerExtraContract(schainName, tokenManagerERC11552.address);
-
             await tokenManagerERC11552
                 .connect(user)
                 .transferToSchainERC1155(schainName, erc1155OnOriginChain.address, id, amount);
@@ -1170,6 +1224,7 @@ describe("TokenManagerERC1155", () => {
             tokenManagerERC11552 = await deployTokenManagerERC1155(newSchainName, messageProxyForSchain2.address, tokenManagerLinker2, communityLocker2, fakeDepositBox);
             await erc1155OnTargetChain.connect(deployer).grantRole(await erc1155OnTargetChain.MINTER_ROLE(), tokenManagerERC11552.address);
             await tokenManagerLinker2.registerTokenManager(tokenManagerERC11552.address);
+            await messageProxyForSchain2.registerExtraContractForAll(tokenManagerERC11552.address);
         });
 
         it("should invoke `transferToSchainERC1155` without mistakes", async () => {
@@ -1203,6 +1258,93 @@ describe("TokenManagerERC1155", () => {
             outgoingMessagesCounter.should.be.deep.equal(BigNumber.from(1));
         });
 
+        it("should reject `transferToSchainERC1155` when executing earlier then allowed", async () => {
+            // add connected chain:
+            await messageProxyForSchain.connect(deployer).grantRole(await messageProxyForSchain.CHAIN_CONNECTOR_ROLE(), deployer.address);
+            await messageProxyForSchain.connect(deployer).addConnectedChain(newSchainName);
+
+            await erc1155OnOriginChain.connect(deployer).mintBatch(user.address, ids, [5, 5, 5, 5], "0x");
+            await erc1155OnOriginChain.connect(user).setApprovalForAll(tokenManagerERC1155.address, true);
+
+            await tokenManagerERC1155
+                .connect(user)
+                .transferToSchainERC1155Batch(newSchainName, erc1155OnOriginChain.address, ids, [1, 1, 1, 1])
+                .should.be.eventually.rejectedWith("Incorrect Token Manager address");
+
+            await tokenManagerERC1155
+                .connect(user)
+                .transferToSchainERC1155Batch("Mainnet", erc1155OnOriginChain.address, ids, [1, 1, 1, 1])
+                .should.be.eventually.rejectedWith("This function is not for transferring to Mainnet");
+
+            await tokenManagerERC1155.addTokenManager(newSchainName, tokenManagerERC11552.address);
+
+            await tokenManagerERC1155
+                .connect(user)
+                .transferToSchainERC1155Batch(newSchainName, erc1155OnOriginChain.address, ids, [1, 1, 1, 1]);
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(1));
+
+
+            await tokenManagerERC1155
+                .connect(user)
+                .transferToSchainERC1155Batch(newSchainName, erc1155OnOriginChain.address, ids, [1, 1, 1, 1]);
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(2));
+
+            await communityLocker.grantRole(await communityLocker.CONSTANT_SETTER_ROLE(), deployer.address);
+
+            await communityLocker.setTimeLimitPerMessage(newSchainName, 100);
+
+            await tokenManagerERC1155
+                .connect(user)
+                .transferToSchainERC1155Batch(newSchainName, erc1155OnOriginChain.address, ids, [1, 1, 1, 1])
+                .should.be.eventually.rejectedWith("Exceeded message rate limit");
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(2));
+
+            await skipTime(90);
+
+            await tokenManagerERC1155
+                .connect(user)
+                .transferToSchainERC1155Batch(newSchainName, erc1155OnOriginChain.address, ids, [1, 1, 1, 1])
+                .should.be.eventually.rejectedWith("Exceeded message rate limit");
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(2));
+
+            await skipTime(20);
+
+            await tokenManagerERC1155
+                .connect(user)
+                .transferToSchainERC1155Batch(newSchainName, erc1155OnOriginChain.address, ids, [1, 1, 1, 1]);
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(3));
+
+            await communityLocker.setTimeLimitPerMessage(newSchainName, 0);
+
+            await tokenManagerERC1155
+                .connect(user)
+                .transferToSchainERC1155Batch(newSchainName, erc1155OnOriginChain.address, ids, [1, 1, 1, 1]);
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(4));
+
+            await communityLocker.setTimeLimitPerMessage(newSchainName, 100);
+
+            await tokenManagerERC1155
+                .connect(user)
+                .transferToSchainERC1155Batch(newSchainName, erc1155OnOriginChain.address, ids, [1, 1, 1, 1])
+                .should.be.eventually.rejectedWith("Exceeded message rate limit");
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(4));
+
+            await skipTime(110);
+
+            await tokenManagerERC1155
+                .connect(user)
+                .transferToSchainERC1155Batch(newSchainName, erc1155OnOriginChain.address, ids, [1, 1, 1, 1]);
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(5));
+        });
+
         it("should invoke `transferToSchainERC1155` and receive tokens without mistakes", async () => {
             // add connected chain:
             await messageProxyForSchain.connect(deployer).grantRole(await messageProxyForSchain.CHAIN_CONNECTOR_ROLE(), deployer.address);
@@ -1524,13 +1666,6 @@ describe("TokenManagerERC1155", () => {
 
             await targetErc1155OnChain.connect(user).setApprovalForAll(tokenManagerERC11552.address, true);
 
-            await tokenManagerERC11552
-                .connect(user)
-                .transferToSchainERC1155Batch(schainName, erc1155OnOriginChain.address, ids, amounts)
-                .should.be.eventually.rejectedWith("Sender contract is not registered");
-
-            await messageProxyForSchain2.registerExtraContract(schainName, tokenManagerERC11552.address);
-
             await tokenManagerERC11552
                 .connect(user)
                 .transferToSchainERC1155Batch(schainName, erc1155OnOriginChain.address, ids, amounts);
@@ -1621,13 +1756,6 @@ describe("TokenManagerERC1155", () => {
 
             await erc1155OnTargetChain.connect(user).setApprovalForAll(tokenManagerERC11552.address, true);
 
-            await tokenManagerERC11552
-                .connect(user)
-                .transferToSchainERC1155Batch(schainName, erc1155OnOriginChain.address, ids, amounts)
-                .should.be.eventually.rejectedWith("Sender contract is not registered");
-
-            await messageProxyForSchain2.registerExtraContract(schainName, tokenManagerERC11552.address);
-
             await tokenManagerERC11552
                 .connect(user)
                 .transferToSchainERC1155Batch(schainName, erc1155OnOriginChain.address, ids, amounts);
@@ -1730,13 +1858,6 @@ describe("TokenManagerERC1155", () => {
 
             await targetErc1155OnChain.connect(user).setApprovalForAll(tokenManagerERC11552.address, true);
 
-            await tokenManagerERC11552
-                .connect(user)
-                .transferToSchainERC1155Batch(schainName, erc1155OnOriginChain.address, ids, amounts)
-                .should.be.eventually.rejectedWith("Sender contract is not registered");
-
-            await messageProxyForSchain2.registerExtraContract(schainName, tokenManagerERC11552.address);
-
             await tokenManagerERC11552
                 .connect(user)
                 .transferToSchainERC1155Batch(schainName, erc1155OnOriginChain.address, ids, amounts);
@@ -1917,13 +2038,6 @@ describe("TokenManagerERC1155", () => {
 
             await erc1155OnTargetChain.connect(user).setApprovalForAll(tokenManagerERC11552.address, true);
 
-            await tokenManagerERC11552
-                .connect(user)
-                .transferToSchainERC1155Batch(schainName, erc1155OnOriginChain.address, ids, amounts)
-                .should.be.eventually.rejectedWith("Sender contract is not registered");
-
-            await messageProxyForSchain2.registerExtraContract(schainName, tokenManagerERC11552.address);
-
             await tokenManagerERC11552
                 .connect(user)
                 .transferToSchainERC1155Batch(schainName, erc1155OnOriginChain.address, ids, amounts);
@@ -2037,8 +2151,6 @@ describe("TokenManagerERC1155", () => {
         });
 
         it("should not be able to transfer X->Y->Z", async () => {
-            // await messageProxyForSchain.registerExtraContract(newSchainName, tokenManagerERC1155.address);
-
             // add connected chain:
             await messageProxyForSchain.connect(deployer).grantRole(await messageProxyForSchain.CHAIN_CONNECTOR_ROLE(), deployer.address);
             await messageProxyForSchain.connect(deployer).addConnectedChain(newSchainName);
@@ -2105,7 +2217,6 @@ describe("TokenManagerERC1155", () => {
             await messageProxyForSchain2.connect(deployer).grantRole(await messageProxyForSchain2.CHAIN_CONNECTOR_ROLE(), deployer.address);
             await messageProxyForSchain2.connect(deployer).addConnectedChain(newSchainNameZ);
 
-            await messageProxyForSchain2.registerExtraContract(newSchainNameZ, tokenManagerERC11552.address);
             await tokenManagerERC11552.addTokenManager(newSchainNameZ, tokenManagerERC1155Z.address);
 
             await erc1155OnTargetChain.connect(user).setApprovalForAll(tokenManagerERC11552.address, true);
@@ -2122,8 +2233,6 @@ describe("TokenManagerERC1155", () => {
         });
 
         it("should not be able to transfer main chain token or clone to mainnet", async () => {
-            // await messageProxyForSchain.registerExtraContract(newSchainName, tokenManagerERC1155.address);
-
             // add connected chain:
             await messageProxyForSchain.connect(deployer).grantRole(await messageProxyForSchain.CHAIN_CONNECTOR_ROLE(), deployer.address);
             await messageProxyForSchain.connect(deployer).addConnectedChain(newSchainName);
@@ -2186,8 +2295,6 @@ describe("TokenManagerERC1155", () => {
                 .exitToMainERC1155Batch(erc1155OnTargetChain.address, ids, amounts)
                 .should.be.eventually.rejectedWith("Incorrect main chain token");
 
-            await messageProxyForSchain2.registerExtraContract(schainName, tokenManagerERC11552.address);
-
             await tokenManagerERC11552
                 .connect(user)
                 .transferToSchainERC1155Batch(schainName, erc1155OnOriginChain.address, ids, amounts);
diff --git a/proxy/test/TokenManagerERC20.ts b/proxy/test/TokenManagerERC20.ts
index 2e327d982..7cc74b06f 100644
--- a/proxy/test/TokenManagerERC20.ts
+++ b/proxy/test/TokenManagerERC20.ts
@@ -31,10 +31,10 @@ import {
     TokenManagerERC20,
     TokenManagerLinker,
     MessageProxyForSchainTester,
-    CommunityLocker
+    CommunityLocker,
+    EtherbaseMock
 } from "../typechain";
 
-import { randomString, stringValue } from "./utils/helper";
 
 chai.should();
 chai.use((chaiAsPromised as any));
@@ -46,12 +46,13 @@ import { deployTokenManagerLinker } from "./utils/deploy/schain/tokenManagerLink
 import { deployMessages } from "./utils/deploy/messages";
 import { deployCommunityLocker } from "./utils/deploy/schain/communityLocker";
 
-import { ethers, web3 } from "hardhat";
+import { ethers } from "hardhat";
 import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address";
 import { BigNumber } from "ethers";
 
 import { assert, expect } from "chai";
 import { deployKeyStorageMock } from "./utils/deploy/test/keyStorageMock";
+import { skipTime } from "./utils/time";
 
 describe("TokenManagerERC20", () => {
     let deployer: SignerWithAddress;
@@ -60,8 +61,8 @@ describe("TokenManagerERC20", () => {
 
     const mainnetName = "Mainnet";
     const schainName = "D2-chain";
-    const schainId = stringValue(web3.utils.soliditySha3(schainName));
-    const mainnetId = stringValue(web3.utils.soliditySha3("Mainnet"));
+    const schainId = ethers.utils.solidityKeccak256(["string"], [schainName]);
+    const mainnetId = ethers.utils.solidityKeccak256(["string"], ["Mainnet"]);
     let fakeDepositBox: string;
     let fakeCommunityPool: any;
     let erc20OnChain: ERC20OnChain;
@@ -133,6 +134,8 @@ describe("TokenManagerERC20", () => {
         const data2 = await messages.encodeTransferErc20AndTokenInfoMessage(erc20OnMainnet.address, to, amount, totalSupply, { name, symbol, decimals: 18 });
 
         await tokenManagerErc20.connect(schainOwner).enableAutomaticDeploy();
+        await messageProxyForSchain.registerExtraContractForAll(tokenManagerErc20.address);
+
         // execution
         const res = await (await messageProxyForSchain.postMessage(tokenManagerErc20.address, mainnetId, fakeDepositBox, data)).wait();
 
@@ -142,7 +145,7 @@ describe("TokenManagerERC20", () => {
             assert("No events were emitted");
         } else {
             newAddress = "0x" + res.events[res.events.length - 1].topics[3].slice(-40);
-            const newERC20Contract = await (await ethers.getContractFactory("ERC20OnChain")).attach(newAddress) as ERC20OnChain;
+            const newERC20Contract = (await ethers.getContractFactory("ERC20OnChain")).attach(newAddress) as ERC20OnChain;
             let balance = await newERC20Contract.functions.balanceOf(to);
             parseInt(balance.toString(), 10).should.be.equal(amount);
             // expectation
@@ -255,7 +258,7 @@ describe("TokenManagerERC20", () => {
         let tokenManagerErc202: TokenManagerERC20;
         let communityLocker2: CommunityLocker;
         const newSchainName = "NewChain";
-        const newSchainId = stringValue(web3.utils.soliditySha3(newSchainName));
+        const newSchainId = ethers.utils.solidityKeccak256(["string"], [newSchainName]);
 
         beforeEach(async () => {
             erc20OnOriginChain = await deployERC20OnChain("NewToken", "NTN");
@@ -304,6 +307,107 @@ describe("TokenManagerERC20", () => {
             outgoingMessagesCounter.should.be.deep.equal(BigNumber.from(1));
         });
 
+        it("should reject `transferToSchainERC20` when executing earlier than allowed", async () => {
+            const amount = "20000000000000000";
+            await messageProxyForSchain.registerExtraContract(newSchainName, tokenManagerErc20.address);
+
+            // add connected chain:
+            await messageProxyForSchain.connect(deployer).grantRole(await messageProxyForSchain.CHAIN_CONNECTOR_ROLE(), deployer.address);
+            await messageProxyForSchain.connect(deployer).addConnectedChain(newSchainName);
+
+            await erc20OnOriginChain.connect(deployer).mint(user.address, amount);
+            await erc20OnOriginChain.connect(user).approve(tokenManagerErc20.address, amount);
+
+            await tokenManagerErc20
+                .connect(user)
+                .transferToSchainERC20(newSchainName, erc20OnOriginChain.address, amount)
+                .should.be.eventually.rejectedWith("Incorrect Token Manager address");
+
+            await tokenManagerErc20
+                .connect(user)
+                .transferToSchainERC20("Mainnet", erc20OnOriginChain.address, amount)
+                .should.be.eventually.rejectedWith("This function is not for transferring to Mainnet");
+
+            await tokenManagerErc20.addTokenManager(newSchainName, tokenManagerErc202.address);
+
+            await tokenManagerErc20
+                .connect(user)
+                .transferToSchainERC20(newSchainName, erc20OnOriginChain.address, amount);
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(1));
+
+            await erc20OnOriginChain.connect(deployer).mint(user.address, amount);
+            await erc20OnOriginChain.connect(user).approve(tokenManagerErc20.address, amount);
+
+            await tokenManagerErc20
+                .connect(user)
+                .transferToSchainERC20(newSchainName, erc20OnOriginChain.address, amount);
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(2));
+
+            await communityLocker.grantRole(await communityLocker.CONSTANT_SETTER_ROLE(), deployer.address);
+
+            await communityLocker.setTimeLimitPerMessage(newSchainName, 100);
+
+            await erc20OnOriginChain.connect(deployer).mint(user.address, amount);
+            await erc20OnOriginChain.connect(user).approve(tokenManagerErc20.address, amount);
+
+            await tokenManagerErc20
+                .connect(user)
+                .transferToSchainERC20(newSchainName, erc20OnOriginChain.address, amount)
+                .should.be.eventually.rejectedWith("Exceeded message rate limit");
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(2));
+
+            await skipTime(90);
+
+            await tokenManagerErc20
+                .connect(user)
+                .transferToSchainERC20(newSchainName, erc20OnOriginChain.address, amount)
+                .should.be.eventually.rejectedWith("Exceeded message rate limit");
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(2));
+
+            await skipTime(20);
+
+            await tokenManagerErc20
+                .connect(user)
+                .transferToSchainERC20(newSchainName, erc20OnOriginChain.address, amount);
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(3));
+
+            await communityLocker.setTimeLimitPerMessage(newSchainName, 0);
+
+            await erc20OnOriginChain.connect(deployer).mint(user.address, amount);
+            await erc20OnOriginChain.connect(user).approve(tokenManagerErc20.address, amount);
+
+            await tokenManagerErc20
+                .connect(user)
+                .transferToSchainERC20(newSchainName, erc20OnOriginChain.address, amount);
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(4));
+
+            await communityLocker.setTimeLimitPerMessage(newSchainName, 100);
+
+            await erc20OnOriginChain.connect(deployer).mint(user.address, amount);
+            await erc20OnOriginChain.connect(user).approve(tokenManagerErc20.address, amount);
+
+            await tokenManagerErc20
+                .connect(user)
+                .transferToSchainERC20(newSchainName, erc20OnOriginChain.address, amount)
+                .should.be.eventually.rejectedWith("Exceeded message rate limit");
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(4));
+
+            await skipTime(110);
+
+            await tokenManagerErc20
+                .connect(user)
+                .transferToSchainERC20(newSchainName, erc20OnOriginChain.address, amount);
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(5));
+        });
+
         it("should invoke `transferToSchainERC20` and receive tokens without mistakes", async () => {
             const amount = "20000000000000000";
             await messageProxyForSchain.registerExtraContract(newSchainName, tokenManagerErc20.address);
@@ -358,18 +462,20 @@ describe("TokenManagerERC20", () => {
             await messageProxyForSchain2.postMessage(tokenManagerErc202.address, schainId, tokenManagerErc20.address, data).should.be.eventually.rejectedWith("Automatic deploy is disabled");
 
             await tokenManagerErc202.enableAutomaticDeploy();
+            await messageProxyForSchain2.registerExtraContractForAll(tokenManagerErc202.address);
 
             await messageProxyForSchain2.postMessage(tokenManagerErc202.address, schainId, tokenManagerErc20.address, data);
 
             const addressERC20OnSchain = await tokenManagerErc202.clonesErc20(schainId, erc20OnOriginChain.address);
-            const targetErc20OnChain = await (await ethers.getContractFactory("ERC20OnChain")).attach(addressERC20OnSchain) as ERC20OnChain;
+            const targetErc20OnChain = (await ethers.getContractFactory("ERC20OnChain")).attach(addressERC20OnSchain) as ERC20OnChain;
             expect((await targetErc20OnChain.functions.balanceOf(user.address)).toString()).to.be.equal(amount);
 
         });
 
-        it("should invoke `transferToSchainERC20` and receive tokens without mistakes double", async () => {
+        it("should invoke `transferToSchainERC20` and receive tokens without mistakes back and forward twice", async () => {
             const amount = "20000000000000000";
             await messageProxyForSchain.registerExtraContract(newSchainName, tokenManagerErc20.address);
+            await messageProxyForSchain2.registerExtraContractForAll(tokenManagerErc202.address);
 
             // add connected chain:
             await messageProxyForSchain.connect(deployer).grantRole(await messageProxyForSchain.CHAIN_CONNECTOR_ROLE(), deployer.address);
@@ -425,7 +531,7 @@ describe("TokenManagerERC20", () => {
             await messageProxyForSchain2.postMessage(tokenManagerErc202.address, schainId, tokenManagerErc20.address, data);
 
             const addressERC20OnSchain = await tokenManagerErc202.clonesErc20(schainId, erc20OnOriginChain.address);
-            const targetErc20OnChain = await (await ethers.getContractFactory("ERC20OnChain")).attach(addressERC20OnSchain) as ERC20OnChain;
+            const targetErc20OnChain = (await ethers.getContractFactory("ERC20OnChain")).attach(addressERC20OnSchain) as ERC20OnChain;
             expect((await targetErc20OnChain.functions.balanceOf(user.address)).toString()).to.be.equal(amount);
 
             const amount2 = "50000000000000000";
@@ -455,6 +561,7 @@ describe("TokenManagerERC20", () => {
         it("should invoke `transferToSchainERC20` and receive tokens without mistakes double with attached token", async () => {
             const amount = "20000000000000000";
             await messageProxyForSchain.registerExtraContract(newSchainName, tokenManagerErc20.address);
+            await messageProxyForSchain2.registerExtraContractForAll(tokenManagerErc202.address);
 
             // add connected chain:
             await messageProxyForSchain.connect(deployer).grantRole(await messageProxyForSchain.CHAIN_CONNECTOR_ROLE(), deployer.address);
@@ -538,7 +645,8 @@ describe("TokenManagerERC20", () => {
 
         it("should invoke `transferToSchainERC20` and transfer back without mistakes", async () => {
             const amount = "20000000000000000";
-            await messageProxyForSchain.registerExtraContract(newSchainName, tokenManagerErc20.address);
+            await messageProxyForSchain.registerExtraContractForAll(tokenManagerErc20.address);
+            await messageProxyForSchain2.registerExtraContractForAll(tokenManagerErc202.address);
 
             // add connected chain:
             await messageProxyForSchain.connect(deployer).grantRole(await messageProxyForSchain.CHAIN_CONNECTOR_ROLE(), deployer.address);
@@ -594,7 +702,7 @@ describe("TokenManagerERC20", () => {
             await messageProxyForSchain2.postMessage(tokenManagerErc202.address, schainId, tokenManagerErc20.address, data);
 
             const addressERC20OnSchain = await tokenManagerErc202.clonesErc20(schainId, erc20OnOriginChain.address);
-            const targetErc20OnChain = await (await ethers.getContractFactory("ERC20OnChain")).attach(addressERC20OnSchain) as ERC20OnChain;
+            const targetErc20OnChain = (await ethers.getContractFactory("ERC20OnChain")).attach(addressERC20OnSchain) as ERC20OnChain;
             expect((await targetErc20OnChain.functions.balanceOf(user.address)).toString()).to.be.equal(amount);
 
             await tokenManagerErc202
@@ -614,13 +722,6 @@ describe("TokenManagerERC20", () => {
 
             await targetErc20OnChain.connect(user).approve(tokenManagerErc202.address, amount);
 
-            await tokenManagerErc202
-                .connect(user)
-                .transferToSchainERC20(schainName, erc20OnOriginChain.address, amount)
-                .should.be.eventually.rejectedWith("Sender contract is not registered");
-
-            await messageProxyForSchain2.registerExtraContract(schainName, tokenManagerErc202.address);
-
             await tokenManagerErc202
                 .connect(user)
                 .transferToSchainERC20(schainName, erc20OnOriginChain.address, amount);
@@ -638,7 +739,8 @@ describe("TokenManagerERC20", () => {
 
         it("should invoke `transferToSchainERC20` and transfer back without mistakes with attached tokens", async () => {
             const amount = "20000000000000000";
-            await messageProxyForSchain.registerExtraContract(newSchainName, tokenManagerErc20.address);
+            await messageProxyForSchain.registerExtraContractForAll(tokenManagerErc20.address);
+            await messageProxyForSchain2.registerExtraContractForAll(tokenManagerErc202.address);
 
             // add connected chain:
             await messageProxyForSchain.connect(deployer).grantRole(await messageProxyForSchain.CHAIN_CONNECTOR_ROLE(), deployer.address);
@@ -709,13 +811,6 @@ describe("TokenManagerERC20", () => {
 
             await erc20OnTargetChain.connect(user).approve(tokenManagerErc202.address, amount);
 
-            await tokenManagerErc202
-                .connect(user)
-                .transferToSchainERC20(schainName, erc20OnOriginChain.address, amount)
-                .should.be.eventually.rejectedWith("Sender contract is not registered");
-
-            await messageProxyForSchain2.registerExtraContract(schainName, tokenManagerErc202.address);
-
             await tokenManagerErc202
                 .connect(user)
                 .transferToSchainERC20(schainName, erc20OnOriginChain.address, amount);
@@ -734,7 +829,8 @@ describe("TokenManagerERC20", () => {
 
         it("should invoke `transferToSchainERC20` and transfer back without mistakes double", async () => {
             const amount = "20000000000000000";
-            await messageProxyForSchain.registerExtraContract(newSchainName, tokenManagerErc20.address);
+            await messageProxyForSchain.registerExtraContractForAll(tokenManagerErc20.address);
+            await messageProxyForSchain2.registerExtraContractForAll(tokenManagerErc202.address);
 
             // add connected chain:
             await messageProxyForSchain.connect(deployer).grantRole(await messageProxyForSchain.CHAIN_CONNECTOR_ROLE(), deployer.address);
@@ -790,7 +886,7 @@ describe("TokenManagerERC20", () => {
             await messageProxyForSchain2.postMessage(tokenManagerErc202.address, schainId, tokenManagerErc20.address, data);
 
             const addressERC20OnSchain = await tokenManagerErc202.clonesErc20(schainId, erc20OnOriginChain.address);
-            const targetErc20OnChain = await (await ethers.getContractFactory("ERC20OnChain")).attach(addressERC20OnSchain) as ERC20OnChain;
+            const targetErc20OnChain = (await ethers.getContractFactory("ERC20OnChain")).attach(addressERC20OnSchain) as ERC20OnChain;
             expect((await targetErc20OnChain.functions.balanceOf(user.address)).toString()).to.be.equal(amount);
 
             await tokenManagerErc202
@@ -810,13 +906,6 @@ describe("TokenManagerERC20", () => {
 
             await targetErc20OnChain.connect(user).approve(tokenManagerErc202.address, amount);
 
-            await tokenManagerErc202
-                .connect(user)
-                .transferToSchainERC20(schainName, erc20OnOriginChain.address, amount)
-                .should.be.eventually.rejectedWith("Sender contract is not registered");
-
-            await messageProxyForSchain2.registerExtraContract(schainName, tokenManagerErc202.address);
-
             await tokenManagerErc202
                 .connect(user)
                 .transferToSchainERC20(schainName, erc20OnOriginChain.address, amount);
@@ -904,7 +993,8 @@ describe("TokenManagerERC20", () => {
 
         it("should invoke `transferToSchainERC20` and transfer back without mistakes double with attached tokens", async () => {
             const amount = "20000000000000000";
-            await messageProxyForSchain.registerExtraContract(newSchainName, tokenManagerErc20.address);
+            await messageProxyForSchain.registerExtraContractForAll(tokenManagerErc20.address);
+            await messageProxyForSchain2.registerExtraContractForAll(tokenManagerErc202.address);
 
             // add connected chain:
             await messageProxyForSchain.connect(deployer).grantRole(await messageProxyForSchain.CHAIN_CONNECTOR_ROLE(), deployer.address);
@@ -975,13 +1065,6 @@ describe("TokenManagerERC20", () => {
 
             await erc20OnTargetChain.connect(user).approve(tokenManagerErc202.address, amount);
 
-            await tokenManagerErc202
-                .connect(user)
-                .transferToSchainERC20(schainName, erc20OnOriginChain.address, amount)
-                .should.be.eventually.rejectedWith("Sender contract is not registered");
-
-            await messageProxyForSchain2.registerExtraContract(schainName, tokenManagerErc202.address);
-
             await tokenManagerErc202
                 .connect(user)
                 .transferToSchainERC20(schainName, erc20OnOriginChain.address, amount);
@@ -1109,6 +1192,7 @@ describe("TokenManagerERC20", () => {
 
             await tokenManagerErc202.connect(deployer).grantRole(await tokenManagerErc202.TOKEN_REGISTRAR_ROLE(), schainOwner.address);
             await tokenManagerErc202.connect(schainOwner).addERC20TokenByOwner(schainName,  erc20OnOriginChain.address, erc20OnTargetChain.address);
+            await messageProxyForSchain2.registerExtraContractForAll(tokenManagerErc202.address);
 
             await messageProxyForSchain2.postMessage(tokenManagerErc202.address, schainId, tokenManagerErc20.address, data);
 
@@ -1134,7 +1218,6 @@ describe("TokenManagerERC20", () => {
             await messageProxyForSchain2.connect(deployer).grantRole(await messageProxyForSchain2.CHAIN_CONNECTOR_ROLE(), deployer.address);
             await messageProxyForSchain2.connect(deployer).addConnectedChain(newSchainNameZ);
 
-            await messageProxyForSchain2.registerExtraContract(newSchainNameZ, tokenManagerErc202.address);
             await tokenManagerErc202.addTokenManager(newSchainNameZ, tokenManagerErc20Z.address);
 
             await erc20OnTargetChain.connect(user).approve(tokenManagerErc202.address, amount);
@@ -1152,7 +1235,8 @@ describe("TokenManagerERC20", () => {
 
         it("should not be able to transfer main chain token or clone to mainnet", async () => {
             const amount = "20000000000000000";
-            await messageProxyForSchain.registerExtraContract(newSchainName, tokenManagerErc20.address);
+            await messageProxyForSchain.registerExtraContractForAll(tokenManagerErc20.address);
+            await messageProxyForSchain2.registerExtraContractForAll(tokenManagerErc202.address);
 
             // add connected chain:
             await messageProxyForSchain.connect(deployer).grantRole(await messageProxyForSchain.CHAIN_CONNECTOR_ROLE(), deployer.address);
@@ -1213,7 +1297,6 @@ describe("TokenManagerERC20", () => {
                 .exitToMainERC20(erc20OnTargetChain.address, amount)
                 .should.be.eventually.rejectedWith("Incorrect main chain token");
 
-            await messageProxyForSchain2.registerExtraContract(schainName, tokenManagerErc202.address);
 
             await tokenManagerErc202
                 .connect(user)
@@ -1230,8 +1313,6 @@ describe("TokenManagerERC20", () => {
 
             await erc20OnOriginChain.connect(user).approve(tokenManagerErc20.address, amount);
 
-            await messageProxyForSchain.registerExtraContract("Mainnet", tokenManagerErc20.address);
-
             await tokenManagerErc20
                 .connect(user)
                 .exitToMainERC20(erc20OnOriginChain.address, amount)
@@ -1247,13 +1328,17 @@ describe("TokenManagerERC20", () => {
     });
 
     describe("tests for `postMessage` function", async () => {
+        beforeEach(async () => {
+            await messageProxyForSchain.registerExtraContractForAll(tokenManagerErc20.address);
+        });
+
         it("should transfer ERC20 token with token info", async () => {
             //  preparation
             const amount = 10;
             const to = user.address;
             const remoteTokenManagerAddress = fakeDepositBox;
-            const fromSchainName = randomString(10);
-            const fromSchainHash = stringValue(web3.utils.soliditySha3(fromSchainName));
+            const fromSchainName = "fromSchainName";
+            const fromSchainHash = ethers.utils.solidityKeccak256(["string"], [fromSchainName]);
             await tokenManagerErc20.addTokenManager(fromSchainName, remoteTokenManagerAddress);
             // await tokenManagerErc20.connect(schainOwner).addERC20TokenByOwner(mainnetName,  erc20OnMainnet.address, erc20OnChain.address);
 
@@ -1275,7 +1360,7 @@ describe("TokenManagerERC20", () => {
             await messageProxyForSchain.postMessage(tokenManagerErc20.address, fromSchainHash, remoteTokenManagerAddress, data);
             // expectation
             const addressERC20OnSchain = await tokenManagerErc20.clonesErc20(fromSchainHash, erc20OnMainnet.address);
-            const targetErc20OnChain = await (await ethers.getContractFactory("ERC20OnChain")).attach(addressERC20OnSchain) as ERC20OnChain;
+            const targetErc20OnChain = (await ethers.getContractFactory("ERC20OnChain")).attach(addressERC20OnSchain) as ERC20OnChain;
             expect(parseInt((await targetErc20OnChain.functions.balanceOf(to)).toString(), 10))
                 .to.be.equal(amount);
         });
@@ -1283,8 +1368,8 @@ describe("TokenManagerERC20", () => {
         it("should transfer ERC20 token to schain when token add by schain owner", async () => {
             //  preparation
             const remoteTokenManagerAddress = fakeDepositBox;
-            const fromSchainName = randomString(10);
-            const fromSchainHash = stringValue(web3.utils.soliditySha3(fromSchainName));
+            const fromSchainName = "fromSchainName";
+            const fromSchainHash = ethers.utils.solidityKeccak256(["string"], [fromSchainName]);
             await messageProxyForSchain.connect(deployer).addConnectedChain(fromSchainName);
             await tokenManagerErc20.addTokenManager(fromSchainName, remoteTokenManagerAddress);
             await tokenManagerErc20.connect(schainOwner).addERC20TokenByOwner(fromSchainName,  erc20OnMainnet.address, erc20OnChain.address);
@@ -1304,16 +1389,16 @@ describe("TokenManagerERC20", () => {
             await messageProxyForSchain.postMessage(tokenManagerErc20.address, fromSchainHash, remoteTokenManagerAddress, data);
             // expectation
             const addressERC20OnSchain = await tokenManagerErc20.clonesErc20(fromSchainHash, erc20OnMainnet.address);
-            const targetErc20OnChain = await (await ethers.getContractFactory("ERC20OnChain")).attach(addressERC20OnSchain) as ERC20OnChain;
+            const targetErc20OnChain = (await ethers.getContractFactory("ERC20OnChain")).attach(addressERC20OnSchain) as ERC20OnChain;
             expect(parseInt((await targetErc20OnChain.functions.balanceOf(to)).toString(), 10))
                 .to.be.equal(amount);
         });
 
-        it("should should transfer token to schain and automaticaly deploy", async () => {
+        it("should should transfer token to schain and automatically deploy", async () => {
             //  preparation
             const remoteTokenManagerAddress = fakeDepositBox;
-            const fromSchainName = randomString(10);
-            const fromSchainHash = stringValue(web3.utils.soliditySha3(fromSchainName));
+            const fromSchainName = "fromSchainName";
+            const fromSchainHash = ethers.utils.solidityKeccak256(["string"], [fromSchainName]);
             await tokenManagerErc20.addTokenManager(fromSchainName, remoteTokenManagerAddress);
 
             const amount = 10;
@@ -1341,7 +1426,7 @@ describe("TokenManagerERC20", () => {
 
             // expectation
             const addressERC20OnSchain = await tokenManagerErc20.clonesErc20(fromSchainHash, erc20OnMainnet.address);
-            const targetErc20OnChain = await (await ethers.getContractFactory("ERC20OnChain")).attach(addressERC20OnSchain) as ERC20OnChain;
+            const targetErc20OnChain = (await ethers.getContractFactory("ERC20OnChain")).attach(addressERC20OnSchain) as ERC20OnChain;
             expect(parseInt((await targetErc20OnChain.functions.balanceOf(to)).toString(), 10))
                 .to.be.equal(amount);
         });
@@ -1361,8 +1446,8 @@ describe("TokenManagerERC20", () => {
             const amount = 10;
             const to = user.address;
             const remoteTokenManagerAddress = fakeDepositBox;
-            const fromSchainName = randomString(10);
-            const fromSchainHash = stringValue(web3.utils.soliditySha3(fromSchainName));
+            const fromSchainName = "fromSchainName";
+            const fromSchainHash = ethers.utils.solidityKeccak256(["string"], [fromSchainName]);
             await tokenManagerErc20.addTokenManager(fromSchainName, remoteTokenManagerAddress);
             await tokenManagerErc20.connect(schainOwner).addERC20TokenByOwner(mainnetName,  erc20OnMainnet.address, erc20OnChain.address);
 
@@ -1393,5 +1478,53 @@ describe("TokenManagerERC20", () => {
             await messageProxyForSchain.postMessage(tokenManagerErc20.address, mainnetId, fakeDepositBox, data)
                 .should.be.eventually.rejectedWith("Total supply exceeded");
         });
+
+        it("should top up a receiver", async () => {
+            const amount = 10;
+            const receiver = ethers.Wallet.createRandom().connect(ethers.provider);
+            const remoteTokenManager = ethers.Wallet.createRandom();
+            const sourceSchainName = "sourceSchain";
+            const sourceSchainHash = ethers.utils.solidityKeccak256(["string"], [sourceSchainName])
+            await tokenManagerErc20.addTokenManager(sourceSchainName, remoteTokenManager.address);
+            const etherbase = await (await ethers.getContractFactory("EtherbaseMock")).deploy() as EtherbaseMock;
+            await etherbase.initialize(deployer.address);
+            await etherbase.grantRole(await etherbase.ETHER_MANAGER_ROLE(), messageProxyForSchain.address);
+            await messageProxyForSchain.setEtherbase(etherbase.address);
+            await deployer.sendTransaction({to: etherbase.address, value: ethers.utils.parseEther("3")});
+
+            (await receiver.getBalance()).should.be.equal(0);
+
+            const data = await messages.encodeTransferErc20AndTokenInfoMessage(
+                erc20OnMainnet.address,
+                receiver.address,
+                amount,
+                2 * amount,
+                {
+                    name: await erc20OnMainnet.name(),
+                    symbol: await erc20OnMainnet.symbol(),
+                    decimals: await erc20OnMainnet.decimals()
+                }
+            );
+            await tokenManagerErc20.connect(schainOwner).enableAutomaticDeploy();
+            await messageProxyForSchain.postMessage(
+                tokenManagerErc20.address,
+                sourceSchainHash,
+                remoteTokenManager.address,
+                data);
+
+            (await receiver.getBalance()).should.be.equal(0);
+
+            await expect(messageProxyForSchain.setMinimumReceiverBalance(ethers.utils.parseEther("2")))
+                .to.emit(messageProxyForSchain, 'MinimumReceiverBalanceChanged')
+                .withArgs(0, ethers.utils.parseEther("2"));
+
+            await messageProxyForSchain.postMessage(
+                tokenManagerErc20.address,
+                sourceSchainHash,
+                remoteTokenManager.address,
+                data);
+
+            (await receiver.getBalance()).should.be.equal(await messageProxyForSchain.minimumReceiverBalance());
+        })
     });
 });
diff --git a/proxy/test/TokenManagerERC721.ts b/proxy/test/TokenManagerERC721.ts
index 19cc6971e..29bf4d2a9 100644
--- a/proxy/test/TokenManagerERC721.ts
+++ b/proxy/test/TokenManagerERC721.ts
@@ -34,7 +34,8 @@ import {
     CommunityLocker
 } from "../typechain";
 
-import { randomString, stringValue } from "./utils/helper";
+import { stringValue } from "./utils/helper";
+import { skipTime } from "./utils/time";
 
 chai.should();
 chai.use((chaiAsPromised as any));
@@ -120,7 +121,7 @@ describe("TokenManagerERC721", () => {
         await messageProxyForSchain.connect(deployer).grantRole(extraContractRegistrarRole, deployer.address);
 
         await communityLocker.grantRole(await communityLocker.CONSTANT_SETTER_ROLE(), deployer.address);
-        await communityLocker.setTimeLimitPerMessage(0);
+        await communityLocker.setTimeLimitPerMessage("Mainnet", 0);
     });
 
     it("should change depositBox address", async () => {
@@ -224,6 +225,7 @@ describe("TokenManagerERC721", () => {
             tokenManagerERC7212 = await deployTokenManagerERC721(newSchainName, messageProxyForSchain2.address, tokenManagerLinker2, communityLocker2, fakeDepositBox);
             await erc721OnTargetChain.connect(deployer).grantRole(await erc721OnTargetChain.MINTER_ROLE(), tokenManagerERC7212.address);
             await tokenManagerLinker2.registerTokenManager(tokenManagerERC7212.address);
+            await messageProxyForSchain2.registerExtraContractForAll(tokenManagerERC7212.address);
         });
 
         it("should invoke `transferToSchainERC721` without mistakes", async () => {
@@ -259,6 +261,106 @@ describe("TokenManagerERC721", () => {
             outgoingMessagesCounter.should.be.deep.equal(BigNumber.from(1));
         });
 
+        it("should reject `transferToSchainERC721` when executing earlier then allowed", async () => {
+            await messageProxyForSchain.registerExtraContract(newSchainName, tokenManagerERC721.address);
+
+            // add connected chain:
+            await messageProxyForSchain.connect(deployer).grantRole(await messageProxyForSchain.CHAIN_CONNECTOR_ROLE(), deployer.address);
+            await messageProxyForSchain.connect(deployer).addConnectedChain(newSchainName);
+
+            await erc721OnOriginChain.connect(deployer).mint(user.address, 1);
+            await erc721OnOriginChain.connect(user).approve(tokenManagerERC721.address, 1);
+
+            await tokenManagerERC721
+                .connect(user)
+                .transferToSchainERC721(newSchainName, erc721OnOriginChain.address, 1)
+                .should.be.eventually.rejectedWith("Incorrect Token Manager address");
+
+            await tokenManagerERC721
+                .connect(user)
+                .transferToSchainERC721("Mainnet", erc721OnOriginChain.address, 1)
+                .should.be.eventually.rejectedWith("This function is not for transferring to Mainnet");
+
+            await tokenManagerERC721.addTokenManager(newSchainName, tokenManagerERC7212.address);
+
+            await tokenManagerERC721
+                .connect(user)
+                .transferToSchainERC721(newSchainName, erc721OnOriginChain.address, 1);
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(1));
+
+            await erc721OnOriginChain.connect(deployer).mint(user.address, 2);
+            await erc721OnOriginChain.connect(user).approve(tokenManagerERC721.address, 2);
+
+            await tokenManagerERC721
+                .connect(user)
+                .transferToSchainERC721(newSchainName, erc721OnOriginChain.address, 2);
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(2));
+
+            await communityLocker.grantRole(await communityLocker.CONSTANT_SETTER_ROLE(), deployer.address);
+
+            await communityLocker.setTimeLimitPerMessage(newSchainName, 100);
+
+            await erc721OnOriginChain.connect(deployer).mint(user.address, 3);
+            await erc721OnOriginChain.connect(user).approve(tokenManagerERC721.address, 3);
+
+            await tokenManagerERC721
+                .connect(user)
+                .transferToSchainERC721(newSchainName, erc721OnOriginChain.address, 3)
+                .should.be.eventually.rejectedWith("Exceeded message rate limit");
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(2));
+
+            await skipTime(90);
+
+            await tokenManagerERC721
+                .connect(user)
+                .transferToSchainERC721(newSchainName, erc721OnOriginChain.address, 3)
+                .should.be.eventually.rejectedWith("Exceeded message rate limit");
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(2));
+
+            await skipTime(20);
+
+            await tokenManagerERC721
+                .connect(user)
+                .transferToSchainERC721(newSchainName, erc721OnOriginChain.address, 3);
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(3));
+
+            await communityLocker.setTimeLimitPerMessage(newSchainName, 0);
+
+            await erc721OnOriginChain.connect(deployer).mint(user.address, 4);
+            await erc721OnOriginChain.connect(user).approve(tokenManagerERC721.address, 4);
+
+            await tokenManagerERC721
+                .connect(user)
+                .transferToSchainERC721(newSchainName, erc721OnOriginChain.address, 4);
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(4));
+
+            await communityLocker.setTimeLimitPerMessage(newSchainName, 100);
+
+            await erc721OnOriginChain.connect(deployer).mint(user.address, 5);
+            await erc721OnOriginChain.connect(user).approve(tokenManagerERC721.address, 5);
+
+            await tokenManagerERC721
+                .connect(user)
+                .transferToSchainERC721(newSchainName, erc721OnOriginChain.address, 5)
+                .should.be.eventually.rejectedWith("Exceeded message rate limit");
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(4));
+
+            await skipTime(110);
+
+            await tokenManagerERC721
+                .connect(user)
+                .transferToSchainERC721(newSchainName, erc721OnOriginChain.address, 5);
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(5));
+        });
+
         it("should invoke `transferToSchainERC721` and receive tokens without mistakes", async () => {
             await messageProxyForSchain.registerExtraContract(newSchainName, tokenManagerERC721.address);
 
@@ -479,7 +581,7 @@ describe("TokenManagerERC721", () => {
         });
 
         it("should invoke `transferToSchainERC721` and transfer back without mistakes", async () => {
-            await messageProxyForSchain.registerExtraContract(newSchainName, tokenManagerERC721.address);
+            await messageProxyForSchain.registerExtraContractForAll(tokenManagerERC721.address);
 
             // add connected chain:
             await messageProxyForSchain.connect(deployer).grantRole(await messageProxyForSchain.CHAIN_CONNECTOR_ROLE(), deployer.address);
@@ -553,13 +655,6 @@ describe("TokenManagerERC721", () => {
 
             await targetErc721OnChain.connect(user).approve(tokenManagerERC7212.address, tokenId);
 
-            await tokenManagerERC7212
-                .connect(user)
-                .transferToSchainERC721(schainName, erc721OnOriginChain.address, tokenId)
-                .should.be.eventually.rejectedWith("Sender contract is not registered");
-
-            await messageProxyForSchain2.registerExtraContract(schainName, tokenManagerERC7212.address);
-
             await tokenManagerERC7212
                 .connect(user)
                 .transferToSchainERC721(schainName, erc721OnOriginChain.address, tokenId);
@@ -576,7 +671,7 @@ describe("TokenManagerERC721", () => {
         });
 
         it("should invoke `transferToSchainERC721` and transfer back without mistakes with attached tokens", async () => {
-            await messageProxyForSchain.registerExtraContract(newSchainName, tokenManagerERC721.address);
+            await messageProxyForSchain.registerExtraContractForAll(tokenManagerERC721.address);
 
             // add connected chain:
             await messageProxyForSchain.connect(deployer).grantRole(await messageProxyForSchain.CHAIN_CONNECTOR_ROLE(), deployer.address);
@@ -645,13 +740,6 @@ describe("TokenManagerERC721", () => {
 
             await erc721OnTargetChain.connect(user).approve(tokenManagerERC7212.address, tokenId);
 
-            await tokenManagerERC7212
-                .connect(user)
-                .transferToSchainERC721(schainName, erc721OnOriginChain.address, tokenId)
-                .should.be.eventually.rejectedWith("Sender contract is not registered");
-
-            await messageProxyForSchain2.registerExtraContract(schainName, tokenManagerERC7212.address);
-
             await tokenManagerERC7212
                 .connect(user)
                 .transferToSchainERC721(schainName, erc721OnOriginChain.address, tokenId);
@@ -669,7 +757,7 @@ describe("TokenManagerERC721", () => {
 
 
         it("should invoke `transferToSchainERC721` and transfer back without mistakes double", async () => {
-            await messageProxyForSchain.registerExtraContract(newSchainName, tokenManagerERC721.address);
+            await messageProxyForSchain.registerExtraContractForAll(tokenManagerERC721.address);
 
             // add connected chain:
             await messageProxyForSchain.connect(deployer).grantRole(await messageProxyForSchain.CHAIN_CONNECTOR_ROLE(), deployer.address);
@@ -743,13 +831,6 @@ describe("TokenManagerERC721", () => {
 
             await targetErc721OnChain.connect(user).approve(tokenManagerERC7212.address, tokenId);
 
-            await tokenManagerERC7212
-                .connect(user)
-                .transferToSchainERC721(schainName, erc721OnOriginChain.address, tokenId)
-                .should.be.eventually.rejectedWith("Sender contract is not registered");
-
-            await messageProxyForSchain2.registerExtraContract(schainName, tokenManagerERC7212.address);
-
             await tokenManagerERC7212
                 .connect(user)
                 .transferToSchainERC721(schainName, erc721OnOriginChain.address, tokenId);
@@ -834,7 +915,7 @@ describe("TokenManagerERC721", () => {
         });
 
         it("should invoke `transferToSchainERC721` and transfer back without mistakes double with attached tokens", async () => {
-            await messageProxyForSchain.registerExtraContract(newSchainName, tokenManagerERC721.address);
+            await messageProxyForSchain.registerExtraContractForAll(tokenManagerERC721.address);
 
             // add connected chain:
             await messageProxyForSchain.connect(deployer).grantRole(await messageProxyForSchain.CHAIN_CONNECTOR_ROLE(), deployer.address);
@@ -903,13 +984,6 @@ describe("TokenManagerERC721", () => {
 
             await erc721OnTargetChain.connect(user).approve(tokenManagerERC7212.address, tokenId);
 
-            await tokenManagerERC7212
-                .connect(user)
-                .transferToSchainERC721(schainName, erc721OnOriginChain.address, tokenId)
-                .should.be.eventually.rejectedWith("Sender contract is not registered");
-
-            await messageProxyForSchain2.registerExtraContract(schainName, tokenManagerERC7212.address);
-
             await tokenManagerERC7212
                 .connect(user)
                 .transferToSchainERC721(schainName, erc721OnOriginChain.address, tokenId);
@@ -1056,7 +1130,6 @@ describe("TokenManagerERC721", () => {
             await messageProxyForSchain2.connect(deployer).grantRole(await messageProxyForSchain2.CHAIN_CONNECTOR_ROLE(), deployer.address);
             await messageProxyForSchain2.connect(deployer).addConnectedChain(newSchainNameZ);
 
-            await messageProxyForSchain2.registerExtraContract(newSchainNameZ, tokenManagerERC7212.address);
             await tokenManagerERC7212.addTokenManager(newSchainNameZ, tokenManagerERC721Z.address);
 
             await erc721OnTargetChain.connect(user).approve(tokenManagerERC7212.address, tokenId);
@@ -1073,7 +1146,7 @@ describe("TokenManagerERC721", () => {
         });
 
         it("should not be able to transfer main chain token or clone to mainnet", async () => {
-            await messageProxyForSchain.registerExtraContract(newSchainName, tokenManagerERC721.address);
+            await messageProxyForSchain.registerExtraContractForAll(tokenManagerERC721.address);
 
             // add connected chain:
             await messageProxyForSchain.connect(deployer).grantRole(await messageProxyForSchain.CHAIN_CONNECTOR_ROLE(), deployer.address);
@@ -1132,8 +1205,6 @@ describe("TokenManagerERC721", () => {
                 .exitToMainERC721(erc721OnTargetChain.address, tokenId)
                 .should.be.eventually.rejectedWith("Incorrect main chain token");
 
-            await messageProxyForSchain2.registerExtraContract(schainName, tokenManagerERC7212.address);
-
             await tokenManagerERC7212
                 .connect(user)
                 .transferToSchainERC721(schainName, erc721OnOriginChain.address, tokenId);
@@ -1149,8 +1220,6 @@ describe("TokenManagerERC721", () => {
 
             await erc721OnOriginChain.connect(user).approve(tokenManagerERC721.address, tokenId);
 
-            await messageProxyForSchain.registerExtraContract("Mainnet", tokenManagerERC721.address);
-
             await tokenManagerERC721
                 .connect(user)
                 .exitToMainERC721(erc721OnOriginChain.address, tokenId)
@@ -1166,6 +1235,9 @@ describe("TokenManagerERC721", () => {
     });
 
     describe("tests for `postMessage` function", async () => {
+        beforeEach(async () => {
+            await messageProxyForSchain.registerExtraContractForAll(tokenManagerERC721.address);
+        });
 
         it("should transfer ERC721 token token with token info", async () => {
             //  preparation
diff --git a/proxy/test/TokenManagerERC721WithMetadata.ts b/proxy/test/TokenManagerERC721WithMetadata.ts
index 1aea17527..163f5689c 100644
--- a/proxy/test/TokenManagerERC721WithMetadata.ts
+++ b/proxy/test/TokenManagerERC721WithMetadata.ts
@@ -34,7 +34,8 @@ import {
     CommunityLocker
 } from "../typechain";
 
-import { randomString, stringValue } from "./utils/helper";
+import { stringValue } from "./utils/helper";
+import { skipTime } from "./utils/time";
 
 chai.should();
 chai.use((chaiAsPromised as any));
@@ -121,7 +122,7 @@ describe("TokenManagerERC721WithMetadata", () => {
         await messageProxyForSchain.connect(deployer).grantRole(extraContractRegistrarRole, deployer.address);
 
         await communityLocker.grantRole(await communityLocker.CONSTANT_SETTER_ROLE(), deployer.address);
-        await communityLocker.setTimeLimitPerMessage(0);
+        await communityLocker.setTimeLimitPerMessage("Mainnet", 0);
     });
 
     it("should change depositBox address", async () => {
@@ -229,6 +230,7 @@ describe("TokenManagerERC721WithMetadata", () => {
             tokenManagerERC721WithMetadata2 = await deployTokenManagerERC721WithMetadata(newSchainName, messageProxyForSchain2.address, tokenManagerLinker2, communityLocker2, fakeDepositBox);
             await erc721OnTargetChain.connect(deployer).grantRole(await erc721OnTargetChain.MINTER_ROLE(), tokenManagerERC721WithMetadata2.address);
             await tokenManagerLinker2.registerTokenManager(tokenManagerERC721WithMetadata2.address);
+            await messageProxyForSchain2.registerExtraContractForAll(tokenManagerERC721WithMetadata2.address);
         });
 
         it("should invoke `transferToSchainERC721` without mistakes", async () => {
@@ -265,6 +267,106 @@ describe("TokenManagerERC721WithMetadata", () => {
             outgoingMessagesCounter.should.be.deep.equal(BigNumber.from(1));
         });
 
+        it("should reject `transferToSchainERC721` when executing earlier then allowed", async () => {
+            await messageProxyForSchain.registerExtraContract(newSchainName, tokenManagerERC721WithMetadata.address);
+
+            // add connected chain:
+            await messageProxyForSchain.connect(deployer).grantRole(await messageProxyForSchain.CHAIN_CONNECTOR_ROLE(), deployer.address);
+            await messageProxyForSchain.connect(deployer).addConnectedChain(newSchainName);
+
+            await erc721OnOriginChain.connect(deployer).mint(user.address, 1);
+            await erc721OnOriginChain.connect(user).approve(tokenManagerERC721WithMetadata.address, 1);
+
+            await tokenManagerERC721WithMetadata
+                .connect(user)
+                .transferToSchainERC721(newSchainName, erc721OnOriginChain.address, 1)
+                .should.be.eventually.rejectedWith("Incorrect Token Manager address");
+
+            await tokenManagerERC721WithMetadata
+                .connect(user)
+                .transferToSchainERC721("Mainnet", erc721OnOriginChain.address, 1)
+                .should.be.eventually.rejectedWith("This function is not for transferring to Mainnet");
+
+            await tokenManagerERC721WithMetadata.addTokenManager(newSchainName, tokenManagerERC721WithMetadata2.address);
+
+            await tokenManagerERC721WithMetadata
+                .connect(user)
+                .transferToSchainERC721(newSchainName, erc721OnOriginChain.address, 1);
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(1));
+
+            await erc721OnOriginChain.connect(deployer).mint(user.address, 2);
+            await erc721OnOriginChain.connect(user).approve(tokenManagerERC721WithMetadata.address, 2);
+
+            await tokenManagerERC721WithMetadata
+                .connect(user)
+                .transferToSchainERC721(newSchainName, erc721OnOriginChain.address, 2);
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(2));
+
+            await communityLocker.grantRole(await communityLocker.CONSTANT_SETTER_ROLE(), deployer.address);
+
+            await communityLocker.setTimeLimitPerMessage(newSchainName, 100);
+
+            await erc721OnOriginChain.connect(deployer).mint(user.address, 3);
+            await erc721OnOriginChain.connect(user).approve(tokenManagerERC721WithMetadata.address, 3);
+
+            await tokenManagerERC721WithMetadata
+                .connect(user)
+                .transferToSchainERC721(newSchainName, erc721OnOriginChain.address, 3)
+                .should.be.eventually.rejectedWith("Exceeded message rate limit");
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(2));
+
+            await skipTime(90);
+
+            await tokenManagerERC721WithMetadata
+                .connect(user)
+                .transferToSchainERC721(newSchainName, erc721OnOriginChain.address, 3)
+                .should.be.eventually.rejectedWith("Exceeded message rate limit");
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(2));
+
+            await skipTime(20);
+
+            await tokenManagerERC721WithMetadata
+                .connect(user)
+                .transferToSchainERC721(newSchainName, erc721OnOriginChain.address, 3);
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(3));
+
+            await communityLocker.setTimeLimitPerMessage(newSchainName, 0);
+
+            await erc721OnOriginChain.connect(deployer).mint(user.address, 4);
+            await erc721OnOriginChain.connect(user).approve(tokenManagerERC721WithMetadata.address, 4);
+
+            await tokenManagerERC721WithMetadata
+                .connect(user)
+                .transferToSchainERC721(newSchainName, erc721OnOriginChain.address, 4);
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(4));
+
+            await communityLocker.setTimeLimitPerMessage(newSchainName, 100);
+
+            await erc721OnOriginChain.connect(deployer).mint(user.address, 5);
+            await erc721OnOriginChain.connect(user).approve(tokenManagerERC721WithMetadata.address, 5);
+
+            await tokenManagerERC721WithMetadata
+                .connect(user)
+                .transferToSchainERC721(newSchainName, erc721OnOriginChain.address, 5)
+                .should.be.eventually.rejectedWith("Exceeded message rate limit");
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(4));
+
+            await skipTime(110);
+
+            await tokenManagerERC721WithMetadata
+                .connect(user)
+                .transferToSchainERC721(newSchainName, erc721OnOriginChain.address, 5);
+
+            BigNumber.from(await messageProxyForSchain.getOutgoingMessagesCounter(newSchainName)).should.be.deep.equal(BigNumber.from(5));
+        });
+
         it("should invoke `transferToSchainERC721` and receive tokens without mistakes", async () => {
             await messageProxyForSchain.registerExtraContract(newSchainName, tokenManagerERC721WithMetadata.address);
 
@@ -504,7 +606,7 @@ describe("TokenManagerERC721WithMetadata", () => {
         });
 
         it("should invoke `transferToSchainERC721` and transfer back without mistakes", async () => {
-            await messageProxyForSchain.registerExtraContract(newSchainName, tokenManagerERC721WithMetadata.address);
+            await messageProxyForSchain.registerExtraContractForAll(tokenManagerERC721WithMetadata.address);
 
             // add connected chain:
             await messageProxyForSchain.connect(deployer).grantRole(await messageProxyForSchain.CHAIN_CONNECTOR_ROLE(), deployer.address);
@@ -581,13 +683,6 @@ describe("TokenManagerERC721WithMetadata", () => {
 
             await targetErc721OnChain.connect(user).approve(tokenManagerERC721WithMetadata2.address, tokenId);
 
-            await tokenManagerERC721WithMetadata2
-                .connect(user)
-                .transferToSchainERC721(schainName, erc721OnOriginChain.address, tokenId)
-                .should.be.eventually.rejectedWith("Sender contract is not registered");
-
-            await messageProxyForSchain2.registerExtraContract(schainName, tokenManagerERC721WithMetadata2.address);
-
             await tokenManagerERC721WithMetadata2
                 .connect(user)
                 .transferToSchainERC721(schainName, erc721OnOriginChain.address, tokenId);
@@ -606,7 +701,7 @@ describe("TokenManagerERC721WithMetadata", () => {
         });
 
         it("should invoke `transferToSchainERC721` and transfer back without mistakes with attached tokens", async () => {
-            await messageProxyForSchain.registerExtraContract(newSchainName, tokenManagerERC721WithMetadata.address);
+            await messageProxyForSchain.registerExtraContractForAll(tokenManagerERC721WithMetadata.address);
 
             // add connected chain:
             await messageProxyForSchain.connect(deployer).grantRole(await messageProxyForSchain.CHAIN_CONNECTOR_ROLE(), deployer.address);
@@ -677,13 +772,6 @@ describe("TokenManagerERC721WithMetadata", () => {
 
             await erc721OnTargetChain.connect(user).approve(tokenManagerERC721WithMetadata2.address, tokenId);
 
-            await tokenManagerERC721WithMetadata2
-                .connect(user)
-                .transferToSchainERC721(schainName, erc721OnOriginChain.address, tokenId)
-                .should.be.eventually.rejectedWith("Sender contract is not registered");
-
-            await messageProxyForSchain2.registerExtraContract(schainName, tokenManagerERC721WithMetadata2.address);
-
             await tokenManagerERC721WithMetadata2
                 .connect(user)
                 .transferToSchainERC721(schainName, erc721OnOriginChain.address, tokenId);
@@ -703,7 +791,7 @@ describe("TokenManagerERC721WithMetadata", () => {
 
 
         it("should invoke `transferToSchainERC721` and transfer back without mistakes double", async () => {
-            await messageProxyForSchain.registerExtraContract(newSchainName, tokenManagerERC721WithMetadata.address);
+            await messageProxyForSchain.registerExtraContractForAll(tokenManagerERC721WithMetadata.address);
 
             // add connected chain:
             await messageProxyForSchain.connect(deployer).grantRole(await messageProxyForSchain.CHAIN_CONNECTOR_ROLE(), deployer.address);
@@ -780,13 +868,6 @@ describe("TokenManagerERC721WithMetadata", () => {
 
             await targetErc721OnChain.connect(user).approve(tokenManagerERC721WithMetadata2.address, tokenId);
 
-            await tokenManagerERC721WithMetadata2
-                .connect(user)
-                .transferToSchainERC721(schainName, erc721OnOriginChain.address, tokenId)
-                .should.be.eventually.rejectedWith("Sender contract is not registered");
-
-            await messageProxyForSchain2.registerExtraContract(schainName, tokenManagerERC721WithMetadata2.address);
-
             await tokenManagerERC721WithMetadata2
                 .connect(user)
                 .transferToSchainERC721(schainName, erc721OnOriginChain.address, tokenId);
@@ -885,7 +966,7 @@ describe("TokenManagerERC721WithMetadata", () => {
         });
 
         it("should invoke `transferToSchainERC721` and transfer back without mistakes double with attached tokens", async () => {
-            await messageProxyForSchain.registerExtraContract(newSchainName, tokenManagerERC721WithMetadata.address);
+            await messageProxyForSchain.registerExtraContractForAll(tokenManagerERC721WithMetadata.address);
 
             // add connected chain:
             await messageProxyForSchain.connect(deployer).grantRole(await messageProxyForSchain.CHAIN_CONNECTOR_ROLE(), deployer.address);
@@ -956,13 +1037,6 @@ describe("TokenManagerERC721WithMetadata", () => {
 
             await erc721OnTargetChain.connect(user).approve(tokenManagerERC721WithMetadata2.address, tokenId);
 
-            await tokenManagerERC721WithMetadata2
-                .connect(user)
-                .transferToSchainERC721(schainName, erc721OnOriginChain.address, tokenId)
-                .should.be.eventually.rejectedWith("Sender contract is not registered");
-
-            await messageProxyForSchain2.registerExtraContract(schainName, tokenManagerERC721WithMetadata2.address);
-
             await tokenManagerERC721WithMetadata2
                 .connect(user)
                 .transferToSchainERC721(schainName, erc721OnOriginChain.address, tokenId);
@@ -1126,7 +1200,6 @@ describe("TokenManagerERC721WithMetadata", () => {
             await messageProxyForSchain2.connect(deployer).grantRole(await messageProxyForSchain2.CHAIN_CONNECTOR_ROLE(), deployer.address);
             await messageProxyForSchain2.connect(deployer).addConnectedChain(newSchainNameZ);
 
-            await messageProxyForSchain2.registerExtraContract(newSchainNameZ, tokenManagerERC721WithMetadata2.address);
             await tokenManagerERC721WithMetadata2.addTokenManager(newSchainNameZ, tokenManagerERC721WithMetadataZ.address);
 
             await erc721OnTargetChain.connect(user).approve(tokenManagerERC721WithMetadata2.address, tokenId);
@@ -1143,7 +1216,7 @@ describe("TokenManagerERC721WithMetadata", () => {
         });
 
         it("should not be able to transfer main chain token or clone to mainnet", async () => {
-            await messageProxyForSchain.registerExtraContract(newSchainName, tokenManagerERC721WithMetadata.address);
+            await messageProxyForSchain.registerExtraContractForAll(tokenManagerERC721WithMetadata.address);
 
             // add connected chain:
             await messageProxyForSchain.connect(deployer).grantRole(await messageProxyForSchain.CHAIN_CONNECTOR_ROLE(), deployer.address);
@@ -1205,8 +1278,6 @@ describe("TokenManagerERC721WithMetadata", () => {
                 .exitToMainERC721(erc721OnTargetChain.address, tokenId)
                 .should.be.eventually.rejectedWith("Incorrect main chain token");
 
-            await messageProxyForSchain2.registerExtraContract(schainName, tokenManagerERC721WithMetadata2.address);
-
             await tokenManagerERC721WithMetadata2
                 .connect(user)
                 .transferToSchainERC721(schainName, erc721OnOriginChain.address, tokenId);
@@ -1224,8 +1295,6 @@ describe("TokenManagerERC721WithMetadata", () => {
 
             await erc721OnOriginChain.connect(user).approve(tokenManagerERC721WithMetadata.address, tokenId);
 
-            await messageProxyForSchain.registerExtraContract("Mainnet", tokenManagerERC721WithMetadata.address);
-
             await tokenManagerERC721WithMetadata
                 .connect(user)
                 .exitToMainERC721(erc721OnOriginChain.address, tokenId)
@@ -1241,6 +1310,9 @@ describe("TokenManagerERC721WithMetadata", () => {
     });
 
     describe("tests for `postMessage` function", async () => {
+        beforeEach(async () => {
+            await messageProxyForSchain.registerExtraContractForAll(tokenManagerERC721WithMetadata.address);
+        });
 
         it("should transfer ERC721 token token with token info", async () => {
             //  preparation
diff --git a/proxy/test/TokenManagerEth.ts b/proxy/test/TokenManagerEth.ts
index 635d73d0e..edf174fb2 100644
--- a/proxy/test/TokenManagerEth.ts
+++ b/proxy/test/TokenManagerEth.ts
@@ -34,7 +34,7 @@ import {
     TokenManagerLinker,
 } from "../typechain";
 import { gasMultiplier } from "./utils/command_line";
-import { randomString, stringValue } from "./utils/helper";
+import { stringValue } from "./utils/helper";
 
 chai.should();
 chai.use((chaiAsPromised as any));
@@ -66,8 +66,8 @@ describe("TokenManagerEth", () => {
     let messages: MessagesTester;
     let ethERC20: EthErc20;
     let communityLocker: CommunityLocker;
-    let fakeDepositBox: any;
-    let fakeCommunityPool: any;
+    let fakeDepositBox: string;
+    let fakeCommunityPool: string;
     const mainnetHash = stringValue(web3.utils.soliditySha3("Mainnet"));
 
     before(async () => {
@@ -217,7 +217,7 @@ describe("TokenManagerEth", () => {
         await messageProxyForSchain.postMessage(communityLocker.address, mainnetHash, fakeCommunityPool, data1);
 
         await tokenManagerEth.connect(user).exitToMain(amountTo)
-            .should.be.eventually.rejectedWith("Trying to send messages too often");
+            .should.be.eventually.rejectedWith("Exceeded message rate limit");
 
     });
 
@@ -277,16 +277,15 @@ describe("TokenManagerEth", () => {
 
         it("should transfer eth", async () => {
             //  preparation
-            const fromSchainName = randomString(10);
+            await messageProxyForSchain.registerExtraContractForAll(tokenManagerEth.address);
+            const fromSchainName = "fromSchainName";
             const fromSchainId = stringValue(web3.utils.soliditySha3(fromSchainName));
             const amount = "10";
             const sender = deployer.address;
             const to = user.address;
             // for transfer eth bytesData should be equal `0x01`. See the `.fallbackOperationTypeConvert` function
             const bytesData = await messages.encodeTransferEthMessage(to, amount);
-            // redeploy tokenManagerEth with `developer` address instead `messageProxyForSchain.address`
-            // to avoid `Not a sender` error
-            tokenManagerEth = await deployTokenManagerEth(schainName, deployer.address, tokenManagerLinker, communityLocker, fakeDepositBox, ethERC20.address);
+
             // add schain to avoid the `Receiver chain is incorrect` error
             await tokenManagerEth
                 .connect(deployer)
@@ -294,19 +293,26 @@ describe("TokenManagerEth", () => {
             await ethERC20.connect(deployer).grantRole(await ethERC20.MINTER_ROLE(), tokenManagerEth.address);
             await ethERC20.connect(deployer).grantRole(await ethERC20.BURNER_ROLE(), tokenManagerEth.address);
             // execution
-            await tokenManagerEth
-                .connect(deployer)
-                .postMessage(fromSchainId, sender, bytesData)
-                .should.be.eventually.rejectedWith("Receiver chain is incorrect");
-
-            await tokenManagerEth
-                .connect(deployer)
-                .postMessage(mainnetHash, sender, bytesData)
-                .should.be.eventually.rejectedWith("Receiver chain is incorrect");
-
-            await tokenManagerEth
-                .connect(deployer)
-                .postMessage(mainnetHash, fakeDepositBox, bytesData);
+            await messageProxyForSchain.postMessage(
+                tokenManagerEth.address,
+                fromSchainId,
+                sender,
+                bytesData
+            ).should.be.eventually.rejectedWith("Receiver chain is incorrect");
+
+            await messageProxyForSchain.postMessage(
+                tokenManagerEth.address,
+                mainnetHash,
+                sender,
+                bytesData
+            ).should.be.eventually.rejectedWith("Receiver chain is incorrect");
+
+            await messageProxyForSchain.postMessage(
+                tokenManagerEth.address,
+                mainnetHash,
+                fakeDepositBox,
+                bytesData
+            );
             // expectation
             expect(parseInt((BigNumber.from(await ethERC20.balanceOf(to))).toString(), 10))
                 .to.be.equal(parseInt(amount, 10));
diff --git a/proxy/test/TokenManagerLinker.ts b/proxy/test/TokenManagerLinker.ts
index 1b98236c8..23a05dc9a 100644
--- a/proxy/test/TokenManagerLinker.ts
+++ b/proxy/test/TokenManagerLinker.ts
@@ -52,7 +52,6 @@ import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-wit
 
 import { expect } from "chai";
 import { deployKeyStorageMock } from "./utils/deploy/test/keyStorageMock";
-import { randomString } from "./utils/helper";
 
 describe("TokenManagerLinker", () => {
     let deployer: SignerWithAddress;
@@ -76,7 +75,7 @@ describe("TokenManagerLinker", () => {
     });
 
     beforeEach(async () => {
-        newSchainName = randomString(10);
+        newSchainName = "newSchainName";
         const keyStorage = await deployKeyStorageMock();
         messageProxy = await deployMessageProxyForSchainTester(keyStorage.address, schainName);
         const fakeLinker = deployer.address;
diff --git a/proxy/test/extensions/ERC721MintingFromSchainToMainnet.ts b/proxy/test/extensions/ERC721MintingFromSchainToMainnet.ts
index f83cbbf34..ac17832a3 100644
--- a/proxy/test/extensions/ERC721MintingFromSchainToMainnet.ts
+++ b/proxy/test/extensions/ERC721MintingFromSchainToMainnet.ts
@@ -84,7 +84,7 @@ import { deployTokenManagerERC721 } from "../utils/deploy/schain/tokenManagerERC
 import { deployMessageProxyForSchain } from "../utils/deploy/schain/messageProxyForSchain";
 import { deployMessages } from "../utils/deploy/messages";
 
-import { randomString, stringValue, getPublicKey } from "../utils/helper";
+import { stringValue, getPublicKey } from "../utils/helper";
 
 import { ethers, web3 } from "hardhat";
 import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address";
@@ -313,19 +313,17 @@ describe("ERC721MintingFromSchainToMainnet", () => {
     });
 
     it("should send message", async () => {
-        await ERC721TokenOnSchain.connect(user).setTokenURI(1, "MyToken1");
+        const tokenURI = "MyToken1";
+        const mainnetHash = stringValue(web3.utils.soliditySha3("Mainnet"));
+        await ERC721TokenOnSchain.connect(user).setTokenURI(1, tokenURI);
         await ERC721TokenOnSchain.connect(user).approve(extensionSchain.address, 1);
         await messageProxyForSchain.connect(deployer).registerExtraContract("Mainnet", extensionSchain.address);
-        const res = await (await extensionSchain.connect(user).sendTokenToMainnet(user.address, 1)).wait();
-        if (!res.events) {
-            assert("No events were emitted");
-        } else {
-            const last = res.events.length - 1;
-            expect(res.events[last]?.topics[0]).to.equal(stringValue(web3.utils.soliditySha3("OutgoingMessage(bytes32,uint256,address,address,bytes)")));
-            expect(res.events[last]?.topics[1]).to.equal(stringValue(web3.utils.soliditySha3("Mainnet")));
-            expect(BigNumber.from(res.events[last]?.topics[2]).toString()).to.equal("0");
-            expect(stringValue(web3.utils.toChecksumAddress("0x" + res.events[last]?.topics[3].slice(-40)))).to.equal(extensionSchain.address);
-        }
+        const res = await extensionSchain.connect(user).sendTokenToMainnet(user.address, 1);
+
+        const encodedData = ethers.utils.defaultAbiCoder.encode(["address", "uint", "string"], [user.address, 1, tokenURI]);
+        await expect(res)
+            .to.emit(messageProxyForSchain, "OutgoingMessage")
+            .withArgs(mainnetHash, 0, extensionSchain.address, extensionMainnet.address, encodedData);
     });
 
     it("should POST message for token 1", async () => {
diff --git a/proxy/test/utils/helper.ts b/proxy/test/utils/helper.ts
index 813dc856c..d876282af 100644
--- a/proxy/test/utils/helper.ts
+++ b/proxy/test/utils/helper.ts
@@ -29,16 +29,6 @@ const secp256k1EC = new ec("secp256k1");
  * @copyright SKALE Labs 2019-Present
  */
 
-export function randomString(length: number) {
-    const random13chars = () => {
-        return Math.random().toString(16).substring(2, 15);
-    };
-    const loops = Math.ceil(length / 13);
-    return new Array(loops).fill(random13chars).reduce((stringg, func) => {
-        return stringg + func();
-    }, "").substring(0, length);
-}
-
 export function createBytes32(str: string) {
     const numberOfSymbolsInBytes32: number = 64;
     const length: number = str.length;
diff --git a/test/tools/blockchain.py b/test/tools/blockchain.py
index bb00268aa..16f7c2147 100644
--- a/test/tools/blockchain.py
+++ b/test/tools/blockchain.py
@@ -250,7 +250,7 @@ def recharge_user_wallet(self, from_key, schainName, amount_wei):
     def set_time_limit_per_message(self, from_key, time_limit):
         sender_address = self.key_to_address(from_key)
         community_locker = self._get_contract_on_schain('community_locker')
-        time_limit_abi = community_locker.encodeABI(fn_name="setTimeLimitPerMessage", args=[time_limit])
+        time_limit_abi = community_locker.encodeABI(fn_name="setTimeLimitPerMessage", args=["Mainnet", time_limit])
         signed_txn = self.web3_schain.eth.account.signTransaction(dict(
                 nonce=self.web3_schain.eth.getTransactionCount(sender_address),
                 gasPrice=self.web3_schain.eth.gasPrice,