diff --git a/.circleci/config.yml b/.circleci/config.yml index 62c0de7115..f584618308 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,7 +7,7 @@ parameters: e2e_environment_docker_image: type: string - default: raidennetwork/lightclient-e2e-environment:v1.1.2 + default: raidennetwork/lightclient-e2e-environment:v1.1.4 working_directory: type: string diff --git a/e2e-environment/Dockerfile b/e2e-environment/Dockerfile index 28e479bb57..eaa2a3543a 100644 --- a/e2e-environment/Dockerfile +++ b/e2e-environment/Dockerfile @@ -1,12 +1,13 @@ -ARG RAIDEN_VERSION="v1.2.0" -ARG CONTRACTS_PACKAGE_VERSION="0.37.1" +ARG RAIDEN_VERSION="v2.0.0" +ARG CONTRACTS_PACKAGE_VERSION="0.37.6" ARG CONTRACTS_VERSION="0.37.0" -ARG SERVICES_VERSION="v0.13.1" -ARG SYNAPSE_VERSION="v1.19.1" +ARG SERVICES_VERSION="v0.15.4" +ARG SYNAPSE_VERSION="v1.35.1" +ARG RAIDEN_SYNAPSE_MODULES="0.1.3" ARG OS_NAME="LINUX" -ARG GETH_VERSION="1.9.23" -ARG GETH_URL_LINUX="https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.9.23-8c2f2715.tar.gz" -ARG GETH_MD5_LINUX="4817ce02025ba3f876f7055e1e456555" +ARG GETH_VERSION="1.10.3" +ARG GETH_URL_LINUX="https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.10.3-991384a7.tar.gz" +ARG GETH_MD5_LINUX="c601f6a651e23878229dd4a39fa2e6e4" FROM python:3.9 as raiden-builder ARG RAIDEN_VERSION @@ -21,13 +22,27 @@ RUN git checkout ${RAIDEN_VERSION} RUN make install FROM python:3.9 as synapse-builder + +RUN python -m venv /synapse-venv && /synapse-venv/bin/pip install wheel + ARG SYNAPSE_VERSION +ARG RAIDEN_SYNAPSE_MODULES + +RUN /synapse-venv/bin/pip install \ + "matrix-synapse[postgres,redis]==${SYNAPSE_VERSION}" \ + psycopg2 \ + coincurve \ + pycryptodome \ + "twisted>=20.3.0" \ + click==7.1.2 \ + docker-py \ + raiden-synapse-modules==${RAIDEN_SYNAPSE_MODULES} -RUN python -m venv /synapse-venv \ - && /synapse-venv/bin/pip install "matrix-synapse==${SYNAPSE_VERSION}" \ - && /synapse-venv/bin/pip install psycopg2 coincurve pycryptodome +# XXX Temporary hot-patch while https://github.com/matrix-org/synapse/pull/9820 is not released yet -- note: this should +# be run in in workerless setup +RUN sed -i 's/\(\s*\)if self.worker_type/\1if True or self.worker_type/' /synapse-venv/lib/python3.9/site-packages/raiden_synapse_modules/pfs_presence_router.py -COPY synapse/auth/ /synapse-venv/lib/python3.7/site-packages/ +COPY synapse/auth/ /synapse-venv/lib/python3.9/site-packages/ FROM python:3.9 LABEL maintainer="Raiden Network Team " @@ -67,7 +82,7 @@ ARG LOCAL_BASE=/usr/local ARG DATA_DIR=/opt/chain RUN download_geth.sh && deploy.sh \ - && cp -R /opt/deployment/* ${VENV}/lib/python3.7/site-packages/raiden_contracts/data_${CONTRACTS_VERSION}/ + && cp -R /opt/deployment/* ${VENV}/lib/python3.9/site-packages/raiden_contracts/data_${CONTRACTS_VERSION}/ RUN mkdir -p /opt/synapse/config \ && mkdir -p /opt/synapse/data_well_known \ @@ -88,11 +103,13 @@ RUN git checkout "${SERVICES_VERSION}" RUN apt-get update \ && apt-get install -y --no-install-recommends python3-dev \ + # FIXME: why use the system 3.7 here? && /usr/bin/python3 -m virtualenv -p /usr/bin/python3 /opt/services/venv \ + && /opt/services/venv/bin/pip install -U pip wheel \ && /opt/services/venv/bin/pip install -r requirements.txt \ && /opt/services/venv/bin/pip install -e . \ - && mkdir -p /opt/services/keystore \ - && cp -R /opt/raiden/lib/python3.7/site-packages/raiden_contracts/data_${CONTRACTS_VERSION}/* /opt/services/venv/lib/python3.7/site-packages/raiden_contracts/data \ + && mkdir -p /opt/services/keystore +RUN cp -R ${VENV}/lib/python3.9/site-packages/raiden_contracts/data_${CONTRACTS_VERSION}/* /opt/services/venv/lib/python3.7/site-packages/raiden_contracts/data \ && rm -rf ~/.cache/pip \ && apt-get -y remove python3-dev \ && apt-get -y autoremove \ @@ -104,10 +121,28 @@ ENV DEPLOYMENT_SERVICES_INFO=/opt/deployment/deployment_services_private_net.jso COPY services/keystore/UTC--2020-03-11T15-39-16.935381228Z--2b5e1928c25c5a326dbb61fc9713876dd2904e34 /opt/services/keystore +ENV ETH_RPC="http://localhost:8545" + RUN setup_channels.sh -# HTTP, HTTP metrics, TCP replication, HTTP replication -# GETH | RAIDEN |SUP |PFS | Matrix -EXPOSE 8545 8546 8547 30303 30303/udp 5001 5002 9001 5555 80 9101 9092 9093 + +## GETH +EXPOSE 8545 8546 8547 30303 30303/udp +## PFS +EXPOSE 5555 +## RAIDEN +# HTTP +EXPOSE 5001 5002 +## MATRIX +# HTTP +EXPOSE 8008 80 +# HTTP metrics +EXPOSE 9101 +# TCP replication +EXPOSE 9092 +# HTTP replication +EXPOSE 9093 +## ??? +EXPOSE 9001 COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf diff --git a/e2e-environment/README.md b/e2e-environment/README.md index ff1ba653da..64ca222ae4 100644 --- a/e2e-environment/README.md +++ b/e2e-environment/README.md @@ -25,9 +25,10 @@ docker pull raidennetwork/lightclient-e2e-environment ``` Alternatively it can be built locally as well. This requires to use the -according script for the maintaince of the deployment information files. These +according script for the maintenance of the deployment information files. These will be automatically staged to the VCS after the script has run and need to be -commited afterwards. +committed afterwards. + ```sh bash ./build-e2e-environment.sh ``` @@ -170,7 +171,6 @@ The configuration has been slightly modified over the original `RSB` configuration to fit the purposes of the integration image. When merging changes from upstream please evaluate if these changes are required or not. -- `setup/room_ensurer.py` is based on [room_ensurer.py](https://github.com/raiden-network/raiden-service-bundle/blob/master/build/room_ensurer/room_ensurer.py) - `synapse/auth/admin_user_auth_provider.py` is based on [admin_user_auth_provider.py](https://github.com/raiden-network/raiden-service-bundle/blob/master/build/synapse/admin_user_auth_provider.py) - `synapse/auth/eth_auth_provider.py` is based on [eth_auth_provider.py](https://github.com/raiden-network/raiden-service-bundle/blob/master/build/synapse/eth_auth_provider.py) - `synapse/exec/synapse-entrypoint.sh` is based on [synapse-entrypoint.sh](https://github.com/raiden-network/raiden-service-bundle/blob/master/build/synapse/synapse-entrypoint.sh) @@ -185,3 +185,25 @@ file. Watch-out for the `SYNAPSE_VERSION` constant variable. Then update the ```dockerfile ARG SYNAPSE_VERSION=1.10.1 ``` + +## Upgrade image version for tests and CI + +To upgrade the end-to-end environment you need to build and upload a new image. +First update any component as described above. + +Then build and test those versions locally. Finally you need to increment +`DOCKER_IMAGE_TAG` in `shared-script.sh` and build the new image version: + +```sh +./build-e2e-environment.sh +``` + +This will also tag the image. You can then upload it to docker hub: + +``` +docker push raidennetwork/lightclient-e2e-environment:v1.1.4 +``` + +Once this is done, don't forget to update the image version used in CI. In +`.circleci/config.yml` update `e2e_environment_docker_image` to the version you +just created. diff --git a/e2e-environment/deployment_information/deployment_private_net.json b/e2e-environment/deployment_information/deployment_private_net.json index d971be02de..ba218c15e6 100644 --- a/e2e-environment/deployment_information/deployment_private_net.json +++ b/e2e-environment/deployment_information/deployment_private_net.json @@ -3,19 +3,19 @@ "chain_id": 4321, "contracts": { "SecretRegistry": { - "address": "0x6D214Ac1068e70bfc0B7C8FD695D885DD29a2f1f", - "transaction_hash": "0xa9c0eab62235069586feb8479c169222361acf116ccefe288ecdc1d4da311ce1", + "address": "0x970514b8403ae51fd278Cd6bdDB65030B0D6E0c4", + "transaction_hash": "0x6f321e34860ab1bba642463e45bf99edb1014e1705ef45b8c6bd90b6156bc967", "block_number": 4, "gas_cost": 292942, "constructor_arguments": [] }, "TokenNetworkRegistry": { - "address": "0x7439D10dd5EfB111c2434C190C23934baf6101be", - "transaction_hash": "0xb28ca185ec39f2bc1be824d22f80c61b188981a07dbcf6e9930dd03c649aa80d", + "address": "0x0b5b27dB8F19F5a2E4cebCe21BAD26DAd7021d3f", + "transaction_hash": "0x29e4ab150530586192a465edf5ee44551108375e492d8944fd4abf213b53ac5f", "block_number": 14, "gas_cost": 4907586, "constructor_arguments": [ - "0x6D214Ac1068e70bfc0B7C8FD695D885DD29a2f1f", + "0x970514b8403ae51fd278Cd6bdDB65030B0D6E0c4", 4321, 20, 555428, diff --git a/e2e-environment/deployment_information/deployment_services_private_net.json b/e2e-environment/deployment_information/deployment_services_private_net.json index 353cbf6831..f18b958f67 100644 --- a/e2e-environment/deployment_information/deployment_services_private_net.json +++ b/e2e-environment/deployment_information/deployment_services_private_net.json @@ -3,13 +3,13 @@ "chain_id": 4321, "contracts": { "ServiceRegistry": { - "address": "0x2bC1401E8995d7F6a6eA289226E6DD59451cc632", - "transaction_hash": "0xd2d50a6ba84f20eebfb8d962a6f146b22037c52baf35193007a7f23e8ac24461", - "block_number": 57, + "address": "0x1BDA6f48607297E2E361Ec9ed714DABFa3D2566c", + "transaction_hash": "0x9016279be30fe4d8349515cea39cb24288919a0bb3af183f5343c7fae860581a", + "block_number": 56, "gas_cost": 2496148, "constructor_arguments": [ - "0xb8f14F655A867D738bB1F9F3A829C87437E3c157", - "0x78b601c1464d6e8B1b7ae030ba9771dC18E13a5c", + "0xD68bA462913F1c4397bb9562326eA2D5DE042514", + "0xcAe3348675485be8885cdf8Db0072de3503f8cbA", 2000000000000000000000, 6, 5, @@ -19,36 +19,36 @@ ] }, "UserDeposit": { - "address": "0xCeC4467491Eb80DA00bfb3cA1A0b5A72808CBE99", - "transaction_hash": "0x5b6b23dc49fb35a31f8efb633abc2c2428d2af23f3d65d7a458a37c4fc804c13", - "block_number": 67, + "address": "0x8129F1294058086171F945196fE0A4E07593f522", + "transaction_hash": "0x302423bcf6ef7e228f3470e74d7d8929e942555091d41c9a4ead3a91bd825f45", + "block_number": 66, "gas_cost": 1874250, "constructor_arguments": [ - "0xb8f14F655A867D738bB1F9F3A829C87437E3c157", + "0xD68bA462913F1c4397bb9562326eA2D5DE042514", 115792089237316195423570985008687907853269984665640564039457584007913129639935 ] }, "MonitoringService": { - "address": "0x2045951D64A323B19C284D1C9782be29A29257A9", - "transaction_hash": "0xc7b7669ec409c244fb0bf1554922dc7901d3feb284af69ffa8757dcb81694a7b", - "block_number": 77, - "gas_cost": 2415017, + "address": "0x0F8ad70f3DEe8118a6E2cA71321c312E7C3dc3ee", + "transaction_hash": "0x53050f3fdac4b9ff4d7b47c0d4fe1ab8f9d718a9df4c3c343a93de5edc2303e0", + "block_number": 76, + "gas_cost": 2415081, "constructor_arguments": [ - "0xb8f14F655A867D738bB1F9F3A829C87437E3c157", - "0x2bC1401E8995d7F6a6eA289226E6DD59451cc632", - "0xCeC4467491Eb80DA00bfb3cA1A0b5A72808CBE99", - "0x7439D10dd5EfB111c2434C190C23934baf6101be" + "0xD68bA462913F1c4397bb9562326eA2D5DE042514", + "0x1BDA6f48607297E2E361Ec9ed714DABFa3D2566c", + "0x8129F1294058086171F945196fE0A4E07593f522", + "0x0b5b27dB8F19F5a2E4cebCe21BAD26DAd7021d3f" ] }, "OneToN": { - "address": "0x20E39a07624623f64a60825b2282Bf1069D04A45", - "transaction_hash": "0x22a8f8448ebb9343eb8e98cfb12555531dcdabcbcf115cdca9ac8a297c52d14e", - "block_number": 87, - "gas_cost": 1326177, + "address": "0xfd5500337063D12236bB6cc9330D32b210c1e5FD", + "transaction_hash": "0x9f368f62adf7ade9fd8b60901a6e82b47a83645a426c7a735c4f2a1f2008ef93", + "block_number": 86, + "gas_cost": 1326241, "constructor_arguments": [ - "0xCeC4467491Eb80DA00bfb3cA1A0b5A72808CBE99", + "0x8129F1294058086171F945196fE0A4E07593f522", 4321, - "0x2bC1401E8995d7F6a6eA289226E6DD59451cc632" + "0x1BDA6f48607297E2E361Ec9ed714DABFa3D2566c" ] } } diff --git a/e2e-environment/deployment_information/smartcontracts.sh b/e2e-environment/deployment_information/smartcontracts.sh index 0e42c840de..cccef34d78 100755 --- a/e2e-environment/deployment_information/smartcontracts.sh +++ b/e2e-environment/deployment_information/smartcontracts.sh @@ -1,7 +1,7 @@ #!/bin/sh -export TTT_TOKEN_ADDRESS=0x0a5A3BEc2Fd467872740fd587A49f8EeBd6DF39e -export SVT_TOKEN_ADDRESS=0xb8f14F655A867D738bB1F9F3A829C87437E3c157 -export USER_DEPOSIT_ADDRESS=0xCeC4467491Eb80DA00bfb3cA1A0b5A72808CBE99 -export TOKEN_NETWORK_REGISTRY_ADDRESS=0x7439D10dd5EfB111c2434C190C23934baf6101be -export TOKEN_NETWORK_REGISTRY_ADDRESS=0x7439D10dd5EfB111c2434C190C23934baf6101be -export ONE_TO_N_ADDRESS=0x20E39a07624623f64a60825b2282Bf1069D04A45 +export TTT_TOKEN_ADDRESS=0xe814154d412F0D708090873B198E0c9d6764D42f +export SVT_TOKEN_ADDRESS=0xD68bA462913F1c4397bb9562326eA2D5DE042514 +export USER_DEPOSIT_ADDRESS=0x8129F1294058086171F945196fE0A4E07593f522 +export TOKEN_NETWORK_REGISTRY_ADDRESS=0x0b5b27dB8F19F5a2E4cebCe21BAD26DAd7021d3f +export SERVICE_REGISTRY=0x1BDA6f48607297E2E361Ec9ed714DABFa3D2566c +export ONE_TO_N_ADDRESS=0xfd5500337063D12236bB6cc9330D32b210c1e5FD diff --git a/e2e-environment/deployment_information/version b/e2e-environment/deployment_information/version index 0f1acbd565..c641220244 100644 --- a/e2e-environment/deployment_information/version +++ b/e2e-environment/deployment_information/version @@ -1 +1 @@ -v1.1.2 +v1.1.4 diff --git a/e2e-environment/geth/deploy.sh b/e2e-environment/geth/deploy.sh index af3573891f..98edd00047 100755 --- a/e2e-environment/geth/deploy.sh +++ b/e2e-environment/geth/deploy.sh @@ -6,6 +6,7 @@ echo "Setting up private chain" VENV=/tmp/deploy_venv python3 -m venv $VENV source $VENV/bin/activate +pip install -U pip wheel pip install mypy_extensions pip install click>=7.0 pip install raiden-contracts==${CONTRACTS_PACKAGE_VERSION} @@ -21,16 +22,15 @@ echo "${ACCOUNT}" > "${DEPLOYMENT_DIRECTORY}"/miner.sh genesis.py --validator "${ACCOUNT}" --output /tmp/genesis.json geth --datadir "${DATA_DIR}" init /tmp/genesis.json -geth --rpc --syncmode full \ +geth --syncmode full \ --gcmode archive \ --datadir "${DATA_DIR}" \ --networkid 4321 \ --nodiscover \ - --rpc \ - --rpcapi "eth,net,web3,txpool" \ - --minerthreads=1 \ + --http \ + --http.api "eth,net,web3,txpool" \ + --miner.threads 1 \ --mine \ - --nousb \ --unlock "${ACCOUNT}" \ --password "${PASSWORD_FILE}" \ --allow-insecure-unlock & @@ -43,8 +43,8 @@ deploy_contracts.py --contract-version "${CONTRACTS_VERSION}" \ --password "${PASSWORD}" if [[ -f ${SMARTCONTRACTS_ENV_FILE} ]]; then - cp ${VENV}/lib/python3.7/site-packages/raiden_contracts/data_${CONTRACTS_VERSION}/deployment_private_net.json /opt/deployment/ - cp ${VENV}/lib/python3.7/site-packages/raiden_contracts/data_${CONTRACTS_VERSION}/deployment_services_private_net.json /opt/deployment/ + cp ${VENV}/lib/python3.9/site-packages/raiden_contracts/data_${CONTRACTS_VERSION}/deployment_private_net.json /opt/deployment/ + cp ${VENV}/lib/python3.9/site-packages/raiden_contracts/data_${CONTRACTS_VERSION}/deployment_services_private_net.json /opt/deployment/ if [[ ! -f /opt/deployment/deployment_private_net.json ]]; then echo 'Could not find the deployment_private_net.json' @@ -68,7 +68,3 @@ else kill -s TERM ${GETH_PID} exit 1 fi - - - - diff --git a/e2e-environment/geth/deploy_contracts.py b/e2e-environment/geth/deploy_contracts.py index be77a224a2..ea418e1a04 100755 --- a/e2e-environment/geth/deploy_contracts.py +++ b/e2e-environment/geth/deploy_contracts.py @@ -5,7 +5,7 @@ import click from eth_account import Account from raiden_contracts.constants import CONTRACT_TOKEN_NETWORK_REGISTRY, CONTRACT_USER_DEPOSIT, \ - CONTRACT_ONE_TO_N + CONTRACT_ONE_TO_N, CONTRACT_SERVICE_REGISTRY from raiden_contracts.deploy.__main__ import ( ContractDeployer, ) @@ -142,7 +142,8 @@ def main(keystore_file: str, contract_version: str, password: str, output: str, decay_constant=SERVICE_DEPOSIT_DECAY_CONSTANT, min_price=SERVICE_DEPOSIT_MIN_PRICE, registration_duration=SERVICE_REGISTRATION_DURATION, - token_network_registry_address=token_network_registry_address + token_network_registry_address=token_network_registry_address, + reuse_service_registry_from_deploy_file=None, ) except Exception as err: print(f'Service contract deployment failed: {err}') @@ -169,12 +170,14 @@ def main(keystore_file: str, contract_version: str, password: str, output: str, contracts_info = deployed_service_contracts_info['contracts'] user_deposit_address = contracts_info[CONTRACT_USER_DEPOSIT]['address'] one_to_n_address = contracts_info[CONTRACT_ONE_TO_N]['address'] + service_registry_address = contracts_info[CONTRACT_SERVICE_REGISTRY]['address'] + address_file.write(f'#!/bin/sh\n') address_file.write(f'export TTT_TOKEN_ADDRESS={ttt_token_address}\n') address_file.write(f'export SVT_TOKEN_ADDRESS={svt_token_address}\n') address_file.write(f'export USER_DEPOSIT_ADDRESS={user_deposit_address}\n') address_file.write(f'export TOKEN_NETWORK_REGISTRY_ADDRESS={token_network_registry_address}\n') - address_file.write(f'export TOKEN_NETWORK_REGISTRY_ADDRESS={token_network_registry_address}\n') + address_file.write(f'export SERVICE_REGISTRY={service_registry_address}\n') address_file.write(f'export ONE_TO_N_ADDRESS={one_to_n_address}\n') address_file.close() except Exception as err: diff --git a/e2e-environment/geth/genesis.py b/e2e-environment/geth/genesis.py index 983b027566..84dae111cf 100755 --- a/e2e-environment/geth/genesis.py +++ b/e2e-environment/geth/genesis.py @@ -43,7 +43,8 @@ def main(validator: str, output: str): "0x517aAD51D0e9BbeF3c64803F86b3B9136641D9ec", "0x14791697260E4c9A71f18484C9f997B308e59325", "0x4C42F75ceae7b0CfA9588B940553EB7008546C29", - "0x9fe3a28D581c2e9d7b3632EfDdfc76022E9FA7Ea" + "0x9fe3a28D581c2e9d7b3632EfDdfc76022E9FA7Ea", + "0x2b5e1928c25c5a326dbb61fc9713876dd2904e34" ] GENESIS_STUB['config']['clique']['validators'].append(validator) diff --git a/e2e-environment/raiden/node1.toml b/e2e-environment/raiden/node1.toml index df1671816e..a379db4c08 100644 --- a/e2e-environment/raiden/node1.toml +++ b/e2e-environment/raiden/node1.toml @@ -19,6 +19,7 @@ routing-mode = "pfs" pathfinding-service-address = "http://localhost:5555" # Misc address = "0x517aAD51D0e9BbeF3c64803F86b3B9136641D9ec" +blockchain-query-interval = 1.0 log-json = true diff --git a/e2e-environment/raiden/node2.toml b/e2e-environment/raiden/node2.toml index 97b5b714c7..b6b650fb61 100644 --- a/e2e-environment/raiden/node2.toml +++ b/e2e-environment/raiden/node2.toml @@ -19,6 +19,7 @@ routing-mode = "pfs" pathfinding-service-address = "http://localhost:5555" # Misc address = "0xCBC49ec22c93DB69c78348C90cd03A323267db86" +blockchain-query-interval = 1.0 log-json = true diff --git a/e2e-environment/setup/room_ensurer.py b/e2e-environment/setup/room_ensurer.py deleted file mode 100644 index 0cb80741b7..0000000000 --- a/e2e-environment/setup/room_ensurer.py +++ /dev/null @@ -1,361 +0,0 @@ -""" -Utility that initializes public rooms and ensures correct federation - -In Raiden we use a public discovery room that all nodes join which provides the following features: -- Nodes are findable in the server-side user search -- All nodes receive presence updates about existing and newly joined nodes - -The global room is initially created on one server, after that it is federated to all other servers -and a server-local alias is added to it so it's discoverable on every server. - -This utility uses for following algorithm to ensure there are no races in room creation: -- Sort list of known servers lexicographically -- Connect to all known servers -- If not all servers are reachable, sleep and retry later -- Try to join room `#:` -- Compare room_id of all found rooms -- If either the room_ids differ or no room is found on a specific server: - - If `own_server` is the first server in the list: - - Create a room if it doesn't exist and assign the server-local alias - - Else: - - If a room with alias `#:` exists, remove that alias - - Wait for the room with `#:` to appear - - Add server-local alias to the first_server-room -""" -from gevent.monkey import patch_all # isort:skip - -patch_all() # isort:skip - -import json -import os -import sys -from dataclasses import dataclass -from enum import IntEnum, Enum -from itertools import chain -from json import JSONDecodeError -from typing import Any, Dict, Optional, Set, TextIO, Tuple, Union -from urllib.parse import urlparse - -import click -import gevent -from eth_utils import encode_hex, to_normalized_address -from matrix_client.errors import MatrixError -from raiden_contracts.utils.type_aliases import ChainID -from structlog import get_logger - -from raiden.constants import ( - DISCOVERY_DEFAULT_ROOM, - MONITORING_BROADCASTING_ROOM, - PATH_FINDING_BROADCASTING_ROOM, - Environment, -) -from raiden.log_config import configure_logging -from raiden.network.transport.matrix import make_room_alias -from raiden.network.transport.matrix.client import GMatrixHttpApi -from raiden.settings import DEFAULT_MATRIX_KNOWN_SERVERS -from raiden.tests.utils.factories import make_signer - -ENV_KEY_KNOWN_SERVERS = "URL_KNOWN_FEDERATION_SERVERS" - - -class Networks(Enum): - INTEGRATION = ChainID(4321) - - -class MatrixPowerLevels(IntEnum): - USER = 0 - MODERATOR = 50 - ADMINISTRATOR = 100 - - -log = get_logger(__name__) - - -class EnsurerError(Exception): - pass - - -class MultipleErrors(EnsurerError): - pass - - -@dataclass(frozen=True) -class RoomInfo: - room_id: str - aliases: Set[str] - server_name: str - - -class RoomEnsurer: - def __init__( - self, - username: str, - password: str, - own_server_name: str, - known_servers_url: Optional[str] = None, - ): - self._username = username - self._password = password - self._own_server_name = own_server_name - - if known_servers_url is None: - known_servers_url = DEFAULT_MATRIX_KNOWN_SERVERS[Environment.PRODUCTION] - - self._known_servers: Dict[str, str] = { - own_server_name: f"http://{own_server_name}:80" - } - - - if not self._known_servers: - raise RuntimeError(f"No known servers found from list at {known_servers_url}.") - self._first_server_name = list(self._known_servers.keys())[0] - self._is_first_server = own_server_name == self._first_server_name - self._apis: Dict[str, GMatrixHttpApi] = self._connect_all() - self._own_api = self._apis[own_server_name] - - log.debug( - "Room ensurer initialized", - own_server_name=own_server_name, - known_servers=self._known_servers.keys(), - first_server_name=self._first_server_name, - is_first_server=self._is_first_server, - ) - - def ensure_rooms(self) -> None: - exceptions = {} - for network in Networks: - for alias_fragment in [ - DISCOVERY_DEFAULT_ROOM, - MONITORING_BROADCASTING_ROOM, - PATH_FINDING_BROADCASTING_ROOM, - ]: - try: - self._ensure_room_for_network(network, alias_fragment) - except (MatrixError, EnsurerError) as ex: - log.exception(f"Error while ensuring room for {network.name}.") - exceptions[network] = ex - if exceptions: - log.error("Exceptions happened", exceptions=exceptions) - raise MultipleErrors(exceptions) - - def _ensure_room_for_network(self, network: Networks, alias_fragment: str) -> None: - log.info(f"Ensuring {alias_fragment} room for {network.name}") - room_alias_prefix = make_room_alias(ChainID(network.value), alias_fragment) - room_infos: Dict[str, Optional[RoomInfo]] = { - server_name: self._get_room(server_name, room_alias_prefix) - for server_name in self._known_servers.keys() - } - first_server_room_info = room_infos[self._first_server_name] - - if not first_server_room_info: - log.warning("First server room missing") - if self._is_first_server: - log.info("Creating room", server_name=self._own_server_name) - first_server_room_info = self._create_room( - self._own_server_name, room_alias_prefix - ) - room_infos[self._first_server_name] = first_server_room_info - else: - raise EnsurerError("First server room missing.") - - are_all_rooms_the_same = all( - room_info is not None and room_info.room_id == first_server_room_info.room_id - for room_info in room_infos.values() - ) - if not are_all_rooms_the_same: - log.warning( - "Room id mismatch", - alias_prefix=room_alias_prefix, - expected=first_server_room_info.room_id, - found={ - server_name: room_info.room_id if room_info else None - for server_name, room_info in room_infos.items() - }, - ) - own_server_room_info = room_infos.get(self._own_server_name) - own_server_room_alias = f"#{room_alias_prefix}:{self._own_server_name}" - first_server_room_alias = f"#{room_alias_prefix}:{self._first_server_name}" - if not own_server_room_info: - log.warning( - "Room missing on own server, adding alias", - server_name=self._own_server_name, - room_id=first_server_room_info.room_id, - new_room_alias=own_server_room_alias, - ) - self._join_and_alias_room(first_server_room_alias, own_server_room_alias) - log.info("Room alias set", alias=own_server_room_alias) - elif own_server_room_info.room_id != first_server_room_info.room_id: - log.warning( - "Conflicting local room, reassigning alias", - server_name=self._own_server_name, - expected_room_id=first_server_room_info.room_id, - current_room_id=own_server_room_info.room_id, - ) - self._own_api.remove_room_alias(own_server_room_alias) - self._join_and_alias_room(first_server_room_alias, own_server_room_alias) - log.info( - "Room alias updated", - alias=own_server_room_alias, - room_id=first_server_room_info.room_id, - ) - else: - log.warning("Mismatching rooms on other servers. Doing nothing.") - else: - log.info( - "Room state ok.", - network=network, - server_rooms={ - server_name: room_info.room_id if room_info else None - for server_name, room_info in room_infos.items() - }, - ) - - def _join_and_alias_room( - self, first_server_room_alias: str, own_server_room_alias: str - ) -> None: - response = self._own_api.join_room(first_server_room_alias) - own_room_id = response.get("room_id") - if not own_room_id: - raise EnsurerError("Couldn't join first server room via federation.") - log.debug("Joined room on first server", own_room_id=own_room_id) - self._own_api.set_room_alias(own_room_id, own_server_room_alias) - - def _get_room(self, server_name: str, room_alias_prefix: str) -> Optional[RoomInfo]: - api = self._apis[server_name] - room_alias_local = f"#{room_alias_prefix}:{server_name}" - try: - response = api.join_room(room_alias_local) - room_id = response.get("room_id") - if not room_id: - log.debug("Couldn't find room", room_alias=room_alias_local) - return None - room_state = api.get_room_state(response["room_id"]) - except MatrixError: - log.debug("Room doesn't exist", room_alias=room_alias_local) - return None - existing_room_aliases = set( - chain.from_iterable( - event["content"]["aliases"] - for event in room_state - if event["type"] == "m.room.aliases" - ) - ) - - log.debug( - "Room aliases", server_name=server_name, room_id=room_id, aliases=existing_room_aliases - ) - return RoomInfo(room_id=room_id, aliases=existing_room_aliases, server_name=server_name) - - def _create_server_user_power_levels(self) -> Dict[str, Any]: - - server_admin_power_levels: Dict[str, Union[int, Dict[str, int]]] = { - "users": {}, - "users_default": MatrixPowerLevels.USER, - "events": { - "m.room.power_levels": MatrixPowerLevels.ADMINISTRATOR, - "m.room.history_visibility": MatrixPowerLevels.ADMINISTRATOR, - }, - "events_default": MatrixPowerLevels.USER, - "state_default": MatrixPowerLevels.MODERATOR, - "ban": MatrixPowerLevels.MODERATOR, - "kick": MatrixPowerLevels.MODERATOR, - "redact": MatrixPowerLevels.MODERATOR, - "invite": MatrixPowerLevels.MODERATOR, - } - - for server_name in self._known_servers: - username = f"admin-{server_name}".replace(":", "-") - user_id = f"@{username}:{server_name}" - server_admin_power_levels["users"][user_id] = MatrixPowerLevels.MODERATOR - - own_user_id = f"@{self._username}:{self._own_server_name}" - server_admin_power_levels["users"][own_user_id] = MatrixPowerLevels.ADMINISTRATOR - - return server_admin_power_levels - - def _create_room(self, server_name: str, room_alias_prefix: str) -> RoomInfo: - api = self._apis[server_name] - server_admin_power_levels = self._create_server_user_power_levels() - response = api.create_room( - room_alias_prefix, - is_public=True, - # power_level_content_override=server_admin_power_levels, - ) - room_alias = f"#{room_alias_prefix}:{server_name}" - return RoomInfo(response["room_id"], {room_alias}, server_name) - - def _connect_all(self) -> Dict[str, GMatrixHttpApi]: - jobs = { - gevent.spawn(self._connect, server_name, server_url) - for server_name, server_url in self._known_servers.items() - } - gevent.joinall(jobs, raise_error=True) - log.info("All servers connected") - return {server_name: matrix_api for server_name, matrix_api in (job.get() for job in jobs)} - - def _connect(self, server_name: str, server_url: str) -> Tuple[str, GMatrixHttpApi]: - log.debug("Connecting", server=server_name) - api = GMatrixHttpApi(server_url) - username = self._username - password = self._password - - if server_name != self._own_server_name: - signer = make_signer() - username = str(to_normalized_address(signer.address)) - password = encode_hex(signer.sign(server_name.encode())) - - response = api.login("m.login.password", user=username, password=password) - api.token = response["access_token"] - log.debug("Connected", server=server_name) - return server_name, api - - -@click.command() -@click.option("--own-server", required=True) -@click.option( - "-i", - "--interval", - default=3600, - help="How often to perform the room check. Set to 0 to disable.", -) -@click.option("-l", "--log-level", default="INFO") -@click.option("-c", "--credentials-file", required=True, type=click.File("rt")) -def main(own_server: str, interval: int, credentials_file: TextIO, log_level: str) -> None: - configure_logging( - {"": log_level, "raiden": log_level, "__main__": log_level}, disable_debug_logfile=True - ) - known_servers_url = os.environ.get(ENV_KEY_KNOWN_SERVERS) - - try: - credentials = json.loads(credentials_file.read()) - username = credentials["username"] - password = credentials["password"] - - except (JSONDecodeError, UnicodeDecodeError, OSError, KeyError): - log.exception("Invalid credentials file") - sys.exit(1) - - while True: - try: - room_ensurer = RoomEnsurer(username, password, own_server, known_servers_url) - except MatrixError: - log.exception("Failure while communicating with matrix servers. Retrying in 60s") - gevent.sleep(60) - continue - - try: - room_ensurer.ensure_rooms() - except EnsurerError: - log.error("Retrying in 60s.") - gevent.sleep(60) - continue - - if interval == 0: - break - - log.info("Run finished, sleeping.", duration=interval) - gevent.sleep(interval) - - -if __name__ == "__main__": - main() # pylint: disable=no-value-for-parameter diff --git a/e2e-environment/setup/setup_channels.sh b/e2e-environment/setup/setup_channels.sh index 528f026e26..f9591a3efa 100755 --- a/e2e-environment/setup/setup_channels.sh +++ b/e2e-environment/setup/setup_channels.sh @@ -2,37 +2,41 @@ source "${SMARTCONTRACTS_ENV_FILE}" -synapse-entrypoint.sh & -SYNAPSE_PID=$! - -echo Synapse server is running at "${SYNAPSE_PID}" - -source /opt/raiden/bin/activate - -echo Preparing ROOMS - -python3 /usr/local/bin/room_ensurer.py --own-server "${SERVER_NAME}" \ - --log-level "DEBUG" \ - --credentials-file /opt/synapse/config/admin_user_cred.json \ - -i 0 - echo Starting Chain ACCOUNT=$(cat /opt/deployment/miner.sh) -geth --rpc --syncmode full --gcmode archive --datadir "${DATA_DIR}" \ +geth --syncmode full --gcmode archive --datadir "${DATA_DIR}" \ --networkid 4321 \ --nodiscover \ - --rpc \ - --rpcapi "eth,net,web3,txpool" \ - --minerthreads=1 \ + --http \ + --http.api "eth,net,web3,txpool" \ + --miner.threads 1 \ --mine \ - --nousb \ --unlock "${ACCOUNT}" \ --password "${PASSWORD_FILE}" \ --allow-insecure-unlock & GETH_PID=$! +source /opt/services/venv/bin/activate +python3 -m raiden_libs.service_registry register \ + --log-level DEBUG \ + --keystore-file /opt/services/keystore/UTC--2020-03-11T15-39-16.935381228Z--2b5e1928c25c5a326dbb61fc9713876dd2904e34 \ + --password 1234 \ + --eth-rpc "http://localhost:8545" \ + --accept-all \ + --service-url "http://test.rsb" +deactivate + +synapse-entrypoint.sh & +SYNAPSE_PID=$! + +echo Synapse server is running at "${SYNAPSE_PID}" + +source /opt/raiden/bin/activate + + + echo Start PFS source /opt/services/venv/bin/activate pfs-entrypoint.sh & @@ -49,6 +53,7 @@ until $(curl --output /dev/null --silent --get --fail http://localhost:5555/api/ fi echo "Waiting for Pathfinding service to start (${PFS_RETRIES})" PFS_RETRIES=$((PFS_RETRIES + 1)) + sleep 20 done diff --git a/e2e-environment/shared-script.sh b/e2e-environment/shared-script.sh index 796a4ab20a..55c5b4797d 100644 --- a/e2e-environment/shared-script.sh +++ b/e2e-environment/shared-script.sh @@ -5,7 +5,7 @@ # very beginning of each script. DOCKER_IMAGE_REPOSITORY="raidennetwork/lightclient-e2e-environment" -DOCKER_IMAGE_TAG="v1.1.2" +DOCKER_IMAGE_TAG="v1.1.4" DOCKER_IMAGE_NAME="${DOCKER_IMAGE_REPOSITORY}:${DOCKER_IMAGE_TAG}" DOCKER_CONTAINER_NAME="lc-e2e" E2E_ENVIRONMENT_DIRECTORY="$(realpath "$(dirname "${BASH_SOURCE[0]}")")" diff --git a/e2e-environment/supervisord.conf b/e2e-environment/supervisord.conf index f41af83ab8..1440136506 100644 --- a/e2e-environment/supervisord.conf +++ b/e2e-environment/supervisord.conf @@ -2,7 +2,7 @@ nodaemon=true [program:geth] -command=/usr/local/bin/geth --rpc --syncmode full --gcmode archive --datadir /opt/chain --networkid 4321 --nodiscover --rpc --rpcaddr 0.0.0.0 --rpcapi "eth,net,web3,txpool" --rpccorsdomain "*" --minerthreads=1 --mine --nousb --unlock %(ENV_MINER_ACCOUNT)s --password %(ENV_PASSWORD_FILE)s --allow-insecure-unlock +command=/usr/local/bin/geth --syncmode full --gcmode archive --datadir /opt/chain --networkid 4321 --nodiscover --http --http.addr 0.0.0.0 --http.api "eth,net,web3,txpool" --rpccorsdomain "*" --miner.threads=1 --mine --unlock %(ENV_MINER_ACCOUNT)s --password %(ENV_PASSWORD_FILE)s --allow-insecure-unlock [program:synapse] command=/usr/local/bin/synapse-entrypoint.sh @@ -14,4 +14,4 @@ command=/usr/local/bin/pfs-entrypoint.sh command=/opt/raiden/bin/raiden --config-file /opt/raiden/config/node1.toml --no-sync-check --user-deposit-contract-address %(ENV_USER_DEPOSIT_ADDRESS)s [program:node2] -command=/opt/raiden/bin/raiden --config-file /opt/raiden/config/node2.toml --no-sync-check --user-deposit-contract-address %(ENV_USER_DEPOSIT_ADDRESS)s \ No newline at end of file +command=/opt/raiden/bin/raiden --config-file /opt/raiden/config/node2.toml --no-sync-check --user-deposit-contract-address %(ENV_USER_DEPOSIT_ADDRESS)s diff --git a/e2e-environment/synapse/exec/render_config_template.py b/e2e-environment/synapse/exec/render_config_template.py index f661dbdbfb..6f5659bc57 100644 --- a/e2e-environment/synapse/exec/render_config_template.py +++ b/e2e-environment/synapse/exec/render_config_template.py @@ -3,6 +3,8 @@ import random import string from pathlib import Path +from eth_typing import ChecksumAddress +from eth_utils import to_checksum_address PATH_CONFIG = Path("/opt/synapse/config/synapse.yaml") PATH_CONFIG_TEMPLATE = Path("/opt/synapse/config/synapse.template.yaml") @@ -22,11 +24,17 @@ def get_macaroon_key() -> str: return macaroon -def render_synapse_config(server_name: str) -> None: +def render_synapse_config( + server_name: str, + eth_rpc_url: str, + service_registry_address: ChecksumAddress, +) -> None: template_content = PATH_CONFIG_TEMPLATE.read_text() rendered_config = string.Template(template_content).substitute( MACAROON_KEY=get_macaroon_key(), - SERVER_NAME=server_name + SERVER_NAME=server_name, + ETH_RPC=eth_rpc_url, + SERVICE_REGISTRY=service_registry_address, ) PATH_CONFIG.write_text(rendered_config) @@ -53,9 +61,13 @@ def generate_admin_user_credentials(): def main() -> None: server_name = os.environ["SERVER_NAME"] + eth_rpc_url = os.environ["ETH_RPC"] + service_registry_address = to_checksum_address(os.environ["SERVICE_REGISTRY"]) render_synapse_config( - server_name=server_name + server_name=server_name, + eth_rpc_url=eth_rpc_url, + service_registry_address=service_registry_address, ) render_well_known_file(server_name=server_name) generate_admin_user_credentials() diff --git a/e2e-environment/synapse/synapse.template.yaml b/e2e-environment/synapse/synapse.template.yaml index 72d9aab4dc..9d8dd44045 100644 --- a/e2e-environment/synapse/synapse.template.yaml +++ b/e2e-environment/synapse/synapse.template.yaml @@ -102,30 +102,26 @@ bcrypt_rounds: 12 trusted_third_party_id_servers: - localhost +presence: + enabled: true + presence_router: + module: raiden_synapse_modules.pfs_presence_router.PFSPresenceRouter + config: + ethereum_rpc: ${ETH_RPC} + service_registry_address: ${SERVICE_REGISTRY} + blockchain_sync_seconds: 15 ## Metrics ### enable_metrics: False report_stats: False - -## API Configuration ## -room_invite_state_types: - - "m.room.join_rules" - - "m.room.canonical_alias" - - "m.room.name" - ## Room Creation Rules ## alias_creation_rules: - - user_id: "@admin*" - alias: "#raiden_*_*" - room_id: "*" - action: allow - user_id: "*" - alias: "#raiden_*_*" + alias: "*" room_id: "*" action: deny - # A list of application service config file to use app_service_config_files: [] diff --git a/raiden-dapp/.env.staging b/raiden-dapp/.env.staging index f568d8a352..bcd1f3ea95 100644 --- a/raiden-dapp/.env.staging +++ b/raiden-dapp/.env.staging @@ -1,8 +1,5 @@ VUE_APP_PUBLIC_PATH=/staging/ VUE_APP_HUB=hub.raiden.eth VUE_APP_ALLOW_MAINNET=false -VUE_APP_PFS=https://pfs.demo001.env.raiden.network -VUE_APP_MATRIX_SERVER=https://transport.demo001.env.raiden.network -VUE_APP_UDC_ADDRESS=0x0794F09913AA8C77C8c5bdd1Ec4Bb51759Ee0cC5 VUE_APP_IMPRINT=https://raiden.network/privacy.html VUE_APP_TERMS=https://github.com/raiden-network/light-client/blob/master/TERMS.md diff --git a/raiden-ts/CHANGELOG.md b/raiden-ts/CHANGELOG.md index 92b13d8f10..01c1b3246c 100644 --- a/raiden-ts/CHANGELOG.md +++ b/raiden-ts/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## [Unreleased] +### Removed +- [#2571] **BREAKING** Remove ability to join and send messages to global service rooms +- [#2822] **BREAKING** Do not join global rooms anymore, so Matrix-based presence won't work + +### Changed +- [#2572] **BREAKING** Send services messages through `toDevice` instead of global rooms +- [#2822] **BREAKING** Presence now gets fetched from PFS and requires a Bespin-compatible (Raiden 2.0) service and transport network + +### Added +- [#2822] Added ability to use peer's presence from `LockedTransfer`'s `metadata.routes.address_metadata` + +[#2571]: https://github.com/raiden-network/light-client/issues/2571 +[#2572]: https://github.com/raiden-network/light-client/issues/2572 +[#2822]: https://github.com/raiden-network/light-client/pull/2822 + ## [0.17.0] - 2021-06-15 ### Added - [#1576] Add functionality to deploy token networks diff --git a/raiden-ts/package.json b/raiden-ts/package.json index dbec9547fe..26993165d0 100644 --- a/raiden-ts/package.json +++ b/raiden-ts/package.json @@ -101,6 +101,7 @@ "io-ts": "^2.2.16", "isomorphic-fetch": "^3.0.0", "json-bigint": "^1.0.0", + "json-canonicalize": "^1.0.4", "lodash": "^4.17.21", "loglevel": "^1.7.1", "matrix-js-sdk": "^11.2.0", diff --git a/raiden-ts/src/config.ts b/raiden-ts/src/config.ts index 0464c8f736..d3df12f910 100644 --- a/raiden-ts/src/config.ts +++ b/raiden-ts/src/config.ts @@ -9,7 +9,6 @@ import { Capabilities, DEFAULT_CONFIRMATIONS } from './constants'; import { PfsMode, PfsModeC } from './services/types'; import { exponentialBackoff } from './transfers/epics/utils'; import { Caps } from './transport/types'; -import { getNetworkName } from './utils/ethers'; import { Address, UInt } from './utils/types'; const RTCIceServer = t.type({ urls: t.union([t.string, t.array(t.string)]) }); @@ -30,16 +29,10 @@ const RTCIceServer = t.type({ urls: t.union([t.string, t.array(t.string)]) }); * - expiryFactor - Multiply revealTimeout to get how far in the future * transfer expiration block should be * - httpTimeout - Used in http fetch requests - * - discoveryRoom - Discovery Room to auto-join, use null to disable * - additionalServices - Array of extra services URLs (or addresses, if URL set on SecretRegistry) * - pfsMode - One of 'disabled' (disables PFS usage and notifications), 'auto' (notifies all of * registered and additionalServices, picks cheapest for transfers without explicit pfs), * or 'onlyAdditional' (notifies all, but pick first responding from additionalServices only). - * - pfsRoom - PFS Room to auto-join and send PFSCapacityUpdate to, use null to disable - * - monitoringRoom - MS global room to auto-join and send RequestMonitoring messages; - * use null to disable - * - pfs - Array of Path Finding Service Addresses (require PFS to be registered) or URLs. - * Set to false to disable, or true to enable automatic fetching from ServiceRegistry. * - pfsSafetyMargin - Safety margin to be added to fees received from PFS. Either a fee * multiplier, or a [fee, amount] pair ofmultipliers. Use `1.1` to add a 10% over estimated fee * margin, or `[0.03, 0.0005]` to add a 3% over fee plus 0.05% over amount. @@ -75,11 +68,8 @@ export const RaidenConfig = t.readonly( settleTimeout: t.number, expiryFactor: t.number, // must be > 1.0 httpTimeout: t.number, - discoveryRoom: t.union([t.string, t.null]), additionalServices: t.readonlyArray(t.union([Address, t.string])), pfsMode: PfsModeC, - pfsRoom: t.union([t.string, t.null]), - monitoringRoom: t.union([t.string, t.null]), pfsSafetyMargin: t.union([t.number, t.tuple([t.number, t.number])]), pfsMaxPaths: t.number, pfsMaxFee: UInt(32), @@ -131,7 +121,6 @@ export function makeDefaultConfig( { network }: { network: Network }, overwrites?: PartialRaidenConfig, ): RaidenConfig { - const networkName = getNetworkName(network); const matrixServerInfos = network.chainId === 1 ? 'https://raw.githubusercontent.com/raiden-network/raiden-service-bundle/master/known_servers/known_servers-production-v1.2.0.json' @@ -153,11 +142,8 @@ export function makeDefaultConfig( revealTimeout: 50, expiryFactor: 1.1, // must be > 1.0 httpTimeout: 30e3, - discoveryRoom: `raiden_${networkName}_discovery`, additionalServices: [], pfsMode: PfsMode.auto, - pfsRoom: `raiden_${networkName}_path_finding`, - monitoringRoom: `raiden_${networkName}_monitoring`, pfsSafetyMargin: 1.0, // multiplier pfsMaxPaths: 3, pfsMaxFee: parseEther('0.05') as UInt<32>, // in SVT/RDN, 18 decimals diff --git a/raiden-ts/src/messages/actions.ts b/raiden-ts/src/messages/actions.ts index 7f69ce0379..2584d43cb6 100644 --- a/raiden-ts/src/messages/actions.ts +++ b/raiden-ts/src/messages/actions.ts @@ -2,6 +2,7 @@ import * as t from 'io-ts'; import { ServiceC } from '../services/types'; +import { Via } from '../transport/types'; import type { ActionType } from '../utils/actions'; import { createAction, createAsyncAction } from '../utils/actions'; import { Address, Signed } from '../utils/types'; @@ -17,7 +18,8 @@ export const messageSend = createAsyncAction( 'message/send/failure', t.intersection([ t.type({ message: t.union([t.string, Signed(Message)]) }), - t.partial({ msgtype: t.string, userId: t.string }), + t.partial({ msgtype: t.string }), + Via, ]), t.union([t.undefined, t.type({ via: t.string, tookMs: t.number, retries: t.number })]), ); diff --git a/raiden-ts/src/messages/types.ts b/raiden-ts/src/messages/types.ts index 414a7cf1cc..89bce8f642 100644 --- a/raiden-ts/src/messages/types.ts +++ b/raiden-ts/src/messages/types.ts @@ -3,7 +3,9 @@ * They include BigNumber strings validation, enum validation (if needed), Address checksum * validation, etc, and converting everything to its respective object, where needed. */ +import { isLeft } from 'fp-ts/lib/Either'; import * as t from 'io-ts'; +import isEmpty from 'lodash/isEmpty'; import { Lock } from '../channels/types'; import { Address, Hash, Int, Secret, Signature, UInt } from '../utils/types'; @@ -99,14 +101,56 @@ const EnvelopeMessage = t.readonly( ]), ); -const _RouteMetadata = t.readonly( +const _AddressMetadata = t.readonly( t.type({ - route: t.readonlyArray(Address), + user_id: t.string, + displayname: t.string, + capabilities: t.string, }), ); +export interface AddressMetadata extends t.TypeOf {} +export interface AddressMetadataC + extends t.Type> {} +export const AddressMetadata: AddressMetadataC = _AddressMetadata; + +const _RouteMetadata = t.readonly( + t.intersection([ + t.type({ route: t.readonlyArray(Address) }), + t.partial({ address_metadata: t.record(t.string, AddressMetadata) }), + ]), +); export interface RouteMetadata extends t.TypeOf {} export interface RouteMetadataC extends t.Type> {} -export const RouteMetadata: RouteMetadataC = _RouteMetadata; +// FIXME: remove all this decoding workaround when additional_hash is hash of exact canonicalized +// metadata, without changes; it's needed while PC hashes the checksummed addresses but sends the +// lowercased serialized version +const routeMetadataPredicate = (u: RouteMetadata) => + !u.address_metadata || t.array(Address).is(Object.keys(u.address_metadata)); +export const RouteMetadata: RouteMetadataC = new t.RefinementType( + 'RouteMetadata', + (u): u is RouteMetadata => _RouteMetadata.is(u) && routeMetadataPredicate(u), + (i, c) => { + const e = _RouteMetadata.validate(i, c); + if (isLeft(e)) return e; + const a = e.right; + if (!a.address_metadata) return t.success(a); + else if (isEmpty(a.address_metadata)) { + const { address_metadata: _, ...rest } = a; + return t.success(rest); + } + const address_metadata: NonNullable = {}; + // for each key of address_metadata's record, validate/decode it as Address + for (const [addr, meta] of Object.entries(a.address_metadata!)) { + const ev = Address.validate(addr, c); + if (isLeft(ev)) return ev; + address_metadata[ev.right] = meta; + } + return t.success({ ...a, address_metadata }); + }, + _RouteMetadata.encode, + _RouteMetadata, + routeMetadataPredicate, +); const _Metadata = t.readonly( t.type({ diff --git a/raiden-ts/src/messages/utils.ts b/raiden-ts/src/messages/utils.ts index e23621bbeb..d55947e515 100644 --- a/raiden-ts/src/messages/utils.ts +++ b/raiden-ts/src/messages/utils.ts @@ -6,6 +6,7 @@ import { encode as rlpEncode } from '@ethersproject/rlp'; import { toUtf8Bytes } from '@ethersproject/strings'; import { verifyMessage } from '@ethersproject/wallet'; import type * as t from 'io-ts'; +import { canonicalize } from 'json-canonicalize'; import logging from 'loglevel'; import type { BalanceProof } from '../channels/types'; @@ -15,7 +16,7 @@ import { encode, jsonParse, jsonStringify } from '../utils/data'; import type { Address, Hash, HexString } from '../utils/types'; import { decode, Signature, Signed } from '../utils/types'; import { messageReceived } from './actions'; -import type { EnvelopeMessage, Metadata } from './types'; +import type { AddressMetadata, EnvelopeMessage, Metadata } from './types'; import { Message, MessageType } from './types'; const CMDIDs: { readonly [T in MessageType]: number } = { @@ -51,8 +52,7 @@ export enum MessageTypeId { * @returns Hash of the metadata. */ function createMetadataHash(metadata: Metadata): Hash { - const routeHashes = metadata.routes.map((value) => keccak256(rlpEncode(value.route)) as Hash); - return keccak256(rlpEncode(routeHashes)) as Hash; + return keccak256(toUtf8Bytes(canonicalize(metadata))) as Hash; } /** @@ -408,3 +408,22 @@ export function isMessageReceivedOfType(messageCodecs: C | C[ ? messageCodecs.some((c) => c.is(action.payload.message)) : messageCodecs.is(action.payload.message)); } + +/** + * Validates metadata was signed by address + * + * @param metadata - Peer's metadata + * @param address - Peer's address + * @param opts - Options + * @param opts.log - Logger instance + * @returns Metadata iff it's valid and was signed by address + */ +export function validateAddressMetadata( + metadata: AddressMetadata | undefined, + address: Address, + { log }: { log: logging.Logger } = { log: logging }, +): AddressMetadata | undefined { + if (metadata && verifyMessage(metadata.user_id, metadata.displayname) === address) + return metadata; + else if (metadata) log?.warn('Invalid address metadata', { address, metadata }); +} diff --git a/raiden-ts/src/raiden.ts b/raiden-ts/src/raiden.ts index 2966d81ae3..0e19dc3760 100644 --- a/raiden-ts/src/raiden.ts +++ b/raiden-ts/src/raiden.ts @@ -83,6 +83,7 @@ import { getSecrethash, makePaymentId, makeSecret, + metadataFromPaths, raidenTransfer, transferKey, transferKeyToMeta, @@ -954,10 +955,10 @@ export class Raiden { tokenNetwork, target, value: decodedValue, - paths, paymentId, secret, expiration, + ...metadataFromPaths(paths), }, { secrethash, direction: Direction.SENT }, ), diff --git a/raiden-ts/src/services/epics/helpers.ts b/raiden-ts/src/services/epics/helpers.ts index 36689e961d..7a55254182 100644 --- a/raiden-ts/src/services/epics/helpers.ts +++ b/raiden-ts/src/services/epics/helpers.ts @@ -4,6 +4,8 @@ import { Zero } from '@ethersproject/constants'; import { toUtf8Bytes } from '@ethersproject/strings'; import { verifyMessage } from '@ethersproject/wallet'; import { Decimal } from 'decimal.js'; +import * as t from 'io-ts'; +import isEmpty from 'lodash/isEmpty'; import type { Observable } from 'rxjs'; import { defer, from, of } from 'rxjs'; import { fromFetch } from 'rxjs/fetch'; @@ -15,25 +17,38 @@ import { channelAmounts, channelKey } from '../../channels/utils'; import type { RaidenConfig } from '../../config'; import { intervalFromConfig } from '../../config'; import { Capabilities } from '../../constants'; +import { AddressMetadata } from '../../messages/types'; import type { RaidenState } from '../../state'; import type { matrixPresence } from '../../transport/actions'; -import { getCap } from '../../transport/utils'; +import { getCap, stringifyCaps } from '../../transport/utils'; import type { Latest, RaidenEpicDeps } from '../../types'; import { jsonParse, jsonStringify } from '../../utils/data'; import { assert, ErrorCodes, networkErrors, RaidenError } from '../../utils/error'; import { lastMap, mergeWith, retryWhile } from '../../utils/rx'; -import type { Address, Signature, Signed } from '../../utils/types'; -import { decode, Int, UInt } from '../../utils/types'; +import type { Signature, Signed } from '../../utils/types'; +import { Address, decode, Int, UInt } from '../../utils/types'; import { iouClear, iouPersist, pathFind } from '../actions'; import type { IOU, Paths, PFS } from '../types'; -import { LastIOUResults, PathResults, PfsMode } from '../types'; +import { LastIOUResults, PfsMode } from '../types'; import { packIOU, pfsInfo, pfsListInfo, ServiceError, signIOU } from '../utils'; -interface Route { - iou: Signed | undefined; - paths?: Paths; - error?: ServiceError; -} +type Route = { iou: Signed | undefined } & ({ paths: Paths } | { error: ServiceError }); + +/** + * Codec for PFS API returned data + */ +const PathResults = t.type({ + result: t.array( + t.intersection([ + t.type({ + path: t.array(Address), + estimated_fee: Int(32), + }), + t.partial({ address_metadata: t.record(t.string, AddressMetadata) }), + ]), + ), +}); +type PathResults = t.TypeOf; /** * Returns a ISO string with millisecond resolution (same as PC) @@ -216,13 +231,14 @@ function getRouteFromPfs$(action: pathFind.request, deps: RaidenEpicDeps): Obser function filterPaths( state: RaidenState, action: pathFind.request, - { address, log }: RaidenEpicDeps, + { address, log }: Pick, paths: Paths, ): Paths { - const filteredPaths: Paths = []; + const filteredPaths: Mutable = []; const invalidatedRecipients = new Set
(); - for (const { path, fee } of paths) { + for (const { path, fee, ...rest } of paths) { + // FIXME: don't trim path; validate us as 1st and partner as 2nd addr, once PC can handle it const cleanPath = getCleanPath(path, address); const recipient = cleanPath[0]; if (invalidatedRecipients.has(recipient)) continue; @@ -244,7 +260,7 @@ function filterPaths( } else shouldSelectPath = true; if (shouldSelectPath) { - filteredPaths.push({ path: cleanPath, fee }); + filteredPaths.push({ path: cleanPath, fee, ...rest }); } else { log.warn('Invalidated received route. Reason:', reasonToNotSelect, 'Route:', cleanPath); invalidatedRecipients.add(recipient); @@ -356,31 +372,24 @@ function addFeeSafetyMargin( ); } -function parsePfsResponse(amount: UInt<32>, data: unknown, config: RaidenConfig) { +function parsePfsResponse(amount: UInt<32>, data: unknown, config: RaidenConfig): Paths { // decode results and cap also client-side for pfsMaxPaths const results = decode(PathResults, data).result.slice(0, config.pfsMaxPaths); - return results.map(({ path, estimated_fee }) => { - let fee; - if (estimated_fee.isZero()) fee = estimated_fee; + return results.map(({ estimated_fee, ...rest }) => { // add fee margins iff estimated_fee is not zero - else { - fee = addFeeSafetyMargin(estimated_fee, amount, config); - } - return { path, fee } as const; + const fee = estimated_fee.isZero() + ? estimated_fee + : addFeeSafetyMargin(estimated_fee, amount, config); + return { ...rest, fee }; }); } function shouldPersistIou(route: Route): boolean { - const { paths, error } = route; - return paths !== undefined || isNoRouteFoundError(error); + return 'paths' in route || isNoRouteFoundError(route.error); } -function getCleanPath(path: readonly Address[], address: Address): readonly Address[] { - if (path[0] === address) { - return path.slice(1); - } else { - return path; - } +function getCleanPath(path: readonly Address[], address: Address) { + return path.slice(path.indexOf(address) + 1); } function isNoRouteFoundError(error: ServiceError | undefined): boolean { @@ -401,7 +410,7 @@ export function getRoute$( deps: RaidenEpicDeps, { state, config }: Pick, targetPresence: matrixPresence.success, -): Observable<{ paths?: Paths; iou: Signed | undefined; error?: ServiceError }> { +): Observable { validateRouteTarget(action, state, targetPresence); const { tokenNetwork, target, value } = action.meta; @@ -416,8 +425,22 @@ export function getRoute$( value, ) === true ) { + const addressMetadata: { [k: string]: AddressMetadata } = {}; + if (state.transport.setup) { + addressMetadata[deps.address] = { + user_id: state.transport.setup.userId, + displayname: state.transport.setup.displayName, + capabilities: stringifyCaps(config.caps ?? {}), + }; + } return of({ - paths: [{ path: [deps.address, target], fee: Zero as Int<32> }], + paths: [ + { + path: [deps.address, target], + fee: Zero as Int<32>, + ...(!isEmpty(addressMetadata) ? { address_metadata: addressMetadata } : {}), + }, + ], iou: undefined, }); } else if (pfsIsDisabled(action, config)) { @@ -441,7 +464,7 @@ export function validateRoute$( route: Route, ): Observable { const { tokenNetwork } = action.meta; - const { iou, paths, error } = route; + const { iou } = route; return from( // looks like mergeMap with generator doesn't handle exceptions correctly @@ -458,7 +481,8 @@ export function validateRoute$( } } - if (error) { + if ('error' in route) { + const { error } = route; if (isNoRouteFoundError(error)) { throw new RaidenError(ErrorCodes.PFS_NO_ROUTES_BETWEEN_NODES); } else { @@ -469,7 +493,8 @@ export function validateRoute$( } } - const filteredPaths = paths ? filterPaths(state, action, deps, paths) : []; + const { paths } = route; + const filteredPaths = filterPaths(state, action, deps, paths); if (filteredPaths.length) { yield pathFind.success({ paths: filteredPaths }, action.meta); diff --git a/raiden-ts/src/services/types.ts b/raiden-ts/src/services/types.ts index 54fd3f35f7..fa47ef5807 100644 --- a/raiden-ts/src/services/types.ts +++ b/raiden-ts/src/services/types.ts @@ -1,6 +1,7 @@ import * as t from 'io-ts'; import invert from 'lodash/invert'; +import { AddressMetadata } from '../messages/types'; import type { Decodable } from '../utils/types'; import { Address, Int, Signed, UInt } from '../utils/types'; @@ -25,35 +26,18 @@ export const PfsMode = { export type PfsMode = typeof PfsMode[keyof typeof PfsMode]; export const PfsModeC = t.keyof(invert(PfsMode) as { [D in PfsMode]: string }); -/** - * Codec for PFS API returned data - */ -export const PathResults = t.readonly( - t.intersection([ - t.type({ - result: t.array( - t.readonly( - t.type({ - path: t.readonlyArray(Address), - estimated_fee: Int(32), - }), - ), - ), - }), - t.partial({ feedback_token: t.string }), - ]), -); -export interface PathResults extends t.TypeOf {} - /** * Codec for raiden-ts internal representation of a PFS result/routes */ -export const Paths = t.array( +export const Paths = t.readonlyArray( t.readonly( - t.type({ - path: t.readonlyArray(Address), - fee: Int(32), - }), + t.intersection([ + t.type({ + path: t.readonlyArray(Address), + fee: Int(32), + }), + t.partial({ address_metadata: t.readonly(t.record(t.string, AddressMetadata)) }), + ]), ), ); export type Paths = t.TypeOf; diff --git a/raiden-ts/src/services/utils.ts b/raiden-ts/src/services/utils.ts index 6f9c17c4ff..95569b8c02 100644 --- a/raiden-ts/src/services/utils.ts +++ b/raiden-ts/src/services/utils.ts @@ -5,13 +5,17 @@ import memoize from 'lodash/memoize'; import uniqBy from 'lodash/uniqBy'; import type { Observable } from 'rxjs'; import { defer, EMPTY, from } from 'rxjs'; +import { fromFetch } from 'rxjs/fetch'; import { catchError, first, map, mergeMap, toArray } from 'rxjs/operators'; import type { ServiceRegistry } from '../contracts'; -import { MessageTypeId } from '../messages/utils'; +import { AddressMetadata } from '../messages/types'; +import { MessageTypeId, validateAddressMetadata } from '../messages/utils'; +import type { Caps } from '../transport/types'; +import { parseCaps } from '../transport/utils'; import type { RaidenEpicDeps } from '../types'; import { encode, jsonParse } from '../utils/data'; -import { assert, ErrorCodes, networkErrors } from '../utils/error'; +import { assert, ErrorCodes, networkErrors, RaidenError } from '../utils/error'; import { LruCache } from '../utils/lru'; import { retryAsync$ } from '../utils/rx'; import type { Signature, Signed } from '../utils/types'; @@ -25,6 +29,22 @@ const serviceRegistryToken = memoize( }).toPromise() as Promise
, ); +/** + * Fetch, validate and cache the service URL for a given URL or service address + * (if registered on ServiceRegistry) + * + * @param pfsAddressUrl - service Address or URL + * @returns Promise to validated URL + */ +const pfsAddressUrl = memoize(async function pfsAddressUrl_( + pfsAddrOrUrl: string, + { serviceRegistryContract }: Pick, +): Promise { + let url = pfsAddrOrUrl; + if (Address.is(pfsAddrOrUrl)) url = await serviceRegistryContract.callStatic.urls(pfsAddrOrUrl); + return validatePfsUrl(url); +}); + const urlRegex = process.env.NODE_ENV === 'production' ? /^(?:https:\/\/)?[^\s\/$.?#&"']+\.[^\s\/$?#&"']+$/ @@ -68,7 +88,16 @@ export type ServiceError = t.TypeOf; */ export async function pfsInfo( pfsAddrOrUrl: Address | string, - { serviceRegistryContract, network, contractsInfo, provider, config$ }: RaidenEpicDeps, + { + serviceRegistryContract, + network, + contractsInfo, + provider, + config$, + }: Pick< + RaidenEpicDeps, + 'serviceRegistryContract' | 'network' | 'contractsInfo' | 'provider' | 'config$' + >, ): Promise { const { pfsMaxFee } = await config$.pipe(first()).toPromise(); /** @@ -88,10 +117,8 @@ export async function pfsInfo( }); // if it's an address, fetch url from ServiceRegistry, else it's already the URL - let url = pfsAddrOrUrl; - if (Address.is(pfsAddrOrUrl)) url = await serviceRegistryContract.callStatic.urls(pfsAddrOrUrl); + const url = await pfsAddressUrl(pfsAddrOrUrl, { serviceRegistryContract }); - url = validatePfsUrl(url); const start = Date.now(); const res = await fetch(url + '/api/v1/info', { mode: 'cors' }); const text = await res.text(); @@ -122,7 +149,13 @@ export async function pfsInfo( * @returns Promise to Address of PFS on given URL */ export const pfsInfoAddress = Object.assign( - async function pfsInfoAddress(url: string, deps: RaidenEpicDeps): Promise
{ + async function pfsInfoAddress( + url: string, + deps: Pick< + RaidenEpicDeps, + 'serviceRegistryContract' | 'network' | 'contractsInfo' | 'provider' | 'config$' + >, + ): Promise
{ url = validatePfsUrl(url); let addrPromise = pfsAddressCache_.get(url); if (!addrPromise) { @@ -149,7 +182,10 @@ export const pfsInfoAddress = Object.assign( */ export function pfsListInfo( pfsList: readonly (string | Address)[], - deps: RaidenEpicDeps, + deps: Pick< + RaidenEpicDeps, + 'log' | 'serviceRegistryContract' | 'network' | 'contractsInfo' | 'provider' | 'config$' + >, ): Observable { const { log } = deps; return from(pfsList).pipe( @@ -176,6 +212,42 @@ export function pfsListInfo( ); } +/** + * @param peer - Peer address to fetch presence for + * @param pfsAddrOrUrl - PFS/service address to fetch presence from + * @param deps - Epics dependencies subset + * @param deps.serviceRegistryContract - Contract instance + * @returns Observable to peer's presence or error + */ +export function getPresenceFromService$( + peer: Address, + pfsAddrOrUrl: string, + { serviceRegistryContract }: Pick, +): Observable<{ readonly user_id: string; readonly capabilities: Caps }> { + return defer(async () => pfsAddressUrl(pfsAddrOrUrl, { serviceRegistryContract })).pipe( + mergeMap((url) => fromFetch(`${url}/api/v1/address/${peer}/metadata`)), + mergeMap(async (res) => res.json()), + map((json) => { + try { + const presence = decode(AddressMetadata, json); + assert(validateAddressMetadata(presence, peer), [ + 'Invalid metadata signature', + { peer, presence }, + ]); + const capabilities = parseCaps(presence.capabilities); + assert(capabilities, ['Invalid capabilities format', presence.capabilities]); + return { ...presence, capabilities }; + } catch (err) { + try { + const { errors: msg, ...details } = decode(ServiceError, json); + err = new RaidenError(msg, details); + } catch (e) {} + throw err; + } + }), + ); +} + /** * Pack an IOU for signing or verification * diff --git a/raiden-ts/src/transfers/actions.ts b/raiden-ts/src/transfers/actions.ts index 65d465ae8f..a765ae222b 100644 --- a/raiden-ts/src/transfers/actions.ts +++ b/raiden-ts/src/transfers/actions.ts @@ -5,6 +5,7 @@ import { BalanceProof } from '../channels/types'; import { LockedTransfer, LockExpired, + Metadata, Processed, SecretRequest, SecretReveal, @@ -13,7 +14,7 @@ import { WithdrawExpired, WithdrawRequest, } from '../messages/types'; -import { Paths } from '../services/types'; +import { Via } from '../transport/types'; import type { ActionType } from '../utils/actions'; import { createAction, createAsyncAction } from '../utils/actions'; import { Address, Hash, Int, Secret, Signed, UInt } from '../utils/types'; @@ -51,9 +52,12 @@ export const transfer = createAsyncAction( tokenNetwork: Address, target: Address, value: UInt(32), - paths: Paths, paymentId: UInt(8), + metadata: Metadata, + fee: Int(32), + partner: Address, }), + Via, t.partial({ secret: Secret, expiration: t.number, @@ -72,7 +76,10 @@ export namespace transfer { /** A LockedTransfer was signed and should be sent to partner */ export const transferSigned = createAction( 'transfer/signed', - t.type({ message: Signed(LockedTransfer), fee: Int(32), partner: Address }), + t.intersection([ + t.type({ message: Signed(LockedTransfer), fee: Int(32), partner: Address }), + Via, + ]), TransferId, ); export interface transferSigned extends ActionType {} @@ -80,7 +87,7 @@ export interface transferSigned extends ActionType {} /** Partner acknowledge they received and processed our LockedTransfer */ export const transferProcessed = createAction( 'transfer/processed', - t.type({ message: Signed(Processed) }), + t.intersection([t.type({ message: Signed(Processed) }), Via]), TransferId, ); export interface transferProcessed extends ActionType {} @@ -117,7 +124,7 @@ export namespace transferSecretRegister { /** A valid SecretRequest received from target */ export const transferSecretRequest = createAction( 'transfer/secret/request', - t.type({ message: Signed(SecretRequest) }), + t.intersection([t.type({ message: Signed(SecretRequest) }), Via]), TransferId, ); export interface transferSecretRequest extends ActionType {} @@ -125,7 +132,7 @@ export interface transferSecretRequest extends ActionType {} @@ -135,8 +142,8 @@ export const transferUnlock = createAsyncAction( 'transfer/unlock/request', 'transfer/unlock/success', 'transfer/unlock/failure', - undefined, - t.type({ message: Signed(Unlock), partner: Address }), + t.union([t.undefined, Via]), + t.intersection([t.type({ message: Signed(Unlock), partner: Address }), Via]), ); export namespace transferUnlock { @@ -148,7 +155,7 @@ export namespace transferUnlock { /** Partner acknowledge they received and processed our Unlock */ export const transferUnlockProcessed = createAction( 'transfer/unlock/processed', - t.type({ message: Signed(Processed) }), + t.intersection([t.type({ message: Signed(Processed) }), Via]), TransferId, ); export interface transferUnlockProcessed extends ActionType {} @@ -178,7 +185,7 @@ export namespace transferExpire { /** Partner acknowledge they received and processed our LockExpired */ export const transferExpireProcessed = createAction( 'transfer/expire/processed', - t.type({ message: Signed(Processed) }), + t.intersection([t.type({ message: Signed(Processed) }), Via]), TransferId, ); export interface transferExpireProcessed extends ActionType {} diff --git a/raiden-ts/src/transfers/epics/locked.ts b/raiden-ts/src/transfers/epics/locked.ts index dfc17df49e..ffa130fff8 100644 --- a/raiden-ts/src/transfers/epics/locked.ts +++ b/raiden-ts/src/transfers/epics/locked.ts @@ -21,7 +21,7 @@ import { channelAmounts, channelKey, channelUniqueKey } from '../../channels/uti import type { RaidenConfig } from '../../config'; import { Capabilities } from '../../constants'; import { messageReceived, messageSend } from '../../messages/actions'; -import type { Metadata, Processed, SecretRequest } from '../../messages/types'; +import type { Processed, SecretRequest } from '../../messages/types'; import { LockedTransfer, LockExpired, @@ -65,7 +65,14 @@ import { } from '../actions'; import type { TransferState } from '../state'; import { Direction } from '../state'; -import { getLocksroot, getSecrethash, getTransfer, makeMessageId, transferKey } from '../utils'; +import { + getLocksroot, + getSecrethash, + getTransfer, + makeMessageId, + searchValidViaAddress, + transferKey, +} from '../utils'; import { matchWithdraw } from './utils'; // calculate locks array for channel end without lock with given secrethash @@ -117,15 +124,7 @@ function makeAndSignTransfer$( { revealTimeout, confirmationBlocks, expiryFactor }: RaidenConfig, { log, address, network, signer }: RaidenEpicDeps, ): Observable { - // assume paths are valid and recipient is first hop of first route - // compose metadata from it, and use first path fee - const metadata: Metadata = { - routes: action.payload.paths.map(({ path }) => ({ route: path })), - }; - const fee = action.payload.paths[0].fee; - const partner = action.payload.paths[0].path[0]; - - const tokenNetwork = action.payload.tokenNetwork; + const { tokenNetwork, fee, partner, userId } = action.payload; const channel = getOpenChannel(state, { tokenNetwork, partner }); assert( @@ -170,7 +169,7 @@ function makeAndSignTransfer$( ', to', action.payload.target, ', through routes', - action.payload.paths, + action.payload.metadata.routes, ', paying', fee.toString(), 'in fees.', @@ -192,13 +191,13 @@ function makeAndSignTransfer$( lock, target: action.payload.target, initiator: action.payload.initiator ?? address, - metadata, + metadata: action.payload.metadata, // passthrough unchanged metadata }; return from(signMessage(signer, message, { log })).pipe( mergeMap(function* (signed) { // messageSend LockedTransfer handled by transferRetryMessageEpic - yield transferSigned({ message: signed, fee, partner }, action.meta); + yield transferSigned({ message: signed, fee, partner, userId }, action.meta); // besides transferSigned, also yield transferSecret (for registering) if we know it if (action.payload.secret) yield transferSecret({ secret: action.payload.secret }, action.meta); @@ -309,7 +308,10 @@ function makeAndSignUnlock$( channel.own.locks.find((lock) => lock.secrethash === secrethash && lock.registered), 'lock expired', ); - return transferUnlock.success({ message: signed, partner }, action.meta); + return transferUnlock.success( + { message: signed, partner, userId: action.payload?.userId }, + action.meta, + ); // messageSend Unlock handled by transferRetryMessageEpic // we don't check if transfer was refunded. If partner refunded the transfer but still // forwarded the payment, we still act honestly and unlock if they revealed @@ -463,7 +465,10 @@ function receiveTransferSigned( ) { // transferProcessed again will trigger messageSend.request return of( - transferProcessed({ message: untime(transferState.transferProcessed!) }, meta), + transferProcessed( + { message: untime(transferState.transferProcessed!), userId: action.payload.userId }, + meta, + ), ); } return EMPTY; @@ -562,12 +567,18 @@ function receiveTransferSigned( mergeMap(function* ([processed, request]) { yield transferSigned({ message: locked, fee: Zero as Int<32>, partner }, meta); // sets TransferState.transferProcessed - yield transferProcessed({ message: processed }, meta); + yield transferProcessed({ message: processed, userId: action.payload.userId }, meta); if (request) { // request initiator's presence, to be able to request secret yield matrixPresence.request(undefined, { address: locked.initiator }); // request secret iff we're the target and receiving is enabled - yield transferSecretRequest({ message: request }, meta); + yield transferSecretRequest( + { + message: request, + ...searchValidViaAddress(locked.metadata, locked.initiator), + }, + meta, + ); } }), ); @@ -604,7 +615,10 @@ function receiveTransferUnlocked( ) { // transferProcessed again will trigger messageSend.request return of( - transferUnlockProcessed({ message: untime(transferState.unlockProcessed!) }, meta), + transferUnlockProcessed( + { message: untime(transferState.unlockProcessed!), userId: action.payload.userId }, + meta, + ), ); } else return EMPTY; } @@ -639,7 +653,10 @@ function receiveTransferUnlocked( if (!transferState.secret) yield transferSecret({ secret: unlock.secret }, meta); yield transferUnlock.success({ message: unlock, partner }, meta); // sets TransferState.transferProcessed - yield transferUnlockProcessed({ message: processed }, meta); + yield transferUnlockProcessed( + { message: processed, userId: action.payload.userId }, + meta, + ); yield transfer.success( { balanceProof: getBalanceProofFromEnvelopeMessage(unlock) }, meta, @@ -680,7 +697,10 @@ function receiveTransferExpired( ) { // transferProcessed again will trigger messageSend.request return of( - transferExpireProcessed({ message: untime(transferState.expiredProcessed!) }, meta), + transferExpireProcessed( + { message: untime(transferState.expiredProcessed!), userId: action.payload.userId }, + meta, + ), ); } else return EMPTY; } @@ -718,7 +738,10 @@ function receiveTransferExpired( mergeMap(function* (processed) { yield transferExpire.success({ message: expired, partner }, meta); // sets TransferState.transferProcessed - yield transferExpireProcessed({ message: processed }, meta); + yield transferExpireProcessed( + { message: processed, userId: action.payload.userId }, + meta, + ); yield transfer.failure( new RaidenError(ErrorCodes.XFER_EXPIRED, { block: locked.lock.expiration.toString(), diff --git a/raiden-ts/src/transfers/epics/processed.ts b/raiden-ts/src/transfers/epics/processed.ts index 173d26237c..3df7392e6e 100644 --- a/raiden-ts/src/transfers/epics/processed.ts +++ b/raiden-ts/src/transfers/epics/processed.ts @@ -98,13 +98,10 @@ export function transferProcessedSendEpic( ), ), map(([action, transferState]) => - messageSend.request( - { message: action.payload.message }, - { - address: transferState.partner, - msgId: action.payload.message.message_identifier.toString(), - }, - ), + messageSend.request(action.payload, { + address: transferState.partner, + msgId: action.payload.message.message_identifier.toString(), + }), ), ); } diff --git a/raiden-ts/src/transfers/epics/retry.ts b/raiden-ts/src/transfers/epics/retry.ts index 6ff844c375..cfd9752041 100644 --- a/raiden-ts/src/transfers/epics/retry.ts +++ b/raiden-ts/src/transfers/epics/retry.ts @@ -52,11 +52,13 @@ export function transferRetryMessageEpic( const transfer$ = state$.pipe(pluckDistinct('transfers', transferKey(action.meta))); let to: Address | undefined; + let viaUserId: string | undefined; let stop$: Observable | undefined; switch (action.type) { case transferSigned.type: if (action.meta.direction === Direction.SENT) { to = action.payload.partner; + viaUserId = action.payload.userId; stop$ = transfer$.pipe( filter( (transfer) => @@ -73,6 +75,7 @@ export function transferRetryMessageEpic( case transferUnlock.success.type: if (action.meta.direction === Direction.SENT) { to = action.payload.partner; + viaUserId = action.payload.userId; stop$ = transfer$.pipe( filter((transfer) => !!(transfer.unlockProcessed || transfer.channelClosed)), ); @@ -89,6 +92,7 @@ export function transferRetryMessageEpic( case transferSecretRequest.type: if (action.meta.direction === Direction.RECEIVED && transfer) { to = transfer.transfer.initiator; + viaUserId = action.payload.userId; stop$ = combineLatest([state$, transfer$]).pipe( filter( ([{ blockNumber }, transfer]) => @@ -104,6 +108,7 @@ export function transferRetryMessageEpic( case transferSecretReveal.type: if (action.meta.direction === Direction.RECEIVED && transfer) { to = transfer.partner; + viaUserId = action.payload.userId; stop$ = transfer$.pipe( filter((transfer) => !!(transfer.unlock || transfer.channelClosed)), ); @@ -115,7 +120,7 @@ export function transferRetryMessageEpic( return retrySendUntil$( messageSend.request( - { message: action.payload.message }, + { message: action.payload.message, userId: viaUserId }, { address: to, msgId: action.payload.message.message_identifier.toString() }, ), action$, diff --git a/raiden-ts/src/transfers/epics/secret.ts b/raiden-ts/src/transfers/epics/secret.ts index 9a48c593a3..cf8f220102 100644 --- a/raiden-ts/src/transfers/epics/secret.ts +++ b/raiden-ts/src/transfers/epics/secret.ts @@ -42,7 +42,7 @@ import { transferUnlock, } from '../actions'; import { Direction } from '../state'; -import { getSecrethash, makeMessageId, transferKey } from '../utils'; +import { getSecrethash, makeMessageId, searchValidViaAddress, transferKey } from '../utils'; import { dispatchAndWait$ } from './utils'; /** @@ -82,7 +82,7 @@ export function transferSecretRequestedEpic( return; } yield transferSecretRequest( - { message }, + { message, userId: action.payload.userId }, { secrethash: message.secrethash, direction: Direction.SENT }, ); }), @@ -146,7 +146,7 @@ const secretReveal$ = ( mergeMap(function* (message) { yield transferSecretReveal({ message }, action.meta); yield messageSend.request( - { message }, + { message, userId: action.payload.userId }, { address: target, msgId: message.message_identifier.toString() }, ); }), @@ -223,8 +223,11 @@ export function transferSecretRevealedEpic( !sent.channelClosed // accepts secretReveal/unlock request even if registered on-chain ) { + let viaPayload: transferUnlock.request['payload']; + // unlock _through_ sender iff message is signed + if ('signature' in message) viaPayload = { userId: action.payload.userId }; // request unlock to be composed, signed & sent to partner - yield transferUnlock.request(undefined, meta); + yield transferUnlock.request(viaPayload, meta); } } // avoid unlocking received transfers if receiving is disabled @@ -268,19 +271,27 @@ export function transferRequestUnlockEpic( first(), filter(({ transfers }) => transferKey(action.meta) in transfers), mergeMap(({ transfers }) => { - const cached = transfers[transferKey(action.meta)]?.secretReveal; + const transferState = transfers[transferKey(action.meta)]!; + const cached = transferState.secretReveal; + let signed$; if (cached) { - return of(untime(cached)); + signed$ = of(untime(cached)); } else { const message: SecretReveal = { type: MessageType.SECRET_REVEAL, message_identifier: makeMessageId(), secret: action.payload.secret, }; - return signMessage(signer, message, { log }); + signed$ = from(signMessage(signer, message, { log })); } + const via = searchValidViaAddress( + transferState.transfer.metadata, + transferState.partner, + ); + return signed$.pipe( + map((message) => transferSecretReveal({ message, ...via }, action.meta)), + ); }), - map((message) => transferSecretReveal({ message }, action.meta)), catchError((err) => { log.warn('Error trying to sign SecretReveal - ignoring', err, action.meta); return EMPTY; diff --git a/raiden-ts/src/transfers/epics/utils.ts b/raiden-ts/src/transfers/epics/utils.ts index bffa040061..ac8597ca6a 100644 --- a/raiden-ts/src/transfers/epics/utils.ts +++ b/raiden-ts/src/transfers/epics/utils.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { Observable } from 'rxjs'; -import { merge, of } from 'rxjs'; +import { defer, merge, of } from 'rxjs'; import { filter, ignoreElements, take } from 'rxjs/operators'; import type { RaidenAction } from '../../actions'; @@ -73,10 +73,17 @@ export function retrySendUntil$( notifier: Observable, delayMs: number | Iterator = 30e3, ): Observable { - return dispatchAndWait$(action$, send, isResponseOf(messageSend, send.meta)).pipe( - repeatUntil(notifier, delayMs), - completeWith(action$), - ); + let first = true; + return defer(() => { + if (first) { + first = false; + } else if (send.payload.userId) { + // from 1st retry on, pop payload.userId, to force re-fetch presence/metadata + const { userId: _, ...payload } = send.payload; + send = { ...send, payload }; + } + return dispatchAndWait$(action$, send, isResponseOf(messageSend, send.meta)); + }).pipe(repeatUntil(notifier, delayMs), completeWith(action$)); } /** diff --git a/raiden-ts/src/transfers/mediate/epics.ts b/raiden-ts/src/transfers/mediate/epics.ts index 64214e8560..47a745a236 100644 --- a/raiden-ts/src/transfers/mediate/epics.ts +++ b/raiden-ts/src/transfers/mediate/epics.ts @@ -6,15 +6,15 @@ import { ChannelState } from '../../channels/state'; import { channelKey } from '../../channels/utils'; import type { RaidenConfig } from '../../config'; import { Capabilities } from '../../constants'; -import type { RouteMetadata } from '../../messages/types'; +import { validateAddressMetadata } from '../../messages/utils'; import type { RaidenState } from '../../state'; +import type { Via } from '../../transport/types'; import { getCap } from '../../transport/utils'; import type { RaidenEpicDeps } from '../../types'; import type { Address, Int } from '../../utils/types'; import { isntNil } from '../../utils/types'; import { transfer, transferSigned } from '../actions'; import { Direction } from '../state'; -import { transferKey } from '../utils'; function shouldMediate(action: transferSigned, address: Address, { caps }: RaidenConfig): boolean { const isMediationEnabled = getCap(caps, Capabilities.MEDIATE); @@ -30,52 +30,63 @@ function shouldMediate(action: transferSigned, address: Address, { caps }: Raide * we return a clean set of routes where all of them go through the same partner, since we need to * know the outbound channel in order to calculate the mediation fees. * - * @param routes - RouteMetadata array containing the received routes + * @param received - received transferSigned * @param state - Current state (to check for open channels) - * @param tokenNetwork - Transfer's TokenNetwork address + * @param config - Current config * @param deps - Epics dependencies subset * @param deps.address - Our address * @param deps.log - Logger instance + * @param deps.mediationFeeCalculator - Calculator for mediating transfer's fee * @returns Filtered and cleaned routes array, or undefined if no valid route was found */ -function filterAndCleanValidRoutes( - routes: readonly RouteMetadata[], +function findValidPartner( + received: transferSigned, state: RaidenState, - tokenNetwork: Address, - { address, log }: Pick, -) { - const allPartnersWithOpenChannels = new Set( + config: RaidenConfig, + { address, log, mediationFeeCalculator }: RaidenEpicDeps, +): Pick | undefined { + const message = received.payload.message; + const inPartner = received.payload.partner; + const tokenNetwork = message.token_network_address; + const partnersWithOpenChannels = new Set( Object.values(state.channels) .filter((channel) => channel.tokenNetwork === tokenNetwork) .filter((channel) => channel.state === ChannelState.open) .map(({ partner }) => partner.address), ); + for (const { route, address_metadata } of message.metadata.routes) { + const outPartner = route[route.indexOf(address) + 1]; + if (!outPartner || !partnersWithOpenChannels.has(outPartner)) continue; - function clearRoute(metadata: RouteMetadata) { - let route = metadata.route; - const ourIndex = route.findIndex((hop) => hop === address); - // if we're in the path (front or mid), forward only remaining path, starting with partner/next hop - if (ourIndex >= 0) route = route.slice(ourIndex + 1); - // next hop must be our partner, otherwise return null to drop this route - if (!allPartnersWithOpenChannels.has(route[0])) return null; - return { ...metadata, route }; - } - let result = routes.map(clearRoute).filter(isntNil); + const channelIn = state.channels[channelKey({ tokenNetwork, partner: inPartner })]; + const channelOut = state.channels[channelKey({ tokenNetwork, partner: outPartner })]; - const outPartner = result[0]?.route[0]; - if (!outPartner) { - log.warn('Mediation: could not find a suitable route, ignoring', { - inputRoutes: routes, - openPartners: allPartnersWithOpenChannels, - }); - return; - } - // filter only for routes matching first hop; in the old days of RefundTransfer, - // we could/should retry the following partners upon receiving a refund, but now we only - // support a single outgoing partner, and drop the others (if any) - result = result.filter(({ route }) => route[0] === outPartner); + let fee: Int<32>; + try { + fee = mediationFeeCalculator.fee( + config.mediationFees, + channelIn, + channelOut, + )(message.lock.amount); + } catch (error) { + log.warn('Mediation: could not calculate mediation fee, ignoring', { error }); + return; + } + // on a transfer.request, fee is *added* to the value to get final sent amount, + // therefore here it needs to contain a negative fee, which we will "earn" instead of pay + fee = fee.mul(-1) as Int<32>; - return result; + const outPartnerMetadata = validateAddressMetadata( + address_metadata?.[outPartner], + outPartner, + { log }, + ); + return { partner: outPartner, fee, userId: outPartnerMetadata?.user_id }; + } + log.warn('Mediation: could not find a suitable route, ignoring', { + inputRoutes: message.metadata.routes, + openPartners: partnersWithOpenChannels, + }); } /** @@ -99,45 +110,30 @@ function filterAndCleanValidRoutes( export function transferMediateEpic( action$: Observable, state$: Observable, - { address, config$, log, mediationFeeCalculator }: RaidenEpicDeps, + deps: RaidenEpicDeps, ) { + const { address, config$ } = deps; return action$.pipe( filter(transferSigned.is), withLatestFrom(config$, state$), filter(([action, config]) => shouldMediate(action, address, config)), map(([action, config, state]) => { - const receivedState = state.transfers[transferKey(action.meta)]; const message = action.payload.message; - - const tokenNetwork = message.token_network_address; const secrethash = action.meta.secrethash; - const inPartner = receivedState.partner; - - const cleanRoutes = filterAndCleanValidRoutes(message.metadata.routes, state, tokenNetwork, { - address, - log, - }); - if (!cleanRoutes) return; - const outPartner = cleanRoutes[0].route[0]; - const channelIn = state.channels[channelKey({ tokenNetwork, partner: inPartner })]; - const channelOut = state.channels[channelKey({ tokenNetwork, partner: outPartner })]; + const outVia = findValidPartner(action, state, config, deps); + if (!outVia) return; - let fee: Int<32>; - try { - fee = mediationFeeCalculator.fee( - config.mediationFees, - channelIn, - channelOut, - )(message.lock.amount); - } catch (error) { - log.warn('Mediation: could not calculate mediation fee, ignoring', { error }); - return; - } - // on a transfer.request, fee is *added* to the value to get final sent amount, - // therefore here it needs to contain a negative fee, which we will "earn" instead of pay - fee = fee.mul(-1) as Int<32>; - const paths = cleanRoutes.map(({ route }) => ({ path: route, fee })); + // FIXME: passthrough metadata unchanged once PC supports searching themselves in the route + // clean up routes (strip hops before and up to us), while PC mediators require receiving + // routes where they're the 1st address and their partner, 2nd + const metadata: typeof message.metadata = { + ...message.metadata, + routes: message.metadata.routes.map(({ route, ...rest }) => ({ + ...rest, + route: route.slice(route.indexOf(deps.address) + 1), + })), + }; // request an outbound transfer to target; will fail if already sent return transfer.request( @@ -146,9 +142,10 @@ export function transferMediateEpic( target: message.target, paymentId: message.payment_identifier, value: message.lock.amount, - paths, expiration: message.lock.expiration.toNumber(), initiator: message.initiator, + metadata, + ...outVia, }, { secrethash, direction: Direction.SENT }, ); diff --git a/raiden-ts/src/transfers/utils.ts b/raiden-ts/src/transfers/utils.ts index 09746639b9..03e2e3c99d 100644 --- a/raiden-ts/src/transfers/utils.ts +++ b/raiden-ts/src/transfers/utils.ts @@ -4,6 +4,7 @@ import { HashZero } from '@ethersproject/constants'; import { keccak256 } from '@ethersproject/keccak256'; import { randomBytes } from '@ethersproject/random'; import { sha256 } from '@ethersproject/sha2'; +import isEmpty from 'lodash/isEmpty'; import type { Observable } from 'rxjs'; import { defer, from, of } from 'rxjs'; import { filter, first, map, mergeMap } from 'rxjs/operators'; @@ -13,12 +14,20 @@ import type { Lock } from '../channels/types'; import { BalanceProofZero } from '../channels/types'; import { channelUniqueKey } from '../channels/utils'; import type { RaidenDatabase, TransferStateish } from '../db/types'; -import { createBalanceHash, getBalanceProofFromEnvelopeMessage } from '../messages'; +import type { Metadata } from '../messages'; +import { + createBalanceHash, + getBalanceProofFromEnvelopeMessage, + validateAddressMetadata, +} from '../messages'; +import type { Paths } from '../services/types'; import type { RaidenState } from '../state'; +import type { Via } from '../transport/types'; import { assert } from '../utils'; import { encode } from '../utils/data'; -import type { Hash, HexString, Secret, UInt } from '../utils/types'; +import type { Address, Hash, HexString, Secret, UInt } from '../utils/types'; import { decode, isntNil } from '../utils/types'; +import type { transfer } from './actions'; import type { RaidenTransfer } from './state'; import { Direction, RaidenTransferStatus, TransferState } from './state'; @@ -246,3 +255,43 @@ export async function getTransfer( if (key in state.transfers) return state.transfers[key]; return decode(TransferState, await db.get(key)); } + +/** + * Contructs transfer.request's payload paramaters from received PFS's Paths + * + * @param paths - Paths array coming from PFS + * @returns Respective members of transfer.request's payload + */ +export function metadataFromPaths( + paths: Paths, +): Pick { + // paths may come with undesired parameters, so map&filter here before passing to metadata + const routes = paths.map(({ path: route, fee: _, address_metadata }) => ({ + route, + ...(address_metadata && !isEmpty(address_metadata) ? { address_metadata } : {}), + })); + const metadata: Metadata = { routes }; + const viaPath = paths[0]; // assume incoming Paths is clean and best path is first + const partner = viaPath.path[0]; + const fee = viaPath.fee; + const partnerMetadata = validateAddressMetadata(viaPath.address_metadata?.[partner], partner); + const via: Via = { userId: partnerMetadata?.user_id }; + return { metadata, fee, partner, ...via }; +} + +/** + * @param metadata - Transfer metadata to search on + * @param address - Address metadata to search for + * @returns Via object or undefined + */ +export function searchValidViaAddress( + metadata: Metadata | undefined, + address: Address | undefined, +): Via | undefined { + let userId; + if (!metadata || !address) return; + for (const { address_metadata } of metadata.routes) { + if ((userId = validateAddressMetadata(address_metadata?.[address], address)?.user_id)) + return { userId }; + } +} diff --git a/raiden-ts/src/transport/epics/helpers.ts b/raiden-ts/src/transport/epics/helpers.ts deleted file mode 100644 index e15409828d..0000000000 --- a/raiden-ts/src/transport/epics/helpers.ts +++ /dev/null @@ -1,88 +0,0 @@ -import curry from 'lodash/curry'; -import type { MatrixClient, Room } from 'matrix-js-sdk'; -import type { Observable } from 'rxjs'; -import { fromEvent, of } from 'rxjs'; -import { filter, take } from 'rxjs/operators'; - -import type { RaidenConfig } from '../../config'; -import type { Message } from '../../messages/types'; -import { decodeJsonMessage, getMessageSigner } from '../../messages/utils'; -import type { RaidenEpicDeps } from '../../types'; -import { ErrorCodes, RaidenError } from '../../utils/error'; -import type { Address, Signed } from '../../utils/types'; -import { isntNil } from '../../utils/types'; - -/** - * Return the array of configured global rooms - * - * @param config - object to gather the list from - * @returns Array of room names - */ -export function globalRoomNames(config: RaidenConfig) { - return [config.discoveryRoom, config.pfsRoom, config.monitoringRoom].filter(isntNil); -} - -/** - * Curried function (arity=2) which matches room passed as second argument based on roomId, name or - * alias passed as first argument - * - * @param roomIdOrAlias - Room Id, name, canonical or normal alias for room - * @param room - Room to test - * @returns True if room matches term, false otherwise - */ -export const roomMatch = curry( - (roomIdOrAlias: string, room: Room) => - roomIdOrAlias === room.roomId || - roomIdOrAlias === room.name || - roomIdOrAlias === room.getCanonicalAlias() || - room.getAliases().includes(roomIdOrAlias), -); - -/** - * Returns an observable to a (possibly pending) room matching roomId or some alias - * This method doesn't try to join the room, just wait for it to show up in MatrixClient. - * - * @param matrix - Client instance to fetch room info from - * @param roomIdOrAlias - room id or alias to look for - * @returns Observable to populated room instance - */ -export function getRoom$(matrix: MatrixClient, roomIdOrAlias: string): Observable { - let room: Room | null | undefined = matrix.getRoom(roomIdOrAlias); - if (!room) room = matrix.getRooms().find(roomMatch(roomIdOrAlias)); - if (room) return of(room); - return fromEvent(matrix, 'Room').pipe(filter(roomMatch(roomIdOrAlias)), take(1)); -} - -/** - * Parse a received message into either a Message or Signed - * If Signed, the signer must match the sender's address. - * Errors are logged and undefined returned - * - * @param line - String to be parsed as a single message - * @param address - Sender's address - * @param deps - Dependencies - * @param deps.log - Logger instance - * @returns Validated Signed or unsigned Message, or undefined - */ -export function parseMessage( - line: any, // eslint-disable-line @typescript-eslint/no-explicit-any - address: Address, - { log }: Pick, -): Message | Signed | undefined { - if (typeof line !== 'string') return; - try { - const message = decodeJsonMessage(line); - // if Signed, accept only if signature matches sender address - if ('signature' in message) { - const signer = getMessageSigner(message); - if (signer !== address) - throw new RaidenError(ErrorCodes.TRNS_MESSAGE_SIGNATURE_MISMATCH, { - sender: address, - signer, - }); - } - return message; - } catch (err) { - log.warn(`Could not decode message: ${line}: ${err}`); - } -} diff --git a/raiden-ts/src/transport/epics/init.ts b/raiden-ts/src/transport/epics/init.ts index 0d6ae83925..6b0553e8ca 100644 --- a/raiden-ts/src/transport/epics/init.ts +++ b/raiden-ts/src/transport/epics/init.ts @@ -3,7 +3,7 @@ import constant from 'lodash/constant'; import isEmpty from 'lodash/isEmpty'; import sortBy from 'lodash/sortBy'; import type { Filter, LoginPayload, MatrixClient } from 'matrix-js-sdk'; -import { createClient, MatrixEvent } from 'matrix-js-sdk'; +import { createClient } from 'matrix-js-sdk'; import { logger as matrixLogger } from 'matrix-js-sdk/lib/logger'; import type { AsyncSubject, Observable } from 'rxjs'; import { combineLatest, defer, EMPTY, from, merge, of, throwError, timer } from 'rxjs'; @@ -18,7 +18,6 @@ import { map, mapTo, mergeMap, - pluck, retryWhen, take, tap, @@ -43,56 +42,18 @@ import { matrixSetup } from '../actions'; import type { RaidenMatrixSetup } from '../state'; import type { Caps } from '../types'; import { stringifyCaps } from '../utils'; -import { globalRoomNames } from './helpers'; - -/** - * Joins the global broadcast rooms and returns the room ids. - * - * @param config - The {@link RaidenConfig} provides the global room aliases. - * @param matrix - The {@link MatrixClient} instance used to create the filter. - * @returns Observable of the list of room ids for the the broadcast rooms. - */ -function joinGlobalRooms(config: RaidenConfig, matrix: MatrixClient): Observable { - const serverName = getServerName(matrix.getHomeserverUrl())!; - return from(globalRoomNames(config)).pipe( - map((globalRoom) => `#${globalRoom}:${serverName}`), - mergeMap((alias) => - matrix.joinRoom(alias).then((room) => { - // set alias in room state directly - // this trick is needed because global rooms aren't synced - const event = { - type: 'm.room.aliases' as const, - state_key: serverName, - origin_server_ts: Date.now(), - content: { aliases: [alias] }, - event_id: `$local_${Date.now()}`, - room_id: room.roomId, - sender: matrix.getUserId()!, - }; - room.currentState.setStateEvents([ - new MatrixEvent(event), - ] as MatrixEvent[]); // eslint-disable-line @typescript-eslint/no-explicit-any - matrix.store.storeRoom(room); - matrix.emit('Room', room); - return room; - }), - ), - pluck('roomId'), - toArray(), - ); -} /** * Creates and returns a matrix filter. The filter reduces the size of the initial sync by * filtering out broadcast rooms, emphemeral messages like receipts etc. * * @param matrix - The {@link MatrixClient} instance used to create the filter. - * @param roomIds - The ids of the rooms to filter out during sync. + * @param notRooms - The ids of the rooms to filter out during sync. * @returns Observable of the {@link Filter} that was created. */ -async function createMatrixFilter(matrix: MatrixClient, roomIds: string[]): Promise { +async function createMatrixFilter(matrix: MatrixClient, notRooms: string[] = []): Promise { const roomFilter = { - not_rooms: roomIds, + not_rooms: notRooms, ephemeral: { not_types: ['m.receipt', 'm.typing'], }, @@ -124,8 +85,7 @@ function startMatrixSync( // wait 1s before starting matrix, so event listeners can be registered delayWhen(([, { pollingInterval }]) => timer(Math.ceil(pollingInterval / 5))), mergeMap(([, config]) => - joinGlobalRooms(config, matrix).pipe( - mergeMap(async (roomIds) => createMatrixFilter(matrix, roomIds)), + defer(async () => createMatrixFilter(matrix)).pipe( mergeMap(async (filter) => { await matrix.setPushRuleEnabled('global', 'override', '.m.rule.master', true); return filter; diff --git a/raiden-ts/src/transport/epics/messages.ts b/raiden-ts/src/transport/epics/messages.ts index f7e51aae4f..6cd5f389bf 100644 --- a/raiden-ts/src/transport/epics/messages.ts +++ b/raiden-ts/src/transport/epics/messages.ts @@ -1,4 +1,6 @@ -import type { MatrixClient, MatrixEvent } from 'matrix-js-sdk'; +import constant from 'lodash/constant'; +import uniq from 'lodash/uniq'; +import type { MatrixEvent } from 'matrix-js-sdk'; import type { Observable } from 'rxjs'; import { asapScheduler, @@ -15,8 +17,10 @@ import { import { catchError, concatMap, + delayWhen, + endWith, filter, - groupBy, + ignoreElements, map, mergeMap, pluck, @@ -24,17 +28,18 @@ import { take, takeUntil, tap, + toArray, withLatestFrom, } from 'rxjs/operators'; import type { RaidenAction } from '../../actions'; -import type { RaidenConfig } from '../../config'; import { intervalFromConfig } from '../../config'; import { messageReceived, messageSend, messageServiceSend } from '../../messages/actions'; import type { Delivered, Message } from '../../messages/types'; import { MessageType, Processed, SecretRequest, SecretReveal } from '../../messages/types'; import { encodeJsonMessage, isMessageReceivedOfType, signMessage } from '../../messages/utils'; -import { Service } from '../../services/types'; +import { ServiceDeviceId } from '../../services/types'; +import { pfsInfoAddress } from '../../services/utils'; import type { RaidenState } from '../../state'; import type { RaidenEpicDeps } from '../../types'; import { isActionOf } from '../../utils/actions'; @@ -43,16 +48,14 @@ import { LruCache } from '../../utils/lru'; import { getServerName } from '../../utils/matrix'; import { completeWith, - concatBuffer, dispatchRequestAndGetResponse, mergeWith, pluckDistinct, retryWhile, } from '../../utils/rx'; -import { isntNil, Signed } from '../../utils/types'; +import { Address, isntNil, Signed } from '../../utils/types'; import { matrixPresence } from '../actions'; -import { getAddressFromUserId, getNoDeliveryPeers } from '../utils'; -import { getRoom$, globalRoomNames, parseMessage } from './helpers'; +import { getAddressFromUserId, getNoDeliveryPeers, parseMessage } from '../utils'; function getMessageBody(message: string | Signed): string { return typeof message === 'string' ? message : encodeJsonMessage(message); @@ -254,76 +257,73 @@ export function matrixMessageSendEpic( ); } -function sendGlobalMessages( - actions: readonly messageServiceSend.request[], - matrix: MatrixClient, - config: RaidenConfig, - { config$ }: Pick, -): Observable { - const servicesToRoomName = { - [Service.PFS]: config.pfsRoom, - [Service.MS]: config.monitoringRoom, - }; - const roomName = servicesToRoomName[actions[0].meta.service]; - const globalRooms = globalRoomNames(config); - assert(roomName && globalRooms.includes(roomName), [ - 'messageServiceSend for unknown global room', - { roomName, globalRooms: globalRooms.join(',') }, - ]); - const serverName = getServerName(matrix.getHomeserverUrl()); - const roomAlias = `#${roomName}:${serverName}`; - // batch action messages in a single text body - const body = actions.map((action) => getMessageBody(action.payload.message)).join('\n'); - const start = Date.now(); - let retries = -1; - return getRoom$(matrix, roomAlias).pipe( - // send message! - mergeMap(async (room) => { - retries++; - await matrix.sendEvent(room.roomId, 'm.room.message', { body, msgtype: textMsgType }, ''); - return { via: room.roomId, tookMs: Date.now() - start, retries }; +function sendServiceMessage(request: messageServiceSend.request, deps: RaidenEpicDeps) { + const { matrix$, config$, latest$ } = deps; + return matrix$.pipe( + withLatestFrom(latest$, config$), + mergeWith(([, , { additionalServices }]) => + from(additionalServices).pipe( + mergeMap( + (serviceAddrOrUrl) => + Address.is(serviceAddrOrUrl) + ? of(serviceAddrOrUrl) + : defer(async () => pfsInfoAddress(serviceAddrOrUrl, deps)).pipe( + catchError(constant(EMPTY)), + ), + 5, + ), + toArray(), + ), + ), + mergeMap(([[matrix, { state }], additionalServicesAddrs]) => { + const servicesAddrs = uniq( + additionalServicesAddrs.concat(Object.keys(state.services) as Address[]), + ); + assert(servicesAddrs.length, 'no services to messageServiceSend to'); + const serverName = getServerName(matrix.getHomeserverUrl()); + const userIds = servicesAddrs.map((service) => `@${service.toLowerCase()}:${serverName}`); + // batch action messages in a single text body + const content = { + msgtype: 'm.text', + body: getMessageBody(request.payload.message), + }; + const payload = Object.fromEntries( + userIds.map((uid) => [uid, { [ServiceDeviceId[request.meta.service]]: content }]), + ); + const start = Date.now(); + let retries = -1; + return defer(async () => { + retries++; + await matrix.sendToDevice('m.room.message', payload); // send message! + return messageServiceSend.success( + { via: userIds, tookMs: Date.now() - start, retries }, + request.meta, + ); + }).pipe(retryWhile(intervalFromConfig(config$), { maxRetries: 3, onErrors: networkErrors })); }), - retryWhile(intervalFromConfig(config$), { maxRetries: 3, onErrors: networkErrors }), + catchError((err) => of(messageServiceSend.failure(err, request.meta))), ); } /** - * Handles a [[messageServiceSend.request]] action and send one-shot message to a global room + * Handles a [[messageServiceSend.request]] action and send one-shot message to a service * * @param action$ - Observable of messageServiceSend actions * @param state$ - Observable of RaidenStates - * @param deps - RaidenEpicDeps members - * @param deps.log - Logger instance - * @param deps.matrix$ - MatrixClient async subject - * @param deps.config$ - Config observable - * @returns Empty observable (whole side-effect on matrix instance) + * @param deps - Epics dependencies + * @returns Observable of messageServiceSend.success|failure actions */ -export function matrixMessageGlobalSendEpic( +export function matrixMessageServiceSendEpic( action$: Observable, {}: Observable, deps: RaidenEpicDeps, ): Observable { - const { matrix$, config$ } = deps; return action$.pipe( filter(isActionOf(messageServiceSend.request)), - groupBy((action) => action.meta.service), - mergeMap((grouped$) => - grouped$.pipe( - concatBuffer((actions) => { - return matrix$.pipe( - withLatestFrom(config$), - mergeMap(([matrix, config]) => sendGlobalMessages(actions, matrix, config, deps)), - mergeMap((payload) => - from(actions.map((action) => messageServiceSend.success(payload, action.meta))), - ), - catchError((err) => - from(actions.map((action) => messageServiceSend.failure(err, action.meta))), - ), - completeWith(action$, 10), - ); - }, 20), - ), - ), + // wait until init$ is completed before handling requests, so state.services is populated + delayWhen(() => deps.init$.pipe(ignoreElements(), endWith(true))), + mergeMap((request) => sendServiceMessage(request, deps), 5), + completeWith(action$, 10), ); } diff --git a/raiden-ts/src/transport/epics/presence.ts b/raiden-ts/src/transport/epics/presence.ts index 4407e23e84..d27474cb35 100644 --- a/raiden-ts/src/transport/epics/presence.ts +++ b/raiden-ts/src/transport/epics/presence.ts @@ -1,15 +1,15 @@ -import { verifyMessage } from '@ethersproject/wallet'; -import getOr from 'lodash/fp/getOr'; import isEmpty from 'lodash/isEmpty'; import isEqual from 'lodash/isEqual'; -import minBy from 'lodash/minBy'; +import uniq from 'lodash/uniq'; import type { Observable } from 'rxjs'; -import { defer, EMPTY, merge, of } from 'rxjs'; +import { combineLatest, defer, from, merge, of } from 'rxjs'; import { catchError, + concatMap, distinctUntilChanged, exhaustMap, filter, + first, groupBy, ignoreElements, map, @@ -18,89 +18,63 @@ import { skip, switchMap, tap, - toArray, + timeout, withLatestFrom, } from 'rxjs/operators'; import type { RaidenAction } from '../../actions'; import { channelMonitored } from '../../channels/actions'; import { intervalFromConfig } from '../../config'; +import { PfsMode } from '../../services/types'; +import { getPresenceFromService$ } from '../../services/utils'; import type { RaidenState } from '../../state'; import type { RaidenEpicDeps } from '../../types'; import { isActionOf } from '../../utils/actions'; -import { assert, ErrorCodes, networkErrors } from '../../utils/error'; -import { getUserPresence } from '../../utils/matrix'; -import { completeWith, mergeWith, retryWhile } from '../../utils/rx'; +import { networkErrors } from '../../utils/error'; +import { catchAndLog, completeWith, retryWhile } from '../../utils/rx'; import type { Address } from '../../utils/types'; import { matrixPresence } from '../actions'; -import { getAddressFromUserId, parseCaps, stringifyCaps } from '../utils'; - -// unavailable just means the user didn't do anything over a certain amount of time, but they're -// still there, so we consider the user as available/online then -const AVAILABLE = ['online', 'unavailable']; +import { stringifyCaps } from '../utils'; /** - * Search user directory for valid users matching a given address and return latest + * Fetch peer's presence info from services * * @param address - Address of interest - * @param deps - Epics dependencies subset - * @param deps.log - Logger instance - * @param deps.matrix$ - Matrix client instance observable - * @param deps.config$ - Config observable + * @param deps - Epics dependencies * @returns Observable of user with most recent presence */ function searchAddressPresence$( address: Address, - { log, matrix$, config$ }: Pick, + deps: Pick, ) { - // search for any user containing the address of interest in its userId - return matrix$.pipe( - mergeWith(async (matrix) => matrix.searchUserDirectory({ term: address.toLowerCase() })), - retryWhile(intervalFromConfig(config$), { onErrors: networkErrors }), - // for every result matches, verify displayName signature is address of interest - mergeWith(function* ([, { results }]) { - for (const user of results) { - if (!user.display_name) continue; - try { - if (getAddressFromUserId(user.user_id) !== address) continue; - const recovered = verifyMessage(user.user_id, user.display_name); - if (!recovered || recovered !== address) continue; - } catch (err) { - continue; - } - yield user; - } - }), - mergeMap( - ([[matrix], user]) => - defer(async () => getUserPresence(matrix, user.user_id)).pipe( - map((presence) => ({ ...presence, ...user })), - retryWhile(intervalFromConfig(config$), { onErrors: networkErrors }), - catchError((err) => { - log.info('Error fetching user presence, ignoring:', err); - return EMPTY; - }), + const { config$, latest$ } = deps; + return combineLatest([latest$, config$]).pipe( + first(), + mergeMap(([{ state }, { pfsMode, additionalServices, httpTimeout }]) => { + let services = additionalServices; + if (pfsMode !== PfsMode.onlyAdditional) + services = uniq([...services, ...Object.keys(state.services)]); + return from(services).pipe( + concatMap((service) => + getPresenceFromService$(address, service, deps).pipe( + timeout(httpTimeout), + catchAndLog( + { onErrors: networkErrors, maxRetries: 1 }, + 'Error fetching presence from service', + address, + ), + ), ), - 3, // max parallelism on these requests - ), - toArray(), - // for all matched/verified users, get its presence through dedicated API - // it's required because, as the user events could already have been handled - // and filtered out by matrixPresenceUpdateEpic because it wasn't yet a - // user-of-interest, we could have missed presence updates, then we need to - // fetch it here directly, and from now on, that other epic will monitor its - // updates, and sort by most recently seen user - map((presences) => { - assert(presences.length, [ErrorCodes.TRNS_NO_VALID_USER, { address }]); - return minBy(presences, getOr(Number.POSITIVE_INFINITY, 'last_active_ago'))!; + first(), + ); }), - map(({ presence, user_id: userId, avatar_url }) => + map(({ user_id: userId, capabilities }) => matrixPresence.success( { userId, - available: AVAILABLE.includes(presence), + available: true, ts: Date.now(), - caps: parseCaps(avatar_url), + caps: capabilities, }, { address }, ), diff --git a/raiden-ts/src/transport/epics/rooms.ts b/raiden-ts/src/transport/epics/rooms.ts index 6f99af4266..53af729cd7 100644 --- a/raiden-ts/src/transport/epics/rooms.ts +++ b/raiden-ts/src/transport/epics/rooms.ts @@ -6,12 +6,10 @@ import { delayWhen, filter, ignoreElements, mergeMap, withLatestFrom } from 'rxj import type { RaidenAction } from '../../actions'; import type { RaidenState } from '../../state'; import type { RaidenEpicDeps } from '../../types'; -import { getServerName } from '../../utils/matrix'; import { completeWith, mergeWith } from '../../utils/rx'; -import { globalRoomNames, roomMatch } from './helpers'; /** - * Leave any (new or invited) room which is not global + * Leave any (new or invited) room * * @param action$ - Observable of RaidenActions * @param state$ - Observable of RaidenStates @@ -36,12 +34,9 @@ export function matrixLeaveUnknownRoomsEpic( ), completeWith(state$), // filter for leave events to us - filter(([[matrix, room], config]) => { + filter(([[, room]]) => { const myMembership = room.getMyMembership(); if (!myMembership || myMembership === 'leave') return false; // room already gone while waiting - const serverName = getServerName(matrix.getHomeserverUrl()); - if (globalRoomNames(config).some((g) => roomMatch(`#${g}:${serverName}`, room))) - return false; return true; }), mergeMap(async ([[matrix, room]]) => { diff --git a/raiden-ts/src/transport/epics/webrtc.ts b/raiden-ts/src/transport/epics/webrtc.ts index 5936ad3a68..75afc097d2 100644 --- a/raiden-ts/src/transport/epics/webrtc.ts +++ b/raiden-ts/src/transport/epics/webrtc.ts @@ -66,8 +66,7 @@ import { import type { Address } from '../../utils/types'; import { decode, isntNil, last } from '../../utils/types'; import { matrixPresence, rtcChannel } from '../actions'; -import { getCap } from '../utils'; -import { parseMessage } from './helpers'; +import { getCap, parseMessage } from '../utils'; interface CallInfo { callId: string; diff --git a/raiden-ts/src/transport/types.ts b/raiden-ts/src/transport/types.ts index 03daa72268..b3fb817560 100644 --- a/raiden-ts/src/transport/types.ts +++ b/raiden-ts/src/transport/types.ts @@ -1,10 +1,8 @@ import * as t from 'io-ts'; -import type { matrixPresence } from './actions'; - -export interface Presences { - [address: string]: matrixPresence.success; -} +/** Partner through which to send transfer through */ +export const Via = t.partial({ userId: t.string }); +export type Via = t.TypeOf; export const CapsPrimitive = t.union([t.string, t.number, t.boolean, t.null], 'CapsPrimitive'); export type CapsPrimitive = t.TypeOf; diff --git a/raiden-ts/src/transport/utils.ts b/raiden-ts/src/transport/utils.ts index 1770699f25..b41dc2255a 100644 --- a/raiden-ts/src/transport/utils.ts +++ b/raiden-ts/src/transport/utils.ts @@ -5,8 +5,12 @@ import { filter, scan, startWith } from 'rxjs/operators'; import type { RaidenAction } from '../actions'; import { Capabilities, CapsFallback } from '../constants'; +import type { Message } from '../messages/types'; +import { decodeJsonMessage, getMessageSigner } from '../messages/utils'; +import type { RaidenEpicDeps } from '../types'; import { jsonParse } from '../utils/data'; -import type { Address } from '../utils/types'; +import { assert, ErrorCodes } from '../utils/error'; +import type { Address, Signed } from '../utils/types'; import { matrixPresence } from './actions'; import type { Caps, CapsPrimitive } from './types'; @@ -116,3 +120,39 @@ export function getNoDeliveryPeers(): OperatorFunction + * If Signed, the signer must match the sender's address. + * Errors are logged and undefined returned + * + * @param line - String to be parsed as a single message + * @param address - Sender's address + * @param deps - Dependencies + * @param deps.log - Logger instance + * @returns Validated Signed or unsigned Message, or undefined + */ +export function parseMessage( + line: any, // eslint-disable-line @typescript-eslint/no-explicit-any + address: Address, + { log }: Pick, +): Message | Signed | undefined { + if (typeof line !== 'string') return; + try { + const message = decodeJsonMessage(line); + // if Signed, accept only if signature matches sender address + if ('signature' in message) { + const signer = getMessageSigner(message); + assert(signer === address, [ + ErrorCodes.TRNS_MESSAGE_SIGNATURE_MISMATCH, + { + sender: address, + signer, + }, + ]); + } + return message; + } catch (err) { + log.warn(`Could not decode message: ${line}: ${err}`); + } +} diff --git a/raiden-ts/tests/e2e/e2e.spec.ts b/raiden-ts/tests/e2e/e2e.spec.ts index 2fae0e1c86..05490c4307 100644 --- a/raiden-ts/tests/e2e/e2e.spec.ts +++ b/raiden-ts/tests/e2e/e2e.spec.ts @@ -60,6 +60,9 @@ async function createRaiden(account: number | string | Signer): Promise confirmationBlocks: 5, autoSettle: false, // required to use `settleChannel` later autoUDCWithdraw: false, // required to use `withdrawFromUDC` later + caps: { + [Capabilities.RECEIVE]: 1, + }, }, ); } @@ -122,7 +125,7 @@ describe('e2e', () => { * - Deposit and withdrawal from the UDC, including token minting * - Opening of channels * - Deposit and withdrawal of channels with PC and LC partners - * - Direct and mediatied transfers using PC and LC implementations + * - Direct and mediated transfers using PC and LC implementations * - Channel closing and settlement * * The topology of the nodes is the following: @@ -266,7 +269,7 @@ describe('e2e', () => { /* * Send mediated payment with LC as mediator * - * For this we need to enable medition in LC2 + * For this we need to enable mediation in LC2 */ raiden2.updateConfig({ caps: { diff --git a/raiden-ts/tests/integration/fixtures.ts b/raiden-ts/tests/integration/fixtures.ts index 19640df80c..617764e599 100644 --- a/raiden-ts/tests/integration/fixtures.ts +++ b/raiden-ts/tests/integration/fixtures.ts @@ -17,11 +17,13 @@ import { getTransfer, makePaymentId, makeSecret, + metadataFromPaths, transferKey, } from '@/transfers/utils'; import { matrixPresence } from '@/transport/actions'; +import { stringifyCaps } from '@/transport/utils'; +import type { Latest } from '@/types'; import { assert } from '@/utils'; -import { isResponseOf } from '@/utils/actions'; import type { Address, Hash, Int, Secret, UInt } from '@/utils/types'; import { makeAddress, makeHash, sleep } from '../utils'; @@ -122,6 +124,7 @@ export async function ensureChannelIsOpen( if (getChannel(raiden, partner)) return; const openBlock = raiden.deps.provider.blockNumber + 1; const tokenNetworkContract = raiden.deps.getTokenNetworkContract(tokenNetwork); + await ensurePresence([raiden, partner]); await providersEmit( {}, makeLog({ @@ -286,8 +289,11 @@ export async function ensureTransferPending( tokenNetwork, target: partner.address, value, - paths: [{ path: [partner.address], fee: Zero as Int<32> }], paymentId, + metadata: { routes: [{ route: [partner.address] }] }, + fee: Zero as Int<32>, + partner: partner.address, + userId: (await partner.deps.matrix$.toPromise()).getUserId()!, }, { secrethash: secrethash_, direction: Direction.SENT }, ), @@ -365,15 +371,31 @@ export async function ensurePresence([raiden, partner]: [ MockedRaiden, MockedRaiden, ]): Promise { - const raidenPromise = raiden.action$ - .pipe(first(isResponseOf(matrixPresence, { address: partner.address }))) - .toPromise(); - const partnerPromise = partner.action$ - .pipe(first(isResponseOf(matrixPresence, { address: raiden.address }))) - .toPromise(); + partner.store.dispatch( + matrixPresence.success( + { + userId: (await raiden.deps.matrix$.toPromise()).getUserId()!, + available: true, + ts: Date.now() + 120e3, + caps: (await raiden.deps.latest$.pipe(first()).toPromise()).config.caps!, + }, + { address: raiden.address }, + ), + ); + raiden.store.dispatch( + matrixPresence.success( + { + userId: (await partner.deps.matrix$.toPromise()).getUserId()!, + available: true, + ts: Date.now() + 120e3, + caps: (await partner.deps.latest$.pipe(first()).toPromise()).config.caps!, + }, + { address: partner.address }, + ), + ); + await sleep(); partner.store.dispatch(matrixPresence.request(undefined, { address: raiden.address })); raiden.store.dispatch(matrixPresence.request(undefined, { address: partner.address })); - await Promise.all([raidenPromise, partnerPromise]); } /** @@ -385,3 +407,38 @@ export function expectChannelsAreInSync([raiden, partner]: [MockedRaiden, Mocked expect(getChannel(raiden, partner).own).toStrictEqual(getChannel(partner, raiden).partner); expect(getChannel(raiden, partner).partner).toStrictEqual(getChannel(partner, raiden).own); } + +/** + * @param clients - Clients list + * @param clients."0" - Main/our raiden instance + * @param clients."1" - Other clients in path + * @param fee_ - Estimated transfer fee + * @returns metadataFromPaths for a tansfer.request's payload + */ +export function metadataFromClients( + [raiden, ...hops]: readonly [T, ...T[]], + fee_ = fee, +) { + const isRaiden = (c: T): c is T & MockedRaiden => typeof c !== 'string'; + return metadataFromPaths([ + { + path: hops.map((c) => (isRaiden(c) ? c.address : (c as Address))), + fee: fee_, + address_metadata: Object.fromEntries( + [raiden, ...hops].filter(isRaiden).map(({ address, store, deps }) => { + const setup = store.getState().transport.setup!; + let latest!: Latest; + deps.latest$.pipe(first()).subscribe((l) => (latest = l)); + return [ + address, + { + user_id: setup.userId, + displayname: setup.displayName, + capabilities: stringifyCaps(latest.config.caps!), + }, + ] as const; + }), + ), + }, + ]); +} diff --git a/raiden-ts/tests/integration/mediate.spec.ts b/raiden-ts/tests/integration/mediate.spec.ts index 5015ef856d..f10baa5bce 100644 --- a/raiden-ts/tests/integration/mediate.spec.ts +++ b/raiden-ts/tests/integration/mediate.spec.ts @@ -4,6 +4,7 @@ import { ensureChannelIsOpen, ensurePresence, getOrWaitTransfer, + metadataFromClients, secret, secrethash, token, @@ -47,8 +48,8 @@ describe('mediate transfers', () => { target: target.address, value: amount, paymentId: makePaymentId(), - paths: [{ path: [partner.address, target.address], fee: flat }], secret, + ...metadataFromClients([raiden, partner, target], flat), }, { secrethash, direction: Direction.SENT }, ), @@ -74,16 +75,25 @@ describe('mediate transfers', () => { { secrethash, direction: Direction.RECEIVED }, ), ); - expect(partner.output).toContainEqual( + expect(partner.output.find(transfer.request.is)).toEqual( transfer.request( { tokenNetwork, target: target.address, value: amount.add(flat) as UInt<32>, paymentId: transf.payment_identifier, - paths: [{ path: [target.address], fee: flat.mul(-1) as Int<32> }], expiration: transf.lock.expiration.toNumber(), initiator: raiden.address, + fee: flat.mul(-1) as Int<32>, + metadata: expect.objectContaining({ + routes: expect.arrayContaining([ + expect.objectContaining({ + route: [target.address], + }), + ]), + }), + partner: target.address, + userId: (await target.deps.matrix$.toPromise()).getUserId()!, }, { secrethash, direction: Direction.SENT }, ), @@ -114,8 +124,8 @@ describe('mediate transfers', () => { target: target.address, value: amount, paymentId: makePaymentId(), - paths: [{ path: [partner.address, target.address], fee: Zero as Int<32> }], secret, + ...metadataFromClients([raiden, partner, target], Zero as Int<32>), }, { secrethash, direction: Direction.SENT }, ), @@ -163,8 +173,8 @@ describe('mediate transfers', () => { target: unknownTarget, value: amount, paymentId: makePaymentId(), - paths: [{ path: [partner.address, unknownTarget], fee: Zero as Int<32> }], secret, + ...metadataFromClients([raiden, partner, unknownTarget], Zero as Int<32>), }, { secrethash, direction: Direction.SENT }, ), diff --git a/raiden-ts/tests/integration/mocks.ts b/raiden-ts/tests/integration/mocks.ts index c12762306c..c4637599c1 100644 --- a/raiden-ts/tests/integration/mocks.ts +++ b/raiden-ts/tests/integration/mocks.ts @@ -659,6 +659,7 @@ export async function makeRaiden( const signer = (wallet ?? makeWallet()).connect(provider); const address = signer.address as Address; const log = logging.getLogger(`raiden:${address}`); + log.setLevel(logging.levels.INFO); Object.assign(provider, { _network: network }); jest.spyOn(provider, 'on'); diff --git a/raiden-ts/tests/integration/patches.ts b/raiden-ts/tests/integration/patches.ts index f9ca090f44..a1bb07e9ee 100644 --- a/raiden-ts/tests/integration/patches.ts +++ b/raiden-ts/tests/integration/patches.ts @@ -36,12 +36,9 @@ const patchVerifyMessage = () => { return { ...jest.requireActual('@ethersproject/wallet'), __esModule: true, - verifyMessage: jest.fn((msg: string, sig: string): string => { - // TODO: remove userId special case after mockedMatrixCreateClient is used - const match = /^@(0x[0-9a-f]{40})[.:]/i.exec(msg); - if (match?.[1]) return getAddress(match[1]); - return getAddress('0x' + sig.substr(-44, 40)); - }), + verifyMessage: jest.fn((_: string, sig: string): string => + getAddress('0x' + sig.substr(-44, 40)), + ), }; }); }; diff --git a/raiden-ts/tests/integration/path.spec.ts b/raiden-ts/tests/integration/path.spec.ts index 58aa153300..ef7d61af78 100644 --- a/raiden-ts/tests/integration/path.spec.ts +++ b/raiden-ts/tests/integration/path.spec.ts @@ -4,6 +4,7 @@ import { ensureChannelIsClosed, ensureChannelIsDeposited, ensureChannelIsOpen, + ensurePresence, fee, getChannel, openBlock, @@ -15,6 +16,7 @@ import { fetch, makeLog, makeRaiden, makeRaidens, providersEmit, waitBlock } fro import { defaultAbiCoder } from '@ethersproject/abi'; import { BigNumber } from '@ethersproject/bignumber'; import { AddressZero, One, Zero } from '@ethersproject/constants'; +import { first } from 'rxjs/operators'; import { raidenConfigUpdate, raidenShutdown } from '@/actions'; import { Capabilities } from '@/constants'; @@ -24,6 +26,7 @@ import { iouClear, iouPersist, pathFind, servicesValid } from '@/services/action import { IOU, PfsMode, Service } from '@/services/types'; import { signIOU } from '@/services/utils'; import { matrixPresence } from '@/transport/actions'; +import { stringifyCaps } from '@/transport/utils'; import { jsonStringify } from '@/utils/data'; import { ErrorCodes } from '@/utils/error'; import type { Address, Int, Signature, UInt } from '@/utils/types'; @@ -61,6 +64,7 @@ describe('PFS: pfsRequestEpic', () => { const mockedPfsInfoResponse: jest.MockedFunction = jest.fn(); const mockedIouResponse: jest.MockedFunction = jest.fn(); const mockedPfsResponse: jest.MockedFunction = jest.fn(); + const mockedPresenceResponse: jest.MockedFunction = jest.fn(); function makePfsInfoResponse() { return { @@ -109,12 +113,41 @@ describe('PFS: pfsRequestEpic', () => { }; }); + mockedPresenceResponse.mockImplementation(async (url) => { + const addr = /\/(0x[0-9a-f]{40})\/metadata/i.exec(url!)?.[1]; + let client: MockedRaiden | undefined; + if (!addr || !(client = [raiden, partner, target].find(({ address }) => address === addr))) { + return { + status: 404, + ok: false, + json: jest.fn(async () => ({})), + text: jest.fn(async () => ''), + }; + } + const userId = (await client!.deps.matrix$.toPromise()).getUserId()!; + const result = { + user_id: userId, + displayname: await client!.deps.signer.signMessage(userId), + capabilities: stringifyCaps( + (await client!.deps.latest$.pipe(first()).toPromise()).config.caps!, + ), + }; + return { + status: 200, + ok: true, + json: jest.fn(async () => result), + text: jest.fn(async () => jsonStringify(result)), + }; + }); + fetch.mockImplementation(async (...args) => { const url = args[0]; - if (url?.includes?.('/iou')) { + if (url?.includes('/iou')) { return mockedIouResponse(...args); - } else if (url?.includes?.('/info')) { + } else if (url?.includes('/info')) { return mockedPfsInfoResponse(...args); + } else if (url?.includes('/metadata')) { + return mockedPresenceResponse(...args); } else { return mockedPfsResponse(...args); } @@ -136,6 +169,7 @@ describe('PFS: pfsRequestEpic', () => { mockedPfsInfoResponse.mockRestore(); mockedIouResponse.mockRestore(); mockedPfsResponse.mockRestore(); + mockedPresenceResponse.mockRestore(); }); test('fail unknown tokenNetwork', async () => { @@ -203,11 +237,8 @@ describe('PFS: pfsRequestEpic', () => { test('fail on failing matrix presence request', async () => { expect.assertions(2); - const matrix = await raiden.deps.matrix$.toPromise(); const matrixError = new Error('Unspecific matrix error for testing purpose'); - ( - matrix.searchUserDirectory as jest.MockedFunction - ).mockRejectedValue(matrixError); + mockedPresenceResponse.mockRejectedValue(matrixError); const pathFindMeta = { tokenNetwork, @@ -337,7 +368,15 @@ describe('PFS: pfsRequestEpic', () => { // self should be taken out of route expect(raiden.output).toContainEqual( pathFind.success( - { paths: [{ path: [partner.address], fee: Zero as Int<32> }] }, + { + paths: [ + { + path: [partner.address], + fee: Zero as Int<32>, + address_metadata: expect.objectContaining({ [raiden.address]: expect.anything() }), + }, + ], + }, pathFindMeta, ), ); @@ -511,6 +550,7 @@ describe('PFS: pfsRequestEpic', () => { expect.assertions(1); raiden.store.dispatch(raidenConfigUpdate({ pfsMode: PfsMode.auto, additionalServices: [] })); + await ensurePresence([raiden, target]); // invalid url raiden.deps.serviceRegistryContract.urls.mockResolvedValueOnce('""'); @@ -519,7 +559,9 @@ describe('PFS: pfsRequestEpic', () => { // invalid schema (on development mode, both http & https are accepted) raiden.deps.serviceRegistryContract.urls.mockResolvedValueOnce('ftp://not.https.url'); - raiden.store.dispatch(servicesValid(makeValidServices([pfsAddress, pfsAddress, pfsAddress]))); + raiden.store.dispatch( + servicesValid(makeValidServices([makeAddress(), makeAddress(), makeAddress()])), + ); await waitBlock(); const pathFindMeta = { @@ -606,7 +648,6 @@ describe('PFS: pfsRequestEpic', () => { }); test('success with free pfs and valid route', async () => { - // Original test(old version) fails expect.assertions(1); const freePfsInfoResponse = { ...makePfsInfoResponse(), price_info: 0 }; diff --git a/raiden-ts/tests/integration/receive.spec.ts b/raiden-ts/tests/integration/receive.spec.ts index 17c11bee68..9ecaaecf7b 100644 --- a/raiden-ts/tests/integration/receive.spec.ts +++ b/raiden-ts/tests/integration/receive.spec.ts @@ -1,8 +1,11 @@ import { + amount, ensureChannelIsDeposited, ensureTransferPending, expectChannelsAreInSync, + fee, getOrWaitTransfer, + metadataFromClients, secret, secrethash, tokenNetwork, @@ -16,7 +19,6 @@ import { waitBlock, } from './mocks'; -import { BigNumber } from '@ethersproject/bignumber'; import { One, Zero } from '@ethersproject/constants'; import { first } from 'rxjs/operators'; @@ -54,8 +56,7 @@ import { makeHash, sleep } from '../utils'; const direction = Direction.RECEIVED; const paymentId = makePaymentId(); -const value = BigNumber.from(10) as UInt<32>; -const fee = BigNumber.from(3) as Int<32>; +const value = amount; const receivedMeta = { secrethash, direction }; const sentMeta = { secrethash, direction: Direction.SENT }; @@ -81,8 +82,8 @@ describe('receive transfers', () => { tokenNetwork, target: raiden.address, value, - paths: [{ path: [raiden.address], fee }], paymentId, + ...metadataFromClients([partner, raiden]), }, sentMeta, ), @@ -149,6 +150,7 @@ describe('receive transfers', () => { type: MessageType.SECRET_REQUEST, secrethash, }), + userId: (await partner.deps.matrix$.toPromise()).getUserId()!, }, receivedMeta, ), @@ -173,8 +175,8 @@ describe('receive transfers', () => { tokenNetwork, target: raiden.address, value, - paths: [{ path: [raiden.address], fee }], paymentId, + ...metadataFromClients([partner, raiden]), }, sentMeta, ), diff --git a/raiden-ts/tests/integration/send.spec.ts b/raiden-ts/tests/integration/send.spec.ts index 33bb6b554a..0c2cc6f94b 100644 --- a/raiden-ts/tests/integration/send.spec.ts +++ b/raiden-ts/tests/integration/send.spec.ts @@ -1,10 +1,13 @@ import { + amount, ensureChannelIsDeposited, ensureTransferPending, ensureTransferUnlocked, expectChannelsAreInSync, + fee, getChannel, getOrWaitTransfer, + metadataFromClients, secret, secrethash, tokenNetwork, @@ -12,7 +15,7 @@ import { import { makeLog, makeRaiden, makeRaidens, providersEmit, waitBlock } from './mocks'; import { BigNumber } from '@ethersproject/bignumber'; -import { Two, Zero } from '@ethersproject/constants'; +import { MaxUint256, Zero } from '@ethersproject/constants'; import { keccak256 } from '@ethersproject/keccak256'; import { first, pluck } from 'rxjs/operators'; @@ -42,9 +45,7 @@ import type { MockedRaiden } from './mocks'; const direction = Direction.SENT; const paymentId = makePaymentId(); -const value = BigNumber.from(10) as UInt<32>; -const fee = BigNumber.from(3) as Int<32>; -const maxUInt256 = Two.pow(256).sub(1) as UInt<32>; +const value = amount; const meta = { secrethash, direction }; describe('send transfer', () => { @@ -60,9 +61,9 @@ describe('send transfer', () => { tokenNetwork, target: partner.address, value, - paths: [{ path: [partner.address], fee }], paymentId, secret, + ...metadataFromClients([raiden, partner]), }, meta, ); @@ -90,6 +91,7 @@ describe('send transfer', () => { message: expectedLockedTransfer, fee, partner: partner.address, + userId: (await partner.deps.matrix$.toPromise()).getUserId()!, }, meta, ), @@ -119,10 +121,10 @@ describe('send transfer', () => { { tokenNetwork, target: partner.address, - value: maxUInt256, - paths: [{ path: [partner.address], fee }], + value: MaxUint256 as UInt<32>, paymentId, secret, + ...metadataFromClients([raiden, partner]), }, meta, ); @@ -151,9 +153,9 @@ describe('send transfer', () => { tokenNetwork, target: partner.address, value, - paths: [{ path: [partner.address], fee }], paymentId, secret, + ...metadataFromClients([raiden, partner]), }, meta, ), @@ -208,6 +210,7 @@ describe('send transfer', () => { { message: expectedUnlock, partner: partner.address, + userId: (await partner.deps.matrix$.toPromise()).getUserId()!, }, meta, ), @@ -466,8 +469,8 @@ describe('transferRetryMessageEpic', () => { tokenNetwork, target: partner.address, value, - paths: [{ path: [partner.address], fee: Zero as Int<32> }], paymentId, + ...metadataFromClients([raiden, partner], Zero as Int<32>), }, meta, ), diff --git a/raiden-ts/tests/integration/transport.spec.ts b/raiden-ts/tests/integration/transport.spec.ts index b25edc4b52..1bff94fbe9 100644 --- a/raiden-ts/tests/integration/transport.spec.ts +++ b/raiden-ts/tests/integration/transport.spec.ts @@ -1,4 +1,4 @@ -import { ensureChannelIsOpen, ensurePresence, matrixServer, token } from './fixtures'; +import { ensureChannelIsOpen, ensurePresence, matrixServer } from './fixtures'; import { fetch, makeRaiden, makeRaidens, makeSignature } from './mocks'; import { verifyMessage } from '@ethersproject/wallet'; @@ -12,15 +12,18 @@ import { messageReceived, messageSend, messageServiceSend } from '@/messages/act import type { Delivered, Processed } from '@/messages/types'; import { MessageType } from '@/messages/types'; import { encodeJsonMessage, signMessage } from '@/messages/utils'; -import { Service } from '@/services/types'; +import { servicesValid } from '@/services/actions'; +import { Service, ServiceDeviceId } from '@/services/types'; import { makeMessageId } from '@/transfers/utils'; import { matrixPresence, matrixSetup, rtcChannel } from '@/transport/actions'; import { getSortedAddresses } from '@/transport/utils'; +import { jsonStringify } from '@/utils/data'; import { ErrorCodes } from '@/utils/error'; +import { getServerName } from '@/utils/matrix'; import type { Address, Signed } from '@/utils/types'; import { isntNil } from '@/utils/types'; -import { sleep } from '../utils'; +import { makeAddress, sleep } from '../utils'; import type { MockedRaiden } from './mocks'; const accessToken = 'access_token'; @@ -297,16 +300,19 @@ test('matrixShutdownEpic: stopClient called on action$ completion', async () => }); describe('matrixMonitorPresenceEpic', () => { + const json = jest.fn, []>(async () => ({})); + const capabilities = 'mxc://test?Delivery=0'; + + beforeAll(() => fetch.mockClear()); + beforeEach(() => fetch.mockImplementation(async () => ({ ok: true, status: 200, json }))); + afterEach(() => fetch.mockRestore()); + test('fails when users does not have displayName', async () => { expect.assertions(1); const [raiden, partner] = await makeRaidens(2); - const matrix = (await raiden.deps.matrix$.toPromise()) as jest.Mocked; - const partnerMatrix = (await partner.deps.matrix$.toPromise()) as jest.Mocked; - matrix.searchUserDirectory.mockImplementationOnce(async () => ({ - limited: false, - results: [{ user_id: partnerMatrix.getUserId()! }], - })); + const partnerUserId = (await partner.deps.matrix$.toPromise()).getUserId()!; + json.mockImplementationOnce(async () => ({ user_id: partnerUserId })); raiden.store.dispatch(matrixPresence.request(undefined, { address: partner.address })); @@ -319,31 +325,10 @@ describe('matrixMonitorPresenceEpic', () => { test('fails when users does not have valid addresses', async () => { expect.assertions(1); const [raiden, partner] = await makeRaidens(2); - const matrix = (await raiden.deps.matrix$.toPromise()) as jest.Mocked; - - matrix.searchUserDirectory.mockImplementation(async () => ({ - limited: false, - results: [{ user_id: `@invalidUser:${matrixServer}`, display_name: 'display_name' }], - })); - - raiden.store.dispatch(matrixPresence.request(undefined, { address: partner.address })); - - await sleep(2 * raiden.config.pollingInterval); - expect(raiden.output).toContainEqual( - matrixPresence.failure(expect.any(Error), { address: partner.address }), - ); - }); - - test('fails when users does not have presence or unknown address', async () => { - expect.assertions(1); - - const [raiden, partner] = await makeRaidens(2); - const matrix = (await raiden.deps.matrix$.toPromise()) as jest.Mocked; - const partnerMatrix = (await partner.deps.matrix$.toPromise()) as jest.Mocked; - (verifyMessage as jest.Mock).mockReturnValueOnce(token); - matrix.searchUserDirectory.mockImplementation(async () => ({ - limited: false, - results: [{ user_id: partnerMatrix.getUserId()!, display_name: 'display_name' }], + json.mockImplementationOnce(async () => ({ + user_id: `@invalidUser:${matrixServer}`, + displayname: '0x1234', + capabilities, })); raiden.store.dispatch(matrixPresence.request(undefined, { address: partner.address })); @@ -358,12 +343,11 @@ describe('matrixMonitorPresenceEpic', () => { expect.assertions(1); const [raiden, partner] = await makeRaidens(2); - const matrix = (await raiden.deps.matrix$.toPromise()) as jest.Mocked; - const partnerMatrix = (await partner.deps.matrix$.toPromise()) as jest.Mocked; - - matrix.searchUserDirectory.mockImplementation(async () => ({ - limited: false, - results: [{ user_id: partnerMatrix.getUserId()!, display_name: 'display_name' }], + const partnerUserId = (await partner.deps.matrix$.toPromise()).getUserId()!; + json.mockImplementationOnce(async () => ({ + user_id: partnerUserId, + displayname: '0x1234', + capabilities, })); (verifyMessage as jest.Mock).mockImplementationOnce(() => { throw new Error('invalid signature'); @@ -377,37 +361,15 @@ describe('matrixMonitorPresenceEpic', () => { ); }); - test('success with previously monitored user', async () => { - expect.assertions(1); - const [raiden, partner] = await makeRaidens(2); - const partnerMatrix = await partner.deps.matrix$.toPromise(); - const presence = matrixPresence.success( - { userId: partnerMatrix.getUserId()!, available: false, ts: Date.now() }, - { address: partner.address }, - ); - - raiden.store.dispatch(presence); - const sliceLength = raiden.output.length; - raiden.store.dispatch(matrixPresence.request(undefined, { address: partner.address })); - - await sleep(2 * raiden.config.pollingInterval); - expect(raiden.output.slice(sliceLength)).toContainEqual(presence); - }); - - test('success with searchUserDirectory and getUserPresence', async () => { + test('success', async () => { expect.assertions(1); const [raiden, partner] = await makeRaidens(2); - const matrix = (await raiden.deps.matrix$.toPromise()) as jest.Mocked; - const partnerMatrix = (await partner.deps.matrix$.toPromise()) as jest.Mocked; - matrix.searchUserDirectory.mockImplementation(async ({ term }) => ({ - results: [ - { - user_id: `@${term}:${matrixServer}`, - display_name: `${term}_display_name`, - avatar_url: 'mxc://raiden.network/cap?Delivery=0&randomCap=test', - }, - ], + const partnerUserId = (await partner.deps.matrix$.toPromise()).getUserId()!; + json.mockImplementationOnce(async () => ({ + user_id: partnerUserId, + displayname: partner.store.getState().transport.setup!.displayName, + capabilities: capabilities + '&randomCap=test', })); raiden.store.dispatch(matrixPresence.request(undefined, { address: partner.address })); @@ -416,7 +378,7 @@ describe('matrixMonitorPresenceEpic', () => { expect(raiden.output).toContainEqual( matrixPresence.success( { - userId: partnerMatrix.getUserId()!, + userId: partnerUserId, available: true, ts: expect.any(Number), caps: { [Capabilities.DELIVERY]: 0, randomCap: 'test' }, @@ -425,32 +387,6 @@ describe('matrixMonitorPresenceEpic', () => { ), ); }); - - test('success even if some getUserPresence fails', async () => { - expect.assertions(1); - - const [raiden, partner] = await makeRaidens(2); - const matrix = (await raiden.deps.matrix$.toPromise()) as jest.Mocked; - const partnerMatrix = (await partner.deps.matrix$.toPromise()) as jest.Mocked; - matrix.searchUserDirectory.mockImplementationOnce(async () => ({ - limited: false, - results: [ - { user_id: `@${partner.address.toLowerCase()}.2:${matrixServer}`, display_name: '2' }, - { user_id: partnerMatrix.getUserId()!, display_name: '1' }, - ], - })); - matrix._http.authedRequest.mockRejectedValueOnce(new Error('Could not fetch presence')); - - raiden.store.dispatch(matrixPresence.request(undefined, { address: partner.address })); - - await sleep(2 * raiden.config.pollingInterval); - expect(raiden.output).toContainEqual( - matrixPresence.success( - { userId: partnerMatrix.getUserId()!, available: true, ts: expect.any(Number) }, - { address: partner.address }, - ), - ); - }); }); test('matrixUpdateCapsEpic', async () => { @@ -488,57 +424,29 @@ test('matrixUpdateCapsEpic', async () => { ); }); -describe('matrixLeaveUnknownRoomsEpic', () => { - test('leave unknown rooms', async () => { - expect.assertions(3); - - const raiden = await makeRaiden(); - const matrix = (await raiden.deps.matrix$.toPromise()) as jest.Mocked; - const roomId = `!unknownRoomId:${matrixServer}`; +test('matrixLeaveUnknownRoomsEpic', async () => { + expect.assertions(3); - matrix.emit('Room', { - roomId, - getCanonicalAlias: jest.fn(), - getAliases: jest.fn(() => []), - getMyMembership: jest.fn(() => 'join'), - }); - - await sleep(); - - // we should wait a little before leaving rooms - expect(matrix.leave).not.toHaveBeenCalled(); - - await sleep(500); + const raiden = await makeRaiden(); + const matrix = (await raiden.deps.matrix$.toPromise()) as jest.Mocked; + const roomId = `!unknownRoomId:${matrixServer}`; - expect(matrix.leave).toHaveBeenCalledTimes(1); - expect(matrix.leave).toHaveBeenCalledWith(roomId); + matrix.emit('Room', { + roomId, + getCanonicalAlias: jest.fn(), + getAliases: jest.fn(() => []), + getMyMembership: jest.fn(() => 'join'), }); - test('do not leave global room', async () => { - expect.assertions(2); - - const roomId = `!discoveryRoomId:${matrixServer}`; - const raiden = await makeRaiden(undefined); - const matrix = (await raiden.deps.matrix$.toPromise()) as jest.Mocked; - const roomAlias = `#raiden_${raiden.deps.network.name}_discovery:${matrixServer}`; + await sleep(); - matrix.emit('Room', { - roomId, - getCanonicalAlias: jest.fn(), - getAliases: jest.fn(() => [roomAlias]), - getMyMembership: jest.fn(() => 'join'), - }); + // we should wait a little before leaving rooms + expect(matrix.leave).not.toHaveBeenCalled(); - await sleep(); + await sleep(500); - // we should wait a little before leaving rooms - expect(matrix.leave).not.toHaveBeenCalled(); - - await sleep(500); - - // even after some time, discovery room isn't left - expect(matrix.leave).not.toHaveBeenCalled(); - }); + expect(matrix.leave).toHaveBeenCalledTimes(1); + expect(matrix.leave).toHaveBeenCalledWith(roomId); }); describe('matrixMessageSendEpic', () => { @@ -549,7 +457,7 @@ describe('matrixMessageSendEpic', () => { const [raiden, partner] = getSortedClients(await makeRaidens(2)); raiden.store.dispatch(raidenConfigUpdate({ httpTimeout: 30 })); const matrix = (await raiden.deps.matrix$.toPromise()) as jest.Mocked; - const partnerMatrix = (await partner.deps.matrix$.toPromise()) as jest.Mocked; + const userId = '@peer:server'; await ensureChannelIsOpen([raiden, partner]); await sleep(); @@ -562,7 +470,7 @@ describe('matrixMessageSendEpic', () => { .mockRejectedValueOnce(Object.assign(new Error('Failed 4'), { httpStatus: 500 })); raiden.store.dispatch( - messageSend.request({ message }, { address: partner.address, msgId: message }), + messageSend.request({ message, userId }, { address: partner.address, msgId: message }), ); await sleep(200); @@ -576,7 +484,7 @@ describe('matrixMessageSendEpic', () => { expect(matrix.sendToDevice).toHaveBeenCalledWith( 'm.room.message', expect.objectContaining({ - [partnerMatrix.getUserId()!]: { ['*']: { body: message, msgtype: 'm.text' } }, + [userId]: { ['*']: { body: message, msgtype: 'm.text' } }, }), ); }); @@ -589,16 +497,13 @@ describe('matrixMessageSendEpic', () => { const partnerMatrix = (await partner.deps.matrix$.toPromise()) as jest.Mocked; const message = await signMessage(raiden.deps.signer, processed); - raiden.store.dispatch(matrixPresence.request(undefined, { address: partner.address })); - partner.store.dispatch(matrixPresence.request(undefined, { address: raiden.address })); - // fail once, succeed on retry matrix.sendToDevice.mockRejectedValueOnce( Object.assign(new Error('Failed'), { httpStatus: 500 }), ); raiden.store.dispatch( messageSend.request( - { message }, + { message, userId: partnerMatrix.getUserId()! }, { address: partner.address, msgId: message.message_identifier.toString() }, ), ); @@ -619,7 +524,7 @@ describe('matrixMessageSendEpic', () => { }); }); - test('success: batch multiple recipients', async () => { + test('success: batch multiple recipients, request presence', async () => { expect.assertions(4); const [raiden, p1, p2] = await makeRaidens(3); @@ -679,8 +584,6 @@ describe('matrixMessageSendEpic', () => { describe('matrixMessageReceivedEpic', () => { test('receive: success', async () => { expect.assertions(1); - // Gets a log.warn(`Could not decode message: ${line}: ${err}`); - // at Object.parseMessage (src/transport/epics/helpers.ts:203:9) const message = 'test message'; const [raiden, partner] = getSortedClients(await makeRaidens(2)); const partnerMatrix = (await partner.deps.matrix$.toPromise()) as jest.Mocked; @@ -898,51 +801,81 @@ describe('deliveredEpic', () => { }); }); -test('matrixMessageGlobalSendEpic', async () => { +test('matrixMessageServiceSendEpic', async () => { expect.assertions(6); const raiden = await makeRaiden(); + const additionalServices = raiden.config.additionalServices; + // disable additionalServices for now + raiden.store.dispatch(raidenConfigUpdate({ additionalServices: [] })); + + const pfsAddress = makeAddress(); + const pfsInfoResponse = { + message: 'pfs message', + network_info: { + chain_id: raiden.deps.network.chainId, + token_network_registry_address: raiden.deps.contractsInfo.TokenNetworkRegistry.address, + }, + operator: 'pfs operator', + payment_address: pfsAddress, + price_info: 2, + version: '0.4.1', + }; + fetch.mockResolvedValueOnce({ + status: 200, + ok: true, + json: jest.fn(async () => pfsInfoResponse), + text: jest.fn(async () => jsonStringify(pfsInfoResponse)), + }); + const matrix = (await raiden.deps.matrix$.toPromise()) as jest.Mocked; + const service = makeAddress(); + const serviceUid = `@${service.toLowerCase()}:${getServerName(matrix.getHomeserverUrl())}`; const msgId = '123'; const message = await signMessage(raiden.deps.signer, processed), text = encodeJsonMessage(message); - raiden.store.dispatch(messageServiceSend.request({ message }, { service: Service.PFS, msgId })); + const meta = { service: Service.PFS, msgId }; + raiden.store.dispatch(messageServiceSend.request({ message }, meta)); await sleep(2 * raiden.config.pollingInterval); - expect(matrix.sendEvent).toHaveBeenCalledTimes(1); - expect(matrix.sendEvent).toHaveBeenCalledWith( - expect.any(String), - 'm.room.message', - expect.objectContaining({ body: text, msgtype: 'm.text' }), - expect.anything(), - ); + expect(matrix.sendToDevice).not.toHaveBeenCalled(); + expect(raiden.output).not.toContainEqual(messageServiceSend.success(expect.anything(), meta)); expect(raiden.output).toContainEqual( - messageServiceSend.success( - { via: expect.stringMatching(/!.*:/), tookMs: expect.any(Number), retries: 0 }, - { service: Service.PFS, msgId }, + messageServiceSend.failure( + expect.objectContaining({ message: expect.stringContaining('no services') }), + meta, ), ); - // test graceful failure raiden.output.splice(0, raiden.output.length); - matrix.sendEvent.mockClear(); - matrix.sendEvent.mockRejectedValueOnce(Object.assign(new Error('Failed'), { httpStatus: 429 })); + matrix.sendToDevice.mockClear(); + // re-enable additionalServices + raiden.store.dispatch(raidenConfigUpdate({ additionalServices })); + // network errors must be retried + matrix.sendToDevice.mockRejectedValueOnce( + Object.assign(new Error('Failed'), { httpStatus: 429 }), + ); + raiden.store.dispatch(servicesValid({ [service]: Date.now() + 1e8 })); - raiden.store.dispatch(messageServiceSend.request({ message }, { service: Service.PFS, msgId })); + raiden.store.dispatch(messageServiceSend.request({ message }, meta)); await sleep(raiden.config.httpTimeout); - expect(matrix.sendEvent).toHaveBeenCalledTimes(2); - expect(matrix.sendEvent).toHaveBeenCalledWith( - expect.any(String), + expect(matrix.sendToDevice).toHaveBeenCalledTimes(2); + expect(matrix.sendToDevice).toHaveBeenCalledWith( 'm.room.message', - expect.objectContaining({ body: text, msgtype: 'm.text' }), - expect.anything(), + expect.objectContaining({ + [serviceUid]: { [ServiceDeviceId[meta.service]]: { body: text, msgtype: 'm.text' } }, + }), ); expect(raiden.output).toContainEqual( messageServiceSend.success( - { via: expect.stringMatching(/!.*:/), tookMs: expect.any(Number), retries: 1 }, - { service: Service.PFS, msgId }, + { + via: expect.arrayContaining([serviceUid]), + tookMs: expect.any(Number), + retries: 1, + }, + meta, ), ); }); diff --git a/raiden-ts/tests/unit/messages/LockedTransfer.json b/raiden-ts/tests/unit/messages/LockedTransfer.json index 18c7c8ae59..c70765afb7 100644 --- a/raiden-ts/tests/unit/messages/LockedTransfer.json +++ b/raiden-ts/tests/unit/messages/LockedTransfer.json @@ -23,7 +23,7 @@ "nonce": "1", "payment_identifier": "1", "recipient": "0x7461726765747461726765747461726765747461", - "signature": "0x4136eebf7095249ac02c3e6cb3d55eaf0e6ab41a5f8b114b5fb173697be9d6056e18d291798760ed3d72a48c7fdd94fb5fd0400e9bc4e5bfa0cab6513c27f40f1c", + "signature": "0xa8c6aa0cbf6c6b8f8dab91d63c3860701693e1b4309550bace19a1ed6204622c5d84dddbe9aeeaaebd3acaf8ee9df765549f1ec109fdf78fa111dc1355ad79981b", "target": "0x7461726765747461726765747461726765747461", "token": "0x746F6b656E746F6b656E746f6b656e746f6B656e", "token_network_address": "0x6E6574776F726b6E6574776F726b6e6574776f72", diff --git a/raiden-ts/tests/unit/raiden.spec.ts b/raiden-ts/tests/unit/raiden.spec.ts index b7d10fd659..e5b439a6ee 100644 --- a/raiden-ts/tests/unit/raiden.spec.ts +++ b/raiden-ts/tests/unit/raiden.spec.ts @@ -1275,8 +1275,8 @@ describe('Raiden', () => { tokenNetwork: tokenNetwork, target: partner, value: One as UInt<32>, - paths: expect.anything(), paymentId: lockedTransferMessage.payment_identifier, + metadata: expect.anything(), }), { secrethash: lockedTransferMessage.lock.secrethash, diff --git a/yarn.lock b/yarn.lock index 4ced3461fb..1c93ef801b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12421,6 +12421,11 @@ json-buffer@3.0.0: resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= +json-canonicalize@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/json-canonicalize/-/json-canonicalize-1.0.4.tgz#efb2d0b07df12365e39028aa70f879237ec102ea" + integrity sha512-YNr/ePzgReHwlnAm3EVV1pcimwesI+1DZr5v7WBKOc1zE1t7pjxWAPRxJFT3ll6flLIdRe0DPia/8cl2FLAZNA== + json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"