diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..db04eeaa
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,10 @@
+version: 2
+updates:
+- package-ecosystem: npm
+ directory: "/src"
+ schedule:
+ interval: "weekly"
+ day: "saturday"
+ open-pull-requests-limit: 10
+ allow:
+ - dependency-type: "production"
diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml
new file mode 100644
index 00000000..66b7580a
--- /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/.github/workflows/comprehensive.yml b/.github/workflows/comprehensive.yml
new file mode 100644
index 00000000..04fb5259
--- /dev/null
+++ b/.github/workflows/comprehensive.yml
@@ -0,0 +1,445 @@
+
+name: Comprehensive test
+
+on:
+ push:
+ branches-ignore:
+ - 'docs-v*'
+ pull_request:
+ branches-ignore:
+ - 'docs-v*'
+
+jobs:
+ cancel-runs:
+ name: Cancel Previous Runs
+ runs-on: ubuntu-latest
+ steps:
+ - name: Cancel Previous Runs
+ uses: styfle/cancel-workflow-action@0.9.1
+ with:
+ access_token: ${{ github.token }}
+ test-comprehensive:
+
+ runs-on: ubuntu-latest
+
+ steps:
+
+ - uses: actions/checkout@v2
+ with:
+ submodules: recursive
+
+ - uses: oven-sh/setup-bun@v1
+
+ - name: INFO - github environment variable checks
+ run: |
+ echo ------------ GIT_CURRENT_BRANCH
+ export GIT_CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
+ echo $GIT_CURRENT_BRANCH
+ echo ------------ GIT_SYMBOLIC_BRANCH
+ export GIT_SYMBOLIC_BRANCH=$(git symbolic-ref --short HEAD)
+ echo $GIT_SYMBOLIC_BRANCH
+ echo ------------ GITHUB_WORKFLOW
+ echo $GITHUB_WORKFLOW
+ echo ------------ GITHUB_RUN_ID
+ echo $GITHUB_RUN_ID
+ echo ------------ GITHUB_RUN_NUMBER
+ echo $GITHUB_RUN_NUMBER
+ echo ------------ GITHUB_ACTION
+ echo $GITHUB_ACTION
+ echo ------------ GITHUB_ACTIONS
+ echo $GITHUB_ACTIONS
+ echo ------------ GITHUB_ACTOR
+ echo $GITHUB_ACTOR
+ echo ------------ GITHUB_REPOSITORY
+ echo $GITHUB_REPOSITORY
+ echo ------------ GITHUB_EVENT_NAME
+ echo $GITHUB_EVENT_NAME
+ echo ------------ GITHUB_EVENT_PATH
+ echo $GITHUB_EVENT_PATH
+ echo ------------ GITHUB_WORKSPACE
+ echo $GITHUB_WORKSPACE
+ echo ------------ GITHUB_SHA
+ echo $GITHUB_SHA
+ echo ------------ GITHUB_REF
+ echo $GITHUB_REF
+ echo ------------ GITHUB_HEAD_REF
+ echo $GITHUB_HEAD_REF
+ echo ------------ GITHUB_BASE_REF
+ echo $GITHUB_BASE_REF
+
+ - name: INFO - user information checks
+ run: |
+ echo ------------ user
+ echo $USER
+ echo ------------ home
+ echo $HOME
+ echo ------------ path
+ echo $PATH
+
+ - name: INFO - system information checks
+ run: |
+ echo ------------ pwd
+ pwd
+ echo ------------ unix name - a
+ uname -a || true
+ echo ------------ unix name - r
+ uname -r || true
+ echo ------------ lsb_release - cat
+ cat /etc/lsb-release
+ echo ------------ lsb_release - a
+ lsb_release -a || true
+ echo ------------ hostnamectl
+ hostnamectl || true
+ echo ------------ /etc/os-release
+ cat /etc/os-release || true
+ echo ------------ /proc/version
+ cat /proc/version || true
+ echo ------------ lscpu
+ lscpu || true
+
+ - name: UPDATE - system deps and install libc6, net-tools, btrfs-progs, zip, unzip, bash, procps, curl
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y libc6 net-tools btrfs-progs zip unzip build-essential
+ sudo apt-get install -y bash
+ sudo apt-get install -y procps
+ sudo apt-get install -y curl
+
+ - name: INIT - install docker
+ run: |
+ sudo apt-get remove -y docker* containerd* || true
+ curl -fsSL https://get.docker.com -o install-docker.sh
+ sudo sh install-docker.sh
+ docker --version
+ docker-compose --version
+
+ - name: INIT - reconfigure docker and restart its daemon
+ run: |
+ sudo systemctl unmask docker
+ sudo groupadd docker || true
+ sudo gpasswd -a username docker || true
+ sudo service docker restart || true
+
+ - name: INIT - uninstall old Node JS if any
+ run: |
+ sudo npm cache clean -f || true &> /dev/null
+ sudo apt-get remove -y nodejs npm node -y || true &> /dev/null
+ sudo apt-get purge -y nodejs -y || true &> /dev/null
+ sudo apt-get autoremove -y || true &> /dev/null
+ sudo rm -rf /usr/bin/node || true &> /dev/null
+ sudo rm -rf /usr/include/node || true &> /dev/null
+ sudo rm -rf /usr/lib/node_modules || true &> /dev/null
+ sudo rm -rf /usr/local/bin/npm || true &> /dev/null
+ sudo rm -rf /usr/local/share/man/man1/node* || true &> /dev/null
+ sudo rm -rf /usr/local/lib/dtrace/node.d || true &> /dev/null
+ sudo rm -rf ~/.npm || true &> /dev/null
+ sudo rm -rf ~/.node-gyp || true &> /dev/null
+ sudo rm -rf /opt/local/bin/node || true &> /dev/null
+ sudo rm -rf /opt/local/include/node || true &> /dev/null
+ sudo rm -rf /opt/local/lib/node_modules || true &> /dev/null
+ sudo rm -rf /usr/local/lib/node* || true &> /dev/null
+ sudo rm -rf /usr/local/include/node* || true &> /dev/null
+ sudo rm -rf /usr/local/bin/node* || true &> /dev/null
+
+ - name: INIT - install Node JS
+ run: |
+ curl -sL https://deb.nodesource.com/setup_18.x | sudo bash -
+ sudo apt-get install -y nodejs
+ sudo ln -s /usr/bin/node /usr/local/bin/node || true
+
+ - name: INIT - install Node utilities
+ run: |
+ sudo npm install --global npm
+ sudo npm install --global color-support
+ sudo npm install --global yarn
+ sudo npm install --global node-gyp
+
+ - name: INIT - version checks of Node JS and its utilities
+ run: |
+ which node
+ node --version
+ which npx
+ npx --version
+ which npm
+ npm --version
+ which yarn
+ yarn --version
+ which node-gyp
+ node-gyp --version
+
+ - name: INIT - version checks of Bun SH
+ run: |
+ which bun
+ bun --version
+
+ - name: INIT - Download comprehensive-test
+ working-directory: ${{env.working-directory}}
+ run: |
+ export IMA_AGENT_ROOT_DIR=$(pwd)
+ echo IMA_AGENT_ROOT_DIR = $IMA_AGENT_ROOT_DIR
+ git clone https://github.com/skalenetwork/comprehensive-test.git --recursive
+
+ - name: INIT - install Ethereum Main Net emulation
+ working-directory: ${{env.working-directory}}
+ run: |
+ cd comprehensive-test
+ pwd
+ cd cli-hh
+ ./clean.sh
+ ./init.sh
+ cd ../..
+
+ - name: INIT - startup Ethereum Main Net emulation
+ working-directory: ${{env.working-directory}}
+ run: |
+ cd comprehensive-test
+ cd cli-hh
+ ./run.sh &> ../local_mainnet_output_log.txt &
+ cd ../..
+
+ - name: INIT - Install PYTHON
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.8
+
+ - name: INIT - Install everything in IMA
+ working-directory: ${{env.working-directory}}
+ run: |
+ yarn install
+
+ - name: INIT - build IMA
+ working-directory: ${{env.working-directory}}
+ run: |
+ yarn rebuild
+
+ - name: INIT - initialize comprehensive test tokens
+ working-directory: ${{env.working-directory}}
+ run: |
+ cd comprehensive-test/test_tokens
+ yarn install
+ cd ../..
+
+ - name: INIT - initialize S-Chain configuration creator
+ working-directory: ${{env.working-directory}}
+ run: |
+ cd comprehensive-test
+ cd s_chain_gen
+ yarn install
+ cd ..
+ cd ..
+
+ - name: INIT - initialize S-Chain configuration creator
+ working-directory: ${{env.working-directory}}
+ run: |
+ cd comprehensive-test
+ cd s_chain_gen
+ yarn install
+ cd ..
+ cd ..
+
+ - name: INIT - check skaled can run
+ working-directory: ${{env.working-directory}}
+ run: |
+ cd comprehensive-test
+ export DETECTED_UBUNTU_VERSION=$(lsb_release -r | cut -f2)
+ export TRYING_SKALED_AT_PATH=./app_cache/bin_$DETECTED_UBUNTU_VERSION/skaled
+ $TRYING_SKALED_AT_PATH --colors --version
+ $TRYING_SKALED_AT_PATH --help
+ cd ..
+
+ - name: INIT - generate configuration files for S-Chain nodes
+ working-directory: ${{env.working-directory}}
+ run: |
+ cd comprehensive-test
+ cd s_chain_gen
+ ./init.sh
+ cd ../..
+
+ - name: INIT - download Skale Manager
+ working-directory: ${{env.working-directory}}
+ run: |
+ cd comprehensive-test
+ git clone https://github.com/skalenetwork/skale-manager.git --recursive
+ cd ..
+
+ - name: INIT - install Skale Manager dependencies
+ working-directory: ${{env.working-directory}}
+ run: |
+ cd comprehensive-test
+ cd skale-manager
+ yarn install
+ cd ../..
+
+ - name: INIT - install comprehensive engine dependencies
+ working-directory: ${{env.working-directory}}
+ run: |
+ cd comprehensive-test
+ cd engine
+ yarn install
+ cd ../..
+
+ - name: SELF-TEST A - use IMA to browse S-Chain via node 00-00 and test last is alive
+ working-directory: ${{env.working-directory}}
+ run: |
+ node ./src/build/main.js --colors --no-gathered --browse-s-chain --url-s-chain=http://127.0.0.1:2164 || true
+
+ - name: SELF-TEST B - use IMA to browse S-Chain via node 00-01 and test last is alive
+ working-directory: ${{env.working-directory}}
+ run: |
+ node ./src/build/main.js --colors --no-gathered --browse-s-chain --url-s-chain=http://127.0.0.2:2264 || true
+
+ - name: SELF-TEST C - use IMA to browse S-Chain via node 01-00 and test last is alive
+ working-directory: ${{env.working-directory}}
+ run: |
+ node ./src/build/main.js --colors --no-gathered --browse-s-chain --url-s-chain=http://127.0.0.3:2364 || true
+
+ - name: SELF-TEST D - use IMA to browse S-Chain via node 01-01 and test last is alive
+ working-directory: ${{env.working-directory}}
+ run: |
+ node ./src/build/main.js --colors --no-gathered --browse-s-chain --url-s-chain=http://127.0.0.4:2464 || true
+
+ - name: INIT - download SGX Wallet
+ working-directory: ${{env.working-directory}}
+ run: |
+ cd comprehensive-test
+ git clone https://github.com/skalenetwork/sgxwallet.git --recursive
+ cd sgxwallet
+ git checkout develop
+ git fetch
+ git pull
+ git branch
+ git status
+ cd ..
+ cd ..
+
+ - name: INIT - update docker image SGX Wallet in the emulation mode
+ working-directory: ${{env.working-directory}}
+ run: |
+ cd comprehensive-test
+ cd sgxwallet/run_sgx_sim
+ rm -rf ../../local_sgxwallet_output_log.txt &> /dev/null
+ echo " --------------------------- stopping sgx wallet ------------------------------------------------------------------------------------------------------ "
+ docker-compose down
+ echo " --------------------------- fixing sgx wallets docker config ----------------------------------------------------------------------------------------- "
+ mv docker-compose.yml docker-compose.yml.old-previous || true
+ echo "version: '3'" > docker-compose.yml
+ echo 'services:' >> docker-compose.yml
+ echo ' sgxwallet:' >> docker-compose.yml
+ echo ' image: skalenetwork/sgxwallet_sim:1.83.0-develop.19' >> docker-compose.yml
+ echo ' restart: unless-stopped' >> docker-compose.yml
+ echo ' ports:' >> docker-compose.yml
+ echo ' - "1026:1026"' >> docker-compose.yml
+ echo ' - "1027:1027"' >> docker-compose.yml
+ echo ' - "1028:1028"' >> docker-compose.yml
+ echo ' - "1029:1029"' >> docker-compose.yml
+ echo ' - "1030:1030"' >> docker-compose.yml
+ echo ' - "1031:1031"' >> docker-compose.yml
+ echo ' volumes:' >> docker-compose.yml
+ echo ' - ./sgx_data:/usr/src/sdk/sgx_data' >> docker-compose.yml
+ echo ' - /dev/urandom:/dev/random' >> docker-compose.yml
+ echo ' logging:' >> docker-compose.yml
+ echo ' driver: json-file' >> docker-compose.yml
+ echo ' options:' >> docker-compose.yml
+ echo ' max-size: "10m"' >> docker-compose.yml
+ echo ' max-file: "4"' >> docker-compose.yml
+ echo ' command: -s -y -V -d' >> docker-compose.yml
+ echo " --------------------------- pulling sgx wallet ------------------------------------------------------------------------------------------------------- "
+ docker-compose pull
+ cd ../..
+ cd ..
+
+ - name: INIT - start SGX Wallet
+ working-directory: ${{env.working-directory}}
+ run: |
+ cd comprehensive-test
+ cd sgxwallet/run_sgx_sim
+ docker-compose up &> ../../local_sgxwallet_output_log.txt &
+ sleep 90
+ cd ../..
+ cd ..
+
+ - name: INIT - check SGX Wallet has listening ports
+ working-directory: ${{env.working-directory}}
+ run: |
+ echo "sleeping 45 seconds..."
+ sleep 45
+ echo "checking port 1026 commonly used by SGX Wallet for HTTPS..."
+ sudo netstat -tulpn | grep 1026
+ echo "...Done"
+ echo "checking port 1027 commonly used by SGX Wallet for HTTP..."
+ sudo netstat -tulpn | grep 1027
+ echo "...Done"
+
+ - name: INIT - download transaction manager
+ working-directory: ${{env.working-directory}}
+ run: |
+ cd comprehensive-test
+ rm -rf transaction-manager || true
+ git clone https://github.com/skalenetwork/transaction-manager --recursive
+ cd ..
+
+ - name: INIT - start transaction manager and redis
+ working-directory: ${{env.working-directory}}
+ run: |
+ cd comprehensive-test
+ cd transaction-manager
+ export SGX_SERVER_URL=http://127.0.0.1:1027
+ export ENDPOINT=http://127.0.0.1:8545
+ export ETH_PRIVATE_KEY=23ABDBD3C61B5330AF61EBE8BEF582F4E5CC08E554053A718BDCE7813B9DC1FC
+ ./scripts/run-test-containers.sh
+ cd ..
+ echo "------------------------------------------------------------------------------------"
+ docker ps
+ echo "------------------------------------------------------------------------------------"
+ export TM_CONTAINER_ID=$(docker ps | grep transaction-manager | awk '{print $1;}')
+ echo "Transaction manager docker container ID:" $TM_CONTAINER_ID
+ docker logs $TM_CONTAINER_ID -f &> engine/tm.log &
+ echo "------------------------------------------------------------------------------------"
+ export REDIS_CONTAINER_ID=$(docker ps | grep redis | awk '{print $1;}')
+ echo "Redis container ID:" $REDIS_CONTAINER_ID
+ docker logs $REDIS_CONTAINER_ID -f &> engine/redis.log &
+ cd ..
+
+ - name: INIT - prepare ulimit
+ working-directory: ${{env.working-directory}}
+ run: |
+ ulimit -n 65535 > /dev/null
+ echo "ulimit is now set to" $(ulimit -n)
+
+ - name: RUN - create certificates
+ working-directory: ${{env.working-directory}}
+ run: |
+ cd comprehensive-test
+ cd engine/create_pems
+ ./create_pems.sh
+ cd ../..
+ cd ..
+
+ - name: RUN - main engine steps
+ working-directory: ${{env.working-directory}}
+ run: |
+ export IMA_AGENT_ROOT_DIR=$(pwd)
+ echo IMA_AGENT_ROOT_DIR = $IMA_AGENT_ROOT_DIR
+ cd comprehensive-test/engine
+ export ALL_SKALE_TEST_CLOUD_RUN=1
+ export SEPARATED_IMA_AGENT_MODE=1
+ node ./index.js
+ cd ../..
+
+ - name: SHUTDOWN - stop SGX Wallet
+ run: |
+ cd comprehensive-test
+ cd sgxwallet/run_sgx_sim
+ docker-compose down
+ cd ../..
+ cd ..
+
+ - name: SHUTDOWN - stop transaction manager and redis
+ run: |
+ docker stop $TM_CONTAINER_ID $REDIS_CONTAINER_ID || true
+ docker rm $TM_CONTAINER_ID $REDIS_CONTAINER_ID || true
+
+ - name: SHUTDOWN - zombie cleanup, if any
+ run: |
+ killall -9 skaled node npx python python3 || true
+ pkill -9 -f skaled || true
diff --git a/.github/workflows/container-test.yml b/.github/workflows/container-test.yml
new file mode 100644
index 00000000..9d94445b
--- /dev/null
+++ b/.github/workflows/container-test.yml
@@ -0,0 +1,493 @@
+
+name: IMA container test
+
+on:
+ push:
+ branches-ignore:
+ - 'docs-v*'
+ pull_request:
+ branches-ignore:
+ - 'docs-v*'
+
+jobs:
+ cancel-runs:
+ name: Cancel Previous Runs
+ runs-on: ubuntu-latest
+ steps:
+ - name: Cancel Previous Runs
+ uses: styfle/cancel-workflow-action@0.9.1
+ with:
+ access_token: ${{ github.token }}
+ test-comprehensive:
+
+ runs-on: self-hosted
+
+ steps:
+
+ - uses: oven-sh/setup-bun@v1
+
+ - name: INIT - force pre-clean
+ run: |
+ echo "------------------------------------------------------------------------------------"
+ sudo docker ps
+ echo "------------------------------------------------------------------------------------"
+ sudo docker stop run_sgx_sim_sgxwallet_1 || true
+ sudo docker stop tm || true
+ sudo docker stop redis || true
+ echo "------------------------------------------------------------------------------------"
+ sudo docker ps
+ echo "------------------------------------------------------------------------------------"
+ sudo rm -rf comprehensive-test || true
+ sudo rm -rf ./sgxwallet || true
+ sudo rm -rf ./transaction-manager || true
+
+ - uses: actions/checkout@v2
+ with:
+ submodules: recursive
+
+ - uses: oven-sh/setup-bun@v1
+
+ - name: INFO - github environment variable checks
+ run: |
+ echo ------------ GIT_CURRENT_BRANCH
+ export GIT_CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
+ echo $GIT_CURRENT_BRANCH
+ echo ------------ GIT_SYMBOLIC_BRANCH
+ export GIT_SYMBOLIC_BRANCH=$(git symbolic-ref --short HEAD)
+ echo $GIT_SYMBOLIC_BRANCH
+ echo ------------ GITHUB_WORKFLOW
+ echo $GITHUB_WORKFLOW
+ echo ------------ GITHUB_RUN_ID
+ echo $GITHUB_RUN_ID
+ echo ------------ GITHUB_RUN_NUMBER
+ echo $GITHUB_RUN_NUMBER
+ echo ------------ GITHUB_ACTION
+ echo $GITHUB_ACTION
+ echo ------------ GITHUB_ACTIONS
+ echo $GITHUB_ACTIONS
+ echo ------------ GITHUB_ACTOR
+ echo $GITHUB_ACTOR
+ echo ------------ GITHUB_REPOSITORY
+ echo $GITHUB_REPOSITORY
+ echo ------------ GITHUB_EVENT_NAME
+ echo $GITHUB_EVENT_NAME
+ echo ------------ GITHUB_EVENT_PATH
+ echo $GITHUB_EVENT_PATH
+ echo ------------ GITHUB_WORKSPACE
+ echo $GITHUB_WORKSPACE
+ echo ------------ GITHUB_SHA
+ echo $GITHUB_SHA
+ echo ------------ GITHUB_REF
+ echo $GITHUB_REF
+ echo ------------ GITHUB_HEAD_REF
+ echo $GITHUB_HEAD_REF
+ echo ------------ GITHUB_BASE_REF
+ echo $GITHUB_BASE_REF
+
+ - name: INFO - user information checks
+ run: |
+ echo ------------ user
+ echo $USER
+ echo ------------ home
+ echo $HOME
+ echo ------------ path
+ echo $PATH
+
+ - name: INFO - system information checks
+ run: |
+ echo ------------ pwd
+ pwd
+ echo ------------ unix name - a
+ uname -a || true
+ echo ------------ unix name - r
+ uname -r || true
+ echo ------------ lsb_release - cat
+ cat /etc/lsb-release
+ echo ------------ lsb_release - a
+ lsb_release -a || true
+ echo ------------ hostnamectl
+ hostnamectl || true
+ echo ------------ /etc/os-release
+ cat /etc/os-release || true
+ echo ------------ /proc/version
+ cat /proc/version || true
+ echo ------------ lscpu
+ lscpu || true
+
+ - name: UPDATE - system deps and install libc6, net-tools, btrfs-progs, zip, unzip, bash, procps, curl, jq
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y libc6 net-tools btrfs-progs zip unzip build-essential
+ sudo apt-get install -y bash
+ sudo apt-get install -y procps
+ sudo apt-get install -y curl
+ sudo apt-get install -y jq
+
+ - name: INIT - uninstall old Node JS if any
+ run: |
+ sudo npm cache clean -f || true &> /dev/null
+ sudo apt-get remove -y nodejs npm node -y || true &> /dev/null
+ sudo apt-get purge -y nodejs -y || true &> /dev/null
+ sudo apt-get autoremove -y || true &> /dev/null
+ sudo rm -rf /usr/bin/node || true &> /dev/null
+ sudo rm -rf /usr/include/node || true &> /dev/null
+ sudo rm -rf /usr/lib/node_modules || true &> /dev/null
+ sudo rm -rf /usr/local/bin/npm || true &> /dev/null
+ sudo rm -rf /usr/local/share/man/man1/node* || true &> /dev/null
+ sudo rm -rf /usr/local/lib/dtrace/node.d || true &> /dev/null
+ sudo rm -rf ~/.npm || true &> /dev/null
+ sudo rm -rf ~/.node-gyp || true &> /dev/null
+ sudo rm -rf /opt/local/bin/node || true &> /dev/null
+ sudo rm -rf /opt/local/include/node || true &> /dev/null
+ sudo rm -rf /opt/local/lib/node_modules || true &> /dev/null
+ sudo rm -rf /usr/local/lib/node* || true &> /dev/null
+ sudo rm -rf /usr/local/include/node* || true &> /dev/null
+ sudo rm -rf /usr/local/bin/node* || true &> /dev/null
+
+ - name: INIT - install Node JS
+ run: |
+ curl -sL https://deb.nodesource.com/setup_18.x | sudo bash -
+ sudo apt-get install -y nodejs
+ sudo ln -s /usr/bin/node /usr/local/bin/node || true
+
+ - name: INIT - install Node utilities
+ run: |
+ sudo npm install --global npm
+ sudo npm install --global color-support
+ sudo npm install --global yarn
+ sudo npm install --global node-gyp
+
+ - name: INIT - version checks of Node JS and its utilities
+ run: |
+ which node
+ node --version
+ which npx
+ npx --version
+ which npm
+ npm --version
+ which yarn
+ yarn --version
+ which node-gyp
+ node-gyp --version
+
+ - name: INIT - version checks of Bun SH
+ run: |
+ which bun
+ bun --version
+
+ - name: INIT - Download comprehensive-test
+ working-directory: ${{env.working-directory}}
+ run: |
+ sudo rm -rf ./comprehensive-test || true
+ export IMA_AGENT_ROOT_DIR=$(pwd)
+ echo IMA_AGENT_ROOT_DIR = $IMA_AGENT_ROOT_DIR
+ git clone https://github.com/skalenetwork/comprehensive-test.git --recursive
+
+ - name: INIT - install Ethereum Main Net emulation
+ working-directory: ${{env.working-directory}}
+ run: |
+ cd comprehensive-test
+ pwd
+ cd cli-hh
+ ./clean.sh
+ ./init.sh
+ cd ../..
+
+ - name: INIT - startup Ethereum Main Net emulation
+ working-directory: ${{env.working-directory}}
+ run: |
+ cd comprehensive-test
+ cd cli-hh
+ ./run.sh &> ../local_mainnet_output_log.txt &
+ cd ../..
+
+ - name: INIT - Install PYTHON
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.8
+
+ - name: INIT - Install venv for PYTHON
+ working-directory: ${{env.working-directory}}
+ run: |
+ sudo apt-get install -y python3.8-venv
+
+ - name: INIT - Install everything in IMA
+ working-directory: ${{env.working-directory}}
+ run: |
+ yarn install
+
+ - name: INIT - build IMA
+ working-directory: ${{env.working-directory}}
+ run: |
+ yarn rebuild
+
+ - name: INIT - initialize comprehensive test tokens
+ working-directory: ${{env.working-directory}}
+ run: |
+ cd comprehensive-test/test_tokens
+ yarn install
+ cd ../..
+
+ - name: INIT - initialize S-Chain configuration creator
+ working-directory: ${{env.working-directory}}
+ run: |
+ cd comprehensive-test
+ cd s_chain_gen
+ yarn install
+ cd ..
+ cd ..
+
+ - name: INIT - initialize S-Chain configuration creator
+ working-directory: ${{env.working-directory}}
+ run: |
+ cd comprehensive-test
+ cd s_chain_gen
+ yarn install
+ cd ..
+ cd ..
+
+ - name: INIT - check skaled can run
+ working-directory: ${{env.working-directory}}
+ run: |
+ cd comprehensive-test
+ export DETECTED_UBUNTU_VERSION=$(lsb_release -r | cut -f2)
+ export TRYING_SKALED_AT_PATH=./app_cache/bin_$DETECTED_UBUNTU_VERSION/skaled
+ $TRYING_SKALED_AT_PATH --colors --version
+ $TRYING_SKALED_AT_PATH --help
+ cd ..
+
+ - name: INIT - generate configuration files for S-Chain nodes
+ working-directory: ${{env.working-directory}}
+ run: |
+ cd comprehensive-test
+ cd s_chain_gen
+ ./init.sh
+ cd ../..
+
+ - name: INIT - download Skale Manager
+ working-directory: ${{env.working-directory}}
+ run: |
+ cd comprehensive-test
+ sudo rm -rf ./skale-manager || true
+ git clone https://github.com/skalenetwork/skale-manager.git --recursive
+ cd ..
+
+ - name: INIT - install Skale Manager dependencies
+ working-directory: ${{env.working-directory}}
+ run: |
+ cd comprehensive-test
+ cd skale-manager
+ yarn install
+ cd ../..
+
+ - name: INIT - install comprehensive engine dependencies
+ working-directory: ${{env.working-directory}}
+ run: |
+ cd comprehensive-test
+ cd engine
+ yarn install
+ cd ../..
+
+ - name: SELF-TEST A - use IMA to browse S-Chain via node 00-00 and test last is alive
+ working-directory: ${{env.working-directory}}
+ run: |
+ node ./src/build/main.js --colors --no-gathered --browse-s-chain --url-s-chain=http://127.0.0.1:2164 || true
+
+ - name: SELF-TEST B - use IMA to browse S-Chain via node 00-01 and test last is alive
+ working-directory: ${{env.working-directory}}
+ run: |
+ node ./src/build/main.js --colors --no-gathered --browse-s-chain --url-s-chain=http://127.0.0.2:2264 || true
+
+ - name: SELF-TEST C - use IMA to browse S-Chain via node 01-00 and test last is alive
+ working-directory: ${{env.working-directory}}
+ run: |
+ node ./src/build/main.js --colors --no-gathered --browse-s-chain --url-s-chain=http://127.0.0.3:2364 || true
+
+ - name: SELF-TEST D - use IMA to browse S-Chain via node 01-01 and test last is alive
+ working-directory: ${{env.working-directory}}
+ run: |
+ node ./src/build/main.js --colors --no-gathered --browse-s-chain --url-s-chain=http://127.0.0.4:2464 || true
+
+ - name: INIT - build IMA docker container
+ working-directory: ${{env.working-directory}}
+ run: |
+ echo ------------ download binaries
+ LIB_BLS_RELEASE_TAG="0.2.0-develop.3" bash ./scripts/download_binaries.sh
+ echo ------------ docker image
+ export RELEASE=false
+ export VERSION=13.13.13-test.13
+ echo "Docker image(will build) version is:" $VERSION
+ export NAME=ima
+ export BRANCH=$GIT_CURRENT_BRANCH
+ export REPO_NAME=skalenetwork/$NAME
+ export IMAGE_NAME=$REPO_NAME:$VERSION
+ export LATEST_IMAGE_NAME=$REPO_NAME:latest
+ echo "Docker image(will build) name for IMA Agent is:" $IMAGE_NAME
+ bash ./scripts/build_image.sh "" ""
+
+ - name: INIT - download SGX Wallet
+ working-directory: ${{env.working-directory}}
+ run: |
+ cd comprehensive-test
+ sudo docker stop run_sgx_sim_sgxwallet_1 || true
+ sudo rm -rf ./sgxwallet || true
+ git clone https://github.com/skalenetwork/sgxwallet.git --recursive
+ cd sgxwallet
+ git checkout develop
+ git fetch
+ git pull
+ git branch
+ git status
+ cd ..
+ cd ..
+
+ - name: INIT - update docker image SGX Wallet in the emulation mode
+ working-directory: ${{env.working-directory}}
+ run: |
+ cd comprehensive-test
+ cd sgxwallet/run_sgx_sim
+ rm -rf ../../local_sgxwallet_output_log.txt &> /dev/null
+ echo " --------------------------- stopping sgx wallet ------------------------------------------------------------------------------------------------------ "
+ docker-compose down
+ echo " --------------------------- fixing sgx wallets docker config ----------------------------------------------------------------------------------------- "
+ mv docker-compose.yml docker-compose.yml.old-previous || true
+ echo "version: '3'" > docker-compose.yml
+ echo 'services:' >> docker-compose.yml
+ echo ' sgxwallet:' >> docker-compose.yml
+ echo ' image: skalenetwork/sgxwallet_sim:1.9.0-develop.11' >> docker-compose.yml
+ echo ' restart: unless-stopped' >> docker-compose.yml
+ echo ' network_mode: "host"' >> docker-compose.yml
+ # echo ' ports:' >> docker-compose.yml
+ # echo ' - "1026:1026"' >> docker-compose.yml
+ # echo ' - "1027:1027"' >> docker-compose.yml
+ # echo ' - "1028:1028"' >> docker-compose.yml
+ # echo ' - "1029:1029"' >> docker-compose.yml
+ # echo ' - "1030:1030"' >> docker-compose.yml
+ # echo ' - "1031:1031"' >> docker-compose.yml
+ echo ' volumes:' >> docker-compose.yml
+ echo ' - ./sgx_data:/usr/src/sdk/sgx_data' >> docker-compose.yml
+ echo ' - /dev/urandom:/dev/random' >> docker-compose.yml
+ echo ' logging:' >> docker-compose.yml
+ echo ' driver: json-file' >> docker-compose.yml
+ echo ' options:' >> docker-compose.yml
+ echo ' max-size: "10m"' >> docker-compose.yml
+ echo ' max-file: "4"' >> docker-compose.yml
+ echo ' command: -s -y -V -d' >> docker-compose.yml
+ echo " --------------------------- pulling sgx wallet ------------------------------------------------------------------------------------------------------- "
+ docker-compose pull
+ cd ../..
+ cd ..
+
+ - name: INIT - start SGX Wallet
+ working-directory: ${{env.working-directory}}
+ run: |
+ cd comprehensive-test
+ cd sgxwallet/run_sgx_sim
+ docker-compose up &> ../../local_sgxwallet_output_log.txt &
+ sleep 90
+ cd ../..
+ cd ..
+
+ - name: INIT - check SGX Wallet has listening ports
+ working-directory: ${{env.working-directory}}
+ run: |
+ echo "sleeping 180 seconds..."
+ sleep 180
+ echo "checking port 1026 commonly used by SGX Wallet for HTTPS..."
+ sudo netstat -tulpn | grep 1026
+ echo "...Done"
+ echo "checking port 1027 commonly used by SGX Wallet for HTTP..."
+ sudo netstat -tulpn | grep 1027
+ echo "...Done"
+
+ - name: INIT - download transaction manager
+ working-directory: ${{env.working-directory}}
+ run: |
+ cd comprehensive-test
+ sudo rm -rf ./transaction-manager || true
+ git clone https://github.com/skalenetwork/transaction-manager --recursive
+ cd ..
+
+ - name: INIT - start transaction manager and redis
+ working-directory: ${{env.working-directory}}
+ run: |
+ cd comprehensive-test
+ cd transaction-manager
+ export SGX_SERVER_URL=http://127.0.0.1:1027
+ export ENDPOINT=http://127.0.0.1:8545
+ #export ETH_PRIVATE_KEY=23ABDBD3C61B5330AF61EBE8BEF582F4E5CC08E554053A718BDCE7813B9DC1FC
+ export ETH_PRIVATE_KEY=fd6d151c4afe5c1c856e5a234950252be7a90210f3aa2bad4e0db037355c1dd6
+ ./scripts/run-test-containers.sh
+ cd ..
+ echo "------------------------------------------------------------------------------------"
+ docker ps
+ echo "------------------------------------------------------------------------------------"
+ export TM_CONTAINER_ID=$(docker ps | grep transaction-manager | awk '{print $1;}')
+ echo "Transaction manager docker container ID:" $TM_CONTAINER_ID
+ docker logs $TM_CONTAINER_ID -f &> engine/tm.log &
+ echo "------------------------------------------------------------------------------------"
+ export REDIS_CONTAINER_ID=$(docker ps | grep redis | awk '{print $1;}')
+ echo "Redis container ID:" $REDIS_CONTAINER_ID
+ docker logs $REDIS_CONTAINER_ID -f &> engine/redis.log &
+ cd ..
+
+ - name: INIT - prepare ulimit
+ working-directory: ${{env.working-directory}}
+ run: |
+ ulimit -n 65535 > /dev/null
+ echo "ulimit is now set to" $(ulimit -n)
+
+ - name: RUN - create certificates
+ working-directory: ${{env.working-directory}}
+ run: |
+ cd comprehensive-test
+ cd engine/create_pems
+ sudo rm -rf ./new_certs/* || true
+ sudo rm -rf ./k.key || true
+ sudo rm -rf ./k.pem || true
+ sudo rm -rf ./k.crt || true
+ sudo rm -rf ./client.key || true
+ sudo rm -rf ./client.pem || true
+ sudo rm -rf ./client.crt || true
+ ./create_pems.sh
+ cd ../..
+ cd ..
+
+ - name: RUN - main engine steps
+ working-directory: ${{env.working-directory}}
+ run: |
+ export IMA_AGENT_ROOT_DIR=$(pwd)
+ echo IMA_AGENT_ROOT_DIR = $IMA_AGENT_ROOT_DIR
+ cd comprehensive-test/engine
+ export ALL_SKALE_TEST_CLOUD_RUN=1
+ #
+ export VERSION=13.13.13-test.13
+ echo "Docker image(will run) version is:" $VERSION
+ export NAME=ima
+ export BRANCH=$GIT_CURRENT_BRANCH
+ export REPO_NAME=skalenetwork/$NAME
+ export IMAGE_NAME=$REPO_NAME:$VERSION
+ export LATEST_IMAGE_NAME=$REPO_NAME:latest
+ echo "Docker image(will run) name for IMA Agent is:" $IMAGE_NAME
+ #
+ export SEPARATED_IMA_AGENT_MODE=1
+ node ./index.js
+ cd ../..
+
+ - name: SHUTDOWN - stop SGX Wallet
+ run: |
+ cd comprehensive-test
+ cd sgxwallet/run_sgx_sim
+ docker-compose down
+ cd ../..
+ cd ..
+
+ - name: SHUTDOWN - stop transaction manager and redis
+ run: |
+ docker stop $TM_CONTAINER_ID $REDIS_CONTAINER_ID || true
+ docker rm $TM_CONTAINER_ID $REDIS_CONTAINER_ID || true
+
+ - name: SHUTDOWN - zombie cleanup, if any
+ run: |
+ killall -9 skaled node npx python python3 || true
+ pkill -9 -f skaled || true
diff --git a/.github/workflows/issue_check.yml b/.github/workflows/issue_check.yml
new file mode 100644
index 00000000..ee5ec6b2
--- /dev/null
+++ b/.github/workflows/issue_check.yml
@@ -0,0 +1,19 @@
+name: Get linked issues
+on:
+ pull_request:
+ types: [ edited, synchronize, opened, reopened ]
+
+jobs:
+ check-linked-issues:
+ name: Check if pull request has linked issues
+ runs-on: ubuntu-latest
+ steps:
+ - name: Get issues
+ id: get-issues
+ uses: mondeja/pr-linked-issues-action@v2
+ env:
+ GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
+ - name: PR has not linked issues
+ if: join(steps.get-issues.outputs.issues) == ''
+ run:
+ exit 1
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 00000000..eaca4402
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,292 @@
+# This is a basic workflow to help you get started with Actions
+
+name: Build and test
+
+on:
+ push:
+ branches-ignore:
+ - 'docs-v*'
+ pull_request:
+ branches-ignore:
+ - 'docs-v*'
+
+# A workflow run is made up of one or more jobs that can run sequentially or in parallel
+jobs:
+ test-agent:
+ # The type of runner that the job will run on
+ runs-on: ubuntu-latest
+
+ # Steps represent a sequence of tasks that will be executed as part of the job
+ steps:
+ # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
+ - uses: actions/checkout@v2
+ with:
+ submodules: recursive
+
+ - uses: oven-sh/setup-bun@v1
+
+ - name: System Version Checks
+ run: |
+ echo ------------ GIT_CURRENT_BRANCH
+ export GIT_CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
+ echo $GIT_CURRENT_BRANCH
+ echo ------------ GIT_SYMBOLIC_BRANCH
+ export GIT_SYMBOLIC_BRANCH=$(git symbolic-ref --short HEAD)
+ echo $GIT_SYMBOLIC_BRANCH
+ echo ------------ GITHUB_WORKFLOW
+ echo $GITHUB_WORKFLOW
+ echo ------------ GITHUB_RUN_ID
+ echo $GITHUB_RUN_ID
+ echo ------------ GITHUB_RUN_NUMBER
+ echo $GITHUB_RUN_NUMBER
+ echo ------------ GITHUB_ACTION
+ echo $GITHUB_ACTION
+ echo ------------ GITHUB_ACTIONS
+ echo $GITHUB_ACTIONS
+ echo ------------ GITHUB_ACTOR
+ echo $GITHUB_ACTOR
+ echo ------------ GITHUB_REPOSITORY
+ echo $GITHUB_REPOSITORY
+ echo ------------ GITHUB_EVENT_NAME
+ echo $GITHUB_EVENT_NAME
+ echo ------------ GITHUB_EVENT_PATH
+ echo $GITHUB_EVENT_PATH
+ echo ------------ GITHUB_WORKSPACE
+ echo $GITHUB_WORKSPACE
+ echo ------------ GITHUB_SHA
+ echo $GITHUB_SHA
+ echo ------------ GITHUB_REF
+ echo $GITHUB_REF
+ echo ------------ GITHUB_HEAD_REF
+ echo $GITHUB_HEAD_REF
+ echo ------------ GITHUB_BASE_REF
+ echo $GITHUB_BASE_REF
+ echo ------------ user
+ echo $USER
+ echo ------------ home
+ echo $HOME
+ echo ------------ path
+ echo $PATH
+ echo ------------ pwd
+ pwd
+ echo ------------ unix name - a
+ uname -a || true
+ echo ------------ unix name - r
+ uname -r || true
+ echo ------------ lsb
+ lsb_release -a || true
+ echo ------------ hostnamectl
+ hostnamectl || true
+ echo ------------ /etc/os-release
+ cat /etc/os-release || true
+ echo ------------ /proc/version
+ cat /proc/version || true
+ echo ------------ lscpu
+ lscpu || true
+
+ - name: INIT - install needed utilities
+ run: |
+ sudo apt-get install -y jq sed
+
+ - name: INIT - install Node JS
+ run: |
+ curl -sL https://deb.nodesource.com/setup_18.x | sudo bash -
+ sudo apt-get install -y nodejs
+ sudo ln -s /usr/bin/node /usr/local/bin/node || true
+
+ - name: INIT - install Node utilities
+ run: |
+ sudo npm install --global npm
+ sudo npm install --global color-support
+ sudo npm install --global yarn
+ sudo npm install --global node-gyp
+
+ - name: INIT - version checks of Node JS and its utilities
+ run: |
+ which node
+ node --version
+ which npx
+ npx --version
+ which npm
+ npm --version
+ which yarn
+ yarn --version
+ which node-gyp
+ node-gyp --version
+
+ - name: INIT - version checks of Bun SH
+ run: |
+ which bun
+ bun --version
+
+ - name: Install ESLINT
+ run: |
+ sudo npm install -g eslint
+ eslint --version
+
+ - name: Install all NPMs at root
+ run: |
+ yarn install
+ ls -1
+
+ - name: JS Lint Check src
+ run: |
+ yarn run lint-check
+ echo "Looks like no JS code formatting errors so far)"
+
+ - name: JS Lint Check network-browser
+ run: |
+ yarn run lint-nb
+
+ - name: Python Version Checks
+ run: |
+ echo ------------ python version check
+ which python || echo "----> Looks like python was not installed, next command will fail"
+ python --version
+ echo ------------ python3 version check
+ which python3 || echo "----> Looks like python3 was not installed, next command will fail"
+ python3 --version
+
+ - name: Install Python Prerequisites
+ run: |
+ echo ------------ py3 installs
+ sudo apt-get install -y python3-pip python3-setuptools python3-dev
+ echo ------------ py3 wheel - apt
+ sudo apt-get install -y python3-wheel
+ echo ------------ py3 wheel - pip
+ pip3 install wheel
+ # echo ------------ slither analyzer install
+ # #pip3 install slither-analyzer==0.8.3
+ # pip3 install -r IMA/proxy/scripts/requirements.txt
+ # echo ------------ slither search attempt
+ # sudo find / -name slither || true
+ # echo ------------ slither location detection - after install
+ # export PATH=$PATH:/home/$USER/.local/bin
+ # which slither || echo "----> Looks like slither was not installed, next command will fail"
+ # echo ------------ slither version check - after install
+ # slither --version || true
+
+ test-integration:
+ runs-on: ubuntu-latest
+
+ env:
+ working-directory: ./test
+
+ steps:
+ - uses: actions/checkout@v2
+ with:
+ submodules: recursive
+
+ - uses: oven-sh/setup-bun@v1
+
+ - name: Get yarn cache directory path
+ id: yarn-cache-dir-path
+ run: echo "::set-output name=dir::$(yarn cache dir)"
+
+ - uses: actions/cache@v2
+ id: yarn-cache
+ with:
+ path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
+ key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-yarn-
+
+ - uses: actions/cache@v2
+ with:
+ path: ~/.cache/pip
+ key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+ restore-keys: |
+ ${{ runner.os }}-pip-
+
+ - name: INIT - install Node JS
+ run: |
+ curl -sL https://deb.nodesource.com/setup_18.x | sudo bash -
+ sudo apt-get install -y nodejs
+ sudo ln -s /usr/bin/node /usr/local/bin/node || true
+
+ - name: INIT - install Node utilities
+ run: |
+ sudo npm install --global npm
+ sudo npm install --global color-support
+ sudo npm install --global yarn
+ sudo npm install --global node-gyp
+
+ - name: INIT - version checks of Node JS and its utilities
+ run: |
+ which node
+ node --version
+ which npx
+ npx --version
+ which npm
+ npm --version
+ which yarn
+ yarn --version
+ which node-gyp
+ node-gyp --version
+
+ - name: INIT - version checks of Bun SH
+ run: |
+ which bun
+ bun --version
+
+ - name: Install Python 3.8
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.8
+
+ - name: Install all NPMs in src
+ working-directory: ./src
+ run: |
+ yarn install
+
+ - name: INIT - build IMA
+ working-directory: ./src
+ run: |
+ yarn rebuild
+
+ - name: Compile contracts
+ working-directory: ./IMA/proxy
+ run: |
+ yarn install
+
+ - name: Install all NPMs in test
+ working-directory: ./test
+ run: |
+ yarn install
+
+ - name: Start background ganache
+ working-directory: ./IMA/proxy
+ run: |
+ npx ganache --miner.blockGasLimit 12000000 --logging.quiet --chain.allowUnlimitedContractSize --wallet.defaultBalance 2000000 --wallet.accountKeysPath ../../test/accounts.json &
+ sleep 3
+ echo "Content of \"accounts.json\" is:"
+ cat ../../test/accounts.json | jq
+
+ - name: Prepare test (PY part)
+ working-directory: ./test
+ run: |
+ python3 ../scripts/config_from_accounts.py accounts.json config.json
+ echo "Content of \"config.json\" is:"
+ cat config.json | jq
+ export PRIVATE_KEY_FOR_ETHEREUM=$( cat config.json | jq -M .PRIVATE_KEY_FOR_ETHEREUM | tr -d '"' | sed -e "s/^0x//" )
+ echo "Value of \"PRIVATE_KEY_FOR_ETHEREUM\" is" $PRIVATE_KEY_FOR_ETHEREUM
+ export PRIVATE_KEY_FOR_SCHAIN=$( cat config.json | jq -M .PRIVATE_KEY_FOR_SCHAIN | tr -d '"' | sed -e "s/^0x//" )
+ echo "Value of \"PRIVATE_KEY_FOR_SCHAIN\" is" $PRIVATE_KEY_FOR_SCHAIN
+ pip3 install -r requirements.txt
+
+ - name: Run test (PY part)
+ working-directory: ./test
+ run: |
+ export URL_W3_ETHEREUM="http://127.0.0.1:8545"
+ export URL_W3_S_CHAIN="http://127.0.0.1:8545"
+ python3 test.py
+
+ - name: Prepare test (JS part)
+ working-directory: ./test
+ run: |
+ yarn install
+
+ - name: Run test (JS part)
+ working-directory: ./test
+ run: |
+ yarn test
diff --git a/.github/workflows/network-browser-test.yml b/.github/workflows/network-browser-test.yml
new file mode 100644
index 00000000..96da99a1
--- /dev/null
+++ b/.github/workflows/network-browser-test.yml
@@ -0,0 +1,28 @@
+name: network-browser test
+on: [push, pull_request]
+env:
+ ETH_PRIVATE_KEY: ${{ secrets.ETH_PRIVATE_KEY }}
+ MANAGER_TAG: "1.9.3-beta.0"
+jobs:
+ test_network_browser:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ with:
+ submodules: true
+ - uses: oven-sh/setup-bun@v1
+ - name: Launch hardhat node
+ working-directory: hardhat-node
+ run: docker-compose up -d
+ - name: Deploy manager contracts
+ run: |
+ bash ./helper-scripts/deploy_test_manager.sh
+ docker rmi -f skalenetwork/skale-manager:${{ env.MANAGER_TAG }}
+
+ - name: Install network-browser dependencies
+ working-directory: network-browser
+ run: bun i
+
+ - name: Run network-browser tests
+ working-directory: network-browser
+ run: bash run_tests.sh
\ No newline at end of file
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 00000000..0b906e79
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,97 @@
+name: Build and publish IMA Agent
+
+on:
+ pull_request:
+ types: [closed]
+ branches:
+ - 'v*.*.*'
+ - develop
+ - beta
+ - stable
+ push:
+ tags:
+ - 'custom-release-*'
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ if: github.event.pull_request.merged == true
+ env:
+ DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
+ DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
+ steps:
+ - uses: actions/checkout@v2
+ with:
+ submodules: true
+
+ - uses: oven-sh/setup-bun@v1
+
+ - name: Get yarn cache directory path
+ id: yarn-cache-dir-path
+ run: echo "::set-output name=dir::$(yarn cache dir)"
+
+ - uses: actions/cache@v2
+ id: yarn-cache
+ with:
+ path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
+ key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-yarn-
+
+ - name: INIT - install Node JS
+ run: |
+ curl -sL https://deb.nodesource.com/setup_18.x | sudo bash -
+ sudo apt-get install -y nodejs
+ sudo ln -s /usr/bin/node /usr/local/bin/node || true
+
+ - name: INIT - install Node utilities
+ run: |
+ sudo npm install --global npm
+ sudo npm install --global color-support
+ sudo npm install --global yarn
+ sudo npm install --global node-gyp
+
+ - name: INIT - version checks of Node JS and its utilities
+ run: |
+ which node
+ node --version
+ which npx
+ npx --version
+ which npm
+ npm --version
+ which yarn
+ yarn --version
+ which node-gyp
+ node-gyp --version
+
+ - name: Set up Python 3.8
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.8
+
+ - name: Build and publish container
+ run: |
+ export BRANCH=${GITHUB_REF##*/}
+ echo "Branch $BRANCH"
+ export VERSION=$(bash ./scripts/calculate_version.sh)
+ echo "VERSION=$VERSION" >> $GITHUB_ENV
+ echo "Version $VERSION"
+ ( test $BRANCH = "stable" && export PRERELEASE=false ) || export PRERELEASE=true
+ echo "PRERELEASE=$PRERELEASE" >> $GITHUB_ENV
+ echo ------------ download binaries
+ LIB_BLS_RELEASE_TAG=${{ secrets.LIB_BLS_RELEASE_TAG }} bash ./scripts/download_binaries.sh
+ export RELEASE=true
+ echo "RELEASE=$RELEASE" >> $GITHUB_ENV
+ echo ------------ docker image
+ bash ./scripts/build_image.sh ${{ secrets.DOCKER_USERNAME }} ${{ secrets.DOCKER_PASSWORD }} #|| echo "----> Looks like deploy failed"
+
+ - name: Create Release
+ id: create_release
+ uses: actions/create-release@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ tag_name: ${{ env.VERSION }}
+ release_name: ${{ env.VERSION }}
+ draft: false
+ prerelease: ${{ env.PRERELEASE }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..cd376f14
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,79 @@
+# idea
+.DS_Store
+.idea
+
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+*.egg-info
+
+# nyc test coverage
+.nyc_output
+
+# Dependency directories
+node_modules
+src/node_modules
+src/test/node_modules
+test/node_modules
+test-tokens/node_modules
+
+# TypeScript v1 declaration files
+typings/
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+.env.test
+
+wallet*.json
+*_ktm_info.tar.gz
+
+__pycache__/
+**/*.bak
+
+ima_data.json
+
+test/working
+test/config.json
+test/accounts.json
+
+.vscode
+
+actions-runner/
+
+bls_glue
+hash_g1
+verify_bls
+
+venv
+
+# network-browser
+
+network-browser/test*.json
+
+src/build
+network-browser/build/*
+network-browser.js
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 00000000..99e85c7e
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,9 @@
+[submodule "IMA"]
+ path = IMA
+ url = https://github.com/skalenetwork/IMA.git
+[submodule "hardhat-node"]
+ path = hardhat-node
+ url = https://github.com/skalenetwork/hardhat-node.git
+[submodule "helper-scripts"]
+ path = helper-scripts
+ url = https://github.com/skalenetwork/helper-scripts.git
diff --git a/CODEOWNERS b/CODEOWNERS
new file mode 100644
index 00000000..f2953899
--- /dev/null
+++ b/CODEOWNERS
@@ -0,0 +1,2 @@
+* @sergiy-skalelabs @kladkogex @DmytroNazarenko
+*.md @skalenetwork/docowners
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000..1ece5042
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,72 @@
+FROM ubuntu:jammy
+
+RUN apt-get update
+RUN apt-get install --no-install-recommends -yq software-properties-common
+RUN apt-get update
+RUN apt-get install --no-install-recommends -y build-essential zlib1g-dev libncurses5-dev libgdbm-dev libnss3-dev libssl-dev libreadline-dev libffi-dev wget curl sudo git
+
+# NOTICE: we need to install SSL 1.1 manually here in order to make BLS command line tools working
+RUN echo "deb http://security.ubuntu.com/ubuntu focal-security main" | tee /etc/apt/sources.list.d/focal-security.list
+RUN apt-get update
+RUN apt-get install --no-install-recommends -y unzip curl wget
+# NOTICE: to remove extra dep above: sudo rm /etc/apt/sources.list.d/focal-security.list
+
+RUN wget https://www.openssl.org/source/old/1.1.0/openssl-1.1.0l.tar.gz
+RUN tar xfz openssl-1.1.0l.tar.gz
+RUN cd openssl-1.1.0l && ./config && make && make install && cd ..
+RUN ldconfig
+
+RUN curl -fsSL https://bun.sh/install | BUN_INSTALL=/usr bash -s "bun-v1.0.16"
+RUN bun --version
+
+RUN curl -sL https://deb.nodesource.com/setup_18.x | bash
+RUN apt-get install --no-install-recommends -y nodejs
+RUN npm install npm --global
+RUN npm install --global yarn
+RUN npm --version
+RUN yarn --version
+
+RUN curl -O https://www.python.org/ftp/python/3.7.3/Python-3.7.3.tar.xz
+RUN tar -xf Python-3.7.3.tar.xz
+RUN cd Python-3.7.3; ./configure --enable-optimizations; make -j 4 build_all; make altinstall; cd ..
+RUN python3.7 --version
+RUN which python3.7
+RUN rm -f /usr/bin/python3
+RUN ln -s /usr/local/bin/python3.7 /usr/bin/python3
+RUN python3 --version
+RUN which python3
+
+RUN mkdir /ima
+WORKDIR /ima
+
+COPY package.json package.json
+
+COPY runner runner
+COPY src src
+RUN mkdir IMA
+COPY IMA/proxy IMA/proxy
+COPY IMA/package.json IMA/package.json
+COPY IMA/postinstall.sh IMA/postinstall.sh
+COPY IMA/VERSION IMA/VERSION
+COPY package.json package.json
+COPY VERSION VERSION
+
+COPY network-browser network-browser
+RUN cd network-browser && bun install && bun build:rollup
+
+RUN mkdir /ima/bls_binaries
+COPY scripts/bls_binaries /ima/bls_binaries
+
+RUN chmod +x /ima/bls_binaries/bls_glue
+RUN chmod +x /ima/bls_binaries/hash_g1
+RUN chmod +x /ima/bls_binaries/verify_bls
+
+RUN npm install -g node-gyp
+RUN which node-gyp
+RUN node-gyp --version
+
+WORKDIR /ima
+RUN yarn install
+RUN yarn rebuild
+
+CMD ["bash", "/ima/runner/run.sh"]
diff --git a/IMA b/IMA
new file mode 160000
index 00000000..d4c05181
--- /dev/null
+++ b/IMA
@@ -0,0 +1 @@
+Subproject commit d4c051814e89ca243933794ef1fc01d3c2118b93
diff --git a/README.md b/README.md
index cab5df7f..6ecb56b4 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,49 @@
-# ima-agent
-Containerized SKALE Interchain Messaging Agent
+
+
+# SKALE Interchain Messaging Agent (IMA)
+
+[![Discord](https://img.shields.io/discord/534485763354787851.svg)](https://discord.gg/vvUtWJB)
+
+## Components Structure
+
+### Message Transferring Agent App
+
+IMA Agent is the main application implementing connectivity and message transfer between `Mainnet` and `SKALE Chains`. The Agent also provides an easy way to perform ETH, ERC20, and ERC721 transfers between `Main Net` and `S-Chain` nevertheless, this can be done without it.
+
+#### Core
+
+A module implementing core IMA functionality.
+
+#### OWASP
+
+Data validity verifier module. See [OWASP document](https://www.gitbook.com/download/pdf/book/checkmarx/JS-SCP).
+
+#### SKALE Observer
+
+[SKALE Network Browser](src/SNB.md). Responsible for providing description of all SKALE chains.
+
+#### IMA Log
+
+Console and log file output with rotation.
+
+#### IMA CC
+
+ANSI colorizer for console and log output.
+
+## For more information
+
+- [SKALE Network Website](https://skale.network)
+- [SKALE Network Twitter](https://twitter.com/SkaleNetwork)
+- [SKALE Network Blog](https://skale.network/blog)
+
+Learn more about the SKALE community over on [Discord](https://discord.gg/vvUtWJB).
+
+## Security and Liability
+
+All contracts are WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+## License
+
+[![License](https://img.shields.io/github/license/skalenetwork/IMA)](LICENSE)
+All contributions are made under the [GNU Affero General Public License v3](https://www.gnu.org/licenses/agpl-3.0.en.html). See [LICENSE](LICENSE).
+Copyright (C) 2019-Present SKALE Labs.
diff --git a/VERSION b/VERSION
new file mode 100644
index 00000000..7ec1d6db
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+2.1.0
diff --git a/hardhat-node b/hardhat-node
new file mode 160000
index 00000000..8a4b03fd
--- /dev/null
+++ b/hardhat-node
@@ -0,0 +1 @@
+Subproject commit 8a4b03fd1051960a3e0182280bf4bfdc43129997
diff --git a/helper-scripts b/helper-scripts
new file mode 160000
index 00000000..34adbed6
--- /dev/null
+++ b/helper-scripts
@@ -0,0 +1 @@
+Subproject commit 34adbed6050a23aec351eb90501b2ac2846c0f4b
diff --git a/network-browser/.env-template b/network-browser/.env-template
new file mode 100644
index 00000000..3ee04298
--- /dev/null
+++ b/network-browser/.env-template
@@ -0,0 +1,18 @@
+# required
+
+MAINNET_RPC_URL=
+SCHAIN_RPC_URL=
+SCHAIN_NAME=
+
+SCHAIN_PROXY_PATH=
+MANAGER_ABI_PATH=
+IMA_NETWORK_BROWSER_DATA_PATH=
+
+# optional
+
+MULTICALL=false
+CONNECTED_ONLY=true
+
+POST_ERROR_DELAY=5
+NETWORK_BROWSER_DELAY=10800
+NETWORK_BROWSER_TIMEOUT=1200
\ No newline at end of file
diff --git a/network-browser/.eslintrc.cjs b/network-browser/.eslintrc.cjs
new file mode 100644
index 00000000..8fbed79b
--- /dev/null
+++ b/network-browser/.eslintrc.cjs
@@ -0,0 +1,30 @@
+module.exports = {
+ env: {
+ browser: true,
+ es2022: true,
+ node: true
+ },
+ extends: 'standard-with-typescript',
+ overrides: [
+ {
+ env: {
+ node: true
+ },
+ files: ['.eslintrc.{js,cjs}'],
+ parserOptions: {
+ sourceType: 'script'
+ }
+ }
+ ],
+ parserOptions: {
+ ecmaVersion: 'latest',
+ sourceType: 'module',
+ project: ['tsconfig.json']
+ },
+ rules: {
+ '@typescript-eslint/indent': ['error', 4],
+ '@typescript-eslint/quotes': ['error', 'single'],
+ '@typescript-eslint/space-before-function-paren': 0,
+ 'no-unused-vars': 2
+ }
+}
diff --git a/network-browser/.prettierrc b/network-browser/.prettierrc
new file mode 100644
index 00000000..4b014aad
--- /dev/null
+++ b/network-browser/.prettierrc
@@ -0,0 +1,9 @@
+{
+ "semi": false,
+ "trailingComma": "none",
+ "singleQuote": true,
+ "printWidth": 100,
+ "endOfLine": "auto",
+ "bracketSpacing": true,
+ "tabWidth": 4
+}
\ No newline at end of file
diff --git a/network-browser/README.md b/network-browser/README.md
new file mode 100644
index 00000000..98c28abd
--- /dev/null
+++ b/network-browser/README.md
@@ -0,0 +1,62 @@
+# SKALE Network Browser
+
+## Configuration
+
+Required env variables:
+
+- `MAINNET_RPC_URL` - endpoint of the Mainnet network where skale-manager contracts are deployed
+- `SCHAIN_RPC_URL` - endpoint of the current sChain
+- `SCHAIN_NAME` - name of the current sChain
+
+- `SCHAIN_PROXY_PATH` - IMA ABI from sChain
+- `MANAGER_ABI_PATH` - skale-manager ABI from Mainnet
+- `IMA_NETWORK_BROWSER_DATA_PATH` - path to JSON file where network-browser results will be saved
+
+Optional env variables:
+
+- `MULTICALL` - use ethers multicall provider (default: `false`)
+- `CONNECTED_ONLY` - collect info only for connected chains (default: `true`)
+
+- `POST_ERROR_DELAY` - delay before retry if error happened in browser loop (seconds, default: `5`)
+- `NETWORK_BROWSER_DELAY` - delay between iterations of the network-browser (seconds, default: `10800`)
+- `NETWORK_BROWSER_TIMEOUT` - maximum amount of time allocated to the browse function (seconds, default: `1200`)
+- `NETWORK_BROWSER_LOG_LEVEL` - log level (0: silly, 1: trace, 2: debug, 3: info, 4: warn, 5: error, 6: fatal, default: `1`)
+- `NETWORK_BROWSER_LOG_PRETTY` - colored logs, (boolean, default: `false`)
+
+## Development
+
+To use network-browser it's recommended to use [bun.sh](https://bun.sh/), installation script can be found on their website.
+
+Install dependencies:
+
+```bash
+bun install
+```
+
+Run in dev mode:
+
+```bash
+bun dev
+```
+
+Pre-commit hook:
+
+```bash
+bun pre
+```
+
+## Build & run in production mode
+
+For target bun:
+
+```bash
+bun build:bun
+bun network-browser.js
+```
+
+For target node:
+
+```bash
+bun build:rollup
+node network-browser.js
+```
diff --git a/network-browser/bun.lockb b/network-browser/bun.lockb
new file mode 100755
index 00000000..b9f92824
Binary files /dev/null and b/network-browser/bun.lockb differ
diff --git a/network-browser/ethers_hotfix.sh b/network-browser/ethers_hotfix.sh
new file mode 100644
index 00000000..d40facdf
--- /dev/null
+++ b/network-browser/ethers_hotfix.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+# Temporary hotfix for ethers/bun.sh issue
+# Fix be removed when https://github.com/oven-sh/bun/issues/5309 is fixed
+
+set -e
+
+OS=$(uname)
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
+FILE="$DIR/node_modules/ethers/lib.esm/utils/geturl.js"
+
+if [ -f "$FILE" ]; then
+ if [ "$OS" = "Darwin" ]; then
+ echo "Running on macOS..."
+ sed -i '' 's/body = getBytes(gunzipSync(body));/body = getBytes(gunzipSync(Buffer.from(body)));/' "$FILE"
+ else
+ echo "Running on other OS..."
+ sed -i 's/body = getBytes(gunzipSync(body));/body = getBytes(gunzipSync(Buffer.from(body)));/' "$FILE"
+ fi
+ echo "The line has been replaced in $FILE."
+else
+ echo "Error: $FILE does not exist."
+fi
diff --git a/network-browser/index.ts b/network-browser/index.ts
new file mode 100644
index 00000000..6fac8be5
--- /dev/null
+++ b/network-browser/index.ts
@@ -0,0 +1,82 @@
+/**
+ * @license
+ * SKALE network-browser
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+/**
+ * @file index.ts
+ * @copyright SKALE Labs 2023-Present
+ */
+
+import {
+ schainsInternalContract,
+ nodesContract,
+ getMainnetManagerAbi,
+ getMainnetProvider
+} from './src/contracts'
+import { delay, pingUrl, withTimeout } from './src/tools'
+import { BrowserTimeoutError } from './src/errors'
+import { browse } from './src/browser'
+import {
+ MAINNET_RPC_URL,
+ SCHAIN_NAME,
+ NETWORK_BROWSER_TIMEOUT,
+ POST_ERROR_DELAY,
+ NETWORK_BROWSER_DELAY,
+ MULTICALL,
+ CONNECTED_ONLY,
+ SCHAIN_RPC_URL,
+ LOG_LEVEL,
+ LOG_PRETTY
+} from './src/constants'
+
+import { Logger, type ILogObj } from 'tslog'
+
+const log = new Logger({ minLevel: LOG_LEVEL, stylePrettyLogs: LOG_PRETTY })
+
+async function safeNetworkBrowserLoop() {
+ log.info(`Running network-browser...`)
+ log.info(`SCHAIN_NAME: ${SCHAIN_NAME}`)
+ log.info(`POST_ERROR_DELAY: ${POST_ERROR_DELAY}`)
+ log.info(`MULTICALL: ${MULTICALL}`)
+ log.info(`CONNECTED_ONLY: ${CONNECTED_ONLY}`)
+ log.info(`NETWORK_BROWSER_TIMEOUT: ${NETWORK_BROWSER_TIMEOUT}`)
+ log.info(`NETWORK_BROWSER_DELAY: ${NETWORK_BROWSER_DELAY}`)
+
+ const provider = await getMainnetProvider(MAINNET_RPC_URL, MULTICALL)
+ const managerAbi = getMainnetManagerAbi()
+ const schainsInternal = schainsInternalContract(managerAbi, provider)
+ const nodes = nodesContract(managerAbi, provider)
+ log.info(`Trying to connect to the sChain RPC: ${SCHAIN_RPC_URL}`)
+ await pingUrl(SCHAIN_RPC_URL)
+ while (true) {
+ try {
+ await withTimeout(browse(schainsInternal, nodes), NETWORK_BROWSER_TIMEOUT)
+ await delay(NETWORK_BROWSER_DELAY)
+ } catch (error) {
+ if (error instanceof BrowserTimeoutError) {
+ console.error(
+ `A timeout (${NETWORK_BROWSER_TIMEOUT} ms) error occurred:`,
+ error.message
+ )
+ } else {
+ console.error('An error occurred in browse:', error)
+ }
+ await delay(POST_ERROR_DELAY)
+ }
+ }
+}
+
+safeNetworkBrowserLoop()
diff --git a/network-browser/package.json b/network-browser/package.json
new file mode 100644
index 00000000..6fe9fcee
--- /dev/null
+++ b/network-browser/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "network-browser",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "bun _fix && bun --hot index.ts",
+ "browse": "bun _fix && bun index.ts",
+ "build:rollup": "rollup index.ts --config rollup.config.js",
+ "build:bun": "bun build index.ts --target=bun --outfile=network-browser.js",
+ "build:node": "bun build index.ts --target=node --outfile=network-browser.js",
+ "build:exec": "bun build index.ts --compile --outfile=network-browser",
+ "lint-check": "eslint -c .eslintrc.cjs ./src/* && prettier --check \"**/*.{ts,tsx,js,mdx}\"",
+ "lint-fix": "bun _lint && bun _prettier",
+ "_prettier": "prettier --write \"**/*.{ts,tsx,js,mdx}\"",
+ "_lint": "eslint -c .eslintrc.cjs --fix ./src/*",
+ "_fix": "bash ethers_hotfix.sh"
+ },
+ "dependencies": {
+ "ethers": "6.8.1",
+ "ethers-multicall-provider": "^5.0.0",
+ "tslog": "^4.9.2"
+ },
+ "devDependencies": {
+ "@rollup/plugin-typescript": "^11.1.5",
+ "@types/elliptic": "^6.4.18",
+ "bun-types": "^1.0.18-1",
+ "elliptic": "^6.5.4",
+ "eslint": "^8.53.0",
+ "eslint-config-standard-with-typescript": "^39.1.1",
+ "prettier": "^3.1.0",
+ "rollup": "^4.7.0",
+ "typescript": "5.3.3"
+ }
+}
\ No newline at end of file
diff --git a/network-browser/rollup.config.js b/network-browser/rollup.config.js
new file mode 100644
index 00000000..626aecfc
--- /dev/null
+++ b/network-browser/rollup.config.js
@@ -0,0 +1,16 @@
+import typescript from '@rollup/plugin-typescript'
+
+export default {
+ input: 'index.ts',
+ output: {
+ dir: 'build',
+ format: 'es',
+ preserveModules: true
+ },
+ plugins: [
+ typescript({
+ include: ['src/**', 'index.ts'],
+ exclude: ['**/tests', '**/build']
+ })
+ ]
+}
diff --git a/network-browser/run_tests.sh b/network-browser/run_tests.sh
new file mode 100644
index 00000000..03c1bd01
--- /dev/null
+++ b/network-browser/run_tests.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+
+set -e
+
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
+
+export IMA_NETWORK_BROWSER_DATA_PATH="$DIR/test_schainsData.json"
+export SCHAIN_PROXY_PATH="$DIR/test_ima_schain.json"
+export MANAGER_ABI_PATH="$DIR/../helper-scripts/contracts_data/manager.json"
+
+export MAINNET_RPC_URL="http://127.0.0.1:8545"
+export SCHAIN_RPC_URL="http://127.0.0.1:8545"
+export SCHAIN_NAME="test"
+export CONNECTED_ONLY=false
+
+bun test
\ No newline at end of file
diff --git a/network-browser/src/browser.ts b/network-browser/src/browser.ts
new file mode 100644
index 00000000..0a344679
--- /dev/null
+++ b/network-browser/src/browser.ts
@@ -0,0 +1,69 @@
+/**
+ * @license
+ * SKALE network-browser
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+/**
+ * @file browser.ts
+ * @copyright SKALE Labs 2023-Present
+ */
+
+import { type Contract } from 'ethers'
+import { Logger, type ILogObj } from 'tslog'
+
+import { type SChain, type NetworkBrowserData } from './interfaces'
+import {
+ getSChainHashes,
+ getSChains,
+ filterConnectedOnly,
+ getNodeIdsInGroups,
+ filterConnectedHashes
+} from './schains'
+import { getNodesGroups } from './nodes'
+import { CONNECTED_ONLY, IMA_NETWORK_BROWSER_DATA_PATH, LOG_LEVEL, LOG_PRETTY } from './constants'
+import { writeJson, currentTimestamp, chainIdInt } from './tools'
+
+const log = new Logger({ minLevel: LOG_LEVEL, stylePrettyLogs: LOG_PRETTY })
+
+export async function browse(schainsInternal: Contract, nodes: Contract): Promise {
+ log.info('Browse iteration started, collecting chains')
+
+ const start = performance.now()
+ let schainsHashes = await getSChainHashes(schainsInternal)
+ let schains: SChain[] = await getSChains(schainsInternal, schainsHashes)
+ if (CONNECTED_ONLY) {
+ schains = await filterConnectedOnly(schains)
+ schainsHashes = filterConnectedHashes(schainsHashes, schains)
+ }
+ log.info(`Going to gather information about ${schains.length} chains`)
+
+ const nodesInGroups = await getNodeIdsInGroups(schainsInternal, schainsHashes)
+ const nodesInfo = await getNodesGroups(nodes, schainsInternal, nodesInGroups, schainsHashes)
+ schains.forEach((schain, index) => {
+ schain.chainId = chainIdInt(schain.name)
+ schain.nodes = nodesInfo[index]
+ })
+
+ const networkBrowserData: NetworkBrowserData = {
+ updatedAt: currentTimestamp(),
+ schains
+ }
+ writeJson(IMA_NETWORK_BROWSER_DATA_PATH, networkBrowserData)
+ const execTime = performance.now() - start
+ log.info(
+ `Browse execution time: ${execTime} ms (${execTime / 1000} s) for ${schains.length} chains`
+ )
+}
diff --git a/network-browser/src/constants.ts b/network-browser/src/constants.ts
new file mode 100644
index 00000000..f3843661
--- /dev/null
+++ b/network-browser/src/constants.ts
@@ -0,0 +1,58 @@
+/**
+ * @license
+ * SKALE network-browser
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+/**
+ * @file constants.ts
+ * @copyright SKALE Labs 2023-Present
+ */
+
+import { requiredEnv, booleanEnv, secondsEnv, optionalEnvNumber } from './envTools'
+
+// internal
+
+export const PORTS_PER_SCHAIN = 64
+export const DEFAULT_PING_DELAY = 10000
+export const DEFAULT_PING_ITERATIONS = 50000
+
+export const NETWORKS_WITH_MULTICALL = [
+ 1n, // mainnet
+ 3n, // ropsten
+ 4n, // rinkeby
+ 5n // goerli
+]
+
+// required
+
+export const MAINNET_RPC_URL = requiredEnv('MAINNET_RPC_URL')
+export const SCHAIN_RPC_URL = requiredEnv('SCHAIN_RPC_URL')
+export const SCHAIN_NAME = requiredEnv('SCHAIN_NAME')
+
+export const SCHAIN_PROXY_PATH = requiredEnv('SCHAIN_PROXY_PATH')
+export const MANAGER_ABI_PATH = requiredEnv('MANAGER_ABI_PATH')
+export const IMA_NETWORK_BROWSER_DATA_PATH = requiredEnv('IMA_NETWORK_BROWSER_DATA_PATH')
+
+// optional
+
+export const MULTICALL = booleanEnv('MULTICALL', true)
+export const CONNECTED_ONLY = booleanEnv('CONNECTED_ONLY', true)
+
+export const POST_ERROR_DELAY = secondsEnv(process.env.POST_ERROR_DELAY, 5)
+export const NETWORK_BROWSER_DELAY = secondsEnv(process.env.NETWORK_BROWSER_DELAY, 10800)
+export const NETWORK_BROWSER_TIMEOUT = secondsEnv(process.env.NETWORK_BROWSER_TIMEOUT, 1200)
+
+export const LOG_LEVEL = optionalEnvNumber('NETWORK_BROWSER_LOG_LEVEL', 1)
+export const LOG_PRETTY = booleanEnv('NETWORK_BROWSER_LOG_PRETTY', false)
diff --git a/network-browser/src/contracts.ts b/network-browser/src/contracts.ts
new file mode 100644
index 00000000..2992c367
--- /dev/null
+++ b/network-browser/src/contracts.ts
@@ -0,0 +1,65 @@
+/**
+ * @license
+ * SKALE network-browser
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+/**
+ * @file contracts.ts
+ * @copyright SKALE Labs 2023-Present
+ */
+
+import { JsonRpcProvider, type Provider, Contract, type Network, type ContractRunner } from 'ethers'
+import mc from 'ethers-multicall-provider'
+import { type SkaleManagerAbi, type SChainImaAbi } from './interfaces'
+import { readJson } from './tools'
+
+import { SCHAIN_PROXY_PATH, MANAGER_ABI_PATH, NETWORKS_WITH_MULTICALL } from './constants'
+
+export function getSChainImaAbi(): SChainImaAbi {
+ return readJson(SCHAIN_PROXY_PATH)
+}
+
+export function getMainnetManagerAbi(): SkaleManagerAbi {
+ return readJson(MANAGER_ABI_PATH)
+}
+
+function hasMulticall(network: Network): boolean {
+ return NETWORKS_WITH_MULTICALL.includes(network.chainId)
+}
+
+export async function getMainnetProvider(endpoint: string, multicall: boolean): Promise {
+ const nativeProvider = new JsonRpcProvider(endpoint)
+ const network = await nativeProvider.getNetwork()
+ return multicall && hasMulticall(network)
+ ? mc.MulticallWrapper.wrap(nativeProvider)
+ : nativeProvider
+}
+
+export function getSChainProvider(endpoint: string): Provider {
+ return new JsonRpcProvider(endpoint)
+}
+
+export function schainsInternalContract(abi: SkaleManagerAbi, runner: ContractRunner): Contract {
+ return new Contract(abi.schains_internal_address, abi.schains_internal_abi, runner)
+}
+
+export function messageProxyContract(abi: SChainImaAbi, runner: ContractRunner): Contract {
+ return new Contract(abi.message_proxy_chain_address, abi.message_proxy_chain_abi, runner)
+}
+
+export function nodesContract(abi: SkaleManagerAbi, runner: ContractRunner): Contract {
+ return new Contract(abi.nodes_address, abi.nodes_abi, runner)
+}
diff --git a/network-browser/src/dataclasses.ts b/network-browser/src/dataclasses.ts
new file mode 100644
index 00000000..8012c70e
--- /dev/null
+++ b/network-browser/src/dataclasses.ts
@@ -0,0 +1,35 @@
+/**
+ * @license
+ * SKALE network-browser
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+/**
+ * @file dataclasses.ts
+ * @copyright SKALE Labs 2023-Present
+ */
+
+export enum SkaledPorts {
+ PROPOSAL = 0,
+ CATCHUP = 1,
+ WS_JSON = 2,
+ HTTP_JSON = 3,
+ BINARY_CONSENSUS = 4,
+ ZMQ_BROADCAST = 5,
+ IMA_MONITORING = 6,
+ WSS_JSON = 7,
+ HTTPS_JSON = 8,
+ INFO_HTTP_JSON = 9
+}
diff --git a/network-browser/src/endpoints.ts b/network-browser/src/endpoints.ts
new file mode 100644
index 00000000..6a69ff63
--- /dev/null
+++ b/network-browser/src/endpoints.ts
@@ -0,0 +1,88 @@
+/**
+ * @license
+ * SKALE network-browser
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+/**
+ * @file endpoints.ts
+ * @copyright SKALE Labs 2023-Present
+ */
+
+import { PORTS_PER_SCHAIN } from './constants'
+import {
+ type Node,
+ type NodeEndpoints,
+ type SChainPorts,
+ type EndpointsSet,
+ type PortProtocols
+} from './interfaces'
+import { SkaledPorts } from './dataclasses'
+
+const URL_PREFIXES: EndpointsSet = {
+ http: 'http://',
+ https: 'https://',
+ ws: 'ws://',
+ wss: 'wss://',
+ infoHttp: 'http://'
+}
+
+function getSChainIndexInNode(schainHash: string, schainHashes: string[]): number {
+ const index = schainHashes.findIndex((hash) => hash === schainHash)
+ if (index === -1) {
+ throw new Error(
+ `sChain ${schainHash} is not found in the list: ${JSON.stringify(schainHashes)}`
+ )
+ }
+ return index
+}
+
+function calcSChainBasePort(basePortOfNode: bigint, sChainIndex: number): bigint {
+ return basePortOfNode + BigInt(sChainIndex * PORTS_PER_SCHAIN)
+}
+
+function calcPorts(schainBasePort: number): SChainPorts {
+ return {
+ http: schainBasePort + SkaledPorts.HTTP_JSON,
+ https: schainBasePort + SkaledPorts.HTTPS_JSON,
+ ws: schainBasePort + SkaledPorts.WS_JSON,
+ wss: schainBasePort + SkaledPorts.WSS_JSON,
+ infoHttp: schainBasePort + SkaledPorts.INFO_HTTP_JSON
+ }
+}
+
+function composeEndpoints(
+ node: Node,
+ ports: SChainPorts,
+ endpointType: 'domainName' | 'publicIP'
+): EndpointsSet {
+ const endpoints: Record = {}
+ for (const prefixName in URL_PREFIXES) {
+ const protocol = prefixName as PortProtocols
+ endpoints[prefixName] = `${URL_PREFIXES[protocol]}${node[endpointType]}:${ports[protocol]}`
+ }
+ return endpoints as EndpointsSet
+}
+
+export function calcEndpoints(node: Node, schainHash: string): NodeEndpoints {
+ if (node.schainHashes === undefined) throw new Error('schainHashes is not found in node')
+ const sChainIndex = getSChainIndexInNode(schainHash, node.schainHashes)
+ const basePort = calcSChainBasePort(node.port, sChainIndex)
+ const ports = calcPorts(Number(basePort))
+ return {
+ ports,
+ domain: composeEndpoints(node, ports, 'domainName'),
+ ip: composeEndpoints(node, ports, 'publicIP')
+ }
+}
diff --git a/network-browser/src/envTools.ts b/network-browser/src/envTools.ts
new file mode 100644
index 00000000..58ff6511
--- /dev/null
+++ b/network-browser/src/envTools.ts
@@ -0,0 +1,63 @@
+/**
+ * @license
+ * SKALE network-browser
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+/**
+ * @file envTools.ts
+ * @copyright SKALE Labs 2023-Present
+ */
+
+const MS_MULTIPLIER = 1000
+
+export function isValidNumber(str: string): boolean {
+ return !isNaN(+str) && str.trim().length > 0
+}
+
+export function secondsEnv(envValue: string | undefined, defaultSeconds: number): number {
+ return (envValue !== undefined ? Number(envValue) : defaultSeconds) * MS_MULTIPLIER
+}
+
+export function booleanEnv(envVar: string, defaultValue: boolean): boolean {
+ const value = process.env[envVar]
+ if (value === undefined) {
+ return defaultValue
+ }
+ return value.toLowerCase() === 'true'
+}
+
+export function requiredEnv(name: string): string {
+ const value = process.env[name]
+ if (value === undefined || value === '') {
+ throw new Error(`The required environment variable '${name}' is not set.`)
+ }
+ return value
+}
+
+export function optionalEnv(name: string, defaultValue: string): string {
+ const value = process.env[name]
+ if (value === undefined) {
+ return defaultValue
+ }
+ return value
+}
+
+export function optionalEnvNumber(name: string, defaultValue: number): number {
+ const value = process.env[name]
+ if (value === undefined || !isValidNumber(value)) {
+ return defaultValue
+ }
+ return Number(value)
+}
diff --git a/network-browser/src/errors.ts b/network-browser/src/errors.ts
new file mode 100644
index 00000000..83833e4e
--- /dev/null
+++ b/network-browser/src/errors.ts
@@ -0,0 +1,29 @@
+/**
+ * @license
+ * SKALE network-browser
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+/**
+ * @file errors.ts
+ * @copyright SKALE Labs 2023-Present
+ */
+
+export class BrowserTimeoutError extends Error {
+ constructor(message: string) {
+ super(message)
+ this.name = 'BrowserTimeoutError'
+ }
+}
diff --git a/network-browser/src/interfaces.ts b/network-browser/src/interfaces.ts
new file mode 100644
index 00000000..d0031ff7
--- /dev/null
+++ b/network-browser/src/interfaces.ts
@@ -0,0 +1,106 @@
+/**
+ * @license
+ * SKALE network-browser
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+/**
+ * @file interfaces.ts
+ * @copyright SKALE Labs 2023-Present
+ */
+
+import { type InterfaceAbi } from 'ethers'
+
+export type AddressType = `0x${string}`
+
+export type NodeArray = [string, string, string, bigint, bigint, bigint, bigint, number, bigint]
+
+export interface Node {
+ name: string
+ ip: string
+ publicIP: string
+ port: bigint
+ startBlock: bigint
+ lastRewardDate: bigint
+ finishTime: bigint
+ status: number
+ validatorId: bigint
+ domainName: string
+ endpoints?: NodeEndpoints
+ schainHashes?: string[]
+}
+
+export type PortProtocols = 'http' | 'https' | 'ws' | 'wss' | 'infoHttp'
+export type SChainPorts = { [protocol in PortProtocols]: number }
+export type EndpointsSet = { [protocol in PortProtocols]: string }
+
+export interface NodeEndpoints {
+ ports: { [protocol in PortProtocols]: number }
+ domain: EndpointsSet
+ ip: EndpointsSet
+}
+
+export type SChainArray = [
+ string,
+ string,
+ bigint,
+ bigint,
+ bigint,
+ bigint,
+ bigint,
+ bigint,
+ bigint,
+ bigint,
+ string
+]
+
+export interface SChain {
+ name: string
+ mainnetOwner: string
+ indexInOwnerList: bigint
+ partOfNode: bigint
+ lifetime: bigint
+ startDate: bigint
+ startBlock: bigint
+ deposit: bigint
+ index: bigint
+ generation: bigint
+ originator: string
+ nodes?: Node[]
+ chainId?: number
+}
+
+export interface SkaleManagerAbi {
+ nodes_address: string
+ nodes_abi: InterfaceAbi
+ schains_address: string
+ schains_abi: InterfaceAbi
+ schains_internal_address: string
+ schains_internal_abi: InterfaceAbi
+ validator_service_address: string
+ validator_service_abi: InterfaceAbi
+ skale_manager_address: string
+ skale_manager_abi: InterfaceAbi
+}
+
+export interface SChainImaAbi {
+ message_proxy_chain_address: string
+ message_proxy_chain_abi: InterfaceAbi
+}
+
+export interface NetworkBrowserData {
+ schains: SChain[]
+ updatedAt: number
+}
diff --git a/network-browser/src/nodes.ts b/network-browser/src/nodes.ts
new file mode 100644
index 00000000..d65f547d
--- /dev/null
+++ b/network-browser/src/nodes.ts
@@ -0,0 +1,105 @@
+/**
+ * @license
+ * SKALE network-browser
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+/**
+ * @file nodes.ts
+ * @copyright SKALE Labs 2023-Present
+ */
+
+import { type Contract } from 'ethers'
+import { type Node, type NodeArray } from './interfaces'
+import { calcEndpoints } from './endpoints'
+import { hexToIp } from './tools'
+
+export async function getNodesGroups(
+ nodes: Contract,
+ schainsInternal: Contract,
+ nodeIdsArrays: bigint[][] | number[][],
+ schainsHashes: string[]
+): Promise {
+ return await Promise.all(
+ nodeIdsArrays.map(
+ async (nodeIds, index) =>
+ await getNodes(nodes, schainsInternal, nodeIds, schainsHashes[index])
+ )
+ )
+}
+
+export async function getNodes(
+ nodes: Contract,
+ schainsInternal: Contract,
+ nodeIds: bigint[] | number[],
+ schainHash: string
+): Promise {
+ const rawNodes: Array = await getNodesRaw(nodes, schainsInternal, nodeIds)
+ const nodesData: Node[] = []
+ for (let i = 0; i < rawNodes.length; i += 3) {
+ nodesData.push(
+ nodeStruct(
+ rawNodes[i] as NodeArray,
+ rawNodes[i + 1] as string,
+ schainHash,
+ rawNodes[i + 2] as string[],
+ true
+ )
+ )
+ }
+ return nodesData
+}
+
+async function getNodesRaw(
+ nodes: Contract,
+ schainsInternal: Contract,
+ nodeIds: bigint[] | number[]
+): Promise> {
+ return await Promise.all(
+ nodeIds
+ .map((nodeId) => [
+ nodes.nodes(nodeId),
+ nodes.getNodeDomainName(nodeId),
+ schainsInternal.getSchainHashesForNode(nodeId)
+ ])
+ .flat()
+ )
+}
+
+export function nodeStruct(
+ nodeArray: NodeArray,
+ domainName: string,
+ schainHash?: string,
+ schainHashes?: string[],
+ endpoints?: boolean
+): Node {
+ const node: Node = {
+ name: nodeArray[0],
+ ip: hexToIp(nodeArray[1]),
+ publicIP: hexToIp(nodeArray[2]),
+ port: nodeArray[3],
+ startBlock: nodeArray[4],
+ lastRewardDate: nodeArray[5],
+ finishTime: nodeArray[6],
+ status: nodeArray[7],
+ validatorId: nodeArray[8],
+ domainName
+ }
+ if (schainHashes !== undefined) node.schainHashes = schainHashes
+ if (endpoints !== undefined && endpoints && schainHash !== undefined) {
+ node.endpoints = calcEndpoints(node, schainHash)
+ }
+ return node
+}
diff --git a/network-browser/src/schains.ts b/network-browser/src/schains.ts
new file mode 100644
index 00000000..326673ee
--- /dev/null
+++ b/network-browser/src/schains.ts
@@ -0,0 +1,98 @@
+/**
+ * @license
+ * SKALE network-browser
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+/**
+ * @file schains.ts
+ * @copyright SKALE Labs 2023-Present
+ */
+
+import { type Contract, id } from 'ethers'
+import { type SChain, type SChainArray } from './interfaces'
+import { getSChainImaAbi, getSChainProvider, messageProxyContract } from './contracts'
+import { SCHAIN_RPC_URL } from './constants'
+
+export async function getSChainHashes(schainsInternal: Contract): Promise {
+ return await schainsInternal.getSchains()
+}
+
+export async function getSChain(schainsInternal: Contract, schainName: string): Promise {
+ const schain: SChainArray = await schainsInternal.schains(nameToHash(schainName))
+ return schainStruct(schain)
+}
+
+export async function getAllSChains(schainsInternal: Contract): Promise {
+ return await getSChains(schainsInternal, await getSChainHashes(schainsInternal))
+}
+
+export async function getSChains(
+ schainsInternal: Contract,
+ schainsHashes: string[]
+): Promise {
+ const schainsArray: SChainArray[] = await Promise.all(
+ schainsHashes.map(async (hash) => await schainsInternal.schains(hash))
+ )
+ return schainsArray.map((schainArray) => schainStruct(schainArray))
+}
+
+export async function filterConnectedOnly(schains: SChain[]): Promise {
+ const promiseArray = schains.map(async (schain) => {
+ const conditionResult = await isChainConnected(schain.name)
+ return { schain, conditionResult }
+ })
+ const results = await Promise.all(promiseArray)
+ return results.filter((result) => result.conditionResult).map((result) => result.schain)
+}
+
+export function filterConnectedHashes(schainsHashes: string[], schains: SChain[]): string[] {
+ return schainsHashes.filter((hash) => schains.some((schain) => id(schain.name) === hash))
+}
+
+export async function getNodeIdsInGroups(
+ schainsInternal: Contract,
+ schainsHashes: string[]
+): Promise {
+ return await Promise.all(
+ schainsHashes.map(async (hash) => await schainsInternal.getNodesInGroup(hash))
+ )
+}
+
+async function isChainConnected(schainName: string): Promise {
+ const provider = getSChainProvider(SCHAIN_RPC_URL)
+ const messageProxy = messageProxyContract(getSChainImaAbi(), provider)
+ return await messageProxy.isConnectedChain(schainName)
+}
+
+function nameToHash(schainName: string): string {
+ return id(schainName)
+}
+
+function schainStruct(schainArray: SChainArray): SChain {
+ return {
+ name: schainArray[0],
+ mainnetOwner: schainArray[1],
+ indexInOwnerList: schainArray[2],
+ partOfNode: schainArray[3],
+ lifetime: schainArray[4],
+ startDate: schainArray[5],
+ startBlock: schainArray[6],
+ deposit: schainArray[7],
+ index: schainArray[8],
+ generation: schainArray[9],
+ originator: schainArray[10]
+ }
+}
diff --git a/network-browser/src/tools.ts b/network-browser/src/tools.ts
new file mode 100644
index 00000000..d06fcea5
--- /dev/null
+++ b/network-browser/src/tools.ts
@@ -0,0 +1,119 @@
+/**
+ * @license
+ * SKALE network-browser
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+/**
+ * @file tools.ts
+ * @copyright SKALE Labs 2023-Present
+ */
+
+import { id, toBeHex } from 'ethers'
+import { Logger, type ILogObj } from 'tslog'
+
+import { readFileSync, writeFileSync, renameSync } from 'fs'
+import { BrowserTimeoutError } from './errors'
+import { DEFAULT_PING_DELAY, DEFAULT_PING_ITERATIONS, LOG_LEVEL, LOG_PRETTY } from './constants'
+
+const log = new Logger({ minLevel: LOG_LEVEL, stylePrettyLogs: LOG_PRETTY })
+
+export function stringifyBigInt(obj: any): string {
+ return JSON.stringify(
+ obj,
+ (_, value) => (typeof value === 'bigint' ? value.toString() : value),
+ 4
+ )
+}
+
+export async function delay(ms: number): Promise {
+ return await new Promise((resolve) => setTimeout(resolve, ms))
+}
+
+export async function withTimeout(promise: Promise, ms: number): Promise {
+ const timeout = new Promise((_resolve, reject) => {
+ setTimeout(() => {
+ reject(new BrowserTimeoutError('Operation timed out'))
+ }, ms)
+ })
+ return await Promise.race([promise, timeout])
+}
+
+export function hexToIp(hexString: string): string {
+ const hex = hexString.startsWith('0x') ? hexString.slice(2) : hexString
+ const paddedHex = hex.padStart(8, '0')
+ return [
+ parseInt(paddedHex.substring(0, 2), 16),
+ parseInt(paddedHex.substring(2, 4), 16),
+ parseInt(paddedHex.substring(4, 6), 16),
+ parseInt(paddedHex.substring(6, 8), 16)
+ ].join('.')
+}
+
+export function currentTimestamp(): number {
+ return Math.floor(Date.now() / 1000)
+}
+
+export function readJson(filepath: string): any {
+ const data = readFileSync(filepath, 'utf8')
+ return JSON.parse(data)
+}
+
+export function writeJson(filepath: string, data: any): void {
+ log.info(`Going to save data to file: ${filepath}`)
+ const tmpFilepath = `${filepath}.tmp`
+ writeFileSync(tmpFilepath, stringifyBigInt(data), 'utf8')
+ moveFile(tmpFilepath, filepath)
+}
+
+function moveFile(source: string, destination: string): void {
+ renameSync(source, destination)
+ log.info(`Successfully moved the file from ${source} to ${destination}`)
+}
+
+export async function pingUrl(
+ url: string,
+ maxAttempts: number = DEFAULT_PING_ITERATIONS,
+ delay: number = DEFAULT_PING_DELAY
+): Promise {
+ let attempt = 0
+ while (attempt < maxAttempts) {
+ try {
+ const response = await fetch(url)
+ if (response.ok) {
+ log.info(`URL is available: ${url}`)
+ return
+ } else {
+ log.info(`Attempt ${attempt + 1} failed with status: ${response.status}`)
+ }
+ } catch (error) {
+ log.info(
+ `${url} connection failed - ${attempt + 1}/${maxAttempts}, retrying in ${
+ delay / 1000
+ } seconds...`
+ )
+ }
+ await new Promise((resolve) => setTimeout(resolve, delay))
+ attempt++
+ }
+ log.info('Max attempts reached, URL is not available.')
+}
+
+export function chainIdHex(schainName: string): string {
+ return toBeHex(id(schainName).substring(0, 15))
+}
+
+export function chainIdInt(schainName: string): number {
+ return parseInt(chainIdHex(schainName))
+}
diff --git a/network-browser/tests/browser.test.ts b/network-browser/tests/browser.test.ts
new file mode 100644
index 00000000..48f6afdf
--- /dev/null
+++ b/network-browser/tests/browser.test.ts
@@ -0,0 +1,98 @@
+import { unlinkSync, existsSync } from 'node:fs'
+import { describe, beforeAll, test, expect, beforeEach } from 'bun:test'
+import { Contract, Wallet } from 'ethers'
+
+import {
+ getMainnetProvider,
+ nodesContract,
+ getMainnetManagerAbi,
+ schainsInternalContract
+} from '../src/contracts'
+import { browse } from '../src/browser'
+import { getSChain } from '../src/schains'
+import { readJson } from '../src/tools'
+import { NetworkBrowserData } from '../src/interfaces'
+import { MAINNET_RPC_URL, IMA_NETWORK_BROWSER_DATA_PATH } from '../src/constants'
+
+import {
+ ETH_PRIVATE_KEY,
+ NODES_IN_SCHAIN,
+ validatorsContract,
+ managerContract,
+ schainsContract,
+ addAllPermissions,
+ generateWallets,
+ initDefaultValidator,
+ linkNodes,
+ registerNodes,
+ addTestSchainTypes,
+ createSchain,
+ randomString
+} from './testUtils'
+
+describe('browser module test', () => {
+ let nodes: Contract
+ let schainsInternal: Contract
+ let wallet: Wallet
+ const chainName = randomString()
+
+ beforeAll(async () => {
+ console.log('initializing provider and contracts')
+ const provider = await getMainnetProvider(MAINNET_RPC_URL, false)
+ wallet = new Wallet(ETH_PRIVATE_KEY, provider)
+
+ const managerAbi = getMainnetManagerAbi()
+ const validators = validatorsContract(managerAbi, wallet)
+ schainsInternal = schainsInternalContract(managerAbi, wallet)
+ const schains = schainsContract(managerAbi, wallet)
+ const manager = managerContract(managerAbi, wallet)
+
+ nodes = nodesContract(managerAbi, provider)
+
+ await addAllPermissions(validators, schainsInternal, schains, wallet)
+ await initDefaultValidator(validators)
+ const wallets = await generateWallets(provider, wallet, NODES_IN_SCHAIN)
+ await linkNodes(validators, wallet, wallets)
+ await registerNodes(nodes, wallets)
+
+ await addTestSchainTypes(schainsInternal)
+ await createSchain(schains, chainName, wallet.address)
+ })
+
+ beforeEach(async () => {
+ if (existsSync(IMA_NETWORK_BROWSER_DATA_PATH)) {
+ console.log('removing browse results')
+ unlinkSync(IMA_NETWORK_BROWSER_DATA_PATH)
+ }
+ })
+
+ test('browse', async () => {
+ const schain = await getSChain(schainsInternal, chainName)
+ expect(schain.name).toBe(chainName)
+ expect(schain.mainnetOwner).toBe(wallet.address)
+
+ expect(existsSync(IMA_NETWORK_BROWSER_DATA_PATH)).toBeFalse
+ await browse(schainsInternal, nodes)
+ expect(existsSync(IMA_NETWORK_BROWSER_DATA_PATH)).toBeTrue
+
+ const nbData: NetworkBrowserData = readJson(IMA_NETWORK_BROWSER_DATA_PATH)
+
+ expect(nbData.updatedAt).toBeNumber
+ expect(nbData.schains).toBeArray
+ expect(nbData.schains[0].name).toBeString
+ expect(nbData.schains[0].mainnetOwner).toBeString
+ expect(nbData.schains[0].indexInOwnerList).toBeString
+ expect(nbData.schains[0].partOfNode).toBeString
+ expect(nbData.schains[0].lifetime).toBeString
+ expect(nbData.schains[0].startBlock).toBeString
+ expect(nbData.schains[0].deposit).toBeString
+ expect(nbData.schains[0].index).toBeString
+ expect(nbData.schains[0].generation).toBeString
+ expect(nbData.schains[0].chainId).toBeNumber
+ expect(nbData.schains[0].nodes).toBeArrayOfSize(NODES_IN_SCHAIN)
+ if (nbData.schains[0].nodes) {
+ expect(nbData.schains[0].nodes[0].endpoints?.domain.https).toBeString
+ expect(nbData.schains[0].nodes[0].endpoints?.ip.ws).toBeString
+ }
+ })
+})
diff --git a/network-browser/tests/nodes.test.ts b/network-browser/tests/nodes.test.ts
new file mode 100644
index 00000000..ec82b90b
--- /dev/null
+++ b/network-browser/tests/nodes.test.ts
@@ -0,0 +1,82 @@
+import { describe, beforeAll, test, expect } from 'bun:test'
+import { Contract, Wallet, id } from 'ethers'
+
+import {
+ getMainnetProvider,
+ nodesContract,
+ getMainnetManagerAbi,
+ schainsInternalContract
+} from '../src/contracts'
+import { getNodes } from '../src/nodes'
+import { getNodeIdsInGroups } from '../src/schains'
+import { MAINNET_RPC_URL } from '../src/constants'
+import { Node } from '../src/interfaces'
+
+import {
+ ETH_PRIVATE_KEY,
+ NODES_IN_SCHAIN,
+ validatorsContract,
+ managerContract,
+ schainsContract,
+ addAllPermissions,
+ generateWallets,
+ initDefaultValidator,
+ linkNodes,
+ registerNodes,
+ addTestSchainTypes,
+ createSchain,
+ randomString,
+ nodeNamesToIds
+} from './testUtils'
+
+describe('nodes module test', () => {
+ let nodes: Contract
+ let schainsInternal: Contract
+ let wallet: Wallet
+ let nodeNames: string[]
+ let nodeIds: number[]
+ const chainName = randomString()
+
+ beforeAll(async () => {
+ console.log('initializing provider and contracts')
+ const provider = await getMainnetProvider(MAINNET_RPC_URL, false)
+ wallet = new Wallet(ETH_PRIVATE_KEY, provider)
+
+ const managerAbi = getMainnetManagerAbi()
+ const validators = validatorsContract(managerAbi, wallet)
+ schainsInternal = schainsInternalContract(managerAbi, wallet)
+ const schains = schainsContract(managerAbi, wallet)
+ const manager = managerContract(managerAbi, wallet)
+
+ nodes = nodesContract(managerAbi, provider)
+
+ await addAllPermissions(validators, schainsInternal, schains, wallet)
+ await initDefaultValidator(validators)
+ const wallets = await generateWallets(provider, wallet, NODES_IN_SCHAIN)
+ await linkNodes(validators, wallet, wallets)
+ nodeNames = await registerNodes(nodes, wallets)
+
+ nodeIds = await nodeNamesToIds(nodes, nodeNames)
+
+ await addTestSchainTypes(schainsInternal)
+ await createSchain(schains, chainName, wallet.address)
+ })
+ test('getNodes', async () => {
+ const chainHash = id(chainName)
+ const nodeIds = await getNodeIdsInGroups(schainsInternal, [chainHash])
+ const nodesRes: Node[] = await getNodes(nodes, schainsInternal, nodeIds[0], chainHash)
+
+ expect(nodesRes).toBeArrayOfSize(NODES_IN_SCHAIN)
+
+ expect(nodesRes[0].endpoints).toBeDefined
+ expect(nodesRes[0].endpoints?.domain.http).toBeString
+ expect(nodesRes[0].endpoints?.domain.https).toBeString
+ expect(nodesRes[0].endpoints?.domain.ws).toBeString
+ expect(nodesRes[0].endpoints?.domain.wss).toBeString
+
+ expect(nodesRes[0].endpoints?.ip.http).toBeString
+ expect(nodesRes[0].endpoints?.ip.https).toBeString
+ expect(nodesRes[0].endpoints?.ip.ws).toBeString
+ expect(nodesRes[0].endpoints?.ip.wss).toBeString
+ })
+})
diff --git a/network-browser/tests/testUtils.ts b/network-browser/tests/testUtils.ts
new file mode 100644
index 00000000..f2c3cad1
--- /dev/null
+++ b/network-browser/tests/testUtils.ts
@@ -0,0 +1,238 @@
+import {
+ type Provider,
+ Contract,
+ Wallet,
+ TransactionReceipt,
+ parseEther,
+ Signer,
+ getBytes,
+ solidityPackedKeccak256,
+ BytesLike,
+ hexlify,
+ id,
+ zeroPadValue,
+ TransactionResponse
+} from 'ethers'
+import { ec } from 'elliptic'
+
+import { getMainnetManagerAbi } from '../src/contracts'
+
+const secp256k1EC = new ec('secp256k1')
+
+import { type SkaleManagerAbi } from '../src/interfaces'
+
+export const ETH_PRIVATE_KEY = process.env.ETH_PRIVATE_KEY!
+
+export const NODES_IN_SCHAIN = 16
+export const TEST_VALIDATOR_NAME = 'test_val'
+const TEST_VALIDATOR_ID = 1n
+const ETH_TRANSFER_AMOUNT = '0.1'
+const CONFIRMATION_BLOCKS = 2
+const GAS_MULTIPLIER = 1.2
+
+export function validatorsContract(abi: SkaleManagerAbi, wallet: Wallet): Contract {
+ return new Contract(abi.validator_service_address, abi.validator_service_abi, wallet)
+}
+
+export function schainsContract(abi: SkaleManagerAbi, wallet: Wallet): Contract {
+ return new Contract(abi.schains_address, abi.schains_abi, wallet)
+}
+
+export function managerContract(abi: SkaleManagerAbi, wallet: Wallet): Contract {
+ return new Contract(abi.skale_manager_address, abi.skale_manager_abi, wallet)
+}
+
+export async function addAllPermissions(
+ validators: Contract,
+ schainsInternal: Contract,
+ schains: Contract,
+ wallet: Wallet
+): Promise {
+ const VALIDATOR_MANAGER_ROLE = await validators.VALIDATOR_MANAGER_ROLE()
+ let hasRole = await validators.hasRole(VALIDATOR_MANAGER_ROLE, wallet.address)
+ if (!hasRole) {
+ console.log('granting ROLE: VALIDATOR_MANAGER_ROLE')
+ await sendTx(validators.grantRole, [VALIDATOR_MANAGER_ROLE, wallet.address])
+ }
+ const SCHAIN_TYPE_MANAGER_ROLE = await schainsInternal.SCHAIN_TYPE_MANAGER_ROLE()
+ hasRole = await schainsInternal.hasRole(SCHAIN_TYPE_MANAGER_ROLE, wallet.address)
+ if (!hasRole) {
+ console.log('granting ROLE: SCHAIN_TYPE_MANAGER_ROLE')
+ await sendTx(schainsInternal.grantRole, [SCHAIN_TYPE_MANAGER_ROLE, wallet.address])
+ }
+ const SCHAIN_CREATOR_ROLE = await schains.SCHAIN_CREATOR_ROLE()
+ hasRole = await schains.hasRole(SCHAIN_CREATOR_ROLE, wallet.address)
+ if (!hasRole) {
+ console.log('granting ROLE: SCHAIN_CREATOR_ROLE')
+ await sendTx(schains.grantRole, [SCHAIN_CREATOR_ROLE, wallet.address])
+ }
+ console.log('all roles granted')
+}
+
+export async function initDefaultValidator(validators: Contract): Promise {
+ let number = await validators.numberOfValidators()
+ if (number === 0n) {
+ console.log('going to register validator')
+ await sendTx(validators.registerValidator, [TEST_VALIDATOR_NAME, '', 10, 0])
+
+ number = await validators.numberOfValidators()
+ console.log(`number of active validators: ${number}`)
+
+ console.log('going to enable validator')
+ await sendTx(validators.enableValidator, [1])
+ console.log('validator registered and enabled')
+ } else {
+ console.log('validator already exist, skipping')
+ }
+}
+
+export async function generateWallets(
+ provider: Provider,
+ adminWallet: Wallet,
+ num: number
+): Promise {
+ const wallets: Promise[] = []
+ const baseNonce = await adminWallet.getNonce()
+ for (let i = 0; i < num; i++) {
+ console.log(`${i + 1}/${num} generating new wallet...`)
+ wallets.push(generateWallet(provider, adminWallet, baseNonce + i))
+ }
+ return await Promise.all(wallets)
+}
+
+export async function generateWallet(
+ provider: Provider,
+ adminWallet: Wallet,
+ nonce?: number
+): Promise {
+ const wallet = Wallet.createRandom()
+ wallet.connect(provider)
+ await sendEth(adminWallet, wallet.address, ETH_TRANSFER_AMOUNT, provider, nonce)
+ console.log(`new wallet generated: ${wallet.address}, eth transferred: ${ETH_TRANSFER_AMOUNT}`)
+ return new Wallet(wallet.privateKey, provider)
+}
+
+async function sendEth(
+ senderWallet: Wallet,
+ recipientAddress: string,
+ amountEth: string, // Amount in ETH, e.g., "0.1" for 0.1 ETH
+ provider: Provider,
+ nonce?: number
+): Promise {
+ senderWallet = senderWallet.connect(provider)
+ const amountWei = parseEther(amountEth)
+ const tx = {
+ to: recipientAddress,
+ value: amountWei,
+ nonce: nonce
+ }
+ const txResponse = await senderWallet.sendTransaction(tx)
+ return provider.waitForTransaction(txResponse.hash)
+}
+
+async function getValidatorIdSignature(validatorId: bigint, signer: Signer) {
+ return await signer.signMessage(getBytes(solidityPackedKeccak256(['uint'], [validatorId])))
+}
+
+export async function linkNodes(
+ validators: Contract,
+ adminWallet: Wallet,
+ wallets: Wallet[]
+): Promise {
+ const baseNonce = await adminWallet.getNonce()
+ const promises = wallets.map(async (wallet, i) => {
+ console.log(`linking node address: ${wallet.address}`)
+ const signature = await getValidatorIdSignature(TEST_VALIDATOR_ID, wallet)
+ await validators.linkNodeAddress(wallet.address, signature, { nonce: baseNonce + i })
+ console.log(`linked node address: ${wallet.address}`)
+ })
+ await Promise.all(promises)
+}
+
+export async function registerNodes(nodes: Contract, wallets: Wallet[]): Promise {
+ const promises = wallets.map(async (wallet, i) => {
+ console.log(`${i + 1}/${wallets.length} registering node for: ${wallet.address}`)
+ const managerAbi = getMainnetManagerAbi()
+ const manager = managerContract(managerAbi, wallet)
+
+ const { ip, port, name, domainName, publicIp } = generateNodeInfo(wallet.address, i)
+
+ const skaleNonce = randNum(0, 10000)
+ const pkPartsBytes = getPublicKey(wallet)
+
+ await sendTx(manager.createNode, [
+ port,
+ skaleNonce,
+ ipToHex(ip),
+ ipToHex(publicIp),
+ pkPartsBytes,
+ name,
+ domainName
+ ])
+ console.log(`new node created: ${ip}:${port} - ${name} - ${domainName}`)
+ return name
+ })
+ return await Promise.all(promises)
+}
+
+export async function nodeNamesToIds(nodes: Contract, nodeNames: string[]): Promise {
+ const promises = nodeNames.map(async (name, i) => {
+ return await nodes.nodesNameToIndex(id(name))
+ })
+ return await Promise.all(promises)
+}
+
+export async function addTestSchainTypes(schains: Contract): Promise {
+ await sendTx(schains.addSchainType, [8, NODES_IN_SCHAIN])
+ await sendTx(schains.addSchainType, [8, NODES_IN_SCHAIN])
+}
+
+export async function createSchain(schains: Contract, name: string, owner: string): Promise {
+ console.log(`creating new schain: ${name}, owner: ${owner}`)
+ await sendTx(schains.addSchainByFoundation, [1000, 1, 10, name, owner, owner, []])
+ console.log(`schain created: ${name}`)
+}
+
+function generateNodeInfo(address: string, seed: number): any {
+ return {
+ ip: getRandomIp(),
+ port: 10000 + seed * randNum(1, 10),
+ name: `node-${address}`,
+ domainName: `nd.${address}.com`,
+ publicIp: getRandomIp()
+ }
+}
+
+function getRandomIp(): string {
+ return `${randNum(0, 255)}.${randNum(0, 255)}.${randNum(0, 255)}.${randNum(0, 255)}`
+}
+
+function randNum(min: number, max: number): number {
+ return Math.floor(Math.random() * (max - min + 1)) + min
+}
+
+function getPublicKey(wallet: Wallet): [BytesLike, BytesLike] {
+ const publicKey = secp256k1EC.keyFromPrivate(wallet.privateKey.slice(2)).getPublic()
+ const pubA = zeroPadValue(hexlify(publicKey.getX().toBuffer()), 32)
+ const pubB = zeroPadValue(hexlify(publicKey.getY().toBuffer()), 32)
+ return [pubA, pubB]
+}
+
+function ipToHex(ip: string): string {
+ const parts = ip.split('.')
+ const hexParts = parts.map((part) => parseInt(part).toString(16).padStart(2, '0')).join('')
+ const hexIp = `0x${hexParts}`
+ return hexIp
+}
+
+export async function sendTx(func: any, args: any[]): Promise {
+ const estimatedGas = await func.estimateGas(...args)
+ const response: TransactionResponse = await func(...args, {
+ gasLimit: BigInt(Math.round(Number(estimatedGas) * GAS_MULTIPLIER))
+ })
+ return await response.wait(CONFIRMATION_BLOCKS)
+}
+
+export function randomString(): string {
+ return (Math.random() + 1).toString(36).substring(7)
+}
diff --git a/network-browser/tests/tools.test.ts b/network-browser/tests/tools.test.ts
new file mode 100644
index 00000000..207137d3
--- /dev/null
+++ b/network-browser/tests/tools.test.ts
@@ -0,0 +1,56 @@
+import { describe, expect, test } from 'bun:test'
+
+import {
+ stringifyBigInt,
+ hexToIp,
+ currentTimestamp,
+ delay,
+ withTimeout,
+ chainIdHex,
+ chainIdInt
+} from '../src/tools'
+import { isValidNumber } from '../src/envTools'
+import { BrowserTimeoutError } from '../src/errors'
+
+describe('tools module test', () => {
+ test('stringifyBigInt', () => {
+ const res = stringifyBigInt({ a: 1000n })
+ expect(res).toBe('{\n "a": "1000"\n}')
+ })
+ test('hexToIp', () => {
+ expect(hexToIp('0x5E0C3880')).toBe('94.12.56.128')
+ expect(hexToIp('01010101')).toBe('1.1.1.1')
+ expect(hexToIp('0xFFFFFFFF')).toBe('255.255.255.255')
+ expect(hexToIp('0xFFFFFFFFFFFFFFFF')).toBe('255.255.255.255')
+ })
+ test('currentTimestamp', () => {
+ const ts = currentTimestamp()
+ expect(typeof ts).toBe('number')
+ expect(ts.toString()).toHaveLength(10)
+ })
+
+ test('delay', () => {
+ expect(delay(10)).resolves
+ })
+
+ test('withTimeout', async () => {
+ expect(withTimeout(delay(10), 100)).resolves.toBe(undefined)
+ expect(withTimeout(delay(1000), 100)).rejects.toThrow(
+ new BrowserTimeoutError('Operation timed out')
+ )
+ })
+
+ test('chainId', () => {
+ expect(chainIdHex('elated-tan-skat')).toBe('0x79f99296')
+ expect(chainIdInt('elated-tan-skat')).toBe(2046399126)
+ })
+
+ test('isValidNumber', () => {
+ expect(isValidNumber('123')).toBeTrue
+ expect(isValidNumber('12.34')).toBeTrue
+ expect(isValidNumber('-123')).toBeTrue
+ expect(isValidNumber('abc')).toBeFalse
+ expect(isValidNumber('123abc')).toBeFalse
+ expect(isValidNumber('')).toBeFalse
+ })
+})
diff --git a/network-browser/tsconfig.json b/network-browser/tsconfig.json
new file mode 100644
index 00000000..da516b48
--- /dev/null
+++ b/network-browser/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ /* Visit https://aka.ms/tsconfig.json to read more about this file */
+ "target": "es2022" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
+ "module": "es2022" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
+ "declaration": true /* Generates corresponding '.d.ts' file. */,
+ "outDir": "build" /* Redirect output structure to the directory. */,
+ /* Strict Type-Checking Options */
+ "strict": true /* Enable all strict type-checking options. */,
+ "resolveJsonModule": true,
+ /* Module Resolution Options */
+ "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
+ "types": ["bun-types"] /* Type declaration files to be included in compilation. */,
+ "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
+ /* Advanced Options */
+ "skipLibCheck": true /* Skip type checking of declaration files. */,
+ "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
+ }
+}
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 00000000..f08e0406
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,784 @@
+{
+ "name": "skale-ima-agent",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "skale-ima-agent",
+ "hasInstallScript": true,
+ "license": "AGPL-3.0",
+ "dependencies": {
+ "remark": "^15.0.1"
+ },
+ "devDependencies": {}
+ },
+ "node_modules/@types/debug": {
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
+ "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
+ "dependencies": {
+ "@types/ms": "*"
+ }
+ },
+ "node_modules/@types/mdast": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.3.tgz",
+ "integrity": "sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg==",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/ms": {
+ "version": "0.7.34",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz",
+ "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
+ },
+ "node_modules/@types/unist": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz",
+ "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ=="
+ },
+ "node_modules/bail": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
+ "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
+ "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decode-named-character-reference": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz",
+ "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==",
+ "dependencies": {
+ "character-entities": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/devlop": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
+ "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
+ "dependencies": {
+ "dequal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
+ },
+ "node_modules/is-plain-obj": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
+ "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/longest-streak": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
+ "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/mdast-util-from-markdown": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.0.tgz",
+ "integrity": "sha512-n7MTOr/z+8NAX/wmhhDji8O3bRvPTV/U0oTCaZJkjhPSKTPhS3xufVhKGF8s1pJ7Ox4QgoIU7KHseh09S+9rTA==",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark": "^4.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-phrasing": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.0.0.tgz",
+ "integrity": "sha512-xadSsJayQIucJ9n053dfQwVu1kuXg7jCTdYsMK8rqzKZh52nLfSH/k0sAxE0u+pj/zKZX+o5wB+ML5mRayOxFA==",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-markdown": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz",
+ "integrity": "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "longest-streak": "^3.0.0",
+ "mdast-util-phrasing": "^4.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "unist-util-visit": "^5.0.0",
+ "zwitch": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
+ "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",
+ "dependencies": {
+ "@types/mdast": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.0.tgz",
+ "integrity": "sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "@types/debug": "^4.0.0",
+ "debug": "^4.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-core-commonmark": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-core-commonmark": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.0.tgz",
+ "integrity": "sha512-jThOz/pVmAYUtkroV3D5c1osFXAMv9e0ypGDOIZuCeAe91/sD6BoE2Sjzt30yuXtwOYUmySOhMas/PVyh02itA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-factory-destination": "^2.0.0",
+ "micromark-factory-label": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-factory-title": "^2.0.0",
+ "micromark-factory-whitespace": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-html-tag-name": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-destination": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz",
+ "integrity": "sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-label": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.0.tgz",
+ "integrity": "sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-space": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz",
+ "integrity": "sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-title": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.0.tgz",
+ "integrity": "sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-whitespace": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.0.tgz",
+ "integrity": "sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-character": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.0.1.tgz",
+ "integrity": "sha512-3wgnrmEAJ4T+mGXAUfMvMAbxU9RDG43XmGce4j6CwPtVxB3vfwXSZ6KhFwDzZ3mZHhmPimMAXg71veiBGzeAZw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-chunked": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz",
+ "integrity": "sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-classify-character": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz",
+ "integrity": "sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-combine-extensions": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz",
+ "integrity": "sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-numeric-character-reference": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.1.tgz",
+ "integrity": "sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-string": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.0.tgz",
+ "integrity": "sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-encode": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz",
+ "integrity": "sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ]
+ },
+ "node_modules/micromark-util-html-tag-name": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.0.tgz",
+ "integrity": "sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ]
+ },
+ "node_modules/micromark-util-normalize-identifier": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.0.tgz",
+ "integrity": "sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-resolve-all": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz",
+ "integrity": "sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-sanitize-uri": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz",
+ "integrity": "sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-subtokenize": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.0.tgz",
+ "integrity": "sha512-vc93L1t+gpR3p8jxeVdaYlbV2jTYteDje19rNSS/H5dlhxUYll5Fy6vJ2cDwP8RnsXi818yGty1ayP55y3W6fg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-symbol": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz",
+ "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ]
+ },
+ "node_modules/micromark-util-types": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz",
+ "integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ]
+ },
+ "node_modules/ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ },
+ "node_modules/remark": {
+ "version": "15.0.1",
+ "resolved": "https://registry.npmjs.org/remark/-/remark-15.0.1.tgz",
+ "integrity": "sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-stringify": "^11.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-parse": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
+ "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-stringify": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
+ "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/trough": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz",
+ "integrity": "sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/unified": {
+ "version": "11.0.4",
+ "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.4.tgz",
+ "integrity": "sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "bail": "^2.0.0",
+ "devlop": "^1.0.0",
+ "extend": "^3.0.0",
+ "is-plain-obj": "^4.0.0",
+ "trough": "^2.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-is": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz",
+ "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-stringify-position": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
+ "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz",
+ "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit-parents": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz",
+ "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vfile": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz",
+ "integrity": "sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-stringify-position": "^4.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vfile-message": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz",
+ "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/zwitch": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
+ "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..e9a68daf
--- /dev/null
+++ b/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "skale-ima-agent",
+ "license": "AGPL-3.0",
+ "author": "SKALE Labs and contributors",
+ "scripts": {
+ "build": "cd src && yarn build && cd ..",
+ "rebuild": "cd src && yarn rebuild && cd ..",
+ "clean-build": "cd src && yarn clean-build && cd ..",
+ "lint-check": "cd src && yarn lint-check && cd ..",
+ "lint-nb": "cd network-browser && yarn lint-check && cd ..",
+ "install-nb": "cd network-browser && bun install && cd ..",
+ "lint-fix": "cd src && yarn lint-fix && cd ..",
+ "postinstall": "cd src && yarn install && cd .. && cd IMA && yarn install && cd .. && yarn install-nb",
+ "check-outdated": "yarn outdated; cd src; yarn outdated; cd ..; cd IMA; yarn outdated; cd ..",
+ "upgrade-to-latest": "yarn upgrade --latest; cd src; yarn upgrade --latest; cd ..; cd IMA; yarn upgrade --latest; cd .."
+ },
+ "dependencies": {
+ "remark": "^15.0.1"
+ }
+}
diff --git a/runner/run.sh b/runner/run.sh
new file mode 100644
index 00000000..f6d35e2e
--- /dev/null
+++ b/runner/run.sh
@@ -0,0 +1,122 @@
+#!/usr/bin/env bash
+
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
+
+# Required IMA variables
+
+: "${SCHAIN_DIR?Need to set SCHAIN_DIR}"
+
+: "${MAINNET_PROXY_PATH?Need to set MAINNET_PROXY_PATH}"
+: "${SCHAIN_PROXY_PATH?Need to set SCHAIN_PROXY_PATH}"
+
+: "${STATE_FILE?Need to set STATE_FILE}"
+
+: "${SCHAIN_NAME?Need to set SCHAIN_NAME}"
+
+: "${SCHAIN_RPC_URL?Need to set SCHAIN_RPC_URL}"
+: "${MAINNET_RPC_URL?Need to set MAINNET_RPC_URL}"
+
+: "${NODE_NUMBER?Need to set NODE_NUMBER}"
+: "${NODES_COUNT?Need to set NODES_COUNT}"
+
+: "${RPC_PORT?Need to set RPC_PORT}"
+: "${MONITORING_PORT?Need to set MONITORING_PORT}"
+
+: "${TM_URL_MAIN_NET?Need to set TM_URL_MAIN_NET}"
+: "${IMA_NETWORK_BROWSER_DATA_PATH?Need to set IMA_NETWORK_BROWSER_DATA_PATH}"
+
+# SGX variables
+
+: "${SGX_URL?Need to set SGX_URL}"
+: "${ECDSA_KEY_NAME?Need to set ECDSA_KEY_NAME}"
+: "${BLS_KEY_NAME?Need to set BLS_KEY_NAME}"
+: "${SGX_SSL_KEY_PATH?Need to set SGX_SSL_KEY_PATH}"
+: "${SGX_SSL_CERT_PATH?Need to set SGX_SSL_CERT_PATH}"
+: "${NODE_ADDRESS?Need to set NODE_ADDRESS}"
+
+# Optional IMA variables
+
+export GAS_PRICE_MULTIPLIER=${GAS_PRICE_MULTIPLIER:-2}
+export GAS_MULTIPLIER=${GAS_MULTIPLIER:-2}
+export VERBOSE=${VERBOSE:-9}
+
+export M2S_TRANSFER_BLOCK_SIZE=${M2S_TRANSFER_BLOCK_SIZE:-4}
+export S2M_TRANSFER_BLOCK_SIZE=${S2M_TRANSFER_BLOCK_SIZE:-4}
+export S2S_TRANSFER_BLOCK_SIZE=${S2S_TRANSFER_BLOCK_SIZE:-4}
+export M2S_MAX_TRANSACTIONS=${M2S_MAX_TRANSACTIONS:-0}
+export S2M_MAX_TRANSACTIONS=${S2M_MAX_TRANSACTIONS:-0}
+export S2S_MAX_TRANSACTIONS=${S2S_MAX_TRANSACTIONS:-0}
+export M2S_AWAIT_BLOCKS=${M2S_AWAIT_BLOCKS:-0}
+export S2M_AWAIT_BLOCKS=${S2M_AWAIT_BLOCKS:-0}
+export S2S_AWAIT_BLOCKS=${S2S_AWAIT_BLOCKS:-0}
+export M2S_AWAIT_TIME=${M2S_AWAIT_TIME:-0}
+export S2M_AWAIT_TIME=${S2M_AWAIT_TIME:-0}
+export S2S_AWAIT_TIME=${S2S_AWAIT_TIME:-0}
+
+export PERIOD=${PERIOD:-10}
+export TIME_FRAMING=${TIME_FRAMING:-300}
+export TIME_GAP=${TIME_GAP:-15}
+
+export CID_MAIN_NET=${CID_MAIN_NET:--4}
+export CID_SCHAIN=${CID_SCHAIN:--4}
+
+
+BASE_OPTIONS="--gas-price-multiplier=$GAS_PRICE_MULTIPLIER \
+--gas-multiplier=$GAS_MULTIPLIER \
+--verbose=$VERBOSE \
+--cross-ima \
+--json-rpc-port=$RPC_PORT \
+--s2s-enable \
+--abi-skale-manager=$MANAGER_ABI_PATH \
+--url-main-net=$MAINNET_RPC_URL \
+--url-s-chain=$SCHAIN_RPC_URL \
+--id-main-net=Mainnet \
+--id-s-chain=$SCHAIN_NAME \
+--cid-main-net=$CID_MAIN_NET \
+--cid-s-chain=$CID_SCHAIN \
+--abi-main-net=$MAINNET_PROXY_PATH \
+--abi-s-chain=$SCHAIN_PROXY_PATH \
+--sgx-url=$SGX_URL \
+--sgx-bls-key=$BLS_KEY_NAME \
+--sgx-ecdsa-key=$ECDSA_KEY_NAME \
+--sgx-ssl-key=$SGX_SSL_KEY_PATH \
+--sgx-ssl-cert=$SGX_SSL_CERT_PATH \
+--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 \
+--bls-verify=/ima/bls_binaries/verify_bls \
+--m2s-transfer-block-size=$M2S_TRANSFER_BLOCK_SIZE \
+--s2m-transfer-block-size=$S2M_TRANSFER_BLOCK_SIZE \
+--s2s-transfer-block-size=$S2S_TRANSFER_BLOCK_SIZE \
+--m2s-max-transactions=$M2S_MAX_TRANSACTIONS \
+--s2m-max-transactions=$S2M_MAX_TRANSACTIONS \
+--s2s-max-transactions=$S2S_MAX_TRANSACTIONS \
+--m2s-await-blocks=$M2S_AWAIT_BLOCKS \
+--s2m-await-blocks=$S2M_AWAIT_BLOCKS \
+--s2s-await-blocks=$S2S_AWAIT_BLOCKS \
+--m2s-await-time=$M2S_AWAIT_TIME \
+--s2m-await-time=$S2M_AWAIT_TIME \
+--s2s-await-time=$S2S_AWAIT_TIME \
+--period=$PERIOD \
+--node-number=$NODE_NUMBER \
+--nodes-count=$NODES_COUNT \
+--time-framing=$TIME_FRAMING \
+--tm-url-main-net=$TM_URL_MAIN_NET \
+--time-gap=$TIME_GAP \
+--monitoring-port=$MONITORING_PORT \
+--network-browser-path=$IMA_NETWORK_BROWSER_DATA_PATH \
+--pwa \
+--no-expose-pwa \
+--auto-exit=86400"
+
+IMA_LOOP_CMD="node $DIR/../src/build/main.js --loop $BASE_OPTIONS"
+NETWORK_BROWSER_CMD="node $DIR/../network-browser/build/index.js"
+
+echo "$(date) - Running IMA loop and network-browser"
+node "$DIR/startup.js" "$IMA_LOOP_CMD" "$NETWORK_BROWSER_CMD"
diff --git a/runner/startup.js b/runner/startup.js
new file mode 100644
index 00000000..2aa84427
--- /dev/null
+++ b/runner/startup.js
@@ -0,0 +1,40 @@
+const { spawn } = require( "child_process" );
+
+function startProcess( command ) {
+ // Start the process in a new process group
+ const parts = command.split( " " );
+ return spawn( parts[0], parts.slice( 1 ), { stdio: "inherit", shell: true, detached: true } );
+}
+
+if( process.argv.length !== 4 ) {
+ console.error( "Usage: node startup.js \"\" \"\"" );
+ process.exit( 1 );
+}
+
+const child1 = startProcess( process.argv[2] );
+const child2 = startProcess( process.argv[3] );
+
+function terminateProcessGroup( processToKill ) {
+ if( processToKill && !processToKill.killed ) {
+ // Use negative PID to kill the process group
+ process.kill( -processToKill.pid, "SIGTERM" );
+ }
+}
+
+function onChildExit( otherChild, code, signal ) {
+ terminateProcessGroup( otherChild );
+ // Exit with the code or signal of the process that ended first
+ process.exit( code || signal );
+}
+
+child1.on( "exit", ( code, signal ) => onChildExit( child2, code, signal ) );
+child2.on( "exit", ( code, signal ) => onChildExit( child1, code, signal ) );
+
+process.on( "SIGINT", () => {
+ console.log( "Received SIGINT. Exiting..." );
+
+ terminateProcessGroup( child1 );
+ terminateProcessGroup( child2 );
+
+ process.exit( 130 );
+} );
diff --git a/scripts/bls_binaries/.keep b/scripts/bls_binaries/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/scripts/build_image.sh b/scripts/build_image.sh
new file mode 100755
index 00000000..7b10d9f4
--- /dev/null
+++ b/scripts/build_image.sh
@@ -0,0 +1,35 @@
+#!/usr/bin/env bash
+
+if [ -z "$VERSION" ]
+then
+ echo "No VERSION provided, exiting"
+ exit 1
+fi
+DOCKER_USERNAME=$1
+DOCKER_PASSWORD=$2
+
+NAME=ima
+REPO_NAME=skalenetwork/$NAME
+IMAGE_NAME=$REPO_NAME:$VERSION
+LATEST_IMAGE_NAME=$REPO_NAME:latest
+
+if [ -z "$SKIP_BUILD" ]
+then
+
+ docker build -t "$IMAGE_NAME" . || exit $?
+
+ if [ "$RELEASE" = true ]
+ then
+ docker tag "$IMAGE_NAME" "$LATEST_IMAGE_NAME"
+ fi
+fi
+
+if [[ ! -z "$DOCKER_USERNAME" ]]
+then
+ echo "$DOCKER_PASSWORD" | docker login --username "$DOCKER_USERNAME" --password-stdin
+ docker push "$IMAGE_NAME" || exit $?
+ if [ "$RELEASE" = true ]
+ then
+ docker push $LATEST_IMAGE_NAME || exit $?
+ fi
+fi
diff --git a/scripts/calculate_version.sh b/scripts/calculate_version.sh
new file mode 100755
index 00000000..96860215
--- /dev/null
+++ b/scripts/calculate_version.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+
+VERSION=$(cat VERSION)
+USAGE_MSG='Usage: BRANCH=[BRANCH] calculate_version.sh'
+
+if [ -z "$BRANCH" ]
+then
+ (>&2 echo 'You should provide branch')
+ echo "$USAGE_MSG"
+ exit 1
+fi
+
+
+if [ -z "$VERSION" ]; then
+ echo "The base version is not set."
+ exit 1
+fi
+
+if [[ $BRANCH == 'stable' ]]; then
+ echo "$VERSION"
+ exit 1
+fi
+
+git fetch --tags > /dev/null
+
+TAG=$BRANCH
+if [[ $BRANCH == v*.*.* ]]; then TAG='develop'; fi
+
+for (( NUMBER=0; ; NUMBER++ ))
+do
+ FULL_VERSION="$VERSION-$TAG.$NUMBER"
+ if ! [[ $(git tag -l | grep "$FULL_VERSION") ]]; then
+ echo "$FULL_VERSION" | tr / -
+ break
+ fi
+done
diff --git a/scripts/config_from_accounts.py b/scripts/config_from_accounts.py
new file mode 100644
index 00000000..ac19dc5f
--- /dev/null
+++ b/scripts/config_from_accounts.py
@@ -0,0 +1,43 @@
+#!/usr/bin/python3
+
+# SPDX-License-Identifier: AGPL-3.0-only
+
+# -*- coding: utf-8 -*-
+#
+# This file is part of SKALE IMA.
+#
+# Copyright (C) 2019-Present SKALE Labs
+#
+# 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 .
+
+from sys import argv
+import json
+
+def main():
+ if len(argv) < 2:
+ print('Usage: config_from_accounts.py {file with accounts} {target config file}')
+ exit(1)
+
+ accounts_filename, config_filename = argv[1], argv[2]
+ with open(accounts_filename) as accounts_file:
+ accounts = json.load(accounts_file)
+ addresses = list(accounts['private_keys'].keys())
+ config = {'PRIVATE_KEY_FOR_ETHEREUM': accounts['private_keys'][addresses[0]],
+ 'PRIVATE_KEY_FOR_SCHAIN': accounts['private_keys'][addresses[1]]}
+ with open(config_filename, 'w') as config_file:
+ json.dump(config, config_file)
+
+
+if __name__ == '__main__':
+ main()
\ No newline at end of file
diff --git a/scripts/download_binaries.sh b/scripts/download_binaries.sh
new file mode 100644
index 00000000..cb588ead
--- /dev/null
+++ b/scripts/download_binaries.sh
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
+
+: "${LIB_BLS_RELEASE_TAG?Need to set LIB_BLS_RELEASE_TAG}"
+
+BASE_URL="https://github.com/skalenetwork/libBLS/releases/download"
+
+BLS_GLUE_URL=$BASE_URL/$LIB_BLS_RELEASE_TAG/bls_glue
+HASH_G1_URL=$BASE_URL/$LIB_BLS_RELEASE_TAG/hash_g1
+BLS_VERIFY_URL=$BASE_URL/$LIB_BLS_RELEASE_TAG/verify_bls
+
+echo "Downloading BLS binaries..."
+curl -L "$BLS_GLUE_URL" > "$DIR/bls_binaries/bls_glue"
+curl -L "$HASH_G1_URL" > "$DIR/bls_binaries/hash_g1"
+curl -L "$BLS_VERIFY_URL" > "$DIR/bls_binaries/verify_bls"
diff --git a/src/.eslintrc.cjs b/src/.eslintrc.cjs
new file mode 100644
index 00000000..2b01af34
--- /dev/null
+++ b/src/.eslintrc.cjs
@@ -0,0 +1,45 @@
+module.exports = {
+ "env": {
+ browser: false,
+ es2020: true,
+ node: true
+ },
+ extends: "standard-with-typescript",
+ "globals": {
+ "Atomics": "readonly",
+ "SharedArrayBuffer": "readonly"
+ },
+ "parserOptions": {
+ "ecmaVersion": 2020,
+ "sourceType": "module"
+ },
+ "rules": {
+ "linebreak-style": [ "error", "unix" ],
+ "eqeqeq": "off",
+ "comma-dangle": [ "error", "never" ],
+ "comma-style": [ "error", "last" ],
+ "space-in-parens": [ "error", "always" ],
+ "space-before-blocks": [ "error", "always" ],
+ "array-bracket-spacing": [ "error", "always" ],
+ "object-curly-spacing": [ "error", "always" ],
+ "curly": [ "error", "multi-or-nest" ],
+ "nonblock-statement-body-position": [ "error", "below" ],
+ "no-cond-assign": [ "error", "always" ],
+ "max-len": [ "error", { "code": 100, "tabWidth": 4 } ],
+ "max-lines-per-function": [ "error", { "max": 200, "skipBlankLines": false } ],
+ "@typescript-eslint/indent": [ "error", 4, { "ignoredNodes": [ "SwitchCase" ] } ],
+ "@typescript-eslint/quotes": [ "error", "double" ],
+ "@typescript-eslint/semi": [ "error", "always" ],
+ "@typescript-eslint/space-before-function-paren": "off",
+ "@typescript-eslint/keyword-spacing": [ "error", {
+ "overrides": {
+ "if": { "before": false, "after": false },
+ "else": { "before": true, "after": true },
+ "for": { "before": false, "after": false },
+ "while": { "before": false, "after": false }
+ }
+ } ],
+ "@typescript-eslint/strict-boolean-expressions": "off",
+ "@typescript-eslint/no-this-alias": "off"
+ }
+};
diff --git a/src/CLI.md b/src/CLI.md
new file mode 100644
index 00000000..c0b6e592
--- /dev/null
+++ b/src/CLI.md
@@ -0,0 +1,260 @@
+
+
+# IMA Agent 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.
+ - Configure 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 specified **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 connected 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_TARGET 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 operations, **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 appropriate 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 following 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 following 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 amount of **ETH** to be specified 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 reimbursement** 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 reimbursement** 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 various 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.
+ --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.
+
+**IMA** pending work analysis subsystem allows to detect busy state of previous **IMA Agent** running long work outside its time frame:
+
+ --pwa...........................Enable pending work analysis to avoid transaction conflicts. Default mode.
+ --no-pwa........................Disable pending work analysis. Not recommended for slow and overloaded blockchains.
+ --pwa-timeout=seconds...........Node state timeout during pending work analysis. Default is 60 seconds.
+
+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.
+ --expose-pwa....................Expose IMA agent pending work analysis information
+ --no-expose-pwa.................Do not expose IMA agent pending work analysis information. Default mode.
+
+Command line output and logging can be plain or ANSI-colorized:
+
+ --colors........................Use ANSI-colorized logging.
+ --no-colors.....................Use monochrome logging.
diff --git a/src/IMA-CORE.md b/src/IMA-CORE.md
new file mode 100644
index 00000000..579209fc
--- /dev/null
+++ b/src/IMA-CORE.md
@@ -0,0 +1,119 @@
+
+
+# IMA Agent Core module
+
+Implements **SKALE Interchain Messaging Agent** APIs.
+
+Typical usage:
+
+ const IMA = require( "../npms/skale-ima" );
+ const cc = IMA.cc;
+ const log = IMA.log;
+ const w3mod = IMA.w3mod;
+ let ethereumjs_tx = IMA.ethereumjs_tx;
+ let ethereumjs_wallet = IMA.ethereumjs_wallet;
+ let ethereumjs_util = IMA.ethereumjs_util;
+
+All the **IMA** require externally pre-initialized **Web3** connections, ABI, contract and account description objects.
+
+Mainnet and S-Chains should be pre initialized as following:
+
+ let g_str_url_main_net = "http://127.0.0.1:8545";
+ let g_str_url_s_chain = "http://127.0.0.1:2231";
+ let g_chain_id_main_net = "Mainnet";
+ let g_chain_id_s_chain = "id-S-chain";
+ const g_w3_main_net = new w3mod( new w3mod.providers.HttpProvider( g_str_url_main_net ) );
+ const g_w3_s_chain = new w3mod( new w3mod.providers.HttpProvider( g_str_url_s_chain ) );
+
+The **joTrufflePublishResult_main_net** and **joTrufflePublishResult_s_chain** ABI description objects are ABIs loaded from **truffle**-generated files. They are used to initialize contract description objects:
+
+ let g_jo_deposit_box = new g_w3_main_net.eth.Contract( joTrufflePublishResult_main_net.deposit_box_abi, joTrufflePublishResult_main_net.deposit_box_address ); // only main net
+ let g_jo_token_manager = new g_w3_s_chain .eth.Contract( joTrufflePublishResult_s_chain .token_manager_abi, joTrufflePublishResult_s_chain .token_manager_address ); // only s-chain
+ let g_jo_message_proxy_main_net = new g_w3_main_net.eth.Contract( joTrufflePublishResult_main_net.message_proxy_mainnet_abi, joTrufflePublishResult_main_net.message_proxy_mainnet_address );
+ let g_jo_message_proxy_s_chain = new g_w3_s_chain .eth.Contract( joTrufflePublishResult_s_chain .message_proxy_chain_abi, joTrufflePublishResult_s_chain .message_proxy_chain_address );
+
+The following function registers new **S-Chain** on _Mainnet_ and vice versa:
+
+ async function register_all() {
+ var b1 = await IMA.register_s_chain_in_deposit_box(
+ g_w3_main_net,
+ g_jo_deposit_box, // only main net
+ joAccountMN,
+ g_jo_token_manager, // only s-chain
+ g_chain_id_s_chain,
+ tc_main_net
+ );
+ var b2 = await IMA.register_main_net_depositBox_on_s_chain(
+ g_w3_s_chain,
+ g_jo_token_manager, // only s-chain
+ g_jo_deposit_box, // only main net
+ joAccountSC,
+ chainID_s_chain,
+ tc_s_chain
+ );
+ var b3 = b1 && b2;
+ return b2;
+ }
+
+The following code demonstrates money transfer event processing:
+
+ var b1 = await IMA.do_transfer( // main-net --> s-chain
+ /**/ g_w3_main_net,
+ g_jo_message_proxy_main_net,
+ joAccountMN,
+ g_w3_s_chain,
+ g_jo_message_proxy_s_chain,
+ joAccountSC,
+ g_chain_id_main_net,
+ g_chain_id_s_chain,
+ chainID_main_net,
+ g_cit_s_chain,
+ null, // imaState.jo_deposit_box - for logs validation on mainnet
+ jo_token_manager, // for logs validation on s-chain
+ g_nTransferBlockSizeM2S,
+ g_nMaxTransactionsM2S,
+ g_nBlockAwaitDepthM2S,
+ g_nBlockAgeM2S,
+ fn_do_sign_messages_m2s, // fn_sign_messages or null
+ tc_s_chain,
+ null // optsPendingTxAnalysis
+ );
+ var b2 = await IMA.do_transfer( // s-chain --> main-net
+ /**/ g_w3_s_chain,
+ g_jo_message_proxy_s_chain,
+ joAccountSC,
+ g_w3_main_net,
+ g_jo_message_proxy_main_net,
+ /**/ joAccountMN,
+ g_chain_id_s_chain,
+ g_chain_id_main_net,
+ chainID_s_chain,
+ chainID_main_net,
+ imaState.jo_deposit_box, // for logs validation on mainnet
+ null, // imaState.jo_token_manager, // for logs validation on s-chain
+ g_nTransferBlockSizeS2M,
+ g_nMaxTransactionsS2M,
+ g_nBlockAwaitDepthS2M,
+ g_nBlockAgeS2M,
+ fn_do_sign_messages_s2m, // fn_sign_messages or null
+ tc_main_net,
+ null // optsPendingTxAnalysis
+ );
+
+The following code demonstrates cross-chain payments:
+
+ IMA.do_payment_from_main_net(
+ g_w3_main_net,
+ joAccountMN,
+ joAccountSC,
+ g_jo_deposit_box, // only main net
+ g_chain_id_s_chain,
+ g_wei_amount // how much money to send
+ );
+ await IMA.do_payment_from_s_chain(
+ g_w3_s_chain,
+ joAccountSC,
+ joAccountMN,
+ g_jo_token_manager, // only s-chain
+ g_wei_amount // how much money to send
+ );
diff --git a/src/README.md b/src/README.md
new file mode 100644
index 00000000..fc031ac9
--- /dev/null
+++ b/src/README.md
@@ -0,0 +1,731 @@
+
+
+# SKALE Interchain Messaging Agent
+
+## Overview
+
+This article refers to **SKALE Interchain Messaging Agent** as **IMA**.
+
+**IMA** consists of the following parts:
+
+ - Contracts on Mainnet
+ - Contracts on a SKALE Chain
+ - NodeJS based app
+
+## Contracts installation
+
+### Contracts prerequisites
+
+Get source code of Solidity contracts and install dependencies:
+
+```shell
+git clone git@github.com:skalenetwork/IMA.git
+cd ./IMA
+```
+
+### Node JS prerequisites
+
+Install required **Node JS** packages everywhere:
+
+```shell
+ export IMA_ROOT=.....
+ cd $IMA_ROOT
+ yarn install
+```
+
+Export required environment variables:
+
+```shell
+ export NETWORK_FOR_ETHEREUM="mainnet"
+ export PRIVATE_KEY_FOR_ETHEREUM=""
+ export NETWORK_FOR_SCHAIN="schain"
+ export PRIVATE_KEY_FOR_SCHAIN=""
+ export CHAIN_NAME_SCHAIN="Bob"
+ export URL_W3_ETHEREUM="http://127.0.0.1:8545"
+ export URL_W3_S_CHAIN="http://127.0.0.1:15000"
+ export ACCOUNT_FOR_ETHEREUM=""
+ export ACCOUNT_FOR_SCHAIN="""
+```
+
+Notice: `ACCOUNT_FOR_ETHEREUM` address corresponds to `PRIVATE_KEY_FOR_ETHEREUM` private key, `ACCOUNT_FOR_SCHAIN` address corresponds to `PRIVATE_KEY_FOR_SCHAIN` private key.
+
+Rebuild all the contracts once to ensure everything initialized OK:
+
+```shell
+ cd $IMA_ROOT/proxy
+ npx hardhat clean && npx hardhat compile
+```
+
+### Contracts pre-installation on Mainnet and SKALE Chain
+
+For mainnet, invoke:
+
+```shell
+ cd $IMA_ROOT/proxy
+ yarn run deploy-to-mainnet
+ ls -1 ./data/
+```
+
+You should see **proxyMainnet.json** file listed.
+
+For SKALE chain, invoke:
+
+```shell
+ cd $IMA_ROOT/proxy
+ yarn run deploy-to-schain
+ ls -1 ./data/
+```
+
+You should see **proxySchain_*s-chain-name-here*.json** file listed.
+
+## IMA transaction signing
+
+**IMA** supports two ways of signing transactions:
+
+```shell
+ - Direct private key
+ - SGX Wallet
+```
+
+Private keys can be specified directly in **IMA** command line:
+
+```shell
+ --key-main-net=$PRIVATE_KEY_FOR_ETHEREUM \
+ --key-s-chain=$PRIVATE_KEY_FOR_SCHAIN
+```
+
+If **IMA** action needs to sign only **Main Net** transaction(s) and do read-only actions on **S-Chain**:
+
+```shell
+ --key-main-net=$PRIVATE_KEY_FOR_ETHEREUM \
+ --address-s-chain=0x66c5a87f4a49dd75e970055a265e8dd5c3f8f852
+```
+
+If **IMA** action needs to sign only **S-Chain** transaction(s) and do read-only actions on **Main Net**:
+
+```shell
+ --address-main-net=$ACCOUNT_FOR_ETHEREUM \
+ --key-s-chain=$PRIVATE_KEY_FOR_SCHAIN \
+```
+
+If **IMA** should use **SGX Wallet** to sign transactions, then the parameters above should be replaced with:
+
+```shell
+ --sgx-url-main-net=...\
+ --sgx-url-s-chain=...\
+ --sgx-ecdsa-key-main-net=...\
+ --sgx-ecdsa-key-s-chain=...\
+ --sgx-ssl-key-main-net=...\
+ --sgx-ssl-key-s-chain=...\
+ --sgx-ssl-cert-main-net=...\
+ --sgx-ssl-cert-s-chain=...\
+ --address-main-net=...\
+ --address-s-chain=...
+```
+
+If **IMA** should use **Transaction Manager** to sign transactions, then the parameters above should be replaced with:
+
+```shell
+ --tm-url-main-net=...\
+ --tm-url-s-chain=... \
+ --address-main-net=...\
+ --address-s-chain=...
+```
+
+**IMA** can use different ways of signing messages for **Main Net** and **S-Chain** by mixing connectivity parameters specified above.
+
+Where `--sgx-url-main-net` and `--sgx-url-s-chain` command line parameters provide **HTTPS** URLs for **SGX Wallets** for **Main Net** and **S-Chain**. These URLs can be equal. The `--sgx-ssl-key-main-net`, `--sgx-ssl-key-s-chain`, `--sgx-ssl-cert-main-net` and `--sgx-ssl-cert-s-chain` command line parameters provide SSL certificate and key files. The `--sgx-ecdsa-key-main-net` and `--sgx-ecdsa-key-s-chain` command line parameters provide registered name of **ECDSA key** in **SGX Wallets**, for example `NEK:000`. The `--address-main-net` and `--address-s-chain` command line parameters provide Ethereum wallet addresses corresponding to specified names of **ECDSA key** in **SGX Wallets**.
+
+It's possible to mix **SGX Wallet** and direct private key usage. I.e. **Main Net** and **S-Chain** can use different transaction signing ways. Nevertheless this is never needed in real life.
+
+## IMA installation
+
+### Bind IMA to Main-net
+
+You can check whether **IMA** is already bound with:
+
+```shell
+ node ./main.js --verbose=9 --expose --colors \
+ --check-registration \
+ --url-main-net=$URL_W3_ETHEREUM \
+ --url-s-chain=$URL_W3_S_CHAIN \
+ --id-main-net=Mainnet \
+ --id-s-chain=Bob \
+ --cid-main-net=-4 \
+ --cid-s-chain=-4 \
+ --abi-main-net=../proxy/data/proxyMainnet.json \
+ --abi-s-chain=../proxy/data/proxySchain_Bob.json \
+ --key-main-net=$PRIVATE_KEY_FOR_ETHEREUM \
+ --key-s-chain=$PRIVATE_KEY_FOR_SCHAIN
+```
+
+**IMA** works as S-Chain extension. It should be registered on Main-net before performing any money transfers between blockchains:
+
+```shell
+ node ./main.js --verbose=9 --expose --colors \
+ --register \
+ --url-main-net=$URL_W3_ETHEREUM \
+ --url-s-chain=$URL_W3_S_CHAIN \
+ --id-main-net=Mainnet \
+ --id-s-chain=Bob \
+ --cid-main-net=-4 \
+ --cid-s-chain=-4 \
+ --abi-main-net=../proxy/data/proxyMainnet.json \
+ --abi-s-chain=../proxy/data/proxySchain_Bob.json \
+ --key-main-net=$PRIVATE_KEY_FOR_ETHEREUM \
+ --key-s-chain=$PRIVATE_KEY_FOR_SCHAIN
+```
+
+### Run IMA transfer loop for particular S-Chain
+
+Performed with the **--loop** command line option:
+
+```shell
+ node ./main.js --verbose=9 --expose --colors \
+ --loop \
+ --url-main-net=$URL_W3_ETHEREUM \
+ --url-s-chain=$URL_W3_S_CHAIN \
+ --id-main-net=Mainnet \
+ --id-s-chain=Bob \
+ --cid-main-net=-4 \
+ --cid-s-chain=-4 \
+ --abi-main-net=../proxy/data/proxyMainnet.json \
+ --abi-s-chain=../proxy/data/proxySchain_Bob.json \
+ --key-main-net=$PRIVATE_KEY_FOR_ETHEREUM \
+ --key-s-chain=$PRIVATE_KEY_FOR_SCHAIN
+```
+
+Notice: the command above can be run in forever while loop of shell script or became a part of daemon service file.
+
+### Gas reimbursement
+
+IMA transfers from **S-Chain** back to **Main Net** needs to be payed. Gas reimbursement feature allows to charge special wallet inside IMA for further transfer cost payments.
+
+Show balance:
+
+```shell
+ node ./main.js --verbose=9 --expose --colors \
+ --url-main-net=$URL_W3_ETHEREUM \
+ --url-s-chain=$URL_W3_S_CHAIN \
+ --id-main-net=Mainnet \
+ --id-s-chain=Bob \
+ --cid-main-net=-4 \
+ --cid-s-chain=-4 \
+ --abi-main-net=../proxy/data/proxyMainnet.json \
+ --abi-s-chain=../proxy/data/proxySchain_Bob.json \
+ --reimbursement-chain=Bob \
+ --reimbursement-balance
+ --receiver=$ADDRESS_FOR_ETHEREUM
+```
+
+Estimate amount:
+
+```shell
+ node ./main.js --verbose=9 --expose --colors \
+ --url-main-net=$URL_W3_ETHEREUM \
+ --url-s-chain=$URL_W3_S_CHAIN \
+ --id-main-net=Mainnet \
+ --id-s-chain=Bob \
+ --cid-main-net=-4 \
+ --cid-s-chain=-4 \
+ --abi-main-net=../proxy/data/proxyMainnet.json \
+ --abi-s-chain=../proxy/data/proxySchain_Bob.json \
+ --reimbursement-chain=Bob \
+ --reimbursement-estimate \
+ --receiver=$ADDRESS_FOR_ETHEREUM
+```
+
+Recharge balance with 1 ETH:
+
+```shell
+ node ./main.js --verbose=9 --expose --colors \
+ --url-main-net=$URL_W3_ETHEREUM \
+ --url-s-chain=$URL_W3_S_CHAIN \
+ --id-main-net=Mainnet \
+ --id-s-chain=Bob \
+ --cid-main-net=-4 \
+ --cid-s-chain=-4 \
+ --abi-main-net=../proxy/data/proxyMainnet.json \
+ --abi-s-chain=../proxy/data/proxySchain_Bob.json \
+ --key-main-net=$PRIVATE_KEY_FOR_ETHEREUM \
+ --reimbursement-chain=Bob \
+ --reimbursement-recharge=1eth
+```
+
+Withdraw 1 ETH:
+
+```shell
+ node ./main.js --verbose=9 --expose --colors \
+ --url-main-net=$URL_W3_ETHEREUM \
+ --url-s-chain=$URL_W3_S_CHAIN \
+ --id-main-net=Mainnet \
+ --id-s-chain=Bob \
+ --cid-main-net=-4 \
+ --cid-s-chain=-4 \
+ --abi-main-net=../proxy/data/proxyMainnet.json \
+ --abi-s-chain=../proxy/data/proxySchain_Bob.json \
+ --key-main-net=$PRIVATE_KEY_FOR_ETHEREUM \
+ --reimbursement-chain=Bob \
+ --reimbursement-withdraw=1eth
+```
+
+Set minimal time range in seconds between **S-Chain** back to **Main Net** messages to `0` seconds(default value is `5` minutes):
+
+```shell
+ node ./main.js --verbose=9 --expose --colors \
+ --url-main-net=$URL_W3_ETHEREUM \
+ --url-s-chain=$URL_W3_S_CHAIN \
+ --id-main-net=Mainnet \
+ --id-s-chain=Bob \
+ --cid-main-net=-4 \
+ --cid-s-chain=-4 \
+ --abi-main-net=../proxy/data/proxyMainnet.json \
+ --abi-s-chain=../proxy/data/proxySchain_Bob.json \
+ --key-main-net=$PRIVATE_KEY_FOR_ETHEREUM \
+ --key-s-chain=$PRIVATE_KEY_FOR_S_CHAIN \
+ --reimbursement-range=0
+```
+
+## Other IMA tasks
+
+### Getting command line help
+
+```shell
+ node ./main.js --colors --help
+```
+
+### Displaying run-time variables and arguments
+
+```shell
+ node ./main.js --colors --show-config
+```
+
+### Listing available output detail options
+
+```shell
+ node ./main.js --colors --verbose-list
+```
+
+### Specifying logging options
+
+Log output is always printed to standard output. Log also can be mirrored to file using **--log**=**path** command line option.
+
+By default mirrored log file grows with no limit and no log rotation is performed. To turn log rotation on and specify maximal size of log in bytes you should use the **--log-size**=**value** and **--log-files**=**value** command line options.
+
+## Asset transfer commands
+
+### Money transfer from Main-net account to S-Chain
+
+Performed with the **--m2s-payment** command line option:
+
+```shell
+ node ./main.js --verbose=9 --expose --colors \
+ --m2s-payment \
+ --ether=1 \
+ --url-main-net=$URL_W3_ETHEREUM \
+ --url-s-chain=$URL_W3_S_CHAIN \
+ --id-main-net=Mainnet \
+ --id-s-chain=Bob \
+ --cid-main-net=-4 \
+ --cid-s-chain=-4 \
+ --abi-main-net=../proxy/data/proxyMainnet.json \
+ --abi-s-chain=../proxy/data/proxySchain_Bob.json \
+ --key-main-net=$PRIVATE_KEY_FOR_ETHEREUM \
+ --address-s-chain=$ACCOUNT_FOR_SCHAIN
+```
+
+Notice: The command above does payment from Main-net and that is why we need to specify private key for source account inside Main-net blockchain using the **--key-main-net** command line argument. Target S-chain account is specified as address with the **--address-s-chain** command line argument. We don't need to specify private key for target account.
+
+### Money transfer from S-Chain account to Main-net
+
+Performed with the **--s2m-payment** command line option:
+
+```shell
+ node ./main.js --verbose=9 --expose --colors \
+ --s2m-payment \
+ --ether=1 \
+ --url-main-net=$URL_W3_ETHEREUM \
+ --url-s-chain=$URL_W3_S_CHAIN \
+ --id-main-net=Mainnet \
+ --id-s-chain=Bob \
+ --cid-main-net=-4 \
+ --cid-s-chain=-4 \
+ --abi-main-net=../proxy/data/proxyMainnet.json \
+ --abi-s-chain=../proxy/data/proxySchain_Bob.json \
+ --address-main-net=$ACCOUNT_FOR_ETHEREUM \
+ --key-s-chain=$PRIVATE_KEY_FOR_SCHAIN
+```
+
+Notice: The command above does payment from Main-net and that is why we need to specify private key for source account inside S-chain blockchain using the **--key-s-chain** command line argument. Target Main-net account is specified as address with the **--address-main-net** command line argument. We don't need to specify private key for target account.
+
+### View how much ETH you can receive from S-Chain account to Main-net
+
+Performed with the **--s2m-view** command line option:
+
+```shell
+ node ./main.js --verbose=9 --expose --colors \
+ --s2m-view \
+ --url-main-net=$URL_W3_ETHEREUM \
+ --url-s-chain=$URL_W3_S_CHAIN \
+ --id-main-net=Mainnet \
+ --cid-main-net=-4 \
+ --cid-s-chain=-4 \
+ --id-s-chain=Bob \
+ --abi-main-net=../proxy/data/proxyMainnet.json \
+ --abi-s-chain=../proxy/data/proxySchain_Bob.json \
+ --key-main-net=$PRIVATE_KEY_FOR_ETHEREUM
+```
+
+Notice: this operation is related to ETH transfers only.
+
+### Receive money transfer from S-Chain account to Main-net
+
+Performed with the **--s2m-receive** command line option:
+
+```shell
+ node ./main.js --verbose=9 --expose --colors \
+ --s2m-receive \
+ --url-main-net=$URL_W3_ETHEREUM \
+ --url-s-chain=$URL_W3_S_CHAIN \
+ --id-main-net=Mainnet \
+ --id-s-chain=Bob \
+ --cid-main-net=-4 \
+ --cid-s-chain=-4 \
+ --abi-main-net=../proxy/data/proxyMainnet.json \
+ --abi-s-chain=../proxy/data/proxySchain_Bob.json \
+ --key-main-net=$PRIVATE_KEY_FOR_ETHEREUM
+```
+
+Notice: this operation is related to ETH transfers only.
+
+### Money amount specification for transfer operations
+
+Amount of money should be specified with one of the following command line options
+
+```shell
+ --value=moneySpec..............Amount of eth/finney/szabo/shannon/lovelace/babbage/wei/ to transfer. For instance "1ether" or "100000wei".
+ --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.
+```
+
+### Single transfer loops
+
+Single transfer operations are similar to the **--loop** normal mode but perform single loop iteration and exit.
+
+#### Single transfer iteration from Main-net to S-chain
+
+Performed with the **--m2s-transfer** command line option:
+
+```shell
+ node ./main.js --verbose=9 --expose --colors \
+ --m2s-transfer \
+ --url-main-net=$URL_W3_ETHEREUM \
+ --url-s-chain=$URL_W3_S_CHAIN \
+ --id-main-net=Mainnet \
+ --id-s-chain=Bob \
+ --cid-main-net=-4 \
+ --cid-s-chain=-4 \
+ --abi-main-net=../proxy/data/proxyMainnet.json \
+ --abi-s-chain=../proxy/data/proxySchain_Bob.json \
+ --key-main-net=PRIVATE_KEY_FOR_ETHEREUM \
+ --key-s-chain=$PRIVATE_KEY_FOR_SCHAIN
+```
+
+#### Single transfer iteration from S-chain to Main-net
+
+Performed with the **--s2m-transfer** command line option:
+
+```shell
+ node ./main.js --verbose=9 --expose --colors \
+ --s2m-transfer \
+ --url-main-net=$URL_W3_ETHEREUM \
+ --url-s-chain=$URL_W3_S_CHAIN \
+ --id-main-net=Mainnet \
+ --id-s-chain=Bob \
+ --cid-main-net=-4 \
+ --cid-s-chain=-4 \
+ --abi-main-net=../proxy/data/proxyMainnet.json \
+ --abi-s-chain=../proxy/data/proxySchain_Bob.json \
+ --key-main-net=$PRIVATE_KEY_FOR_ETHEREUM \
+ --key-s-chain=$PRIVATE_KEY_FOR_SCHAIN
+```
+
+#### Single bidirectional transfer iteration between Main-net and S-chain
+
+Performed with the **--transfer** command line option:
+
+```shell
+ node ./main.js --verbose=9 --expose --colors \
+ --transfer \
+ --url-main-net=$URL_W3_ETHEREUM \
+ --url-s-chain=$URL_W3_S_CHAIN \
+ --id-main-net=Mainnet \
+ --id-s-chain=Bob \
+ --cid-main-net=-4 \
+ --cid-s-chain=-4 \
+ --abi-main-net=../proxy/data/proxyMainnet.json \
+ --abi-s-chain=../proxy/data/proxySchain_Bob.json \
+ --key-main-net=$PRIVATE_KEY_FOR_ETHEREUM \
+ --key-s-chain=$PRIVATE_KEY_FOR_SCHAIN
+```
+
+### Transfer loop parameters
+
+ - --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).
+ - --period - Transfer loop period(seconds).
+ - --node-number=value - S-Chain node number(zero based).
+ - --nodes-count=value - S-Chain nodes count.
+ - --time-framing=value - Specifies period(in seconds) for time framing. Zero means disable time framing.
+ - --time-gap=value - Specifies gap(in seconds) before next time frame.
+
+### S-Chain specific configuration for more then one node S-Chains
+
+The **--node-number** and **--nodes-count** must me used for **IMA** instances running on S-Chain nodes which are part of multi-node S-Chain.
+
+### ERC20 transfer from Main-net account to S-Chain
+
+Performed with the **--m2s-payment** command line option:
+
+```shell
+ node ./main.js --verbose=9 --expose --colors \
+ --m2s-payment \
+ --amount=1 \
+ --url-main-net=$URL_W3_ETHEREUM \
+ --url-s-chain=$URL_W3_S_CHAIN \
+ --id-main-net=Mainnet \
+ --id-s-chain=Bob \
+ --cid-main-net=-4 \
+ --cid-s-chain=-4 \
+ --abi-main-net=../proxy/data/proxyMainnet.json \
+ --abi-s-chain=../proxy/data/proxySchain_Bob.json \
+ --erc20-main-net=...path.../data-mn.json \
+ --erc20-s-chain=...path.../data-sc.json \
+ --key-main-net=$PRIVATE_KEY_FOR_ETHEREUM \
+ --address-s-chain=0x66c5a87f4a49dd75e970055a265e8dd5c3f8f852
+```
+
+Notice: The command above does payment from Main-net and that is why we need to specify private key for source account inside Main-net blockchain using the **--key-main-net** command line argument. Target S-chain account is specified as address with the **--address-s-chain** command line argument. We don't need to specify private key for target account.
+
+### ERC721 transfer from Main-net account to S-Chain
+
+Same as ERC20 above. But use **721** instead of **20** in command names. Also use **--tid** to specify ERC721 token id to send instead of **--amount**.
+
+### ERC1155 transfer from Main-net account to S-Chain
+
+Same as ERC20 above. But use **1155** instead of **20** in command names. Also use **--tid** to specify ERC1155 token id to send withing **--amount** of ERC1155 tokens.
+
+### ERC1155 batch transfer from Main-net account to S-Chain
+
+Similar to ERC1155 single token transfer.Use **--tids** to specify ERC1155 token ids in form of array `[1,2,3]` to send withing **--amounts** as array `[100,200,300]` with number of appropriate ERC1155 tokens to sent.
+
+### ERC20 transfer from S-Chain account to Main-net
+
+Performed with the **--s2m-payment** command line option:
+
+```shell
+ node ./main.js --verbose=9 --expose --colors \
+ --s2m-payment \
+ --amount=1 \
+ --sleep-between-tx=5000 \
+ --url-main-net=$URL_W3_ETHEREUM \
+ --url-s-chain=$URL_W3_S_CHAIN \
+ --id-main-net=Mainnet \
+ --id-s-chain=Bob \
+ --cid-main-net=-4 \
+ --cid-s-chain=-4 \
+ --abi-main-net=../proxy/data/proxyMainnet.json \
+ --abi-s-chain=../proxy/data/proxySchain_Bob.json \
+ --erc20-main-net=...path.../data-mn.json \
+ --erc20-s-chain=...path.../data-sc.json \
+ --address-main-net=$ACCOUNT_FOR_ETHEREUM \
+ --key-s-chain=$PRIVATE_KEY_FOR_SCHAIN
+```
+
+Notice: The command above does payment from Main-net and that is why we need to specify private key for source account inside S-chain blockchain using the **--key-s-chain** command line argument. Target Main-net account is specified as address with the **--address-main-net** command line argument. We don't need to specify private key for target account.
+
+### ERC721 transfer from S-Chain account to Main-net
+
+Same as above. But use **721** instead of **20** in command names. Also use **--tid** to specify ERC721 token id to send instead of **--amount**.
+
+### ERC1155 transfer from S-Chain account to Main-net
+
+Same as ERC20 above. But use **1155** instead of **20** in command names. Also use **--tid** to specify ERC1155 token id to send withing **--amount** of ERC1155 tokens.
+
+### ERC1155 batch transfer from S-Chain account to Main-net
+
+Similar to ERC1155 single token transfer.Use **--tids** to specify ERC1155 token ids in form of array `[1,2,3]` to send withing **--amounts** as array `[100,200,300]` with number of appropriate ERC1155 tokens to sent.
+
+## Other options and commands
+
+### Show asset balances and owners
+
+You can asl **IMA Agent** to show ETH and, optionally, various token balances and/or ERC721 token owners:
+
+```shell
+ node ./main.js --verbose=9 --expose --colors \
+ --show-balance \
+ --tids="[1,2,3] \
+ --url-main-net=$URL_W3_ETHEREUM \
+ --url-s-chain=$URL_W3_S_CHAIN \
+ --id-main-net=Mainnet \
+ --id-s-chain=Bob \
+ --cid-main-net=-4 \
+ --cid-s-chain=-4 \
+ --abi-main-net=../proxy/data/proxyMainnet.json \
+ --abi-s-chain=../proxy/data/proxySchain_Bob.json \
+ --address-main-net=$ACCOUNT_FOR_ETHEREUM \
+ --address-s-chain=$ACCOUNT_FOR_SCHAIN \
+ --erc20-main-net=.....path-to.....ERC20.abi.mn.json \
+ --erc20-s-chain=.....path-to.....ERC20.abi.sc00.json \
+ --erc721-main-net=.....path-to.....ERC721.abi.mn.json \
+ --erc721-s-chain=.....path-to.....ERC721.abi.sc00.json \
+ --erc1155-main-net=.....path-to.....ERC1155.abi.mn.json \
+ --erc1155-s-chain=.....path-to.....ERC1155.abi.sc00.json
+```
+
+Example command above will always show: real ETH on Main Net, ETH user can receive on Main Net, real ETH on S-Chain, stored as ERC20 and local S-Chain ETH.
+
+The token balances and owners display is optional and depends on set of ABI and token IDs (`--tids`) arguments provided.
+
+### Browse S-Chain network
+
+You can ask agent app to scan **S-Chain** network information and parameters, print it and exit:
+
+```shell
+ node ./main.js --verbose=9 --expose --colors \
+ --url-s-chain=$URL_W3_S_CHAIN --browse-s-chain
+```
+
+This information is used to sign messages on all **S-Chain** nodes.
+
+### Sign messages
+
+Message signing performed only for message sent from **S-Chain** to **MainNet**.
+
+Adding **--sign-messages** command line parameter turns on **BLS message signing** algorithm.
+Agent app will scan **S-Chain** network and ask each of nodes to sign messages transferred from **MainNet** to **S-Chain**.
+This options requires all **S-Chain** nodes to be configured with **SGX Wallet** or **Emu Wallet** access information.
+
+The **--bls-glue** command line parameter must be used to specify path to the **bls_glue** application.
+This parameter must be specified if **--sign-messages** parameter is present.
+
+The **--bls-verify** command line parameter must be used to specify path to the **verify_bls** application.
+This parameter is optional. If it was specified, then **IMA Agent** application will verify gathered BLS signatures.
+
+The **--hash-g1** command line parameter must be used to specify path to the **hash_g1** application.
+
+Message signing will work only on **S-Chain** where each **skaled** node configured properly and able to:
+
+ - provide brows information for entire **S-Chain** network
+ - provide **IMA** signing APIs and parameters
+
+Here is example of correct **config.json** file for **skaled** node:
+
+```json
+"skaleConfig": {
+ "nodeInfo": {
+ "nodeName": "???????????", "nodeID": ????,
+ "bindIP": "??.??.??.??, "bindIP6": "???????????", "basePort": ????, "basePort6": ????,
+ "logLevel": "trace", "logLevelProposal": "trace",
+ "emptyBlockIntervalMs": ?????, "ipc": false, "ipcpath": "./ipcx", "db-path": "./node",
+ "httpRpcPort": ????, "httpsRpcPort": ????, "wsRpcPort": ????, "wssRpcPort": ????,
+ "httpRpcPort6": ????, "httpsRpcPort6": ????, "wsRpcPort6": ????, "wssRpcPort6": ????,
+ "acceptors": 1, "max-connections": 0,
+ "web3-trace": true, "enable-debug-behavior-apis": false, "unsafe-transactions": false,
+ "aa": "always", "web3-shutdown": false,
+ "imaMainNet": "????://??.??.??.??:????",
+ "imaMessageProxySChain": "0x????????????????????????????????????????,
+ "imaMessageProxyMainNet": "0x????????????????????????????????????????",
+ "imaCallerAddressSChain": "0x????????????????????????????????????????",
+ "imaCallerAddressMainNet": "0x????????????????????????????????????????",
+ "wallets": {
+ "ima": {
+ "url": ""????://??.??.??.??:????" "keyShareName": "???????????????", "t": 2, "n": 2,
+ "BLSPublicKey??????????????????????????????????????????????????????????????????????????",
+ "BLSPublicKey??????????????????????????????????????????????????????????????????????????",
+ "BLSPublicKey??????????????????????????????????????????????????????????????????????????",
+ "BLSPublicKey??????????????????????????????????????????????????????????????????????????",
+ "commonBLSPublicKey??????????????????????????????????????????????????????????????????????????",
+ "commonBLSPublicKey??????????????????????????????????????????????????????????????????????????",
+ "commonBLSPublicKey??????????????????????????????????????????????????????????????????????????",
+ "commonBLSPublicKey??????????????????????????????????????????????????????????????????????????"
+ }
+ }
+ },
+ "sChain": {
+ "schainID": 1234, "schainName": "????????????????????",
+ "nodes": [ {
+ "schainIndex": 1, "nodeID": ????,
+ "ip": "??.??.??.??", "ip6": "????????????????????", "basePort": ????, "basePort6": ????,
+ "httpRpcPort": ????, "httpsRpcPort": ????, "wsRpcPort": ????, "wssRpcPort": ????,
+ "httpRpcPort6": ????, "httpsRpcPort6": ????, "wsRpcPort6": ????, "wssRpcPort6": ????
+ }, {
+ "schainIndex": 1, "nodeID": ????,
+ "ip": "??.??.??.??", "ip6": "????????????????????", "basePort": ????, "basePort6": ????,
+ "httpRpcPort": ????, "httpsRpcPort": ????, "wsRpcPort": ????, "wssRpcPort": ????,
+ "httpRpcPort6": ????, "httpsRpcPort6": ????, "wsRpcPort6": ????, "wssRpcPort6": ????
+ } ]
+ }
+}
+```
+
+Here is example of IMA message processing loop invocation with BLS support:
+
+```shell
+ reset; node ./main.js --verbose=9 --expose --colors \
+ --loop \
+ --url-main-net=$URL_W3_ETHEREUM \
+ --url-s-chain=$URL_W3_S_CHAIN \
+ --id-main-net=Mainnet \
+ --id-s-chain=Bob \
+ --cid-main-net=-4 \
+ --cid-s-chain=-4 \
+ --abi-main-net=../proxy/data/proxyMainnet.json \
+ --abi-s-chain=../proxy/data/proxySchain_Bob.json \
+ --key-main-net=$PRIVATE_KEY_FOR_ETHEREUM \
+ --key-s-chain=$PRIVATE_KEY_FOR_SCHAIN \
+ --sign-messages \
+ --bls-glue=...path.../bls_glue \
+ --hash-g1=...path.../hash_g1 \
+ --bls-verify=...path.../verify_bls
+
+ reset; node ./main.js --verbose=9 --expose --colors \
+ --loop \
+ --url-main-net=$URL_W3_ETHEREUM \
+ --url-s-chain=$URL_W3_S_CHAIN \
+ --id-main-net=Mainnet \
+ --id-s-chain=Bob \
+ --cid-main-net=-4 \
+ --cid-s-chain=-4 \
+ --abi-main-net=../proxy/data/proxyMainnet.json \
+ --abi-s-chain=../proxy/data/proxySchain_Bob.json \
+ --key-main-net=$PRIVATE_KEY_FOR_ETHEREUM \
+ --key-s-chain=$PRIVATE_KEY_FOR_SCHAIN \
+ --sign-messages \
+ --bls-glue=/home/serge/Work/skaled/build/libconsensus/libBLS/bls_glue \
+ --hash-g1=/home/serge/Work/skaled/build/libconsensus/libBLS/hash_g1 \
+ --bls-verify=/home/serge/Work/skaled/build/libconsensus/libBLS/verify_bls
+```
+
+### Gas computation and transaction customization
+
+All transactions performed by **IMA Agent** use estimated gas and dry-run. You may multiply estimated gas by `2` in sent transactions by specifying `--gas-multiplier=2` command line option. Per-chain independent options `--gas-multiplier-mn` and `gas-multiplier-sc` allow to use different gas multipliers. Similar command line options are provided to multiply gas price returned by blockchain: `--gas-price-multiplier`, `--gas-price-multiplier-mn` and `--gas-price-multiplier-sc`.
diff --git a/src/SNB.md b/src/SNB.md
new file mode 100644
index 00000000..a4b24fec
--- /dev/null
+++ b/src/SNB.md
@@ -0,0 +1,177 @@
+# SKALE NETWORK BROWSER
+
+## General Description
+
+**SKALE Network Browser** (**SNB**) or **SKALE Observer** is part of IMA responsible for providing description of all SKALE chains. This is done via set of calls to **SKALE Manager**.
+
+**SNB** maintains cache of S-Chain descriptions and refreshes all descriptions periodically. These descriptions and needed for S-Chain to S-Chain IMA message transfers when IMA needs to know how to connect to other S-Chain.
+
+First SKALE network scan is performed by **SNB** on IMA startup. Next network description refreshes are performed periodically.
+
+**SNB** works can work in parallel thread to avoid any delay of main IMA's message transfer loop.
+
+## Implementation details
+
+The `SchainsInternal.numberOfSchains` contract call returns number of created S-Chains to load from **SKALE Manager**. 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 address, domain name and, base port of a node, maintenance 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.
+
+S-Chain descriptions directly affect on S-Chain to S-Chain transfers because they contain JSON RPC URLs of all `skaled`s of all S-Chains.
+
+**SNB** can be invoked from command line of IMA agent using `--browse-skale-network` or `--browse-connected-schains` command line options. The`--browse-skale-network` command line options invokes download entire SKALE network description, all S-Chains, all `skaled` nodes. The `--browse-connected-schains` command line options invokes download of which are S-Chains connected to S-Chain with name specified in `--id-s-chain` command line parameter.
+
+Example of **SNB** invocation:
+
+```shell
+node agent/main.js --colors --browse-skale-network \
+ --abi-skale-manager=.../sm.json \
+ --abi-main-net=.../mn.json \
+ --abi-s-chain=.../sc.json \
+ --url-main-net=http://127.0.0.1:8545 \
+ --url-s-chain=http://127.0.0.1:15000 \
+ --id-main-net=Mainnet \
+ --id-s-chain=Bob1000 \
+ --cid-main-net=456 \
+ --cid-s-chain=1000 \
+ --key-main-net=... \
+ --key-s-chain=...
+```
+
+Example of downloaded S-Chains description containing 2 S-Chains named `Bob1000` and `Bob1001`, 2 `skaled` nodes each:
+
+```json
+[
+ {
+ "data":{
+ "name":"Bob1000",
+ "owner":"0x7aa5E36AA15E93D10F4F26357C30F052DacDde5F",
+ "indexInOwnerList":"0",
+ "partOfNode":"0",
+ "lifetime":"5",
+ "startDate":"1641992759",
+ "startBlock":"249",
+ "deposit":"100000000000000000000",
+ "index":"0",
+ "generation":"0",
+ "originator":"0x0000000000000000000000000000000000000000",
+ "computed":{
+ "ports":{
+ "httpRpcPort":2264,
+ "httpsRpcPort":2269,
+ "wsRpcPort":2263,
+ "wssRpcPort":2268,
+ "infoHttpRpcPort":2270
+ },
+ "schain_id":"0x975a4814cff8b9fd85b48879dade195028650b0a23f339ca81bd3b1231f72974",
+ "chainId":"0x975a4814cff8b9",
+ "nodes":[
+ {
+ "id":"0",
+ "name":"Aldo",
+ "ip":"127.0.0.1",
+ "base_port":"2161",
+ "domain":"test.domain.name.here",
+ "isMaintenance":false,
+ "schain_base_port":2161,
+ "http_endpoint_ip":"http://127.0.0.1:2164",
+ "https_endpoint_ip":"https://127.0.0.1:2169",
+ "ws_endpoint_ip":"ws://127.0.0.1:2163",
+ "wss_endpoint_ip":"wss://127.0.0.1:2168",
+ "info_http_endpoint_ip":"http://127.0.0.1:2170",
+ "http_endpoint_domain":"http://test.domain.name.here:2164",
+ "https_endpoint_domain":"https://test.domain.name.here:2169",
+ "ws_endpoint_domain":"ws://test.domain.name.here:2163",
+ "wss_endpoint_domain":"wss://test.domain.name.here:2168",
+ "info_http_endpoint_domain":"http://test.domain.name.here:2170"
+ },
+ {
+ "id":"1",
+ "name":"Bear",
+ "ip":"127.0.0.2",
+ "base_port":"2261",
+ "domain":"test.domain.name.here",
+ "isMaintenance":false,
+ "schain_base_port":2261,
+ "http_endpoint_ip":"http://127.0.0.2:2264",
+ "https_endpoint_ip":"https://127.0.0.2:2269",
+ "ws_endpoint_ip":"ws://127.0.0.2:2263",
+ "wss_endpoint_ip":"wss://127.0.0.2:2268",
+ "info_http_endpoint_ip":"http://127.0.0.2:2270",
+ "http_endpoint_domain":"http://test.domain.name.here:2264",
+ "https_endpoint_domain":"https://test.domain.name.here:2269",
+ "ws_endpoint_domain":"ws://test.domain.name.here:2263",
+ "wss_endpoint_domain":"wss://test.domain.name.here:2268",
+ "info_http_endpoint_domain":"http://test.domain.name.here:2270"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "data":{
+ "name":"Bob1001",
+ "owner":"0x7aa5E36AA15E93D10F4F26357C30F052DacDde5F",
+ "indexInOwnerList":"1",
+ "partOfNode":"0",
+ "lifetime":"5",
+ "startDate":"1641992767",
+ "startBlock":"260",
+ "deposit":"100000000000000000000",
+ "index":"1",
+ "generation":"0",
+ "originator":"0x0000000000000000000000000000000000000000",
+ "computed":{
+ "ports":{
+ "httpRpcPort":2364,
+ "httpsRpcPort":2369,
+ "wsRpcPort":2363,
+ "wssRpcPort":2368,
+ "infoHttpRpcPort":2370
+ },
+ "schain_id":"0xde9b5e1c7bac0a60f917397dfab6ead3f6441acf0399ec81145568874dd829e9",
+ "chainId":"0xde9b5e1c7bac0a",
+ "nodes":[
+ {
+ "id":"3",
+ "name":"Seed",
+ "ip":"127.0.0.4",
+ "base_port":"2461",
+ "domain":"test.domain.name.here",
+ "isMaintenance":false,
+ "schain_base_port":2461,
+ "http_endpoint_ip":"http://127.0.0.4:2464",
+ "https_endpoint_ip":"https://127.0.0.4:2469",
+ "ws_endpoint_ip":"ws://127.0.0.4:2463",
+ "wss_endpoint_ip":"wss://127.0.0.4:2468",
+ "info_http_endpoint_ip":"http://127.0.0.4:2470",
+ "http_endpoint_domain":"http://test.domain.name.here:2464",
+ "https_endpoint_domain":"https://test.domain.name.here:2469",
+ "ws_endpoint_domain":"ws://test.domain.name.here:2463",
+ "wss_endpoint_domain":"wss://test.domain.name.here:2468",
+ "info_http_endpoint_domain":"http://test.domain.name.here:2470"
+ },
+ {
+ "id":"2",
+ "name":"John",
+ "ip":"127.0.0.3",
+ "base_port":"2361",
+ "domain":"test.domain.name.here",
+ "isMaintenance":false,
+ "schain_base_port":2361,
+ "http_endpoint_ip":"http://127.0.0.3:2364",
+ "https_endpoint_ip":"https://127.0.0.3:2369",
+ "ws_endpoint_ip":"ws://127.0.0.3:2363",
+ "wss_endpoint_ip":"wss://127.0.0.3:2368",
+ "info_http_endpoint_ip":"http://127.0.0.3:2370",
+ "http_endpoint_domain":"http://test.domain.name.here:2364",
+ "https_endpoint_domain":"https://test.domain.name.here:2369",
+ "ws_endpoint_domain":"ws://test.domain.name.here:2363",
+ "wss_endpoint_domain":"wss://test.domain.name.here:2368",
+ "info_http_endpoint_domain":"http://test.domain.name.here:2370"
+ }
+ ]
+ }
+ }
+ }
+]
+```
diff --git a/src/about.txt b/src/about.txt
new file mode 100644
index 00000000..1dc79a22
--- /dev/null
+++ b/src/about.txt
@@ -0,0 +1,214 @@
+IMA AGENT version 2.1.0
+GENERAL options:
+ --help..................................Show this help info and exit.
+ --version...............................Show version info and exit.
+ --colors................................Use ANSI-colorized logging.
+ --no-colors.............................Use monochrome logging.
+BLOCKCHAIN NETWORK options:
+ --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 not specified.
+ --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_TARGET 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.
+BLOCKCHAIN INTERFACE options:
+ --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.
+ERC20 INTERFACE 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 INTERFACE 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 INTERFACE 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.
+USER ACCOUNT options:
+ --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.
+ --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.
+ --sgx-url=URL...........................SGX server URL for all chains.
+ --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-ecdsa-key=name....................SGX/ECDSA key name for all chains.
+ --sgx-bls-key-main-net=name.............SGX/BLS key name for Main-net Value is automatically loaded from the BLS_KEY_ETHEREUM environment variable if not specified.
+ --sgx-bls-key-s-chain=name..............SGX/BLS key name for S-chain Value is automatically loaded from the BLS_KEY_S_CHAIN environment variable if not specified.
+ --sgx-bls-key-t-chain=name..............SGX/BLS key name for S<->S Target S-chain Value is automatically loaded from the BLS_KEY_S_CHAIN_TARGET environment variable if not specified.
+ --sgx-bls-key=name......................SGX/BLS key name for all chains.
+ --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-key=path......................Path to SSL key file for SGX wallet of all chains.
+ --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.
+ --sgx-ssl-cert=path.....................Path to SSL certificate file for all chains.
+ --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.
+ --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.
+GENERAL TRANSFER 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.
+ --amount=number.........................Amount of tokens to transfer.
+ --tid=number............................ERC721 or ERC1155 to ken id to transfer.
+ --amounts=array of numbers..............ERC1155 to ken id to transfer in batch.
+ --tids=array of numbers.................ERC1155 to ken 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.
+S-CHAIN TO S-CHAIN TRANSFER options:
+ --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.
+ --network-browser-path=path.............Path to SKALE NETWORK browse data file in JSON format containing descriptions of S-Chains connected to this S-Chain.
+PAYMENT TRANSACTION options:
+ --gas-price-multiplier-mn ..............Sets Gas Price Multiplier for Main Net transactions, Default value is 1.25. Specify value 0.0 to disable Gas Price Customization for Main Net.
+ --gas-price-multiplier-sc ..............Sets Gas Price Multiplier for S-Chain transactions, Default value is 0.0.
+ --gas-price-multiplier-tc ..............Sets Gas Price Multiplier for S<->S Target S-Chain transactions, Default value is 0.0.
+ --gas-price-multiplier .................Sets Gas Price Multiplier for both Main Net and S-Chain(s).
+ --gas-multiplier-mn ....................Sets Gas Value Multiplier for Main Net transactions, Default value is 1.25. Specify value 0.0 to disable Gas Price Customization for Main Net.
+ --gas-multiplier-sc ....................Sets Gas Value Multiplier S-Chain transactions, Default value is 1.25.
+ --gas-multiplier-tc ....................Sets Gas Value Multiplier for S<->S Target S-Chain transactions, Default value is 1.25.
+ --gas-multiplier .......................Sets Gas Value Multiplier for both Main Net and S-Chain(s).
+REGISTRATION commands:
+ --register .............................Register(perform all steps).
+ --register1 ............................Perform registration step 1 - register S-Chain on Main-net.
+ --check-registration ...................Perform registration status check(perform all steps).
+ --check-registration1 ..................Perform registration status check step 1 - register S-Chain on Main-net.
+ --check-registration2 ..................Perform registration status check step 2 - register S-Chain in deposit box.
+ --check-registration3 ..................Perform registration status check step 3 - register Main-net's deposit box on S-Chain.
+ACTION commands:
+ --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 message transfer loop from Main-net to S-chain.
+ --s2m-transfer .........................Do single message transfer loop from S-chain to Main-net.
+ --s2s-transfer .........................Do single message 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 loops in parallel threads.
+ --simple-loop ..........................Run M<->S and, optionally, S->S transfer loops in main thread only.
+ADDITIONAL ACTION options:
+ --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 invocation before payed contract method calls.
+ --no-ignore-dry-run ....................Use error results of dry run contract method calls as actual errors and stop execute.
+ --m2s-transfer-block-size=value.........Number of transactions in one block to use in message transfer loop from Main-net to S-chain. Default is 4.
+ --s2m-transfer-block-size=value.........Number of transactions in one block to use in message transfer loop from S-chain to Main-net. Default is 4.
+ --s2s-transfer-block-size=value.........Number of transactions in one block to use in message transfer loop from S-chain to S-chain. Default is 4.
+ --transfer-block-size=value.............Number of transactions in one block to use in all message transfer loops.
+ --m2s-transfer-steps=value..............Maximal number of blocks to transfer at a job run from Main-net to S-chain. Value 0 is unlimited. Default is 8.
+ --s2m-transfer-steps=value..............Maximal number of blocks to transfer at a job run from S-chain to Main-net. Value 0 is unlimited. Default is 8.
+ --s2s-transfer-steps=value..............Maximal number of blocks to transfer at a job run from S-chain to S-chain. Value 0 is unlimited. Default is 8.
+ --transfer-steps=value..................Maximal number of blocks to transfer at a job run in all transfer loops. Value 0 is unlimited.
+ --m2s-max-transactions=number...........Maximal number of transactions to do in message transfer loop from Main-net to S-chain(0 is unlimited). Default is 0.
+ --s2m-max-transactions=number...........Maximal number of transactions to do in message transfer loop from S-chain to Main-net(0 is unlimited). Default is 0.
+ --s2s-max-transactions=number...........Maximal number of transactions to do in message transfer loop from S-chain to S-chain(0 is unlimited). Default is 0.
+ --max-transactions=number...............Maximal number of transactions to do in all message transfer loops(0 is unlimited).
+ --m2s-await-blocks=number...............Maximal number of blocks to wait to appear in blockchain before transaction from Main-net to S-chain(0 is no wait). Default is 0.
+ --s2m-await-blocks=number...............Maximal number of blocks to wait to appear in blockchain before transaction from S-chain to Main-net(0 is no wait). Default is 0.
+ --s2s-await-blocks=number...............Maximal number of blocks to wait to appear in blockchain before transaction from S-chain to S-chain(0 is no wait). Default is 0.
+ --await-blocks=number...................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=seconds................Minimal age of transaction message(in seconds) before it will be transferred from Main-net to S-chain(0 is no wait). Default is 0.
+ --s2m-await-time=seconds................Minimal age of transaction message(in seconds) before it will be transferred from S-chain to Main-net(0 is no wait). Default is 0.
+ --s2s-await-time=seconds................Minimal age of transaction message(in seconds) before it will be transferred from S-chain to S-chain(0 is no wait). Default is 0.
+ --await-time=seconds....................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.
+ --auto-exit=seconds.....................Automatically exit IMA Agent after specified number of seconds(0 is no automatic exit, 3600 is no default).
+TOKEN TESTING commands:
+ --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.
+IMA WORK STATE ANALYSIS options:
+ --pwa ..................................Enable pending work analysis to avoid transaction conflicts. Default mode.
+ --no-pwa ...............................Disable pending work analysis. Not recommended for slow and overloaded blockchains.
+ --pwa-timeout=seconds...................Node state timeout during pending work analysis. Default is 60 seconds.
+MESSAGE SIGNING options:
+ --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.
+MONITORING options:
+ --monitoring-port=number................Run monitoring web socket RPC server on specified port. Specify 0 to disable. By default monitoring server is disabled.
+ --monitoring-log........................Enable logging on monitoring web socket RPC server. By default these log messages are disabled.
+GAS REIMBURSEMENT 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.
+PAST EVENTS SCAN 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.
+ORACLE BASED GAS REIMBURSEMENT options:
+ --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 JSON RPC SERVER options:
+ --json-rpc-port=number..................Run IMA JSON RPC server on specified port. Specify 0 to disable. Default is 0.
+ --cross-ima ............................Enable calls to IMA JSON RPC servers to compute BLS signature parts and operation state inside time frames. Use calls to IMA Agent.
+ --no-cross-ima .........................Disable calls to IMA JSON RPC servers to compute BLS signature parts and operation state inside time frames. Use calls to skaled. Default mode.
+TEST options:
+ --browse-s-chain .......................Download own S-Chain's network information.
+LOGGING 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.
+ --expose-pwa ...........................Expose IMA agent pending work analysis information
+ --no-expose-pwa ........................Do not expose IMA agent pending work analysis information. Default mode.
+ --accumulated-log-in-transfer ..........Use accumulated log in message transfer loop.
+ --accumulated-log-in-bls-signer ........Use accumulated log in BLS signer.
+ --dynamic-log-in-transfer ..............Use realtime log in message transfer loop.
+ --dynamic-log-in-bls-signer ............Use realtime log in BLS signer.
diff --git a/src/bls.ts b/src/bls.ts
new file mode 100644
index 00000000..4d7be1b7
--- /dev/null
+++ b/src/bls.ts
@@ -0,0 +1,2453 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+
+/**
+ * @license
+ * SKALE IMA
+ *
+ * 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 .
+ */
+
+/**
+ * @file bls.ts
+ * @copyright SKALE Labs 2019-Present
+ */
+
+import * as fs from "fs";
+import * as path from "path";
+import * as log from "./log.js";
+import * as owaspUtils from "./owaspUtils.js";
+import * as childProcessModule from "child_process";
+import * as rpcCall from "./rpcCall.js";
+import * as shellMod from "shelljs";
+import * as imaUtils from "./utils.js";
+import * as sha3Module from "sha3";
+import * as skaleObserver from "./observer.js";
+import * as discoveryTools from "./discoveryTools.js";
+import * as threadInfo from "./threadInfo.js";
+import * as utils from "./socketUtils.js";
+import * as state from "./state.js";
+import type * as loop from "./loop.js";
+import type * as IMA from "./imaCore.js";
+
+export interface TQAInformation {
+ skaledNumber: number
+ sequenceId: string
+ ts: string
+}
+
+export interface TSignResult {
+ errorMessage?: string | null
+ signatureShare?: string
+ status?: number
+ error?: string | null
+}
+
+export interface TGatheringTracker {
+ nCountReceivedPrevious: number
+ nCountReceived: number
+ nCountErrors: number
+ nCountSkipped: number
+ nWaitIntervalStepMilliseconds: number
+ nWaitIntervalMaxSteps: number
+}
+
+export interface TSignOperationOptions {
+ imaState: state.TIMAState
+ nTransferLoopCounter: number
+ strDirection: string
+ jarrMessages: any[]
+ nIdxCurrentMsgBlockStart: number
+ strFromChainName: string
+ joExtraSignOpts?: loop.TExtraSignOpts | null
+ fn: IMA.TFunctionAfterSigningMessages
+ bHaveResultReportCalled: boolean
+ strLogPrefix: string
+ strLogPrefixA: string
+ strLogPrefixB: string
+ joGatheringTracker: TGatheringTracker
+ arrSignResults: any[]
+ details: log.TLogger
+ strGatheredDetailsName: string
+ sequenceId: string
+ jarrNodes: discoveryTools.TSChainNode[]
+ nThreshold: number
+ nParticipants: number
+ nCountOfBlsPartsToCollect: number
+ errGathering: Error | string | null
+ targetChainName: string
+ fromChainName: string
+ targetChainID: string | number
+ fromChainID: string | number
+}
+
+export interface TBSU256CallData {
+ params: {
+ valueToSign: string
+ qa?: TQAInformation
+ }
+}
+
+export interface TBSU256Options {
+ joCallData: TBSU256CallData
+ imaState: state.TIMAState
+ strLogPrefix: string
+ details: log.TLogger
+ joRetVal: any | null
+ isSuccess: boolean
+ nThreshold: number
+ nParticipants: number
+ u256: any | null
+ strMessageHash: string
+ joAccount: state.TAccount | null
+ qa?: TQAInformation
+}
+
+export interface THandleVerifyAndSignCallDataParams {
+ startMessageIdx: number
+ srcChainName: string
+ dstChainName: string
+ srcChainID: string
+ dstChainID: string
+ direction: string
+ messages: any[]
+ qa?: TQAInformation
+}
+
+export interface THandleVerifyAndSignCallData {
+ params: THandleVerifyAndSignCallDataParams
+}
+
+export interface THandleVerifyAndSignOptions {
+ joCallData: THandleVerifyAndSignCallData
+ imaState: state.TIMAState
+ strLogPrefix: string
+ details: log.TLogger
+ joRetVal: any
+ isSuccess: boolean
+ nIdxCurrentMsgBlockStart: number
+ strFromChainName: string
+ strToChainName: string
+ strFromChainID: string
+ strToChainID: string
+ strDirection: string
+ jarrMessages: any[]
+ strMessageHash: string
+ joExtraSignOpts: loop.TExtraSignOpts | null
+ nThreshold: number
+ nParticipants: number
+}
+
+export interface TSignU256Options {
+ u256: any
+ fn: IMA.TFunctionAfterSigningMessages
+ details: log.TLogger
+ imaState: state.TIMAState
+ strLogPrefix: string
+ joGatheringTracker: {
+ nCountReceivedPrevious: number
+ nCountReceived: number
+ nCountErrors: number
+ nCountSkipped: number
+ nWaitIntervalStepMilliseconds: number
+ nWaitIntervalMaxSteps: number // 10 is 1 second
+ }
+ arrSignResults: any[]
+ jarrNodes: discoveryTools.TSChainNode[]
+ nThreshold: number
+ nParticipants: number
+ nCountOfBlsPartsToCollect: number
+ errGathering: Error | string | null
+}
+
+const anyShellMod: any = shellMod as any;
+const shell = anyShellMod.default;
+
+const Keccak = sha3Module.Keccak;
+
+function discoverBlsThreshold( joSChainNetworkInfo: discoveryTools.TSChainNetworkInfo ): number {
+ const imaState: state.TIMAState = state.get();
+ joSChainNetworkInfo = joSChainNetworkInfo || imaState.joSChainNetworkInfo;
+ if( !joSChainNetworkInfo )
+ return -1;
+ const jarrNodes = joSChainNetworkInfo.network;
+ for( let i = 0; i < jarrNodes.length; ++i ) {
+ const joNode = jarrNodes[i];
+ if( discoveryTools.isSChainNodeFullyDiscovered( joNode ) )
+ return joNode.imaInfo.t;
+ }
+ return -1;
+}
+
+function discoverBlsParticipants( joSChainNetworkInfo: discoveryTools.TSChainNetworkInfo ): number {
+ const imaState: state.TIMAState = state.get();
+ joSChainNetworkInfo = joSChainNetworkInfo || imaState.joSChainNetworkInfo;
+ if( !joSChainNetworkInfo )
+ return -1;
+ const jarrNodes = joSChainNetworkInfo.network;
+ for( let i = 0; i < jarrNodes.length; ++i ) {
+ const joNode = jarrNodes[i];
+ if( discoveryTools.isSChainNodeFullyDiscovered( joNode ) )
+ return joNode.imaInfo.n;
+ }
+ return -1;
+}
+
+function checkBlsThresholdAndBlsParticipants(
+ nThreshold: number, nParticipants: number, strOperation: string, details: log.TLogger
+): boolean {
+ details = details || log;
+ if( nThreshold <= 0 ) {
+ details.fatal( "Operation {} will fail because discovered BLS threshold {}" +
+ " is invalid number or bad value", strOperation, nThreshold );
+ return false;
+ }
+ if( nParticipants <= 0 ) {
+ details.fatal( "Operation {} will fail because discovered BLS number of participants {}" +
+ " is invalid number or bad value", strOperation, nParticipants );
+ return false;
+ }
+ if( nThreshold > nParticipants ) {
+ details.fatal( "Operation {} will fail because discovered BLS threshold {} is greater " +
+ "than BLS number of participants {}", strOperation, nThreshold, nParticipants );
+ return false;
+ }
+ return true;
+}
+
+function discoverPublicKeyByIndex(
+ nNodeIndex: number, joSChainNetworkInfo: discoveryTools.TSChainNetworkInfo,
+ details: log.TLogger, isThrowException: boolean
+): discoveryTools.TBLSPublicKey | null {
+ details = details || log;
+ const imaState: state.TIMAState = state.get();
+ joSChainNetworkInfo = joSChainNetworkInfo || imaState.joSChainNetworkInfo;
+ const jarrNodes = joSChainNetworkInfo.network;
+ const cntNodes = jarrNodes.length;
+ const joNode = jarrNodes[nNodeIndex];
+ if( discoveryTools.isSChainNodeFullyDiscovered( joNode ) ) {
+ return {
+ BLSPublicKey0: joNode.imaInfo.BLSPublicKey0,
+ BLSPublicKey1: joNode.imaInfo.BLSPublicKey1,
+ BLSPublicKey2: joNode.imaInfo.BLSPublicKey2,
+ BLSPublicKey3: joNode.imaInfo.BLSPublicKey3
+ };
+ }
+ details.fatal( "BLS 1/{} public key discovery failed for node #{}, node data is: {}",
+ cntNodes, nNodeIndex, joNode );
+ if( isThrowException )
+ throw new Error( `BLS 1/${cntNodes} public key discovery failed for node #${nNodeIndex}` );
+ return null;
+}
+
+function discoverCommonPublicKey(
+ details: log.TLogger, joSChainNetworkInfo: discoveryTools.TSChainNetworkInfo,
+ isThrowException: boolean ): discoveryTools.TBLSCommonPublicKey | null {
+ const imaState: state.TIMAState = state.get();
+ joSChainNetworkInfo = joSChainNetworkInfo || imaState.joSChainNetworkInfo;
+ const jarrNodes = joSChainNetworkInfo.network;
+ for( let i = 0; i < jarrNodes.length; ++i ) {
+ const joNode = jarrNodes[i];
+ if( discoveryTools.isSChainNodeFullyDiscovered( joNode ) ) {
+ return {
+ commonBLSPublicKey0: joNode.imaInfo.commonBLSPublicKey0,
+ commonBLSPublicKey1: joNode.imaInfo.commonBLSPublicKey1,
+ commonBLSPublicKey2: joNode.imaInfo.commonBLSPublicKey2,
+ commonBLSPublicKey3: joNode.imaInfo.commonBLSPublicKey3
+ };
+ }
+ }
+ details.fatal( "BLS common public key discovery failed, chain data is: {}",
+ joSChainNetworkInfo );
+ if( isThrowException )
+ throw new Error( "BLS common public key discovery failed" );
+ return null;
+}
+
+function hexPrepare(
+ strHex: string, isInvertBefore: boolean, isInvertAfter: boolean
+): Uint8Array {
+ if( isInvertBefore == undefined )
+ isInvertBefore = true;
+ if( isInvertAfter == undefined )
+ isInvertAfter = true;
+ let arrBytes = imaUtils.hexToBytes( strHex );
+ if( isInvertBefore )
+ arrBytes = arrBytes.reverse();
+ arrBytes = imaUtils.bytesAlignLeftWithZeroes( arrBytes, 32 );
+ if( isInvertAfter )
+ arrBytes = arrBytes.reverse();
+ return arrBytes;
+}
+
+function stringToKeccak256( s: string ): Uint8Array {
+ const strU256 = owaspUtils.ethersMod.ethers.utils.id( s );
+ return hexPrepare( strU256, true, true );
+}
+
+function arrayToKeccak256( arrBytes: Uint8Array ): Uint8Array {
+ const k = new Keccak( 256 );
+ k.update( imaUtils.toBuffer( arrBytes ) );
+ const h = k.digest( "hex" );
+ return imaUtils.hexToBytes( "0x" + h );
+}
+
+function keccak256Message(
+ jarrMessages: any[], nIdxCurrentMsgBlockStart: number, strFromChainName: string
+): string {
+ let arrBytes = stringToKeccak256( strFromChainName );
+ arrBytes = imaUtils.bytesConcat(
+ arrBytes,
+ hexPrepare(
+ owaspUtils.ensureStartsWith0x( nIdxCurrentMsgBlockStart.toString( 16 ) ),
+ false, false )
+ );
+ arrBytes = arrayToKeccak256( arrBytes );
+ const cnt = jarrMessages.length;
+ for( let i = 0; i < cnt; ++i ) {
+ const joMessage = jarrMessages[i];
+ let bytesSender = imaUtils.hexToBytes( joMessage.sender.toString() );
+ bytesSender = imaUtils.bytesAlignLeftWithZeroes( bytesSender, 32 );
+ arrBytes = imaUtils.bytesConcat( arrBytes, bytesSender );
+ let bytesDestinationContract = imaUtils.hexToBytes( joMessage.destinationContract );
+ bytesDestinationContract =
+ imaUtils.bytesAlignLeftWithZeroes( bytesDestinationContract, 32 );
+ arrBytes = imaUtils.bytesConcat( arrBytes, bytesDestinationContract );
+ const bytesData = imaUtils.hexToBytes( joMessage.data );
+ arrBytes = imaUtils.bytesConcat( arrBytes, bytesData );
+ arrBytes = arrayToKeccak256( arrBytes );
+ }
+ return owaspUtils.ensureStartsWith0x( imaUtils.bytesToHex( arrBytes, false ) );
+}
+
+export function keccak256U256( u256: any, isHash: boolean ): string {
+ let arrBytes = new Uint8Array();
+ let bytesU256 = imaUtils.hexToBytes( u256 );
+ bytesU256 = bytesU256.reverse();
+ bytesU256 = imaUtils.bytesAlignLeftWithZeroes( bytesU256, 32 );
+ bytesU256 = bytesU256.reverse();
+ arrBytes = imaUtils.bytesConcat( arrBytes, bytesU256 );
+ let strMessageHash = "";
+ if( isHash ) {
+ const hash = new Keccak( 256 );
+ hash.update( imaUtils.toBuffer( arrBytes ) );
+ strMessageHash = hash.digest( "hex" );
+ } else
+ strMessageHash = "0x" + imaUtils.bytesToHex( arrBytes );
+ return strMessageHash;
+}
+
+export function keccak256ForPendingWorkAnalysis(
+ nNodeNumber: number, strLoopWorkType: string, isStart: boolean, ts: any
+): string {
+ let arrBytes = new Uint8Array();
+
+ let bytesU256 = imaUtils.hexToBytes( nNodeNumber );
+ bytesU256 = bytesU256.reverse();
+ bytesU256 = imaUtils.bytesAlignLeftWithZeroes( bytesU256, 32 );
+ bytesU256 = bytesU256.reverse();
+ arrBytes = imaUtils.bytesConcat( arrBytes, bytesU256 );
+
+ arrBytes = imaUtils.bytesConcat( arrBytes, stringToKeccak256( strLoopWorkType ) );
+
+ bytesU256 = imaUtils.hexToBytes( isStart ? 1 : 0 );
+ bytesU256 = bytesU256.reverse();
+ bytesU256 = imaUtils.bytesAlignLeftWithZeroes( bytesU256, 32 );
+ bytesU256 = bytesU256.reverse();
+ arrBytes = imaUtils.bytesConcat( arrBytes, bytesU256 );
+
+ bytesU256 = imaUtils.hexToBytes( ts );
+ bytesU256 = bytesU256.reverse();
+ bytesU256 = imaUtils.bytesAlignLeftWithZeroes( bytesU256, 32 );
+ bytesU256 = bytesU256.reverse();
+ arrBytes = imaUtils.bytesConcat( arrBytes, bytesU256 );
+
+ const hash = new Keccak( 256 );
+ hash.update( imaUtils.toBuffer( arrBytes ) );
+ const strMessageHash = hash.digest( "hex" );
+ return strMessageHash;
+}
+
+function splitSignatureShare( signatureShare: any ): any {
+ const jarr = signatureShare.split( ":" );
+ if( jarr.length < 2 )
+ throw new Error( `Failed to split signatureShare=${signatureShare.toString()}` );
+ return { X: jarr[0], Y: jarr[1] };
+}
+
+function getBlsGlueTmpDir(): string {
+ const strTmpDir = "/tmp/ima-bls-glue";
+ shell.mkdir( "-p", strTmpDir );
+ return strTmpDir;
+}
+
+function allocBlsTmpActionDir(): string {
+ const strActionDir = getBlsGlueTmpDir() + "/" + imaUtils.replaceAll( imaUtils.uuid(), "-", "" );
+ if( !fs.existsSync( strActionDir ) )
+ fs.mkdirSync( strActionDir, { recursive: true } );
+ return strActionDir;
+}
+
+function performBlsGlue(
+ details: log.TLogger, strDirection: string, jarrMessages: any[],
+ nIdxCurrentMsgBlockStart: number, strFromChainName: string, arrSignResults: any[]
+): any {
+ const imaState: state.TIMAState = state.get();
+ if( !imaState.joSChainNetworkInfo )
+ throw new Error( "No own S-Chain network information" );
+ const strLogPrefix = `${strDirection}/BLS/Glue: `;
+ let joGlueResult: any = null;
+ const nThreshold = discoverBlsThreshold( imaState.joSChainNetworkInfo );
+ const nParticipants = discoverBlsParticipants( imaState.joSChainNetworkInfo );
+ details.debug( "{p}Discovered BLS threshold is {}.", strLogPrefix, nThreshold );
+ details.debug( "{p}Discovered number of BLS participants is {}.", strLogPrefix, nParticipants );
+ if( !checkBlsThresholdAndBlsParticipants( nThreshold, nParticipants, "BLS glue", details ) )
+ return null;
+ const strMessageHash = owaspUtils.removeStarting0x(
+ keccak256Message( jarrMessages, nIdxCurrentMsgBlockStart, strFromChainName ) );
+ details.debug( "{p}Message hash to sign is {}", strLogPrefix, strMessageHash );
+ const strActionDir = allocBlsTmpActionDir();
+ details.trace( "{p}{sunny} will work in {} director with {} sign results...",
+ strLogPrefix, "performBlsGlue", strActionDir, arrSignResults.length );
+ const fnShellRestore = function(): void { shell.rm( "-rf", strActionDir ); };
+ const strOutput = "";
+ try {
+ let strInput = "";
+ const cnt = arrSignResults.length;
+ for( let i = 0; i < cnt; ++i ) {
+ const jo: any = arrSignResults[i];
+ if( !jo )
+ throw new Error( `Failed to save BLS part ${i} because it's not JSON object` );
+ const strPath = strActionDir + "/sign-result" + jo.index + ".json";
+ details.trace( "{p}Saving {} file containing {}", strLogPrefix, strPath, jo );
+ imaUtils.jsonFileSave( strPath, jo );
+ strInput += " --input " + strPath;
+ }
+ const strGlueCommand =
+ imaState.strPathBlsGlue +
+ " --t " + nThreshold +
+ " --n " + nParticipants +
+ strInput +
+ " --output " + strActionDir + "/glue-result.json";
+ details.trace( "{p}Will execute BLS glue command: {}", strLogPrefix, strGlueCommand );
+ let strOutput = childProcessModule.execSync( strGlueCommand, { cwd: strActionDir } );
+ details.trace( "{p}BLS glue output is:\n{raw}", strLogPrefix, strOutput || "<>" );
+ joGlueResult = imaUtils.jsonFileLoad( path.join( strActionDir, "glue-result.json" ) );
+ details.trace( "{p}BLS glue result is: {}", strLogPrefix, joGlueResult );
+ if( joGlueResult && "X" in joGlueResult.signature && "Y" in joGlueResult.signature ) {
+ details.success( "{p}BLS glue success", strLogPrefix );
+ joGlueResult.hashSrc = strMessageHash;
+ details.trace( "{p}Computing G1 hash point...", strLogPrefix );
+ const strPath = strActionDir + "/hash.json";
+ details.trace( "{p}Saving {} file...", strLogPrefix, strPath );
+ imaUtils.jsonFileSave( strPath, { message: strMessageHash } );
+ const strHasG1Command =
+ imaState.strPathHashG1 +
+ " --t " + nThreshold +
+ " --n " + nParticipants;
+ details.trace( "{p}Will execute HashG1 command {}", strLogPrefix, strHasG1Command );
+ strOutput = childProcessModule.execSync( strHasG1Command, { cwd: strActionDir } );
+ details.trace( "{p}HashG1 output is:\n{raw}", strLogPrefix, strOutput || "<>" );
+ const joResultHashG1 = imaUtils.jsonFileLoad( path.join( strActionDir, "g1.json" ) );
+ details.trace( "{p}HashG1 result is: {}", strLogPrefix, joResultHashG1 );
+ if( "g1" in joResultHashG1 &&
+ "hint" in joResultHashG1.g1 &&
+ "hashPoint" in joResultHashG1.g1 &&
+ "X" in joResultHashG1.g1.hashPoint &&
+ "Y" in joResultHashG1.g1.hashPoint
+ ) {
+ joGlueResult.hashPoint = joResultHashG1.g1.hashPoint;
+ joGlueResult.hint = joResultHashG1.g1.hint;
+ } else {
+ joGlueResult = null;
+ throw new Error( `malformed HashG1 result: ${JSON.stringify( joResultHashG1 )}` );
+ }
+ } else {
+ const joSavedGlueResult = joGlueResult;
+ joGlueResult = null;
+ throw new Error( `malformed BLS glue result: ${JSON.stringify( joSavedGlueResult )}` );
+ }
+ fnShellRestore();
+ } catch ( err ) {
+ details.critical( "{p}BLS glue error description is: {err}, stack is: \n{stack}",
+ strLogPrefix, err, err );
+ details.critical( "{p}BLS glue output is:\n{raw}", strLogPrefix, strOutput || "<>" );
+ fnShellRestore();
+ joGlueResult = null;
+ }
+ return joGlueResult;
+}
+
+function performBlsGlueU256( details: log.TLogger, u256: any, arrSignResults: any[] ): any {
+ const imaState: state.TIMAState = state.get();
+ if( !imaState.joSChainNetworkInfo )
+ throw new Error( "No own S-Chain network information" );
+ const strLogPrefix = "BLS/Glue: ";
+ let joGlueResult: any = null;
+ const nThreshold = discoverBlsThreshold( imaState.joSChainNetworkInfo );
+ const nParticipants = discoverBlsParticipants( imaState.joSChainNetworkInfo );
+ details.debug( "{p}Discovered BLS threshold is {}.", strLogPrefix, nThreshold );
+ details.debug( "{p}Discovered number of BLS participants is {}.", strLogPrefix, nParticipants );
+ if( !checkBlsThresholdAndBlsParticipants( nThreshold, nParticipants, "BLS glue-256", details ) )
+ return null;
+ details.trace( "{p}Original long message is {}", strLogPrefix, keccak256U256( u256, false ) );
+ const strMessageHash = keccak256U256( u256, true );
+ details.trace( "{p}Message hash to sign is {}", strLogPrefix, strMessageHash );
+ const strActionDir = allocBlsTmpActionDir();
+ details.trace( "{p}performBlsGlueU256 will work in {} director with {} sign results...",
+ strLogPrefix, strActionDir, arrSignResults.length );
+ const fnShellRestore = function(): void { shell.rm( "-rf", strActionDir ); };
+ let strOutput = "";
+ try {
+ let strInput = "";
+ const cnt = arrSignResults.length;
+ for( let i = 0; i < cnt; ++i ) {
+ const jo: any = arrSignResults[i];
+ if( !jo )
+ throw new Error( `Failed to save BLS part ${i} because it's not JSON object` );
+ const strPath = strActionDir + "/sign-result" + jo.index + ".json";
+ details.trace( "{p}Saving {} file...", strLogPrefix, strPath );
+ imaUtils.jsonFileSave( strPath, jo );
+ strInput += " --input " + strPath;
+ }
+ const strGlueCommand =
+ imaState.strPathBlsGlue +
+ " --t " + nThreshold +
+ " --n " + nParticipants +
+ strInput +
+ " --output " + strActionDir + "/glue-result.json";
+ details.trace( "{p}Will execute BLS glue command: {}", strLogPrefix, strGlueCommand );
+ strOutput =
+ childProcessModule.execSync( strGlueCommand, { cwd: strActionDir } ).toString( "utf8" );
+ details.trace( "{p}BLS glue output is:\n{raw}", strLogPrefix, strOutput || "<>" );
+ joGlueResult = imaUtils.jsonFileLoad( path.join( strActionDir, "glue-result.json" ) );
+ details.trace( "{p}BLS glue result is:\n{}", strLogPrefix, joGlueResult );
+ if( joGlueResult && "X" in joGlueResult.signature && "Y" in joGlueResult.signature ) {
+ details.success( "{p}BLS glue success", strLogPrefix );
+ joGlueResult.hashSrc = strMessageHash;
+ details.trace( "{p}Computing G1 hash point...", strLogPrefix );
+ const strPath = strActionDir + "/hash.json";
+ details.trace( "{p}Saving {} file...", strLogPrefix, strPath );
+ imaUtils.jsonFileSave( strPath, { message: strMessageHash } );
+ const strHasG1Command =
+ imaState.strPathHashG1 +
+ " --t " + nThreshold +
+ " --n " + nParticipants;
+ details.trace( "{p}Will execute HashG1 command: {}", strLogPrefix, strHasG1Command );
+ strOutput = childProcessModule.execSync( strHasG1Command, { cwd: strActionDir } )
+ .toString( "utf8" );
+ details.trace( "{p}HashG1 output is:\n{raw}", strLogPrefix, strOutput || "<>" );
+ const joResultHashG1 = imaUtils.jsonFileLoad( path.join( strActionDir, "g1.json" ) );
+ details.trace( "{p}HashG1 result is: {}", strLogPrefix, joResultHashG1 );
+ if( "g1" in joResultHashG1 &&
+ "hint" in joResultHashG1.g1 &&
+ "hashPoint" in joResultHashG1.g1 &&
+ "X" in joResultHashG1.g1.hashPoint &&
+ "Y" in joResultHashG1.g1.hashPoint
+ ) {
+ joGlueResult.hashPoint = joResultHashG1.g1.hashPoint;
+ joGlueResult.hint = joResultHashG1.g1.hint;
+ } else {
+ joGlueResult = null;
+ throw new Error( `malformed HashG1 result: ${JSON.stringify( joResultHashG1 )}` );
+ }
+ } else {
+ const joSavedGlueResult = joGlueResult;
+ joGlueResult = null;
+ throw new Error( `malformed BLS glue result: ${JSON.stringify( joSavedGlueResult )}` );
+ }
+ fnShellRestore();
+ } catch ( err ) {
+ details.critical( "BLS glue error description is: {err}, stack is: \n{stack}",
+ err, err );
+ details.critical( "BLS glue output is:\n{raw}", strOutput || "<>" );
+ fnShellRestore();
+ joGlueResult = null;
+ }
+ return joGlueResult;
+}
+
+function performBlsVerifyI(
+ details: log.TLogger, strDirection: string, nZeroBasedNodeIndex: number,
+ joResultFromNode: any,
+ jarrMessages: any[], nIdxCurrentMsgBlockStart: number, strFromChainName: string,
+ joPublicKey: discoveryTools.TBLSPublicKey
+): boolean {
+ if( !joResultFromNode )
+ return true;
+ const imaState: state.TIMAState = state.get();
+ if( !imaState.joSChainNetworkInfo )
+ throw new Error( "No own S-Chain network information" );
+ const strLogPrefix = `${strDirection}/BLS/#${nZeroBasedNodeIndex}: `;
+ const nThreshold = discoverBlsThreshold( imaState.joSChainNetworkInfo );
+ const nParticipants = discoverBlsParticipants( imaState.joSChainNetworkInfo );
+ if( !checkBlsThresholdAndBlsParticipants( nThreshold, nParticipants, "BLS verify-I", details ) )
+ return false;
+ const strActionDir = allocBlsTmpActionDir();
+ const fnShellRestore = function(): void { shell.rm( "-rf", strActionDir ); };
+ let strOutput = "";
+ try {
+ details.trace( "{p}BLS node #{} - first message nonce is {}",
+ strLogPrefix, nZeroBasedNodeIndex, nIdxCurrentMsgBlockStart );
+ details.trace( "{p}BLS node #{} - first source chain name is {}",
+ strLogPrefix, nZeroBasedNodeIndex, strFromChainName );
+ details.trace( "{p}BLS node #{} - messages array {}",
+ strLogPrefix, nZeroBasedNodeIndex, jarrMessages );
+ const strMessageHash = owaspUtils.removeStarting0x(
+ keccak256Message( jarrMessages, nIdxCurrentMsgBlockStart, strFromChainName ) );
+ details.trace( "{p}BLS node #{} - hashed verify message is {}",
+ strLogPrefix, nZeroBasedNodeIndex, strMessageHash );
+ const joMsg = { message: strMessageHash };
+ details.debug(
+ "{p}BLS node #{} - composed {} composed from {} using glue {} and public key {}",
+ strLogPrefix, nZeroBasedNodeIndex, joMsg, jarrMessages, joResultFromNode, joPublicKey );
+ const strSignResultFileName = strActionDir + "/sign-result" + nZeroBasedNodeIndex + ".json";
+ imaUtils.jsonFileSave( strSignResultFileName, joResultFromNode );
+ imaUtils.jsonFileSave( strActionDir + "/hash.json", joMsg );
+ imaUtils.jsonFileSave(
+ strActionDir + "/BLS_keys" + nZeroBasedNodeIndex + ".json", joPublicKey );
+ const strVerifyCommand =
+ imaState.strPathBlsVerify +
+ " --t " + nThreshold +
+ " --n " + nParticipants +
+ " --j " + nZeroBasedNodeIndex +
+ " --input " + strSignResultFileName;
+ details.trace( "{p}Will execute node #{} BLS verify command: {}", strLogPrefix,
+ nZeroBasedNodeIndex, strVerifyCommand );
+ strOutput = childProcessModule.execSync( strVerifyCommand, { cwd: strActionDir } )
+ .toString( "utf8" );
+ details.trace( "{p}BLS node #{} verify output is:\n{raw}", strLogPrefix,
+ nZeroBasedNodeIndex, strOutput || "<>" );
+ details.success( "{p}BLS node #{} verify success", strLogPrefix, nZeroBasedNodeIndex );
+ fnShellRestore();
+ return true;
+ } catch ( err ) {
+ details.critical( "{p}BLS node #{} verify error:, error description is: {err}, " +
+ "stack is: \n{stack}", strLogPrefix, nZeroBasedNodeIndex, err, err );
+ details.critical( "{p}BLS node #{} verify output is:\n{raw}",
+ strLogPrefix, nZeroBasedNodeIndex, strOutput || "<>" );
+ fnShellRestore();
+ }
+ return false;
+}
+
+function performBlsVerifyIU256(
+ details: log.TLogger,
+ nZeroBasedNodeIndex: number, joResultFromNode: any, u256: any,
+ joPublicKey: discoveryTools.TBLSPublicKey
+): boolean {
+ if( !joResultFromNode )
+ return true;
+ const imaState: state.TIMAState = state.get();
+ if( !imaState.joSChainNetworkInfo )
+ throw new Error( "No own S-Chain network information" );
+ const strLogPrefix = `BLS/#${nZeroBasedNodeIndex}: `;
+ const nThreshold = discoverBlsThreshold( imaState.joSChainNetworkInfo );
+ const nParticipants = discoverBlsParticipants( imaState.joSChainNetworkInfo );
+ if( !checkBlsThresholdAndBlsParticipants(
+ nThreshold, nParticipants, "BLS verify-I-U256", details ) )
+ return false;
+ const strActionDir = allocBlsTmpActionDir();
+ const fnShellRestore = function(): void { shell.rm( "-rf", strActionDir ); };
+ let strOutput = "";
+ try {
+ const joMsg = { message: keccak256U256( u256, true ) };
+ details.debug( "{p}BLS u256 node #{} verify message {} composed from {} using glue {} " +
+ "and public key {}", strLogPrefix, nZeroBasedNodeIndex, joMsg, u256,
+ joResultFromNode, joPublicKey );
+ const strSignResultFileName = strActionDir + "/sign-result" + nZeroBasedNodeIndex + ".json";
+ imaUtils.jsonFileSave( strSignResultFileName, joResultFromNode );
+ imaUtils.jsonFileSave( strActionDir + "/hash.json", joMsg );
+ imaUtils.jsonFileSave(
+ strActionDir + "/BLS_keys" + nZeroBasedNodeIndex + ".json", joPublicKey );
+ const strVerifyCommand =
+ imaState.strPathBlsVerify +
+ " --t " + nThreshold +
+ " --n " + nParticipants +
+ " --j " + nZeroBasedNodeIndex +
+ " --input " + strSignResultFileName;
+ details.trace( "{p}Will execute node #{} BLS u256 verify command: {}",
+ strLogPrefix, nZeroBasedNodeIndex, strVerifyCommand );
+ strOutput = childProcessModule.execSync( strVerifyCommand, { cwd: strActionDir } )
+ .toString( "utf8" );
+ details.trace( "{p}BLS u256 node #{} verify output is:\n{raw}", strLogPrefix,
+ nZeroBasedNodeIndex, strOutput || "<>" );
+ details.success( "{p}BLS u256 node #{} verify success", strLogPrefix, nZeroBasedNodeIndex );
+ fnShellRestore();
+ return true;
+ } catch ( err ) {
+ details.error( "{p}BLS u256 node #{} verify error, error description is: {err}, " +
+ "stack is: \n{stack}", strLogPrefix, nZeroBasedNodeIndex, err, err );
+ details.error( "{p}BLS u256 node #{} verify output is:\n{raw}",
+ strLogPrefix, nZeroBasedNodeIndex, strOutput || "<>" );
+ fnShellRestore();
+ }
+ return false;
+}
+
+function performBlsVerify(
+ details: log.TLogger, strDirection: string, joGlueResult: any,
+ jarrMessages: any[], nIdxCurrentMsgBlockStart: number, strFromChainName: string,
+ joCommonPublicKey: discoveryTools.TBLSCommonPublicKey
+): boolean {
+ if( !joGlueResult )
+ return true;
+ const imaState: state.TIMAState = state.get();
+ if( !imaState.joSChainNetworkInfo )
+ throw new Error( "No own S-Chain network information" );
+ const nThreshold = discoverBlsThreshold( imaState.joSChainNetworkInfo );
+ const nParticipants = discoverBlsParticipants( imaState.joSChainNetworkInfo );
+ if( !checkBlsThresholdAndBlsParticipants( nThreshold, nParticipants, "BLS verify", details ) )
+ return false;
+ const strActionDir = allocBlsTmpActionDir();
+ const fnShellRestore = function(): void { shell.rm( "-rf", strActionDir ); };
+ let strOutput = "";
+ const strLogPrefix = `${strDirection}/BLS/Summary: "`;
+ try {
+ details.trace( "{p}BLS/summary verify message - first message nonce is {}",
+ strLogPrefix, nIdxCurrentMsgBlockStart );
+ details.trace( "{p}BLS/summary verify message - first source chain name is {}",
+ strLogPrefix, strFromChainName );
+ details.trace( "{p}BLS/summary verify message - messages array {}",
+ strLogPrefix, jarrMessages );
+ const strMessageHash = owaspUtils.removeStarting0x(
+ keccak256Message( jarrMessages, nIdxCurrentMsgBlockStart, strFromChainName ) );
+ details.trace( "{p}BLS/summary verify message - hashed verify message is {}",
+ strLogPrefix, strMessageHash );
+ const joMsg = { message: strMessageHash };
+ details.debug(
+ "{p}BLS/summary verify message - composed JSON {} from messages array {}" +
+ " using glue {} and common public key {}",
+ strLogPrefix, joMsg, jarrMessages, joGlueResult, joCommonPublicKey );
+ imaUtils.jsonFileSave( strActionDir + "/glue-result.json", joGlueResult );
+ imaUtils.jsonFileSave( strActionDir + "/hash.json", joMsg );
+ const joCommonPublicKeyToSave: discoveryTools.TBLSCommonPublicKey = {
+ commonBLSPublicKey0: joCommonPublicKey.commonBLSPublicKey0,
+ commonBLSPublicKey1: joCommonPublicKey.commonBLSPublicKey1,
+ commonBLSPublicKey2: joCommonPublicKey.commonBLSPublicKey2,
+ commonBLSPublicKey3: joCommonPublicKey.commonBLSPublicKey3
+ };
+ imaUtils.jsonFileSave( strActionDir + "/common_public_key.json", joCommonPublicKeyToSave );
+ details.trace( "{p}BLS common public key for verification is:\n{}",
+ strLogPrefix, joCommonPublicKey );
+ const strVerifyCommand =
+ imaState.strPathBlsVerify +
+ " --t " + nThreshold +
+ " --n " + nParticipants +
+ " --input " + "./glue-result.json";
+ details.trace( "{p}Will execute BLS/summary verify command: {}",
+ strLogPrefix, strVerifyCommand );
+ strOutput = childProcessModule.execSync( strVerifyCommand, { cwd: strActionDir } )
+ .toString( "utf8" );
+ details.trace( "{p}BLS/summary verify output is:\n{raw}", strLogPrefix,
+ strOutput || "<>" );
+ details.success( "{p}BLS/summary verify success", strLogPrefix );
+ fnShellRestore();
+ return true;
+ } catch ( err ) {
+ details.error( "{p}BLS/summary verify error description is: {err}, stack is:\n{stack}",
+ strLogPrefix, err, err );
+ details.error( "BLS/summary verify output is:\n{raw}", strOutput || "<>" );
+ fnShellRestore();
+ }
+ return false;
+}
+
+function performBlsVerifyU256(
+ details: log.TLogger, joGlueResult: any, u256: any,
+ joCommonPublicKey: discoveryTools.TBLSCommonPublicKey
+): boolean {
+ if( !joGlueResult )
+ return true;
+ const imaState: state.TIMAState = state.get();
+ if( !imaState.joSChainNetworkInfo )
+ throw new Error( "No own S-Chain network information" );
+ const nThreshold = discoverBlsThreshold( imaState.joSChainNetworkInfo );
+ const nParticipants = discoverBlsParticipants( imaState.joSChainNetworkInfo );
+ if( !checkBlsThresholdAndBlsParticipants(
+ nThreshold, nParticipants, "BLS verify-U256", details ) )
+ return false;
+ const strActionDir = allocBlsTmpActionDir();
+ const fnShellRestore = function(): void { shell.rm( "-rf", strActionDir ); };
+ let strOutput = "";
+ const strLogPrefix = "BLS u256/Summary: ";
+ try {
+ const joMsg = { message: keccak256U256( u256, true ) };
+ details.debug(
+ "{p}BLS u256/summary verify message {} composed from {} using glue {}" +
+ " and common public key {}",
+ strLogPrefix, joMsg, u256, joGlueResult, joCommonPublicKey );
+ imaUtils.jsonFileSave( strActionDir + "/glue-result.json", joGlueResult );
+ imaUtils.jsonFileSave( strActionDir + "/hash.json", joMsg );
+ const joCommonPublicKeyToSave: discoveryTools.TBLSCommonPublicKey = {
+ commonBLSPublicKey0: joCommonPublicKey.commonBLSPublicKey0,
+ commonBLSPublicKey1: joCommonPublicKey.commonBLSPublicKey1,
+ commonBLSPublicKey2: joCommonPublicKey.commonBLSPublicKey2,
+ commonBLSPublicKey3: joCommonPublicKey.commonBLSPublicKey3
+ };
+ imaUtils.jsonFileSave( strActionDir + "/common_public_key.json", joCommonPublicKeyToSave );
+ details.trace( "{p}BLS u256 common public key for verification is:\n{}",
+ strLogPrefix, joCommonPublicKey );
+ const strVerifyCommand =
+ imaState.strPathBlsVerify +
+ " --t " + nThreshold +
+ " --n " + nParticipants +
+ " --input " + "./glue-result.json";
+ details.trace( "{p}Will execute BLS u256/summary verify command: {}",
+ strLogPrefix, strVerifyCommand );
+ strOutput = childProcessModule.execSync( strVerifyCommand, { cwd: strActionDir } )
+ .toString( "utf8" );
+ details.trace( "{p}BLS u256/summary verify output is:\n{raw}", strLogPrefix,
+ strOutput || "<>" );
+ details.success( "{p}BLS u256/summary verify success", strLogPrefix );
+ fnShellRestore();
+ return true;
+ } catch ( err ) {
+ details.error( "{p}BLS u256/summary error description is: {err}, stack is: \n{stack}",
+ strLogPrefix, err, err );
+ details.error( "{p}BLS u256/summary verify output is:\n{raw}", strLogPrefix,
+ strOutput || "<>" );
+ fnShellRestore();
+ }
+ return false;
+}
+
+async function checkCorrectnessOfMessagesToSign(
+ details: log.TLogger, strLogPrefix: string, strDirection: string,
+ jarrMessages: any[], nIdxCurrentMsgBlockStart: number,
+ joExtraSignOpts?: loop.TExtraSignOpts | null
+): Promise < void > {
+ const imaState: state.TIMAState = state.get();
+ let joMessageProxy: owaspUtils.ethersMod.ethers.Contract | null = null;
+ let joAccount: state.TAccount | null = null;
+ let joChainName: string | null = null;
+ if( strDirection == "M2S" ) {
+ joMessageProxy = imaState.joMessageProxyMainNet;
+ joAccount = imaState.chainProperties.mn.joAccount;
+ joChainName = imaState.chainProperties.sc.strChainName;
+ } else if( strDirection == "S2M" ) {
+ joMessageProxy = imaState.joMessageProxySChain;
+ joAccount = imaState.chainProperties.sc.joAccount;
+ joChainName = imaState.chainProperties.mn.strChainName;
+ } else if( strDirection == "S2S" ) {
+ joAccount = imaState.chainProperties.sc.joAccount;
+ if( ( !joExtraSignOpts?.chainNameDst ) )
+ throw new Error( "Missing destination chain name for BLS signing" );
+ joChainName = joExtraSignOpts.chainNameDst;
+ const ethersProvider: owaspUtils.ethersMod.ethers.providers.JsonRpcProvider | null =
+ ( joExtraSignOpts && "ethersProviderSrc" in joExtraSignOpts &&
+ joExtraSignOpts.ethersProviderSrc )
+ ? joExtraSignOpts.ethersProviderSrc
+ : null;
+ if( !ethersProvider ) {
+ throw new Error( "CRITICAL ERROR: No provider specified in extra signing options " +
+ `for checking messages of direction ${strDirection}` );
+ }
+ joMessageProxy = new owaspUtils.ethersMod.ethers.Contract(
+ imaState.chainProperties.sc.joAbiIMA.message_proxy_chain_address,
+ imaState.chainProperties.sc.joAbiIMA.message_proxy_chain_abi,
+ ethersProvider );
+ } else {
+ throw new Error( "CRITICAL ERROR: Failed checkCorrectnessOfMessagesToSign() with " +
+ `unknown direction ${strDirection}` );
+ }
+
+ const strCallerAccountAddress = joAccount.address();
+ details.debug(
+ "{p}{bright} message correctness validation through call to {sunny} method of " +
+ "MessageProxy contract with address {}, caller account address is {}, message(s) " +
+ "count is {}, message(s) to process are {}, first real message index is {}, messages " +
+ "will be sent to chain name {}",
+ strLogPrefix, strDirection, "verifyOutgoingMessageData",
+ joMessageProxy ? joMessageProxy.address : "",
+ strCallerAccountAddress, jarrMessages.length, jarrMessages,
+ nIdxCurrentMsgBlockStart, joChainName );
+ let cntBadMessages = 0; let i = 0;
+ const cnt = jarrMessages.length;
+ if( strDirection == "S2M" || strDirection == "S2S" ) {
+ for( i = 0; i < cnt; ++i ) {
+ const joMessage = jarrMessages[i];
+ const idxMessage = nIdxCurrentMsgBlockStart + i;
+ try {
+ details.trace(
+ "{p}{bright} Will validate message {} of {}, real message index is {}, " +
+ "source contract is {}, destination contract is {}, message data is {}",
+ strLogPrefix, strDirection, i, cnt, idxMessage, joMessage.sender,
+ joMessage.destinationContract, joMessage.data );
+ const outgoingMessageData: any = {
+ dstChainHash: owaspUtils.ethersMod.ethers.utils.id( joChainName ),
+ msgCounter: idxMessage,
+ srcContract: joMessage.sender,
+ dstContract: joMessage.destinationContract,
+ data: joMessage.data
+ };
+ if( !joMessageProxy )
+ throw new Error( "No message proxy available" );
+ const isValidMessage = await joMessageProxy.callStatic.verifyOutgoingMessageData(
+ outgoingMessageData, { from: strCallerAccountAddress } );
+ details.trace(
+ "{p}{bright} Got verification call result {}, real message index is: {}, " +
+ "saved msgCounter is: {}", strLogPrefix, strDirection,
+ isValidMessage, +idxMessage, outgoingMessageData.msgCounter );
+ if( !isValidMessage ) {
+ throw new Error( "Bad message detected, " +
+ `message is: ${JSON.stringify( joMessage )}` );
+ }
+ } catch ( err ) {
+ ++cntBadMessages;
+ details.critical(
+ "{p}{bright} Correctness validation failed for message {} sent to {}, " +
+ "message is: {}, error information: {err}, stack is:\n{stack}",
+ strLogPrefix, strDirection, idxMessage, joChainName, joMessage,
+ err, err );
+ }
+ }
+ }
+ // TODO: M2S - check events
+ if( cntBadMessages > 0 ) {
+ details.critical( "{p}Correctness validation failed for {} of {} message(s)",
+ strLogPrefix, cntBadMessages, cnt );
+ } else
+ details.success( "{p}Correctness validation passed for {} message(s)", strLogPrefix, cnt );
+}
+
+async function prepareSignMessagesImpl(
+ optsSignOperation: TSignOperationOptions ): Promise < boolean > {
+ optsSignOperation.fn = optsSignOperation.fn ||
+ // eslint-disable-next-line n/handle-callback-err
+ async function(
+ err: Error | string | null,
+ jarrMessages: any[],
+ joGlueResult: any | null
+ ): Promise < void > {};
+ optsSignOperation.sequenceId =
+ owaspUtils.removeStarting0x(
+ owaspUtils.ethersMod.ethers.utils.id( log.generateTimestampString( null, false ) )
+ );
+ optsSignOperation.jarrNodes =
+ ( optsSignOperation.imaState.bSignMessages &&
+ "joSChainNetworkInfo" in optsSignOperation.imaState &&
+ optsSignOperation.imaState.joSChainNetworkInfo &&
+ typeof optsSignOperation.imaState.joSChainNetworkInfo === "object" &&
+ "network" in optsSignOperation.imaState.joSChainNetworkInfo &&
+ typeof optsSignOperation.imaState.joSChainNetworkInfo.network === "object"
+ )
+ ? optsSignOperation.imaState.joSChainNetworkInfo.network
+ : [];
+ optsSignOperation.details.trace(
+ "{p} Invoking {bright} signing messages procedure, message signing is {oo}",
+ optsSignOperation.strLogPrefix, optsSignOperation.strDirection,
+ optsSignOperation.imaState.bSignMessages );
+ if( !( optsSignOperation.imaState.bSignMessages &&
+ optsSignOperation.imaState.strPathBlsGlue.length > 0 &&
+ optsSignOperation.imaState.joSChainNetworkInfo
+ ) ) {
+ optsSignOperation.bHaveResultReportCalled = true;
+ optsSignOperation.details.debug(
+ "{p}BLS message signing is turned off, first real message index is: {}, have {} " +
+ "message(s) to process {}", optsSignOperation.strLogPrefix,
+ optsSignOperation.nIdxCurrentMsgBlockStart, optsSignOperation.jarrMessages.length,
+ optsSignOperation.jarrMessages );
+ optsSignOperation.details.exposeDetailsTo(
+ log.globalStream(), optsSignOperation.strGatheredDetailsName, false );
+ optsSignOperation.details.close();
+ await checkCorrectnessOfMessagesToSign(
+ optsSignOperation.details, optsSignOperation.strLogPrefix,
+ optsSignOperation.strDirection,
+ optsSignOperation.jarrMessages,
+ optsSignOperation.nIdxCurrentMsgBlockStart,
+ optsSignOperation.joExtraSignOpts );
+ await optsSignOperation.fn( null, optsSignOperation.jarrMessages );
+ return true;
+ }
+ await checkCorrectnessOfMessagesToSign(
+ optsSignOperation.details, optsSignOperation.strLogPrefix,
+ optsSignOperation.strDirection,
+ optsSignOperation.jarrMessages, optsSignOperation.nIdxCurrentMsgBlockStart,
+ optsSignOperation.joExtraSignOpts
+ );
+ optsSignOperation.details.trace( "{p}Will sign {} message(s), sequence ID is {}...",
+ optsSignOperation.strLogPrefix, optsSignOperation.jarrMessages.length,
+ optsSignOperation.sequenceId );
+ optsSignOperation.details.trace( "{p}Will query to sign {} skaled node(s)...",
+ optsSignOperation.strLogPrefix, optsSignOperation.jarrNodes.length );
+ optsSignOperation.nThreshold =
+ discoverBlsThreshold( optsSignOperation.imaState.joSChainNetworkInfo );
+ optsSignOperation.nParticipants =
+ discoverBlsParticipants( optsSignOperation.imaState.joSChainNetworkInfo );
+ optsSignOperation.details.trace( "{p}Discovered BLS threshold is {}.",
+ optsSignOperation.strLogPrefix, optsSignOperation.nThreshold );
+ optsSignOperation.details.trace( "{p}Discovered number of BLS participants is {}.",
+ optsSignOperation.strLogPrefix, optsSignOperation.nParticipants );
+ if( !checkBlsThresholdAndBlsParticipants(
+ optsSignOperation.nThreshold,
+ optsSignOperation.nParticipants,
+ "prepare sign messages " + optsSignOperation.strDirection,
+ optsSignOperation.details ) ) {
+ optsSignOperation.bHaveResultReportCalled = true;
+ optsSignOperation.details.exposeDetailsTo(
+ log.globalStream(), optsSignOperation.strGatheredDetailsName, false );
+ optsSignOperation.details.close();
+ await optsSignOperation.fn(
+ "signature error(1), S-Chain information " +
+ "was not discovered properly and BLS threshold/participants are unknown",
+ optsSignOperation.jarrMessages, null );
+ return false;
+ }
+ optsSignOperation.nCountOfBlsPartsToCollect = optsSignOperation.nThreshold;
+ optsSignOperation.details.trace( "{p}Will BLS-collect {} from {} nodes, sequence ID is {}",
+ optsSignOperation.strLogPrefix, optsSignOperation.nCountOfBlsPartsToCollect,
+ optsSignOperation.jarrNodes.length, optsSignOperation.sequenceId );
+ return true;
+}
+
+async function gatherSigningCheckFinish(
+ optsSignOperation: TSignOperationOptions ): Promise < boolean > {
+ const cntSuccess = optsSignOperation.arrSignResults.length;
+ if( optsSignOperation.joGatheringTracker.nCountReceivedPrevious !=
+ optsSignOperation.joGatheringTracker.nCountReceived ) {
+ optsSignOperation.details.debug(
+ "{bright}/#{} BLS signature gathering progress updated, now have {} BLS " +
+ "parts of needed {} arrived, have {} success(es) and {} error(s)",
+ optsSignOperation.strDirection, optsSignOperation.nTransferLoopCounter,
+ optsSignOperation.joGatheringTracker.nCountReceived,
+ optsSignOperation.nCountOfBlsPartsToCollect, cntSuccess,
+ optsSignOperation.joGatheringTracker.nCountErrors );
+ optsSignOperation.joGatheringTracker.nCountReceivedPrevious =
+ owaspUtils.toInteger( optsSignOperation.joGatheringTracker.nCountReceived );
+ }
+ if( cntSuccess < optsSignOperation.nCountOfBlsPartsToCollect )
+ return false;
+ optsSignOperation.strLogPrefixB = `${optsSignOperation.strDirection} /# ` +
+ `${optsSignOperation.nTransferLoopCounter}/BLS/Summary: `;
+ let strError: string | null = null; let strSuccessfulResultDescription: string | null = null;
+ const joGlueResult = performBlsGlue( optsSignOperation.details,
+ optsSignOperation.strDirection, optsSignOperation.jarrMessages,
+ optsSignOperation.nIdxCurrentMsgBlockStart, optsSignOperation.strFromChainName,
+ optsSignOperation.arrSignResults );
+ if( joGlueResult ) {
+ optsSignOperation.details.success( "{p}Got BLS glue result: {}",
+ optsSignOperation.strLogPrefixB, joGlueResult );
+ if( optsSignOperation.imaState.strPathBlsVerify.length > 0 ) {
+ if( !optsSignOperation.imaState.joSChainNetworkInfo )
+ throw new Error( "No own S-Chain network information" );
+ const joCommonPublicKey = discoverCommonPublicKey(
+ optsSignOperation.details, optsSignOperation.imaState.joSChainNetworkInfo, false );
+ if( !joCommonPublicKey ) {
+ strError = "No BLS common public key";
+ optsSignOperation.details.error( "{p}{err}",
+ optsSignOperation.strLogPrefixB, strError );
+ } else if( performBlsVerify(
+ optsSignOperation.details, optsSignOperation.strDirection,
+ joGlueResult, optsSignOperation.jarrMessages,
+ optsSignOperation.nIdxCurrentMsgBlockStart,
+ optsSignOperation.strFromChainName, joCommonPublicKey
+ ) ) {
+ strSuccessfulResultDescription = "Got successful summary BLS verification result";
+ optsSignOperation.details.success( "{p}{bright}",
+ optsSignOperation.strLogPrefixB, strSuccessfulResultDescription );
+ } else {
+ strError = "BLS verification failed";
+ optsSignOperation.details.error( "{p}{err}",
+ optsSignOperation.strLogPrefixB, strError );
+ }
+ }
+ } else {
+ strError = "BLS glue failed, no glue result arrived";
+ optsSignOperation.details.error(
+ "{p}Problem(1) in BLS sign result handler: {err}",
+ optsSignOperation.strLogPrefixB, strError );
+ }
+ optsSignOperation.details.trace(
+ "Will call signed-hash answer-sending callback {}, messages is(are) {}, " +
+ "glue result is {}", strError ? log.fmtError( " with error {}", strError ) : "",
+ optsSignOperation.jarrMessages, joGlueResult );
+ optsSignOperation.fn(
+ strError, optsSignOperation.jarrMessages, joGlueResult )
+ .catch( function( err: Error | string ): void {
+ optsSignOperation.details.critical(
+ "Problem(2) in BLS sign result handler: {err}", err );
+ optsSignOperation.errGathering = "Problem(2) in BLS sign " +
+ `result handler: ${owaspUtils.extractErrorMessage( err )}`;
+ } );
+ optsSignOperation.bHaveResultReportCalled = true;
+ return true;
+}
+
+async function gatherSigningCheckOverflow(
+ optsSignOperation: TSignOperationOptions ): Promise < boolean > {
+ if( optsSignOperation.joGatheringTracker.nCountReceived < optsSignOperation.jarrNodes.length )
+ return false;
+ optsSignOperation.fn(
+ `signature error(2), got ${optsSignOperation.joGatheringTracker.nCountErrors}` +
+ ` errors(s) for ${optsSignOperation.jarrNodes.length} node(s)`,
+ optsSignOperation.jarrMessages, null
+ ).catch( function( err: Error | string ): void {
+ const cntSuccess = optsSignOperation.arrSignResults.length;
+ optsSignOperation.details.error(
+ "Problem(3) in BLS sign result handler, not enough successful BLS signature " +
+ "parts({}) when all attempts done, error details: {err}", cntSuccess, err );
+ optsSignOperation.errGathering = "Problem(3) in BLS sign result handler, not enough " +
+ `successful BLS signature parts(${cntSuccess}) when all attempts done, ` +
+ `error details: ${owaspUtils.extractErrorMessage( err )}`;
+ } );
+ optsSignOperation.bHaveResultReportCalled = true;
+ return true;
+}
+
+async function gatherSigningStartImpl(
+ optsSignOperation: TSignOperationOptions ): Promise < void > {
+ optsSignOperation.details.debug( "{p}Waiting for BLS glue result...",
+ optsSignOperation.strLogPrefix );
+ optsSignOperation.errGathering = null;
+ for( let idxStep = 0; idxStep < optsSignOperation.joGatheringTracker.nWaitIntervalMaxSteps;
+ ++idxStep ) {
+ await threadInfo.sleep(
+ optsSignOperation.joGatheringTracker.nWaitIntervalStepMilliseconds );
+ if( await gatherSigningCheckFinish( optsSignOperation ) )
+ return;
+ if( await gatherSigningCheckOverflow( optsSignOperation ) )
+ return;
+ }
+ // timeout
+ optsSignOperation.fn(
+ `signature error(3), got ${optsSignOperation.joGatheringTracker.nCountErrors}` +
+ ` errors(s) for ${optsSignOperation.jarrNodes.length} node(s)`,
+ optsSignOperation.jarrMessages, null
+ ).catch( function( err: Error | string ): void {
+ const cntSuccess = optsSignOperation.arrSignResults.length;
+ optsSignOperation.details.critical(
+ "Problem(4) in BLS sign result handler, not enough successful BLS signature " +
+ "parts({}) and timeout reached, error details: {err}", cntSuccess, err );
+ optsSignOperation.errGathering = "Problem(4) in BLS sign result handler, not enough " +
+ `successful BLS signature parts(${cntSuccess}) and timeout reached, ` +
+ `error details: ${owaspUtils.extractErrorMessage( err )}`;
+ } );
+ optsSignOperation.bHaveResultReportCalled = true;
+}
+
+async function gatherSigningFinishImpl(
+ optsSignOperation: TSignOperationOptions ): Promise < void > {
+ optsSignOperation.details.trace( "{p}Will await for message BLS verification and sending...",
+ optsSignOperation.strLogPrefix );
+ if( optsSignOperation.errGathering ) {
+ optsSignOperation.details.error( "Failed BLS sign result awaiting(1): {err}",
+ optsSignOperation.errGathering.toString() );
+ if( !optsSignOperation.bHaveResultReportCalled ) {
+ optsSignOperation.bHaveResultReportCalled = true;
+ await optsSignOperation.fn(
+ `Failed to gather BLS signatures in ${optsSignOperation.jarrNodes.length} ` +
+ "node(s), tracker data is: " +
+ `${JSON.stringify( optsSignOperation.joGatheringTracker )} , ` +
+ `error is: ${optsSignOperation.errGathering.toString()}`,
+ optsSignOperation.jarrMessages, null
+ ).catch( function( err: Error | string ): void {
+ const cntSuccess = optsSignOperation.arrSignResults.length;
+ optsSignOperation.details.error(
+ "Problem(5) in BLS sign result handler, not enough successful BLS " +
+ "signature parts({}) and timeout reached, error details: {err}",
+ cntSuccess, err );
+ optsSignOperation.details.exposeDetailsTo(
+ log.globalStream(), optsSignOperation.strGatheredDetailsName, false );
+ optsSignOperation.details.close();
+ optsSignOperation.details = log.globalStream();
+ } );
+ }
+ return;
+ }
+ if( !optsSignOperation.bHaveResultReportCalled ) {
+ optsSignOperation.details.error( "Failed BLS sign result awaiting(2): {err}",
+ "No reports were arrived" );
+ optsSignOperation.bHaveResultReportCalled = true;
+ await optsSignOperation.fn(
+ `Failed to gather BLS signatures in ${optsSignOperation.jarrNodes.length} node(s), ` +
+ `tracker data is: ${JSON.stringify( optsSignOperation.joGatheringTracker )}`,
+ optsSignOperation.jarrMessages, null
+ ).catch( function( err: Error | string ): void {
+ const cntSuccess = optsSignOperation.arrSignResults.length;
+ optsSignOperation.details.error(
+ "Problem(6) in BLS sign result handler, not enough successful BLS signature " +
+ "parts({}) and timeout reached, error details: {err}", cntSuccess, err );
+ optsSignOperation.details.exposeDetailsTo(
+ log.globalStream(), optsSignOperation.strGatheredDetailsName, false );
+ optsSignOperation.details.close();
+ optsSignOperation.details = log.globalStream();
+ } );
+ }
+}
+
+async function doSignConfigureChainAccessParams(
+ optsSignOperation: TSignOperationOptions ): Promise < void > {
+ optsSignOperation.targetChainName = "";
+ optsSignOperation.fromChainName = "";
+ optsSignOperation.targetChainID = -4;
+ optsSignOperation.fromChainID = -4;
+ if( optsSignOperation.strDirection == "M2S" ) {
+ optsSignOperation.targetChainName =
+ ( optsSignOperation.imaState.chainProperties.sc.strChainName
+ ? optsSignOperation.imaState.chainProperties.sc.strChainName
+ : "" );
+ optsSignOperation.fromChainName =
+ ( optsSignOperation.imaState.chainProperties.mn.strChainName
+ ? optsSignOperation.imaState.chainProperties.mn.strChainName
+ : "" );
+ optsSignOperation.targetChainID = optsSignOperation.imaState.chainProperties.sc.chainId;
+ optsSignOperation.fromChainID = optsSignOperation.imaState.chainProperties.mn.chainId;
+ } else if( optsSignOperation.strDirection == "S2M" ) {
+ optsSignOperation.targetChainName =
+ ( optsSignOperation.imaState.chainProperties.mn.strChainName
+ ? optsSignOperation.imaState.chainProperties.mn.strChainName
+ : "" );
+ optsSignOperation.fromChainName =
+ ( optsSignOperation.imaState.chainProperties.sc.strChainName
+ ? optsSignOperation.imaState.chainProperties.sc.strChainName
+ : "" );
+ optsSignOperation.targetChainID = optsSignOperation.imaState.chainProperties.mn.chainId;
+ optsSignOperation.fromChainID = optsSignOperation.imaState.chainProperties.sc.chainId;
+ } else if( optsSignOperation.strDirection == "S2S" ) {
+ if( !optsSignOperation.joExtraSignOpts )
+ throw new Error( "No S2S signing options provided" );
+ optsSignOperation.targetChainName = optsSignOperation.joExtraSignOpts.chainNameDst;
+ optsSignOperation.fromChainName = optsSignOperation.joExtraSignOpts.chainNameSrc;
+ optsSignOperation.targetChainID = optsSignOperation.joExtraSignOpts.chainIdDst;
+ optsSignOperation.fromChainID = optsSignOperation.joExtraSignOpts.chainIdSrc;
+ } else {
+ throw new Error( "CRITICAL ERROR: Failed doSignMessagesImpl() with " +
+ `unknown direction ${optsSignOperation.strDirection}` );
+ }
+}
+
+async function doSignProcessHandleCall(
+ optsSignOperation: TSignOperationOptions, joParams: any, joCall: rpcCall.TRPCCall,
+ joIn: any, joOut: any, i: number ): Promise < void > {
+ const imaState: state.TIMAState = state.get();
+ const isThisNode = ( i == imaState.nNodeNumber );
+ const joNode = optsSignOperation.jarrNodes[i];
+ const strNodeURL = optsSignOperation.imaState.isCrossImaBlsMode
+ ? imaUtils.composeImaAgentNodeUrl( joNode, isThisNode )
+ : imaUtils.composeSChainNodeUrl( joNode );
+ const strNodeDescColorized = log.fmtDebug( "{url} ({}/{}, ID {}), sequence ID is {}",
+ strNodeURL, i, optsSignOperation.jarrNodes.length, joNode.nodeID,
+ optsSignOperation.sequenceId );
+ ++optsSignOperation.joGatheringTracker.nCountReceived;
+ optsSignOperation.details.trace(
+ "{p}{} Got answer from {bright}(node #{} via {url} for transfer from chain {} " +
+ "to chain {} with params {}, answer is {}, sequence ID is {}",
+ optsSignOperation.strLogPrefix, log.generateTimestampString( null, true ),
+ "skale_imaVerifyAndSign", i, strNodeURL, optsSignOperation.fromChainName,
+ optsSignOperation.targetChainName, joParams, joOut, optsSignOperation.sequenceId );
+ if( ( !joOut ) || typeof joOut !== "object" || ( !( "result" in joOut ) ) ||
+ ( !joOut.result ) || typeof joOut.result !== "object" ||
+ ( "error" in joOut && joOut.error ) ) {
+ ++optsSignOperation.joGatheringTracker.nCountErrors;
+ optsSignOperation.details.critical(
+ "{p}S-Chain node {} reported wallet error: {err}, sequence ID is ",
+ optsSignOperation.strLogPrefix, strNodeDescColorized,
+ owaspUtils.extractErrorMessage( joOut, "unknown wallet error(1)" ),
+ optsSignOperation.sequenceId );
+ await joCall.disconnect();
+ return;
+ }
+ optsSignOperation.details.debug( "{p}Node {} sign result: {}",
+ optsSignOperation.strLogPrefix, joNode.nodeID, joOut.result ? joOut.result : null );
+ try {
+ if( joOut.result.signResult.signatureShare.length > 0 &&
+ joOut.result.signResult.status === 0
+ ) {
+ const nZeroBasedNodeIndex = joNode.imaInfo.thisNodeIndex - 1;
+ // partial BLS verification for one participant
+ let bNodeSignatureOKay = false; // initially assume signature is wrong
+ optsSignOperation.strLogPrefixA =
+ `${optsSignOperation.strDirection}/BLS/#${nZeroBasedNodeIndex}: `;
+ try {
+ const cntSuccess = optsSignOperation.arrSignResults.length;
+ if( cntSuccess > optsSignOperation.nCountOfBlsPartsToCollect ) {
+ ++optsSignOperation.joGatheringTracker.nCountSkipped;
+ optsSignOperation.details.notice(
+ "{p}Will ignore sign result for node {} because {}/{} threshold number " +
+ "of BLS signature parts already gathered",
+ optsSignOperation.strLogPrefixA, nZeroBasedNodeIndex,
+ optsSignOperation.nThreshold, optsSignOperation.nCountOfBlsPartsToCollect );
+ await joCall.disconnect();
+ return;
+ }
+ const arrTmp = joOut.result.signResult.signatureShare.split( ":" );
+ const joResultFromNode: any = {
+ index: nZeroBasedNodeIndex.toString(),
+ signature: {
+ X: arrTmp[0],
+ Y: arrTmp[1]
+ }
+ };
+ optsSignOperation.details.trace( "{p}Will verify sign result for node {}",
+ optsSignOperation.strLogPrefixA, nZeroBasedNodeIndex );
+ if( !optsSignOperation.imaState.joSChainNetworkInfo )
+ throw new Error( "No own S-Chain network information" );
+ const joPublicKey = discoverPublicKeyByIndex( nZeroBasedNodeIndex,
+ optsSignOperation.imaState.joSChainNetworkInfo, optsSignOperation.details,
+ true );
+ if( !joPublicKey )
+ throw new Error( `No BLS public key for node ${nZeroBasedNodeIndex}` );
+ if( performBlsVerifyI(
+ optsSignOperation.details, optsSignOperation.strDirection,
+ nZeroBasedNodeIndex, joResultFromNode,
+ optsSignOperation.jarrMessages,
+ optsSignOperation.nIdxCurrentMsgBlockStart,
+ optsSignOperation.strFromChainName,
+ joPublicKey
+ ) ) {
+ optsSignOperation.details.success(
+ "{p}Got successful BLS verification result for node {} with index {}",
+ optsSignOperation.strLogPrefixA, joNode.nodeID, nZeroBasedNodeIndex );
+ bNodeSignatureOKay = true; // node verification passed
+ } else {
+ optsSignOperation.details.error( "{p} BLS verification failed",
+ optsSignOperation.strLogPrefixA );
+ }
+ } catch ( err ) {
+ optsSignOperation.details.critical(
+ "{p}S-Chain node {} partial signature fail from with index {}" +
+ ", error is {err}, sequence ID is {}, stack is:\n{stack}",
+ optsSignOperation.strLogPrefixA, strNodeDescColorized, nZeroBasedNodeIndex,
+ err, optsSignOperation.sequenceId, err );
+ }
+ if( bNodeSignatureOKay ) {
+ optsSignOperation.arrSignResults.push( {
+ index: nZeroBasedNodeIndex.toString(),
+ signature: splitSignatureShare( joOut.result.signResult.signatureShare ),
+ fromNode: joNode, // extra, not needed for bls_glue
+ signResult: joOut.result.signResult
+ } );
+ } else
+ ++optsSignOperation.joGatheringTracker.nCountErrors;
+ }
+ } catch ( err ) {
+ ++optsSignOperation.joGatheringTracker.nCountErrors;
+ optsSignOperation.details.critical(
+ "{p}S-Chain node {} signature fail from node {}, error is {err}" +
+ ", sequence ID is {}, stack is:\n{stack}",
+ optsSignOperation.strLogPrefix, strNodeDescColorized, joNode.nodeID,
+ err, optsSignOperation.sequenceId, err );
+ }
+ await joCall.disconnect();
+}
+
+async function doSignProcessOneImpl(
+ i: number, optsSignOperation: TSignOperationOptions ): Promise < void > {
+ const imaState: state.TIMAState = state.get();
+ const isThisNode = ( i == imaState.nNodeNumber );
+ const joNode = optsSignOperation.jarrNodes[i];
+ const strNodeURL = optsSignOperation.imaState.isCrossImaBlsMode
+ ? imaUtils.composeImaAgentNodeUrl( joNode, isThisNode )
+ : imaUtils.composeSChainNodeUrl( joNode );
+ const strNodeDescColorized = log.fmtDebug( "{url} ({}/{}, ID {}), sequence ID is {}",
+ strNodeURL, i, optsSignOperation.jarrNodes.length, joNode.nodeID,
+ optsSignOperation.sequenceId );
+ const rpcCallOpts: rpcCall.TRPCCallOpts | null = null;
+ const joCall =
+ await rpcCall.create( strNodeURL, rpcCallOpts
+ ).catch( function( err: Error | string ): void {
+ ++optsSignOperation.joGatheringTracker.nCountReceived;
+ ++optsSignOperation.joGatheringTracker.nCountErrors;
+ optsSignOperation.details.error(
+ "{p}JSON RPC call(doSignProcessOneImpl) to S-Chain node {} failed, " +
+ "RPC call failed, error is: {err}, sequence ID is {}",
+ optsSignOperation.strLogPrefix, strNodeDescColorized,
+ err, optsSignOperation.sequenceId );
+ if( joCall )
+ joCall.disconnect().then( function(): void {} ).catch( function(): void {} );
+ } );
+ if( !joCall )
+ return;
+ await doSignConfigureChainAccessParams( optsSignOperation );
+ const joParams: THandleVerifyAndSignCallDataParams = {
+ direction: optsSignOperation.strDirection,
+ startMessageIdx: optsSignOperation.nIdxCurrentMsgBlockStart,
+ dstChainName: optsSignOperation.targetChainName,
+ srcChainName: optsSignOperation.fromChainName,
+ dstChainID: optsSignOperation.targetChainID.toString(),
+ srcChainID: optsSignOperation.fromChainID.toString(),
+ messages: optsSignOperation.jarrMessages,
+ qa: {
+ skaledNumber: owaspUtils.toInteger( i ),
+ sequenceId: optsSignOperation.sequenceId,
+ ts: log.generateTimestampString( null, false )
+ }
+ };
+ optsSignOperation.details.trace(
+ "{p}{} Will invoke {bright} to node #{} via {url} for transfer from chain {} " +
+ "to chain {} with params {}, sequence ID is {}", optsSignOperation.strLogPrefix,
+ log.generateTimestampString( null, true ), "skale_imaVerifyAndSign", i, strNodeURL,
+ optsSignOperation.fromChainName, optsSignOperation.targetChainName,
+ joParams, optsSignOperation.sequenceId );
+ const joIn: any = { method: "skale_imaVerifyAndSign", params: joParams };
+ const joOut = await joCall.call( joIn );
+ await doSignProcessHandleCall( optsSignOperation, joParams, joCall, joIn, joOut, i );
+}
+
+async function doSignMessagesImpl(
+ nTransferLoopCounter: number, strDirection: string,
+ jarrMessages: any[], nIdxCurrentMsgBlockStart: number, strFromChainName: string,
+ joExtraSignOpts?: loop.TExtraSignOpts | null, fn?: IMA.TFunctionAfterSigningMessages
+): Promise < void > {
+ const optsSignOperation: TSignOperationOptions = {
+ imaState: state.get(),
+ nTransferLoopCounter,
+ strDirection,
+ jarrMessages,
+ nIdxCurrentMsgBlockStart,
+ strFromChainName,
+ joExtraSignOpts,
+ // eslint-disable-next-line n/handle-callback-err
+ fn: fn ?? async function(
+ err: Error | string | null, jarrMessages: any[], joGlueResult: any | null
+ ): Promise < void > {},
+ bHaveResultReportCalled: false,
+ strLogPrefix: "",
+ strLogPrefixA: "",
+ strLogPrefixB: "",
+ joGatheringTracker: {
+ nCountReceivedPrevious: 0,
+ nCountReceived: 0,
+ nCountErrors: 0,
+ nCountSkipped: 0,
+ nWaitIntervalStepMilliseconds: 500,
+ nWaitIntervalMaxSteps: 10 * 60 * 3 // 10 is 1 second
+ },
+ arrSignResults: [],
+ details: log.globalStream(),
+ strGatheredDetailsName: "",
+ sequenceId: "",
+ jarrNodes: [],
+ nThreshold: 1,
+ nParticipants: 1,
+ nCountOfBlsPartsToCollect: 1,
+ errGathering: null,
+ targetChainName: "",
+ fromChainName: "",
+ targetChainID: -4,
+ fromChainID: -4
+ };
+ optsSignOperation.strLogPrefix = `${optsSignOperation.strDirection}/#` +
+ `${optsSignOperation.nTransferLoopCounter} Sign msgs via ` +
+ `${optsSignOperation.imaState.isCrossImaBlsMode ? "IMA agent" : "skaled"}: `;
+ optsSignOperation.joGatheringTracker = {
+ nCountReceivedPrevious: 0,
+ nCountReceived: 0,
+ nCountErrors: 0,
+ nCountSkipped: 0,
+ nWaitIntervalStepMilliseconds: 500,
+ nWaitIntervalMaxSteps: 10 * 60 * 3 // 10 is 1 second
+ };
+ optsSignOperation.details = optsSignOperation.imaState.isDynamicLogInBlsSigner
+ ? log.globalStream()
+ : log.createMemoryStream();
+ optsSignOperation.strGatheredDetailsName = optsSignOperation.strDirection + "-" +
+ "doSignMessagesImpl-#" + optsSignOperation.nTransferLoopCounter +
+ "-" + optsSignOperation.strFromChainName + "-msg#" +
+ optsSignOperation.nIdxCurrentMsgBlockStart;
+ try {
+ if( !( await prepareSignMessagesImpl( optsSignOperation ) ) )
+ return;
+ for( let i = 0; i < optsSignOperation.jarrNodes.length; ++i ) {
+ const cntSuccess = optsSignOperation.arrSignResults.length;
+ if( cntSuccess >= optsSignOperation.nCountOfBlsPartsToCollect ) {
+ optsSignOperation.details.trace(
+ "{p}{} Stop invoking {bright} for transfer from chain {} at #{} because " +
+ "successfully gathered count is reached ", optsSignOperation.strLogPrefix,
+ log.generateTimestampString( null, true ), "skale_imaVerifyAndSign",
+ strFromChainName, i, cntSuccess );
+ break;
+ }
+ doSignProcessOneImpl( i, optsSignOperation )
+ .then( function(): void {} ).catch( function( err ): void {
+ log.error(
+ "Failed single BLS sign processing, reported error is: {err}", err );
+ } );
+ }
+ await gatherSigningStartImpl( optsSignOperation );
+ await gatherSigningFinishImpl( optsSignOperation );
+ } catch ( err ) {
+ if( optsSignOperation.details ) {
+ optsSignOperation.details.critical( "Failed BLS sign due to generic " +
+ "flow exception: {err}, stack is:\n{stack}", err, err );
+ }
+ if( !optsSignOperation.bHaveResultReportCalled ) {
+ optsSignOperation.bHaveResultReportCalled = true;
+ await optsSignOperation.fn( "Failed BLS sign due to exception: " +
+ `${owaspUtils.extractErrorMessage( err )}`, optsSignOperation.jarrMessages, null
+ ).catch( function( err: Error | string ): void {
+ log.critical( "Failed BLS sign due to error-reporting callback exception: {err}",
+ err );
+ if( optsSignOperation.details ) {
+ optsSignOperation.details.critical(
+ "Failed BLS sign due to error-reporting callback exception: {err}",
+ err );
+ optsSignOperation.details.exposeDetailsTo(
+ log.globalStream(), optsSignOperation.strGatheredDetailsName, false );
+ optsSignOperation.details.close();
+ }
+ } );
+ }
+ }
+ optsSignOperation.details.success( "{p} completed", optsSignOperation.strGatheredDetailsName );
+ if( optsSignOperation.details ) {
+ optsSignOperation.details.exposeDetailsTo(
+ log.globalStream(), optsSignOperation.strGatheredDetailsName, true );
+ optsSignOperation.details.close();
+ }
+}
+
+export async function doSignMessagesM2S(
+ nTransferLoopCounter: number,
+ jarrMessages: any[], nIdxCurrentMsgBlockStart: number, strFromChainName: string,
+ joExtraSignOpts?: loop.TExtraSignOpts | null, fn?: IMA.TFunctionAfterSigningMessages
+): Promise < void > {
+ await doSignMessagesImpl(
+ nTransferLoopCounter, "M2S",
+ jarrMessages, nIdxCurrentMsgBlockStart, strFromChainName,
+ joExtraSignOpts, fn );
+}
+
+export async function doSignMessagesS2M(
+ nTransferLoopCounter: number,
+ jarrMessages: any[], nIdxCurrentMsgBlockStart: number, strFromChainName: string,
+ joExtraSignOpts?: loop.TExtraSignOpts | null, fn?: IMA.TFunctionAfterSigningMessages
+): Promise < void > {
+ await doSignMessagesImpl(
+ nTransferLoopCounter, "S2M",
+ jarrMessages, nIdxCurrentMsgBlockStart, strFromChainName,
+ joExtraSignOpts, fn );
+}
+
+export async function doSignMessagesS2S(
+ nTransferLoopCounter: number,
+ jarrMessages: any[], nIdxCurrentMsgBlockStart: number, strFromChainName: string,
+ joExtraSignOpts?: loop.TExtraSignOpts | null, fn?: IMA.TFunctionAfterSigningMessages
+): Promise < void > {
+ await doSignMessagesImpl(
+ nTransferLoopCounter, "S2S",
+ jarrMessages, nIdxCurrentMsgBlockStart, strFromChainName,
+ joExtraSignOpts, fn );
+}
+
+async function prepareSignU256( optsSignU256: TSignU256Options ): Promise < boolean > {
+ optsSignU256.details.debug( "{p}Will sign {} value...",
+ optsSignU256.strLogPrefix, optsSignU256.u256 );
+ optsSignU256.details.trace( "{p}Will query to sign {} skaled node(s)...",
+ optsSignU256.strLogPrefix, optsSignU256.jarrNodes.length );
+ if( !optsSignU256.imaState.joSChainNetworkInfo )
+ throw new Error( "No own S-Chain network information" );
+ optsSignU256.nThreshold = discoverBlsThreshold( optsSignU256.imaState.joSChainNetworkInfo );
+ optsSignU256.nParticipants =
+ discoverBlsParticipants( optsSignU256.imaState.joSChainNetworkInfo );
+ optsSignU256.details.trace( "{p}Discovered BLS threshold is {}.",
+ optsSignU256.strLogPrefix, optsSignU256.nThreshold );
+ optsSignU256.details.trace( "{p}Discovered number of BLS participants is {}.",
+ optsSignU256.strLogPrefix, optsSignU256.nParticipants );
+ if( !checkBlsThresholdAndBlsParticipants(
+ optsSignU256.nThreshold,
+ optsSignU256.nParticipants,
+ "prepare sign-U256",
+ optsSignU256.details ) ) {
+ await optsSignU256.fn(
+ "signature error(1, u256), S-Chain information " +
+ "was not discovered properly and BLS threshold/participants are unknown",
+ optsSignU256.u256, null );
+ return false;
+ }
+ optsSignU256.nCountOfBlsPartsToCollect = optsSignU256.nThreshold;
+ optsSignU256.details.trace( "{p}Will(optsSignU256.u256) collect {} from {} nodes",
+ optsSignU256.strLogPrefix, optsSignU256.nCountOfBlsPartsToCollect,
+ optsSignU256.jarrNodes.length );
+ return true;
+}
+
+async function doSignU256OneImplHandleCallResult(
+ i: number, optsSignU256: TSignU256Options, joCall: rpcCall.TRPCCall, joIn: any, joOut: any
+): Promise < void > {
+ const imaState: state.TIMAState = state.get();
+ const isThisNode = ( i == imaState.nNodeNumber );
+ const joNode = optsSignU256.jarrNodes[i];
+ const strNodeURL = optsSignU256.imaState.isCrossImaBlsMode
+ ? imaUtils.composeImaAgentNodeUrl( joNode, isThisNode )
+ : imaUtils.composeSChainNodeUrl( joNode );
+ const strNodeDescColorized = log.fmtDebug( "{url} ({}/{}, ID {})",
+ strNodeURL, i, optsSignU256.jarrNodes.length, joNode.nodeID );
+ ++optsSignU256.joGatheringTracker.nCountReceived;
+ optsSignU256.details.trace( "{p}Did invoked {} for to sign value {}, answer is: {}",
+ optsSignU256.strLogPrefix, "skale_imaBSU256", optsSignU256.u256.toString(), joOut );
+ if( ( !joOut ) || typeof joOut !== "object" || ( !( "result" in joOut ) ) ||
+ ( "error" in joOut && joOut.error ) ||
+ ( !joOut.result ) || typeof joOut.result !== "object" ||
+ ( !( "signature" in joOut.result ) ) || joOut.result.signature != "object"
+ ) {
+ ++optsSignU256.joGatheringTracker.nCountErrors;
+ const strErrorMessage =
+ owaspUtils.extractErrorMessage( joOut, "unknown wallet error(2)" );
+ optsSignU256.details.error( "{p}S-Chain node {} reported wallet error: {err}",
+ optsSignU256.strLogPrefix, strNodeDescColorized, strErrorMessage );
+ await joCall.disconnect();
+ return;
+ }
+ optsSignU256.details.trace( "{p}Node {} sign result: ",
+ optsSignU256.strLogPrefix, joNode.nodeID, joOut.result ? joOut.result : null );
+ try {
+ if( joOut.result.signResult.signatureShare.length > 0 &&
+ joOut.result.signResult.status === 0
+ ) {
+ const nZeroBasedNodeIndex = joNode.imaInfo.thisNodeIndex - 1;
+ // partial BLS verification for one participant
+ let bNodeSignatureOKay = false; // initially assume signature is wrong
+ const strLogPrefixA = `BLS/#${nZeroBasedNodeIndex}: `;
+ try {
+ const cntSuccess = optsSignU256.arrSignResults.length;
+ if( cntSuccess > optsSignU256.nCountOfBlsPartsToCollect ) {
+ ++optsSignU256.joGatheringTracker.nCountSkipped;
+ optsSignU256.details.notice(
+ "{p}Will ignore sign result for node {} because {}/{} threshold " +
+ "number of BLS signature parts already gathered", strLogPrefixA,
+ nZeroBasedNodeIndex, optsSignU256.nThreshold,
+ optsSignU256.nCountOfBlsPartsToCollect );
+ return;
+ }
+ const arrTmp = joOut.result.signResult.signatureShare.split( ":" );
+ const joResultFromNode: any = {
+ index: nZeroBasedNodeIndex.toString(),
+ signature: { X: arrTmp[0], Y: arrTmp[1] }
+ };
+ optsSignU256.details.trace( "{p}Will verify sign result for node {}",
+ strLogPrefixA, nZeroBasedNodeIndex );
+ if( !optsSignU256.imaState.joSChainNetworkInfo )
+ throw new Error( "No own S-Chain network information" );
+ const joPublicKey = discoverPublicKeyByIndex( nZeroBasedNodeIndex,
+ optsSignU256.imaState.joSChainNetworkInfo, optsSignU256.details,
+ true );
+ if( !joPublicKey )
+ throw new Error( `No BLS public key for node ${nZeroBasedNodeIndex}` );
+ if( performBlsVerifyIU256(
+ optsSignU256.details, nZeroBasedNodeIndex, joResultFromNode,
+ optsSignU256.u256, joPublicKey ) ) {
+ optsSignU256.details.success(
+ "{p}Got successful BLS verification result for node {} " +
+ "with index {}", strLogPrefixA, joNode.nodeID,
+ nZeroBasedNodeIndex );
+ bNodeSignatureOKay = true; // node verification passed
+ } else {
+ optsSignU256.details.error( "{p} BLS u256 one node verify failed",
+ strLogPrefixA );
+ }
+ } catch ( err ) {
+ optsSignU256.details.critical(
+ "{p}S-Chain node {} sign CRITICAL ERROR: partial signature fail from " +
+ "with index {}, error is {err}, stack is:\n{stack}",
+ strLogPrefixA, strNodeDescColorized, nZeroBasedNodeIndex,
+ err, err );
+ }
+ if( bNodeSignatureOKay ) {
+ optsSignU256.arrSignResults.push( {
+ index: nZeroBasedNodeIndex.toString(),
+ signature:
+ splitSignatureShare( joOut.result.signResult.signatureShare ),
+ fromNode: joNode, // extra, not needed for bls_glue
+ signResult: joOut.result.signResult
+ } );
+ } else
+ ++optsSignU256.joGatheringTracker.nCountErrors;
+ }
+ } catch ( err ) {
+ ++optsSignU256.joGatheringTracker.nCountErrors;
+ optsSignU256.details.critical(
+ "{p}S-Chain node {} signature fail from node {}, error is {err}, " +
+ "stack is:\n{stack}", optsSignU256.strLogPrefix,
+ strNodeDescColorized, joNode.nodeID, err, err );
+ }
+ await joCall.disconnect();
+}
+
+async function doSignU256OneImpl(
+ i: number, optsSignU256: TSignU256Options ): Promise < boolean> {
+ const imaState: state.TIMAState = state.get();
+ const isThisNode = ( i == imaState.nNodeNumber );
+ const joNode = optsSignU256.jarrNodes[i];
+ const strNodeURL = optsSignU256.imaState.isCrossImaBlsMode
+ ? imaUtils.composeImaAgentNodeUrl( joNode, isThisNode )
+ : imaUtils.composeSChainNodeUrl( joNode );
+ const strNodeDescColorized = log.fmtDebug( "{url} ({}/{}, ID {})",
+ strNodeURL, i, optsSignU256.jarrNodes.length, joNode.nodeID );
+ const rpcCallOpts: rpcCall.TRPCCallOpts | null = null;
+ let joCall: rpcCall.TRPCCall | null = null;
+ try {
+ joCall = await rpcCall.create( strNodeURL, rpcCallOpts );
+ if( !joCall )
+ throw new Error( `Failed to create JSON RPC call object to ${strNodeURL}` );
+ optsSignU256.details.trace( "{p}Will invoke skale_imaBSU256 for to sign value {}",
+ optsSignU256.strLogPrefix, optsSignU256.u256.toString() );
+ const joIn: any = {
+ method: "skale_imaBSU256",
+ params: {
+ valueToSign: optsSignU256.u256 // must be 0x string, came from outside 0x string
+ }
+ };
+ const joOut = await joCall.call( joIn );
+ await doSignU256OneImplHandleCallResult( i, optsSignU256, joCall, joIn, joOut );
+ return true;
+ } catch ( err ) {
+ ++optsSignU256.joGatheringTracker.nCountReceived;
+ ++optsSignU256.joGatheringTracker.nCountErrors;
+ optsSignU256.details.error(
+ "{p}JSON RPC call(doSignU256OneImpl) to S-Chain node {} failed, RPC call was " +
+ "not created, error is: {err}",
+ optsSignU256.strLogPrefix, strNodeDescColorized, err );
+ if( joCall )
+ await joCall.disconnect();
+ return true;
+ }
+}
+
+async function gatherSigningCheckFinish256(
+ optsSignU256: TSignU256Options ): Promise < boolean > {
+ const cntSuccess = optsSignU256.arrSignResults.length;
+ if( optsSignU256.joGatheringTracker.nCountReceivedPrevious !=
+ optsSignU256.joGatheringTracker.nCountReceived ) {
+ optsSignU256.details.debug(
+ "BLS u256 - BLS signature gathering progress updated, now have {} BLS parts " +
+ "of needed {} arrived, have {} success(es) and {} error(s)",
+ optsSignU256.joGatheringTracker.nCountReceived,
+ optsSignU256.nCountOfBlsPartsToCollect, cntSuccess,
+ optsSignU256.joGatheringTracker.nCountErrors );
+ optsSignU256.joGatheringTracker.nCountReceivedPrevious =
+ owaspUtils.toInteger( optsSignU256.joGatheringTracker.nCountReceived );
+ }
+ if( cntSuccess < optsSignU256.nCountOfBlsPartsToCollect )
+ return false;
+ const strLogPrefixB = "BLS u256/Summary: ";
+ const strError = null;
+ const joGlueResult = performBlsGlueU256(
+ optsSignU256.details, optsSignU256.u256, optsSignU256.arrSignResults );
+ if( joGlueResult ) {
+ optsSignU256.details.success( "{p}Got BLS glue u256 result: {}",
+ strLogPrefixB, joGlueResult );
+ if( optsSignU256.imaState.strPathBlsVerify.length > 0 ) {
+ if( !optsSignU256.imaState.joSChainNetworkInfo )
+ throw new Error( "No own S-Chain network information" );
+ const joCommonPublicKey = discoverCommonPublicKey(
+ optsSignU256.details, optsSignU256.imaState.joSChainNetworkInfo, false );
+ if( !joCommonPublicKey ) {
+ if( !optsSignU256.imaState.joSChainNetworkInfo )
+ throw new Error( "No own S-Chain network information" );
+ const strError = "No BLS common public key";
+ optsSignU256.details.error( "{p}{}", strLogPrefixB, strError );
+ } else if( performBlsVerifyU256( optsSignU256.details, joGlueResult,
+ optsSignU256.u256, joCommonPublicKey ) ) {
+ const strSuccessfulResultDescription =
+ "Got successful summary BLS u256 verification result";
+ optsSignU256.details.success( "{p}{}", strLogPrefixB,
+ strSuccessfulResultDescription );
+ } else {
+ const strError = "BLS verification failed";
+ optsSignU256.details.error( "{p}BLS verification failure:{}",
+ strLogPrefixB, strError );
+ }
+ }
+ } else {
+ const strError = "BLS u256 glue failed, no glue result arrived";
+ optsSignU256.details.error(
+ "{p}Problem(1) in BLS u256 sign result handler: {err}",
+ strLogPrefixB, strError );
+ }
+ optsSignU256.details.trace(
+ "Will call signed-256 answer-sending callback {}, u256 is {}, " +
+ "glue result is {}", strError ? ( " with error " + log.fmtError( "{err}", strError ) ) : "",
+ optsSignU256.u256, joGlueResult );
+ optsSignU256.fn( strError, optsSignU256.u256, joGlueResult )
+ .catch( function( err: Error | string ): void {
+ optsSignU256.details.critical(
+ "Problem(2) in BLS u256 sign result handler: {err}", err );
+ optsSignU256.errGathering = "Problem(2) in BLS u256 sign result " +
+ `handler: ${owaspUtils.extractErrorMessage( err )}`;
+ } );
+ return true;
+}
+
+async function gatherSigningCheckOverflow256(
+ optsSignU256: TSignU256Options ): Promise < boolean > {
+ if( optsSignU256.joGatheringTracker.nCountReceived < optsSignU256.jarrNodes.length )
+ return false;
+ optsSignU256.fn(
+ "signature error(2, u256), got " +
+ `${optsSignU256.joGatheringTracker.nCountErrors} errors(s) for ` +
+ `${optsSignU256.jarrNodes.length} node(s)`, optsSignU256.u256, null
+ ).catch( function( err: Error | string ): void {
+ const cntSuccess = optsSignU256.arrSignResults.length;
+ optsSignU256.details.critical(
+ "Problem(3) in BLS u256 sign result handler, not enough successful BLS " +
+ "signature parts({} when all attempts done, error details: {err}",
+ cntSuccess, err );
+ optsSignU256.errGathering = "Problem(3) in BLS u256 sign result handler, not " +
+ `enough successful BLS signature parts(${cntSuccess} when all attempts ` +
+ `done, error details: ${owaspUtils.extractErrorMessage( err )}`;
+ } );
+ return true;
+}
+
+async function doSignU256Gathering( optsSignU256: TSignU256Options ): Promise < void > {
+ optsSignU256.details.debug( "{p}Waiting for BLS glue result ", optsSignU256.strLogPrefix );
+ optsSignU256.errGathering = null;
+ for( let idxStep = 0; idxStep < optsSignU256.joGatheringTracker.nWaitIntervalMaxSteps;
+ ++idxStep ) {
+ await threadInfo.sleep(
+ optsSignU256.joGatheringTracker.nWaitIntervalStepMilliseconds );
+ if( await gatherSigningCheckFinish256( optsSignU256 ) )
+ return;
+ if( await gatherSigningCheckOverflow256( optsSignU256 ) )
+ return;
+ }
+ // timeout
+ optsSignU256.fn(
+ "signature error(3, u256), got " +
+ `${optsSignU256.joGatheringTracker.nCountErrors} errors(s) for ` +
+ `${optsSignU256.jarrNodes.length} node(s)`,
+ optsSignU256.u256, null
+ ).catch( function( err: Error | string ): void {
+ const cntSuccess = optsSignU256.arrSignResults.length;
+ optsSignU256.details.error(
+ "Problem(4) in BLS u256 sign result handler, not enough successful BLS " +
+ "signature parts({}) and timeout reached, error details: {err",
+ cntSuccess, err );
+ optsSignU256.errGathering = "Problem(4) in BLS u256 sign result handler, not " +
+ `enough successful BLS signature parts(${cntSuccess}) and timeout ` +
+ `reached, error details: ${owaspUtils.extractErrorMessage( err )}`;
+ } );
+}
+
+export async function doSignU256( u256: any, details: log.TLogger,
+ fn: IMA.TFunctionAfterSigningMessages ): Promise < void > {
+ const optsSignU256: TSignU256Options = {
+ u256,
+ fn,
+ details,
+ imaState: state.get(),
+ strLogPrefix: "Sign u256: ",
+ joGatheringTracker: {
+ nCountReceivedPrevious: 0,
+ nCountReceived: 0,
+ nCountErrors: 0,
+ nCountSkipped: 0,
+ nWaitIntervalStepMilliseconds: 500,
+ nWaitIntervalMaxSteps: 10 * 60 * 3 // 10 is 1 second
+ },
+ arrSignResults: [],
+ jarrNodes: [],
+ nThreshold: 1,
+ nParticipants: 1,
+ nCountOfBlsPartsToCollect: 1,
+ errGathering: null
+ };
+ if( !optsSignU256.imaState.joSChainNetworkInfo )
+ throw new Error( "No own S-Chain network information" );
+ optsSignU256.jarrNodes = optsSignU256.imaState.joSChainNetworkInfo.network;
+ optsSignU256.details.trace( "{p}Invoking signing u256 procedure...",
+ optsSignU256.strLogPrefix );
+ optsSignU256.fn = optsSignU256.fn || function(): void {};
+ if( !(
+ optsSignU256.imaState.strPathBlsGlue.length > 0 &&
+ optsSignU256.imaState.joSChainNetworkInfo
+ ) ) {
+ optsSignU256.details.warning( "{p}BLS u256 signing is unavailable",
+ optsSignU256.strLogPrefix );
+ await optsSignU256.fn( "BLS u256 signing is unavailable", optsSignU256.u256, null );
+ return;
+ }
+ if( !( await prepareSignU256( optsSignU256 ) ) )
+ return;
+ for( let i = 0; i < optsSignU256.jarrNodes.length; ++i )
+ await doSignU256OneImpl( i, optsSignU256 );
+ await doSignU256Gathering( optsSignU256 );
+ optsSignU256.details.trace( "Will await BLS u256 sign result..." );
+ if( optsSignU256.errGathering ) {
+ optsSignU256.details.error( "Failed BLS u256 sign result awaiting: {err}",
+ optsSignU256.errGathering.toString() );
+ return;
+ }
+ optsSignU256.details.information( "{p}Completed signing u256 procedure",
+ optsSignU256.strLogPrefix );
+}
+
+export async function doVerifyReadyHash(
+ strMessageHash: string, nZeroBasedNodeIndex: number,
+ signature: any, isExposeOutput: boolean
+): Promise < boolean > {
+ const imaState: state.TIMAState = state.get();
+ const strDirection = "RAW";
+ const strLogPrefix = `${strDirection}/BLS/#${nZeroBasedNodeIndex}: `;
+ const details = log.createMemoryStream();
+ let isSuccess = false;
+ const arrTmp = signature.signatureShare.split( ":" );
+ const joResultFromNode: any = {
+ index: nZeroBasedNodeIndex.toString(),
+ signature: {
+ X: arrTmp[0],
+ Y: arrTmp[1]
+ }
+ };
+ if( !imaState.joSChainNetworkInfo )
+ throw new Error( "No own S-Chain network information" );
+ const nThreshold = discoverBlsThreshold( imaState.joSChainNetworkInfo );
+ const nParticipants = discoverBlsParticipants( imaState.joSChainNetworkInfo );
+ if( !checkBlsThresholdAndBlsParticipants(
+ nThreshold, nParticipants, "verify ready hash", details ) )
+ return false;
+ const strActionDir = allocBlsTmpActionDir();
+ const fnShellRestore = function(): void { shell.rm( "-rf", strActionDir ); };
+ let strOutput = "";
+ try {
+ const joPublicKey = discoverPublicKeyByIndex(
+ nZeroBasedNodeIndex, imaState.joSChainNetworkInfo, details, true );
+ details.trace( "{p}BLS node #{} - hashed verify message is {}",
+ strLogPrefix, nZeroBasedNodeIndex, strMessageHash );
+ const joMsg = { message: strMessageHash };
+ details.debug( "{p}BLS node #{} - composed {} using hash {} and glue {} and public key {}",
+ strLogPrefix, nZeroBasedNodeIndex, joMsg, strMessageHash,
+ joResultFromNode, joPublicKey );
+ const strSignResultFileName =
+ strActionDir + "/sign-result" + nZeroBasedNodeIndex + ".json";
+ imaUtils.jsonFileSave( strSignResultFileName, joResultFromNode );
+ imaUtils.jsonFileSave( strActionDir + "/hash.json", joMsg );
+ imaUtils.jsonFileSave(
+ strActionDir + "/BLS_keys" + nZeroBasedNodeIndex + ".json", joPublicKey );
+ const strVerifyCommand =
+ imaState.strPathBlsVerify +
+ " --t " + nThreshold +
+ " --n " + nParticipants +
+ " --j " + nZeroBasedNodeIndex +
+ " --input " + strSignResultFileName;
+ details.trace( "{p}Will execute node #{} BLS verify command: {}",
+ strLogPrefix, nZeroBasedNodeIndex, strVerifyCommand );
+ strOutput = childProcessModule.execSync( strVerifyCommand, { cwd: strActionDir } )
+ .toString( "utf8" );
+ details.trace( "{p}BLS node #{} verify output is:\n{raw}", strLogPrefix,
+ nZeroBasedNodeIndex, strOutput || "<>" );
+ details.success( "{p}BLS node #{} verify success", strLogPrefix, nZeroBasedNodeIndex );
+ fnShellRestore();
+ isSuccess = true;
+ } catch ( err ) {
+ details.critical( "{p}BLS node #{} verify error, error description is: {err}" +
+ ", stack is:\n{stack}", strLogPrefix, nZeroBasedNodeIndex, err, err );
+ details.critical( "{p}BLS node #{} verify output is:\n{raw}",
+ strLogPrefix, nZeroBasedNodeIndex, strOutput || "<>" );
+ fnShellRestore();
+ isSuccess = false;
+ }
+ if( isExposeOutput || ( !isSuccess ) )
+ details.exposeDetailsTo( log.globalStream(), "BLS-raw-verifier", isSuccess );
+ details.close();
+ return isSuccess;
+}
+
+async function doSignReadyHashHandleCallResult(
+ strLogPrefix: string, details: log.TLogger,
+ strMessageHash: string, isExposeOutput: boolean, joCall: rpcCall.TRPCCall,
+ joIn: any, joOut: any
+): Promise < object > {
+ details.trace( "{p}Call to ", "SGX done, answer is: {}", strLogPrefix, joOut );
+ let joSignResult: TSignResult = joOut;
+ if( joOut.result != null && joOut.result != undefined &&
+ typeof joOut.result === "object" )
+ joSignResult = joOut.result;
+ if( joOut.signResult != null && joOut.signResult != undefined &&
+ typeof joOut.signResult === "object" )
+ joSignResult = joOut.signResult;
+ if( !joSignResult ) {
+ const strError = "No signature arrived";
+ details.error( "{p}BLS-sign(1) finished with error: {err}", strLogPrefix, strError );
+ await joCall.disconnect();
+ throw new Error( strError );
+ }
+ if( "errorMessage" in joSignResult &&
+ typeof joSignResult.errorMessage === "string" &&
+ joSignResult.errorMessage.length > 0
+ ) {
+ const strError = `BLS-sign finished with error: ${joSignResult.errorMessage};`;
+ details.error( "{p}BLS-sign(2) finished with error: {err}",
+ strLogPrefix, joSignResult.errorMessage );
+ await joCall.disconnect();
+ throw new Error( strError );
+ }
+ joSignResult.error = null;
+ await joCall.disconnect();
+ return joSignResult;
+}
+
+export async function doSignReadyHash(
+ strMessageHash: string, isExposeOutput: any ): Promise < TSignResult | null > {
+ const imaState: state.TIMAState = state.get();
+ const strLogPrefix = "";
+ const details: log.TLogger = log.createMemoryStream();
+ let joSignResult: TSignResult | null = null;
+ let joCall: rpcCall.TRPCCall | null = null;
+ try {
+ if( !imaState.joSChainNetworkInfo )
+ throw new Error( "No own S-Chain network information" );
+ const nThreshold = discoverBlsThreshold( imaState.joSChainNetworkInfo );
+ const nParticipants = discoverBlsParticipants( imaState.joSChainNetworkInfo );
+ details.debug( "{p}Will BLS-sign ready hash.", strLogPrefix );
+ details.trace( "{p}Discovered BLS threshold is {}.", strLogPrefix, nThreshold );
+ details.trace( "{p}Discovered number of BLS participants is {}.",
+ strLogPrefix, nParticipants );
+ details.trace( "{p}hash value to sign is {}", strLogPrefix, strMessageHash );
+ if( !checkBlsThresholdAndBlsParticipants(
+ nThreshold, nParticipants, "sign ready hash", details ) )
+ return null;
+ let joAccount: state.TAccount = imaState.chainProperties.sc.joAccount;
+ if( !joAccount.strSgxURL ) {
+ joAccount = imaState.chainProperties.mn.joAccount;
+ if( !joAccount.strSgxURL )
+ throw new Error( "SGX URL is unknown, cannot sign U256" );
+ if( !joAccount.strBlsKeyName )
+ throw new Error( "BLS keys name is unknown, cannot sign U256" );
+ }
+ let rpcCallOpts: rpcCall.TRPCCallOpts | null = null;
+ if( "strPathSslKey" in joAccount && typeof joAccount.strPathSslKey === "string" &&
+ joAccount.strPathSslKey.length > 0 && "strPathSslCert" in joAccount &&
+ typeof joAccount.strPathSslCert === "string" && joAccount.strPathSslCert.length > 0
+ ) {
+ rpcCallOpts = {
+ cert: fs.readFileSync( joAccount.strPathSslCert, "utf8" ),
+ key: fs.readFileSync( joAccount.strPathSslKey, "utf8" )
+ };
+ } else
+ details.warning( "Will sign via SGX without SSL options" );
+ joCall = await rpcCall.create( joAccount.strSgxURL, rpcCallOpts );
+ if( !joCall )
+ throw new Error( `Failed to create JSON RPC call object to ${joAccount.strSgxURL}` );
+ const joIn: any = {
+ jsonrpc: "2.0",
+ id: utils.randomCallID(),
+ method: "blsSignMessageHash",
+ params: {
+ keyShareName: joAccount.strBlsKeyName,
+ messageHash: strMessageHash,
+ n: nParticipants,
+ t: nThreshold
+ }
+ };
+ details.trace( "{p}Will invoke SGX with call data {}", strLogPrefix, joIn );
+ const joOut = await joCall.call( joIn );
+ joSignResult = await doSignReadyHashHandleCallResult(
+ strLogPrefix, details, strMessageHash, isExposeOutput, joCall, joIn, joOut );
+ } catch ( err ) {
+ const strError = owaspUtils.extractErrorMessage( err );
+ joSignResult = { };
+ joSignResult.error = strError;
+ details.error( "{p}JSON RPC call to SGX failed, error is: {err}, stack is:\n{stack}",
+ strLogPrefix, err, err );
+ if( joCall )
+ await joCall.disconnect();
+ }
+ const isSuccess = !!( (
+ joSignResult && typeof joSignResult === "object" && ( !joSignResult.error ) ) );
+ if( isExposeOutput || ( !isSuccess ) )
+ details.exposeDetailsTo( log.globalStream(), "BLS-raw-signer", isSuccess );
+ details.close();
+ return joSignResult;
+}
+
+async function prepareHandlingOfSkaleImaVerifyAndSign(
+ optsHandleVerifyAndSign: THandleVerifyAndSignOptions ): Promise < boolean > {
+ optsHandleVerifyAndSign.details.debug( "{p}Will verify and sign {}",
+ optsHandleVerifyAndSign.strLogPrefix, optsHandleVerifyAndSign.joCallData );
+ optsHandleVerifyAndSign.nIdxCurrentMsgBlockStart =
+ optsHandleVerifyAndSign.joCallData.params.startMessageIdx;
+ optsHandleVerifyAndSign.strFromChainName =
+ optsHandleVerifyAndSign.joCallData.params.srcChainName;
+ optsHandleVerifyAndSign.strToChainName =
+ optsHandleVerifyAndSign.joCallData.params.dstChainName;
+ optsHandleVerifyAndSign.strFromChainID =
+ optsHandleVerifyAndSign.joCallData.params.srcChainID;
+ optsHandleVerifyAndSign.strToChainID =
+ optsHandleVerifyAndSign.joCallData.params.dstChainID;
+ optsHandleVerifyAndSign.strDirection =
+ optsHandleVerifyAndSign.joCallData.params.direction;
+ optsHandleVerifyAndSign.jarrMessages =
+ optsHandleVerifyAndSign.joCallData.params.messages;
+ optsHandleVerifyAndSign.details.trace(
+ "{p}{bright} verification algorithm will work for transfer from chain {}/{} to " +
+ "chain {}/{} and work with array of message(s) {}",
+ optsHandleVerifyAndSign.strLogPrefix, optsHandleVerifyAndSign.strDirection,
+ optsHandleVerifyAndSign.strFromChainName, optsHandleVerifyAndSign.strFromChainID,
+ optsHandleVerifyAndSign.strToChainName, optsHandleVerifyAndSign.strToChainID,
+ optsHandleVerifyAndSign.jarrMessages );
+ if( !optsHandleVerifyAndSign.imaState.joSChainNetworkInfo )
+ throw new Error( "No own S-Chain network information" );
+ optsHandleVerifyAndSign.nThreshold =
+ discoverBlsThreshold( optsHandleVerifyAndSign.imaState.joSChainNetworkInfo );
+ optsHandleVerifyAndSign.nParticipants =
+ discoverBlsParticipants( optsHandleVerifyAndSign.imaState.joSChainNetworkInfo );
+ optsHandleVerifyAndSign.details.debug(
+ "{p}{bright} verification algorithm discovered BLS threshold is {}.",
+ optsHandleVerifyAndSign.strLogPrefix, optsHandleVerifyAndSign.strDirection,
+ optsHandleVerifyAndSign.nThreshold );
+ optsHandleVerifyAndSign.details.debug(
+ "{p}{bright} verification algorithm discovered number of BLS participants is {}.",
+ optsHandleVerifyAndSign.strLogPrefix, optsHandleVerifyAndSign.strDirection,
+ optsHandleVerifyAndSign.nParticipants );
+ if( !checkBlsThresholdAndBlsParticipants(
+ optsHandleVerifyAndSign.nThreshold,
+ optsHandleVerifyAndSign.nParticipants,
+ "prepare handling of skale_imaVerifyAndSign",
+ optsHandleVerifyAndSign.details ) )
+ return false;
+ optsHandleVerifyAndSign.strMessageHash = owaspUtils.removeStarting0x( keccak256Message(
+ optsHandleVerifyAndSign.jarrMessages,
+ optsHandleVerifyAndSign.nIdxCurrentMsgBlockStart,
+ optsHandleVerifyAndSign.strFromChainName
+ ) );
+ optsHandleVerifyAndSign.details.debug(
+ "{p}{bright} verification algorithm message hash to sign is {}",
+ optsHandleVerifyAndSign.strLogPrefix, optsHandleVerifyAndSign.strDirection,
+ optsHandleVerifyAndSign.strMessageHash );
+ return true;
+}
+
+async function prepareS2sOfSkaleImaVerifyAndSign(
+ optsHandleVerifyAndSign: THandleVerifyAndSignOptions ): Promise < void > {
+ const strSChainNameSrc = optsHandleVerifyAndSign.joCallData.params.srcChainName;
+ const strSChainNameDst = optsHandleVerifyAndSign.joCallData.params.dstChainName;
+ optsHandleVerifyAndSign.details.trace(
+ "{p}{bright} verification algorithm will use for source chain name {} and destination " +
+ "chain name {}", optsHandleVerifyAndSign.strLogPrefix,
+ optsHandleVerifyAndSign.strDirection, strSChainNameSrc, strSChainNameDst );
+ const arrSChainsCached = skaleObserver.getLastCachedSChains();
+ if( ( !arrSChainsCached ) || arrSChainsCached.length == 0 ) {
+ throw new Error( `Could not handle ${optsHandleVerifyAndSign.strDirection} ` +
+ "skale_imaVerifyAndSign(1), no S-Chains in SKALE NETWORK observer cached yet, " +
+ "try again later" );
+ }
+
+ let joSChainSrc: any = null; let strUrlSrcSChain: string | null = null;
+ for( let idxSChain = 0; idxSChain < arrSChainsCached.length; ++idxSChain ) {
+ const joSChain = arrSChainsCached[idxSChain];
+ if( joSChain.name.toString() == strSChainNameSrc.toString() ) {
+ joSChainSrc = joSChain;
+ strUrlSrcSChain = skaleObserver.pickRandomSChainUrl( joSChain );
+ break;
+ }
+ }
+ if( joSChainSrc == null || strUrlSrcSChain == null || strUrlSrcSChain.length == 0 ) {
+ throw new Error( `Could not handle ${optsHandleVerifyAndSign.strDirection} ` +
+ "skale_imaVerifyAndSign(2), failed to discover source chain access parameters, " +
+ "try again later" );
+ }
+ optsHandleVerifyAndSign.details.trace(
+ "{p}{bright} verification algorithm discovered source chain URL is {url}, chain name " +
+ "is {}, chain id is {}", optsHandleVerifyAndSign.strLogPrefix,
+ optsHandleVerifyAndSign.strDirection, strUrlSrcSChain,
+ joSChainSrc.name, joSChainSrc.chainId );
+ optsHandleVerifyAndSign.joExtraSignOpts = {
+ ethersProviderSrc: owaspUtils.getEthersProviderFromURL( strUrlSrcSChain ),
+ chainNameSrc: optsHandleVerifyAndSign.strFromChainName,
+ chainNameDst: optsHandleVerifyAndSign.strToChainName,
+ chainIdSrc: optsHandleVerifyAndSign.strFromChainID,
+ chainIdDst: optsHandleVerifyAndSign.strToChainID
+ };
+}
+
+async function handleBlsSignMessageHashResult(
+ optsHandleVerifyAndSign: THandleVerifyAndSignOptions, joCallData: THandleVerifyAndSignCallData,
+ joAccount: state.TAccount, joCall: rpcCall.TRPCCall, joIn: any, joOut: any
+): Promise < TSignResult > {
+ optsHandleVerifyAndSign.details.trace( "{p}{bright} Call to SGX done, " +
+ "answer is: {}", optsHandleVerifyAndSign.strLogPrefix,
+ optsHandleVerifyAndSign.strDirection, joOut );
+ let joSignResult: TSignResult = joOut;
+ if( joOut.result != null && joOut.result != undefined &&
+ typeof joOut.result === "object" )
+ joSignResult = joOut.result;
+ if( joOut.signResult != null && joOut.signResult != undefined &&
+ typeof joOut.signResult === "object" )
+ joSignResult = joOut.signResult;
+ if( "qa" in optsHandleVerifyAndSign.joCallData.params )
+ optsHandleVerifyAndSign.joRetVal.qa = optsHandleVerifyAndSign.joCallData.params.qa;
+ if( !joSignResult ) {
+ const strError = "No signature arrived";
+ optsHandleVerifyAndSign.joRetVal.error = strError;
+ optsHandleVerifyAndSign.details.error(
+ "{p}BLS-sign(1) finished with error: {err}",
+ optsHandleVerifyAndSign.strLogPrefix, strError );
+ await joCall.disconnect();
+ throw new Error( strError );
+ }
+ if( "errorMessage" in joSignResult &&
+ typeof joSignResult.errorMessage === "string" &&
+ joSignResult.errorMessage.length > 0
+ ) {
+ optsHandleVerifyAndSign.isSuccess = false;
+ const strError = `BLS-sign finished with error: ${joSignResult.errorMessage};`;
+ optsHandleVerifyAndSign.joRetVal.error = strError;
+ optsHandleVerifyAndSign.details.error(
+ "{p}BLS-sign(2) finished with error: {err}",
+ optsHandleVerifyAndSign.strLogPrefix, joSignResult.errorMessage );
+ await joCall.disconnect();
+ throw new Error( strError );
+ }
+ optsHandleVerifyAndSign.isSuccess = true;
+ optsHandleVerifyAndSign.joRetVal.result = { signResult: joSignResult };
+ if( "qa" in optsHandleVerifyAndSign.joCallData.params )
+ optsHandleVerifyAndSign.joRetVal.qa = optsHandleVerifyAndSign.joCallData.params.qa;
+ await joCall.disconnect();
+ return joSignResult;
+}
+
+export async function handleSkaleImaVerifyAndSign(
+ joCallData: THandleVerifyAndSignCallData ): Promise < object | null > {
+ const optsHandleVerifyAndSign: THandleVerifyAndSignOptions = {
+ joCallData,
+ imaState: state.get(),
+ strLogPrefix: "",
+ details: log.createMemoryStream(),
+ joRetVal: {},
+ isSuccess: false,
+ nIdxCurrentMsgBlockStart: 0,
+ strFromChainName: "",
+ strToChainName: "",
+ strFromChainID: "",
+ strToChainID: "",
+ strDirection: "",
+ jarrMessages: [],
+ strMessageHash: "",
+ joExtraSignOpts: null,
+ nThreshold: 1,
+ nParticipants: 1
+ };
+ let joCall: rpcCall.TRPCCall | null = null;
+ try {
+ if( !( await prepareHandlingOfSkaleImaVerifyAndSign( optsHandleVerifyAndSign ) ) )
+ return null;
+ optsHandleVerifyAndSign.joExtraSignOpts = null;
+ if( optsHandleVerifyAndSign.strDirection == "S2S" )
+ await prepareS2sOfSkaleImaVerifyAndSign( optsHandleVerifyAndSign );
+
+ await checkCorrectnessOfMessagesToSign(
+ optsHandleVerifyAndSign.details, optsHandleVerifyAndSign.strLogPrefix,
+ optsHandleVerifyAndSign.strDirection, optsHandleVerifyAndSign.jarrMessages,
+ optsHandleVerifyAndSign.nIdxCurrentMsgBlockStart,
+ optsHandleVerifyAndSign.joExtraSignOpts
+ );
+ optsHandleVerifyAndSign.details.debug( "{p}Will BLS-sign verified messages.",
+ optsHandleVerifyAndSign.strLogPrefix );
+ let joAccount = optsHandleVerifyAndSign.imaState.chainProperties.sc.joAccount;
+ if( !joAccount.strSgxURL ) {
+ joAccount = optsHandleVerifyAndSign.imaState.chainProperties.mn.joAccount;
+ if( !joAccount.strSgxURL )
+ throw new Error( "SGX URL is unknown, cannot sign(handle) IMA message(s)" );
+ if( !joAccount.strBlsKeyName )
+ throw new Error( "BLS keys name is unknown, cannot sign IMA message(s)" );
+ }
+ let rpcCallOpts: rpcCall.TRPCCallOpts | null = null;
+ if( "strPathSslKey" in joAccount && typeof joAccount.strPathSslKey === "string" &&
+ joAccount.strPathSslKey.length > 0 && "strPathSslCert" in joAccount &&
+ typeof joAccount.strPathSslCert === "string" && joAccount.strPathSslCert.length > 0
+ ) {
+ rpcCallOpts = {
+ cert: fs.readFileSync( joAccount.strPathSslCert, "utf8" ),
+ key: fs.readFileSync( joAccount.strPathSslKey, "utf8" )
+ };
+ } else
+ optsHandleVerifyAndSign.details.warning( "Will sign via SGX without SSL options" );
+ joCall = await rpcCall.create( joAccount.strSgxURL, rpcCallOpts );
+ if( !joCall )
+ throw new Error( `Failed to create JSON RPC call object to ${joAccount.strSgxURL}` );
+ const joIn = {
+ jsonrpc: "2.0",
+ id: utils.randomCallID(),
+ method: "blsSignMessageHash",
+ params: {
+ keyShareName: joAccount.strBlsKeyName,
+ messageHash: optsHandleVerifyAndSign.strMessageHash,
+ n: optsHandleVerifyAndSign.nParticipants,
+ t: optsHandleVerifyAndSign.nThreshold
+ }
+ };
+ optsHandleVerifyAndSign.details.trace(
+ "{p}{bright} verification algorithm will invoke SGX with call data {}",
+ optsHandleVerifyAndSign.strLogPrefix, optsHandleVerifyAndSign.strDirection, joIn );
+ const joOut = await joCall.call( joIn );
+ await handleBlsSignMessageHashResult(
+ optsHandleVerifyAndSign, joCallData, joAccount, joCall, joIn, joOut );
+ } catch ( err ) {
+ optsHandleVerifyAndSign.details.error(
+ "{p}{bright}JSON RPC call(handleSkaleImaVerifyAndSign) " +
+ "to SGX failed, RPC call failed, error is: {err}",
+ optsHandleVerifyAndSign.strLogPrefix, optsHandleVerifyAndSign.strDirection, err );
+ if( joCall )
+ await joCall.disconnect();
+ throw new Error( "JSON RPC call(handleSkaleImaVerifyAndSign) to SGX failed, " +
+ `RPC call failed, error is: ${owaspUtils.extractErrorMessage( err )}` );
+ }
+ optsHandleVerifyAndSign.details.exposeDetailsTo(
+ log.globalStream(), "IMA messages verifier/signer", optsHandleVerifyAndSign.isSuccess );
+ optsHandleVerifyAndSign.details.close();
+ return optsHandleVerifyAndSign.joRetVal;
+}
+
+async function handleSkaleImaBSU256Prepare( optsBSU256: TBSU256Options ): Promise < boolean > {
+ optsBSU256.details.debug( "{p}Will U256-BLS-sign {}",
+ optsBSU256.strLogPrefix, optsBSU256.joCallData );
+ if( !optsBSU256.imaState.joSChainNetworkInfo )
+ throw new Error( "No own S-Chain network information" );
+ optsBSU256.nThreshold = discoverBlsThreshold( optsBSU256.imaState.joSChainNetworkInfo );
+ optsBSU256.nParticipants = discoverBlsParticipants( optsBSU256.imaState.joSChainNetworkInfo );
+ optsBSU256.details.trace( "{p}Discovered BLS threshold is {}.",
+ optsBSU256.strLogPrefix, optsBSU256.nThreshold );
+ optsBSU256.details.trace( "{p}Discovered number of BLS participants is {}.",
+ optsBSU256.strLogPrefix, optsBSU256.nParticipants );
+ if( !checkBlsThresholdAndBlsParticipants(
+ optsBSU256.nThreshold,
+ optsBSU256.nParticipants,
+ "handle BSU256Prepare",
+ optsBSU256.details ) )
+ return false;
+ optsBSU256.u256 = optsBSU256.joCallData.params.valueToSign;
+ optsBSU256.details.trace( "{p}U256 original value is {}",
+ optsBSU256.strLogPrefix, optsBSU256.u256 );
+ optsBSU256.strMessageHash = keccak256U256( optsBSU256.u256, true );
+ optsBSU256.details.trace( "{p}hash of U256 value to sign is {}",
+ optsBSU256.strLogPrefix, optsBSU256.strMessageHash );
+ optsBSU256.details.trace( "{p}Will BLS-sign U256.", optsBSU256.strLogPrefix );
+ if( !optsBSU256.joAccount )
+ throw new Error( "No account to perform blsSignMessageHash for U256" );
+ optsBSU256.joAccount = optsBSU256.imaState.chainProperties.sc.joAccount;
+ if( !optsBSU256.joAccount.strSgxURL ) {
+ optsBSU256.joAccount = optsBSU256.imaState.chainProperties.mn.joAccount;
+ if( !optsBSU256.joAccount.strSgxURL )
+ throw new Error( "SGX URL is unknown, cannot sign U256" );
+ if( !optsBSU256.joAccount.strBlsKeyName )
+ throw new Error( "BLS keys name is unknown, cannot sign U256" );
+ }
+ return true;
+}
+
+async function handleBlsSignMessageHash256Result(
+ optsBSU256: TBSU256Options, joCallData: TBSU256CallData,
+ joCall: rpcCall.TRPCCall, joIn: any, joOut: any
+): Promise < object > {
+ optsBSU256.details.trace( "{p}Call to SGX done, answer is: {}",
+ optsBSU256.strLogPrefix, joOut );
+ let joSignResult: TSignResult = joOut;
+ if( joOut.result != null && joOut.result != undefined &&
+ typeof joOut.result === "object" )
+ joSignResult = joOut.result;
+ if( joOut.signResult != null && joOut.signResult != undefined &&
+ typeof joOut.signResult === "object" )
+ joSignResult = joOut.signResult;
+ if( !joSignResult ) {
+ const strError = "No signature arrived";
+ optsBSU256.joRetVal.error = strError;
+ optsBSU256.details.error(
+ "{p}U256/BLS-sign(1) finished with error: {err}", optsBSU256.strLogPrefix, strError );
+ await joCall.disconnect();
+ throw new Error( strError );
+ }
+ if( "errorMessage" in joSignResult &&
+ typeof joSignResult.errorMessage === "string" &&
+ joSignResult.errorMessage.length > 0 ) {
+ optsBSU256.isSuccess = false;
+ const strError = "BLS-sign finished with " +
+ `error: ${joSignResult.errorMessage}`;
+ optsBSU256.joRetVal.error = strError;
+ optsBSU256.details.error( "{p}U256/BLS-sign(2) finished with error: {err}",
+ optsBSU256.strLogPrefix, joSignResult.errorMessage );
+ await joCall.disconnect();
+ throw new Error( strError );
+ }
+ optsBSU256.isSuccess = true;
+ optsBSU256.joRetVal.result = { signResult: joSignResult };
+ if( "qa" in optsBSU256.joCallData.params )
+ optsBSU256.joRetVal.qa = optsBSU256.joCallData.params.qa;
+ await joCall.disconnect();
+ return joSignResult;
+}
+
+export async function handleSkaleImaBSU256(
+ joCallData: TBSU256CallData ): Promise < object | null > {
+ const optsBSU256: TBSU256Options = {
+ joCallData,
+ imaState: state.get(),
+ strLogPrefix: "",
+ details: log.createMemoryStream(),
+ joRetVal: {},
+ isSuccess: false,
+ nThreshold: 1,
+ nParticipants: 1,
+ u256: null,
+ strMessageHash: "",
+ joAccount: null
+ };
+ let joCall: rpcCall.TRPCCall | null = null;
+ try {
+ if( !( await handleSkaleImaBSU256Prepare( optsBSU256 ) ) )
+ return null;
+ if( !optsBSU256.joAccount )
+ throw new Error( "No account to perform blsSignMessageHash for U256" );
+ let rpcCallOpts: rpcCall.TRPCCallOpts | null = null;
+ if( "strPathSslKey" in optsBSU256.joAccount &&
+ typeof optsBSU256.joAccount.strPathSslKey === "string" &&
+ optsBSU256.joAccount.strPathSslKey.length > 0 &&
+ "strPathSslCert" in optsBSU256.joAccount &&
+ typeof optsBSU256.joAccount.strPathSslCert === "string" &&
+ optsBSU256.joAccount.strPathSslCert.length > 0
+ ) {
+ rpcCallOpts = {
+ cert: fs.readFileSync( optsBSU256.joAccount.strPathSslCert, "utf8" ),
+ key: fs.readFileSync( optsBSU256.joAccount.strPathSslKey, "utf8" )
+ };
+ } else
+ optsBSU256.details.warning( "Will sign via SGX without SSL options" );
+ joCall = await rpcCall.create( optsBSU256.joAccount.strSgxURL, rpcCallOpts );
+ if( !joCall ) {
+ throw new Error( "Failed to create JSON RPC call object " +
+ `to ${optsBSU256.joAccount.strSgxURL}` );
+ }
+ const joIn = {
+ jsonrpc: "2.0",
+ id: utils.randomCallID(),
+ method: "blsSignMessageHash",
+ params: {
+ keyShareName: optsBSU256.joAccount.strBlsKeyName,
+ messageHash: optsBSU256.strMessageHash,
+ n: optsBSU256.nParticipants,
+ t: optsBSU256.nThreshold
+ }
+ };
+ optsBSU256.details.trace( "{p}Will invoke SGX with call data {}",
+ optsBSU256.strLogPrefix, joIn );
+ const joOut = await joCall.call( joIn );
+ await handleBlsSignMessageHash256Result( optsBSU256, joCallData, joCall, joIn, joOut );
+ } catch ( err ) {
+ optsBSU256.isSuccess = false;
+ const strError = owaspUtils.extractErrorMessage( err );
+ optsBSU256.joRetVal.error = strError;
+ optsBSU256.details.error(
+ "{p}JSON RPC call(handleSkaleImaBSU256) to SGX failed, " +
+ "RPC call failed, error is: {err}", optsBSU256.strLogPrefix, err );
+ if( joCall )
+ await joCall.disconnect();
+ throw new Error( "JSON RPC call(handleSkaleImaBSU256) to SGX failed, " +
+ `RPC call failed, error is: ${owaspUtils.extractErrorMessage( err )}` );
+ }
+ optsBSU256.details.exposeDetailsTo(
+ log.globalStream(), "U256-BLS-signer", optsBSU256.isSuccess );
+ optsBSU256.details.close();
+ return optsBSU256.joRetVal;
+}
diff --git a/src/cc.ts b/src/cc.ts
new file mode 100644
index 00000000..29149b58
--- /dev/null
+++ b/src/cc.ts
@@ -0,0 +1,1032 @@
+// 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 .
+ */
+
+/**
+ * @file cc.ts
+ * @copyright SKALE Labs 2019-Present
+ */
+
+let gFlagIsEnabled: boolean = false;
+
+export function autoEnableFromCommandLineArgs(): void {
+ const b: boolean =
+ !!( ( process.argv.includes( "--colors" ) || process.argv.includes( "-colors" ) ) );
+ enable( b );
+}
+
+export function enable( b?: boolean ): void {
+ gFlagIsEnabled = !!b;
+}
+
+export function isStringAlreadyColorized( s?: any ): boolean {
+ if( s && typeof s === "string" && s.length > 0 && s[0] == "\x1b" )
+ return true;
+ return false;
+}
+
+export function isEnabled(): boolean {
+ return !!gFlagIsEnabled;
+}
+
+export function replaceAll( str: string, find: string, replace: string ): string {
+ return str.replace( new RegExp( find, "g" ), replace );
+}
+
+export function validateRadix( value: any, radix?: any ): boolean {
+ value = ( value ? value.toString() : "10" ).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;
+}
+
+export function validateInteger( value: any, radix?: any ): boolean {
+ try {
+ if( value === null || value === undefined )
+ return false;
+ if( value === 0 || value === 0.0 )
+ return true;
+ const s = value ? value.toString().trim() : "";
+ if( s.length < 1 )
+ return false;
+ radix = validateRadix( value, radix );
+ if( ( !isNaN( value ) ) &&
+ ( parseInt( value, radix ) == value || radix !== 10 ) &&
+ ( !isNaN( parseInt( value, radix ) ) )
+ )
+ return true;
+ } catch ( err ) {
+ }
+ return false;
+}
+
+export function toInteger( value: any, radix?: any ): number {
+ try {
+ if( value === 0 || value === 0.0 || value === null || value === undefined )
+ return 0;
+ value = ( value ? value.toString().trim() : "" ).trim();
+ radix = validateRadix( value, radix );
+ if( !validateInteger( value, radix ) )
+ return NaN;
+ return parseInt( value.toString().trim(), radix );
+ } catch ( err ) {
+ }
+ return 0;
+}
+
+export function validateFloat( value: any ): boolean {
+ try {
+ if( value === null || value === undefined )
+ return false;
+ if( value === 0 || value === 0.0 )
+ return true;
+ const f = parseFloat( value.toString().trim() );
+ if( isNaN( f ) )
+ return false;
+ return true;
+ } catch ( err ) {
+ }
+ return false;
+}
+
+export function toFloat( value: any ): number {
+ try {
+ if( value === 0 || value === 0.0 || value === null || value === undefined )
+ return 0.0;
+ const f = parseFloat( value.toString().trim() );
+ return f;
+ } catch ( err ) {
+ }
+ return 0.0;
+}
+
+export function toBoolean( value?: any ): boolean {
+ let b = false;
+ try {
+ if( typeof value === "boolean" )
+ return value;
+ if( typeof value === "string" ) {
+ const ch = value[0].toLowerCase();
+ if( ch == "y" || ch == "t" )
+ b = true;
+ else if( /^-?\d+$/.test( value ) ) // check string is integer
+ b = !!parseInt( value, 10 );
+ else if( /^-?\d+(?:[.,]\d*?)?$/.test( value ) ) // check string is float
+ b = !!toFloat( value ); else
+ b = !!b;
+ } else
+ b = !!b;
+ } catch ( err ) {
+ b = false;
+ }
+ b = !!b;
+ return b;
+}
+
+export function yn( flag?: any ): string {
+ if( !gFlagIsEnabled )
+ return flag ? "true" : "false";
+ return toBoolean( flag ) ? yes( "yes" ) : no( "no" );
+}
+
+export function tf( flag?: any ): string {
+ if( !gFlagIsEnabled )
+ return flag ? "true" : "false";
+ return toBoolean( flag ) ? yes( "true" ) : no( "false" );
+}
+
+export function onOff( flag?: any ): string {
+ if( !gFlagIsEnabled )
+ return flag ? "true" : "false";
+ return toBoolean( flag ) ? yes( "on" ) : no( "off" );
+}
+const gMapColorDefinitions: any = {
+ reset: "\x1b[0m",
+ enlight: "\x1b[1m",
+ dim: "\x1b[2m",
+ underscore: "\x1b[4m",
+ blink: "\x1b[5m",
+ reverse: "\x1b[7m",
+ hidden: "\x1b[8m",
+ fgBlack: "\x1b[30m",
+ fgRed: "\x1b[31m",
+ fgGreen: "\x1b[32m",
+ fgYellow: "\x1b[33m",
+ fgBlue: "\x1b[34m",
+ fgMagenta: "\x1b[35m",
+ fgCyan: "\x1b[36m",
+ fgWhite: "\x1b[37m",
+ bgBlack: "\x1b[40m",
+ bgRed: "\x1b[41m",
+ bgGreen: "\x1b[42m",
+ bgYellow: "\x1b[43m",
+ bgBlue: "\x1b[44m",
+ bgMagenta: "\x1b[45m",
+ bgCyan: "\x1b[46m",
+ bBgWhite: "\x1b[47m"
+};
+
+const gArrRainbowParts: any[] = [
+ gMapColorDefinitions.enlight + gMapColorDefinitions.fgRed,
+ gMapColorDefinitions.fgRed,
+ gMapColorDefinitions.enlight + gMapColorDefinitions.fgYellow,
+ gMapColorDefinitions.fgYellow,
+ gMapColorDefinitions.enlight + gMapColorDefinitions.fgGreen,
+ gMapColorDefinitions.fgGreen,
+ gMapColorDefinitions.enlight + gMapColorDefinitions.fgCyan,
+ gMapColorDefinitions.fgCyan,
+ gMapColorDefinitions.enlight + gMapColorDefinitions.fgBlue,
+ gMapColorDefinitions.fgBlue,
+ gMapColorDefinitions.enlight + gMapColorDefinitions.fgMagenta,
+ gMapColorDefinitions.fgMagenta
+];
+
+export function rainbowPart( s: string, i: number ): string {
+ if( !gFlagIsEnabled )
+ return s ? s.toString() : JSON.stringify( s );
+ const j = i % gArrRainbowParts.length;
+ return gArrRainbowParts[j] + s + gMapColorDefinitions.reset;
+}
+
+export function rainbow( s?: any ): string {
+ if( ( !gFlagIsEnabled ) || ( !s ) || ( typeof s !== "string" ) || s.length == 0 )
+ return s ? s.toString() : JSON.stringify( s );
+ let res = "";
+ const cnt = s.length;
+ for( let i = 0; i < cnt; ++i )
+ res = res + rainbowPart( s[i], i );
+ return res;
+}
+
+export function isInt2( n?: any ): boolean {
+ const intRegex = /^-?\d+$/;
+ if( !intRegex.test( n ) )
+ return false;
+
+ const intVal = parseInt( n, 10 );
+ return parseFloat( n ) == intVal && !isNaN( intVal );
+}
+
+export function isFloat2( n?: any ): boolean {
+ const val = parseFloat( n );
+ return !isNaN( val );
+}
+
+function urlObjColorized( objURL?: any ): string {
+ let strURL = "";
+ if( !objURL )
+ return strURL;
+ if( objURL.protocol && objURL.protocol !== null && objURL.protocol !== undefined )
+ strURL += yellow( objURL.protocol ) + normal( "//" );
+ if( objURL.username && objURL.username !== null && objURL.username !== undefined ) {
+ strURL += magenta( objURL.username );
+ if( objURL.password && objURL.password !== null && objURL.password !== undefined )
+ strURL += normal( ":" ) + yellow( objURL.password );
+ strURL += normal( "@" );
+ }
+ if( objURL.hostname )
+ strURL += magenta( logArgToStringAsIpv4( objURL.hostname ) );
+ if( objURL.port && objURL.port !== null && objURL.port !== undefined )
+ strURL += normal( ":" ) + logArgToString( objURL.port );
+ if( objURL.pathname && objURL.pathname !== null &&
+ objURL.pathname !== undefined && objURL.pathname !== "/" )
+ strURL += yellow( replaceAll( objURL.pathname, "/", normal( "/" ) ) );
+ if( objURL.search && objURL.search !== null && objURL.search !== undefined )
+ strURL += magenta( objURL.search );
+ return strURL;
+}
+
+export function urlStrColorized( s?: any ): string {
+ const objURL = safeURL( s );
+ if( !objURL )
+ return "";
+ return urlObjColorized( objURL );
+}
+
+export function urlColorized( x?: any ): string {
+ if( typeof x === "string" || x instanceof String )
+ return urlStrColorized( x );
+ return urlObjColorized( x );
+}
+
+export function u( x?: any ): string {
+ return urlColorized( x );
+}
+
+export function safeURL( arg?: any ): URL | null {
+ try {
+ const sc = arg[0];
+ if( sc == "\"" || sc == "'" ) {
+ const cnt = arg.length;
+ if( arg[cnt - 1] == sc ) {
+ const ss = arg.substring( 1, cnt - 1 );
+ const objURL = safeURL( ss );
+ if( objURL != null && objURL != undefined ) {
+ const anyURL: any = objURL;
+ anyURL.strStrippedStringComma = sc;
+ }
+
+ return objURL;
+ }
+ return null;
+ }
+ const objURL = new URL( arg );
+ if( !objURL.hostname )
+ return null;
+
+ if( objURL.hostname.length === 0 )
+ return null;
+
+ const anyURL: any = objURL;
+ anyURL.strStrippedStringComma = null;
+ return objURL;
+ } catch ( err ) {
+ return null;
+ }
+}
+
+export function toIpv4Arr( s: string ): any[] | null {
+ // eslint-disable-next-line max-len
+ if( /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test( s ) ) {
+ const arr = s.split( "." );
+ if( ( !arr ) || arr.length !== 4 )
+ return null;
+
+ return arr;
+ }
+ return null;
+}
+
+export function logArgToStringAsIpv4( arg?: any ): string {
+ const arr = toIpv4Arr( arg );
+ if( !arr )
+ return arg.toString();
+ let s = "";
+ for( let i = 0; i < 4; ++i ) {
+ if( i > 0 )
+ s += normal( "." );
+
+ s += logArgToString( arr[i] );
+ }
+ return s;
+}
+
+export function logArgToString( ...args: any[] ): string {
+ let i;
+ const cnt = arguments.length;
+ let s = "";
+ for( i = 0; i < cnt; ++i ) {
+ const arg = arguments[i];
+ if( arg === undefined ) {
+ s += undefval( arg );
+ continue;
+ }
+ if( arg === null ) {
+ s += nullval( arg );
+ continue;
+ }
+ if( isNaN( arg ) ) {
+ s += nanval( arg );
+ continue;
+ }
+ if( typeof arg === "boolean" ) {
+ s += tf( arg );
+ continue;
+ }
+ if( typeof arg === "object" && typeof arg.valueOf() === "boolean" )
+ s += tf( arg.valueOf() );
+
+ if( typeof arg === "number" || typeof arg === "bigint" ) {
+ s += number( arg );
+ continue;
+ }
+ if( typeof arg === "object" &&
+ ( typeof arg.valueOf() === "number" || typeof arg.valueOf() === "bigint" ) ) {
+ s += number( arg.valueOf() );
+ continue;
+ }
+ if( typeof arg === "string" || arg instanceof String ) {
+ const objURL = safeURL( arg );
+ if( objURL != null && objURL != undefined ) {
+ let strURL = "";
+ const anyURL: any = objURL;
+ if( anyURL.strStrippedStringComma )
+ strURL += normal( anyURL.strStrippedStringComma );
+
+ if( objURL.protocol )
+ strURL += yellow( objURL.protocol ) + normal( "//" );
+
+ if( objURL.username ) {
+ strURL += magenta( objURL.username );
+ if( objURL.password )
+ strURL += normal( ":" ) + yellow( objURL.password );
+
+ strURL += normal( "@" );
+ }
+ if( objURL.hostname )
+ strURL += magenta( logArgToStringAsIpv4( objURL.hostname ) );
+
+ if( objURL.port )
+ strURL += normal( ":" ) + logArgToString( objURL.port );
+
+ if( objURL.pathname )
+ strURL += yellow( replaceAll( objURL.pathname, "/", normal( "/" ) ) );
+
+ if( objURL.search )
+ strURL += magenta( objURL.search );
+
+ if( anyURL.strStrippedStringComma )
+ strURL += normal( anyURL.strStrippedStringComma );
+
+ s += strURL;
+ continue;
+ }
+ if( ( arg.length > 1 && arg[0] == "-" && arg[1] != "-" ) ||
+ ( arg.length > 2 && arg[0] == "-" && arg[1] == "-" && arg[2] != "-" )
+ ) {
+ s += cla( arg );
+ continue;
+ }
+ if( arg.length > 0 && ( arg[0] == "\"" || arg[0] == "'" ) ) {
+ s += strval( arg );
+ continue;
+ }
+ if( isFloat2( arg ) ) {
+ s += real( arg );
+ continue;
+ }
+ if( isInt2( arg ) ) {
+ s += number( arg );
+ continue;
+ }
+ }
+ if( Array.isArray( arg ) || typeof arg === "object" ) {
+ s += jsonColorizer.prettyPrintConsole( arg );
+ continue;
+ }
+ s += kk( arg );
+ }
+ return s;
+}
+
+export const getCircularReplacerForJsonStringify = (): any => {
+ const seen = new WeakSet();
+ return ( key: any, value: any ): any => {
+ if( typeof value === "object" && value !== null ) {
+ if( seen.has( value ) )
+ return;
+ seen.add( value );
+ }
+ return value;
+ };
+};
+
+export const jsonColorizer: any = { // see http://jsfiddle.net/unLSJ/
+ cntCensoredMax: 30000, // zero to disable censoring
+ censor: ( censor: any ): any => {
+ let i = 0;
+ return ( key: any, value: any ) => {
+ if( i !== 0 && typeof ( censor ) === "object" &&
+ typeof ( value ) === "object" && censor == value
+ )
+ return "[Circular]";
+
+ if( i >= jsonColorizer.cntCensoredMax )
+ return "[Unknown]";
+
+ ++i; // so we know we aren't using the original object anymore
+ return value;
+ };
+ },
+ replacerHTML: ( match?: any, pIndent?: any, pKey?: any, pVal?: any, pEnd?: any ): any => {
+ const key = "";
+ const val = "";
+ const str = "";
+ let r = pIndent || "";
+ if( pKey )
+ r = r + key + pKey.replace( /[": ]/g, "" ) + ": ";
+
+ if( pVal )
+ r = r + ( pVal[0] == "\"" ? str : val ) + pVal + "";
+
+ return r + ( pEnd || "" );
+ },
+ prettyPrintHTML: ( obj?: any ) => {
+ const jsonLine = /^( *)("[\w]+": )?("[^"]*"|[\w.+-]*)?([,[{])?$/mg;
+ const s =
+ JSON.stringify(
+ obj, ( jsonColorizer.cntCensoredMax > 0 )
+ ? jsonColorizer.censor( obj )
+ : null, 4
+ )
+ .replace( /&/g, "&" ).replace( /\\"/g, """ )
+ .replace( //g, ">" )
+ .replace( jsonLine, jsonColorizer.replacerHTML );
+ return s;
+ },
+ replacerConsole: ( match?: any, pIndent?: any, pKey?: any, pVal?: any, pEnd?: any ): any => {
+ let r = pIndent || "";
+ if( pKey )
+ r = r + logArgToString( pKey.replace( /[": ]/g, "" ) ) + ": ";
+
+ if( pVal )
+ r = r + logArgToString( pVal );
+
+ return r + ( pEnd || "" );
+ },
+ prettyPrintConsole: ( obj?: any ): any => {
+ if( !gFlagIsEnabled ) {
+ if( obj === null )
+ return "null";
+ if( obj === undefined )
+ return "undefined";
+ try {
+ const s = JSON.stringify( obj );
+ return s;
+ } catch ( err ) { }
+ try {
+ const s = JSON.stringify( obj, getCircularReplacerForJsonStringify() );
+ return s;
+ } catch ( err ) { }
+ try {
+ const s = obj.toString();
+ return s;
+ } catch ( err ) { }
+ return obj;
+ }
+ const cntSpaces: number = 4;
+ const jsonLine = /^( *)("[\w]+": )?("[^"]*"|[\w.+-]*)?([,[{])?$/mg;
+ try {
+ const tmp: string = JSON.stringify(
+ obj,
+ ( jsonColorizer.cntCensoredMax > 0 ) ? jsonColorizer.censor( obj ) : null,
+ cntSpaces
+ );
+ const s = tmp
+ ? tmp.replace( jsonLine, jsonColorizer.replacerConsole )
+ : ( tmp.toString() );
+ return s;
+ } catch ( err ) { }
+ obj = JSON.parse( JSON.stringify( obj, getCircularReplacerForJsonStringify() ) );
+ const tmp = JSON.stringify(
+ obj,
+ ( jsonColorizer.cntCensoredMax > 0 ) ? jsonColorizer.censor( obj ) : null,
+ cntSpaces
+ );
+ const s = tmp ? tmp.replace( jsonLine, jsonColorizer.replacerConsole ) : ( tmp.toString() );
+ return s;
+ }
+};
+
+// see:
+// http://jsfiddle.net/KJQ9K/554
+// https://qastack.ru/programming/4810841/pretty-print-json-using-javascript
+export function syntaxHighlightJSON( jo?: any, strKeyNamePrefix?: string ): string {
+ strKeyNamePrefix = strKeyNamePrefix ?? "";
+ jo = jo.replace( /&/g, "&" ).replace( //g, ">" );
+ return jo.replace(
+ // eslint-disable-next-line max-len
+ /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g,
+ function( match: any ): string {
+ if( !gFlagIsEnabled )
+ return match;
+ let cls = "number";
+ if( /^"/.test( match ) ) {
+ if( /:$/.test( match ) )
+ cls = "key";
+ else
+ cls = "string";
+ } else if( /true|false/.test( match ) )
+ cls = "boolean";
+ else if( /null/.test( match ) )
+ cls = "null";
+ else if( /NaN/.test( match ) )
+ cls = "nan";
+ else if( /undefined/.test( match ) )
+ cls = "undefined";
+ else if( ( typeof match === "string" || match instanceof String ) &&
+ match.length >= 2 &&
+ ( ( match[0] == "\"" && match[match.length - 1] == "\"" ) ||
+ ( match[0] == "'" && match[match.length - 1] == "'" ) )
+ )
+ cls = "string";
+ switch ( cls ) {
+ case "key": return strKeyNamePrefix +
+ logArgToString( match.replace( /[": ]/g, "" ) ) + ": ";
+ case "boolean":
+ return tf( match );
+ case "null":
+ return nullval( match );
+ case "undefined":
+ return undefval( match );
+ case "nan":
+ return nanval( match );
+ case "string":
+ return strval( match );
+ case "number":
+ return number( match );
+ }
+ return logArgToString( match );
+ } );
+}
+
+export function safeStringifyJSON( jo?: any, n?: number ): string | undefined {
+ try {
+ const s = JSON.stringify( jo, getCircularReplacerForJsonStringify(), n );
+ return s;
+ } catch ( err ) {
+ }
+ return undefined;
+}
+
+export function jn( x?: any ): string {
+ return jsonColorizer.prettyPrintConsole( x );
+}
+
+export function j1( x?: any, n?: number, strKeyNamePrefix?: string ): string {
+ let isDefaultKeyNamePrefix = false;
+ if( typeof strKeyNamePrefix !== "string" ) {
+ strKeyNamePrefix = " ";
+ isDefaultKeyNamePrefix = true;
+ }
+ let s = safeStringifyJSON( x, n );
+ if( !gFlagIsEnabled )
+ return s ?? "";
+ s = syntaxHighlightJSON( s, strKeyNamePrefix );
+ if( isDefaultKeyNamePrefix && s.length > 9 && s[0] == " " )
+ s = s.substring( 1, s.length );
+ return s;
+}
+
+export function j( x?: any ): string {
+ return j1( x ); // jn
+}
+
+const reset = gMapColorDefinitions.reset;
+const enlight = gMapColorDefinitions.enlight;
+const dim = gMapColorDefinitions.dim;
+const underscore = gMapColorDefinitions.underscore;
+const blink = gMapColorDefinitions.blink;
+const reverse = gMapColorDefinitions.reverse;
+const hidden = gMapColorDefinitions.hidden;
+const fgBlack = gMapColorDefinitions.fgBlack;
+const fgRed = gMapColorDefinitions.fgRed;
+const fgGreen = gMapColorDefinitions.fgGreen;
+const fgYellow = gMapColorDefinitions.fgYellow;
+const fgBlue = gMapColorDefinitions.fgBlue;
+const fgMagenta = gMapColorDefinitions.fgMagenta;
+const fgCyan = gMapColorDefinitions.fgCyan;
+const fgWhite = gMapColorDefinitions.fgWhite;
+const bgBlack = gMapColorDefinitions.bgBlack;
+const bgRed = gMapColorDefinitions.bgRed;
+const bgGreen = gMapColorDefinitions.bgGreen;
+const bgYellow = gMapColorDefinitions.bgYellow;
+const bgBlue = gMapColorDefinitions.bgBlue;
+const bgMagenta = gMapColorDefinitions.bgMagenta;
+const bgCyan = gMapColorDefinitions.bgCyan;
+const bBgWhite = gMapColorDefinitions.bBgWhite;
+export {
+ reset,
+ enlight,
+ dim,
+ underscore,
+ blink,
+ reverse,
+ hidden,
+ fgBlack,
+ fgRed,
+ fgGreen,
+ fgYellow,
+ fgBlue,
+ fgMagenta,
+ fgCyan,
+ fgWhite,
+ bgBlack,
+ bgRed,
+ bgGreen,
+ bgYellow,
+ bgBlue,
+ bgMagenta,
+ bgCyan,
+ bBgWhite
+};
+
+export function normal( s?: any ): string {
+ if( !gFlagIsEnabled )
+ return s ? s.toString() : JSON.stringify( s );
+ return fgWhite + s + reset;
+}
+
+export function trace( s?: any ): string {
+ if( !gFlagIsEnabled )
+ return s ? s.toString() : JSON.stringify( s );
+ return fgWhite + s + reset;
+}
+
+export function debug( s?: any ): string {
+ if( !gFlagIsEnabled )
+ return s ? s.toString() : JSON.stringify( s );
+ return fgWhite + s + reset;
+}
+export function debugDark( s?: any ): string {
+ if( !gFlagIsEnabled )
+ return s ? s.toString() : JSON.stringify( s );
+ return fgBlack + enlight + s + reset;
+}
+
+export function note( s?: any ): string {
+ if( !gFlagIsEnabled )
+ return s ? s.toString() : JSON.stringify( s );
+ return fgBlue + s + reset;
+}
+
+export function notice( s?: any ): string {
+ if( !gFlagIsEnabled )
+ return s ? s.toString() : JSON.stringify( s );
+ return fgMagenta + s + reset;
+}
+
+export function info( s?: any ): string {
+ if( !gFlagIsEnabled )
+ return s ? s.toString() : JSON.stringify( s );
+ return fgBlue + enlight + s + reset;
+}
+
+export function warning( s?: any ): string {
+ if( !gFlagIsEnabled )
+ return s ? s.toString() : JSON.stringify( s );
+ return fgYellow + s + reset;
+}
+
+export function warn( s?: any ): string {
+ if( !gFlagIsEnabled )
+ return s ? s.toString() : JSON.stringify( s );
+ return fgYellow + s + reset;
+}
+
+export function error( s?: any ): string {
+ if( !gFlagIsEnabled )
+ return s ? s.toString() : JSON.stringify( s );
+ return fgRed + s + reset;
+}
+
+export function fatal( s?: any ): string {
+ if( !gFlagIsEnabled )
+ return s ? s.toString() : JSON.stringify( s );
+ return bgRed + fgYellow + enlight + s + reset;
+}
+
+export function success( s?: any ): string {
+ if( !gFlagIsEnabled )
+ return s ? s.toString() : JSON.stringify( s );
+ return fgGreen + enlight + s + reset;
+}
+
+export function attention( s?: any ): string {
+ if( !gFlagIsEnabled )
+ return s ? s.toString() : JSON.stringify( s );
+ return fgCyan + s + reset;
+}
+
+export function bright( s?: any ): string {
+ if( !gFlagIsEnabled )
+ return s ? s.toString() : JSON.stringify( s );
+ return fgWhite + enlight + s + reset;
+}
+
+export function sunny( s?: any ): string {
+ if( !gFlagIsEnabled )
+ return s ? s.toString() : JSON.stringify( s );
+ return fgYellow + enlight + s + reset;
+}
+
+export function rx( s?: any ): string {
+ if( !gFlagIsEnabled )
+ return s ? s.toString() : JSON.stringify( s );
+ return fgMagenta + s + reset;
+}
+
+export function rxa( s?: any ): string {
+ if( !gFlagIsEnabled )
+ return s ? s.toString() : JSON.stringify( s );
+ return fgMagenta + enlight + s + reset;
+}
+
+export function tx( s?: any ): string {
+ if( !gFlagIsEnabled )
+ return s ? s.toString() : JSON.stringify( s );
+ return fgGreen + s + reset;
+}
+
+export function txa( s?: any ): string {
+ if( !gFlagIsEnabled )
+ return s ? s.toString() : JSON.stringify( s );
+ return fgGreen + enlight + s + reset;
+}
+
+export function date( s?: any ): string {
+ if( !gFlagIsEnabled )
+ return s ? s.toString() : JSON.stringify( s );
+ return fgYellow + s + reset;
+}
+
+export function time( s?: any ): string {
+ if( !gFlagIsEnabled )
+ return s ? s.toString() : JSON.stringify( s );
+ return fgMagenta + enlight + s + reset;
+}
+
+export function fracTime( s?: any ): string {
+ if( !gFlagIsEnabled )
+ return s ? s.toString() : JSON.stringify( s );
+ return fgMagenta + s + reset;
+}
+
+export function yes( s?: any ): string {
+ if( !gFlagIsEnabled )
+ return s ? s.toString() : JSON.stringify( s );
+ return fgGreen + enlight + s + reset;
+}
+
+export function no( s?: any ): string {
+ if( !gFlagIsEnabled )
+ return s ? s.toString() : JSON.stringify( s );
+ return fgRed + s + reset;
+}
+
+export function number( s?: any ): string {
+ if( !gFlagIsEnabled )
+ return s ? s.toString() : JSON.stringify( s );
+ return fgBlue + enlight + s + reset;
+}
+
+export function real( s?: any ): string {
+ if( !gFlagIsEnabled )
+ return s ? s.toString() : JSON.stringify( s );
+ return fgMagenta + s + reset;
+}
+
+export function undefval( s?: any ): string {
+ if( !gFlagIsEnabled )
+ return s ? s.toString() : JSON.stringify( s );
+ return fgGreen + enlight + s + reset;
+}
+
+export function nullval( s?: any ): string {
+ if( !gFlagIsEnabled )
+ return s ? s.toString() : JSON.stringify( s );
+ return fgGreen + enlight + s + reset;
+}
+
+export function nanval( s?: any ): string {
+ if( !gFlagIsEnabled )
+ return s ? s.toString() : JSON.stringify( s );
+ return fgGreen + enlight + s + reset;
+}
+
+export function yellow( s?: any ): string {
+ if( !gFlagIsEnabled )
+ return s ? s.toString() : JSON.stringify( s );
+ return fgYellow + s + reset;
+}
+
+export function magenta( s?: any ): string {
+ if( !gFlagIsEnabled )
+ return s ? s.toString() : JSON.stringify( s );
+ return fgMagenta + s + reset;
+}
+
+export function cla( s?: any ): string {
+ if( !gFlagIsEnabled )
+ return s ? s.toString() : JSON.stringify( s );
+ return fgBlue + dim + s + reset;
+}
+
+export function kk( s?: any ): string {
+ if( !gFlagIsEnabled )
+ return s ? s.toString() : JSON.stringify( s );
+ return fgYellow + enlight + s + reset;
+}
+
+export function strval( s?: any ): string {
+ if( !gFlagIsEnabled )
+ return s ? s.toString() : JSON.stringify( s );
+ return fgYellow + s + reset;
+}
+
+export function n2s( n: any, sz: number ): string {
+ let s = n ? n.toString() : "";
+ while( s.length < sz )
+ s = "0" + s;
+ return s;
+}
+
+export function timestampHR(): number {
+ const newDateTime = new Date();
+ const ts = Math.floor( ( newDateTime ).getTime() );
+ return ts;
+}
+
+export function timestampUnix(): number {
+ const newDateTime = new Date();
+ const ts = Math.floor( ( newDateTime ).getTime() / 1000 );
+ return ts;
+}
+
+function trimLeftUnneededTimestampZeros( s?: any ): string {
+ while( s.length >= 2 ) {
+ if( s[0] == "0" && s[1] >= "0" && s[1] <= "9" )
+ s = s.substring( 1 );
+ else
+ break;
+ }
+ return s;
+}
+
+export function getDurationString( tsFrom: number, tsTo: number ): string {
+ let s = "";
+ let n = tsTo - tsFrom;
+
+ const ms = n % 1000;
+ n = Math.floor( n / 1000 );
+ s += "." + n2s( ms, 3 );
+ if( n == 0 )
+ return "0" + s;
+
+ const secs = n % 60;
+ n = Math.floor( n / 60 );
+ s = n2s( secs, 2 ) + s;
+ if( n == 0 )
+ return trimLeftUnneededTimestampZeros( s );
+ s = ":" + s;
+
+ const mins = n % 60;
+ n = Math.floor( n / 60 );
+ s = n2s( mins, 2 ) + s;
+ if( n == 0 )
+ return trimLeftUnneededTimestampZeros( s );
+ s = ":" + s;
+
+ const hours = n % 24;
+ n = Math.floor( n / 24 );
+ s = n2s( hours, 2 ) + s;
+ if( n == 0 )
+ return trimLeftUnneededTimestampZeros( s );
+
+ return ( n ? n.toString() : "" ) + " " + ( ( n > 1 ) ? "days" : "day" ) + "," + s;
+}
+
+export function capitalizeFirstLetter( s?: any ): string {
+ if( !s )
+ return JSON.stringify( s );
+ let s2 = s.toString();
+ if( !s2 )
+ return s.toString();
+ s2 = s2.charAt( 0 ).toUpperCase() + s2.slice( 1 );
+ return s2;
+}
+
+function errFnDottedName( s?: any ): string {
+ const arr = s.split( "." );
+ const cnt = arr.length;
+ let i; let s2 = "";
+ for( i = 0; i < cnt; ++i ) {
+ if( i > 0 )
+ s2 += bright( "." );
+ s2 += sunny( arr[i] );
+ }
+ return s2;
+}
+
+function errFnName( s?: any ): string {
+ if( s.indexOf( "async " ) == 0 )
+ return bright( "async" ) + " " + errFnDottedName( s.substring( 6 ) );
+ return errFnDottedName( s );
+}
+
+function errLocLn( s: string, isWithBraces?: boolean ): string {
+ let s2 = "";
+ s = s.replace( "file://", "" );
+ s = s.replace( "node:", "" );
+ if( isWithBraces )
+ s2 += " " + debug( "(" );
+ const arrCodePoint = s.split( ":" );
+ if( arrCodePoint.length > 0 ) {
+ s2 += trace( arrCodePoint[0] );
+ for( let j = 1; j < arrCodePoint.length; ++j ) {
+ s2 += debug( ":" );
+ if( j == 1 )
+ s2 += info( arrCodePoint[j] );
+ else
+ s2 += attention( arrCodePoint[j] );
+ }
+ } else
+ s2 += trace( s );
+ if( isWithBraces )
+ s2 += debug( ")" );
+ return s2;
+}
+
+export function stack( err?: any ): string {
+ if( !err )
+ return "";
+ if( err && "stack" in err ) {
+ const st = err.stack;
+ if( st && typeof st === "string" )
+ err = st;
+ }
+ try {
+ const arr = ( typeof err === "string" ) ? err.split( "\n" ) : err;
+ const cnt = arr.length;
+ let i;
+ for( i = 0; i < cnt; ++i ) {
+ let s = arr[i].replace( /\s+/g, " " ).trim();
+ if( s.indexOf( "at " ) == 0 ) {
+ // stack entry
+ s = s.substring( 3 );
+ let s2 = " " + debug( "-->" ) + " ";
+ const n = s.indexOf( " (" );
+ if( n > 0 ) {
+ s2 += errFnName( s.substring( 0, n ) );
+ s = s.substring( n + 2 );
+ if( s[s.length - 1] == ")" )
+ s = s.substring( 0, s.length - 1 );
+ s2 += errLocLn( s, true );
+ } else
+ s2 += errLocLn( s, false );
+ s = s2;
+ } else {
+ // probably error description line
+ const n = s.indexOf( ":" );
+ if( n >= 0 ) {
+ s = error(
+ s.substring( 0, n ) ) + normal( ":" ) + warning( s.substring( n + 1 ) );
+ } else
+ s = error( s );
+ }
+ arr[i] = s;
+ }
+ return arr.join( "\n" );
+ } catch ( errCaught ) {
+ return err.toString();
+ }
+}
diff --git a/src/cli.ts b/src/cli.ts
new file mode 100644
index 00000000..3bdfa11c
--- /dev/null
+++ b/src/cli.ts
@@ -0,0 +1,2818 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+
+/**
+ * @license
+ * SKALE IMA
+ *
+ * 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 .
+ */
+
+/**
+ * @file cli.ts
+ * @copyright SKALE Labs 2019-Present
+ */
+
+import * as path from "path";
+import * as url from "url";
+import * as os from "os";
+import * as log from "./log.js";
+import * as owaspUtils from "./owaspUtils.js";
+import * as imaUtils from "./utils.js";
+import * as rpcCall from "./rpcCall.js";
+import * as imaHelperAPIs from "./imaHelperAPIs.js";
+import * as imaTransferErrorHandling from "./imaTransferErrorHandling.js";
+import * as imaOracleOperations from "./imaOracleOperations.js";
+import * as imaTx from "./imaTx.js";
+import * as state from "./state.js";
+
+// eslint-disable-next-line @typescript-eslint/naming-convention
+const __dirname: string = path.dirname( url.fileURLToPath( import.meta.url ) );
+
+const gStrAppName = "IMA AGENT";
+const gStrVersion =
+ imaUtils.fileLoad( path.join( __dirname, "../VERSION" ), "N/A" ).toString().trim();
+
+function att( ...args: any[] ): string { return log.fmtAttention( ...args ); };
+
+export function printAbout( isLog?: boolean ): boolean {
+ isLog = isLog ?? false;
+ const strMsg = log.fmtTrace( att( gStrAppName ), " version ", log.fmtNotice( gStrVersion ) );
+ if( isLog )
+ log.information( strMsg );
+ else
+ console.log( strMsg );
+ return true;
+}
+
+export function parseCommandLineArgument( s: string ): any {
+ const joArg = {
+ name: "",
+ value: ""
+ };
+ try {
+ if( !s )
+ return joArg;
+ while( s.length > 0 && s[0] == "-" )
+ s = s.substring( 1 );
+ const n = s.indexOf( "=" );
+ if( n < 0 ) {
+ joArg.name = s;
+ return joArg;
+ }
+ joArg.name = s.substring( 0, n );
+ joArg.value = s.substring( n + 1 );
+ } catch ( err ) {}
+ return joArg;
+}
+
+// check correctness of command line arguments
+export function ensureHaveValue(
+ name: string, value: any, isExitIfEmpty: boolean,
+ isPrintValue: boolean, fnNameColorizer?: any, fnValueColorizer?: any
+): boolean {
+ isExitIfEmpty = isExitIfEmpty || false;
+ isPrintValue = isPrintValue || false;
+ fnNameColorizer = fnNameColorizer || ( ( x: any ) => {
+ return log.fmtInformation( x );
+ } );
+ fnValueColorizer = fnValueColorizer || ( ( x: any ) => {
+ return log.fmtNotice( x );
+ } );
+ let retVal = true;
+ value = value ? value.toString() : "";
+ if( value.length === 0 ) {
+ retVal = false;
+ if( !isPrintValue )
+ console.log( log.fmtError( "WARNING:, missing value for ", fnNameColorizer( name ) ) );
+ if( isExitIfEmpty )
+ process.exit( 126 );
+ }
+ let strDots = "...";
+ let n = 50 - name.length;
+ for( ; n > 0; --n )
+ strDots += ".";
+ if( isPrintValue )
+ log.debug( "{sunny}{raw}{}", fnNameColorizer( name ), strDots, fnValueColorizer( value ) );
+ return retVal;
+}
+
+export function ensureHaveCredentials(
+ strFriendlyChainName: string, joAccount: state.TAccount,
+ isExitIfEmpty: boolean, isPrintValue: boolean
+): boolean {
+ strFriendlyChainName = strFriendlyChainName || "";
+ if( !( typeof joAccount === "object" ) ) {
+ log.error( "ARGUMENTS VALIDATION WARNING: bad account specified for {} chain",
+ strFriendlyChainName );
+ if( isExitIfEmpty )
+ process.exit( 126 );
+ }
+ let cntAccountVariantsSpecified = 0;
+ if( "strTransactionManagerURL" in joAccount &&
+ typeof joAccount.strTransactionManagerURL === "string" &&
+ joAccount.strTransactionManagerURL.length > 0
+ ) {
+ ++cntAccountVariantsSpecified;
+ ensureHaveValue(
+ strFriendlyChainName + "/TM/URL",
+ joAccount.strTransactionManagerURL, isExitIfEmpty, isPrintValue );
+ }
+ if( "strSgxURL" in joAccount &&
+ typeof joAccount.strSgxURL === "string" &&
+ joAccount.strSgxURL.length > 0
+ ) {
+ ++cntAccountVariantsSpecified;
+ ensureHaveValue(
+ strFriendlyChainName + "/SGX/URL",
+ joAccount.strSgxURL, isExitIfEmpty, isPrintValue );
+ if( "strPathSslKey" in joAccount &&
+ typeof joAccount.strPathSslKey === "string" &&
+ joAccount.strPathSslKey.length > 0
+ ) {
+ ensureHaveValue(
+ strFriendlyChainName + "/SGX/SSL/keyPath",
+ joAccount.strPathSslKey, isExitIfEmpty, isPrintValue );
+ }
+ if( "strPathSslCert" in joAccount &&
+ typeof joAccount.strPathSslCert === "string" &&
+ joAccount.strPathSslCert.length > 0
+ ) {
+ ensureHaveValue(
+ strFriendlyChainName + "/SGX/SSL/certPath",
+ joAccount.strPathSslCert, isExitIfEmpty, isPrintValue );
+ }
+ }
+ if( "strSgxKeyName" in joAccount &&
+ typeof joAccount.strSgxKeyName === "string" &&
+ joAccount.strSgxKeyName.length > 0
+ ) {
+ ++cntAccountVariantsSpecified;
+ ensureHaveValue(
+ strFriendlyChainName + "/SGX/keyName",
+ joAccount.strSgxKeyName, isExitIfEmpty, isPrintValue );
+ }
+ if( "privateKey" in joAccount &&
+ typeof joAccount.privateKey === "string" &&
+ joAccount.privateKey.length > 0
+ ) {
+ ++cntAccountVariantsSpecified;
+ ensureHaveValue(
+ strFriendlyChainName + "/privateKey",
+ joAccount.privateKey, isExitIfEmpty, isPrintValue );
+ }
+ if( "address_" in joAccount &&
+ typeof joAccount.address_ === "string" && joAccount.address_.length > 0 ) {
+ ++cntAccountVariantsSpecified;
+ ensureHaveValue(
+ strFriendlyChainName + "/walletAddress",
+ joAccount.address_, isExitIfEmpty, isPrintValue );
+ }
+ if( cntAccountVariantsSpecified == 0 ) {
+ log.error( "ARGUMENTS VALIDATION WARNING: bad credentials information " +
+ "specified for {bright} chain, no explicit SGX, no explicit private key, " +
+ "no wallet address found", strFriendlyChainName );
+ if( isExitIfEmpty )
+ process.exit( 126 );
+ }
+ return true;
+}
+
+export function findNodeIndex( joSChainNodeConfiguration: any ): number {
+ try {
+ const searchID = joSChainNodeConfiguration.skaleConfig.nodeInfo.nodeID;
+ const cnt = joSChainNodeConfiguration.skaleConfig.sChain.nodes.length;
+ for( let i = 0; i < cnt; ++i ) {
+ const joNodeDescription = joSChainNodeConfiguration.skaleConfig.sChain.nodes[i];
+ if( joNodeDescription.nodeID == searchID )
+ return i;
+ }
+ } catch ( err ) {}
+ return 0;
+}
+
+function parseHelp( imaState: state.TIMAState, joArg: any ): boolean { // exits process on "--help"
+ if( joArg.name != "help" )
+ return false;
+ printAbout();
+ const strAboutText =
+ imaUtils.fileLoad( path.join( __dirname, "../about.txt" ), "N/A" )
+ .toString();
+ console.log( strAboutText );
+ process.exit( 0 );
+}
+
+function parseVersion( imaState: state.TIMAState, joArg: any ): boolean {
+ // exits process on "--version"
+ if( joArg.name != "version" )
+ return false;
+ printAbout();
+ process.exit( 0 );
+}
+
+function parseBasicArgs( imaState: state.TIMAState, joArg: any ): boolean {
+ if( joArg.name == "colors" ) {
+ log.enableColorization( true );
+ return true;
+ }
+ if( joArg.name == "no-colors" ) {
+ log.enableColorization( false );
+ return true;
+ }
+ if( joArg.name == "expose" ) {
+ log.exposeDetailsSet( true );
+ return true;
+ }
+ if( joArg.name == "no-expose" ) {
+ log.exposeDetailsSet( false );
+ return true;
+ }
+ if( joArg.name == "verbose" ) {
+ log.verboseSet( log.verboseParse( joArg.value ) );
+ return true;
+ }
+ if( joArg.name == "verbose-list" ) {
+ log.verboseList();
+ return true;
+ }
+ return false;
+}
+
+function parseChainAccessArgs( imaState: state.TIMAState, joArg: any ): boolean {
+ if( joArg.name == "url-main-net" ) {
+ owaspUtils.verifyArgumentIsURL( joArg );
+ imaState.chainProperties.mn.strURL = joArg.value;
+ return true;
+ }
+ if( joArg.name == "url-s-chain" ) {
+ owaspUtils.verifyArgumentIsURL( joArg );
+ imaState.chainProperties.sc.strURL = joArg.value;
+ return true;
+ }
+ if( joArg.name == "url-t-chain" ) {
+ owaspUtils.verifyArgumentIsURL( joArg );
+ imaState.chainProperties.tc.strURL = joArg.value;
+ return true;
+ }
+ if( joArg.name == "id-main-net" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.chainProperties.mn.strChainName = joArg.value;
+ return true;
+ }
+ if( joArg.name == "id-s-chain" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.chainProperties.sc.strChainName = joArg.value;
+ return true;
+ }
+ if( joArg.name == "id-origin-chain" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.strChainNameOriginChain = joArg.value;
+ return true;
+ }
+ if( joArg.name == "id-t-chain" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.chainProperties.tc.strChainName = joArg.value;
+ return true;
+ }
+ if( joArg.name == "cid-main-net" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaState.chainProperties.mn.chainId = owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ if( joArg.name == "cid-s-chain" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaState.chainProperties.sc.chainId = owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ if( joArg.name == "cid-t-chain" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaState.chainProperties.tc.chainId = owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ return false;
+}
+
+function parseTransactionManagerArgs( imaState: state.TIMAState, joArg: any ): boolean {
+ if( joArg.name == "tm-url-main-net" ) {
+ owaspUtils.verifyArgumentIsURL( joArg );
+ const strURL = joArg.value.toString();
+ imaState.chainProperties.mn.joAccount.strTransactionManagerURL = strURL;
+ return true;
+ }
+ if( joArg.name == "tm-url-s-chain" ) {
+ owaspUtils.verifyArgumentIsURL( joArg );
+ const strURL = joArg.value.toString();
+ imaState.chainProperties.sc.joAccount.strTransactionManagerURL = strURL;
+ return true;
+ }
+ if( joArg.name == "tm-url-t-chain" ) {
+ owaspUtils.verifyArgumentIsURL( joArg );
+ const strURL = joArg.value.toString();
+ imaState.chainProperties.tc.joAccount.strTransactionManagerURL = strURL;
+ return true;
+ }
+ if( joArg.name == "tm-priority-main-net" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaState.chainProperties.mn.joAccount.nTmPriority =
+ owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ if( joArg.name == "tm-priority-s-chain" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaState.chainProperties.sc.joAccount.nTmPriority =
+ owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ if( joArg.name == "tm-priority-t-chain" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaState.chainProperties.tc.joAccount.nTmPriority =
+ owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ return false;
+}
+
+function parseSgxArgs( imaState: state.TIMAState, joArg: any ): boolean {
+ if( joArg.name == "sgx-url-main-net" ) {
+ owaspUtils.verifyArgumentIsURL( joArg );
+ imaState.chainProperties.mn.joAccount.strSgxURL = joArg.value;
+ return true;
+ }
+ if( joArg.name == "sgx-url-s-chain" ) {
+ owaspUtils.verifyArgumentIsURL( joArg );
+ imaState.chainProperties.sc.joAccount.strSgxURL = joArg.value;
+ return true;
+ }
+ if( joArg.name == "sgx-url-t-chain" ) {
+ owaspUtils.verifyArgumentIsURL( joArg );
+ imaState.chainProperties.tc.joAccount.strSgxURL = joArg.value;
+ return true;
+ }
+ if( joArg.name == "sgx-url" ) {
+ owaspUtils.verifyArgumentIsURL( joArg );
+ imaState.chainProperties.mn.joAccount.strSgxURL =
+ imaState.chainProperties.sc.joAccount.strSgxURL =
+ imaState.chainProperties.tc.joAccount.strSgxURL =
+ joArg.value;
+ return true;
+ }
+ if( joArg.name == "sgx-ecdsa-key-main-net" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.chainProperties.mn.joAccount.strSgxKeyName = joArg.value;
+ return true;
+ }
+ if( joArg.name == "sgx-ecdsa-key-s-chain" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.chainProperties.sc.joAccount.strSgxKeyName = joArg.value;
+ return true;
+ }
+ if( joArg.name == "sgx-ecdsa-key-t-chain" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.chainProperties.tc.joAccount.strSgxKeyName = joArg.value;
+ return true;
+ }
+ if( joArg.name == "sgx-ecdsa-key" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.chainProperties.mn.joAccount.strSgxKeyName =
+ imaState.chainProperties.sc.joAccount.strSgxKeyName =
+ imaState.chainProperties.tc.joAccount.strSgxKeyName =
+ joArg.value;
+ return true;
+ }
+ if( joArg.name == "sgx-bls-key-main-net" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.chainProperties.mn.joAccount.strBlsKeyName = joArg.value;
+ return true;
+ }
+ if( joArg.name == "sgx-bls-key-s-chain" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.chainProperties.sc.joAccount.strBlsKeyName = joArg.value;
+ return true;
+ }
+ if( joArg.name == "sgx-bls-key-t-chain" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.chainProperties.tc.joAccount.strBlsKeyName = joArg.value;
+ return true;
+ }
+ if( joArg.name == "sgx-bls-key" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.chainProperties.mn.joAccount.strBlsKeyName =
+ imaState.chainProperties.sc.joAccount.strBlsKeyName =
+ imaState.chainProperties.tc.joAccount.strBlsKeyName =
+ joArg.value;
+ return true;
+ }
+ if( joArg.name == "sgx-ssl-key-main-net" ) {
+ owaspUtils.verifyArgumentIsPathToExistingFile( joArg );
+ imaState.chainProperties.mn.joAccount.strPathSslKey =
+ imaUtils.normalizePath( joArg.value );
+ return true;
+ }
+ if( joArg.name == "sgx-ssl-key-s-chain" ) {
+ owaspUtils.verifyArgumentIsPathToExistingFile( joArg );
+ imaState.chainProperties.sc.joAccount.strPathSslKey =
+ imaUtils.normalizePath( joArg.value );
+ return true;
+ }
+ if( joArg.name == "sgx-ssl-key-t-chain" ) {
+ owaspUtils.verifyArgumentIsPathToExistingFile( joArg );
+ imaState.chainProperties.tc.joAccount.strPathSslKey =
+ imaUtils.normalizePath( joArg.value );
+ return true;
+ }
+ if( joArg.name == "sgx-ssl-key" ) {
+ owaspUtils.verifyArgumentIsPathToExistingFile( joArg );
+ imaState.chainProperties.mn.joAccount.strPathSslKey =
+ imaState.chainProperties.sc.joAccount.strPathSslKey =
+ imaState.chainProperties.tc.joAccount.strPathSslKey =
+ imaUtils.normalizePath( joArg.value );
+ return true;
+ }
+ if( joArg.name == "sgx-ssl-cert-main-net" ) {
+ owaspUtils.verifyArgumentIsPathToExistingFile( joArg );
+ imaState.chainProperties.mn.joAccount.strPathSslCert =
+ imaUtils.normalizePath( joArg.value );
+ return true;
+ }
+ if( joArg.name == "sgx-ssl-cert-s-chain" ) {
+ owaspUtils.verifyArgumentIsPathToExistingFile( joArg );
+ imaState.chainProperties.sc.joAccount.strPathSslCert =
+ imaUtils.normalizePath( joArg.value );
+ return true;
+ }
+ if( joArg.name == "sgx-ssl-cert-t-chain" ) {
+ owaspUtils.verifyArgumentIsPathToExistingFile( joArg );
+ imaState.chainProperties.tc.joAccount.strPathSslCert =
+ imaUtils.normalizePath( joArg.value );
+ return true;
+ }
+ if( joArg.name == "sgx-ssl-cert" ) {
+ owaspUtils.verifyArgumentIsPathToExistingFile( joArg );
+ imaState.chainProperties.mn.joAccount.strPathSslCert =
+ imaState.chainProperties.sc.joAccount.strPathSslCert =
+ imaState.chainProperties.tc.joAccount.strPathSslCert =
+ imaUtils.normalizePath( joArg.value );
+ return true;
+ }
+ return false;
+}
+
+function parseCredentialsArgs( imaState: state.TIMAState, joArg: any ): boolean {
+ if( joArg.name == "address-main-net" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.chainProperties.mn.joAccount.address_ = joArg.value;
+ return true;
+ }
+ if( joArg.name == "address-s-chain" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.chainProperties.sc.joAccount.address_ = joArg.value;
+ return true;
+ }
+ if( joArg.name == "address-t-chain" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.chainProperties.tc.joAccount.address_ = joArg.value;
+ return true;
+ }
+ if( joArg.name == "receiver" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.receiver = joArg.value;
+ return true;
+ }
+ if( joArg.name == "key-main-net" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.chainProperties.mn.joAccount.privateKey = joArg.value;
+ return true;
+ }
+ if( joArg.name == "key-s-chain" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.chainProperties.sc.joAccount.privateKey = joArg.value;
+ return true;
+ }
+ if( joArg.name == "key-t-chain" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.chainProperties.tc.joAccount.privateKey = joArg.value;
+ return true;
+ }
+ return false;
+}
+
+function parseAbiArgs( imaState: state.TIMAState, joArg: any ): boolean {
+ if( joArg.name == "abi-skale-manager" ) {
+ owaspUtils.verifyArgumentIsPathToExistingFile( joArg );
+ imaState.strPathAbiJsonSkaleManager = imaUtils.normalizePath( joArg.value );
+ return true;
+ }
+ if( joArg.name == "abi-main-net" ) {
+ owaspUtils.verifyArgumentIsPathToExistingFile( joArg );
+ imaState.chainProperties.mn.strPathAbiJson = imaUtils.normalizePath( joArg.value );
+ return true;
+ }
+ if( joArg.name == "abi-s-chain" ) {
+ owaspUtils.verifyArgumentIsPathToExistingFile( joArg );
+ imaState.chainProperties.sc.strPathAbiJson = imaUtils.normalizePath( joArg.value );
+ return true;
+ }
+ if( joArg.name == "abi-t-chain" ) {
+ owaspUtils.verifyArgumentIsPathToExistingFile( joArg );
+ imaState.chainProperties.tc.strPathAbiJson = imaUtils.normalizePath( joArg.value );
+ return true;
+ }
+ return false;
+}
+
+function parseErcArgs( imaState: state.TIMAState, joArg: any ): boolean {
+ if( joArg.name == "erc20-main-net" ) {
+ owaspUtils.verifyArgumentIsPathToExistingFile( joArg );
+ imaState.chainProperties.mn.strPathJsonErc20 = imaUtils.normalizePath( joArg.value );
+ return true;
+ }
+ if( joArg.name == "erc20-s-chain" ) {
+ owaspUtils.verifyArgumentIsPathToExistingFile( joArg );
+ imaState.chainProperties.sc.strPathJsonErc20 = imaUtils.normalizePath( joArg.value );
+ return true;
+ }
+ if( joArg.name == "addr-erc20-s-chain" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.strAddrErc20Explicit = joArg.value;
+ return true;
+ }
+ if( joArg.name == "erc20-t-chain" ) {
+ owaspUtils.verifyArgumentIsPathToExistingFile( joArg );
+ imaState.chainProperties.tc.strPathJsonErc20 = imaUtils.normalizePath( joArg.value );
+ return true;
+ }
+ if( joArg.name == "addr-erc20-t-chain" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.strAddrErc20ExplicitTarget = joArg.value;
+ return true;
+ }
+
+ if( joArg.name == "erc721-main-net" ) {
+ owaspUtils.verifyArgumentIsPathToExistingFile( joArg );
+ imaState.chainProperties.mn.strPathJsonErc721 = imaUtils.normalizePath( joArg.value );
+ return true;
+ }
+ if( joArg.name == "erc721-s-chain" ) {
+ owaspUtils.verifyArgumentIsPathToExistingFile( joArg );
+ imaState.chainProperties.sc.strPathJsonErc721 = imaUtils.normalizePath( joArg.value );
+ return true;
+ }
+ if( joArg.name == "addr-erc721-s-chain" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.strAddrErc721Explicit = joArg.value;
+ return true;
+ }
+ if( joArg.name == "erc721-t-chain" ) {
+ owaspUtils.verifyArgumentIsPathToExistingFile( joArg );
+ imaState.chainProperties.tc.strPathJsonErc721 = imaUtils.normalizePath( joArg.value );
+ return true;
+ }
+ if( joArg.name == "addr-erc721-t-chain" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.strAddrErc721ExplicitTarget = joArg.value;
+ return true;
+ }
+
+ if( joArg.name == "erc1155-main-net" ) {
+ owaspUtils.verifyArgumentIsPathToExistingFile( joArg );
+ imaState.chainProperties.mn.strPathJsonErc1155 = imaUtils.normalizePath( joArg.value );
+ return true;
+ }
+ if( joArg.name == "erc1155-s-chain" ) {
+ owaspUtils.verifyArgumentIsPathToExistingFile( joArg );
+ imaState.chainProperties.sc.strPathJsonErc1155 = imaUtils.normalizePath( joArg.value );
+ return true;
+ }
+ if( joArg.name == "addr-erc1155-s-chain" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.strAddrErc1155Explicit = joArg.value;
+ return true;
+ }
+ if( joArg.name == "erc1155-t-chain" ) {
+ owaspUtils.verifyArgumentIsPathToExistingFile( joArg );
+ imaState.chainProperties.tc.strPathJsonErc1155 = imaUtils.normalizePath( joArg.value );
+ return true;
+ }
+ if( joArg.name == "addr-erc1155-t-chain" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.strAddrErc1155ExplicitTarget = joArg.value;
+ return true;
+ }
+ if( joArg.name == "with-metadata" ) {
+ imaState.isWithMetadata721 = true;
+ return true;
+ }
+ return false;
+}
+
+function parseTransactionArgs( imaState: state.TIMAState, joArg: any ): boolean {
+ if( joArg.name == "sleep-between-tx" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaHelperAPIs.setSleepBetweenTransactionsOnSChainMilliseconds( joArg.value );
+ return true;
+ }
+ if( joArg.name == "wait-next-block" ) {
+ imaHelperAPIs.setWaitForNextBlockOnSChain( true );
+ return true;
+ }
+ if( joArg.name == "gas-price-multiplier-mn" ) {
+ let gasPriceMultiplier: number = owaspUtils.toFloat( joArg.value );
+ if( gasPriceMultiplier < 0.0 )
+ gasPriceMultiplier = 0.0;
+ imaState.chainProperties.mn.transactionCustomizer.gasPriceMultiplier =
+ gasPriceMultiplier;
+ return true;
+ }
+ if( joArg.name == "gas-price-multiplier-sc" ) {
+ let gasPriceMultiplier = owaspUtils.toFloat( joArg.value );
+ if( gasPriceMultiplier < 0.0 )
+ gasPriceMultiplier = 0.0;
+ imaState.chainProperties.sc.transactionCustomizer.gasPriceMultiplier =
+ gasPriceMultiplier;
+ return true;
+ }
+ if( joArg.name == "gas-price-multiplier-tc" ) {
+ let gasPriceMultiplier = owaspUtils.toFloat( joArg.value );
+ if( gasPriceMultiplier < 0.0 )
+ gasPriceMultiplier = 0.0;
+ imaState.chainProperties.tc.transactionCustomizer.gasPriceMultiplier =
+ gasPriceMultiplier;
+ return true;
+ }
+ if( joArg.name == "gas-price-multiplier" ) {
+ let gasPriceMultiplier = owaspUtils.toFloat( joArg.value );
+ if( gasPriceMultiplier < 0.0 )
+ gasPriceMultiplier = 0.0;
+ imaState.chainProperties.mn.transactionCustomizer.gasPriceMultiplier =
+ imaState.chainProperties.sc.transactionCustomizer.gasPriceMultiplier =
+ imaState.chainProperties.tc.transactionCustomizer.gasPriceMultiplier =
+ gasPriceMultiplier;
+ return true;
+ }
+
+ if( joArg.name == "gas-multiplier-mn" ) {
+ let gasMultiplier = owaspUtils.toFloat( joArg.value );
+ if( gasMultiplier < 0.0 )
+ gasMultiplier = 0.0;
+ imaState.chainProperties.mn.transactionCustomizer.gasMultiplier =
+ gasMultiplier;
+ return true;
+ }
+ if( joArg.name == "gas-multiplier-sc" ) {
+ let gasMultiplier = owaspUtils.toFloat( joArg.value );
+ if( gasMultiplier < 0.0 )
+ gasMultiplier = 0.0;
+ imaState.chainProperties.sc.transactionCustomizer.gasMultiplier =
+ gasMultiplier;
+ return true;
+ }
+ if( joArg.name == "gas-multiplier-tc" ) {
+ let gasMultiplier = owaspUtils.toFloat( joArg.value );
+ if( gasMultiplier < 0.0 )
+ gasMultiplier = 0.0;
+ imaState.chainProperties.tc.transactionCustomizer.gasMultiplier =
+ gasMultiplier;
+ return true;
+ }
+ if( joArg.name == "gas-multiplier" ) {
+ let gasMultiplier = owaspUtils.toFloat( joArg.value );
+ if( gasMultiplier < 0.0 )
+ gasMultiplier = 0.0;
+ imaState.chainProperties.mn.transactionCustomizer.gasMultiplier =
+ imaState.chainProperties.sc.transactionCustomizer.gasMultiplier =
+ imaState.chainProperties.tc.transactionCustomizer.gasMultiplier =
+ gasMultiplier;
+ return true;
+ }
+ if( joArg.name == "skip-dry-run" ) {
+ imaTx.dryRunEnable( false );
+ return true;
+ }
+ if( joArg.name == "no-skip-dry-run" ) {
+ imaTx.dryRunEnable( true );
+ return true;
+ }
+ if( joArg.name == "ignore-dry-run" ) {
+ imaTx.dryRunIgnore( true );
+ return true;
+ }
+ if( joArg.name == "dry-run" || joArg.name == "no-ignore-dry-run" ) {
+ imaTx.dryRunIgnore( false );
+ return true;
+ }
+ return false;
+}
+
+function parsePaymentAmountArgs( imaState: state.TIMAState, joArg: any ): boolean {
+ if( joArg.name == "value" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.nAmountOfWei = owaspUtils.parseMoneySpecToWei( joArg.value.toString(), true );
+ return true;
+ }
+ if( joArg.name == "wei" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.nAmountOfWei =
+ owaspUtils.parseMoneySpecToWei( joArg.value.toString() + "wei", true );
+ return true;
+ }
+ if( joArg.name == "babbage" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.nAmountOfWei =
+ owaspUtils.parseMoneySpecToWei( joArg.value.toString() + "babbage", true );
+ return true;
+ }
+ if( joArg.name == "lovelace" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.nAmountOfWei =
+ owaspUtils.parseMoneySpecToWei( joArg.value.toString() + "lovelace", true );
+ return true;
+ }
+ if( joArg.name == "shannon" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.nAmountOfWei =
+ owaspUtils.parseMoneySpecToWei( joArg.value.toString() + "shannon", true );
+ return true;
+ }
+ if( joArg.name == "szabo" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.nAmountOfWei =
+ owaspUtils.parseMoneySpecToWei( joArg.value.toString() + "szabo", true );
+ return true;
+ }
+ if( joArg.name == "finney" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.nAmountOfWei =
+ owaspUtils.parseMoneySpecToWei( joArg.value.toString() + "finney", true );
+ return true;
+ }
+ if( joArg.name == "ether" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.nAmountOfWei =
+ owaspUtils.parseMoneySpecToWei( joArg.value.toString() + "ether", true );
+ return true;
+ }
+ if( joArg.name == "amount" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.nAmountOfToken = joArg.value;
+ return true;
+ }
+ if( joArg.name == "tid" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.idToken = joArg.value;
+ imaState.haveOneTokenIdentifier = true;
+ return true;
+ }
+ if( joArg.name == "amounts" ) {
+ imaState.arrAmountsOfTokens = owaspUtils.verifyArgumentIsArrayOfIntegers( joArg );
+ return true;
+ }
+ if( joArg.name == "tids" ) {
+ imaState.idTokens = owaspUtils.verifyArgumentIsArrayOfIntegers( joArg );
+ imaState.haveArrayOfTokenIdentifiers = true;
+ return true;
+ }
+ return false;
+}
+
+function parseTransferArgs( imaState: state.TIMAState, joArg: any ): boolean {
+ if( joArg.name == "s2s-forward" ) {
+ imaHelperAPIs.setForwardS2S();
+ return true;
+ }
+ if( joArg.name == "s2s-reverse" ) {
+ imaHelperAPIs.setReverseS2S();
+ return true;
+ }
+ if( joArg.name == "s2s-enable" ) {
+ imaState.optsS2S.isEnabled = true;
+ return true;
+ }
+ if( joArg.name == "s2s-disable" ) {
+ imaState.optsS2S.isEnabled = false;
+ return true;
+ }
+ if( joArg.name == "no-wait-s-chain" ) {
+ imaState.bNoWaitSChainStarted = true;
+ return true;
+ }
+ if( joArg.name == "max-wait-attempts" ) {
+ imaState.nMaxWaitSChainAttempts = owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ if( joArg.name == "m2s-transfer-block-size" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaState.nTransferBlockSizeM2S = owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ if( joArg.name == "s2m-transfer-block-size" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaState.nTransferBlockSizeS2M = owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ if( joArg.name == "s2s-transfer-block-size" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaState.nTransferBlockSizeS2S = owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ if( joArg.name == "transfer-block-size" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaState.nTransferBlockSizeM2S =
+ imaState.nTransferBlockSizeS2M =
+ imaState.nTransferBlockSizeS2S =
+ owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ if( joArg.name == "m2s-transfer-steps" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaState.nTransferStepsM2S = owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ if( joArg.name == "s2m-transfer-steps" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaState.nTransferStepsS2M = owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ if( joArg.name == "s2s-transfer-steps" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaState.nTransferStepsS2S = owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ if( joArg.name == "transfer-steps" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaState.nTransferStepsM2S =
+ imaState.nTransferStepsS2M =
+ imaState.nTransferStepsS2S =
+ owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ if( joArg.name == "m2s-max-transactions" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaState.nMaxTransactionsM2S = owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ if( joArg.name == "s2m-max-transactions" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaState.nMaxTransactionsS2M = owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ if( joArg.name == "s2s-max-transactions" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaState.nMaxTransactionsS2S = owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ if( joArg.name == "max-transactions" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaState.nMaxTransactionsM2S =
+ imaState.nMaxTransactionsS2M =
+ imaState.nMaxTransactionsS2S =
+ owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ if( joArg.name == "m2s-await-blocks" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaState.nBlockAwaitDepthM2S = owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ if( joArg.name == "s2m-await-blocks" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaState.nBlockAwaitDepthS2M = owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ if( joArg.name == "s2s-await-blocks" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaState.nBlockAwaitDepthS2S = owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ if( joArg.name == "await-blocks" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaState.nBlockAwaitDepthM2S =
+ imaState.nBlockAwaitDepthS2M =
+ imaState.nBlockAwaitDepthS2S =
+ owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ if( joArg.name == "m2s-await-time" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaState.nBlockAgeM2S = owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ if( joArg.name == "s2m-await-time" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaState.nBlockAgeS2M = owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ if( joArg.name == "s2s-await-time" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaState.nBlockAgeS2S = owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ if( joArg.name == "await-time" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaState.nBlockAgeM2S =
+ imaState.nBlockAgeS2M =
+ imaState.nBlockAgeS2S =
+ owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ if( joArg.name == "period" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaState.nLoopPeriodSeconds = owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ if( joArg.name == "node-number" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaState.nNodeNumber = owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ if( joArg.name == "nodes-count" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaState.nNodesCount = owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ if( joArg.name == "time-framing" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaState.nTimeFrameSeconds = owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ if( joArg.name == "time-gap" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaState.nNextFrameGap = owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ return false;
+}
+
+function parseMulticallArgs( imaState: state.TIMAState, joArg: any ): boolean {
+ if( joArg.name == "enable-multicall" ) {
+ imaState.isEnabledMultiCall = true;
+ return true;
+ }
+ if( joArg.name == "disable-multicall" ) {
+ imaState.isEnabledMultiCall = false;
+ return true;
+ }
+ return false;
+}
+
+function parsePendingWorkAnalysisArgs( imaState: state.TIMAState, joArg: any ): boolean {
+ if( joArg.name == "pwa" ) {
+ imaState.isPWA = true;
+ return true;
+ }
+ if( joArg.name == "no-pwa" ) {
+ imaState.isPWA = false;
+ return true;
+ }
+ if( joArg.name == "pwa-timeout" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaState.nTimeoutSecondsPWA = owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ if( joArg.name == "expose-pwa" ) {
+ imaState.isPrintPWA = true;
+ return true;
+ }
+ if( joArg.name == "no-expose-pwa" ) {
+ imaState.isPrintPWA = false;
+ return true;
+ }
+ return false;
+}
+
+function parseLoggingArgs( imaState: state.TIMAState, joArg: any ): boolean {
+ if( joArg.name == "gathered" ) {
+ imaState.isPrintGathered = true;
+ return true;
+ }
+ if( joArg.name == "no-gathered" ) {
+ imaState.isPrintGathered = false;
+ return true;
+ }
+ if( joArg.name == "expose-security-info" ) {
+ imaState.isPrintSecurityValues = true;
+ return true;
+ }
+ if( joArg.name == "no-expose-security-info" ) {
+ imaState.isPrintSecurityValues = false;
+ return true;
+ }
+ if( joArg.name == "log-size" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaState.nLogMaxSizeBeforeRotation = owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ if( joArg.name == "log-files" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaState.nLogMaxFilesCount = owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ if( joArg.name == "accumulated-log-in-transfer" ) {
+ imaState.isDynamicLogInDoTransfer = false;
+ return true;
+ }
+ if( joArg.name == "accumulated-log-in-bls-signer" ) {
+ imaState.isDynamicLogInBlsSigner = false;
+ return true;
+ }
+ if( joArg.name == "dynamic-log-in-transfer" ) {
+ imaState.isDynamicLogInDoTransfer = true;
+ return true;
+ }
+ if( joArg.name == "dynamic-log-in-bls-signer" ) {
+ imaState.isDynamicLogInBlsSigner = true;
+ return true;
+ }
+ if( joArg.name == "log" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.strLogFilePath = joArg.value.toString();
+ return true;
+ }
+ return false;
+}
+
+function parseBlsArgs( imaState: state.TIMAState, joArg: any ): boolean {
+ if( joArg.name == "sign-messages" ) {
+ imaState.bSignMessages = true;
+ return true;
+ }
+ if( joArg.name == "bls-glue" ) {
+ owaspUtils.verifyArgumentIsPathToExistingFile( joArg );
+ imaState.strPathBlsGlue = joArg.value.toString();
+ return true;
+ }
+ if( joArg.name == "hash-g1" ) {
+ owaspUtils.verifyArgumentIsPathToExistingFile( joArg );
+ imaState.strPathHashG1 = joArg.value.toString();
+ return true;
+ }
+ if( joArg.name == "bls-verify" ) {
+ owaspUtils.verifyArgumentIsPathToExistingFile( joArg );
+ imaState.strPathBlsVerify = joArg.value.toString();
+ return true;
+ }
+ return false;
+}
+
+function parseMonitoringArgs( imaState: state.TIMAState, joArg: any ): boolean {
+ if( joArg.name == "monitoring-port" ) {
+ owaspUtils.verifyArgumentIsIntegerIpPortNumber( joArg );
+ imaState.nMonitoringPort = owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ if( joArg.name == "monitoring-log" ) {
+ owaspUtils.verifyArgumentIsIntegerIpPortNumber( joArg );
+ imaState.bLogMonitoringServer = true;
+ return true;
+ }
+ return false;
+}
+
+function parseReimbursementArgs( imaState: state.TIMAState, joArg: any ): boolean {
+ if( joArg.name == "reimbursement-chain" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.strReimbursementChain = joArg.value.trim();
+ return true;
+ }
+ if( joArg.name == "reimbursement-recharge" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.nReimbursementRecharge =
+ owaspUtils.parseMoneySpecToWei( joArg.value.toString(), true );
+ return true;
+ }
+ if( joArg.name == "reimbursement-withdraw" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.nReimbursementWithdraw =
+ owaspUtils.parseMoneySpecToWei( joArg.value.toString(), true );
+ return true;
+ }
+ if( joArg.name == "reimbursement-balance" ) {
+ imaState.isShowReimbursementBalance = true;
+ return true;
+ }
+ if( joArg.name == "reimbursement-estimate" ) {
+ imaState.isReimbursementEstimate = true;
+ return true;
+ }
+ if( joArg.name == "reimbursement-range" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.nReimbursementRange = owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ return false;
+}
+
+function parseOracleArgs( imaState: state.TIMAState, joArg: any ): boolean {
+ if( joArg.name == "enable-oracle" ) {
+ imaOracleOperations.setEnabledOracle( true );
+ return true;
+ }
+ if( joArg.name == "disable-oracle" ) {
+ imaOracleOperations.setEnabledOracle( false );
+ return true;
+ }
+ return false;
+}
+
+function parseNetworkDiscoveryArgs( imaState: state.TIMAState, joArg: any ): boolean {
+ if( joArg.name == "network-browser-path" ) {
+ owaspUtils.verifyArgumentWithNonEmptyValue( joArg );
+ imaState.optsS2S.strNetworkBrowserPath = joArg.value.toString();
+ return true;
+ }
+ return false;
+}
+
+function parseBlockScannerArgs( imaState: state.TIMAState, joArg: any ): boolean {
+ if( joArg.name == "bs-step-size" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaHelperAPIs.setBlocksCountInInIterativeStepOfEventsScan(
+ owaspUtils.toInteger( joArg.value ) );
+ return true;
+ }
+ if( joArg.name == "bs-max-all-range" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaHelperAPIs.setMaxIterationsInAllRangeEventsScan( owaspUtils.toInteger( joArg.value ) );
+ return true;
+ }
+ if( joArg.name == "bs-progressive-enable" ) {
+ imaTransferErrorHandling.setEnabledProgressiveEventsScan( true );
+ return true;
+ }
+ if( joArg.name == "bs-progressive-disable" ) {
+ imaTransferErrorHandling.setEnabledProgressiveEventsScan( false );
+ return true;
+ }
+ return false;
+}
+
+function parseJsonRpcServerArgs( imaState: state.TIMAState, joArg: any ): boolean {
+ if( joArg.name == "json-rpc-port" ) {
+ owaspUtils.verifyArgumentIsIntegerIpPortNumber( joArg );
+ imaState.nJsonRpcPort = owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ return false;
+}
+
+function parseCrossImaCommunicationArgs( imaState: state.TIMAState, joArg: any ): boolean {
+ if( joArg.name == "cross-ima" ) {
+ imaState.isCrossImaBlsMode = true;
+ return true;
+ }
+ if( joArg.name == "no-cross-ima" ) {
+ imaState.isCrossImaBlsMode = false;
+ return true;
+ }
+ return false;
+}
+
+function parseShowConfigArgs( imaState: state.TIMAState, joArg: any ): boolean {
+ if( joArg.name == "show-config" ) {
+ imaState.bShowConfigMode = true;
+ return true;
+ }
+ return false;
+}
+
+function parseOtherArgs( imaState: state.TIMAState, joArg: any ): boolean {
+ if( joArg.name == "auto-exit" ) {
+ owaspUtils.verifyArgumentIsInteger( joArg );
+ imaState.nAutoExitAfterSeconds = owaspUtils.toInteger( joArg.value );
+ return true;
+ }
+ return false;
+}
+
+export function parse( joExternalHandlers: any, argv?: any[] ): number {
+ const imaState: state.TIMAState = state.get();
+ argv = argv ?? process.argv;
+ const cntArgs = argv.length;
+ for( let idxArg = 2; idxArg < cntArgs; ++idxArg ) {
+ const joArg = parseCommandLineArgument( argv[idxArg] );
+ parseHelp( imaState, joArg ); // exits process on "--help"
+ parseVersion( imaState, joArg ); // exits process on "--version"
+ if( parseBasicArgs( imaState, joArg ) )
+ continue;
+ if( parseChainAccessArgs( imaState, joArg ) )
+ continue;
+ if( parseTransactionManagerArgs( imaState, joArg ) )
+ continue;
+ if( parseSgxArgs( imaState, joArg ) )
+ continue;
+ if( parseCredentialsArgs( imaState, joArg ) )
+ continue;
+ if( parseAbiArgs( imaState, joArg ) )
+ continue;
+ if( parseErcArgs( imaState, joArg ) )
+ continue;
+ if( parseTransactionArgs( imaState, joArg ) )
+ continue;
+ if( parsePaymentAmountArgs( imaState, joArg ) )
+ continue;
+ if( parseTransferArgs( imaState, joArg ) )
+ continue;
+ if( parseMulticallArgs( imaState, joArg ) )
+ continue;
+ if( parsePendingWorkAnalysisArgs( imaState, joArg ) )
+ continue;
+ if( parseLoggingArgs( imaState, joArg ) )
+ continue;
+ if( parseBlsArgs( imaState, joArg ) )
+ continue;
+ if( parseMonitoringArgs( imaState, joArg ) )
+ continue; if( parseBlockScannerArgs( imaState, joArg ) )
+ continue;
+ if( parseReimbursementArgs( imaState, joArg ) )
+ continue;
+ if( parseOracleArgs( imaState, joArg ) )
+ continue;
+ if( parseNetworkDiscoveryArgs( imaState, joArg ) )
+ continue;
+ if( parseBlockScannerArgs( imaState, joArg ) )
+ continue;
+ if( parseJsonRpcServerArgs( imaState, joArg ) )
+ continue;
+ if( parseCrossImaCommunicationArgs( imaState, joArg ) )
+ continue;
+ if( parseShowConfigArgs( imaState, joArg ) )
+ continue;
+ if( parseOtherArgs( imaState, joArg ) )
+ continue;
+ if( joArg.name == "register" ||
+ joArg.name == "register1" ||
+ joArg.name == "check-registration" ||
+ joArg.name == "check-registration1" ||
+ joArg.name == "check-registration2" ||
+ joArg.name == "check-registration3" ||
+ joArg.name == "mint-erc20" ||
+ joArg.name == "mint-erc721" ||
+ joArg.name == "mint-erc1155" ||
+ joArg.name == "burn-erc20" ||
+ joArg.name == "burn-erc721" ||
+ joArg.name == "burn-erc1155" ||
+ joArg.name == "show-balance" ||
+ joArg.name == "m2s-payment" ||
+ joArg.name == "s2m-payment" ||
+ joArg.name == "s2m-receive" ||
+ joArg.name == "s2m-view" ||
+ joArg.name == "s2s-payment" ||
+ joArg.name == "m2s-transfer" ||
+ joArg.name == "s2m-transfer" ||
+ joArg.name == "s2s-transfer" ||
+ joArg.name == "transfer" ||
+ joArg.name == "loop" ||
+ joArg.name == "simple-loop" ||
+ joArg.name == "browse-s-chain"
+ ) {
+ joExternalHandlers[joArg.name]();
+ continue;
+ }
+ console.log( log.fmtFatal( "COMMAND LINE PARSER ERROR: unknown command line argument {}",
+ joArg.name ) );
+ return 666;
+ }
+ return 0;
+}
+
+async function asyncCheckUrlAtStartup( u: URL | string, name: string ): Promise < boolean > {
+ const details = log.createMemoryStream();
+ const nTimeoutMilliseconds = 10 * 1000;
+ try {
+ details.debug( "Will check URL {url} connectivity for {} at start-up...", u, name );
+ const isLog = false;
+ const isOnLine = await rpcCall.checkUrl( u, nTimeoutMilliseconds, isLog );
+ if( isOnLine ) {
+ details.success( "Done, start-up checking URL {url} connectivity for {}, " +
+ "URL is on-line.", u, name );
+ } else {
+ details.warning( "Done, start-up checking URL {url} connectivity for {}, " +
+ "URL is off-line.", u, name );
+ }
+ return isOnLine;
+ } catch ( err ) {
+ details.error( "Failed to check URL {url} connectivity for {} at start-up, " +
+ "error is: {err}, stack is:\n{stack}", u, name, err, err );
+ }
+ return false;
+}
+
+function commonInitPrintSysInfo(): void {
+ const imaState: state.TIMAState = state.get();
+ const isPrintGathered = ( !!( imaState.isPrintGathered ) );
+ if( isPrintGathered ) {
+ log.debug( "This process {sunny} is {}", "versions", process.versions );
+ log.debug( "This process {sunny} is {}", "PID", process.pid );
+ log.debug( "This process {sunny} is {}", "PPID", process.ppid );
+ log.debug( "This process {sunny} is {}", "EGID",
+ process.getegid ? process.getegid() : "N/A" );
+ log.debug( "This process {sunny} is {}", "EUID",
+ process.geteuid ? process.geteuid() : "N/A" );
+ log.debug( "This process {sunny} is {}", "GID",
+ process.getgid ? process.getgid() : "N/A" );
+ log.debug( "This process {sunny} is {}", "UID",
+ process.getuid ? process.getuid() : "N/A" );
+ log.debug( "This process {sunny} are {}", "groups",
+ process.getgroups ? process.getgroups() : "N/A" );
+ log.debug( "This process {sunny} is {}", "CWD", process.cwd() );
+ log.debug( "This process {sunny} is {}", "architecture", process.arch );
+ log.debug( "This process {sunny} is {}", "execPath", process.execPath );
+ log.debug( "This process {sunny} is {}", "platform", process.platform );
+ log.debug( "This process {sunny} is {}", "release", process.release );
+ log.debug( "This process {sunny} is {}", "report", process.report );
+ log.debug( "This process {sunny} is {}", "config", process.config );
+ log.debug( "Node JS {sunny} is {}", "detailed version information", process.versions );
+ log.debug( "OS {sunny} is {}", "type", os.type() );
+ log.debug( "OS {sunny} is {}", "platform", os.platform() );
+ log.debug( "OS {sunny} is {}", "release", os.release() );
+ log.debug( "OS {sunny} is {}", "architecture", os.arch() );
+ log.debug( "OS {sunny} is {}", "endianness", os.endianness() );
+ log.debug( "OS {sunny} is {}", "host name", os.hostname() );
+ log.debug( "OS {sunny} is {}", "CPUs are ", os.cpus() );
+ log.debug( "OS {sunny} are {}", "network interfaces", os.networkInterfaces() );
+ log.debug( "OS {sunny} is {}", "home dir", os.homedir() );
+ log.debug( "OS {sunny} is {}", "tmp dir", os.tmpdir() );
+ log.debug( "OS {sunny} is {}", "uptime", os.uptime() );
+ log.debug( "OS {sunny} is {}", "user", os.userInfo() );
+ const joMemory: any = { total: os.totalmem(), free: os.freemem() };
+ joMemory.freePercent = ( joMemory.free / joMemory.total ) * 100.0;
+ log.debug( "OS {sunny} is {}", "memory", joMemory );
+ const joLA = os.loadavg();
+ log.debug( "OS {sunny} is {}", "average load", joLA );
+ }
+}
+
+function commonInitCheckAbiPaths(): void {
+ const imaState: state.TIMAState = state.get();
+ if( imaState.strPathAbiJsonSkaleManager &&
+ ( typeof imaState.strPathAbiJsonSkaleManager === "string" ) &&
+ imaState.strPathAbiJsonSkaleManager.length > 0
+ ) {
+ imaState.joAbiSkaleManager =
+ imaUtils.jsonFileLoad( imaState.strPathAbiJsonSkaleManager, null );
+ imaState.bHaveSkaleManagerABI = true;
+ } else {
+ imaState.bHaveSkaleManagerABI = false;
+ log.warning( "WARNING: No Skale Manager ABI file path is provided in command line " +
+ "arguments(needed for particular operations only)" );
+ }
+
+ if( imaState.chainProperties.mn.strPathAbiJson &&
+ typeof imaState.chainProperties.mn.strPathAbiJson === "string" &&
+ imaState.chainProperties.mn.strPathAbiJson.length > 0 ) {
+ imaState.chainProperties.mn.joAbiIMA =
+ imaUtils.jsonFileLoad( imaState.chainProperties.mn.strPathAbiJson, null );
+ imaState.chainProperties.mn.bHaveAbiIMA = true;
+ } else {
+ imaState.chainProperties.mn.bHaveAbiIMA = false;
+ log.warning( "WARNING: No Main-net IMA ABI file path is provided in command line " +
+ "arguments(needed for particular operations only)" );
+ }
+
+ if( imaState.chainProperties.sc.strPathAbiJson &&
+ typeof imaState.chainProperties.sc.strPathAbiJson === "string" &&
+ imaState.chainProperties.sc.strPathAbiJson.length > 0
+ ) {
+ imaState.chainProperties.sc.joAbiIMA =
+ imaUtils.jsonFileLoad( imaState.chainProperties.sc.strPathAbiJson, null );
+ imaState.chainProperties.sc.bHaveAbiIMA = true;
+ } else {
+ imaState.chainProperties.sc.bHaveAbiIMA = false;
+ log.warning( "WARNING: No S-Chain IMA ABI file path is provided in command line arguments" +
+ "(needed for particular operations only)" );
+ }
+
+ if( imaState.chainProperties.tc.strPathAbiJson &&
+ typeof imaState.chainProperties.tc.strPathAbiJson === "string" &&
+ imaState.chainProperties.tc.strPathAbiJson.length > 0
+ ) {
+ imaState.chainProperties.tc.joAbiIMA =
+ imaUtils.jsonFileLoad( imaState.chainProperties.tc.strPathAbiJson, null );
+ imaState.chainProperties.tc.bHaveAbiIMA = true;
+ } else {
+ imaState.chainProperties.tc.bHaveAbiIMA = false;
+ log.warning( "WARNING: No S<->S Target S-Chain IMA ABI file path is provided " +
+ "in command line arguments(needed for particular operations only)" );
+ }
+}
+
+function commonInitCheckContractPresences(): void {
+ const imaState: state.TIMAState = state.get();
+ if( imaState.bHaveSkaleManagerABI ) {
+ imaUtils.checkKeysExistInABI( "skale-manager",
+ imaState.strPathAbiJsonSkaleManager,
+ imaState.joAbiSkaleManager, [
+ // partial list of Skale Manager's contracts specified here:
+ "constants_holder_abi",
+ "constants_holder_address",
+ "nodes_abi",
+ "nodes_address",
+ "key_storage_abi",
+ "key_storage_address",
+ "schains_abi",
+ "schains_address",
+ "schains_internal_abi",
+ "schains_internal_address",
+ "skale_d_k_g_abi",
+ "skale_d_k_g_address",
+ "skale_manager_abi",
+ "skale_manager_address",
+ "skale_token_abi",
+ "skale_token_address",
+ "validator_service_abi",
+ "validator_service_address",
+ "wallets_abi",
+ "wallets_address"
+ ] );
+ } else if( imaState.optsS2S.isEnabled )
+ log.warning( "WARNING: Missing Skale Manager ABI path for S-Chain to S-Chain transfers" );
+
+ if( imaState.chainProperties.mn.bHaveAbiIMA ) {
+ imaUtils.checkKeysExistInABI( "main-net",
+ imaState.chainProperties.mn.strPathAbiJson,
+ imaState.chainProperties.mn.joAbiIMA, [
+ "deposit_box_eth_abi",
+ "deposit_box_eth_address",
+ "message_proxy_mainnet_abi",
+ "message_proxy_mainnet_address",
+ "linker_abi",
+ "linker_address",
+ "deposit_box_erc20_abi",
+ "deposit_box_erc20_address",
+ "deposit_box_erc721_abi",
+ "deposit_box_erc721_address",
+ "deposit_box_erc1155_abi",
+ "deposit_box_erc1155_address",
+ "deposit_box_erc721_with_metadata_abi",
+ "deposit_box_erc721_with_metadata_address",
+ "community_pool_abi",
+ "community_pool_address"
+ ] );
+ }
+ if( imaState.chainProperties.sc.bHaveAbiIMA ) {
+ imaUtils.checkKeysExistInABI( "S-Chain",
+ imaState.chainProperties.sc.strPathAbiJson,
+ imaState.chainProperties.sc.joAbiIMA, [
+ "token_manager_eth_abi",
+ "token_manager_eth_address",
+ "token_manager_erc20_abi",
+ "token_manager_erc20_address",
+ "token_manager_erc721_abi",
+ "token_manager_erc721_address",
+ "token_manager_erc1155_abi",
+ "token_manager_erc1155_address",
+ "token_manager_erc721_with_metadata_abi",
+ "token_manager_erc721_with_metadata_address",
+ "message_proxy_chain_abi",
+ "message_proxy_chain_address",
+ "token_manager_linker_abi",
+ "token_manager_linker_address",
+ "community_locker_abi",
+ "community_locker_address"
+ ] );
+ }
+ if( imaState.chainProperties.tc.bHaveAbiIMA ) {
+ imaUtils.checkKeysExistInABI( "S<->S Target S-Chain",
+ imaState.chainProperties.tc.strPathAbiJson,
+ imaState.chainProperties.tc.joAbiIMA, [
+ "token_manager_eth_abi",
+ "token_manager_eth_address",
+ "token_manager_erc20_abi",
+ "token_manager_erc20_address",
+ "token_manager_erc721_abi",
+ "token_manager_erc721_address",
+ "token_manager_erc1155_abi",
+ "token_manager_erc1155_address",
+ "token_manager_erc721_with_metadata_abi",
+ "token_manager_erc721_with_metadata_address",
+ "message_proxy_chain_abi",
+ "message_proxy_chain_address",
+ "token_manager_linker_abi",
+ "token_manager_linker_address",
+ "community_locker_abi",
+ "community_locker_address"
+ ] );
+ }
+}
+
+function commonInitPrintFoundContracts(): void {
+ const imaState: state.TIMAState = state.get();
+ const isPrintGathered = ( !!( imaState.isPrintGathered ) );
+ // deposit_box_eth_address --> deposit_box_eth_abi
+ // deposit_box_erc20_address --> deposit_box_erc20_abi
+ // deposit_box_erc721_address --> deposit_box_erc721_abi
+ // deposit_box_erc1155_address --> deposit_box_erc1155_abi
+ // deposit_box_erc721_with_metadata_address --> deposit_box_erc721_with_metadata_abi
+ // linker_address --> linker_abi
+ // token_manager_eth_address --> token_manager_eth_abi
+ // token_manager_erc20_address --> token_manager_erc20_abi
+ // token_manager_erc721_address --> token_manager_erc721_abi
+ // token_manager_erc1155_address --> token_manager_erc1155_abi
+ // token_manager_erc721_with_metadata_address --> token_manager_erc721_with_metadata_abi
+ // token_manager_linker_address --> token_manager_linker_abi
+ // message_proxy_mainnet_address --> message_proxy_mainnet_abi
+ // message_proxy_chain_address --> message_proxy_chain_abi
+
+ const oct = function(
+ joContract?: owaspUtils.ethersMod.ethers.Contract | null ): string {
+ // optional contract address
+ if( joContract && "address" in joContract && joContract.address )
+ return log.fmtInformation( "{}", joContract.address );
+ return log.fmtError( "contract is not available" );
+ };
+
+ if( isPrintGathered ) {
+ log.debug( "IMA contracts(Main Net):" );
+ log.debug( "{sunny}...................address is.....{}", "DepositBoxEth",
+ oct( imaState.joDepositBoxETH ) );
+ log.debug( "{sunny}.................address is.....{}", "DepositBoxERC20",
+ oct( imaState.joDepositBoxERC20 ) );
+ log.debug( "{sunny}................address is.....{}", "DepositBoxERC721",
+ oct( imaState.joDepositBoxERC721 ) );
+ log.debug( "{sunny}...............address is.....{}", "DepositBoxERC1155",
+ oct( imaState.joDepositBoxERC1155 ) );
+ log.debug( "{sunny}....address is.....{}", "DepositBoxERC721WithMetadata",
+ oct( imaState.joDepositBoxERC721WithMetadata ) );
+ log.debug( "{sunny}...................address is.....{}", "CommunityPool",
+ oct( imaState.joCommunityPool ) );
+ log.debug( "{sunny}....................address is.....{}", "MessageProxy",
+ oct( imaState.joMessageProxyMainNet ) );
+ log.debug( "{sunny}..........................address is.....{}", "Linker",
+ oct( imaState.joLinker ) );
+ log.debug( "IMA contracts(S-Chain):" );
+ log.debug( "{sunny}.................address is.....{}", "TokenManagerEth",
+ oct( imaState.joTokenManagerETH ) );
+ log.debug( "{sunny}...............address is.....{}", "TokenManagerERC20",
+ oct( imaState.joTokenManagerERC20 ) );
+ log.debug( "{sunny}..............address is.....{}", "TokenManagerERC721",
+ oct( imaState.joTokenManagerERC721 ) );
+ log.debug( "{sunny}.............address is.....{}", "TokenManagerERC1155",
+ oct( imaState.joTokenManagerERC1155 ) );
+ log.debug( "{sunny}..address is.....{}", "TokenManagerERC721WithMetadata",
+ oct( imaState.joTokenManagerERC721WithMetadata ) );
+ log.debug( "{sunny}.................address is.....{}", "CommunityLocker",
+ oct( imaState.joCommunityLocker ) );
+ log.debug( "{sunny}....................address is.....{}", "MessageProxy",
+ oct( imaState.joMessageProxySChain ) );
+ log.debug( "{sunny}..............address is.....{}", "TokenManagerLinker",
+ oct( imaState.joTokenManagerLinker ) );
+ log.debug( "{sunny} ..........................address is.....{}", "ERC20",
+ oct( imaState.joEthErc20 ) );
+ log.debug( "IMA contracts(Target S-Chain):" );
+ log.debug( "{sunny}...............address is.....{}", "TokenManagerERC20",
+ oct( imaState.joTokenManagerERC20Target ) );
+ log.debug( "{sunny}..............address is.....{}", "TokenManagerERC721",
+ oct( imaState.joTokenManagerERC721Target ) );
+ log.debug( "{sunny}.............address is.....{}", "TokenManagerERC1155",
+ oct( imaState.joTokenManagerERC1155Target ) );
+ log.debug( "{sunny}..address is.....{}", "TokenManagerERC721WithMetadata",
+ oct( imaState.joTokenManagerERC721WithMetadataTarget ) );
+ log.debug( "{sunny}.................address is.....{}", "CommunityLocker",
+ oct( imaState.joCommunityLockerTarget ) );
+ log.debug( "{sunny}....................address is.....{}", "MessageProxy",
+ oct( imaState.joMessageProxySChainTarget ) );
+ log.debug( "{sunny}..............address is.....{}", "TokenManagerLinker",
+ oct( imaState.joTokenManagerLinkerTarget ) );
+ log.debug( "{sunny} ..........................address is.....{}", "ERC20",
+ oct( imaState.joEthErc20Target ) );
+
+ log.debug( "Skale Manager contracts:" );
+ log.debug( "{sunny}.................address is.....{}", "ConstantsHolder",
+ oct( imaState.joConstantsHolder ) );
+ log.debug( "{sunny}...........................address is.....{}", "Nodes",
+ oct( imaState.joNodes ) );
+ log.debug( "{sunny}......................address is.....{}", "KeyStorage",
+ oct( imaState.joKeyStorage ) );
+ log.debug( "{sunny}.........................address is.....{}", "Schains",
+ oct( imaState.joSChains ) );
+ log.debug( "{sunny}.................address is.....{}", "SchainsInternal",
+ oct( imaState.joSChainsInternal ) );
+ log.debug( "{sunny}........................address is.....{}", "SkaleDKG",
+ oct( imaState.joSkaleDKG ) );
+ log.debug( "{sunny}....................address is.....{}", "SkaleManager",
+ oct( imaState.joSkaleManager ) );
+ log.debug( "{sunny}......................address is.....{}", "SkaleToken",
+ oct( imaState.joSkaleToken ) );
+ log.debug( "{sunny}................address is.....{}", "ValidatorService",
+ oct( imaState.joValidatorService ) );
+ log.debug( "{sunny}.........................address is.....{}", "Wallets",
+ oct( imaState.joWallets ) );
+ }
+}
+
+function commonInitCheckErc20(): void {
+ const imaState: state.TIMAState = state.get();
+ const isPrintGathered = ( !!( imaState.isPrintGathered ) );
+ let n1 = 0;
+ let n2 = 0;
+ if( imaState.chainProperties.mn.strPathJsonErc20.length > 0 ) {
+ if( isPrintGathered ) {
+ log.information( "Loading Main-net ERC20 ABI from {}",
+ imaState.chainProperties.mn.strPathJsonErc20 );
+ }
+ imaState.chainProperties.mn.joErc20 =
+ imaUtils.jsonFileLoad( imaState.chainProperties.mn.strPathJsonErc20, null );
+ n1 = Object.keys( imaState.chainProperties.mn.joErc20 ).length;
+ if( imaState.chainProperties.sc.strPathJsonErc20.length > 0 ) {
+ if( isPrintGathered ) {
+ log.information( "Loading S-Chain ERC20 ABI from {}",
+ imaState.chainProperties.sc.strPathJsonErc20 );
+ }
+ imaState.chainProperties.sc.joErc20 =
+ imaUtils.jsonFileLoad( imaState.chainProperties.sc.strPathJsonErc20, null );
+ n2 = Object.keys( imaState.chainProperties.sc.joErc20 ).length;
+ }
+ if( n1 > 0 ) {
+ imaState.chainProperties.tc.strCoinNameErc20 =
+ imaUtils.discoverCoinNameInJSON( imaState.chainProperties.mn.joErc20 );
+ if( n2 > 0 ) {
+ imaState.chainProperties.sc.strCoinNameErc20 =
+ imaUtils.discoverCoinNameInJSON( imaState.chainProperties.sc.joErc20 );
+ }
+ n1 = imaState.chainProperties.tc.strCoinNameErc20.length;
+ if( n2 > 0 )
+ n2 = imaState.chainProperties.sc.strCoinNameErc20.length;
+ if( n1 > 0 ) {
+ if( isPrintGathered &&
+ ( !imaState.bShowConfigMode )
+ ) {
+ if( isPrintGathered ) {
+ log.information( "Loaded Main-net ERC20 ABI {}",
+ imaState.chainProperties.tc.strCoinNameErc20 );
+ }
+ if( isPrintGathered && n2 > 0 ) {
+ log.information( "Loaded S-Chain ERC20 ABI {}",
+ imaState.chainProperties.sc.strCoinNameErc20 );
+ }
+ }
+ } else {
+ if( n1 === 0 )
+ log.error( "Main-net ERC20 token name is not discovered(malformed JSON)" );
+
+ if( n2 === 0 && imaState.chainProperties.sc.strPathJsonErc20.length > 0 )
+ log.error( "S-Chain ERC20 token name is not discovered(malformed JSON)" );
+
+ imaState.chainProperties.mn.joErc20 = null;
+ imaState.chainProperties.sc.joErc20 = null;
+ imaState.chainProperties.tc.strCoinNameErc20 = "";
+ imaState.chainProperties.sc.strCoinNameErc20 = "";
+ process.exit( 126 );
+ }
+ } else {
+ if( n1 === 0 )
+ log.error( "Main-net ERC20 JSON is invalid" );
+ if( n2 === 0 && imaState.chainProperties.sc.strPathJsonErc20.length > 0 )
+ log.error( "S-Chain ERC20 JSON is invalid" );
+ imaState.chainProperties.mn.joErc20 = null;
+ imaState.chainProperties.sc.joErc20 = null;
+ imaState.chainProperties.tc.strCoinNameErc20 = "";
+ imaState.chainProperties.sc.strCoinNameErc20 = "";
+ process.exit( 126 );
+ }
+ } else {
+ if( imaState.chainProperties.sc.strPathJsonErc20.length > 0 ) {
+ n1 = 0;
+ n2 = 0;
+ if( isPrintGathered ) {
+ log.information( "Loading S-Chain ERC20 ABI from {}",
+ imaState.chainProperties.sc.strPathJsonErc20 );
+ }
+ imaState.chainProperties.sc.joErc20 =
+ imaUtils.jsonFileLoad( imaState.chainProperties.sc.strPathJsonErc20, null );
+ n2 = Object.keys( imaState.chainProperties.sc.joErc20 ).length;
+ if( n2 > 0 ) {
+ imaState.chainProperties.sc.strCoinNameErc20 =
+ imaUtils.discoverCoinNameInJSON( imaState.chainProperties.sc.joErc20 );
+ n2 = imaState.chainProperties.sc.strCoinNameErc20.length;
+ if( n2 > 0 ) {
+ if( isPrintGathered ) {
+ log.information( "Loaded S-Chain ERC20 ABI {}",
+ imaState.chainProperties.sc.strCoinNameErc20 );
+ }
+ } else {
+ if( n2 === 0 && imaState.chainProperties.sc.strPathJsonErc20.length > 0 )
+ log.error( "S-Chain ERC20 token name is not discovered(malformed JSON)" );
+ imaState.chainProperties.mn.joErc20 = null;
+ imaState.chainProperties.sc.joErc20 = null;
+ imaState.chainProperties.tc.strCoinNameErc20 = "";
+ imaState.chainProperties.sc.strCoinNameErc20 = "";
+ process.exit( 126 );
+ }
+ }
+ }
+ }
+ if( n1 !== 0 && n2 === 0 ) {
+ if( imaState.strAddrErc20Explicit.length === 0 ) {
+ log.warning( "IMPORTANT NOTICE: Both S-Chain ERC20 JSON and explicit " +
+ "ERC20 address are not specified" );
+ } else {
+ if( isPrintGathered )
+ log.attention( "IMPORTANT NOTICE: S-Chain ERC20 ABI will be auto-generated" );
+ imaState.chainProperties.sc.strCoinNameErc20 =
+ imaState.chainProperties.tc.strCoinNameErc20 ?? ""; // assume same
+ imaState.chainProperties.sc.joErc20 =
+ JSON.parse( JSON.stringify( imaState.chainProperties.mn.joErc20 ) ); // clone
+ imaState.chainProperties.sc.joErc20[
+ imaState.chainProperties.sc.strCoinNameErc20 + "_address"] =
+ imaState.strAddrErc20Explicit; // set explicit address
+ }
+ }
+
+ if( imaState.chainProperties.tc.strPathJsonErc20.length > 0 ) {
+ if( isPrintGathered ) {
+ log.information( "Loading S<->S Target S-Chain ERC20 ABI from {}",
+ imaState.chainProperties.tc.strPathJsonErc20 );
+ }
+ imaState.chainProperties.tc.joErc20 =
+ imaUtils.jsonFileLoad( imaState.chainProperties.tc.strPathJsonErc20, null );
+ n2 = Object.keys( imaState.chainProperties.tc.joErc20 ).length;
+ if( n2 > 0 ) {
+ imaState.chainProperties.tc.strCoinNameErc20 =
+ imaUtils.discoverCoinNameInJSON( imaState.chainProperties.tc.joErc20 );
+ n2 = imaState.chainProperties.tc.strCoinNameErc20.length;
+ if( n2 > 0 ) {
+ if( isPrintGathered ) {
+ log.information( "Loaded S<->S Target S-Chain ERC20 ABI {}",
+ imaState.chainProperties.tc.strCoinNameErc20 );
+ }
+ } else {
+ if( n2 === 0 && imaState.chainProperties.tc.strPathJsonErc20.length > 0 ) {
+ log.fatal( "S<->S Target S-Chain ERC20 token name " +
+ "is not discovered(malformed JSON)" );
+ }
+ imaState.chainProperties.tc.joErc20 = null;
+ imaState.chainProperties.tc.strCoinNameErc20 = "";
+ process.exit( 126 );
+ }
+ }
+ }
+ if( isPrintGathered &&
+ imaState.strAddrErc20ExplicitTarget.length === 0 &&
+ imaState.chainProperties.tc.strCoinNameErc20.length === 0 &&
+ imaState.chainProperties.sc.strCoinNameErc20.length > 0
+ ) {
+ log.warning( "IMPORTANT NOTICE: Both S<->S Target S-Chain ERC20 JSON and explicit " +
+ "ERC20 address are not specified" );
+ }
+}
+
+function commonInitCheckErc721(): void {
+ const imaState: state.TIMAState = state.get();
+ const isPrintGathered = ( !!( imaState.isPrintGathered ) );
+ let n1 = 0;
+ let n2 = 0;
+ if( imaState.chainProperties.mn.strPathJsonErc721.length > 0 ) {
+ if( isPrintGathered ) {
+ log.information( "Loading Main-net ERC721 ABI from {}",
+ imaState.chainProperties.mn.strPathJsonErc721 );
+ }
+ imaState.chainProperties.mn.joErc721 =
+ imaUtils.jsonFileLoad( imaState.chainProperties.mn.strPathJsonErc721, null );
+ n1 = Object.keys( imaState.chainProperties.mn.joErc721 ).length;
+ if( imaState.chainProperties.sc.strPathJsonErc721.length > 0 ) {
+ if( isPrintGathered ) {
+ log.information( "Loading S-Chain ERC721 ABI from {}",
+ imaState.chainProperties.sc.strPathJsonErc721 );
+ }
+ imaState.chainProperties.sc.joErc721 =
+ imaUtils.jsonFileLoad( imaState.chainProperties.sc.strPathJsonErc721, null );
+ n2 = Object.keys( imaState.chainProperties.sc.joErc721 ).length;
+ }
+ if( n1 > 0 ) {
+ imaState.chainProperties.mn.strCoinNameErc721 =
+ imaUtils.discoverCoinNameInJSON( imaState.chainProperties.mn.joErc721 );
+ if( n2 > 0 ) {
+ imaState.chainProperties.sc.strCoinNameErc721 =
+ imaUtils.discoverCoinNameInJSON( imaState.chainProperties.sc.joErc721 );
+ }
+ n1 = imaState.chainProperties.mn.strCoinNameErc721.length;
+ if( n2 > 0 )
+ n2 = imaState.chainProperties.sc.strCoinNameErc721.length;
+ if( n1 > 0 ) {
+ if( !imaState.bShowConfigMode ) {
+ if( isPrintGathered ) {
+ log.information( "Loaded Main-net ERC721 ABI {}",
+ imaState.chainProperties.mn.strCoinNameErc721 );
+ }
+ if( n2 > 0 && isPrintGathered ) {
+ log.information( "Loaded S-Chain ERC721 ABI {}",
+ imaState.chainProperties.sc.strCoinNameErc721 );
+ }
+ }
+ } else {
+ if( n1 === 0 )
+ log.fatal( "Main-net ERC721 token name is not discovered(malformed JSON)" );
+ if( n2 === 0 && imaState.chainProperties.sc.strPathJsonErc721.length > 0 )
+ log.fatal( "S-Chain ERC721 token name is not discovered(malformed JSON)" );
+ imaState.chainProperties.mn.joErc721 = null;
+ imaState.chainProperties.sc.joErc721 = null;
+ imaState.chainProperties.mn.strCoinNameErc721 = "";
+ imaState.chainProperties.sc.strCoinNameErc721 = "";
+ process.exit( 126 );
+ }
+ } else {
+ if( n1 === 0 )
+ log.fatal( "Main-net ERC721 JSON is invalid" );
+ if( n2 === 0 && imaState.chainProperties.sc.strPathJsonErc721.length > 0 )
+ log.fatal( "S-Chain ERC721 JSON is invalid" );
+ imaState.chainProperties.mn.joErc721 = null;
+ imaState.chainProperties.sc.joErc721 = null;
+ imaState.chainProperties.mn.strCoinNameErc721 = "";
+ imaState.chainProperties.sc.strCoinNameErc721 = "";
+ process.exit( 126 );
+ }
+ } else {
+ if( imaState.chainProperties.sc.strPathJsonErc721.length > 0 ) {
+ n1 = 0;
+ n2 = 0;
+ if( isPrintGathered ) {
+ log.information( "Loading S-Chain ERC721 ABI from {}",
+ imaState.chainProperties.sc.strPathJsonErc721 );
+ }
+ imaState.chainProperties.sc.joErc721 =
+ imaUtils.jsonFileLoad( imaState.chainProperties.sc.strPathJsonErc721, null );
+ n2 = Object.keys( imaState.chainProperties.sc.joErc721 ).length;
+
+ if( n2 > 0 ) {
+ imaState.chainProperties.sc.strCoinNameErc721 =
+ imaUtils.discoverCoinNameInJSON( imaState.chainProperties.sc.joErc721 );
+ n2 = imaState.chainProperties.sc.strCoinNameErc721.length;
+ if( n2 > 0 ) {
+ if( isPrintGathered ) {
+ log.information( "Loaded S-Chain ERC721 ABI {}",
+ imaState.chainProperties.sc.strCoinNameErc721 );
+ } else {
+ if( n2 === 0 && imaState.chainProperties.sc.strPathJsonErc721.length > 0 ) {
+ log.fatal( "S-Chain ERC721 token name is not " +
+ "discovered(malformed JSON)" );
+ }
+ imaState.chainProperties.mn.joErc721 = null;
+ imaState.chainProperties.sc.joErc721 = null;
+ imaState.chainProperties.mn.strCoinNameErc721 = "";
+ imaState.chainProperties.sc.strCoinNameErc721 = "";
+ process.exit( 126 );
+ }
+ }
+ }
+ }
+ }
+ if( n1 !== 0 && n2 === 0 ) {
+ if( imaState.strAddrErc721Explicit.length === 0 ) {
+ if( isPrintGathered ) {
+ log.warning( "IMPORTANT NOTICE: Both S-Chain ERC721 JSON and explicit " +
+ "ERC721 address are not specified" );
+ }
+ } else {
+ if( isPrintGathered )
+ log.attention( "IMPORTANT NOTICE: S-Chain ERC721 ABI will be auto-generated" );
+ imaState.chainProperties.sc.strCoinNameErc721 =
+ imaState.chainProperties.mn.strCoinNameErc721 ?? ""; // assume same
+ imaState.chainProperties.sc.joErc721 =
+ JSON.parse( JSON.stringify( imaState.chainProperties.mn.joErc721 ) ); // clone
+ imaState.chainProperties.sc.joErc721[
+ imaState.chainProperties.sc.strCoinNameErc721 + "_address"] =
+ imaState.strAddrErc721Explicit; // set explicit address
+ }
+ }
+
+ if( imaState.chainProperties.tc.strPathJsonErc721.length > 0 &&
+ isPrintGathered
+ ) {
+ log.information( "Loading S<->S Target S-Chain ERC721 ABI from {}",
+ imaState.chainProperties.tc.strPathJsonErc721 );
+ imaState.chainProperties.tc.joErc721 =
+ imaUtils.jsonFileLoad( imaState.chainProperties.tc.strPathJsonErc721, null );
+ n2 = Object.keys( imaState.chainProperties.tc.joErc721 ).length;
+ if( n2 > 0 ) {
+ imaState.chainProperties.tc.strCoinNameErc721 =
+ imaUtils.discoverCoinNameInJSON( imaState.chainProperties.tc.joErc721 );
+ n2 = imaState.chainProperties.tc.strCoinNameErc721.length;
+ if( n2 > 0 && isPrintGathered ) {
+ log.information( "Loaded S<->S Target S-Chain ERC721 ABI {}",
+ imaState.chainProperties.tc.strCoinNameErc721 );
+ } else {
+ if( n2 === 0 &&
+ imaState.chainProperties.tc.strPathJsonErc721.length > 0 &&
+ isPrintGathered
+ ) {
+ log.fatal( "S<->S Target S-Chain ERC721 token name " +
+ "is not discovered(malformed JSON)" );
+ }
+ imaState.chainProperties.tc.joErc721 = null;
+ imaState.chainProperties.tc.strCoinNameErc721 = "";
+ process.exit( 126 );
+ }
+ }
+ }
+ if( isPrintGathered &&
+ imaState.strAddrErc721ExplicitTarget.length === 0 &&
+ imaState.chainProperties.tc.strCoinNameErc721.length === 0 &&
+ imaState.chainProperties.sc.strCoinNameErc721.length > 0
+ ) {
+ log.warning( "IMPORTANT NOTICE: Both S<->S Target S-Chain ERC721 JSON and " +
+ "explicit ERC721 address are not specified" );
+ }
+}
+
+function commonInitCheckErc1155(): void {
+ const imaState: state.TIMAState = state.get();
+ const isPrintGathered = ( !!( imaState.isPrintGathered ) );
+ let n1 = 0;
+ let n2 = 0;
+ if( imaState.chainProperties.mn.strPathJsonErc1155.length > 0 ) {
+ if( isPrintGathered ) {
+ log.information( "Loading Main-net ERC1155 ABI from {}",
+ imaState.chainProperties.mn.strPathJsonErc1155 );
+ }
+ imaState.chainProperties.mn.joErc1155 =
+ imaUtils.jsonFileLoad( imaState.chainProperties.mn.strPathJsonErc1155, null );
+ n1 = Object.keys( imaState.chainProperties.mn.joErc1155 ).length;
+ if( imaState.chainProperties.sc.strPathJsonErc1155.length > 0 ) {
+ if( isPrintGathered ) {
+ log.information( "Loading S-Chain ERC1155 ABI from {}",
+ imaState.chainProperties.sc.strPathJsonErc1155 );
+ }
+ imaState.chainProperties.sc.joErc1155 =
+ imaUtils.jsonFileLoad( imaState.chainProperties.sc.strPathJsonErc1155, null );
+ n2 = Object.keys( imaState.chainProperties.sc.joErc1155 ).length;
+ }
+ if( n1 > 0 ) {
+ imaState.chainProperties.mn.strCoinNameErc1155 =
+ imaUtils.discoverCoinNameInJSON( imaState.chainProperties.mn.joErc1155 );
+ if( n2 > 0 ) {
+ imaState.chainProperties.sc.strCoinNameErc1155 =
+ imaUtils.discoverCoinNameInJSON( imaState.chainProperties.sc.joErc1155 );
+ }
+ n1 = imaState.chainProperties.mn.strCoinNameErc1155.length;
+ if( n2 > 0 )
+ n2 = imaState.chainProperties.sc.strCoinNameErc1155.length;
+ if( n1 > 0 ) {
+ if( !imaState.bShowConfigMode ) {
+ if( isPrintGathered ) {
+ log.information( "Loaded Main-net ERC1155 ABI {}",
+ imaState.chainProperties.mn.strCoinNameErc1155 );
+ }
+ if( n2 > 0 && isPrintGathered ) {
+ log.information( "Loaded S-Chain ERC1155 ABI {}",
+ imaState.chainProperties.sc.strCoinNameErc1155 );
+ }
+ }
+ } else {
+ if( n1 === 0 )
+ log.fatal( "Main-net ERC1155 token name is not discovered(malformed JSON)" );
+ if( n2 === 0 && imaState.chainProperties.sc.strPathJsonErc1155.length > 0 )
+ log.fatal( "S-Chain ERC1155 token name is not discovered(malformed JSON)" );
+ imaState.chainProperties.mn.joErc1155 = null;
+ imaState.chainProperties.sc.joErc1155 = null;
+ imaState.chainProperties.mn.strCoinNameErc1155 = "";
+ imaState.chainProperties.sc.strCoinNameErc1155 = "";
+ process.exit( 126 );
+ }
+ } else {
+ if( n1 === 0 )
+ log.fatal( "Main-net ERC1155 JSON is invalid" );
+ if( n2 === 0 && imaState.chainProperties.sc.strPathJsonErc1155.length > 0 )
+ log.fatal( "S-Chain ERC1155 JSON is invalid" );
+
+ imaState.chainProperties.mn.joErc1155 = null;
+ imaState.chainProperties.sc.joErc1155 = null;
+ imaState.chainProperties.mn.strCoinNameErc1155 = "";
+ imaState.chainProperties.sc.strCoinNameErc1155 = "";
+ process.exit( 126 );
+ }
+ } else {
+ if( imaState.chainProperties.sc.strPathJsonErc1155.length > 0 ) {
+ n1 = 0;
+ n2 = 0;
+ if( isPrintGathered ) {
+ log.information( "Loading S-Chain ERC1155 ABI from {}",
+ imaState.chainProperties.sc.strPathJsonErc1155 );
+ }
+ imaState.chainProperties.sc.joErc1155 =
+ imaUtils.jsonFileLoad( imaState.chainProperties.sc.strPathJsonErc1155, null );
+ n2 = Object.keys( imaState.chainProperties.sc.joErc1155 ).length;
+
+ if( n2 > 0 ) {
+ imaState.chainProperties.sc.strCoinNameErc1155 =
+ imaUtils.discoverCoinNameInJSON( imaState.chainProperties.sc.joErc1155 );
+ n2 = imaState.chainProperties.sc.strCoinNameErc1155.length;
+ if( n2 > 0 ) {
+ if( isPrintGathered ) {
+ log.information( "Loaded S-Chain ERC1155 ABI {}",
+ imaState.chainProperties.sc.strCoinNameErc1155 );
+ }
+ } else {
+ if( n2 === 0 && imaState.chainProperties.sc.strPathJsonErc1155.length > 0 )
+ log.fatal( "S-Chain ERC1155 token name is not discovered(malformed JSON)" );
+ imaState.chainProperties.mn.joErc1155 = null;
+ imaState.chainProperties.sc.joErc1155 = null;
+ imaState.chainProperties.mn.strCoinNameErc1155 = "";
+ imaState.chainProperties.sc.strCoinNameErc1155 = "";
+ process.exit( 126 );
+ }
+ }
+ }
+ }
+ if( n1 !== 0 && n2 === 0 ) {
+ if( imaState.strAddrErc1155Explicit.length === 0 ) {
+ if( isPrintGathered ) {
+ log.warning( "IMPORTANT NOTICE: Both S-Chain ERC1155 JSON and " +
+ "explicit ERC1155 address are not specified" );
+ }
+ } else {
+ if( isPrintGathered )
+ log.attention( "IMPORTANT NOTICE: S-Chain ERC1155 ABI will be auto-generated" );
+ imaState.chainProperties.sc.strCoinNameErc1155 =
+ imaState.chainProperties.mn.strCoinNameErc1155 ?? ""; // assume same
+ imaState.chainProperties.sc.joErc1155 =
+ JSON.parse( JSON.stringify( imaState.chainProperties.mn.joErc1155 ) ); // clone
+ imaState.chainProperties.sc.joErc1155[
+ imaState.chainProperties.sc.strCoinNameErc1155 + "_address"] =
+ imaState.strAddrErc1155Explicit; // set explicit address
+ }
+ }
+
+ if( imaState.chainProperties.tc.strPathJsonErc1155.length > 0 ) {
+ if( isPrintGathered ) {
+ log.information( "Loading S<->S Target S-Chain ERC1155 ABI from {}",
+ imaState.chainProperties.tc.strPathJsonErc1155 );
+ }
+ imaState.chainProperties.tc.joErc1155 =
+ imaUtils.jsonFileLoad( imaState.chainProperties.tc.strPathJsonErc1155, null );
+ n2 = Object.keys( imaState.chainProperties.tc.joErc1155 ).length;
+ if( n2 > 0 ) {
+ imaState.chainProperties.tc.strCoinNameErc1155 =
+ imaUtils.discoverCoinNameInJSON( imaState.chainProperties.tc.joErc1155 );
+ n2 = imaState.chainProperties.tc.strCoinNameErc1155.length;
+ if( n2 > 0 ) {
+ if( isPrintGathered ) {
+ log.information( "Loaded S<->S Target S-Chain ERC1155 ABI {}",
+ imaState.chainProperties.tc.strCoinNameErc1155 );
+ }
+ } else {
+ if( n2 === 0 &&
+ imaState.chainProperties.tc.strPathJsonErc1155.length > 0 &&
+ isPrintGathered
+ ) {
+ log.fatal( " S<->S Target S-Chain ERC1155 token name " +
+ "is not discovered(malformed JSON)" );
+ }
+ imaState.chainProperties.tc.joErc1155 = null;
+ imaState.chainProperties.tc.strCoinNameErc1155 = "";
+ process.exit( 126 );
+ }
+ }
+ }
+ if( isPrintGathered &&
+ imaState.strAddrErc1155ExplicitTarget.length === 0 &&
+ imaState.chainProperties.tc.strCoinNameErc1155.length === 0 &&
+ imaState.chainProperties.sc.strCoinNameErc1155.length > 0
+ ) {
+ log.warning( "IMPORTANT NOTICE: Both S<->S Target S-Chain ERC1155 JSON and " +
+ "explicit ERC1155 address are not specified" );
+ }
+}
+
+function commonInitCheckGeneralArgs(): void {
+ const imaState: state.TIMAState = state.get();
+ const isPrintGathered = ( !!( imaState.isPrintGathered ) );
+ const isPrintSecurityValues = ( !!( imaState.isPrintSecurityValues ) );
+ if( isPrintGathered ) {
+ printAbout( true );
+ log.information( "IMA AGENT is using Ethers JS version ",
+ att(
+ owaspUtils.ethersMod.ethers.version.toString().replace( "ethers/", "" ) ) );
+ }
+ ensureHaveValue(
+ "App path",
+ path.join( __dirname, "main.js" ), false, isPrintGathered, null, ( x: any ) => {
+ return att( x );
+ } );
+ ensureHaveValue(
+ "Verbose level",
+ log.verboseLevelAsTextForLog( log.verboseGet() ),
+ false, isPrintGathered, null, ( x: any ) => {
+ return att( x );
+ } );
+ ensureHaveValue(
+ "Multi-call optimizations",
+ imaState.isEnabledMultiCall, false, isPrintGathered, null, ( x: any ) => {
+ return log.yn( x );
+ } );
+ ensureHaveValue(
+ "Main-net URL",
+ imaState.chainProperties.mn.strURL, false,
+ isPrintGathered && isPrintSecurityValues, null, ( x: any ) => {
+ return log.u( x );
+ } );
+ ensureHaveValue(
+ "S-chain URL",
+ imaState.chainProperties.sc.strURL, false,
+ isPrintGathered && isPrintSecurityValues, null, ( x: any ) => {
+ return log.u( x );
+ } );
+ ensureHaveValue(
+ "S<->S Target S-chain URL",
+ imaState.chainProperties.tc.strURL, false,
+ isPrintGathered && isPrintSecurityValues, null, ( x: any ) => {
+ return log.u( x );
+ } );
+ ensureHaveValue(
+ "Main-net Ethereum network name",
+ imaState.chainProperties.mn.strChainName, false,
+ isPrintGathered && isPrintSecurityValues, null, ( x: any ) => {
+ return log.fmtNote( x );
+ } );
+ ensureHaveValue(
+ "S-Chain Ethereum network name",
+ imaState.chainProperties.sc.strChainName, false,
+ isPrintGathered && isPrintSecurityValues, null, ( x: any ) => {
+ return log.fmtNote( x );
+ } );
+ ensureHaveValue(
+ "S<->S Target S-Chain Ethereum network name",
+ imaState.chainProperties.tc.strChainName, false,
+ isPrintGathered && isPrintSecurityValues, null, ( x: any ) => {
+ return log.fmtNote( x );
+ } );
+ ensureHaveValue(
+ "Main-net Ethereum chain ID",
+ imaState.chainProperties.mn.chainId, false,
+ isPrintGathered && isPrintSecurityValues, null, ( x: any ) => {
+ return log.fmtNote( x );
+ } );
+ ensureHaveValue(
+ "S-Chain Ethereum chain ID",
+ imaState.chainProperties.sc.chainId, false,
+ isPrintGathered && isPrintSecurityValues, null, ( x: any ) => {
+ return log.fmtNote( x );
+ } );
+ ensureHaveValue(
+ "S<->S Target S-Chain Ethereum chain ID",
+ imaState.chainProperties.tc.chainId, false,
+ isPrintGathered && isPrintSecurityValues, null, ( x: any ) => {
+ return log.fmtNote( x );
+ } );
+ ensureHaveValue(
+ "Skale Manager ABI JSON file path",
+ imaState.strPathAbiJsonSkaleManager, false, isPrintGathered, null, ( x: any ) => {
+ return log.fmtWarning( x );
+ } );
+ ensureHaveValue(
+ "Main-net ABI JSON file path",
+ imaState.chainProperties.mn.strPathAbiJson, false, isPrintGathered, null, ( x: any ) => {
+ return log.fmtWarning( x );
+ } );
+ ensureHaveValue(
+ "S-Chain ABI JSON file path",
+ imaState.chainProperties.sc.strPathAbiJson, false, isPrintGathered, null, ( x: any ) => {
+ return log.fmtWarning( x );
+ } );
+ ensureHaveValue(
+ "S<->S Target S-Chain ABI JSON file path",
+ imaState.chainProperties.tc.strPathAbiJson, false, isPrintGathered, null, ( x: any ) => {
+ return log.fmtWarning( x );
+ } );
+
+ try {
+ ensureHaveValue( "Main-net user account address",
+ imaState.chainProperties.mn.joAccount.address(), false,
+ isPrintGathered && isPrintSecurityValues );
+ } catch ( err ) {}
+ try {
+ ensureHaveValue( "S-chain user account address",
+ imaState.chainProperties.sc.joAccount.address(), false,
+ isPrintGathered && isPrintSecurityValues );
+ } catch ( err ) {}
+ try {
+ ensureHaveValue(
+ "S<->S Target S-chain user account address",
+ imaState.chainProperties.tc.joAccount.address(),
+ false, isPrintGathered );
+ } catch ( err ) {}
+}
+
+function commonInitCheckCredentialsArgs(): void {
+ const imaState: state.TIMAState = state.get();
+ const isPrintGathered = ( !!( imaState.isPrintGathered ) );
+ const isPrintSecurityValues = ( !!( imaState.isPrintSecurityValues ) );
+ try {
+ ensureHaveCredentials(
+ "Main Net",
+ imaState.chainProperties.mn.joAccount, false,
+ isPrintGathered && isPrintSecurityValues );
+ } catch ( err ) {}
+ try {
+ ensureHaveCredentials(
+ "S-Chain",
+ imaState.chainProperties.sc.joAccount, false,
+ isPrintGathered && isPrintSecurityValues );
+ } catch ( err ) {}
+ try {
+ commonInitCheckTransferAmountArgs();
+ ensureHaveCredentials(
+ "S<->S Target S-Chain",
+ imaState.chainProperties.tc.joAccount, false,
+ isPrintGathered && isPrintSecurityValues );
+ } catch ( err ) {}
+ if( isPrintGathered && isPrintSecurityValues ) {
+ if( imaState.chainProperties.mn.joAccount.strBlsKeyName ) {
+ ensureHaveValue(
+ "BLS/Main Net key name",
+ imaState.chainProperties.mn.joAccount.strBlsKeyName,
+ false, isPrintGathered, null, ( x: any ) => {
+ return att( x );
+ } );
+ }
+ if( imaState.chainProperties.sc.joAccount.strBlsKeyName ) {
+ ensureHaveValue(
+ "BLS/S-Chain key name",
+ imaState.chainProperties.sc.joAccount.strBlsKeyName,
+ false, isPrintGathered, null, ( x: any ) => {
+ return att( x );
+ } );
+ }
+ if( imaState.chainProperties.tc.joAccount.strBlsKeyName ) {
+ ensureHaveValue(
+ "BLS/Target S-Chain key name",
+ imaState.chainProperties.tc.joAccount.strBlsKeyName,
+ false, isPrintGathered, null, ( x: any ) => {
+ return att( x );
+ } );
+ }
+ }
+}
+
+function commonInitCheckTransferAmountArgs(): void {
+ const imaState: state.TIMAState = state.get();
+ const isPrintGathered = ( !!( imaState.isPrintGathered ) );
+ ensureHaveValue(
+ "Amount of wei to transfer", imaState.nAmountOfWei,
+ false, isPrintGathered, null, ( x: any ) => {
+ return log.fmtInformation( x );
+ } );
+}
+
+function commonInitTransferringArgs(): void {
+ const imaState: state.TIMAState = state.get();
+ const isPrintGathered = ( !!( imaState.isPrintGathered ) );
+ ensureHaveValue(
+ "M->S transfer block size", imaState.nTransferBlockSizeM2S,
+ false, isPrintGathered, null, ( x: any ) => {
+ return log.fmtNote( x );
+ } );
+ ensureHaveValue(
+ "S->M transfer block size", imaState.nTransferBlockSizeS2M,
+ false, isPrintGathered, null, ( x: any ) => {
+ return log.fmtNote( x );
+ } );
+ if( imaState.bHaveSkaleManagerABI ) {
+ ensureHaveValue(
+ "S->S transfer block size", imaState.nTransferBlockSizeS2S,
+ false, isPrintGathered, null, ( x: any ) => {
+ return log.fmtNote( x );
+ } );
+ }
+ ensureHaveValue(
+ "M->S transfer job steps", imaState.nTransferStepsM2S,
+ false, isPrintGathered, null, ( x: any ) => {
+ return log.fmtNote( x );
+ } );
+ ensureHaveValue(
+ "S->M transfer job steps", imaState.nTransferStepsS2M,
+ false, isPrintGathered, null, ( x: any ) => {
+ return log.fmtNote( x );
+ } );
+ if( imaState.bHaveSkaleManagerABI ) {
+ ensureHaveValue(
+ "S->S transfer job steps", imaState.nTransferStepsS2S,
+ false, isPrintGathered, null, ( x: any ) => {
+ return log.fmtNote( x );
+ } );
+ }
+ ensureHaveValue(
+ "M->S transactions limit", imaState.nMaxTransactionsM2S,
+ false, isPrintGathered, null, ( x: any ) => {
+ return log.fmtNote( x );
+ } );
+ ensureHaveValue(
+ "S->M transactions limit", imaState.nMaxTransactionsS2M,
+ false, isPrintGathered, null, ( x: any ) => {
+ return log.fmtNote( x );
+ } );
+ if( imaState.bHaveSkaleManagerABI ) {
+ ensureHaveValue(
+ "S->S transactions limit", imaState.nMaxTransactionsS2S,
+ false, isPrintGathered, null, ( x: any ) => {
+ return log.fmtNote( x );
+ } );
+ }
+ ensureHaveValue(
+ "M->S await blocks", imaState.nBlockAwaitDepthM2S, false,
+ isPrintGathered, null, ( x: any ) => {
+ return log.fmtNote( x );
+ } );
+ ensureHaveValue(
+ "S->M await blocks", imaState.nBlockAwaitDepthS2M, false,
+ isPrintGathered, null, ( x: any ) => {
+ return log.fmtNote( x );
+ } );
+ if( imaState.bHaveSkaleManagerABI ) {
+ ensureHaveValue(
+ "S->S await blocks", imaState.nBlockAwaitDepthS2S, false,
+ isPrintGathered, null, ( x: any ) => {
+ return log.fmtNote( x );
+ } );
+ }
+ ensureHaveValue(
+ "M->S minimal block age", imaState.nBlockAgeM2S,
+ false, isPrintGathered, null, ( x: any ) => {
+ return log.fmtNote( x );
+ } );
+ ensureHaveValue(
+ "S->M minimal block age", imaState.nBlockAgeS2M,
+ false, isPrintGathered, null, ( x: any ) => {
+ return log.fmtNote( x );
+ } );
+ if( imaState.bHaveSkaleManagerABI ) {
+ ensureHaveValue(
+ "S->S minimal block age", imaState.nBlockAgeS2S,
+ false, isPrintGathered, null, ( x: any ) => {
+ return log.fmtNote( x );
+ } );
+ }
+ ensureHaveValue(
+ "Transfer loop period(seconds)", imaState.nLoopPeriodSeconds,
+ false, isPrintGathered, null, ( x: any ) => {
+ return log.fmtSuccess( x );
+ } );
+ if( imaState.nTimeFrameSeconds > 0 ) {
+ ensureHaveValue(
+ "Time framing(seconds)", imaState.nTimeFrameSeconds,
+ false, isPrintGathered );
+ ensureHaveValue(
+ "Next frame gap(seconds)", imaState.nNextFrameGap,
+ false, isPrintGathered );
+ } else {
+ ensureHaveValue(
+ "Time framing", log.fmtError( "disabled" ),
+ false, isPrintGathered
+ );
+ }
+}
+
+function commonInitCheckAccessArgs(): void {
+ const imaState: state.TIMAState = state.get();
+ const isPrintGathered = ( !!( imaState.isPrintGathered ) );
+ ensureHaveValue(
+ "S-Chain node number(zero based)",
+ imaState.nNodeNumber, false, isPrintGathered, null, ( x: any ) => {
+ return log.fmtInformation( x );
+ } );
+ ensureHaveValue(
+ "S-Chain nodes count",
+ imaState.nNodesCount, false, isPrintGathered, null, ( x: any ) => {
+ return log.fmtInformation( x );
+ } );
+}
+
+function commonInitErcTokensArgs(): void {
+ const imaState: state.TIMAState = state.get();
+ const isPrintGathered = ( !!( imaState.isPrintGathered ) );
+ if( imaState.chainProperties.tc.strCoinNameErc20.length > 0 ) {
+ ensureHaveValue(
+ "Loaded Main-net ERC20 ABI ",
+ imaState.chainProperties.tc.strCoinNameErc20,
+ false, isPrintGathered, null, ( x: any ) => {
+ return att( x );
+ } );
+ ensureHaveValue(
+ "Loaded S-Chain ERC20 ABI ",
+ imaState.chainProperties.sc.strCoinNameErc20,
+ false, isPrintGathered, null, ( x: any ) => {
+ return att( x );
+ } );
+ ensureHaveValue(
+ "Amount of tokens to transfer",
+ imaState.nAmountOfToken,
+ false, isPrintGathered, null, ( x: any ) => {
+ return log.fmtInformation( x );
+ } );
+ if( isPrintGathered ) {
+ log.information( "ERC20 explicit S-Chain address is {}",
+ imaState.strAddrErc20Explicit );
+ }
+ }
+ if( imaState.chainProperties.tc.strCoinNameErc20.length > 0 ) {
+ ensureHaveValue(
+ "Loaded S<->S Target S-Chain ERC20 ABI ",
+ imaState.chainProperties.tc.strCoinNameErc20,
+ false, isPrintGathered, null, ( x: any ) => {
+ return att( x );
+ } );
+ }
+ if( imaState.chainProperties.mn.strCoinNameErc721.length > 0 ) {
+ ensureHaveValue(
+ "Loaded Main-net ERC721 ABI ",
+ imaState.chainProperties.mn.strCoinNameErc721,
+ false, isPrintGathered, null, ( x: any ) => {
+ return att( x );
+ } );
+ ensureHaveValue(
+ "Loaded S-Chain ERC721 ABI ",
+ imaState.chainProperties.sc.strCoinNameErc721,
+ false, isPrintGathered, null, ( x: any ) => {
+ return att( x );
+ } );
+ ensureHaveValue(
+ "ERC721 token id ",
+ imaState.idToken, false,
+ isPrintGathered, null, ( x: any ) => {
+ return log.fmtInformation( x );
+ } );
+ if( isPrintGathered ) {
+ log.information( "ERC721 explicit S-Chain address is {}",
+ imaState.strAddrErc721Explicit );
+ }
+ }
+ if( imaState.chainProperties.tc.strCoinNameErc721.length > 0 ) {
+ ensureHaveValue(
+ "Loaded S<->S Target S-Chain ERC721 ABI ",
+ imaState.chainProperties.tc.strCoinNameErc721,
+ false, isPrintGathered, null, ( x: any ) => {
+ return att( x );
+ } );
+ }
+ if( imaState.chainProperties.mn.strCoinNameErc1155.length > 0 ) {
+ ensureHaveValue( "Loaded Main-net ERC1155 ABI ",
+ imaState.chainProperties.mn.strCoinNameErc1155,
+ false, isPrintGathered, null, ( x: any ) => {
+ return att( x );
+ } );
+ ensureHaveValue( "Loaded S-Chain ERC1155 ABI ",
+ imaState.chainProperties.sc.strCoinNameErc1155,
+ false, isPrintGathered, null, ( x: any ) => {
+ return att( x );
+ } );
+ try {
+ ensureHaveValue( "ERC1155 token id ",
+ imaState.idToken, false, isPrintGathered, null, ( x: any ) => {
+ return log.fmtInformation( x );
+ } );
+ ensureHaveValue( "ERC1155 token amount ",
+ imaState.nAmountOfToken, false, isPrintGathered, null, ( x: any ) => {
+ return log.fmtInformation( x );
+ } );
+ } catch ( e1 ) {
+ try {
+ ensureHaveValue(
+ "ERC1155 batch of token ids ",
+ imaState.idTokens, false,
+ isPrintGathered, null, ( x: any ) => {
+ return log.fmtInformation( x );
+ } );
+ ensureHaveValue(
+ "ERC1155 batch of token amounts ",
+ imaState.arrAmountsOfTokens, false,
+ isPrintGathered, null, ( x: any ) => {
+ return log.fmtInformation( x );
+ } );
+ } catch ( e2 ) {
+ log.warning( "Please check your params in ERC1155 transfer" );
+ log.warning( "Error 1 {}", e1 );
+ log.warning( "Error 2 {}", e2 );
+ process.exit( 126 );
+ }
+ }
+ if( isPrintGathered ) {
+ log.information( "ERC1155 explicit S-Chain address is {}",
+ imaState.strAddrErc1155Explicit );
+ }
+ }
+ if( imaState.chainProperties.tc.strCoinNameErc1155.length > 0 ) {
+ ensureHaveValue(
+ "Loaded S<->S Target S-Chain ERC1155 ABI ",
+ imaState.chainProperties.tc.strCoinNameErc1155,
+ false, isPrintGathered, null, ( x: any ) => {
+ return att( x );
+ } );
+ }
+}
+
+function commonInitGasMultipliersAndTransactionArgs(): void {
+ const imaState: state.TIMAState = state.get();
+ const isPrintGathered = ( !!( imaState.isPrintGathered ) );
+ if( isPrintGathered ) {
+ log.debug( log.fmtInformation( "Main Net Gas Price Multiplier is" ),
+ "....................." +
+ ( imaState.chainProperties.mn.transactionCustomizer.gasPriceMultiplier
+ ? log.fmtInformation( imaState.chainProperties.mn.transactionCustomizer
+ .gasPriceMultiplier.toString() )
+ : log.fmtError( "disabled" ) ) );
+ log.debug( log.fmtInformation( "S-Chain Gas Price Multiplier is" ),
+ "......................" +
+ ( imaState.chainProperties.sc.transactionCustomizer.gasPriceMultiplier
+ ? log.fmtInformation( imaState.chainProperties.sc.transactionCustomizer
+ .gasPriceMultiplier.toString() )
+ : log.fmtError( "disabled" ) ) );
+ log.debug( log.fmtInformation( "Target S-Chain Gas Price Multiplier is" ),
+ "..............." +
+ ( imaState.chainProperties.tc.transactionCustomizer.gasPriceMultiplier
+ ? log.fmtInformation( imaState.chainProperties.tc.transactionCustomizer
+ .gasPriceMultiplier.toString() )
+ : log.fmtError( "disabled" ) ) );
+ log.debug( log.fmtInformation( "Main Net Gas Value Multiplier is" ),
+ "....................." +
+ ( imaState.chainProperties.mn.transactionCustomizer.gasMultiplier
+ ? log.fmtInformation( imaState.chainProperties.mn
+ .transactionCustomizer.gasMultiplier.toString() )
+ : log.fmtNotice( "default" ) ) );
+ log.debug( log.fmtInformation( "S-Chain Gas Value Multiplier is" ),
+ "......................" +
+ ( imaState.chainProperties.sc.transactionCustomizer.gasMultiplier
+ ? log.fmtInformation( imaState.chainProperties.sc
+ .transactionCustomizer.gasMultiplier.toString() )
+ : log.fmtNotice( "default" ) ) );
+ log.debug( log.fmtInformation( "Target S-Chain Gas Value Multiplier is" ),
+ "..............." +
+ ( imaState.chainProperties.tc.transactionCustomizer.gasMultiplier
+ ? log.fmtInformation( imaState.chainProperties.tc
+ .transactionCustomizer.gasMultiplier.toString() )
+ : log.fmtNotice( "default" ) ) );
+ log.debug( log.fmtInformation( "Pending work analysis(PWA) is" ),
+ "........................" +
+ ( imaState.isPWA ? log.fmtSuccess( "enabled" ) : log.fmtError( "disabled" ) ) );
+ log.debug( log.fmtInformation( "Expose PWA details to log is" ),
+ "........................." +
+ ( imaState.isPrintPWA ? log.fmtSuccess( "enabled" ) : log.fmtError( "disabled" ) ) );
+ log.debug( log.fmtInformation( "Oracle based gas reimbursement is" ),
+ "...................." +
+ ( imaOracleOperations.getEnabledOracle()
+ ? log.fmtSuccess( "enabled" )
+ : log.fmtError( "disabled" ) ) );
+ log.debug( log.fmtInformation(
+ "S-Chain to S-Chain transferring is" ) +
+ "..................." + ( imaState.optsS2S.isEnabled
+ ? log.fmtSuccess( "enabled" )
+ : log.fmtError( "disabled" ) ) );
+ log.debug( log.fmtInformation( "SKALE network browser file path is" ),
+ "................." +
+ ( imaState.optsS2S.strNetworkBrowserPath
+ ? log.fmtInformation( imaState.optsS2S.strNetworkBrowserPath )
+ : log.fmtError( "N/A" ) ) );
+ log.debug( log.fmtInformation( "S<->S transfer mode is" ),
+ "..............................." +
+ imaHelperAPIs.getS2STransferModeDescriptionColorized() );
+ log.debug( log.fmtInformation( "IMA JSON RPC server port is" ),
+ ".........................." +
+ ( ( imaState.nJsonRpcPort > 0 )
+ ? log.fmtInformation( imaState.nJsonRpcPort )
+ : log.fmtError( "disabled" ) ) );
+ log.debug( log.fmtInformation( "Cross-IMA mode is" ),
+ "...................................." +
+ ( imaState.isCrossImaBlsMode
+ ? log.fmtSuccess( "enabled" )
+ : log.fmtError( "disabled" ) ) );
+ log.debug( log.fmtInformation( "Dry-run is enabled" ),
+ "..................................." +
+ log.yn( imaTx.dryRunIsEnabled() ) );
+ log.debug( log.fmtInformation( "Dry-run execution result is ignored" ) +
+ ".................." +
+ log.yn( imaTx.dryRunIsIgnored() ) );
+ }
+}
+
+function commonInitLoggingArgs(): void {
+ const imaState: state.TIMAState = state.get();
+ const isPrintGathered = ( !!( imaState.isPrintGathered ) );
+ if( imaState.strLogFilePath.length > 0 ) {
+ ensureHaveValue(
+ "Log file path",
+ imaState.strLogFilePath, false,
+ isPrintGathered, null, ( x: any ) => {
+ return log.fmtInformation( x );
+ } );
+ ensureHaveValue(
+ "Max size of log file path",
+ imaState.nLogMaxSizeBeforeRotation, false,
+ isPrintGathered, null, ( x: any ) => {
+ return ( x <= 0 ) ? log.fmtWarning( "unlimited" ) : log.fmtNote( x );
+ } );
+ ensureHaveValue(
+ "Max rotated count of log files",
+ imaState.nLogMaxFilesCount,
+ false, isPrintGathered, null, ( x: any ) => {
+ return ( x <= 1 ) ? log.fmtWarning( "not set" ) : log.fmtNote( x );
+ } );
+ }
+}
+
+function commonInitAutomaticExitArgs(): void {
+ const imaState: state.TIMAState = state.get();
+ const isPrintGathered = ( !!( imaState.isPrintGathered ) );
+ const isPrintSecurityValues = ( !!( imaState.isPrintSecurityValues ) );
+ ensureHaveValue(
+ "Automatic exit(seconds)",
+ imaState.nAutoExitAfterSeconds, false,
+ isPrintGathered && isPrintSecurityValues );
+}
+
+export function commonInit(): void {
+ const imaState: state.TIMAState = state.get();
+ commonInitPrintSysInfo();
+ commonInitCheckAbiPaths();
+ commonInitCheckContractPresences();
+ commonInitCheckErc20();
+ commonInitCheckErc721();
+ commonInitCheckErc1155();
+ if( log.verboseGet() > log.verboseName2Number( "debug" ) || imaState.bShowConfigMode ) {
+ commonInitCheckGeneralArgs();
+ commonInitCheckCredentialsArgs();
+ commonInitCheckTransferAmountArgs();
+ commonInitTransferringArgs();
+ commonInitCheckAccessArgs();
+ commonInitErcTokensArgs();
+ commonInitGasMultipliersAndTransactionArgs();
+ commonInitLoggingArgs();
+ commonInitAutomaticExitArgs();
+ }
+} // commonInit
+
+export function imaInitEthersProviders(): void {
+ const imaState: state.TIMAState = state.get();
+ if( imaState.chainProperties.mn.strURL &&
+ typeof imaState.chainProperties.mn.strURL === "string" &&
+ imaState.chainProperties.mn.strURL.length > 0
+ ) {
+ const u = imaState.chainProperties.mn.strURL;
+ asyncCheckUrlAtStartup( u, "Main-net" )
+ .then( function(): void {} ).catch( function(): void {} );
+ imaState.chainProperties.mn.ethersProvider = owaspUtils.getEthersProviderFromURL( u );
+ } else {
+ log.warning( "No Main-net URL specified in command line arguments" +
+ "(needed for particular operations only)" );
+ }
+
+ if( imaState.chainProperties.sc.strURL &&
+ typeof imaState.chainProperties.sc.strURL === "string" &&
+ imaState.chainProperties.sc.strURL.length > 0
+ ) {
+ const u = imaState.chainProperties.sc.strURL;
+ asyncCheckUrlAtStartup( u, "S-Chain" )
+ .then( function(): void {} ).catch( function(): void {} );
+ imaState.chainProperties.sc.ethersProvider = owaspUtils.getEthersProviderFromURL( u );
+ } else {
+ log.warning( "No S-Chain URL specified in command line arguments" +
+ "(needed for particular operations only)" );
+ }
+
+ if( imaState.chainProperties.tc.strURL &&
+ typeof imaState.chainProperties.tc.strURL === "string" &&
+ imaState.chainProperties.tc.strURL.length > 0
+ ) {
+ const u = imaState.chainProperties.tc.strURL;
+ asyncCheckUrlAtStartup( u, "S<->S Target S-Chain" )
+ .then( function(): void {} ).catch( function(): void {} );
+ imaState.chainProperties.tc.ethersProvider = owaspUtils.getEthersProviderFromURL( u );
+ } else {
+ log.warning( "No S<->S Target S-Chain URL specified in command line arguments" +
+ "(needed for particular operations only)" );
+ }
+} // imaInitEthersProviders
+
+function initContractsIMA(): void {
+ const imaState: state.TIMAState = state.get();
+ if( imaState.chainProperties.mn.bHaveAbiIMA ) {
+ const cp = imaState.chainProperties.mn;
+ if( cp.ethersProvider ) {
+ const ep: owaspUtils.ethersMod.ethers.providers.JsonRpcProvider = cp.ethersProvider;
+ const joABI = cp.joAbiIMA;
+ imaState.joDepositBoxETH = new owaspUtils.ethersMod.ethers.Contract(
+ joABI.deposit_box_eth_address, joABI.deposit_box_eth_abi, ep ); // only main net
+ imaState.joDepositBoxERC20 = new owaspUtils.ethersMod.ethers.Contract(
+ joABI.deposit_box_erc20_address, joABI.deposit_box_erc20_abi, ep ); // only main net
+ imaState.joDepositBoxERC721 = new owaspUtils.ethersMod.ethers.Contract(
+ joABI.deposit_box_erc721_address, joABI.deposit_box_erc721_abi, ep
+ ); // only main net
+ imaState.joDepositBoxERC1155 = new owaspUtils.ethersMod.ethers.Contract(
+ joABI.deposit_box_erc1155_address, joABI.deposit_box_erc1155_abi, ep );
+ // only main net
+ imaState.joDepositBoxERC721WithMetadata = new owaspUtils.ethersMod.ethers.Contract(
+ joABI.deposit_box_erc721_with_metadata_address,
+ joABI.deposit_box_erc721_with_metadata_abi, ep ); // only main net
+ imaState.joCommunityPool = new owaspUtils.ethersMod.ethers.Contract(
+ joABI.community_pool_address, joABI.community_pool_abi, ep ); // only main net
+ imaState.joLinker = new owaspUtils.ethersMod.ethers.Contract(
+ joABI.linker_address, joABI.linker_abi, ep ); // only main net
+ imaState.joMessageProxyMainNet = new owaspUtils.ethersMod.ethers.Contract(
+ joABI.message_proxy_mainnet_address, joABI.message_proxy_mainnet_abi, ep );
+ }
+ }
+ if( imaState.chainProperties.sc.bHaveAbiIMA ) {
+ const cp = imaState.chainProperties.sc;
+ if( cp.ethersProvider ) {
+ const ep: owaspUtils.ethersMod.ethers.providers.JsonRpcProvider | null =
+ cp.ethersProvider;
+ const joABI = cp.joAbiIMA;
+ imaState.joTokenManagerETH = new owaspUtils.ethersMod.ethers.Contract(
+ joABI.token_manager_eth_address, joABI.token_manager_eth_abi, ep ); // only s-chain
+ imaState.joTokenManagerERC20 = new owaspUtils.ethersMod.ethers.Contract(
+ joABI.token_manager_erc20_address, joABI.token_manager_erc20_abi,
+ ep ); // only s-chain
+ imaState.joTokenManagerERC721 = new owaspUtils.ethersMod.ethers.Contract(
+ joABI.token_manager_erc721_address, joABI.token_manager_erc721_abi,
+ ep ); // only s-chain
+ imaState.joTokenManagerERC1155 = new owaspUtils.ethersMod.ethers.Contract(
+ joABI.token_manager_erc1155_address, joABI.token_manager_erc1155_abi,
+ ep ); // only s-chain
+ imaState.joTokenManagerERC721WithMetadata = new owaspUtils.ethersMod.ethers.Contract(
+ joABI.token_manager_erc721_with_metadata_address,
+ joABI.token_manager_erc721_with_metadata_abi, ep ); // only s-chain
+ imaState.joCommunityLocker = new owaspUtils.ethersMod.ethers.Contract(
+ joABI.community_locker_address, joABI.community_locker_abi, ep ); // only s-chain
+ imaState.joMessageProxySChain = new owaspUtils.ethersMod.ethers.Contract(
+ joABI.message_proxy_chain_address, joABI.message_proxy_chain_abi, ep );
+ imaState.joTokenManagerLinker = new owaspUtils.ethersMod.ethers.Contract(
+ joABI.token_manager_linker_address, joABI.token_manager_linker_abi, ep );
+ imaState.joEthErc20 = new owaspUtils.ethersMod.ethers.Contract(
+ joABI.eth_erc20_address, joABI.eth_erc20_abi, ep ); // only s-chain
+ }
+ }
+ if( imaState.chainProperties.tc.bHaveAbiIMA ) {
+ const cp = imaState.chainProperties.tc;
+ if( cp.ethersProvider ) {
+ const ep: owaspUtils.ethersMod.ethers.providers.JsonRpcProvider = cp.ethersProvider;
+ const joABI = cp.joAbiIMA;
+ imaState.joTokenManagerETHTarget = new owaspUtils.ethersMod.ethers.Contract(
+ joABI.token_manager_eth_address, joABI.token_manager_eth_abi, ep ); // only s-chain
+ imaState.joTokenManagerERC20Target = new owaspUtils.ethersMod.ethers.Contract(
+ joABI.token_manager_erc20_address, joABI.token_manager_erc20_abi,
+ ep ); // only s-chain
+ imaState.joTokenManagerERC721Target = new owaspUtils.ethersMod.ethers.Contract(
+ joABI.token_manager_erc721_address, joABI.token_manager_erc721_abi,
+ ep ); // only s-chain
+ imaState.joTokenManagerERC1155Target = new owaspUtils.ethersMod.ethers.Contract(
+ joABI.token_manager_erc1155_address, joABI.token_manager_erc1155_abi,
+ ep ); // only s-chain
+ imaState.joTokenManagerERC721WithMetadataTarget =
+ new owaspUtils.ethersMod.ethers.Contract(
+ joABI.token_manager_erc721_with_metadata_address,
+ joABI.token_manager_erc721_with_metadata_abi, ep ); // only s-chain
+ imaState.joCommunityLockerTarget = new owaspUtils.ethersMod.ethers.Contract(
+ joABI.community_locker_address, joABI.community_locker_abi, ep ); // only s-chain
+ imaState.joMessageProxySChainTarget = new owaspUtils.ethersMod.ethers.Contract(
+ joABI.message_proxy_chain_address, joABI.message_proxy_chain_abi, ep );
+ imaState.joTokenManagerLinkerTarget = new owaspUtils.ethersMod.ethers.Contract(
+ joABI.token_manager_linker_address, joABI.token_manager_linker_abi, ep );
+ imaState.joEthErc20Target = new owaspUtils.ethersMod.ethers.Contract(
+ joABI.eth_erc20_address, joABI.eth_erc20_abi, ep ); // only s-chain
+ }
+ }
+}
+
+function initContractsSkaleManager(): void {
+ const imaState: state.TIMAState = state.get();
+ if( imaState.bHaveSkaleManagerABI ) {
+ const cp = imaState.chainProperties.mn;
+ if( cp.ethersProvider ) {
+ const ep: owaspUtils.ethersMod.ethers.providers.JsonRpcProvider = cp.ethersProvider;
+ const joABI = imaState.joAbiSkaleManager;
+ imaState.joConstantsHolder = new owaspUtils.ethersMod.ethers.Contract(
+ joABI.constants_holder_address, joABI.constants_holder_abi, ep );
+ imaState.joNodes = new owaspUtils.ethersMod.ethers.Contract(
+ joABI.nodes_address, joABI.nodes_abi, ep );
+ imaState.joKeyStorage = new owaspUtils.ethersMod.ethers.Contract(
+ joABI.key_storage_address, joABI.key_storage_abi, ep );
+ imaState.joSChains = new owaspUtils.ethersMod.ethers.Contract(
+ joABI.schains_address, joABI.schains_abi, ep );
+ imaState.joSChainsInternal = new owaspUtils.ethersMod.ethers.Contract(
+ joABI.schains_internal_address, joABI.schains_internal_abi, ep );
+ imaState.joSkaleDKG = new owaspUtils.ethersMod.ethers.Contract(
+ joABI.skale_d_k_g_address, joABI.skale_d_k_g_abi, ep );
+ imaState.joSkaleManager = new owaspUtils.ethersMod.ethers.Contract(
+ joABI.skale_manager_address, joABI.skale_manager_abi, ep );
+ imaState.joSkaleToken = new owaspUtils.ethersMod.ethers.Contract(
+ joABI.skale_token_address, joABI.skale_token_abi, ep );
+ imaState.joValidatorService = new owaspUtils.ethersMod.ethers.Contract(
+ joABI.validator_service_address, joABI.validator_service_abi, ep );
+ imaState.joWallets = new owaspUtils.ethersMod.ethers.Contract(
+ joABI.wallets_address, joABI.wallets_abi, ep );
+ }
+ }
+}
+
+export function initContracts(): void {
+ imaInitEthersProviders();
+ initContractsIMA();
+ initContractsSkaleManager();
+ commonInitPrintFoundContracts();
+}
diff --git a/src/clpTools.ts b/src/clpTools.ts
new file mode 100644
index 00000000..e970b0ce
--- /dev/null
+++ b/src/clpTools.ts
@@ -0,0 +1,1837 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+
+/**
+ * @license
+ * SKALE IMA
+ *
+ * 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 .
+ */
+
+/**
+ * @file clpTools.ts
+ * @copyright SKALE Labs 2019-Present
+ */
+
+import * as owaspUtils from "./owaspUtils.js";
+import * as log from "./log.js";
+import * as imaCLI from "./cli.js";
+import * as rpcCall from "./rpcCall.js";
+import * as state from "./state.js";
+import * as IMA from "./imaCore.js";
+import * as imaHelperAPIs from "./imaHelperAPIs.js";
+import * as imaGasUsage from "./imaGasUsageOperations.js";
+import * as imaReimbursement from "./imaReimbursementOperations.js";
+import * as imaReg from "./imaRegistrationOperations.js";
+import * as imaEth from "./imaEthOperations.js";
+import * as imaToken from "./imaTokenOperations.js";
+import * as skaleObserver from "./observer.js";
+import * as discoveryTools from "./discoveryTools.js";
+import * as loop from "./loop.js";
+import * as imaUtils from "./utils.js";
+import * as imaBLS from "./bls.js";
+import type * as imaTx from "./imaTx.js";
+
+export async function registerAll( isPrintSummaryRegistrationCosts: boolean ): Promise < boolean > {
+ if( !await registerStep1( false ) )
+ return false;
+ if( isPrintSummaryRegistrationCosts )
+ printSummaryRegistrationCosts( log );
+ return true;
+}
+
+export async function checkRegistrationAll(): Promise {
+ const b1 = await checkRegistrationStep1();
+ return b1;
+}
+
+export interface TRegistrationCostInformation {
+ mn: any[]
+ sc: any[]
+}
+
+const gInfoRegistrationCost: TRegistrationCostInformation = {
+ mn: [],
+ sc: []
+};
+
+export async function registerStep1( isPrintSummaryRegistrationCosts: boolean ): Promise {
+ const imaState: state.TIMAState = state.get();
+ imaCLI.initContracts();
+ const strLogPrefix = "Reg 1: ";
+ log.information( "{p}Will check chain registration now...", strLogPrefix );
+ if( !imaState.chainProperties.mn.ethersProvider )
+ throw new Error( "No provider for MN" );
+ let bSuccess = await imaReg.checkIsRegisteredSChainInDepositBoxes( // step 1
+ imaState.chainProperties.mn.ethersProvider,
+ imaState.joLinker,
+ imaState.chainProperties.mn.joAccount,
+ imaState.chainProperties.sc.strChainName
+ );
+ log.information( "{p}Chain is {}", strLogPrefix,
+ log.posNeg( bSuccess, "already registered", "not registered yet" ) );
+ if( bSuccess )
+ return true;
+ const jarrReceipts: any =
+ await imaReg.registerSChainInDepositBoxes( // step 1
+ imaState.chainProperties.mn.ethersProvider,
+ imaState.joLinker,
+ imaState.chainProperties.mn.joAccount,
+ imaState.joTokenManagerETH, // only s-chain
+ imaState.joTokenManagerERC20, // only s-chain
+ imaState.joTokenManagerERC721, // only s-chain
+ imaState.joTokenManagerERC1155, // only s-chain
+ imaState.joTokenManagerERC721WithMetadata, // only s-chain
+ imaState.joCommunityLocker, // only s-chain
+ imaState.joTokenManagerLinker, // only s-chain
+ imaState.chainProperties.sc.strChainName,
+ imaState.chainProperties.mn.chainId.toString(),
+ imaState.chainProperties.mn.transactionCustomizer //,
+ );
+ bSuccess = !!( ( jarrReceipts != null && jarrReceipts.length > 0 ) );
+ log.information( "{p}Chain was {}", strLogPrefix,
+ log.posNeg( bSuccess, "registered successfully", "not registered" ) );
+ if( bSuccess ) {
+ gInfoRegistrationCost.mn =
+ gInfoRegistrationCost.mn.concat( gInfoRegistrationCost.mn, jarrReceipts );
+ }
+ if( isPrintSummaryRegistrationCosts )
+ printSummaryRegistrationCosts();
+ if( !bSuccess ) {
+ const nRetCode = 163;
+ log.fatal( "{p}failed to register S-Chain in deposit box, will return code {}",
+ strLogPrefix, nRetCode );
+ process.exit( nRetCode );
+ }
+ return true;
+}
+
+export async function checkRegistrationStep1(): Promise {
+ const imaState: state.TIMAState = state.get();
+ imaCLI.initContracts();
+ if( !imaState.chainProperties.mn.ethersProvider )
+ throw new Error( "No provider for MN" );
+ const bRetVal = await imaReg.checkIsRegisteredSChainInDepositBoxes( // step 1
+ imaState.chainProperties.mn.ethersProvider,
+ imaState.joLinker,
+ imaState.chainProperties.mn.joAccount,
+ imaState.chainProperties.sc.strChainName
+ );
+ return bRetVal;
+}
+
+export function printSummaryRegistrationCosts( details?: any ): void {
+ if( !details )
+ details = log;
+ imaGasUsage.printGasUsageReportFromArray(
+ "Main Net REGISTRATION", gInfoRegistrationCost.mn, details );
+ imaGasUsage.printGasUsageReportFromArray(
+ "S-Chain REGISTRATION", gInfoRegistrationCost.sc, details );
+}
+
+export function commandLineTaskRegister(): void {
+ const imaState: state.TIMAState = state.get();
+ imaState.arrActions.push( {
+ name: "Full registration(all steps)",
+ fn: async function(): Promise < boolean > {
+ if( !imaState.bNoWaitSChainStarted )
+ await discoveryTools.waitUntilSChainStarted(); // registerAll
+ return await registerAll( true );
+ }
+ } );
+}
+
+export function commandLineTaskRegister1(): void {
+ const imaState: state.TIMAState = state.get();
+ imaState.arrActions.push( {
+ name: "Registration step 1, register S-Chain in deposit box",
+ fn: async function(): Promise < boolean > {
+ if( !imaState.bNoWaitSChainStarted )
+ await discoveryTools.waitUntilSChainStarted(); // registerStep1
+ return await registerStep1( true );
+ }
+ } );
+}
+
+export function commandLineTaskCheckRegistration(): void {
+ const imaState: state.TIMAState = state.get();
+ imaState.arrActions.push( {
+ name: "Full registration status check(all steps)",
+ fn: async function(): Promise < boolean > {
+ if( !imaState.bNoWaitSChainStarted )
+ await discoveryTools.waitUntilSChainStarted(); // checkRegistrationAll
+ const b = await checkRegistrationAll();
+ // nExitCode is: 0 - OKay - registered; non-zero - not registered or error
+ const nExitCode = b ? 0 : 150;
+ log.notice( "Exiting with code {}", nExitCode );
+ process.exit( nExitCode );
+ }
+ } );
+}
+
+export function commandLineTaskCheckRegistration1(): void {
+ const imaState: state.TIMAState = state.get();
+ imaState.arrActions.push( {
+ name: "Registration status check step 1, register S-Chain in deposit box",
+ fn: async function(): Promise < boolean > {
+ if( !imaState.bNoWaitSChainStarted )
+ await discoveryTools.waitUntilSChainStarted(); // checkRegistrationStep1
+ const b = await checkRegistrationStep1();
+ // nExitCode is: 0 - OKay - registered; non-zero - not registered or error
+ const nExitCode = b ? 0 : 152;
+ log.notice( "Exiting with code {}", nExitCode );
+ process.exit( nExitCode );
+ }
+ } );
+}
+
+export function commandLineTaskMintErc20(): void {
+ const imaState: state.TIMAState = state.get();
+ imaState.arrActions.push( {
+ name: "mint ERC20",
+ fn: async function(): Promise < boolean > {
+ let bMintIsOK = false;
+ if( imaState.chainProperties.tc.strCoinNameErc20.length > 0 ) {
+ try {
+ const strAddressMintTo = // same as caller/transaction signer
+ imaState.chainProperties.tc.joAccount.address();
+ if( !imaState.chainProperties.tc.ethersProvider )
+ throw new Error( "No provider for target chain" );
+ bMintIsOK =
+ !!await imaToken.mintErc20(
+ imaState.chainProperties.tc.ethersProvider,
+ imaState.chainProperties.tc.chainId.toString(),
+ imaState.chainProperties.tc.strChainName,
+ imaState.chainProperties.tc.joAccount,
+ strAddressMintTo,
+ imaState.nAmountOfToken,
+ imaState.chainProperties.tc.joErc20[imaState.chainProperties
+ .tc.strCoinNameErc20 + "_address"],
+ imaState.chainProperties.tc.joErc20[imaState.chainProperties
+ .tc.strCoinNameErc20 + "_abi"],
+ imaState.chainProperties.tc.transactionCustomizer
+ );
+ } catch ( err ) {
+ bMintIsOK = false;
+ }
+ }
+ return bMintIsOK;
+ }
+ } );
+}
+
+export function commandLineTaskMintErc721(): void {
+ const imaState: state.TIMAState = state.get();
+ imaState.arrActions.push( {
+ name: "mint ERC721",
+ fn: async function(): Promise < boolean > {
+ let bMintIsOK = false;
+ if( imaState.chainProperties.tc.strCoinNameErc721.length > 0 ) {
+ try {
+ const strAddressMintTo = // same as caller/transaction signer
+ imaState.chainProperties.tc.joAccount.address();
+ const idTokens: any[] =
+ ( imaState.haveArrayOfTokenIdentifiers && imaState.idTokens )
+ ? imaState.idTokens
+ : [];
+ if( imaState.haveOneTokenIdentifier )
+ idTokens.push( imaState.idToken );
+ if( idTokens.length > 0 ) {
+ for( let i = 0; i < idTokens.length; ++i ) {
+ const idToken = idTokens[i];
+ if( !imaState.chainProperties.tc.ethersProvider )
+ throw new Error( "No provider for target chain" );
+ bMintIsOK =
+ !!await imaToken.mintErc721(
+ imaState.chainProperties.tc.ethersProvider,
+ imaState.chainProperties.tc.chainId.toString(),
+ imaState.chainProperties.tc.strChainName,
+ imaState.chainProperties.tc.joAccount,
+ strAddressMintTo,
+ idToken,
+ imaState.chainProperties.tc.joErc721[imaState
+ .chainProperties.tc.strCoinNameErc721 + "_address"],
+ imaState.chainProperties.tc.joErc721[imaState
+ .chainProperties.tc.strCoinNameErc721 + "_abi"],
+ imaState.chainProperties.tc.transactionCustomizer
+ );
+ }
+ }
+ } catch ( err ) {
+ bMintIsOK = false;
+ }
+ }
+ return bMintIsOK;
+ }
+ } );
+}
+
+export function commandLineTaskMintErc1155(): void {
+ const imaState: state.TIMAState = state.get();
+ imaState.arrActions.push( {
+ name: "mint ERC1155",
+ fn: async function(): Promise < boolean > {
+ let bMintIsOK = false;
+ if( imaState.chainProperties.tc.strCoinNameErc1155.length > 0 ) {
+ try {
+ const strAddressMintTo = // same as caller/transaction signer
+ imaState.chainProperties.tc.joAccount.address();
+ const idTokens: any[] =
+ ( imaState.haveArrayOfTokenIdentifiers && imaState.idTokens )
+ ? imaState.idTokens
+ : [];
+ if( imaState.haveOneTokenIdentifier )
+ idTokens.push( imaState.idToken );
+ if( idTokens.length > 0 ) {
+ for( let i = 0; i < idTokens.length; ++i ) {
+ const idToken = idTokens[i];
+ if( !imaState.chainProperties.tc.ethersProvider )
+ throw new Error( "No provider for target chain" );
+ bMintIsOK =
+ !!await imaToken.mintErc1155(
+ imaState.chainProperties.tc.ethersProvider,
+ imaState.chainProperties.tc.chainId.toString(),
+ imaState.chainProperties.tc.strChainName,
+ imaState.chainProperties.tc.joAccount,
+ strAddressMintTo,
+ idToken,
+ imaState.nAmountOfToken,
+ imaState.chainProperties.tc
+ .joErc1155[imaState.chainProperties.tc
+ .strCoinNameErc1155 + "_address"],
+ imaState.chainProperties.tc
+ .joErc1155[imaState.chainProperties.tc
+ .strCoinNameErc1155 + "_abi"],
+ imaState.chainProperties.tc.transactionCustomizer
+ );
+ }
+ }
+ } catch ( err ) {
+ bMintIsOK = false;
+ }
+ }
+ return bMintIsOK;
+ }
+ } );
+}
+
+export function commandLineTaskBurnErc20(): void {
+ const imaState: state.TIMAState = state.get();
+ imaState.arrActions.push( {
+ name: "burn ERC20",
+ fn: async function(): Promise < boolean > {
+ let bBurnIsOK = false;
+ if( imaState.chainProperties.tc.strCoinNameErc20.length > 0 ) {
+ try {
+ const strAddressBurnFrom = // same as caller/transaction signer
+ imaState.chainProperties.tc.joAccount.address();
+ if( !imaState.chainProperties.tc.ethersProvider )
+ throw new Error( "No provider for target chain" );
+ bBurnIsOK =
+ !!await imaToken.burnErc20(
+ imaState.chainProperties.tc.ethersProvider,
+ imaState.chainProperties.tc.chainId.toString(),
+ imaState.chainProperties.tc.strChainName,
+ imaState.chainProperties.tc.joAccount,
+ strAddressBurnFrom,
+ imaState.nAmountOfToken,
+ imaState.chainProperties.tc
+ .joErc20[imaState.chainProperties
+ .tc.strCoinNameErc20 + "_address"],
+ imaState.chainProperties.tc
+ .joErc20[imaState.chainProperties
+ .tc.strCoinNameErc20 + "_abi"],
+ imaState.chainProperties.tc.transactionCustomizer
+ );
+ } catch ( err ) {
+ bBurnIsOK = false;
+ }
+ }
+ return bBurnIsOK;
+ }
+ } );
+}
+
+export function commandLineTaskBurnErc721(): void {
+ const imaState: state.TIMAState = state.get();
+ imaState.arrActions.push( {
+ name: "burn ERC721",
+ fn: async function(): Promise < boolean > {
+ let bBurnIsOK = false;
+ if( imaState.chainProperties.tc.strCoinNameErc721.length > 0 ) {
+ try {
+ const idTokens: any[] =
+ ( imaState.haveArrayOfTokenIdentifiers && imaState.idTokens )
+ ? imaState.idTokens
+ : [];
+ if( imaState.haveOneTokenIdentifier )
+ idTokens.push( imaState.idToken );
+ if( idTokens.length > 0 ) {
+ for( let i = 0; i < idTokens.length; ++i ) {
+ const idToken = idTokens[i];
+ if( !imaState.chainProperties.tc.ethersProvider )
+ throw new Error( "No provider for target chain" );
+ bBurnIsOK =
+ !!await imaToken.burnErc721(
+ imaState.chainProperties.tc.ethersProvider,
+ imaState.chainProperties.tc.chainId.toString(),
+ imaState.chainProperties.tc.strChainName,
+ imaState.chainProperties.tc.joAccount,
+ idToken,
+ imaState.chainProperties.tc
+ .joErc721[imaState.chainProperties
+ .tc.strCoinNameErc721 + "_address"],
+ imaState.chainProperties.tc
+ .joErc721[imaState.chainProperties
+ .tc.strCoinNameErc721 + "_abi"],
+ imaState.chainProperties.tc.transactionCustomizer
+ );
+ }
+ }
+ } catch ( err ) {
+ bBurnIsOK = false;
+ }
+ }
+ return bBurnIsOK;
+ }
+ } );
+}
+
+export function commandLineTaskBurnErc1155(): void {
+ const imaState: state.TIMAState = state.get();
+ imaState.arrActions.push( {
+ name: "burn ERC1155",
+ fn: async function(): Promise < boolean > {
+ let bBurnIsOK = false;
+ if( imaState.chainProperties.tc.strCoinNameErc1155.length > 0 ) {
+ try {
+ const strAddressBurnFrom = // same as caller/transaction signer
+ imaState.chainProperties.tc.joAccount.address();
+ const idTokens: any[] =
+ ( imaState.haveArrayOfTokenIdentifiers && imaState.idTokens )
+ ? imaState.idTokens
+ : [];
+ if( imaState.haveOneTokenIdentifier )
+ idTokens.push( imaState.idToken );
+ if( idTokens.length > 0 ) {
+ for( let i = 0; i < idTokens.length; ++i ) {
+ const idToken = idTokens[i];
+ if( !imaState.chainProperties.tc.ethersProvider )
+ throw new Error( "No provider for target chain" );
+ bBurnIsOK =
+ !!await imaToken.burnErc1155(
+ imaState.chainProperties.tc.ethersProvider,
+ imaState.chainProperties.tc.chainId.toString(),
+ imaState.chainProperties.tc.strChainName,
+ imaState.chainProperties.tc.joAccount,
+ strAddressBurnFrom,
+ idToken,
+ imaState.nAmountOfToken,
+ imaState.chainProperties.tc
+ .joErc1155[imaState.chainProperties
+ .tc.strCoinNameErc1155 + "_address"],
+ imaState.chainProperties.tc
+ .joErc1155[imaState.chainProperties
+ .tc.strCoinNameErc1155 + "_abi"],
+ imaState.chainProperties.tc.transactionCustomizer
+ );
+ }
+ }
+ } catch ( err ) {
+ bBurnIsOK = false;
+ }
+ }
+ return bBurnIsOK;
+ }
+ } );
+}
+
+export async function commandLineTaskShowBalanceEth(
+ arrBalancesMN: any[], arrBalancesSC: any[], arrBalancesTC: any[]
+): Promise {
+ const imaState: state.TIMAState = state.get();
+ let assetAddress: string | null = null;
+ if( imaState.chainProperties.mn.ethersProvider ) {
+ arrBalancesMN.push( {
+ assetName: "RealETH",
+ balance: await imaEth.getBalanceEth( true, // isMainNet
+ imaState.chainProperties.mn.ethersProvider,
+ imaState.chainProperties.mn.chainId.toString(),
+ imaState.chainProperties.mn.joAccount, null )
+ } );
+ if( !imaState.joDepositBoxETH )
+ throw new Error( "No DepositBoxETH contract" );
+ arrBalancesMN.push( {
+ assetName: "CanReceiveETH",
+ balance: await imaEth.viewEthPaymentFromSchainOnMainNet(
+ imaState.chainProperties.mn.ethersProvider,
+ imaState.chainProperties.mn.joAccount, imaState.joDepositBoxETH )
+ } );
+ }
+ try {
+ assetAddress = imaState.joEthErc20 ? imaState.joEthErc20.address : "";
+ } catch ( err ) {
+ assetAddress = null;
+ }
+ if( imaState.chainProperties.sc.ethersProvider ) {
+ arrBalancesSC.push( {
+ assetName: "S-Chain Real ETH as ERC20",
+ assetAddress,
+ balance: await imaEth.getBalanceEth( false, // isMainNet
+ imaState.chainProperties.sc.ethersProvider,
+ imaState.chainProperties.sc.chainId.toString(),
+ imaState.chainProperties.sc.joAccount, imaState.joEthErc20 )
+ } );
+ arrBalancesSC.push( {
+ assetName: "S-Chain ETH Fuel",
+ balance: await imaEth.getBalanceEth( true, // isMainNet=true here, but we call S-Chain
+ imaState.chainProperties.sc.ethersProvider,
+ imaState.chainProperties.sc.chainId.toString(),
+ imaState.chainProperties.sc.joAccount, null )
+ } );
+ }
+ if( imaState.chainProperties.tc.ethersProvider ) {
+ arrBalancesSC.push( {
+ assetName: "Target S-Chain Real ETH as ERC20",
+ assetAddress,
+ balance: await imaEth.getBalanceEth( false, // isMainNet
+ imaState.chainProperties.tc.ethersProvider,
+ imaState.chainProperties.sc.chainId.toString(),
+ imaState.chainProperties.tc.joAccount, imaState.joEthErc20 )
+ } );
+ arrBalancesTC.push( {
+ assetName: "Target S-Chain ETH Fuel",
+ balance: await imaEth.getBalanceEth( true, // isMainNet=true here, but we call S-Chain
+ imaState.chainProperties.tc.ethersProvider,
+ imaState.chainProperties.tc.chainId.toString(),
+ imaState.chainProperties.tc.joAccount, null )
+ } );
+ }
+}
+
+export async function commandLineTaskShowBalanceErc20(
+ arrBalancesMN: any[], arrBalancesSC: any[], arrBalancesTC: any[]
+): Promise {
+ const imaState: state.TIMAState = state.get();
+ let assetAddress = null;
+ if( imaState.chainProperties.mn.ethersProvider &&
+ imaState.chainProperties.mn.strCoinNameErc20.length > 0
+ ) {
+ try {
+ assetAddress = imaState.chainProperties.mn.joErc20[
+ imaState.chainProperties.mn.strCoinNameErc20 + "_address"];
+ } catch ( err ) { assetAddress = null; }
+ arrBalancesMN.push( {
+ assetName: "ERC20",
+ assetAddress,
+ balance: await imaToken.getBalanceErc20( true, // isMainNet
+ imaState.chainProperties.mn.ethersProvider,
+ imaState.chainProperties.mn.chainId.toString(),
+ imaState.chainProperties.mn.joAccount,
+ imaState.chainProperties.mn.strCoinNameErc20,
+ imaState.chainProperties.mn.joErc20 )
+ } );
+ }
+ if( imaState.chainProperties.sc.ethersProvider &&
+ imaState.chainProperties.sc.strCoinNameErc20.length > 0
+ ) {
+ try {
+ assetAddress = imaState.chainProperties.sc.joErc20[
+ imaState.chainProperties.sc.strCoinNameErc20 + "_address"];
+ } catch ( err ) { assetAddress = null; }
+ arrBalancesSC.push( {
+ assetName: "ERC20",
+ assetAddress,
+ balance: await imaToken.getBalanceErc20( false, // isMainNet
+ imaState.chainProperties.sc.ethersProvider,
+ imaState.chainProperties.sc.chainId.toString(),
+ imaState.chainProperties.sc.joAccount,
+ imaState.chainProperties.sc.strCoinNameErc20,
+ imaState.chainProperties.sc.joErc20 )
+ } );
+ }
+ if( imaState.chainProperties.tc.ethersProvider &&
+ imaState.chainProperties.tc.strCoinNameErc20.length > 0
+ ) {
+ try {
+ assetAddress = imaState.chainProperties.tc.joErc20[
+ imaState.chainProperties.tc.strCoinNameErc20 + "_address"];
+ } catch ( err ) { assetAddress = null; }
+ arrBalancesTC.push( {
+ assetName: "ERC20",
+ assetAddress,
+ balance: await imaToken.getBalanceErc20( true, // isMainNet
+ imaState.chainProperties.tc.ethersProvider,
+ imaState.chainProperties.mn.chainId.toString(),
+ imaState.chainProperties.tc.joAccount,
+ imaState.chainProperties.tc.strCoinNameErc20,
+ imaState.chainProperties.tc.joErc20 )
+ } );
+ }
+}
+
+export async function commandLineTaskShowBalanceErc721(
+ arrBalancesMN: any[], arrBalancesSC: any[], arrBalancesTC: any[], idTokens: any[]
+): Promise {
+ const imaState: state.TIMAState = state.get();
+ let assetAddress = null;
+ if( imaState.chainProperties.mn.ethersProvider &&
+ imaState.chainProperties.mn.strCoinNameErc721.length > 0
+ ) {
+ for( let i = 0; i < idTokens.length; ++i ) {
+ const idToken = idTokens[i];
+ try {
+ assetAddress = imaState.chainProperties.mn.joErc721[
+ imaState.chainProperties.mn.strCoinNameErc721 + "_address"];
+ } catch ( err ) { assetAddress = null; }
+ arrBalancesMN.push( {
+ assetName: "ERC721",
+ assetAddress,
+ idToken,
+ owner: await imaToken.getOwnerOfErc721( true, // isMainNet
+ imaState.chainProperties.mn.ethersProvider,
+ imaState.chainProperties.mn.chainId.toString(),
+ imaState.chainProperties.mn.joAccount,
+ imaState.chainProperties.mn.strCoinNameErc721,
+ imaState.chainProperties.mn.joErc721, idToken )
+ } );
+ }
+ }
+ if( imaState.chainProperties.sc.ethersProvider &&
+ imaState.chainProperties.sc.strCoinNameErc721.length > 0
+ ) {
+ for( let i = 0; i < idTokens.length; ++i ) {
+ const idToken = idTokens[i];
+ try {
+ assetAddress = imaState.chainProperties.sc.joErc721[
+ imaState.chainProperties.sc.strCoinNameErc721 + "_address"];
+ } catch ( err ) { assetAddress = null; }
+ arrBalancesSC.push( {
+ assetName: "ERC721",
+ assetAddress,
+ idToken,
+ owner: await imaToken.getOwnerOfErc721( false, // isMainNet
+ imaState.chainProperties.sc.ethersProvider,
+ imaState.chainProperties.sc.chainId.toString(),
+ imaState.chainProperties.sc.joAccount,
+ imaState.chainProperties.sc.strCoinNameErc721,
+ imaState.chainProperties.sc.joErc721, idToken )
+ } );
+ }
+ }
+ if( imaState.chainProperties.tc.ethersProvider &&
+ imaState.chainProperties.tc.strCoinNameErc721.length > 0
+ ) {
+ for( let i = 0; i < idTokens.length; ++i ) {
+ const idToken = idTokens[i];
+ try {
+ assetAddress = imaState.chainProperties.tc.joErc721[
+ imaState.chainProperties.tc.strCoinNameErc721 + "_address"];
+ } catch ( err ) { assetAddress = null; }
+ arrBalancesTC.push( {
+ assetName: "ERC721",
+ assetAddress,
+ idToken,
+ owner: await imaToken.getOwnerOfErc721( false, // isMainNet
+ imaState.chainProperties.tc.ethersProvider,
+ imaState.chainProperties.tc.chainId.toString(),
+ imaState.chainProperties.tc.joAccount,
+ imaState.chainProperties.tc.strCoinNameErc721,
+ imaState.chainProperties.tc.joErc721, idToken )
+ } );
+ }
+ }
+}
+
+export async function commandLineTaskShowBalanceErc1155(
+ arrBalancesMN: any[], arrBalancesSC: any[], arrBalancesTC: any[], idTokens: any[]
+): Promise {
+ const imaState: state.TIMAState = state.get();
+ let assetAddress = null;
+ if( imaState.chainProperties.mn.ethersProvider &&
+ imaState.chainProperties.mn.strCoinNameErc1155.length > 0
+ ) {
+ for( let i = 0; i < idTokens.length; ++i ) {
+ const idToken = idTokens[i];
+ try {
+ assetAddress = imaState.chainProperties.mn.joErc1155[
+ imaState.chainProperties.mn.strCoinNameErc1155 + "_address"];
+ } catch ( err ) { assetAddress = null; }
+ arrBalancesMN.push( {
+ assetName: "ERC1155",
+ assetAddress,
+ idToken,
+ balance: await imaToken.getBalanceErc1155( true, // isMainNet
+ imaState.chainProperties.mn.ethersProvider,
+ imaState.chainProperties.mn.chainId.toString(),
+ imaState.chainProperties.mn.joAccount,
+ imaState.chainProperties.mn.strCoinNameErc1155,
+ imaState.chainProperties.mn.joErc1155, idToken )
+ } );
+ }
+ }
+ if( imaState.chainProperties.sc.ethersProvider &&
+ imaState.chainProperties.sc.strCoinNameErc1155.length > 0
+ ) {
+ for( let i = 0; i < idTokens.length; ++i ) {
+ const idToken = idTokens[i];
+ try {
+ assetAddress = imaState.chainProperties.sc.joErc1155[
+ imaState.chainProperties.sc.strCoinNameErc1155 + "_address"];
+ } catch ( err ) { assetAddress = null; }
+ arrBalancesSC.push( {
+ assetName: "ERC1155",
+ assetAddress,
+ idToken,
+ balance: await imaToken.getBalanceErc1155( false, // isMainNet
+ imaState.chainProperties.sc.ethersProvider,
+ imaState.chainProperties.sc.chainId.toString(),
+ imaState.chainProperties.sc.joAccount,
+ imaState.chainProperties.sc.strCoinNameErc1155,
+ imaState.chainProperties.sc.joErc1155, idToken )
+ } );
+ }
+ }
+ if( imaState.chainProperties.tc.ethersProvider &&
+ imaState.chainProperties.tc.strCoinNameErc1155.length > 0
+ ) {
+ for( let i = 0; i < idTokens.length; ++i ) {
+ const idToken = idTokens[i];
+ try {
+ assetAddress = imaState.chainProperties.tc.joErc1155[
+ imaState.chainProperties.tc.strCoinNameErc1155 + "_address"];
+ } catch ( err ) { assetAddress = null; }
+ arrBalancesTC.push( {
+ assetName: "ERC1155",
+ assetAddress,
+ idToken,
+ balance: await imaToken.getBalanceErc1155( false, // isMainNet
+ imaState.chainProperties.tc.ethersProvider,
+ imaState.chainProperties.tc.chainId.toString(),
+ imaState.chainProperties.tc.joAccount,
+ imaState.chainProperties.tc.strCoinNameErc1155,
+ imaState.chainProperties.tc.joErc1155, idToken )
+ } );
+ }
+ }
+}
+
+export function commandLineTaskShowBalance(): void {
+ const imaState: state.TIMAState = state.get();
+ imaState.arrActions.push( {
+ name: "show balance",
+ fn: async function(): Promise < boolean > {
+ const arrBalancesMN: any = [];
+ const arrBalancesSC: any = []; const arrBalancesTC: any = [];
+ await commandLineTaskShowBalanceEth(
+ arrBalancesMN, arrBalancesSC, arrBalancesTC );
+ await commandLineTaskShowBalanceErc20(
+ arrBalancesMN, arrBalancesSC, arrBalancesTC );
+ const idTokens: any[] =
+ ( imaState.haveArrayOfTokenIdentifiers && imaState.idTokens )
+ ? imaState.idTokens
+ : [];
+ if( imaState.haveOneTokenIdentifier )
+ idTokens.push( imaState.idToken );
+ if( idTokens.length > 0 ) {
+ await commandLineTaskShowBalanceErc721(
+ arrBalancesMN, arrBalancesSC, arrBalancesTC, idTokens );
+ await commandLineTaskShowBalanceErc1155(
+ arrBalancesMN, arrBalancesSC, arrBalancesTC, idTokens );
+ }
+ if( arrBalancesMN.length > 0 || arrBalancesSC.length > 0 || arrBalancesTC.length > 0 ) {
+ if( arrBalancesMN.length > 0 ) {
+ const strAddress = imaState.chainProperties.mn.joAccount.address();
+ log.information( "Main Net {} of {}:",
+ ( arrBalancesMN.length > 1 ? "balances" : "balance" ), strAddress );
+ for( let i = 0; i < arrBalancesMN.length; ++i ) {
+ const bi = arrBalancesMN[i];
+ log.information( " {}",
+ discoveryTools.formatBalanceInfo( bi, strAddress ) );
+ }
+ }
+ if( arrBalancesSC.length > 0 ) {
+ const strAddress = imaState.chainProperties.sc.joAccount.address();
+ log.information( "S-Chain {} of {}:",
+ ( arrBalancesMN.length > 1 ? "balances" : "balance" ), strAddress );
+ for( let i = 0; i < arrBalancesSC.length; ++i ) {
+ const bi = arrBalancesSC[i];
+ log.information( " {}",
+ discoveryTools.formatBalanceInfo( bi, strAddress ) );
+ }
+ }
+ if( arrBalancesTC.length > 0 ) {
+ const strAddress = imaState.chainProperties.mn.joAccount.address();
+ log.information( "Target S-Chain {} of {}:",
+ arrBalancesTC.length > 1 ? "balances" : "balance", strAddress );
+ for( let i = 0; i < arrBalancesTC.length; ++i ) {
+ const bi = arrBalancesTC[i];
+ log.information( " {}",
+ discoveryTools.formatBalanceInfo( bi, strAddress ) );
+ }
+ }
+ } else
+ log.warning( "No balances to scan." );
+ return true;
+ }
+ } );
+}
+
+export function commandLineTaskPaymentM2S(): void {
+ const imaState: state.TIMAState = state.get();
+ imaState.arrActions.push( {
+ name: "one M->S single payment",
+ fn: async function(): Promise < boolean > {
+ if( imaState.chainProperties.mn.strCoinNameErc721.length > 0 ) {
+ // ERC721 payment
+ log.information( "one M->S single ERC721 payment: {}", imaState.idToken );
+ if( !imaState.chainProperties.mn.ethersProvider )
+ throw new Error( "No provider for MN" );
+ if( !imaState.chainProperties.sc.ethersProvider )
+ throw new Error( "No provider for SC" );
+ const joDepositBoxERC721 = imaState.isWithMetadata721
+ ? imaState.joDepositBoxERC721WithMetadata
+ : imaState.joDepositBoxERC721;
+ if( !joDepositBoxERC721 )
+ throw new Error( "No DepositBoxERC721 contract" );
+ if( !imaState.joMessageProxyMainNet )
+ throw new Error( "No MessageProxyMainNet contract" );
+ const joTokenManagerERC721 = imaState.isWithMetadata721
+ ? imaState.joTokenManagerERC721WithMetadata
+ : imaState.joTokenManagerERC721;
+ if( !joTokenManagerERC721 )
+ throw new Error( "No TokenManagerERC721 contract" );
+ return await imaToken.doErc721PaymentFromMainNet(
+ imaState.chainProperties.mn.ethersProvider,
+ imaState.chainProperties.sc.ethersProvider,
+ imaState.chainProperties.mn.chainId.toString(),
+ imaState.chainProperties.sc.chainId.toString(),
+ imaState.chainProperties.mn.joAccount,
+ imaState.chainProperties.sc.joAccount,
+ joDepositBoxERC721, // only main net
+ imaState.joMessageProxyMainNet, // for checking logs
+ imaState.chainProperties.sc.strChainName,
+ imaState.idToken, // which ERC721 token id to send
+ imaState.nAmountOfWei, // how much to send
+ joTokenManagerERC721, // only s-chain
+ imaState.chainProperties.mn.strCoinNameErc721,
+ imaState.chainProperties.mn.joErc721,
+ imaState.chainProperties.sc.strCoinNameErc721,
+ imaState.chainProperties.sc.joErc721,
+ imaState.chainProperties.mn.transactionCustomizer
+ );
+ }
+ if( imaState.chainProperties.tc.strCoinNameErc20.length > 0 ) {
+ // ERC20 payment
+ log.information( "one M->S single ERC20 payment: {}", imaState.nAmountOfToken );
+ if( !imaState.chainProperties.mn.ethersProvider )
+ throw new Error( "No provider for MN" );
+ if( !imaState.chainProperties.sc.ethersProvider )
+ throw new Error( "No provider for SC" );
+ if( !imaState.joDepositBoxERC20 )
+ throw new Error( "No DepositBoxERC20 contract" );
+ if( !imaState.joMessageProxyMainNet )
+ throw new Error( "No MessageProxyMainNet contract" );
+ if( !imaState.joTokenManagerERC20 )
+ throw new Error( "No TokenManagerERC20 contract" );
+ return await imaToken.doErc20PaymentFromMainNet(
+ imaState.chainProperties.mn.ethersProvider,
+ imaState.chainProperties.sc.ethersProvider,
+ imaState.chainProperties.mn.chainId.toString(),
+ imaState.chainProperties.sc.chainId.toString(),
+ imaState.chainProperties.mn.joAccount,
+ imaState.chainProperties.sc.joAccount,
+ imaState.joDepositBoxERC20, // only main net
+ imaState.joMessageProxyMainNet, // for checking logs
+ imaState.chainProperties.sc.strChainName,
+ imaState.nAmountOfToken, // how much ERC20 tokens to send
+ imaState.nAmountOfWei, // how much to send
+ imaState.joTokenManagerERC20, // only s-chain
+ imaState.chainProperties.tc.strCoinNameErc20,
+ imaState.chainProperties.mn.joErc20,
+ imaState.chainProperties.sc.strCoinNameErc20,
+ imaState.chainProperties.sc.joErc20,
+ imaState.chainProperties.mn.transactionCustomizer
+ );
+ }
+ if( imaState.chainProperties.mn.strCoinNameErc1155.length > 0 &&
+ imaState.idToken && imaState.idToken !== null &&
+ imaState.idToken !== undefined &&
+ imaState.nAmountOfToken && imaState.nAmountOfToken !== null &&
+ imaState.nAmountOfToken !== undefined &&
+ ( ( !imaState.idTokens ) || imaState.idTokens === null ||
+ imaState.idTokens === undefined ) &&
+ ( ( !imaState.arrAmountsOfTokens ) || imaState.arrAmountsOfTokens === null ||
+ imaState.arrAmountsOfTokens === undefined )
+ ) {
+ // ERC1155 payment
+ log.information( "one M->S single ERC1155 payment: {} {}",
+ imaState.idToken, imaState.nAmountOfToken );
+ if( !imaState.chainProperties.mn.ethersProvider )
+ throw new Error( "No provider for MN" );
+ if( !imaState.chainProperties.sc.ethersProvider )
+ throw new Error( "No provider for SC" );
+ if( !imaState.joDepositBoxERC1155 )
+ throw new Error( "No DepositBoxERC1155 contract" );
+ if( !imaState.joMessageProxyMainNet )
+ throw new Error( "No MessageProxyMainNet contract" );
+ if( !imaState.joTokenManagerERC1155 )
+ throw new Error( "No TokenManagerERC1155 contract" );
+ return await imaToken.doErc1155PaymentFromMainNet(
+ imaState.chainProperties.mn.ethersProvider,
+ imaState.chainProperties.sc.ethersProvider,
+ imaState.chainProperties.mn.chainId.toString(),
+ imaState.chainProperties.sc.chainId.toString(),
+ imaState.chainProperties.mn.joAccount,
+ imaState.chainProperties.sc.joAccount,
+ imaState.joDepositBoxERC1155, // only main net
+ imaState.joMessageProxyMainNet, // for checking logs
+ imaState.chainProperties.sc.strChainName,
+ imaState.idToken, // which ERC1155 token id to send
+ imaState.nAmountOfToken, // which ERC1155 token amount to send
+ imaState.nAmountOfWei, // how much to send
+ imaState.joTokenManagerERC1155, // only s-chain
+ imaState.chainProperties.mn.strCoinNameErc1155,
+ imaState.chainProperties.mn.joErc1155,
+ imaState.chainProperties.sc.strCoinNameErc1155,
+ imaState.chainProperties.sc.joErc1155,
+ imaState.chainProperties.mn.transactionCustomizer
+ );
+ }
+ if(
+ imaState.chainProperties.mn.strCoinNameErc1155.length > 0 &&
+ imaState.idTokens &&
+ imaState.idTokens !== null &&
+ imaState.idTokens !== undefined &&
+ imaState.arrAmountsOfTokens &&
+ imaState.arrAmountsOfTokens !== null &&
+ imaState.arrAmountsOfTokens !== undefined &&
+ ( !imaState.idToken ||
+ imaState.idToken === null ||
+ imaState.idToken === undefined ) &&
+ ( !imaState.nAmountOfToken ||
+ imaState.nAmountOfToken === null ||
+ imaState.nAmountOfToken === undefined )
+ ) {
+ // ERC1155 Batch payment
+ log.information( "one M->S single ERC1155 Batch payment: {} {}",
+ imaState.idTokens, imaState.arrAmountsOfTokens );
+ if( !imaState.chainProperties.mn.ethersProvider )
+ throw new Error( "No provider for MN" );
+ if( !imaState.chainProperties.sc.ethersProvider )
+ throw new Error( "No provider for SC" );
+ if( !imaState.joMessageProxyMainNet )
+ throw new Error( "No MessageProxyMainNet contract" );
+ if( !imaState.joDepositBoxERC1155 )
+ throw new Error( "No DepositBoxERC1155 contract" );
+ if( !imaState.joTokenManagerERC1155 )
+ throw new Error( "No TokenManagerERC1155 contract" );
+ return await imaToken.doErc1155BatchPaymentFromMainNet(
+ imaState.chainProperties.mn.ethersProvider,
+ imaState.chainProperties.sc.ethersProvider,
+ imaState.chainProperties.mn.chainId.toString(),
+ imaState.chainProperties.sc.chainId.toString(),
+ imaState.chainProperties.mn.joAccount,
+ imaState.chainProperties.sc.joAccount,
+ imaState.joDepositBoxERC1155, // only main net
+ imaState.joMessageProxyMainNet, // for checking logs
+ imaState.chainProperties.sc.strChainName,
+ imaState.idTokens, // which ERC1155 token id to send
+ imaState.arrAmountsOfTokens, // which ERC1155 token amount to send
+ imaState.nAmountOfWei, // how much to send
+ imaState.joTokenManagerERC1155, // only s-chain
+ imaState.chainProperties.mn.strCoinNameErc1155,
+ imaState.chainProperties.mn.joErc1155,
+ imaState.chainProperties.sc.strCoinNameErc1155,
+ imaState.chainProperties.sc.joErc1155,
+ imaState.chainProperties.mn.transactionCustomizer
+ );
+ }
+ // ETH payment
+ log.information( "one M->S single ETH payment: {}", imaState.nAmountOfWei );
+ if( !imaState.chainProperties.mn.ethersProvider )
+ throw new Error( "No provider for MN" );
+ if( !imaState.joDepositBoxETH )
+ throw new Error( "No DepositBoxETH contract" );
+ if( !imaState.joMessageProxyMainNet )
+ throw new Error( "No MessageProxyMainNet contract" );
+ return await imaEth.doEthPaymentFromMainNet(
+ imaState.chainProperties.mn.ethersProvider,
+ imaState.chainProperties.mn.chainId.toString(),
+ imaState.chainProperties.mn.joAccount,
+ imaState.chainProperties.sc.joAccount,
+ imaState.joDepositBoxETH, // only main net
+ imaState.joMessageProxyMainNet, // for checking logs
+ imaState.chainProperties.sc.strChainName,
+ imaState.nAmountOfWei, // how much WEI money to send
+ imaState.chainProperties.mn.transactionCustomizer
+ );
+ }
+ } );
+}
+
+export function commandLineTaskPaymentS2M(): void {
+ const imaState: state.TIMAState = state.get();
+ imaState.arrActions.push( {
+ name: "one S->M single payment",
+ fn: async function(): Promise < boolean > {
+ if( imaState.chainProperties.sc.strCoinNameErc721.length > 0 ) {
+ // ERC721 payment
+ log.information( "one S->M single ERC721 payment: {}", imaState.idToken );
+ if( !imaState.chainProperties.mn.ethersProvider )
+ throw new Error( "No provider for MN" );
+ if( !imaState.chainProperties.sc.ethersProvider )
+ throw new Error( "No provider for SC" );
+ const joTokenManagerERC721 = imaState.isWithMetadata721
+ ? imaState.joTokenManagerERC721WithMetadata
+ : imaState.joTokenManagerERC721;
+ if( !joTokenManagerERC721 )
+ throw new Error( "No TokenManagerERC721 contract" );
+ if( !imaState.joMessageProxySChain )
+ throw new Error( "No oMessageProxySChain contract" );
+ const joDepositBoxERC721 = imaState.isWithMetadata721
+ ? imaState.joDepositBoxERC721WithMetadata
+ : imaState.joDepositBoxERC721;
+ if( !joTokenManagerERC721 )
+ throw new Error( "No DepositBoxERC721 contract" );
+ if( !joDepositBoxERC721 )
+ throw new Error( "No DepositBoxERC721 contract" );
+ return await imaToken.doErc721PaymentFromSChain(
+ imaState.chainProperties.mn.ethersProvider,
+ imaState.chainProperties.sc.ethersProvider,
+ imaState.chainProperties.mn.chainId.toString(),
+ imaState.chainProperties.sc.chainId.toString(),
+ imaState.chainProperties.sc.joAccount,
+ imaState.chainProperties.mn.joAccount,
+ joTokenManagerERC721, // only s-chain
+ imaState.joMessageProxySChain, // for checking logs
+ joDepositBoxERC721, // only main net
+ imaState.idToken, // which ERC721 token id to send
+ imaState.nAmountOfWei, // how much to send
+ imaState.chainProperties.mn.strCoinNameErc721,
+ imaState.chainProperties.mn.joErc721,
+ imaState.chainProperties.sc.strCoinNameErc721,
+ imaState.chainProperties.sc.joErc721,
+ imaState.chainProperties.sc.transactionCustomizer
+ );
+ }
+ if( imaState.chainProperties.sc.strCoinNameErc20.length > 0 ) {
+ // ERC20 payment
+ log.information( "one S->M single ERC20 payment: {}", imaState.nAmountOfToken );
+ if( !imaState.chainProperties.mn.ethersProvider )
+ throw new Error( "No provider for MN" );
+ if( !imaState.chainProperties.sc.ethersProvider )
+ throw new Error( "No provider for SC" );
+ if( !imaState.joTokenManagerERC20 )
+ throw new Error( "No TokenManagerERC20 contract" );
+ if( !imaState.joMessageProxySChain )
+ throw new Error( "No MessageProxySChain contract" );
+ if( !imaState.joDepositBoxERC20 )
+ throw new Error( "No DepositBoxERC20 contract" );
+ return await imaToken.doErc20PaymentFromSChain(
+ imaState.chainProperties.mn.ethersProvider,
+ imaState.chainProperties.sc.ethersProvider,
+ imaState.chainProperties.mn.chainId.toString(),
+ imaState.chainProperties.sc.chainId.toString(),
+ imaState.chainProperties.sc.joAccount,
+ imaState.chainProperties.mn.joAccount,
+ imaState.joTokenManagerERC20, // only s-chain
+ imaState.joMessageProxySChain, // for checking logs
+ imaState.joDepositBoxERC20, // only main net
+ imaState.nAmountOfToken, // how ERC20 tokens money to send
+ imaState.nAmountOfWei, // how much to send
+ imaState.chainProperties.tc.strCoinNameErc20,
+ imaState.chainProperties.mn.joErc20,
+ imaState.chainProperties.sc.strCoinNameErc20,
+ imaState.chainProperties.sc.joErc20,
+ imaState.chainProperties.sc.transactionCustomizer
+ );
+ }
+ if(
+ imaState.chainProperties.sc.strCoinNameErc1155.length > 0 &&
+ imaState.idToken &&
+ imaState.idToken !== null &&
+ imaState.idToken !== undefined &&
+ imaState.nAmountOfToken &&
+ imaState.nAmountOfToken !== null &&
+ imaState.nAmountOfToken !== undefined &&
+ ( ( !imaState.idTokens ) ||
+ imaState.idTokens === null ||
+ imaState.idTokens === undefined ) &&
+ ( ( !imaState.arrAmountsOfTokens ) ||
+ imaState.arrAmountsOfTokens === null ||
+ imaState.arrAmountsOfTokens === undefined )
+ ) {
+ // ERC1155 payment
+ log.information( "one S->M single ERC1155 payment: {} {}",
+ imaState.idToken, imaState.nAmountOfToken );
+ if( !imaState.chainProperties.mn.ethersProvider )
+ throw new Error( "No provider for MN" );
+ if( !imaState.chainProperties.sc.ethersProvider )
+ throw new Error( "No provider for SC" );
+ if( !imaState.joTokenManagerERC1155 )
+ throw new Error( "No TokenManagerERC1155 contract" );
+ if( !imaState.joMessageProxySChain )
+ throw new Error( "No MessageProxySChain contract" );
+ if( !imaState.joDepositBoxERC1155 )
+ throw new Error( "No DepositBoxERC1155 contract" );
+ return await imaToken.doErc1155PaymentFromSChain(
+ imaState.chainProperties.mn.ethersProvider,
+ imaState.chainProperties.sc.ethersProvider,
+ imaState.chainProperties.mn.chainId.toString(),
+ imaState.chainProperties.sc.chainId.toString(),
+ imaState.chainProperties.sc.joAccount,
+ imaState.chainProperties.mn.joAccount,
+ imaState.joTokenManagerERC1155, // only s-chain
+ imaState.joMessageProxySChain, // for checking logs
+ imaState.joDepositBoxERC1155, // only main net
+ imaState.idToken, // which ERC1155 token id to send
+ imaState.nAmountOfToken, // which ERC1155 token amount to send
+ imaState.nAmountOfWei, // how much to send
+ imaState.chainProperties.mn.strCoinNameErc1155,
+ imaState.chainProperties.mn.joErc1155,
+ imaState.chainProperties.sc.strCoinNameErc1155,
+ imaState.chainProperties.sc.joErc1155,
+ imaState.chainProperties.sc.transactionCustomizer
+ );
+ }
+ if(
+ imaState.chainProperties.sc.strCoinNameErc1155.length > 0 &&
+ imaState.idTokens &&
+ imaState.idTokens !== null &&
+ imaState.idTokens !== undefined &&
+ imaState.arrAmountsOfTokens &&
+ imaState.arrAmountsOfTokens !== null &&
+ imaState.arrAmountsOfTokens !== undefined &&
+ ( !imaState.idToken ||
+ imaState.idToken === null ||
+ imaState.idToken === undefined ) &&
+ ( !imaState.nAmountOfToken ||
+ imaState.nAmountOfToken === null ||
+ imaState.nAmountOfToken === undefined )
+ ) {
+ // ERC1155 payment
+ log.information( "one S->M single ERC1155 payment: {} {}",
+ imaState.idTokens, imaState.arrAmountsOfTokens );
+ if( !imaState.chainProperties.mn.ethersProvider )
+ throw new Error( "No provider for MN" );
+ if( !imaState.chainProperties.sc.ethersProvider )
+ throw new Error( "No provider for SC" );
+ if( !imaState.joTokenManagerERC1155 )
+ throw new Error( "No TokenManagerERC1155 contract" );
+ if( !imaState.joMessageProxySChain )
+ throw new Error( "No MessageProxySChain contract" );
+ if( !imaState.joDepositBoxERC1155 )
+ throw new Error( "No DepositBoxERC1155 contract" );
+ return await imaToken.doErc1155BatchPaymentFromSChain(
+ imaState.chainProperties.mn.ethersProvider,
+ imaState.chainProperties.sc.ethersProvider,
+ imaState.chainProperties.mn.chainId.toString(),
+ imaState.chainProperties.sc.chainId.toString(),
+ imaState.chainProperties.sc.joAccount,
+ imaState.chainProperties.mn.joAccount,
+ imaState.joTokenManagerERC1155, // only s-chain
+ imaState.joMessageProxySChain, // for checking logs
+ imaState.joDepositBoxERC1155, // only main net
+ imaState.idTokens, // which ERC1155 token id to send
+ imaState.arrAmountsOfTokens, // which ERC1155 token amount to send
+ imaState.nAmountOfWei, // how much to send
+ imaState.chainProperties.mn.strCoinNameErc1155,
+ imaState.chainProperties.mn.joErc1155,
+ imaState.chainProperties.sc.strCoinNameErc1155,
+ imaState.chainProperties.sc.joErc1155,
+ imaState.chainProperties.sc.transactionCustomizer
+ );
+ }
+ // ETH payment
+ log.information( "one S->M single ETH payment: {}", imaState.nAmountOfWei );
+ if( !imaState.chainProperties.sc.ethersProvider )
+ throw new Error( "No provider for SC" );
+ if( !imaState.joTokenManagerETH )
+ throw new Error( "No TokenManagerETH contract" );
+ if( !imaState.joMessageProxySChain )
+ throw new Error( "No MessageProxySChain contract" );
+ return await imaEth.doEthPaymentFromSChain(
+ imaState.chainProperties.sc.ethersProvider,
+ imaState.chainProperties.sc.chainId.toString(),
+ imaState.chainProperties.sc.joAccount,
+ imaState.chainProperties.mn.joAccount,
+ imaState.joTokenManagerETH, // only s-chain
+ imaState.joMessageProxySChain, // for checking logs
+ imaState.nAmountOfWei, // how much WEI money to send
+ imaState.chainProperties.sc.transactionCustomizer
+ );
+ }
+ } );
+}
+
+export function commandLineTaskPaymentS2S(): void {
+ const imaState: state.TIMAState = state.get();
+ imaState.arrActions.push( {
+ name: "one S->S single payment",
+ fn: async function(): Promise < boolean > {
+ const isForward = imaHelperAPIs.isForwardS2S();
+ const sc = imaState.chainProperties.sc; const tc = imaState.chainProperties.tc;
+ const ethersProviderSrc: owaspUtils.ethersMod.ethers.providers.JsonRpcProvider | null =
+ isForward ? sc.ethersProvider : tc.ethersProvider;
+ const chainIdSrc = isForward ? sc.chainId : tc.chainId;
+ const joAccountSrc = isForward ? sc.joAccount : tc.joAccount;
+ const joTokenManagerERC20Src: owaspUtils.ethersMod.ethers.Contract | null = isForward
+ ? imaState.joTokenManagerERC20
+ : imaState.joTokenManagerERC20Target;
+ const joTokenManagerERC721Src: owaspUtils.ethersMod.ethers.Contract | null = isForward
+ ? ( imaState.isWithMetadata721
+ ? imaState.joTokenManagerERC721WithMetadata
+ : imaState.joTokenManagerERC721 )
+ : ( imaState.isWithMetadata721
+ ? imaState.joTokenManagerERC721WithMetadataTarget
+ : imaState.joTokenManagerERC721Target );
+
+ const joTokenManagerERC1155Src: owaspUtils.ethersMod.ethers.Contract | null = isForward
+ ? imaState.joTokenManagerERC1155
+ : imaState.joTokenManagerERC1155Target;
+ const strChainNameDst = isForward ? tc.strChainName : sc.strChainName;
+ const strCoinNameErc20Src = isForward ? sc.strCoinNameErc20 : tc.strCoinNameErc20;
+ const strCoinNameErc721Src = isForward ? sc.strCoinNameErc721 : tc.strCoinNameErc721;
+ const strCoinNameErc1155Src =
+ isForward ? sc.strCoinNameErc1155 : tc.strCoinNameErc1155;
+ const joSrcErc20 = isForward ? sc.joErc20 : tc.joErc20;
+ const joSrcErc721 = isForward ? sc.joErc721 : tc.joErc721;
+ const joSrcErc1155 = isForward ? sc.joErc1155 : tc.joErc1155;
+ let strAddrErc20Explicit = imaState.strAddrErc20Explicit;
+ let strAddrErc20ExplicitTarget = imaState.strAddrErc20ExplicitTarget;
+ let strAddrErc721Explicit = imaState.strAddrErc721Explicit;
+ let strAddrErc721ExplicitTarget = imaState.strAddrErc721ExplicitTarget;
+ let strAddrErc1155Explicit = imaState.strAddrErc1155Explicit;
+ let strAddrErc1155ExplicitTarget = imaState.strAddrErc1155ExplicitTarget;
+ if( ( !strAddrErc20Explicit ) && sc.joErc20 && sc.strCoinNameErc20 )
+ strAddrErc20Explicit = sc.joErc20[sc.strCoinNameErc20 + "_address"];
+ if( ( !strAddrErc20ExplicitTarget ) && tc.joErc20 && tc.strCoinNameErc20 )
+ strAddrErc20ExplicitTarget = tc.joErc20[tc.strCoinNameErc20 + "_address"];
+ if( ( !strAddrErc721Explicit ) && sc.joErc721 && sc.strCoinNameErc721 )
+ strAddrErc721Explicit = sc.joErc721[sc.strCoinNameErc721 + "_address"];
+ if( ( !strAddrErc721ExplicitTarget ) && tc.joErc721 && tc.strCoinNameErc721 )
+ strAddrErc721ExplicitTarget = tc.joErc721[tc.strCoinNameErc721 + "_address"];
+ if( ( !strAddrErc1155Explicit ) && sc.joErc1155 && sc.strCoinNameErc1155 )
+ strAddrErc1155Explicit = sc.joErc1155[sc.strCoinNameErc1155 + "_address"];
+ if( ( !strAddrErc1155ExplicitTarget ) && tc.joErc1155 && tc.strCoinNameErc1155 )
+ strAddrErc1155ExplicitTarget = tc.joErc1155[tc.strCoinNameErc1155 + "_address"];
+ const strAddrErc20Dst = isForward
+ ? strAddrErc20ExplicitTarget
+ : strAddrErc20Explicit;
+ const strAddrErc721Dst = isForward
+ ? strAddrErc721ExplicitTarget
+ : strAddrErc721Explicit;
+ const strAddrErc1155Dst = isForward
+ ? strAddrErc1155ExplicitTarget
+ : strAddrErc1155Explicit;
+ const txCustomizer: imaTx.TransactionCustomizer =
+ isForward ? sc.transactionCustomizer : tc.transactionCustomizer;
+ if( strCoinNameErc721Src.length > 0 ) {
+ // ERC721 payment
+ log.information( "one S->S single ERC721 payment: {}", imaState.idToken );
+ if( !ethersProviderSrc )
+ throw new Error( "No S2S source provider" );
+ if( !joTokenManagerERC721Src )
+ throw new Error( "No S2S source TokenManagerERC721 contract" );
+ return await imaToken.doErc721PaymentS2S(
+ isForward,
+ ethersProviderSrc,
+ chainIdSrc.toString(),
+ strChainNameDst,
+ joAccountSrc,
+ joTokenManagerERC721Src,
+ imaState.idToken, // which ERC721 token id to send
+ imaState.nAmountOfWei, // how much to send
+ strCoinNameErc721Src,
+ joSrcErc721,
+ strAddrErc721Dst, // only reverse payment needs it
+ txCustomizer
+ );
+ }
+ if( strCoinNameErc20Src.length > 0 ) {
+ // ERC20 payment
+ log.information( "one S->S single ERC20 payment: {}", imaState.nAmountOfToken );
+ if( !ethersProviderSrc )
+ throw new Error( "No S2S source provider" );
+ if( !joTokenManagerERC20Src )
+ throw new Error( "No S2S source TokenManagerERC20Src contract" );
+ return await imaToken.doErc20PaymentS2S(
+ isForward,
+ ethersProviderSrc,
+ chainIdSrc.toString(),
+ strChainNameDst,
+ joAccountSrc,
+ joTokenManagerERC20Src,
+ imaState.nAmountOfToken, // how much ERC20 tokens to send
+ imaState.nAmountOfWei, // how much to send
+ strCoinNameErc20Src,
+ joSrcErc20,
+ strAddrErc20Dst, // only reverse payment needs it
+ txCustomizer
+ );
+ }
+ if(
+ strCoinNameErc1155Src.length > 0 &&
+ imaState.idToken &&
+ imaState.idToken !== null &&
+ imaState.idToken !== undefined &&
+ imaState.nAmountOfToken &&
+ imaState.nAmountOfToken !== null &&
+ imaState.nAmountOfToken !== undefined &&
+ ( ( !imaState.idTokens ) ||
+ imaState.idTokens === null ||
+ imaState.idTokens === undefined ) &&
+ ( ( !imaState.arrAmountsOfTokens ) ||
+ imaState.arrAmountsOfTokens === null ||
+ imaState.arrAmountsOfTokens === undefined )
+ ) {
+ // ERC1155 payment
+ log.information( "one S->S single ERC1155 payment: {} {}",
+ imaState.idToken, imaState.nAmountOfToken );
+ if( !ethersProviderSrc )
+ throw new Error( "No S2S source provider" );
+ if( !joTokenManagerERC1155Src )
+ throw new Error( "No S2S source joTokenManagerERC1155 contract" );
+ return await imaToken.doErc1155PaymentS2S(
+ isForward,
+ ethersProviderSrc,
+ chainIdSrc.toString(),
+ strChainNameDst,
+ joAccountSrc,
+ joTokenManagerERC1155Src,
+ imaState.idToken, // which ERC1155 token id to send
+ imaState.nAmountOfToken, // how much ERC1155 tokens to send
+ imaState.nAmountOfWei, // how much to send
+ strCoinNameErc1155Src,
+ joSrcErc1155,
+ strAddrErc1155Dst, // only reverse payment needs it
+ txCustomizer
+ );
+ }
+ if(
+ strCoinNameErc1155Src.length > 0 &&
+ imaState.idTokens &&
+ imaState.idTokens !== null &&
+ imaState.idTokens !== undefined &&
+ imaState.arrAmountsOfTokens &&
+ imaState.arrAmountsOfTokens !== null &&
+ imaState.arrAmountsOfTokens !== undefined &&
+ ( !imaState.idToken ||
+ imaState.idToken === null ||
+ imaState.idToken === undefined ) &&
+ ( !imaState.nAmountOfToken ||
+ imaState.nAmountOfToken === null ||
+ imaState.nAmountOfToken === undefined )
+ ) {
+ // ERC1155 Batch payment
+ log.information( "one S->S single ERC1155 Batch payment: {} {}",
+ imaState.idTokens, imaState.arrAmountsOfTokens );
+ if( !ethersProviderSrc )
+ throw new Error( "No S2S source provider" );
+ if( !joTokenManagerERC1155Src )
+ throw new Error( "No S2S source joTokenManagerERC1155 contract" );
+ return await imaToken.doErc1155BatchPaymentS2S(
+ isForward,
+ ethersProviderSrc,
+ chainIdSrc.toString(),
+ strChainNameDst,
+ joAccountSrc,
+ joTokenManagerERC1155Src,
+ imaState.idTokens, // which ERC1155 token id to send
+ imaState.arrAmountsOfTokens, // which ERC1155 token amount to send
+ imaState.nAmountOfWei, // how much to send
+ strCoinNameErc1155Src,
+ joSrcErc1155,
+ strAddrErc1155Dst,
+ txCustomizer
+ );
+ }
+ // ETH payment
+ log.information( "one S->S single ETH payment: {}", imaState.nAmountOfWei );
+ log.fatal( "S->S ETH payment(s) are neither supported nor allowed" );
+ process.exit( 154 );
+ }
+ } );
+}
+
+export function commandLineTaskReceiveS2M(): void {
+ const imaState: state.TIMAState = state.get();
+ imaState.arrActions.push( {
+ name: "receive one S->M single ETH payment",
+ fn: async function(): Promise < boolean > {
+ log.information( "receive one S->M single ETH payment:" );
+ if( !imaState.chainProperties.mn.ethersProvider )
+ throw new Error( "No provider for MN" );
+ if( !imaState.joDepositBoxETH )
+ throw new Error( "No DepositBoxETH contract" );
+ return await imaEth.receiveEthPaymentFromSchainOnMainNet(
+ imaState.chainProperties.mn.ethersProvider,
+ imaState.chainProperties.mn.chainId.toString(),
+ imaState.chainProperties.mn.joAccount,
+ imaState.joDepositBoxETH,
+ imaState.chainProperties.mn.transactionCustomizer
+ );
+ }
+ } );
+}
+
+export function commandLineTaskViewS2M(): void {
+ const imaState: state.TIMAState = state.get();
+ imaState.arrActions.push( {
+ name: "view one S->M single ETH payment",
+ fn: async function(): Promise < boolean > {
+ log.information( "view one S->M single ETH payment:" );
+ log.information( "receive one S->M single ETH payment:" );
+ if( !imaState.chainProperties.mn.ethersProvider )
+ throw new Error( "No provider for MN" );
+ if( !imaState.joDepositBoxETH )
+ throw new Error( "No DepositBoxETH contract" );
+ const xWei = await imaEth.viewEthPaymentFromSchainOnMainNet(
+ imaState.chainProperties.mn.ethersProvider,
+ imaState.chainProperties.mn.joAccount,
+ imaState.joDepositBoxETH
+ );
+ if( xWei === null || xWei === undefined )
+ return false;
+ const xEth =
+ owaspUtils.ethersMod.ethers.utils.formatEther( owaspUtils.toBN( xWei ) );
+ log.success( "Main-net user can receive: {} wei = {} eth", xWei, xEth );
+ return true;
+ }
+ } );
+}
+
+export function commandLineTaskTransferM2S(): void {
+ const imaState: state.TIMAState = state.get();
+ imaState.arrActions.push( {
+ name: "single M->S transfer loop",
+ fn: async function(): Promise < boolean > {
+ if( !imaState.bNoWaitSChainStarted )
+ await discoveryTools.waitUntilSChainStarted(); // main-net --> s-chain transfer
+ const joRuntimeOpts: loop.TRuntimeOpts = {
+ isInsideWorker: false,
+ idxChainKnownForS2S: 0,
+ cntChainsKnownForS2S: 0
+ };
+ if( !imaState.chainProperties.mn.ethersProvider )
+ throw new Error( "No provider for MN" );
+ if( !imaState.chainProperties.sc.ethersProvider )
+ throw new Error( "No provider for SC" );
+ if( !imaState.joMessageProxyMainNet )
+ throw new Error( "No MessageProxyMainNet contract" );
+ if( !imaState.joMessageProxySChain )
+ throw new Error( "No MessageProxySChain contract" );
+ return await IMA.doTransfer( // main-net --> s-chain
+ "M2S",
+ joRuntimeOpts,
+ imaState.chainProperties.mn.ethersProvider,
+ imaState.joMessageProxyMainNet,
+ imaState.chainProperties.mn.joAccount,
+ imaState.chainProperties.sc.ethersProvider,
+ imaState.joMessageProxySChain,
+ imaState.chainProperties.sc.joAccount,
+ imaState.chainProperties.mn.strChainName,
+ imaState.chainProperties.sc.strChainName,
+ imaState.chainProperties.mn.chainId.toString(),
+ imaState.chainProperties.sc.chainId.toString(),
+ null,
+ imaState.joTokenManagerETH, // for logs validation on s-chain
+ imaState.nTransferBlockSizeM2S,
+ imaState.nTransferStepsM2S,
+ imaState.nMaxTransactionsM2S,
+ imaState.nBlockAwaitDepthM2S,
+ imaState.nBlockAgeM2S,
+ imaBLS.doSignMessagesM2S,
+ null,
+ imaState.chainProperties.sc.transactionCustomizer
+ );
+ }
+ } );
+}
+
+export function commandLineTaskTransferS2M(): void {
+ const imaState: state.TIMAState = state.get();
+ imaState.arrActions.push( {
+ name: "single S->M transfer loop",
+ fn: async function(): Promise < boolean > {
+ if( !imaState.bNoWaitSChainStarted )
+ await discoveryTools.waitUntilSChainStarted(); // s-chain --> main-net transfer
+ const joRuntimeOpts: loop.TRuntimeOpts = {
+ isInsideWorker: false,
+ idxChainKnownForS2S: 0,
+ cntChainsKnownForS2S: 0
+ };
+ if( !imaState.chainProperties.mn.ethersProvider )
+ throw new Error( "No provider for MN" );
+ if( !imaState.chainProperties.sc.ethersProvider )
+ throw new Error( "No provider for SC" );
+ if( !imaState.joMessageProxyMainNet )
+ throw new Error( "No MessageProxyMainNet contract" );
+ if( !imaState.joMessageProxySChain )
+ throw new Error( "No MessageProxySChain contract" );
+ return await IMA.doTransfer( // s-chain --> main-net
+ "S2M",
+ joRuntimeOpts,
+ imaState.chainProperties.sc.ethersProvider,
+ imaState.joMessageProxySChain,
+ imaState.chainProperties.sc.joAccount,
+ imaState.chainProperties.mn.ethersProvider,
+ imaState.joMessageProxyMainNet,
+ imaState.chainProperties.mn.joAccount,
+ imaState.chainProperties.sc.strChainName,
+ imaState.chainProperties.mn.strChainName,
+ imaState.chainProperties.sc.chainId.toString(),
+ imaState.chainProperties.mn.chainId.toString(),
+ imaState.joDepositBoxETH, // for logs validation on mainnet
+ null,
+ imaState.nTransferBlockSizeS2M,
+ imaState.nTransferStepsS2M,
+ imaState.nMaxTransactionsS2M,
+ imaState.nBlockAwaitDepthS2M,
+ imaState.nBlockAgeS2M,
+ imaBLS.doSignMessagesS2M,
+ null,
+ imaState.chainProperties.mn.transactionCustomizer
+ );
+ }
+ } );
+}
+
+export function commandLineTaskTransferS2S(): void {
+ const imaState: state.TIMAState = state.get();
+ imaState.arrActions.push( {
+ name: "single S->S transfer loop",
+ fn: async function(): Promise < boolean > {
+ if( !imaState.optsS2S.isEnabled )
+ return true;
+ if( !imaState.bNoWaitSChainStarted )
+ await discoveryTools.waitUntilSChainStarted(); // s-chain --> main-net transfer
+ const joRuntimeOpts: loop.TRuntimeOpts = {
+ isInsideWorker: false,
+ idxChainKnownForS2S: 0,
+ cntChainsKnownForS2S: 0
+ };
+ if( !imaState.chainProperties.sc.ethersProvider )
+ throw new Error( "No provider for SC" );
+ if( !imaState.joMessageProxySChain )
+ throw new Error( "No MessageProxySChain contract" );
+ if( !imaState.joTokenManagerETH )
+ throw new Error( "No TokenManagerETH contract" );
+ return await IMA.doAllS2S( // s-chain --> s-chain
+ joRuntimeOpts,
+ imaState,
+ skaleObserver,
+ imaState.chainProperties.sc.ethersProvider,
+ imaState.joMessageProxySChain,
+ imaState.chainProperties.sc.joAccount,
+ imaState.chainProperties.sc.strChainName,
+ imaState.chainProperties.sc.chainId.toString(),
+ imaState.joTokenManagerETH, // for logs validation on s-chain
+ imaState.nTransferBlockSizeM2S,
+ imaState.nTransferStepsS2S,
+ imaState.nMaxTransactionsM2S,
+ imaState.nBlockAwaitDepthM2S,
+ imaState.nBlockAgeM2S,
+ imaBLS.doSignMessagesM2S,
+ imaState.chainProperties.sc.transactionCustomizer
+ );
+ }
+ } );
+}
+
+export function commandLineTaskTransfer(): void {
+ const imaState: state.TIMAState = state.get();
+ imaState.arrActions.push( {
+ name: "Single M<->S transfer loop iteration",
+ fn: async function(): Promise < boolean > {
+ if( !imaState.bNoWaitSChainStarted )
+ await discoveryTools.waitUntilSChainStarted();
+ const joRuntimeOpts: loop.TRuntimeOpts = {
+ isInsideWorker: false,
+ idxChainKnownForS2S: 0,
+ cntChainsKnownForS2S: 0
+ };
+ const optsLoop: loop.TLoopOptions = {
+ joRuntimeOpts,
+ isDelayFirstRun: false,
+ enableStepOracle: true,
+ enableStepM2S: true,
+ enableStepS2M: true,
+ enableStepS2S: true
+ };
+ return await loop.singleTransferLoop( optsLoop );
+ }
+ } );
+}
+
+export function commandLineTaskLoop(): void {
+ const imaState: state.TIMAState = state.get();
+ imaState.arrActions.push( {
+ name: "M<->S and S->S transfer loop, startup in parallel mode",
+ fn: async function(): Promise < boolean > {
+ state.setPreventExitAfterLastAction( true );
+ if( !imaState.bNoWaitSChainStarted )
+ await discoveryTools.waitUntilSChainStarted(); // M<->S transfer loop
+ let isPrintSummaryRegistrationCosts = false;
+ if( !await checkRegistrationStep1() ) {
+ if( !await registerStep1( false ) )
+ return false;
+ isPrintSummaryRegistrationCosts = true;
+ }
+ if( isPrintSummaryRegistrationCosts )
+ printSummaryRegistrationCosts();
+ const opts: loop.TParallelLoopRunOptions = {
+ imaState,
+ details: log.globalStream()
+ };
+ return await loop.runParallelLoops( opts );
+ }
+ } );
+}
+
+export function commandLineTaskLoopSimple(): void {
+ const imaState: state.TIMAState = state.get();
+ imaState.arrActions.push( {
+ name: "M<->S and S->S transfer loop, startup simple mode",
+ fn: async function(): Promise < boolean > {
+ state.setPreventExitAfterLastAction( true );
+ if( !imaState.bNoWaitSChainStarted )
+ await discoveryTools.waitUntilSChainStarted(); // M<->S transfer loop
+ let isPrintSummaryRegistrationCosts = false;
+ if( !await checkRegistrationStep1() ) {
+ if( !await registerStep1( false ) )
+ return false;
+ isPrintSummaryRegistrationCosts = true;
+ }
+ if( isPrintSummaryRegistrationCosts )
+ printSummaryRegistrationCosts();
+ const joRuntimeOpts: loop.TRuntimeOpts = {
+ isInsideWorker: false,
+ idxChainKnownForS2S: 0,
+ cntChainsKnownForS2S: 0
+ };
+ const optsLoop: loop.TLoopOptions = {
+ joRuntimeOpts,
+ isDelayFirstRun: false,
+ enableStepOracle: true,
+ enableStepM2S: true,
+ enableStepS2M: true,
+ enableStepS2S: true
+ };
+ return await loop.runTransferLoop( optsLoop );
+ }
+ } );
+}
+
+async function handleBrowseSkaleModesRpcInfoResult(
+ strLogPrefix: string, joCall: rpcCall.TRPCCall, joIn: any, joOut: any
+): Promise {
+ const imaState: state.TIMAState = state.get();
+ log.information( "{p}S-Chain network information: {}",
+ strLogPrefix, joOut.result );
+ let nCountReceivedImaDescriptions = 0;
+ const jarrNodes = joOut.result.network;
+ for( let i = 0; i < jarrNodes.length; ++i ) {
+ const joNode = jarrNodes[i];
+ if( !joNode ) {
+ log.critical( "{p}Discovery node {} is completely unknown and will be skipped",
+ strLogPrefix, i );
+ continue;
+ }
+ const strNodeURL = imaUtils.composeSChainNodeUrl( joNode );
+ const rpcCallOpts: rpcCall.TRPCCallOpts | null = null;
+ let joCall: rpcCall.TRPCCall | null = null;
+ try {
+ joCall = await rpcCall.create( strNodeURL, rpcCallOpts );
+ if( !joCall )
+ throw new Error( `Failed to create JSON RPC call object to ${strNodeURL}` );
+ const jIn: any = { method: "skale_imaInfo", params: { } };
+ if( discoveryTools.isSendImaAgentIndex() )
+ jIn.params.fromImaAgentIndex = imaState.nNodeNumber;
+ const joOut = await joCall.call( joIn );
+ ++nCountReceivedImaDescriptions;
+ log.information( "{p}Node {} IMA information: {}",
+ strLogPrefix, joNode.nodeID, joOut.result );
+ await joCall.disconnect();
+ } catch ( err ) {
+ log.fatal( "JSON RPC call to S-Chain failed, error: {err}", err );
+ if( joCall )
+ await joCall.disconnect();
+ process.exit( 159 );
+ }
+ }
+ const iv = setInterval( function(): void {
+ if( nCountReceivedImaDescriptions == jarrNodes.length ) {
+ clearInterval( iv );
+ process.exit( 0 );
+ }
+ }, 100 );
+ await joCall.disconnect();
+}
+
+export function commandLineTaskBrowseSChain(): void {
+ const imaState: state.TIMAState = state.get();
+ imaState.bIsNeededCommonInit = false;
+ imaState.arrActions.push( {
+ name: "Browse S-Chain network",
+ fn: async function(): Promise < boolean > {
+ const strLogPrefix = "S-Chain Browse: ";
+ if( imaState.chainProperties.sc.strURL.length === 0 ) {
+ log.fatal( "Missing S-Chain URL, please specify {}", "--url-s-chain" );
+ process.exit( 155 );
+ }
+ log.information( "{p}Downloading S-Chain network information...", strLogPrefix );
+ const rpcCallOpts: rpcCall.TRPCCallOpts | null = null;
+ let joCall: rpcCall.TRPCCall | null = null;
+ try {
+ joCall = await rpcCall.create( imaState.chainProperties.sc.strURL, rpcCallOpts );
+ if( !joCall ) {
+ throw new Error( "Failed to create JSON RPC call object " +
+ `to ${imaState.chainProperties.sc.strURL}` );
+ }
+ const joIn: any = { method: "skale_nodesRpcInfo", params: { } };
+ if( discoveryTools.isSendImaAgentIndex() )
+ joIn.params.fromImaAgentIndex = imaState.nNodeNumber;
+ const joOut = await joCall.call( joIn );
+ await handleBrowseSkaleModesRpcInfoResult( strLogPrefix, joCall, joIn, joOut );
+ } catch ( err ) {
+ log.fatal( "JSON RPC call to S-Chain failed, error: {err}", err );
+ if( joCall )
+ await joCall.disconnect();
+ process.exit( 159 );
+ }
+ return true;
+ }
+ } );
+}
+
+export function commandLineTaskReimbursementShowBalance(): void {
+ const imaState: state.TIMAState = state.get();
+ imaState.arrActions.push( {
+ name: "Gas Reimbursement - Show Balance",
+ fn: async function(): Promise < boolean > {
+ if( !imaState.chainProperties.mn.ethersProvider )
+ throw new Error( "No provider for MN" );
+ if( !imaState.joCommunityPool )
+ throw new Error( "No CommunityPool contract" );
+ await imaReimbursement.reimbursementShowBalance(
+ imaState.chainProperties.mn.ethersProvider,
+ imaState.joCommunityPool,
+ imaState.chainProperties.mn.joAccount.address(),
+ imaState.chainProperties.mn.strChainName,
+ imaState.chainProperties.mn.chainId.toString(),
+ imaState.chainProperties.mn.transactionCustomizer,
+ imaState.strReimbursementChain,
+ true
+ );
+ return true;
+ }
+ } );
+}
+
+export function commandLineTaskReimbursementEstimateAmount(): void {
+ const imaState: state.TIMAState = state.get();
+ imaState.arrActions.push( {
+ name: "Gas Reimbursement - Estimate Amount",
+ fn: async function(): Promise < boolean > {
+ if( !imaState.chainProperties.mn.ethersProvider )
+ throw new Error( "No provider for MN" );
+ if( !imaState.joCommunityPool )
+ throw new Error( "No CommunityPool contract" );
+ await imaReimbursement.reimbursementEstimateAmount(
+ imaState.chainProperties.mn.ethersProvider,
+ imaState.joCommunityPool,
+ imaState.chainProperties.mn.joAccount.address(),
+ imaState.chainProperties.mn.strChainName,
+ imaState.chainProperties.mn.chainId.toString(),
+ imaState.chainProperties.mn.transactionCustomizer,
+ imaState.strReimbursementChain,
+ true
+ );
+ return true;
+ }
+ } );
+}
+
+export function commandLineTaskReimbursementRecharge(): void {
+ const imaState: state.TIMAState = state.get();
+ imaState.arrActions.push( {
+ name: "Gas Reimbursement - Recharge User Wallet",
+ fn: async function(): Promise < boolean > {
+ if( !imaState.chainProperties.mn.ethersProvider )
+ throw new Error( "No provider for MN" );
+ if( !imaState.joCommunityPool )
+ throw new Error( "No CommunityPool contract" );
+ await imaReimbursement.reimbursementWalletRecharge(
+ imaState.chainProperties.mn.ethersProvider,
+ imaState.joCommunityPool,
+ imaState.chainProperties.mn.joAccount,
+ imaState.chainProperties.mn.strChainName,
+ imaState.chainProperties.mn.chainId.toString(),
+ imaState.chainProperties.mn.transactionCustomizer,
+ imaState.strReimbursementChain,
+ imaState.nReimbursementRecharge
+ );
+ return true;
+ }
+ } );
+}
+
+export function commandLineTaskReimbursementWithdraw(): void {
+ const imaState: state.TIMAState = state.get();
+ imaState.arrActions.push( {
+ name: "Gas Reimbursement - Withdraw User Wallet",
+ fn: async function(): Promise < boolean > {
+ if( !imaState.chainProperties.mn.ethersProvider )
+ throw new Error( "No provider for MN" );
+ if( !imaState.joCommunityPool )
+ throw new Error( "No CommunityPool contract" );
+ await imaReimbursement.reimbursementWalletWithdraw(
+ imaState.chainProperties.mn.ethersProvider,
+ imaState.joCommunityPool,
+ imaState.chainProperties.mn.joAccount,
+ imaState.chainProperties.mn.strChainName,
+ imaState.chainProperties.mn.chainId.toString(),
+ imaState.chainProperties.mn.transactionCustomizer,
+ imaState.strReimbursementChain,
+ imaState.nReimbursementWithdraw
+ );
+ return true;
+ }
+ } );
+}
+
+export function commandLineTaskReimbursementSetRange(): void {
+ const imaState: state.TIMAState = state.get();
+ imaState.arrActions.push( {
+ name: "Gas Reimbursement - Set Minimal time interval from S2M and S2S transfers",
+ fn: async function(): Promise < boolean > {
+ if( !imaState.chainProperties.sc.ethersProvider )
+ throw new Error( "No provider for SC" );
+ if( !imaState.joCommunityLocker )
+ throw new Error( "No CommunityLocker contract" );
+ await imaReimbursement.reimbursementSetRange(
+ imaState.chainProperties.sc.ethersProvider,
+ imaState.joCommunityLocker,
+ imaState.chainProperties.sc.joAccount,
+ imaState.chainProperties.sc.strChainName,
+ imaState.chainProperties.sc.chainId.toString(),
+ imaState.chainProperties.sc.transactionCustomizer,
+ imaState.strChainNameOriginChain,
+ imaState.nReimbursementRange
+ );
+ return true;
+ }
+ } );
+}
diff --git a/src/discoveryTools.ts b/src/discoveryTools.ts
new file mode 100644
index 00000000..a37cfcae
--- /dev/null
+++ b/src/discoveryTools.ts
@@ -0,0 +1,782 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+
+/**
+ * @license
+ * SKALE IMA
+ *
+ * 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 .
+ */
+
+/**
+ * @file discoveryTools.ts
+ * @copyright SKALE Labs 2019-Present
+ */
+
+import * as log from "./log.js";
+import * as rpcCall from "./rpcCall.js";
+import * as state from "./state.js";
+import * as imaUtils from "./utils.js";
+import * as threadInfo from "./threadInfo.js";
+import * as owaspUtils from "./owaspUtils.js";
+
+export type TFunctionAfterDiscovery = (
+ err?: Error | string | null,
+ joSChainNetworkInfo?: TSChainNetworkInfo | null
+) => void;
+
+export type TFunctionAfterRediscovery = (
+ status: boolean
+) => void;
+
+export interface TBLSPublicKey {
+ BLSPublicKey0: string
+ BLSPublicKey1: string
+ BLSPublicKey2: string
+ BLSPublicKey3: string
+}
+
+export interface TBLSCommonPublicKey {
+ commonBLSPublicKey0: string
+ commonBLSPublicKey1: string
+ commonBLSPublicKey2: string
+ commonBLSPublicKey3: string
+}
+
+export interface TNodeImaInfo extends TBLSPublicKey, TBLSCommonPublicKey {
+ n: number
+ t: number
+ thisNodeIndex: number
+}
+
+export interface TPWAStateItem {
+ isInProgress: boolean
+ ts: number
+}
+
+export interface TPWAState {
+ oracle: TPWAStateItem
+ m2s: TPWAStateItem
+ s2m: TPWAStateItem
+ s2s: { mapS2S: any }
+}
+
+export interface TSChainNode {
+ httpRpcPort?: number
+ httpRpcPort6?: number
+ httpsRpcPort?: number
+ httpsRpcPort6?: number
+ imaAgentRpcPort?: number
+ ip?: string
+ ip6?: string
+ nodeID: number
+ schainIndex: number
+ wsRpcPort?: number
+ wsRpcPort6?: number
+ wssRpcPort?: number
+ wssRpcPort6?: number
+ imaInfo: TNodeImaInfo
+ pwaState?: TPWAState
+}
+
+export interface TThisNodeProperties {
+ acceptors: number
+ bindIP: string
+ bindIP6: string
+ "enable-admin-apis"?: boolean
+ "enable-debug-behavior-apis"?: boolean
+ "enable-performance-tracker-apis"?: boolean
+ "enable-personal-apis"?: true
+ httpRpcPort?: number
+ httpRpcPort6?: number
+ httpsRpcPort?: number
+ httpsRpcPort6?: number
+ nodeID: number
+ schainName: string
+ thisNodeIndex: number
+ "unsafe-transactions"?: boolean
+ wsRpcPort?: number
+ wsRpcPort6?: number
+ wssRpcPort?: number
+ wssRpcPort6?: number
+}
+
+export interface TSChainNetworkInfo {
+ network: TSChainNode[]
+ node: TThisNodeProperties
+ schainID: number
+}
+
+export interface TDiscoveryOptions {
+ fnAfter: TFunctionAfterDiscovery | null
+ isSilentReDiscovery: boolean
+ joPrevSChainNetworkInfo: TSChainNetworkInfo | null
+ nCountToWait: number
+ imaState: state.TIMAState
+ strLogPrefix: string
+ joSChainNetworkInfo: TSChainNetworkInfo | null
+ jarrNodes: TSChainNode[]
+ cntNodes: number
+ cntFailed: number
+ nCountReceivedImaDescriptions: number
+ nCountAvailable: number
+}
+
+export function formatBalanceInfo( bi: any, strAddress: string ): string {
+ let s = "";
+ s += log.fmtInformation( "{p}", bi.assetName );
+ if( "assetAddress" in bi &&
+ typeof bi.assetAddress === "string" && bi.assetAddress.length > 0 )
+ s += log.fmtDebug( "/{}", bi.assetAddress );
+ if( "idToken" in bi )
+ s += log.fmtDebug( " token ID {}", bi.idToken );
+ s += log.posNeg( ( bi.assetName == "ERC721" ), " owner is ", " balance is " );
+ s += ( bi.assetName == "ERC721" )
+ ? log.fmtInformation( "{p}", bi.owner )
+ : log.fmtInformation( "{p}", bi.balance );
+ if( bi.assetName == "ERC721" ) {
+ const isSame =
+ ( bi.owner.trim().toLowerCase() == strAddress.trim().toLowerCase() );
+ s += " " + ( isSame
+ ? log.fmtSuccess( "same (as account {} specified in the command line arguments)",
+ strAddress )
+ : log.fmtError( "different (than account {} specified in the command line arguments)",
+ strAddress )
+ );
+ }
+ return s;
+}
+
+function getSChainNodesCount( joSChainNetworkInfo: TSChainNetworkInfo ): number {
+ try {
+ if( !joSChainNetworkInfo )
+ return 0;
+ const jarrNodes = joSChainNetworkInfo.network;
+ const cntNodes = jarrNodes.length;
+ return cntNodes;
+ } catch ( err ) {
+ return 0;
+ }
+}
+
+export function isSChainNodeFullyDiscovered( joNode: TSChainNode ): boolean {
+ if( !joNode )
+ return false;
+ if( joNode && "imaInfo" in joNode && joNode?.imaInfo &&
+ "t" in joNode.imaInfo && joNode.imaInfo?.t && joNode.imaInfo.t > 0 &&
+ "n" in joNode.imaInfo && joNode.imaInfo?.n && joNode.imaInfo.n > 0 &&
+ "BLSPublicKey0" in joNode.imaInfo && joNode.imaInfo?.BLSPublicKey0 &&
+ joNode.imaInfo.BLSPublicKey0.length > 0 &&
+ "BLSPublicKey1" in joNode.imaInfo && joNode.imaInfo?.BLSPublicKey1 &&
+ joNode.imaInfo.BLSPublicKey1.length > 0 &&
+ "BLSPublicKey2" in joNode.imaInfo && joNode.imaInfo?.BLSPublicKey2 &&
+ joNode.imaInfo.BLSPublicKey2.length > 0 &&
+ "BLSPublicKey3" in joNode.imaInfo && joNode.imaInfo?.BLSPublicKey3 &&
+ joNode.imaInfo.BLSPublicKey3.length > 0 &&
+ "commonBLSPublicKey0" in joNode.imaInfo && joNode.imaInfo?.commonBLSPublicKey0 &&
+ joNode.imaInfo.commonBLSPublicKey0.length > 0 &&
+ "commonBLSPublicKey1" in joNode.imaInfo && joNode.imaInfo?.commonBLSPublicKey1 &&
+ joNode.imaInfo.commonBLSPublicKey1.length > 0 &&
+ "commonBLSPublicKey2" in joNode.imaInfo && joNode.imaInfo?.commonBLSPublicKey2 &&
+ joNode.imaInfo.commonBLSPublicKey2.length > 0 &&
+ "commonBLSPublicKey3" in joNode.imaInfo && joNode.imaInfo?.commonBLSPublicKey3 &&
+ joNode.imaInfo.commonBLSPublicKey3.length > 0
+ )
+ return true;
+ return false;
+}
+
+export function getSChainDiscoveredNodesCount(
+ joSChainNetworkInfo: TSChainNetworkInfo | null ): number {
+ try {
+ if( !joSChainNetworkInfo )
+ return 0;
+ if( !( "network" in joSChainNetworkInfo && joSChainNetworkInfo.network ) )
+ return 0;
+ const jarrNodes = joSChainNetworkInfo.network;
+ const cntNodes = jarrNodes.length;
+ if( cntNodes <= 0 )
+ return 0;
+ let cntDiscovered = 0;
+ for( let i = 0; i < cntNodes; ++i ) {
+ try {
+ const joNode = joSChainNetworkInfo.network[i];
+ if( isSChainNodeFullyDiscovered( joNode ) )
+ ++cntDiscovered;
+ } catch ( err ) {
+ return 0;
+ }
+ }
+ return cntDiscovered;
+ } catch ( err ) {
+ return 0;
+ }
+}
+
+export async function waitUntilSChainStarted(): Promise {
+ const imaState: state.TIMAState = state.get();
+ log.debug( "Checking S-Chain is accessible and sane..." );
+ if( ( !imaState.chainProperties.sc.strURL ) ||
+ imaState.chainProperties.sc.strURL.length === 0
+ ) {
+ log.warning( "Skipped, S-Chain URL was not provided." );
+ return;
+ }
+ let bSuccess = false;
+ let idxWaitAttempt = 0;
+ const isSilentReDiscovery = true; // it must be silent during S-Chain sanity check
+ for( ; !bSuccess; ) {
+ try {
+ log.attention( "This S-Chain discovery will be done for startup pre-requisite" );
+ const nCountToWait = -1;
+ let isError = false;
+ const joSChainNetworkInfo = await discoverSChainNetwork(
+ null, isSilentReDiscovery, null, nCountToWait
+ ).catch( function( err: Error | string ): void {
+ log.critical( "S-Chain network discovery attempt failed: {err}", err );
+ isError = true;
+ } );
+ if( ( !isError ) && joSChainNetworkInfo && typeof joSChainNetworkInfo === "object" ) {
+ imaState.joSChainNetworkInfo = joSChainNetworkInfo;
+ bSuccess = true;
+ }
+ } catch ( err ) {
+ bSuccess = false;
+ }
+ if( !bSuccess )
+ ++idxWaitAttempt;
+ if( idxWaitAttempt >= imaState.nMaxWaitSChainAttempts ) {
+ log.warning( "Incomplete, S-Chain sanity check failed after {} attempts.",
+ idxWaitAttempt );
+ return;
+ }
+ await threadInfo.sleep( 1000 );
+ }
+ log.success( "Done, S-Chain is accessible and sane." );
+}
+
+export function isSendImaAgentIndex(): boolean {
+ return true;
+}
+
+let gTimerSChainDiscovery: any = null;
+let gFlagIsInSChainDiscovery: boolean = false;
+
+function composeStillUnknownNodesMessage(
+ joSChainNetworkInfo: TSChainNetworkInfo,
+ cntStillUnknown: number, cntNodesOnChain: number ): string {
+ let strMessage = log.fmtSuccess( ", {} of {} still unknown (",
+ cntStillUnknown, cntNodesOnChain );
+ try {
+ const jarrNodes = joSChainNetworkInfo.network;
+ let cntBad = 0;
+ for( let i = 0; i < jarrNodes.length; ++i ) {
+ const joNode = jarrNodes[i];
+ try {
+ if( isSChainNodeFullyDiscovered( joNode ) )
+ continue;
+ if( cntBad > 0 )
+ strMessage += log.fmtSuccess( ", " );
+ const strNodeURL = imaUtils.composeSChainNodeUrl( joNode );
+ const strNodeDescColorized = log.fmtAttention( "#{}({url})", i, strNodeURL );
+ strMessage += strNodeDescColorized;
+ ++cntBad;
+ } catch ( err ) { }
+ }
+ } catch ( err ) { }
+ strMessage += log.fmtSuccess( ")" );
+ return strMessage;
+}
+
+async function handlePeriodicDiscoveryAttemptActions(
+ isSilentReDiscovery: boolean, fnAfter: TFunctionAfterDiscovery | null
+): Promise {
+ if( gFlagIsInSChainDiscovery ) {
+ log.information( "Notice: long this S-Chain re-discovery is in progress now..." );
+ return;
+ }
+ const imaState: state.TIMAState = state.get();
+ if( !imaState.joSChainNetworkInfo )
+ throw new Error( "No own S-Chain network information" );
+ fnAfter = fnAfter ?? function(): void {};
+ gFlagIsInSChainDiscovery = true;
+ const cntNodesOnChain = getSChainNodesCount( imaState.joSChainNetworkInfo );
+ try {
+ let nCountToWait = ( cntNodesOnChain > 2 )
+ ? Math.ceil( cntNodesOnChain * 2 / 3 + 1 )
+ : cntNodesOnChain;
+ if( nCountToWait > cntNodesOnChain )
+ nCountToWait = cntNodesOnChain;
+ const cntDiscovered = getSChainDiscoveredNodesCount( imaState.joSChainNetworkInfo );
+ if( cntDiscovered >= cntNodesOnChain ) {
+ if( !isSilentReDiscovery ) {
+ log.information( "Everything is discovered about this S-Chain. ",
+ "No re-discovery is needed" );
+ }
+ if( gTimerSChainDiscovery != null ) {
+ clearInterval( gTimerSChainDiscovery );
+ gTimerSChainDiscovery = null;
+ if( !isSilentReDiscovery )
+ log.information( "This S-Chain re-discovery stopped" );
+ }
+ // fnAfter() will be called here inside async call at beginning
+ gFlagIsInSChainDiscovery = false;
+ return;
+ }
+ if( cntDiscovered < cntNodesOnChain ) {
+ if( !isSilentReDiscovery ) {
+ const cntUnDiscoveredYet = cntNodesOnChain - cntDiscovered;
+ log.information( "Have {} of {} nodes of this S-Chain not discovered yet " +
+ "on re-discovery step.", cntUnDiscoveredYet, cntNodesOnChain );
+ }
+ }
+ if( !isSilentReDiscovery ) {
+ log.information( "This S-Chain discovery will be done for re-discover task" );
+ log.information( "Will re-discover {}-nodes S-Chain network, {} node(s) already " +
+ "discovered...", nCountToWait, cntDiscovered );
+ }
+ let isError = false;
+ const joSChainNetworkInfo: TSChainNetworkInfo = await discoverSChainNetwork(
+ null, isSilentReDiscovery, imaState.joSChainNetworkInfo, nCountToWait
+ ).catch( function( err: Error | string ): void {
+ isError = true;
+ log.critical( "S-Chain network re-discovery failed: {err}", err );
+ } ) as TSChainNetworkInfo;
+ if( !isError ) {
+ const cntDiscoveredNow = getSChainDiscoveredNodesCount( joSChainNetworkInfo );
+ let strMessage =
+ log.fmtSuccess( "S-Chain network was re-discovered, {} of {} node(s)" +
+ "({} nodes known)", cntDiscoveredNow, nCountToWait, cntDiscoveredNow );
+ const cntStillUnknown = cntNodesOnChain - cntDiscoveredNow;
+ if( cntStillUnknown > 0 ) {
+ strMessage += composeStillUnknownNodesMessage(
+ joSChainNetworkInfo, cntStillUnknown, cntNodesOnChain );
+ }
+ if( !isSilentReDiscovery ) {
+ strMessage += log.fmtSuccess( ", complete re-discovered S-Chain " +
+ "network info: {}", joSChainNetworkInfo );
+ }
+ log.information( strMessage );
+ imaState.joSChainNetworkInfo = joSChainNetworkInfo;
+ }
+ fnAfter();
+ continueSChainDiscoveryInBackgroundIfNeeded( isSilentReDiscovery, null )
+ .then( function(): void {} ).catch( function( err ): void {
+ log.error(
+ "Failed to continue S-chain discovery, reported error is: {err}", err );
+ } );
+ } catch ( err ) { }
+ gFlagIsInSChainDiscovery = false;
+ // fnAfter() will be called here inside async call at beginning
+ continueSChainDiscoveryInBackgroundIfNeeded( isSilentReDiscovery, fnAfter )
+ .then( function(): void {} ).catch( function( err ): void {
+ log.error(
+ "Failed to continue S-chain discovery, reported error is: {err}", err );
+ } );
+}
+
+export async function continueSChainDiscoveryInBackgroundIfNeeded(
+ isSilentReDiscovery: boolean, fnAfter: TFunctionAfterDiscovery | null
+): Promise {
+ if( gTimerSChainDiscovery != null )
+ return;
+ fnAfter = fnAfter ?? function(): void {};
+ const imaState: state.TIMAState = state.get();
+ if( imaState.joSChainDiscovery.repeatIntervalMilliseconds <= 0 ) {
+ if( !isSilentReDiscovery )
+ log.information( "This S-Chain re-discovery will not be preformed" );
+ fnAfter();
+ return; // no S-Chain re-discovery, special mode
+ }
+ if( !imaState.joSChainNetworkInfo )
+ throw new Error( "No own S-Chain network information" );
+ const cntNodesOnChain = getSChainNodesCount( imaState.joSChainNetworkInfo );
+ let nCountToWait = ( cntNodesOnChain > 2 )
+ ? Math.ceil( cntNodesOnChain * 2 / 3 + 1 )
+ : cntNodesOnChain;
+ if( nCountToWait > cntNodesOnChain )
+ nCountToWait = cntNodesOnChain;
+ const cntDiscovered = getSChainDiscoveredNodesCount( imaState.joSChainNetworkInfo );
+ if( cntDiscovered >= cntNodesOnChain ) {
+ if( !isSilentReDiscovery ) {
+ log.attention( "Everything is discovered about this S-Chain. " +
+ "No re-discovery is needed" );
+ }
+ if( gTimerSChainDiscovery != null ) {
+ clearInterval( gTimerSChainDiscovery );
+ gTimerSChainDiscovery = null;
+ if( !isSilentReDiscovery )
+ log.notice( "This S-Chain re-discovery stopped" );
+ }
+ fnAfter();
+ return;
+ }
+ if( cntDiscovered < cntNodesOnChain ) {
+ if( !isSilentReDiscovery ) {
+ const cntUnDiscoveredYet = cntNodesOnChain - cntDiscovered;
+ log.information( "Have {} of {} nodes of this S-Chain not discovered yet before " +
+ "continuing re-discovery.", cntUnDiscoveredYet, cntNodesOnChain );
+ }
+ }
+ gTimerSChainDiscovery = setInterval( function(): void {
+ handlePeriodicDiscoveryAttemptActions( isSilentReDiscovery, fnAfter )
+ .then( function(): void {} ).catch( function(): void {} );
+ }, imaState.joSChainDiscovery.periodicDiscoveryInterval );
+}
+
+function handleDiscoverSkaleImaInfoResult(
+ optsDiscover: TDiscoveryOptions, strNodeDescColorized: string,
+ joNode: any, joCall: rpcCall.TRPCCall, joIn: any, joOut: any
+): void {
+ joNode.imaInfo = joOut.result;
+ if( isSChainNodeFullyDiscovered( joNode ) )
+ ++optsDiscover.nCountReceivedImaDescriptions;
+ if( !optsDiscover.isSilentReDiscovery ) {
+ log.success( "{p}OK, got {} node {} IMA information({} of {}).",
+ optsDiscover.strLogPrefix, strNodeDescColorized, joNode.nodeID,
+ optsDiscover.nCountReceivedImaDescriptions, optsDiscover.cntNodes );
+ }
+}
+
+async function discoverSChainWalkNodes( optsDiscover: TDiscoveryOptions ): Promise {
+ optsDiscover.cntFailed = 0;
+ for( let i = 0; i < optsDiscover.cntNodes; ++i ) {
+ const nCurrentNodeIdx = owaspUtils.toInteger( i );
+ const joNode = optsDiscover.jarrNodes[nCurrentNodeIdx];
+ const strNodeURL = imaUtils.composeSChainNodeUrl( joNode );
+ const strNodeDescColorized = log.fmtAttention( "#{}({url})", nCurrentNodeIdx, strNodeURL );
+ if( !optsDiscover.isSilentReDiscovery ) {
+ log.information( "{p}Will try to discover S-Chain node {}...",
+ optsDiscover.strLogPrefix, strNodeDescColorized );
+ }
+ try {
+ if( optsDiscover.joPrevSChainNetworkInfo &&
+ "network" in optsDiscover.joPrevSChainNetworkInfo &&
+ optsDiscover.joPrevSChainNetworkInfo.network ) {
+ const joPrevNode =
+ optsDiscover.joPrevSChainNetworkInfo.network[nCurrentNodeIdx];
+ if( isSChainNodeFullyDiscovered( joPrevNode ) ) {
+ joNode.imaInfo = JSON.parse( JSON.stringify( joPrevNode.imaInfo ) );
+ if( !optsDiscover.isSilentReDiscovery ) {
+ log.information(
+ "{p}OK, in case of {} node {} will use previous discovery result.",
+ optsDiscover.strLogPrefix, strNodeDescColorized, joNode.nodeID );
+ }
+ continue; // skip this node discovery, enrich rest of nodes
+ }
+ }
+ } catch ( err ) { }
+ const rpcCallOpts: rpcCall.TRPCCallOpts | null = null;
+ let joCall: rpcCall.TRPCCall | null = null;
+ try {
+ joCall = await rpcCall.create( strNodeURL, rpcCallOpts );
+ if( !joCall )
+ throw new Error( `Failed to create JSON RPC call object to ${strNodeURL}` );
+ const joIn: any = { method: "skale_imaInfo", params: { } };
+ if( isSendImaAgentIndex() )
+ joIn.params.fromImaAgentIndex = optsDiscover.imaState.nNodeNumber;
+ const joOut = await joCall.call( joIn );
+ handleDiscoverSkaleImaInfoResult(
+ optsDiscover, strNodeDescColorized, joNode, joCall, joIn, joOut );
+ } catch ( err ) {
+ if( !optsDiscover.isSilentReDiscovery ) {
+ log.critical(
+ "{p}JSON RPC call(err) to S-Chain node {} failed: {err}, stack is:\n{stack}",
+ optsDiscover.strLogPrefix, strNodeDescColorized, err, err );
+ }
+ ++optsDiscover.cntFailed;
+ if( joCall )
+ await joCall.disconnect();
+ }
+ }
+}
+
+async function discoverSChainWait( optsDiscover: TDiscoveryOptions ): Promise {
+ if( !optsDiscover.isSilentReDiscovery ) {
+ log.debug( "{p}Waiting for response from at least {} node(s)...",
+ optsDiscover.strLogPrefix, optsDiscover.nCountToWait );
+ }
+ let nWaitAttempt = 0;
+ const nWaitStepMilliseconds = 1 * 1000; // step can be small here
+ let cntWaitAttempts = Math.floor(
+ optsDiscover.imaState.joSChainDiscovery.repeatIntervalMilliseconds /
+ nWaitStepMilliseconds ) - 3;
+ if( cntWaitAttempts < 1 )
+ cntWaitAttempts = 1;
+ const iv = setInterval( function(): void {
+ optsDiscover.nCountAvailable =
+ optsDiscover.cntNodes - optsDiscover.cntFailed;
+ // notice, below provided up-to-date count of available and fully discovered nodes:
+ optsDiscover.nCountReceivedImaDescriptions =
+ getSChainDiscoveredNodesCount( optsDiscover.joSChainNetworkInfo );
+ if( !optsDiscover.isSilentReDiscovery ) {
+ log.debug(
+ "Waiting (S-Chain discovery) attempt {} of {} for S-Chain nodes, " +
+ "total {}, available {}, expected at least {}, discovered {}",
+ nWaitAttempt, cntWaitAttempts, optsDiscover.cntNodes, optsDiscover.nCountAvailable,
+ optsDiscover.nCountToWait, optsDiscover.nCountReceivedImaDescriptions );
+ }
+ if( !optsDiscover.isSilentReDiscovery ) {
+ log.information( "{p}Have S-Chain description response about {} of {} node(s).",
+ optsDiscover.strLogPrefix, optsDiscover.nCountReceivedImaDescriptions,
+ optsDiscover.cntNodes );
+ }
+ if( optsDiscover.nCountReceivedImaDescriptions >= optsDiscover.nCountToWait ) {
+ if( !optsDiscover.isSilentReDiscovery ) {
+ log.success(
+ "{p}This S-Chain discovery will finish with {} of {} node(s) discovered.",
+ optsDiscover.strLogPrefix, optsDiscover.nCountReceivedImaDescriptions,
+ optsDiscover.cntNodes );
+ }
+ clearInterval( iv );
+ if( optsDiscover.fnAfter )
+ optsDiscover.fnAfter( null, optsDiscover.joSChainNetworkInfo );
+ return;
+ }
+ ++nWaitAttempt;
+ if( nWaitAttempt >= cntWaitAttempts ) {
+ clearInterval( iv );
+ const strErrorDescription = "S-Chain network discovery wait timeout, " +
+ "network will be re-discovered later";
+ if( !optsDiscover.isSilentReDiscovery ) {
+ log.warning( "{p}This S-Chain discovery will finish due to: {err}",
+ optsDiscover.strLogPrefix, strErrorDescription );
+ }
+ if( getSChainDiscoveredNodesCount( optsDiscover.joSChainNetworkInfo ) > 0 ) {
+ if( optsDiscover.fnAfter )
+ optsDiscover.fnAfter( null, optsDiscover.joSChainNetworkInfo );
+ } else {
+ if( optsDiscover.fnAfter )
+ optsDiscover.fnAfter( new Error( strErrorDescription ), null );
+ }
+ return;
+ }
+ if( !optsDiscover.isSilentReDiscovery ) {
+ log.debug( "{p}S-Chain discovery waiting attempt {} of {} for {} node answer(s)",
+ optsDiscover.strLogPrefix, nWaitAttempt, cntWaitAttempts,
+ ( optsDiscover.nCountToWait - optsDiscover.nCountReceivedImaDescriptions ) );
+ }
+ }, nWaitStepMilliseconds );
+}
+
+async function handleDiscoverSkaleNodesRpcInfoResult(
+ optsDiscover: TDiscoveryOptions, scURL: string,
+ joCall: rpcCall.TRPCCall, joIn: any, joOut: any ): Promise {
+ if( !optsDiscover.isSilentReDiscovery ) {
+ log.trace( "{p}OK, got (own) S-Chain network information: {}",
+ optsDiscover.strLogPrefix, joOut.result );
+ log.success( "{p}OK, got S-Chain {url} network information.",
+ optsDiscover.strLogPrefix, scURL );
+ }
+ optsDiscover.nCountReceivedImaDescriptions = 0;
+ optsDiscover.joSChainNetworkInfo = joOut.result;
+ if( !optsDiscover.joSChainNetworkInfo ) {
+ const err2 = new Error(
+ "Got wrong response, network information description was not detected" );
+ if( !optsDiscover.isSilentReDiscovery ) {
+ log.critical( "{p}Network was not detected via call to {url}: {err}",
+ optsDiscover.strLogPrefix, scURL, err2 );
+ }
+ if( optsDiscover.fnAfter )
+ optsDiscover.fnAfter( err2, null );
+ await joCall.disconnect();
+ throw err2;
+ }
+ optsDiscover.jarrNodes = optsDiscover.joSChainNetworkInfo.network;
+ optsDiscover.cntNodes = optsDiscover.jarrNodes.length;
+ if( optsDiscover.nCountToWait <= 0 || optsDiscover.nCountToWait >= optsDiscover.cntNodes ) {
+ optsDiscover.nCountToWait = ( optsDiscover.cntNodes > 2 )
+ ? Math.ceil( optsDiscover.cntNodes * 2 / 3 )
+ : optsDiscover.cntNodes;
+ }
+ if( optsDiscover.nCountToWait > optsDiscover.cntNodes )
+ optsDiscover.nCountToWait = optsDiscover.cntNodes;
+ if( !optsDiscover.isSilentReDiscovery ) {
+ log.information( "{p}Will gather details of {} of {} node(s)...",
+ optsDiscover.strLogPrefix, optsDiscover.nCountToWait, optsDiscover.cntNodes );
+ }
+ await discoverSChainWalkNodes( optsDiscover );
+ optsDiscover.nCountAvailable = optsDiscover.cntNodes - optsDiscover.cntFailed;
+ if( !optsDiscover.isSilentReDiscovery ) {
+ log.debug( "Waiting for S-Chain nodes, total {}, available {}, expected at least {}",
+ optsDiscover.cntNodes, optsDiscover.nCountAvailable, optsDiscover.nCountToWait );
+ }
+ if( optsDiscover.nCountAvailable < optsDiscover.nCountToWait ) {
+ if( !optsDiscover.isSilentReDiscovery ) {
+ log.critical(
+ "{p}Not enough nodes available on S-Chain, total {}, " +
+ "available {}, expected at least {}",
+ optsDiscover.strLogPrefix, optsDiscover.cntNodes,
+ optsDiscover.nCountAvailable, optsDiscover.nCountToWait );
+ }
+ const err = new Error( "Not enough nodes available on S-Chain, " +
+ `total ${optsDiscover.cntNodes}, available ${optsDiscover.nCountAvailable}, ` +
+ `expected at least ${optsDiscover.nCountToWait}` );
+ if( optsDiscover.fnAfter )
+ optsDiscover.fnAfter( err, null );
+ throw err;
+ }
+ let rv = false;
+ await discoverSChainWait( optsDiscover ).then( function(): void {
+ if( optsDiscover.fnAfter )
+ optsDiscover.fnAfter( null, optsDiscover.joSChainNetworkInfo );
+ rv = true;
+ } ).catch( function( err: Error | string ): void {
+ log.error(
+ "Failed to wait until S-chain discovery complete, reported error is: {err}", err );
+ if( optsDiscover.fnAfter )
+ optsDiscover.fnAfter( err, null );
+ } );
+ return rv;
+}
+
+export async function discoverSChainNetwork(
+ fnAfter: TFunctionAfterDiscovery | null, isSilentReDiscovery: boolean,
+ joPrevSChainNetworkInfo: any, nCountToWait: number
+): Promise {
+ const optsDiscover: TDiscoveryOptions = {
+ fnAfter,
+ isSilentReDiscovery: ( !!isSilentReDiscovery ),
+ joPrevSChainNetworkInfo: joPrevSChainNetworkInfo || null,
+ nCountToWait,
+ imaState: state.get(),
+ strLogPrefix: "S-Chain network discovery: ",
+ joSChainNetworkInfo: null,
+ jarrNodes: [],
+ cntNodes: 0,
+ cntFailed: 0,
+ nCountReceivedImaDescriptions: 0,
+ nCountAvailable: 0
+ };
+ if( optsDiscover.nCountToWait == null ||
+ optsDiscover.nCountToWait == undefined ||
+ optsDiscover.nCountToWait < 0 )
+ optsDiscover.nCountToWait = 0;
+ if( !optsDiscover.isSilentReDiscovery )
+ log.information( "{p}This S-Chain discovery will start...", optsDiscover.strLogPrefix );
+ let joCall: rpcCall.TRPCCall | null = null;
+ try {
+ const scURL = optsDiscover.imaState.chainProperties.sc.strURL;
+ const rpcCallOpts: rpcCall.TRPCCallOpts | null = null;
+ joCall = await rpcCall.create( scURL, rpcCallOpts );
+ if( !joCall )
+ throw new Error( `Failed to create JSON RPC call object to ${scURL}` );
+ const joIn: any = { method: "skale_nodesRpcInfo", params: { } };
+ if( isSendImaAgentIndex() )
+ joIn.params.fromImaAgentIndex = optsDiscover.imaState.nNodeNumber;
+ const joOut = await joCall.call( joIn );
+ await handleDiscoverSkaleNodesRpcInfoResult(
+ optsDiscover, scURL, joCall, joIn, joOut
+ ).catch( function( err: Error | string ): void {
+ log.critical(
+ "{p}JSON RPC call(in discoverSChainNetwork) error: {err}, stack is:\n{stack}",
+ optsDiscover.strLogPrefix, err, err );
+ } );
+ } catch ( err ) {
+ if( !optsDiscover.isSilentReDiscovery ) {
+ log.critical(
+ "{p}JSON RPC call(discoverSChainNetwork) to S-Chain failed: " +
+ "{err}, stack is:\n{stack}", optsDiscover.strLogPrefix,
+ err, err );
+ }
+ optsDiscover.joSChainNetworkInfo = null;
+ if( optsDiscover.fnAfter )
+ optsDiscover.fnAfter( err as Error, null );
+ if( joCall )
+ await joCall.disconnect();
+ throw err;
+ }
+ return optsDiscover.joSChainNetworkInfo;
+}
+
+let gIntervalPeriodicDiscovery: any = null;
+
+function checkPeriodicDiscoveryNoLongerNeeded(
+ joSChainNetworkInfo: TSChainNetworkInfo | null, isSilentReDiscovery: boolean
+): boolean {
+ if( !joSChainNetworkInfo )
+ return false;
+ const imaState: state.TIMAState = state.get();
+ if( !imaState.joSChainNetworkInfo )
+ throw new Error( "No own S-Chain network information" );
+ const cntNodesOnChain = getSChainNodesCount( imaState.joSChainNetworkInfo );
+ const cntAlreadyDiscovered = getSChainDiscoveredNodesCount( joSChainNetworkInfo );
+ if( !isSilentReDiscovery ) {
+ log.notice( "Periodic S-Chain re-discovery already have {} of {} node(s) discovered",
+ cntAlreadyDiscovered, cntNodesOnChain );
+ }
+ if( cntAlreadyDiscovered >= cntNodesOnChain ) {
+ if( gIntervalPeriodicDiscovery ) {
+ clearInterval( gIntervalPeriodicDiscovery );
+ gIntervalPeriodicDiscovery = null;
+ }
+ return true;
+ }
+ return false;
+}
+
+export async function doPeriodicSChainNetworkDiscoveryIfNeeded(
+ isSilentReDiscovery: boolean, fnAfterRediscover?: TFunctionAfterRediscovery
+): Promise {
+ if( gIntervalPeriodicDiscovery )
+ return; // already started
+ const imaState: state.TIMAState = state.get();
+ let joPrevSChainNetworkInfo = imaState.joSChainNetworkInfo;
+ if( checkPeriodicDiscoveryNoLongerNeeded(
+ joPrevSChainNetworkInfo, isSilentReDiscovery ) ) {
+ if( !isSilentReDiscovery )
+ log.success( "Periodic S-Chain re-discovery is not needed right from startup" );
+ return; // not needed right from very beginning
+ }
+ if( !imaState.joSChainNetworkInfo )
+ throw new Error( "No own S-Chain network information" );
+ const cntNodesOnChain = getSChainNodesCount( imaState.joSChainNetworkInfo );
+ let periodicDiscoveryInterval = imaState.joSChainDiscovery.periodicDiscoveryInterval;
+ if( periodicDiscoveryInterval <= 0 )
+ periodicDiscoveryInterval = 5 * 60 * 1000;
+ if( !isSilentReDiscovery ) {
+ log.debug( "Periodic S-Chain re-discovery will be done with {} interval...",
+ periodicDiscoveryInterval );
+ }
+ fnAfterRediscover = fnAfterRediscover ?? function( status: boolean ) { };
+ gIntervalPeriodicDiscovery = setInterval( function(): void {
+ let nCountToWait = ( cntNodesOnChain > 2 )
+ ? Math.ceil( cntNodesOnChain * 2 / 3 )
+ : cntNodesOnChain;
+ if( nCountToWait > cntNodesOnChain )
+ nCountToWait = cntNodesOnChain;
+ if( !isSilentReDiscovery )
+ log.information( "This S-Chain discovery will be done for periodic discovery update" );
+ discoverSChainNetwork(
+ null, isSilentReDiscovery, joPrevSChainNetworkInfo, nCountToWait )
+ .then( function(): void {
+ joPrevSChainNetworkInfo = imaState.joSChainNetworkInfo;
+ if( checkPeriodicDiscoveryNoLongerNeeded(
+ joPrevSChainNetworkInfo, isSilentReDiscovery ) ) {
+ if( !isSilentReDiscovery )
+ log.information( "Final periodic S-Chain re-discovery done" );
+ if( fnAfterRediscover )
+ fnAfterRediscover( true );
+ return; // not needed anymore, all nodes completely discovered
+ }
+ if( !isSilentReDiscovery )
+ log.information( "Partial periodic S-Chain re-discovery done" );
+ if( fnAfterRediscover )
+ fnAfterRediscover( false );
+ } ).catch( function(): void {} );
+ }, periodicDiscoveryInterval );
+ if( !isSilentReDiscovery ) {
+ log.information( "Periodic S-Chain re-discovery was started with interval {}" +
+ " millisecond(s)", periodicDiscoveryInterval );
+ }
+}
diff --git a/src/eventDispatcher.ts b/src/eventDispatcher.ts
new file mode 100644
index 00000000..9c95e8ae
--- /dev/null
+++ b/src/eventDispatcher.ts
@@ -0,0 +1,125 @@
+// 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 .
+ */
+
+/**
+ * @file eventDispatcher.ts
+ * @copyright SKALE Labs 2019-Present
+ */
+
+export class UniversalDispatcherEvent {
+ type: any;
+ constructor ( type: any, jo: any ) {
+ this.type = type;
+ for( const [ key, value ] of Object.entries( jo ) ) {
+ if( key in this ) {
+ console.warn( "UniversalDispatcherEvent will skip", key, "data field" );
+ continue;
+ }
+ const anyThis: any = this;
+ anyThis[key] = value;
+ }
+ }
+};
+
+export class EventDispatcher {
+ // see https://stackoverflow.com/questions/36675693/eventtarget-interface-in-safari
+ _listeners: any[];
+ isDisposing: boolean;
+ isDisposed: boolean;
+ constructor () {
+ this._listeners = [];
+ this.isDisposed = false;
+ this.isDisposing = false;
+ }
+
+ dispose(): void {
+ if( this.isDisposed )
+ return;
+ this.isDisposing = true;
+ this.isDisposed = true;
+ this.dispatchEvent(
+ new UniversalDispatcherEvent( "dispose", { detail: { ref: this } } )
+ );
+ this.removeAllEventListeners();
+ }
+
+ hasEventListener( type: any, listener: any ): boolean {
+ return this._listeners.some( item => item.type === type && item.listener === listener );
+ }
+
+ addEventListener( type: any, listener: any ): EventDispatcher {
+ if( !this.hasEventListener( type, listener ) ) {
+ this._listeners.push( {
+ type,
+ listener,
+ options: { once: false }
+ } );
+ }
+ return this;
+ }
+
+ removeEventListener( type: any, listener: any ): EventDispatcher {
+ 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;
+ }
+ return this;
+ }
+
+ removeAllEventListeners(): EventDispatcher {
+ this._listeners = [];
+ return this;
+ }
+
+ on( type: any, listener: any ): EventDispatcher {
+ return this.addEventListener( type, listener );
+ }
+
+ off( type: any, listener: any ): EventDispatcher {
+ return this.removeEventListener( type, listener );
+ }
+
+ offAll(): EventDispatcher {
+ return this.removeAllEventListeners();
+ }
+
+ dispatchEvent( evt: any ): EventDispatcher {
+ 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 );
+ }
+ return this;
+ }
+};
diff --git a/src/get_abi_json_for_main_net.sh b/src/get_abi_json_for_main_net.sh
new file mode 100755
index 00000000..cb77b72e
--- /dev/null
+++ b/src/get_abi_json_for_main_net.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+#FILE_SRC="../IMA/proxy/proxy.json"
+FILE_SRC="../IMA/proxy/data/pseudo_main_net_proxy.json"
+FILE_DST="./abi_main_net.json"
+FILE_BAK="${FILE_DST}.bak"
+echo "Source file......... '${FILE_SRC}'"
+echo "Destination file.... '${FILE_DST}'"
+#echo "Backup file......... '${FILE_BAK}'"
+
+if [ ! -f "$FILE_SRC" ]
+then
+ echo "Source file '${FILE_SRC}' not found."
+ exit 101
+fi
+
+if [ -f "$FILE_DST" ]
+then
+ if [ -f "$FILE_BAK" ]
+ then
+ echo "Removing backup file '${FILE_BAK}'"
+ rm -f "$FILE_BAK" > /dev/null
+ fi
+ echo "Backing-up file '${FILE_DST}' to ${FILE_BAK}'"
+ mv "${FILE_DST}" "${FILE_BAK}"
+fi
+
+echo "Copying file '${FILE_SRC}' to ${FILE_DST}'"
+rm -f "$FILE_DST" > /dev/null
+cp "${FILE_SRC}" "${FILE_DST}"
+
+echo "Done."
diff --git a/src/get_abi_json_for_s_chain.sh b/src/get_abi_json_for_s_chain.sh
new file mode 100755
index 00000000..7f052614
--- /dev/null
+++ b/src/get_abi_json_for_s_chain.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+#FILE_SRC="../IMA/proxy/proxy.json"
+FILE_SRC="../IMA/proxy/data/local_proxy.json"
+FILE_DST="./abi_s_chain.json"
+FILE_BAK="${FILE_DST}.bak"
+echo "Source file......... '${FILE_SRC}'"
+echo "Destination file.... '${FILE_DST}'"
+#echo "Backup file......... '${FILE_BAK}'"
+
+if [ ! -f "$FILE_SRC" ]
+then
+ echo "Source file '${FILE_SRC}' not found."
+ exit 101
+fi
+
+if [ -f "$FILE_DST" ]
+then
+ if [ -f "$FILE_BAK" ]
+ then
+ echo "Removing backup file '${FILE_BAK}'"
+ rm -f "$FILE_BAK" > /dev/null
+ fi
+ echo "Backing-up file '${FILE_DST}' to ${FILE_BAK}'"
+ mv "${FILE_DST}" "${FILE_BAK}"
+fi
+
+echo "Copying file '${FILE_SRC}' to ${FILE_DST}'"
+rm -f "$FILE_DST" > /dev/null
+cp "${FILE_SRC}" "${FILE_DST}"
+
+echo "Done."
diff --git a/src/imaCore.ts b/src/imaCore.ts
new file mode 100644
index 00000000..bc698ddd
--- /dev/null
+++ b/src/imaCore.ts
@@ -0,0 +1,1310 @@
+// 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 .
+ */
+
+/**
+ * @file index.ts
+ * @copyright SKALE Labs 2019-Present
+ */
+
+import * as log from "./log.js";
+import * as owaspUtils from "./owaspUtils.js";
+import * as loop from "./loop.js";
+import * as pwa from "./pwa.js";
+import * as state from "./state.js";
+import * as imaHelperAPIs from "./imaHelperAPIs.js";
+import * as imaTx from "./imaTx.js";
+import * as imaGasUsage from "./imaGasUsageOperations.js";
+import * as imaEventLogScan from "./imaEventLogScan.js";
+import * as imaTransferErrorHandling from "./imaTransferErrorHandling.js";
+import * as skaleObserver from "./observer.js";
+import * as threadInfo from "./threadInfo.js";
+
+export type TFunctionAfterSigningMessages =
+ ( err: Error | string | null, jarrMessages: any[], joGlueResult?: any | null
+ ) => Promise < void >;
+export type TFunctionDoSignMessages =
+ ( nTransferLoopCounter: number, jarrMessages: any[], nIdxCurrentMsgBlockStart: number,
+ chainNameSrc: string, joExtraSignOpts?: loop.TExtraSignOpts | null,
+ fnAfter?: TFunctionAfterSigningMessages ) => Promise < void >;
+
+export interface TTransferOptions {
+ strDirection: string
+ joRuntimeOpts: loop.TRuntimeOpts
+ ethersProviderSrc: owaspUtils.ethersMod.ethers.providers.JsonRpcProvider
+ joMessageProxySrc: owaspUtils.ethersMod.Contract
+ joAccountSrc: state.TAccount
+ ethersProviderDst: owaspUtils.ethersMod.ethers.providers.JsonRpcProvider
+ joMessageProxyDst: owaspUtils.ethersMod.Contract
+ joAccountDst: state.TAccount
+ chainNameSrc: string
+ chainNameDst: string
+ chainIdSrc: string
+ chainIdDst: string
+ joDepositBoxMainNet: owaspUtils.ethersMod.Contract | null // for logs validation on mainnet
+ joTokenManagerSChain: owaspUtils.ethersMod.Contract | null // for logs validation on s-chain
+ nTransactionsCountInBlock: number
+ nTransferSteps: number
+ nMaxTransactionsCount: number
+ nBlockAwaitDepth: number
+ nBlockAge: number
+ fnSignMessages: TFunctionDoSignMessages
+ joExtraSignOpts?: loop.TExtraSignOpts | null
+ transactionCustomizerDst: imaTx.TransactionCustomizer
+ imaState: state.TIMAState
+ nTransferLoopCounter: number
+ strTransferErrorCategoryName: string
+ strGatheredDetailsName: string
+ details: log.TLogger
+ jarrReceipts: any[]
+ bErrorInSigningMessages: boolean
+ strLogPrefixShort: string
+ strLogPrefix: string
+ nStepsDone: number
+ strActionName: string
+ nIdxCurrentMsg: number
+ nOutMsgCnt: number
+ nIncMsgCnt: number
+ cntProcessed: number
+ arrMessageCounters: any[]
+ jarrMessages: any[]
+ nIdxCurrentMsgBlockStart: number
+ cntAccumulatedForBlock: number
+ arrLogRecordReferences: any[]
+ cntNodesShouldPass: number
+ cntNodesMayFail: number
+}
+
+export interface TOutgoingMessageAnalysisOptions {
+ idxMessage: number
+ idxImaMessage: number
+ joMessage: any
+ joNode: skaleObserver.TSChainNode | null
+ idxNode: number
+ cntNodes: number
+ cntPassedNodes: number
+ cntFailedNodes: number
+}
+
+log.enableColorization( false );
+log.addStdout();
+
+const perMessageGasForTransfer = 1000000;
+const additionalS2MTransferOverhead = 200000;
+
+async function findOutReferenceLogRecord(
+ details: log.TLogger, strLogPrefix: string,
+ ethersProvider: owaspUtils.ethersMod.ethers.providers.JsonRpcProvider,
+ joMessageProxy: owaspUtils.ethersMod.ethers.Contract,
+ bnBlockId: any, nMessageNumberToFind: number, isVerbose?: boolean
+): Promise {
+ const bnMessageNumberToFind = owaspUtils.toBN( nMessageNumberToFind.toString() );
+ const strEventName = "PreviousMessageReference";
+ const arrLogRecords = await imaEventLogScan.safeGetPastEventsProgressive(
+ details, strLogPrefix, ethersProvider, 10, joMessageProxy, strEventName,
+ bnBlockId, bnBlockId, joMessageProxy.filters[strEventName]() );
+ const cntLogRecord = arrLogRecords.length;
+ if( isVerbose ) {
+ details.debug( "{p}Got {} log record(s) ({}) with data: {}",
+ strLogPrefix, cntLogRecord, strEventName, arrLogRecords );
+ }
+ for( let idxLogRecord = 0; idxLogRecord < cntLogRecord; ++idxLogRecord ) {
+ const joEvent = arrLogRecords[idxLogRecord];
+ const ev = {
+ currentMessage: joEvent.args[0],
+ previousOutgoingMessageBlockId: joEvent.args[1]
+ };
+ const joReferenceLogRecord: any = {
+ currentMessage: ev.currentMessage,
+ previousOutgoingMessageBlockId: ev.previousOutgoingMessageBlockId,
+ currentBlockId: bnBlockId
+ };
+ const bnCurrentMessage =
+ owaspUtils.toBN( joReferenceLogRecord.currentMessage.toString() );
+ if( bnCurrentMessage.eq( bnMessageNumberToFind ) ) {
+ if( isVerbose ) {
+ details.success( "{p}Found {} log record {} for message {}",
+ strLogPrefix, strEventName, joReferenceLogRecord, nMessageNumberToFind );
+ }
+ return joReferenceLogRecord;
+ }
+ }
+ if( isVerbose ) {
+ details.error( "{p}Failed to find {} log record for message {}", strLogPrefix,
+ strEventName, nMessageNumberToFind );
+ }
+ return null;
+}
+
+async function findOutAllReferenceLogRecords(
+ details: log.TLogger, strLogPrefix: string,
+ ethersProvider: owaspUtils.ethersMod.ethers.providers.JsonRpcProvider,
+ joMessageProxy: owaspUtils.ethersMod.ethers.Contract,
+ bnBlockId: any, nIncMsgCnt: number, nOutMsgCnt: number, isVerbose?: boolean
+): Promise {
+ if( isVerbose ) {
+ details.debug(
+ "{p}Optimized IMA message search algorithm will start at block {}" +
+ ", will search for outgoing message counter {} and approach down to incoming " +
+ "message counter {}", strLogPrefix, bnBlockId.toString(),
+ nOutMsgCnt.toString(), nIncMsgCnt.toString() );
+ }
+ const arrLogRecordReferences: any = [];
+ const cntExpected = nOutMsgCnt - nIncMsgCnt;
+ if( cntExpected <= 0 ) {
+ if( isVerbose ) {
+ details.success( "{p}Optimized IMA message search algorithm success, " +
+ "nothing to search, result is empty", strLogPrefix );
+ }
+ return arrLogRecordReferences; // nothing to search
+ }
+ let nWalkMsgNumber = nOutMsgCnt - 1;
+ let nWalkBlockId = bnBlockId;
+ for( ; nWalkMsgNumber >= nIncMsgCnt; --nWalkMsgNumber ) {
+ const joReferenceLogRecord = await findOutReferenceLogRecord( details, strLogPrefix,
+ ethersProvider, joMessageProxy, nWalkBlockId, nWalkMsgNumber, isVerbose );
+ if( joReferenceLogRecord == null )
+ break;
+ nWalkBlockId = owaspUtils.toBN( joReferenceLogRecord.previousOutgoingMessageBlockId );
+ arrLogRecordReferences.unshift( joReferenceLogRecord );
+ }
+ const cntFound = arrLogRecordReferences.length;
+ if( cntFound != cntExpected ) {
+ if( isVerbose ) {
+ details.error(
+ "{p}Optimized IMA message search algorithm fail, found {} log " +
+ "record(s), expected {} log record(s), found records are: {}",
+ strLogPrefix, cntFound, cntExpected, arrLogRecordReferences );
+ }
+ } else {
+ if( isVerbose ) {
+ details.success( "{p}Optimized IMA message search algorithm success, found all {}" +
+ " log record(s): {}", strLogPrefix, cntFound, arrLogRecordReferences );
+ }
+ }
+ return arrLogRecordReferences;
+}
+
+let gTransferLoopCounter = 0;
+
+// Do real money movement from main-net to S-chain by sniffing events
+// 1) main-net.MessageProxyForMainnet.getOutgoingMessagesCounter -> save to nOutMsgCnt
+// 2) S-chain.MessageProxySchain.getIncomingMessagesCounter -> save to nIncMsgCnt
+// 3) Will transfer all in range from [ nIncMsgCnt ... (nOutMsgCnt-1) ] ...
+// assume current counter index is nIdxCurrentMsg
+//
+// One transaction transfer is:
+// 1) Find events main-net.MessageProxyForMainnet.OutgoingMessage
+// where msgCounter member is in range
+// 2) Publish it to S-chain.MessageProxySchain.postIncomingMessages(
+// main-net chain id // uint64 srcChainID
+// nIdxCurrentMsg // uint64 startingCounter
+// [srcContract] // address[] memory senders
+// [dstContract] // address[] memory dstContracts
+// [to] // address[] memory to
+// [amount] // uint256[] memory amount / *uint256[2] memory blsSignature* /
+// )
+async function doQueryOutgoingMessageCounter( optsTransfer: TTransferOptions ): Promise {
+ let nPossibleIntegerValue = 0;
+ optsTransfer.details.debug( "{p}SRC MessageProxy address is.....{}",
+ optsTransfer.strLogPrefixShort, optsTransfer.joMessageProxySrc.address );
+ optsTransfer.details.debug( "{p}DST MessageProxy address is.....",
+ optsTransfer.strLogPrefixShort, optsTransfer.joMessageProxyDst.address );
+ optsTransfer.strActionName = "src-chain.MessageProxy.getOutgoingMessagesCounter()";
+ try {
+ optsTransfer.details.debug( "{p}Will call {bright}...",
+ optsTransfer.strLogPrefix, optsTransfer.strActionName );
+ nPossibleIntegerValue =
+ await optsTransfer.joMessageProxySrc.callStatic.getOutgoingMessagesCounter(
+ optsTransfer.chainNameDst,
+ { from: optsTransfer.joAccountSrc.address() } );
+ if( !owaspUtils.validateInteger( nPossibleIntegerValue ) ) {
+ throw new Error( `DST chain ${optsTransfer.chainNameDst} returned outgoing ` +
+ `message counter ${nPossibleIntegerValue} which is not a valid integer` );
+ }
+ optsTransfer.nOutMsgCnt = owaspUtils.toInteger( nPossibleIntegerValue );
+ optsTransfer.details.information( "{p}Result of {bright} call: {}",
+ optsTransfer.strLogPrefix, optsTransfer.strActionName, optsTransfer.nOutMsgCnt );
+ } catch ( err ) {
+ optsTransfer.details.critical(
+ "(IMMEDIATE) error caught during {bright}, error details: {err}, stack is:\n{stack}",
+ optsTransfer.strActionName, err, err );
+ }
+
+ optsTransfer.strActionName = "dst-chain.MessageProxy.getIncomingMessagesCounter()";
+ optsTransfer.details.debug( "{p}Will call {bright}...",
+ optsTransfer.strLogPrefix, optsTransfer.strActionName );
+ nPossibleIntegerValue =
+ await optsTransfer.joMessageProxyDst.callStatic.getIncomingMessagesCounter(
+ optsTransfer.chainNameSrc, { from: optsTransfer.joAccountDst.address() } );
+ if( !owaspUtils.validateInteger( nPossibleIntegerValue ) ) {
+ throw new Error( `SRC chain ${optsTransfer.chainNameSrc} returned incoming message ` +
+ `counter ${nPossibleIntegerValue} which is not a valid integer` );
+ }
+ optsTransfer.nIncMsgCnt = owaspUtils.toInteger( nPossibleIntegerValue );
+ optsTransfer.details.debug( "{p}Result of {bright} call: {}",
+ optsTransfer.strLogPrefix, optsTransfer.strActionName, optsTransfer.nIncMsgCnt );
+ optsTransfer.strActionName = "src-chain.MessageProxy.getIncomingMessagesCounter()";
+ nPossibleIntegerValue =
+ await optsTransfer.joMessageProxySrc.callStatic.getIncomingMessagesCounter(
+ optsTransfer.chainNameDst, { from: optsTransfer.joAccountSrc.address() } );
+ if( !owaspUtils.validateInteger( nPossibleIntegerValue ) ) {
+ throw new Error( `DST chain ${optsTransfer.chainNameDst} returned incoming ` +
+ `message counter ${nPossibleIntegerValue} + which is not a valid integer` );
+ }
+ const idxLastToPopNotIncluding = owaspUtils.toInteger( nPossibleIntegerValue );
+ optsTransfer.details.debug( "{p}Result of {bright} call: {}",
+ optsTransfer.strLogPrefix, optsTransfer.strActionName, idxLastToPopNotIncluding );
+ // first, try optimized scanner
+ optsTransfer.arrLogRecordReferences = [];
+ try {
+ optsTransfer.strActionName =
+ "in-getOutgoingMessagesCounter()--joMessageProxySrc.getLastOutgoingMessageBlockId()";
+ const bnBlockId =
+ owaspUtils.toBN(
+ await optsTransfer.joMessageProxySrc.callStatic.getLastOutgoingMessageBlockId(
+ optsTransfer.chainNameDst,
+ { from: optsTransfer.joAccountSrc.address() } ) );
+ try {
+ if( bnBlockId ) {
+ optsTransfer.strActionName =
+ "in-getOutgoingMessagesCounter()--findOutAllReferenceLogRecords()";
+ optsTransfer.arrLogRecordReferences =
+ await findOutAllReferenceLogRecords( optsTransfer.details,
+ optsTransfer.strLogPrefixShort, optsTransfer.ethersProviderSrc,
+ optsTransfer.joMessageProxySrc, bnBlockId, optsTransfer.nIncMsgCnt,
+ optsTransfer.nOutMsgCnt, true );
+ return true; // success, finish at this point
+ }
+ } catch ( err ) {
+ optsTransfer.arrLogRecordReferences = [];
+ optsTransfer.details.error(
+ "{p}Optimized log search is off. Running old IMA smart contracts? " +
+ "Please upgrade, if possible. Error is: {err}, stack is:\n{stack}",
+ optsTransfer.strLogPrefix, err, err );
+ }
+ } catch ( err ) {
+ optsTransfer.arrLogRecordReferences = [];
+ optsTransfer.details.error( "{p}Optimized log search is un-available.",
+ optsTransfer.strLogPrefix );
+ }
+ // second, use classic raw events search
+ optsTransfer.strActionName = "in-getOutgoingMessagesCounter()--classic-records-scanner";
+ const attempts = 10;
+ const strEventName = "OutgoingMessage";
+ const nBlockFrom = 0;
+ const nBlockTo = "latest";
+ for( let nWalkMsgNumber = optsTransfer.nIncMsgCnt;
+ nWalkMsgNumber < optsTransfer.nOutMsgCnt;
+ ++nWalkMsgNumber
+ ) {
+ const joFilter = optsTransfer.joMessageProxySrc.filters[strEventName](
+ owaspUtils.ethersMod.ethers.utils.id( optsTransfer.chainNameDst ),
+ owaspUtils.toBN( nWalkMsgNumber ) );
+ const arrLogRecordReferencesWalk =
+ await imaEventLogScan.safeGetPastEventsProgressive( optsTransfer.details,
+ optsTransfer.strLogPrefixShort, optsTransfer.ethersProviderSrc, attempts,
+ optsTransfer.joMessageProxySrc, strEventName, nBlockFrom, nBlockTo, joFilter );
+ optsTransfer.arrLogRecordReferences =
+ optsTransfer.arrLogRecordReferences.concat( arrLogRecordReferencesWalk );
+ }
+
+ return true;
+}
+
+async function analyzeGatheredRecords( optsTransfer: TTransferOptions, r: any ): Promise {
+ let joValues: any = null;
+ const strChainHashWeAreLookingFor =
+ owaspUtils.ethersMod.ethers.utils.id( optsTransfer.chainNameDst );
+ optsTransfer.details.debug(
+ "{p}Will review {} found event records(in reverse order, newest to oldest) while looking " +
+ "for hash {} of destination chain {}", optsTransfer.strLogPrefix, r.length,
+ strChainHashWeAreLookingFor, optsTransfer.chainNameDst );
+ for( let i = r.length - 1; i >= 0; i-- ) {
+ const joEvent = r[i];
+ optsTransfer.details.debug( "{p}Will review found event record {} with data {}",
+ optsTransfer.strLogPrefix, i, joEvent );
+ const ev = {
+ dstChainHash: joEvent.args[0],
+ msgCounter: joEvent.args[1],
+ srcContract: joEvent.args[2],
+ dstContract: joEvent.args[3],
+ data: joEvent.args[4]
+ };
+ if( ev.dstChainHash == strChainHashWeAreLookingFor ) {
+ joValues = ev;
+ joValues.savedBlockNumberForOptimizations = r[i].blockNumber;
+ optsTransfer.details.success(
+ "{p}Found event record {} reviewed and accepted for processing, found event " +
+ "values are {}, found block number is {}", optsTransfer.strLogPrefix, i, joValues,
+ joValues.savedBlockNumberForOptimizations );
+ break;
+ } else {
+ optsTransfer.details.debug( "{p}Found event record {} reviewed and skipped",
+ optsTransfer.strLogPrefix, i );
+ }
+ }
+ if( joValues == "" ) {
+ optsTransfer.details.critical( "{p}Can't get events from MessageProxy",
+ optsTransfer.strLogPrefix );
+ optsTransfer.details.exposeDetailsTo(
+ log.globalStream(), optsTransfer.strGatheredDetailsName, false );
+ imaTransferErrorHandling.saveTransferError(
+ optsTransfer.strTransferErrorCategoryName, optsTransfer.details.toString() );
+ optsTransfer.details.close();
+ return null; // caller will return false if we return null here
+ }
+ return joValues;
+}
+
+async function gatherMessages( optsTransfer: TTransferOptions ): Promise {
+ optsTransfer.arrMessageCounters = [];
+ optsTransfer.jarrMessages = [];
+ optsTransfer.nIdxCurrentMsgBlockStart = owaspUtils.toInteger( optsTransfer.nIdxCurrentMsg );
+ let r;
+ optsTransfer.cntAccumulatedForBlock = 0;
+ for( let idxInBlock = 0; // inner loop wil create block of transactions
+ optsTransfer.nIdxCurrentMsg < optsTransfer.nOutMsgCnt &&
+ idxInBlock < optsTransfer.nTransactionsCountInBlock;
+ ++optsTransfer.nIdxCurrentMsg, ++idxInBlock, ++optsTransfer.cntAccumulatedForBlock
+ ) {
+ const idxProcessing = optsTransfer.cntProcessed + idxInBlock;
+ if( idxProcessing > optsTransfer.nMaxTransactionsCount )
+ break;
+ let nBlockFrom = 0; let nBlockTo = "latest";
+ if( optsTransfer.arrLogRecordReferences.length > 0 ) {
+ const joReferenceLogRecord = optsTransfer.arrLogRecordReferences.shift();
+ if( joReferenceLogRecord && "currentBlockId" in joReferenceLogRecord &&
+ joReferenceLogRecord.currentBlockId ) {
+ nBlockFrom = joReferenceLogRecord.currentBlockId;
+ nBlockTo = joReferenceLogRecord.currentBlockId;
+ }
+ }
+ optsTransfer.strActionName = "src-chain->MessageProxy->scan-past-events()";
+ const strEventName = "OutgoingMessage";
+ optsTransfer.details.debug( "{p}Will call {bright} for {} event...",
+ optsTransfer.strLogPrefix, optsTransfer.strActionName, strEventName );
+ r = await imaEventLogScan.safeGetPastEventsProgressive( optsTransfer.details,
+ optsTransfer.strLogPrefixShort, optsTransfer.ethersProviderSrc, 10,
+ optsTransfer.joMessageProxySrc, strEventName, nBlockFrom, nBlockTo,
+ optsTransfer.joMessageProxySrc.filters[strEventName](
+ owaspUtils.ethersMod.ethers.utils.id( optsTransfer.chainNameDst ),
+ owaspUtils.toBN( optsTransfer.nIdxCurrentMsg ) ) );
+ const joValues = await analyzeGatheredRecords( optsTransfer, r );
+ if( joValues == null )
+ return false;
+ if( optsTransfer.nBlockAwaitDepth > 0 ) {
+ let bSecurityCheckPassed = true;
+ const strActionNameOld = optsTransfer.strActionName;
+ optsTransfer.strActionName = "security check: evaluate block depth";
+ try {
+ const transactionHash = r[0].transactionHash;
+ optsTransfer.details.debug( "{p}Event transactionHash is {}",
+ optsTransfer.strLogPrefix, transactionHash );
+ const blockNumber = r[0].blockNumber;
+ optsTransfer.details.debug( "{p}Event blockNumber is {}",
+ optsTransfer.strLogPrefix, blockNumber );
+ const nLatestBlockNumber = await imaHelperAPIs.safeGetBlockNumber(
+ optsTransfer.details, 10, optsTransfer.ethersProviderSrc );
+ optsTransfer.details.debug( "{p}Latest blockNumber is {}",
+ optsTransfer.strLogPrefix, nLatestBlockNumber );
+ const nDist = nLatestBlockNumber - blockNumber;
+ if( nDist < optsTransfer.nBlockAwaitDepth )
+ bSecurityCheckPassed = false;
+ optsTransfer.details.debug( "{p}Distance by blockNumber is {}, await check is {}",
+ optsTransfer.strLogPrefix, nDist,
+ log.posNeg( bSecurityCheckPassed, "PASSED", "FAILED" ) );
+ } catch ( err ) {
+ bSecurityCheckPassed = false;
+ optsTransfer.details.critical(
+ "{p}Exception(evaluate block depth) while getting transaction hash and " +
+ "block number during {bright}: {err}, stack is:\n{stack}",
+ optsTransfer.strLogPrefix, optsTransfer.strActionName, err, err );
+ optsTransfer.details.exposeDetailsTo(
+ log.globalStream(), optsTransfer.strGatheredDetailsName, false );
+ imaTransferErrorHandling.saveTransferError(
+ optsTransfer.strTransferErrorCategoryName, optsTransfer.details.toString() );
+ optsTransfer.details.close();
+ return false;
+ }
+ optsTransfer.strActionName = strActionNameOld.toString();
+ if( !bSecurityCheckPassed ) {
+ optsTransfer.details.warning( "{p}Block depth check was not passed, canceling " +
+ "search for transfer events", optsTransfer.strLogPrefix );
+ break;
+ }
+ }
+ if( optsTransfer.nBlockAge > 0 ) {
+ let bSecurityCheckPassed = true;
+ const strActionNameOld = optsTransfer.strActionName;
+ optsTransfer.strActionName = "security check: evaluate block age";
+ try {
+ const transactionHash = r[0].transactionHash;
+ optsTransfer.details.debug( "{p}Event transactionHash is {}",
+ optsTransfer.strLogPrefix, transactionHash );
+ const blockNumber = r[0].blockNumber;
+ optsTransfer.details.debug( "{p}Event blockNumber is {}",
+ optsTransfer.strLogPrefix, blockNumber );
+ const joBlock = await optsTransfer.ethersProviderSrc.getBlock( blockNumber );
+ if( !owaspUtils.validateInteger( joBlock.timestamp ) ) {
+ throw new Error( "Block timestamp is not a valid " +
+ `integer value: ${joBlock.timestamp}` );
+ }
+ const timestampBlock = owaspUtils.toInteger( joBlock.timestamp );
+ optsTransfer.details.debug( "{p}Block TS is {}",
+ optsTransfer.strLogPrefix, timestampBlock );
+ const timestampCurrent = imaHelperAPIs.currentTimestamp();
+ optsTransfer.details.debug( "{p}Current TS is {}",
+ optsTransfer.strLogPrefix, timestampCurrent );
+ const tsDiff = timestampCurrent - timestampBlock;
+ optsTransfer.details.debug( "{p}Diff TS is {}",
+ optsTransfer.strLogPrefix, tsDiff );
+ optsTransfer.details.debug( "{p}Expected diff {}",
+ optsTransfer.strLogPrefix, optsTransfer.nBlockAge );
+ if( tsDiff < optsTransfer.nBlockAge )
+ bSecurityCheckPassed = false;
+ optsTransfer.details.debug( "{p}Block age check is {}",
+ optsTransfer.strLogPrefix,
+ log.posNeg( bSecurityCheckPassed, "PASSED", "FAILED" ) );
+ } catch ( err ) {
+ bSecurityCheckPassed = false;
+ optsTransfer.details.critical(
+ "{p}Exception(evaluate block age) while getting block number and timestamp " +
+ "during {bright}: {err}, stack is:\n{stack}", optsTransfer.strLogPrefix,
+ optsTransfer.strActionName, err, err );
+ optsTransfer.details.exposeDetailsTo(
+ log.globalStream(), optsTransfer.strGatheredDetailsName, false );
+ imaTransferErrorHandling.saveTransferError(
+ optsTransfer.strTransferErrorCategoryName, optsTransfer.details.toString() );
+ optsTransfer.details.close();
+ return false;
+ }
+ optsTransfer.strActionName = strActionNameOld.toString();
+ if( !bSecurityCheckPassed ) {
+ optsTransfer.details.warning( "{p}Block age check was not passed, " +
+ "canceling search for transfer events", optsTransfer.strLogPrefix );
+ break;
+ }
+ }
+ optsTransfer.details.success(
+ "{p}Got event details from getPastEvents() event invoked with msgCounter set to {} " +
+ "and dstChain set to {}, event description: ", optsTransfer.strLogPrefix,
+ optsTransfer.nIdxCurrentMsg, optsTransfer.chainNameDst, joValues );
+ optsTransfer.details.debug( "{p}Will process message counter value {}",
+ optsTransfer.strLogPrefix, optsTransfer.nIdxCurrentMsg );
+ optsTransfer.arrMessageCounters.push( optsTransfer.nIdxCurrentMsg );
+ const joMessage: any = {
+ sender: joValues.srcContract,
+ destinationContract: joValues.dstContract,
+ to: joValues.to,
+ amount: joValues.amount,
+ data: joValues.data,
+ savedBlockNumberForOptimizations: joValues.savedBlockNumberForOptimizations
+ };
+ optsTransfer.jarrMessages.push( joMessage );
+ }
+ return true;
+}
+
+async function preCheckAllMessagesSign(
+ optsTransfer: TTransferOptions, err: Error | string | null,
+ jarrMessages: any[], joGlueResult: any ): Promise {
+ const strDidInvokedSigningCallbackMessage = log.fmtDebug(
+ "{p}Did invoked message signing callback, first real message index is: {}, have {} " +
+ "message(s) to process {}", optsTransfer.strLogPrefix,
+ optsTransfer.nIdxCurrentMsgBlockStart, optsTransfer.jarrMessages.length,
+ optsTransfer.jarrMessages );
+ optsTransfer.details.debug( strDidInvokedSigningCallbackMessage );
+ if( err ) {
+ optsTransfer.bErrorInSigningMessages = true;
+ optsTransfer.details.critical( "{p}Error signing messages: {err}",
+ optsTransfer.strLogPrefix, err );
+ imaTransferErrorHandling.saveTransferError(
+ optsTransfer.strTransferErrorCategoryName, optsTransfer.details.toString() );
+ return false;
+ }
+ if( !loop.checkTimeFraming( null, optsTransfer.strDirection, optsTransfer.joRuntimeOpts ) ) {
+ optsTransfer.details.warning( "{p}Time framing overflow (after signing messages)",
+ optsTransfer.strLogPrefix );
+ imaTransferErrorHandling.saveTransferSuccessAll();
+ return false;
+ }
+ return true;
+}
+
+async function callbackAllMessagesSign(
+ optsTransfer: TTransferOptions, err: Error | string | null,
+ jarrMessages: any[], joGlueResult: any ): Promise {
+ if( !await preCheckAllMessagesSign( optsTransfer, err, jarrMessages, joGlueResult ) )
+ return;
+ const nBlockSize = optsTransfer.arrMessageCounters.length;
+ optsTransfer.strActionName = "dst-chain.MessageProxy.postIncomingMessages()";
+ const strWillCallPostIncomingMessagesAction = log.fmtDebug(
+ "{p}Will call {bright} for block size set to {}, message counters = are {}...",
+ optsTransfer.strLogPrefix, optsTransfer.strActionName,
+ nBlockSize, optsTransfer.arrMessageCounters );
+ optsTransfer.details.debug( strWillCallPostIncomingMessagesAction );
+ let signature: owaspUtils.TXYSignature | null = joGlueResult ? joGlueResult.signature : null;
+ if( !signature )
+ signature = { X: "0", Y: "0" };
+ let hashPoint = joGlueResult ? joGlueResult.hashPoint : null;
+ if( !hashPoint )
+ hashPoint = { X: "0", Y: "0" };
+ let hint = joGlueResult ? joGlueResult.hint : null;
+ if( !hint )
+ hint = "0";
+ const sign: owaspUtils.TBLSSignature = {
+ blsSignature: [ signature.X, signature.Y ], // BLS glue of signatures
+ hashA: hashPoint.X, // G1.X from joGlueResult.hashSrc
+ hashB: hashPoint.Y, // G1.Y from joGlueResult.hashSrc
+ counter: hint
+ };
+ const arrArgumentsPostIncomingMessages = [
+ optsTransfer.chainNameSrc, optsTransfer.nIdxCurrentMsgBlockStart,
+ optsTransfer.jarrMessages, sign ];
+ const joDebugArgs = [
+ optsTransfer.chainNameSrc, optsTransfer.chainNameDst,
+ optsTransfer.nIdxCurrentMsgBlockStart,
+ optsTransfer.jarrMessages, [ signature.X, signature.Y ], // BLS glue of signatures
+ hashPoint.X, // G1.X from joGlueResult.hashSrc
+ hashPoint.Y, // G1.Y from joGlueResult.hashSrc
+ hint ];
+ optsTransfer.details.debug( "{p}....debug args for msgCounter set to {}: {}",
+ optsTransfer.strLogPrefix, optsTransfer.nIdxCurrentMsgBlockStart, joDebugArgs );
+ optsTransfer.strActionName = optsTransfer.strDirection + " - Post incoming messages";
+ const weiHowMuchPostIncomingMessages = undefined;
+ const gasPrice = await optsTransfer.transactionCustomizerDst.computeGasPrice(
+ optsTransfer.ethersProviderDst, 200000000000 );
+ optsTransfer.details.debug( "{p}Using computed gasPrice {}={}",
+ optsTransfer.strLogPrefix, gasPrice );
+ let estimatedGasPostIncomingMessages =
+ await optsTransfer.transactionCustomizerDst.computeGas(
+ optsTransfer.details, optsTransfer.ethersProviderDst,
+ "MessageProxy", optsTransfer.joMessageProxyDst,
+ "postIncomingMessages", arrArgumentsPostIncomingMessages,
+ optsTransfer.joAccountDst, optsTransfer.strActionName,
+ gasPrice, 10000000, weiHowMuchPostIncomingMessages, null );
+ optsTransfer.details.debug( "{p}Using estimated gas={}",
+ optsTransfer.strLogPrefix, estimatedGasPostIncomingMessages );
+ if( optsTransfer.strDirection == "S2M" ) {
+ const expectedGasLimit = perMessageGasForTransfer * optsTransfer.jarrMessages.length +
+ additionalS2MTransferOverhead;
+ estimatedGasPostIncomingMessages =
+ Math.max( estimatedGasPostIncomingMessages, expectedGasLimit );
+ }
+ const isIgnorePostIncomingMessages = false;
+ const strErrorOfDryRun = await imaTx.dryRunCall(
+ optsTransfer.details, optsTransfer.ethersProviderDst,
+ "MessageProxy", optsTransfer.joMessageProxyDst,
+ "postIncomingMessages", arrArgumentsPostIncomingMessages,
+ optsTransfer.joAccountDst, optsTransfer.strActionName,
+ isIgnorePostIncomingMessages,
+ gasPrice, estimatedGasPostIncomingMessages,
+ weiHowMuchPostIncomingMessages, null );
+ if( strErrorOfDryRun )
+ throw new Error( strErrorOfDryRun );
+ const opts: imaTx.TCustomPayedCallOptions = {
+ isCheckTransactionToSchain:
+ ( optsTransfer.chainNameDst !== "Mainnet" )
+ };
+ const joReceipt = await imaTx.payedCall(
+ optsTransfer.details, optsTransfer.ethersProviderDst,
+ "MessageProxy", optsTransfer.joMessageProxyDst,
+ "postIncomingMessages", arrArgumentsPostIncomingMessages,
+ optsTransfer.joAccountDst, optsTransfer.strActionName,
+ gasPrice, estimatedGasPostIncomingMessages,
+ weiHowMuchPostIncomingMessages, opts );
+ if( joReceipt ) {
+ optsTransfer.jarrReceipts.push( {
+ description: "doTransfer/postIncomingMessages()",
+ "optsTransfer.detailsString": optsTransfer.strGatheredDetailsName,
+ receipt: joReceipt
+ } );
+ imaGasUsage.printGasUsageReportFromArray( "(intermediate result) TRANSFER " +
+ optsTransfer.chainNameSrc + " -> " + optsTransfer.chainNameDst,
+ optsTransfer.jarrReceipts, optsTransfer.details );
+ }
+ optsTransfer.cntProcessed += optsTransfer.cntAccumulatedForBlock;
+ optsTransfer.details.information( "{p}Validating transfer from {} to {}...",
+ optsTransfer.strLogPrefix, optsTransfer.chainNameSrc, optsTransfer.chainNameDst );
+ // check DepositBox -> Error on Mainnet only
+ if( optsTransfer.chainNameDst == "Mainnet" ) {
+ optsTransfer.details.debug( "{p}Validating transfer to Main Net via MessageProxy error " +
+ "absence on Main Net...", optsTransfer.strLogPrefix );
+ if( optsTransfer.joDepositBoxMainNet ) {
+ if( joReceipt && "blockNumber" in joReceipt && "transactionHash" in joReceipt ) {
+ const strEventName = "PostMessageError";
+ optsTransfer.details.debug(
+ "{p}Verifying the {} event of the MessageProxy/{} contract...",
+ optsTransfer.strLogPrefix, strEventName,
+ optsTransfer.joMessageProxyDst.address );
+ const joEvents = await imaEventLogScan.getContractCallEvents(
+ optsTransfer.details, optsTransfer.strLogPrefixShort,
+ optsTransfer.ethersProviderDst, optsTransfer.joMessageProxyDst, strEventName,
+ joReceipt.blockNumber, joReceipt.transactionHash,
+ optsTransfer.joMessageProxyDst.filters[strEventName]() );
+ if( joEvents.length == 0 ) {
+ optsTransfer.details.success(
+ "{p}Success, verified the {} event of the MessageProxy/{} contract, " +
+ "no events found", optsTransfer.strLogPrefix, strEventName,
+ optsTransfer.joMessageProxyDst.address );
+ } else {
+ optsTransfer.details.critical(
+ "{p}Failed verification of the PostMessageError event of the " +
+ "MessageProxy/{} contract, found event(s): {}", optsTransfer.strLogPrefix,
+ optsTransfer.joMessageProxyDst.address, joEvents );
+ imaTransferErrorHandling.saveTransferError(
+ optsTransfer.strTransferErrorCategoryName,
+ optsTransfer.details.toString() );
+ throw new Error( "Verification failed for the PostMessageError event " +
+ `of the MessageProxy ${optsTransfer.joMessageProxyDst.address} ` +
+ "contract, error events found" );
+ }
+ optsTransfer.details.success( "{p}Done, validated transfer to Main Net via " +
+ "MessageProxy error absence on Main Net", optsTransfer.strLogPrefix );
+ } else {
+ optsTransfer.details.warning( "{p}Cannot validate transfer to Main Net via " +
+ "MessageProxy error absence on Main Net, no valid transaction receipt provided",
+ optsTransfer.strLogPrefix );
+ }
+ } else {
+ optsTransfer.details.warning( "{p}Cannot validate transfer to Main Net via " +
+ "MessageProxy error absence on Main Net, no MessageProxy provided",
+ optsTransfer.strLogPrefix );
+ }
+ }
+}
+
+async function handleAllMessagesSigning( optsTransfer: TTransferOptions ): Promise {
+ try {
+ let strErrFinal: string = "";
+ await optsTransfer.fnSignMessages( optsTransfer.nTransferLoopCounter,
+ optsTransfer.jarrMessages, optsTransfer.nIdxCurrentMsgBlockStart,
+ optsTransfer.chainNameSrc, optsTransfer.joExtraSignOpts,
+ async function( err: Error | string | null, jarrMessages: any[], joGlueResult: any ) {
+ await callbackAllMessagesSign( optsTransfer, err, jarrMessages, joGlueResult );
+ } ).catch( function( err: Error | string ): void {
+ // callback fn as argument of optsTransfer.fnSignMessages
+ optsTransfer.bErrorInSigningMessages = true;
+ optsTransfer.details.error( "{p}Problem in transfer handler(in signer): {err}",
+ optsTransfer.strLogPrefix, err );
+ imaTransferErrorHandling.saveTransferError(
+ optsTransfer.strTransferErrorCategoryName, optsTransfer.details.toString() );
+ strErrFinal = err.toString();
+ } );
+ if( strErrFinal )
+ throw new Error( strErrFinal );
+ return true;
+ } catch ( err ) {
+ optsTransfer.details.error( "{p}Problem in transfer handler(general): {err}",
+ optsTransfer.strLogPrefix, err );
+ imaTransferErrorHandling.saveTransferError( optsTransfer.strTransferErrorCategoryName,
+ optsTransfer.details.toString() );
+ return false;
+ }
+}
+
+async function checkOutgoingMessageEventInOneNode(
+ optsTransfer: TTransferOptions,
+ optsOutgoingMessageAnalysis: TOutgoingMessageAnalysisOptions
+): Promise {
+ if( !optsOutgoingMessageAnalysis.joNode ) {
+ optsTransfer.details.error(
+ "{p}{bright} no S-Chain node provided",
+ optsTransfer.strLogPrefix, optsTransfer.strDirection );
+ return false;
+ }
+ const sc = optsTransfer.imaState.chainProperties.sc;
+ const strUrlHttp = optsOutgoingMessageAnalysis.joNode.endpoints.ip.http;
+ optsTransfer.details.trace(
+ "{p}Validating {bright} message {} on node {} using URL {url}...",
+ optsTransfer.strLogPrefix, optsTransfer.strDirection,
+ optsOutgoingMessageAnalysis.idxMessage + 1,
+ optsOutgoingMessageAnalysis.joNode.name, strUrlHttp );
+ const joMessage = optsOutgoingMessageAnalysis.joMessage;
+ let bEventIsFound = false;
+ try {
+ const ethersProviderNode = owaspUtils.getEthersProviderFromURL( strUrlHttp );
+ const joMessageProxyNode: owaspUtils.ethersMod.ethers.Contract =
+ new owaspUtils.ethersMod.ethers.Contract(
+ sc.joAbiIMA.message_proxy_chain_address,
+ sc.joAbiIMA.message_proxy_chain_abi,
+ ethersProviderNode );
+ const strEventName = "OutgoingMessage";
+ const nodeRV = await imaEventLogScan.safeGetPastEventsProgressive(
+ optsTransfer.details, optsTransfer.strLogPrefixShort, ethersProviderNode,
+ 10, joMessageProxyNode, strEventName,
+ joMessage.savedBlockNumberForOptimizations,
+ joMessage.savedBlockNumberForOptimizations,
+ joMessageProxyNode.filters[strEventName](
+ owaspUtils.ethersMod.ethers.utils.id( optsTransfer.chainNameDst ),
+ owaspUtils.toBN( optsOutgoingMessageAnalysis.idxImaMessage ) ) );
+ const cntEvents = nodeRV.length;
+ optsTransfer.details.trace(
+ "{p}Got {} event(s) ({}) on node {} with data: {}",
+ optsTransfer.strLogPrefix, cntEvents, strEventName,
+ optsOutgoingMessageAnalysis.joNode.name, nodeRV );
+ for( let idxEvent = 0; idxEvent < cntEvents; ++idxEvent ) {
+ const joEvent = nodeRV[idxEvent];
+ const eventValuesByName: any = {
+ dstChainHash: joEvent.args[0],
+ msgCounter: joEvent.args[1],
+ srcContract: joEvent.args[2],
+ dstContract: joEvent.args[3],
+ data: joEvent.args[4]
+ };
+ if( owaspUtils.ensureStartsWith0x( joMessage.sender ).toLowerCase() ==
+ owaspUtils.ensureStartsWith0x( eventValuesByName.srcContract ).toLowerCase() &&
+ owaspUtils.ensureStartsWith0x( joMessage.destinationContract ).toLowerCase() ==
+ owaspUtils.ensureStartsWith0x( eventValuesByName.dstContract ).toLowerCase()
+ ) {
+ bEventIsFound = true;
+ break;
+ }
+ }
+ } catch ( err ) {
+ ++optsOutgoingMessageAnalysis.cntFailedNodes;
+ optsTransfer.details.error(
+ "{p}{bright} message analysis error: Failed to scan events on node {}, " +
+ "detailed node description is: {}, error is: {err}, stack is: ",
+ optsTransfer.strLogPrefix, optsTransfer.strDirection,
+ optsOutgoingMessageAnalysis.joNode.name, optsOutgoingMessageAnalysis.joNode,
+ err, err );
+ return true; // continue nodes analysis
+ }
+ if( bEventIsFound ) {
+ ++optsOutgoingMessageAnalysis.cntPassedNodes;
+ optsTransfer.details.success(
+ "{p}{bright} message {} validation on node {} using URL {url} is passed",
+ optsTransfer.strLogPrefix, optsTransfer.strDirection,
+ optsOutgoingMessageAnalysis.idxMessage + 1,
+ optsOutgoingMessageAnalysis.joNode.name, strUrlHttp );
+ } else {
+ ++optsOutgoingMessageAnalysis.cntFailedNodes;
+ optsTransfer.details.error(
+ "{p}{bright} message {} validation on node {} using URL {url} is failed",
+ optsTransfer.strLogPrefix, optsTransfer.strDirection,
+ optsOutgoingMessageAnalysis.idxMessage + 1,
+ optsOutgoingMessageAnalysis.joNode.name, strUrlHttp );
+ }
+ if( optsOutgoingMessageAnalysis.cntFailedNodes > optsTransfer.cntNodesMayFail )
+ return false;
+ if( optsOutgoingMessageAnalysis.cntPassedNodes >= optsTransfer.cntNodesShouldPass ) {
+ optsTransfer.details.information(
+ "{p}{bright} message {} validation on node {} using URL {url} is passed",
+ optsTransfer.strLogPrefix, optsTransfer.strDirection,
+ optsOutgoingMessageAnalysis.idxMessage + 1,
+ optsOutgoingMessageAnalysis.joNode.name, strUrlHttp );
+ return false;
+ }
+ return true;
+}
+
+async function checkOutgoingMessageEvent(
+ optsTransfer: TTransferOptions, joSChain: skaleObserver.TSChainInformation ): Promise {
+ const cntNodes = joSChain.nodes.length;
+ const cntMessages = optsTransfer.jarrMessages.length;
+ for( let idxMessage = 0; idxMessage < cntMessages; ++idxMessage ) {
+ const idxImaMessage = optsTransfer.arrMessageCounters[idxMessage];
+ const joMessage = optsTransfer.jarrMessages[idxMessage];
+ optsTransfer.details.trace(
+ "{p}{bright} message analysis for message {} of {} with IMA message index {} and " +
+ "message envelope data: {}", optsTransfer.strLogPrefix, optsTransfer.strDirection,
+ idxMessage + 1, cntMessages, idxImaMessage, joMessage );
+ const optsOutgoingMessageAnalysis: TOutgoingMessageAnalysisOptions = {
+ idxMessage,
+ idxImaMessage,
+ joMessage,
+ joNode: null,
+ idxNode: 0,
+ cntNodes,
+ cntPassedNodes: 0,
+ cntFailedNodes: 0
+ };
+ try {
+ for( optsOutgoingMessageAnalysis.idxNode = 0;
+ optsOutgoingMessageAnalysis.idxNode < cntNodes;
+ ++optsOutgoingMessageAnalysis.idxNode
+ ) {
+ optsOutgoingMessageAnalysis.joNode = joSChain.nodes[
+ optsOutgoingMessageAnalysis.idxNode];
+ const isContinueNodesAnalysis = await checkOutgoingMessageEventInOneNode(
+ optsTransfer, optsOutgoingMessageAnalysis );
+ if( !isContinueNodesAnalysis )
+ break;
+ }
+ } catch ( err ) {
+ const strUrlHttp = optsOutgoingMessageAnalysis.joNode
+ ? optsOutgoingMessageAnalysis.joNode.endpoints.ip.http
+ : "";
+ const strNodeName = optsOutgoingMessageAnalysis.joNode
+ ? log.fmtInformation( optsOutgoingMessageAnalysis.joNode.name )
+ : log.fmtError( "<>" );
+ optsTransfer.details.critical(
+ "{p}{bright} message analysis error: Failed to process events for {} message {} " +
+ "on node {} using URL {}, error is: {err}, stack is:\n{stack}",
+ optsTransfer.strLogPrefix, optsTransfer.strDirection, optsTransfer.strDirection,
+ idxMessage + 1, strNodeName,
+ log.posNeg( optsOutgoingMessageAnalysis.joNode,
+ log.u( strUrlHttp ), "<>" ),
+ err, err );
+ }
+ if( optsOutgoingMessageAnalysis.cntFailedNodes > optsTransfer.cntNodesMayFail ) {
+ optsTransfer.details.critical(
+ "{p}Error validating {bright} messages, failed node count {} is greater then " +
+ "allowed to fail {}", optsTransfer.strLogPrefix, optsTransfer.strDirection,
+ optsOutgoingMessageAnalysis.cntFailedNodes, optsTransfer.cntNodesMayFail );
+ optsTransfer.details.exposeDetailsTo(
+ log.globalStream(), optsTransfer.strGatheredDetailsName, false );
+ imaTransferErrorHandling.saveTransferError(
+ optsTransfer.strTransferErrorCategoryName,
+ optsTransfer.details.toString() );
+ optsTransfer.details.close();
+ return false;
+ }
+ if( !( optsOutgoingMessageAnalysis.cntPassedNodes >= optsTransfer.cntNodesShouldPass ) ) {
+ optsTransfer.details.critical(
+ "{p}Error validating {bright} messages, passed node count {} is less then " +
+ "needed count {}", optsTransfer.strLogPrefix, optsTransfer.strDirection,
+ optsOutgoingMessageAnalysis.cntFailedNodes, optsTransfer.cntNodesShouldPass );
+ optsTransfer.details.exposeDetailsTo(
+ log.globalStream(), optsTransfer.strGatheredDetailsName, false );
+ imaTransferErrorHandling.saveTransferError(
+ optsTransfer.strTransferErrorCategoryName, optsTransfer.details.toString() );
+ optsTransfer.details.close();
+ return false;
+ }
+ }
+ return true;
+}
+
+async function doMainTransferLoopActions( optsTransfer: TTransferOptions ): Promise {
+ // classic scanner with optional usage of optimized IMA messages search algorithm
+ // outer loop is block former/creator, then transfer
+ optsTransfer.nIdxCurrentMsg = optsTransfer.nIncMsgCnt;
+ while( optsTransfer.nIdxCurrentMsg < optsTransfer.nOutMsgCnt ) {
+ if( optsTransfer.nStepsDone > optsTransfer.nTransferSteps ) {
+ optsTransfer.details.warning( "{p}Transfer step count overflow",
+ optsTransfer.strLogPrefix );
+ optsTransfer.details.close();
+ imaTransferErrorHandling.saveTransferSuccessAll();
+ return false;
+ }
+ optsTransfer.details.trace(
+ "{p}Entering block former iteration with message counter set to {}, transfer step " +
+ "number is {}, can transfer up to {} message(s) per step, can perform up to {} " +
+ "transfer step(s)", optsTransfer.strLogPrefix, optsTransfer.nIdxCurrentMsg,
+ optsTransfer.nStepsDone, optsTransfer.nMaxTransactionsCount,
+ optsTransfer.nTransferSteps );
+ if( !loop.checkTimeFraming(
+ null, optsTransfer.strDirection, optsTransfer.joRuntimeOpts ) ) {
+ optsTransfer.details.warning( "{p}WARNING: Time framing overflow" +
+ "(after entering block former iteration loop)", optsTransfer.strLogPrefix );
+ optsTransfer.details.close();
+ imaTransferErrorHandling.saveTransferSuccessAll();
+ return false;
+ }
+ await gatherMessages( optsTransfer );
+ if( optsTransfer.cntAccumulatedForBlock == 0 )
+ break;
+ if( !loop.checkTimeFraming(
+ null, optsTransfer.strDirection, optsTransfer.joRuntimeOpts ) ) {
+ optsTransfer.details.warning( "{p}Time framing overflow" +
+ "(after forming block of messages)", optsTransfer.strLogPrefix );
+ optsTransfer.details.close();
+ imaTransferErrorHandling.saveTransferSuccessAll();
+ return false;
+ }
+ if( optsTransfer.strDirection == "S2S" ) {
+ optsTransfer.strActionName = "S2S message analysis";
+ if( !optsTransfer.joExtraSignOpts ) {
+ throw new Error( "Could not validate S2S messages, " +
+ "no extra options provided to transfer algorithm" );
+ }
+ const arrSChainsCached = skaleObserver.getLastCachedSChains();
+ if( ( !arrSChainsCached ) || arrSChainsCached.length == 0 ) {
+ throw new Error( "Could not validate S2S messages, " +
+ "no S-Chains in SKALE NETWORK observer cached yet, try again later" );
+ }
+ const idxSChain = skaleObserver.findSChainIndexInArrayByName(
+ arrSChainsCached, optsTransfer.chainNameSrc );
+ if( idxSChain < 0 ) {
+ throw new Error( "Could not validate S2S messages, source " +
+ `S-Chain ${optsTransfer.chainNameSrc} is not in SKALE NETWORK observer ` +
+ `cache yet or it's not connected to this ${optsTransfer.chainNameDst} ` +
+ "S-Chain yet, try again later" );
+ }
+ const cntMessages = optsTransfer.jarrMessages.length;
+ const joSChain = arrSChainsCached[idxSChain];
+ const cntNodes = joSChain.nodes.length;
+ optsTransfer.cntNodesShouldPass = Math.ceil( ( cntNodes * 2 ) / 3 );
+ optsTransfer.cntNodesMayFail = cntNodes - optsTransfer.cntNodesShouldPass;
+ optsTransfer.details.trace(
+ "{p}{bright} message analysis will be performed on S-Chain {} with {} node(s), " +
+ "{} node(s) should have same message(s), {} node(s) allowed to fail message(s) " +
+ "comparison, {} message(s) to check...", optsTransfer.strLogPrefix,
+ optsTransfer.strDirection, optsTransfer.chainNameSrc, cntNodes,
+ optsTransfer.cntNodesShouldPass, optsTransfer.cntNodesMayFail, cntMessages );
+ if( !( await checkOutgoingMessageEvent( optsTransfer, joSChain ) ) )
+ return false;
+ }
+
+ optsTransfer.strActionName = "sign messages";
+ const strWillInvokeSigningCallbackMessage = log.fmtDebug(
+ "{p}Will invoke message signing callback, first real message index is: {}, have {} " +
+ "message(s) to process {}", optsTransfer.strLogPrefix,
+ optsTransfer.nIdxCurrentMsgBlockStart, optsTransfer.jarrMessages.length,
+ optsTransfer.jarrMessages );
+ optsTransfer.details.information( strWillInvokeSigningCallbackMessage );
+ // will re-open optsTransfer.details B log here for next step,
+ // it can be delayed so we will flush accumulated optsTransfer.details A now
+ if( log.exposeDetailsGet() && optsTransfer.details.exposeDetailsTo ) {
+ optsTransfer.details.exposeDetailsTo(
+ log.globalStream(), optsTransfer.strGatheredDetailsName, true );
+ }
+
+ optsTransfer.details.close();
+ optsTransfer.details = optsTransfer.imaState.isDynamicLogInDoTransfer
+ ? log.globalStream()
+ : log.createMemoryStream();
+ optsTransfer.strGatheredDetailsName = `${optsTransfer.strDirection}/#` +
+ `${optsTransfer.nTransferLoopCounter}-doTransfer-B-${optsTransfer.chainNameSrc}` +
+ `-->${optsTransfer.chainNameDst}`;
+ try {
+ if( !( await handleAllMessagesSigning( optsTransfer ) ) )
+ return false;
+ } catch ( err ) {
+ optsTransfer.details.critical(
+ "{p}Exception from signing messages function: {err}, stack is:\n{stack}",
+ optsTransfer.strLogPrefix, err, err );
+ }
+ if( optsTransfer.bErrorInSigningMessages )
+ break;
+ ++optsTransfer.nStepsDone;
+ }
+ return true;
+}
+
+let gIsOneTransferInProgressInThisThread = false;
+
+export async function doTransfer(
+ strDirection: string, joRuntimeOpts: loop.TRuntimeOpts,
+ ethersProviderSrc: owaspUtils.ethersMod.ethers.providers.JsonRpcProvider,
+ joMessageProxySrc: owaspUtils.ethersMod.ethers.Contract, joAccountSrc: state.TAccount,
+ ethersProviderDst: owaspUtils.ethersMod.ethers.providers.JsonRpcProvider,
+ joMessageProxyDst: owaspUtils.ethersMod.ethers.Contract, joAccountDst: state.TAccount,
+ chainNameSrc: string, chainNameDst: string, chainIdSrc: string, chainIdDst: string,
+ joDepositBoxMainNet:
+ owaspUtils.ethersMod.ethers.Contract | null, // for logs validation on mainnet
+ joTokenManagerSChain:
+ owaspUtils.ethersMod.ethers.Contract | null, // for logs validation on s-chain
+ nTransactionsCountInBlock: number,
+ nTransferSteps: number, nMaxTransactionsCount: number,
+ nBlockAwaitDepth: number, nBlockAge: number,
+ fnSignMessages: TFunctionDoSignMessages, joExtraSignOpts: loop.TExtraSignOpts | null,
+ transactionCustomizerDst: imaTx.TransactionCustomizer
+): Promise {
+ const optsTransfer: TTransferOptions = {
+ strDirection,
+ joRuntimeOpts,
+ ethersProviderSrc,
+ joMessageProxySrc,
+ joAccountSrc,
+ ethersProviderDst,
+ joMessageProxyDst,
+ joAccountDst,
+ chainNameSrc,
+ chainNameDst,
+ chainIdSrc,
+ chainIdDst,
+ joDepositBoxMainNet, // for logs validation on mainnet
+ joTokenManagerSChain, // for logs validation on s-chain
+ nTransactionsCountInBlock,
+ nTransferSteps,
+ nMaxTransactionsCount,
+ nBlockAwaitDepth,
+ nBlockAge,
+ fnSignMessages,
+ joExtraSignOpts,
+ transactionCustomizerDst,
+ imaState: state.get(),
+ nTransferLoopCounter: owaspUtils.toInteger( gTransferLoopCounter ),
+ strTransferErrorCategoryName: "loop-" + strDirection,
+ strGatheredDetailsName: "",
+ details: log.globalStream(),
+ jarrReceipts: [],
+ bErrorInSigningMessages: false,
+ strLogPrefixShort: "",
+ strLogPrefix: "",
+ nStepsDone: 0,
+ strActionName: "",
+ nIdxCurrentMsg: 0,
+ nOutMsgCnt: 0,
+ nIncMsgCnt: 0,
+ cntProcessed: 0,
+ arrMessageCounters: [],
+ jarrMessages: [],
+ nIdxCurrentMsgBlockStart: 0,
+ cntAccumulatedForBlock: 0,
+ arrLogRecordReferences: [],
+ cntNodesShouldPass: 0,
+ cntNodesMayFail: 0
+ };
+ ++gTransferLoopCounter;
+ optsTransfer.strGatheredDetailsName =
+ `${optsTransfer.strDirection}/#${optsTransfer.nTransferLoopCounter}-doTransfer-A-` +
+ `${optsTransfer.chainNameSrc}-->${optsTransfer.chainNameDst}`;
+ optsTransfer.details = optsTransfer.imaState.isDynamicLogInDoTransfer
+ ? log.globalStream()
+ : log.createMemoryStream();
+ optsTransfer.strLogPrefixShort =
+ `${optsTransfer.strDirection}/#${optsTransfer.nTransferLoopCounter} `;
+ optsTransfer.strLogPrefix = `${optsTransfer.strLogPrefixShort}transfer loop from ` +
+ `${optsTransfer.chainNameSrc} to ${optsTransfer.chainNameDst}: `;
+ if( gIsOneTransferInProgressInThisThread ) {
+ optsTransfer.details.warning( "{p}Transfer loop step is skipped because previous one " +
+ "is still in progress", optsTransfer.strLogPrefix );
+ if( log.exposeDetailsGet() && optsTransfer.details.exposeDetailsTo ) {
+ optsTransfer.details.exposeDetailsTo(
+ log.globalStream(), optsTransfer.strGatheredDetailsName, true );
+ }
+ optsTransfer.details.close();
+ return false;
+ }
+ try {
+ gIsOneTransferInProgressInThisThread = true;
+ optsTransfer.details.debug( "{p}Message signing is {oo}",
+ optsTransfer.strLogPrefix, optsTransfer.imaState.bSignMessages );
+ if( optsTransfer.fnSignMessages == null || optsTransfer.fnSignMessages == undefined ||
+ ( !optsTransfer.imaState.bSignMessages )
+ ) {
+ optsTransfer.details.debug( "{p}Using internal signing stub function",
+ optsTransfer.strLogPrefix );
+ optsTransfer.fnSignMessages = async function(
+ nTransferLoopCounter: number, jarrMessages: any[],
+ nIdxCurrentMsgBlockStart: number, strFromChainName: string,
+ joExtraSignOpts?: loop.TExtraSignOpts | null,
+ fnAfter?: TFunctionAfterSigningMessages
+ ) {
+ optsTransfer.details.debug(
+ "{p}Message signing callback was not provided to IMA, first real message " +
+ "index is: {}, have {} message(s) to process {}", optsTransfer.strLogPrefix,
+ nIdxCurrentMsgBlockStart, optsTransfer.jarrMessages.length,
+ optsTransfer.jarrMessages );
+ if( fnAfter ) // null - no error, null - no signatures
+ await fnAfter( null, jarrMessages, null );
+ };
+ } else {
+ optsTransfer.details.debug( "{p}Using externally provided signing function",
+ optsTransfer.strLogPrefix );
+ }
+ optsTransfer.nTransactionsCountInBlock = optsTransfer.nTransactionsCountInBlock || 5;
+ optsTransfer.nTransferSteps = optsTransfer.nTransferSteps || Number.MAX_SAFE_INTEGER;
+ optsTransfer.nMaxTransactionsCount =
+ optsTransfer.nMaxTransactionsCount || Number.MAX_SAFE_INTEGER;
+ if( optsTransfer.nTransactionsCountInBlock < 1 )
+ optsTransfer.nTransactionsCountInBlock = 1;
+ if( optsTransfer.nBlockAwaitDepth < 0 )
+ optsTransfer.nBlockAwaitDepth = 0;
+ if( optsTransfer.nBlockAge < 0 )
+ optsTransfer.nBlockAge = 0;
+ try {
+ if( !( await doQueryOutgoingMessageCounter( optsTransfer ) ) ) {
+ gIsOneTransferInProgressInThisThread = false;
+ return false;
+ }
+ if( !( await doMainTransferLoopActions( optsTransfer ) ) ) {
+ gIsOneTransferInProgressInThisThread = false;
+ return false;
+ }
+ } catch ( err ) {
+ optsTransfer.details.critical( "{p}Error in {} during {bright}: {err}, " +
+ "stack is:\n{stack}", optsTransfer.strLogPrefix,
+ optsTransfer.strGatheredDetailsName, optsTransfer.strActionName, err, err );
+ optsTransfer.details.exposeDetailsTo( log.globalStream(),
+ optsTransfer.strGatheredDetailsName, false );
+ imaTransferErrorHandling.saveTransferError(
+ optsTransfer.strTransferErrorCategoryName, optsTransfer.details.toString() );
+ optsTransfer.details.close();
+ gIsOneTransferInProgressInThisThread = false;
+ return false;
+ }
+ imaGasUsage.printGasUsageReportFromArray( "TRANSFER " + optsTransfer.chainNameSrc +
+ " to " + optsTransfer.chainNameDst, optsTransfer.jarrReceipts, optsTransfer.details );
+ if( optsTransfer.details ) {
+ if( log.exposeDetailsGet() && optsTransfer.details.exposeDetailsTo ) {
+ optsTransfer.details.exposeDetailsTo(
+ log.globalStream(), optsTransfer.strGatheredDetailsName, true );
+ }
+ optsTransfer.details.close();
+ }
+ if( !optsTransfer.bErrorInSigningMessages ) {
+ imaTransferErrorHandling.saveTransferSuccess(
+ optsTransfer.strTransferErrorCategoryName );
+ }
+ gIsOneTransferInProgressInThisThread = false;
+ return true;
+ } catch ( err ) {
+ gIsOneTransferInProgressInThisThread = false;
+ optsTransfer.details.error(
+ "{p}Transfer loop step failed with error: {err} in {}, stack is:\n{stack}",
+ optsTransfer.strLogPrefix, err, threadInfo.threadDescription(), err );
+ if( log.exposeDetailsGet() && optsTransfer.details.exposeDetailsTo ) {
+ optsTransfer.details.exposeDetailsTo(
+ log.globalStream(), optsTransfer.strGatheredDetailsName, true );
+ }
+ optsTransfer.details.close();
+ return false;
+ }
+}
+
+export async function doAllS2S( // s-chain --> s-chain
+ joRuntimeOpts: loop.TRuntimeOpts,
+ imaState: state.TIMAState,
+ skaleObserver: any,
+ ethersProviderDst: owaspUtils.ethersMod.ethers.providers.JsonRpcProvider,
+ joMessageProxyDst: owaspUtils.ethersMod.ethers.Contract,
+ joAccountDst: state.TAccount,
+ chainNameDst: string,
+ chainIdDst: string,
+ joTokenManagerSChain: owaspUtils.ethersMod.ethers.Contract, // for logs validation on s-chain
+ nTransactionsCountInBlock: number,
+ nTransferSteps: number,
+ nMaxTransactionsCount: number,
+ nBlockAwaitDepth: number,
+ nBlockAge: number,
+ fnSignMessages: TFunctionDoSignMessages,
+ transactionCustomizerDst: imaTx.TransactionCustomizer
+): Promise {
+ let cntOK = 0; let cntFail = 0; let nIndexS2S = 0;
+ const sc = imaState.chainProperties.sc;
+ const strDirection = "S2S";
+ const arrSChainsCached = skaleObserver.getLastCachedSChains();
+ const cntSChains = arrSChainsCached.length;
+ log.information( "Have {} S-Chain(s) connected to this S-Chain for performing " +
+ "S2S transfers in {}.", cntSChains, threadInfo.threadDescription() );
+ for( let idxSChain = 0; idxSChain < cntSChains; ++idxSChain ) {
+ const joSChain = arrSChainsCached[idxSChain];
+ const urlSrc = skaleObserver.pickRandomSChainUrl( joSChain );
+ const ethersProviderSrc: owaspUtils.ethersMod.ethers.providers.JsonRpcProvider =
+ owaspUtils.getEthersProviderFromURL( urlSrc );
+ const joAccountSrc = joAccountDst; // ???
+ const chainNameSrc = joSChain.name;
+ const chainIdSrc = joSChain.chainId;
+ log.information( "S2S transfer walk trough {}/{} S-Chain in {}...",
+ chainNameSrc, chainIdSrc, threadInfo.threadDescription() );
+ let bOK = false;
+ try {
+ nIndexS2S = idxSChain;
+ if( !await pwa.checkOnLoopStart( imaState, "s2s", nIndexS2S ) ) {
+ imaState.loopState.s2s.wasInProgress = false;
+ log.notice( "Skipped(s2s) due to cancel mode reported from PWA in {}",
+ threadInfo.threadDescription() );
+ } else {
+ if( loop.checkTimeFraming( null, "s2s", joRuntimeOpts ) ) {
+ // ??? assuming all S-Chains have same ABIs here
+ const joMessageProxySrc: owaspUtils.ethersMod.ethers.Contract =
+ new owaspUtils.ethersMod.ethers.Contract(
+ sc.joAbiIMA.message_proxy_chain_address,
+ sc.joAbiIMA.message_proxy_chain_abi,
+ ethersProviderSrc );
+ const joDepositBoxSrc: owaspUtils.ethersMod.ethers.Contract =
+ new owaspUtils.ethersMod.ethers.Contract(
+ sc.joAbiIMA.message_proxy_chain_address,
+ sc.joAbiIMA.message_proxy_chain_abi,
+ ethersProviderSrc );
+ const joExtraSignOpts: loop.TExtraSignOpts = {
+ chainNameSrc,
+ chainIdSrc,
+ chainNameDst,
+ chainIdDst,
+ joAccountSrc,
+ joAccountDst,
+ ethersProviderSrc,
+ ethersProviderDst
+ };
+ joRuntimeOpts.idxChainKnownForS2S = idxSChain;
+ joRuntimeOpts.cntChainsKnownForS2S = cntSChains;
+ joRuntimeOpts.joExtraSignOpts = joExtraSignOpts;
+
+ imaState.loopState.s2s.isInProgress = true;
+ await pwa.notifyOnLoopStart( imaState, "s2s", nIndexS2S );
+
+ bOK = await doTransfer(
+ strDirection,
+ joRuntimeOpts,
+ ethersProviderSrc,
+ joMessageProxySrc,
+ joAccountSrc,
+ ethersProviderDst,
+ joMessageProxyDst,
+ joAccountDst,
+ chainNameSrc,
+ chainNameDst,
+ chainIdSrc,
+ chainIdDst,
+ joDepositBoxSrc, // for logs validation on mainnet or source S-Chain
+ joTokenManagerSChain, // for logs validation on s-chain
+ nTransactionsCountInBlock,
+ nTransferSteps,
+ nMaxTransactionsCount,
+ nBlockAwaitDepth,
+ nBlockAge,
+ fnSignMessages,
+ joExtraSignOpts,
+ transactionCustomizerDst );
+ imaState.loopState.s2s.isInProgress = false;
+ await pwa.notifyOnLoopEnd( imaState, "s2s", nIndexS2S );
+ } else {
+ bOK = true;
+ const strLogPrefix = "S2S Loop: ";
+ log.notice( "Skipped(s2s) in {} due to time framing check",
+ strLogPrefix, threadInfo.threadDescription() );
+ }
+ }
+ } catch ( err ) {
+ bOK = false;
+ log.error( "S2S step error from S-Chain {}, error is: {err} in {}, stack is:\n{stack}",
+ chainNameSrc, err, threadInfo.threadDescription(), err );
+ imaState.loopState.s2s.isInProgress = false;
+ await pwa.notifyOnLoopEnd( imaState, "s2s", nIndexS2S );
+ }
+ if( bOK )
+ ++cntOK;
+ else
+ ++cntFail;
+ }
+ joRuntimeOpts.idxChainKnownForS2S = 0; // reset/clear
+ joRuntimeOpts.cntChainsKnownForS2S = 0; // reset/clear
+ if( "joExtraSignOpts" in joRuntimeOpts )
+ delete joRuntimeOpts.joExtraSignOpts; // reset/clear
+ if( cntOK > 0 || cntFail > 0 ) {
+ let s = log.fmtDebug( "Stats for S2S steps in {}: ", threadInfo.threadDescription() );
+ if( cntOK > 0 ) {
+ s += " " + log.fmtInformation( "{p}", cntOK ) + " " +
+ log.fmtSuccess( "S-Chain(s) processed OKay" ) + log.fmtDebug( ", " );
+ }
+ if( cntFail > 0 ) {
+ s += " " + log.fmtInformation( "{p}", cntFail ) + " " +
+ log.fmtError( "S-Chain(s) failed" );
+ }
+ log.debug( s );
+ }
+ return ( cntFail == 0 );
+}
diff --git a/src/imaEthOperations.ts b/src/imaEthOperations.ts
new file mode 100644
index 00000000..fa8dedc6
--- /dev/null
+++ b/src/imaEthOperations.ts
@@ -0,0 +1,348 @@
+// 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 .
+ */
+
+/**
+ * @file imaEthOperations.ts
+ * @copyright SKALE Labs 2019-Present
+ */
+
+import * as log from "./log.js";
+import * as owaspUtils from "./owaspUtils.js";
+import * as imaHelperAPIs from "./imaHelperAPIs.js";
+import * as imaTx from "./imaTx.js";
+import * as imaGasUsage from "./imaGasUsageOperations.js";
+import * as imaEventLogScan from "./imaEventLogScan.js";
+import * as threadInfo from "./threadInfo.js";
+import type * as state from "./state.js";
+
+export async function getBalanceEth(
+ isMainNet: boolean,
+ ethersProvider: owaspUtils.ethersMod.ethers.providers.JsonRpcProvider | null,
+ chainId: string,
+ joAccount: state.TAccount | null,
+ contractERC20: owaspUtils.ethersMod.Contract | null
+): Promise {
+ const strLogPrefix = "getBalanceEth() call ";
+ try {
+ if( !( ethersProvider && joAccount ) )
+ return "";
+ const strAddress = joAccount.address();
+ if( ( !isMainNet ) && contractERC20 ) {
+ const balance =
+ await contractERC20.callStatic.balanceOf( strAddress, { from: strAddress } );
+ return balance;
+ }
+ const balance = await ethersProvider.getBalance( strAddress );
+ return balance;
+ } catch ( err ) {
+ log.error( "{p}balance fetching error details: {err}, stack is:\n{stack}",
+ strLogPrefix, err, err );
+ }
+ return "";
+}
+
+// transfer money from main-net to S-chain
+// main-net.DepositBox call: function deposit(string schainName, address to) public payable
+// Where:
+// schainName...obvious
+// to.........address in S-chain
+// Notice:
+// this function is available for everyone in main-net
+// money is sent from caller
+// "value" JSON arg is used to specify amount of money to sent
+export async function doEthPaymentFromMainNet(
+ ethersProviderMainNet: owaspUtils.ethersMod.ethers.providers.JsonRpcProvider,
+ chainIdMainNet: string,
+ joAccountSrc: state.TAccount,
+ joAccountDst: state.TAccount,
+ joDepositBox: owaspUtils.ethersMod.ethers.Contract,
+ joMessageProxyMainNet: owaspUtils.ethersMod.ethers.Contract, // for checking logs
+ chainIdSChain: string,
+ weiHowMuch: any, // how much WEI money to send
+ transactionCustomizerMainNet: imaTx.TransactionCustomizer
+): Promise {
+ const details = log.createMemoryStream();
+ const jarrReceipts: any[] = [];
+ let strActionName = "";
+ const strLogPrefix = "M2S ETH Payment: ";
+ try {
+ details.debug( "{p}Doing payment from mainnet with chainIdSChain={}...",
+ strLogPrefix, chainIdSChain );
+ strActionName = "ETH payment from Main Net, deposit";
+ const arrArguments = [
+ chainIdSChain
+ ];
+ const gasPrice = await transactionCustomizerMainNet.computeGasPrice(
+ ethersProviderMainNet, 200000000000 );
+ details.trace( "{p}Using computed gasPrice={}", strLogPrefix, gasPrice );
+ const estimatedGas = await transactionCustomizerMainNet.computeGas(
+ details, ethersProviderMainNet,
+ "DepositBox", joDepositBox, "deposit", arrArguments,
+ joAccountSrc, strActionName,
+ gasPrice, 3000000 );
+ details.trace( "{p}Using estimated gas={}", strLogPrefix, estimatedGas );
+ const isIgnore = false;
+ const strErrorOfDryRun = await imaTx.dryRunCall(
+ details, ethersProviderMainNet,
+ "DepositBox", joDepositBox, "deposit", arrArguments,
+ joAccountSrc, strActionName, isIgnore,
+ gasPrice, estimatedGas );
+ if( strErrorOfDryRun )
+ throw new Error( strErrorOfDryRun );
+
+ const joReceipt = await imaTx.payedCall(
+ details, ethersProviderMainNet,
+ "DepositBox", joDepositBox, "deposit", arrArguments,
+ joAccountSrc, strActionName,
+ gasPrice, estimatedGas, weiHowMuch );
+ if( joReceipt ) {
+ jarrReceipts.push( {
+ description: "doEthPaymentFromMainNet",
+ receipt: joReceipt
+ } );
+ }
+
+ // Must-have event(s) analysis as indicator(s) of success
+ const strEventName = "OutgoingMessage";
+ if( joMessageProxyMainNet ) {
+ details.debug( "{p}Verifying the {} event of the ", "MessageProxy/{} contract ...",
+ strLogPrefix, strEventName, joMessageProxyMainNet.address );
+ await threadInfo.sleep(
+ imaHelperAPIs.getMillisecondsSleepBeforeFetchOutgoingMessageEvent() );
+ const joEvents = await imaEventLogScan.getContractCallEvents(
+ details, strLogPrefix,
+ ethersProviderMainNet, joMessageProxyMainNet, strEventName,
+ joReceipt.blockNumber, joReceipt.transactionHash,
+ joMessageProxyMainNet.filters[strEventName]()
+ );
+ if( joEvents.length > 0 ) {
+ details.success(
+ "{p}Success, verified the {} event of the MessageProxy/{} contract, " +
+ "found event(s): {}", strLogPrefix, strEventName,
+ joMessageProxyMainNet.address, joEvents );
+ } else {
+ throw new Error( "Verification failed for the OutgoingMessage event of the " +
+ `MessageProxy ${joMessageProxyMainNet.address} contract, no events found` );
+ }
+ }
+ } catch ( err ) {
+ details.critical( "{p}Payment error in {bright}: {err}, stack is:\n{stack}",
+ strLogPrefix, strActionName, err, err );
+ details.exposeDetailsTo( log.globalStream(), "doEthPaymentFromMainNet", false );
+ details.close();
+ return false;
+ }
+ imaGasUsage.printGasUsageReportFromArray( "ETH PAYMENT FROM MAIN NET", jarrReceipts, details );
+ if( log.exposeDetailsGet() )
+ details.exposeDetailsTo( log.globalStream(), "doEthPaymentFromMainNet", true );
+ details.close();
+ return true;
+}
+
+// transfer money from S-chain to main-net
+// S-chain.TokenManager call: function exitToMain(address to) public payable
+// Where:
+// to.........address in main-net
+// Notice:
+// this function is available for everyone in S-chain
+// money is sent from caller
+// "value" JSON arg is used to specify amount of money to sent
+export async function doEthPaymentFromSChain(
+ ethersProviderSChain: owaspUtils.ethersMod.ethers.providers.JsonRpcProvider,
+ chainIdSChain: string,
+ joAccountSrc: state.TAccount,
+ joAccountDst: state.TAccount,
+ joTokenManagerETH: owaspUtils.ethersMod.ethers.Contract,
+ joMessageProxySChain: owaspUtils.ethersMod.ethers.Contract, // for checking logs
+ weiHowMuch: any, // how much WEI money to send
+ transactionCustomizerSChain: imaTx.TransactionCustomizer
+): Promise {
+ const details = log.createMemoryStream();
+ const jarrReceipts: any = [];
+ let strActionName = "";
+ const strLogPrefix = "S2M ETH Payment: ";
+ try {
+ strActionName = "ETH payment from S-Chain, exitToMain";
+ const arrArguments = [
+ owaspUtils.toBN( weiHowMuch )
+ ];
+ const gasPrice = await transactionCustomizerSChain.computeGasPrice(
+ ethersProviderSChain, 200000000000 );
+ details.trace( "{p}Using computed gasPrice={}", strLogPrefix, gasPrice );
+ const estimatedGas = await transactionCustomizerSChain.computeGas(
+ details, ethersProviderSChain,
+ "TokenManagerETH", joTokenManagerETH, "exitToMain", arrArguments,
+ joAccountSrc, strActionName,
+ gasPrice, 6000000, 0, null );
+ details.trace( "{p}Using estimated gas={}", strLogPrefix, estimatedGas );
+ const isIgnore = true;
+ const strErrorOfDryRun = await imaTx.dryRunCall(
+ details, ethersProviderSChain,
+ "TokenManagerETH", joTokenManagerETH, "exitToMain", arrArguments,
+ joAccountSrc, strActionName, isIgnore,
+ gasPrice, estimatedGas, 0, null );
+ if( strErrorOfDryRun )
+ throw new Error( strErrorOfDryRun );
+
+ const opts: imaTx.TCustomPayedCallOptions = {
+ isCheckTransactionToSchain: true
+ };
+ const joReceipt = await imaTx.payedCall(
+ details, ethersProviderSChain,
+ "TokenManagerETH", joTokenManagerETH, "exitToMain", arrArguments,
+ joAccountSrc, strActionName,
+ gasPrice, estimatedGas, 0, opts );
+ if( joReceipt ) {
+ jarrReceipts.push( {
+ description: "doEthPaymentFromSChain",
+ receipt: joReceipt
+ } );
+ }
+
+ // Must-have event(s) analysis as indicator(s) of success
+ const strEventName = "OutgoingMessage";
+ if( joMessageProxySChain ) {
+ details.debug( "{p}Verifying the {} event of the MessageProxy/{} contract ...",
+ strLogPrefix, strEventName, joMessageProxySChain.address );
+ await threadInfo.sleep(
+ imaHelperAPIs.getMillisecondsSleepBeforeFetchOutgoingMessageEvent() );
+ const joEvents = await imaEventLogScan.getContractCallEvents(
+ details, strLogPrefix,
+ ethersProviderSChain, joMessageProxySChain, strEventName,
+ joReceipt.blockNumber, joReceipt.transactionHash,
+ joMessageProxySChain.filters[strEventName]()
+ );
+ if( joEvents.length > 0 ) {
+ details.success(
+ "{p}Success, verified the {} event of the MessageProxy/{} contract, " +
+ "found event(s): {}", strLogPrefix, strEventName, joMessageProxySChain.address,
+ joEvents );
+ } else {
+ throw new Error( "Verification failed for the OutgoingMessage event of the " +
+ `MessageProxy ${joMessageProxySChain.address} contract, no events found` );
+ }
+ }
+ } catch ( err ) {
+ details.critical( "{p}Payment error in {bright}: {err}, stack is:\n{stack}",
+ strLogPrefix, strActionName, err, err );
+ details.exposeDetailsTo( log.globalStream(), "doEthPaymentFromSChain", false );
+ details.close();
+ return false;
+ }
+ imaGasUsage.printGasUsageReportFromArray( "ETH PAYMENT FROM S-CHAIN", jarrReceipts, details );
+ if( log.exposeDetailsGet() )
+ details.exposeDetailsTo( log.globalStream(), "doEthPaymentFromSChain", true );
+ details.close();
+ return true;
+}
+
+export async function receiveEthPaymentFromSchainOnMainNet(
+ ethersProviderMainNet: owaspUtils.ethersMod.ethers.providers.JsonRpcProvider,
+ chainIdMainNet: string,
+ joAccountMN: state.TAccount,
+ joDepositBoxETH: owaspUtils.ethersMod.ethers.Contract,
+ transactionCustomizerMainNet: imaTx.TransactionCustomizer
+): Promise {
+ const details = log.createMemoryStream();
+ const jarrReceipts: any = [];
+ let strActionName = "";
+ const strLogPrefix = "M2S ETH Receive: ";
+ try {
+ strActionName = "Receive ETH payment from S-Chain on Main Met, getMyEth";
+ const arrArguments: any = [];
+ const gasPrice = await transactionCustomizerMainNet.computeGasPrice(
+ ethersProviderMainNet, 200000000000 );
+ details.trace( "{p}Using computed gasPrice={}", strLogPrefix, gasPrice );
+ const estimatedGas = await transactionCustomizerMainNet.computeGas(
+ details, ethersProviderMainNet,
+ "DepositBoxETH", joDepositBoxETH, "getMyEth", arrArguments,
+ joAccountMN, strActionName,
+ gasPrice, 3000000 );
+ details.trace( "{p}Using estimated gas={}", strLogPrefix, estimatedGas );
+ const isIgnore = false;
+ const strErrorOfDryRun = await imaTx.dryRunCall(
+ details, ethersProviderMainNet,
+ "DepositBoxETH", joDepositBoxETH,
+ "getMyEth", arrArguments,
+ joAccountMN, strActionName, isIgnore,
+ gasPrice, estimatedGas );
+ if( strErrorOfDryRun )
+ throw new Error( strErrorOfDryRun );
+
+ const joReceipt = await imaTx.payedCall(
+ details, ethersProviderMainNet,
+ "DepositBoxETH", joDepositBoxETH,
+ "getMyEth", arrArguments,
+ joAccountMN, strActionName,
+ gasPrice, estimatedGas );
+ if( joReceipt ) {
+ jarrReceipts.push( {
+ description: "receiveEthPaymentFromSchainOnMainNet",
+ receipt: joReceipt
+ } );
+ }
+ } catch ( err ) {
+ details.critical( "{p}Receive payment error in {bright}: {err}, stack is:\n{stack}",
+ strLogPrefix, strActionName, err, err );
+ details.exposeDetailsTo( log.globalStream(),
+ "receiveEthPaymentFromSchainOnMainNet", false );
+ details.close();
+ return false;
+ }
+ imaGasUsage.printGasUsageReportFromArray( "RECEIVE ETH ON MAIN NET", jarrReceipts, details );
+ if( log.exposeDetailsGet() )
+ details.exposeDetailsTo( log.globalStream(), "receiveEthPaymentFromSchainOnMainNet", true );
+ details.close();
+ return true;
+}
+
+export async function viewEthPaymentFromSchainOnMainNet(
+ ethersProviderMainNet: owaspUtils.ethersMod.ethers.providers.JsonRpcProvider,
+ joAccountMN: state.TAccount,
+ joDepositBoxETH: owaspUtils.ethersMod.ethers.Contract
+): Promise