From d273ba9c314a7fa4167713b38a56440bbd363e1d Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Wed, 29 Jan 2020 12:23:39 +0200 Subject: [PATCH] # Interledger BTP: Futures 0.3 Transition (#599) * feat(btp): update traits to be async * refactor(btp/wrapped-ws): refactor WsWrap to a separate file Ideally, we would want to get rid of it by doing a `StreamExt::map_ok` and `SinkExt::with` to map both WebSocket return types to the same value. We also use `filter_map` to get rid of any errors from the WebSocket. The WsError type has been removed as a result of that. * feat(btp/client): port to async/await * feat(btp/server): move to async/await * feat(btp/service): move service to async/await * We refactored the service to be more readable. Basically, we split the websocket in a Sink (write) and a Stream (read). We also create a `tx`/`rx` pair per account. The rx receiver gets attached to the sink, meaning any data sent over by the `tx` sender will get forwarded to the sink, which will forward it to the other end of the websocket. Unfortunately, due to being unable to combine the read and write sockets, we have to spawn them separately. This means that we have to remove the hook which cancels the streams. # Interledger HTTP: Futures 0.3 Transition (#600) * feat(http): Update HttpStore trait to futures 0.3 and deserialize_json method * feat(http): Update HTTP Errors and client * feat(http): Update HTTP Server * docs(http): extend http docs # Interledger Stream: Futures 0.3 Transition (#601) * feat(stream): Update Stream server * feat(stream): Update Stream client * docs(stream): extend stream docs * fix(stream): add extra limits to ensure all the pending request futures are thread safe # Interledger Settlement: Futures 0.3 Transition (#602) * feat(settlement/core): Upgrade types and idempotency * feat(settlement/core): Upgrade engines API Warp interface * feat(settlement/core): Upgrade Redis backend implementation * feat(settlement/api): Upgrade the message service * feat(settlement/api): Upgrade the settlement client * feat(settlement/api): Upgrade the Settlement API exposed by the node * chore(settlement): remove need to pass future wrapped in closure * docs(settlement): extend settlement docs # Interledger SPSP: Futures 0.3 Transition (#603) * feat(spsp): move to futures 0.3 and async/await * docs(spsp): extend spsp docs * fix(spsp): tighten trait bounds to account for stream changes # Interledger Service Util: Futures 0.3 Transition (#604) * feat(service-util): update validator service * feat(service-util): update rate limit service * feat(service-util): update max packet amount service * feat(service-util): update expiry shortener service * feat(service-util): update exchange rate service and providers * feat(service-util): update echo service * feat(service-util): update balance service # Interledger API: Futures 0.3 Transition (#605) * feat(api): update trait definitions and dependencies * feat(api): update http retry client * test(api): migrate test helpers * feat(api): update node-settings route * test(api): update node-settings route tests * feat(api): update accounts route * test(api): update accounts route tests * chore(api): add missing doc # Interledger Store: Futures 0.3 Transition (#606) * feat(store): Update redis reconnect * feat(store): Update base redis struct * feat(store): Update AccountStore trait * feat(store): Update StreamNotificationsStore trait * feat(store): Update BalanceStore trait * feat(store): Update BtpStore trait * feat(store): Update HttpStore trait * feat(store): Update NodeStore trait * feat(store): Update AddressStore trait * feat(store): Update RouteManagerStore trait * feat(store): Update RateLimitStore trait * feat(store): Update IdempotentStore trait * feat(store): Update SettlementStore trait * feat(store): Update LeftoversStore trait * feat(store): Update update_routes * test(store): convert all tests to tokio::test with async/await * feat(store): update secrecy/bytes/zeroize * docs(store): add more docs # ILP CLI: Futures 0.3 Transition (#607) * feat(ilp-cli): update CLI to async/await # ILP Node: Futures 0.3 Transition (#608) (#609) * test(ilp-node): migrate tests to futures 0.3 * feat(ilp-node): move metrics related files to feature-gated module * feat(ilp-node): remove deprecated insert account function * feat(ilp-node): make the node run on async/await * ci(ilp-node): disable some advisories and update README * fix(ilp-node): spawn prometheus filter # Service * feat(service): Box wrapper methods to avoid exponential type blowup --- .circleci/config.yml | 59 +- Cargo.lock | 1973 +++++++++++++- Cargo.toml | 22 +- README.md | 16 +- crates/ilp-cli/Cargo.toml | 6 +- crates/ilp-cli/src/interpreter.rs | 5 +- crates/ilp-cli/src/main.rs | 36 +- crates/ilp-node/Cargo.toml | 41 +- crates/ilp-node/src/google_pubsub.rs | 186 -- .../src/instrumentation/google_pubsub.rs | 187 ++ .../ilp-node/src/instrumentation/metrics.rs | 82 + crates/ilp-node/src/instrumentation/mod.rs | 10 + .../src/instrumentation/prometheus.rs | 90 + .../src/{ => instrumentation}/trace.rs | 47 +- crates/ilp-node/src/lib.rs | 11 +- crates/ilp-node/src/main.rs | 24 +- crates/ilp-node/src/metrics.rs | 82 - crates/ilp-node/src/node.rs | 563 ++-- crates/ilp-node/src/redis_store.rs | 46 +- crates/ilp-node/tests/redis/btp.rs | 196 +- crates/ilp-node/tests/redis/exchange_rates.rs | 139 +- crates/ilp-node/tests/redis/prometheus.rs | 182 +- crates/ilp-node/tests/redis/redis_helpers.rs | 24 +- crates/ilp-node/tests/redis/redis_tests.rs | 6 +- crates/ilp-node/tests/redis/test_helpers.rs | 125 +- crates/ilp-node/tests/redis/three_nodes.rs | 361 +-- crates/interledger-api/Cargo.toml | 20 +- crates/interledger-api/src/http_retry.rs | 63 +- crates/interledger-api/src/lib.rs | 137 +- crates/interledger-api/src/routes/accounts.rs | 882 +++--- .../src/routes/node_settings.rs | 353 +-- .../src/routes/test_helpers.rs | 137 +- crates/interledger-btp/Cargo.toml | 27 +- crates/interledger-btp/src/client.rs | 128 +- crates/interledger-btp/src/lib.rs | 273 +- crates/interledger-btp/src/server.rs | 283 +- crates/interledger-btp/src/service.rs | 525 ++-- crates/interledger-btp/src/wrapped_ws.rs | 92 + crates/interledger-http/Cargo.toml | 14 +- crates/interledger-http/src/client.rs | 147 +- .../interledger-http/src/error/error_types.rs | 36 +- crates/interledger-http/src/error/mod.rs | 59 +- crates/interledger-http/src/lib.rs | 111 +- crates/interledger-http/src/server.rs | 200 +- crates/interledger-service-util/Cargo.toml | 15 +- .../src/balance_service.rs | 196 +- .../src/echo_service.rs | 88 +- .../src/exchange_rate_providers/coincap.rs | 78 +- .../exchange_rate_providers/cryptocompare.rs | 74 +- .../src/exchange_rate_providers/mod.rs | 2 + .../src/exchange_rates_service.rs | 166 +- .../src/expiry_shortener_service.rs | 32 +- crates/interledger-service-util/src/lib.rs | 10 + .../src/max_packet_amount_service.rs | 22 +- .../src/rate_limit_service.rs | 91 +- .../src/validator_service.rs | 176 +- crates/interledger-service/src/lib.rs | 238 +- crates/interledger-settlement/Cargo.toml | 17 +- .../interledger-settlement/src/api/client.rs | 78 +- .../src/api/message_service.rs | 174 +- crates/interledger-settlement/src/api/mod.rs | 5 +- .../src/api/node_api.rs | 641 ++--- .../src/api/test_helpers.rs | 183 +- .../src/core/backends_common/redis/mod.rs | 339 ++- .../backends_common/redis/test_helpers/mod.rs | 2 +- .../redis/test_helpers/redis_helpers.rs | 11 +- .../redis/test_helpers/store_helpers.rs | 23 +- .../src/core/engines_api.rs | 379 ++- .../src/core/idempotency.rs | 218 +- crates/interledger-settlement/src/core/mod.rs | 31 +- .../interledger-settlement/src/core/types.rs | 149 +- crates/interledger-settlement/src/lib.rs | 4 +- crates/interledger-spsp/Cargo.toml | 12 +- crates/interledger-spsp/src/client.rs | 72 +- crates/interledger-spsp/src/lib.rs | 7 + crates/interledger-spsp/src/server.rs | 41 +- crates/interledger-store/Cargo.toml | 17 +- crates/interledger-store/src/account.rs | 73 +- crates/interledger-store/src/crypto.rs | 12 +- crates/interledger-store/src/lib.rs | 3 + crates/interledger-store/src/redis/mod.rs | 2369 ++++++++--------- .../interledger-store/src/redis/reconnect.rs | 120 +- .../tests/redis/accounts_test.rs | 662 ++--- .../tests/redis/balances_test.rs | 441 ++- .../interledger-store/tests/redis/btp_test.rs | 118 +- .../tests/redis/http_test.rs | 127 +- .../tests/redis/rate_limiting_test.rs | 154 +- .../tests/redis/rates_test.rs | 41 +- .../tests/redis/redis_tests.rs | 82 +- .../tests/redis/routing_test.rs | 582 ++-- .../tests/redis/settlement_test.rs | 595 ++--- crates/interledger-stream/Cargo.toml | 6 +- crates/interledger-stream/src/client.rs | 295 +- crates/interledger-stream/src/congestion.rs | 15 + crates/interledger-stream/src/crypto.rs | 36 + crates/interledger-stream/src/error.rs | 1 + crates/interledger-stream/src/lib.rs | 51 +- crates/interledger-stream/src/packet.rs | 145 +- crates/interledger-stream/src/server.rs | 63 +- 99 files changed, 9664 insertions(+), 7924 deletions(-) delete mode 100644 crates/ilp-node/src/google_pubsub.rs create mode 100644 crates/ilp-node/src/instrumentation/google_pubsub.rs create mode 100644 crates/ilp-node/src/instrumentation/metrics.rs create mode 100644 crates/ilp-node/src/instrumentation/mod.rs create mode 100644 crates/ilp-node/src/instrumentation/prometheus.rs rename crates/ilp-node/src/{ => instrumentation}/trace.rs (82%) delete mode 100644 crates/ilp-node/src/metrics.rs create mode 100644 crates/interledger-btp/src/wrapped_ws.rs diff --git a/.circleci/config.yml b/.circleci/config.yml index 9a591e954..419583c3c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -44,7 +44,11 @@ jobs: cargo clippy --all-targets --all-features -- -D warnings - run: name: Audit Dependencies - command: cargo audit + # Disable: + # 1. lazy_static advisory: https://github.com/interledger-rs/interledger-rs/issues/588 + # 2. http/hyper advisory: outdated http due to yup-oauth2 3.1.1, tungstenite 0.9.2 + command: cargo audit --ignore RUSTSEC-2019-0033 --ignore RUSTSEC-2019-0034 --ignore RUSTSEC-2019-0031 + test-md: docker: - image: circleci/rust @@ -57,37 +61,34 @@ jobs: steps: - checkout - run: - name: Disabled - command: echo "temporarily disabled" - # - run: - # name: Install Dependencies - # command: | - # # install system dependeicies - # sudo apt-get update - # sudo apt-get install -y redis-server redis-tools lsof libssl-dev + name: Install Dependencies + command: | + # install system dependeicies + sudo apt-get update + sudo apt-get install -y redis-server redis-tools lsof libssl-dev - # # install nvm - # curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash - # export NVM_DIR="/home/circleci/.nvm" - # source $NVM_DIR/nvm.sh - # nvm install "v11.15.0" + # install nvm + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash + export NVM_DIR="/home/circleci/.nvm" + source $NVM_DIR/nvm.sh + nvm install "v11.15.0" - # # install yarn & components (ganache-cli ilp-settlement-xrp conventional-changelog-cli) - # curl -o- -L https://yarnpkg.com/install.sh | bash - # export PATH="/home/circleci/.yarn/bin:/home/circleci/.config/yarn/global/node_modules/.bin:$PATH" - # yarn global add ganache-cli ilp-settlement-xrp conventional-changelog-cli + # install yarn & components (ganache-cli ilp-settlement-xrp conventional-changelog-cli) + curl -o- -L https://yarnpkg.com/install.sh | bash + export PATH="/home/circleci/.yarn/bin:/home/circleci/.config/yarn/global/node_modules/.bin:$PATH" + yarn global add ganache-cli ilp-settlement-xrp conventional-changelog-cli - # # env - # echo 'export NVM_DIR="/home/circleci/.nvm"' >> ${BASH_ENV} - # echo 'source $NVM_DIR/nvm.sh' >> ${BASH_ENV} - # echo "export PATH=/home/circleci/.cargo/bin:$PATH" >> ${BASH_ENV} - # - run: - # name: Run run-md Test - # command: | - # scripts/run-md-test.sh '^.*$' 1 - # - store_artifacts: - # path: /tmp/run-md-test - # destination: run-md-test + # env + echo 'export NVM_DIR="/home/circleci/.nvm"' >> ${BASH_ENV} + echo 'source $NVM_DIR/nvm.sh' >> ${BASH_ENV} + echo "export PATH=/home/circleci/.cargo/bin:$PATH" >> ${BASH_ENV} + - run: + name: Run run-md Test + command: | + scripts/run-md-test.sh '^.*$' 1 + - store_artifacts: + path: /tmp/run-md-test + destination: run-md-test update-docker-images: docker: - image: circleci/rust diff --git a/Cargo.lock b/Cargo.lock index 64704512a..ad16b4195 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,10 +1,39 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "aho-corasick" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "anyhow" version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "approx" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "arc-swap" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "arrayvec" version = "0.4.12" @@ -13,14 +42,28 @@ dependencies = [ "nodrop 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "ascii" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "assert-json-diff" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "async-trait" version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -37,6 +80,11 @@ name = "autocfg" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "autocfg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "base64" version = "0.10.1" @@ -45,11 +93,35 @@ dependencies = [ "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "base64" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "bitflags" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "block-buffer" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "block-padding 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "block-padding" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "bstr" version = "0.2.8" @@ -66,6 +138,11 @@ name = "bumpalo" version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "byteorder" version = "1.3.2" @@ -77,6 +154,7 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)", "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -118,8 +196,9 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", - "num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -140,6 +219,45 @@ dependencies = [ "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "combine" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ascii 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "config" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", + "toml 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", + "yaml-rust 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "core-foundation" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "core-foundation-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "core-foundation-sys" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "criterion" version = "0.3.0" @@ -152,7 +270,7 @@ dependencies = [ "csv 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "itertools 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "rand_os 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "rand_xoshiro 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -195,6 +313,19 @@ dependencies = [ "scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "crossbeam-epoch" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "memoffset 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "crossbeam-queue" version = "0.1.2" @@ -212,6 +343,16 @@ dependencies = [ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "crossbeam-utils" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "csv" version = "1.1.1" @@ -232,16 +373,100 @@ dependencies = [ "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "ct-logs" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "sct 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "difference" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "dtoa" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "either" version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "encoding_rs" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "failure" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "failure_derive 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "failure_derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", + "synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "fnv" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "fuchsia-zircon" version = "0.3.3" @@ -289,6 +514,15 @@ name = "futures-core" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "futures-cpupool" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", + "num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "futures-executor" version = "0.3.1" @@ -310,9 +544,19 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "proc-macro-hack 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "futures-retry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "pin-project 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -330,6 +574,7 @@ name = "futures-util" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ + "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", "futures-channel 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "futures-core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "futures-io 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -343,6 +588,14 @@ dependencies = [ "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "generic-array" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "typenum 1.11.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "getrandom" version = "0.1.13" @@ -354,74 +607,474 @@ dependencies = [ ] [[package]] -name = "heck" -version = "0.3.1" +name = "h2" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", + "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "indexmap 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "string 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "hex" -version = "0.4.0" +name = "h2" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "interledger-ccp" -version = "0.3.0" dependencies = [ - "async-trait 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", - "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", - "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "hex 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "interledger-packet 0.4.0", - "interledger-service 0.4.0", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-sink 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-util 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "indexmap 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", - "ring 0.16.9 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", + "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "tokio 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", - "uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-util 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "interledger-ildcp" -version = "0.4.0" +name = "hdrhistogram" +version = "6.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "async-trait 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", - "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "interledger-packet 0.4.0", - "interledger-service 0.4.0", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", - "uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "interledger-packet" -version = "0.4.0" +name = "headers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", - "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", + "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "bytes 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", - "chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)", - "criterion 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "hex 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_test 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", + "headers-core 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", + "sha-1 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "interledger-router" -version = "0.4.0" +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "heck" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "hex" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "http" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", + "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "http" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "http-body" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-buf 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "http-body" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "httparse" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "hyper" +version = "0.12.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-cpupool 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "h2 0.1.26 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "http-body 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-buf 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-executor 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-reactor 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-tcp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-threadpool 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-timer 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", + "want 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "hyper" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-channel 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-util 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "h2 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "http-body 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", + "pin-project 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "tower-service 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "want 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "hyper-rustls" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", + "ct-logs 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper 0.12.35 (registry+https://github.com/rust-lang/crates.io-index)", + "rustls 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-rustls 0.10.3 (registry+https://github.com/rust-lang/crates.io-index)", + "webpki 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)", + "webpki-roots 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "hyper-tls" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)", + "native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-tls 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "idna" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-normalization 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "idna" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-normalization 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ilp-cli" +version = "0.3.0" +dependencies = [ + "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "reqwest 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", + "thiserror 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", + "tungstenite 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", + "url 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ilp-node" +version = "0.6.0" +dependencies = [ + "approx 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)", + "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", + "config 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "hex 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "interledger 0.6.0", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", + "metrics 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", + "metrics-core 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "metrics-runtime 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", + "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", + "num-bigint 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", + "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", + "redis 0.15.1 (registry+https://github.com/rust-lang/crates.io-index)", + "reqwest 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", + "ring 0.16.9 (registry+https://github.com/rust-lang/crates.io-index)", + "secrecy 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-retry 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing-futures 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing-subscriber 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "url 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "warp 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "yup-oauth2 3.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "im" +version = "12.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "sized-chunks 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "typenum 1.11.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "indexmap" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "input_buffer" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "interledger" +version = "0.6.0" +dependencies = [ + "interledger-api 0.3.0", + "interledger-btp 0.4.0", + "interledger-ccp 0.3.0", + "interledger-http 0.4.0", + "interledger-ildcp 0.4.0", + "interledger-packet 0.4.0", + "interledger-router 0.4.0", + "interledger-service 0.4.0", + "interledger-service-util 0.4.0", + "interledger-settlement 0.3.0", + "interledger-spsp 0.4.0", + "interledger-store 0.4.0", + "interledger-stream 0.4.0", +] + +[[package]] +name = "interledger-api" +version = "0.3.0" +dependencies = [ + "async-trait 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-retry 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "interledger-btp 0.4.0", + "interledger-ccp 0.3.0", + "interledger-http 0.4.0", + "interledger-ildcp 0.4.0", + "interledger-packet 0.4.0", + "interledger-router 0.4.0", + "interledger-service 0.4.0", + "interledger-service-util 0.4.0", + "interledger-settlement 0.3.0", + "interledger-spsp 0.4.0", + "interledger-stream 0.4.0", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "reqwest 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", + "secrecy 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_path_to_error 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "url 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "warp 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "interledger-btp" +version = "0.4.0" +dependencies = [ + "async-trait 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "hex 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "interledger-packet 0.4.0", + "interledger-service 0.4.0", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", + "num-bigint 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", + "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "pin-project 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", + "quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", + "secrecy 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "stream-cancel 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-tungstenite 0.10.0 (git+https://github.com/snapview/tokio-tungstenite)", + "tungstenite 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", + "url 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "warp 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "interledger-ccp" +version = "0.3.0" +dependencies = [ + "async-trait 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "hex 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "interledger-packet 0.4.0", + "interledger-service 0.4.0", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "ring 0.16.9 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "interledger-http" +version = "0.4.0" +dependencies = [ + "async-trait 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "interledger-packet 0.4.0", + "interledger-service 0.4.0", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "reqwest 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", + "secrecy 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_path_to_error 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "url 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "warp 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "interledger-ildcp" +version = "0.4.0" +dependencies = [ + "async-trait 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "interledger-packet 0.4.0", + "interledger-service 0.4.0", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "interledger-packet" +version = "0.4.0" +dependencies = [ + "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)", + "criterion 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "hex 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_test 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "interledger-router" +version = "0.4.0" dependencies = [ "async-trait 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", "interledger-packet 0.4.0", @@ -451,6 +1104,147 @@ dependencies = [ "uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "interledger-service-util" +version = "0.4.0" +dependencies = [ + "async-trait 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "hex 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "interledger-packet 0.4.0", + "interledger-service 0.4.0", + "interledger-settlement 0.3.0", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "reqwest 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", + "ring 0.16.9 (registry+https://github.com/rust-lang/crates.io-index)", + "secrecy 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "interledger-settlement" +version = "0.3.0" +dependencies = [ + "async-trait 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)", + "interledger-http 0.4.0", + "interledger-packet 0.4.0", + "interledger-service 0.4.0", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "mockito 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)", + "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", + "num-bigint 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", + "redis 0.15.1 (registry+https://github.com/rust-lang/crates.io-index)", + "reqwest 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", + "ring 0.16.9 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-retry 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "url 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "warp 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "interledger-spsp" +version = "0.4.0" +dependencies = [ + "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)", + "interledger-packet 0.4.0", + "interledger-service 0.4.0", + "interledger-stream 0.4.0", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "reqwest 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "interledger-store" +version = "0.4.0" +dependencies = [ + "async-trait 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "interledger-api 0.3.0", + "interledger-btp 0.4.0", + "interledger-ccp 0.3.0", + "interledger-http 0.4.0", + "interledger-packet 0.4.0", + "interledger-router 0.4.0", + "interledger-service 0.4.0", + "interledger-service-util 0.4.0", + "interledger-settlement 0.3.0", + "interledger-stream 0.4.0", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", + "num-bigint 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", + "os_type 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", + "redis 0.15.1 (registry+https://github.com/rust-lang/crates.io-index)", + "ring 0.16.9 (registry+https://github.com/rust-lang/crates.io-index)", + "secrecy 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "url 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "zeroize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "interledger-stream" +version = "0.4.0" +dependencies = [ + "async-trait 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", + "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)", + "csv 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "hex 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "interledger-ildcp 0.4.0", + "interledger-packet 0.4.0", + "interledger-router 0.4.0", + "interledger-service 0.4.0", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "pin-project 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", + "ring 0.16.9 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "iovec" version = "0.1.4" @@ -499,6 +1293,11 @@ name = "libc" version = "0.2.65" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "linked-hash-map" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "lock_api" version = "0.3.3" @@ -507,6 +1306,14 @@ dependencies = [ "scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "log" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "log" version = "0.4.8" @@ -515,6 +1322,19 @@ dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "matchers" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "regex-automata 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "maybe-uninit" version = "2.0.0" @@ -536,6 +1356,69 @@ dependencies = [ "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "metrics" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "metrics-core 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "metrics-core" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "metrics-observer-prometheus" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "hdrhistogram 6.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "metrics-core 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "metrics-util 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "metrics-runtime" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "arc-swap 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)", + "crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", + "im 12.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "metrics 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", + "metrics-core 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "metrics-observer-prometheus 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "metrics-util 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "quanta 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "metrics-util" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "crossbeam-epoch 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "mime_guess" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", + "unicase 2.6.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "mio" version = "0.6.21" @@ -576,52 +1459,146 @@ dependencies = [ ] [[package]] -name = "net2" -version = "0.2.33" +name = "mockito" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "assert-json-diff 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "native-tls" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl 0.10.26 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-sys 0.9.53 (registry+https://github.com/rust-lang/crates.io-index)", + "schannel 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", + "security-framework 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "security-framework-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "net2" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "nom" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-bigint" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-integer" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-traits" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num_cpus" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "nodrop" -version = "0.1.14" +name = "opaque-debug" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] -name = "nom" -version = "4.2.3" +name = "openssl" +version = "0.10.26" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-sys 0.9.53 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "num-integer" -version = "0.1.41" +name = "openssl-probe" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "openssl-sys" +version = "0.9.53" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", + "vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "num-traits" -version = "0.2.8" +name = "os_type" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "num_cpus" -version = "1.10.1" +name = "owning_ref" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", + "stable_deref_trait 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -648,6 +1625,16 @@ dependencies = [ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "percent-encoding" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "pin-project" version = "0.4.7" @@ -661,9 +1648,9 @@ name = "pin-project-internal" version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -676,6 +1663,11 @@ name = "pin-utils" version = "0.1.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "pkg-config" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "ppv-lite86" version = "0.2.6" @@ -686,9 +1678,9 @@ name = "proc-macro-hack" version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -698,12 +1690,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "proc-macro2" -version = "1.0.6" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "quanta" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "quick-error" version = "1.2.2" @@ -714,7 +1715,19 @@ name = "quote" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -738,6 +1751,19 @@ dependencies = [ "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "rand_core" version = "0.5.1" @@ -793,6 +1819,33 @@ dependencies = [ "num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "redis" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "combine 3.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dtoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-executor 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-util 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "pin-project-lite 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "sha1 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-util 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "url 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "redox_syscall" version = "0.1.56" @@ -803,7 +1856,10 @@ name = "regex" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ + "aho-corasick 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "regex-syntax 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", + "thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -812,6 +1868,8 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", + "utf8-ranges 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -819,12 +1877,56 @@ name = "regex-syntax" version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "remove_dir_all" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "reqwest" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "encoding_rs 0.8.22 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-util 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "http-body 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper-tls 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "js-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", + "mime_guess 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "pin-project-lite 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_urlencoded 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-tls 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "url 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-futures 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "web-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)", + "winreg 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "ring" version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", "spin 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", "untrusted 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -840,6 +1942,18 @@ dependencies = [ "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "rustls" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "ring 0.16.9 (registry+https://github.com/rust-lang/crates.io-index)", + "sct 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "webpki 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "ryu" version = "1.0.2" @@ -853,11 +1967,72 @@ dependencies = [ "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "schannel" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "scoped-tls" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "scopeguard" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "sct" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ring 0.16.9 (registry+https://github.com/rust-lang/crates.io-index)", + "untrusted 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "secrecy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", + "zeroize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "secrecy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", + "zeroize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "security-framework" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "core-foundation 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)", + "core-foundation-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", + "security-framework-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "security-framework-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "core-foundation-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "semver" version = "0.9.0" @@ -884,9 +2059,9 @@ name = "serde_derive" version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -899,6 +2074,14 @@ dependencies = [ "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "serde_test" version = "1.0.102" @@ -907,6 +2090,41 @@ dependencies = [ "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "serde_urlencoded" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "dtoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", + "url 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "sha-1" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "block-buffer 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", + "digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "sha1" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "sized-chunks" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "typenum 1.11.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "slab" version = "0.4.2" @@ -926,31 +2144,115 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] -name = "sourcefile" -version = "0.1.4" +name = "sourcefile" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "stable_deref_trait" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "stream-cancel" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures-core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-util 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "pin-project 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "string" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "syn" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "synstructure" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tempfile" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", + "remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "thiserror" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "thiserror-impl 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", +] [[package]] -name = "spin" -version = "0.5.2" +name = "thiserror-impl" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", +] [[package]] -name = "syn" -version = "1.0.7" +name = "thread_local" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "textwrap" -version = "0.11.0" +name = "time" +version = "0.1.42" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -991,11 +2293,30 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bytes 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "mio 0.6.21 (registry+https://github.com/rust-lang/crates.io-index)", + "mio-uds 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)", + "num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "pin-project-lite 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-macros 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "tokio-buf" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", + "either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "tokio-codec" version = "0.1.1" @@ -1050,7 +2371,7 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1071,6 +2392,29 @@ dependencies = [ "tokio-sync 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "tokio-retry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-timer 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-rustls" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", + "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "rustls 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", + "webpki 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "tokio-sync" version = "0.1.7" @@ -1120,6 +2464,29 @@ dependencies = [ "tokio-executor 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "tokio-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.10.0" +source = "git+https://github.com/snapview/tokio-tungstenite#308d9680c0e59dd1e8651659a775c05df937934e" +dependencies = [ + "futures 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "pin-project 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-tls 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tungstenite 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "tokio-udp" version = "0.1.5" @@ -1151,12 +2518,39 @@ dependencies = [ "tokio-reactor 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "tokio-util" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-sink 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "pin-project-lite 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "toml" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tower-service" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "tracing" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "tracing-attributes 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "tracing-core 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1167,7 +2561,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1190,6 +2584,61 @@ dependencies = [ "tracing 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "tracing-log" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing-core 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tracing-subscriber" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "matchers 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "owning_ref 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "smallvec 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing-core 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing-log 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "try-lock" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "tungstenite" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "input_buffer 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", + "sha-1 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", + "url 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "utf-8 0.7.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "typenum" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "unicase" version = "2.6.0" @@ -1198,6 +2647,14 @@ dependencies = [ "version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "unicode-bidi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "unicode-normalization" version = "0.1.12" @@ -1221,19 +2678,69 @@ name = "unicode-xid" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "unreachable" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "untrusted" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "url" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "url" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "urlencoding" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "utf-8" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "utf8-ranges" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "uuid" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "vcpkg" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "version_check" version = "0.1.5" @@ -1244,6 +2751,11 @@ name = "version_check" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "walkdir" version = "2.2.9" @@ -1254,6 +2766,49 @@ dependencies = [ "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "want" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "try-lock 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "try-lock 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "warp" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "headers 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", + "mime_guess 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "pin-project 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", + "scoped-tls 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_urlencoded 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "tower-service 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tungstenite 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", + "urlencoding 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "wasi" version = "0.7.0" @@ -1265,6 +2820,8 @@ version = "0.2.58" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", "wasm-bindgen-macro 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1276,12 +2833,23 @@ dependencies = [ "bumpalo 3.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", "wasm-bindgen-shared 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "js-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", + "web-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.58" @@ -1296,9 +2864,9 @@ name = "wasm-bindgen-macro-support" version = "0.2.58" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", "wasm-bindgen-backend 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", "wasm-bindgen-shared 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1316,9 +2884,9 @@ dependencies = [ "anyhow 1.0.26 (registry+https://github.com/rust-lang/crates.io-index)", "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", "wasm-bindgen-backend 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", "weedle 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1335,6 +2903,23 @@ dependencies = [ "wasm-bindgen-webidl 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "webpki" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ring 0.16.9 (registry+https://github.com/rust-lang/crates.io-index)", + "untrusted 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "webpki-roots" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "webpki 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "weedle" version = "0.10.0" @@ -1380,6 +2965,14 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "winreg" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "ws2_32-sys" version = "0.2.1" @@ -1389,16 +2982,63 @@ dependencies = [ "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "yaml-rust" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "yup-oauth2" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper 0.12.35 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper-rustls 0.17.1 (registry+https://github.com/rust-lang/crates.io-index)", + "itertools 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", + "rustls 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-threadpool 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-timer 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "zeroize" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [metadata] +"checksum aho-corasick 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)" = "58fb5e95d83b38284460a5fda7d6470aa0b8844d283a0b614b8535e880800d2d" +"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" "checksum anyhow 1.0.26 (registry+https://github.com/rust-lang/crates.io-index)" = "7825f6833612eb2414095684fcf6c635becf3ce97fe48cf6421321e93bfbd53c" +"checksum approx 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f0e60b75072ecd4168020818c0107f2857bb6c4e64252d8d3983f6263b40a5c3" +"checksum arc-swap 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)" = "bc4662175ead9cd84451d5c35070517777949a2ed84551764129cedb88384841" "checksum arrayvec 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)" = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9" +"checksum ascii 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" +"checksum assert-json-diff 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9881d306dee755eccf052d652b774a6b2861e86b4772f555262130e58e4f81d2" "checksum async-trait 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)" = "c8df72488e87761e772f14ae0c2480396810e51b2c2ade912f97f0f7e5b95e3c" "checksum atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "1803c647a3ec87095e7ae7acfca019e98de5ec9a7d01343f611cf3152ed71a90" "checksum autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" +"checksum autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" "checksum base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" +"checksum base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" "checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +"checksum block-buffer 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +"checksum block-padding 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" "checksum bstr 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8d6c2c5b58ab920a4f5aeaaca34b4488074e8cc7596af94e6f8c6ff247c60245" "checksum bumpalo 3.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5fb8038c1ddc0a5f73787b130f4cc75151e96ed33e417fde765eb5a81e3532f4" +"checksum byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" "checksum byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5" "checksum bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)" = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" "checksum bytes 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "10004c15deb332055f7a4a208190aed362cf9a7c2f6ab70a305fba50e1105f38" @@ -1409,31 +3049,71 @@ dependencies = [ "checksum chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e8493056968583b0193c1bb04d6f7684586f3726992d6c573261941a895dbd68" "checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" "checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +"checksum combine 3.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "da3da6baa321ec19e1cc41d31bf599f00c783d0517095cdaf0332e3fe8d20680" +"checksum config 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "f9107d78ed62b3fa5a86e7d18e647abed48cfd8f8fab6c72f4cdb982d196f7e6" +"checksum core-foundation 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "25b9e03f145fd4f2bf705e07b900cd41fc636598fe5dc452fd0db1441c3f496d" +"checksum core-foundation-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b" "checksum criterion 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "938703e165481c8d612ea3479ac8342e5615185db37765162e762ec3523e2fc6" "checksum criterion-plot 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "eccdc6ce8bbe352ca89025bee672aa6d24f4eb8c53e3a8b5d1bc58011da072a2" "checksum crossbeam-deque 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b18cd2e169ad86297e6bc0ad9aa679aee9daa4f19e8163860faf7c164e4f5a71" "checksum crossbeam-epoch 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "fedcd6772e37f3da2a9af9bf12ebe046c0dfe657992377b4df982a2b54cd37a9" +"checksum crossbeam-epoch 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5064ebdbf05ce3cb95e45c8b086f72263f4166b29b97f6baff7ef7fe047b55ac" "checksum crossbeam-queue 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7c979cd6cfe72335896575c6b5688da489e420d36a27a0b9eb0c73db574b4a4b" "checksum crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)" = "04973fa96e96579258a5091af6003abde64af786b860f18622b82e026cca60e6" +"checksum crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ce446db02cdc3165b94ae73111e570793400d0794e46125cc4056c81cbb039f4" "checksum csv 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "37519ccdfd73a75821cac9319d4fce15a81b9fcf75f951df5b9988aa3a0af87d" "checksum csv-core 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "9b5cadb6b25c77aeff80ba701712494213f4a8418fcda2ee11b6560c3ad0bf4c" +"checksum ct-logs 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4d3686f5fa27dbc1d76c751300376e167c5a43387f44bb451fd1c24776e49113" +"checksum difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" +"checksum digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +"checksum dtoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "ea57b42383d091c85abcc2706240b94ab2a8fa1fc81c10ff23c4de06e2a90b5e" "checksum either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" +"checksum encoding_rs 0.8.22 (registry+https://github.com/rust-lang/crates.io-index)" = "cd8d03faa7fe0c1431609dfad7bbe827af30f82e1e2ae6f7ee4fca6bd764bc28" +"checksum env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +"checksum failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "f8273f13c977665c5db7eb2b99ae520952fe5ac831ae4cd09d80c4c7042b5ed9" +"checksum failure_derive 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0bc225b78e0391e4b8683440bf2e63c2deeeb2ce5189eab46e2b68c6d3725d08" +"checksum fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" "checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" +"checksum foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +"checksum foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +"checksum fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" "checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" "checksum futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)" = "1b980f2816d6ee8673b6517b52cb0e808a180efc92e5c19d02cdda79066703ef" "checksum futures 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b6f16056ecbb57525ff698bb955162d0cd03bee84e6241c27ff75c08d8ca5987" "checksum futures-channel 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "fcae98ca17d102fd8a3603727b9259fcf7fa4239b603d2142926189bc8999b86" "checksum futures-core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "79564c427afefab1dfb3298535b21eda083ef7935b4f0ecbfcb121f0aec10866" +"checksum futures-cpupool 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "ab90cde24b3319636588d0c35fe03b1333857621051837ed769faefb4c2162e4" "checksum futures-executor 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1e274736563f686a837a0568b478bdabfeaec2dca794b5649b04e2fe1627c231" "checksum futures-io 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e676577d229e70952ab25f3945795ba5b16d63ca794ca9d2c860e5595d20b5ff" "checksum futures-macro 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "52e7c56c15537adb4f76d0b7a76ad131cb4d2f4f32d3b0bcabcbe1c7c5e87764" +"checksum futures-retry 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cc9a95ec273db7b9d07559e25f9cd75074fee2f437f1e502b0c3b610d129d554" "checksum futures-sink 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "171be33efae63c2d59e6dbba34186fe0d6394fb378069a76dfd80fdcffd43c16" "checksum futures-task 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0bae52d6b29cf440e298856fec3965ee6fa71b06aa7495178615953fd669e5f9" "checksum futures-util 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c0d66274fb76985d3c62c886d1da7ac4c0903a8c9f754e8fe0f35a6a6cc39e76" +"checksum generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec" "checksum getrandom 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "e7db7ca94ed4cd01190ceee0d8a8052f08a247aa1b469a7f68c6a3b71afcf407" +"checksum h2 0.1.26 (registry+https://github.com/rust-lang/crates.io-index)" = "a5b34c246847f938a410a03c5458c7fee2274436675e76d8b903c08efc29c462" +"checksum h2 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b9433d71e471c1736fd5a61b671fc0b148d7a2992f666c958d03cd8feb3b88d1" +"checksum hdrhistogram 6.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "08d331ebcdbca4acbefe5da8c3299b2e246f198a8294cc5163354e743398b89d" +"checksum headers 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c9836ffd533e1fb207cfdb2e357079addbd17ef5c68eea5afe2eece40555b905" +"checksum headers-core 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" "checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" "checksum hex 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "023b39be39e3a2da62a94feb433e91e8bcd37676fbc8bea371daf52b7a769a3e" +"checksum http 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)" = "d6ccf5ede3a895d8856620237b2f02972c1bbc78d2965ad7fe8838d4a0ed41f0" +"checksum http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b708cc7f06493459026f53b9a61a7a121a5d1ec6238dee58ea4941132b30156b" +"checksum http-body 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6741c859c1b2463a423a1dbce98d418e6c3c3fc720fb0d45528657320920292d" +"checksum http-body 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" +"checksum httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" +"checksum hyper 0.12.35 (registry+https://github.com/rust-lang/crates.io-index)" = "9dbe6ed1438e1f8ad955a4701e9a944938e9519f6888d12d8558b645e247d5f6" +"checksum hyper 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8bf49cfb32edee45d890537d9057d1b02ed55f53b7b6a30bae83a38c9231749e" +"checksum hyper-rustls 0.17.1 (registry+https://github.com/rust-lang/crates.io-index)" = "719d85c7df4a7f309a77d145340a063ea929dcb2e025bae46a80345cffec2952" +"checksum hyper-tls 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3adcd308402b9553630734e9c36b77a7e48b3821251ca2493e8cd596763aafaa" +"checksum idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" +"checksum idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" +"checksum im 12.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "de38d1511a0ce7677538acb1e31b5df605147c458e061b2cdb89858afb1cd182" +"checksum indexmap 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0b54058f0a6ff80b6803da8faf8997cde53872b38f4023728f6830b06cd3c0dc" +"checksum input_buffer 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8e1b822cc844905551931d6f81608ed5f50a79c1078a4e2b4d42dbc7c1eedfbf" "checksum iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" "checksum itertools 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "87fa75c9dea7b07be3138c49abbb83fd4bea199b5cdc76f9804458edc5da0d6e" "checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f" @@ -1441,65 +3121,119 @@ dependencies = [ "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" "checksum libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)" = "1a31a0627fdf1f6a39ec0dd577e101440b7db22672c0901fe00a9a6fbb5c24e8" +"checksum linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ae91b68aebc4ddb91978b11a1b02ddd8602a05ec19002801c5666000e05e0f83" "checksum lock_api 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "79b2de95ecb4691949fea4716ca53cdbcfccb2c612e19644a8bad05edcf9f47b" +"checksum log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" "checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" +"checksum matchers 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f099785f7595cc4b4553a174ce30dd7589ef93391ff414dbb67f62392b9e0ce1" +"checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" "checksum maybe-uninit 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" "checksum memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "88579771288728879b57485cc7d6b07d648c9f0141eb955f8ab7f9d45394468e" "checksum memoffset 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ce6075db033bbbb7ee5a0bbd3a3186bbae616f57fb001c485c7ff77955f8177f" +"checksum metrics 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "51b70227ece8711a1aa2f99655efd795d0cff297a5b9fe39645a93aacf6ad39d" +"checksum metrics-core 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7c064b3a1ff41f4bf6c91185c8a0caeccf8a8a27e9d0f92cc54cf3dbec812f48" +"checksum metrics-observer-prometheus 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4f9bb94f40e189c87cf70ef1c78815b949ab9d28fe76ebb81f15f79bd19a33d6" +"checksum metrics-runtime 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "15ef9de8e4a0dd82d38f8588ef40c11db7e12b75c45945fd3ef6993a708f7ced" +"checksum metrics-util 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d11f8090a8886339f9468a04eeea0711e4cf27538b134014664308041307a1c5" +"checksum mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)" = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +"checksum mime_guess 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1a0ed03949aef72dbdf3116a383d7b38b4768e6f960528cd6a6044aa9ed68599" "checksum mio 0.6.21 (registry+https://github.com/rust-lang/crates.io-index)" = "302dec22bcf6bae6dfb69c647187f4b4d0fb6f535521f7bc022430ce8e12008f" "checksum mio-uds 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)" = "966257a94e196b11bb43aca423754d87429960a768de9414f3691d6957abf125" "checksum miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" +"checksum mockito 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aee38c301104cc75a6628a4360be706fbdf84290c15a120b7e54eca5881c3450" +"checksum native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4b2df1a4c22fd44a62147fd8f13dd0f95c9d8ca7b2610299b2a2f9cf8964274e" "checksum net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88" "checksum nodrop 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" "checksum nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" -"checksum num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)" = "b85e541ef8255f6cf42bbfe4ef361305c6c135d10919ecc26126c4e5ae94bc09" -"checksum num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "6ba9a427cfca2be13aa6f6403b0b7e7368fe982bfa16fccc450ce74c46cd9b32" +"checksum num-bigint 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f6f115de20ad793e857f76da2563ff4a09fbcfd6fe93cca0c5d996ab5f3ee38d" +"checksum num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba" +"checksum num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" "checksum num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bcef43580c035376c0705c42792c294b66974abbfd2789b511784023f71f3273" +"checksum opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" +"checksum openssl 0.10.26 (registry+https://github.com/rust-lang/crates.io-index)" = "3a3cc5799d98e1088141b8e01ff760112bbd9f19d850c124500566ca6901a585" +"checksum openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" +"checksum openssl-sys 0.9.53 (registry+https://github.com/rust-lang/crates.io-index)" = "465d16ae7fc0e313318f7de5cecf57b2fbe7511fd213978b457e1c96ff46736f" +"checksum os_type 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7edc011af0ae98b7f88cf7e4a83b70a54a75d2b8cb013d6efd02e5956207e9eb" +"checksum owning_ref 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "49a4b8ea2179e6a2e27411d3bca09ca6dd630821cf6894c6c7c8467a8ee7ef13" "checksum parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252" "checksum parking_lot_core 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b876b1b9e7ac6e1a74a6da34d25c42e17e8862aa409cbbbdcfc8d86c6f3bc62b" +"checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" +"checksum percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" "checksum pin-project 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "75fca1c4ff21f60ca2d37b80d72b63dab823a9d19d3cda3a81d18bc03f0ba8c5" "checksum pin-project-internal 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "6544cd4e4ecace61075a6ec78074beeef98d58aa9a3d07d053d993b2946a90d6" "checksum pin-project-lite 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "237844750cfbb86f67afe27eee600dfbbcb6188d734139b534cbfbf4f96792ae" "checksum pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5894c618ce612a3fa23881b152b608bafb8c56cfc22f434a3ba3120b40f7b587" +"checksum pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)" = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" "checksum ppv-lite86 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b" "checksum proc-macro-hack 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)" = "ecd45702f76d6d3c75a80564378ae228a85f0b59d2f3ed43c91b4a69eb2ebfc5" "checksum proc-macro-nested 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "369a6ed065f249a159e06c45752c780bda2fb53c995718f9e484d08daa9eb42e" -"checksum proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "9c9e470a8dc4aeae2dee2f335e8f533e2d4b347e1434e5671afc49b054592f27" +"checksum proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)" = "3acb317c6ff86a4e579dfa00fc5e6cca91ecbb4e7eb2df0468805b674eb88548" +"checksum quanta 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f4f7a1905379198075914bc93d32a5465c40474f90a078bb13439cb00c547bcc" "checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0" "checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" +"checksum rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" "checksum rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" "checksum rand_chacha 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "03a2a90da8c7523f554344f921aa97283eadf6ac484a6d2a7d0212fa7f8d6853" +"checksum rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +"checksum rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" "checksum rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" "checksum rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" "checksum rand_os 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a788ae3edb696cfcba1c19bfd388cc4b8c21f8a408432b199c072825084da58a" "checksum rand_xoshiro 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0e18c91676f670f6f0312764c759405f13afb98d5d73819840cf72a518487bff" "checksum rayon 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "83a27732a533a1be0a0035a111fe76db89ad312f6f0347004c220c57f209a123" "checksum rayon-core 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "98dcf634205083b17d0861252431eb2acbfb698ab7478a2d20de07954f47ec7b" +"checksum rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +"checksum redis 0.15.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3eeb1fe3fc011cde97315f370bc88e4db3c23b08709a04915921e02b1d363b20" "checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" "checksum regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dc220bd33bdce8f093101afe22a037b8eb0e5af33592e6a9caafff0d4cb81cbd" "checksum regex-automata 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "92b73c2a1770c255c240eaa4ee600df1704a38dc3feaa6e949e7fcd4f8dc09f9" "checksum regex-syntax 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)" = "11a7e20d1cce64ef2fed88b66d347f88bd9babb82845b2b858f3edbf59a4f716" +"checksum remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" +"checksum reqwest 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c0e798e19e258bf6c30a304622e3e9ac820e483b06a1857a026e1f109b113fe4" "checksum ring 0.16.9 (registry+https://github.com/rust-lang/crates.io-index)" = "6747f8da1f2b1fabbee1aaa4eb8a11abf9adef0bf58a41cee45db5d59cecdfac" "checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +"checksum rustls 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b25a18b1bf7387f0145e7f8324e700805aade3842dd3db2e74e4cdeb4677c09e" "checksum ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8" "checksum same-file 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "585e8ddcedc187886a30fa705c47985c3fa88d06624095856b36ca0b82ff4421" +"checksum schannel 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "87f550b06b6cba9c8b8be3ee73f391990116bf527450d2556e9b9ce263b9a021" +"checksum scoped-tls 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" "checksum scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b42e15e59b18a828bbf5c58ea01debb36b9b096346de35d941dcb89009f24a0d" +"checksum sct 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c" +"checksum secrecy 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f2309a011083016deb67a984e8edb0845e42b4c6aaadae7658ebdcb47b91fdbc" +"checksum secrecy 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9182278ed645df3477a9c27bfee0621c621aa16f6972635f7f795dae3d81070f" +"checksum security-framework 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8ef2429d7cefe5fd28bd1d2ed41c944547d4ff84776f5935b456da44593a16df" +"checksum security-framework-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e31493fc37615debb8c5090a7aeb4a9730bc61e77ab10b9af59f1a202284f895" "checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" "checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" "checksum serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)" = "0c4b39bd9b0b087684013a792c59e3e07a46a01d2322518d8a1104641a0b1be0" "checksum serde_derive 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)" = "ca13fc1a832f793322228923fbb3aba9f3f44444898f835d31ad1b74fa0a2bf8" "checksum serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)" = "2f72eb2a68a7dc3f9a691bfda9305a1c017a6215e5a4545c258500d2099a37c2" +"checksum serde_path_to_error 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "359b895005d818163c78a24d272cc98567cce80c2461cf73f513da1d296c0b62" "checksum serde_test 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)" = "00d9d9443b1a25de2526ad21a2efc89267df5387c36035fe3902fbda8a79d83c" +"checksum serde_urlencoded 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9ec5d77e2d4c73717816afac02670d5c4f534ea95ed430442cad02e7a6e32c97" +"checksum sha-1 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" +"checksum sha1 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" +"checksum sized-chunks 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "9d3e7f23bad2d6694e0f46f5e470ec27eb07b8f3e8b309a4b0dc17501928b9f2" "checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" "checksum smallvec 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "f7b0758c52e15a8b5e3691eae6cc559f08eee9406e548a4477ba4e67770a82b6" "checksum smallvec 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "44e59e0c9fa00817912ae6e4e6e3c4fe04455e75699d06eedc7d85917ed8e8f4" "checksum sourcefile 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4bf77cb82ba8453b42b6ae1d692e4cdc92f9a47beaf89a847c8be83f4e328ad3" "checksum spin 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" -"checksum syn 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "0e7bedb3320d0f3035594b0b723c8a28d7d336a3eda3881db79e61d676fb644c" +"checksum stable_deref_trait 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8" +"checksum stream-cancel 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "65851535511fa506e51dfa6f718f91ad2ea91f61a11392a093e0341f9730ebd5" +"checksum string 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d24114bfcceb867ca7f71a0d3fe45d45619ec47a6fbfa98cb14e14250bfa5d6d" +"checksum syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)" = "af6f3550d8dff9ef7dc34d384ac6f107e5d31c8f57d9f28e0081503f547ac8f5" +"checksum synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545" +"checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" "checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +"checksum thiserror 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)" = "6f357d1814b33bc2dc221243f8424104bfe72dbe911d5b71b3816a2dff1c977e" +"checksum thiserror-impl 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)" = "eb2e25d25307eb8436894f727aba8f65d07adf02e5b35a13cebed48bd282bfef" +"checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" +"checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" "checksum tinytemplate 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4574b75faccaacddb9b284faecdf0b544b80b6b294f3d062d325c5726a209c20" "checksum tokio 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)" = "5a09c0b5bb588872ab2f09afa13ee6e9dac11e10a0ec9e8e3ba39a5a5d530af6" "checksum tokio 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "c1fc73332507b971a5010664991a441b5ee0de92017f5a0e8b00fd684573045b" +"checksum tokio-buf 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8fb220f46c53859a4b7ec083e41dec9778ff0b1851c0942b211edb89e0ccdc46" "checksum tokio-codec 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5c501eceaf96f0e1793cf26beb63da3d11c738c4a943fdf3746d81d64684c39f" "checksum tokio-current-thread 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "d16217cad7f1b840c5a97dfb3c43b0c871fef423a6e8d2118c604e843662a443" "checksum tokio-executor 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "ca6df436c42b0c3330a82d855d2ef017cd793090ad550a6bc2184f4b933532ab" @@ -1507,34 +3241,61 @@ dependencies = [ "checksum tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "5090db468dad16e1a7a54c8c67280c5e4b544f3d3e018f0b913b400261f85926" "checksum tokio-macros 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "50a61f268a3db2acee8dcab514efc813dc6dbe8a00e86076f935f94304b59a7a" "checksum tokio-reactor 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "6732fe6b53c8d11178dcb77ac6d9682af27fc6d4cb87789449152e5377377146" +"checksum tokio-retry 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9c03755b956458582182941061def32b8123a26c98b08fc6ddcf49ae89d18f33" +"checksum tokio-rustls 0.10.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2d7cf08f990090abd6c6a73cab46fed62f85e8aef8b99e4b918a9f4a637f0676" "checksum tokio-sync 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "d06554cce1ae4a50f42fba8023918afa931413aded705b560e29600ccf7c6d76" "checksum tokio-tcp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1d14b10654be682ac43efee27401d792507e30fd8d26389e1da3b185de2e4119" "checksum tokio-threadpool 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)" = "f0c32ffea4827978e9aa392d2f743d973c1dfa3730a2ed3f22ce1e6984da848c" "checksum tokio-timer 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)" = "1739638e364e558128461fc1ad84d997702c8e31c2e6b18fb99842268199e827" +"checksum tokio-tls 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7bde02a3a5291395f59b06ec6945a3077602fac2b07eeeaf0dee2122f3619828" +"checksum tokio-tungstenite 0.10.0 (git+https://github.com/snapview/tokio-tungstenite)" = "" "checksum tokio-udp 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f02298505547f73e60f568359ef0d016d5acd6e830ab9bc7c4a5b3403440121b" "checksum tokio-uds 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "037ffc3ba0e12a0ab4aca92e5234e0dedeb48fddf6ccd260f1f150a36a9f2445" +"checksum tokio-util 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "571da51182ec208780505a32528fc5512a8fe1443ab960b3f2f3ef093cd16930" +"checksum toml 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)" = "758664fc71a3a69038656bee8b6be6477d2a6c315a6b81f7081f591bffa4111f" +"checksum tower-service 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" "checksum tracing 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "1e213bd24252abeb86a0b7060e02df677d367ce6cb772cef17e9214b8390a8d3" "checksum tracing-attributes 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "04cfd395def5a60236e187e1ff905cb55668a59f29928dec05e6e1b1fd2ac1f3" "checksum tracing-core 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "13a46f11e372b8bd4b4398ea54353412fdd7fd42a8370c7e543e218cf7661978" "checksum tracing-futures 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "33848db47a7c848ab48b66aab3293cb9c61ea879a3586ecfcd17302fcea0baf1" +"checksum tracing-log 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5e0f8c7178e13481ff6765bd169b33e8d554c5d2bbede5e32c356194be02b9b9" +"checksum tracing-subscriber 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "192ca16595cdd0661ce319e8eede9c975f227cdaabc4faaefdc256f43d852e45" +"checksum try-lock 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382" +"checksum tungstenite 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8a0c2bd5aeb7dcd2bb32e472c8872759308495e5eccc942e929a513cd8d36110" +"checksum typenum 1.11.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6d2783fe2d6b8c1101136184eb41be8b1ad379e4657050b8aaff0c79ee7575f9" "checksum unicase 2.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +"checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" "checksum unicode-normalization 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "5479532badd04e128284890390c1e876ef7a993d0570b3597ae43dfa1d59afa4" "checksum unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" "checksum unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7007dbd421b92cc6e28410fe7362e2e0a2503394908f417b68ec8d1c364c4e20" "checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" +"checksum unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" "checksum untrusted 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "60369ef7a31de49bcb3f6ca728d4ba7300d9a1658f94c727d4cab8c8d9f4aece" +"checksum url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" +"checksum url 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "75b414f6c464c879d7f9babf951f23bc3743fb7313c081b2e6ca719067ea9d61" +"checksum urlencoding 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3df3561629a8bb4c57e5a2e4c43348d9e29c7c29d9b1c4c1f47166deca8f37ed" +"checksum utf-8 0.7.5 (registry+https://github.com/rust-lang/crates.io-index)" = "05e42f7c18b8f902290b009cde6d651262f956c98bc51bca4cd1d511c9cd85c7" +"checksum utf8-ranges 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b4ae116fef2b7fea257ed6440d3cfcff7f190865f170cdad00bb6465bf18ecba" "checksum uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11" +"checksum vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "3fc439f2794e98976c88a2a2dafce96b930fe8010b0a256b3c2199a773933168" "checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" "checksum version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce" +"checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" "checksum walkdir 2.2.9 (registry+https://github.com/rust-lang/crates.io-index)" = "9658c94fa8b940eab2250bd5a457f9c48b748420d71293b165c8cdbe2f55f71e" +"checksum want 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b6395efa4784b027708f7451087e647ec73cc74f5d9bc2e418404248d679a230" +"checksum want 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +"checksum warp 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b11768dcc95dbbc7db573192cda35cdbbe59793f8409a4e11b87141a0930d6ed" "checksum wasi 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b89c3ce4ce14bdc6fb6beaf9ec7928ca331de5df7e5ea278375642a2f478570d" "checksum wasm-bindgen 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)" = "5205e9afdf42282b192e2310a5b463a6d1c1d774e30dc3c791ac37ab42d2616c" "checksum wasm-bindgen-backend 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)" = "11cdb95816290b525b32587d76419facd99662a07e59d3cdb560488a819d9a45" +"checksum wasm-bindgen-futures 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8bbdd49e3e28b40dec6a9ba8d17798245ce32b019513a845369c641b275135d9" "checksum wasm-bindgen-macro 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)" = "574094772ce6921576fb6f2e3f7497b8a76273b6db092be18fc48a082de09dc3" "checksum wasm-bindgen-macro-support 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)" = "e85031354f25eaebe78bb7db1c3d86140312a911a106b2e29f9cc440ce3e7668" "checksum wasm-bindgen-shared 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)" = "f5e7e61fc929f4c0dddb748b102ebf9f632e2b8d739f2016542b4de2965a9601" "checksum wasm-bindgen-webidl 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)" = "ef012a0d93fc0432df126a8eaf547b2dce25a8ce9212e1d3cbeef5c11157975d" "checksum web-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)" = "aaf97caf6aa8c2b1dac90faf0db529d9d63c93846cca4911856f78a83cebf53b" +"checksum webpki 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d7e664e770ac0110e2384769bcc59ed19e329d81f555916a6e072714957b81b4" +"checksum webpki-roots 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a262ae37dd9d60f60dd473d1158f9fbebf110ba7b6a5051c8160460f6043718b" "checksum weedle 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3bb43f70885151e629e2a19ce9e50bd730fd436cfd4b666894c9ce4de9141164" "checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" "checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" @@ -1542,4 +3303,8 @@ dependencies = [ "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" "checksum winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7168bab6e1daee33b4557efd0e95d5ca70a03706d39fa5f3fe7a236f584b03c9" "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +"checksum winreg 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9" "checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +"checksum yaml-rust 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "65923dd1784f44da1d2c3dbbc5e822045628c590ba72123e1c73d3c230c4434d" +"checksum yup-oauth2 3.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "687c1c52bf66691f1e7426e7520aecec25bf659835179095280a4acfcb24f63f" +"checksum zeroize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3cbac2ed2ba24cc90f5e06485ac8c7c1e5449fe8911aef4d8877218af021a5b8" diff --git a/Cargo.toml b/Cargo.toml index 389f73706..316310f9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,20 +1,20 @@ [workspace] members = [ - # "./crates/ilp-cli", - # "./crates/ilp-node", - # "./crates/interledger", - # "./crates/interledger-api", - # "./crates/interledger-btp", + "./crates/ilp-cli", + "./crates/ilp-node", + "./crates/interledger", + "./crates/interledger-api", + "./crates/interledger-btp", "./crates/interledger-ccp", - # "./crates/interledger-http", + "./crates/interledger-http", "./crates/interledger-ildcp", "./crates/interledger-packet", "./crates/interledger-router", "./crates/interledger-service", - # "./crates/interledger-service-util", - # "./crates/interledger-settlement", - # "./crates/interledger-spsp", - # "./crates/interledger-store", - # "./crates/interledger-stream", + "./crates/interledger-service-util", + "./crates/interledger-settlement", + "./crates/interledger-spsp", + "./crates/interledger-store", + "./crates/interledger-stream", ] diff --git a/README.md b/README.md index 1bca9edef..fd54c5d42 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,16 @@ [![crates.io](https://img.shields.io/crates/v/interledger.svg)](https://crates.io/crates/interledger) [![Interledger.rs Documentation](https://docs.rs/interledger/badge.svg)](https://docs.rs/interledger) [![CircleCI](https://circleci.com/gh/interledger-rs/interledger-rs.svg?style=shield)](https://circleci.com/gh/interledger-rs/interledger-rs) -![Rust Version](https://img.shields.io/badge/rust-stable-Success) -[![Docker Image](https://img.shields.io/docker/pulls/interledgerrs/node.svg?maxAge=2592000)](https://hub.docker.com/r/interledgerrs/node/) +![rustc](https://img.shields.io/badge/rustc-1.39+-red.svg) +![Rust](https://img.shields.io/badge/rust-stable-Success) +[![Docker Image](https://img.shields.io/docker/pulls/interledgerrs/ilp-node.svg?maxAge=2592000)](https://hub.docker.com/r/interledgerrs/ilp-node/) + +## Requirements + +All crates require Rust 2018 edition and are tested on the following channels: + +- `1.39.0` (minimum supported) +- `stable` ## Connecting to the Testnet @@ -34,7 +42,7 @@ To run the Interledger.rs components by themselves (rather than the `testnet-bun #### Install ```bash # -docker pull interledgerrs/node +docker pull interledgerrs/ilp-node docker pull interledgerrs/ilp-cli docker pull interledgerrs/ilp-settlement-ethereum ``` @@ -43,7 +51,7 @@ docker pull interledgerrs/ilp-settlement-ethereum ```bash # # This runs the sender / receiver / router bundle -docker run -it interledgerrs/node +docker run -it interledgerrs/ilp-node # This is a simple CLI for interacting with the node's HTTP API docker run -it --rm interledgerrs/ilp-cli diff --git a/crates/ilp-cli/Cargo.toml b/crates/ilp-cli/Cargo.toml index 9ad32951c..d8b3e99a0 100644 --- a/crates/ilp-cli/Cargo.toml +++ b/crates/ilp-cli/Cargo.toml @@ -10,9 +10,9 @@ repository = "https://github.com/interledger-rs/interledger-rs" [dependencies] clap = { version = "2.33.0", default-features = false } thiserror = { version = "1.0.4", default-features = false } -http = { version = "0.1.18", default-features = false } -reqwest = { version = "0.9.22", default-features = false, features = ["default-tls"] } -serde = { version = "1.0.101", default-features = false } +http = { version = "0.2", default-features = false } +reqwest = { version = "0.10.1", default-features = false, features = ["default-tls", "blocking", "json"] } +serde = { version = "1.0.101", default-features = false, features = ["derive"] } serde_json = { version = "1.0.41", default-features = false } tungstenite = { version = "0.9.1", default-features = false, features = ["tls"] } url = { version = "2.1.0", default-features = false } diff --git a/crates/ilp-cli/src/interpreter.rs b/crates/ilp-cli/src/interpreter.rs index d49b9a2da..6d878df66 100644 --- a/crates/ilp-cli/src/interpreter.rs +++ b/crates/ilp-cli/src/interpreter.rs @@ -1,6 +1,9 @@ use clap::ArgMatches; use http; -use reqwest::{self, Client, Response}; +use reqwest::{ + self, + blocking::{Client, Response}, +}; use std::{borrow::Cow, collections::HashMap}; use tungstenite::{connect, handshake::client::Request}; use url::Url; diff --git a/crates/ilp-cli/src/main.rs b/crates/ilp-cli/src/main.rs index b88e1268d..2b971903d 100644 --- a/crates/ilp-cli/src/main.rs +++ b/crates/ilp-cli/src/main.rs @@ -23,26 +23,28 @@ pub fn main() { eprintln!("ilp-cli error: {}", e); exit(1); } - Ok(mut response) => match response.text() { - Err(e) => { - eprintln!("ilp-cli error: Failed to parse HTTP response: {}", e); - exit(1); - } - Ok(body) => { - if response.status().is_success() { - if !matches.is_present("quiet") { - println!("{}", body); - } - } else { - eprintln!( - "ilp-cli error: Unexpected response from server: {}: {}", - response.status(), - body, - ); + Ok(response) => { + let status = response.status(); + match response.text() { + Err(e) => { + eprintln!("ilp-cli error: Failed to parse HTTP response: {}", e); exit(1); } + Ok(body) => { + if status.is_success() { + if !matches.is_present("quiet") { + println!("{}", body); + } + } else { + eprintln!( + "ilp-cli error: Unexpected response from server: {}: {}", + status, body, + ); + exit(1); + } + } } - }, + } } } diff --git a/crates/ilp-node/Cargo.toml b/crates/ilp-node/Cargo.toml index 695170d77..b7fa07280 100644 --- a/crates/ilp-node/Cargo.toml +++ b/crates/ilp-node/Cargo.toml @@ -11,10 +11,19 @@ default-run = "ilp-node" [features] default = ["balance-tracking", "redis"] balance-tracking = [] +redis = ["redis_crate", "interledger/redis"] + # This is an experimental feature that enables submitting packet # records to Google Cloud PubSub. This may be removed in the future. google-pubsub = ["base64", "chrono", "parking_lot", "reqwest", "serde_json", "yup-oauth2"] -redis = ["redis_crate", "interledger/redis"] +# This enables monitoring and tracing related features +monitoring = [ + "metrics", + "metrics-core", + "metrics-runtime", + "tracing-futures", + "tracing-subscriber", +] [[test]] name = "redis_tests" @@ -24,26 +33,21 @@ required-features = ["redis"] [dependencies] bytes = { version = "0.4.12", default-features = false } +bytes05 = { package = "bytes", version = "0.5", default-features = false } clap = { version = "2.33.0", default-features = false } config = { version = "0.9.3", default-features = false, features = ["json", "toml", "yaml"] } -futures = { version = "0.1.29", default-features = false } +futures = { version = "0.3.1", default-features = false, features = ["compat"] } hex = { version = "0.4.0", default-features = false } interledger = { path = "../interledger", version = "^0.6.0", default-features = false, features = ["node"] } lazy_static = { version = "1.4.0", default-features = false } -metrics = { version = "0.12.0", default-features = false, features = ["std"] } -metrics-core = { version = "0.5.1", default-features = false } -metrics-runtime = { version = "0.12.0", default-features = false, features = ["metrics-observer-prometheus"] } num-bigint = { version = "0.2.3", default-features = false, features = ["std"] } -redis_crate = { package = "redis", version = "0.13.0", default-features = false, features = ["executor"], optional = true } +redis_crate = { package = "redis", version = "0.15.1", optional = true, features = ["tokio-rt-core"] } ring = { version = "0.16.9", default-features = false } serde = { version = "1.0.101", default-features = false } -tokio = { version = "0.1.22", default-features = false } -tracing = { version = "0.1.9", default-features = true, features = ["log"] } -tracing-futures = { version = "0.1.1", default-features = true, features = ["tokio", "futures-01"] } -tracing-subscriber = { version = "0.1.6", default-features = true, features = ["tracing-log"] } +tokio = { version = "0.2.8", features = ["rt-core", "macros", "time"] } url = { version = "2.1.0", default-features = false } libc = { version = "0.2.62", default-features = false } -warp = { version = "0.1.20", default-features = false, features = ["websocket"] } +warp = { version = "0.2", default-features = false, features = ["websocket"] } secrecy = { version = "0.5.1", default-features = false, features = ["alloc", "serde"] } uuid = { version = "0.8.1", default-features = false} @@ -51,19 +55,28 @@ uuid = { version = "0.8.1", default-features = false} base64 = { version = "0.10.1", default-features = false, optional = true } chrono = { version = "0.4.9", default-features = false, features = [], optional = true} parking_lot = { version = "0.9.0", default-features = false, optional = true } -reqwest = { version = "0.9.22", default-features = false, features = ["default-tls"], optional = true } +reqwest = { version = "0.10.0", default-features = false, features = ["default-tls", "json"], optional = true } serde_json = { version = "1.0.41", default-features = false, optional = true } yup-oauth2 = { version = "3.1.1", default-features = false, optional = true } + +# Tracing / metrics / prometheus for instrumentation +tracing = { version = "0.1.12", default-features = true, features = ["log"] } +tracing-futures = { version = "0.2", default-features = true, features = ["tokio", "futures-03"], optional = true } +tracing-subscriber = { version = "0.1.6", default-features = true, features = ["tracing-log"], optional = true } +metrics = { version = "0.12.0", default-features = false, features = ["std"], optional = true } +metrics-core = { version = "0.5.1", default-features = false, optional = true } +metrics-runtime = { version = "0.12.0", default-features = false, features = ["metrics-observer-prometheus"], optional = true } + [dev-dependencies] approx = { version = "0.3.2", default-features = false } base64 = { version = "0.10.1", default-features = false } net2 = { version = "0.2.33", default-features = false } rand = { version = "0.7.2", default-features = false } -reqwest = { version = "0.9.22", default-features = false, features = ["default-tls"] } +reqwest = { version = "0.10.0", default-features = false, features = ["default-tls", "json"] } serde_json = { version = "1.0.41", default-features = false } tokio-retry = { version = "0.2.0", default-features = false } [badges] circle-ci = { repository = "interledger-rs/interledger-rs" } -codecov = { repository = "interledger-rs/interledger-rs" } +codecov = { repository = "interledger-rs/interledger-rs" } \ No newline at end of file diff --git a/crates/ilp-node/src/google_pubsub.rs b/crates/ilp-node/src/google_pubsub.rs deleted file mode 100644 index 3f667a49e..000000000 --- a/crates/ilp-node/src/google_pubsub.rs +++ /dev/null @@ -1,186 +0,0 @@ -use base64; -use chrono::Utc; -use futures::{ - future::{ok, Either}, - Future, -}; -use interledger::{ - packet::Address, - service::{Account, BoxedIlpFuture, OutgoingRequest, OutgoingService, Username}, -}; -use parking_lot::Mutex; -use reqwest::r#async::Client; -use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, sync::Arc}; -use tokio::spawn; -use tracing::{error, info}; -use yup_oauth2::{service_account_key_from_file, GetToken, ServiceAccountAccess}; - -static TOKEN_SCOPES: Option<&str> = Some("https://www.googleapis.com/auth/pubsub"); - -/// Configuration for the Google PubSub packet publisher -#[derive(Deserialize, Clone, Debug)] -pub struct PubsubConfig { - /// Path to the Service Account Key JSON file. - /// You can obtain this file by logging into [console.cloud.google.com](https://console.cloud.google.com/) - service_account_credentials: String, - project_id: String, - topic: String, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct PubsubMessage { - message_id: Option, - data: Option, - attributes: Option>, - publish_time: Option, -} - -#[derive(Serialize)] -struct PubsubRequest { - messages: Vec, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct PacketRecord { - prev_hop_account: Username, - prev_hop_asset_code: String, - prev_hop_asset_scale: u8, - prev_hop_amount: u64, - next_hop_account: Username, - next_hop_asset_code: String, - next_hop_asset_scale: u8, - next_hop_amount: u64, - destination_ilp_address: Address, - fulfillment: String, - timestamp: String, -} - -/// Create an Interledger service wrapper that publishes records -/// of fulfilled packets to Google Cloud PubSub. -/// -/// This is an experimental feature that may be removed in the future. -pub fn create_google_pubsub_wrapper< - A: Account + 'static, - O: OutgoingService + Clone + Send + 'static, ->( - config: Option, -) -> impl Fn(OutgoingRequest, O) -> BoxedIlpFuture + Clone { - // If Google credentials were passed in, create an HTTP client and - // OAuth2 client that will automatically fetch and cache access tokens - let utilities = if let Some(config) = config { - let key = service_account_key_from_file(config.service_account_credentials.as_str()) - .expect("Unable to load Google Cloud credentials from file"); - let access = ServiceAccountAccess::new(key); - // This needs to be wrapped in a Mutex because the .token() - // method takes a mutable reference to self and we want to - // reuse the same fetcher so that it caches the tokens - let token_fetcher = Arc::new(Mutex::new(access.build())); - - // TODO make sure the client uses HTTP/2 - let client = Client::new(); - let api_endpoint = Arc::new(format!( - "https://pubsub.googleapis.com/v1/projects/{}/topics/{}:publish", - config.project_id, config.topic - )); - info!("Fulfilled packets will be submitted to Google Cloud Pubsub (project ID: {}, topic: {})", config.project_id, config.topic); - Some((client, api_endpoint, token_fetcher)) - } else { - None - }; - - move |request: OutgoingRequest, mut next: O| -> BoxedIlpFuture { - match &utilities { - // Just pass the request on if no Google Pubsub details were configured - None => Box::new(next.send_request(request)), - Some((client, api_endpoint, token_fetcher)) => { - let prev_hop_account = request.from.username().clone(); - let prev_hop_asset_code = request.from.asset_code().to_string(); - let prev_hop_asset_scale = request.from.asset_scale(); - let prev_hop_amount = request.original_amount; - let next_hop_account = request.to.username().clone(); - let next_hop_asset_code = request.to.asset_code().to_string(); - let next_hop_asset_scale = request.to.asset_scale(); - let next_hop_amount = request.prepare.amount(); - let destination_ilp_address = request.prepare.destination(); - let client = client.clone(); - let api_endpoint = api_endpoint.clone(); - let token_fetcher = token_fetcher.clone(); - - Box::new(next.send_request(request).map(move |fulfill| { - // Only fulfilled packets are published for now - let fulfillment = base64::encode(fulfill.fulfillment()); - - let get_token_future = token_fetcher.lock() - .token(TOKEN_SCOPES) - .map_err(|err| { - error!("Error fetching OAuth token for Google PubSub: {:?}", err) - }); - // Spawn a task to submit the packet to PubSub so we - // don't block returning the fulfillment - // Note this means that if there is a problem submitting the - // packet record to PubSub, it will only log an error - spawn( - get_token_future - .and_then(move |token| { - let record = PacketRecord { - prev_hop_account, - prev_hop_asset_code, - prev_hop_asset_scale, - prev_hop_amount, - next_hop_account, - next_hop_asset_code, - next_hop_asset_scale, - next_hop_amount, - destination_ilp_address, - fulfillment, - timestamp: Utc::now().to_rfc3339(), - }; - let data = base64::encode(&serde_json::to_string(&record).unwrap()); - - client - .post(api_endpoint.as_str()) - .bearer_auth(token.access_token) - .json(&PubsubRequest { - messages: vec![PubsubMessage { - // TODO should there be an ID? - message_id: None, - data: Some(data), - attributes: None, - publish_time: None, - }], - }) - .send() - .map_err(|err| { - error!( - "Error sending packet details to Google PubSub: {:?}", - err - ) - }) - .and_then(|mut res| { - if res.status().is_success() { - Either::A(ok(())) - } else { - let status = res.status(); - Either::B(res.text() - .map_err(|err| error!("Error getting response body: {:?}", err)) - .and_then(move |body| { - error!( - %status, - "Error sending packet details to Google PubSub: {}", - body - ); - Ok(()) - })) - } - }) - }), - ); - fulfill - })) - } - } - } -} diff --git a/crates/ilp-node/src/instrumentation/google_pubsub.rs b/crates/ilp-node/src/instrumentation/google_pubsub.rs new file mode 100644 index 000000000..402ab7169 --- /dev/null +++ b/crates/ilp-node/src/instrumentation/google_pubsub.rs @@ -0,0 +1,187 @@ +#[cfg(feature = "google_pubsub")] +use base64; +use chrono::Utc; +use futures::{compat::Future01CompatExt, Future, TryFutureExt}; +use interledger::{ + packet::Address, + service::{Account, IlpResult, OutgoingRequest, OutgoingService, Username}, +}; +use parking_lot::Mutex; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, sync::Arc}; +use tokio::spawn; +use tracing::{error, info}; +use yup_oauth2::{service_account_key_from_file, GetToken, ServiceAccountAccess}; + +static TOKEN_SCOPES: Option<&str> = Some("https://www.googleapis.com/auth/pubsub"); + +/// Configuration for the Google PubSub packet publisher +#[derive(Deserialize, Clone, Debug)] +pub struct PubsubConfig { + /// Path to the Service Account Key JSON file. + /// You can obtain this file by logging into [console.cloud.google.com](https://console.cloud.google.com/) + service_account_credentials: String, + project_id: String, + topic: String, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct PubsubMessage { + message_id: Option, + data: Option, + attributes: Option>, + publish_time: Option, +} + +#[derive(Serialize)] +struct PubsubRequest { + messages: Vec, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct PacketRecord { + prev_hop_account: Username, + prev_hop_asset_code: String, + prev_hop_asset_scale: u8, + prev_hop_amount: u64, + next_hop_account: Username, + next_hop_asset_code: String, + next_hop_asset_scale: u8, + next_hop_amount: u64, + destination_ilp_address: Address, + fulfillment: String, + timestamp: String, +} + +use std::pin::Pin; +type BoxedIlpFuture = Box + Send + 'static>; + +/// Create an Interledger service wrapper that publishes records +/// of fulfilled packets to Google Cloud PubSub. +/// +/// This is an experimental feature that may be removed in the future. +pub fn create_google_pubsub_wrapper( + config: Option, +) -> impl Fn(OutgoingRequest, Box + Send>) -> Pin + Clone +{ + // If Google credentials were passed in, create an HTTP client and + // OAuth2 client that will automatically fetch and cache access tokens + let utilities = if let Some(config) = config { + let key = service_account_key_from_file(config.service_account_credentials.as_str()) + .expect("Unable to load Google Cloud credentials from file"); + let access = ServiceAccountAccess::new(key); + // This needs to be wrapped in a Mutex because the .token() + // method takes a mutable reference to self and we want to + // reuse the same fetcher so that it caches the tokens + let token_fetcher = Arc::new(Mutex::new(access.build())); + + // TODO make sure the client uses HTTP/2 + let client = Client::new(); + let api_endpoint = Arc::new(format!( + "https://pubsub.googleapis.com/v1/projects/{}/topics/{}:publish", + config.project_id, config.topic + )); + info!("Fulfilled packets will be submitted to Google Cloud Pubsub (project ID: {}, topic: {})", config.project_id, config.topic); + Some((client, api_endpoint, token_fetcher)) + } else { + None + }; + + move |request: OutgoingRequest, + mut next: Box + Send>| + -> Pin { + let (client, api_endpoint, token_fetcher) = if let Some(utilities) = utilities.clone() { + utilities + } else { + return Box::pin(async move { next.send_request(request).await }); + }; + + // Just pass the request on if no Google Pubsub details were configured + let prev_hop_account = request.from.username().clone(); + let prev_hop_asset_code = request.from.asset_code().to_string(); + let prev_hop_asset_scale = request.from.asset_scale(); + let prev_hop_amount = request.original_amount; + let next_hop_account = request.to.username().clone(); + let next_hop_asset_code = request.to.asset_code().to_string(); + let next_hop_asset_scale = request.to.asset_scale(); + let next_hop_amount = request.prepare.amount(); + let destination_ilp_address = request.prepare.destination(); + + Box::pin(async move { + let result = next.send_request(request).await; + + // Only fulfilled packets are published for now + if let Ok(fulfill) = result.clone() { + let fulfillment = base64::encode(fulfill.fulfillment()); + let get_token_future = + token_fetcher + .lock() + .token(TOKEN_SCOPES) + .compat() + .map_err(|err| { + error!("Error fetching OAuth token for Google PubSub: {:?}", err) + }); + + // Spawn a task to submit the packet to PubSub so we + // don't block returning the fulfillment + // Note this means that if there is a problem submitting the + // packet record to PubSub, it will only log an error + spawn(async move { + let token = get_token_future.await?; + let record = PacketRecord { + prev_hop_account, + prev_hop_asset_code, + prev_hop_asset_scale, + prev_hop_amount, + next_hop_account, + next_hop_asset_code, + next_hop_asset_scale, + next_hop_amount, + destination_ilp_address, + fulfillment, + timestamp: Utc::now().to_rfc3339(), + }; + let data = base64::encode(&serde_json::to_string(&record).unwrap()); + let res = client + .post(api_endpoint.as_str()) + .bearer_auth(token.access_token) + .json(&PubsubRequest { + messages: vec![PubsubMessage { + // TODO should there be an ID? + message_id: None, + data: Some(data), + attributes: None, + publish_time: None, + }], + }) + .send() + .map_err(|err| { + error!("Error sending packet details to Google PubSub: {:?}", err) + }) + .await?; + + // Log the error + if !res.status().is_success() { + let status = res.status(); + let body = res + .text() + .map_err(|err| error!("Error getting response body: {:?}", err)) + .await?; + error!( + %status, + "Error sending packet details to Google PubSub: {}", + body + ); + } + + Ok::<(), ()>(()) + }); + } + + result + }) + } +} diff --git a/crates/ilp-node/src/instrumentation/metrics.rs b/crates/ilp-node/src/instrumentation/metrics.rs new file mode 100644 index 000000000..8f3553a13 --- /dev/null +++ b/crates/ilp-node/src/instrumentation/metrics.rs @@ -0,0 +1,82 @@ +use interledger::{ + ccp::CcpRoutingAccount, + service::{ + Account, IlpResult, IncomingRequest, IncomingService, OutgoingRequest, OutgoingService, + }, +}; +use metrics::{self, labels, recorder, Key}; +use std::time::Instant; + +pub async fn incoming_metrics( + request: IncomingRequest, + mut next: Box + Send>, +) -> IlpResult { + let labels = labels!( + "from_asset_code" => request.from.asset_code().to_string(), + "from_routing_relation" => request.from.routing_relation().to_string(), + ); + recorder().increment_counter( + Key::from_name_and_labels("requests.incoming.prepare", labels.clone()), + 1, + ); + let start_time = Instant::now(); + + let result = next.handle_request(request).await; + if result.is_ok() { + recorder().increment_counter( + Key::from_name_and_labels("requests.incoming.fulfill", labels.clone()), + 1, + ); + } else { + recorder().increment_counter( + Key::from_name_and_labels("requests.incoming.reject", labels.clone()), + 1, + ); + } + + recorder().record_histogram( + Key::from_name_and_labels("requests.incoming.duration", labels), + (Instant::now() - start_time).as_nanos() as u64, + ); + result +} + +pub async fn outgoing_metrics( + request: OutgoingRequest, + mut next: Box + Send>, +) -> IlpResult { + let labels = labels!( + "from_asset_code" => request.from.asset_code().to_string(), + "to_asset_code" => request.to.asset_code().to_string(), + "from_routing_relation" => request.from.routing_relation().to_string(), + "to_routing_relation" => request.to.routing_relation().to_string(), + ); + + // TODO replace these calls with the counter! macro if there's a way to easily pass in the already-created labels + // right now if you pass the labels into one of the other macros, it gets a recursion limit error while expanding the macro + recorder().increment_counter( + Key::from_name_and_labels("requests.outgoing.prepare", labels.clone()), + 1, + ); + let start_time = Instant::now(); + + let result = next.send_request(request).await; + if result.is_ok() { + recorder().increment_counter( + Key::from_name_and_labels("requests.outgoing.fulfill", labels.clone()), + 1, + ); + } else { + recorder().increment_counter( + Key::from_name_and_labels("requests.outgoing.reject", labels.clone()), + 1, + ); + } + + recorder().record_histogram( + Key::from_name_and_labels("requests.outgoing.duration", labels.clone()), + (Instant::now() - start_time).as_nanos() as u64, + ); + + result +} diff --git a/crates/ilp-node/src/instrumentation/mod.rs b/crates/ilp-node/src/instrumentation/mod.rs new file mode 100644 index 000000000..66113214f --- /dev/null +++ b/crates/ilp-node/src/instrumentation/mod.rs @@ -0,0 +1,10 @@ +#[cfg(feature = "monitoring")] +pub mod metrics; +#[cfg(feature = "monitoring")] +pub mod trace; + +#[cfg(feature = "monitoring")] +pub mod prometheus; + +#[cfg(feature = "google-pubsub")] +pub mod google_pubsub; diff --git a/crates/ilp-node/src/instrumentation/prometheus.rs b/crates/ilp-node/src/instrumentation/prometheus.rs new file mode 100644 index 000000000..bbd0d5184 --- /dev/null +++ b/crates/ilp-node/src/instrumentation/prometheus.rs @@ -0,0 +1,90 @@ +use crate::InterledgerNode; +use metrics_core::{Builder, Drain, Observe}; +use metrics_runtime; +use serde::Deserialize; +use std::{net::SocketAddr, sync::Arc, time::Duration}; +use tracing::{error, info}; +use warp::{ + http::{Response, StatusCode}, + Filter, +}; + +/// Configuration for [Prometheus](https://prometheus.io) metrics collection. +#[derive(Deserialize, Clone)] +pub struct PrometheusConfig { + /// IP address and port to host the Prometheus endpoint on. + pub bind_address: SocketAddr, + /// Amount of time, in milliseconds, that the node will collect data points for the + /// Prometheus histograms. Defaults to 300000ms (5 minutes). + #[serde(default = "PrometheusConfig::default_histogram_window")] + pub histogram_window: u64, + /// Granularity, in milliseconds, that the node will use to roll off old data. + /// For example, a value of 1000ms (1 second) would mean that the node forgets the oldest + /// 1 second of histogram data points every second. Defaults to 10000ms (10 seconds). + #[serde(default = "PrometheusConfig::default_histogram_granularity")] + pub histogram_granularity: u64, +} + +impl PrometheusConfig { + fn default_histogram_window() -> u64 { + 300_000 + } + + fn default_histogram_granularity() -> u64 { + 10_000 + } +} + +/// Starts a Prometheus metrics server that will listen on the configured address. +/// +/// # Errors +/// This will fail if another Prometheus server is already running in this +/// process or on the configured port. +#[allow(clippy::cognitive_complexity)] +pub async fn serve_prometheus(node: InterledgerNode) -> Result<(), ()> { + let prometheus = if let Some(ref prometheus) = node.prometheus { + prometheus + } else { + error!(target: "interledger-node", "No prometheus configuration provided"); + return Err(()); + }; + + // Set up the metrics collector + let receiver = metrics_runtime::Builder::default() + .histogram( + Duration::from_millis(prometheus.histogram_window), + Duration::from_millis(prometheus.histogram_granularity), + ) + .build() + .expect("Failed to create metrics Receiver"); + + let controller = receiver.controller(); + // Try installing the global recorder + match metrics::set_boxed_recorder(Box::new(receiver)) { + Ok(_) => { + let observer = Arc::new(metrics_runtime::observers::PrometheusBuilder::default()); + + let filter = warp::get().and(warp::path::end()).map(move || { + let mut observer = observer.build(); + controller.observe(&mut observer); + let prometheus_response = observer.drain(); + Response::builder() + .status(StatusCode::OK) + .header("Content-Type", "text/plain; version=0.0.4") + .body(prometheus_response) + }); + + info!(target: "interledger-node", + "Prometheus metrics server listening on: {}", + prometheus.bind_address + ); + + tokio::spawn(warp::serve(filter).bind(prometheus.bind_address)); + Ok(()) + } + Err(e) => { + error!(target: "interledger-node", "Error installing global metrics recorder (this is likely caused by trying to run two nodes with Prometheus metrics in the same process): {:?}", e); + Err(()) + } + } +} diff --git a/crates/ilp-node/src/trace.rs b/crates/ilp-node/src/instrumentation/trace.rs similarity index 82% rename from crates/ilp-node/src/trace.rs rename to crates/ilp-node/src/instrumentation/trace.rs index 911517926..7f886c52c 100644 --- a/crates/ilp-node/src/trace.rs +++ b/crates/ilp-node/src/instrumentation/trace.rs @@ -1,8 +1,9 @@ -use futures::Future; use interledger::{ ccp::{CcpRoutingAccount, RoutingRelation}, packet::{ErrorCode, Fulfill, Reject}, - service::{Account, IncomingRequest, IncomingService, OutgoingRequest, OutgoingService}, + service::{ + Account, IlpResult, IncomingRequest, IncomingService, OutgoingRequest, OutgoingService, + }, }; use std::str; use tracing::{debug_span, error_span, info, info_span}; @@ -12,10 +13,10 @@ use uuid::Uuid; /// Add tracing context for the incoming request. /// This adds minimal information for the ERROR log /// level and more information for the DEBUG level. -pub fn trace_incoming( +pub async fn trace_incoming( request: IncomingRequest, - mut next: impl IncomingService, -) -> impl Future { + mut next: Box + Send>, +) -> IlpResult { let request_span = error_span!(target: "interledger-node", "incoming", request.id = %Uuid::new_v4(), @@ -37,19 +38,17 @@ pub fn trace_incoming( ); let _details_scope = details_span.enter(); - next.handle_request(request) - .then(trace_response) - .in_current_span() + next.handle_request(request).in_current_span().await } /// Add tracing context when the incoming request is /// being forwarded and turned into an outgoing request. /// This adds minimal information for the ERROR log /// level and more information for the DEBUG level. -pub fn trace_forwarding( +pub async fn trace_forwarding( request: OutgoingRequest, - mut next: impl OutgoingService, -) -> impl Future { + mut next: Box + Send>, +) -> IlpResult { // Here we only include the outgoing details because this will be // inside the "incoming" span that includes the other details let request_span = error_span!(target: "interledger-node", @@ -66,16 +65,16 @@ pub fn trace_forwarding( ); let _details_scope = details_span.enter(); - next.send_request(request).in_current_span() + next.send_request(request).in_current_span().await } /// Add tracing context for the outgoing request (created by this node). /// This adds minimal information for the ERROR log /// level and more information for the DEBUG level. -pub fn trace_outgoing( +pub async fn trace_outgoing( request: OutgoingRequest, - mut next: impl OutgoingService, -) -> impl Future { + mut next: Box + Send>, +) -> IlpResult { let request_span = error_span!(target: "interledger-node", "outgoing", request.id = %Uuid::new_v4(), @@ -100,16 +99,14 @@ pub fn trace_outgoing( // because there's a good chance they'll be offline let ignore_rejects = request.prepare.destination().scheme() == "peer" && request.to.routing_relation() == RoutingRelation::Child; - next.send_request(request) - .then(move |result| { - if let Err(ref err) = result { - if err.code() == ErrorCode::F02_UNREACHABLE && ignore_rejects { - return result; - } - } - trace_response(result) - }) - .in_current_span() + + let result = next.send_request(request).in_current_span().await; + if let Err(ref err) = result { + if err.code() == ErrorCode::F02_UNREACHABLE && ignore_rejects { + return result; + } + } + trace_response(result) } /// Log whether the response was a Fulfill or Reject diff --git a/crates/ilp-node/src/lib.rs b/crates/ilp-node/src/lib.rs index 1086e4991..7f20db21b 100644 --- a/crates/ilp-node/src/lib.rs +++ b/crates/ilp-node/src/lib.rs @@ -1,15 +1,8 @@ -#![type_length_limit = "1152909"] - -mod metrics; +#![type_length_limit = "5000000"] +mod instrumentation; mod node; -mod trace; -#[cfg(feature = "google-pubsub")] -mod google_pubsub; #[cfg(feature = "redis")] mod redis_store; pub use node::*; -#[allow(deprecated)] -#[cfg(feature = "redis")] -pub use redis_store::insert_account_with_redis_store; diff --git a/crates/ilp-node/src/main.rs b/crates/ilp-node/src/main.rs index eb4921fa4..a4b155e80 100644 --- a/crates/ilp-node/src/main.rs +++ b/crates/ilp-node/src/main.rs @@ -1,11 +1,7 @@ -#![type_length_limit = "1152909"] +#![type_length_limit = "5000000"] +mod instrumentation; +pub mod node; -mod metrics; -mod node; -mod trace; - -#[cfg(feature = "google-pubsub")] -mod google_pubsub; #[cfg(feature = "redis")] mod redis_store; @@ -19,12 +15,16 @@ use std::{ io::Read, vec::Vec, }; + +#[cfg(feature = "monitoring")] use tracing_subscriber::{ filter::EnvFilter, fmt::{time::ChronoUtc, Subscriber}, }; -pub fn main() { +#[tokio::main] +async fn main() { + #[cfg(feature = "monitoring")] Subscriber::builder() .with_timer(ChronoUtc::rfc3339()) .with_env_filter(EnvFilter::from_default_env()) @@ -143,7 +143,13 @@ pub fn main() { } let matches = app.clone().get_matches(); merge_args(&mut config, &matches); - config.try_into::().unwrap().run(); + + let node = config.try_into::().unwrap(); + node.serve().await.unwrap(); + + // Add a future which is always pending. This will ensure main does not exist + // TODO: Is there a better way of doing this? + futures::future::pending().await } // returns (subcommand paths, config path) diff --git a/crates/ilp-node/src/metrics.rs b/crates/ilp-node/src/metrics.rs deleted file mode 100644 index 35ba411ab..000000000 --- a/crates/ilp-node/src/metrics.rs +++ /dev/null @@ -1,82 +0,0 @@ -use futures::Future; -use interledger::{ - ccp::CcpRoutingAccount, - packet::{Fulfill, Reject}, - service::{Account, IncomingRequest, IncomingService, OutgoingRequest, OutgoingService}, -}; -use metrics::{self, labels, recorder, Key}; -use std::time::Instant; - -pub fn incoming_metrics( - request: IncomingRequest, - mut next: impl IncomingService, -) -> impl Future { - let labels = labels!( - "from_asset_code" => request.from.asset_code().to_string(), - "from_routing_relation" => request.from.routing_relation().to_string(), - ); - recorder().increment_counter( - Key::from_name_and_labels("requests.incoming.prepare", labels.clone()), - 1, - ); - let start_time = Instant::now(); - - next.handle_request(request).then(move |result| { - if result.is_ok() { - recorder().increment_counter( - Key::from_name_and_labels("requests.incoming.fulfill", labels.clone()), - 1, - ); - } else { - recorder().increment_counter( - Key::from_name_and_labels("requests.incoming.reject", labels.clone()), - 1, - ); - } - recorder().record_histogram( - Key::from_name_and_labels("requests.incoming.duration", labels), - (Instant::now() - start_time).as_nanos() as u64, - ); - result - }) -} - -pub fn outgoing_metrics( - request: OutgoingRequest, - mut next: impl OutgoingService, -) -> impl Future { - let labels = labels!( - "from_asset_code" => request.from.asset_code().to_string(), - "to_asset_code" => request.to.asset_code().to_string(), - "from_routing_relation" => request.from.routing_relation().to_string(), - "to_routing_relation" => request.to.routing_relation().to_string(), - ); - - // TODO replace these calls with the counter! macro if there's a way to easily pass in the already-created labels - // right now if you pass the labels into one of the other macros, it gets a recursion limit error while expanding the macro - recorder().increment_counter( - Key::from_name_and_labels("requests.outgoing.prepare", labels.clone()), - 1, - ); - let start_time = Instant::now(); - - next.send_request(request).then(move |result| { - if result.is_ok() { - recorder().increment_counter( - Key::from_name_and_labels("requests.outgoing.fulfill", labels.clone()), - 1, - ); - } else { - recorder().increment_counter( - Key::from_name_and_labels("requests.outgoing.reject", labels.clone()), - 1, - ); - } - - recorder().record_histogram( - Key::from_name_and_labels("requests.outgoing.duration", labels.clone()), - (Instant::now() - start_time).as_nanos() as u64, - ); - result - }) -} diff --git a/crates/ilp-node/src/node.rs b/crates/ilp-node/src/node.rs index c8275c0aa..4c9f2404b 100644 --- a/crates/ilp-node/src/node.rs +++ b/crates/ilp-node/src/node.rs @@ -1,10 +1,29 @@ -use crate::metrics::{incoming_metrics, outgoing_metrics}; -use crate::trace::{trace_forwarding, trace_incoming, trace_outgoing}; -use bytes::Bytes; -use futures::{ - future::{err, Either}, - Future, +#[cfg(feature = "google-pubsub")] +use crate::instrumentation::google_pubsub::{create_google_pubsub_wrapper, PubsubConfig}; + +#[cfg(feature = "monitoring")] +use tracing_futures::Instrument; + +#[cfg(feature = "monitoring")] +use tracing::debug_span; + +#[cfg(feature = "monitoring")] +use crate::instrumentation::{ + metrics::{incoming_metrics, outgoing_metrics}, + prometheus::{serve_prometheus, PrometheusConfig}, + trace::{trace_forwarding, trace_incoming, trace_outgoing}, }; + +#[cfg(feature = "monitoring")] +use interledger::service::IncomingService; +#[cfg(any(feature = "monitoring", feature = "google-pubsub"))] +use interledger::service::OutgoingService; + +#[cfg(feature = "monitoring")] +use futures::FutureExt; + +use bytes::Bytes; +use futures::TryFutureExt; use hex::FromHex; use interledger::{ api::{NodeApi, NodeStore}, @@ -16,8 +35,7 @@ use interledger::{ packet::{ErrorCode, RejectBuilder}, router::{Router, RouterStore}, service::{ - outgoing_service_fn, Account as AccountTrait, AccountStore, IncomingService, - OutgoingRequest, OutgoingService, Username, + outgoing_service_fn, Account as AccountTrait, AccountStore, OutgoingRequest, Username, }, service_util::{ BalanceStore, EchoService, ExchangeRateFetcher, ExchangeRateService, ExchangeRateStore, @@ -35,25 +53,15 @@ use interledger::{ stream::{StreamNotificationsStore, StreamReceiverService}, }; use lazy_static::lazy_static; -use metrics_core::{Builder, Drain, Observe}; -use metrics_runtime; use num_bigint::BigUint; use serde::{de::Error as DeserializeError, Deserialize, Deserializer}; -use std::sync::Arc; use std::{convert::TryFrom, net::SocketAddr, str, str::FromStr, time::Duration}; use tokio::spawn; -use tracing::{debug, debug_span, error, info}; -use tracing_futures::Instrument; +use tracing::{debug, error, info}; use url::Url; use uuid::Uuid; -use warp::{ - self, - http::{Response, StatusCode}, - Filter, -}; +use warp::{self, Filter}; -#[cfg(feature = "google-pubsub")] -use crate::google_pubsub::{create_google_pubsub_wrapper, PubsubConfig}; #[cfg(feature = "redis")] use crate::redis_store::*; #[cfg(feature = "balance-tracking")] @@ -121,32 +129,6 @@ where } } -/// Configuration for [Prometheus](https://prometheus.io) metrics collection. -#[derive(Deserialize, Clone)] -pub struct PrometheusConfig { - /// IP address and port to host the Prometheus endpoint on. - pub bind_address: SocketAddr, - /// Amount of time, in milliseconds, that the node will collect data points for the - /// Prometheus histograms. Defaults to 300000ms (5 minutes). - #[serde(default = "PrometheusConfig::default_histogram_window")] - pub histogram_window: u64, - /// Granularity, in milliseconds, that the node will use to roll off old data. - /// For example, a value of 1000ms (1 second) would mean that the node forgets the oldest - /// 1 second of histogram data points every second. Defaults to 10000ms (10 seconds). - #[serde(default = "PrometheusConfig::default_histogram_granularity")] - pub histogram_granularity: u64, -} - -impl PrometheusConfig { - fn default_histogram_window() -> u64 { - 300_000 - } - - fn default_histogram_granularity() -> u64 { - 10_000 - } -} - /// Configuration for calculating exchange rates between various pairs. #[derive(Deserialize, Clone, Default)] pub struct ExchangeRateConfig { @@ -226,6 +208,8 @@ pub struct InterledgerNode { pub exchange_rate: ExchangeRateConfig, /// Configuration for [Prometheus](https://prometheus.io) metrics collection. /// If this configuration is not provided, the node will not collect metrics. + /// Needs the feature flag "monitoring" to be enabled + #[cfg(feature = "monitoring")] #[serde(default)] pub prometheus: Option, #[cfg(feature = "google-pubsub")] @@ -239,19 +223,26 @@ impl InterledgerNode { /// also run the Prometheus metrics server on the given address. // TODO when a BTP connection is made, insert a outgoing HTTP entry into the Store to tell other // connector instances to forward packets for that account to us - pub fn serve(self) -> impl Future { - if self.prometheus.is_some() { - Either::A( - self.serve_prometheus() - .join(self.serve_node()) - .and_then(|_| Ok(())), - ) - } else { - Either::B(self.serve_node()) - } + pub async fn serve(self) -> Result<(), ()> { + #[cfg(feature = "monitoring")] + let f = + futures::future::join(serve_prometheus(self.clone()), self.serve_node()).then(|r| { + async move { + if r.0.is_ok() || r.1.is_ok() { + Ok(()) + } else { + Err(()) + } + } + }); + + #[cfg(not(feature = "monitoring"))] + let f = self.serve_node(); + + f.await } - fn serve_node(self) -> Box + Send + 'static> { + async fn serve_node(self) -> Result<(), ()> { let ilp_address = if let Some(address) = &self.ilp_address { address.clone() } else { @@ -266,26 +257,22 @@ impl InterledgerNode { "The string '{}' could not be parsed as a URL: {}", &self.database_url, e ); - return Box::new(err(())); + return Err(()); } }; match database_url.scheme() { #[cfg(feature = "redis")] - "redis" | "redis+unix" => Box::new(serve_redis_node(self, ilp_address)), + "redis" | "redis+unix" => serve_redis_node(self, ilp_address).await, other => { error!("unsupported data source scheme: {}", other); - Box::new(err(())) + Err(()) } } } #[allow(clippy::cognitive_complexity)] - pub(crate) fn chain_services( - self, - store: S, - ilp_address: Address, - ) -> impl Future + pub(crate) async fn chain_services(self, store: S, ilp_address: Address) -> Result<(), ()> where S: NodeStore + BtpStore @@ -327,257 +314,211 @@ impl InterledgerNode { #[cfg(feature = "google-pubsub")] let google_pubsub = self.google_pubsub.clone(); - store.clone().get_btp_outgoing_accounts() - .map_err(|_| error!(target: "interledger-node", "Error getting accounts")) - .and_then(move |btp_accounts| { - let outgoing_service = - outgoing_service_fn(move |request: OutgoingRequest| { - // Don't log anything for failed route updates sent to child accounts - // because there's a good chance they'll be offline - if request.prepare.destination().scheme() != "peer" - || request.to.routing_relation() != RoutingRelation::Child { - error!(target: "interledger-node", "No route found for outgoing request"); - } - Err(RejectBuilder { - code: ErrorCode::F02_UNREACHABLE, - message: &format!( - // TODO we might not want to expose the internal account ID in the error - "No outgoing route for account: {} (ILP address of the Prepare packet: {})", - request.to.id(), - request.prepare.destination(), - ) - .as_bytes(), - triggered_by: Some(&ilp_address_clone), - data: &[], - } - .build()) - }); - - // Connect to all of the accounts that have outgoing ilp_over_btp_urls configured - // but don't fail if we are unable to connect - // TODO try reconnecting to those accounts later - connect_client(ilp_address_clone2.clone(), btp_accounts, false, outgoing_service) - .and_then( - move |btp_client_service| { - let btp_server_service = BtpOutgoingService::new(ilp_address_clone2, btp_client_service.clone()); - let btp_server_service_clone = btp_server_service.clone(); - let btp = btp_client_service.clone(); - - // The BTP service is both an Incoming and Outgoing one so we pass it first as the Outgoing - // service to others like the router and then call handle_incoming on it to set up the incoming handler - let outgoing_service = btp_server_service.clone(); - let outgoing_service = HttpClientService::new( - store.clone(), - outgoing_service, - ); - - let outgoing_service = outgoing_service.wrap(outgoing_metrics); - - // Note: the expiry shortener must come after the Validator so that the expiry duration - // is shortened before we check whether there is enough time left - let outgoing_service = ValidatorService::outgoing( - store.clone(), - outgoing_service - ); - let outgoing_service = - ExpiryShortenerService::new(outgoing_service); - let outgoing_service = StreamReceiverService::new( - secret_seed.clone(), - store.clone(), - outgoing_service, - ); - #[cfg(feature = "balance-tracking")] - let outgoing_service = BalanceService::new( - store.clone(), - outgoing_service, - ); - let outgoing_service = ExchangeRateService::new( - exchange_rate_spread, - store.clone(), - outgoing_service, - ); - - #[cfg(feature = "google-pubsub")] - let outgoing_service = outgoing_service.wrap(create_google_pubsub_wrapper(google_pubsub)); - - // Set up the Router and Routing Manager - let incoming_service = Router::new( - store.clone(), - // Add tracing to add the outgoing request details to the incoming span - outgoing_service.clone().wrap(trace_forwarding), - ); - - // Add tracing to track the outgoing request details - let outgoing_service = outgoing_service.wrap(trace_outgoing).in_current_span(); - - let mut ccp_builder = CcpRouteManagerBuilder::new( - ilp_address.clone(), - store.clone(), - outgoing_service.clone(), - incoming_service, - ); - ccp_builder.ilp_address(ilp_address.clone()); - if let Some(ms) = route_broadcast_interval { - ccp_builder.broadcast_interval(ms); - } - let incoming_service = ccp_builder.to_service(); - let incoming_service = EchoService::new(store.clone(), incoming_service); - let incoming_service = SettlementMessageService::new(incoming_service); - let incoming_service = IldcpService::new(incoming_service); - let incoming_service = - MaxPacketAmountService::new( - store.clone(), - incoming_service - ); - let incoming_service = - ValidatorService::incoming(store.clone(), incoming_service); - let incoming_service = RateLimitService::new( - store.clone(), - incoming_service, - ); - - // Add tracing to track the incoming request details - let incoming_service = incoming_service.wrap(trace_incoming).in_current_span(); - - let incoming_service = incoming_service.wrap(incoming_metrics); - - // Handle incoming packets sent via BTP - btp_server_service.handle_incoming(incoming_service.clone().wrap(|request, mut next| { - let btp = debug_span!(target: "interledger-node", "btp"); - let _btp_scope = btp.enter(); - next.handle_request(request).in_current_span() - }).in_current_span()); - btp_client_service.handle_incoming(incoming_service.clone().wrap(|request, mut next| { - let btp = debug_span!(target: "interledger-node", "btp"); - let _btp_scope = btp.enter(); - next.handle_request(request).in_current_span() - }).in_current_span()); - - // Node HTTP API - let mut api = NodeApi::new( - secret_seed, - admin_auth_token, - store.clone(), - incoming_service.clone().wrap(|request, mut next| { - let api = debug_span!(target: "interledger-node", "api"); - let _api_scope = api.enter(); - next.handle_request(request).in_current_span() - }).in_current_span(), - outgoing_service.clone(), - btp.clone(), - ); - if let Some(username) = default_spsp_account { - api.default_spsp_account(username); - } - api.node_version(env!("CARGO_PKG_VERSION").to_string()); - // add an API of ILP over HTTP and add rejection handler - let api = api.into_warp_filter() - .or(IlpOverHttpServer::new(incoming_service.clone().wrap(|request, mut next| { - let http = debug_span!(target: "interledger-node", "http"); - let _http_scope = http.enter(); - next.handle_request(request).in_current_span() - }).in_current_span(), store.clone()).as_filter()) - .or(btp_service_as_filter(btp_server_service_clone, store.clone())) - .recover(default_rejection_handler) - .with(warp::log("interledger-api")).boxed(); - - info!(target: "interledger-node", "Interledger.rs node HTTP API listening on: {}", http_bind_address); - spawn(warp::serve(api).bind(http_bind_address)); - - // Settlement API - let settlement_api = create_settlements_filter( - store.clone(), - outgoing_service.clone(), - ); - info!(target: "interledger-node", "Settlement API listening on: {}", settlement_api_bind_address); - spawn(warp::serve(settlement_api).bind(settlement_api_bind_address)); - - // Exchange Rate Polling - if let Some(provider) = exchange_rate_provider { - let exchange_rate_fetcher = ExchangeRateFetcher::new(provider, exchange_rate_poll_failure_tolerance, store.clone()); - exchange_rate_fetcher.spawn_interval(Duration::from_millis(exchange_rate_poll_interval)); - } else { - debug!(target: "interledger-node", "Not using exchange rate provider. Rates must be set via the HTTP API"); - } + let btp_accounts = store + .get_btp_outgoing_accounts() + .map_err(|_| error!(target: "interledger-node", "Error getting accounts")) + .await?; + + let outgoing_service = outgoing_service_fn(move |request: OutgoingRequest| { + // Don't log anything for failed route updates sent to child accounts + // because there's a good chance they'll be offline + if request.prepare.destination().scheme() != "peer" + || request.to.routing_relation() != RoutingRelation::Child + { + error!(target: "interledger-node", "No route found for outgoing request"); + } + Err(RejectBuilder { + code: ErrorCode::F02_UNREACHABLE, + message: &format!( + // TODO we might not want to expose the internal account ID in the error + "No outgoing route for account: {} (ILP address of the Prepare packet: {})", + request.to.id(), + request.prepare.destination(), + ) + .as_bytes(), + triggered_by: Some(&ilp_address_clone), + data: &[], + } + .build()) + }); + + // Connect to all of the accounts that have outgoing ilp_over_btp_urls configured + // but don't fail if we are unable to connect + // TODO try reconnecting to those accounts later + let btp_client_service = connect_client( + ilp_address_clone2.clone(), + btp_accounts, + false, + outgoing_service, + ) + .await?; + let btp_server_service = + BtpOutgoingService::new(ilp_address_clone2, btp_client_service.clone()); + let btp_server_service_clone = btp_server_service.clone(); + let btp = btp_client_service.clone(); + + // The BTP service is both an Incoming and Outgoing one so we pass it first as the Outgoing + // service to others like the router and then call handle_incoming on it to set up the incoming handler + let outgoing_service = btp_server_service.clone(); + let outgoing_service = HttpClientService::new(store.clone(), outgoing_service); + + #[cfg(feature = "monitoring")] + let outgoing_service = outgoing_service.wrap(outgoing_metrics); + + // Note: the expiry shortener must come after the Validator so that the expiry duration + // is shortened before we check whether there is enough time left + let outgoing_service = ValidatorService::outgoing(store.clone(), outgoing_service); + let outgoing_service = ExpiryShortenerService::new(outgoing_service); + let outgoing_service = + StreamReceiverService::new(secret_seed.clone(), store.clone(), outgoing_service); + #[cfg(feature = "balance-tracking")] + let outgoing_service = BalanceService::new(store.clone(), outgoing_service); + let outgoing_service = + ExchangeRateService::new(exchange_rate_spread, store.clone(), outgoing_service); - Ok(()) - }, - ) - }) - .in_current_span() - } + #[cfg(feature = "google-pubsub")] + let outgoing_service = outgoing_service.wrap(create_google_pubsub_wrapper(google_pubsub)); + + // Add tracing to add the outgoing request details to the incoming span + #[cfg(feature = "monitoring")] + let outgoing_service_fwd = outgoing_service + .clone() + .wrap(trace_forwarding) + .in_current_span(); + #[cfg(not(feature = "monitoring"))] + let outgoing_service_fwd = outgoing_service.clone(); + + // Set up the Router and Routing Manager + let incoming_service = Router::new(store.clone(), outgoing_service_fwd); + + // Add tracing to track the outgoing request details + #[cfg(feature = "monitoring")] + let outgoing_service = outgoing_service.wrap(trace_outgoing).in_current_span(); + + let mut ccp_builder = CcpRouteManagerBuilder::new( + ilp_address.clone(), + store.clone(), + outgoing_service.clone(), + incoming_service, + ); + ccp_builder.ilp_address(ilp_address.clone()); + if let Some(ms) = route_broadcast_interval { + ccp_builder.broadcast_interval(ms); + } - /// Starts a Prometheus metrics server that will listen on the configured address. - /// - /// # Errors - /// This will fail if another Prometheus server is already running in this - /// process or on the configured port. - #[allow(clippy::cognitive_complexity)] - fn serve_prometheus(&self) -> impl Future { - Box::new(if let Some(ref prometheus) = self.prometheus { - // Set up the metrics collector - let receiver = metrics_runtime::Builder::default() - .histogram( - Duration::from_millis(prometheus.histogram_window), - Duration::from_millis(prometheus.histogram_granularity), - ) - .build() - .expect("Failed to create metrics Receiver"); - let controller = receiver.controller(); - // Try installing the global recorder - match metrics::set_boxed_recorder(Box::new(receiver)) { - Ok(_) => { - let observer = - Arc::new(metrics_runtime::observers::PrometheusBuilder::default()); - - let filter = warp::get2().and(warp::path::end()).map(move || { - let mut observer = observer.build(); - controller.observe(&mut observer); - let prometheus_response = observer.drain(); - Response::builder() - .status(StatusCode::OK) - .header("Content-Type", "text/plain; version=0.0.4") - .body(prometheus_response) - }); - - info!(target: "interledger-node", - "Prometheus metrics server listening on: {}", - prometheus.bind_address - ); - Either::A( - warp::serve(filter) - .bind(prometheus.bind_address) - .map_err(|_| { - error!(target: "interledger-node", "Error binding Prometheus server to the configured address") - }), - ) + let incoming_service = ccp_builder.to_service(); + let incoming_service = EchoService::new(store.clone(), incoming_service); + let incoming_service = SettlementMessageService::new(incoming_service); + let incoming_service = IldcpService::new(incoming_service); + let incoming_service = MaxPacketAmountService::new(store.clone(), incoming_service); + let incoming_service = ValidatorService::incoming(store.clone(), incoming_service); + let incoming_service = RateLimitService::new(store.clone(), incoming_service); + + // Add tracing to track the incoming request details + #[cfg(feature = "monitoring")] + let incoming_service = incoming_service + .wrap(trace_incoming) + .in_current_span() + .wrap(incoming_metrics); + + // Handle incoming packets sent via BTP + #[cfg(feature = "monitoring")] + let incoming_service_btp = incoming_service + .clone() + .wrap(|request, mut next| { + async move { + let btp = debug_span!(target: "interledger-node", "btp"); + let _btp_scope = btp.enter(); + next.handle_request(request).in_current_span().await } - Err(e) => { - error!(target: "interledger-node", "Error installing global metrics recorder (this is likely caused by trying to run two nodes with Prometheus metrics in the same process): {:?}", e); - Either::B(err(())) + }) + .in_current_span(); + #[cfg(not(feature = "monitoring"))] + let incoming_service_btp = incoming_service.clone(); + + btp_server_service + .handle_incoming(incoming_service_btp.clone()) + .await; + + btp_client_service + .handle_incoming(incoming_service_btp) + .await; + + #[cfg(feature = "monitoring")] + let incoming_service_api = incoming_service + .clone() + .wrap(|request, mut next| { + async move { + let api = debug_span!(target: "interledger-node", "api"); + let _api_scope = api.enter(); + next.handle_request(request).in_current_span().await } - } + }) + .in_current_span(); + #[cfg(not(feature = "monitoring"))] + let incoming_service_api = incoming_service.clone(); + + // Node HTTP API + let mut api = NodeApi::new( + bytes05::Bytes::copy_from_slice(secret_seed.as_ref()), + admin_auth_token, + store.clone(), + incoming_service_api.clone(), + outgoing_service.clone(), + btp.clone(), // btp client service! + ); + if let Some(username) = default_spsp_account { + api.default_spsp_account(username); + } + api.node_version(env!("CARGO_PKG_VERSION").to_string()); + + #[cfg(feature = "monitoring")] + let incoming_service_http = incoming_service + .clone() + .wrap(|request, mut next| { + async move { + let http = debug_span!(target: "interledger-node", "http"); + let _http_scope = http.enter(); + next.handle_request(request).in_current_span().await + } + }) + .in_current_span(); + #[cfg(not(feature = "monitoring"))] + let incoming_service_http = incoming_service.clone(); + + // add an API of ILP over HTTP and add rejection handler + let api = api + .into_warp_filter() + .or(IlpOverHttpServer::new(incoming_service_http, store.clone()).as_filter()) + .or(btp_service_as_filter( + btp_server_service_clone, + store.clone(), + )) + .recover(default_rejection_handler) + .with(warp::log("interledger-api")) + .boxed(); + + info!(target: "interledger-node", "Interledger.rs node HTTP API listening on: {}", http_bind_address); + spawn(warp::serve(api).bind(http_bind_address)); + + // Settlement API + let settlement_api = create_settlements_filter(store.clone(), outgoing_service.clone()); + info!(target: "interledger-node", "Settlement API listening on: {}", settlement_api_bind_address); + spawn(warp::serve(settlement_api).bind(settlement_api_bind_address)); + + // Exchange Rate Polling + if let Some(provider) = exchange_rate_provider { + let exchange_rate_fetcher = ExchangeRateFetcher::new( + provider, + exchange_rate_poll_failure_tolerance, + store.clone(), + ); + exchange_rate_fetcher + .spawn_interval(Duration::from_millis(exchange_rate_poll_interval)); } else { - error!(target: "interledger-node", "No prometheus configuration provided"); - Either::B(err(())) - }) - } + debug!(target: "interledger-node", "Not using exchange rate provider. Rates must be set via the HTTP API"); + } - /// Run the node on the default Tokio runtime - pub fn run(self) { - tokio_run(self.serve()); + Ok(()) } } - -#[doc(hidden)] -pub fn tokio_run(fut: impl Future + Send + 'static) { - let mut runtime = tokio::runtime::Builder::new() - // Don't swallow panics - .panic_handler(|err| std::panic::resume_unwind(err)) - .name_prefix("interledger-rs-worker-") - .build() - .expect("failed to start new runtime"); - - runtime.spawn(fut); - runtime.shutdown_on_idle().wait().unwrap(); -} diff --git a/crates/ilp-node/src/redis_store.rs b/crates/ilp-node/src/redis_store.rs index ef9a957c0..7377ab884 100644 --- a/crates/ilp-node/src/redis_store.rs +++ b/crates/ilp-node/src/redis_store.rs @@ -1,7 +1,7 @@ #![cfg(feature = "redis")] use crate::node::InterledgerNode; -use futures::{future::result, Future}; +use futures::TryFutureExt; pub use interledger::{ api::{AccountDetails, NodeStore}, packet::Address, @@ -10,8 +10,7 @@ pub use interledger::{ }; pub use redis_crate::{ConnectionInfo, IntoConnectionInfo}; use ring::hmac; -use tracing::{debug, error}; -use uuid::Uuid; +use tracing::error; static REDIS_SECRET_GENERATION_STRING: &str = "ilp_redis_secret"; @@ -22,18 +21,16 @@ pub fn default_redis_url() -> String { // This function could theoretically be defined as an inherent method on InterledgerNode itself. // However, we define it in this module in order to consolidate conditionally-compiled code // into as few discrete units as possible. -pub fn serve_redis_node( - node: InterledgerNode, - ilp_address: Address, -) -> impl Future { +pub async fn serve_redis_node(node: InterledgerNode, ilp_address: Address) -> Result<(), ()> { let redis_connection_info = node.database_url.clone().into_connection_info().unwrap(); let redis_addr = redis_connection_info.addr.clone(); let redis_secret = generate_redis_secret(&node.secret_seed); - Box::new(RedisStoreBuilder::new(redis_connection_info, redis_secret) - .node_ilp_address(ilp_address.clone()) - .connect() - .map_err(move |err| error!(target: "interledger-node", "Error connecting to Redis: {:?} {:?}", redis_addr, err)) - .and_then(move |store| node.chain_services(store, ilp_address))) + let store = RedisStoreBuilder::new(redis_connection_info, redis_secret) + .node_ilp_address(ilp_address.clone()) + .connect() + .map_err(move |err| error!(target: "interledger-node", "Error connecting to Redis: {:?} {:?}", redis_addr, err)) + .await?; + node.chain_services(store, ilp_address).await } pub fn generate_redis_secret(secret_seed: &[u8; 32]) -> [u8; 32] { @@ -45,28 +42,3 @@ pub fn generate_redis_secret(secret_seed: &[u8; 32]) -> [u8; 32] { redis_secret.copy_from_slice(sig.as_ref()); redis_secret } - -#[doc(hidden)] -#[allow(dead_code)] -#[deprecated(note = "use HTTP API instead")] -pub fn insert_account_with_redis_store( - node: &InterledgerNode, - account: AccountDetails, -) -> impl Future { - let redis_secret = generate_redis_secret(&node.secret_seed); - result(node.database_url.clone().into_connection_info()) - .map_err( - |err| error!(target: "interledger-node", "Invalid Redis connection details: {:?}", err), - ) - .and_then(move |redis_url| RedisStoreBuilder::new(redis_url, redis_secret).connect()) - .map_err(|err| error!(target: "interledger-node", "Error connecting to Redis: {:?}", err)) - .and_then(move |store| { - store - .insert_account(account) - .map_err(|_| error!(target: "interledger-node", "Unable to create account")) - .and_then(|account| { - debug!(target: "interledger-node", "Created account: {}", account.id()); - Ok(account.id()) - }) - }) -} diff --git a/crates/ilp-node/tests/redis/btp.rs b/crates/ilp-node/tests/redis/btp.rs index 2ccbaebe8..fbe98fb8b 100644 --- a/crates/ilp-node/tests/redis/btp.rs +++ b/crates/ilp-node/tests/redis/btp.rs @@ -1,16 +1,14 @@ use crate::redis_helpers::*; use crate::test_helpers::*; -use futures::{future::join_all, Future}; use ilp_node::InterledgerNode; use serde_json::{self, json}; -use tokio::runtime::Builder as RuntimeBuilder; -use tracing::error_span; -use tracing_futures::Instrument; +// use tracing::error_span; +// use tracing_futures::Instrument; -#[test] -fn two_nodes_btp() { +#[tokio::test] +async fn two_nodes_btp() { // Nodes 1 and 2 are peers, Node 2 is the parent of Node 2 - install_tracing_subscriber(); + // install_tracing_subscriber(); let context = TestContext::new(); // Each node will use its own DB within the redis instance @@ -24,11 +22,6 @@ fn two_nodes_btp() { let node_b_http = get_open_port(None); let node_b_settlement = get_open_port(None); - let mut runtime = RuntimeBuilder::new() - .panic_handler(|err| std::panic::resume_unwind(err)) - .build() - .unwrap(); - let alice_on_a = json!({ "username": "alice_on_a", "asset_code": "XYZ", @@ -87,117 +80,82 @@ fn two_nodes_btp() { })) .expect("Error creating node_b."); - // FIXME This should be fixed after SQL store is implemented. - // https://github.com/interledger-rs/interledger-rs/issues/464 - let alice_fut = create_account_on_node(node_a_http, alice_on_a, "admin") - .and_then(move |_| create_account_on_node(node_a_http, b_on_a, "admin")); + node_b.serve().await.unwrap(); + create_account_on_node(node_b_http, a_on_b, "admin") + .await + .unwrap(); + create_account_on_node(node_b_http, bob_on_b, "admin") + .await + .unwrap(); - runtime.spawn( - node_a - .serve() - .instrument(error_span!(target: "interledger", "node_a")), - ); + node_a.serve().await.unwrap(); + create_account_on_node(node_a_http, alice_on_a, "admin") + .await + .unwrap(); + create_account_on_node(node_a_http, b_on_a, "admin") + .await + .unwrap(); - let bob_fut = join_all(vec![ - create_account_on_node(node_b_http, a_on_b, "admin"), - create_account_on_node(node_b_http, bob_on_b, "admin"), - ]); + let get_balances = move || { + futures::future::join_all(vec![ + get_balance("alice_on_a", node_a_http, "admin"), + get_balance("bob_on_b", node_b_http, "admin"), + ]) + }; - runtime.spawn( - node_b - .serve() - .instrument(error_span!(target: "interledger", "node_b")), - ); + send_money_to_username( + node_a_http, + node_b_http, + 1000, + "bob_on_b", + "alice_on_a", + "default account holder", + ) + .await + .unwrap(); - runtime - .block_on( - // Wait for the nodes to spin up - delay(500) - .map_err(|_| panic!("Something strange happened when `delay`")) - .and_then(move |_| { - bob_fut - .and_then(|_| alice_fut) - .and_then(|_| delay(500).map_err(|_| panic!("delay error"))) - }) - .and_then(move |_| { - let send_1_to_2 = send_money_to_username( - node_a_http, - node_b_http, - 1000, - "bob_on_b", - "alice_on_a", - "default account holder", - ); - let send_2_to_1 = send_money_to_username( - node_b_http, - node_a_http, - 2000, - "alice_on_a", - "bob_on_b", - "default account holder", - ); + let ret = get_balances().await; + let ret: Vec<_> = ret.into_iter().map(|r| r.unwrap()).collect(); + assert_eq!( + ret[0], + BalanceData { + asset_code: "XYZ".to_owned(), + balance: -1e-6 + } + ); + assert_eq!( + ret[1], + BalanceData { + asset_code: "XYZ".to_owned(), + balance: 1e-6 + } + ); - let get_balances = move || { - futures::future::join_all(vec![ - get_balance("alice_on_a", node_a_http, "admin"), - get_balance("bob_on_b", node_b_http, "admin"), - ]) - }; + send_money_to_username( + node_b_http, + node_a_http, + 2000, + "alice_on_a", + "bob_on_b", + "default account holder", + ) + .await + .unwrap(); - send_1_to_2 - .map_err(|err| { - eprintln!("Error sending from node 1 to node 2: {:?}", err); - err - }) - .and_then(move |_| { - get_balances().and_then(move |ret| { - assert_eq!( - ret[0], - BalanceData { - asset_code: "XYZ".to_owned(), - balance: -1e-6 - } - ); - assert_eq!( - ret[1], - BalanceData { - asset_code: "XYZ".to_owned(), - balance: 1e-6 - } - ); - Ok(()) - }) - }) - .and_then(move |_| { - send_2_to_1.map_err(|err| { - eprintln!("Error sending from node 2 to node 1: {:?}", err); - err - }) - }) - .and_then(move |_| { - get_balances().and_then(move |ret| { - assert_eq!( - ret[0], - BalanceData { - asset_code: "XYZ".to_owned(), - balance: 1e-6 - } - ); - assert_eq!( - ret[1], - BalanceData { - asset_code: "XYZ".to_owned(), - balance: -1e-6 - } - ); - Ok(()) - }) - }) - }), - ) - .map_err(|err| { - eprintln!("Error executing tests: {:?}", err); - err - }) - .unwrap(); + let ret = get_balances().await; + let ret: Vec<_> = ret.into_iter().map(|r| r.unwrap()).collect(); + assert_eq!( + ret[0], + BalanceData { + asset_code: "XYZ".to_owned(), + balance: 1e-6 + } + ); + assert_eq!( + ret[1], + BalanceData { + asset_code: "XYZ".to_owned(), + balance: -1e-6 + } + ); } diff --git a/crates/ilp-node/tests/redis/exchange_rates.rs b/crates/ilp-node/tests/redis/exchange_rates.rs index 194ceff9a..cd811feb6 100644 --- a/crates/ilp-node/tests/redis/exchange_rates.rs +++ b/crates/ilp-node/tests/redis/exchange_rates.rs @@ -1,26 +1,17 @@ use crate::redis_helpers::*; use crate::test_helpers::*; -use futures::Future; use ilp_node::InterledgerNode; -use reqwest::r#async::Client; +use reqwest::Client; use secrecy::SecretString; use serde_json::{self, json, Value}; use std::env; -use tokio::runtime::Builder as RuntimeBuilder; -use tokio_retry::{strategy::FibonacciBackoff, Retry}; +use std::time::Duration; use tracing::error; -use tracing_subscriber; -#[test] -fn coincap() { - install_tracing_subscriber(); +#[tokio::test] +async fn coincap() { let context = TestContext::new(); - let mut runtime = RuntimeBuilder::new() - .panic_handler(|err| std::panic::resume_unwind(err)) - .build() - .unwrap(); - let http_port = get_open_port(None); let node: InterledgerNode = serde_json::from_value(json!({ @@ -33,57 +24,38 @@ fn coincap() { "secret_seed": random_secret(), "route_broadcast_interval": 200, "exchange_rate": { - "poll_interval": 60000, + "poll_interval": 100, "provider": "coincap", }, })) .unwrap(); - runtime.spawn(node.serve()); + node.serve().await.unwrap(); - let get_rates = move || { - Client::new() - .get(&format!("http://localhost:{}/rates", http_port)) - .send() - .map_err(|_| panic!("Error getting rates")) - .and_then(|mut res| res.json().map_err(|_| panic!("Error getting body"))) - .and_then(|body: Value| { - if let Value::Object(obj) = body { - if obj.is_empty() { - error!("Rates are empty"); - return Err(()); - } - assert_eq!( - format!("{}", obj.get("USD").expect("Should have USD rate")).as_str(), - "1.0" - ); - assert!(obj.contains_key("EUR")); - assert!(obj.contains_key("JPY")); - assert!(obj.contains_key("BTC")); - assert!(obj.contains_key("ETH")); - assert!(obj.contains_key("XRP")); - } else { - panic!("Not an object"); - } + // Wait a few seconds so our node can poll the API + tokio::time::delay_for(Duration::from_millis(1000)).await; - Ok(()) - }) - }; - - runtime - .block_on( - delay(1000) - .map_err(|_| panic!("Something strange happened")) - .and_then(move |_| { - Retry::spawn(FibonacciBackoff::from_millis(1000).take(5), get_rates) - }), - ) + let ret = Client::new() + .get(&format!("http://localhost:{}/rates", http_port)) + .send() + .await .unwrap(); + let txt = ret.text().await.unwrap(); + let obj: Value = serde_json::from_str(&txt).unwrap(); + + assert_eq!( + format!("{}", obj.get("USD").expect("Should have USD rate")).as_str(), + "1.0" + ); + assert!(obj.get("EUR").is_some()); + assert!(obj.get("JPY").is_some()); + assert!(obj.get("BTC").is_some()); + assert!(obj.get("ETH").is_some()); + assert!(obj.get("XRP").is_some()); } // TODO can we disable this with conditional compilation? -#[test] -fn cryptocompare() { - tracing_subscriber::fmt::try_init().unwrap_or(()); +#[tokio::test] +async fn cryptocompare() { let context = TestContext::new(); let api_key = env::var("ILP_TEST_CRYPTOCOMPARE_API_KEY"); @@ -93,11 +65,6 @@ fn cryptocompare() { } let api_key = SecretString::new(api_key.unwrap()); - let mut runtime = RuntimeBuilder::new() - .panic_handler(|err| std::panic::resume_unwind(err)) - .build() - .unwrap(); - let http_port = get_open_port(Some(3011)); let node: InterledgerNode = serde_json::from_value(json!({ @@ -110,7 +77,7 @@ fn cryptocompare() { "secret_seed": random_secret(), "route_broadcast_interval": 200, "exchange_rate": { - "poll_interval": 60000, + "poll_interval": 100, "provider": { "cryptocompare": api_key }, @@ -118,42 +85,24 @@ fn cryptocompare() { }, })) .unwrap(); - runtime.spawn(node.serve()); + node.serve().await.unwrap(); - let get_rates = move || { - Client::new() - .get(&format!("http://localhost:{}/rates", http_port)) - .send() - .map_err(|_| panic!("Error getting rates")) - .and_then(|mut res| res.json().map_err(|_| panic!("Error getting body"))) - .and_then(|body: Value| { - if let Value::Object(obj) = body { - if obj.is_empty() { - error!("Rates are empty"); - return Err(()); - } - assert_eq!( - format!("{}", obj.get("USD").expect("Should have USD rate")).as_str(), - "1.0" - ); - assert!(obj.contains_key("BTC")); - assert!(obj.contains_key("ETH")); - assert!(obj.contains_key("XRP")); - } else { - panic!("Not an object"); - } + // Wait a few seconds so our node can poll the API + tokio::time::delay_for(Duration::from_millis(1000)).await; - Ok(()) - }) - }; - - runtime - .block_on( - delay(1000) - .map_err(|_| panic!("Something strange happened")) - .and_then(move |_| { - Retry::spawn(FibonacciBackoff::from_millis(1000).take(5), get_rates) - }), - ) + let ret = Client::new() + .get(&format!("http://localhost:{}/rates", http_port)) + .send() + .await .unwrap(); + let txt = ret.text().await.unwrap(); + let obj: Value = serde_json::from_str(&txt).unwrap(); + + assert_eq!( + format!("{}", obj.get("USD").expect("Should have USD rate")).as_str(), + "1.0" + ); + assert!(obj.get("BTC").is_some()); + assert!(obj.get("ETH").is_some()); + assert!(obj.get("XRP").is_some()); } diff --git a/crates/ilp-node/tests/redis/prometheus.rs b/crates/ilp-node/tests/redis/prometheus.rs index a9631314b..8ac012e1a 100644 --- a/crates/ilp-node/tests/redis/prometheus.rs +++ b/crates/ilp-node/tests/redis/prometheus.rs @@ -1,15 +1,18 @@ use crate::redis_helpers::*; use crate::test_helpers::*; -use futures::{future::join_all, Future}; +use futures::TryFutureExt; use ilp_node::InterledgerNode; -use reqwest::r#async::Client; +use reqwest::Client; use serde_json::{self, json}; -use tokio::runtime::Builder as RuntimeBuilder; -#[test] -fn prometheus() { +#[tokio::test] +async fn prometheus() { // Nodes 1 and 2 are peers, Node 2 is the parent of Node 2 - install_tracing_subscriber(); + tracing_subscriber::fmt::Subscriber::builder() + .with_timer(tracing_subscriber::fmt::time::ChronoUtc::rfc3339()) + .with_env_filter(tracing_subscriber::filter::EnvFilter::from_default_env()) + .try_init() + .unwrap_or(()); let context = TestContext::new(); // Each node will use its own DB within the redis instance @@ -24,11 +27,6 @@ fn prometheus() { let node_b_settlement = get_open_port(None); let prometheus_port = get_open_port(None); - let mut runtime = RuntimeBuilder::new() - .panic_handler(|err| std::panic::resume_unwind(err)) - .build() - .unwrap(); - let alice_on_a = json!({ "ilp_address": "example.node_a.alice", "username": "alice_on_a", @@ -91,102 +89,72 @@ fn prometheus() { })) .unwrap(); - let alice_fut = join_all(vec![ - create_account_on_node(node_a_http, alice_on_a, "admin"), - create_account_on_node(node_a_http, b_on_a, "admin"), - ]); - - runtime.spawn(node_a.serve()); - - let bob_fut = join_all(vec![ - create_account_on_node(node_b_http, a_on_b, "admin"), - create_account_on_node(node_b_http, bob_on_b, "admin"), - ]); - - runtime.spawn(node_b.serve()); - - runtime - .block_on( - // Wait for the nodes to spin up - delay(500) - .map_err(|_| panic!("Something strange happened")) - .and_then(move |_| { - bob_fut - .and_then(|_| alice_fut) - .and_then(|_| delay(500).map_err(|_| panic!("delay error"))) - }) - .and_then(move |_| { - let send_1_to_2 = send_money_to_username( - node_a_http, - node_b_http, - 1000, - "bob_on_b", - "alice_on_a", - "token", - ); - let send_2_to_1 = send_money_to_username( - node_b_http, - node_a_http, - 2000, - "alice_on_a", - "bob_on_b", - "token", - ); - - let check_metrics = move || { - Client::new() - .get(&format!("http://127.0.0.1:{}", prometheus_port)) - .send() - .map_err(|err| eprintln!("Error getting metrics {:?}", err)) - .and_then(|mut res| { - res.text().map_err(|err| { - eprintln!("Response was not a string: {:?}", err) - }) - }) - }; + node_a.serve().await.unwrap(); + node_b.serve().await.unwrap(); - send_1_to_2 - .map_err(|err| { - eprintln!("Error sending from node 1 to node 2: {:?}", err); - err - }) - .and_then(move |_| { - check_metrics().and_then(move |ret| { - assert!(ret.starts_with("# metrics snapshot")); - assert!(ret.contains("requests_incoming_fulfill")); - assert!(ret.contains("requests_incoming_prepare")); - assert!(ret.contains("requests_incoming_reject")); - assert!(ret.contains("requests_incoming_duration")); - assert!(ret.contains("requests_outgoing_fulfill")); - assert!(ret.contains("requests_outgoing_prepare")); - assert!(ret.contains("requests_outgoing_reject")); - assert!(ret.contains("requests_outgoing_duration")); - // TODO check the specific numbers of packets - Ok(()) - }) - }) - .and_then(move |_| { - send_2_to_1.map_err(|err| { - eprintln!("Error sending from node 2 to node 1: {:?}", err); - err - }) - }) - .and_then(move |_| { - check_metrics().and_then(move |ret| { - assert!(ret.starts_with("# metrics snapshot")); - assert!(ret.contains("requests_incoming_fulfill")); - assert!(ret.contains("requests_incoming_prepare")); - assert!(ret.contains("requests_incoming_reject")); - assert!(ret.contains("requests_incoming_duration")); - assert!(ret.contains("requests_outgoing_fulfill")); - assert!(ret.contains("requests_outgoing_prepare")); - assert!(ret.contains("requests_outgoing_reject")); - assert!(ret.contains("requests_outgoing_duration")); - // TODO check the specific numbers of packets - Ok(()) - }) - }) - }), - ) + create_account_on_node(node_b_http, a_on_b, "admin") + .await .unwrap(); + create_account_on_node(node_b_http, bob_on_b, "admin") + .await + .unwrap(); + create_account_on_node(node_a_http, alice_on_a, "admin") + .await + .unwrap(); + create_account_on_node(node_a_http, b_on_a, "admin") + .await + .unwrap(); + + let check_metrics = move || { + Client::new() + .get(&format!("http://127.0.0.1:{}", prometheus_port)) + .send() + .map_err(|err| eprintln!("Error getting metrics {:?}", err)) + .and_then(|res| { + res.text() + .map_err(|err| eprintln!("Response was not a string: {:?}", err)) + }) + }; + + send_money_to_username( + node_a_http, + node_b_http, + 1000, + "bob_on_b", + "alice_on_a", + "token", + ) + .await + .unwrap(); + let ret = check_metrics().await.unwrap(); + assert!(ret.starts_with("# metrics snapshot")); + assert!(ret.contains("requests_incoming_fulfill")); + assert!(ret.contains("requests_incoming_prepare")); + assert!(ret.contains("requests_incoming_reject")); + assert!(ret.contains("requests_incoming_duration")); + assert!(ret.contains("requests_outgoing_fulfill")); + assert!(ret.contains("requests_outgoing_prepare")); + assert!(ret.contains("requests_outgoing_reject")); + assert!(ret.contains("requests_outgoing_duration")); + + send_money_to_username( + node_b_http, + node_a_http, + 2000, + "alice_on_a", + "bob_on_b", + "token", + ) + .await + .unwrap(); + let ret = check_metrics().await.unwrap(); + assert!(ret.starts_with("# metrics snapshot")); + assert!(ret.contains("requests_incoming_fulfill")); + assert!(ret.contains("requests_incoming_prepare")); + assert!(ret.contains("requests_incoming_reject")); + assert!(ret.contains("requests_incoming_duration")); + assert!(ret.contains("requests_outgoing_fulfill")); + assert!(ret.contains("requests_outgoing_prepare")); + assert!(ret.contains("requests_outgoing_reject")); + assert!(ret.contains("requests_outgoing_duration")); } diff --git a/crates/ilp-node/tests/redis/redis_helpers.rs b/crates/ilp-node/tests/redis/redis_helpers.rs index 0c61f4475..b541650fd 100644 --- a/crates/ilp-node/tests/redis/redis_helpers.rs +++ b/crates/ilp-node/tests/redis/redis_helpers.rs @@ -1,15 +1,14 @@ // Copied from https://github.com/mitsuhiko/redis-rs/blob/9a1777e8a90c82c315a481cdf66beb7d69e681a2/tests/support/mod.rs #![allow(dead_code)] -use futures::Future; +use futures::TryFutureExt; use redis_crate::{self as redis, ConnectionAddr, ConnectionInfo, RedisError}; use std::env; use std::fs; use std::path::PathBuf; use std::process; use std::thread::sleep; -use std::time::{Duration, Instant}; -use tokio::timer::Delay; +use std::time::Duration; #[allow(unused)] pub fn connection_info_to_string(info: ConnectionInfo) -> String { @@ -40,8 +39,8 @@ pub fn get_open_port(try_port: Option) -> u16 { panic!("Cannot find open port!"); } -pub fn delay(ms: u64) -> impl Future { - Delay::new(Instant::now() + Duration::from_millis(ms)).map_err(|err| panic!(err)) +pub async fn delay(ms: u64) { + tokio::time::delay_for(Duration::from_millis(ms)).await; } #[derive(PartialEq)] @@ -190,20 +189,21 @@ impl TestContext { self.client.get_connection().unwrap() } - pub fn async_connection( - &self, - ) -> impl Future { - self.client.get_async_connection() + pub async fn async_connection(&self) -> Result { + self.client + .get_async_connection() + .map_err(|err| panic!(err)) + .await } pub fn stop_server(&mut self) { self.server.stop(); } - pub fn shared_async_connection( + pub async fn shared_async_connection( &self, - ) -> impl Future { - self.client.get_shared_async_connection() + ) -> Result { + self.client.get_multiplexed_tokio_connection().await } } diff --git a/crates/ilp-node/tests/redis/redis_tests.rs b/crates/ilp-node/tests/redis/redis_tests.rs index 0ec67ff68..e1db4d529 100644 --- a/crates/ilp-node/tests/redis/redis_tests.rs +++ b/crates/ilp-node/tests/redis/redis_tests.rs @@ -1,7 +1,11 @@ +#![type_length_limit = "5000000"] mod btp; mod exchange_rates; -mod prometheus; mod three_nodes; +// Only run prometheus tests if the monitoring feature is turned on +#[cfg(feature = "monitoring")] +mod prometheus; + mod redis_helpers; mod test_helpers; diff --git a/crates/ilp-node/tests/redis/test_helpers.rs b/crates/ilp-node/tests/redis/test_helpers.rs index 8f2b0e00a..aea1407c9 100644 --- a/crates/ilp-node/tests/redis/test_helpers.rs +++ b/crates/ilp-node/tests/redis/test_helpers.rs @@ -1,4 +1,4 @@ -use futures::{stream::Stream, Future}; +use futures::TryFutureExt; use hex; use interledger::stream::StreamDelivery; use interledger::{packet::Address, service::Account as AccountTrait, store::account::Account}; @@ -8,17 +8,9 @@ use serde_json::json; use std::collections::HashMap; use std::fmt::{Debug, Display}; use std::str; -use tracing_subscriber; +// use tracing_subscriber; use uuid::Uuid; -pub fn install_tracing_subscriber() { - tracing_subscriber::fmt::Subscriber::builder() - .with_timer(tracing_subscriber::fmt::time::ChronoUtc::rfc3339()) - .with_env_filter(tracing_subscriber::filter::EnvFilter::from_default_env()) - .try_init() - .unwrap_or(()); -} - #[allow(unused)] pub fn random_secret() -> String { let mut bytes: [u8; 32] = [0; 32]; @@ -33,56 +25,58 @@ pub struct BalanceData { } #[allow(unused)] -pub fn create_account_on_node( +pub async fn create_account_on_node( api_port: u16, data: T, auth: &str, -) -> impl Future { - let client = reqwest::r#async::Client::new(); - client +) -> Result { + let client = reqwest::Client::new(); + let res = client .post(&format!("http://localhost:{}/accounts", api_port)) .header("Content-Type", "application/json") .header("Authorization", format!("Bearer {}", auth)) .json(&data) .send() - .and_then(move |res| res.error_for_status()) - .and_then(move |res| res.into_body().concat2()) - .map_err(|err| { - eprintln!("Error creating account on node: {:?}", err); - }) - .and_then(move |chunk| Ok(str::from_utf8(&chunk).unwrap().to_string())) + .map_err(|_| ()) + .await?; + + let res = res.error_for_status().map_err(|_| ())?; + + Ok(res.json::().map_err(|_| ()).await.unwrap()) } #[allow(unused)] -pub fn create_account_on_engine( +pub async fn create_account_on_engine( engine_port: u16, account_id: T, -) -> impl Future { - let client = reqwest::r#async::Client::new(); - client +) -> Result { + let client = reqwest::Client::new(); + let res = client .post(&format!("http://localhost:{}/accounts", engine_port)) .header("Content-Type", "application/json") .json(&json!({ "id": account_id })) .send() - .and_then(move |res| res.error_for_status()) - .and_then(move |res| res.into_body().concat2()) - .map_err(|err| { - eprintln!("Error creating account: {:?}", err); - }) - .and_then(move |chunk| Ok(str::from_utf8(&chunk).unwrap().to_string())) + .map_err(|_| ()) + .await?; + + let res = res.error_for_status().map_err(|_| ())?; + + let data: bytes05::Bytes = res.bytes().map_err(|_| ()).await?; + + Ok(str::from_utf8(&data).unwrap().to_string()) } #[allow(unused)] -pub fn send_money_to_username( +pub async fn send_money_to_username( from_port: u16, to_port: u16, amount: u64, to_username: T, from_username: &str, from_auth: &str, -) -> impl Future { - let client = reqwest::r#async::Client::new(); - client +) -> Result { + let client = reqwest::Client::new(); + let res = client .post(&format!( "http://localhost:{}/accounts/{}/payments", from_port, from_username @@ -93,36 +87,27 @@ pub fn send_money_to_username( "source_amount": amount, })) .send() - .and_then(|res| res.error_for_status()) - .and_then(|res| res.into_body().concat2()) - .map_err(|err| { - eprintln!("Error sending SPSP payment: {:?}", err); - }) - .and_then(move |body| { - let ret: StreamDelivery = serde_json::from_slice(&body).unwrap(); - Ok(ret) - }) + .map_err(|_| ()) + .await?; + + let res = res.error_for_status().map_err(|_| ())?; + Ok(res.json::().await.unwrap()) } #[allow(unused)] -pub fn get_all_accounts( - node_port: u16, - admin_token: &str, -) -> impl Future, Error = ()> { - let client = reqwest::r#async::Client::new(); - client +pub async fn get_all_accounts(node_port: u16, admin_token: &str) -> Result, ()> { + let client = reqwest::Client::new(); + let res = client .get(&format!("http://localhost:{}/accounts", node_port)) .header("Authorization", format!("Bearer {}", admin_token)) .send() - .and_then(|res| res.error_for_status()) - .and_then(|res| res.into_body().concat2()) - .map_err(|err| { - eprintln!("Error getting account data: {:?}", err); - }) - .and_then(move |body| { - let ret: Vec = serde_json::from_slice(&body).unwrap(); - Ok(ret) - }) + .map_err(|_| ()) + .await?; + + let res = res.error_for_status().map_err(|_| ())?; + let body: bytes05::Bytes = res.bytes().map_err(|_| ()).await?; + let ret: Vec = serde_json::from_slice(&body).unwrap(); + Ok(ret) } #[allow(unused)] @@ -135,26 +120,24 @@ pub fn accounts_to_ids(accounts: Vec) -> HashMap { } #[allow(unused)] -pub fn get_balance( +pub async fn get_balance( account_id: T, node_port: u16, admin_token: &str, -) -> impl Future { - let client = reqwest::r#async::Client::new(); - client +) -> Result { + let client = reqwest::Client::new(); + let res = client .get(&format!( "http://localhost:{}/accounts/{}/balance", node_port, account_id )) .header("Authorization", format!("Bearer {}", admin_token)) .send() - .and_then(|res| res.error_for_status()) - .and_then(|res| res.into_body().concat2()) - .map_err(|err| { - eprintln!("Error getting account data: {:?}", err); - }) - .and_then(|body| { - let ret: BalanceData = serde_json::from_slice(&body).unwrap(); - Ok(ret) - }) + .map_err(|_| ()) + .await?; + + let res = res.error_for_status().map_err(|_| ())?; + let body: bytes05::Bytes = res.bytes().map_err(|_| ()).await?; + let ret: BalanceData = serde_json::from_slice(&body).unwrap(); + Ok(ret) } diff --git a/crates/ilp-node/tests/redis/three_nodes.rs b/crates/ilp-node/tests/redis/three_nodes.rs index caf90cb8d..95f3f3d92 100644 --- a/crates/ilp-node/tests/redis/three_nodes.rs +++ b/crates/ilp-node/tests/redis/three_nodes.rs @@ -1,21 +1,13 @@ use crate::redis_helpers::*; use crate::test_helpers::*; -use futures::{future::join_all, stream::*, sync::mpsc, Future}; use ilp_node::InterledgerNode; use interledger::packet::Address; -use interledger::stream::StreamDelivery; use serde_json::json; use std::str::FromStr; -use tokio::runtime::Builder as RuntimeBuilder; -use tracing::{debug, error_span}; -use tracing_futures::Instrument; -const LOG_TARGET: &str = "interledger-tests-three-nodes"; - -#[test] -fn three_nodes() { +#[tokio::test] +async fn three_nodes() { // Nodes 1 and 2 are peers, Node 2 is the parent of Node 3 - install_tracing_subscriber(); let context = TestContext::new(); // Each node will use its own DB within the redis instance @@ -32,12 +24,6 @@ fn three_nodes() { let node2_settlement = get_open_port(None); let node3_http = get_open_port(None); let node3_settlement = get_open_port(None); - - let mut runtime = RuntimeBuilder::new() - .panic_handler(|err| std::panic::resume_unwind(err)) - .build() - .unwrap(); - let alice_on_alice = json!({ "ilp_address": "example.alice", "username": "alice_on_a", @@ -138,213 +124,154 @@ fn three_nodes() { })) .expect("Error creating node3."); - let (finish_sender, finish_receiver) = mpsc::channel(0); + node1.serve().await.unwrap(); + create_account_on_node(node1_http, alice_on_alice, "admin") + .await + .unwrap(); + create_account_on_node(node1_http, bob_on_alice, "admin") + .await + .unwrap(); - let alice_fut = join_all(vec![ - create_account_on_node(node1_http, alice_on_alice, "admin"), - create_account_on_node(node1_http, bob_on_alice, "admin"), - ]); + node2.serve().await.unwrap(); + create_account_on_node(node2_http, alice_on_bob, "admin") + .await + .unwrap(); + create_account_on_node(node2_http, charlie_on_bob, "admin") + .await + .unwrap(); + // Also set exchange rates + let client = reqwest::Client::new(); + client + .put(&format!("http://localhost:{}/rates", node2_http)) + .header("Authorization", "Bearer admin") + .json(&json!({"ABC": 1, "XYZ": 2})) + .send() + .await + .unwrap(); - let mut node1_finish_sender = finish_sender.clone(); - runtime.spawn( - node1 - .serve() - .and_then(move |_| alice_fut) - .and_then(move |_| { - node1_finish_sender - .try_send(1) - .expect("Could not send message from node_1"); - Ok(()) - }) - .instrument(error_span!(target: "interledger", "node1")), - ); + node3.serve().await.unwrap(); + create_account_on_node(node3_http, charlie_on_charlie, "admin") + .await + .unwrap(); + create_account_on_node(node3_http, bob_on_charlie, "admin") + .await + .unwrap(); - let bob_fut = join_all(vec![ - create_account_on_node(node2_http, alice_on_bob, "admin"), - create_account_on_node(node2_http, charlie_on_bob, "admin"), - ]); + delay(1000).await; - let mut node2_finish_sender = finish_sender; - runtime.spawn( - node2 - .serve() - .and_then(move |_| bob_fut) - .and_then(move |_| { - let client = reqwest::r#async::Client::new(); - client - .put(&format!("http://localhost:{}/rates", node2_http)) - .header("Authorization", "Bearer admin") - .json(&json!({"ABC": 1, "XYZ": 2})) - .send() - .map_err(|err| panic!(err)) - .and_then(move |res| { - res.error_for_status() - .expect("Error setting exchange rates"); - node2_finish_sender - .try_send(2) - .expect("Could not send message from node_2"); - Ok(()) - }) - }) - .instrument(error_span!(target: "interledger", "node2")), - ); + let get_balances = move || { + futures::future::join_all(vec![ + get_balance("alice_on_a", node1_http, "admin"), + get_balance("charlie_on_b", node2_http, "admin"), + get_balance("charlie_on_c", node3_http, "admin"), + ]) + }; - // We execute the futures one after the other to avoid race conditions where - // Bob gets added before the node's main account - let charlie_fut = create_account_on_node(node3_http, charlie_on_charlie, "admin") - .and_then(move |_| create_account_on_node(node3_http, bob_on_charlie, "admin")); + // Node 1 sends 1000 to Node 3. However, Node1's scale is 9, + // while Node 3's scale is 6. This means that Node 3 will + // see 1000x less. In addition, the conversion rate is 2:1 + // for 3's asset, so he will receive 2 total. + let receipt = send_money_to_username( + node1_http, + node3_http, + 1000, + "charlie_on_c", + "alice_on_a", + "default account holder", + ) + .await + .unwrap(); - runtime - .block_on( - node3 - .serve() - .and_then(move |_| finish_receiver.collect()) - .and_then(move |messages| { - debug!( - target: LOG_TARGET, - "Received finish messages: {:?}", messages - ); - charlie_fut - }) - .instrument(error_span!(target: "interledger", "node3")) - // we wait some time after the node is up so that we get the - // necessary routes from bob - .and_then(move |_| { - delay(1000).map_err(|_| panic!("Something strange happened when `delay`")) - }) - .and_then(move |_| { - let send_1_to_3 = send_money_to_username( - node1_http, - node3_http, - 1000, - "charlie_on_c", - "alice_on_a", - "default account holder", - ); - let send_3_to_1 = send_money_to_username( - node3_http, - node1_http, - 1000, - "alice_on_a", - "charlie_on_c", - "default account holder", - ); + assert_eq!( + receipt.from, + Address::from_str("example.alice").unwrap(), + "Payment receipt incorrect (1)" + ); + assert!(receipt + .to + .to_string() + .starts_with("example.bob.charlie_on_b.charlie_on_c.")); + assert_eq!(receipt.sent_asset_code, "XYZ"); + assert_eq!(receipt.sent_asset_scale, 9); + assert_eq!(receipt.sent_amount, 1000); + assert_eq!(receipt.delivered_asset_code.unwrap(), "ABC"); + assert_eq!(receipt.delivered_amount, 2); + assert_eq!(receipt.delivered_asset_scale.unwrap(), 6); + let ret = get_balances().await; + let ret: Vec<_> = ret.into_iter().map(|r| r.unwrap()).collect(); + // -1000 divided by asset scale 9 + assert_eq!( + ret[0], + BalanceData { + asset_code: "XYZ".to_owned(), + balance: -1e-6 + } + ); + // 2 divided by asset scale 6 + assert_eq!( + ret[1], + BalanceData { + asset_code: "ABC".to_owned(), + balance: 2e-6 + } + ); + // 2 divided by asset scale 6 + assert_eq!( + ret[2], + BalanceData { + asset_code: "ABC".to_owned(), + balance: 2e-6 + } + ); - let get_balances = move || { - futures::future::join_all(vec![ - get_balance("alice_on_a", node1_http, "admin"), - get_balance("charlie_on_b", node2_http, "admin"), - get_balance("charlie_on_c", node3_http, "admin"), - ]) - }; + // Charlie sends to Alice + let receipt = send_money_to_username( + node3_http, + node1_http, + 1000, + "alice_on_a", + "charlie_on_c", + "default account holder", + ) + .await + .unwrap(); - // Node 1 sends 1000 to Node 3. However, Node1's scale is 9, - // while Node 3's scale is 6. This means that Node 3 will - // see 1000x less. In addition, the conversion rate is 2:1 - // for 3's asset, so he will receive 2 total. - send_1_to_3 - .map_err(|err| { - eprintln!("Error sending from node 1 to node 3: {:?}", err); - err - }) - .and_then(move |receipt: StreamDelivery| { - debug!(target: LOG_TARGET, "send_1_to_3 receipt: {:?}", receipt); - assert_eq!( - receipt.from, - Address::from_str("example.alice").unwrap(), - "Payment receipt incorrect (1)" - ); - assert!(receipt - .to - .to_string() - .starts_with("example.bob.charlie_on_b.charlie_on_c.")); - assert_eq!(receipt.sent_asset_code, "XYZ"); - assert_eq!(receipt.sent_asset_scale, 9); - assert_eq!(receipt.sent_amount, 1000); - assert_eq!(receipt.delivered_asset_code.unwrap(), "ABC"); - assert_eq!(receipt.delivered_amount, 2); - assert_eq!(receipt.delivered_asset_scale.unwrap(), 6); - get_balances().and_then(move |ret| { - // -1000 divided by asset scale 9 - assert_eq!( - ret[0], - BalanceData { - asset_code: "XYZ".to_owned(), - balance: -1e-6 - } - ); - // 2 divided by asset scale 6 - assert_eq!( - ret[1], - BalanceData { - asset_code: "ABC".to_owned(), - balance: 2e-6 - } - ); - // 2 divided by asset scale 6 - assert_eq!( - ret[2], - BalanceData { - asset_code: "ABC".to_owned(), - balance: 2e-6 - } - ); - Ok(()) - }) - }) - .and_then(move |_| { - send_3_to_1.map_err(|err| { - eprintln!("Error sending from node 3 to node 1: {:?}", err); - err - }) - }) - .and_then(move |receipt| { - debug!(target: LOG_TARGET, "send_3_to_1 receipt: {:?}", receipt); - assert_eq!( - receipt.from, - Address::from_str("example.bob.charlie_on_b.charlie_on_c").unwrap(), - "Payment receipt incorrect (2)" - ); - assert!(receipt.to.to_string().starts_with("example.alice")); - assert_eq!(receipt.sent_asset_code, "ABC"); - assert_eq!(receipt.sent_asset_scale, 6); - assert_eq!(receipt.sent_amount, 1000); - assert_eq!(receipt.delivered_asset_code.unwrap(), "XYZ"); - assert_eq!(receipt.delivered_amount, 500_000); - assert_eq!(receipt.delivered_asset_scale.unwrap(), 9); - get_balances().and_then(move |ret| { - // 499,000 divided by asset scale 9 - assert_eq!( - ret[0], - BalanceData { - asset_code: "XYZ".to_owned(), - balance: 499e-6 - } - ); - // -998 divided by asset scale 6 - assert_eq!( - ret[1], - BalanceData { - asset_code: "ABC".to_owned(), - balance: -998e-6 - } - ); - // -998 divided by asset scale 6 - assert_eq!( - ret[2], - BalanceData { - asset_code: "ABC".to_owned(), - balance: -998e-6 - } - ); - Ok(()) - }) - }) - }), - ) - .map_err(|err| { - eprintln!("Error executing tests: {:?}", err); - err - }) - .unwrap(); + assert_eq!( + receipt.from, + Address::from_str("example.bob.charlie_on_b.charlie_on_c").unwrap(), + "Payment receipt incorrect (2)" + ); + assert!(receipt.to.to_string().starts_with("example.alice")); + assert_eq!(receipt.sent_asset_code, "ABC"); + assert_eq!(receipt.sent_asset_scale, 6); + assert_eq!(receipt.sent_amount, 1000); + assert_eq!(receipt.delivered_asset_code.unwrap(), "XYZ"); + assert_eq!(receipt.delivered_amount, 500_000); + assert_eq!(receipt.delivered_asset_scale.unwrap(), 9); + let ret = get_balances().await; + let ret: Vec<_> = ret.into_iter().map(|r| r.unwrap()).collect(); + // 499,000 divided by asset scale 9 + assert_eq!( + ret[0], + BalanceData { + asset_code: "XYZ".to_owned(), + balance: 499e-6 + } + ); + // -998 divided by asset scale 6 + assert_eq!( + ret[1], + BalanceData { + asset_code: "ABC".to_owned(), + balance: -998e-6 + } + ); + // -998 divided by asset scale 6 + assert_eq!( + ret[2], + BalanceData { + asset_code: "ABC".to_owned(), + balance: -998e-6 + } + ); } diff --git a/crates/interledger-api/Cargo.toml b/crates/interledger-api/Cargo.toml index 6d32c8850..d692a50b6 100644 --- a/crates/interledger-api/Cargo.toml +++ b/crates/interledger-api/Cargo.toml @@ -8,10 +8,10 @@ edition = "2018" repository = "https://github.com/interledger-rs/interledger-rs" [dependencies] -bytes = { version = "0.4.12", default-features = false } -futures = { version = "0.1.29", default-features = false } -futures-retry = { version = "0.3.3", default-features = false } -http = { version = "0.1.18", default-features = false } +bytes = { version = "0.5", default-features = false } +futures = { version = "0.3.1", default-features = false } +futures-retry = { version = "0.4", default-features = false } +http = { version = "0.2", default-features = false } interledger-packet = { path = "../interledger-packet", version = "^0.4.0", default-features = false } interledger-http = { path = "../interledger-http", version = "^0.4.0", default-features = false } interledger-ildcp = { path = "../interledger-ildcp", version = "^0.4.0", default-features = false } @@ -26,13 +26,17 @@ interledger-btp = { path = "../interledger-btp", version = "^0.4.0", default-fea log = { version = "0.4.8", default-features = false } serde = { version = "1.0.101", default-features = false, features = ["derive"] } serde_json = { version = "1.0.41", default-features = false } -serde_path_to_error = { version = "0.1", default-features = false } -reqwest = { version = "0.9.22", default-features = false, features = ["default-tls"] } +serde_path_to_error = { version = "0.1.2", default-features = false } +reqwest = { version = "0.10", default-features = false, features = ["default-tls", "json"] } url = { version = "2.1.0", default-features = false, features = ["serde"] } uuid = { version = "0.8.1", default-features = false} -warp = { version = "0.1.20", default-features = false } -secrecy = { version = "0.5.2", default-features = false, features = ["serde"] } +warp = { version = "0.2", default-features = false } +secrecy = { version = "0.6", default-features = false, features = ["serde"] } lazy_static = "1.4.0" +async-trait = "0.1.22" + +[dev-dependencies] +tokio = { version = "0.2.9", features = ["rt-core", "macros"] } [badges] circle-ci = { repository = "interledger-rs/interledger-rs" } diff --git a/crates/interledger-api/src/http_retry.rs b/crates/interledger-api/src/http_retry.rs index 8fb35f03f..83f6be434 100644 --- a/crates/interledger-api/src/http_retry.rs +++ b/crates/interledger-api/src/http_retry.rs @@ -1,9 +1,9 @@ // Adapted from the futures-retry example: https://gitlab.com/mexus/futures-retry/blob/master/examples/tcp-client-complex.rs -use futures::future::Future; +use futures::TryFutureExt; use futures_retry::{ErrorHandler, FutureRetry, RetryPolicy}; use http::StatusCode; use log::trace; -use reqwest::r#async::Client as HttpClient; +use reqwest::Client as HttpClient; use serde_json::json; use std::{default::Default, fmt::Display, time::Duration}; use url::Url; @@ -28,12 +28,13 @@ impl Client { } } - pub fn create_engine_account( + pub async fn create_engine_account( &self, engine_url: Url, id: T, - ) -> impl Future { + ) -> Result { let mut se_url = engine_url.clone(); + let id: String = id.to_string(); se_url .path_segments_mut() .expect("Invalid settlement engine URL") @@ -46,26 +47,24 @@ impl Client { // The actual HTTP request which gets made to the engine let client = self.client.clone(); - let create_settlement_engine_account = move || { - client - .post(se_url.as_ref()) - .json(&json!({"id" : id.to_string()})) - .send() - .and_then(move |response| { - // If the account is not found on the peer's connector, the - // retry logic will not get triggered. When the counterparty - // tries to add the account, they will complete the handshake. - Ok(response.status()) - }) - }; - FutureRetry::new( - create_settlement_engine_account, - IoHandler::new( - self.max_retries, - format!("[Engine: {}, Account: {}]", engine_url, id), - ), + // If the account is not found on the peer's connector, the + // retry logic will not get triggered. When the counterparty + // tries to add the account, they will complete the handshake. + + let msg = format!("[Engine: {}, Account: {}]", engine_url, id); + let res = FutureRetry::new( + move || { + client + .post(se_url.as_ref()) + .json(&json!({ "id": id })) + .send() + .map_ok(move |response| response.status()) + }, + IoHandler::new(self.max_retries, msg), ) + .await?; + Ok(res) } } @@ -111,12 +110,22 @@ where self.max_attempts ); - if e.is_client_error() { - // do not retry 4xx - RetryPolicy::ForwardError(e) - } else if e.is_timeout() || e.is_server_error() { - // Retry timeouts and 5xx every 5 seconds + // TODO: Should we make this policy more sophisticated? + + // Retry timeouts every 5s + if e.is_timeout() { RetryPolicy::WaitRetry(Duration::from_secs(5)) + } else if let Some(status) = e.status() { + if status.is_client_error() { + // do not retry 4xx + RetryPolicy::ForwardError(e) + } else if status.is_server_error() { + // Retry 5xx every 5 seconds + RetryPolicy::WaitRetry(Duration::from_secs(5)) + } else { + // Otherwise just retry every second + RetryPolicy::WaitRetry(Duration::from_secs(1)) + } } else { // Retry other errors slightly more frequently since they may be // related to the engine not having started yet diff --git a/crates/interledger-api/src/lib.rs b/crates/interledger-api/src/lib.rs index 7e85a47af..73fbd2270 100644 --- a/crates/interledger-api/src/lib.rs +++ b/crates/interledger-api/src/lib.rs @@ -1,5 +1,7 @@ +use async_trait::async_trait; use bytes::Bytes; -use futures::Future; +use interledger_btp::{BtpAccount, BtpOutgoingService}; +use interledger_ccp::CcpRoutingAccount; use interledger_http::{HttpAccount, HttpStore}; use interledger_packet::Address; use interledger_router::RouterStore; @@ -7,17 +9,15 @@ use interledger_service::{Account, AddressStore, IncomingService, OutgoingServic use interledger_service_util::{BalanceStore, ExchangeRateStore}; use interledger_settlement::core::types::{SettlementAccount, SettlementStore}; use interledger_stream::StreamNotificationsStore; +use secrecy::SecretString; use serde::{de, Deserialize, Serialize}; use std::{boxed::*, collections::HashMap, fmt::Display, net::SocketAddr, str::FromStr}; +use url::Url; use uuid::Uuid; use warp::{self, Filter}; -mod routes; -use interledger_btp::{BtpAccount, BtpOutgoingService}; -use interledger_ccp::CcpRoutingAccount; -use secrecy::SecretString; -use url::Url; pub(crate) mod http_retry; +mod routes; // This enum and the following functions are used to allow clients to send either // numbers or strings and have them be properly deserialized into the appropriate @@ -71,52 +71,57 @@ where // One argument against doing that is that the NodeStore allows admin-only // modifications to the values, whereas many of the other traits mostly // read from the configured values. +#[async_trait] pub trait NodeStore: AddressStore + Clone + Send + Sync + 'static { type Account: Account; - fn insert_account( - &self, - account: AccountDetails, - ) -> Box + Send>; + /// Inserts an account to the store. Generates a UUID and returns the full Account object. + async fn insert_account(&self, account: AccountDetails) -> Result; - fn delete_account(&self, id: Uuid) -> Box + Send>; + /// Deletes the account corresponding to the provided id and returns it + async fn delete_account(&self, id: Uuid) -> Result; - fn update_account( - &self, - id: Uuid, - account: AccountDetails, - ) -> Box + Send>; + /// Overwrites the account corresponding to the provided id with the provided details + async fn update_account(&self, id: Uuid, account: AccountDetails) -> Result; - fn modify_account_settings( + /// Modifies the account corresponding to the provided id with the provided settings. + /// `modify_account_settings` allows **users** to update their account settings with a set of + /// limited fields of account details. However `update_account` allows **admins** to fully + /// update account settings. + async fn modify_account_settings( &self, id: Uuid, settings: AccountSettings, - ) -> Box + Send>; + ) -> Result; // TODO limit the number of results and page through them - fn get_all_accounts(&self) -> Box, Error = ()> + Send>; + /// Gets all stored accounts + async fn get_all_accounts(&self) -> Result, ()>; - fn set_static_routes(&self, routes: R) -> Box + Send> + /// Sets the static routes for routing + async fn set_static_routes(&self, routes: R) -> Result<(), ()> where - R: IntoIterator; + // The 'async_trait lifetime is used after recommendation here: + // https://github.com/dtolnay/async-trait/issues/8#issuecomment-514812245 + R: IntoIterator + Send + 'async_trait; - fn set_static_route( - &self, - prefix: String, - account_id: Uuid, - ) -> Box + Send>; + /// Sets a single static route + async fn set_static_route(&self, prefix: String, account_id: Uuid) -> Result<(), ()>; - fn set_default_route(&self, account_id: Uuid) -> Box + Send>; + /// Sets the default route ("") to be the provided account id + /// (acts as a catch-all route if all other routes don't match) + async fn set_default_route(&self, account_id: Uuid) -> Result<(), ()>; - fn set_settlement_engines( + /// Sets the default settlement engines to be used for the provided asset codes + async fn set_settlement_engines( &self, - asset_to_url_map: impl IntoIterator, - ) -> Box + Send>; + // The 'async_trait lifetime is used after recommendation here: + // https://github.com/dtolnay/async-trait/issues/8#issuecomment-514812245 + asset_to_url_map: impl IntoIterator + Send + 'async_trait, + ) -> Result<(), ()>; - fn get_asset_settlement_engine( - &self, - asset_code: &str, - ) -> Box, Error = ()> + Send>; + /// Gets the default settlement engine for the provided asset code + async fn get_asset_settlement_engine(&self, asset_code: &str) -> Result, ()>; } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -131,17 +136,27 @@ pub struct ExchangeRates( /// their HTTP/BTP endpoints, since they may change their network configuration. #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct AccountSettings { + /// The account's incoming ILP over HTTP token. pub ilp_over_http_incoming_token: Option, + /// The account's incoming ILP over BTP token. pub ilp_over_btp_incoming_token: Option, + /// The account's outgoing ILP over HTTP token pub ilp_over_http_outgoing_token: Option, + /// The account's outgoing ILP over BTP token. + /// This must match the ILP over BTP incoming token on the peer's node if exchanging + /// packets with that peer. pub ilp_over_btp_outgoing_token: Option, + /// The account's ILP over HTTP URL (this is where packets are sent over HTTP from your node) pub ilp_over_http_url: Option, + /// The account's ILP over BTP URL (this is where packets are sent over WebSockets from your node) pub ilp_over_btp_url: Option, + /// The threshold after which the balance service will trigger a settlement #[serde(default, deserialize_with = "optional_number_or_string")] pub settle_threshold: Option, - // Note that this is intentionally an unsigned integer because users should - // not be able to set the settle_to value to be negative (meaning the node - // would pre-fund with the user) + /// The amount which the balance service will attempt to settle down to. + /// Note that this is intentionally an unsigned integer because users should + /// not be able to set the settle_to value to be negative (meaning the node + /// would pre-fund with the user) #[serde(default, deserialize_with = "optional_number_or_string")] pub settle_to: Option, } @@ -159,45 +174,82 @@ pub struct EncryptedAccountSettings { pub ilp_over_http_url: Option, pub ilp_over_btp_url: Option, #[serde(default, deserialize_with = "optional_number_or_string")] + /// The threshold after which the balance service will trigger a settlement pub settle_threshold: Option, #[serde(default, deserialize_with = "optional_number_or_string")] + /// The amount which the balance service will attempt to settle down to pub settle_to: Option, } /// The Account type for the RedisStore. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AccountDetails { + /// The account's Interledger Protocol address. + /// If none is provided, the node should generate one pub ilp_address: Option
, + /// The account's username pub username: Username, + /// The account's currency pub asset_code: String, #[serde(deserialize_with = "number_or_string")] + /// The account's asset scale pub asset_scale: u8, #[serde(default = "u64::max_value", deserialize_with = "number_or_string")] + /// The max amount per packet which can be routed for this account pub max_packet_amount: u64, + /// The minimum balance this account can have (consider this as a credit/trust limit) #[serde(default, deserialize_with = "optional_number_or_string")] pub min_balance: Option, + /// The account's ILP over HTTP URL (this is where packets are sent over HTTP from your node) pub ilp_over_http_url: Option, + /// The account's API and incoming ILP over HTTP token. + /// This must match the ILP over HTTP outgoing token on the peer's node if receiving + /// packets from that peer + // TODO: The incoming token is used for both ILP over HTTP, and for authorizing actions from the HTTP API. + // Should we add 1 more token, for more granular permissioning? pub ilp_over_http_incoming_token: Option, + /// The account's outgoing ILP over HTTP token + /// This must match the ILP over HTTP incoming token on the peer's node if sending + /// packets to that peer pub ilp_over_http_outgoing_token: Option, + /// The account's ILP over BTP URL (this is where packets are sent over WebSockets from your node) pub ilp_over_btp_url: Option, + /// The account's outgoing ILP over BTP token. + /// This must match the ILP over BTP incoming token on the peer's node if exchanging + /// packets with that peer. pub ilp_over_btp_outgoing_token: Option, + /// The account's incoming ILP over BTP token. + /// This must match the ILP over BTP outgoing token on the peer's node if exchanging + /// packets with that peer. pub ilp_over_btp_incoming_token: Option, + /// The threshold after which the balance service will trigger a settlement #[serde(default, deserialize_with = "optional_number_or_string")] pub settle_threshold: Option, + /// The amount which the balance service will attempt to settle down to #[serde(default, deserialize_with = "optional_number_or_string")] pub settle_to: Option, + /// The routing relation of the account pub routing_relation: Option, + /// The round trip time of the account (should be set depending on how + /// well the network connectivity of the account and the node is) #[serde(default, deserialize_with = "optional_number_or_string")] pub round_trip_time: Option, + /// The maximum amount the account can send per minute #[serde(default, deserialize_with = "optional_number_or_string")] pub amount_per_minute_limit: Option, + /// The limit of packets the account can send per minute #[serde(default, deserialize_with = "optional_number_or_string")] pub packets_per_minute_limit: Option, + /// The account's settlement engine URL. If a global engine url is configured + /// for the account's asset code, that will be used instead (even if the account is + /// configured with a specific one) pub settlement_engine_url: Option, } pub struct NodeApi { store: S, + /// The admin's API token, used to make admin-only changes + // TODO: Make this a SecretString admin_api_token: String, default_spsp_account: Option, incoming_handler: I, @@ -207,6 +259,7 @@ pub struct NodeApi { // The BTP service is included here so that we can add a new client // connection when an account is added with BTP details btp: BtpOutgoingService, + /// Server secret used to instantiate SPSP/Stream connections server_secret: Bytes, node_version: Option, } @@ -253,16 +306,21 @@ where } } + /// Sets the default SPSP account. When SPSP payments are sent to the root domain, + /// the payment pointer is resolved to /.well-known/pay. This value determines + /// which account those payments will be sent to. pub fn default_spsp_account(&mut self, username: Username) -> &mut Self { self.default_spsp_account = Some(username); self } + /// Sets the node version pub fn node_version(&mut self, version: String) -> &mut Self { self.node_version = Some(version); self } + /// Returns a Warp Filter which exposes the accounts and admin APIs pub fn into_warp_filter(self) -> warp::filters::BoxedFilter<(impl warp::Reply,)> { routes::accounts_api( self.server_secret, @@ -281,8 +339,9 @@ where .boxed() } - pub fn bind(self, addr: SocketAddr) -> impl Future { - warp::serve(self.into_warp_filter()).bind(addr) + /// Serves the API at the provided address + pub async fn bind(self, addr: SocketAddr) { + warp::serve(self.into_warp_filter()).bind(addr).await } } diff --git a/crates/interledger-api/src/routes/accounts.rs b/crates/interledger-api/src/routes/accounts.rs index bca63e768..85f5d5361 100644 --- a/crates/interledger-api/src/routes/accounts.rs +++ b/crates/interledger-api/src/routes/accounts.rs @@ -1,9 +1,6 @@ use crate::{http_retry::Client, number_or_string, AccountDetails, AccountSettings, NodeStore}; use bytes::Bytes; -use futures::{ - future::{err, join_all, ok, Either}, - Future, Stream, -}; +use futures::{future::join_all, Future, FutureExt, StreamExt, TryFutureExt}; use interledger_btp::{connect_to_service_account, BtpAccount, BtpOutgoingService}; use interledger_ccp::{CcpRoutingAccount, Mode, RouteControlRequest, RoutingRelation}; use interledger_http::{deserialize_json, error::*, HttpAccount, HttpStore}; @@ -23,7 +20,7 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use std::convert::TryFrom; use uuid::Uuid; -use warp::{self, Filter, Rejection}; +use warp::{self, reply::Json, Filter, Rejection}; pub const BEARER_TOKEN_START: usize = 7; @@ -64,140 +61,111 @@ where + 'static, { // TODO can we make any of the Filters const or put them in lazy_static? + let with_store = warp::any().map(move || store.clone()).boxed(); + let with_incoming_handler = warp::any().map(move || incoming_handler.clone()).boxed(); // Helper filters let admin_auth_header = format!("Bearer {}", admin_api_token); + let admin_auth_header_clone = admin_auth_header.clone(); + let with_admin_auth_header = warp::any().map(move || admin_auth_header.clone()).boxed(); let admin_only = warp::header::("authorization") - .and_then( - move |authorization: SecretString| -> Result<(), Rejection> { + .and_then(move |authorization: SecretString| { + let admin_auth_header = admin_auth_header_clone.clone(); + async move { if authorization.expose_secret() == &admin_auth_header { - Ok(()) + Ok::<(), Rejection>(()) } else { - Err(ApiError::unauthorized().into()) + Err(Rejection::from(ApiError::unauthorized())) } - }, - ) + } + }) // This call makes it so we do not pass on a () value on // success to the next filter, it just gets rid of it .untuple_one() .boxed(); - let with_store = warp::any().map(move || store.clone()).boxed(); - let admin_auth_header = format!("Bearer {}", admin_api_token); - let with_admin_auth_header = warp::any().map(move || admin_auth_header.clone()).boxed(); - let with_incoming_handler = warp::any().map(move || incoming_handler.clone()).boxed(); - // Note that the following path filters should be applied before others - // (such as method and authentication) to avoid triggering unexpected errors for requests - // that do not match this path. - let accounts = warp::path("accounts"); - let accounts_index = accounts.and(warp::path::end()); - // This is required when using `admin_or_authorized_user_only` or `authorized_user_only` filter. - // Sets Username from path into ext for context. - let account_username = accounts - .and(warp::path::param2::()) - .and_then(|username: Username| -> Result<_, Rejection> { - warp::filters::ext::set(username); - Ok(()) - }) - .untuple_one() - .boxed(); - let account_username_to_id = accounts - .and(warp::path::param2::()) + + // Converts an account username to an account id or errors out + let account_username_to_id = warp::path::param::() .and(with_store.clone()) - .and_then(|username: Username, store: S| { - store - .get_account_id_from_username(&username) - .map_err::<_, Rejection>(move |_| { - // TODO differentiate between server error and not found - error!("Error getting account id from username: {}", username); - ApiError::account_not_found().into() - }) + .and_then(move |username: Username, store: S| { + async move { + store + .get_account_id_from_username(&username) + .map_err(|_| { + // TODO differentiate between server error and not found + error!("Error getting account id from username: {}", username); + Rejection::from(ApiError::account_not_found()) + }) + .await + } }) .boxed(); - // Receives parameters which were prepared by `account_username` and - // considers the request is eligible to be processed or not, checking the auth. - // Why we separate `account_username` and this filter is that - // we want to check whether the sender is eligible to access this path but at the same time, - // we don't want to spawn any `Rejection`s at `account_username`. - // At the point of `account_username`, there might be some other - // remaining path filters. So we have to process those first, not to spawn errors of - // unauthorized that the the request actually should not cause. - // This function needs parameters which can be prepared by `account_username`. - let admin_or_authorized_user_only = warp::filters::ext::get::() + let is_authorized_user = move |store: S, path_username: Username, auth_string: SecretString| { + async move { + if auth_string.expose_secret().len() < BEARER_TOKEN_START { + return Err(Rejection::from(ApiError::bad_request())); + } + + // Try getting the account from the store + let authorized_account = store + .get_account_from_http_auth( + &path_username, + &auth_string.expose_secret()[BEARER_TOKEN_START..], + ) + .map_err(|_| Rejection::from(ApiError::unauthorized())) + .await?; + + // Only return the account if the provided username matched the fetched one + // This maybe is redundant? + if &path_username == authorized_account.username() { + Ok(authorized_account) + } else { + Err(ApiError::unauthorized().into()) + } + } + }; + + // Checks if the account is an admin or if they have provided a valid password + let admin_or_authorized_user_only = warp::path::param::() .and(warp::header::("authorization")) .and(with_store.clone()) - .and(with_admin_auth_header.clone()) + .and(with_admin_auth_header) .and_then( - |path_username: Username, - auth_string: SecretString, - store: S, - admin_auth_header: String| { - if auth_string.expose_secret().len() < BEARER_TOKEN_START { - return Either::A(err(ApiError::bad_request().into())); + move |path_username: Username, + auth_string: SecretString, + store: S, + admin_auth_header: String| { + async move { + // If it's an admin, there's no need for more checks + if auth_string.expose_secret() == &admin_auth_header { + let account_id = store + .get_account_id_from_username(&path_username) + .map_err(|_| { + // TODO differentiate between server error and not found + error!("Error getting account id from username: {}", path_username); + Rejection::from(ApiError::account_not_found()) + }) + .await?; + return Ok(account_id); + } + let account = is_authorized_user(store, path_username, auth_string).await?; + Ok::(account.id()) } - Either::B(store.get_account_id_from_username(&path_username).then( - move |account_id: Result| { - if account_id.is_err() { - return Either::A(err::( - ApiError::account_not_found().into(), - )); - } - let account_id = account_id.unwrap(); - if auth_string.expose_secret() == &admin_auth_header { - return Either::A(ok(account_id)); - } - Either::B( - store - .get_account_from_http_auth( - &path_username, - &auth_string.expose_secret()[BEARER_TOKEN_START..], - ) - .then(move |authorized_account: Result| { - if authorized_account.is_err() { - return err(ApiError::unauthorized().into()); - } - let authorized_account = authorized_account.unwrap(); - if &path_username == authorized_account.username() { - ok(authorized_account.id()) - } else { - err(ApiError::unauthorized().into()) - } - }), - ) - }, - )) }, ) .boxed(); - // The same structure as `admin_or_authorized_user_only`. - // This function needs parameters which can be prepared by `account_username`. - let authorized_user_only = warp::filters::ext::get::() + // Checks if the account has provided a valid password (same as admin-or-auth call, minus one call, can we refactor them together?) + let authorized_user_only = warp::path::param::() .and(warp::header::("authorization")) .and(with_store.clone()) .and_then( - |path_username: Username, auth_string: SecretString, store: S| { - if auth_string.expose_secret().len() < BEARER_TOKEN_START { - return Either::A(err(ApiError::bad_request().into())); + move |path_username: Username, auth_string: SecretString, store: S| { + async move { + let account = is_authorized_user(store, path_username, auth_string).await?; + Ok::(account) } - Either::B( - store - .get_account_from_http_auth( - &path_username, - &auth_string.expose_secret()[BEARER_TOKEN_START..], - ) - .then(move |authorized_account: Result| { - if authorized_account.is_err() { - return err::(ApiError::unauthorized().into()); - } - let authorized_account = authorized_account.unwrap(); - if &path_username == authorized_account.username() { - ok(authorized_account) - } else { - err(ApiError::unauthorized().into()) - } - }), - ) }, ) .boxed(); @@ -205,261 +173,283 @@ where // POST /accounts let btp_clone = btp.clone(); let outgoing_handler_clone = outgoing_handler.clone(); - let post_accounts = warp::post2() - .and(accounts_index) + let post_accounts = warp::post() + .and(warp::path("accounts")) + .and(warp::path::end()) .and(admin_only.clone()) - .and(deserialize_json()) + .and(deserialize_json()) // Why does warp::body::json not work? .and(with_store.clone()) .and_then(move |account_details: AccountDetails, store: S| { let store_clone = store.clone(); let handler = outgoing_handler_clone.clone(); let btp = btp_clone.clone(); - store - .insert_account(account_details.clone()) - .map_err(move |_| { - error!("Error inserting account into store: {:?}", account_details); - // TODO need more information - ApiError::internal_server_error().into() - }) - .and_then(move |account| { - connect_to_external_services(handler, account, store_clone, btp) - }) - .and_then(|account: A| Ok(warp::reply::json(&account))) + async move { + let account = store + .insert_account(account_details.clone()) + .map_err(move |_| { + error!("Error inserting account into store: {:?}", account_details); + // TODO need more information + Rejection::from(ApiError::internal_server_error()) + }) + .await?; + + connect_to_external_services(handler, account.clone(), store_clone, btp).await?; + Ok::(warp::reply::json(&account)) + } }) .boxed(); // GET /accounts - let get_accounts = warp::get2() - .and(accounts_index) + let get_accounts = warp::get() + .and(warp::path("accounts")) + .and(warp::path::end()) .and(admin_only.clone()) .and(with_store.clone()) .and_then(|store: S| { - store - .get_all_accounts() - .map_err::<_, Rejection>(|_| ApiError::internal_server_error().into()) - .and_then(|accounts| Ok(warp::reply::json(&accounts))) + async move { + let accounts = store + .get_all_accounts() + .map_err(|_| Rejection::from(ApiError::internal_server_error())) + .await?; + Ok::(warp::reply::json(&accounts)) + } }) .boxed(); // PUT /accounts/:username - let put_account = warp::put2() + let put_account = warp::put() + .and(warp::path("accounts")) .and(account_username_to_id.clone()) .and(warp::path::end()) .and(admin_only.clone()) - .and(deserialize_json()) + .and(deserialize_json()) // warp::body::json() is not able to decode this! .and(with_store.clone()) .and_then(move |id: Uuid, account_details: AccountDetails, store: S| { - let store_clone = store.clone(); - let handler = outgoing_handler.clone(); + let outgoing_handler = outgoing_handler.clone(); let btp = btp.clone(); - store - .update_account(id, account_details) - .map_err::<_, Rejection>(move |_| ApiError::internal_server_error().into()) - .and_then(move |account| { - connect_to_external_services(handler, account, store_clone, btp) - }) - .and_then(|account: A| Ok(warp::reply::json(&account))) + async move { + let account = store + .update_account(id, account_details) + .map_err(|_| Rejection::from(ApiError::internal_server_error())) + .await?; + connect_to_external_services(outgoing_handler, account.clone(), store, btp).await?; + + Ok::(warp::reply::json(&account)) + } }) .boxed(); // GET /accounts/:username - let get_account = warp::get2() - .and(account_username.clone()) - .and(warp::path::end()) + let get_account = warp::get() + .and(warp::path("accounts")) + // takes the username and the authorization header and checks if it's authorized, returns the uid .and(admin_or_authorized_user_only.clone()) + .and(warp::path::end()) .and(with_store.clone()) .and_then(|id: Uuid, store: S| { - store - .get_accounts(vec![id]) - .map_err::<_, Rejection>(|_| ApiError::account_not_found().into()) - .and_then(|accounts| Ok(warp::reply::json(&accounts[0]))) + async move { + let accounts = store + .get_accounts(vec![id]) + .map_err(|_| Rejection::from(ApiError::account_not_found())) + .await?; + + Ok::(warp::reply::json(&accounts[0])) + } }) .boxed(); // GET /accounts/:username/balance - let get_account_balance = warp::get2() - .and(account_username.clone()) + let get_account_balance = warp::get() + .and(warp::path("accounts")) + // takes the username and the authorization header and checks if it's authorized, returns the uid + .and(admin_or_authorized_user_only.clone()) .and(warp::path("balance")) .and(warp::path::end()) - .and(admin_or_authorized_user_only.clone()) .and(with_store.clone()) .and_then(|id: Uuid, store: S| { - // TODO reduce the number of store calls it takes to get the balance - store - .get_accounts(vec![id]) - .map_err(|_| warp::reject::not_found()) - .and_then(move |mut accounts| { - let account = accounts.pop().unwrap(); - let acc_clone = account.clone(); - let asset_scale = acc_clone.asset_scale(); - let asset_code = acc_clone.asset_code().to_owned(); - store - .get_balance(account) - .map_err(move |_| { - error!("Error getting balance for account: {}", id); - ApiError::internal_server_error().into() - }) - .and_then(move |balance: i64| { - Ok(warp::reply::json(&json!({ - // normalize to the base unit - "balance": balance as f64 / 10_u64.pow(asset_scale.into()) as f64, - "asset_code": asset_code, - }))) - }) - }) + async move { + // TODO reduce the number of store calls it takes to get the balance + let mut accounts = store + .get_accounts(vec![id]) + .map_err(|_| warp::reject::not_found()) + .await?; + let account = accounts.pop().unwrap(); + + let balance = store + .get_balance(account.clone()) + .map_err(move |_| { + error!("Error getting balance for account: {}", id); + Rejection::from(ApiError::internal_server_error()) + }) + .await?; + + let asset_scale = account.asset_scale(); + let asset_code = account.asset_code().to_owned(); + Ok::(warp::reply::json(&json!({ + // normalize to the base unit + "balance": balance as f64 / 10_u64.pow(asset_scale.into()) as f64, + "asset_code": asset_code, + }))) + } }) .boxed(); // DELETE /accounts/:username - let delete_account = warp::delete2() + let delete_account = warp::delete() + .and(warp::path("accounts")) .and(account_username_to_id.clone()) .and(warp::path::end()) - .and(admin_only.clone()) + .and(admin_only) .and(with_store.clone()) .and_then(|id: Uuid, store: S| { - store - .delete_account(id) - .map_err::<_, Rejection>(move |_| { - error!("Error deleting account {}", id); - ApiError::internal_server_error().into() - }) - .and_then(|account| Ok(warp::reply::json(&account))) + async move { + let account = store + .delete_account(id) + .map_err(|_| { + error!("Error deleting account {}", id); + Rejection::from(ApiError::internal_server_error()) + }) + .await?; + Ok::(warp::reply::json(&account)) + } }) .boxed(); // PUT /accounts/:username/settings - let put_account_settings = warp::put2() - .and(account_username.clone()) + let put_account_settings = warp::put() + .and(warp::path("accounts")) + .and(admin_or_authorized_user_only.clone()) .and(warp::path("settings")) .and(warp::path::end()) - .and(admin_or_authorized_user_only.clone()) .and(deserialize_json()) .and(with_store.clone()) .and_then(|id: Uuid, settings: AccountSettings, store: S| { - store - .modify_account_settings(id, settings) - .map_err::<_, Rejection>(move |_| { - error!("Error updating account settings {}", id); - ApiError::internal_server_error().into() - }) - .and_then(|settings| Ok(warp::reply::json(&settings))) + async move { + let modified_account = store + .modify_account_settings(id, settings) + .map_err(move |_| { + error!("Error updating account settings {}", id); + Rejection::from(ApiError::internal_server_error()) + }) + .await?; + Ok::(warp::reply::json(&modified_account)) + } }) .boxed(); // (Websocket) /accounts/:username/payments/incoming - let incoming_payment_notifications = account_username - .clone() + let incoming_payment_notifications = warp::path("accounts") + .and(admin_or_authorized_user_only) .and(warp::path("payments")) .and(warp::path("incoming")) .and(warp::path::end()) - .and(admin_or_authorized_user_only.clone()) - .and(warp::ws2()) + .and(warp::ws()) .and(with_store.clone()) - .map(|id: Uuid, ws: warp::ws::Ws2, store: S| { + .map(|id: Uuid, ws: warp::ws::Ws, store: S| { ws.on_upgrade(move |ws: warp::ws::WebSocket| { - let (tx, rx) = futures::sync::mpsc::unbounded::(); - store.add_payment_notification_subscription(id, tx); - rx.map_err(|_| -> warp::Error { unreachable!("unbounded rx never errors") }) - .map(|notification| { - warp::ws::Message::text(serde_json::to_string(¬ification).unwrap()) - }) - .forward(ws) - .map(|_| ()) - .map_err(|err| error!("Error forwarding notifications to websocket: {:?}", err)) + notify_user(ws, id, store).map(|result| result.unwrap()) }) }) .boxed(); // POST /accounts/:username/payments - let post_payments = warp::post2() - .and(account_username.clone()) + let post_payments = warp::post() + .and(warp::path("accounts")) + .and(authorized_user_only) .and(warp::path("payments")) .and(warp::path::end()) - .and(authorized_user_only.clone()) .and(deserialize_json()) - .and(with_incoming_handler.clone()) + .and(with_incoming_handler) .and_then( move |account: A, pay_request: SpspPayRequest, incoming_handler: I| { - pay( - incoming_handler, - account.clone(), - &pay_request.receiver, - pay_request.source_amount, - ) - .and_then(move |receipt| { + async move { + let receipt = pay( + incoming_handler, + account.clone(), + &pay_request.receiver, + pay_request.source_amount, + ) + .map_err(|err| { + error!("Error sending SPSP payment: {:?}", err); + // TODO give a different error message depending on what type of error it is + Rejection::from(ApiError::internal_server_error()) + }) + .await?; + debug!("Sent SPSP payment, receipt: {:?}", receipt); - Ok(warp::reply::json(&json!(receipt))) - }) - .map_err::<_, Rejection>(|err| { - error!("Error sending SPSP payment: {:?}", err); - // TODO give a different error message depending on what type of error it is - ApiError::internal_server_error().into() - }) + Ok::(warp::reply::json(&json!(receipt))) + } }, ) .boxed(); // GET /accounts/:username/spsp let server_secret_clone = server_secret.clone(); - let get_spsp = warp::get2() - .and(account_username_to_id.clone()) + let get_spsp = warp::get() + .and(warp::path("accounts")) + .and(account_username_to_id) .and(warp::path("spsp")) .and(warp::path::end()) .and(with_store.clone()) .and_then(move |id: Uuid, store: S| { let server_secret_clone = server_secret_clone.clone(); - store - .get_accounts(vec![id]) - .map_err::<_, Rejection>(|_| ApiError::internal_server_error().into()) - .and_then(move |accounts| { - // TODO return the response without instantiating an SpspResponder (use a simple fn) - Ok(SpspResponder::new( + async move { + let accounts = store + .get_accounts(vec![id]) + .map_err(|_| Rejection::from(ApiError::internal_server_error())) + .await?; + // TODO return the response without instantiating an SpspResponder (use a simple fn) + Ok::<_, Rejection>( + SpspResponder::new( accounts[0].ilp_address().clone(), server_secret_clone.clone(), ) - .generate_http_response()) - }) + .generate_http_response(), + ) + } }) .boxed(); // GET /.well-known/pay // This is the endpoint a [Payment Pointer](https://github.com/interledger/rfcs/blob/master/0026-payment-pointers/0026-payment-pointers.md) // with no path resolves to - let server_secret_clone = server_secret.clone(); - let get_spsp_well_known = warp::get2() + let get_spsp_well_known = warp::get() .and(warp::path(".well-known")) .and(warp::path("pay")) .and(warp::path::end()) - .and(with_store.clone()) + .and(with_store) .and_then(move |store: S| { - // TODO don't clone this - if let Some(username) = default_spsp_account.clone() { - let server_secret_clone = server_secret_clone.clone(); - Either::A( - store + let default_spsp_account = default_spsp_account.clone(); + let server_secret_clone = server_secret.clone(); + async move { + // TODO don't clone this + if let Some(username) = default_spsp_account.clone() { + let id = store .get_account_id_from_username(&username) - .map_err(move |_| { + .map_err(|_| { error!("Account not found: {}", username); warp::reject::not_found() }) - .and_then(move |id| { - // TODO this shouldn't take multiple store calls - store - .get_accounts(vec![id]) - .map_err(|_| ApiError::internal_server_error().into()) - .map(|mut accounts| accounts.pop().unwrap()) - }) - .and_then(move |account| { - // TODO return the response without instantiating an SpspResponder (use a simple fn) - Ok(SpspResponder::new( - account.ilp_address().clone(), - server_secret_clone.clone(), - ) - .generate_http_response()) - }), - ) - } else { - Either::B(err(ApiError::not_found().into())) + .await?; + + // TODO this shouldn't take multiple store calls + let mut accounts = store + .get_accounts(vec![id]) + .map_err(|_| Rejection::from(ApiError::internal_server_error())) + .await?; + + let account = accounts.pop().unwrap(); + // TODO return the response without instantiating an SpspResponder (use a simple fn) + Ok::<_, Rejection>( + SpspResponder::new( + account.ilp_address().clone(), + server_secret_clone.clone(), + ) + .generate_http_response(), + ) + } else { + Err(Rejection::from(ApiError::not_found())) + } } }) .boxed(); @@ -469,20 +459,46 @@ where .or(post_accounts) .or(get_accounts) .or(put_account) + .or(delete_account) .or(get_account) .or(get_account_balance) - .or(delete_account) .or(put_account_settings) .or(incoming_payment_notifications) .or(post_payments) .boxed() } -fn get_address_from_parent_and_update_routes( +fn notify_user( + socket: warp::ws::WebSocket, + id: Uuid, + store: impl StreamNotificationsStore, +) -> impl Future> { + let (tx, rx) = futures::channel::mpsc::unbounded::(); + // the client is now subscribed + store.add_payment_notification_subscription(id, tx); + + // Anytime something is written to tx, it will reach rx + // and get converted to a warp::ws::Message + let rx = rx.map(|notification: PaymentNotification| { + let msg = warp::ws::Message::text(serde_json::to_string(¬ification).unwrap()); + Ok(msg) + }); + + // Then it gets forwarded to the client + rx.forward(socket) + .map(|result| { + if let Err(e) = result { + eprintln!("websocket send error: {}", e); + } + }) + .then(futures::future::ok) +} + +async fn get_address_from_parent_and_update_routes( mut service: O, parent: A, store: S, -) -> impl Future +) -> Result<(), ()> where O: OutgoingService + Clone + Send + Sync + 'static, A: CcpRoutingAccount + Clone + Send + Sync + 'static, @@ -494,7 +510,7 @@ where parent.id() ); let prepare = IldcpRequest {}.to_prepare(); - service + let fulfill = service .send_request(OutgoingRequest { from: parent.clone(), // Does not matter what we put here, they will get the account from the HTTP/BTP credentials to: parent.clone(), @@ -502,54 +518,53 @@ where original_amount: 0, }) .map_err(|err| error!("Error getting ILDCP info: {:?}", err)) - .and_then(|fulfill| { - let response = IldcpResponse::try_from(fulfill.into_data().freeze()).map_err(|err| { - error!( - "Unable to parse ILDCP response from fulfill packet: {:?}", - err - ); - }); - debug!("Got ILDCP response from parent: {:?}", response); - let ilp_address = match response { - Ok(info) => info.ilp_address(), - Err(_) => return err(()), - }; - ok(ilp_address) - }) - .and_then(move |ilp_address| { - debug!("ILP address is now: {}", ilp_address); - // TODO we may want to make this trigger the CcpRouteManager to request - let prepare = RouteControlRequest { - mode: Mode::Sync, - last_known_epoch: 0, - last_known_routing_table_id: [0; 16], - features: Vec::new(), - } - .to_prepare(); - debug!("Asking for routes from {:?}", parent.clone()); - join_all(vec![ - // Set the parent to be the default route for everything - // that starts with their global prefix - store.set_default_route(parent.id()), - // Update our store's address - store.set_ilp_address(ilp_address), - // Get the parent's routes for us - Box::new( - service - .send_request(OutgoingRequest { - from: parent.clone(), - to: parent.clone(), - original_amount: prepare.amount(), - prepare: prepare.clone(), - }) - .and_then(move |_| Ok(())) - .map_err(move |err| { - error!("Got error when trying to update routes {:?}", err) - }), - ), - ]) - }) - .and_then(move |_| Ok(())) + .await?; + + let info = IldcpResponse::try_from(fulfill.into_data().freeze()).map_err(|err| { + error!( + "Unable to parse ILDCP response from fulfill packet: {:?}", + err + ); + })?; + debug!("Got ILDCP response from parent: {:?}", info); + let ilp_address = info.ilp_address(); + + debug!("ILP address is now: {}", ilp_address); + // TODO we may want to make this trigger the CcpRouteManager to request + let prepare = RouteControlRequest { + mode: Mode::Sync, + last_known_epoch: 0, + last_known_routing_table_id: [0; 16], + features: Vec::new(), + } + .to_prepare(); + + debug!("Asking for routes from {:?}", parent.clone()); + let ret = join_all(vec![ + // Set the parent to be the default route for everything + // that starts with their global prefix + store.set_default_route(parent.id()), + // Update our store's address + store.set_ilp_address(ilp_address), + // Get the parent's routes for us + Box::pin( + service + .send_request(OutgoingRequest { + from: parent.clone(), + to: parent.clone(), + original_amount: prepare.amount(), + prepare: prepare.clone(), + }) + .map_err(|_| ()) + .map_ok(|_| ()), + ), + ]) + .await; + // If any of the 3 futures errored, propagate the error outside + if ret.into_iter().any(|r| r.is_err()) { + return Err(()); + } + Ok(()) } // Helper function which gets called whenever a new account is added or @@ -562,12 +577,12 @@ where // 2b. Perform a RouteControl Request to make them send us any new routes // 3. If they have a settlement engine endpoitn configured: Make a POST to the // engine's account creation endpoint with the account's id -fn connect_to_external_services( +async fn connect_to_external_services( service: O, account: A, store: S, btp: BtpOutgoingService, -) -> impl Future +) -> Result where O: OutgoingService + Clone + Send + Sync + 'static, A: CcpRoutingAccount + BtpAccount + SettlementAccount + Clone + Send + Sync + 'static, @@ -576,158 +591,136 @@ where { // Try to connect to the account's BTP socket if they have // one configured - let btp_connect_fut = if account.get_ilp_over_btp_url().is_some() { + if account.get_ilp_over_btp_url().is_some() { trace!("Newly inserted account has a BTP URL configured, will try to connect"); - Either::A( - connect_to_service_account(account.clone(), true, btp) - .map_err(|_| ApiError::internal_server_error().into()), - ) - } else { - Either::B(ok(())) - }; + connect_to_service_account(account.clone(), true, btp) + .map_err(|_| Rejection::from(ApiError::internal_server_error())) + .await? + } + + // If we added a parent, get the address assigned to us by + // them and update all of our routes + if account.routing_relation() == RoutingRelation::Parent { + get_address_from_parent_and_update_routes(service, account.clone(), store.clone()) + .map_err(|_| Rejection::from(ApiError::internal_server_error())) + .await?; + } + + // Register the account with the settlement engine + // if a settlement_engine_url was configured on the account + // or if there is a settlement engine configured for that + // account's asset_code + let default_settlement_engine = store + .get_asset_settlement_engine(account.asset_code()) + .map_err(|_| Rejection::from(ApiError::internal_server_error())) + .await?; + + let settlement_engine_url = account + .settlement_engine_details() + .map(|details| details.url) + .or(default_settlement_engine); + if let Some(se_url) = settlement_engine_url { + let id = account.id(); + let http_client = Client::default(); + trace!( + "Sending account {} creation request to settlement engine: {:?}", + id, + se_url.clone() + ); - btp_connect_fut.and_then(move |_| { - // If we added a parent, get the address assigned to us by - // them and update all of our routes - let get_ilp_address_fut = if account.routing_relation() == RoutingRelation::Parent { - Either::A( - get_address_from_parent_and_update_routes(service, account.clone(), store.clone()) - .map_err(|_| ApiError::internal_server_error().into()) - ) + let status_code = http_client + .create_engine_account(se_url, id) + .map_err(|_| Rejection::from(ApiError::internal_server_error())) + .await?; + + if status_code.is_success() { + trace!("Account {} created on the SE", id); } else { - Either::B(ok(())) - }; - - let default_settlement_engine_fut = store.get_asset_settlement_engine(account.asset_code()) - .map_err(|_| ApiError::internal_server_error().into()); - - // Register the account with the settlement engine - // if a settlement_engine_url was configured on the account - // or if there is a settlement engine configured for that - // account's asset_code - default_settlement_engine_fut.join(get_ilp_address_fut).and_then(move |(default_settlement_engine, _)| { - let settlement_engine_url = account.settlement_engine_details().map(|details| details.url).or(default_settlement_engine); - if let Some(se_url) = settlement_engine_url { - let id = account.id(); - let http_client = Client::default(); - trace!( - "Sending account {} creation request to settlement engine: {:?}", - id, - se_url.clone() - ); - Either::A( - http_client.create_engine_account(se_url, id) - .map_err(|_| ApiError::internal_server_error().into()) - .and_then(move |status_code| { - if status_code.is_success() { - trace!("Account {} created on the SE", id); - } else { - error!("Error creating account. Settlement engine responded with HTTP code: {}", status_code); - } - Ok(()) - }) - .and_then(move |_| { - Ok(account) - })) - } else { - Either::B(ok(account)) - } - }) - }) + error!( + "Error creating account. Settlement engine responded with HTTP code: {}", + status_code + ); + } + } + + Ok(account) } #[cfg(test)] mod tests { use crate::routes::test_helpers::*; + // TODO: Add test for GET /accounts/:username/spsp and /.well_known - #[test] - fn only_admin_can_create_account() { + #[tokio::test] + async fn only_admin_can_create_account() { let api = test_accounts_api(); - let resp = api_call(&api, "POST", "/accounts", "admin", DETAILS.clone()); + let resp = api_call(&api, "POST", "/accounts", "admin", DETAILS.clone()).await; assert_eq!(resp.status().as_u16(), 200); - let resp = api_call(&api, "POST", "/accounts", "wrong", DETAILS.clone()); + let resp = api_call(&api, "POST", "/accounts", "wrong", DETAILS.clone()).await; assert_eq!(resp.status().as_u16(), 401); } - #[test] - fn only_admin_can_delete_account() { + #[tokio::test] + async fn only_admin_can_delete_account() { let api = test_accounts_api(); - let resp = api_call(&api, "DELETE", "/accounts/alice", "admin", DETAILS.clone()); + let resp = api_call(&api, "DELETE", "/accounts/alice", "admin", DETAILS.clone()).await; assert_eq!(resp.status().as_u16(), 200); - let resp = api_call(&api, "DELETE", "/accounts/alice", "wrong", DETAILS.clone()); + let resp = api_call(&api, "DELETE", "/accounts/alice", "wrong", DETAILS.clone()).await; assert_eq!(resp.status().as_u16(), 401); } - #[test] - fn only_admin_can_modify_whole_account() { + #[tokio::test] + async fn only_admin_can_modify_whole_account() { let api = test_accounts_api(); - let resp = api_call(&api, "PUT", "/accounts/alice", "admin", DETAILS.clone()); + let resp = api_call(&api, "PUT", "/accounts/alice", "admin", DETAILS.clone()).await; assert_eq!(resp.status().as_u16(), 200); - let resp = api_call(&api, "PUT", "/accounts/alice", "wrong", DETAILS.clone()); + let resp = api_call(&api, "PUT", "/accounts/alice", "wrong", DETAILS.clone()).await; assert_eq!(resp.status().as_u16(), 401); } - #[test] - fn only_admin_can_get_all_accounts() { + #[tokio::test] + async fn only_admin_can_get_all_accounts() { let api = test_accounts_api(); - let resp = api_call(&api, "GET", "/accounts", "admin", DETAILS.clone()); + let resp = api_call(&api, "GET", "/accounts", "admin", None).await; assert_eq!(resp.status().as_u16(), 200); - let resp = api_call(&api, "GET", "/accounts", "wrong", DETAILS.clone()); + let resp = api_call(&api, "GET", "/accounts", "wrong", None).await; assert_eq!(resp.status().as_u16(), 401); } - #[test] - fn only_admin_or_user_can_get_account() { + #[tokio::test] + async fn only_admin_or_user_can_get_account() { let api = test_accounts_api(); - let resp = api_call(&api, "GET", "/accounts/alice", "admin", DETAILS.clone()); + let resp = api_call(&api, "GET", "/accounts/alice", "admin", DETAILS.clone()).await; assert_eq!(resp.status().as_u16(), 200); // TODO: Make this not require the username in the token - let resp = api_call(&api, "GET", "/accounts/alice", "password", DETAILS.clone()); + let resp = api_call(&api, "GET", "/accounts/alice", "password", DETAILS.clone()).await; assert_eq!(resp.status().as_u16(), 200); - let resp = api_call(&api, "GET", "/accounts/alice", "wrong", DETAILS.clone()); + let resp = api_call(&api, "GET", "/accounts/alice", "wrong", DETAILS.clone()).await; assert_eq!(resp.status().as_u16(), 401); } - #[test] - fn only_admin_or_user_can_get_accounts_balance() { + #[tokio::test] + async fn only_admin_or_user_can_get_accounts_balance() { let api = test_accounts_api(); - let resp = api_call( - &api, - "GET", - "/accounts/alice/balance", - "admin", - DETAILS.clone(), - ); + let resp = api_call(&api, "GET", "/accounts/alice/balance", "admin", None).await; assert_eq!(resp.status().as_u16(), 200); // TODO: Make this not require the username in the token - let resp = api_call( - &api, - "GET", - "/accounts/alice/balance", - "password", - DETAILS.clone(), - ); + let resp = api_call(&api, "GET", "/accounts/alice/balance", "password", None).await; assert_eq!(resp.status().as_u16(), 200); - let resp = api_call( - &api, - "GET", - "/accounts/alice/balance", - "wrong", - DETAILS.clone(), - ); + let resp = api_call(&api, "GET", "/accounts/alice/balance", "wrong", None).await; assert_eq!(resp.status().as_u16(), 401); } - #[test] - fn only_admin_or_user_can_modify_accounts_settings() { + #[tokio::test] + async fn only_admin_or_user_can_modify_accounts_settings() { let api = test_accounts_api(); let resp = api_call( &api, @@ -735,7 +728,8 @@ mod tests { "/accounts/alice/settings", "admin", DETAILS.clone(), - ); + ) + .await; assert_eq!(resp.status().as_u16(), 200); // TODO: Make this not require the username in the token @@ -745,7 +739,8 @@ mod tests { "/accounts/alice/settings", "password", DETAILS.clone(), - ); + ) + .await; assert_eq!(resp.status().as_u16(), 200); let resp = api_call( @@ -754,7 +749,50 @@ mod tests { "/accounts/alice/settings", "wrong", DETAILS.clone(), - ); + ) + .await; + assert_eq!(resp.status().as_u16(), 401); + } + + #[tokio::test] + async fn only_admin_or_user_can_send_payment() { + let payment: Option = Some(serde_json::json!({ + "receiver": "some_receiver", + "source_amount" : 10, + })); + let api = test_accounts_api(); + let resp = api_call( + &api, + "POST", + "/accounts/alice/payments", + "password", + payment.clone(), + ) + .await; + // This should return an internal server error since we're making an invalid payment request + // We could have set up a mockito mock to set that pay is called correctly but we merely want + // to check that authorization and paths work as expected + assert_eq!(resp.status().as_u16(), 500); + + // Note that the operator has indirect access to the user's token since they control the store + let resp = api_call( + &api, + "POST", + "/accounts/alice/payments", + "admin", + payment.clone(), + ) + .await; + assert_eq!(resp.status().as_u16(), 401); + + let resp = api_call( + &api, + "POST", + "/accounts/alice/payments", + "wrong", + payment.clone(), + ) + .await; assert_eq!(resp.status().as_u16(), 401); } } diff --git a/crates/interledger-api/src/routes/node_settings.rs b/crates/interledger-api/src/routes/node_settings.rs index 40f12819b..c08fcf3ba 100644 --- a/crates/interledger-api/src/routes/node_settings.rs +++ b/crates/interledger-api/src/routes/node_settings.rs @@ -1,9 +1,6 @@ use crate::{http_retry::Client, ExchangeRates, NodeStore}; -use bytes::Buf; -use futures::{ - future::{err, join_all, Either}, - Future, -}; +use bytes::Bytes; +use futures::TryFutureExt; use interledger_http::{deserialize_json, error::*, HttpAccount, HttpStore}; use interledger_packet::Address; use interledger_router::RouterStore; @@ -19,7 +16,8 @@ use std::{ str::{self, FromStr}, }; use url::Url; -use warp::{self, Filter, Rejection}; +use uuid::Uuid; +use warp::{self, reply::Json, Filter, Rejection}; // TODO add more to this response #[derive(Clone, Serialize)] @@ -41,20 +39,21 @@ where + BalanceStore + ExchangeRateStore + RouterStore, - A: Account + HttpAccount + SettlementAccount + Serialize + 'static, + A: Account + HttpAccount + Send + Sync + SettlementAccount + Serialize + 'static, { // Helper filters let admin_auth_header = format!("Bearer {}", admin_api_token); let admin_only = warp::header::("authorization") - .and_then( - move |authorization: SecretString| -> Result<(), Rejection> { + .and_then(move |authorization: SecretString| { + let admin_auth_header = admin_auth_header.clone(); + async move { if authorization.expose_secret() == &admin_auth_header { - Ok(()) + Ok::<(), Rejection>(()) } else { - Err(ApiError::unauthorized().into()) + Err(Rejection::from(ApiError::unauthorized())) } - }, - ) + } + }) // This call makes it so we do not pass on a () value on // success to the next filter, it just gets rid of it .untuple_one() @@ -62,7 +61,7 @@ where let with_store = warp::any().map(move || store.clone()).boxed(); // GET / - let get_root = warp::get2() + let get_root = warp::get() .and(warp::path::end()) .and(with_store.clone()) .map(move |store: S| { @@ -75,197 +74,200 @@ where .boxed(); // PUT /rates - let put_rates = warp::put2() + let put_rates = warp::put() .and(warp::path("rates")) .and(warp::path::end()) .and(admin_only.clone()) .and(deserialize_json()) .and(with_store.clone()) - .and_then(|rates: ExchangeRates, store: S| -> Result<_, Rejection> { - if store.set_exchange_rates(rates.0.clone()).is_ok() { - Ok(warp::reply::json(&rates)) - } else { - error!("Error setting exchange rates"); - Err(ApiError::internal_server_error().into()) + .and_then(|rates: ExchangeRates, store: S| { + async move { + if store.set_exchange_rates(rates.0.clone()).is_ok() { + Ok(warp::reply::json(&rates)) + } else { + error!("Error setting exchange rates"); + Err(Rejection::from(ApiError::internal_server_error())) + } } }) .boxed(); // GET /rates - let get_rates = warp::get2() + let get_rates = warp::get() .and(warp::path("rates")) .and(warp::path::end()) .and(with_store.clone()) - .and_then(|store: S| -> Result<_, Rejection> { - if let Ok(rates) = store.get_all_exchange_rates() { - Ok(warp::reply::json(&rates)) - } else { - error!("Error getting exchange rates"); - Err(ApiError::internal_server_error().into()) + .and_then(|store: S| { + async move { + if let Ok(rates) = store.get_all_exchange_rates() { + Ok::(warp::reply::json(&rates)) + } else { + error!("Error getting exchange rates"); + Err(Rejection::from(ApiError::internal_server_error())) + } } }) .boxed(); // GET /routes // Response: Map of ILP Address prefix -> Username - let get_routes = warp::get2() + let get_routes = warp::get() .and(warp::path("routes")) .and(warp::path::end()) .and(with_store.clone()) .and_then(|store: S| { - // Convert the account IDs listed in the routing table - // to the usernames for the API response - let routes = store.routing_table().clone(); - store - .get_accounts(routes.values().cloned().collect()) - .map_err::<_, Rejection>(|_| { - error!("Error getting accounts from store"); - ApiError::internal_server_error().into() - }) - .and_then(move |accounts| { - let routes: HashMap = HashMap::from_iter( - routes - .iter() - .map(|(prefix, _)| prefix.to_string()) - .zip(accounts.into_iter().map(|a| a.username().to_string())), - ); + async move { + // Convert the account IDs listed in the routing table + // to the usernames for the API response + let routes = store.routing_table().clone(); + let accounts = store + .get_accounts(routes.values().cloned().collect()) + .map_err(|_| { + error!("Error getting accounts from store"); + Rejection::from(ApiError::internal_server_error()) + }) + .await?; + let routes: HashMap = HashMap::from_iter( + routes + .iter() + .map(|(prefix, _)| prefix.to_string()) + .zip(accounts.into_iter().map(|a| a.username().to_string())), + ); - Ok(warp::reply::json(&routes)) - }) + Ok::(warp::reply::json(&routes)) + } }) .boxed(); // PUT /routes/static // Body: Map of ILP Address prefix -> Username - let put_static_routes = warp::put2() + let put_static_routes = warp::put() .and(warp::path("routes")) .and(warp::path("static")) .and(warp::path::end()) .and(admin_only.clone()) .and(deserialize_json()) .and(with_store.clone()) - .and_then(|routes: HashMap, store: S| { - // Convert the usernames to account IDs to set the routes in the store - let store_clone = store.clone(); - let usernames: Vec = routes.values().cloned().collect(); - // TODO use one store call to look up all of the usernames - join_all(usernames.into_iter().map(move |username| { - store_clone - .get_account_id_from_username(&username) - .map_err(move |_| { - error!("No account exists with username: {}", username); - ApiError::account_not_found().into() - }) - })) - .and_then(move |account_ids| { + .and_then(move |routes: HashMap, store: S| { + async move { + // Convert the usernames to account IDs to set the routes in the store + let mut usernames: Vec = Vec::new(); + for username in routes.values() { + let user = match Username::from_str(&username) { + Ok(u) => u, + Err(_) => return Err(Rejection::from(ApiError::bad_request())), + }; + usernames.push(user); + } + + let mut account_ids: Vec = Vec::new(); + for username in usernames { + account_ids.push( + store + .get_account_id_from_username(&username) + .map_err(|_| { + error!("Error setting static routes"); + Rejection::from(ApiError::internal_server_error()) + }) + .await?, + ); + } + let prefixes = routes.keys().map(|s| s.to_string()); store .set_static_routes(prefixes.zip(account_ids.into_iter())) - .map_err::<_, Rejection>(|_| { + .map_err(|_| { error!("Error setting static routes"); - ApiError::internal_server_error().into() + Rejection::from(ApiError::internal_server_error()) }) - .map(move |_| warp::reply::json(&routes)) - }) + .await?; + Ok::(warp::reply::json(&routes)) + } }) .boxed(); // PUT /routes/static/:prefix // Body: Username - let put_static_route = warp::put2() + let put_static_route = warp::put() .and(warp::path("routes")) .and(warp::path("static")) - .and(warp::path::param2::()) + .and(warp::path::param::()) .and(warp::path::end()) .and(admin_only.clone()) - .and(warp::body::concat()) + .and(warp::body::bytes()) .and(with_store.clone()) - .and_then(|prefix: String, body: warp::body::FullBody, store: S| { - if let Ok(username) = str::from_utf8(body.bytes()) - .map_err(|_| ()) - .and_then(|string| Username::from_str(string).map_err(|_| ())) - { + .and_then(|prefix: String, body: Bytes, store: S| { + async move { + let username_str = + str::from_utf8(&body).map_err(|_| Rejection::from(ApiError::bad_request()))?; + let username = Username::from_str(username_str) + .map_err(|_| Rejection::from(ApiError::bad_request()))?; // Convert the username to an account ID to set it in the store - let username_clone = username.clone(); - Either::A( - store - .clone() - .get_account_id_from_username(&username) - .map_err(move |_| { - error!("No account exists with username: {}", username_clone); - ApiError::account_not_found().into() - }) - .and_then(move |account_id| { - store - .set_static_route(prefix, account_id) - .map_err::<_, Rejection>(|_| { - error!("Error setting static route"); - ApiError::internal_server_error().into() - }) - }) - .map(move |_| username.to_string()), - ) - } else { - Either::B(err(ApiError::bad_request().into())) + let account_id = store + .get_account_id_from_username(&username) + .map_err(|_| { + error!("No account exists with username: {}", username); + Rejection::from(ApiError::account_not_found()) + }) + .await?; + store + .set_static_route(prefix, account_id) + .map_err(|_| { + error!("Error setting static route"); + Rejection::from(ApiError::internal_server_error()) + }) + .await?; + Ok::(username.to_string()) } }) .boxed(); // PUT /settlement/engines - let put_settlement_engines = warp::put2() + let put_settlement_engines = warp::put() .and(warp::path("settlement")) .and(warp::path("engines")) .and(warp::path::end()) - .and(admin_only.clone()) - .and(deserialize_json()) - .and(with_store.clone()) - .and_then(|asset_to_url_map: HashMap, store: S| { + .and(admin_only) + .and(warp::body::json()) + .and(with_store) + .and_then(move |asset_to_url_map: HashMap, store: S| async move { let asset_to_url_map_clone = asset_to_url_map.clone(); store .set_settlement_engines(asset_to_url_map.clone()) - .map_err::<_, Rejection>(|_| { + .map_err(|_| { error!("Error setting settlement engines"); - ApiError::internal_server_error().into() - }) - .and_then(move |_| { - // Create the accounts on the settlement engines for any - // accounts that are using the default settlement engine URLs - // (This is done in case we modify the globally configured settlement - // engine URLs after accounts have already been added) + Rejection::from(ApiError::internal_server_error()) + }).await?; + // Create the accounts on the settlement engines for any + // accounts that are using the default settlement engine URLs + // (This is done in case we modify the globally configured settlement + // engine URLs after accounts have already been added) - // TODO we should come up with a better way of ensuring - // the accounts are created that doesn't involve loading - // all of the accounts from the database into memory - // (even if this isn't called often, it could crash the node at some point) - store.get_all_accounts() - .map_err(|_| ApiError::internal_server_error().into()) - .and_then(move |accounts| { - let client = Client::default(); - let create_settlement_accounts = - accounts.into_iter().filter_map(move |account| { - let id = account.id(); - // Try creating the account on the settlement engine if the settlement_engine_url of the - // account is the one we just configured as the default for the account's asset code - if let Some(details) = account.settlement_engine_details() { - if Some(&details.url) == asset_to_url_map.get(account.asset_code()) { - return Some(client.create_engine_account(details.url, account.id()) - .map_err(|_| ApiError::internal_server_error().into()) - .and_then(move |status_code| { - if status_code.is_success() { - trace!("Account {} created on the SE", id); - } else { - error!("Error creating account. Settlement engine responded with HTTP code: {}", status_code); - } - Ok(()) - })); - } - } - None - }); - join_all(create_settlement_accounts) - }) - }) - .and_then(move |_| Ok(warp::reply::json(&asset_to_url_map_clone))) + // TODO we should come up with a better way of ensuring + // the accounts are created that doesn't involve loading + // all of the accounts from the database into memory + // (even if this isn't called often, it could crash the node at some point) + let accounts = store.get_all_accounts() + .map_err(|_| Rejection::from(ApiError::internal_server_error())).await?; + + let client = Client::default(); + // Try creating the account on the settlement engine if the settlement_engine_url of the + // account is the one we just configured as the default for the account's asset code + for account in accounts { + if let Some(details) = account.settlement_engine_details() { + if Some(&details.url) == asset_to_url_map.get(account.asset_code()) { + let status_code = client.create_engine_account(details.url, account.id()) + .map_err(|_| Rejection::from(ApiError::internal_server_error())) + .await?; + if status_code.is_success() { + trace!("Account {} created on the SE", account.id()); + } else { + error!("Error creating account. Settlement engine responded with HTTP code: {}", status_code); + } + } + } + } + Ok::(warp::reply::json(&asset_to_url_map_clone)) }) .boxed(); @@ -284,10 +286,10 @@ mod tests { use crate::routes::test_helpers::{api_call, test_node_settings_api}; use serde_json::{json, Value}; - #[test] - fn gets_status() { + #[tokio::test] + async fn gets_status() { let api = test_node_settings_api(); - let resp = api_call(&api, "GET", "/", "", None); + let resp = api_call(&api, "GET", "/", "", None).await; assert_eq!(resp.status().as_u16(), 200); assert_eq!( resp.body(), @@ -295,10 +297,10 @@ mod tests { ); } - #[test] - fn gets_rates() { + #[tokio::test] + async fn gets_rates() { let api = test_node_settings_api(); - let resp = api_call(&api, "GET", "/rates", "", None); + let resp = api_call(&api, "GET", "/rates", "", None).await; assert_eq!(resp.status().as_u16(), 200); assert_eq!( serde_json::from_slice::(resp.body()).unwrap(), @@ -306,56 +308,60 @@ mod tests { ); } - #[test] - fn gets_routes() { + #[tokio::test] + async fn gets_routes() { let api = test_node_settings_api(); - let resp = api_call(&api, "GET", "/routes", "", None); + let resp = api_call(&api, "GET", "/routes", "", None).await; assert_eq!(resp.status().as_u16(), 200); } - #[test] - fn only_admin_can_put_rates() { + #[tokio::test] + async fn only_admin_can_put_rates() { let api = test_node_settings_api(); let rates = json!({"ABC": 1.0}); - let resp = api_call(&api, "PUT", "/rates", "admin", Some(rates.clone())); + let resp = api_call(&api, "PUT", "/rates", "admin", Some(rates.clone())).await; assert_eq!(resp.status().as_u16(), 200); - let resp = api_call(&api, "PUT", "/rates", "wrong", Some(rates)); + let resp = api_call(&api, "PUT", "/rates", "wrong", Some(rates)).await; assert_eq!(resp.status().as_u16(), 401); } - #[test] - fn only_admin_can_put_static_routes() { + #[tokio::test] + async fn only_admin_can_put_static_routes() { let api = test_node_settings_api(); let routes = json!({"g.node1": "alice", "example.eu": "bob"}); - let resp = api_call(&api, "PUT", "/routes/static", "admin", Some(routes.clone())); + let resp = api_call(&api, "PUT", "/routes/static", "admin", Some(routes.clone())).await; assert_eq!(resp.status().as_u16(), 200); - let resp = api_call(&api, "PUT", "/routes/static", "wrong", Some(routes)); + let resp = api_call(&api, "PUT", "/routes/static", "wrong", Some(routes)).await; assert_eq!(resp.status().as_u16(), 401); } - #[test] - fn only_admin_can_put_single_static_route() { + #[tokio::test] + async fn only_admin_can_put_single_static_route() { let api = test_node_settings_api(); - let api_put = move |auth: &str| { - warp::test::request() - .method("PUT") - .path("/routes/static/g.node1") - .body("alice") - .header("Authorization", format!("Bearer {}", auth.to_string())) - .reply(&api) + let api_put = |auth: String| { + let auth = format!("Bearer {}", auth); + async { + warp::test::request() + .method("PUT") + .path("/routes/static/g.node1") + .body("alice") + .header("Authorization", auth) + .reply(&api) + .await + } }; - let resp = api_put("admin"); + let resp = api_put("admin".to_owned()).await; assert_eq!(resp.status().as_u16(), 200); - let resp = api_put("wrong"); + let resp = api_put("wrong".to_owned()).await; assert_eq!(resp.status().as_u16(), 401); } - #[test] - fn only_admin_can_put_engines() { + #[tokio::test] + async fn only_admin_can_put_engines() { let api = test_node_settings_api(); let engines = json!({"ABC": "http://localhost:3000", "XYZ": "http://localhost:3001"}); let resp = api_call( @@ -364,10 +370,11 @@ mod tests { "/settlement/engines", "admin", Some(engines.clone()), - ); + ) + .await; assert_eq!(resp.status().as_u16(), 200); - let resp = api_call(&api, "PUT", "/settlement/engines", "wrong", Some(engines)); + let resp = api_call(&api, "PUT", "/settlement/engines", "wrong", Some(engines)).await; assert_eq!(resp.status().as_u16(), 401); } } diff --git a/crates/interledger-api/src/routes/test_helpers.rs b/crates/interledger-api/src/routes/test_helpers.rs index 9e5023569..4f9eac131 100644 --- a/crates/interledger-api/src/routes/test_helpers.rs +++ b/crates/interledger-api/src/routes/test_helpers.rs @@ -2,12 +2,9 @@ use crate::{ routes::{accounts_api, node_settings_api}, AccountDetails, AccountSettings, NodeStore, }; +use async_trait::async_trait; use bytes::Bytes; -use futures::sync::mpsc::UnboundedSender; -use futures::{ - future::{err, ok}, - Future, -}; +use futures::channel::mpsc::UnboundedSender; use http::Response; use interledger_btp::{BtpAccount, BtpOutgoingService}; use interledger_ccp::{CcpRoutingAccount, RoutingRelation}; @@ -32,7 +29,7 @@ use url::Url; use uuid::Uuid; use warp::{self, Filter}; -pub fn api_call( +pub async fn api_call( api: &F, method: &str, endpoint: &str, // /ilp or /accounts/:username/ilp @@ -52,7 +49,7 @@ where ret = ret.header("Content-type", "application/json").json(&d); } - ret.reply(api) + ret.reply(api).await } pub fn test_node_settings_api( @@ -63,20 +60,20 @@ pub fn test_node_settings_api( pub fn test_accounts_api( ) -> impl warp::Filter + Clone { let incoming = incoming_service_fn(|_request| { - Box::new(err(RejectBuilder { + Err(RejectBuilder { code: ErrorCode::F02_UNREACHABLE, message: b"No other incoming handler!", data: &[], triggered_by: None, } - .build())) + .build()) }); let outgoing = outgoing_service_fn(move |_request| { - Box::new(ok(FulfillBuilder { + Ok(FulfillBuilder { fulfillment: &[0; 32], data: b"hello!", } - .build())) + .build()) }); let btp = BtpOutgoingService::new( Address::from_str("example.alice").unwrap(), @@ -174,22 +171,17 @@ impl CcpRoutingAccount for TestAccount { } } +#[async_trait] impl AccountStore for TestStore { type Account = TestAccount; - fn get_accounts( - &self, - _account_ids: Vec, - ) -> Box, Error = ()> + Send> { - Box::new(ok(vec![TestAccount])) + async fn get_accounts(&self, _account_ids: Vec) -> Result, ()> { + Ok(vec![TestAccount]) } // stub implementation (not used in these tests) - fn get_account_id_from_username( - &self, - _username: &Username, - ) -> Box + Send> { - Box::new(ok(Uuid::new_v4())) + async fn get_account_id_from_username(&self, _username: &Username) -> Result { + Ok(Uuid::new_v4()) } } @@ -216,91 +208,74 @@ impl RouterStore for TestStore { } } +#[async_trait] impl NodeStore for TestStore { type Account = TestAccount; - fn insert_account( - &self, - _account: AccountDetails, - ) -> Box + Send> { - Box::new(ok(TestAccount)) + async fn insert_account(&self, _account: AccountDetails) -> Result { + Ok(TestAccount) } - fn delete_account( - &self, - _id: Uuid, - ) -> Box + Send> { - Box::new(ok(TestAccount)) + async fn delete_account(&self, _id: Uuid) -> Result { + Ok(TestAccount) } - fn update_account( + async fn update_account( &self, _id: Uuid, _account: AccountDetails, - ) -> Box + Send> { - Box::new(ok(TestAccount)) + ) -> Result { + Ok(TestAccount) } - fn modify_account_settings( + async fn modify_account_settings( &self, _id: Uuid, _settings: AccountSettings, - ) -> Box + Send> { - Box::new(ok(TestAccount)) + ) -> Result { + Ok(TestAccount) } - fn get_all_accounts(&self) -> Box, Error = ()> + Send> { - Box::new(ok(vec![TestAccount, TestAccount])) + async fn get_all_accounts(&self) -> Result, ()> { + Ok(vec![TestAccount, TestAccount]) } - fn set_static_routes(&self, _routes: R) -> Box + Send> + async fn set_static_routes(&self, _routes: R) -> Result<(), ()> where - R: IntoIterator, + R: IntoIterator + Send + 'async_trait, { - Box::new(ok(())) + Ok(()) } - fn set_static_route( - &self, - _prefix: String, - _account_id: Uuid, - ) -> Box + Send> { - Box::new(ok(())) + async fn set_static_route(&self, _prefix: String, _account_id: Uuid) -> Result<(), ()> { + Ok(()) } - fn set_default_route( - &self, - _account_id: Uuid, - ) -> Box + Send> { + async fn set_default_route(&self, _account_id: Uuid) -> Result<(), ()> { unimplemented!() } - fn set_settlement_engines( + async fn set_settlement_engines( &self, - _asset_to_url_map: impl IntoIterator, - ) -> Box + Send> { - Box::new(ok(())) + _asset_to_url_map: impl IntoIterator + Send + 'async_trait, + ) -> Result<(), ()> { + Ok(()) } - fn get_asset_settlement_engine( - &self, - _asset_code: &str, - ) -> Box, Error = ()> + Send> { - Box::new(ok(None)) + async fn get_asset_settlement_engine(&self, _asset_code: &str) -> Result, ()> { + Ok(None) } } +#[async_trait] impl AddressStore for TestStore { /// Saves the ILP Address in the store's memory and database - fn set_ilp_address( - &self, - _ilp_address: Address, - ) -> Box + Send> { - unimplemented!() + async fn set_ilp_address(&self, _ilp_address: Address) -> Result<(), ()> { + Ok(()) } - fn clear_ilp_address(&self) -> Box + Send> { - unimplemented!() + async fn clear_ilp_address(&self) -> Result<(), ()> { + Ok(()) } /// Get's the store's ilp address from memory @@ -325,47 +300,49 @@ impl StreamNotificationsStore for TestStore { } } +#[async_trait] impl BalanceStore for TestStore { - fn get_balance(&self, _account: TestAccount) -> Box + Send> { - Box::new(ok(1)) + async fn get_balance(&self, _account: TestAccount) -> Result { + Ok(1) } - fn update_balances_for_prepare( + async fn update_balances_for_prepare( &self, _from_account: TestAccount, _incoming_amount: u64, - ) -> Box + Send> { + ) -> Result<(), ()> { unimplemented!() } - fn update_balances_for_fulfill( + async fn update_balances_for_fulfill( &self, _to_account: TestAccount, _outgoing_amount: u64, - ) -> Box + Send> { + ) -> Result<(i64, u64), ()> { unimplemented!() } - fn update_balances_for_reject( + async fn update_balances_for_reject( &self, _from_account: TestAccount, _incoming_amount: u64, - ) -> Box + Send> { + ) -> Result<(), ()> { unimplemented!() } } +#[async_trait] impl HttpStore for TestStore { type Account = TestAccount; - fn get_account_from_http_auth( + async fn get_account_from_http_auth( &self, username: &Username, token: &str, - ) -> Box + Send> { + ) -> Result { if username == &*USERNAME && token == AUTH_PASSWORD { - Box::new(ok(TestAccount)) + Ok(TestAccount) } else { - Box::new(err(())) + Err(()) } } } diff --git a/crates/interledger-btp/Cargo.toml b/crates/interledger-btp/Cargo.toml index 446750055..0d086a1f3 100644 --- a/crates/interledger-btp/Cargo.toml +++ b/crates/interledger-btp/Cargo.toml @@ -11,7 +11,7 @@ repository = "https://github.com/interledger-rs/interledger-rs" bytes = { version = "0.4.12", default-features = false } byteorder = { version = "1.3.2", default-features = false } chrono = { version = "0.4.9", default-features = false } -futures = { version = "0.1.29", default-features = false } +futures = { version = "0.3.1", default-features = false } interledger-packet = { path = "../interledger-packet", version = "^0.4.0", default-features = false } interledger-service = { path = "../interledger-service", version = "^0.4.0", default-features = false } log = { version = "0.4.8", default-features = false } @@ -19,18 +19,21 @@ num-bigint = { version = "0.2.3", default-features = false, features = ["std"] } parking_lot = { version = "0.9.0", default-features = false } quick-error = { version = "1.2.2", default-features = false } rand = { version = "0.7.2", default-features = false, features = ["std"] } -stream-cancel = { version = "0.4.4", default-features = false } -tokio-executor = { version = "0.1.8", default-features = false } -tokio-timer = { version = "0.2.11", default-features = false } -tokio-tungstenite = { version = "0.9.0", default-features = false, features = ["tls", "connect"] } -tungstenite = { version = "0.9.1", default-features = false } -url = { version = "2.1.0", default-features = false } +stream-cancel = { version = "0.5", default-features = false } +tokio-tungstenite = { version = "0.10.0", package = "tokio-tungstenite", git = "https://github.com/snapview/tokio-tungstenite", default-features = false, features = ["tls", "connect"] } + +tungstenite = { version = "0.9.2", default-features = false } +# we must force url v2.1.0 because stripping the "btp+" prefix from a BTP URL +# is an operation which panics +url = { version = "=2.1.0", default-features = false } uuid = { version = "0.8.1", default-features = false, features = ["v4"]} -warp = { version = "0.1.20", default-features = false, features = ["websocket"] } -secrecy = "0.5.1" +warp = { version = "0.2", default-features = false, features = ["websocket"] } +secrecy = "0.6" +async-trait = "0.1.22" +tokio = { version = "0.2.8", features = ["rt-core", "time", "stream", "macros"] } +lazy_static = { version = "1.4.0", default-features = false } +pin-project = "0.4.6" [dev-dependencies] hex = { version = "0.4.0", default-features = false } -lazy_static = { version = "1.4.0", default-features = false } -net2 = { version = "0.2.33", default-features = false } -tokio = { version = "0.1.22", default-features = false } +net2 = { version = "0.2.33", default-features = false } \ No newline at end of file diff --git a/crates/interledger-btp/src/client.rs b/crates/interledger-btp/src/client.rs index 7a0041e6e..027809e60 100644 --- a/crates/interledger-btp/src/client.rs +++ b/crates/interledger-btp/src/client.rs @@ -1,36 +1,26 @@ use super::packet::*; use super::service::BtpOutgoingService; use super::BtpAccount; -use futures::{future::join_all, Future, Sink, Stream}; +use futures::{future::join_all, SinkExt, StreamExt, TryFutureExt}; use interledger_packet::Address; use interledger_service::*; use log::{debug, error, trace}; use rand::random; use tokio_tungstenite::connect_async; use tungstenite::Message; -use url::{ParseError, Url}; - -pub fn parse_btp_url(uri: &str) -> Result { - let uri = if uri.starts_with("btp+") { - uri.split_at(4).1 - } else { - uri - }; - Url::parse(uri) -} /// Create a BtpOutgoingService wrapping BTP connections to the accounts specified. /// Calling `handle_incoming` with an `IncomingService` will turn the returned /// BtpOutgoingService into a bidirectional handler. -pub fn connect_client( +pub async fn connect_client( ilp_address: Address, accounts: Vec, error_on_unavailable: bool, next_outgoing: S, -) -> impl Future, Error = ()> +) -> Result, ()> where S: OutgoingService + Clone + 'static, - A: BtpAccount + 'static, + A: BtpAccount + Send + Sync + 'static, { let service = BtpOutgoingService::new(ilp_address, next_outgoing); let mut connect_btp = Vec::new(); @@ -42,17 +32,23 @@ where service.clone(), )); } - join_all(connect_btp).and_then(move |_| Ok(service)) + join_all(connect_btp).await; + Ok(service) } -pub fn connect_to_service_account( +/// Initiates a BTP connection with the specified account and saves it to the list of connections +/// maintained by the provided service. This is done in the following steps: +/// 1. Initialize a WebSocket connection at the BTP account's URL +/// 2. Send a BTP authorization packet to the peer +/// 3. If successful, consider the BTP connection established and add it to the service +pub async fn connect_to_service_account( account: A, error_on_unavailable: bool, service: BtpOutgoingService, -) -> impl Future +) -> Result<(), ()> where O: OutgoingService + Clone + 'static, - A: BtpAccount + 'static, + A: BtpAccount + Send + Sync + 'static, { let account_id = account.id(); let mut url = account @@ -67,58 +63,62 @@ where .map(|s| s.to_vec()) .unwrap_or_default(); debug!("Connecting to {}", url); - connect_async(url.clone()) + + let (mut connection, _) = connect_async(url.clone()) .map_err(move |err| { error!( "Error connecting to WebSocket server for account: {} {:?}", account_id, err ) }) - .and_then(move |(connection, _)| { - trace!( - "Connected to account {} (URI: {}), sending auth packet", - account_id, - url - ); - // Send BTP authentication - let auth_packet = Message::Binary( - BtpPacket::Message(BtpMessage { - request_id: random(), - protocol_data: vec![ - ProtocolData { - protocol_name: String::from("auth"), - content_type: ContentType::ApplicationOctetStream, - data: vec![], - }, - ProtocolData { - protocol_name: String::from("auth_token"), - content_type: ContentType::TextPlainUtf8, - data: token, - }, - ], - }) - .to_bytes(), - ); + .await?; + + trace!( + "Connected to account {} (UID: {}) (URI: {}), sending auth packet", + account.username(), + account_id, + url + ); - // TODO check that the response is a success before proceeding - // (right now we just assume they'll close the connection if the auth didn't work) - connection - .send(auth_packet) - .map_err(move |_| error!("Error sending auth packet on connection: {}", url)) - .then(move |result| match result { - Ok(connection) => { - debug!("Connected to account {}'s server", account.id()); - let connection = connection.from_err().sink_from_err(); - service.add_connection(account, connection); - Ok(()) - } - Err(_) => { - if error_on_unavailable { - Err(()) - } else { - Ok(()) - } - } - }) + // Send BTP authentication + let auth_packet = Message::binary( + BtpPacket::Message(BtpMessage { + request_id: random(), + protocol_data: vec![ + ProtocolData { + protocol_name: String::from("auth"), + content_type: ContentType::ApplicationOctetStream, + data: vec![], + }, + ProtocolData { + protocol_name: String::from("auth_token"), + content_type: ContentType::TextPlainUtf8, + data: token, + }, + ], }) + .to_bytes(), + ); + + // (right now we just assume they'll close the connection if the auth didn't work) + let result = connection // this just a stream + .send(auth_packet) + .map_err(move |_| error!("Error sending auth packet on connection: {}", url)) + .await; + + match result { + Ok(_) => { + debug!("Connected to account {}'s server", account.id()); + let connection = connection.filter_map(|v| async move { v.ok() }); + service.add_connection(account, connection); + Ok(()) + } + Err(_) => { + if error_on_unavailable { + Err(()) + } else { + Ok(()) + } + } + } } diff --git a/crates/interledger-btp/src/lib.rs b/crates/interledger-btp/src/lib.rs index fbc6981e4..623219055 100644 --- a/crates/interledger-btp/src/lib.rs +++ b/crates/interledger-btp/src/lib.rs @@ -6,7 +6,7 @@ //! Because this protocol uses WebSockets, only one party needs to have a publicly-accessible HTTPS //! endpoint but both sides can send and receive ILP packets. -use futures::Future; +use async_trait::async_trait; use interledger_service::{Account, Username}; use url::Url; @@ -16,37 +16,40 @@ mod oer; mod packet; mod server; mod service; +mod wrapped_ws; -pub use self::client::{connect_client, connect_to_service_account, parse_btp_url}; -pub use self::server::btp_service_as_filter; +pub use self::client::{connect_client, connect_to_service_account}; +pub use self::server::btp_service_as_filter; // This is consumed only by the node. pub use self::service::{BtpOutgoingService, BtpService}; +/// Extension trait for [Account](../interledger_service/trait.Account.html) with [ILP over BTP](https://interledger.org/rfcs/0023-bilateral-transfer-protocol/) related information pub trait BtpAccount: Account { + /// Returns the BTP Websockets URL corresponding to this account fn get_ilp_over_btp_url(&self) -> Option<&Url>; + /// Returns the BTP authentication token which is used when initiating a BTP connection + /// with a peer fn get_ilp_over_btp_outgoing_token(&self) -> Option<&[u8]>; } /// The interface for Store implementations that can be used with the BTP Server. +#[async_trait] pub trait BtpStore { type Account: BtpAccount; /// Load Account details based on the auth token received via BTP. - fn get_account_from_btp_auth( + async fn get_account_from_btp_auth( &self, username: &Username, token: &str, - ) -> Box + Send>; + ) -> Result; /// Load accounts that have a ilp_over_btp_url configured - fn get_btp_outgoing_accounts( - &self, - ) -> Box, Error = ()> + Send>; + async fn get_btp_outgoing_accounts(&self) -> Result, ()>; } #[cfg(test)] mod client_server { use super::*; - use futures::future::{err, lazy, ok, result}; use interledger_packet::{Address, ErrorCode, FulfillBuilder, PrepareBuilder, RejectBuilder}; use interledger_service::*; use net2::TcpBuilder; @@ -56,7 +59,6 @@ mod client_server { sync::Arc, time::{Duration, SystemTime}, }; - use tokio::runtime::Runtime; use uuid::Uuid; use lazy_static::lazy_static; @@ -126,13 +128,11 @@ mod client_server { accounts: Arc>, } + #[async_trait] impl AccountStore for TestStore { type Account = TestAccount; - fn get_accounts( - &self, - account_ids: Vec, - ) -> Box, Error = ()> + Send> { + async fn get_accounts(&self, account_ids: Vec) -> Result, ()> { let accounts: Vec = self .accounts .iter() @@ -145,156 +145,149 @@ mod client_server { }) .collect(); if accounts.len() == account_ids.len() { - Box::new(ok(accounts)) + Ok(accounts) } else { - Box::new(err(())) + Err(()) } } // stub implementation (not used in these tests) - fn get_account_id_from_username( - &self, - _username: &Username, - ) -> Box + Send> { - Box::new(ok(Uuid::new_v4())) + async fn get_account_id_from_username(&self, _username: &Username) -> Result { + Ok(Uuid::new_v4()) } } + #[async_trait] impl BtpStore for TestStore { type Account = TestAccount; - fn get_account_from_btp_auth( + async fn get_account_from_btp_auth( &self, username: &Username, token: &str, - ) -> Box + Send> { - Box::new(result( - self.accounts - .iter() - .find(|account| { - if let Some(account_token) = &account.ilp_over_btp_incoming_token { - account_token == token && account.username() == username - } else { - false - } - }) - .cloned() - .ok_or(()), - )) + ) -> Result { + self.accounts + .iter() + .find(|account| { + if let Some(account_token) = &account.ilp_over_btp_incoming_token { + account_token == token && account.username() == username + } else { + false + } + }) + .cloned() + .ok_or(()) } - fn get_btp_outgoing_accounts( - &self, - ) -> Box, Error = ()> + Send> { - Box::new(ok(self + async fn get_btp_outgoing_accounts(&self) -> Result, ()> { + Ok(self .accounts .iter() .filter(|account| account.ilp_over_btp_url.is_some()) .cloned() - .collect())) + .collect()) } } // TODO should this be an integration test, since it binds to a port? - #[test] - fn client_server_test() { - let mut runtime = Runtime::new().unwrap(); - runtime - .block_on(lazy(|| { - let bind_addr = get_open_port(); - - let server_store = TestStore { - accounts: Arc::new(vec![TestAccount { - id: Uuid::new_v4(), - ilp_over_btp_incoming_token: Some("test_auth_token".to_string()), - ilp_over_btp_outgoing_token: None, - ilp_over_btp_url: None, - }]), - }; - let server_address = Address::from_str("example.server").unwrap(); - let btp_service = BtpOutgoingService::new( - server_address.clone(), - outgoing_service_fn(move |_| { - Err(RejectBuilder { - code: ErrorCode::F02_UNREACHABLE, - message: b"No other outgoing handler", - triggered_by: Some(&server_address), - data: &[], - } - .build()) - }), - ); - let filter = btp_service_as_filter(btp_service.clone(), server_store); - btp_service.handle_incoming(incoming_service_fn(|_| { - Ok(FulfillBuilder { - fulfillment: &[0; 32], - data: b"test data", - } - .build()) - })); - - let account = TestAccount { - id: Uuid::new_v4(), - ilp_over_btp_url: Some( - Url::parse(&format!("btp+ws://{}/accounts/alice/ilp/btp", bind_addr)) - .unwrap(), - ), - ilp_over_btp_outgoing_token: Some("test_auth_token".to_string()), - ilp_over_btp_incoming_token: None, - }; - let accounts = vec![account.clone()]; - let addr = Address::from_str("example.address").unwrap(); - let addr_clone = addr.clone(); - let client = connect_client( - addr.clone(), - accounts, - true, - outgoing_service_fn(move |_| { - Err(RejectBuilder { - code: ErrorCode::F02_UNREACHABLE, - message: &[], - data: &[], - triggered_by: Some(&addr_clone), - } - .build()) - }), - ) - .and_then(move |btp_service| { - let mut btp_service = - btp_service.handle_incoming(incoming_service_fn(move |_| { - Err(RejectBuilder { - code: ErrorCode::F02_UNREACHABLE, - message: &[], - data: &[], - triggered_by: Some(&addr), - } - .build()) - })); - let btp_service_clone = btp_service.clone(); - btp_service - .send_request(OutgoingRequest { - from: account.clone(), - to: account.clone(), - original_amount: 100, - prepare: PrepareBuilder { - destination: Address::from_str("example.destination").unwrap(), - amount: 100, - execution_condition: &[0; 32], - expires_at: SystemTime::now() + Duration::from_secs(30), - data: b"test data", - } - .build(), - }) - .map_err(|reject| println!("Packet was rejected: {:?}", reject)) - .and_then(move |_| { - btp_service_clone.close(); - Ok(()) - }) - }); - let server = warp::serve(filter); - tokio::spawn(server.bind(bind_addr)); - client + #[tokio::test] + async fn client_server_test() { + let bind_addr = get_open_port(); + + let server_store = TestStore { + accounts: Arc::new(vec![TestAccount { + id: Uuid::new_v4(), + ilp_over_btp_incoming_token: Some("test_auth_token".to_string()), + ilp_over_btp_outgoing_token: None, + ilp_over_btp_url: None, + }]), + }; + let server_address = Address::from_str("example.server").unwrap(); + let btp_service = BtpOutgoingService::new( + server_address.clone(), + outgoing_service_fn(move |_| { + Err(RejectBuilder { + code: ErrorCode::F02_UNREACHABLE, + message: b"No other outgoing handler", + triggered_by: Some(&server_address), + data: &[], + } + .build()) + }), + ); + btp_service + .clone() + .handle_incoming(incoming_service_fn(|_| { + Ok(FulfillBuilder { + fulfillment: &[0; 32], + data: b"test data", + } + .build()) + })) + .await; + let filter = btp_service_as_filter(btp_service.clone(), server_store); + let server = warp::serve(filter); + // Spawn the server and listen for incoming connections + tokio::spawn(server.bind(bind_addr)); + + // Try to connect + let account = TestAccount { + id: Uuid::new_v4(), + ilp_over_btp_url: Some( + Url::parse(&format!("btp+ws://{}/accounts/alice/ilp/btp", bind_addr)).unwrap(), + ), + ilp_over_btp_outgoing_token: Some("test_auth_token".to_string()), + ilp_over_btp_incoming_token: None, + }; + let accounts = vec![account.clone()]; + let addr = Address::from_str("example.address").unwrap(); + let addr_clone = addr.clone(); + + let btp_client = connect_client( + addr.clone(), + accounts, + true, + outgoing_service_fn(move |_| { + Err(RejectBuilder { + code: ErrorCode::F02_UNREACHABLE, + message: &[], + data: &[], + triggered_by: Some(&addr_clone), + } + .build()) + }), + ) + .await + .unwrap(); + + let mut btp_client = btp_client + .handle_incoming(incoming_service_fn(move |_| { + Err(RejectBuilder { + code: ErrorCode::F02_UNREACHABLE, + message: &[], + data: &[], + triggered_by: Some(&addr), + } + .build()) })) - .unwrap(); + .await; + + let res = btp_client + .send_request(OutgoingRequest { + from: account.clone(), + to: account.clone(), + original_amount: 100, + prepare: PrepareBuilder { + destination: Address::from_str("example.destination").unwrap(), + amount: 100, + execution_condition: &[0; 32], + expires_at: SystemTime::now() + Duration::from_secs(30), + data: b"test data", + } + .build(), + }) + .await; + assert!(res.is_ok()); + btp_service.close(); } } diff --git a/crates/interledger-btp/src/server.rs b/crates/interledger-btp/src/server.rs index 2933316d4..fba3838c8 100644 --- a/crates/interledger-btp/src/server.rs +++ b/crates/interledger-btp/src/server.rs @@ -1,32 +1,23 @@ -use super::service::{BtpOutgoingService, WsError}; use super::{packet::*, BtpAccount, BtpStore}; -use futures::{future::result, Async, AsyncSink, Future, Poll, Sink, Stream}; +use super::{service::BtpOutgoingService, wrapped_ws::WsWrap}; +use futures::{FutureExt, Sink, Stream}; +use futures::{SinkExt, StreamExt, TryFutureExt}; use interledger_service::*; use log::{debug, error, warn}; use secrecy::{ExposeSecret, SecretString}; -use std::time::Duration; -use tokio_timer::Timeout; -use tungstenite; +// use std::time::Duration; use warp::{ self, - ws::{Message, WebSocket, Ws2}, + ws::{Message, WebSocket, Ws}, Filter, }; // Close the incoming websocket connection if the auth details // have not been received within this timeout -const WEBSOCKET_TIMEOUT: Duration = Duration::from_secs(10); +// const WEBSOCKET_TIMEOUT: Duration = Duration::from_secs(10); +const MAX_MESSAGE_SIZE: usize = 40000; -// const MAX_MESSAGE_SIZE: usize = 40000; - -/// Returns a BtpOutgoingService and a warp Filter. -/// -/// The BtpOutgoingService wraps all BTP/WebSocket connections that come -/// in on the given address. Calling `handle_incoming` with an `IncomingService` will -/// turn the returned BtpOutgoingService into a bidirectional handler. -/// The separation is designed to enable the returned BtpOutgoingService to be passed -/// to another service like the Router, and _then_ for the Router to be passed as the -/// IncomingService to the BTP server. +/// Returns a Warp Filter instantiated for the provided BtpOutgoingService service. /// /// The warp filter handles the websocket upgrades and adds incoming connections /// to the BTP service so that it will handle each of the messages. @@ -37,35 +28,24 @@ pub fn btp_service_as_filter( where O: OutgoingService + Clone + Send + Sync + 'static, S: BtpStore + Clone + Send + Sync + 'static, - A: BtpAccount + 'static, + A: BtpAccount + Send + Sync + 'static, { warp::path("accounts") - .and(warp::path::param2::()) + .and(warp::path::param::()) .and(warp::path("ilp")) .and(warp::path("btp")) .and(warp::path::end()) - .and(warp::ws2()) - .map(move |username: Username, ws: Ws2| { - let store = store.clone(); + .and(warp::ws()) + .map(move |username: Username, ws: Ws| { + // warp Websocket let service_clone = service.clone(); - ws.on_upgrade(move |ws: WebSocket| { - // TODO set max_message_size once https://github.com/seanmonstar/warp/pull/272 is merged - let service_clone = service_clone.clone(); - Timeout::new(validate_auth(store, username, ws), WEBSOCKET_TIMEOUT) - .and_then(move |(account, connection)| { - debug!( - "Added connection for account {}: (id: {})", - account.username(), - account.id() - ); - service_clone.add_connection(account, WsWrap { connection }); - Ok(()) - }) - .or_else(|_| { - warn!("Closing Websocket connection because of an error"); - Ok(()) - }) - }) + let store_clone = store.clone(); + ws.max_message_size(MAX_MESSAGE_SIZE) + .on_upgrade(|socket: WebSocket| { + // wrapper over tungstenite Websocket + add_connections(socket, username, service_clone, store_clone) + .map(|result| result.unwrap()) + }) }) .boxed() } @@ -74,93 +54,40 @@ where /// tungstenite Websocket connection. It is needed for /// compatibility with the BTP service that interacts with the /// websocket implementation from warp and tokio-tungstenite -struct WsWrap { - connection: W, -} - -impl Stream for WsWrap +async fn add_connections( + socket: WebSocket, + username: Username, + service: BtpOutgoingService, + store: S, +) -> Result<(), ()> where - W: Stream - + Sink, + O: OutgoingService + Clone + Send + Sync + 'static, + S: BtpStore + Clone + Send + Sync + 'static, + A: BtpAccount + Send + Sync + 'static, { - type Item = tungstenite::Message; - type Error = WsError; - - fn poll(&mut self) -> Poll, Self::Error> { - match self.connection.poll() { - Ok(Async::NotReady) => Ok(Async::NotReady), - Ok(Async::Ready(None)) => Ok(Async::Ready(None)), - Ok(Async::Ready(Some(message))) => { - let message = if message.is_ping() { - tungstenite::Message::Ping(message.into_bytes()) - } else if message.is_binary() { - tungstenite::Message::Binary(message.into_bytes()) - } else if message.is_text() { - tungstenite::Message::Text(message.to_str().unwrap_or_default().to_string()) - } else if message.is_close() { - tungstenite::Message::Close(None) - } else { - warn!( - "Got unexpected websocket message, closing connection: {:?}", - message - ); - tungstenite::Message::Close(None) - }; - Ok(Async::Ready(Some(message))) - } - Err(err) => Err(WsError::from(err)), + // We ignore all the errors + let socket = socket.filter_map(|v| async move { v.ok() }); + match validate_auth(store, username, socket) + // .timeout(WEBSOCKET_TIMEOUT) // No method found? + .await + { + Ok((account, connection)) => { + // We need to wrap our Warp connection in order to cast the Sink type + // to tungstenite::Message. This probably can be implemented with SinkExt::with + // but couldn't figure out how. + service.add_connection(account.clone(), WsWrap { connection }); + debug!( + "Added connection for account {}: (id: {})", + account.username(), + account.id() + ); } - } -} - -impl Sink for WsWrap -where - W: Stream - + Sink, -{ - type SinkItem = tungstenite::Message; - type SinkError = WsError; - - fn start_send( - &mut self, - item: Self::SinkItem, - ) -> Result, Self::SinkError> { - match item { - tungstenite::Message::Binary(data) => self - .connection - .start_send(Message::binary(data)) - .map(|result| { - if let AsyncSink::NotReady(message) = result { - AsyncSink::NotReady(tungstenite::Message::Binary(message.into_bytes())) - } else { - AsyncSink::Ready - } - }) - .map_err(WsError::from), - tungstenite::Message::Text(data) => { - match self.connection.start_send(Message::text(data)) { - Ok(AsyncSink::NotReady(message)) => { - if let Ok(string) = String::from_utf8(message.into_bytes()) { - Ok(AsyncSink::NotReady(tungstenite::Message::text(string))) - } else { - Err(WsError::Tungstenite(tungstenite::Error::Utf8)) - } - } - Ok(AsyncSink::Ready) => Ok(AsyncSink::Ready), - Err(err) => Err(WsError::from(err)), - } - } - // Ignore other message types because warp's WebSocket type doesn't - // allow us to send any other types of messages - // TODO make sure warp's websocket responds to pings and/or sends them to keep the - // connection alive - _ => Ok(AsyncSink::Ready), + Err(_) => { + warn!("Closing Websocket connection because of an error"); } - } + }; - fn poll_complete(&mut self) -> Poll<(), Self::SinkError> { - self.connection.poll_complete().map_err(WsError::from) - } + Ok(()) } struct Auth { @@ -168,74 +95,61 @@ struct Auth { token: SecretString, } -fn validate_auth( +async fn validate_auth( store: S, username: Username, - connection: impl Stream - + Sink, -) -> impl Future< - Item = ( - A, - impl Stream - + Sink, - ), - Error = (), -> + connection: impl Stream + Sink, +) -> Result<(A, impl Stream + Sink), ()> where S: BtpStore + 'static, A: BtpAccount + 'static, { - get_auth(connection).and_then(move |(auth, connection)| { - debug!("Got BTP connection for username: {}", username); - store - .get_account_from_btp_auth(&username, &auth.token.expose_secret()) - .map_err(move |_| warn!("BTP connection does not correspond to an account")) - .and_then(move |account| { - let auth_response = Message::binary( - BtpResponse { - request_id: auth.request_id, - protocol_data: Vec::new(), - } - .to_bytes(), - ); - connection - .send(auth_response) - .map_err(|_err| error!("warp::Error sending auth response")) - .and_then(|connection| Ok((account, connection))) - }) - }) -} + let (auth, mut connection) = get_auth(Box::pin(connection)).await?; + debug!("Got BTP connection for username: {}", username); + let account = store + .get_account_from_btp_auth(&username, &auth.token.expose_secret()) + .map_err(move |_| warn!("BTP connection does not correspond to an account")) + .await?; + + let auth_response = Message::binary( + BtpResponse { + request_id: auth.request_id, + protocol_data: Vec::new(), + } + .to_bytes(), + ); -fn get_auth( - connection: impl Stream - + Sink, -) -> impl Future< - Item = ( - Auth, - impl Stream - + Sink, - ), - Error = (), -> { connection - .skip_while(|message| { - // Skip non-binary messages like Pings and Pongs - // Note that the BTP protocol spec technically specifies that - // the auth message MUST be the first packet sent over the - // WebSocket connection. However, the JavaScript implementation - // of BTP sends a Ping packet first, so we should ignore it. - // (Be liberal in what you accept but strict in what you send) - Ok(!message.is_binary()) - // TODO: should we error if the client sends something other than a binary or ping packet first? - }) - .into_future() - .map_err(|_err| ()) - .and_then(move |(message, connection)| { - // The first packet sent on the connection MUST be the auth packet - result(parse_auth(message).map(|auth| (auth, connection)).ok_or_else(|| { - warn!("Got a BTP connection where the first packet sent was not a valid BTP Auth message. Closing the connection") - })) - }) + .send(auth_response) + .map_err(|_| error!("warp::Error sending auth response")) + .await?; + + Ok((account, connection)) +} + +/// Reads the first non-empty non-error binary message from the WebSocket and attempts to parse it as an AuthToken +async fn get_auth( + connection: impl Stream + Sink + Unpin, +) -> Result<(Auth, impl Stream + Sink), ()> { + // Skip non-binary messages like Pings and Pongs + // Note that the BTP protocol spec technically specifies that + // the auth message MUST be the first packet sent over the + // WebSocket connection. However, the JavaScript implementation + // of BTP sends a Ping packet first, so we should ignore it. + // (Be liberal in what you accept but strict in what you send) + // TODO: should we error if the client sends something other than a binary or ping packet first? + let mut connection = + connection.skip_while(move |message| futures::future::ready(!message.is_binary())); + + // The first packet sent on the connection MUST be the auth packet + let message = connection.next().await; + match parse_auth(message) { + Some(auth) => Ok((auth, Box::pin(connection))), + None => { + warn!("Got a BTP connection where the first packet sent was not a valid BTP Auth message. Closing the connection"); + Err(()) + } + } } fn parse_auth(ws_packet: Option) -> Option { @@ -245,6 +159,9 @@ fn parse_auth(ws_packet: Option) -> Option { Ok(message) => { let request_id = message.request_id; let mut token: Option = None; + // The primary data should be the "auth" with empty data + // The secondary data MUST have the "auth_token" with the authorization + // token set as the data field for protocol_data in message.protocol_data.iter() { let protocol_name: &str = protocol_data.protocol_name.as_ref(); if protocol_name == "auth_token" { @@ -255,7 +172,7 @@ fn parse_auth(ws_packet: Option) -> Option { if let Some(token) = token { return Some(Auth { request_id, - token: SecretString::new(token.to_string()), + token: SecretString::new(token), }); } else { warn!("BTP packet is missing auth token"); diff --git a/crates/interledger-btp/src/service.rs b/crates/interledger-btp/src/service.rs index 88f0395ad..51643d958 100644 --- a/crates/interledger-btp/src/service.rs +++ b/crates/interledger-btp/src/service.rs @@ -1,68 +1,48 @@ use super::{packet::*, BtpAccount}; +use async_trait::async_trait; use bytes::BytesMut; use futures::{ - future::err, - sync::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}, - sync::oneshot, - Future, Sink, Stream, + channel::{ + mpsc::{unbounded, UnboundedReceiver, UnboundedSender}, + oneshot, + }, + future, FutureExt, Sink, Stream, StreamExt, }; use interledger_packet::{Address, ErrorCode, Fulfill, Packet, Prepare, Reject, RejectBuilder}; use interledger_service::*; +use lazy_static::lazy_static; use log::{debug, error, trace, warn}; use parking_lot::{Mutex, RwLock}; use rand::random; use std::collections::HashMap; -use std::{ - convert::TryFrom, error::Error, fmt, io, iter::IntoIterator, marker::PhantomData, sync::Arc, - time::Duration, -}; +use std::{convert::TryFrom, iter::IntoIterator, marker::PhantomData, sync::Arc, time::Duration}; use stream_cancel::{Trigger, Valve}; -use tokio_executor::spawn; -use tokio_timer::Interval; +use tokio::time; use tungstenite::Message; use uuid::Uuid; -use warp; const PING_INTERVAL: u64 = 30; // seconds -type IlpResultChannel = oneshot::Sender>; -type IncomingRequestBuffer = UnboundedReceiver<(A, u32, Prepare)>; - -#[derive(Debug)] -pub enum WsError { - Tungstenite(tungstenite::Error), - Warp(warp::Error), -} - -impl fmt::Display for WsError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - WsError::Tungstenite(err) => err.fmt(f), - WsError::Warp(err) => err.fmt(f), - } - } -} - -impl Error for WsError {} - -impl From for WsError { - fn from(err: tungstenite::Error) -> Self { - WsError::Tungstenite(err) - } +lazy_static! { + static ref PING: Message = Message::Ping(Vec::with_capacity(0)); + static ref PONG: Message = Message::Pong(Vec::with_capacity(0)); } -impl From for WsError { - fn from(err: warp::Error) -> Self { - WsError::Warp(err) - } -} +type IlpResultChannel = oneshot::Sender>; +type IncomingRequestBuffer = UnboundedReceiver<(A, u32, Prepare)>; -/// A container for BTP/WebSocket connections that implements OutgoingService -/// for sending outgoing ILP Prepare packets over one of the connected BTP connections. +/// The BtpOutgoingService wraps all BTP/WebSocket connections that come +/// in on the given address. It implements OutgoingService for sending +/// outgoing ILP Prepare packets over one of the connected BTP connections. +/// Calling `handle_incoming` with an `IncomingService` will turn the returned +/// BtpOutgoingService into a bidirectional handler. +/// The separation is designed to enable the returned BtpOutgoingService to be passed +/// to another service like the Router, and _then_ for the Router to be passed as the +/// IncomingService to the BTP server. #[derive(Clone)] pub struct BtpOutgoingService { - // TODO support multiple connections per account ilp_address: Address, + /// Outgoing messages for the receiver of the websocket indexed by account uid connections: Arc>>>, pending_outgoing: Arc>>, pending_incoming: Arc>>>, @@ -72,10 +52,76 @@ pub struct BtpOutgoingService { stream_valve: Arc, } +/// Handle the packets based on whether they are an incoming request or a response to something we sent. +/// a. If it's a Prepare packet, it gets buffered in the incoming_sender channel which will get consumed +/// once an incoming handler is added +/// b. If it's a Fulfill/Reject packet, it gets added to the pending_outgoing hashmap which gets consumed +/// by the outgoing service implementation immediately +/// incoming_sender.unbounded_send basically sends data to the self.incoming_receiver +/// to be consumed when we setup the incoming handler +/// Set up a listener to handle incoming packets from the WebSocket connection +#[inline] +async fn handle_message( + message: Message, + tx_clone: UnboundedSender, + account: A, + pending_requests: Arc>>, + incoming_sender: UnboundedSender<(A, u32, Prepare)>, +) { + if message.is_binary() { + match parse_ilp_packet(message) { + // Queues up the prepare packet + Ok((request_id, Packet::Prepare(prepare))) => { + trace!( + "Got incoming Prepare packet on request ID: {} {:?}", + request_id, + prepare + ); + let _ = incoming_sender + .unbounded_send((account, request_id, prepare)) + .map_err(|err| error!("Unable to buffer incoming request: {:?}", err)); + } + // Sends the fulfill/reject to the outgoing service + Ok((request_id, Packet::Fulfill(fulfill))) => { + trace!("Got fulfill response to request id {}", request_id); + if let Some(channel) = (*pending_requests.lock()).remove(&request_id) { + let _ = channel.send(Ok(fulfill)).map_err(|fulfill| error!("Error forwarding Fulfill packet back to the Future that sent the Prepare: {:?}", fulfill)); + } else { + warn!( + "Got Fulfill packet that does not match an outgoing Prepare we sent: {:?}", + fulfill + ); + } + } + Ok((request_id, Packet::Reject(reject))) => { + trace!("Got reject response to request id {}", request_id); + if let Some(channel) = (*pending_requests.lock()).remove(&request_id) { + let _ = channel.send(Err(reject)).map_err(|reject| error!("Error forwarding Reject packet back to the Future that sent the Prepare: {:?}", reject)); + } else { + warn!( + "Got Reject packet that does not match an outgoing Prepare we sent: {:?}", + reject + ); + } + } + Err(_) => { + debug!("Unable to parse ILP packet from BTP packet (if this is the first time this appears, the packet was probably the auth response)"); + // TODO Send error back + } + } + } else if message.is_ping() { + trace!("Responding to Ping message from account {}", account.id()); + // Writes back the PONG to the websocket + let _ = tx_clone + .unbounded_send(PONG.clone()) + .map_err(|err| error!("Error sending Pong message back: {:?}", err)); + } +} + impl BtpOutgoingService where O: OutgoingService + Clone, - A: BtpAccount + 'static, + A: BtpAccount + Send + Sync + 'static, { pub fn new(ilp_address: Address, next: O) -> Self { let (incoming_sender, incoming_receiver) = unbounded(); @@ -100,133 +146,88 @@ where self.close_all_connections.lock().take(); } - /// Set up a WebSocket connection so that outgoing Prepare packets can be sent to it, - /// incoming Prepare packets are buffered in a channel (until an IncomingService is added - /// via the handle_incoming method), and ILP Fulfill and Reject packets will be - /// sent back to the Future that sent the outgoing request originally. + // Set up a WebSocket connection so that outgoing Prepare packets can be sent to it, + // incoming Prepare packets are buffered in a channel (until an IncomingService is added + // via the handle_incoming method), and ILP Fulfill and Reject packets will be + // sent back to the Future that sent the outgoing request originally. pub(crate) fn add_connection( &self, account: A, - connection: impl Stream - + Sink - + Send - + 'static, + ws_stream: impl Stream + Sink + Send + 'static, ) { let account_id = account.id(); - // Set up a channel to forward outgoing packets to the WebSocket connection - let (tx, rx) = unbounded(); - let (sink, stream) = connection.split(); + let (client_tx, client_rx) = unbounded(); + let (write, read) = ws_stream.split(); let (close_connection, valve) = Valve::new(); - let stream = valve.wrap(stream); - let stream = self.stream_valve.wrap(stream); - let forward_to_connection = sink - .send_all(rx.map_err(|_err| { - WsError::Tungstenite(io::Error::from(io::ErrorKind::ConnectionAborted).into()) - })) - .then(move |_| { + + // tx -> rx -> write -> our peer + // Responsible mainly for responding to Pings + let write_to_ws = client_rx.map(Ok).forward(write).then(move |_| { + async move { debug!( "Finished forwarding to WebSocket stream for account: {}", account_id ); + // When this is dropped, the read valve will close drop(close_connection); - Ok(()) - }); + Ok::<(), ()>(()) + } + }); + tokio::spawn(write_to_ws); - // Send pings every PING_INTERVAL until the connection closes or the Service is dropped - let tx_clone = tx.clone(); - let send_pings = valve - .wrap( - self.stream_valve - .wrap(Interval::new_interval(Duration::from_secs(PING_INTERVAL))), + // Process incoming messages depending on their type + let pending_outgoing = self.pending_outgoing.clone(); + let incoming_sender = self.incoming_sender.clone(); + let client_tx_clone = client_tx.clone(); + let handle_message_fn = move |msg: Message| { + handle_message( + msg, + client_tx_clone.clone(), + account.clone(), + pending_outgoing.clone(), + incoming_sender.clone(), ) - .map_err(|err| { - warn!("Timer error on Ping interval: {:?}", err); - }) - .for_each(move |_| { - if let Err(err) = tx_clone.unbounded_send(Message::Ping(Vec::with_capacity(0))) { - warn!( - "Error sending Ping on connection to account {}: {:?}", - account_id, err - ); - } - Ok(()) - }); - spawn(send_pings); + }; - // Set up a listener to handle incoming packets from the WebSocket connection - // TODO do we need all this cloning? - let pending_requests = self.pending_outgoing.clone(); - let incoming_sender = self.incoming_sender.clone(); - let tx_clone = tx.clone(); - let handle_incoming = stream.map_err(move |err| error!("Error reading from WebSocket stream for account {}: {:?}", account_id, err)).for_each(move |message| { - // Handle the packets based on whether they are an incoming request or a response to something we sent - if message.is_binary() { - match parse_ilp_packet(message) { - Ok((request_id, Packet::Prepare(prepare))) => { - trace!("Got incoming Prepare packet on request ID: {} {:?}", request_id, prepare); - incoming_sender.clone().unbounded_send((account.clone(), request_id, prepare)) - .map_err(|err| error!("Unable to buffer incoming request: {:?}", err)) - }, - Ok((request_id, Packet::Fulfill(fulfill))) => { - trace!("Got fulfill response to request id {}", request_id); - if let Some(channel) = (*pending_requests.lock()).remove(&request_id) { - channel.send(Ok(fulfill)).map_err(|fulfill| error!("Error forwarding Fulfill packet back to the Future that sent the Prepare: {:?}", fulfill)) - } else { - warn!("Got Fulfill packet that does not match an outgoing Prepare we sent: {:?}", fulfill); - Ok(()) - } - } - Ok((request_id, Packet::Reject(reject))) => { - trace!("Got reject response to request id {}", request_id); - if let Some(channel) = (*pending_requests.lock()).remove(&request_id) { - channel.send(Err(reject)).map_err(|reject| error!("Error forwarding Reject packet back to the Future that sent the Prepare: {:?}", reject)) - } else { - warn!("Got Reject packet that does not match an outgoing Prepare we sent: {:?}", reject); - Ok(()) - } - }, - Err(_) => { - debug!("Unable to parse ILP packet from BTP packet (if this is the first time this appears, the packet was probably the auth response)"); - // TODO Send error back - Ok(()) - } - } - } else if message.is_ping() { - trace!("Responding to Ping message from account {}", account.id()); - tx_clone.unbounded_send(Message::Pong(Vec::new())).map_err(|err| error!("Error sending Pong message back: {:?}", err)) - } else { - Ok(()) - } - }).then(move |result| { - debug!("Finished reading from WebSocket stream for account: {}", account_id); - result + // Close connections trigger + let read = valve.wrap(read); // close when `write_to_ws` calls `drop(connection)` + let read = self.stream_valve.wrap(read); + let read_from_ws = read.for_each(handle_message_fn).then(move |_| { + async move { + debug!( + "Finished reading from WebSocket stream for account: {}", + account_id + ); + Ok::<(), ()>(()) + } }); + tokio::spawn(read_from_ws); - let connections = self.connections.clone(); - let keep_connections_open = self.close_all_connections.clone(); - let handle_connection = handle_incoming - .select(forward_to_connection) - .then(move |_| { - let _ = keep_connections_open; - let mut connections = connections.write(); - connections.remove(&account_id); - debug!( - "WebSocket connection closed for account {} ({} connections still open)", - account_id, - connections.len() + // Send pings every PING_INTERVAL until the connection closes (when `drop(close_connection)` is called) + // or the Service is dropped (which will implicitly drop `close_all_connections`, closing the stream_valve) + let tx_clone = client_tx.clone(); + let ping_interval = time::interval(Duration::from_secs(PING_INTERVAL)); + let repeat_until_service_drops = self.stream_valve.wrap(ping_interval); + let send_pings = valve.wrap(repeat_until_service_drops).for_each(move |_| { + // For each tick send a ping + if let Err(err) = tx_clone.unbounded_send(PING.clone()) { + warn!( + "Error sending Ping on connection to account {}: {:?}", + account_id, err ); - Ok(()) - }); - spawn(handle_connection); + } + future::ready(()) + }); + tokio::spawn(send_pings); // Save the sender side of the channel so we have a way to forward outgoing requests to the WebSocket - self.connections.write().insert(account_id, tx); + self.connections.write().insert(account_id, client_tx); } /// Convert this BtpOutgoingService into a bidirectional BtpService by adding a handler for incoming requests. /// This will automatically pull all incoming Prepare packets from the channel buffer and call the IncomingService with them. - pub fn handle_incoming(self, incoming_handler: I) -> BtpService + pub async fn handle_incoming(self, incoming_handler: I) -> BtpService where I: IncomingService + Clone + Send + 'static, { @@ -234,14 +235,14 @@ where // the incoming Prepare packets they get in self.pending_incoming // Now that we're adding an incoming handler, this will spawn a task to read // all Prepare packets from the buffer, handle them, and send the responses back - let mut incoming_handler_clone = incoming_handler.clone(); let connections_clone = self.connections.clone(); - let handle_pending_incoming = self + let mut handle_pending_incoming = self .pending_incoming .lock() .take() - .expect("handle_incoming can only be called once") - .for_each(move |(account, request_id, prepare)| { + .expect("handle_incoming can only be called once"); + let handle_pending_incoming_fut = async move { + while let Some((account, request_id, prepare)) = handle_pending_incoming.next().await { let account_id = account.id(); let connections_clone = connections_clone.clone(); let request = IncomingRequest { @@ -254,37 +255,33 @@ where request.from.username(), request.from.id() ); - incoming_handler_clone - .handle_request(request) - .then(move |result| { - let packet = match result { - Ok(fulfill) => Packet::Fulfill(fulfill), - Err(reject) => Packet::Reject(reject), - }; - if let Some(connection) = connections_clone - .read() - .get(&account_id) { - let message = ilp_packet_to_ws_message(request_id, packet); - connection - .clone() - .unbounded_send(message) - .map_err(move |err| { - error!( - "Error sending response to account: {} {:?}", - account_id, err - ) - }) - } else { - error!("Error sending response to account: {}, connection was closed. {:?}", account_id, packet); - Err(()) - } - }) - }) - .then(move |_| { - trace!("Finished reading from pending_incoming buffer"); - Ok(()) - }); - spawn(handle_pending_incoming); + let mut handler = incoming_handler.clone(); + let packet = match handler.handle_request(request).await { + Ok(fulfill) => Packet::Fulfill(fulfill), + Err(reject) => Packet::Reject(reject), + }; + + if let Some(connection) = connections_clone.clone().read().get(&account_id) { + let message = ilp_packet_to_ws_message(request_id, packet); + let _ = connection.unbounded_send(message).map_err(move |err| { + error!( + "Error sending response to account: {} {:?}", + account_id, err + ) + }); + } else { + error!( + "Error sending response to account: {}, connection was closed. {:?}", + account_id, packet + ); + } + } + + trace!("Finished reading from pending_incoming buffer"); + Ok::<(), ()>(()) + }; + + tokio::spawn(handle_pending_incoming_fut); BtpService { outgoing: self, @@ -293,20 +290,20 @@ where } } +#[async_trait] impl OutgoingService for BtpOutgoingService where - O: OutgoingService + Clone, - A: BtpAccount + 'static, + O: OutgoingService + Send + Sync + Clone + 'static, + A: BtpAccount + Send + Sync + Clone + 'static, { - type Future = BoxedIlpFuture; - /// Send an outgoing request to one of the open connections. /// /// If there is no open connection for the Account specified in `request.to`, the /// request will be passed through to the `next` handler. - fn send_request(&mut self, request: OutgoingRequest) -> Self::Future { + async fn send_request(&mut self, request: OutgoingRequest) -> IlpResult { let account_id = request.to.id(); - if let Some(connection) = (*self.connections.read()).get(&account_id) { + let connections = self.connections.read().clone(); // have to clone here to avoid await errors + if let Some(connection) = connections.get(&account_id) { let request_id = random::(); let ilp_address = self.ilp_address.clone(); @@ -315,11 +312,14 @@ where let keep_connections_open = self.close_all_connections.clone(); trace!( - "Sending outgoing request {} to account {}", + "Sending outgoing request {} to {} ({})", request_id, + request.to.username(), account_id ); + // Connection is an unbounded sender which sends to the rx that + // forwards to the sink which sends the data over match connection.unbounded_send(ilp_packet_to_ws_message( request_id, Packet::Prepare(request.prepare), @@ -327,59 +327,53 @@ where Ok(_) => { let (sender, receiver) = oneshot::channel(); (*self.pending_outgoing.lock()).insert(request_id, sender); - Box::new( - receiver - .then(move |result| { - // Drop the trigger here since we've gotten the response - // and don't need to keep the connections open if this was the - // last thing we were waiting for - let _ = keep_connections_open; - result - }) - .map_err(move |err| { - error!( - "Sending request {} to account {} failed: {:?}", - request_id, account_id, err - ); - RejectBuilder { - code: ErrorCode::T00_INTERNAL_ERROR, - message: &[], - triggered_by: Some(&ilp_address), - data: &[], - } - .build() - }) - .and_then(|result| match result { - Ok(fulfill) => Ok(fulfill), - Err(reject) => Err(reject), - }), - ) + let result = receiver.await; + // Drop the trigger here since we've gotten the response + // and don't need to keep the connections open if this was the + // last thing we were waiting for + let _ = keep_connections_open; + match result { + // This can be either a reject or a fulfill packet + Ok(packet) => packet, + Err(err) => { + error!( + "Sending request {} to account {} failed: {:?}", + request_id, account_id, err + ); + Err(RejectBuilder { + code: ErrorCode::T00_INTERNAL_ERROR, + message: &[], + triggered_by: Some(&ilp_address), + data: &[], + } + .build()) + } + } } Err(send_error) => { error!( "Error sending websocket message for request {} to account {}: {:?}", request_id, account_id, send_error ); - let reject = RejectBuilder { + Err(RejectBuilder { code: ErrorCode::T00_INTERNAL_ERROR, message: &[], triggered_by: Some(&ilp_address), data: &[], } - .build(); - Box::new(err(reject)) + .build()) } } - } else if request.to.get_ilp_over_btp_url().is_some() - || request.to.get_ilp_over_btp_outgoing_token().is_some() - { - trace!( - "No open connection for account: {}, forwarding request to the next service", - request.to.id() - ); - Box::new(self.next.send_request(request)) } else { - Box::new(self.next.send_request(request)) + if request.to.get_ilp_over_btp_url().is_some() + || request.to.get_ilp_over_btp_outgoing_token().is_some() + { + trace!( + "No open connection for account: {}, forwarding request to the next service", + request.to.username() + ); + } + self.next.send_request(request).await } } } @@ -394,7 +388,7 @@ impl BtpService where I: IncomingService + Clone + Send + 'static, O: OutgoingService + Clone, - A: BtpAccount + 'static, + A: BtpAccount + Send + Sync + 'static, { /// Close all of the open WebSocket connections pub fn close(&self) { @@ -402,19 +396,19 @@ where } } +#[async_trait] impl OutgoingService for BtpService where - O: OutgoingService + Clone + Send + 'static, - A: BtpAccount + 'static, + I: Send, // This is a async/await requirement + O: OutgoingService + Send + Sync + Clone + 'static, + A: BtpAccount + Send + Sync + Clone + 'static, { - type Future = BoxedIlpFuture; - /// Send an outgoing request to one of the open connections. /// /// If there is no open connection for the Account specified in `request.to`, the /// request will be passed through to the `next` handler. - fn send_request(&mut self, request: OutgoingRequest) -> Self::Future { - self.outgoing.send_request(request) + async fn send_request(&mut self, request: OutgoingRequest) -> IlpResult { + self.outgoing.send_request(request).await } } @@ -460,42 +454,31 @@ fn parse_ilp_packet(message: Message) -> Result<(u32, Packet), ()> { } fn ilp_packet_to_ws_message(request_id: u32, packet: Packet) -> Message { - match packet { - Packet::Prepare(prepare) => { - let data = BytesMut::from(prepare).to_vec(); - let btp_packet = BtpMessage { - request_id, - protocol_data: vec![ProtocolData { - protocol_name: "ilp".to_string(), - content_type: ContentType::ApplicationOctetStream, - data, - }], - }; - Message::binary(btp_packet.to_bytes()) + let (data, is_response) = match packet { + Packet::Prepare(prepare) => (BytesMut::from(prepare).to_vec(), false), + Packet::Fulfill(fulfill) => (BytesMut::from(fulfill).to_vec(), true), + Packet::Reject(reject) => (BytesMut::from(reject).to_vec(), true), + }; + let btp_packet = if is_response { + BtpMessage { + request_id, + protocol_data: vec![ProtocolData { + protocol_name: "ilp".to_string(), + content_type: ContentType::ApplicationOctetStream, + data, + }], } - Packet::Fulfill(fulfill) => { - let data = BytesMut::from(fulfill).to_vec(); - let btp_packet = BtpResponse { - request_id, - protocol_data: vec![ProtocolData { - protocol_name: "ilp".to_string(), - content_type: ContentType::ApplicationOctetStream, - data, - }], - }; - Message::binary(btp_packet.to_bytes()) - } - Packet::Reject(reject) => { - let data = BytesMut::from(reject).to_vec(); - let btp_packet = BtpResponse { - request_id, - protocol_data: vec![ProtocolData { - protocol_name: "ilp".to_string(), - content_type: ContentType::ApplicationOctetStream, - data, - }], - }; - Message::binary(btp_packet.to_bytes()) + .to_bytes() + } else { + BtpResponse { + request_id, + protocol_data: vec![ProtocolData { + protocol_name: "ilp".to_string(), + content_type: ContentType::ApplicationOctetStream, + data, + }], } - } + .to_bytes() + }; + Message::binary(btp_packet) } diff --git a/crates/interledger-btp/src/wrapped_ws.rs b/crates/interledger-btp/src/wrapped_ws.rs new file mode 100644 index 000000000..230bc7417 --- /dev/null +++ b/crates/interledger-btp/src/wrapped_ws.rs @@ -0,0 +1,92 @@ +use futures::stream::Stream; +use futures::Sink; +use log::warn; +use pin_project::pin_project; +use std::pin::Pin; +use std::task::{Context, Poll}; +use warp::ws::Message; + +/// Wrapper struct to unify the Tungstenite WebSocket connection from connect_async +/// with the Warp websocket connection from ws.upgrade. Stream and Sink are re-implemented +/// for this struct, normalizing it to use Tungstenite's messages and a wrapped error type +#[pin_project] +#[derive(Clone)] +pub(crate) struct WsWrap { + #[pin] + pub(crate) connection: W, +} + +impl Stream for WsWrap +where + W: Stream, +{ + type Item = tungstenite::Message; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + let this = self.project(); + match this.connection.poll_next(cx) { + Poll::Pending => Poll::Pending, + Poll::Ready(val) => match val { + Some(v) => { + let v = convert_msg(v); + Poll::Ready(Some(v)) + } + None => Poll::Ready(None), + }, + } + } +} + +impl Sink for WsWrap +where + W: Sink, +{ + type Error = W::Error; + + fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.project(); + this.connection.poll_ready(cx) + } + + fn start_send(self: Pin<&mut Self>, item: tungstenite::Message) -> Result<(), Self::Error> { + let this = self.project(); + let item = match item { + tungstenite::Message::Binary(data) => Message::binary(data), + tungstenite::Message::Text(data) => Message::text(data), + // Ignore other message types because warp's WebSocket type doesn't + // allow us to send any other types of messages + // TODO make sure warp's websocket responds to pings and/or sends them to keep the + // connection alive + _ => return Ok(()), + }; + this.connection.start_send(item) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.project(); + this.connection.poll_flush(cx) + } + + fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.project(); + this.connection.poll_close(cx) + } +} + +fn convert_msg(message: Message) -> tungstenite::Message { + if message.is_ping() { + tungstenite::Message::Ping(message.into_bytes()) + } else if message.is_binary() { + tungstenite::Message::Binary(message.into_bytes()) + } else if message.is_text() { + tungstenite::Message::Text(message.to_str().unwrap_or_default().to_string()) + } else if message.is_close() { + tungstenite::Message::Close(None) + } else { + warn!( + "Got unexpected websocket message, closing connection: {:?}", + message + ); + tungstenite::Message::Close(None) + } +} diff --git a/crates/interledger-http/Cargo.toml b/crates/interledger-http/Cargo.toml index 80e660536..a659fe81e 100644 --- a/crates/interledger-http/Cargo.toml +++ b/crates/interledger-http/Cargo.toml @@ -8,23 +8,25 @@ edition = "2018" repository = "https://github.com/interledger-rs/interledger-rs" [dependencies] -bytes = { version = "0.4.12", default-features = false } -futures = { version = "0.1.29", default-features = false } +bytes = { version = "0.5", default-features = false } +futures = { version = "0.3", default-features = false } interledger-packet = { path = "../interledger-packet", version = "^0.4.0", default-features = false } interledger-service = { path = "../interledger-service", version = "^0.4.0", default-features = false } log = { version = "0.4.8", default-features = false } -reqwest = { version = "0.9.22", default-features = false, features = ["default-tls"] } +reqwest = { version = "0.10.0", default-features = false, features = ["default-tls"] } url = { version = "2.1.0", default-features = false } -warp = { version = "0.1.20", default-features = false } +warp = { version = "0.2", default-features = false } serde = { version = "1.0.101", default-features = false, features = ["derive"] } serde_json = { version = "1.0.41", default-features = false } serde_path_to_error = { version = "0.1", default-features = false } -http = { version = "0.1.18", default-features = false } +http = { version = "0.2.0", default-features = false } chrono = { version = "0.4.9", features = ["clock"], default-features = false } regex = { version ="1.3.1", default-features = false, features = ["std"] } lazy_static = { version ="1.4.0", default-features = false } mime = { version ="0.3.14", default-features = false } -secrecy = "0.5.2" +secrecy = "0.6" +async-trait = "0.1.22" [dev-dependencies] uuid = { version = "0.8.1", features=["v4"]} +tokio = { version = "0.2.6", features = ["rt-core", "macros"]} diff --git a/crates/interledger-http/src/client.rs b/crates/interledger-http/src/client.rs index bb55a11e9..d45700674 100644 --- a/crates/interledger-http/src/client.rs +++ b/crates/interledger-http/src/client.rs @@ -1,20 +1,31 @@ use super::{HttpAccount, HttpStore}; +use async_trait::async_trait; use bytes::BytesMut; -use futures::{future::result, Future, Stream}; -use interledger_packet::{Address, ErrorCode, Fulfill, Packet, Reject, RejectBuilder}; +use futures::future::TryFutureExt; +use interledger_packet::{Address, ErrorCode, Packet, RejectBuilder}; use interledger_service::*; use log::{error, trace}; use reqwest::{ header::{HeaderMap, HeaderName, HeaderValue}, - r#async::{Chunk, Client, ClientBuilder, Response as HttpResponse}, + Client, ClientBuilder, Response as HttpResponse, }; use secrecy::{ExposeSecret, SecretString}; use std::{convert::TryFrom, marker::PhantomData, sync::Arc, time::Duration}; +/// The HttpClientService implements [OutgoingService](../../interledger_service/trait.OutgoingService) +/// for sending ILP Prepare packets over to the HTTP URL associated with the provided account +/// If no [ILP-over-HTTP](https://interledger.org/rfcs/0035-ilp-over-http) URL is specified for +/// the account in the request, then it is forwarded to the next service. #[derive(Clone)] pub struct HttpClientService { + /// An HTTP client configured with a 30 second timeout by default. It is used to send the + /// ILP over HTTP messages to the peer client: Client, + /// The store used by the client to get the node's ILP Address, + /// used to populate the `triggered_by` field in Reject packets store: Arc, + /// The next outgoing service to which non ILP-over-HTTP requests should + /// be forwarded to next: O, account_type: PhantomData, } @@ -25,6 +36,7 @@ where O: OutgoingService + Clone, A: HttpAccount, { + /// Constructs the HttpClientService pub fn new(store: S, next: O) -> Self { let mut headers = HeaderMap::with_capacity(2); headers.insert( @@ -46,18 +58,18 @@ where } } +#[async_trait] impl OutgoingService for HttpClientService where - S: AddressStore + HttpStore, - O: OutgoingService, - A: HttpAccount, + S: AddressStore + HttpStore + Clone, + O: OutgoingService + Clone + Sync + Send, + A: HttpAccount + Clone + Sync + Send, { - type Future = BoxedIlpFuture; - /// Send an OutgoingRequest to a peer that implements the ILP-Over-HTTP. - fn send_request(&mut self, request: OutgoingRequest) -> Self::Future { + async fn send_request(&mut self, request: OutgoingRequest) -> IlpResult { let ilp_address = self.store.get_ilp_address(); let ilp_address_clone = ilp_address.clone(); + let self_clone = self.clone(); if let Some(url) = request.to.get_http_url() { trace!( "Sending outgoing ILP over HTTP packet to account: {} (URL: {})", @@ -68,51 +80,49 @@ where .to .get_http_auth_token() .unwrap_or_else(|| SecretString::new("".to_owned())); - Box::new( - self.client - .post(url.as_ref()) - .header( - "authorization", - &format!("Bearer {}", token.expose_secret()), - ) - .body(BytesMut::from(request.prepare).freeze()) - .send() - .map_err(move |err| { - error!("Error sending HTTP request: {:?}", err); - let code = if err.is_client_error() { - ErrorCode::F00_BAD_REQUEST - } else { - ErrorCode::T01_PEER_UNREACHABLE - }; - let message = if let Some(status) = err.status() { - format!("Error sending ILP over HTTP request: {}", status) - } else if let Some(err) = err.get_ref() { - format!("Error sending ILP over HTTP request: {:?}", err) - } else { - "Error sending ILP over HTTP request".to_string() - }; - RejectBuilder { - code, - message: message.as_str().as_bytes(), - triggered_by: Some(&ilp_address), - data: &[], + let header = format!("Bearer {}", token.expose_secret()); + let body = request.prepare.as_ref().to_owned(); + let resp = self_clone + .client + .post(url.as_ref()) + .header("authorization", &header) + .body(body) + .send() + .map_err(move |err| { + error!("Error sending HTTP request: {:?}", err); + let mut code = ErrorCode::T01_PEER_UNREACHABLE; + if let Some(status) = err.status() { + if status.is_client_error() { + code = ErrorCode::F00_BAD_REQUEST } - .build() - }) - .and_then(move |resp| parse_packet_from_response(resp, ilp_address_clone)), - ) + }; + + let message = format!("Error sending ILP over HTTP request: {}", err); + RejectBuilder { + code, + message: message.as_bytes(), + triggered_by: Some(&ilp_address), + data: &[], + } + .build() + }) + .await?; + parse_packet_from_response(resp, ilp_address_clone).await } else { - Box::new(self.next.send_request(request)) + self.next.send_request(request).await } } } -fn parse_packet_from_response( - response: HttpResponse, - ilp_address: Address, -) -> impl Future { - let ilp_address_clone = ilp_address.clone(); - result(response.error_for_status().map_err(|err| { +/// Parses an ILP over HTTP response. +/// +/// # Errors +/// 1. If the response's status code is an error +/// 1. If the response's body cannot be parsed as bytes +/// 1. If the response's body is not a valid Packet (Fulfill or Reject) +/// 1. If the packet is a Reject packet +async fn parse_packet_from_response(response: HttpResponse, ilp_address: Address) -> IlpResult { + let response = response.error_for_status().map_err(|err| { error!("HTTP error sending ILP over HTTP packet: {:?}", err); let code = if let Some(status) = err.status() { if status.is_client_error() { @@ -131,34 +141,33 @@ fn parse_packet_from_response( data: &[], } .build() - })) - .and_then(move |response: HttpResponse| { - let ilp_address_clone = ilp_address.clone(); - let decoder = response.into_body(); - decoder.concat2().map_err(move |err| { + })?; + + let ilp_address_clone = ilp_address.clone(); + let body = response + .bytes() + .map_err(|err| { error!("Error getting HTTP response body: {:?}", err); RejectBuilder { code: ErrorCode::T01_PEER_UNREACHABLE, message: &[], - triggered_by: Some(&ilp_address_clone.clone()), + triggered_by: Some(&ilp_address_clone), data: &[], } .build() }) - }) - .and_then(move |body: Chunk| { - // TODO can we get the body as a BytesMut so we don't need to copy? - let body = BytesMut::from(body.to_vec()); - match Packet::try_from(body) { - Ok(Packet::Fulfill(fulfill)) => Ok(fulfill), - Ok(Packet::Reject(reject)) => Err(reject), - _ => Err(RejectBuilder { - code: ErrorCode::T01_PEER_UNREACHABLE, - message: &[], - triggered_by: Some(&ilp_address_clone.clone()), - data: &[], - } - .build()), + .await?; + // TODO can we get the body as a BytesMut so we don't need to copy? + let body = BytesMut::from(body.as_ref()); + match Packet::try_from(body) { + Ok(Packet::Fulfill(fulfill)) => Ok(fulfill), + Ok(Packet::Reject(reject)) => Err(reject), + _ => Err(RejectBuilder { + code: ErrorCode::T01_PEER_UNREACHABLE, + message: &[], + triggered_by: Some(&ilp_address_clone), + data: &[], } - }) + .build()), + } } diff --git a/crates/interledger-http/src/error/error_types.rs b/crates/interledger-http/src/error/error_types.rs index b307c2649..63c9c9719 100644 --- a/crates/interledger-http/src/error/error_types.rs +++ b/crates/interledger-http/src/error/error_types.rs @@ -1,37 +1,46 @@ -// APIs should implement their own `ApiErrorType`s to provide more detailed information -// about what were the problem, for example, `JSON_SYNTAX_TYPE` or `ACCOUNT_NOT_FOUND_TYPE`. - use super::{ApiError, ApiErrorType, ProblemType}; use http::StatusCode; use lazy_static::lazy_static; -// Default errors +// Common HTTP errors + #[allow(dead_code)] +/// 400 Bad Request HTTP Status Code pub const DEFAULT_BAD_REQUEST_TYPE: ApiErrorType = ApiErrorType { r#type: &ProblemType::Default, title: "Bad Request", status: StatusCode::BAD_REQUEST, }; + +/// 500 Internal Server Error HTTP Status Code pub const DEFAULT_INTERNAL_SERVER_ERROR_TYPE: ApiErrorType = ApiErrorType { r#type: &ProblemType::Default, title: "Internal Server Error", status: StatusCode::INTERNAL_SERVER_ERROR, }; + +/// 401 Unauthorized HTTP Status Code pub const DEFAULT_UNAUTHORIZED_TYPE: ApiErrorType = ApiErrorType { r#type: &ProblemType::Default, title: "Unauthorized", status: StatusCode::UNAUTHORIZED, }; + +/// 404 Not Found HTTP Status Code pub const DEFAULT_NOT_FOUND_TYPE: ApiErrorType = ApiErrorType { r#type: &ProblemType::Default, title: "Not Found", status: StatusCode::NOT_FOUND, }; + +/// 405 Method Not Allowed HTTP Status Code pub const DEFAULT_METHOD_NOT_ALLOWED_TYPE: ApiErrorType = ApiErrorType { r#type: &ProblemType::Default, title: "Method Not Allowed", status: StatusCode::METHOD_NOT_ALLOWED, }; + +/// 409 Conflict HTTP Status Code (used for Idempotency Conflicts) pub const DEFAULT_IDEMPOTENT_CONFLICT_TYPE: ApiErrorType = ApiErrorType { r#type: &ProblemType::Default, title: "Provided idempotency key is tied to other input", @@ -39,28 +48,36 @@ pub const DEFAULT_IDEMPOTENT_CONFLICT_TYPE: ApiErrorType = ApiErrorType { }; // ILP over HTTP specific errors + +/// ILP over HTTP invalid packet error type (400 Bad Request) pub const INVALID_ILP_PACKET_TYPE: ApiErrorType = ApiErrorType { r#type: &ProblemType::InterledgerHttpApi("ilp-over-http/invalid-packet"), title: "Invalid Packet", status: StatusCode::BAD_REQUEST, }; -// JSON deserialization errors +/// Wrong JSON syntax error type (400 Bad Request) pub const JSON_SYNTAX_TYPE: ApiErrorType = ApiErrorType { r#type: &ProblemType::InterledgerHttpApi("json-syntax"), title: "JSON Syntax Error", status: StatusCode::BAD_REQUEST, }; + +/// Wrong JSON data error type (400 Bad Request) pub const JSON_DATA_TYPE: ApiErrorType = ApiErrorType { r#type: &ProblemType::InterledgerHttpApi("json-data"), title: "JSON Data Error", status: StatusCode::BAD_REQUEST, }; + +/// JSON EOF error type (400 Bad Request) pub const JSON_EOF_TYPE: ApiErrorType = ApiErrorType { r#type: &ProblemType::InterledgerHttpApi("json-eof"), title: "JSON Unexpected EOF", status: StatusCode::BAD_REQUEST, }; + +/// JSON IO error type (400 Bad Request) pub const JSON_IO_TYPE: ApiErrorType = ApiErrorType { r#type: &ProblemType::InterledgerHttpApi("json-io"), title: "JSON IO Error", @@ -68,13 +85,15 @@ pub const JSON_IO_TYPE: ApiErrorType = ApiErrorType { }; // Account specific errors + +/// Account Not Found error type (404 Not Found) pub const ACCOUNT_NOT_FOUND_TYPE: ApiErrorType = ApiErrorType { r#type: &ProblemType::InterledgerHttpApi("accounts/account-not-found"), title: "Account Not Found", status: StatusCode::NOT_FOUND, }; -// Node settings specific errors +/// Invalid Account Id error type (400 Bad Request) pub const INVALID_ACCOUNT_ID_TYPE: ApiErrorType = ApiErrorType { r#type: &ProblemType::InterledgerHttpApi("settings/invalid-account-id"), title: "Invalid Account Id", @@ -84,7 +103,8 @@ pub const INVALID_ACCOUNT_ID_TYPE: ApiErrorType = ApiErrorType { // String used for idempotency errors pub static IDEMPOTENCY_CONFLICT_ERR: &str = "Provided idempotency key is tied to other input"; -// Idempotency errors +/// 409 Conflict HTTP Status Code (used for Idempotency Conflicts) +// TODO: Remove this since it is a duplicate of DEFAULT_IDEMPOTENT_CONFLICT_TYPE pub const IDEMPOTENT_STORE_CALL_ERROR_TYPE: ApiErrorType = ApiErrorType { r#type: &ProblemType::Default, title: "Store idempotency error", @@ -92,6 +112,8 @@ pub const IDEMPOTENT_STORE_CALL_ERROR_TYPE: ApiErrorType = ApiErrorType { }; lazy_static! { + /// Error which must be returned when the same idempotency key is + /// used for more than 1 input pub static ref IDEMPOTENT_STORE_CALL_ERROR: ApiError = ApiError::from_api_error_type(&IDEMPOTENT_STORE_CALL_ERROR_TYPE) .detail("Could not process idempotent data in store"); diff --git a/crates/interledger-http/src/error/mod.rs b/crates/interledger-http/src/error/mod.rs index dcdd70581..4c3c3e8ff 100644 --- a/crates/interledger-http/src/error/mod.rs +++ b/crates/interledger-http/src/error/mod.rs @@ -1,3 +1,5 @@ +/// APIs should implement their own `ApiErrorType`s to provide more detailed information +/// about what were the problem, for example, `JSON_SYNTAX_TYPE` or `ACCOUNT_NOT_FOUND_TYPE`. mod error_types; pub use error_types::*; @@ -12,7 +14,12 @@ use std::{ error::Error as StdError, fmt::{self, Display}, }; -use warp::{reject::custom, reply::json, reply::Response, Rejection, Reply}; +use warp::{ + reject::{custom, Reject}, + reply::json, + reply::Response, + Rejection, Reply, +}; /// API error type prefix of problems. /// This URL prefix is currently not published but we assume that in the future. @@ -65,6 +72,7 @@ pub struct ApiError { pub extension_members: Option>, } +/// Distinguishes between RFC7807 and Interledger API Errors #[derive(Clone, Copy, Debug)] pub enum ProblemType { /// `Default` is a [pre-defined value](https://tools.ietf.org/html/rfc7807#section-4.2) which is @@ -76,10 +84,14 @@ pub enum ProblemType { InterledgerHttpApi(&'static str), } +/// Error type used as a basis for creating Warp-compatible Errors #[derive(Clone, Copy, Debug)] pub struct ApiErrorType { + /// Interledger or RFC7807 Error pub r#type: &'static ProblemType, + /// The Title to be used for the error page pub title: &'static str, + /// The HTTP Status Code for the error pub status: http::StatusCode, } @@ -105,6 +117,7 @@ where } impl ApiError { + /// Constructs an API Error from a [ApiErrorType](./struct.ApiErrorType.html) pub fn from_api_error_type(problem_type: &ApiErrorType) -> Self { ApiError { r#type: problem_type.r#type, @@ -118,39 +131,49 @@ impl ApiError { // Note that we should basically avoid using the following default errors because // we should provide more detailed information for developers + #[allow(dead_code)] + /// Returns a Bad Request [ApiError](./struct.ApiError.html) pub fn bad_request() -> Self { ApiError::from_api_error_type(&DEFAULT_BAD_REQUEST_TYPE) } + /// Returns an Internal Server Error [ApiError](./struct.ApiError.html) pub fn internal_server_error() -> Self { ApiError::from_api_error_type(&DEFAULT_INTERNAL_SERVER_ERROR_TYPE) } + /// Returns an Unauthorized [ApiError](./struct.ApiError.html) pub fn unauthorized() -> Self { ApiError::from_api_error_type(&DEFAULT_UNAUTHORIZED_TYPE) } #[allow(dead_code)] + /// Returns an Error Not Found [ApiError](./struct.ApiError.html) pub fn not_found() -> Self { ApiError::from_api_error_type(&DEFAULT_NOT_FOUND_TYPE) } #[allow(dead_code)] + /// Returns a Method Not Found [ApiError](./struct.ApiError.html) pub fn method_not_allowed() -> Self { ApiError::from_api_error_type(&DEFAULT_METHOD_NOT_ALLOWED_TYPE) } + /// Returns an Account not Found [ApiError](./struct.ApiError.html) pub fn account_not_found() -> Self { ApiError::from_api_error_type(&ACCOUNT_NOT_FOUND_TYPE) .detail("Username was not found.".to_owned()) } #[allow(dead_code)] + /// Returns an Idempotency Conflict [ApiError](./struct.ApiError.html) + /// via the [default idempotency conflict ApiErrorType](./error_types/constant.DEFAULT_IDEMPOTENT_CONFLICT_TYPE.html) pub fn idempotency_conflict() -> Self { ApiError::from_api_error_type(&DEFAULT_IDEMPOTENT_CONFLICT_TYPE) } + /// Returns an Invalid Account Id [ApiError](./struct.ApiError.html) pub fn invalid_account_id(invalid_account_id: Option<&str>) -> Self { let detail = match invalid_account_id { Some(invalid_account_id) => match invalid_account_id.len() { @@ -162,10 +185,12 @@ impl ApiError { ApiError::from_api_error_type(&INVALID_ACCOUNT_ID_TYPE).detail(detail) } + /// Returns an Invalid ILP over HTTP [ApiError](./struct.ApiError.html) pub fn invalid_ilp_packet() -> Self { ApiError::from_api_error_type(&INVALID_ILP_PACKET_TYPE) } + /// Sets the [`detail`](./struct.ApiError.html#structfield.detail) field pub fn detail(mut self, detail: T) -> Self where T: Into, @@ -175,6 +200,7 @@ impl ApiError { } #[allow(dead_code)] + /// Sets the [`instance`](./struct.ApiError.html#structfield.instance) field pub fn instance(mut self, instance: T) -> Self where T: Into, @@ -183,8 +209,9 @@ impl ApiError { self } - pub fn extension_members(mut self, extension_members: Option>) -> Self { - self.extension_members = extension_members; + /// Sets the [`extension_members`](./struct.ApiError.html#structfield.extension_members) field + pub fn extension_members(mut self, extension_members: Map) -> Self { + self.extension_members = Some(extension_members); self } @@ -236,6 +263,8 @@ impl From for Rejection { } } +impl Reject for ApiError {} + lazy_static! { static ref MISSING_FIELD_REGEX: Regex = Regex::new("missing field `(.*)`").unwrap(); } @@ -248,6 +277,7 @@ pub struct JsonDeserializeError { } impl StdError for JsonDeserializeError {} +impl Reject for JsonDeserializeError {} impl Display for JsonDeserializeError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { @@ -290,15 +320,14 @@ impl Reply for JsonDeserializeError { Category::Io => &JSON_IO_TYPE, }; let detail = self.detail; - let extension_members = match extension_members.keys().len() { - 0 => None, - _ => Some(extension_members), - }; - ApiError::from_api_error_type(api_error_type) - .detail(detail) - .extension_members(extension_members) - .into_response() + let mut error = ApiError::from_api_error_type(api_error_type).detail(detail); + + if extension_members.keys().len() > 0 { + error = error.extension_members(extension_members); + } + + error.into_response() } } @@ -309,12 +338,12 @@ impl From for Rejection { } // Receives `ApiError`s and `JsonDeserializeError` and return it in the RFC7807 format. -pub fn default_rejection_handler(err: warp::Rejection) -> Result { - if let Some(api_error) = err.find_cause::() { +pub async fn default_rejection_handler(err: warp::Rejection) -> Result { + if let Some(api_error) = err.find::() { Ok(api_error.clone().into_response()) - } else if let Some(json_error) = err.find_cause::() { + } else if let Some(json_error) = err.find::() { Ok(json_error.clone().into_response()) - } else if err.status() == http::status::StatusCode::METHOD_NOT_ALLOWED { + } else if err.find::().is_some() { Ok(ApiError::from_api_error_type(&DEFAULT_METHOD_NOT_ALLOWED_TYPE).into_response()) } else { Err(err) diff --git a/crates/interledger-http/src/lib.rs b/crates/interledger-http/src/lib.rs index 29eec8b03..3a8734afd 100644 --- a/crates/interledger-http/src/lib.rs +++ b/crates/interledger-http/src/lib.rs @@ -2,76 +2,87 @@ //! //! Client and server implementations of the [ILP-Over-HTTP](https://github.com/interledger/rfcs/blob/master/0035-ilp-over-http/0035-ilp-over-http.md) bilateral communication protocol. //! This protocol is intended primarily for server-to-server communication between peers on the Interledger network. -use bytes::Buf; -use error::*; -use futures::Future; +use async_trait::async_trait; +use bytes::Bytes; use interledger_service::{Account, Username}; use mime::Mime; use secrecy::SecretString; use serde::de::DeserializeOwned; use url::Url; -use warp::{self, filters::body::FullBody, Filter, Rejection}; +use warp::{self, Filter, Rejection}; +/// [ILP over HTTP](https://interledger.org/rfcs/0035-ilp-over-http/) Outgoing Service mod client; -mod server; - -// So that settlement engines can use errors +/// [RFC7807](https://tools.ietf.org/html/rfc7807) compliant errors pub mod error; +/// [ILP over HTTP](https://interledger.org/rfcs/0035-ilp-over-http/) API (implemented with [Warp](https://docs.rs/warp/0.2.0/warp/)) +mod server; pub use self::client::HttpClientService; pub use self::server::HttpServer; +/// Extension trait for [Account](../interledger_service/trait.Account.html) with [ILP over HTTP](https://interledger.org/rfcs/0035-ilp-over-http/) related information pub trait HttpAccount: Account { + /// Returns the HTTP URL corresponding to this account fn get_http_url(&self) -> Option<&Url>; + /// Returns the HTTP token which is sent as an HTTP header on each ILP over HTTP request fn get_http_auth_token(&self) -> Option; } /// The interface for Stores that can be used with the HttpServerService. // TODO do we need all of these constraints? +#[async_trait] pub trait HttpStore: Clone + Send + Sync + 'static { type Account: HttpAccount; /// Load account details based on the full HTTP Authorization header /// received on the incoming HTTP request. - fn get_account_from_http_auth( + async fn get_account_from_http_auth( &self, username: &Username, token: &str, - ) -> Box + Send>; + ) -> Result; } +// TODO: Do we really need this custom deserialization function? +// You'd expect that Serde would be able to handle this. +/// Helper function to deserialize JSON inside Warp +/// The content-type MUST be application/json and if a charset +/// is specified, it MUST be UTF-8 pub fn deserialize_json( ) -> impl Filter + Copy { warp::header::("content-type") - .and(warp::body::concat()) - .and_then(|content_type: String, buf: FullBody| { - let mime_type: Mime = content_type.parse().map_err::(|_| { - error::ApiError::bad_request() - .detail("Invalid content-type header.") - .into() - })?; - if mime_type.type_() != mime::APPLICATION_JSON.type_() { - return Err(error::ApiError::bad_request() - .detail("Invalid content-type.") - .into()); - } else if let Some(charset) = mime_type.get_param("charset") { - // Charset should be UTF-8 - // https://tools.ietf.org/html/rfc8259#section-8.1 - if charset != mime::UTF_8 { - return Err(error::ApiError::bad_request() - .detail("Charset should be UTF-8.") - .into()); + .and(warp::body::bytes()) + .and_then(|content_type: String, buf: Bytes| { + async move { + let mime_type: Mime = content_type.parse().map_err(|_| { + Rejection::from( + error::ApiError::bad_request().detail("Invalid content-type header."), + ) + })?; + if mime_type.type_() != mime::APPLICATION_JSON.type_() { + return Err(Rejection::from( + error::ApiError::bad_request().detail("Invalid content-type."), + )); + } else if let Some(charset) = mime_type.get_param("charset") { + // Charset should be UTF-8 + // https://tools.ietf.org/html/rfc8259#section-8.1 + if charset != mime::UTF_8 { + return Err(Rejection::from( + error::ApiError::bad_request().detail("Charset should be UTF-8."), + )); + } } - } - let deserializer = &mut serde_json::Deserializer::from_slice(&buf.bytes()); - serde_path_to_error::deserialize(deserializer).map_err(|err| { - warp::reject::custom(JsonDeserializeError { - category: err.inner().classify(), - detail: err.inner().to_string(), - path: err.path().clone(), + let deserializer = &mut serde_json::Deserializer::from_slice(&buf); + serde_path_to_error::deserialize(deserializer).map_err(|err| { + warp::reject::custom(error::JsonDeserializeError { + category: err.inner().classify(), + detail: err.inner().to_string(), + path: err.path().clone(), + }) }) - }) + } }) } @@ -86,49 +97,57 @@ mod tests { string_value: String, } - #[test] - fn deserialize_json_header() { + #[tokio::test] + async fn deserialize_json_header() { let json_filter = deserialize_json::(); let body_correct = r#"{"string_value": "some string value"}"#; let body_incorrect = r#"{"other_key": 0}"#; // `content-type` should be provided. - assert_eq!(request().body(body_correct).matches(&json_filter), false); + assert_eq!( + request().body(body_correct).matches(&json_filter).await, + false + ); // Should accept only "application/json" or "application/json; charset=utf-8" assert_eq!( request() .body(body_correct) .header("content-type", "text/plain") - .matches(&json_filter), + .matches(&json_filter) + .await, false ); assert_eq!( request() .body(body_correct) .header("content-type", "application/json") - .matches(&json_filter), + .matches(&json_filter) + .await, true ); assert_eq!( request() .body(body_correct) .header("content-type", "application/json; charset=ascii") - .matches(&json_filter), + .matches(&json_filter) + .await, false ); assert_eq!( request() .body(body_correct) .header("content-type", "application/json; charset=utf-8") - .matches(&json_filter), + .matches(&json_filter) + .await, true ); assert_eq!( request() .body(body_correct) .header("content-type", "application/json; charset=UTF-8") - .matches(&json_filter), + .matches(&json_filter) + .await, true ); @@ -137,14 +156,16 @@ mod tests { request() .body(body_incorrect) .header("content-type", "application/json") - .matches(&json_filter), + .matches(&json_filter) + .await, false ); assert_eq!( request() .body(body_incorrect) .header("content-type", "application/json; charset=utf-8") - .matches(&json_filter), + .matches(&json_filter) + .await, false ); } diff --git a/crates/interledger-http/src/server.rs b/crates/interledger-http/src/server.rs index 1700a1f3e..8954d1a52 100644 --- a/crates/interledger-http/src/server.rs +++ b/crates/interledger-http/src/server.rs @@ -1,105 +1,144 @@ use super::{error::*, HttpStore}; -use bytes::{buf::Buf, Bytes, BytesMut}; -use futures::{ - future::{err, Either, FutureResult}, - Future, -}; +use bytes::{Bytes, BytesMut}; +use futures::TryFutureExt; use interledger_packet::Prepare; use interledger_service::Username; use interledger_service::{IncomingRequest, IncomingService}; use log::error; use secrecy::{ExposeSecret, SecretString}; -use std::{convert::TryFrom, net::SocketAddr}; -use warp::{self, Filter, Rejection}; +use std::convert::TryFrom; +use std::net::SocketAddr; +use warp::{Filter, Rejection}; /// Max message size that is allowed to transfer from a request or a message. pub const MAX_PACKET_SIZE: u64 = 40000; +/// The offset after which the bearer token should be in an ILP over HTTP request +/// e.g. in `token = "Bearer: MyAuthToken"`, `MyAuthToken` can be taken via token[BEARER_TOKEN_START..] pub const BEARER_TOKEN_START: usize = 7; /// A warp filter that parses incoming ILP-Over-HTTP requests, validates the authorization, /// and passes the request to an IncomingService handler. #[derive(Clone)] pub struct HttpServer { + /// The next [incoming service](../interledger_service/trait.IncomingService.html) incoming: I, + /// A store which implements [`HttpStore`](trait.HttpStore.html) store: S, } -impl HttpServer +#[inline] +/// Returns the account which matches the provided username/password combination +/// from the store, or returns an error if the account was not found or if the +/// credentials were incorrect +async fn get_account( + store: S, + path_username: &Username, + password: &SecretString, +) -> Result where - I: IncomingService + Clone + Send + Sync + 'static, S: HttpStore, +{ + if password.expose_secret().len() < BEARER_TOKEN_START { + return Err(()); + } + store + .get_account_from_http_auth( + &path_username, + &password.expose_secret()[BEARER_TOKEN_START..], + ) + .await +} + +#[inline] +/// Implements ILP over HTTP. If account authentication is valid +/// and the provided packet can be parsed as a +/// [Prepare](../../interledger_packet/struct.Prepare.html) packet, +/// then it is forwarded to the next incoming service which will return +/// an Ok result if the response is a [Fulfill](../../interledger_packet/struct.Fulfill.html). +/// +/// # Errors +/// 1. Unauthorized account if invalid credentials are provided +/// 1. The provided `body` could not be parsed as a Prepare packet +/// 1. A Reject packet was returned by the next incoming service +async fn ilp_over_http( + path_username: Username, + password: SecretString, + body: Bytes, + store: S, + incoming: I, +) -> Result +where + S: HttpStore, + I: IncomingService + Clone, +{ + let mut incoming = incoming.clone(); + let account = get_account(store, &path_username, &password) + .map_err(|_| -> Rejection { + error!("Invalid authorization provided for user: {}", path_username); + ApiError::unauthorized().into() + }) + .await?; + + let buffer = bytes::BytesMut::from(body.as_ref()); + if let Ok(prepare) = Prepare::try_from(buffer) { + let result = incoming + .handle_request(IncomingRequest { + from: account, + prepare, + }) + .await; + + let bytes: BytesMut = match result { + Ok(fulfill) => fulfill.into(), + Err(reject) => reject.into(), + }; + + Ok(warp::http::Response::builder() + .header("Content-Type", "application/octet-stream") + .status(200) + .body(bytes.freeze()) // TODO: bring this back + .unwrap()) + } else { + error!("Body was not a valid Prepare packet"); + Err(Rejection::from(ApiError::invalid_ilp_packet())) + } +} + +impl HttpServer +where + I: IncomingService + Clone + Send + Sync, + S: HttpStore + Clone, { pub fn new(incoming: I, store: S) -> Self { HttpServer { incoming, store } } + /// Returns a Warp filter which exposes per-account endpoints for [ILP over HTTP](https://interledger.org/rfcs/0035-ilp-over-http/). + /// The endpoint is /accounts/:username/ilp. pub fn as_filter( &self, - ) -> impl warp::Filter,), Error = warp::Rejection> + Clone - { - let incoming = self.incoming.clone(); + ) -> impl warp::Filter + Clone { let store = self.store.clone(); - - warp::post2() + let incoming = self.incoming.clone(); + let with_store = warp::any().map(move || store.clone()).boxed(); + let with_incoming = warp::any().map(move || incoming.clone()); + warp::post() .and(warp::path("accounts")) - .and(warp::path::param2::()) + .and(warp::path::param::()) .and(warp::path("ilp")) .and(warp::path::end()) .and(warp::header::("authorization")) - .and_then(move |path_username: Username, password: SecretString| { - if password.expose_secret().len() < BEARER_TOKEN_START { - return Either::A(err(ApiError::bad_request().into())); - } - Either::B( - store - .get_account_from_http_auth( - &path_username, - &password.expose_secret()[BEARER_TOKEN_START..], - ) - .map_err(move |_| -> Rejection { - error!("Invalid authorization provided for user: {}", path_username); - ApiError::unauthorized().into() - }), - ) - }) .and(warp::body::content_length_limit(MAX_PACKET_SIZE)) - .and(warp::body::concat()) - .and_then( - move |account: S::Account, - body: warp::body::FullBody| - -> Either<_, FutureResult<_, Rejection>> { - // TODO don't copy ILP packet - let buffer = BytesMut::from(body.bytes()); - if let Ok(prepare) = Prepare::try_from(buffer) { - Either::A( - incoming - .clone() - .handle_request(IncomingRequest { - from: account, - prepare, - }) - .then(|result| { - let bytes: BytesMut = match result { - Ok(fulfill) => fulfill.into(), - Err(reject) => reject.into(), - }; - Ok(warp::http::Response::builder() - .header("Content-Type", "application/octet-stream") - .status(200) - .body(bytes.freeze()) - .unwrap()) - }), - ) - } else { - error!("Body was not a valid Prepare packet"); - Either::B(err(ApiError::invalid_ilp_packet().into())) - } - }, - ) + .and(warp::body::bytes()) + .and(with_store) + .and(with_incoming) + .and_then(ilp_over_http) } - pub fn bind(&self, addr: SocketAddr) -> impl Future + Send { - warp::serve(self.as_filter()).bind(addr) + // Do we really need to bind self to static? + pub async fn bind(&'static self, addr: SocketAddr) { + let filter = self.as_filter(); + warp::serve(filter).run(addr).await } } @@ -107,8 +146,8 @@ where mod tests { use super::*; use crate::HttpAccount; - use bytes::{Bytes, BytesMut}; - use futures::future::ok; + use async_trait::async_trait; + use bytes::BytesMut; use http::Response; use interledger_packet::{Address, ErrorCode, PrepareBuilder, RejectBuilder}; use interledger_service::{incoming_service_fn, Account}; @@ -136,7 +175,7 @@ mod tests { } const AUTH_PASSWORD: &str = "password"; - fn api_call( + async fn api_call( api: &F, endpoint: &str, // /ilp or /accounts/:username/ilp auth: &str, // simple bearer or overloaded username+password @@ -152,19 +191,20 @@ mod tests { .header("Content-length", 1000) .body(PREPARE_BYTES.clone()) .reply(api) + .await } - #[test] - fn new_api_test() { + #[tokio::test] + async fn new_api_test() { let store = TestStore; let incoming = incoming_service_fn(|_request| { - Box::new(err(RejectBuilder { + Err(RejectBuilder { code: ErrorCode::F02_UNREACHABLE, message: b"No other incoming handler!", data: &[], triggered_by: None, } - .build())) + .build()) }); let api = HttpServer::new(incoming, store) .as_filter() @@ -175,11 +215,12 @@ mod tests { &api, "/accounts/alice/ilp", &format!("{}:{}", USERNAME.to_string(), AUTH_PASSWORD), - ); + ) + .await; assert_eq!(resp.status().as_u16(), 401); // Works with just the password - let resp = api_call(&api, "/accounts/alice/ilp", AUTH_PASSWORD); + let resp = api_call(&api, "/accounts/alice/ilp", AUTH_PASSWORD).await; assert_eq!(resp.status().as_u16(), 200); } @@ -218,17 +259,18 @@ mod tests { #[derive(Debug, Clone)] struct TestStore; + #[async_trait] impl HttpStore for TestStore { type Account = TestAccount; - fn get_account_from_http_auth( + async fn get_account_from_http_auth( &self, username: &Username, token: &str, - ) -> Box + Send> { + ) -> Result { if username == &*USERNAME && token == AUTH_PASSWORD { - Box::new(ok(TestAccount)) + Ok(TestAccount) } else { - Box::new(err(())) + Err(()) } } } diff --git a/crates/interledger-service-util/Cargo.toml b/crates/interledger-service-util/Cargo.toml index 8af5d60fe..bdce10aa9 100644 --- a/crates/interledger-service-util/Cargo.toml +++ b/crates/interledger-service-util/Cargo.toml @@ -8,22 +8,23 @@ edition = "2018" repository = "https://github.com/interledger-rs/interledger-rs" [dependencies] -bytes = { version = "0.4.12", default-features = false } +bytes = { version = "0.5", default-features = false } byteorder = { version = "1.3.2", default-features = false } chrono = { version = "0.4.9", default-features = false, features = ["clock"] } -futures = { version = "0.1.29", default-features = false } +futures = { version = "0.3.1", default-features = false } hex = { version = "0.4.0", default-features = false } interledger-packet = { path = "../interledger-packet", version = "^0.4.0", default-features = false } interledger-service = { path = "../interledger-service", version = "^0.4.0", default-features = false } interledger-settlement = { path = "../interledger-settlement", version = "^0.3.0", default-features = false, features = ["settlement_api"] } lazy_static = { version = "1.4.0", default-features = false } log = { version = "0.4.8", default-features = false } -reqwest = { version = "0.9.22", default-features = false, features = ["default-tls"] } +reqwest = { version = "0.10.0", default-features = false, features = ["default-tls"] } ring = { version = "0.16.9", default-features = false } -secrecy = { version = "0.5.1", default-features = false, features = ["alloc", "serde"] } +secrecy = { version = "0.6", default-features = false, features = ["alloc", "serde"] } serde = { version = "1.0.101", default-features = false, features = ["derive"]} -tokio = { version = "0.1.22", default-features = false } -tokio-executor = { version = "0.1.8", default-features = false } +tokio = { version = "0.2.6", default-features = false, features = ["macros", "time"] } +async-trait = "0.1.22" [dev-dependencies] -uuid = { version = "0.8.1", default-features = false} \ No newline at end of file +uuid = { version = "0.8.1", default-features = false} +bytes04 = { package = "bytes", version = "0.4", default-features = false } diff --git a/crates/interledger-service-util/src/balance_service.rs b/crates/interledger-service-util/src/balance_service.rs index 93103a2c0..2d5c34186 100644 --- a/crates/interledger-service-util/src/balance_service.rs +++ b/crates/interledger-service-util/src/balance_service.rs @@ -1,5 +1,6 @@ -use futures::Future; -use interledger_packet::{ErrorCode, Fulfill, Reject, RejectBuilder}; +use async_trait::async_trait; +use futures::TryFutureExt; +use interledger_packet::{ErrorCode, RejectBuilder}; use interledger_service::*; use interledger_settlement::{ api::SettlementClient, @@ -7,32 +8,35 @@ use interledger_settlement::{ }; use log::{debug, error}; use std::marker::PhantomData; -use tokio_executor::spawn; +// TODO: Remove AccountStore dependency, use `AccountId: ToString` as associated type +/// Trait responsible for managing an account's balance in the store +/// as ILP Packets get routed +#[async_trait] pub trait BalanceStore: AccountStore { /// Fetch the current balance for the given account. - fn get_balance(&self, account: Self::Account) - -> Box + Send>; + async fn get_balance(&self, account: Self::Account) -> Result; - fn update_balances_for_prepare( + /// Decreases the sending account's balance before forwarding out a prepare packet + async fn update_balances_for_prepare( &self, from_account: Self::Account, incoming_amount: u64, - ) -> Box + Send>; + ) -> Result<(), ()>; - /// Increases the account's balance, and returns the updated balance + /// Increases the receiving account's balance, and returns the updated balance /// along with the amount which should be settled - fn update_balances_for_fulfill( + async fn update_balances_for_fulfill( &self, to_account: Self::Account, outgoing_amount: u64, - ) -> Box + Send>; + ) -> Result<(i64, u64), ()>; - fn update_balances_for_reject( + async fn update_balances_for_reject( &self, from_account: Self::Account, incoming_amount: u64, - ) -> Box + Send>; + ) -> Result<(), ()>; } /// # Balance Service @@ -64,6 +68,7 @@ where } } +#[async_trait] impl OutgoingService for BalanceService where S: AddressStore @@ -74,10 +79,8 @@ where + Sync + 'static, O: OutgoingService + Send + Clone + 'static, - A: SettlementAccount + 'static, + A: SettlementAccount + Send + Sync + 'static, { - type Future = BoxedIlpFuture; - /// On send message: /// 1. Calls `store.update_balances_for_prepare` with the prepare. /// If it fails, it replies with a reject @@ -86,21 +89,17 @@ where /// INDEPENDENTLY of if the call suceeds or fails. This makes a `sendMoney` call if the fulfill puts the account's balance over the `settle_threshold` /// - if it returns an reject calls `store.update_balances_for_reject` and replies with the fulfill /// INDEPENDENTLY of if the call suceeds or fails - fn send_request( - &mut self, - request: OutgoingRequest, - ) -> Box + Send> { + async fn send_request(&mut self, request: OutgoingRequest) -> IlpResult { // Don't bother touching the store for zero-amount packets. // Note that it is possible for the original_amount to be >0 while the // prepare.amount is 0, because the original amount could be rounded down // to 0 when exchange rate and scale change are applied. if request.prepare.amount() == 0 && request.original_amount == 0 { - return Box::new(self.next.send_request(request)); + return self.next.send_request(request).await; } let mut next = self.next.clone(); let store = self.store.clone(); - let store_clone = store.clone(); let from = request.from.clone(); let from_clone = from.clone(); let from_id = from.id(); @@ -123,79 +122,92 @@ where // _eventually_ be completed. Because of this settlement_engine guarantee, the Connector can // operate as-if the settlement engine has completed. Finally, if the request to the settlement-engine // fails, this amount will be re-added back to balance. - Box::new( - self.store - .update_balances_for_prepare( - from.clone(), - incoming_amount, - ) - .map_err(move |_| { - debug!("Rejecting packet because it would exceed a balance limit"); - RejectBuilder { - code: ErrorCode::T04_INSUFFICIENT_LIQUIDITY, - message: &[], - triggered_by: Some(&ilp_address), - data: &[], - } - .build() - }) - .and_then(move |_| { - next.send_request(request) - .and_then(move |fulfill| { - // We will spawn a task to update the balances in the database - // so that we DO NOT wait for the database before sending the - // Fulfill packet back to our peer. Due to how the flow of ILP - // packets work, once we get the Fulfill back from the next node - // we need to propagate it backwards ASAP. If we do not give the - // previous node the fulfillment in time, they won't pay us back - // for the packet we forwarded. Note this means that we will - // relay the fulfillment _even if saving to the DB fails._ - let fulfill_balance_update = store.update_balances_for_fulfill( - to.clone(), - outgoing_amount, - ) - .map_err(move |_| error!("Error applying balance changes for fulfill from account: {} to account: {}. Incoming amount was: {}, outgoing amount was: {}", from_id, to_id, incoming_amount, outgoing_amount)) - .and_then(move |(balance, amount_to_settle)| { - debug!("Account balance after fulfill: {}. Amount that needs to be settled: {}", balance, amount_to_settle); - if amount_to_settle > 0 && to_has_engine { - // Note that if this program crashes after changing the balance (in the PROCESS_FULFILL script) - // and the send_settlement fails but the program isn't alive to hear that, the balance will be incorrect. - // No other instance will know that it was trying to send an outgoing settlement. We could - // make this more robust by saving something to the DB about the outgoing settlement when we change the balance - // but then we would also need to prevent a situation where every connector instance is polling the - // settlement engine for the status of each - // outgoing settlement and putting unnecessary - // load on the settlement engine. - spawn(settlement_client - .send_settlement(to, amount_to_settle) - .or_else(move |_| store.refund_settlement(to_id, amount_to_settle))); - } - Ok(()) - }); + self.store + .update_balances_for_prepare(from.clone(), incoming_amount) + .map_err(move |_| { + debug!("Rejecting packet because it would exceed a balance limit"); + RejectBuilder { + code: ErrorCode::T04_INSUFFICIENT_LIQUIDITY, + message: &[], + triggered_by: Some(&ilp_address), + data: &[], + } + .build() + }) + .await?; - spawn(fulfill_balance_update); + match next.send_request(request).await { + Ok(fulfill) => { + // We will spawn a task to update the balances in the database + // so that we DO NOT wait for the database before sending the + // Fulfill packet back to our peer. Due to how the flow of ILP + // packets work, once we get the Fulfill back from the next node + // we need to propagate it backwards ASAP. If we do not give the + // previous node the fulfillment in time, they won't pay us back + // for the packet we forwarded. Note this means that we will + // relay the fulfillment _even if saving to the DB fails._ + tokio::spawn(async move { + let (balance, amount_to_settle) = match store + .update_balances_for_fulfill(to.clone(), outgoing_amount) + .await + { + Ok(r) => r, + Err(_) => { + error!("Error applying balance changes for fulfill from account: {} to account: {}. Incoming amount was: {}, outgoing amount was: {}", from_id, to_id, incoming_amount, outgoing_amount); + return Err(()); + } + }; + debug!( + "Account balance after fulfill: {}. Amount that needs to be settled: {}", + balance, amount_to_settle + ); + if amount_to_settle > 0 && to_has_engine { + // Note that if this program crashes after changing the balance (in the PROCESS_FULFILL script) + // and the send_settlement fails but the program isn't alive to hear that, the balance will be incorrect. + // No other instance will know that it was trying to send an outgoing settlement. We could + // make this more robust by saving something to the DB about the outgoing settlement when we change the balance + // but then we would also need to prevent a situation where every connector instance is polling the + // settlement engine for the status of each + // outgoing settlement and putting unnecessary + // load on the settlement engine. + tokio::spawn(async move { + if settlement_client + .send_settlement(to, amount_to_settle) + .await + .is_err() + { + store.refund_settlement(to_id, amount_to_settle).await?; + } + Ok::<(), ()>(()) + }); + } + Ok(()) + }); - Ok(fulfill) - }) - .or_else(move |reject| { - // Similar to the logic for handling the Fulfill packet above, we - // spawn a task to update the balance for the Reject in parallel - // rather than waiting for the database to update before relaying - // the packet back. In this case, the only substantive difference - // would come from if the DB operation fails or takes too long. - // The packet is already rejected so it's more useful for the sender - // to get the error message from the original Reject packet rather - // than a less specific one saying that this node had an "internal - // error" caused by a database issue. - let reject_balance_update = store_clone.update_balances_for_reject( - from_clone.clone(), - incoming_amount, - ).map_err(move |_| error!("Error rolling back balance change for accounts: {} and {}. Incoming amount was: {}, outgoing amount was: {}", from_clone.id(), to_clone.id(), incoming_amount, outgoing_amount)); - spawn(reject_balance_update); + Ok(fulfill) + } + Err(reject) => { + // Similar to the logic for handling the Fulfill packet above, we + // spawn a task to update the balance for the Reject in parallel + // rather than waiting for the database to update before relaying + // the packet back. In this case, the only substantive difference + // would come from if the DB operation fails or takes too long. + // The packet is already rejected so it's more useful for the sender + // to get the error message from the original Reject packet rather + // than a less specific one saying that this node had an "internal + // error" caused by a database issue. + tokio::spawn({ + let store_clone = self.store.clone(); + async move { + store_clone.update_balances_for_reject( + from_clone.clone(), + incoming_amount, + ).map_err(move |_| error!("Error rolling back balance change for accounts: {} and {}. Incoming amount was: {}, outgoing amount was: {}", from_clone.id(), to_clone.id(), incoming_amount, outgoing_amount)).await + } + }); - Err(reject) - }) - }), - ) + Err(reject) + } + } } } diff --git a/crates/interledger-service-util/src/echo_service.rs b/crates/interledger-service-util/src/echo_service.rs index 1cd227f17..6bd7e1941 100644 --- a/crates/interledger-service-util/src/echo_service.rs +++ b/crates/interledger-service-util/src/echo_service.rs @@ -1,7 +1,7 @@ +use async_trait::async_trait; use byteorder::ReadBytesExt; use bytes::{BufMut, BytesMut}; use core::borrow::Borrow; -use futures::future::err; use interledger_packet::{ oer::BufOerExt, Address, ErrorCode, Prepare, PrepareBuilder, RejectBuilder, }; @@ -12,11 +12,6 @@ use std::marker::PhantomData; use std::str; use std::time::SystemTime; -/// A service that responds to the Echo Protocol. -/// Currently, this service only supports bidirectional mode (unidirectional mode is not supported yet). -/// The service doesn't shorten expiry as it expects the expiry to be shortened by another service -/// like `ExpiryShortenerService`. - /// The prefix that echo packets should have in its data section const ECHO_PREFIX: &str = "ECHOECHOECHOECHO"; /// The length of the `ECHO_PREFIX` @@ -27,6 +22,10 @@ enum EchoPacketType { Response = 1, } +/// A service that implements the Echo Protocol. +/// Currently, this service only supports bidirectional mode (unidirectional mode is not supported yet). +/// The service doesn't shorten expiry as it expects the expiry to be shortened by another service +/// like `ExpiryShortenerService`. #[derive(Clone)] pub struct EchoService { store: S, @@ -40,6 +39,7 @@ where I: IncomingService, A: Account, { + /// Simple Constructor pub fn new(store: S, next: I) -> Self { EchoService { store, @@ -49,20 +49,19 @@ where } } +#[async_trait] impl IncomingService for EchoService where - I: IncomingService, - S: AddressStore, - A: Account, + I: IncomingService + Send, + S: AddressStore + Send, + A: Account + Send, { - type Future = BoxedIlpFuture; - - fn handle_request(&mut self, mut request: IncomingRequest) -> Self::Future { + async fn handle_request(&mut self, mut request: IncomingRequest) -> IlpResult { let ilp_address = self.store.get_ilp_address(); let should_echo = request.prepare.destination() == ilp_address && request.prepare.data().starts_with(ECHO_PREFIX.as_bytes()); if !should_echo { - return Box::new(self.next.handle_request(request)); + return self.next.handle_request(request).await; } debug!("Responding to Echo protocol request: {:?}", request); @@ -78,23 +77,23 @@ where Ok(value) => value, Err(error) => { eprintln!("Could not read packet type: {:?}", error); - return Box::new(err(RejectBuilder { + return Err(RejectBuilder { code: ErrorCode::F01_INVALID_PACKET, message: b"Could not read echo packet type.", triggered_by: Some(&ilp_address), data: &[], } - .build())); + .build()); } }; if echo_packet_type == EchoPacketType::Response as u8 { // if the echo packet type is Response, just pass it to the next service // so that the initiator could handle this packet - return Box::new(self.next.handle_request(request)); + return self.next.handle_request(request).await; } if echo_packet_type != EchoPacketType::Request as u8 { eprintln!("The packet type is not acceptable: {}", echo_packet_type); - return Box::new(err(RejectBuilder { + return Err(RejectBuilder { code: ErrorCode::F01_INVALID_PACKET, message: format!( "The echo packet type: {} is not acceptable.", @@ -104,7 +103,7 @@ where triggered_by: Some(&ilp_address), data: &[], } - .build())); + .build()); } // check source address @@ -116,24 +115,24 @@ where "Could not parse source address from echo packet: {:?}", error ); - return Box::new(err(RejectBuilder { + return Err(RejectBuilder { code: ErrorCode::F01_INVALID_PACKET, message: b"Could not parse source address from Echo packet", triggered_by: Some(&ilp_address), data: &[], } - .build())); + .build()); } }, Err(error) => { eprintln!("Could not read source address: {:?}", error); - return Box::new(err(RejectBuilder { + return Err(RejectBuilder { code: ErrorCode::F01_INVALID_PACKET, message: b"Could not read source address.", triggered_by: Some(&ilp_address), data: &[], } - .build())); + .build()); } }; @@ -150,7 +149,7 @@ where } .build(); - Box::new(self.next.handle_request(request)) + self.next.handle_request(request).await } } @@ -171,8 +170,10 @@ pub struct EchoRequestBuilder<'a> { #[cfg(test)] impl<'a> EchoRequestBuilder<'a> { pub fn build(&self) -> Prepare { + use bytes04::BufMut as BufMut04; let source_address_len = oer::predict_var_octet_string(self.source_address.len()); - let mut data_buffer = BytesMut::with_capacity(ECHO_PREFIX_LEN + 1 + source_address_len); + let mut data_buffer = + bytes04::BytesMut::with_capacity(ECHO_PREFIX_LEN + 1 + source_address_len); data_buffer.put(ECHO_PREFIX.as_bytes()); data_buffer.put_u8(EchoPacketType::Request as u8); data_buffer.put_var_octet_string(self.source_address.as_ref() as &[u8]); @@ -214,7 +215,6 @@ impl<'a> EchoResponseBuilder<'a> { #[cfg(test)] mod echo_tests { use super::*; - use futures::future::Future; use interledger_packet::{FulfillBuilder, PrepareBuilder}; use interledger_service::incoming_service_fn; use lazy_static::lazy_static; @@ -232,16 +232,14 @@ mod echo_tests { #[derive(Clone)] struct TestStore(Address); + #[async_trait] impl AddressStore for TestStore { /// Saves the ILP Address in the store's memory and database - fn set_ilp_address( - &self, - _ilp_address: Address, - ) -> Box + Send> { + async fn set_ilp_address(&self, _ilp_address: Address) -> Result<(), ()> { unimplemented!() } - fn clear_ilp_address(&self) -> Box + Send> { + async fn clear_ilp_address(&self) -> Result<(), ()> { unimplemented!() } @@ -279,8 +277,8 @@ mod echo_tests { /// If the destination of the packet is not destined to the node's address, /// the node should not echo the packet. - #[test] - fn test_echo_packet_not_destined() { + #[tokio::test] + async fn test_echo_packet_not_destined() { let amount = 1; let expires_at = SystemTime::now() + Duration::from_secs(30); let fulfillment = &get_random_fulfillment(); @@ -319,14 +317,14 @@ mod echo_tests { // test let result = echo_service .handle_request(IncomingRequest { prepare, from }) - .wait(); + .await; assert!(result.is_ok()); } /// Even if the destination of the packet is the node's address, /// packets that don't have a correct echo prefix will not be handled as echo packets. - #[test] - fn test_echo_packet_without_echo_prefix() { + #[tokio::test] + async fn test_echo_packet_without_echo_prefix() { let amount = 1; let expires_at = SystemTime::now() + Duration::from_secs(30); let fulfillment = &get_random_fulfillment(); @@ -365,14 +363,14 @@ mod echo_tests { // test let result = echo_service .handle_request(IncomingRequest { prepare, from }) - .wait(); + .await; assert!(result.is_ok()); } /// If the destination of the packet is the node's address and the echo packet type is /// request, the service will echo the packet modifying destination to the `source_address`. - #[test] - fn test_echo_packet() { + #[tokio::test] + async fn test_echo_packet() { let amount = 1; let expires_at = SystemTime::now() + Duration::from_secs(30); let fulfillment = &get_random_fulfillment(); @@ -411,13 +409,13 @@ mod echo_tests { // test let result = echo_service .handle_request(IncomingRequest { prepare, from }) - .wait(); + .await; assert!(result.is_ok()); } /// If echo packet type is neither `1` nor `2`, the packet is considered to be malformed. - #[test] - fn test_invalid_echo_packet_type() { + #[tokio::test] + async fn test_invalid_echo_packet_type() { let amount = 1; let expires_at = SystemTime::now() + Duration::from_secs(30); let fulfillment = &get_random_fulfillment(); @@ -452,14 +450,14 @@ mod echo_tests { // test let result = echo_service .handle_request(IncomingRequest { prepare, from }) - .wait(); + .await; assert!(result.is_err()); } /// Even if the destination of the packet is the node's address and the data starts with /// echo prefix correctly, `source_address` may be broken. This is the case. - #[test] - fn test_invalid_source_address() { + #[tokio::test] + async fn test_invalid_source_address() { let amount = 1; let expires_at = SystemTime::now() + Duration::from_secs(30); let fulfillment = &get_random_fulfillment(); @@ -494,7 +492,7 @@ mod echo_tests { // test let result = echo_service .handle_request(IncomingRequest { prepare, from }) - .wait(); + .await; assert!(result.is_err()); } diff --git a/crates/interledger-service-util/src/exchange_rate_providers/coincap.rs b/crates/interledger-service-util/src/exchange_rate_providers/coincap.rs index 1dd0bf3dd..7044c7114 100644 --- a/crates/interledger-service-util/src/exchange_rate_providers/coincap.rs +++ b/crates/interledger-service-util/src/exchange_rate_providers/coincap.rs @@ -1,7 +1,7 @@ -use futures::Future; +use futures::TryFutureExt; use lazy_static::lazy_static; use log::{error, warn}; -use reqwest::{r#async::Client, Url}; +use reqwest::{Client, Url}; use serde::Deserialize; use std::{collections::HashMap, str::FromStr}; @@ -25,50 +25,50 @@ struct RateResponse { data: Vec, } -pub fn query_coincap(client: &Client) -> impl Future, Error = ()> { - query_coincap_endpoint(client, COINCAP_ASSETS_URL.clone()) - .join(query_coincap_endpoint(client, COINCAP_RATES_URL.clone())) - .and_then(|(assets, rates)| { - let all_rates: HashMap = assets - .data - .into_iter() - .chain(rates.data.into_iter()) - .filter_map(|record| match f64::from_str(record.rate_usd.as_str()) { - Ok(rate) => Some((record.symbol.to_uppercase(), rate)), - Err(err) => { - warn!( - "Unable to parse {} rate as an f64: {} {:?}", - record.symbol, record.rate_usd, err - ); - None - } - }) - .collect(); - Ok(all_rates) +pub async fn query_coincap(client: &Client) -> Result, ()> { + let (assets, rates) = futures::future::join( + query_coincap_endpoint(client, COINCAP_ASSETS_URL.clone()), + query_coincap_endpoint(client, COINCAP_RATES_URL.clone()), + ) + .await; + + let all_rates: HashMap = assets? + .data + .into_iter() + .chain(rates?.data.into_iter()) + .filter_map(|record| match f64::from_str(record.rate_usd.as_str()) { + Ok(rate) => Some((record.symbol.to_uppercase(), rate)), + Err(err) => { + warn!( + "Unable to parse {} rate as an f64: {} {:?}", + record.symbol, record.rate_usd, err + ); + None + } }) + .collect(); + Ok(all_rates) } -fn query_coincap_endpoint( - client: &Client, - url: Url, -) -> impl Future { - client +async fn query_coincap_endpoint(client: &Client, url: Url) -> Result { + let res = client .get(url) .send() .map_err(|err| { error!("Error fetching exchange rates from CoinCap: {:?}", err); }) - .and_then(|res| { - res.error_for_status().map_err(|err| { - error!("HTTP error getting exchange rates from CoinCap: {:?}", err); - }) - }) - .and_then(|mut res| { - res.json().map_err(|err| { - error!( - "Error getting exchange rate response body from CoinCap, incorrect type: {:?}", - err - ); - }) + .await?; + + let res = res.error_for_status().map_err(|err| { + error!("HTTP error getting exchange rates from CoinCap: {:?}", err); + })?; + + res.json() + .map_err(|err| { + error!( + "Error getting exchange rate response body from CoinCap, incorrect type: {:?}", + err + ); }) + .await } diff --git a/crates/interledger-service-util/src/exchange_rate_providers/cryptocompare.rs b/crates/interledger-service-util/src/exchange_rate_providers/cryptocompare.rs index 2a418b3af..3f369da83 100644 --- a/crates/interledger-service-util/src/exchange_rate_providers/cryptocompare.rs +++ b/crates/interledger-service-util/src/exchange_rate_providers/cryptocompare.rs @@ -1,7 +1,7 @@ -use futures::Future; +use futures::TryFutureExt; use lazy_static::lazy_static; use log::error; -use reqwest::{r#async::Client, Url}; +use reqwest::{Client, Url}; use secrecy::{ExposeSecret, SecretString}; use serde::Deserialize; use std::{ @@ -47,44 +47,52 @@ struct Response { data: Vec, } -pub fn query_cryptocompare( +pub async fn query_cryptocompare( client: &Client, api_key: &SecretString, -) -> impl Future, Error = ()> { - client +) -> Result, ()> { + // ref: https://github.com/rust-lang/rust/pull/64856 + let header = format!("Apikey {}", api_key.expose_secret()); + let res = client .get(CRYPTOCOMPARE_URL.clone()) // TODO don't copy the api key on every request - .header( - "Authorization", - format!("Apikey {}", api_key.expose_secret()).as_str(), - ) + .header("Authorization", header) .send() .map_err(|err| { - error!("Error fetching exchange rates from CryptoCompare: {:?}", err); + error!( + "Error fetching exchange rates from CryptoCompare: {:?}", + err + ); }) - .and_then(|res| { - res.error_for_status().map_err(|err| { - error!("HTTP error getting exchange rates from CryptoCompare: {:?}", err); - }) - }) - .and_then(|mut res| { - res.json().map_err(|err| { - error!( - "Error getting exchange rate response body from CryptoCompare, incorrect type: {:?}", - err - ); - }) + .await?; + + let res = res.error_for_status().map_err(|err| { + error!( + "HTTP error getting exchange rates from CryptoCompare: {:?}", + err + ); + })?; + + let res: Response = res + .json() + .map_err(|err| { + error!( + "Error getting exchange rate response body from CryptoCompare, incorrect type: {:?}", + err + ); }) - .and_then(|res: Response| { - let rates = res - .data - .into_iter() - .filter_map(|asset| if let Some(raw) = asset.raw { - Some((asset.coin_info.name.to_uppercase(), raw.usd.price)) - } else { - None - }) - .chain(once(("USD".to_string(), 1.0))); - Ok(HashMap::from_iter(rates)) + .await?; + + let rates = res + .data + .into_iter() + .filter_map(|asset| { + if let Some(raw) = asset.raw { + Some((asset.coin_info.name.to_uppercase(), raw.usd.price)) + } else { + None + } }) + .chain(once(("USD".to_string(), 1.0))); + Ok(HashMap::from_iter(rates)) } diff --git a/crates/interledger-service-util/src/exchange_rate_providers/mod.rs b/crates/interledger-service-util/src/exchange_rate_providers/mod.rs index 4b005cf04..01f84c124 100644 --- a/crates/interledger-service-util/src/exchange_rate_providers/mod.rs +++ b/crates/interledger-service-util/src/exchange_rate_providers/mod.rs @@ -1,4 +1,6 @@ +/// Exchange rate provider for [CoinCap](https://coincap.io/) mod coincap; +/// Exchange rate provider for [CryptoCompare](https://www.cryptocompare.com/). REQUIRES [API KEY](https://min-api.cryptocompare.com/). mod cryptocompare; pub use coincap::*; diff --git a/crates/interledger-service-util/src/exchange_rates_service.rs b/crates/interledger-service-util/src/exchange_rates_service.rs index 4e5bb83a3..f6ab7174b 100644 --- a/crates/interledger-service-util/src/exchange_rates_service.rs +++ b/crates/interledger-service-util/src/exchange_rates_service.rs @@ -1,13 +1,11 @@ use super::exchange_rate_providers::*; -use futures::{ - future::{err, Either}, - Future, Stream, -}; -use interledger_packet::{ErrorCode, Fulfill, Reject, RejectBuilder}; +use async_trait::async_trait; +use futures::TryFutureExt; +use interledger_packet::{ErrorCode, RejectBuilder}; use interledger_service::*; use interledger_settlement::core::types::{Convert, ConvertDetails}; use log::{debug, error, trace, warn}; -use reqwest::r#async::Client; +use reqwest::Client; use secrecy::SecretString; use serde::Deserialize; use std::{ @@ -17,17 +15,19 @@ use std::{ atomic::{AtomicU32, Ordering}, Arc, }, - time::{Duration, Instant}, + time::Duration, }; -use tokio::{executor::spawn, timer::Interval}; // TODO should this whole file be moved to its own crate? +/// Store trait responsible for managing the exchange rates of multiple currencies pub trait ExchangeRateStore: Clone { // TODO we may want to make this async if/when we use pubsub to broadcast // rate changes to different instances of a horizontally-scalable node + /// Sets the exchange rate by providing an AssetCode->USD price mapping fn set_exchange_rates(&self, rates: HashMap) -> Result<(), ()>; + /// Gets the exchange rates for the provided asset codes fn get_exchange_rates(&self, asset_codes: &[&str]) -> Result, ()>; // TODO should this be on the API instead? That's where it's actually used @@ -36,6 +36,7 @@ pub trait ExchangeRateStore: Clone { // (so that we don't accidentally lock up the RwLock on the store's exchange_rates) // but in the normal case of getting the rate between two assets, we don't want to // copy all the rate data + /// Gets the exchange rates for all stored asset codes fn get_all_exchange_rates(&self) -> Result, ()>; } @@ -67,25 +68,21 @@ where } } +#[async_trait] impl OutgoingService for ExchangeRateService where // TODO can we make these non-'static? S: AddressStore + ExchangeRateStore + Clone + Send + Sync + 'static, - O: OutgoingService + Send + Clone + 'static, - A: Account + Sync + 'static, + O: OutgoingService + Send + Sync + Clone + 'static, + A: Account + Send + Sync + 'static, { - type Future = BoxedIlpFuture; - /// On send request: /// 1. If the prepare packet's amount is 0, it just forwards /// 1. Retrieves the exchange rate from the store (the store independently is responsible for polling the rates) /// - return reject if the call to the store fails /// 1. Calculates the exchange rate AND scales it up/down depending on how many decimals each asset requires /// 1. Updates the amount in the prepare packet and forwards it - fn send_request( - &mut self, - mut request: OutgoingRequest, - ) -> Box + Send> { + async fn send_request(&mut self, mut request: OutgoingRequest) -> IlpResult { let ilp_address = self.store.get_ilp_address(); if request.prepare.amount() > 0 { let rate: f64 = if request.from.asset_code() == request.to.asset_code() { @@ -105,7 +102,7 @@ where request.from.asset_code(), request.to.asset_code() ); - return Box::new(err(RejectBuilder { + return Err(RejectBuilder { code: ErrorCode::T00_INTERNAL_ERROR, message: format!( "No exchange rate available from asset: {} to: {}", @@ -116,7 +113,7 @@ where triggered_by: Some(&ilp_address), data: &[], } - .build())); + .build()); }; // Apply spread @@ -165,13 +162,13 @@ where ) }; - return Box::new(err(RejectBuilder { + return Err(RejectBuilder { code, message: message.as_bytes(), triggered_by: Some(&ilp_address), data: &[], } - .build())); + .build()); } request.prepare.set_amount(outgoing_amount as u64); trace!("Converted incoming amount of: {} {} (scale {}) from account {} to outgoing amount of: {} {} (scale {}) for account {}", @@ -183,7 +180,7 @@ where // returns an error. Happens due to float // multiplication overflow . // (float overflow in Rust produces +inf) - return Box::new(err(RejectBuilder { + return Err(RejectBuilder { code: ErrorCode::F08_AMOUNT_TOO_LARGE, message: format!( "Could not convert exchange rate from {}:{} to: {}:{}. Got incoming amount: {}", @@ -197,12 +194,12 @@ where triggered_by: Some(&ilp_address), data: &[], } - .build())); + .build()); } } } - Box::new(self.next.send_request(request)) + self.next.send_request(request).await } } @@ -230,6 +227,7 @@ pub enum ExchangeRateProvider { } /// Poll exchange rate providers for the current exchange rates +#[derive(Clone)] pub struct ExchangeRateFetcher { provider: ExchangeRateProvider, consecutive_failed_polls: Arc, @@ -242,6 +240,7 @@ impl ExchangeRateFetcher where S: ExchangeRateStore + Send + Sync + 'static, { + /// Simple constructor pub fn new( provider: ExchangeRateProvider, failed_polls_before_invalidation: u32, @@ -256,47 +255,42 @@ where } } - pub fn fetch_on_interval(self, interval: Duration) -> impl Future { + /// Spawns a future which calls [`self.update_rates()`](./struct.ExchangeRateFetcher.html#method.update_rates) every `interval` + pub fn spawn_interval(self, interval: Duration) { debug!( "Starting interval to poll exchange rate provider: {:?} for rates", self.provider ); - Interval::new(Instant::now(), interval) - .map_err(|err| { - error!( - "Interval error, no longer fetching exchange rates: {:?}", - err - ); - }) - .for_each(move |_| { - self.update_rates().then(|_| { - // Ignore errors so that they don't cause the Interval to stop - Ok(()) - }) - }) - } - - pub fn spawn_interval(self, interval: Duration) { - spawn(self.fetch_on_interval(interval)); + let interval = async move { + let mut interval = tokio::time::interval(interval); + loop { + interval.tick().await; + // Ignore errors so that they don't cause the Interval to stop + let _ = self.update_rates().await; + } + }; + tokio::spawn(interval); } - fn fetch_rates(&self) -> impl Future, Error = ()> { + /// Calls the proper exchange rate provider + async fn fetch_rates(&self) -> Result, ()> { match self.provider { ExchangeRateProvider::CryptoCompare(ref api_key) => { - Either::A(query_cryptocompare(&self.client, api_key)) + query_cryptocompare(&self.client, api_key).await } - ExchangeRateProvider::CoinCap => Either::B(query_coincap(&self.client)), + ExchangeRateProvider::CoinCap => query_coincap(&self.client).await, } } - fn update_rates(&self) -> impl Future { + /// Gets the exchange rates and proceeds to update the store with the newly polled values + async fn update_rates(&self) -> Result<(), ()> { let consecutive_failed_polls = self.consecutive_failed_polls.clone(); let consecutive_failed_polls_zeroer = consecutive_failed_polls.clone(); let failed_polls_before_invalidation = self.failed_polls_before_invalidation; let store = self.store.clone(); let store_clone = self.store.clone(); let provider = self.provider.clone(); - self.fetch_rates() + let mut rates = self.fetch_rates() .map_err(move |_| { // Note that a race between the read on this line and the check on the line after // is quite unlikely as long as the interval between polls is reasonable. @@ -311,29 +305,27 @@ where panic!("Failed to clear exchange rates cache after exchange rates server became unresponsive"); } } - }) - .and_then(move |mut rates| { - trace!("Fetched exchange rates: {:?}", rates); - let num_rates = rates.len(); - rates.insert("USD".to_string(), 1.0); - if store_clone.set_exchange_rates(rates).is_ok() { - // Reset our invalidation counter - consecutive_failed_polls_zeroer.store(0, Ordering::Relaxed); - debug!("Updated {} exchange rates from {:?}", num_rates, provider); - Ok(()) - } else { - error!("Error setting exchange rates in store"); - Err(()) - } - }) + }).await?; + + trace!("Fetched exchange rates: {:?}", rates); + let num_rates = rates.len(); + rates.insert("USD".to_string(), 1.0); + if store_clone.set_exchange_rates(rates).is_ok() { + // Reset our invalidation counter + consecutive_failed_polls_zeroer.store(0, Ordering::Relaxed); + debug!("Updated {} exchange rates from {:?}", num_rates, provider); + Ok(()) + } else { + error!("Error setting exchange rates in store"); + Err(()) + } } } #[cfg(test)] mod tests { use super::*; - use futures::{future::ok, Future}; - use interledger_packet::{Address, FulfillBuilder, PrepareBuilder}; + use interledger_packet::{Address, Fulfill, FulfillBuilder, PrepareBuilder, Reject}; use interledger_service::{outgoing_service_fn, Account}; use lazy_static::lazy_static; use std::collections::HashMap; @@ -348,21 +340,21 @@ mod tests { pub static ref ALICE: Username = Username::from_str("alice").unwrap(); } - #[test] - fn exchange_rate_ok() { + #[tokio::test] + async fn exchange_rate_ok() { // if `to` is worth $2, and `from` is worth 1, then they receive half // the amount of units - let ret = exchange_rate(200, 1, 1.0, 1, 2.0, 0.0); + let ret = exchange_rate(200, 1, 1.0, 1, 2.0, 0.0).await; assert_eq!(ret.1[0].prepare.amount(), 100); - let ret = exchange_rate(1_000_000, 1, 3.0, 1, 2.0, 0.0); + let ret = exchange_rate(1_000_000, 1, 3.0, 1, 2.0, 0.0).await; assert_eq!(ret.1[0].prepare.amount(), 1_500_000); } - #[test] - fn exchange_conversion_error() { + #[tokio::test] + async fn exchange_conversion_error() { // rejects f64 that does not fit in u64 - let ret = exchange_rate(std::u64::MAX, 1, 2.0, 1, 1.0, 0.0); + let ret = exchange_rate(std::u64::MAX, 1, 2.0, 1, 1.0, 0.0).await; let reject = ret.0.unwrap_err(); assert_eq!(reject.code(), ErrorCode::F08_AMOUNT_TOO_LARGE); assert!(reject @@ -370,7 +362,7 @@ mod tests { .starts_with(b"Could not cast to f64, amount too large")); // rejects f64 which gets rounded down to 0 - let ret = exchange_rate(1, 2, 1.0, 1, 1.0, 0.0); + let ret = exchange_rate(1, 2, 1.0, 1, 1.0, 0.0).await; let reject = ret.0.unwrap_err(); assert_eq!(reject.code(), ErrorCode::R01_INSUFFICIENT_SOURCE_AMOUNT); assert!(reject @@ -378,38 +370,38 @@ mod tests { .starts_with(b"Could not cast to f64, amount too small")); // `Convert` errored - let ret = exchange_rate(std::u64::MAX, 1, std::f64::MAX, 255, 1.0, 0.0); + let ret = exchange_rate(std::u64::MAX, 1, std::f64::MAX, 255, 1.0, 0.0).await; let reject = ret.0.unwrap_err(); assert_eq!(reject.code(), ErrorCode::F08_AMOUNT_TOO_LARGE); assert!(reject.message().starts_with(b"Could not convert")); } - #[test] - fn applies_spread() { - let ret = exchange_rate(100, 1, 1.0, 1, 2.0, 0.01); + #[tokio::test] + async fn applies_spread() { + let ret = exchange_rate(100, 1, 1.0, 1, 2.0, 0.01).await; assert_eq!(ret.1[0].prepare.amount(), 49); // Negative spread is unusual but possible - let ret = exchange_rate(200, 1, 1.0, 1, 2.0, -0.01); + let ret = exchange_rate(200, 1, 1.0, 1, 2.0, -0.01).await; assert_eq!(ret.1[0].prepare.amount(), 101); // Rounds down - let ret = exchange_rate(4, 1, 1.0, 1, 2.0, 0.01); + let ret = exchange_rate(4, 1, 1.0, 1, 2.0, 0.01).await; // this would've been 2, but it becomes 1.99 and gets rounded down to 1 assert_eq!(ret.1[0].prepare.amount(), 1); // Spread >= 1 means the node takes everything - let ret = exchange_rate(10_000_000_000, 1, 1.0, 1, 2.0, 1.0); + let ret = exchange_rate(10_000_000_000, 1, 1.0, 1, 2.0, 1.0).await; assert_eq!(ret.1[0].prepare.amount(), 0); // Need to catch when spread > 1 - let ret = exchange_rate(10_000_000_000, 1, 1.0, 1, 2.0, 2.0); + let ret = exchange_rate(10_000_000_000, 1, 1.0, 1, 2.0, 2.0).await; assert_eq!(ret.1[0].prepare.amount(), 0); } // Instantiates an exchange rate service and returns the fulfill/reject // packet and the outgoing request after performing an asset conversion - fn exchange_rate( + async fn exchange_rate( amount: u64, scale1: u8, rate1: f64, @@ -421,11 +413,11 @@ mod tests { let requests_clone = requests.clone(); let outgoing = outgoing_service_fn(move |request| { requests_clone.lock().unwrap().push(request); - Box::new(ok(FulfillBuilder { + Ok(FulfillBuilder { fulfillment: &[0; 32], data: b"hello!", } - .build())) + .build()) }); let mut service = test_service(rate1, rate2, spread, outgoing); let result = service @@ -442,7 +434,7 @@ mod tests { } .build(), }) - .wait(); + .await; let reqs = requests.lock().unwrap(); (result, reqs.clone()) @@ -464,16 +456,14 @@ mod tests { } } + #[async_trait] impl AddressStore for TestStore { /// Saves the ILP Address in the store's memory and database - fn set_ilp_address( - &self, - _ilp_address: Address, - ) -> Box + Send> { + async fn set_ilp_address(&self, _ilp_address: Address) -> Result<(), ()> { unimplemented!() } - fn clear_ilp_address(&self) -> Box + Send> { + async fn clear_ilp_address(&self) -> Result<(), ()> { unimplemented!() } diff --git a/crates/interledger-service-util/src/expiry_shortener_service.rs b/crates/interledger-service-util/src/expiry_shortener_service.rs index db5658447..22f25eed9 100644 --- a/crates/interledger-service-util/src/expiry_shortener_service.rs +++ b/crates/interledger-service-util/src/expiry_shortener_service.rs @@ -1,11 +1,15 @@ +use async_trait::async_trait; use chrono::{DateTime, Duration, Utc}; -use interledger_service::{Account, OutgoingRequest, OutgoingService}; +use interledger_service::{Account, IlpResult, OutgoingRequest, OutgoingService}; use log::trace; pub const DEFAULT_ROUND_TRIP_TIME: u32 = 500; pub const DEFAULT_MAX_EXPIRY_DURATION: u32 = 30000; +/// An account with a round trip time, used by the [`ExpiryShortenerService`](./struct.ExpiryShortenerService.html) +/// to shorten a packet's expiration time to account for latency pub trait RoundTripTimeAccount: Account { + /// The account's round trip time fn round_trip_time(&self) -> u32 { DEFAULT_ROUND_TRIP_TIME } @@ -33,25 +37,26 @@ impl ExpiryShortenerService { } } + // TODO: This isn't used anywhere, should we remove it? + /// Sets the service's max expiry duration pub fn max_expiry_duration(&mut self, milliseconds: u32) -> &mut Self { self.max_expiry_duration = milliseconds; self } } +#[async_trait] impl OutgoingService for ExpiryShortenerService where - O: OutgoingService, - A: RoundTripTimeAccount, + O: OutgoingService + Send + Sync + 'static, + A: RoundTripTimeAccount + Send + Sync + 'static, { - type Future = O::Future; - /// On send request: /// 1. Get the sender and receiver's roundtrip time (default 1000ms) /// 2. Reduce the packet's expiry by that amount /// 3. Ensure that the packet expiry does not exceed the maximum expiry duration /// 4. Forward the request - fn send_request(&mut self, mut request: OutgoingRequest) -> Self::Future { + async fn send_request(&mut self, mut request: OutgoingRequest) -> IlpResult { let time_to_subtract = i64::from(request.from.round_trip_time() + request.to.round_trip_time()); let new_expiry = DateTime::::from(request.prepare.expires_at()) @@ -70,14 +75,13 @@ where }; request.prepare.set_expires_at(new_expiry.into()); - self.next.send_request(request) + self.next.send_request(request).await } } #[cfg(test)] mod tests { use super::*; - use futures::Future; use interledger_packet::{Address, ErrorCode, FulfillBuilder, PrepareBuilder, RejectBuilder}; use interledger_service::{outgoing_service_fn, Username}; use std::str::FromStr; @@ -121,8 +125,8 @@ mod tests { } } - #[test] - fn shortens_expiry_by_round_trip_time() { + #[tokio::test] + async fn shortens_expiry_by_round_trip_time() { let original_expiry = Utc::now() + Duration::milliseconds(30000); let mut service = ExpiryShortenerService::new(outgoing_service_fn(move |request| { if DateTime::::from(request.prepare.expires_at()) @@ -157,12 +161,12 @@ mod tests { .build(), original_amount: 10, }) - .wait() + .await .expect("Should have shortened expiry"); } - #[test] - fn reduces_expiry_to_max_duration() { + #[tokio::test] + async fn reduces_expiry_to_max_duration() { let mut service = ExpiryShortenerService::new(outgoing_service_fn(move |request| { if DateTime::::from(request.prepare.expires_at()) - Utc::now() <= Duration::milliseconds(30000) @@ -196,7 +200,7 @@ mod tests { .build(), original_amount: 10, }) - .wait() + .await .expect("Should have shortened expiry"); } } diff --git a/crates/interledger-service-util/src/lib.rs b/crates/interledger-service-util/src/lib.rs index 40c2e1eef..6f897a19f 100644 --- a/crates/interledger-service-util/src/lib.rs +++ b/crates/interledger-service-util/src/lib.rs @@ -2,13 +2,23 @@ //! //! Miscellaneous, small Interledger Services. +/// Balance tracking service mod balance_service; +/// Service which implements the echo protocol mod echo_service; +/// Utilities for connecting to various exchange rate providers mod exchange_rate_providers; +/// Service responsible for setting and fetching dollar denominated exchange rates mod exchange_rates_service; +/// Service responsible for shortening the expiry time of packets, +/// to take into account for network latency mod expiry_shortener_service; +/// Service responsible for capping the amount an account can send in a packet mod max_packet_amount_service; +/// Service responsible for capping the amount of packets and amount in packets an account can send mod rate_limit_service; +/// Service responsible for checking that packets are not expired and that prepare packets' fulfillment conditions +/// match the fulfillment inside the incoming fulfills mod validator_service; pub use self::balance_service::{BalanceService, BalanceStore}; diff --git a/crates/interledger-service-util/src/max_packet_amount_service.rs b/crates/interledger-service-util/src/max_packet_amount_service.rs index 3dc0aeac0..a60658cbb 100644 --- a/crates/interledger-service-util/src/max_packet_amount_service.rs +++ b/crates/interledger-service-util/src/max_packet_amount_service.rs @@ -1,8 +1,10 @@ -use futures::future::err; +use async_trait::async_trait; use interledger_packet::{ErrorCode, MaxPacketAmountDetails, RejectBuilder}; use interledger_service::*; use log::debug; +/// Extension trait for [`Account`](../interledger_service/trait.Account.html) with the max packet amount +/// allowed for this account pub trait MaxPacketAmountAccount: Account { fn max_packet_amount(&self) -> u64; } @@ -22,26 +24,26 @@ pub struct MaxPacketAmountService { } impl MaxPacketAmountService { + /// Simple constructor pub fn new(store: S, next: I) -> Self { MaxPacketAmountService { store, next } } } +#[async_trait] impl IncomingService for MaxPacketAmountService where - I: IncomingService, - S: AddressStore, - A: MaxPacketAmountAccount, + I: IncomingService + Send + Sync + 'static, + S: AddressStore + Send + Sync + 'static, + A: MaxPacketAmountAccount + Send + Sync + 'static, { - type Future = BoxedIlpFuture; - /// On receive request: /// 1. if request.prepare.amount <= request.from.max_packet_amount forward the request, else error - fn handle_request(&mut self, request: IncomingRequest) -> Self::Future { + async fn handle_request(&mut self, request: IncomingRequest) -> IlpResult { let ilp_address = self.store.get_ilp_address(); let max_packet_amount = request.from.max_packet_amount(); if request.prepare.amount() <= max_packet_amount { - Box::new(self.next.handle_request(request)) + self.next.handle_request(request).await } else { debug!( "Prepare amount:{} exceeds max_packet_amount: {}", @@ -50,13 +52,13 @@ where ); let details = MaxPacketAmountDetails::new(request.prepare.amount(), max_packet_amount).to_bytes(); - Box::new(err(RejectBuilder { + Err(RejectBuilder { code: ErrorCode::F08_AMOUNT_TOO_LARGE, message: &[], triggered_by: Some(&ilp_address), data: &details[..], } - .build())) + .build()) } } } diff --git a/crates/interledger-service-util/src/rate_limit_service.rs b/crates/interledger-service-util/src/rate_limit_service.rs index 28b5627f1..a7d5efaed 100644 --- a/crates/interledger-service-util/src/rate_limit_service.rs +++ b/crates/interledger-service-util/src/rate_limit_service.rs @@ -1,44 +1,56 @@ -use futures::{ - future::{err, Either}, - Future, -}; +use async_trait::async_trait; use interledger_packet::{ErrorCode, RejectBuilder}; -use interledger_service::{ - Account, AddressStore, BoxedIlpFuture, IncomingRequest, IncomingService, -}; +use interledger_service::{Account, AddressStore, IlpResult, IncomingRequest, IncomingService}; use log::{error, warn}; use std::marker::PhantomData; +/// Extension trait for [`Account`](../interledger_service/trait.Account.html) with rate limiting related information pub trait RateLimitAccount: Account { + /// The maximum packets per minute allowed for this account fn packets_per_minute_limit(&self) -> Option { None } + /// The maximum units per minute allowed for this account fn amount_per_minute_limit(&self) -> Option { None } } +/// Rate limiting related errors #[derive(Clone, Debug, PartialEq, Eq)] pub enum RateLimitError { + /// Account exceeded their packet limit PacketLimitExceeded, + /// Account exceeded their amount limit ThroughputLimitExceeded, + /// There was an internal error when trying to connect to the store StoreError, } +/// Store trait which manages the rate limit related information of accounts +#[async_trait] pub trait RateLimitStore { + /// The provided account must implement [`RateLimitAccount`](./trait.RateLimitAccount.html) type Account: RateLimitAccount; - fn apply_rate_limits( + /// Apply rate limits based on the packets per minute and amount of per minute + /// limits set on the provided account + async fn apply_rate_limits( &self, account: Self::Account, prepare_amount: u64, - ) -> Box + Send>; - fn refund_throughput_limit( + ) -> Result<(), RateLimitError>; + + /// Refunds the throughput limit which was charged to an account + /// Called if the node receives a reject packet after trying to forward + /// a packet to a peer, meaning that effectively reject packets do not + /// count towards a node's throughput limits + async fn refund_throughput_limit( &self, account: Self::Account, prepare_amount: u64, - ) -> Box + Send>; + ) -> Result<(), ()>; } /// # Rate Limit Service @@ -74,21 +86,20 @@ where } } +#[async_trait] impl IncomingService for RateLimitService where S: AddressStore + RateLimitStore + Clone + Send + Sync + 'static, I: IncomingService + Clone + Send + Sync + 'static, A: RateLimitAccount + Sync + 'static, { - type Future = BoxedIlpFuture; - /// On receiving a request: /// 1. Apply rate limit based on the sender of the request and the amount in the prepare packet in the request /// 1. If no limits were hit forward the request /// - If it succeeds, OK /// - If the request forwarding failed, the client should not be charged towards their throughput limit, so they are refunded, and return a reject /// 1. If the limit was hit, return a reject with the appropriate ErrorCode. - fn handle_request(&mut self, request: IncomingRequest) -> Self::Future { + async fn handle_request(&mut self, request: IncomingRequest) -> IlpResult { let ilp_address = self.store.get_ilp_address(); let mut next = self.next.clone(); let store = self.store.clone(); @@ -98,43 +109,55 @@ where let has_throughput_limit = account.amount_per_minute_limit().is_some(); // request.from and request.amount are used for apply_rate_limits, can't the previous service // always set the account to have None for both? - Box::new(self.store.apply_rate_limits(request.from.clone(), request.prepare.amount()) - .map_err(move |err| { + match self + .store + .apply_rate_limits(request.from.clone(), request.prepare.amount()) + .await + { + Ok(_) => { + let packet = next.handle_request(request).await; + // If we did not get a fulfill, we should refund the sender + if packet.is_err() && has_throughput_limit { + let refunded = store + .refund_throughput_limit(account_clone, prepare_amount) + .await; + // if refunding failed, that's too bad, we will just return the reject + // from the peer + if let Err(err) = refunded { + error!("Error refunding throughput limit: {:?}", err); + } + } + + // return the packet + packet + } + Err(err) => { let code = match err { RateLimitError::PacketLimitExceeded => { if let Some(limit) = account.packets_per_minute_limit() { warn!("Account {} was rate limited for sending too many packets. Limit is: {} per minute", account.id(), limit); } ErrorCode::T05_RATE_LIMITED - }, + } RateLimitError::ThroughputLimitExceeded => { if let Some(limit) = account.amount_per_minute_limit() { warn!("Account {} was throughput limited for trying to send too much money. Limit is: {} per minute", account.id(), limit); } ErrorCode::T04_INSUFFICIENT_LIQUIDITY - }, + } RateLimitError::StoreError => ErrorCode::T00_INTERNAL_ERROR, }; - RejectBuilder { + + let reject = RejectBuilder { code, triggered_by: Some(&ilp_address), message: &[], data: &[], - }.build() - }) - .and_then(move |_| next.handle_request(request)) - .or_else(move |reject| { - if has_throughput_limit { - Either::A(store.refund_throughput_limit(account_clone, prepare_amount) - .then(|result| { - if let Err(err) = result { - error!("Error refunding throughput limit: {:?}", err); - } - Err(reject) - })) - } else { - Either::B(err(reject)) } - })) + .build(); + + Err(reject) + } + } } } diff --git a/crates/interledger-service-util/src/validator_service.rs b/crates/interledger-service-util/src/validator_service.rs index d8a04267a..38c5ef486 100644 --- a/crates/interledger-service-util/src/validator_service.rs +++ b/crates/interledger-service-util/src/validator_service.rs @@ -1,19 +1,18 @@ +use async_trait::async_trait; use chrono::{DateTime, Duration, Utc}; -use futures::{future::err, Future}; use hex; use interledger_packet::{ErrorCode, RejectBuilder}; use interledger_service::*; use log::error; use ring::digest::{digest, SHA256}; use std::marker::PhantomData; -use tokio::prelude::FutureExt; +use tokio::time::timeout; /// # Validator Service /// /// Incoming or Outgoing Service responsible for rejecting timed out /// requests and checking that fulfillments received match the `execution_condition` from the original `Prepare` packets. /// Forwards everything else. -/// #[derive(Clone)] pub struct ValidatorService { store: S, @@ -27,6 +26,8 @@ where S: AddressStore, A: Account, { + /// Create an incoming validator service + /// Forwards incoming requests if not expired, else rejects pub fn incoming(store: S, next: I) -> Self { ValidatorService { store, @@ -42,6 +43,9 @@ where S: AddressStore, A: Account, { + /// Create an outgoing validator service + /// If outgoing request is not expired, it checks that the provided fulfillment is a valid preimage to the + /// prepare packet's fulfillment condition, and if so it forwards it, else rejects pub fn outgoing(store: S, next: O) -> Self { ValidatorService { store, @@ -51,21 +55,20 @@ where } } +#[async_trait] impl IncomingService for ValidatorService where - I: IncomingService, - S: AddressStore, - A: Account, + I: IncomingService + Send + Sync, + S: AddressStore + Send + Sync, + A: Account + Send + Sync, { - type Future = BoxedIlpFuture; - /// On receiving a request: /// 1. If the prepare packet in the request is not expired, forward it, otherwise return a reject - fn handle_request(&mut self, request: IncomingRequest) -> Self::Future { + async fn handle_request(&mut self, request: IncomingRequest) -> IlpResult { let expires_at = DateTime::::from(request.prepare.expires_at()); let now = Utc::now(); if expires_at >= now { - Box::new(self.next.handle_request(request)) + self.next.handle_request(request).await } else { error!( "Incoming packet expired {}ms ago at {:?} (time now: {:?})", @@ -73,26 +76,24 @@ where expires_at.to_rfc3339(), expires_at.to_rfc3339(), ); - let result = Box::new(err(RejectBuilder { + Err(RejectBuilder { code: ErrorCode::R00_TRANSFER_TIMED_OUT, message: &[], triggered_by: Some(&self.store.get_ilp_address()), data: &[], } - .build())); - Box::new(result) + .build()) } } } +#[async_trait] impl OutgoingService for ValidatorService where - O: OutgoingService, - S: AddressStore, - A: Account, + O: OutgoingService + Send + Sync, + S: AddressStore + Send + Sync, + A: Account + Send + Sync, { - type Future = BoxedIlpFuture; - /// On sending a request: /// 1. If the outgoing packet has expired, return a reject with the appropriate ErrorCode /// 1. Tries to forward the request @@ -101,7 +102,7 @@ where /// - If the forwarding is successful, it should receive a fulfill packet. Depending on if the hash of the fulfillment condition inside the fulfill is a preimage of the condition of the prepare: /// - return the fulfill if it matches /// - otherwise reject - fn send_request(&mut self, request: OutgoingRequest) -> Self::Future { + async fn send_request(&mut self, request: OutgoingRequest) -> IlpResult { let mut condition: [u8; 32] = [0; 32]; condition[..].copy_from_slice(request.prepare.execution_condition()); // why? @@ -109,60 +110,61 @@ where let now = Utc::now(); let time_left = expires_at - now; let ilp_address = self.store.get_ilp_address(); - let ilp_address_clone = ilp_address.clone(); if time_left > Duration::zero() { - Box::new( - self.next - .send_request(request) - .timeout(time_left.to_std().expect("Time left must be positive")) - .map_err(move |err| { - // If the error was caused by the timer, into_inner will return None - if let Some(reject) = err.into_inner() { - reject - } else { - error!( - "Outgoing request timed out after {}ms (expiry was: {})", - time_left.num_milliseconds(), - expires_at, - ); - RejectBuilder { - code: ErrorCode::R00_TRANSFER_TIMED_OUT, - message: &[], - triggered_by: Some(&ilp_address_clone), - data: &[], - } - .build() - } - }) - .and_then(move |fulfill| { - let generated_condition = digest(&SHA256, fulfill.fulfillment()); - if generated_condition.as_ref() == condition { - Ok(fulfill) - } else { - error!("Fulfillment did not match condition. Fulfillment: {}, hash: {}, actual condition: {}", hex::encode(fulfill.fulfillment()), hex::encode(generated_condition), hex::encode(condition)); - Err(RejectBuilder { - code: ErrorCode::F09_INVALID_PEER_RESPONSE, - message: b"Fulfillment did not match condition", - triggered_by: Some(&ilp_address), - data: &[], - } - .build()) - } - }), + // Result of the future + let result = timeout( + time_left.to_std().expect("Time left must be positive"), + self.next.send_request(request), ) + .await; + + let fulfill = match result { + // If the future completed in time, it returns an IlpResult, + // which gives us the fulfill packet + Ok(packet) => packet?, + // If the future timed out, then it results in an error + Err(_) => { + error!( + "Outgoing request timed out after {}ms (expiry was: {})", + time_left.num_milliseconds(), + expires_at, + ); + return Err(RejectBuilder { + code: ErrorCode::R00_TRANSFER_TIMED_OUT, + message: &[], + triggered_by: Some(&ilp_address), + data: &[], + } + .build()); + } + }; + + let generated_condition = digest(&SHA256, fulfill.fulfillment()); + if generated_condition.as_ref() == condition { + Ok(fulfill) + } else { + error!("Fulfillment did not match condition. Fulfillment: {}, hash: {}, actual condition: {}", hex::encode(fulfill.fulfillment()), hex::encode(generated_condition), hex::encode(condition)); + Err(RejectBuilder { + code: ErrorCode::F09_INVALID_PEER_RESPONSE, + message: b"Fulfillment did not match condition", + triggered_by: Some(&ilp_address), + data: &[], + } + .build()) + } } else { error!( "Outgoing packet expired {}ms ago", (Duration::zero() - time_left).num_milliseconds(), ); // Already expired - Box::new(err(RejectBuilder { + Err(RejectBuilder { code: ErrorCode::R00_TRANSFER_TIMED_OUT, message: &[], triggered_by: Some(&ilp_address), data: &[], } - .build())) + .build()) } } } @@ -212,16 +214,14 @@ impl Account for TestAccount { struct TestStore; #[cfg(test)] +#[async_trait] impl AddressStore for TestStore { /// Saves the ILP Address in the store's memory and database - fn set_ilp_address( - &self, - _ilp_address: Address, - ) -> Box + Send> { + async fn set_ilp_address(&self, _ilp_address: Address) -> Result<(), ()> { unimplemented!() } - fn clear_ilp_address(&self) -> Box + Send> { + async fn clear_ilp_address(&self) -> Result<(), ()> { unimplemented!() } @@ -241,8 +241,8 @@ mod incoming { time::{Duration, SystemTime}, }; - #[test] - fn lets_through_valid_incoming_packet() { + #[tokio::test] + async fn lets_through_valid_incoming_packet() { let requests = Arc::new(Mutex::new(Vec::new())); let requests_clone = requests.clone(); let mut validator = ValidatorService::incoming( @@ -271,14 +271,14 @@ mod incoming { } .build(), }) - .wait(); + .await; assert_eq!(requests.lock().unwrap().len(), 1); assert!(result.is_ok()); } - #[test] - fn rejects_expired_incoming_packet() { + #[tokio::test] + async fn rejects_expired_incoming_packet() { let requests = Arc::new(Mutex::new(Vec::new())); let requests_clone = requests.clone(); let mut validator = ValidatorService::incoming( @@ -307,7 +307,7 @@ mod incoming { } .build(), }) - .wait(); + .await; assert!(requests.lock().unwrap().is_empty()); assert!(result.is_err()); @@ -328,30 +328,8 @@ mod outgoing { time::{Duration, SystemTime}, }; - #[derive(Clone)] - struct TestStore; - - impl AddressStore for TestStore { - /// Saves the ILP Address in the store's memory and database - fn set_ilp_address( - &self, - _ilp_address: Address, - ) -> Box + Send> { - unimplemented!() - } - - fn clear_ilp_address(&self) -> Box + Send> { - unimplemented!() - } - - /// Get's the store's ilp address from memory - fn get_ilp_address(&self) -> Address { - Address::from_str("example.connector").unwrap() - } - } - - #[test] - fn lets_through_valid_outgoing_response() { + #[tokio::test] + async fn lets_through_valid_outgoing_response() { let requests = Arc::new(Mutex::new(Vec::new())); let requests_clone = requests.clone(); let mut validator = ValidatorService::outgoing( @@ -382,14 +360,14 @@ mod outgoing { } .build(), }) - .wait(); + .await; assert_eq!(requests.lock().unwrap().len(), 1); assert!(result.is_ok()); } - #[test] - fn returns_reject_instead_of_invalid_fulfillment() { + #[tokio::test] + async fn returns_reject_instead_of_invalid_fulfillment() { let requests = Arc::new(Mutex::new(Vec::new())); let requests_clone = requests.clone(); let mut validator = ValidatorService::outgoing( @@ -420,7 +398,7 @@ mod outgoing { } .build(), }) - .wait(); + .await; assert_eq!(requests.lock().unwrap().len(), 1); assert!(result.is_err()); diff --git a/crates/interledger-service/src/lib.rs b/crates/interledger-service/src/lib.rs index 61508f1fa..11ea447b6 100644 --- a/crates/interledger-service/src/lib.rs +++ b/crates/interledger-service/src/lib.rs @@ -140,8 +140,8 @@ pub trait IncomingService { /// and/or handle the result of calling the inner service. fn wrap(self, f: F) -> WrappedService where - F: Fn(IncomingRequest, Self) -> R, - R: Future + Send + 'static, + F: Send + Sync + Fn(IncomingRequest, Box + Send>) -> R, + R: Future, Self: Clone + Sized, { WrappedService::wrap_incoming(self, f) @@ -162,8 +162,8 @@ pub trait OutgoingService { /// and/or handle the result of calling the inner service. fn wrap(self, f: F) -> WrappedService where - F: Fn(OutgoingRequest, Self) -> R, - R: Future + Send + 'static, + F: Send + Sync + Fn(OutgoingRequest, Box + Send>) -> R, + R: Future, Self: Clone + Sized, { WrappedService::wrap_outgoing(self, f) @@ -259,10 +259,10 @@ pub struct WrappedService { impl WrappedService where - F: Fn(IncomingRequest, IO) -> R, + F: Send + Sync + Fn(IncomingRequest, Box + Send>) -> R, + R: Future, IO: IncomingService + Clone, A: Account, - R: Future + Send + 'static, { /// Wrap the given service such that the provided function will /// be called to handle each request. That function can @@ -280,22 +280,22 @@ where #[async_trait] impl IncomingService for WrappedService where - F: Fn(IncomingRequest, IO) -> R + Send + Sync, - IO: IncomingService + Send + Sync + Clone, - A: Account, + F: Send + Sync + Fn(IncomingRequest, Box + Send>) -> R, R: Future + Send + 'static, + IO: IncomingService + Send + Sync + Clone + 'static, + A: Account + Sync, { async fn handle_request(&mut self, request: IncomingRequest) -> IlpResult { - (self.f)(request, (*self.inner).clone()).await + (self.f)(request, Box::new((*self.inner).clone())).await } } impl WrappedService where - F: Fn(OutgoingRequest, IO) -> R, + F: Send + Sync + Fn(OutgoingRequest, Box + Send>) -> R, + R: Future, IO: OutgoingService + Clone, A: Account, - R: Future + Send + 'static, { /// Wrap the given service such that the provided function will /// be called to handle each request. That function can @@ -313,13 +313,13 @@ where #[async_trait] impl OutgoingService for WrappedService where - F: Fn(OutgoingRequest, IO) -> R + Send + Sync, - IO: OutgoingService + Clone + Send + Sync, - A: Account, + F: Send + Sync + Fn(OutgoingRequest, Box + Send>) -> R, R: Future + Send + 'static, + IO: OutgoingService + Send + Sync + Clone + 'static, + A: Account, { async fn send_request(&mut self, request: OutgoingRequest) -> IlpResult { - (self.f)(request, (*self.inner).clone()).await + (self.f)(request, Box::new((*self.inner).clone())).await } } @@ -345,3 +345,209 @@ pub trait AddressStore: Clone { /// (the value is stored in memory because it is read often by all services) fn get_ilp_address(&self) -> Address; } + +// Even though we wrap the types _a lot_ of times in multiple configurations +// the tests still build nearly instantly. The trick is to make the wrapping function +// take a trait object instead of the concrete type +#[cfg(test)] +mod tests { + use super::*; + use lazy_static::lazy_static; + use std::str::FromStr; + + #[test] + fn incoming_service_no_exponential_blowup_when_wrapping() { + // a normal async function + async fn foo( + request: IncomingRequest, + mut next: Box + Send>, + ) -> IlpResult { + next.handle_request(request).await + } + + // and with a closure (async closure are unstable) + let foo2 = move |request, mut next: Box + Send>| { + async move { next.handle_request(request).await } + }; + + // base layer + let s = BaseService; + // our first layer + let s: LayeredService<_, TestAccount> = LayeredService::new_incoming(s); + + // wrapped in our closure + let s = WrappedService::wrap_incoming(s, foo2); + let s = WrappedService::wrap_incoming(s, foo2); + let s = WrappedService::wrap_incoming(s, foo2); + let s = WrappedService::wrap_incoming(s, foo2); + let s = WrappedService::wrap_incoming(s, foo2); + + // wrap it again in the normal service + let s = LayeredService::new_incoming(s); + + // some short syntax + let s = s.wrap(foo2); + let s = s.wrap(foo); + + // called with the full syntax + let s = WrappedService::wrap_incoming(s, foo); + let s = WrappedService::wrap_incoming(s, foo); + let s = WrappedService::wrap_incoming(s, foo); + let s = WrappedService::wrap_incoming(s, foo); + let s = WrappedService::wrap_incoming(s, foo); + + // more short syntax + let s = s.wrap(foo2); + let s = s.wrap(foo2); + let _s = s.wrap(foo2); + } + + #[test] + fn outgoing_service_no_exponential_blowup_when_wrapping() { + // a normal async function + async fn foo( + request: OutgoingRequest, + mut next: Box + Send>, + ) -> IlpResult { + next.send_request(request).await + } + + // and with a closure (async closure are unstable) + let foo2 = move |request, mut next: Box + Send>| { + async move { next.send_request(request).await } + }; + + // base layer + let s = BaseService; + // our first layer + let s: LayeredService<_, TestAccount> = LayeredService::new_outgoing(s); + + // wrapped in our closure + let s = WrappedService::wrap_outgoing(s, foo2); + let s = WrappedService::wrap_outgoing(s, foo2); + let s = WrappedService::wrap_outgoing(s, foo2); + let s = WrappedService::wrap_outgoing(s, foo2); + let s = WrappedService::wrap_outgoing(s, foo2); + + // wrap it again in the normal service + let s = LayeredService::new_outgoing(s); + + // some short syntax + let s = s.wrap(foo2); + let s = s.wrap(foo); + + // called with the full syntax + let s = WrappedService::wrap_outgoing(s, foo); + let s = WrappedService::wrap_outgoing(s, foo); + let s = WrappedService::wrap_outgoing(s, foo); + let s = WrappedService::wrap_outgoing(s, foo); + let s = WrappedService::wrap_outgoing(s, foo); + + // more short syntax + let s = s.wrap(foo2); + let s = s.wrap(foo2); + let _s = s.wrap(foo2); + } + + #[derive(Clone)] + struct BaseService; + + #[derive(Clone)] + struct LayeredService { + next: I, + account_type: PhantomData, + } + + impl LayeredService + where + I: IncomingService + Send + Sync + 'static, + A: Account, + { + fn new_incoming(next: I) -> Self { + Self { + next, + account_type: PhantomData, + } + } + } + + impl LayeredService + where + I: OutgoingService + Send + Sync + 'static, + A: Account, + { + fn new_outgoing(next: I) -> Self { + Self { + next, + account_type: PhantomData, + } + } + } + + #[async_trait] + impl OutgoingService for BaseService { + async fn send_request(&mut self, _request: OutgoingRequest) -> IlpResult { + unimplemented!() + } + } + + #[async_trait] + impl IncomingService for BaseService { + async fn handle_request(&mut self, _request: IncomingRequest) -> IlpResult { + unimplemented!() + } + } + + #[async_trait] + impl OutgoingService for LayeredService + where + I: OutgoingService + Send + Sync + 'static, + A: Account + Send + Sync + 'static, + { + async fn send_request(&mut self, _request: OutgoingRequest) -> IlpResult { + unimplemented!() + } + } + + #[async_trait] + impl IncomingService for LayeredService + where + I: IncomingService + Send + Sync + 'static, + A: Account + Send + Sync + 'static, + { + async fn handle_request(&mut self, _request: IncomingRequest) -> IlpResult { + unimplemented!() + } + } + + // Account test helpers + #[derive(Clone, Debug)] + pub struct TestAccount; + + lazy_static! { + pub static ref ALICE: Username = Username::from_str("alice").unwrap(); + pub static ref EXAMPLE_ADDRESS: Address = Address::from_str("example.alice").unwrap(); + } + + impl Account for TestAccount { + fn id(&self) -> Uuid { + unimplemented!() + } + + fn username(&self) -> &Username { + &ALICE + } + + fn asset_scale(&self) -> u8 { + 9 + } + + fn asset_code(&self) -> &str { + "XYZ" + } + + fn ilp_address(&self) -> &Address { + &EXAMPLE_ADDRESS + } + } +} diff --git a/crates/interledger-settlement/Cargo.toml b/crates/interledger-settlement/Cargo.toml index 0060f6f57..e645c94fd 100644 --- a/crates/interledger-settlement/Cargo.toml +++ b/crates/interledger-settlement/Cargo.toml @@ -8,14 +8,14 @@ edition = "2018" repository = "https://github.com/interledger-rs/interledger-rs" [dependencies] -bytes = { version = "0.4.12", default-features = false } -futures = { version = "0.1.29", default-features = false } -hyper = { version = "0.12.35", default-features = false } +bytes = { version = "0.5", default-features = false } +futures = { version = "0.3.1", default-features = false, features = ["compat"] } +hyper = { version = "0.13.1", default-features = false } interledger-http = { path = "../interledger-http", version = "^0.4.0", default-features = false } interledger-packet = { path = "../interledger-packet", version = "^0.4.0", default-features = false } interledger-service = { path = "../interledger-service", version = "^0.4.0", default-features = false } log = { version = "0.4.8", default-features = false } -reqwest = { version = "0.9.22", default-features = false, features = ["default-tls"] } +reqwest = { version = "0.10", default-features = false, features = ["default-tls", "json"] } serde = { version = "1.0.101", default-features = false } serde_json = { version = "1.0.41", default-features = false } url = { version = "2.1.0", default-features = false } @@ -23,12 +23,13 @@ lazy_static = { version = "1.4.0", default-features = false } uuid = { version = "0.8.1", default-features = false, features = ["v4"] } ring = { version = "0.16.9", default-features = false } tokio-retry = { version = "0.2.0", default-features = false } -tokio = { version = "0.1.22", default-features = false } +tokio = { version = "0.2.6", default-features = false, features = ["macros", "rt-core"] } num-bigint = { version = "0.2.3", default-features = false, features = ["std"] } num-traits = { version = "0.2.8", default-features = false } -warp = { version = "0.1.20", default-features = false } -http = "0.1.19" -redis_crate = { package = "redis", version = "0.13.0", optional = true } +warp = { version = "0.2", default-features = false } +http = "0.2.0" +redis_crate = { package = "redis", version = "0.15.1", optional = true, features = ["tokio-rt-core"] } +async-trait = "0.1.22" [dev-dependencies] parking_lot = { version = "0.9.0", default-features = false } diff --git a/crates/interledger-settlement/src/api/client.rs b/crates/interledger-settlement/src/api/client.rs index 9b8e4cb43..f8a07feb8 100644 --- a/crates/interledger-settlement/src/api/client.rs +++ b/crates/interledger-settlement/src/api/client.rs @@ -1,31 +1,37 @@ use crate::core::types::{Quantity, SettlementAccount}; -use futures::{ - future::{err, Either}, - Future, -}; +use futures::TryFutureExt; use interledger_service::Account; use log::{debug, error, trace}; -use reqwest::r#async::Client; +use reqwest::Client; use serde_json::json; use uuid::Uuid; +/// Helper struct to execute settlements #[derive(Clone)] pub struct SettlementClient { + /// Asynchronous reqwest client http_client: Client, } impl SettlementClient { + /// Simple constructor pub fn new() -> Self { SettlementClient { http_client: Client::new(), } } - pub fn send_settlement( + /// Sends a settlement request to the node's settlement engine for the provided account and amount + /// + /// # Errors + /// 1. Account has no engine configured + /// 1. HTTP request to engine failed from node side + /// 1. HTTP response from engine was an error + pub async fn send_settlement( &self, account: A, amount: u64, - ) -> impl Future { + ) -> Result<(), ()> { if let Some(settlement_engine) = account.settlement_engine_details() { let mut settlement_engine_url = settlement_engine.url; settlement_engine_url @@ -40,23 +46,37 @@ impl SettlementClient { ); let settlement_engine_url_clone = settlement_engine_url.clone(); let idempotency_uuid = Uuid::new_v4().to_hyphenated().to_string(); - return Either::A(self.http_client.post(settlement_engine_url.as_ref()) + let response = self + .http_client + .post(settlement_engine_url.as_ref()) .header("Idempotency-Key", idempotency_uuid) .json(&json!(Quantity::new(amount, account.asset_scale()))) .send() - .map_err(move |err| error!("Error sending settlement command to settlement engine {}: {:?}", settlement_engine_url, err)) - .and_then(move |response| { - if response.status().is_success() { - trace!("Sent settlement of {} to settlement engine: {}", amount, settlement_engine_url_clone); - Ok(()) - } else { - error!("Error sending settlement. Settlement engine responded with HTTP code: {}", response.status()); - Err(()) - } - })); + .map_err(move |err| { + error!( + "Error sending settlement command to settlement engine {}: {:?}", + settlement_engine_url, err + ) + }) + .await?; + + if response.status().is_success() { + trace!( + "Sent settlement of {} to settlement engine: {}", + amount, + settlement_engine_url_clone + ); + return Ok(()); + } else { + error!( + "Error sending settlement. Settlement engine responded with HTTP code: {}", + response.status() + ); + return Err(()); + } } error!("Cannot send settlement for account {} because it does not have the settlement_engine_url and scale configured", account.id()); - Either::B(err(())) + Err(()) } } @@ -70,37 +90,37 @@ impl Default for SettlementClient { mod tests { use super::*; use crate::api::fixtures::TEST_ACCOUNT_0; - use crate::api::test_helpers::{block_on, mock_settlement}; + use crate::api::test_helpers::mock_settlement; use mockito::Matcher; - #[test] - fn settlement_ok() { + #[tokio::test] + async fn settlement_ok() { let m = mock_settlement(200) .match_header("Idempotency-Key", Matcher::Any) .create(); let client = SettlementClient::new(); - let ret = block_on(client.send_settlement(TEST_ACCOUNT_0.clone(), 100)); + let ret = client.send_settlement(TEST_ACCOUNT_0.clone(), 100).await; m.assert(); assert!(ret.is_ok()); } - #[test] - fn engine_rejects() { + #[tokio::test] + async fn engine_rejects() { let m = mock_settlement(500) .match_header("Idempotency-Key", Matcher::Any) .create(); let client = SettlementClient::new(); - let ret = block_on(client.send_settlement(TEST_ACCOUNT_0.clone(), 100)); + let ret = client.send_settlement(TEST_ACCOUNT_0.clone(), 100).await; m.assert(); assert!(ret.is_err()); } - #[test] - fn account_does_not_have_settlement_engine() { + #[tokio::test] + async fn account_does_not_have_settlement_engine() { let m = mock_settlement(200) .expect(0) .match_header("Idempotency-Key", Matcher::Any) @@ -109,7 +129,7 @@ mod tests { let mut acc = TEST_ACCOUNT_0.clone(); acc.no_details = true; // Hide the settlement engine data from the account - let ret = block_on(client.send_settlement(acc, 100)); + let ret = client.send_settlement(acc, 100).await; m.assert(); assert!(ret.is_err()); diff --git a/crates/interledger-settlement/src/api/message_service.rs b/crates/interledger-settlement/src/api/message_service.rs index f91adcc4c..5ec27aab6 100644 --- a/crates/interledger-settlement/src/api/message_service.rs +++ b/crates/interledger-settlement/src/api/message_service.rs @@ -1,20 +1,24 @@ use crate::core::types::{SettlementAccount, SE_ILP_ADDRESS}; -use futures::{ - future::{err, Either}, - Future, Stream, -}; +use async_trait::async_trait; +use futures::{compat::Future01CompatExt, TryFutureExt}; use interledger_packet::{ErrorCode, FulfillBuilder, RejectBuilder}; -use interledger_service::{Account, BoxedIlpFuture, IncomingRequest, IncomingService}; +use interledger_service::{Account, IlpResult, IncomingRequest, IncomingService}; use log::error; -use reqwest::r#async::Client; +use reqwest::Client; use std::marker::PhantomData; use tokio_retry::{strategy::ExponentialBackoff, Retry}; const PEER_FULFILLMENT: [u8; 32] = [0; 32]; +/// Service which implements [`IncomingService`](../../interledger_service/trait.IncomingService.html). +/// Responsible for catching incoming requests which are sent to `peer.settle` and forward them to +/// the node's settlement engine via HTTP #[derive(Clone)] pub struct SettlementMessageService { + /// The next incoming service which requests that don't get caught get sent to next: I, + /// HTTP client used to notify the engine corresponding to the account about + /// an incoming message from a peer's engine http_client: Client, account_type: PhantomData, } @@ -33,14 +37,13 @@ where } } +#[async_trait] impl IncomingService for SettlementMessageService where I: IncomingService + Send, - A: SettlementAccount + Account, + A: SettlementAccount + Account + Send + Sync, { - type Future = BoxedIlpFuture; - - fn handle_request(&mut self, request: IncomingRequest) -> Self::Future { + async fn handle_request(&mut self, request: IncomingRequest) -> IlpResult { // Only handle the request if the destination address matches the ILP address // of the settlement engine being used for this account if let Some(settlement_engine_details) = request.from.settlement_engine_details() { @@ -67,54 +70,75 @@ where .header("Idempotency-Key", idempotency_uuid.clone()) .body(message.clone()) .send() + .compat() // Wrap to a 0.1 future }; - return Box::new(Retry::spawn(ExponentialBackoff::from_millis(10).take(10), action) - .map_err(move |error| { - error!("Error sending message to settlement engine: {:?}", error); - RejectBuilder { - code: ErrorCode::T00_INTERNAL_ERROR, - message: b"Error sending message to settlement engine", - data: &[], - triggered_by: Some(&SE_ILP_ADDRESS), - }.build() - }) - .and_then(move |response| { - let status = response.status(); - if status.is_success() { - Either::A(response.into_body().concat2().map_err(move |err| { - error!("Error concatenating settlement engine response body: {:?}", err); + // TODO: tokio-retry is still not on futures 0.3. As a result, we wrap our action in a + // 0.1 future, and then wrap the Retry future in a 0.3 future to use async/await. + let response = Retry::spawn(ExponentialBackoff::from_millis(10).take(10), action) + .compat() + .map_err(move |error| { + error!("Error sending message to settlement engine: {:?}", error); + RejectBuilder { + code: ErrorCode::T00_INTERNAL_ERROR, + message: b"Error sending message to settlement engine", + data: &[], + triggered_by: Some(&SE_ILP_ADDRESS), + } + .build() + }) + .await?; + let status = response.status(); + if status.is_success() { + let body = response + .bytes() + .map_err(|err| { + error!( + "Error concatenating settlement engine response body: {:?}", + err + ); RejectBuilder { code: ErrorCode::T00_INTERNAL_ERROR, message: b"Error getting settlement engine response", data: &[], triggered_by: Some(&SE_ILP_ADDRESS), - }.build() + } + .build() }) - .and_then(|body| { - Ok(FulfillBuilder { - fulfillment: &PEER_FULFILLMENT, - data: body.as_ref(), - }.build()) - })) + .await?; + + return Ok(FulfillBuilder { + fulfillment: &PEER_FULFILLMENT, + data: body.as_ref(), + } + .build()); + } else { + error!( + "Settlement engine rejected message with HTTP error code: {}", + response.status() + ); + let code = if status.is_client_error() { + ErrorCode::F00_BAD_REQUEST } else { - error!("Settlement engine rejected message with HTTP error code: {}", response.status()); - let code = if status.is_client_error() { - ErrorCode::F00_BAD_REQUEST - } else { - ErrorCode::T00_INTERNAL_ERROR - }; - Either::B(err(RejectBuilder { - code, - message: format!("Settlement engine rejected request with error code: {}", response.status()).as_str().as_ref(), - data: &[], - triggered_by: Some(&SE_ILP_ADDRESS), - }.build())) + ErrorCode::T00_INTERNAL_ERROR + }; + + return Err(RejectBuilder { + code, + message: format!( + "Settlement engine rejected request with error code: {}", + response.status() + ) + .as_str() + .as_ref(), + data: &[], + triggered_by: Some(&SE_ILP_ADDRESS), } - })); + .build()); + } } } - Box::new(self.next.handle_request(request)) + self.next.handle_request(request).await } } @@ -122,18 +146,18 @@ where mod tests { use super::*; use crate::api::fixtures::{BODY, DATA, SERVICE_ADDRESS, TEST_ACCOUNT_0}; - use crate::api::test_helpers::{block_on, mock_message, test_service}; + use crate::api::test_helpers::{mock_message, test_service}; use interledger_packet::{Address, Fulfill, PrepareBuilder, Reject}; use std::str::FromStr; use std::time::SystemTime; - #[test] - fn settlement_ok() { + #[tokio::test] + async fn settlement_ok() { // happy case let m = mock_message(200).create(); let mut settlement = test_service(); - let fulfill: Fulfill = block_on( - settlement.handle_request(IncomingRequest { + let fulfill: Fulfill = settlement + .handle_request(IncomingRequest { from: TEST_ACCOUNT_0.clone(), prepare: PrepareBuilder { amount: 0, @@ -143,22 +167,22 @@ mod tests { execution_condition: &[0; 32], } .build(), - }), - ) - .unwrap(); + }) + .await + .unwrap(); m.assert(); assert_eq!(fulfill.data(), BODY.as_bytes()); assert_eq!(fulfill.fulfillment(), &[0; 32]); } - #[test] - fn gets_forwarded_if_destination_not_engine_() { + #[tokio::test] + async fn gets_forwarded_if_destination_not_engine_() { let m = mock_message(200).create().expect(0); let mut settlement = test_service(); let destination = Address::from_str("example.some.address").unwrap(); - let reject: Reject = block_on( - settlement.handle_request(IncomingRequest { + let reject: Reject = settlement + .handle_request(IncomingRequest { from: TEST_ACCOUNT_0.clone(), prepare: PrepareBuilder { amount: 0, @@ -168,9 +192,9 @@ mod tests { execution_condition: &[0; 32], } .build(), - }), - ) - .unwrap_err(); + }) + .await + .unwrap_err(); m.assert(); assert_eq!(reject.code(), ErrorCode::F02_UNREACHABLE); @@ -178,14 +202,14 @@ mod tests { assert_eq!(reject.message(), b"No other incoming handler!" as &[u8],); } - #[test] - fn account_does_not_have_settlement_engine() { + #[tokio::test] + async fn account_does_not_have_settlement_engine() { let m = mock_message(200).create().expect(0); let mut settlement = test_service(); let mut acc = TEST_ACCOUNT_0.clone(); acc.no_details = true; // Hide the settlement engine data from the account - let reject: Reject = block_on( - settlement.handle_request(IncomingRequest { + let reject: Reject = settlement + .handle_request(IncomingRequest { from: acc.clone(), prepare: PrepareBuilder { amount: 0, @@ -195,9 +219,9 @@ mod tests { execution_condition: &[0; 32], } .build(), - }), - ) - .unwrap_err(); + }) + .await + .unwrap_err(); m.assert(); assert_eq!(reject.code(), ErrorCode::F02_UNREACHABLE); @@ -205,15 +229,15 @@ mod tests { assert_eq!(reject.message(), b"No other incoming handler!"); } - #[test] - fn settlement_engine_rejects() { + #[tokio::test] + async fn settlement_engine_rejects() { // for whatever reason the engine rejects our request with a 500 code let error_code = 500; let error_str = "Internal Server Error"; let m = mock_message(error_code).create(); let mut settlement = test_service(); - let reject: Reject = block_on( - settlement.handle_request(IncomingRequest { + let reject: Reject = settlement + .handle_request(IncomingRequest { from: TEST_ACCOUNT_0.clone(), prepare: PrepareBuilder { amount: 0, @@ -223,9 +247,9 @@ mod tests { execution_condition: &[0; 32], } .build(), - }), - ) - .unwrap_err(); + }) + .await + .unwrap_err(); m.assert(); assert_eq!(reject.code(), ErrorCode::T00_INTERNAL_ERROR); diff --git a/crates/interledger-settlement/src/api/mod.rs b/crates/interledger-settlement/src/api/mod.rs index 621b1769d..9f7510082 100644 --- a/crates/interledger-settlement/src/api/mod.rs +++ b/crates/interledger-settlement/src/api/mod.rs @@ -1,5 +1,9 @@ +/// Settlement-related client methods mod client; +/// [`IncomingService`](../../interledger_service/trait.IncomingService.html) which catches +/// incoming requests which are sent to `peer.settle` (the node's settlement engine ILP address) mod message_service; +/// The Warp API exposed by the connector mod node_api; #[cfg(test)] @@ -7,7 +11,6 @@ mod fixtures; #[cfg(test)] mod test_helpers; -// Expose the API creation filter method and the necessary services pub use client::SettlementClient; pub use message_service::SettlementMessageService; pub use node_api::create_settlements_filter; diff --git a/crates/interledger-settlement/src/api/node_api.rs b/crates/interledger-settlement/src/api/node_api.rs index d095eed11..e88aa348d 100644 --- a/crates/interledger-settlement/src/api/node_api.rs +++ b/crates/interledger-settlement/src/api/node_api.rs @@ -3,16 +3,12 @@ use crate::core::{ idempotency::*, scale_with_precision_loss, types::{ - ApiResponse, LeftoversStore, Quantity, SettlementAccount, SettlementStore, - CONVERSION_ERROR_TYPE, NO_ENGINE_CONFIGURED_ERROR_TYPE, SE_ILP_ADDRESS, + ApiResponse, ApiResult, LeftoversStore, Quantity, SettlementAccount, SettlementStore, + CONVERSION_ERROR_TYPE, SE_ILP_ADDRESS, }, }; -use bytes::buf::FromBuf; use bytes::Bytes; -use futures::{ - future::{err, result}, - Future, -}; +use futures::TryFutureExt; use hyper::{Response, StatusCode}; use interledger_http::error::*; use interledger_packet::PrepareBuilder; @@ -27,11 +23,96 @@ use std::{ use uuid::Uuid; use warp::{self, reject::Rejection, Filter}; +/// Prepare packet's execution condition as defined in the [RFC](https://interledger.org/rfcs/0038-settlement-engines/#exchanging-messages) static PEER_PROTOCOL_CONDITION: [u8; 32] = [ 102, 104, 122, 173, 248, 98, 189, 119, 108, 143, 193, 139, 142, 159, 142, 32, 8, 151, 20, 133, 110, 226, 51, 179, 144, 42, 89, 29, 13, 95, 41, 37, ]; +/// Makes an idempotent call to [`do_receive_settlement`](./fn.do_receive_settlement.html) +/// Returns Status Code 201 +async fn receive_settlement( + account_id: String, + idempotency_key: Option, + quantity: Quantity, + store: S, +) -> Result +where + S: LeftoversStore + + SettlementStore + + IdempotentStore + + AccountStore + + Clone + + Send + + Sync + + 'static, + A: SettlementAccount + Account + Send + Sync + 'static, +{ + let input = format!("{}{:?}", account_id, quantity); + let input_hash = get_hash_of(input.as_ref()); + + let idempotency_key_clone = idempotency_key.clone(); + let store_clone = store.clone(); + let (status_code, message) = make_idempotent_call( + store, + do_receive_settlement(store_clone, account_id, quantity, idempotency_key_clone), + input_hash, + idempotency_key, + StatusCode::CREATED, + "RECEIVED".into(), + ) + .await?; + Ok(Response::builder() + .status(status_code) + .body(message) + .unwrap()) +} + +/// Makes an idempotent call to [`do_send_outgoing_message`](./fn.do_send_outgoing_message.html) +/// Returns Status Code 201 +async fn send_message( + account_id: String, + idempotency_key: Option, + message: Bytes, + store: S, + outgoing_handler: O, +) -> Result +where + S: LeftoversStore + + SettlementStore + + IdempotentStore + + AccountStore + + Clone + + Send + + Sync + + 'static, + O: OutgoingService + Clone + Send + Sync + 'static, + A: SettlementAccount + Account + Send + Sync + 'static, +{ + let input = format!("{}{:?}", account_id, message); + let input_hash = get_hash_of(input.as_ref()); + + let store_clone = store.clone(); + let (status_code, message) = make_idempotent_call( + store, + do_send_outgoing_message(store_clone, outgoing_handler, account_id, message.to_vec()), + input_hash, + idempotency_key, + StatusCode::CREATED, + "SENT".into(), + ) + .await?; + Ok(Response::builder() + .status(status_code) + .body(message) + .unwrap()) +} + +/// Returns a Node Settlement filter which exposes a Warp-compatible +/// idempotent API which +/// 1. receives messages about incoming settlements from the engine +/// 1. sends messages from the connector's engine to the peer's +/// message service which are sent to the peer's engine pub fn create_settlements_filter( store: S, outgoing_handler: O, @@ -50,94 +131,31 @@ where { let with_store = warp::any().map(move || store.clone()).boxed(); let idempotency = warp::header::optional::("idempotency-key"); - let account_id_filter = warp::path("accounts").and(warp::path::param2::()); // account_id + let account_id_filter = warp::path("accounts").and(warp::path::param::()); // account_id // POST /accounts/:account_id/settlements (optional idempotency-key header) // Body is a Quantity object let settlement_endpoint = account_id_filter.and(warp::path("settlements")); - let settlements = warp::post2() + let settlements = warp::post() .and(settlement_endpoint) .and(warp::path::end()) .and(idempotency) .and(warp::body::json()) .and(with_store.clone()) - .and_then( - move |account_id: String, - idempotency_key: Option, - quantity: Quantity, - store: S| { - let input = format!("{}{:?}", account_id, quantity); - let input_hash = get_hash_of(input.as_ref()); - - let idempotency_key_clone = idempotency_key.clone(); - let store_clone = store.clone(); - let receive_settlement_fn = move || { - do_receive_settlement(store_clone, account_id, quantity, idempotency_key_clone) - }; - make_idempotent_call( - store, - receive_settlement_fn, - input_hash, - idempotency_key, - StatusCode::CREATED, - "RECEIVED".into(), - ) - .map_err::<_, Rejection>(move |err| err.into()) - .and_then(move |(status_code, message)| { - Ok(Response::builder() - .status(status_code) - .body(message) - .unwrap()) - }) - }, - ); + .and_then(receive_settlement); // POST /accounts/:account_id/messages (optional idempotency-key header) // Body is a Vec object let with_outgoing_handler = warp::any().map(move || outgoing_handler.clone()).boxed(); let messages_endpoint = account_id_filter.and(warp::path("messages")); - let messages = warp::post2() + let messages = warp::post() .and(messages_endpoint) .and(warp::path::end()) .and(idempotency) - .and(warp::body::concat()) - .and(with_store.clone()) - .and(with_outgoing_handler.clone()) - .and_then( - move |account_id: String, - idempotency_key: Option, - body: warp::body::FullBody, - store: S, - outgoing_handler: O| { - // Gets called by our settlement engine, forwards the request outwards - // until it reaches the peer's settlement engine. - let message = Vec::from_buf(body); - let input = format!("{}{:?}", account_id, message); - let input_hash = get_hash_of(input.as_ref()); - - let store_clone = store.clone(); - // Wrap do_send_outgoing_message in a closure to be invoked by - // the idempotency wrapper - let send_outgoing_message_fn = move || { - do_send_outgoing_message(store_clone, outgoing_handler, account_id, message) - }; - make_idempotent_call( - store, - send_outgoing_message_fn, - input_hash, - idempotency_key, - StatusCode::CREATED, - "SENT".into(), - ) - .map_err::<_, Rejection>(move |err| err.into()) - .and_then(move |(status_code, message)| { - Ok(Response::builder() - .status(status_code) - .body(message) - .unwrap()) - }) - }, - ); + .and(warp::body::bytes()) + .and(with_store) + .and(with_outgoing_handler) + .and_then(send_message); settlements .or(messages) @@ -145,12 +163,16 @@ where .boxed() } -fn do_receive_settlement( +/// Receives a settlement message from the connector's engine, proceeds to scale it to the +/// asset scale which corresponds to the account, and finally increases the account's balance +/// by the processed amount. This implements the main functionality by which an account's credit +/// is repaid, allowing them to send out more payments +async fn do_receive_settlement( store: S, account_id: String, body: Quantity, idempotency_key: Option, -) -> Box + Send> +) -> ApiResult where S: LeftoversStore + SettlementStore @@ -167,103 +189,111 @@ where let engine_scale = body.scale; // Convert to the desired data types - let account_id = match Uuid::from_str(&account_id) { - Ok(a) => a, - Err(_) => { - let error_msg = format!("Unable to parse account id: {}", account_id); - error!("{}", error_msg); - return Box::new(err(ApiError::invalid_account_id(Some(&account_id)))); - } - }; + let account_id = Uuid::from_str(&account_id).map_err(move |_| { + let err = ApiError::invalid_account_id(Some(&account_id)); + error!("{}", err); + err + })?; + + let engine_amount = BigUint::from_str(&engine_amount).map_err(|_| { + let error_msg = format!("Could not convert amount: {:?}", engine_amount); + error!("{}", error_msg); + ApiError::from_api_error_type(&CONVERSION_ERROR_TYPE).detail(error_msg) + })?; + + let accounts = store + .get_accounts(vec![account_id]) + .map_err(move |_| { + let err = ApiError::account_not_found() + .detail(format!("Account {} was not found", account_id)); + error!("{}", err); + err + }) + .await?; + + let account = &accounts[0]; + if account.settlement_engine_details().is_none() { + let err = ApiError::account_not_found().detail(format!("Account {} has no settlement engine details configured, cannot send a settlement engine message to that account", accounts[0].id())); + error!("{}", err); + return Err(err); + } - let engine_amount = match BigUint::from_str(&engine_amount) { - Ok(a) => a, - Err(_) => { - let error_msg = format!("Could not convert amount: {:?}", engine_amount); + let account_id = account.id(); + let asset_scale = account.asset_scale(); + // Scale to account's scale from the engine's scale + // If we're downscaling we might have some precision error which + // we must save as leftovers. Upscaling is OK since we're using + // biguint's. + let (scaled_engine_amount, precision_loss) = + scale_with_precision_loss(engine_amount, asset_scale, engine_scale); + + // This will load any leftovers (which are saved in the highest + // so far received scale by the engine), will scale them to + // the account's asset scale and return them. If there was any + // precision loss due to downscaling, it will also update the + // leftovers to the new leftovers value + let scaled_leftover_amount = store_clone + .load_uncredited_settlement_amount(account_id, asset_scale) + .map_err(move |_err| { + let error_msg = format!( + "Error getting uncredited settlement amount for: {}", + account.id() + ); error!("{}", error_msg); - return Box::new(err( - ApiError::from_api_error_type(&CONVERSION_ERROR_TYPE).detail(error_msg) - )); - } - }; + let error_type = ApiErrorType { + r#type: &ProblemType::Default, + status: StatusCode::INTERNAL_SERVER_ERROR, + title: "Load uncredited settlement amount error", + }; + ApiError::from_api_error_type(&error_type).detail(error_msg) + }) + .await?; + + // add the leftovers to the scaled engine amount + let total_amount = scaled_engine_amount.clone() + scaled_leftover_amount; + let engine_amount_u64 = total_amount.to_u64().unwrap_or(std::u64::MAX); + + let ret = futures::future::join_all(vec![ + // update the account's balance in the store + store.update_balance_for_incoming_settlement( + account_id, + engine_amount_u64, + idempotency_key, + ), + // save any precision loss that occurred during the + // scaling of the engine's amount to the account's scale + store.save_uncredited_settlement_amount(account_id, (precision_loss, engine_scale)), + ]) + .await; + + // if any of the futures errored, then we should propagate that + if ret.iter().any(|r| r.is_err()) { + let error_msg = format!( + "Error updating the balance and leftovers of account: {}", + account_id + ); + error!("{}", error_msg); + let error_type = ApiErrorType { + r#type: &ProblemType::Default, + status: StatusCode::INTERNAL_SERVER_ERROR, + title: "Balance update error", + }; + return Err(ApiError::from_api_error_type(&error_type).detail(error_msg)); + } - Box::new( - store.get_accounts(vec![account_id]) - .map_err(move |_err| { - let err = ApiError::account_not_found().detail(format!("Account {} was not found", account_id)); - error!("{}", err); - err - }) - .and_then(move |accounts| { - let account = &accounts[0]; - if account.settlement_engine_details().is_some() { - Ok(account.clone()) - } else { - let error_msg = format!("Account {} does not have settlement engine details configured. Cannot handle incoming settlement", account.id()); - error!("{}", error_msg); - Err(ApiError::from_api_error_type(&NO_ENGINE_CONFIGURED_ERROR_TYPE).detail(error_msg)) - } - }) - .and_then(move |account| { - let account_id = account.id(); - let asset_scale = account.asset_scale(); - // Scale to account's scale from the engine's scale - // If we're downscaling we might have some precision error which - // we must save as leftovers. Upscaling is OK since we're using - // biguint's. - let (scaled_engine_amount, precision_loss) = scale_with_precision_loss(engine_amount, asset_scale, engine_scale); - - // This will load any leftovers (which are saved in the highest - // so far received scale by the engine), will scale them to - // the account's asset scale and return them. If there was any - // precision loss due to downscaling, it will also update the - // leftovers to the new leftovers value - store_clone.load_uncredited_settlement_amount(account_id, asset_scale) - .map_err(move |_err| { - let error_msg = format!("Error getting uncredited settlement amount for: {}", account.id()); - error!("{}", error_msg); - let error_type = ApiErrorType { - r#type: &ProblemType::Default, - status: StatusCode::INTERNAL_SERVER_ERROR, - title: "Load uncredited settlement amount error", - }; - ApiError::from_api_error_type(&error_type).detail(error_msg) - }) - .and_then(move |scaled_leftover_amount| { - // add the leftovers to the scaled engine amount - let total_amount = scaled_engine_amount.clone() + scaled_leftover_amount; - let engine_amount_u64 = total_amount.to_u64().unwrap_or(std::u64::MAX); - - futures::future::join_all(vec![ - // update the account's balance in the store - store.update_balance_for_incoming_settlement(account_id, engine_amount_u64, idempotency_key), - // save any precision loss that occurred during the - // scaling of the engine's amount to the account's scale - store.save_uncredited_settlement_amount(account_id, (precision_loss, engine_scale)) - ]) - .map_err(move |_| { - let error_msg = format!("Error updating the balance and leftovers of account: {}", account_id); - error!("{}", error_msg); - let error_type = ApiErrorType { - r#type: &ProblemType::Default, - status: StatusCode::INTERNAL_SERVER_ERROR, - title: "Balance update error" - }; - ApiError::from_api_error_type(&error_type).detail(error_msg) - }) - .and_then(move |_| { - Ok(ApiResponse::Default) - }) - }) - })) + Ok(ApiResponse::Default) } -fn do_send_outgoing_message( +/// Sends a messages via the provided `outgoing_handler` with the `peer.settle` +/// ILP Address as ultimate destination. This messages should get caught by the +/// peer's message service, get forwarded to their engine, and then the response +/// should be communicated back via a Fulfill or Reject packet +async fn do_send_outgoing_message( store: S, - mut outgoing_handler: O, + outgoing_handler: O, account_id: String, body: Vec, -) -> Box + Send> +) -> ApiResult where S: LeftoversStore + SettlementStore @@ -271,69 +301,74 @@ where + AccountStore + Clone + Send - + Sync - + 'static, - O: OutgoingService + Clone + Send + Sync + 'static, - A: SettlementAccount + Account + Send + Sync + 'static, + + Sync, + O: OutgoingService + Clone + Send + Sync, + A: SettlementAccount + Account + Send + Sync, { - Box::new(result(Uuid::from_str(&account_id) - .map_err(move |_| { - let err = ApiError::invalid_account_id(Some(&account_id)); - error!("{}", err); - err - })) - .and_then(move |account_id| { - store.get_accounts(vec![account_id]) - .map_err(move |_| { - let err = ApiError::account_not_found().detail(format!("Account {} was not found", account_id)); - error!("{}", err); - err - }) - }) - .and_then(|accounts| { - let account = &accounts[0]; - if account.settlement_engine_details().is_some() { - Ok(account.clone()) - } else { - let err = ApiError::account_not_found().detail(format!("Account {} has no settlement engine details configured, cannot send a settlement engine message to that account", accounts[0].id())); - error!("{}", err); - Err(err) + let account_id = Uuid::from_str(&account_id).map_err(move |_| { + let err = ApiError::invalid_account_id(Some(&account_id)); + error!("{}", err); + err + })?; + let accounts = store + .get_accounts(vec![account_id]) + .map_err(move |_| { + let err = ApiError::account_not_found() + .detail(format!("Account {} was not found", account_id)); + error!("{}", err); + err + }) + .await?; + + let account = &accounts[0]; + if account.settlement_engine_details().is_none() { + let err = ApiError::account_not_found().detail(format!("Account {} has no settlement engine details configured, cannot send a settlement engine message to that account", accounts[0].id())); + error!("{}", err); + return Err(err); + } + + // Send the message to the peer's settlement engine. + // Note that we use dummy values for the `from` and `original_amount` + // because this `OutgoingRequest` will bypass the router and thus will not + // use either of these values. Including dummy values in the rare case where + // we do not need them seems easier than using + // `Option`s all over the place. + let packet = { + let mut handler = outgoing_handler.clone(); + handler + .send_request(OutgoingRequest { + from: account.clone(), + to: account.clone(), + original_amount: 0, + prepare: PrepareBuilder { + destination: SE_ILP_ADDRESS.clone(), + amount: 0, + expires_at: SystemTime::now() + Duration::from_secs(30), + data: &body, + execution_condition: &PEER_PROTOCOL_CONDITION, } + .build(), }) - .and_then(move |account| { - // Send the message to the peer's settlement engine. - // Note that we use dummy values for the `from` and `original_amount` - // because this `OutgoingRequest` will bypass the router and thus will not - // use either of these values. Including dummy values in the rare case where - // we do not need them seems easier than using - // `Option`s all over the place. - outgoing_handler.send_request(OutgoingRequest { - from: account.clone(), - to: account.clone(), - original_amount: 0, - prepare: PrepareBuilder { - destination: SE_ILP_ADDRESS.clone(), - amount: 0, - expires_at: SystemTime::now() + Duration::from_secs(30), - data: &body, - execution_condition: &PEER_PROTOCOL_CONDITION, - }.build() - }) - .map_err(move |reject| { - let error_msg = format!("Error sending message to peer settlement engine. Packet rejected with code: {}, message: {}", reject.code(), str::from_utf8(reject.message()).unwrap_or_default()); - let error_type = ApiErrorType { - r#type: &ProblemType::Default, - status: StatusCode::BAD_GATEWAY, - title: "Error sending message to peer engine", - }; - error!("{}", error_msg); - ApiError::from_api_error_type(&error_type).detail(error_msg) - }) - }) - .and_then(move |fulfill| { - let data = Bytes::from(fulfill.data()); - Ok(ApiResponse::Data(data)) - })) + .await + }; + + match packet { + Ok(fulfill) => { + // TODO: Can we avoid copying here? + let data = Bytes::copy_from_slice(fulfill.data()); + Ok(ApiResponse::Data(data)) + } + Err(reject) => { + let error_msg = format!("Error sending message to peer settlement engine. Packet rejected with code: {}, message: {}", reject.code(), str::from_utf8(reject.message()).unwrap_or_default()); + let error_type = ApiErrorType { + r#type: &ProblemType::Default, + status: StatusCode::BAD_GATEWAY, + title: "Error sending message to peer engine", + }; + error!("{}", error_msg); + Err(ApiError::from_api_error_type(&error_type).detail(error_msg)) + } + } } #[cfg(test)] @@ -371,11 +406,12 @@ mod tests { // Settlement Tests mod settlement_tests { use super::*; + use bytes::Bytes; use serde_json::json; const OUR_SCALE: u8 = 11; - fn settlement_call( + async fn settlement_call( api: &F, id: &str, amount: u64, @@ -394,11 +430,11 @@ mod tests { if let Some(idempotency_key) = idempotency_key { response = response.header("Idempotency-Key", idempotency_key); } - response.reply(api) + response.reply(api).await } - #[test] - fn settlement_ok() { + #[tokio::test] + async fn settlement_ok() { let id = TEST_ACCOUNT_0.clone().id.to_string(); let store = test_store(false, true); let api = test_api(store.clone(), false); @@ -407,17 +443,17 @@ mod tests { // = 9. When // we send a settlement with scale OUR_SCALE, the connector should respond // with 2 less 0's. - let response = settlement_call(&api, &id, 200, OUR_SCALE, Some(IDEMPOTENCY)); + let response = settlement_call(&api, &id, 200, OUR_SCALE, Some(IDEMPOTENCY)).await; assert_eq!(response.body(), &Bytes::from("RECEIVED")); assert_eq!(response.status(), StatusCode::CREATED); // check that it's idempotent - let response = settlement_call(&api, &id, 200, OUR_SCALE, Some(IDEMPOTENCY)); + let response = settlement_call(&api, &id, 200, OUR_SCALE, Some(IDEMPOTENCY)).await; assert_eq!(response.body(), &Bytes::from("RECEIVED")); assert_eq!(response.status(), StatusCode::CREATED); // fails with different account id - let response = settlement_call(&api, "2", 200, OUR_SCALE, Some(IDEMPOTENCY)); + let response = settlement_call(&api, "2", 200, OUR_SCALE, Some(IDEMPOTENCY)).await; check_error_status_and_message( response, 409, @@ -425,7 +461,7 @@ mod tests { ); // fails with different settlement data and account id - let response = settlement_call(&api, "2", 42, OUR_SCALE, Some(IDEMPOTENCY)); + let response = settlement_call(&api, "2", 42, OUR_SCALE, Some(IDEMPOTENCY)).await; check_error_status_and_message( response, 409, @@ -433,7 +469,7 @@ mod tests { ); // fails with different settlement data and same account id - let response = settlement_call(&api, &id, 42, OUR_SCALE, Some(IDEMPOTENCY)); + let response = settlement_call(&api, &id, 42, OUR_SCALE, Some(IDEMPOTENCY)).await; check_error_status_and_message( response, 409, @@ -441,7 +477,7 @@ mod tests { ); // works without idempotency key - let response = settlement_call(&api, &id, 400, OUR_SCALE, None); + let response = settlement_call(&api, &id, 400, OUR_SCALE, None).await; assert_eq!(response.body(), &Bytes::from("RECEIVED")); assert_eq!(response.status(), StatusCode::CREATED); @@ -452,19 +488,19 @@ mod tests { let cache_hits = s.cache_hits.read(); assert_eq!(*cache_hits, 4); assert_eq!(cached_data.status, StatusCode::CREATED); - assert_eq!(cached_data.body, &Bytes::from("RECEIVED")); + assert_eq!(cached_data.body, &bytes::Bytes::from("RECEIVED")); } - #[test] + #[tokio::test] // The connector must save the difference each time there's precision // loss and try to add it the amount it's being notified to settle for the next time. - fn settlement_leftovers() { + async fn settlement_leftovers() { let id = TEST_ACCOUNT_0.clone().id.to_string(); let store = test_store(false, true); let api = test_api(store.clone(), false); // Send 205 with scale 11, 2 decimals lost -> 0.05 leftovers - let response = settlement_call(&api, &id, 205, 11, None); + let response = settlement_call(&api, &id, 205, 11, None).await; assert_eq!(response.body(), &Bytes::from("RECEIVED")); // balance should be 2 @@ -472,75 +508,75 @@ mod tests { assert_eq!( store .get_uncredited_settlement_amount(TEST_ACCOUNT_0.id) - .wait() + .await .unwrap(), (BigUint::from(5u32), 11) ); // Send 855 with scale 12, 3 decimals lost -> 0.855 leftovers, - let response = settlement_call(&api, &id, 855, 12, None); + let response = settlement_call(&api, &id, 855, 12, None).await; assert_eq!(response.body(), &Bytes::from("RECEIVED")); assert_eq!(store.get_balance(TEST_ACCOUNT_0.id), 2); // total leftover: 0.905 = 0.05 + 0.855 assert_eq!( store .get_uncredited_settlement_amount(TEST_ACCOUNT_0.id) - .wait() + .await .unwrap(), (BigUint::from(905u32), 12) ); // send 110 with scale 11, 2 decimals lost -> 0.1 leftover - let response = settlement_call(&api, &id, 110, 11, None); + let response = settlement_call(&api, &id, 110, 11, None).await; assert_eq!(response.body(), &Bytes::from("RECEIVED")); assert_eq!(store.get_balance(TEST_ACCOUNT_0.id), 3); assert_eq!( store .get_uncredited_settlement_amount(TEST_ACCOUNT_0.id) - .wait() + .await .unwrap(), (BigUint::from(1005u32), 12) ); // send 5 with scale 9, will consume the leftovers and increase // total balance by 6 while updating the rest of the leftovers - let response = settlement_call(&api, &id, 5, 9, None); + let response = settlement_call(&api, &id, 5, 9, None).await; assert_eq!(response.body(), &Bytes::from("RECEIVED")); assert_eq!(store.get_balance(TEST_ACCOUNT_0.id), 9); assert_eq!( store .get_uncredited_settlement_amount(TEST_ACCOUNT_0.id) - .wait() + .await .unwrap(), (BigUint::from(5u32), 12) ); // we send a payment with a smaller scale than the account now - let response = settlement_call(&api, &id, 2, 7, None); + let response = settlement_call(&api, &id, 2, 7, None).await; assert_eq!(response.body(), &Bytes::from("RECEIVED")); assert_eq!(store.get_balance(TEST_ACCOUNT_0.id), 209); // leftovers are still the same assert_eq!( store .get_uncredited_settlement_amount(TEST_ACCOUNT_0.id) - .wait() + .await .unwrap(), (BigUint::from(5u32), 12) ); } - #[test] - fn account_has_no_engine_configured() { + #[tokio::test] + async fn account_has_no_engine_configured() { let id = TEST_ACCOUNT_0.clone().id.to_string(); let store = test_store(false, false); let api = test_api(store.clone(), false); - let response = settlement_call(&api, &id, 100, 18, Some(IDEMPOTENCY)); - check_error_status_and_message(response, 404, "Account 00000000-0000-0000-0000-000000000000 does not have settlement engine details configured. Cannot handle incoming settlement"); + let response = settlement_call(&api, &id, 100, 18, Some(IDEMPOTENCY)).await; + check_error_status_and_message(response, 404, "Account 00000000-0000-0000-0000-000000000000 has no settlement engine details configured, cannot send a settlement engine message to that account"); // check that it's idempotent - let response = settlement_call(&api, &id, 100, 18, Some(IDEMPOTENCY)); - check_error_status_and_message(response, 404, "Account 00000000-0000-0000-0000-000000000000 does not have settlement engine details configured. Cannot handle incoming settlement"); + let response = settlement_call(&api, &id, 100, 18, Some(IDEMPOTENCY)).await; + check_error_status_and_message(response, 404, "Account 00000000-0000-0000-0000-000000000000 has no settlement engine details configured, cannot send a settlement engine message to that account"); let s = store.clone(); let cache = s.cache.read(); @@ -549,34 +585,34 @@ mod tests { let cache_hits = s.cache_hits.read(); assert_eq!(*cache_hits, 1); assert_eq!(cached_data.status, 404); - assert_eq!(cached_data.body, &Bytes::from("Account 00000000-0000-0000-0000-000000000000 does not have settlement engine details configured. Cannot handle incoming settlement")); + assert_eq!(cached_data.body, &bytes::Bytes::from("Account 00000000-0000-0000-0000-000000000000 has no settlement engine details configured, cannot send a settlement engine message to that account")); } - #[test] - fn update_balance_for_incoming_settlement_fails() { + #[tokio::test] + async fn update_balance_for_incoming_settlement_fails() { let id = TEST_ACCOUNT_0.clone().id.to_string(); let store = test_store(true, true); let api = test_api(store, false); - let response = settlement_call(&api, &id, 100, 18, None); + let response = settlement_call(&api, &id, 100, 18, None).await; assert_eq!(response.status().as_u16(), 500); } - #[test] - fn invalid_account_id() { + #[tokio::test] + async fn invalid_account_id() { // the api is configured to take an accountId type // supplying an id that cannot be parsed to that type must fail let id = "a".to_string(); let store = test_store(false, true); let api = test_api(store.clone(), false); - let ret = settlement_call(&api, &id, 100, 18, Some(IDEMPOTENCY)); + let ret = settlement_call(&api, &id, 100, 18, Some(IDEMPOTENCY)).await; check_error_status_and_message(ret, 400, "a is an invalid account ID"); // check that it's idempotent - let ret = settlement_call(&api, &id, 100, 18, Some(IDEMPOTENCY)); + let ret = settlement_call(&api, &id, 100, 18, Some(IDEMPOTENCY)).await; check_error_status_and_message(ret, 400, "a is an invalid account ID"); - let _ret = settlement_call(&api, &id, 100, 18, Some(IDEMPOTENCY)); + let _ret = settlement_call(&api, &id, 100, 18, Some(IDEMPOTENCY)).await; let s = store.clone(); let cache = s.cache.read(); @@ -585,23 +621,26 @@ mod tests { let cache_hits = s.cache_hits.read(); assert_eq!(*cache_hits, 2); assert_eq!(cached_data.status, 400); - assert_eq!(cached_data.body, &Bytes::from("a is an invalid account ID")); + assert_eq!( + cached_data.body, + &bytes::Bytes::from("a is an invalid account ID") + ); } - #[test] - fn account_not_in_store() { + #[tokio::test] + async fn account_not_in_store() { let id = TEST_ACCOUNT_0.clone().id.to_string(); let store = TestStore::new(vec![], false); let api = test_api(store.clone(), false); - let ret = settlement_call(&api, &id, 100, 18, Some(IDEMPOTENCY)); + let ret = settlement_call(&api, &id, 100, 18, Some(IDEMPOTENCY)).await; check_error_status_and_message( ret, 404, "Account 00000000-0000-0000-0000-000000000000 was not found", ); - let ret = settlement_call(&api, &id, 100, 18, Some(IDEMPOTENCY)); + let ret = settlement_call(&api, &id, 100, 18, Some(IDEMPOTENCY)).await; check_error_status_and_message( ret, 404, @@ -616,15 +655,16 @@ mod tests { assert_eq!(cached_data.status, 404); assert_eq!( cached_data.body, - &Bytes::from("Account 00000000-0000-0000-0000-000000000000 was not found") + &bytes::Bytes::from("Account 00000000-0000-0000-0000-000000000000 was not found") ); } } mod message_tests { use super::*; + use bytes::Bytes; - fn messages_call( + async fn messages_call( api: &F, id: &str, message: &[u8], @@ -642,26 +682,26 @@ mod tests { if let Some(idempotency_key) = idempotency_key { response = response.header("Idempotency-Key", idempotency_key); } - response.reply(api) + response.reply(api).await } - #[test] - fn message_ok() { + #[tokio::test] + async fn message_ok() { let id = TEST_ACCOUNT_0.clone().id.to_string(); let store = test_store(false, true); let api = test_api(store.clone(), true); - let ret = messages_call(&api, &id, &[], Some(IDEMPOTENCY)); + let ret = messages_call(&api, &id, &[], Some(IDEMPOTENCY)).await; assert_eq!(ret.status(), StatusCode::CREATED); assert_eq!(ret.body(), &Bytes::from("hello!")); - let ret = messages_call(&api, &id, &[], Some(IDEMPOTENCY)); + let ret = messages_call(&api, &id, &[], Some(IDEMPOTENCY)).await; assert_eq!(ret.status(), StatusCode::CREATED); assert_eq!(ret.body(), &Bytes::from("hello!")); // Using the same idempotency key with different arguments MUST // fail. - let ret = messages_call(&api, "1", &[], Some(IDEMPOTENCY)); + let ret = messages_call(&api, "1", &[], Some(IDEMPOTENCY)).await; check_error_status_and_message( ret, 409, @@ -670,7 +710,7 @@ mod tests { let data = [0, 1, 2]; // fails with different account id and data - let ret = messages_call(&api, "1", &data[..], Some(IDEMPOTENCY)); + let ret = messages_call(&api, "1", &data[..], Some(IDEMPOTENCY)).await; check_error_status_and_message( ret, 409, @@ -678,7 +718,7 @@ mod tests { ); // fails for same account id but different data - let ret = messages_call(&api, &id, &data[..], Some(IDEMPOTENCY)); + let ret = messages_call(&api, &id, &data[..], Some(IDEMPOTENCY)).await; check_error_status_and_message( ret, 409, @@ -691,19 +731,19 @@ mod tests { let cache_hits = s.cache_hits.read(); assert_eq!(*cache_hits, 4); assert_eq!(cached_data.status, StatusCode::CREATED); - assert_eq!(cached_data.body, &Bytes::from("hello!")); + assert_eq!(cached_data.body, &bytes::Bytes::from("hello!")); } - #[test] - fn message_gets_rejected() { + #[tokio::test] + async fn message_gets_rejected() { let id = TEST_ACCOUNT_0.clone().id.to_string(); let store = test_store(false, true); let api = test_api(store.clone(), false); - let ret = messages_call(&api, &id, &[], Some(IDEMPOTENCY)); + let ret = messages_call(&api, &id, &[], Some(IDEMPOTENCY)).await; check_error_status_and_message(ret, 502, "Error sending message to peer settlement engine. Packet rejected with code: F02, message: No other outgoing handler!"); - let ret = messages_call(&api, &id, &[], Some(IDEMPOTENCY)); + let ret = messages_call(&api, &id, &[], Some(IDEMPOTENCY)).await; check_error_status_and_message(ret, 502, "Error sending message to peer settlement engine. Packet rejected with code: F02, message: No other outgoing handler!"); let s = store.clone(); @@ -712,24 +752,24 @@ mod tests { let cache_hits = s.cache_hits.read(); assert_eq!(*cache_hits, 1); assert_eq!(cached_data.status, 502); - assert_eq!(cached_data.body, &Bytes::from("Error sending message to peer settlement engine. Packet rejected with code: F02, message: No other outgoing handler!")); + assert_eq!(cached_data.body, &bytes::Bytes::from("Error sending message to peer settlement engine. Packet rejected with code: F02, message: No other outgoing handler!")); } - #[test] - fn invalid_account_id() { + #[tokio::test] + async fn invalid_account_id() { // the api is configured to take an accountId type // supplying an id that cannot be parsed to that type must fail let id = "a".to_string(); let store = test_store(false, true); let api = test_api(store.clone(), true); - let ret = messages_call(&api, &id, &[], Some(IDEMPOTENCY)); + let ret = messages_call(&api, &id, &[], Some(IDEMPOTENCY)).await; check_error_status_and_message(ret, 400, "a is an invalid account ID"); - let ret = messages_call(&api, &id, &[], Some(IDEMPOTENCY)); + let ret = messages_call(&api, &id, &[], Some(IDEMPOTENCY)).await; check_error_status_and_message(ret, 400, "a is an invalid account ID"); - let ret = messages_call(&api, &id, &[], Some(IDEMPOTENCY)); + let ret = messages_call(&api, &id, &[], Some(IDEMPOTENCY)).await; check_error_status_and_message(ret, 400, "a is an invalid account ID"); let s = store.clone(); @@ -739,23 +779,26 @@ mod tests { let cache_hits = s.cache_hits.read(); assert_eq!(*cache_hits, 2); assert_eq!(cached_data.status, 400); - assert_eq!(cached_data.body, &Bytes::from("a is an invalid account ID")); + assert_eq!( + cached_data.body, + &bytes::Bytes::from("a is an invalid account ID") + ); } - #[test] - fn account_not_in_store() { + #[tokio::test] + async fn account_not_in_store() { let id = TEST_ACCOUNT_0.clone().id.to_string(); let store = TestStore::new(vec![], false); let api = test_api(store.clone(), true); - let ret = messages_call(&api, &id, &[], Some(IDEMPOTENCY)); + let ret = messages_call(&api, &id, &[], Some(IDEMPOTENCY)).await; check_error_status_and_message( ret, 404, "Account 00000000-0000-0000-0000-000000000000 was not found", ); - let ret = messages_call(&api, &id, &[], Some(IDEMPOTENCY)); + let ret = messages_call(&api, &id, &[], Some(IDEMPOTENCY)).await; check_error_status_and_message( ret, 404, @@ -771,7 +814,7 @@ mod tests { assert_eq!(cached_data.status, 404); assert_eq!( cached_data.body, - &Bytes::from("Account 00000000-0000-0000-0000-000000000000 was not found") + &bytes::Bytes::from("Account 00000000-0000-0000-0000-000000000000 was not found") ); } } diff --git a/crates/interledger-settlement/src/api/test_helpers.rs b/crates/interledger-settlement/src/api/test_helpers.rs index ac207ab03..c3abe8363 100644 --- a/crates/interledger-settlement/src/api/test_helpers.rs +++ b/crates/interledger-settlement/src/api/test_helpers.rs @@ -8,10 +8,6 @@ use crate::core::{ }, }; use bytes::Bytes; -use futures::{ - future::{err, ok}, - Future, -}; use hyper::StatusCode; use interledger_packet::{Address, ErrorCode, FulfillBuilder, RejectBuilder}; use interledger_service::{ @@ -22,12 +18,13 @@ use num_bigint::BigUint; use uuid::Uuid; use super::fixtures::{BODY, MESSAGES_API, SERVICE_ADDRESS, SETTLEMENT_API, TEST_ACCOUNT_0}; +use async_trait::async_trait; use lazy_static::lazy_static; use parking_lot::RwLock; +use std::cmp::Ordering; use std::collections::HashMap; use std::str::FromStr; use std::sync::Arc; -use tokio::runtime::Runtime; use url::Url; #[derive(Debug, Clone)] @@ -86,73 +83,75 @@ pub struct TestStore { pub uncredited_settlement_amount: Arc>>, } +#[async_trait] impl SettlementStore for TestStore { type Account = TestAccount; - fn update_balance_for_incoming_settlement( + async fn update_balance_for_incoming_settlement( &self, account_id: Uuid, amount: u64, _idempotency_key: Option, - ) -> Box + Send> { + ) -> Result<(), ()> { let mut accounts = self.accounts.write(); for mut a in &mut *accounts { if a.id() == account_id { a.balance += amount as i64; } } - let ret = if self.should_fail { err(()) } else { ok(()) }; - Box::new(ret) + if self.should_fail { + Err(()) + } else { + Ok(()) + } } - fn refund_settlement( - &self, - _account_id: Uuid, - _settle_amount: u64, - ) -> Box + Send> { - let ret = if self.should_fail { err(()) } else { ok(()) }; - Box::new(ret) + async fn refund_settlement(&self, _account_id: Uuid, _settle_amount: u64) -> Result<(), ()> { + if self.should_fail { + Err(()) + } else { + Ok(()) + } } } +#[async_trait] impl IdempotentStore for TestStore { - fn load_idempotent_data( + async fn load_idempotent_data( &self, idempotency_key: String, - ) -> Box, Error = ()> + Send> { + ) -> Result, ()> { let cache = self.cache.read(); if let Some(data) = cache.get(&idempotency_key) { let mut guard = self.cache_hits.write(); *guard += 1; // used to test how many times this branch gets executed - Box::new(ok(Some(data.clone()))) + Ok(Some(data.clone())) } else { - Box::new(ok(None)) + Ok(None) } } - fn save_idempotent_data( + async fn save_idempotent_data( &self, idempotency_key: String, input_hash: [u8; 32], status_code: StatusCode, data: Bytes, - ) -> Box + Send> { + ) -> Result<(), ()> { let mut cache = self.cache.write(); cache.insert( idempotency_key, IdempotentData::new(status_code, data, input_hash), ); - Box::new(ok(())) + Ok(()) } } +#[async_trait] impl AccountStore for TestStore { type Account = TestAccount; - fn get_accounts( - &self, - account_ids: Vec, - ) -> Box, Error = ()> + Send> { + async fn get_accounts(&self, account_ids: Vec) -> Result, ()> { let accounts: Vec = self .accounts .read() @@ -166,75 +165,77 @@ impl AccountStore for TestStore { }) .collect(); if accounts.len() == account_ids.len() { - Box::new(ok(accounts)) + Ok(accounts) } else { - Box::new(err(())) + Err(()) } } // stub implementation (not used in these tests) - fn get_account_id_from_username( - &self, - _username: &Username, - ) -> Box + Send> { - Box::new(ok(Uuid::new_v4())) + async fn get_account_id_from_username(&self, _username: &Username) -> Result { + Ok(Uuid::new_v4()) } } +#[async_trait] impl LeftoversStore for TestStore { type AccountId = Uuid; type AssetType = BigUint; - fn save_uncredited_settlement_amount( + async fn save_uncredited_settlement_amount( &self, account_id: Uuid, uncredited_settlement_amount: (Self::AssetType, u8), - ) -> Box + Send> { + ) -> Result<(), ()> { let mut guard = self.uncredited_settlement_amount.write(); if let Some(leftovers) = (*guard).get_mut(&account_id) { - if leftovers.1 > uncredited_settlement_amount.1 { - // the current leftovers maintain the scale so we just need to - // upscale the provided leftovers to the existing leftovers' scale - let scaled = uncredited_settlement_amount - .0 - .normalize_scale(ConvertDetails { - from: uncredited_settlement_amount.1, - to: leftovers.1, - }) - .unwrap(); - *leftovers = (leftovers.0.clone() + scaled, leftovers.1); - } else if leftovers.1 == uncredited_settlement_amount.1 { - *leftovers = ( - leftovers.0.clone() + uncredited_settlement_amount.0, - leftovers.1, - ); - } else { - // if the scale of the provided leftovers is bigger than - // existing scale then we update the scale of the leftovers' - // scale - let scaled = leftovers - .0 - .normalize_scale(ConvertDetails { - from: leftovers.1, - to: uncredited_settlement_amount.1, - }) - .unwrap(); - *leftovers = ( - uncredited_settlement_amount.0 + scaled, - uncredited_settlement_amount.1, - ); + match leftovers.1.cmp(&uncredited_settlement_amount.1) { + Ordering::Greater => { + // the current leftovers maintain the scale so we just need to + // upscale the provided leftovers to the existing leftovers' scale + let scaled = uncredited_settlement_amount + .0 + .normalize_scale(ConvertDetails { + from: uncredited_settlement_amount.1, + to: leftovers.1, + }) + .unwrap(); + *leftovers = (leftovers.0.clone() + scaled, leftovers.1); + } + Ordering::Equal => { + *leftovers = ( + leftovers.0.clone() + uncredited_settlement_amount.0, + leftovers.1, + ); + } + _ => { + // if the scale of the provided leftovers is bigger than + // existing scale then we update the scale of the leftovers' + // scale + let scaled = leftovers + .0 + .normalize_scale(ConvertDetails { + from: leftovers.1, + to: uncredited_settlement_amount.1, + }) + .unwrap(); + *leftovers = ( + uncredited_settlement_amount.0 + scaled, + uncredited_settlement_amount.1, + ); + } } } else { (*guard).insert(account_id, uncredited_settlement_amount); } - Box::new(ok(())) + Ok(()) } - fn load_uncredited_settlement_amount( + async fn load_uncredited_settlement_amount( &self, account_id: Uuid, local_scale: u8, - ) -> Box + Send> { + ) -> Result { let mut guard = self.uncredited_settlement_amount.write(); if let Some(l) = guard.get_mut(&account_id) { let ret = l.clone(); @@ -242,28 +243,25 @@ impl LeftoversStore for TestStore { scale_with_precision_loss(ret.0, local_scale, ret.1); // save the new leftovers *l = (leftover_precision_loss, std::cmp::max(local_scale, ret.1)); - Box::new(ok(scaled_leftover_amount)) + Ok(scaled_leftover_amount) } else { - Box::new(ok(BigUint::from(0u32))) + Ok(BigUint::from(0u32)) } } - fn get_uncredited_settlement_amount( + async fn get_uncredited_settlement_amount( &self, account_id: Uuid, - ) -> Box + Send> { + ) -> Result<(Self::AssetType, u8), ()> { let leftovers = self.uncredited_settlement_amount.read(); - Box::new(ok(if let Some(a) = leftovers.get(&account_id) { + Ok(if let Some(a) = leftovers.get(&account_id) { a.clone() } else { (BigUint::from(0u32), 1) - })) + }) } - fn clear_uncredited_settlement_amount( - &self, - _account_id: Uuid, - ) -> Box + Send> { + async fn clear_uncredited_settlement_amount(&self, _account_id: Uuid) -> Result<(), ()> { unreachable!() } } @@ -321,29 +319,16 @@ pub fn mock_message(status_code: usize) -> mockito::Mock { .with_body(BODY) } -// Futures helper taken from the store_helpers in interledger-store-redis. -pub fn block_on(f: F) -> Result -where - F: Future + Send + 'static, - F::Item: Send, - F::Error: Send, -{ - // Only run one test at a time - let _ = env_logger::try_init(); - let mut runtime = Runtime::new().unwrap(); - runtime.block_on(f) -} - pub fn test_service( ) -> SettlementMessageService + Clone, TestAccount> { SettlementMessageService::new(incoming_service_fn(|_request| { - Box::new(err(RejectBuilder { + Err(RejectBuilder { code: ErrorCode::F02_UNREACHABLE, message: b"No other incoming handler!", data: &[], triggered_by: Some(&SERVICE_ADDRESS), } - .build())) + .build()) })) } @@ -359,21 +344,21 @@ pub fn test_api( should_fulfill: bool, ) -> warp::filters::BoxedFilter<(impl warp::Reply,)> { let outgoing = outgoing_service_fn(move |_| { - Box::new(if should_fulfill { - ok(FulfillBuilder { + if should_fulfill { + Ok(FulfillBuilder { fulfillment: &[0; 32], data: b"hello!", } .build()) } else { - err(RejectBuilder { + Err(RejectBuilder { code: ErrorCode::F02_UNREACHABLE, message: b"No other outgoing handler!", data: &[], triggered_by: Some(&SERVICE_ADDRESS), } .build()) - }) + } }); create_settlements_filter(test_store, outgoing) } diff --git a/crates/interledger-settlement/src/core/backends_common/redis/mod.rs b/crates/interledger-settlement/src/core/backends_common/redis/mod.rs index 172afc96e..9df6cac45 100644 --- a/crates/interledger-settlement/src/core/backends_common/redis/mod.rs +++ b/crates/interledger-settlement/src/core/backends_common/redis/mod.rs @@ -4,45 +4,59 @@ use crate::core::{ types::{Convert, ConvertDetails, LeftoversStore}, }; use bytes::Bytes; -use futures::{future::result, Future}; +use futures::TryFutureExt; use http::StatusCode; use num_bigint::BigUint; use redis_crate::{ - self, aio::SharedConnection, cmd, Client, ConnectionInfo, ErrorKind, FromRedisValue, - PipelineCommands, RedisError, RedisWrite, ToRedisArgs, Value, + self, aio::MultiplexedConnection, AsyncCommands, Client, ConnectionInfo, ErrorKind, + FromRedisValue, RedisError, RedisWrite, ToRedisArgs, Value, }; -use std::collections::HashMap as SlowHashMap; +use std::collections::HashMap; use std::str::FromStr; use log::{debug, error, trace}; +use async_trait::async_trait; + #[cfg(test)] mod test_helpers; +/// Domain separator for leftover amounts static UNCREDITED_AMOUNT_KEY: &str = "uncredited_engine_settlement_amount"; + +/// Helper function to get a redis key fn uncredited_amount_key(account_id: &str) -> String { format!("{}:{}", UNCREDITED_AMOUNT_KEY, account_id) } +/// Builder object to create a Redis connection for the engine pub struct EngineRedisStoreBuilder { redis_url: ConnectionInfo, } impl EngineRedisStoreBuilder { + /// Simple constructor pub fn new(redis_url: ConnectionInfo) -> Self { EngineRedisStoreBuilder { redis_url } } - pub fn connect(&self) -> impl Future { - result(Client::open(self.redis_url.clone())) - .map_err(|err| error!("Error creating Redis client: {:?}", err)) - .and_then(|client| { - debug!("Connected to redis: {:?}", client); - client - .get_shared_async_connection() - .map_err(|err| error!("Error connecting to Redis: {:?}", err)) - }) - .and_then(move |connection| Ok(EngineRedisStore { connection })) + /// Connects to the provided redis_url and returns a Redis connection for the Settlement Engine + pub async fn connect(&self) -> Result { + let client = match Client::open(self.redis_url.clone()) { + Ok(c) => c, + Err(err) => { + error!("Error creating Redis client: {:?}", err); + return Err(()); + } + }; + + let connection = client + .get_multiplexed_tokio_connection() + .map_err(|err| error!("Error connecting to Redis: {:?}", err)) + .await?; + debug!("Connected to redis: {:?}", client); + + Ok(EngineRedisStore { connection }) } } @@ -52,56 +66,54 @@ impl EngineRedisStoreBuilder { /// composed in the stores of other Settlement Engines. #[derive(Clone)] pub struct EngineRedisStore { - pub connection: SharedConnection, + pub connection: MultiplexedConnection, } +#[async_trait] impl IdempotentStore for EngineRedisStore { - fn load_idempotent_data( + async fn load_idempotent_data( &self, idempotency_key: String, - ) -> Box, Error = ()> + Send> { + ) -> Result, ()> { let idempotency_key_clone = idempotency_key.clone(); - Box::new( - cmd("HGETALL") - .arg(idempotency_key.clone()) - .query_async(self.connection.clone()) - .map_err(move |err| { - error!( - "Error loading idempotency key {}: {:?}", - idempotency_key_clone, err - ) - }) - .and_then( - move |(_connection, ret): (_, SlowHashMap)| { - if let (Some(status_code), Some(data), Some(input_hash_slice)) = ( - ret.get("status_code"), - ret.get("data"), - ret.get("input_hash"), - ) { - trace!("Loaded idempotency key {:?} - {:?}", idempotency_key, ret); - let mut input_hash: [u8; 32] = Default::default(); - input_hash.copy_from_slice(input_hash_slice.as_ref()); - Ok(Some(IdempotentData::new( - StatusCode::from_str(status_code).unwrap(), - Bytes::from(data.clone()), - input_hash, - ))) - } else { - Ok(None) - } - }, - ), - ) + let mut connection = self.connection.clone(); + let ret: HashMap = connection + .hgetall(idempotency_key.clone()) + .map_err(move |err| { + error!( + "Error loading idempotency key {}: {:?}", + idempotency_key_clone, err + ) + }) + .await?; + + if let (Some(status_code), Some(data), Some(input_hash_slice)) = ( + ret.get("status_code"), + ret.get("data"), + ret.get("input_hash"), + ) { + trace!("Loaded idempotency key {:?} - {:?}", idempotency_key, ret); + let mut input_hash: [u8; 32] = Default::default(); + input_hash.copy_from_slice(input_hash_slice.as_ref()); + Ok(Some(IdempotentData::new( + StatusCode::from_str(status_code).unwrap(), + Bytes::from(data.clone()), + input_hash, + ))) + } else { + Ok(None) + } } - fn save_idempotent_data( + async fn save_idempotent_data( &self, idempotency_key: String, input_hash: [u8; 32], status_code: StatusCode, data: Bytes, - ) -> Box + Send> { + ) -> Result<(), ()> { let mut pipe = redis_crate::pipe(); + let mut connection = self.connection.clone(); pipe.atomic() .cmd("HMSET") // cannot use hset_multiple since data and status_code have different types .arg(&idempotency_key) @@ -114,22 +126,20 @@ impl IdempotentStore for EngineRedisStore { .ignore() .expire(&idempotency_key, 86400) .ignore(); - Box::new( - pipe.query_async(self.connection.clone()) - .map_err(|err| error!("Error caching: {:?}", err)) - .and_then(move |(_connection, _): (_, Vec)| { - trace!( - "Cached {:?}: {:?}, {:?}", - idempotency_key, - status_code, - data, - ); - Ok(()) - }), - ) + pipe.query_async(&mut connection) + .map_err(|err| error!("Error caching: {:?}", err)) + .await?; + trace!( + "Cached {:?}: {:?}, {:?}", + idempotency_key, + status_code, + data, + ); + Ok(()) } } +/// Helper datatype for storing and loading quantities of a number with different scales #[derive(Debug, Clone)] struct AmountWithScale { num: BigUint, @@ -149,11 +159,11 @@ impl ToRedisArgs for AmountWithScale { } impl AmountWithScale { + /// Iterates over all values because in this case it's making + /// an lrange call. This returns all the tuple elements in 1 array, and + /// it cannot differentiate between 1 AmountWithScale value or multiple + /// ones. This looks like a limitation of redis.rs fn parse_multi_values(items: &[Value]) -> Option { - // We have to iterate over all values because in this case we're making - // an lrange call. This returns all the tuple elements in 1 array, and - // it cannot differentiate between 1 AmountWithScale value or multiple - // ones. This looks like a limitation of redis.rs let len = items.len(); let mut iter = items.iter(); @@ -216,151 +226,138 @@ impl FromRedisValue for AmountWithScale { } } +#[async_trait] impl LeftoversStore for EngineRedisStore { type AccountId = String; type AssetType = BigUint; - fn get_uncredited_settlement_amount( + async fn get_uncredited_settlement_amount( &self, account_id: Self::AccountId, - ) -> Box + Send> { - Box::new( - cmd("LRANGE") - .arg(uncredited_amount_key(&account_id)) - .arg(0) - .arg(-1) - .query_async(self.connection.clone()) - .map_err(move |err| error!("Error getting uncredited_settlement_amount {:?}", err)) - .and_then(move |(_, amount): (_, AmountWithScale)| Ok((amount.num, amount.scale))), - ) + ) -> Result<(Self::AssetType, u8), ()> { + let mut connection = self.connection.clone(); + let amount: AmountWithScale = connection + .lrange(uncredited_amount_key(&account_id), 0, -1) + .map_err(move |err| error!("Error getting uncredited_settlement_amount {:?}", err)) + .await?; + Ok((amount.num, amount.scale)) } - fn save_uncredited_settlement_amount( + async fn save_uncredited_settlement_amount( &self, account_id: Self::AccountId, uncredited_settlement_amount: (Self::AssetType, u8), - ) -> Box + Send> { + ) -> Result<(), ()> { trace!( "Saving uncredited_settlement_amount {:?} {:?}", account_id, uncredited_settlement_amount ); - Box::new( - // We store these amounts as lists of strings - // because we cannot do BigNumber arithmetic in the store - // When loading the amounts, we convert them to the appropriate data - // type and sum them up. - cmd("RPUSH") - .arg(uncredited_amount_key(&account_id)) - .arg(AmountWithScale { + // We store these amounts as lists of strings + // because we cannot do BigNumber arithmetic in the store + // When loading the amounts, we convert them to the appropriate data + // type and sum them up. + let mut connection = self.connection.clone(); + connection + .rpush( + uncredited_amount_key(&account_id), + AmountWithScale { num: uncredited_settlement_amount.0, scale: uncredited_settlement_amount.1, - }) - .query_async(self.connection.clone()) - .map_err(move |err| error!("Error saving uncredited_settlement_amount: {:?}", err)) - .and_then(move |(_conn, _ret): (_, Value)| Ok(())), - ) + }, + ) + .map_err(move |err| error!("Error saving uncredited_settlement_amount: {:?}", err)) + .await?; + + Ok(()) } - fn load_uncredited_settlement_amount( + async fn load_uncredited_settlement_amount( &self, account_id: Self::AccountId, local_scale: u8, - ) -> Box + Send> { + ) -> Result { let connection = self.connection.clone(); trace!("Loading uncredited_settlement_amount {:?}", account_id); - Box::new( - self.get_uncredited_settlement_amount(account_id.clone()) - .and_then(move |amount| { - // scale the amount from the max scale to the local scale, and then - // save any potential leftovers to the store - let (scaled_amount, precision_loss) = - scale_with_precision_loss(amount.0, local_scale, amount.1); - let mut pipe = redis_crate::pipe(); - pipe.atomic(); - pipe.del(uncredited_amount_key(&account_id)).ignore(); - pipe.rpush( - uncredited_amount_key(&account_id), - AmountWithScale { - num: precision_loss, - scale: std::cmp::max(local_scale, amount.1), - }, - ) - .ignore(); - - pipe.query_async(connection.clone()) - .map_err(move |err| { - error!("Error saving uncredited_settlement_amount: {:?}", err) - }) - .and_then(move |(_conn, _ret): (_, Value)| Ok(scaled_amount)) - }), + let amount = self + .get_uncredited_settlement_amount(account_id.clone()) + .await?; + // scale the amount from the max scale to the local scale, and then + // save any potential leftovers to the store + let (scaled_amount, precision_loss) = + scale_with_precision_loss(amount.0, local_scale, amount.1); + + let mut pipe = redis_crate::pipe(); + pipe.atomic(); + pipe.del(uncredited_amount_key(&account_id)).ignore(); + pipe.rpush( + uncredited_amount_key(&account_id), + AmountWithScale { + num: precision_loss, + scale: std::cmp::max(local_scale, amount.1), + }, ) + .ignore(); + + pipe.query_async(&mut connection.clone()) + .map_err(move |err| error!("Error saving uncredited_settlement_amount: {:?}", err)) + .await?; + + Ok(scaled_amount) } - fn clear_uncredited_settlement_amount( + async fn clear_uncredited_settlement_amount( &self, account_id: Self::AccountId, - ) -> Box + Send> { + ) -> Result<(), ()> { trace!("Clearing uncredited_settlement_amount {:?}", account_id,); - Box::new( - cmd("DEL") - .arg(uncredited_amount_key(&account_id)) - .query_async(self.connection.clone()) - .map_err(move |err| { - error!("Error clearing uncredited_settlement_amount: {:?}", err) - }) - .and_then(move |(_conn, _ret): (_, Value)| Ok(())), - ) + let mut connection = self.connection.clone(); + connection + .del(uncredited_amount_key(&account_id)) + .map_err(move |err| error!("Error clearing uncredited_settlement_amount: {:?}", err)) + .await?; + + Ok(()) } } #[cfg(test)] mod tests { use super::*; - use test_helpers::{block_on, test_store, IDEMPOTENCY_KEY}; + use test_helpers::{test_store, IDEMPOTENCY_KEY}; mod idempotency { use super::*; - #[test] - fn saves_and_loads_idempotency_key_data_properly() { - block_on(test_store().and_then(|(store, context)| { - let input_hash: [u8; 32] = Default::default(); - store - .save_idempotent_data( - IDEMPOTENCY_KEY.clone(), - input_hash, - StatusCode::OK, - Bytes::from("TEST"), - ) - .map_err(|err| eprintln!("Redis error: {:?}", err)) - .and_then(move |_| { - store - .load_idempotent_data(IDEMPOTENCY_KEY.clone()) - .map_err(|err| eprintln!("Redis error: {:?}", err)) - .and_then(move |data1| { - assert_eq!( - data1.unwrap(), - IdempotentData::new( - StatusCode::OK, - Bytes::from("TEST"), - input_hash - ) - ); - let _ = context; - - store - .load_idempotent_data("asdf".to_string()) - .map_err(|err| eprintln!("Redis error: {:?}", err)) - .and_then(move |data2| { - assert!(data2.is_none()); - let _ = context; - Ok(()) - }) - }) - }) - })) - .unwrap() + #[tokio::test] + async fn saves_and_loads_idempotency_key_data_properly() { + // The context must be loaded into scope + let (store, _context) = test_store().await.unwrap(); + let input_hash: [u8; 32] = Default::default(); + store + .save_idempotent_data( + IDEMPOTENCY_KEY.clone(), + input_hash, + StatusCode::OK, + Bytes::from("TEST"), + ) + .await + .unwrap(); + + let data1 = store + .load_idempotent_data(IDEMPOTENCY_KEY.clone()) + .await + .unwrap(); + assert_eq!( + data1.unwrap(), + IdempotentData::new(StatusCode::OK, Bytes::from("TEST"), input_hash) + ); + + let data2 = store + .load_idempotent_data("asdf".to_string()) + .await + .unwrap(); + assert!(data2.is_none()); } } } diff --git a/crates/interledger-settlement/src/core/backends_common/redis/test_helpers/mod.rs b/crates/interledger-settlement/src/core/backends_common/redis/test_helpers/mod.rs index df4750134..473ea26d8 100644 --- a/crates/interledger-settlement/src/core/backends_common/redis/test_helpers/mod.rs +++ b/crates/interledger-settlement/src/core/backends_common/redis/test_helpers/mod.rs @@ -3,4 +3,4 @@ mod redis_helpers; #[cfg(test)] mod store_helpers; #[cfg(test)] -pub use store_helpers::{block_on, test_store, IDEMPOTENCY_KEY}; +pub use store_helpers::{test_store, IDEMPOTENCY_KEY}; diff --git a/crates/interledger-settlement/src/core/backends_common/redis/test_helpers/redis_helpers.rs b/crates/interledger-settlement/src/core/backends_common/redis/test_helpers/redis_helpers.rs index 44ae632ef..4bdb63bba 100644 --- a/crates/interledger-settlement/src/core/backends_common/redis/test_helpers/redis_helpers.rs +++ b/crates/interledger-settlement/src/core/backends_common/redis/test_helpers/redis_helpers.rs @@ -1,7 +1,7 @@ // Copied from https://github.com/mitsuhiko/redis-rs/blob/9a1777e8a90c82c315a481cdf66beb7d69e681a2/tests/support/mod.rs #![allow(dead_code)] -use futures::Future; +use futures::TryFutureExt; use redis_crate::{self as redis, RedisError}; use std::{env, fs, path::PathBuf, process, thread::sleep, time::Duration}; @@ -155,19 +155,20 @@ impl TestContext { self.client.get_connection().unwrap() } - pub fn async_connection(&self) -> impl Future { + pub async fn async_connection(&self) -> Result { self.client .get_async_connection() .map_err(|err| panic!(err)) + .await } pub fn stop_server(&mut self) { self.server.stop(); } - pub fn shared_async_connection( + pub async fn shared_async_connection( &self, - ) -> impl Future { - self.client.get_shared_async_connection() + ) -> Result { + self.client.get_multiplexed_tokio_connection().await } } diff --git a/crates/interledger-settlement/src/core/backends_common/redis/test_helpers/store_helpers.rs b/crates/interledger-settlement/src/core/backends_common/redis/test_helpers/store_helpers.rs index f4305d907..ba3d96ebe 100644 --- a/crates/interledger-settlement/src/core/backends_common/redis/test_helpers/store_helpers.rs +++ b/crates/interledger-settlement/src/core/backends_common/redis/test_helpers/store_helpers.rs @@ -1,31 +1,16 @@ use super::redis_helpers::TestContext; use crate::core::backends_common::redis::{EngineRedisStore, EngineRedisStoreBuilder}; -use env_logger; -use futures::Future; -use tokio::runtime::Runtime; - use lazy_static::lazy_static; lazy_static! { pub static ref IDEMPOTENCY_KEY: String = String::from("abcd"); } -pub fn test_store() -> impl Future { +pub async fn test_store() -> Result<(EngineRedisStore, TestContext), ()> { let context = TestContext::new(); - EngineRedisStoreBuilder::new(context.get_client_connection_info()) + let store = EngineRedisStoreBuilder::new(context.get_client_connection_info()) .connect() - .and_then(|store| Ok((store, context))) -} - -pub fn block_on(f: F) -> Result -where - F: Future + Send + 'static, - F::Item: Send, - F::Error: Send, -{ - // Only run one test at a time - let _ = env_logger::try_init(); - let mut runtime = Runtime::new().unwrap(); - runtime.block_on(f) + .await?; + Ok((store, context)) } diff --git a/crates/interledger-settlement/src/core/engines_api.rs b/crates/interledger-settlement/src/core/engines_api.rs index f6e57db73..8d7143ce8 100644 --- a/crates/interledger-settlement/src/core/engines_api.rs +++ b/crates/interledger-settlement/src/core/engines_api.rs @@ -7,22 +7,142 @@ use super::{ idempotency::{make_idempotent_call, IdempotentStore}, types::{Quantity, SettlementEngine}, }; -use bytes::buf::FromBuf; use bytes::Bytes; -use futures::Future; use http::StatusCode; use hyper::Response; use interledger_http::error::default_rejection_handler; - use serde::{Deserialize, Serialize}; - -use warp::{self, reject::Rejection, Filter}; +use warp::Filter; #[derive(Serialize, Deserialize, Debug, Clone, Hash)] pub struct CreateAccount { id: String, } +/// Makes an idempotent call to [`engine.create_account`](../types/trait.SettlementEngine.html#tymethod.create_account) +/// Returns `Status Code 201` +async fn create_engine_account( + idempotency_key: Option, + account_id: CreateAccount, + engine: E, + store: S, +) -> Result +where + E: SettlementEngine + Clone + Send + Sync + 'static, + S: IdempotentStore + Clone + Send + Sync + 'static, +{ + let input_hash = get_hash_of(account_id.id.as_ref()); + let (status_code, message) = make_idempotent_call( + store, + engine.create_account(account_id.id), + input_hash, + idempotency_key, + StatusCode::CREATED, + Bytes::from("CREATED"), + ) + .await?; + + Ok(Response::builder() + .header("Content-Type", "application/json") + .status(status_code) + .body(message) + .unwrap()) +} + +/// Makes an idempotent call to [`engine.delete_account`](../types/trait.SettlementEngine.html#tymethod.delete_account) +/// Returns Status Code `204`. +async fn delete_engine_account( + account_id: String, + idempotency_key: Option, + engine: E, + store: S, +) -> Result +where + E: SettlementEngine + Clone + Send + Sync + 'static, + S: IdempotentStore + Clone + Send + Sync + 'static, +{ + let input_hash = get_hash_of(account_id.as_ref()); + let (status_code, message) = make_idempotent_call( + store, + engine.delete_account(account_id), + input_hash, + idempotency_key, + StatusCode::NO_CONTENT, + Bytes::from("DELETED"), + ) + .await?; + + Ok(Response::builder() + .header("Content-Type", "application/json") + .status(status_code) + .body(message) + .unwrap()) +} + +/// Makes an idempotent call to [`engine.send_money`](../types/trait.SettlementEngine.html#tymethod.send_money) +/// Returns Status Code `201` +async fn engine_send_money( + id: String, + idempotency_key: Option, + quantity: Quantity, + engine: E, + store: S, +) -> Result +where + E: SettlementEngine + Clone + Send + Sync + 'static, + S: IdempotentStore + Clone + Send + Sync + 'static, +{ + let input = format!("{}{:?}", id, quantity); + let input_hash = get_hash_of(input.as_ref()); + let (status_code, message) = make_idempotent_call( + store, + engine.send_money(id, quantity), + input_hash, + idempotency_key, + StatusCode::CREATED, + Bytes::from("EXECUTED"), + ) + .await?; + + Ok(Response::builder() + .header("Content-Type", "application/json") + .status(status_code) + .body(message) + .unwrap()) +} + +/// Makes an idempotent call to [`engine.receive_message`](../types/trait.SettlementEngine.html#tymethod.receive_message) +/// Returns Status Code `201` +async fn engine_receive_message( + id: String, + idempotency_key: Option, + message: Bytes, + engine: E, + store: S, +) -> Result +where + E: SettlementEngine + Clone + Send + Sync + 'static, + S: IdempotentStore + Clone + Send + Sync + 'static, +{ + let input = format!("{}{:?}", id, message); + let input_hash = get_hash_of(input.as_ref()); + let (status_code, message) = make_idempotent_call( + store, + engine.receive_message(id, message.to_vec()), + input_hash, + idempotency_key, + StatusCode::CREATED, + Bytes::from("RECEIVED"), + ) + .await?; + + Ok(Response::builder() + .header("Content-Type", "application/json") + .status(status_code) + .body(message) + .unwrap()) +} + /// Returns a Settlement Engine filter which exposes a Warp-compatible /// idempotent API which forwards calls to the provided settlement engine which /// uses the underlying store for idempotency. @@ -37,161 +157,51 @@ where let with_store = warp::any().map(move || store.clone()).boxed(); let with_engine = warp::any().map(move || engine.clone()).boxed(); let idempotency = warp::header::optional::("idempotency-key"); - let account_id = warp::path("accounts").and(warp::path::param2::()); // account_id + let account_id = warp::path("accounts").and(warp::path::param::()); // account_id // POST /accounts/ (optional idempotency-key header) // Body is a Vec object - let accounts = warp::post2() + let accounts = warp::post() .and(warp::path("accounts")) .and(warp::path::end()) .and(idempotency) .and(warp::body::json()) .and(with_engine.clone()) .and(with_store.clone()) - .and_then( - move |idempotency_key: Option, - account_id: CreateAccount, - engine: E, - store: S| { - let account_id = account_id.id; - let input_hash = get_hash_of(account_id.as_ref()); - - // Wrap do_send_outgoing_message in a closure to be invoked by - // the idempotency wrapper - let create_account_fn = move || engine.create_account(account_id); - make_idempotent_call( - store, - create_account_fn, - input_hash, - idempotency_key, - StatusCode::CREATED, - Bytes::from("CREATED"), - ) - .map_err::<_, Rejection>(move |err| err.into()) - .and_then(move |(status_code, message)| { - Ok(Response::builder() - .header("Content-Type", "application/json") - .status(status_code) - .body(message) - .unwrap()) - }) - }, - ); + .and_then(create_engine_account); // DELETE /accounts/:id (optional idempotency-key header) - let del_account = warp::delete2() + let del_account = warp::delete() .and(account_id) .and(warp::path::end()) .and(idempotency) .and(with_engine.clone()) .and(with_store.clone()) - .and_then( - move |id: String, idempotency_key: Option, engine: E, store: S| { - let input_hash = get_hash_of(id.as_ref()); - - // Wrap do_send_outgoing_message in a closure to be invoked by - // the idempotency wrapper - let delete_account_fn = move || engine.delete_account(id); - make_idempotent_call( - store, - delete_account_fn, - input_hash, - idempotency_key, - StatusCode::NO_CONTENT, - Bytes::from("DELETED"), - ) - .map_err::<_, Rejection>(move |err| err.into()) - .and_then(move |(status_code, message)| { - Ok(Response::builder() - .header("Content-Type", "application/json") - .status(status_code) - .body(message) - .unwrap()) - }) - }, - ); - - // POST /accounts/:account_id/settlements (optional idempotency-key header) + .and_then(delete_engine_account); + + // POST /accounts/:aVcount_id/settlements (optional idempotency-key header) // Body is a Quantity object let settlement_endpoint = account_id.and(warp::path("settlements")); - let settlements = warp::post2() + let settlements = warp::post() .and(settlement_endpoint) .and(warp::path::end()) .and(idempotency) .and(warp::body::json()) .and(with_engine.clone()) .and(with_store.clone()) - .and_then( - move |id: String, - idempotency_key: Option, - quantity: Quantity, - engine: E, - store: S| { - let input = format!("{}{:?}", id, quantity); - let input_hash = get_hash_of(input.as_ref()); - let send_money_fn = move || engine.send_money(id, quantity); - make_idempotent_call( - store, - send_money_fn, - input_hash, - idempotency_key, - StatusCode::CREATED, - Bytes::from("EXECUTED"), - ) - .map_err::<_, Rejection>(move |err| err.into()) - .and_then(move |(status_code, message)| { - Ok(Response::builder() - .header("Content-Type", "application/json") - .status(status_code) - .body(message) - .unwrap()) - }) - }, - ); + .and_then(engine_send_money); // POST /accounts/:account_id/messages (optional idempotency-key header) // Body is a Vec object let messages_endpoint = account_id.and(warp::path("messages")); - let messages = warp::post2() + let messages = warp::post() .and(messages_endpoint) .and(warp::path::end()) .and(idempotency) - .and(warp::body::concat()) - .and(with_engine.clone()) - .and(with_store.clone()) - .and_then( - move |id: String, - idempotency_key: Option, - body: warp::body::FullBody, - engine: E, - store: S| { - // Gets called by our settlement engine, forwards the request outwards - // until it reaches the peer's settlement engine. - let message = Vec::from_buf(body); - let input = format!("{}{:?}", id, message); - let input_hash = get_hash_of(input.as_ref()); - - // Wrap do_send_outgoing_message in a closure to be invoked by - // the idempotency wrapper - let receive_message_fn = move || engine.receive_message(id, message); - make_idempotent_call( - store, - receive_message_fn, - input_hash, - idempotency_key, - StatusCode::CREATED, - Bytes::from("RECEIVED"), - ) - .map_err::<_, Rejection>(move |err| err.into()) - .and_then(move |(status_code, message)| { - Ok(Response::builder() - .header("Content-Type", "application/json") - .status(status_code) - .body(message) - .unwrap()) - }) - }, - ); + .and(warp::body::bytes()) + .and(with_engine) + .and(with_store) + .and_then(engine_receive_message); accounts .or(del_account) @@ -205,11 +215,10 @@ where mod tests { use super::*; use crate::core::idempotency::IdempotentData; - use crate::core::types::ApiResponse; + use crate::core::types::{ApiResponse, ApiResult}; + use async_trait::async_trait; use bytes::Bytes; - use futures::future::ok; use http::StatusCode; - use interledger_http::error::ApiError; use parking_lot::RwLock; use serde_json::{json, Value}; use std::collections::HashMap; @@ -242,78 +251,66 @@ mod tests { } } + #[async_trait] impl IdempotentStore for TestStore { - fn load_idempotent_data( + async fn load_idempotent_data( &self, idempotency_key: String, - ) -> Box, Error = ()> + Send> { + ) -> Result, ()> { let cache = self.cache.read(); if let Some(data) = cache.get(&idempotency_key) { let mut guard = self.cache_hits.write(); *guard += 1; // used to test how many times this branch gets executed - Box::new(ok(Some(data.clone()))) + Ok(Some(data.clone())) } else { - Box::new(ok(None)) + Ok(None) } } - fn save_idempotent_data( + async fn save_idempotent_data( &self, idempotency_key: String, input_hash: [u8; 32], status_code: StatusCode, data: Bytes, - ) -> Box + Send> { + ) -> Result<(), ()> { let mut cache = self.cache.write(); cache.insert( idempotency_key, IdempotentData::new(status_code, data, input_hash), ); - Box::new(ok(())) + Ok(()) } } pub static IDEMPOTENCY: &str = "abcd01234"; + #[async_trait] impl SettlementEngine for TestEngine { - fn send_money( - &self, - _account_id: String, - _money: Quantity, - ) -> Box + Send> { - Box::new(ok(ApiResponse::Default)) + async fn send_money(&self, _account_id: String, _money: Quantity) -> ApiResult { + Ok(ApiResponse::Default) } - fn receive_message( - &self, - _account_id: String, - _message: Vec, - ) -> Box + Send> { - Box::new(ok(ApiResponse::Default)) + async fn receive_message(&self, _account_id: String, _message: Vec) -> ApiResult { + Ok(ApiResponse::Default) } - fn create_account( - &self, - _account_id: String, - ) -> Box + Send> { - Box::new(ok(ApiResponse::Default)) + async fn create_account(&self, _account_id: String) -> ApiResult { + Ok(ApiResponse::Default) } - fn delete_account( - &self, - _account_id: String, - ) -> Box + Send> { - Box::new(ok(ApiResponse::Default)) + async fn delete_account(&self, _account_id: String) -> ApiResult { + Ok(ApiResponse::Default) } } - #[test] - fn idempotent_execute_settlement() { + #[tokio::test] + async fn idempotent_execute_settlement() { let store = test_store(); let engine = TestEngine; let api = create_settlement_engine_filter(engine, store.clone()); - let settlement_call = move |id, amount, scale| { + let settlement_call = |id, amount, scale| { warp::test::request() .method("POST") .path(&format!("/accounts/{}/settlements", id)) @@ -322,25 +319,25 @@ mod tests { .reply(&api) }; - let ret = settlement_call("1".to_owned(), 100, 6); + let ret = settlement_call("1".to_owned(), 100, 6).await; assert_eq!(ret.status(), StatusCode::CREATED); assert_eq!(ret.body(), "EXECUTED"); // is idempotent - let ret = settlement_call("1".to_owned(), 100, 6); + let ret = settlement_call("1".to_owned(), 100, 6).await; assert_eq!(ret.status(), StatusCode::CREATED); assert_eq!(ret.body(), "EXECUTED"); - // // fails with different id and same data - let ret = settlement_call("42".to_owned(), 100, 6); + // fails with different id and same data + let ret = settlement_call("42".to_owned(), 100, 6).await; check_error_status_and_message(ret, 409, "Provided idempotency key is tied to other input"); // fails with same id and different data - let ret = settlement_call("1".to_owned(), 42, 6); + let ret = settlement_call("1".to_owned(), 42, 6).await; check_error_status_and_message(ret, 409, "Provided idempotency key is tied to other input"); // fails with different id and different data - let ret = settlement_call("42".to_owned(), 42, 6); + let ret = settlement_call("42".to_owned(), 42, 6).await; check_error_status_and_message(ret, 409, "Provided idempotency key is tied to other input"); let cache = store.cache.read(); @@ -352,13 +349,13 @@ mod tests { assert_eq!(cached_data.body, "EXECUTED".to_string()); } - #[test] - fn idempotent_receive_message() { + #[tokio::test] + async fn idempotent_receive_message() { let store = test_store(); let engine = TestEngine; let api = create_settlement_engine_filter(engine, store.clone()); - let messages_call = move |id, msg| { + let messages_call = |id, msg| { warp::test::request() .method("POST") .path(&format!("/accounts/{}/messages", id)) @@ -367,25 +364,25 @@ mod tests { .reply(&api) }; - let ret = messages_call("1", vec![0]); + let ret = messages_call("1", vec![0]).await; assert_eq!(ret.status().as_u16(), StatusCode::CREATED); assert_eq!(ret.body(), "RECEIVED"); // is idempotent - let ret = messages_call("1", vec![0]); + let ret = messages_call("1", vec![0]).await; assert_eq!(ret.status().as_u16(), StatusCode::CREATED); assert_eq!(ret.body(), "RECEIVED"); - // // fails with different id and same data - let ret = messages_call("42", vec![0]); + // fails with different id and same data + let ret = messages_call("42", vec![0]).await; check_error_status_and_message(ret, 409, "Provided idempotency key is tied to other input"); // fails with same id and different data - let ret = messages_call("1", vec![42]); + let ret = messages_call("1", vec![42]).await; check_error_status_and_message(ret, 409, "Provided idempotency key is tied to other input"); // fails with different id and different data - let ret = messages_call("42", vec![42]); + let ret = messages_call("42", vec![42]).await; check_error_status_and_message(ret, 409, "Provided idempotency key is tied to other input"); let cache = store.cache.read(); @@ -397,13 +394,13 @@ mod tests { assert_eq!(cached_data.body, "RECEIVED".to_string()); } - #[test] - fn idempotent_create_account() { + #[tokio::test] + async fn idempotent_create_account() { let store = test_store(); let engine = TestEngine; let api = create_settlement_engine_filter(engine, store.clone()); - let create_account_call = move |id: &str| { + let create_account_call = |id: &str| { warp::test::request() .method("POST") .path("/accounts") @@ -412,17 +409,17 @@ mod tests { .reply(&api) }; - let ret = create_account_call("1"); + let ret = create_account_call("1").await; assert_eq!(ret.status().as_u16(), StatusCode::CREATED); assert_eq!(ret.body(), "CREATED"); // is idempotent - let ret = create_account_call("1"); + let ret = create_account_call("1").await; assert_eq!(ret.status().as_u16(), StatusCode::CREATED); assert_eq!(ret.body(), "CREATED"); // fails with different id - let ret = create_account_call("42"); + let ret = create_account_call("42").await; check_error_status_and_message(ret, 409, "Provided idempotency key is tied to other input"); let cache = store.cache.read(); @@ -434,13 +431,13 @@ mod tests { assert_eq!(cached_data.body, "CREATED".to_string()); } - #[test] - fn idempotent_delete_account() { + #[tokio::test] + async fn idempotent_delete_account() { let store = test_store(); let engine = TestEngine; let api = create_settlement_engine_filter(engine, store.clone()); - let delete_account_call = move |id: &str| { + let delete_account_call = |id: &str| { warp::test::request() .method("DELETE") .path(&format!("/accounts/{}", id)) @@ -448,17 +445,17 @@ mod tests { .reply(&api) }; - let ret = delete_account_call("1"); + let ret = delete_account_call("1").await; assert_eq!(ret.status(), StatusCode::NO_CONTENT); assert_eq!(ret.body(), "DELETED"); // is idempotent - let ret = delete_account_call("1"); + let ret = delete_account_call("1").await; assert_eq!(ret.status(), StatusCode::NO_CONTENT); assert_eq!(ret.body(), "DELETED"); // fails with different id - let ret = delete_account_call("42"); + let ret = delete_account_call("42").await; check_error_status_and_message(ret, 409, "Provided idempotency key is tied to other input"); let cache = store.cache.read(); diff --git a/crates/interledger-settlement/src/core/idempotency.rs b/crates/interledger-settlement/src/core/idempotency.rs index f79972577..7f8ebea4c 100644 --- a/crates/interledger-settlement/src/core/idempotency.rs +++ b/crates/interledger-settlement/src/core/idempotency.rs @@ -1,22 +1,25 @@ -use super::types::ApiResponse; +use super::types::{ApiResponse, ApiResult}; +use async_trait::async_trait; use bytes::Bytes; -use futures::executor::spawn; -use futures::{ - future::{err, ok, Either}, - Future, -}; +use futures::Future; +use futures::TryFutureExt; use http::StatusCode; use interledger_http::error::*; use log::error; +/// Data stored for the idempotency features #[derive(Debug, Clone, PartialEq)] pub struct IdempotentData { + /// The HTTP Status Code of the API's response pub status: StatusCode, + /// The body of the API's response pub body: Bytes, + /// The hash of the serialized input parameters which generated the API's response pub input_hash: [u8; 32], } impl IdempotentData { + /// Simple constructor pub fn new(status: StatusCode, body: Bytes, input_hash: [u8; 32]) -> Self { Self { status, @@ -26,64 +29,67 @@ impl IdempotentData { } } +/// Store trait which should be implemented for idempotency related features +#[async_trait] pub trait IdempotentStore { /// Returns the API response that was saved when the idempotency key was used /// Also returns a hash of the input data which resulted in the response - fn load_idempotent_data( + async fn load_idempotent_data( &self, idempotency_key: String, - ) -> Box, Error = ()> + Send>; + ) -> Result, ()>; /// Saves the data that was passed along with the api request for later - /// The store MUST also save a hash of the input, so that it errors out on requests - fn save_idempotent_data( + /// The store also saves the hash of the input, so that it errors out on requests + /// with conflicting input hashes for the same idempotency key + async fn save_idempotent_data( &self, idempotency_key: String, input_hash: [u8; 32], status_code: StatusCode, data: Bytes, - ) -> Box + Send>; + ) -> Result<(), ()>; } -// Helper function that returns any idempotent data that corresponds to a -// provided idempotency key. It fails if the hash of the input that -// generated the idempotent data does not match the hash of the provided input. -fn check_idempotency( +/// Helper function that returns any idempotent data that corresponds to a +/// provided idempotency key. It fails if the hash of the input that +/// generated the idempotent data does not match the hash of the provided input. +async fn check_idempotency( store: S, idempotency_key: String, input_hash: [u8; 32], -) -> impl Future, Error = ApiError> +) -> Result, ApiError> where S: IdempotentStore + Clone + Send + Sync + 'static, { - store + let ret: Option = store .load_idempotent_data(idempotency_key.clone()) .map_err(move |_| IDEMPOTENT_STORE_CALL_ERROR.clone()) - .and_then(move |ret: Option| { - if let Some(ret) = ret { - // Check if the hash (ret.2) of the loaded idempotent data matches the hash - // of the provided input data. If not, we should error out since - // the caller provided an idempotency key that was used for a - // different input. - if ret.input_hash == input_hash { - Ok(Some((ret.status, ret.body))) - } else { - Ok(Some(( - StatusCode::from_u16(409).unwrap(), - Bytes::from(IDEMPOTENCY_CONFLICT_ERR), - ))) - } - } else { - Ok(None) - } - }) + .await?; + + if let Some(ret) = ret { + // Check if the hash (ret.2) of the loaded idempotent data matches the hash + // of the provided input data. If not, we should error out since + // the caller provided an idempotency key that was used for a + // different input. + if ret.input_hash == input_hash { + Ok(Some((ret.status, ret.body))) + } else { + Ok(Some(( + StatusCode::from_u16(409).unwrap(), + Bytes::from(IDEMPOTENCY_CONFLICT_ERR), + ))) + } + } else { + Ok(None) + } } // make_idempotent_call takes a function instead of direct arguments so that we // can reuse it for both the messages and the settlements calls -pub fn make_idempotent_call( +pub async fn make_idempotent_call( store: S, - non_idempotent_function: F, + non_idempotent_function: impl Future, input_hash: [u8; 32], idempotency_key: Option, // As per the spec, the success status code is independent of the @@ -91,88 +97,78 @@ pub fn make_idempotent_call( status_code: StatusCode, // The default value is used when the engine returns a default return type default_return_value: Bytes, -) -> impl Future +) -> Result<(StatusCode, Bytes), ApiError> where - F: FnOnce() -> Box + Send>, S: IdempotentStore + Clone + Send + Sync + 'static, { if let Some(idempotency_key) = idempotency_key { // If there an idempotency key was provided, check idempotency - // and the key was not present or conflicting with an existing - // key, perform the call and save the idempotent return data - Either::A( - check_idempotency(store.clone(), idempotency_key.clone(), input_hash).and_then( - move |ret: Option<(StatusCode, Bytes)>| { - if let Some(ret) = ret { - if ret.0.is_success() { - Either::A(Either::A(ok((ret.0, ret.1)))) - } else { - let err_msg = ApiErrorType { - r#type: &ProblemType::Default, - status: ret.0, - title: "Idempotency Error", - }; - // if check_idempotency returns an error, then it - // has to be an idempotency error - let ret_error = ApiError::from_api_error_type(&err_msg) - .detail(String::from_utf8_lossy(&ret.1).to_string()); - Either::A(Either::B(err(ret_error))) + match check_idempotency(store.clone(), idempotency_key.clone(), input_hash).await? { + Some(ret) => { + if ret.0.is_success() { + // Return an OK response if the idempotent call was successful + Ok((ret.0, ret.1)) + } else { + // Return an HTTP Error otherwise + let err_msg = ApiErrorType { + r#type: &ProblemType::Default, + status: ret.0, + title: "Idempotency Error", + }; + // if check_idempotency returns an error, then it + // has to be an idempotency error + let ret_error = ApiError::from_api_error_type(&err_msg) + .detail(String::from_utf8_lossy(&ret.1).to_string()); + Err(ret_error) + } + } + None => { + // If there was no previous entry, make the idempotent call and save it + // Note: The error is also saved idempotently + let ret = match non_idempotent_function.await { + Ok(r) => r, + Err(ret) => { + let status_code = ret.status; + let data = Bytes::from(ret.detail.clone().unwrap_or_default()); + if store + .save_idempotent_data(idempotency_key, input_hash, status_code, data) + .await + .is_err() + { + // Should we be panicking here instead? + error!("Failed to connect to the store! The request will not be idempotent if retried.") } - } else { - Either::B( - non_idempotent_function().map_err({ - let store = store.clone(); - let idempotency_key = idempotency_key.clone(); - move |ret: ApiError| { - let status_code = ret.status; - let data = Bytes::from(ret.detail.clone().unwrap_or_default()); - spawn(store.save_idempotent_data( - idempotency_key, - input_hash, - status_code, - data, - ).map_err(move |_| error!("Failed to connect to the store! The request will not be idempotent if retried."))); - ret - }}) - .map(move |ret| { - let data = match ret { - ApiResponse::Default => default_return_value, - ApiResponse::Data(d) => d, - }; - (status_code, data) - }).and_then( - move |ret: (StatusCode, Bytes)| { - store - .save_idempotent_data( - idempotency_key, - input_hash, - ret.0, - ret.1.clone(), - ) - .map_err(move |_| { - error!("Failed to connect to the store! The request will not be idempotent if retried."); - IDEMPOTENT_STORE_CALL_ERROR.clone() - }) - .and_then(move |_| Ok((ret.0, ret.1))) - }, - ), - ) + return Err(ret); } - }, - ), - ) + }; + + let data = match ret { + ApiResponse::Default => default_return_value, + ApiResponse::Data(d) => d, + }; + // TODO refactor for readability, can unify the 2 idempotency calls, the error and the data + // are both Bytes + store + .save_idempotent_data( + idempotency_key, + input_hash, + status_code, + data.clone(), + ) + .map_err(move |_| { + error!("Failed to connect to the store! The request will not be idempotent if retried."); + IDEMPOTENT_STORE_CALL_ERROR.clone() + }).await?; + + Ok((status_code, data)) + } + } } else { // otherwise just make the call w/o any idempotency saves - Either::B( - non_idempotent_function() - .map(move |ret| { - let data = match ret { - ApiResponse::Default => default_return_value, - ApiResponse::Data(d) => d, - }; - (status_code, data) - }) - .and_then(move |ret: (StatusCode, Bytes)| Ok((ret.0, ret.1))), - ) + let data = match non_idempotent_function.await? { + ApiResponse::Default => default_return_value, + ApiResponse::Data(d) => d, + }; + Ok((status_code, data)) } } diff --git a/crates/interledger-settlement/src/core/mod.rs b/crates/interledger-settlement/src/core/mod.rs index 29fca3b2b..58679fc53 100644 --- a/crates/interledger-settlement/src/core/mod.rs +++ b/crates/interledger-settlement/src/core/mod.rs @@ -2,10 +2,14 @@ /// Only exported if the `backends_common` feature flag is enabled #[cfg(feature = "backends_common")] pub mod backends_common; -/// The REST API for the settlement engines + +/// Web service which exposes settlement related endpoints as described in the [RFC](https://interledger.org/rfcs/0038-settlement-engines/), +/// All endpoints are idempotent. pub mod engines_api; + /// Expose useful utilities for implementing idempotent functionalities pub mod idempotency; + /// Expose useful traits pub mod types; @@ -14,6 +18,27 @@ use num_traits::Zero; use ring::digest::{digest, SHA256}; use types::{Convert, ConvertDetails}; +/// Converts a number from a precision to another while taking precision loss into account +/// +/// # Examples +/// ```rust +/// # use num_bigint::BigUint; +/// # use interledger_settlement::core::scale_with_precision_loss; +/// assert_eq!( +/// scale_with_precision_loss(BigUint::from(905u32), 9, 11), +/// (BigUint::from(9u32), BigUint::from(5u32)) +/// ); +/// +/// assert_eq!( +/// scale_with_precision_loss(BigUint::from(8053u32), 9, 12), +/// (BigUint::from(8u32), BigUint::from(53u32)) +/// ); +/// +/// assert_eq!( +/// scale_with_precision_loss(BigUint::from(1u32), 9, 6), +/// (BigUint::from(1000u32), BigUint::from(0u32)) +/// ); +/// ``` pub fn scale_with_precision_loss( amount: BigUint, local_scale: u8, @@ -49,9 +74,7 @@ pub fn scale_with_precision_loss( } } -// Helper function that returns any idempotent data that corresponds to a -// provided idempotency key. It fails if the hash of the input that -// generated the idempotent data does not match the hash of the provided input. +/// Returns the 32-bytes SHA256 hash of the provided preimage pub fn get_hash_of(preimage: &[u8]) -> [u8; 32] { let mut hash = [0; 32]; hash.copy_from_slice(digest(&SHA256, preimage).as_ref()); diff --git a/crates/interledger-settlement/src/core/types.rs b/crates/interledger-settlement/src/core/types.rs index 4168a0805..347460574 100644 --- a/crates/interledger-settlement/src/core/types.rs +++ b/crates/interledger-settlement/src/core/types.rs @@ -1,5 +1,5 @@ +use async_trait::async_trait; use bytes::Bytes; -use futures::Future; use http::StatusCode; use interledger_http::error::{ApiError, ApiErrorType, ProblemType}; use interledger_packet::Address; @@ -12,14 +12,14 @@ use std::str::FromStr; use url::Url; use uuid::Uuid; -// Account without an engine error +/// No Engine Configured for Account error type (404 Not Found) pub const NO_ENGINE_CONFIGURED_ERROR_TYPE: ApiErrorType = ApiErrorType { r#type: &ProblemType::Default, title: "No settlement engine configured", status: StatusCode::NOT_FOUND, }; -// Number conversion errors +/// Number Conversion error type (404 Not Found) pub const CONVERSION_ERROR_TYPE: ApiErrorType = ApiErrorType { r#type: &ProblemType::Default, title: "Conversion error", @@ -27,16 +27,25 @@ pub const CONVERSION_ERROR_TYPE: ApiErrorType = ApiErrorType { }; lazy_static! { + /// The Settlement ILP Address as defined in the [RFC](https://interledger.org/rfcs/0038-settlement-engines/) pub static ref SE_ILP_ADDRESS: Address = Address::from_str("peer.settle").unwrap(); } +/// The Quantity object as defined in the [RFC](https://interledger.org/rfcs/0038-settlement-engines/) +/// An amount denominated in some unit of a single, fungible asset. +/// (Since each account is denominated in a single asset, the type of asset is implied.) #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] pub struct Quantity { + /// Amount of the unit, which is a non-negative integer. + /// This amount is encoded as a string to ensure no precision + /// is lost on platforms that don't natively support arbitrary precision integers. pub amount: String, + /// Asset scale of the unit pub scale: u8, } impl Quantity { + /// Creates a new Quantity object pub fn new(amount: impl ToString, scale: u8) -> Self { Quantity { amount: amount.to_string(), @@ -45,116 +54,146 @@ impl Quantity { } } +/// Helper enum allowing API responses to not specify any data and let the consumer +/// of the call decide what to do with the success value +// TODO: could this maybe be omitted and replaced with Option in Responses? #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] pub enum ApiResponse { + /// The API call succeeded without any returned data. Default, + /// The API call returned some data which should be consumed. Data(Bytes), } +/// Type alias for Result over [`ApiResponse`](./enum.ApiResponse.html), +/// and [`ApiError`](../../../interledger_http/error/struct.ApiError.html) +pub type ApiResult = Result; + /// Trait consumed by the Settlement Engine HTTP API. Every settlement engine /// MUST implement this trait, so that it can be then be exposed over the API. +#[async_trait] pub trait SettlementEngine { - fn create_account( - &self, - account_id: String, - ) -> Box + Send>; - - fn delete_account( - &self, - account_id: String, - ) -> Box + Send>; - - fn send_money( - &self, - account_id: String, - money: Quantity, - ) -> Box + Send>; - - fn receive_message( - &self, - account_id: String, - message: Vec, - ) -> Box + Send>; + /// Informs the settlement engine that a new account was created + /// within the accounting system using the given account identifier. + /// The settlement engine MAY perform tasks as a prerequisite to settle with the account. + /// For example, a settlement engine implementation might send messages to + /// the peer to exchange ledger identifiers or to negotiate settlement-related fees. + async fn create_account(&self, account_id: String) -> ApiResult; + + /// Instructs the settlement engine that an account was deleted. + async fn delete_account(&self, account_id: String) -> ApiResult; + + /// Asynchronously send an outgoing settlement. The accounting system sends this request and accounts for outgoing settlements. + async fn send_money(&self, account_id: String, money: Quantity) -> ApiResult; + + /// Process and respond to an incoming message from the peer's settlement engine. + /// The connector sends this request when it receives an incoming settlement message + /// from the peer, and returns the response message back to the peer. + async fn receive_message(&self, account_id: String, message: Vec) -> ApiResult; } // TODO: Since we still haven't finalized all the settlement details, we might // end up deciding to add some more values, e.g. some settlement engine uid or similar. // All instances of this struct should be replaced with Url instances once/if we // agree that there is no more info required to refer to an engine. +/// The details associated with a settlement engine pub struct SettlementEngineDetails { /// Base URL of the settlement engine pub url: Url, } +/// Extension trait for [Account](../interledger_service/trait.Account.html) with [settlement](https://interledger.org/rfcs/0038-settlement-engines/) related information pub trait SettlementAccount: Account { + /// The [SettlementEngineDetails](./struct.SettlementEngineDetails.html) (if any) associated with that account fn settlement_engine_details(&self) -> Option { None } } +#[async_trait] +/// Trait used by the connector to adjust account balances on settlement events pub trait SettlementStore { type Account: Account; - fn update_balance_for_incoming_settlement( + /// Increases the account's balance/prepaid amount by the provided amount + /// + /// This is optionally idempotent. If the same idempotency_key is provided + /// then no database operation must happen. If there is an idempotency + /// conflict (same idempotency key, different inputs to function) then + /// it should return an error + async fn update_balance_for_incoming_settlement( &self, account_id: Uuid, amount: u64, idempotency_key: Option, - ) -> Box + Send>; + ) -> Result<(), ()>; - fn refund_settlement( - &self, - account_id: Uuid, - settle_amount: u64, - ) -> Box + Send>; + /// Increases the account's balance by the provided amount. + /// Only call this if a settlement request has failed + async fn refund_settlement(&self, account_id: Uuid, settle_amount: u64) -> Result<(), ()>; } +/// Trait used by the connector and engine to track amounts which should have been +/// settled but were not due to precision loss +#[async_trait] pub trait LeftoversStore { type AccountId: ToString; + /// The data type that the store uses for tracking numbers. type AssetType: ToString; /// Saves the leftover data - fn save_uncredited_settlement_amount( + /// + /// @dev: + /// If your store needs to support Big Integers but cannot, consider setting AssetType to String, + /// and then proceed to save a list of uncredited amounts as strings which would get loaded and summed + /// by the load_uncredited_settlement_amount and get_uncredited_settlement_amount + /// functions + async fn save_uncredited_settlement_amount( &self, // The account id that for which there was a precision loss account_id: Self::AccountId, // The amount for which precision loss occurred, along with their scale uncredited_settlement_amount: (Self::AssetType, u8), - ) -> Box + Send>; + ) -> Result<(), ()>; /// Returns the leftover data scaled to `local_scale` from the saved scale. - /// If any precision loss occurs during the scaling, it should be saved as + /// If any precision loss occurs during the scaling, it is be saved as /// the new leftover value. - fn load_uncredited_settlement_amount( + /// + /// @dev: + /// If the store needs to support Big Integers but cannot, consider setting AssetType to String, + /// save a list of uncredited settlement amounts, and load them and sum them in this function + async fn load_uncredited_settlement_amount( &self, account_id: Self::AccountId, local_scale: u8, - ) -> Box + Send>; + ) -> Result; /// Clears any uncredited settlement amount associated with the account - fn clear_uncredited_settlement_amount( + async fn clear_uncredited_settlement_amount( &self, account_id: Self::AccountId, - ) -> Box + Send>; + ) -> Result<(), ()>; - // Gets the current amount of leftovers in the store - fn get_uncredited_settlement_amount( + /// Gets the current amount of leftovers in the store + async fn get_uncredited_settlement_amount( &self, account_id: Self::AccountId, - ) -> Box + Send>; + ) -> Result<(Self::AssetType, u8), ()>; } +/// Helper struct for converting a quantity's amount from one asset scale to another #[derive(Debug)] pub struct ConvertDetails { pub from: u8, pub to: u8, } -/// Traits for u64 and f64 asset code conversions for amounts and rates +/// Helper trait for u64 and f64 asset code conversions for amounts and rates pub trait Convert { type Item: Sized; - // Returns the scaled result, or an error if there was an overflow + /// Returns the scaled result, or an error if there was an overflow fn normalize_scale(&self, details: ConvertDetails) -> Result; } @@ -234,12 +273,9 @@ mod tests { fn u64_test() { // overflows let huge_number = std::u64::MAX / 10; - assert_eq!( - huge_number - .normalize_scale(ConvertDetails { from: 1, to: 18 }) - .unwrap_err(), - (), - ); + assert!(huge_number + .normalize_scale(ConvertDetails { from: 1, to: 18 }) + .is_err(),); // 1 unit with scale 1, is 1 unit with scale 1 assert_eq!( 1u64.normalize_scale(ConvertDetails { from: 1, to: 1 }) @@ -310,15 +346,12 @@ mod tests { #[test] fn f64_test() { // overflow - assert_eq!( - std::f64::MAX - .normalize_scale(ConvertDetails { - from: 1, - to: std::u8::MAX, - }) - .unwrap_err(), - () - ); + assert!(std::f64::MAX + .normalize_scale(ConvertDetails { + from: 1, + to: std::u8::MAX, + }) + .is_err(),); // 1 unit with base 1, is 1 unit with base 1 assert_eq!( diff --git a/crates/interledger-settlement/src/lib.rs b/crates/interledger-settlement/src/lib.rs index b8de39338..dc6a3f1b1 100644 --- a/crates/interledger-settlement/src/lib.rs +++ b/crates/interledger-settlement/src/lib.rs @@ -1,4 +1,6 @@ -// export the API only when explicitly asked #[cfg(feature = "settlement_api")] +/// Settlement API exposed by the Interledger Node +/// This is only available if the `settlement_api` feature is enabled pub mod api; +/// Core module including types, common store implementations for settlement pub mod core; diff --git a/crates/interledger-spsp/Cargo.toml b/crates/interledger-spsp/Cargo.toml index c810b46e3..688bf0c82 100644 --- a/crates/interledger-spsp/Cargo.toml +++ b/crates/interledger-spsp/Cargo.toml @@ -9,14 +9,18 @@ repository = "https://github.com/interledger-rs/interledger-rs" [dependencies] base64 = { version = "0.10.1", default-features = false } -bytes = { version = "0.4.12", default-features = false } +bytes = { version = "0.5", default-features = false } +bytes04 = { package = "bytes", version = "0.4.12", default-features = false } failure = { version = "0.1.5", default-features = false } -futures = { version = "0.1.29", default-features = false } -hyper = { version = "0.12.35", default-features = false } +futures = { version = "0.3.1", default-features = false } +hyper = { version = "0.13.1", default-features = false } interledger-packet = { path = "../interledger-packet", version = "^0.4.0", features = ["serde"], default-features = false } interledger-service = { path = "../interledger-service", version = "^0.4.0", default-features = false } interledger-stream = { path = "../interledger-stream", version = "^0.4.0", default-features = false } log = { version = "0.4.8", default-features = false } -reqwest = { version = "0.9.22", default-features = false, features = ["default-tls"] } +reqwest = { version = "0.10", default-features = false, features = ["default-tls", "json"] } serde = { version = "1.0.101", default-features = false } serde_json = { version = "1.0.41", default-features = false } + +[dev-dependencies] +tokio = { version = "0.2.8", features = ["macros"] } diff --git a/crates/interledger-spsp/src/client.rs b/crates/interledger-spsp/src/client.rs index 0eaf396b3..ddb0e2cfe 100644 --- a/crates/interledger-spsp/src/client.rs +++ b/crates/interledger-spsp/src/client.rs @@ -1,66 +1,66 @@ use super::{Error, SpspResponse}; -use futures::{future::result, Future}; +use futures::TryFutureExt; use interledger_packet::Address; use interledger_service::{Account, IncomingService}; use interledger_stream::{send_money, StreamDelivery}; use log::{debug, error, trace}; -use reqwest::r#async::Client; +use reqwest::Client; use std::convert::TryFrom; -pub fn query(server: &str) -> impl Future { +/// Get an ILP Address and shared secret by the receiver of this payment for this connection +pub async fn query(server: &str) -> Result { let server = payment_pointer_to_url(server); trace!("Querying receiver: {}", server); let client = Client::new(); - client + let res = client .get(&server) .header("Accept", "application/spsp4+json") .send() .map_err(|err| Error::HttpError(format!("Error querying SPSP receiver: {:?}", err))) - .and_then(|res| { - res.error_for_status() - .map_err(|err| Error::HttpError(format!("Error querying SPSP receiver: {:?}", err))) - }) - .and_then(|mut res| { - res.json::() - .map_err(|err| Error::InvalidSpspServerResponseError(format!("{:?}", err))) - }) + .await?; + + let res = res + .error_for_status() + .map_err(|err| Error::HttpError(format!("Error querying SPSP receiver: {:?}", err)))?; + + res.json::() + .map_err(|err| Error::InvalidSpspServerResponseError(format!("{:?}", err))) + .await } /// Query the details of the given Payment Pointer and send a payment using the STREAM protocol. /// /// This returns the amount delivered, as reported by the receiver and in the receiver's asset's units. -pub fn pay( +pub async fn pay( service: S, from_account: A, receiver: &str, source_amount: u64, -) -> impl Future +) -> Result where - S: IncomingService + Clone, - A: Account, + S: IncomingService + Send + Sync + Clone + 'static, + A: Account + Send + Sync + Clone + 'static, { - query(receiver).and_then(move |spsp| { - let shared_secret = spsp.shared_secret; - let dest = spsp.destination_account; - result(Address::try_from(dest).map_err(move |err| { - error!("Error parsing address"); - Error::InvalidSpspServerResponseError(err.to_string()) - })) - .and_then(move |addr| { - debug!("Sending SPSP payment to address: {}", addr); + let spsp = query(receiver).await?; + let shared_secret = spsp.shared_secret; + let dest = spsp.destination_account; + let addr = Address::try_from(dest).map_err(move |err| { + error!("Error parsing address"); + Error::InvalidSpspServerResponseError(err.to_string()) + })?; + debug!("Sending SPSP payment to address: {}", addr); + + let (receipt, _plugin) = + send_money(service, &from_account, addr, &shared_secret, source_amount) + .map_err(move |err| { + error!("Error sending payment: {:?}", err); + Error::SendMoneyError(source_amount) + }) + .await?; - send_money(service, &from_account, addr, &shared_secret, source_amount) - .map(move |(receipt, _plugin)| { - debug!("Sent SPSP payment. StreamDelivery: {:?}", receipt); - receipt - }) - .map_err(move |err| { - error!("Error sending payment: {:?}", err); - Error::SendMoneyError(source_amount) - }) - }) - }) + debug!("Sent SPSP payment. StreamDelivery: {:?}", receipt); + Ok(receipt) } fn payment_pointer_to_url(payment_pointer: &str) -> String { diff --git a/crates/interledger-spsp/src/lib.rs b/crates/interledger-spsp/src/lib.rs index fe2b4ac9a..1a2c0e910 100644 --- a/crates/interledger-spsp/src/lib.rs +++ b/crates/interledger-spsp/src/lib.rs @@ -10,7 +10,9 @@ use interledger_packet::Address; use interledger_stream::Error as StreamError; use serde::{Deserialize, Serialize}; +/// An SPSP client which can query an SPSP Server's payment pointer and initiate a STREAM payment mod client; +/// An SPSP Server implementing an HTTP Service which generates ILP Addresses and Shared Secrets mod server; pub use client::{pay, query}; @@ -33,14 +35,19 @@ pub enum Error { InvalidPaymentPointerError(String), } +/// An SPSP Response returned by the SPSP server #[derive(Debug, Deserialize, Serialize)] pub struct SpspResponse { + /// The generated ILP Address for this SPSP connection destination_account: Address, + /// Base-64 encoded shared secret between SPSP client and server + /// to be consumed for the STREAM connection #[serde(with = "serde_base64")] shared_secret: Vec, } // From https://github.com/serde-rs/json/issues/360#issuecomment-330095360 +#[doc(hidden)] mod serde_base64 { use base64; use serde::{de, Deserialize, Deserializer, Serializer}; diff --git a/crates/interledger-spsp/src/server.rs b/crates/interledger-spsp/src/server.rs index 65541ceae..2d2ce38dc 100644 --- a/crates/interledger-spsp/src/server.rs +++ b/crates/interledger-spsp/src/server.rs @@ -1,12 +1,14 @@ use super::SpspResponse; use bytes::Bytes; -use futures::future::{ok, FutureResult, IntoFuture}; use hyper::{service::Service as HttpService, Body, Error, Request, Response}; use interledger_packet::Address; use interledger_stream::ConnectionGenerator; use log::debug; use std::error::Error as StdError; -use std::{fmt, str}; +use std::{ + fmt, str, + task::{Context, Poll}, +}; /// A Hyper::Service that responds to incoming SPSP Query requests with newly generated /// details for a STREAM connection. @@ -17,14 +19,19 @@ pub struct SpspResponder { } impl SpspResponder { + /// Constructs a new SPSP Responder by receiving an ILP Address and a server **secret** pub fn new(ilp_address: Address, server_secret: Bytes) -> Self { - let connection_generator = ConnectionGenerator::new(server_secret); + let server_secret_compat = bytes04::Bytes::from(server_secret.as_ref()); + let connection_generator = ConnectionGenerator::new(server_secret_compat); SpspResponder { ilp_address, connection_generator, } } + /// Returns an HTTP Response containing the destination account + /// and shared secret for this connection + /// These fields are generated via [Stream's `ConnectionGenerator`](../interledger_stream/struct.ConnectionGenerator.html#method.generate_address_and_secret) pub fn generate_http_response(&self) -> Response { let (destination_account, shared_secret) = self .connection_generator @@ -47,24 +54,17 @@ impl SpspResponder { } } -impl HttpService for SpspResponder { - type ReqBody = Body; - type ResBody = Body; +impl HttpService> for SpspResponder { + type Response = Response; type Error = Error; - type Future = FutureResult, Error>; + type Future = futures::future::Ready>; - fn call(&mut self, _request: Request) -> Self::Future { - ok(self.generate_http_response()) + fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { + Ok(()).into() } -} - -impl IntoFuture for SpspResponder { - type Item = Self; - type Error = Never; - type Future = FutureResult; - fn into_future(self) -> Self::Future { - ok(self) + fn call(&mut self, _request: Request) -> Self::Future { + futures::future::ok(self.generate_http_response()) } } @@ -87,11 +87,10 @@ impl StdError for Never { #[cfg(test)] mod spsp_server_test { use super::*; - use futures::Future; use std::str::FromStr; - #[test] - fn spsp_response_headers() { + #[tokio::test] + async fn spsp_response_headers() { let addr = Address::from_str("example.receiver").unwrap(); let mut responder = SpspResponder::new(addr, Bytes::from(&[0; 32][..])); let response = responder @@ -103,7 +102,7 @@ mod spsp_server_test { .body(Body::empty()) .unwrap(), ) - .wait() + .await .unwrap(); assert_eq!( response.headers().get("Content-Type").unwrap(), diff --git a/crates/interledger-store/Cargo.toml b/crates/interledger-store/Cargo.toml index 083f1d0ff..6d26efbdb 100644 --- a/crates/interledger-store/Cargo.toml +++ b/crates/interledger-store/Cargo.toml @@ -21,8 +21,8 @@ path = "tests/redis/redis_tests.rs" required-features = ["redis"] [dependencies] -bytes = { version = "0.4.12", default-features = false } -futures = { version = "0.1.29", default-features = false } +bytes = { version = "0.5", default-features = false } +futures = { version = "0.3", default-features = false } interledger-api = { path = "../interledger-api", version = "^0.3.0", default-features = false } interledger-packet = { path = "../interledger-packet", version = "^0.4.0", default-features = false } interledger-btp = { path = "../interledger-btp", version = "^0.4.0", default-features = false } @@ -39,21 +39,20 @@ parking_lot = { version = "0.9.0", default-features = false } ring = { version = "0.16.9", default-features = false } serde = { version = "1.0.101", default-features = false, features = ["derive"] } serde_json = { version = "1.0.41", default-features = false } -tokio-executor = { version = "0.1.8", default-features = false } -tokio-timer = { version = "0.2.11", default-features = false } +tokio = { version = "0.2.6", default-features = false, features = ["macros", "rt-core"] } url = { version = "2.1.0", default-features = false, features = ["serde"] } -http = { version = "0.1.18", default-features = false } -secrecy = { version = "0.5.1", default-features = false, features = ["serde", "bytes"] } -zeroize = { version = "1.0.0", default-features = false, features = ["bytes"] } +http = { version = "0.2", default-features = false } +secrecy = { version = "0.6", features = ["serde", "bytes"] } +zeroize = { version = "1.0.0", default-features = false } num-bigint = { version = "0.2.3", default-features = false, features = ["std"]} uuid = { version = "0.8.1", default-features = false, features = ["serde"] } # redis feature -redis_crate = { package = "redis", version = "0.13.0", default-features = false, features = ["executor"], optional = true } +redis_crate = { package = "redis", version = "0.15.1", default-features = false, features = ["tokio-rt-core"], optional = true } +async-trait = "0.1.22" [dev-dependencies] env_logger = { version = "0.7.0", default-features = false } net2 = { version = "0.2.33", default-features = false } rand = { version = "0.7.2", default-features = false } -tokio = { version = "0.1.22", default-features = false } os_type = { version = "2.2", default-features = false } diff --git a/crates/interledger-store/src/account.rs b/crates/interledger-store/src/account.rs index 6cca6652b..03ca854f7 100644 --- a/crates/interledger-store/src/account.rs +++ b/crates/interledger-store/src/account.rs @@ -11,40 +11,76 @@ use interledger_service_util::{ use interledger_settlement::core::types::{SettlementAccount, SettlementEngineDetails}; use log::error; use ring::aead; -use secrecy::{ExposeSecret, SecretBytes, SecretString}; +use secrecy::{ExposeSecret, SecretBytesMut, SecretString}; use serde::Serializer; use serde::{Deserialize, Serialize}; use std::str::{self, FromStr}; use url::Url; use uuid::Uuid; +/// The account which contains all the data required for a full implementation of Interledger +// TODO: Maybe we should feature gate these fields? e.g. ilp_over_btp variables should only be there +// if btp feature is enabled #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Account { + /// Unique id corresponding to the account's primary key in the database pub(crate) id: Uuid, + /// The account's username pub(crate) username: Username, #[serde(serialize_with = "address_to_string")] + /// The account's Interledger Protocol address pub(crate) ilp_address: Address, // TODO add additional routes + /// The account's currency pub(crate) asset_code: String, + /// The account's asset scale pub(crate) asset_scale: u8, + /// The max amount per packet which can be routed for this account pub(crate) max_packet_amount: u64, + /// The minimum balance this account can have (consider this as a credit/trust limit) pub(crate) min_balance: Option, + /// The account's ILP over HTTP URL (this is where packets are sent over HTTP from your node) pub(crate) ilp_over_http_url: Option, #[serde(serialize_with = "optional_secret_bytes_to_utf8")] - pub(crate) ilp_over_http_incoming_token: Option, + /// The account's API and incoming ILP over HTTP token. + /// This must match the ILP over HTTP outgoing token on the peer's node if receiving + /// packets from that peer + // TODO: The incoming token is used for both ILP over HTTP, and for authorizing actions from the HTTP API. + // Should we add 1 more token, for more granular permissioning? + pub(crate) ilp_over_http_incoming_token: Option, #[serde(serialize_with = "optional_secret_bytes_to_utf8")] - pub(crate) ilp_over_http_outgoing_token: Option, + /// The account's outgoing ILP over HTTP token + /// This must match the ILP over HTTP incoming token on the peer's node if sending + /// packets to that peer + pub(crate) ilp_over_http_outgoing_token: Option, + /// The account's ILP over BTP URL (this is where packets are sent over WebSockets from your node) pub(crate) ilp_over_btp_url: Option, #[serde(serialize_with = "optional_secret_bytes_to_utf8")] - pub(crate) ilp_over_btp_incoming_token: Option, + /// The account's incoming ILP over BTP token. + /// This must match the ILP over BTP outgoing token on the peer's node if exchanging + /// packets with that peer + pub(crate) ilp_over_btp_incoming_token: Option, #[serde(serialize_with = "optional_secret_bytes_to_utf8")] - pub(crate) ilp_over_btp_outgoing_token: Option, + /// The account's outgoing ILP over BTP token. + /// This must match the ILP over BTP incoming token on the peer's node if exchanging + /// packets with that peer + pub(crate) ilp_over_btp_outgoing_token: Option, + /// The threshold after which the balance service will trigger a settlement pub(crate) settle_threshold: Option, + /// The amount which the balance service will attempt to settle down to pub(crate) settle_to: Option, + /// The routing relation of the account pub(crate) routing_relation: RoutingRelation, + /// The round trip time of the account (should be set depending on how + /// well the network connectivity of the account and the node is) pub(crate) round_trip_time: u32, + /// The limit of packets the account can send per minute pub(crate) packets_per_minute_limit: Option, + /// The maximum amount the account can send per minute pub(crate) amount_per_minute_limit: Option, + /// The account's settlement engine URL. If a global engine url is configured + /// for the account's asset code, that will be used instead (even if the account is + /// configured with a specific one) pub(crate) settlement_engine_url: Option, } @@ -56,7 +92,7 @@ where } fn optional_secret_bytes_to_utf8( - _bytes: &Option, + _bytes: &Option, serializer: S, ) -> Result where @@ -66,6 +102,10 @@ where } impl Account { + /// Creates an account from the provided id and details. If there is no ILP Address + /// in the provided details, then the account's ILP Address is generated by appending + /// the `details.username` to the provided `node_ilp_address`. + /// The default RoutingRelation is `NonRoutingAccount` pub fn try_from( id: Uuid, details: AccountDetails, @@ -118,17 +158,17 @@ impl Account { ilp_over_http_url, ilp_over_http_incoming_token: details .ilp_over_http_incoming_token - .map(|token| SecretBytes::new(token.expose_secret().to_string())), + .map(|token| SecretBytesMut::new(token.expose_secret().as_str())), ilp_over_http_outgoing_token: details .ilp_over_http_outgoing_token - .map(|token| SecretBytes::new(token.expose_secret().to_string())), + .map(|token| SecretBytesMut::new(token.expose_secret().as_str())), ilp_over_btp_url, ilp_over_btp_incoming_token: details .ilp_over_btp_incoming_token - .map(|token| SecretBytes::new(token.expose_secret().to_string())), + .map(|token| SecretBytesMut::new(token.expose_secret().as_str())), ilp_over_btp_outgoing_token: details .ilp_over_btp_outgoing_token - .map(|token| SecretBytes::new(token.expose_secret().to_string())), + .map(|token| SecretBytesMut::new(token.expose_secret().as_str())), settle_to: details.settle_to, settle_threshold: details.settle_threshold, routing_relation, @@ -139,30 +179,31 @@ impl Account { }) } + /// Encrypts the account's incoming/outgoing BTP and HTTP keys with the provided encryption key pub fn encrypt_tokens( mut self, encryption_key: &aead::LessSafeKey, ) -> AccountWithEncryptedTokens { if let Some(ref token) = self.ilp_over_btp_outgoing_token { - self.ilp_over_btp_outgoing_token = Some(SecretBytes::from(encrypt_token( + self.ilp_over_btp_outgoing_token = Some(SecretBytesMut::from(encrypt_token( encryption_key, &token.expose_secret(), ))); } if let Some(ref token) = self.ilp_over_http_outgoing_token { - self.ilp_over_http_outgoing_token = Some(SecretBytes::from(encrypt_token( + self.ilp_over_http_outgoing_token = Some(SecretBytesMut::from(encrypt_token( encryption_key, &token.expose_secret(), ))); } if let Some(ref token) = self.ilp_over_btp_incoming_token { - self.ilp_over_btp_incoming_token = Some(SecretBytes::from(encrypt_token( + self.ilp_over_btp_incoming_token = Some(SecretBytesMut::from(encrypt_token( encryption_key, &token.expose_secret(), ))); } if let Some(ref token) = self.ilp_over_http_incoming_token { - self.ilp_over_http_incoming_token = Some(SecretBytes::from(encrypt_token( + self.ilp_over_http_incoming_token = Some(SecretBytesMut::from(encrypt_token( encryption_key, &token.expose_secret(), ))); @@ -171,12 +212,14 @@ impl Account { } } +/// A wrapper over the [`Account`](./struct.Account.html) which contains their encrypt tokens. #[derive(Debug, Clone)] pub struct AccountWithEncryptedTokens { pub(super) account: Account, } impl AccountWithEncryptedTokens { + /// Decrypts the account's incoming/outgoing BTP and HTTP keys with the provided decryption key pub fn decrypt_tokens(mut self, decryption_key: &aead::LessSafeKey) -> Account { if let Some(ref encrypted) = self.account.ilp_over_btp_outgoing_token { self.account.ilp_over_btp_outgoing_token = @@ -227,6 +270,8 @@ impl AccountWithEncryptedTokens { } } +// The following trait implementations are simple accessors to the Account's fields + impl AccountTrait for Account { fn id(&self) -> Uuid { self.id diff --git a/crates/interledger-store/src/crypto.rs b/crates/interledger-store/src/crypto.rs index 00adb05a6..30d70bcc2 100644 --- a/crates/interledger-store/src/crypto.rs +++ b/crates/interledger-store/src/crypto.rs @@ -1,4 +1,4 @@ -use bytes::Bytes; +use bytes::BytesMut; use ring::{ aead, hmac, rand::{SecureRandom, SystemRandom}, @@ -8,7 +8,7 @@ const NONCE_LENGTH: usize = 12; static ENCRYPTION_KEY_GENERATION_STRING: &[u8] = b"ilp_store_redis_encryption_key"; use core::sync::atomic; -use secrecy::{DebugSecret, Secret, SecretBytes}; +use secrecy::{DebugSecret, Secret, SecretBytesMut}; use std::ptr; use zeroize::Zeroize; @@ -117,7 +117,7 @@ pub fn generate_keys(server_secret: &[u8]) -> (Secret, Secret Bytes { +pub fn encrypt_token(encryption_key: &aead::LessSafeKey, token: &[u8]) -> BytesMut { let mut token = token.to_vec(); let mut nonce: [u8; NONCE_LENGTH] = [0; NONCE_LENGTH]; @@ -129,7 +129,7 @@ pub fn encrypt_token(encryption_key: &aead::LessSafeKey, token: &[u8]) -> Bytes match encryption_key.seal_in_place_append_tag(nonce, aead::Aad::from(&[]), &mut token) { Ok(_) => { token.append(&mut nonce_copy.as_ref().to_vec()); - Bytes::from(token) + BytesMut::from(token.as_slice()) } _ => panic!("Unable to encrypt token"), } @@ -138,7 +138,7 @@ pub fn encrypt_token(encryption_key: &aead::LessSafeKey, token: &[u8]) -> Bytes pub fn decrypt_token( decryption_key: &aead::LessSafeKey, encrypted: &[u8], -) -> Result { +) -> Result { if encrypted.len() < aead::MAX_TAG_LEN { return Err(()); } @@ -150,7 +150,7 @@ pub fn decrypt_token( let nonce = aead::Nonce::assume_unique_for_key(nonce); if let Ok(token) = decryption_key.open_in_place(nonce, aead::Aad::empty(), &mut encrypted) { - Ok(SecretBytes::new(token.to_vec())) + Ok(SecretBytesMut::new(&token[..])) } else { Err(()) } diff --git a/crates/interledger-store/src/lib.rs b/crates/interledger-store/src/lib.rs index 3b67b70fc..2f7412d84 100644 --- a/crates/interledger-store/src/lib.rs +++ b/crates/interledger-store/src/lib.rs @@ -2,7 +2,10 @@ //! //! Backend databases for storing account details, balances, the routing table, etc. +/// A module to define the primitive `Account` struct which implements `Account` related traits. pub mod account; +/// Cryptographic utilities for encrypting/decrypting data as well as clearing data from memory pub mod crypto; +/// A redis backend using [redis-rs](https://github.com/mitsuhiko/redis-rs/) #[cfg(feature = "redis")] pub mod redis; diff --git a/crates/interledger-store/src/redis/mod.rs b/crates/interledger-store/src/redis/mod.rs index c8290dc9c..31d373409 100644 --- a/crates/interledger-store/src/redis/mod.rs +++ b/crates/interledger-store/src/redis/mod.rs @@ -19,12 +19,9 @@ use reconnect::RedisReconnect; use super::account::{Account, AccountWithEncryptedTokens}; use super::crypto::{encrypt_token, generate_keys, DecryptionKey, EncryptionKey}; -use bytes::Bytes; -use futures::{ - future::{err, ok, result, Either}, - sync::mpsc::UnboundedSender, - Future, Stream, -}; +use async_trait::async_trait; +use bytes::{Bytes, BytesMut}; +use futures::channel::mpsc::UnboundedSender; use http::StatusCode; use interledger_api::{AccountDetails, AccountSettings, EncryptedAccountSettings, NodeStore}; use interledger_btp::BtpStore; @@ -46,11 +43,12 @@ use lazy_static::lazy_static; use log::{debug, error, trace, warn}; use num_bigint::BigUint; use parking_lot::RwLock; +use redis_crate::AsyncCommands; use redis_crate::{ self, cmd, from_redis_value, Client, ConnectionInfo, ControlFlow, ErrorKind, FromRedisValue, - PipelineCommands, PubSubCommands, RedisError, RedisWrite, Script, ToRedisArgs, Value, + PubSubCommands, RedisError, RedisWrite, Script, ToRedisArgs, Value, }; -use secrecy::{ExposeSecret, Secret, SecretBytes}; +use secrecy::{ExposeSecret, Secret, SecretBytesMut}; use serde::{Deserialize, Serialize}; use serde_json; use std::{ @@ -62,10 +60,8 @@ use std::{ str, str::FromStr, sync::Arc, - time::{Duration, Instant}, + time::Duration, }; -use tokio_executor::spawn; -use tokio_timer::Interval; use url::Url; use uuid::Uuid; use zeroize::Zeroize; @@ -80,14 +76,17 @@ static DEFAULT_ROUTE_KEY: &str = "routes:default"; static STREAM_NOTIFICATIONS_PREFIX: &str = "stream_notifications:"; static SETTLEMENT_ENGINES_KEY: &str = "settlement_engines"; +/// Domain separator for leftover amounts fn uncredited_amount_key(account_id: impl ToString) -> String { format!("uncredited-amount:{}", account_id.to_string()) } +/// Domain separator for idempotency keys fn prefixed_idempotency_key(idempotency_key: String) -> String { format!("idempotency-key:{}", idempotency_key) } +/// Domain separator for accounts fn accounts_key(account_id: Uuid) -> String { format!("accounts:{}", account_id) } @@ -98,37 +97,46 @@ fn accounts_key(account_id: Uuid) -> String { // process is accessing Redis at the same time. // For more information on scripting in Redis, see https://redis.io/commands/eval lazy_static! { + /// The node's default ILP Address static ref DEFAULT_ILP_ADDRESS: Address = Address::from_str("local.host").unwrap(); /// This lua script fetches an account associated with a username. The client /// MUST ensure that the returned account is authenticated. static ref ACCOUNT_FROM_USERNAME: Script = Script::new(include_str!("lua/account_from_username.lua")); - /// Load a list of accounts + /// Lua script which loads a list of accounts /// If an account does not have a settlement_engine_url set /// but there is one configured for that account's currency, /// it will use the globally configured url static ref LOAD_ACCOUNTS: Script = Script::new(include_str!("lua/load_accounts.lua")); + /// Lua script which reduces the provided account's balance before sending a Prepare packet static ref PROCESS_PREPARE: Script = Script::new(include_str!("lua/process_prepare.lua")); + /// Lua script which increases the provided account's balance after receiving a Fulfill packet static ref PROCESS_FULFILL: Script = Script::new(include_str!("lua/process_fulfill.lua")); + /// Lua script which increases the provided account's balance after receiving a Reject packet static ref PROCESS_REJECT: Script = Script::new(include_str!("lua/process_reject.lua")); + /// Lua script which increases the provided account's balance after a settlement attempt failed static ref REFUND_SETTLEMENT: Script = Script::new(include_str!("lua/refund_settlement.lua")); + /// Lua script which increases the provided account's balance after an incoming settlement succeeded static ref PROCESS_INCOMING_SETTLEMENT: Script = Script::new(include_str!("lua/process_incoming_settlement.lua")); } +/// Builder for the Redis Store pub struct RedisStoreBuilder { redis_url: ConnectionInfo, secret: [u8; 32], poll_interval: u64, + /// Connector's ILP Address. Used to insert `Child` accounts as node_ilp_address: Address, } impl RedisStoreBuilder { + /// Simple Constructor pub fn new(redis_url: ConnectionInfo, secret: [u8; 32]) -> Self { RedisStoreBuilder { redis_url, @@ -138,136 +146,142 @@ impl RedisStoreBuilder { } } + /// Sets the ILP Address corresponding to the node pub fn node_ilp_address(&mut self, node_ilp_address: Address) -> &mut Self { self.node_ilp_address = node_ilp_address; self } + /// Sets the poll interval at which the store will update its routes pub fn poll_interval(&mut self, poll_interval: u64) -> &mut Self { self.poll_interval = poll_interval; self } - pub fn connect(&mut self) -> impl Future { + /// Connects to the Redis Store + /// + /// Specifically + /// 1. Generates encryption and decryption keys + /// 1. Connects to the redis store (ensuring that it reconnects in case of drop) + /// 1. Gets the Node address assigned to us by our parent (if it exists) + /// 1. Starts polling for routing table updates + /// 1. Spawns a thread to notify incoming payments over WebSockets + pub async fn connect(&mut self) -> Result { let redis_info = self.redis_url.clone(); let (encryption_key, decryption_key) = generate_keys(&self.secret[..]); self.secret.zeroize(); // clear the secret after it has been used for key generation let poll_interval = self.poll_interval; let ilp_address = self.node_ilp_address.clone(); - RedisReconnect::connect(redis_info.clone()) + let client = Client::open(redis_info.clone()) + .map_err(|err| error!("Error creating subscription Redis client: {:?}", err))?; + debug!("Connected subscription client to redis: {:?}", client); + let mut connection = RedisReconnect::connect(redis_info.clone()) .map_err(|_| ()) - .join( - result(Client::open(redis_info.clone())) - .map_err(|err| error!("Error creating subscription Redis client: {:?}", err)) - .and_then(|client| { - debug!("Connected subscription client to redis: {:?}", client); - client.get_connection().map_err(|err| { - error!("Error connecting subscription client to Redis: {:?}", err) - }) - }), - ) - .and_then(move |(connection, mut sub_connection)| { - // Before initializing the store, check if we have an address - // that was configured due to adding a parent. If no parent was - // found, use the builder's provided address (local.host) or the - // one we decided to override it with - redis_crate::cmd("GET") - .arg(PARENT_ILP_KEY) - .query_async(connection.clone()) - .map_err(|err| { - error!( - "Error checking whether we have a parent configured: {:?}", - err - ) - }) - .and_then(move |(_, address): (RedisReconnect, Option)| { - Ok(if let Some(address) = address { - Address::from_str(&address).unwrap() - } else { - ilp_address - }) - }) - .and_then(move |node_ilp_address| { - let store = RedisStore { - ilp_address: Arc::new(RwLock::new(node_ilp_address)), - connection, - subscriptions: Arc::new(RwLock::new(HashMap::new())), - exchange_rates: Arc::new(RwLock::new(HashMap::new())), - routes: Arc::new(RwLock::new(Arc::new(HashMap::new()))), - encryption_key: Arc::new(encryption_key), - decryption_key: Arc::new(decryption_key), - }; - - // Poll for routing table updates - // Note: if this behavior changes, make sure to update the Drop implementation - let connection_clone = Arc::downgrade(&store.connection.conn); - let redis_info = store.connection.redis_info.clone(); - let routing_table = store.routes.clone(); - let poll_routes = - Interval::new(Instant::now(), Duration::from_millis(poll_interval)) - .map_err(|err| error!("Interval error: {:?}", err)) - .for_each(move |_| { - if let Some(conn) = connection_clone.upgrade() { - Either::A(update_routes( - RedisReconnect { - conn, - redis_info: redis_info.clone(), - }, - routing_table.clone(), - )) - } else { - debug!("Not polling routes anymore because connection was closed"); - // TODO make sure the interval stops - Either::B(err(())) - } - }); - spawn(poll_routes); - - // Here we spawn a worker thread to listen for incoming messages on Redis pub/sub, - // running a callback for each message received. - // This currently must be a thread rather than a task due to the redis-rs driver - // not yet supporting asynchronous subscriptions (see https://github.com/mitsuhiko/redis-rs/issues/183). - let subscriptions_clone = store.subscriptions.clone(); - std::thread::spawn(move || { - let sub_status = - sub_connection.psubscribe::<_, _, Vec>(&["*"], move |msg| { - let channel_name = msg.get_channel_name(); - if channel_name.starts_with(STREAM_NOTIFICATIONS_PREFIX) { - if let Ok(account_id) = Uuid::from_str(&channel_name[STREAM_NOTIFICATIONS_PREFIX.len()..]) { - let message: PaymentNotification = match serde_json::from_slice(msg.get_payload_bytes()) { - Ok(s) => s, - Err(e) => { - error!("Failed to get payload from subscription: {}", e); - return ControlFlow::Continue; - } - }; - trace!("Subscribed message received for account {}: {:?}", account_id, message); - match subscriptions_clone.read().get(&account_id) { - Some(sender) => { - if let Err(err) = sender.unbounded_send(message) { - error!("Failed to send message: {}", err); - } - } - None => trace!("Ignoring message for account {} because there were no open subscriptions", account_id), - } - } else { - error!("Invalid Uuid in channel name: {}", channel_name); - } - } else { - warn!("Ignoring unexpected message from Redis subscription for channel: {}", channel_name); + .await?; + let mut sub_connection = client + .get_connection() + .map_err(|err| error!("Error connecting subscription client to Redis: {:?}", err))?; + // Before initializing the store, check if we have an address + // that was configured due to adding a parent. If no parent was + // found, use the builder's provided address (local.host) or the + // one we decided to override it with + let address: Option = connection + .get(PARENT_ILP_KEY) + .map_err(|err| { + error!( + "Error checking whether we have a parent configured: {:?}", + err + ) + }) + .await?; + let node_ilp_address = if let Some(address) = address { + Address::from_str(&address).unwrap() + } else { + ilp_address + }; + + let store = RedisStore { + ilp_address: Arc::new(RwLock::new(node_ilp_address)), + connection, + subscriptions: Arc::new(RwLock::new(HashMap::new())), + exchange_rates: Arc::new(RwLock::new(HashMap::new())), + routes: Arc::new(RwLock::new(Arc::new(HashMap::new()))), + encryption_key: Arc::new(encryption_key), + decryption_key: Arc::new(decryption_key), + }; + + // Poll for routing table updates + // Note: if this behavior changes, make sure to update the Drop implementation + let connection_clone = Arc::downgrade(&store.connection.conn); + let redis_info = store.connection.redis_info.clone(); + let routing_table = store.routes.clone(); + + let poll_routes = async move { + let mut interval = tokio::time::interval(Duration::from_millis(poll_interval)); + // Irrefutable while pattern, can we do something here? + loop { + interval.tick().await; + if let Some(conn) = connection_clone.upgrade() { + let _ = update_routes( + RedisReconnect { + conn, + redis_info: redis_info.clone(), + }, + routing_table.clone(), + ) + .await; + } else { + debug!("Not polling routes anymore because connection was closed"); + break; + } + } + Ok::<(), ()>(()) + }; + tokio::spawn(poll_routes); + + // Here we spawn a worker thread to listen for incoming messages on Redis pub/sub, + // running a callback for each message received. + // This currently must be a thread rather than a task due to the redis-rs driver + // not yet supporting asynchronous subscriptions (see https://github.com/mitsuhiko/redis-rs/issues/183). + let subscriptions_clone = store.subscriptions.clone(); + std::thread::spawn(move || { + let sub_status = + sub_connection.psubscribe::<_, _, Vec>(&["*"], move |msg| { + let channel_name = msg.get_channel_name(); + if channel_name.starts_with(STREAM_NOTIFICATIONS_PREFIX) { + if let Ok(account_id) = Uuid::from_str(&channel_name[STREAM_NOTIFICATIONS_PREFIX.len()..]) { + let message: PaymentNotification = match serde_json::from_slice(msg.get_payload_bytes()) { + Ok(s) => s, + Err(e) => { + error!("Failed to get payload from subscription: {}", e); + return ControlFlow::Continue; + } + }; + trace!("Subscribed message received for account {}: {:?}", account_id, message); + match subscriptions_clone.read().get(&account_id) { + Some(sender) => { + if let Err(err) = sender.unbounded_send(message) { + error!("Failed to send message: {}", err); } - ControlFlow::Continue - }); - match sub_status { - Err(e) => warn!("Could not issue psubscribe to Redis: {}", e), - Ok(_) => debug!("Successfully subscribed to Redis pubsub"), + } + None => trace!("Ignoring message for account {} because there were no open subscriptions", account_id), } - }); + } else { + error!("Invalid Uuid in channel name: {}", channel_name); + } + } else { + warn!("Ignoring unexpected message from Redis subscription for channel: {}", channel_name); + } + ControlFlow::Continue + }); + match sub_status { + Err(e) => warn!("Could not issue psubscribe to Redis: {}", e), + Ok(_) => debug!("Successfully subscribed to Redis pubsub"), + } + }); - Ok(store) - }) - }) + Ok(store) } } @@ -279,8 +293,11 @@ impl RedisStoreBuilder { /// future versions of it will use PubSub to subscribe to updates. #[derive(Clone)] pub struct RedisStore { + /// The Store's ILP Address ilp_address: Arc>, + /// A connection which reconnects if dropped by accident connection: RedisReconnect, + /// WebSocket sender which publishes incoming payment updates subscriptions: Arc>>>, exchange_rates: Arc>>, /// The store keeps the routing table in memory so that it can be returned @@ -290,29 +307,32 @@ pub struct RedisStore { /// The inner `Arc` is used so that the `routing_table` method can /// return a reference to the routing table without cloning the underlying data. routes: Arc>>>, + /// Encryption Key so that the no cleartext data are stored encryption_key: Arc>, + /// Decryption Key to provide cleartext data to users decryption_key: Arc>, } impl RedisStore { - fn get_all_accounts_ids(&self) -> impl Future, Error = ()> { - let mut pipe = redis_crate::pipe(); - pipe.smembers("accounts"); - pipe.query_async(self.connection.clone()) + /// Gets all the account ids from Redis + async fn get_all_accounts_ids(&self) -> Result, ()> { + let mut connection = self.connection.clone(); + let account_ids: Vec = connection + .smembers("accounts") .map_err(|err| error!("Error getting account IDs: {:?}", err)) - .and_then(|(_conn, account_ids): (_, Vec>)| { - let account_ids: Vec = account_ids[0].iter().map(|rid| rid.0).collect(); - Ok(account_ids) - }) + .await?; + Ok(account_ids.iter().map(|rid| rid.0).collect()) } - fn redis_insert_account( - &self, + /// Inserts the account corresponding to the provided `AccountWithEncryptedtokens` + /// in Redis. Returns the provided account (tokens remain encrypted) + async fn redis_insert_account( + &mut self, encrypted: AccountWithEncryptedTokens, - ) -> Box + Send> { + ) -> Result { let account = encrypted.account.clone(); let ret = encrypted.clone(); - let connection = self.connection.clone(); + let mut connection = self.connection.clone(); let routing_table = self.routes.clone(); // Check that there isn't already an account with values that MUST be unique let mut pipe = redis_crate::pipe(); @@ -322,153 +342,170 @@ impl RedisStore { pipe.exists(PARENT_ILP_KEY); } - Box::new(pipe.query_async(connection.clone()) + let results: Vec = pipe + .query_async(&mut connection.clone()) .map_err(|err| { - error!("Error checking whether account details already exist: {:?}", err) - }) - .and_then( - move |(connection, results): (RedisReconnect, Vec)| { - if results.iter().any(|val| *val) { - warn!("An account already exists with the same {}. Cannot insert account: {:?}", account.id, account); - Err(()) - } else { - Ok((connection, account)) - } + error!( + "Error checking whether account details already exist: {:?}", + err + ) }) - .and_then(move |(connection, account)| { - let mut pipe = redis_crate::pipe(); - pipe.atomic(); + .await?; + if results.iter().any(|val| *val) { + warn!( + "An account already exists with the same {}. Cannot insert account: {:?}", + account.id, account + ); + return Err(()); + } - // Add the account key to the list of accounts - pipe.sadd("accounts", RedisAccountId(account.id)).ignore(); + let mut pipe = redis_crate::pipe(); + pipe.atomic(); - // Save map for Username -> Account ID - pipe.hset("usernames", account.username().as_ref(), RedisAccountId(account.id)).ignore(); + // Add the account key to the list of accounts + pipe.sadd("accounts", RedisAccountId(account.id)).ignore(); - // Set account details - pipe.cmd("HMSET") - .arg(accounts_key(account.id)) - .arg(encrypted).ignore(); + // Save map for Username -> Account ID + pipe.hset( + "usernames", + account.username().as_ref(), + RedisAccountId(account.id), + ) + .ignore(); - // Set balance-related details - pipe.hset_multiple(accounts_key(account.id), &[("balance", 0), ("prepaid_amount", 0)]).ignore(); + // Set account details + pipe.cmd("HMSET") + .arg(accounts_key(account.id)) + .arg(encrypted) + .ignore(); - if account.should_send_routes() { - pipe.sadd("send_routes_to", RedisAccountId(account.id)).ignore(); - } + // Set balance-related details + pipe.hset_multiple( + accounts_key(account.id), + &[("balance", 0), ("prepaid_amount", 0)], + ) + .ignore(); - if account.should_receive_routes() { - pipe.sadd("receive_routes_from", RedisAccountId(account.id)).ignore(); - } + if account.should_send_routes() { + pipe.sadd("send_routes_to", RedisAccountId(account.id)) + .ignore(); + } - if account.ilp_over_btp_url.is_some() { - pipe.sadd("btp_outgoing", RedisAccountId(account.id)).ignore(); - } + if account.should_receive_routes() { + pipe.sadd("receive_routes_from", RedisAccountId(account.id)) + .ignore(); + } - // Add route to routing table - pipe.hset(ROUTES_KEY, account.ilp_address.to_bytes().to_vec(), RedisAccountId(account.id)) - .ignore(); - - // The parent account settings are done via the API. We just - // had to check for the existence of a parent - pipe.query_async(connection) - .map_err(|err| error!("Error inserting account into DB: {:?}", err)) - .and_then(move |(connection, _ret): (RedisReconnect, Value)| { - update_routes(connection, routing_table) - }) - .and_then(move |_| { - debug!("Inserted account {} (ILP address: {})", account.id, account.ilp_address); - Ok(ret) - }) - })) - } - - fn redis_update_account( + if account.ilp_over_btp_url.is_some() { + pipe.sadd("btp_outgoing", RedisAccountId(account.id)) + .ignore(); + } + + // Add route to routing table + pipe.hset( + ROUTES_KEY, + account.ilp_address.to_bytes().to_vec(), + RedisAccountId(account.id), + ) + .ignore(); + + // The parent account settings are done via the API. We just + // had to check for the existence of a parent + pipe.query_async(&mut connection) + .map_err(|err| error!("Error inserting account into DB: {:?}", err)) + .await?; + + update_routes(connection, routing_table).await?; + debug!( + "Inserted account {} (ILP address: {})", + account.id, account.ilp_address + ); + Ok(ret) + } + + /// Overwrites the account corresponding to the provided `AccountWithEncryptedtokens` + /// in Redis. Returns the provided account (tokens remain encrypted) + async fn redis_update_account( &self, encrypted: AccountWithEncryptedTokens, - ) -> Box + Send> { + ) -> Result { let account = encrypted.account.clone(); - let connection = self.connection.clone(); + let mut connection = self.connection.clone(); let routing_table = self.routes.clone(); - Box::new( - // Check to make sure an account with this ID already exists - redis_crate::cmd("EXISTS") - .arg(accounts_key(account.id)) - // TODO this needs to be atomic with the insertions later, - // waiting on #186 - // TODO: Do not allow this update to happen if - // AccountDetails.RoutingRelation == Parent and parent is - // already set - .query_async(connection.clone()) - .map_err(|err| error!("Error checking whether ID exists: {:?}", err)) - .and_then(move |(connection, exists): (RedisReconnect, bool)| { - if !exists { - warn!( - "No account exists with ID {}, cannot update account {:?}", - account.id, account - ); - return Either::A(err(())); - } - let mut pipe = redis_crate::pipe(); - pipe.atomic(); - // Add the account key to the list of accounts - pipe.sadd("accounts", RedisAccountId(account.id)).ignore(); + // Check to make sure an account with this ID already exists + // TODO this needs to be atomic with the insertions later, + // waiting on #186 + // TODO: Do not allow this update to happen if + // AccountDetails.RoutingRelation == Parent and parent is + // already set + let exists: bool = connection + .exists(accounts_key(account.id)) + .map_err(|err| error!("Error checking whether ID exists: {:?}", err)) + .await?; + + if !exists { + warn!( + "No account exists with ID {}, cannot update account {:?}", + account.id, account + ); + return Err(()); + } + let mut pipe = redis_crate::pipe(); + pipe.atomic(); - // Set account details - pipe.cmd("HMSET") - .arg(accounts_key(account.id)) - .arg(encrypted.clone()) - .ignore(); + // Add the account key to the list of accounts + pipe.sadd("accounts", RedisAccountId(account.id)).ignore(); - if account.should_send_routes() { - pipe.sadd("send_routes_to", RedisAccountId(account.id)) - .ignore(); - } + // Set account details + pipe.cmd("HMSET") + .arg(accounts_key(account.id)) + .arg(encrypted.clone()) + .ignore(); - if account.should_receive_routes() { - pipe.sadd("receive_routes_from", RedisAccountId(account.id)) - .ignore(); - } + if account.should_send_routes() { + pipe.sadd("send_routes_to", RedisAccountId(account.id)) + .ignore(); + } - if account.ilp_over_btp_url.is_some() { - pipe.sadd("btp_outgoing", RedisAccountId(account.id)) - .ignore(); - } + if account.should_receive_routes() { + pipe.sadd("receive_routes_from", RedisAccountId(account.id)) + .ignore(); + } - // Add route to routing table - pipe.hset( - ROUTES_KEY, - account.ilp_address.to_bytes().to_vec(), - RedisAccountId(account.id), - ) - .ignore(); - - Either::B( - pipe.query_async(connection) - .map_err(|err| error!("Error inserting account into DB: {:?}", err)) - .and_then(move |(connection, _ret): (RedisReconnect, Value)| { - update_routes(connection, routing_table) - }) - .and_then(move |_| { - debug!( - "Inserted account {} (id: {}, ILP address: {})", - account.username, account.id, account.ilp_address - ); - Ok(encrypted) - }), - ) - }), + if account.ilp_over_btp_url.is_some() { + pipe.sadd("btp_outgoing", RedisAccountId(account.id)) + .ignore(); + } + + // Add route to routing table + pipe.hset( + ROUTES_KEY, + account.ilp_address.to_bytes().to_vec(), + RedisAccountId(account.id), ) + .ignore(); + + pipe.query_async(&mut connection) + .map_err(|err| error!("Error inserting account into DB: {:?}", err)) + .await?; + update_routes(connection, routing_table).await?; + debug!( + "Inserted account {} (id: {}, ILP address: {})", + account.username, account.id, account.ilp_address + ); + Ok(encrypted) } - fn redis_modify_account( + /// Modifies the account corresponding to the provided `id` with the provided `settings` + /// in Redis. Returns the modified account (tokens remain encrypted) + async fn redis_modify_account( &self, id: Uuid, settings: EncryptedAccountSettings, - ) -> Box + Send> { + ) -> Result { let connection = self.connection.clone(); - let self_clone = self.clone(); + let mut self_clone = self.clone(); let mut pipe = redis_crate::pipe(); pipe.atomic(); @@ -521,135 +558,116 @@ impl RedisStore { pipe.hset(accounts_key(id), "settle_to", settle_to); } - Box::new( - pipe.query_async(connection.clone()) - .map_err(|err| error!("Error modifying user account: {:?}", err)) - .and_then(move |(_connection, _ret): (RedisReconnect, Value)| { - // return the updated account - self_clone.redis_get_account(id) - }), - ) + pipe.query_async(&mut connection.clone()) + .map_err(|err| error!("Error modifying user account: {:?}", err)) + .await?; + + // return the updated account + self_clone.redis_get_account(id).await } - fn redis_get_account( - &self, - id: Uuid, - ) -> Box + Send> { - Box::new( - LOAD_ACCOUNTS - .arg(id.to_string()) - .invoke_async(self.connection.clone()) - .map_err(|err| error!("Error loading accounts: {:?}", err)) - .and_then(|(_, mut accounts): (_, Vec)| { - accounts.pop().ok_or(()) - }), - ) + /// Gets the account (tokens remain encrypted) corresponding to the provided `id` from Redis. + async fn redis_get_account(&mut self, id: Uuid) -> Result { + let mut accounts: Vec = LOAD_ACCOUNTS + .arg(id.to_string()) + .invoke_async(&mut self.connection.clone()) + .map_err(|err| error!("Error loading accounts: {:?}", err)) + .await?; + accounts.pop().ok_or(()) } - fn redis_delete_account( - &self, - id: Uuid, - ) -> Box + Send> { - let connection = self.connection.clone(); + /// Deletes the account corresponding to the provided `id` from Redis. + /// Returns the deleted account (tokens remain encrypted) + async fn redis_delete_account(&mut self, id: Uuid) -> Result { + let mut connection = self.connection.clone(); let routing_table = self.routes.clone(); - Box::new(self.redis_get_account(id).and_then(move |encrypted| { - let account = encrypted.account.clone(); - let mut pipe = redis_crate::pipe(); - pipe.atomic(); - - pipe.srem("accounts", RedisAccountId(account.id)).ignore(); + let encrypted = self.redis_get_account(id).await?; + let account = encrypted.account.clone(); + let mut pipe = redis_crate::pipe(); + pipe.atomic(); - pipe.del(accounts_key(account.id)).ignore(); - pipe.hdel("usernames", account.username().as_ref()).ignore(); + pipe.srem("accounts", RedisAccountId(account.id)).ignore(); - if account.should_send_routes() { - pipe.srem("send_routes_to", RedisAccountId(account.id)) - .ignore(); - } + pipe.del(accounts_key(account.id)).ignore(); + pipe.hdel("usernames", account.username().as_ref()).ignore(); - if account.should_receive_routes() { - pipe.srem("receive_routes_from", RedisAccountId(account.id)) - .ignore(); - } + if account.should_send_routes() { + pipe.srem("send_routes_to", RedisAccountId(account.id)) + .ignore(); + } - if account.ilp_over_btp_url.is_some() { - pipe.srem("btp_outgoing", RedisAccountId(account.id)) - .ignore(); - } + if account.should_receive_routes() { + pipe.srem("receive_routes_from", RedisAccountId(account.id)) + .ignore(); + } - pipe.hdel(ROUTES_KEY, account.ilp_address.to_bytes().to_vec()) + if account.ilp_over_btp_url.is_some() { + pipe.srem("btp_outgoing", RedisAccountId(account.id)) .ignore(); + } - pipe.del(uncredited_amount_key(id)); + pipe.hdel(ROUTES_KEY, account.ilp_address.to_bytes().to_vec()) + .ignore(); - pipe.query_async(connection) - .map_err(|err| error!("Error deleting account from DB: {:?}", err)) - .and_then(move |(connection, _ret): (RedisReconnect, Value)| { - update_routes(connection, routing_table) - }) - .and_then(move |_| { - debug!("Deleted account {}", account.id); - Ok(encrypted) - }) - })) + pipe.del(uncredited_amount_key(id)); + + pipe.query_async(&mut connection) + .map_err(|err| error!("Error deleting account from DB: {:?}", err)) + .await?; + + update_routes(connection, routing_table).await?; + debug!("Deleted account {}", account.id); + Ok(encrypted) } } +#[async_trait] impl AccountStore for RedisStore { type Account = Account; // TODO cache results to avoid hitting Redis for each packet - fn get_accounts( - &self, - account_ids: Vec, - ) -> Box, Error = ()> + Send> { + async fn get_accounts(&self, account_ids: Vec) -> Result, ()> { let decryption_key = self.decryption_key.clone(); let num_accounts = account_ids.len(); let mut script = LOAD_ACCOUNTS.prepare_invoke(); for id in account_ids.iter() { script.arg(id.to_string()); } - Box::new( - script - .invoke_async(self.connection.clone()) - .map_err(|err| error!("Error loading accounts: {:?}", err)) - .and_then(move |(_, accounts): (_, Vec)| { - if accounts.len() == num_accounts { - let accounts = accounts - .into_iter() - .map(|account| { - account.decrypt_tokens(&decryption_key.expose_secret().0) - }) - .collect(); - Ok(accounts) - } else { - Err(()) - } - }), - ) + + // Need to clone the connection here to avoid lifetime errors + let connection = self.connection.clone(); + let accounts: Vec = script + .invoke_async(&mut connection.clone()) + .map_err(|err| error!("Error loading accounts: {:?}", err)) + .await?; + + // Decrypt the accounts. TODO: This functionality should be + // decoupled from redis so that it gets reused by the other backends + if accounts.len() == num_accounts { + let accounts = accounts + .into_iter() + .map(|account| account.decrypt_tokens(&decryption_key.expose_secret().0)) + .collect(); + Ok(accounts) + } else { + Err(()) + } } - fn get_account_id_from_username( - &self, - username: &Username, - ) -> Box + Send> { + async fn get_account_id_from_username(&self, username: &Username) -> Result { let username = username.clone(); - Box::new( - cmd("HGET") - .arg("usernames") - .arg(username.as_ref()) - .query_async(self.connection.clone()) - .map_err(move |err| error!("Error getting account id: {:?}", err)) - .and_then( - move |(_connection, id): (_, Option)| match id { - Some(rid) => Ok(rid.0), - None => { - debug!("Username not found: {}", username); - Err(()) - } - }, - ), - ) + let mut connection = self.connection.clone(); + let id: Option = connection + .hget("usernames", username.as_ref()) + .map_err(move |err| error!("Error getting account id: {:?}", err)) + .await?; + match id { + Some(rid) => Ok(rid.0), + None => { + debug!("Username not found: {}", username); + Err(()) + } + } } } @@ -668,147 +686,146 @@ impl StreamNotificationsStore for RedisStore { fn publish_payment_notification(&self, payment: PaymentNotification) { let username = payment.to_username.clone(); let message = serde_json::to_string(&payment).unwrap(); - let connection = self.connection.clone(); - spawn( - self.get_account_id_from_username(&username) - .map_err(move |_| { + let mut connection = self.connection.clone(); + let self_clone = self.clone(); + tokio::spawn(async move { + let account_id = self_clone + .get_account_id_from_username(&username) + .map_err(|_| { error!( "Failed to find account ID corresponding to username: {}", username ) }) - .and_then(move |account_id| { - debug!( - "Publishing payment notification {} for account {}", - message, account_id - ); - redis_crate::cmd("PUBLISH") - .arg(format!("{}{}", STREAM_NOTIFICATIONS_PREFIX, account_id)) - .arg(message) - .query_async(connection) - .map_err(move |err| error!("Error publish message to Redis: {:?}", err)) - .and_then(move |(_, _): (_, i32)| Ok(())) - }), - ); + .await?; + + debug!( + "Publishing payment notification {} for account {}", + message, account_id + ); + // https://github.com/rust-lang/rust/issues/64960#issuecomment-544219926 + let published_args = format!("{}{}", STREAM_NOTIFICATIONS_PREFIX, account_id.clone()); + redis_crate::cmd("PUBLISH") + .arg(published_args) + .arg(message) + .query_async(&mut connection) + .map_err(move |err| error!("Error publish message to Redis: {:?}", err)) + .await?; + + Ok::<(), ()>(()) + }); } } +#[async_trait] impl BalanceStore for RedisStore { /// Returns the balance **from the account holder's perspective**, meaning the sum of /// the Payable Balance and Pending Outgoing minus the Receivable Balance and the Pending Incoming. - fn get_balance(&self, account: Account) -> Box + Send> { - Box::new( - cmd("HMGET") - .arg(accounts_key(account.id)) - .arg(&["balance", "prepaid_amount"]) - .query_async(self.connection.clone()) - .map_err(move |err| { - error!( - "Error getting balance for account: {} {:?}", - account.id, err - ) - }) - .and_then(|(_connection, values): (_, Vec)| { - let balance = values[0]; - let prepaid_amount = values[1]; - Ok(balance + prepaid_amount) - }), - ) + async fn get_balance(&self, account: Account) -> Result { + let mut connection = self.connection.clone(); + let values: Vec = connection + .hget(accounts_key(account.id), &["balance", "prepaid_amount"]) + .map_err(move |err| { + error!( + "Error getting balance for account: {} {:?}", + account.id, err + ) + }) + .await?; + + let balance = values[0]; + let prepaid_amount = values[1]; + Ok(balance + prepaid_amount) } - fn update_balances_for_prepare( + async fn update_balances_for_prepare( &self, from_account: Account, // TODO: Make this take only the id incoming_amount: u64, - ) -> Box + Send> { - if incoming_amount > 0 { - let from_account_id = from_account.id; - Box::new( - PROCESS_PREPARE - .arg(RedisAccountId(from_account_id)) - .arg(incoming_amount) - .invoke_async(self.connection.clone()) - .map_err(move |err| { - warn!( - "Error handling prepare from account: {}: {:?}", - from_account_id, err - ) - }) - .and_then(move |(_connection, balance): (_, i64)| { - trace!( - "Processed prepare with incoming amount: {}. Account {} has balance (including prepaid amount): {} ", - incoming_amount, from_account_id, balance - ); - Ok(()) - }), - ) - } else { - Box::new(ok(())) + ) -> Result<(), ()> { + // Don't do anything if the amount was 0 + if incoming_amount == 0 { + return Ok(()); } + + let from_account_id = from_account.id; + let balance: i64 = PROCESS_PREPARE + .arg(RedisAccountId(from_account_id)) + .arg(incoming_amount) + .invoke_async(&mut self.connection.clone()) + .map_err(move |err| { + warn!( + "Error handling prepare from account: {}: {:?}", + from_account_id, err + ) + }) + .await?; + + trace!( + "Processed prepare with incoming amount: {}. Account {} has balance (including prepaid amount): {} ", + incoming_amount, from_account_id, balance + ); + Ok(()) } - fn update_balances_for_fulfill( + async fn update_balances_for_fulfill( &self, to_account: Account, // TODO: Make this take only the id outgoing_amount: u64, - ) -> Box + Send> { - if outgoing_amount > 0 { - let to_account_id = to_account.id; - Box::new( - PROCESS_FULFILL - .arg(RedisAccountId(to_account_id)) - .arg(outgoing_amount) - .invoke_async(self.connection.clone()) - .map_err(move |err| { - error!( - "Error handling Fulfill received from account: {}: {:?}", - to_account_id, err - ) - }) - .and_then(move |(_connection, (balance, amount_to_settle)): (_, (i64, u64))| { - trace!("Processed fulfill for account {} for outgoing amount {}. Fulfill call result: {} {}", - to_account_id, - outgoing_amount, - balance, - amount_to_settle, - ); - Ok((balance, amount_to_settle)) - }) - ) - } else { - Box::new(ok((0, 0))) + ) -> Result<(i64, u64), ()> { + if outgoing_amount == 0 { + return Ok((0, 0)); } + let to_account_id = to_account.id; + let (balance, amount_to_settle): (i64, u64) = PROCESS_FULFILL + .arg(RedisAccountId(to_account_id)) + .arg(outgoing_amount) + .invoke_async(&mut self.connection.clone()) + .map_err(move |err| { + error!( + "Error handling Fulfill received from account: {}: {:?}", + to_account_id, err + ) + }) + .await?; + + trace!( + "Processed fulfill for account {} for outgoing amount {}. Fulfill call result: {} {}", + to_account_id, + outgoing_amount, + balance, + amount_to_settle, + ); + Ok((balance, amount_to_settle)) } - fn update_balances_for_reject( + async fn update_balances_for_reject( &self, from_account: Account, // TODO: Make this take only the id incoming_amount: u64, - ) -> Box + Send> { - if incoming_amount > 0 { - let from_account_id = from_account.id; - Box::new( - PROCESS_REJECT - .arg(RedisAccountId(from_account_id)) - .arg(incoming_amount) - .invoke_async(self.connection.clone()) - .map_err(move |err| { - warn!( - "Error handling reject for packet from account: {}: {:?}", - from_account_id, err - ) - }) - .and_then(move |(_connection, balance): (_, i64)| { - trace!( - "Processed reject for incoming amount: {}. Account {} has balance (including prepaid amount): {}", - incoming_amount, from_account_id, balance - ); - Ok(()) - }), - ) - } else { - Box::new(ok(())) + ) -> Result<(), ()> { + if incoming_amount == 0 { + return Ok(()); } + + let from_account_id = from_account.id; + let balance: i64 = PROCESS_REJECT + .arg(RedisAccountId(from_account_id)) + .arg(incoming_amount) + .invoke_async(&mut self.connection.clone()) + .map_err(move |err| { + warn!( + "Error handling reject for packet from account: {}: {:?}", + from_account_id, err + ) + }) + .await?; + + trace!( + "Processed reject for incoming amount: {}. Account {} has balance (including prepaid amount): {}", + incoming_amount, from_account_id, balance + ); + Ok(()) } } @@ -816,11 +833,7 @@ impl ExchangeRateStore for RedisStore { fn get_exchange_rates(&self, asset_codes: &[&str]) -> Result, ()> { let rates: Vec = asset_codes .iter() - .filter_map(|code| { - (*self.exchange_rates.read()) - .get(&code.to_string()) - .cloned() - }) + .filter_map(|code| (*self.exchange_rates.read()).get(*code).cloned()) .collect(); if rates.len() == asset_codes.len() { Ok(rates) @@ -840,145 +853,109 @@ impl ExchangeRateStore for RedisStore { } } +#[async_trait] impl BtpStore for RedisStore { type Account = Account; - fn get_account_from_btp_auth( + async fn get_account_from_btp_auth( &self, username: &Username, token: &str, - ) -> Box + Send> { + ) -> Result { // TODO make sure it can't do script injection! // TODO cache the result so we don't hit redis for every packet (is that // necessary if redis is often used as a cache?) let decryption_key = self.decryption_key.clone(); + let mut connection = self.connection.clone(); let token = token.to_owned(); - Box::new( - ACCOUNT_FROM_USERNAME - .arg(username.as_ref()) - .invoke_async(self.connection.clone()) - .map_err(|err| error!("Error getting account from BTP token: {:?}", err)) - .and_then( - move |(_connection, account): (_, Option)| { - if let Some(account) = account { - let account = account.decrypt_tokens(&decryption_key.expose_secret().0); - if let Some(t) = account.ilp_over_btp_incoming_token.clone() { - let t = t.expose_secret().clone(); - if t == Bytes::from(token) { - Ok(account) - } else { - debug!( - "Found account {} but BTP auth token was wrong", - account.username - ); - Err(()) - } - } else { - debug!( - "Account {} does not have an incoming btp token configured", - account.username - ); - Err(()) - } - } else { - warn!("No account found with BTP token"); - Err(()) - } - }, - ), - ) + let username = username.to_owned(); // TODO: Can we avoid taking ownership? + + let account: Option = ACCOUNT_FROM_USERNAME + .arg(username.as_ref()) + .invoke_async(&mut connection) + .map_err(|err| error!("Error getting account from BTP token: {:?}", err)) + .await?; + + if let Some(account) = account { + let account = account.decrypt_tokens(&decryption_key.expose_secret().0); + if let Some(t) = account.ilp_over_btp_incoming_token.clone() { + let t = t.expose_secret().clone(); + if t == Bytes::from(token) { + Ok(account) + } else { + debug!( + "Found account {} but BTP auth token was wrong", + account.username + ); + Err(()) + } + } else { + debug!( + "Account {} does not have an incoming btp token configured", + account.username + ); + Err(()) + } + } else { + warn!("No account found with BTP token"); + Err(()) + } } - fn get_btp_outgoing_accounts( - &self, - ) -> Box, Error = ()> + Send> { - let decryption_key = self.decryption_key.clone(); - Box::new( - cmd("SMEMBERS") - .arg("btp_outgoing") - .query_async(self.connection.clone()) - .map_err(|err| error!("Error getting members of set btp_outgoing: {:?}", err)) - .and_then( - move |(connection, account_ids): (RedisReconnect, Vec)| { - if account_ids.is_empty() { - Either::A(ok(Vec::new())) - } else { - let mut script = LOAD_ACCOUNTS.prepare_invoke(); - for id in account_ids.iter() { - script.arg(id.to_string()); - } - Either::B( - script - .invoke_async(connection.clone()) - .map_err(|err| { - error!( - "Error getting accounts with outgoing BTP details: {:?}", - err - ) - }) - .and_then( - move |(_connection, accounts): ( - RedisReconnect, - Vec, - )| { - let accounts: Vec = accounts - .into_iter() - .map(|account| { - account.decrypt_tokens( - &decryption_key.expose_secret().0, - ) - }) - .collect(); - Ok(accounts) - }, - ), - ) - } - }, - ), - ) + async fn get_btp_outgoing_accounts(&self) -> Result, ()> { + let mut connection = self.connection.clone(); + + let account_ids: Vec = connection + .smembers("btp_outgoing") + .map_err(|err| error!("Error getting members of set btp_outgoing: {:?}", err)) + .await?; + let account_ids: Vec = account_ids.into_iter().map(|id| id.0).collect(); + + if account_ids.is_empty() { + return Ok(Vec::new()); + } + + let accounts = self.get_accounts(account_ids).await?; + Ok(accounts) } } +#[async_trait] impl HttpStore for RedisStore { type Account = Account; /// Checks if the stored token for the provided account id matches the /// provided token, and if so, returns the account associated with that token - fn get_account_from_http_auth( + async fn get_account_from_http_auth( &self, username: &Username, token: &str, - ) -> Box + Send> { + ) -> Result { // TODO make sure it can't do script injection! let decryption_key = self.decryption_key.clone(); let token = token.to_owned(); - Box::new( - ACCOUNT_FROM_USERNAME - .arg(username.as_ref()) - .invoke_async(self.connection.clone()) - .map_err(|err| error!("Error getting account from HTTP auth: {:?}", err)) - .and_then( - move |(_connection, account): (_, Option)| { - if let Some(account) = account { - let account = account.decrypt_tokens(&decryption_key.expose_secret().0); - if let Some(t) = account.ilp_over_http_incoming_token.clone() { - let t = t.expose_secret().clone(); - if t == Bytes::from(token) { - Ok(account) - } else { - Err(()) - } - } else { - Err(()) - } - } else { - warn!("No account found with given HTTP auth"); - Err(()) - } - }, - ), - ) + let account: Option = ACCOUNT_FROM_USERNAME + .arg(username.as_ref()) + .invoke_async(&mut self.connection.clone()) + .map_err(|err| error!("Error getting account from HTTP auth: {:?}", err)) + .await?; + + if let Some(account) = account { + let account = account.decrypt_tokens(&decryption_key.expose_secret().0); + if let Some(t) = account.ilp_over_http_incoming_token.clone() { + let t = t.expose_secret().clone(); + if t == Bytes::from(token) { + Ok(account) + } else { + Err(()) + } + } else { + Err(()) + } + } else { + warn!("No account found with given HTTP auth"); + Err(()) + } } } @@ -988,19 +965,14 @@ impl RouterStore for RedisStore { } } +#[async_trait] impl NodeStore for RedisStore { type Account = Account; - fn insert_account( - &self, - account: AccountDetails, - ) -> Box + Send> { + async fn insert_account(&self, account: AccountDetails) -> Result { let encryption_key = self.encryption_key.clone(); let id = Uuid::new_v4(); - let account = match Account::try_from(id, account, self.get_ilp_address()) { - Ok(account) => account, - Err(_) => return Box::new(err(())), - }; + let account = Account::try_from(id, account, self.get_ilp_address())?; debug!( "Generated account id for {}: {}", account.username.clone(), @@ -1009,32 +981,24 @@ impl NodeStore for RedisStore { let encrypted = account .clone() .encrypt_tokens(&encryption_key.expose_secret().0); - Box::new( - self.redis_insert_account(encrypted) - .and_then(move |_| Ok(account)), - ) + let mut self_clone = self.clone(); + + self_clone.redis_insert_account(encrypted).await?; + Ok(account) } - fn delete_account(&self, id: Uuid) -> Box + Send> { + async fn delete_account(&self, id: Uuid) -> Result { let decryption_key = self.decryption_key.clone(); - Box::new( - self.redis_delete_account(id).and_then(move |account| { - Ok(account.decrypt_tokens(&decryption_key.expose_secret().0)) - }), - ) + let mut self_clone = self.clone(); + let account = self_clone.redis_delete_account(id).await?; + Ok(account.decrypt_tokens(&decryption_key.expose_secret().0)) } - fn update_account( - &self, - id: Uuid, - account: AccountDetails, - ) -> Box + Send> { + async fn update_account(&self, id: Uuid, account: AccountDetails) -> Result { let encryption_key = self.encryption_key.clone(); let decryption_key = self.decryption_key.clone(); - let account = match Account::try_from(id, account, self.get_ilp_address()) { - Ok(account) => account, - Err(_) => return Box::new(err(())), - }; + let account = Account::try_from(id, account, self.get_ilp_address())?; + debug!( "Generated account id for {}: {}", account.username.clone(), @@ -1043,19 +1007,16 @@ impl NodeStore for RedisStore { let encrypted = account .clone() .encrypt_tokens(&encryption_key.expose_secret().0); - Box::new( - self.redis_update_account(encrypted) - .and_then(move |account| { - Ok(account.decrypt_tokens(&decryption_key.expose_secret().0)) - }), - ) + + let account = self.redis_update_account(encrypted).await?; + Ok(account.decrypt_tokens(&decryption_key.expose_secret().0)) } - fn modify_account_settings( + async fn modify_account_settings( &self, id: Uuid, settings: AccountSettings, - ) -> Box + Send> { + ) -> Result { let encryption_key = self.encryption_key.clone(); let decryption_key = self.decryption_key.clone(); let settings = EncryptedAccountSettings { @@ -1068,63 +1029,66 @@ impl NodeStore for RedisStore { &encryption_key.expose_secret().0, token.expose_secret().as_bytes(), ) + .freeze() }), ilp_over_http_incoming_token: settings.ilp_over_http_incoming_token.map(|token| { encrypt_token( &encryption_key.expose_secret().0, token.expose_secret().as_bytes(), ) + .freeze() }), ilp_over_btp_outgoing_token: settings.ilp_over_btp_outgoing_token.map(|token| { encrypt_token( &encryption_key.expose_secret().0, token.expose_secret().as_bytes(), ) + .freeze() }), ilp_over_http_outgoing_token: settings.ilp_over_http_outgoing_token.map(|token| { encrypt_token( &encryption_key.expose_secret().0, token.expose_secret().as_bytes(), ) + .freeze() }), }; - Box::new( - self.redis_modify_account(id, settings) - .and_then(move |account| { - Ok(account.decrypt_tokens(&decryption_key.expose_secret().0)) - }), - ) + let account = self.redis_modify_account(id, settings).await?; + Ok(account.decrypt_tokens(&decryption_key.expose_secret().0)) } // TODO limit the number of results and page through them - fn get_all_accounts(&self) -> Box, Error = ()> + Send> { + async fn get_all_accounts(&self) -> Result, ()> { let decryption_key = self.decryption_key.clone(); - let mut pipe = redis_crate::pipe(); - let connection = self.connection.clone(); - pipe.smembers("accounts"); - Box::new(self.get_all_accounts_ids().and_then(move |account_ids| { - let mut script = LOAD_ACCOUNTS.prepare_invoke(); - for id in account_ids.iter() { - script.arg(id.to_string()); - } - script - .invoke_async(connection.clone()) - .map_err(|err| error!("Error getting account ids: {:?}", err)) - .and_then(move |(_, accounts): (_, Vec)| { - let accounts: Vec = accounts - .into_iter() - .map(|account| account.decrypt_tokens(&decryption_key.expose_secret().0)) - .collect(); - Ok(accounts) - }) - })) + let mut connection = self.connection.clone(); + + let account_ids = self.get_all_accounts_ids().await?; + + let mut script = LOAD_ACCOUNTS.prepare_invoke(); + for id in account_ids.iter() { + script.arg(id.to_string()); + } + + let accounts: Vec = script + .invoke_async(&mut connection) + .map_err(|err| error!("Error getting account ids: {:?}", err)) + .await?; + + // TODO this should be refactored so that it gets reused in multiple backends + let accounts: Vec = accounts + .into_iter() + .map(|account| account.decrypt_tokens(&decryption_key.expose_secret().0)) + .collect(); + + Ok(accounts) } - fn set_static_routes(&self, routes: R) -> Box + Send> + async fn set_static_routes(&self, routes: R) -> Result<(), ()> where - R: IntoIterator, + R: IntoIterator + Send + 'async_trait, { + let mut connection = self.connection.clone(); let routes: Vec<(String, RedisAccountId)> = routes .into_iter() .map(|(s, id)| (s, RedisAccountId(id))) @@ -1137,246 +1101,228 @@ impl NodeStore for RedisStore { } let routing_table = self.routes.clone(); - Box::new(pipe.query_async(self.connection.clone()) - .map_err(|err| error!("Error checking if accounts exist while setting static routes: {:?}", err)) - .and_then(|(connection, accounts_exist): (RedisReconnect, Vec)| { - if accounts_exist.iter().all(|a| *a) { - Ok(connection) - } else { - error!("Error setting static routes because not all of the given accounts exist"); - Err(()) - } + + let accounts_exist: Vec = pipe + .query_async(&mut connection) + .map_err(|err| { + error!( + "Error checking if accounts exist while setting static routes: {:?}", + err + ) }) - .and_then(move |connection| { + .await?; + + if !accounts_exist.iter().all(|a| *a) { + error!("Error setting static routes because not all of the given accounts exist"); + return Err(()); + } + let mut pipe = redis_crate::pipe(); pipe.atomic() .del(STATIC_ROUTES_KEY) .ignore() .hset_multiple(STATIC_ROUTES_KEY, &routes) .ignore(); - pipe.query_async(connection) - .map_err(|err| error!("Error setting static routes: {:?}", err)) - .and_then(move |(connection, _): (RedisReconnect, Value)| { - update_routes(connection, routing_table) - }) - })) + + pipe.query_async(&mut connection) + .map_err(|err| error!("Error setting static routes: {:?}", err)) + .await?; + + update_routes(connection, routing_table).await?; + Ok(()) } - fn set_static_route( - &self, - prefix: String, - account_id: Uuid, - ) -> Box + Send> { + async fn set_static_route(&self, prefix: String, account_id: Uuid) -> Result<(), ()> { let routing_table = self.routes.clone(); let prefix_clone = prefix.clone(); - Box::new( - cmd("EXISTS") - .arg(accounts_key(account_id)) - .query_async(self.connection.clone()) - .map_err(|err| error!("Error checking if account exists before setting static route: {:?}", err)) - .and_then(move |(connection, exists): (RedisReconnect, bool)| { - if exists { - Ok(connection) - } else { - error!("Cannot set static route for prefix: {} because account {} does not exist", prefix_clone, account_id); - Err(()) - } - }) - .and_then(move |connection| { - cmd("HSET") - .arg(STATIC_ROUTES_KEY) - .arg(prefix) - .arg(RedisAccountId(account_id)) - .query_async(connection) - .map_err(|err| error!("Error setting static route: {:?}", err)) - .and_then(move |(connection, _): (RedisReconnect, Value)| { - update_routes(connection, routing_table) - }) + let mut connection = self.connection.clone(); + + let exists: bool = connection + .exists(accounts_key(account_id)) + .map_err(|err| { + error!( + "Error checking if account exists before setting static route: {:?}", + err + ) }) - ) + .await?; + if !exists { + error!( + "Cannot set static route for prefix: {} because account {} does not exist", + prefix_clone, account_id + ); + return Err(()); + } + + connection + .hset(STATIC_ROUTES_KEY, prefix, RedisAccountId(account_id)) + .map_err(|err| error!("Error setting static route: {:?}", err)) + .await?; + + update_routes(connection, routing_table).await?; + + Ok(()) } - fn set_default_route(&self, account_id: Uuid) -> Box + Send> { + async fn set_default_route(&self, account_id: Uuid) -> Result<(), ()> { let routing_table = self.routes.clone(); // TODO replace this with a lua script to do both calls at once - Box::new( - cmd("EXISTS") - .arg(accounts_key(account_id)) - .query_async(self.connection.clone()) - .map_err(|err| { - error!( - "Error checking if account exists before setting default route: {:?}", - err - ) - }) - .and_then(move |(connection, exists): (RedisReconnect, bool)| { - if exists { - Ok(connection) - } else { - error!( - "Cannot set default route because account {} does not exist", - account_id - ); - Err(()) - } - }) - .and_then(move |connection| { - cmd("SET") - .arg(DEFAULT_ROUTE_KEY) - .arg(RedisAccountId(account_id)) - .query_async(connection) - .map_err(|err| error!("Error setting default route: {:?}", err)) - .and_then(move |(connection, _): (RedisReconnect, Value)| { - debug!("Set default route to account id: {}", account_id); - update_routes(connection, routing_table) - }) - }), - ) + let mut connection = self.connection.clone(); + let exists: bool = connection + .exists(accounts_key(account_id)) + .map_err(|err| { + error!( + "Error checking if account exists before setting default route: {:?}", + err + ) + }) + .await?; + if !exists { + error!( + "Cannot set default route because account {} does not exist", + account_id + ); + return Err(()); + } + + connection + .set(DEFAULT_ROUTE_KEY, RedisAccountId(account_id)) + .map_err(|err| error!("Error setting default route: {:?}", err)) + .await?; + debug!("Set default route to account id: {}", account_id); + update_routes(connection, routing_table).await?; + Ok(()) } - fn set_settlement_engines( + async fn set_settlement_engines( &self, - asset_to_url_map: impl IntoIterator, - ) -> Box + Send> { + asset_to_url_map: impl IntoIterator + Send + 'async_trait, + ) -> Result<(), ()> { + let mut connection = self.connection.clone(); let asset_to_url_map: Vec<(String, String)> = asset_to_url_map .into_iter() .map(|(asset_code, url)| (asset_code, url.to_string())) .collect(); debug!("Setting settlement engines to {:?}", asset_to_url_map); - Box::new( - cmd("HMSET") - .arg(SETTLEMENT_ENGINES_KEY) - .arg(asset_to_url_map) - .query_async(self.connection.clone()) - .map_err(|err| error!("Error setting settlement engines: {:?}", err)) - .and_then(|(_, _): (RedisReconnect, Value)| Ok(())), - ) + connection + .hset_multiple(SETTLEMENT_ENGINES_KEY, &asset_to_url_map) + .map_err(|err| error!("Error setting settlement engines: {:?}", err)) + .await?; + Ok(()) } - fn get_asset_settlement_engine( - &self, - asset_code: &str, - ) -> Box, Error = ()> + Send> { - Box::new( - cmd("HGET") - .arg(SETTLEMENT_ENGINES_KEY) - .arg(asset_code) - .query_async(self.connection.clone()) - .map_err(|err| error!("Error getting settlement engine: {:?}", err)) - .map(|(_, url): (_, Option)| { - if let Some(url) = url { - Url::parse(url.as_str()) - .map_err(|err| { - error!( - "Settlement engine URL loaded from Redis was not a valid URL: {:?}", - err - ) - }) - .ok() - } else { - None - } - }), - ) + async fn get_asset_settlement_engine(&self, asset_code: &str) -> Result, ()> { + let mut connection = self.connection.clone(); + let asset_code = asset_code.to_owned(); + + let url: Option = connection + .hget(SETTLEMENT_ENGINES_KEY, asset_code) + .map_err(|err| error!("Error getting settlement engine: {:?}", err)) + .await?; + if let Some(url) = url { + match Url::parse(url.as_str()) { + Ok(url) => Ok(Some(url)), + Err(err) => { + error!( + "Settlement engine URL loaded from Redis was not a valid URL: {:?}", + err + ); + Err(()) + } + } + } else { + Ok(None) + } } } +#[async_trait] impl AddressStore for RedisStore { // Updates the ILP address of the store & iterates over all children and // updates their ILP Address to match the new address. - fn set_ilp_address( - &self, - ilp_address: Address, - ) -> Box + Send> { + async fn set_ilp_address(&self, ilp_address: Address) -> Result<(), ()> { debug!("Setting ILP address to: {}", ilp_address); let routing_table = self.routes.clone(); - let connection = self.connection.clone(); + let mut connection = self.connection.clone(); let ilp_address_clone = ilp_address.clone(); // Set the ILP address we have in memory (*self.ilp_address.write()) = ilp_address.clone(); // Save it to Redis - Box::new( - cmd("SET") - .arg(PARENT_ILP_KEY) - .arg(ilp_address.as_bytes()) - .query_async(self.connection.clone()) - .map_err(|err| error!("Error setting ILP address {:?}", err)) - .and_then(move |(_, _): (RedisReconnect, Value)| Ok(())) - .join(self.get_all_accounts().and_then(move |accounts| { - // TODO: This can be an expensive operation if this function - // gets called often. This currently only gets called when - // inserting a new parent account in the API. It'd be nice - // if we could generate a child's ILP address on the fly, - // instead of having to store the username appended to the - // node's ilp address. Currently this is not possible, as - // account.ilp_address() cannot access any state that exists - // on the store. - let mut pipe = redis_crate::pipe(); - for account in accounts { - // Update the address and routes of all children and non-routing accounts. - if account.routing_relation() != RoutingRelation::Parent - && account.routing_relation() != RoutingRelation::Peer - { - // remove the old route - pipe.hdel(ROUTES_KEY, &account.ilp_address as &str).ignore(); - - // if the username of the account ends with the - // node's address, we're already configured so no - // need to append anything. - let ilp_address_clone2 = ilp_address_clone.clone(); - // Note: We are assuming that if the node's address - // ends with the account's username, then this - // account represents the node's non routing - // account. Is this a reasonable assumption to make? - let new_ilp_address = - if ilp_address_clone2.segments().rev().next().unwrap() - == account.username().to_string() - { - ilp_address_clone2 - } else { - ilp_address_clone - .with_suffix(account.username().as_bytes()) - .unwrap() - }; - pipe.hset( - accounts_key(account.id()), - "ilp_address", - new_ilp_address.as_bytes(), - ) - .ignore(); - - pipe.hset( - ROUTES_KEY, - new_ilp_address.as_bytes(), - RedisAccountId(account.id()), - ) - .ignore(); - } - } - pipe.query_async(connection.clone()) - .map_err(|err| error!("Error updating children: {:?}", err)) - .and_then(move |(connection, _): (RedisReconnect, Value)| { - update_routes(connection, routing_table) - }) - })) - .and_then(move |_| Ok(())), - ) + connection + .set(PARENT_ILP_KEY, ilp_address.as_bytes()) + .map_err(|err| error!("Error setting ILP address {:?}", err)) + .await?; + + let accounts = self.get_all_accounts().await?; + // TODO: This can be an expensive operation if this function + // gets called often. This currently only gets called when + // inserting a new parent account in the API. It'd be nice + // if we could generate a child's ILP address on the fly, + // instead of having to store the username appended to the + // node's ilp address. Currently this is not possible, as + // account.ilp_address() cannot access any state that exists + // on the store. + let mut pipe = redis_crate::pipe(); + for account in accounts { + // Update the address and routes of all children and non-routing accounts. + if account.routing_relation() != RoutingRelation::Parent + && account.routing_relation() != RoutingRelation::Peer + { + // remove the old route + pipe.hdel(ROUTES_KEY, &account.ilp_address as &str).ignore(); + + // if the username of the account ends with the + // node's address, we're already configured so no + // need to append anything. + let ilp_address_clone2 = ilp_address_clone.clone(); + // Note: We are assuming that if the node's address + // ends with the account's username, then this + // account represents the node's non routing + // account. Is this a reasonable assumption to make? + let new_ilp_address = if ilp_address_clone2.segments().rev().next().unwrap() + == account.username().to_string() + { + ilp_address_clone2 + } else { + ilp_address_clone + .with_suffix(account.username().as_bytes()) + .unwrap() + }; + pipe.hset( + accounts_key(account.id()), + "ilp_address", + new_ilp_address.as_bytes(), + ) + .ignore(); + + pipe.hset( + ROUTES_KEY, + new_ilp_address.as_bytes(), + RedisAccountId(account.id()), + ) + .ignore(); + } + } + + pipe.query_async(&mut connection.clone()) + .map_err(|err| error!("Error updating children: {:?}", err)) + .await?; + update_routes(connection, routing_table).await?; + Ok(()) } - fn clear_ilp_address(&self) -> Box + Send> { - let self_clone = self.clone(); - Box::new( - cmd("DEL") - .arg(PARENT_ILP_KEY) - .query_async(self.connection.clone()) - .map_err(|err| error!("Error removing parent address: {:?}", err)) - .and_then(move |(_, _): (RedisReconnect, Value)| { - *(self_clone.ilp_address.write()) = DEFAULT_ILP_ADDRESS.clone(); - Ok(()) - }), - ) + async fn clear_ilp_address(&self) -> Result<(), ()> { + let mut connection = self.connection.clone(); + connection + .del(PARENT_ILP_KEY) + .map_err(|err| error!("Error removing parent address: {:?}", err)) + .await?; + + // overwrite the ilp address with the default value + *(self.ilp_address.write()) = DEFAULT_ILP_ADDRESS.clone(); + Ok(()) } fn get_ilp_address(&self) -> Address { @@ -1387,163 +1333,102 @@ impl AddressStore for RedisStore { type RoutingTable = HashMap; +#[async_trait] impl RouteManagerStore for RedisStore { type Account = Account; - fn get_accounts_to_send_routes_to( + async fn get_accounts_to_send_routes_to( &self, ignore_accounts: Vec, - ) -> Box, Error = ()> + Send> { - let decryption_key = self.decryption_key.clone(); - Box::new( - cmd("SMEMBERS") - .arg("send_routes_to") - .query_async(self.connection.clone()) - .map_err(|err| error!("Error getting members of set send_routes_to: {:?}", err)) - .and_then( - move |(connection, account_ids): (RedisReconnect, Vec)| { - if account_ids.is_empty() { - Either::A(ok(Vec::new())) - } else { - let mut script = LOAD_ACCOUNTS.prepare_invoke(); - for id in account_ids.iter() { - if !ignore_accounts.contains(&id.0) { - script.arg(id.to_string()); - } - } - Either::B( - script - .invoke_async(connection.clone()) - .map_err(|err| { - error!( - "Error getting accounts to send routes to: {:?}", - err - ) - }) - .and_then( - move |(_connection, accounts): ( - RedisReconnect, - Vec, - )| { - let accounts: Vec = accounts - .into_iter() - .map(|account| { - account.decrypt_tokens( - &decryption_key.expose_secret().0, - ) - }) - .collect(); - Ok(accounts) - }, - ), - ) - } - }, - ), - ) + ) -> Result, ()> { + let mut connection = self.connection.clone(); + + let account_ids: Vec = connection + .smembers("send_routes_to") + .map_err(|err| error!("Error getting members of set send_routes_to: {:?}", err)) + .await?; + let account_ids: Vec = account_ids + .into_iter() + .map(|id| id.0) + .filter(|id| !ignore_accounts.contains(&id)) + .collect(); + if account_ids.is_empty() { + return Ok(Vec::new()); + } + + let accounts = self.get_accounts(account_ids).await?; + Ok(accounts) } - fn get_accounts_to_receive_routes_from( - &self, - ) -> Box, Error = ()> + Send> { - let decryption_key = self.decryption_key.clone(); - Box::new( - cmd("SMEMBERS") - .arg("receive_routes_from") - .query_async(self.connection.clone()) - .map_err(|err| { - error!( - "Error getting members of set receive_routes_from: {:?}", - err - ) - }) - .and_then( - |(connection, account_ids): (RedisReconnect, Vec)| { - if account_ids.is_empty() { - Either::A(ok(Vec::new())) - } else { - let mut script = LOAD_ACCOUNTS.prepare_invoke(); - for id in account_ids.iter() { - script.arg(id.to_string()); - } - Either::B( - script - .invoke_async(connection.clone()) - .map_err(|err| { - error!( - "Error getting accounts to receive routes from: {:?}", - err - ) - }) - .and_then( - move |(_connection, accounts): ( - RedisReconnect, - Vec, - )| { - let accounts: Vec = accounts - .into_iter() - .map(|account| { - account.decrypt_tokens( - &decryption_key.expose_secret().0, - ) - }) - .collect(); - Ok(accounts) - }, - ), - ) - } - }, - ), - ) + async fn get_accounts_to_receive_routes_from(&self) -> Result, ()> { + let mut connection = self.connection.clone(); + let account_ids: Vec = connection + .smembers("receive_routes_from") + .map_err(|err| { + error!( + "Error getting members of set receive_routes_from: {:?}", + err + ) + }) + .await?; + let account_ids: Vec = account_ids.into_iter().map(|id| id.0).collect(); + + if account_ids.is_empty() { + return Ok(Vec::new()); + } + + let accounts = self.get_accounts(account_ids).await?; + Ok(accounts) } - fn get_local_and_configured_routes( + async fn get_local_and_configured_routes( &self, - ) -> Box, RoutingTable), Error = ()> + Send> - { - let get_static_routes = cmd("HGETALL") - .arg(STATIC_ROUTES_KEY) - .query_async(self.connection.clone()) + ) -> Result<(RoutingTable, RoutingTable), ()> { + let mut connection = self.connection.clone(); + let static_routes: Vec<(String, RedisAccountId)> = connection + .hgetall(STATIC_ROUTES_KEY) .map_err(|err| error!("Error getting static routes: {:?}", err)) - .and_then( - |(_, static_routes): (RedisReconnect, Vec<(String, RedisAccountId)>)| { - Ok(static_routes) - }, - ); - Box::new(self.get_all_accounts().join(get_static_routes).and_then( - |(accounts, static_routes)| { - let local_table = HashMap::from_iter( - accounts - .iter() - .map(|account| (account.ilp_address.to_string(), account.clone())), - ); + .await?; - let account_map: HashMap = HashMap::from_iter(accounts.iter().map(|account| (account.id, account))); - let configured_table: HashMap = HashMap::from_iter(static_routes.into_iter() - .filter_map(|(prefix, account_id)| { - if let Some(account) = account_map.get(&account_id.0) { - Some((prefix, (*account).clone())) - } else { - warn!("No account for ID: {}, ignoring configured route for prefix: {}", account_id, prefix); - None - } - })); + let accounts = self.get_all_accounts().await?; - Ok((local_table, configured_table)) - }, - )) + let local_table = HashMap::from_iter( + accounts + .iter() + .map(|account| (account.ilp_address.to_string(), account.clone())), + ); + + let account_map: HashMap = + HashMap::from_iter(accounts.iter().map(|account| (account.id, account))); + let configured_table: HashMap = HashMap::from_iter( + static_routes + .into_iter() + .filter_map(|(prefix, account_id)| { + if let Some(account) = account_map.get(&account_id.0) { + Some((prefix, (*account).clone())) + } else { + warn!( + "No account for ID: {}, ignoring configured route for prefix: {}", + account_id, prefix + ); + None + } + }), + ); + + Ok((local_table, configured_table)) } - fn set_routes( + async fn set_routes( &mut self, - routes: impl IntoIterator, - ) -> Box + Send> { + routes: impl IntoIterator + Send + 'async_trait, + ) -> Result<(), ()> { let routes: Vec<(String, RedisAccountId)> = routes .into_iter() .map(|(prefix, account)| (prefix, RedisAccountId(account.id))) .collect(); let num_routes = routes.len(); + let mut connection = self.connection.clone(); // Save routes to Redis let routing_tale = self.routes.clone(); @@ -1553,28 +1438,28 @@ impl RouteManagerStore for RedisStore { .ignore() .hset_multiple(ROUTES_KEY, &routes) .ignore(); - Box::new( - pipe.query_async(self.connection.clone()) - .map_err(|err| error!("Error setting routes: {:?}", err)) - .and_then(move |(connection, _): (RedisReconnect, Value)| { - trace!("Saved {} routes to Redis", num_routes); - update_routes(connection, routing_tale) - }), - ) + + pipe.query_async(&mut connection) + .map_err(|err| error!("Error setting routes: {:?}", err)) + .await?; + trace!("Saved {} routes to Redis", num_routes); + + update_routes(connection, routing_tale).await } } +#[async_trait] impl RateLimitStore for RedisStore { type Account = Account; /// Apply rate limits for number of packets per minute and amount of money per minute /// /// This uses https://github.com/brandur/redis-cell so the redis-cell module MUST be loaded into redis before this is run - fn apply_rate_limits( + async fn apply_rate_limits( &self, account: Account, prepare_amount: u64, - ) -> Box + Send> { + ) -> Result<(), RateLimitError> { if account.amount_per_minute_limit.is_some() || account.packets_per_minute_limit.is_some() { let mut pipe = redis_crate::pipe(); let packet_limit = account.packets_per_minute_limit.is_some(); @@ -1582,8 +1467,9 @@ impl RateLimitStore for RedisStore { if let Some(limit) = account.packets_per_minute_limit { let limit = limit - 1; + let packets_limit = format!("limit:packets:{}", account.id); pipe.cmd("CL.THROTTLE") - .arg(format!("limit:packets:{}", account.id)) + .arg(packets_limit) .arg(limit) .arg(limit) .arg(60) @@ -1592,113 +1478,115 @@ impl RateLimitStore for RedisStore { if let Some(limit) = account.amount_per_minute_limit { let limit = limit - 1; + let throughput_limit = format!("limit:throughput:{}", account.id); pipe.cmd("CL.THROTTLE") - .arg(format!("limit:throughput:{}", account.id)) + .arg(throughput_limit) // TODO allow separate configuration for burst limit .arg(limit) .arg(limit) .arg(60) .arg(prepare_amount); } - Box::new( - pipe.query_async(self.connection.clone()) - .map_err(|err| { - error!("Error applying rate limits: {:?}", err); - RateLimitError::StoreError - }) - .and_then(move |(_, results): (_, Vec>)| { - if packet_limit && amount_limit { - if results[0][0] == 1 { - Err(RateLimitError::PacketLimitExceeded) - } else if results[1][0] == 1 { - Err(RateLimitError::ThroughputLimitExceeded) - } else { - Ok(()) - } - } else if packet_limit && results[0][0] == 1 { - Err(RateLimitError::PacketLimitExceeded) - } else if amount_limit && results[0][0] == 1 { - Err(RateLimitError::ThroughputLimitExceeded) - } else { - Ok(()) - } - }), - ) + + let mut connection = self.connection.clone(); + let results: Vec> = pipe + .query_async(&mut connection) + .map_err(|err| { + error!("Error applying rate limits: {:?}", err); + RateLimitError::StoreError + }) + .await?; + + if packet_limit && amount_limit { + if results[0][0] == 1 { + Err(RateLimitError::PacketLimitExceeded) + } else if results[1][0] == 1 { + Err(RateLimitError::ThroughputLimitExceeded) + } else { + Ok(()) + } + } else if packet_limit && results[0][0] == 1 { + Err(RateLimitError::PacketLimitExceeded) + } else if amount_limit && results[0][0] == 1 { + Err(RateLimitError::ThroughputLimitExceeded) + } else { + Ok(()) + } } else { - Box::new(ok(())) + Ok(()) } } - fn refund_throughput_limit( + async fn refund_throughput_limit( &self, account: Account, prepare_amount: u64, - ) -> Box + Send> { + ) -> Result<(), ()> { if let Some(limit) = account.amount_per_minute_limit { + let mut connection = self.connection.clone(); let limit = limit - 1; - Box::new( - cmd("CL.THROTTLE") - .arg(format!("limit:throughput:{}", account.id)) - .arg(limit) - .arg(limit) - .arg(60) - // TODO make sure this doesn't overflow - .arg(0i64 - (prepare_amount as i64)) - .query_async(self.connection.clone()) - .map_err(|err| error!("Error refunding throughput limit: {:?}", err)) - .and_then(|(_, _): (_, Value)| Ok(())), - ) - } else { - Box::new(ok(())) + let throughput_limit = format!("limit:throughput:{}", account.id); + cmd("CL.THROTTLE") + .arg(throughput_limit) + .arg(limit) + .arg(limit) + .arg(60) + // TODO make sure this doesn't overflow + .arg(0i64 - (prepare_amount as i64)) + .query_async(&mut connection) + .map_err(|err| error!("Error refunding throughput limit: {:?}", err)) + .await?; } + + Ok(()) } } +#[async_trait] impl IdempotentStore for RedisStore { - fn load_idempotent_data( + async fn load_idempotent_data( &self, idempotency_key: String, - ) -> Box, Error = ()> + Send> { + ) -> Result, ()> { let idempotency_key_clone = idempotency_key.clone(); - Box::new( - cmd("HGETALL") - .arg(prefixed_idempotency_key(idempotency_key.clone())) - .query_async(self.connection.clone()) - .map_err(move |err| { - error!( - "Error loading idempotency key {}: {:?}", - idempotency_key_clone, err - ) - }) - .and_then(move |(_connection, ret): (_, HashMap)| { - if let (Some(status_code), Some(data), Some(input_hash_slice)) = ( - ret.get("status_code"), - ret.get("data"), - ret.get("input_hash"), - ) { - trace!("Loaded idempotency key {:?} - {:?}", idempotency_key, ret); - let mut input_hash: [u8; 32] = Default::default(); - input_hash.copy_from_slice(input_hash_slice.as_ref()); - Ok(Some(IdempotentData::new( - StatusCode::from_str(status_code).unwrap(), - Bytes::from(data.clone()), - input_hash, - ))) - } else { - Ok(None) - } - }), - ) + let mut connection = self.connection.clone(); + let ret: HashMap = connection + .hgetall(prefixed_idempotency_key(idempotency_key.clone())) + .map_err(move |err| { + error!( + "Error loading idempotency key {}: {:?}", + idempotency_key_clone, err + ) + }) + .await?; + + if let (Some(status_code), Some(data), Some(input_hash_slice)) = ( + ret.get("status_code"), + ret.get("data"), + ret.get("input_hash"), + ) { + trace!("Loaded idempotency key {:?} - {:?}", idempotency_key, ret); + let mut input_hash: [u8; 32] = Default::default(); + input_hash.copy_from_slice(input_hash_slice.as_ref()); + Ok(Some(IdempotentData::new( + StatusCode::from_str(status_code).unwrap(), + Bytes::from(data.clone()), + input_hash, + ))) + } else { + Ok(None) + } } - fn save_idempotent_data( + async fn save_idempotent_data( &self, idempotency_key: String, input_hash: [u8; 32], status_code: StatusCode, data: Bytes, - ) -> Box + Send> { + ) -> Result<(), ()> { let mut pipe = redis_crate::pipe(); + let mut connection = self.connection.clone(); pipe.atomic() .cmd("HMSET") // cannot use hset_multiple since data and status_code have different types .arg(&prefixed_idempotency_key(idempotency_key.clone())) @@ -1711,79 +1599,83 @@ impl IdempotentStore for RedisStore { .ignore() .expire(&prefixed_idempotency_key(idempotency_key.clone()), 86400) .ignore(); - Box::new( - pipe.query_async(self.connection.clone()) - .map_err(|err| error!("Error caching: {:?}", err)) - .and_then(move |(_connection, _): (_, Vec)| { - trace!( - "Cached {:?}: {:?}, {:?}", - idempotency_key, - status_code, - data, - ); - Ok(()) - }), - ) + pipe.query_async(&mut connection) + .map_err(|err| error!("Error caching: {:?}", err)) + .await?; + + trace!( + "Cached {:?}: {:?}, {:?}", + idempotency_key, + status_code, + data, + ); + Ok(()) } } +#[async_trait] impl SettlementStore for RedisStore { type Account = Account; - fn update_balance_for_incoming_settlement( + async fn update_balance_for_incoming_settlement( &self, account_id: Uuid, amount: u64, idempotency_key: Option, - ) -> Box + Send> { + ) -> Result<(), ()> { let idempotency_key = idempotency_key.unwrap(); - Box::new( - PROCESS_INCOMING_SETTLEMENT + let balance: i64 = PROCESS_INCOMING_SETTLEMENT .arg(RedisAccountId(account_id)) .arg(amount) .arg(idempotency_key) - .invoke_async(self.connection.clone()) - .map_err(move |err| error!("Error processing incoming settlement from account: {} for amount: {}: {:?}", account_id, amount, err)) - .and_then(move |(_connection, balance): (_, i64)| { - trace!("Processed incoming settlement from account: {} for amount: {}. Balance is now: {}", account_id, amount, balance); - Ok(()) - })) + .invoke_async(&mut self.connection.clone()) + .map_err(move |err| { + error!( + "Error processing incoming settlement from account: {} for amount: {}: {:?}", + account_id, amount, err + ) + }) + .await?; + trace!( + "Processed incoming settlement from account: {} for amount: {}. Balance is now: {}", + account_id, + amount, + balance + ); + Ok(()) } - fn refund_settlement( - &self, - account_id: Uuid, - settle_amount: u64, - ) -> Box + Send> { + async fn refund_settlement(&self, account_id: Uuid, settle_amount: u64) -> Result<(), ()> { trace!( "Refunding settlement for account: {} of amount: {}", account_id, settle_amount ); - Box::new( - REFUND_SETTLEMENT - .arg(RedisAccountId(account_id)) - .arg(settle_amount) - .invoke_async(self.connection.clone()) - .map_err(move |err| { - error!( - "Error refunding settlement for account: {} of amount: {}: {:?}", - account_id, settle_amount, err - ) - }) - .and_then(move |(_connection, balance): (_, i64)| { - trace!( - "Refunded settlement for account: {} of amount: {}. Balance is now: {}", - account_id, - settle_amount, - balance - ); - Ok(()) - }), - ) + let balance: i64 = REFUND_SETTLEMENT + .arg(RedisAccountId(account_id)) + .arg(settle_amount) + .invoke_async(&mut self.connection.clone()) + .map_err(move |err| { + error!( + "Error refunding settlement for account: {} of amount: {}: {:?}", + account_id, settle_amount, err + ) + }) + .await?; + + trace!( + "Refunded settlement for account: {} of amount: {}. Balance is now: {}", + account_id, + settle_amount, + balance + ); + Ok(()) } } +// TODO: AmountWithScale is re-implemented on Interledger-Settlement. It'd be nice +// if we could deduplicate this by extracting it to a separate crate which would make +// logical sense #[derive(Debug, Clone)] struct AmountWithScale { num: BigUint, @@ -1870,155 +1762,144 @@ impl FromRedisValue for AmountWithScale { } } +#[async_trait] impl LeftoversStore for RedisStore { type AccountId = Uuid; type AssetType = BigUint; - fn get_uncredited_settlement_amount( + async fn get_uncredited_settlement_amount( &self, account_id: Uuid, - ) -> Box + Send> { + ) -> Result<(Self::AssetType, u8), ()> { let mut pipe = redis_crate::pipe(); pipe.atomic(); // get the amounts and instantly delete them pipe.lrange(uncredited_amount_key(account_id.to_string()), 0, -1); pipe.del(uncredited_amount_key(account_id.to_string())) .ignore(); - Box::new( - pipe.query_async(self.connection.clone()) - .map_err(move |err| error!("Error getting uncredited_settlement_amount {:?}", err)) - .and_then(move |(_, amounts): (_, Vec)| { - // this call will only return 1 element - let amount = amounts[0].clone(); - Ok((amount.num, amount.scale)) - }), - ) + + let amounts: Vec = pipe + .query_async(&mut self.connection.clone()) + .map_err(move |err| error!("Error getting uncredited_settlement_amount {:?}", err)) + .await?; + + // this call will only return 1 element + let amount = amounts[0].clone(); + Ok((amount.num, amount.scale)) } - fn save_uncredited_settlement_amount( + async fn save_uncredited_settlement_amount( &self, account_id: Uuid, uncredited_settlement_amount: (Self::AssetType, u8), - ) -> Box + Send> { + ) -> Result<(), ()> { trace!( "Saving uncredited_settlement_amount {:?} {:?}", account_id, uncredited_settlement_amount ); - Box::new( - // We store these amounts as lists of strings - // because we cannot do BigNumber arithmetic in the store - // When loading the amounts, we convert them to the appropriate data - // type and sum them up. - cmd("RPUSH") - .arg(uncredited_amount_key(account_id)) - .arg(AmountWithScale { + // We store these amounts as lists of strings + // because we cannot do BigNumber arithmetic in the store + // When loading the amounts, we convert them to the appropriate data + // type and sum them up. + let mut connection = self.connection.clone(); + connection + .rpush( + uncredited_amount_key(account_id), + AmountWithScale { num: uncredited_settlement_amount.0, scale: uncredited_settlement_amount.1, - }) - .query_async(self.connection.clone()) - .map_err(move |err| error!("Error saving uncredited_settlement_amount: {:?}", err)) - .and_then(move |(_conn, _ret): (_, Value)| Ok(())), - ) + }, + ) + .map_err(move |err| error!("Error saving uncredited_settlement_amount: {:?}", err)) + .await?; + + Ok(()) } - fn load_uncredited_settlement_amount( + async fn load_uncredited_settlement_amount( &self, account_id: Uuid, local_scale: u8, - ) -> Box + Send> { - let connection = self.connection.clone(); + ) -> Result { + let mut connection = self.connection.clone(); trace!("Loading uncredited_settlement_amount {:?}", account_id); - Box::new( - self.get_uncredited_settlement_amount(account_id) - .and_then(move |amount| { - // scale the amount from the max scale to the local scale, and then - // save any potential leftovers to the store - let (scaled_amount, precision_loss) = - scale_with_precision_loss(amount.0, local_scale, amount.1); - if precision_loss > BigUint::from(0u32) { - Either::A( - cmd("RPUSH") - .arg(uncredited_amount_key(account_id)) - .arg(AmountWithScale { - num: precision_loss, - scale: std::cmp::max(local_scale, amount.1), - }) - .query_async(connection.clone()) - .map_err(move |err| { - error!("Error saving uncredited_settlement_amount: {:?}", err) - }) - .and_then(move |(_conn, _ret): (_, Value)| Ok(scaled_amount)), - ) - } else { - Either::B(ok(scaled_amount)) - } - }), - ) + let amount = self.get_uncredited_settlement_amount(account_id).await?; + // scale the amount from the max scale to the local scale, and then + // save any potential leftovers to the store + let (scaled_amount, precision_loss) = + scale_with_precision_loss(amount.0, local_scale, amount.1); + + if precision_loss > BigUint::from(0u32) { + connection + .rpush( + uncredited_amount_key(account_id), + AmountWithScale { + num: precision_loss, + scale: std::cmp::max(local_scale, amount.1), + }, + ) + .map_err(move |err| error!("Error saving uncredited_settlement_amount: {:?}", err)) + .await?; + } + + Ok(scaled_amount) } - fn clear_uncredited_settlement_amount( - &self, - account_id: Uuid, - ) -> Box + Send> { - trace!("Clearing uncredited_settlement_amount {:?}", account_id,); - Box::new( - cmd("DEL") - .arg(uncredited_amount_key(account_id)) - .query_async(self.connection.clone()) - .map_err(move |err| { - error!("Error clearing uncredited_settlement_amount: {:?}", err) - }) - .and_then(move |(_conn, _ret): (_, Value)| Ok(())), - ) + async fn clear_uncredited_settlement_amount(&self, account_id: Uuid) -> Result<(), ()> { + trace!("Clearing uncredited_settlement_amount {:?}", account_id); + let mut connection = self.connection.clone(); + connection + .del(uncredited_amount_key(account_id)) + .map_err(move |err| error!("Error clearing uncredited_settlement_amount: {:?}", err)) + .await?; + Ok(()) } } type RouteVec = Vec<(String, RedisAccountId)>; +use futures::future::TryFutureExt; + // TODO replace this with pubsub when async pubsub is added upstream: https://github.com/mitsuhiko/redis-rs/issues/183 -fn update_routes( - connection: RedisReconnect, +async fn update_routes( + mut connection: RedisReconnect, routing_table: Arc>>>, -) -> impl Future { +) -> Result<(), ()> { let mut pipe = redis_crate::pipe(); pipe.hgetall(ROUTES_KEY) .hgetall(STATIC_ROUTES_KEY) .get(DEFAULT_ROUTE_KEY); - pipe.query_async(connection) + let (routes, static_routes, default_route): (RouteVec, RouteVec, Option) = pipe + .query_async(&mut connection) .map_err(|err| error!("Error polling for routing table updates: {:?}", err)) - .and_then( - move |(_connection, (routes, static_routes, default_route)): ( - _, - (RouteVec, RouteVec, Option), - )| { - trace!( - "Loaded routes from redis. Static routes: {:?}, default route: {:?}, other routes: {:?}", - static_routes, - default_route, - routes - ); - // If there is a default route set in the db, - // set the entry for "" in the routing table to route to that account - let default_route_iter = iter::once(default_route) - .filter_map(|r| r) - .map(|rid| (String::new(), rid.0)); - let routes = HashMap::from_iter( - routes - .into_iter().map(|(s, rid)| (s, rid.0)) - // Include the default route if there is one - .chain(default_route_iter) - // Having the static_routes inserted after ensures that they will overwrite - // any routes with the same prefix from the first set - .chain(static_routes.into_iter().map(|(s, rid)| (s, rid.0))) - ); - // TODO we may not want to print this because the routing table will be very big - // if the node has a lot of local accounts - trace!("Routing table is: {:?}", routes); - *routing_table.write() = Arc::new(routes); - Ok(()) - }, - ) + .await?; + trace!( + "Loaded routes from redis. Static routes: {:?}, default route: {:?}, other routes: {:?}", + static_routes, + default_route, + routes + ); + // If there is a default route set in the db, + // set the entry for "" in the routing table to route to that account + let default_route_iter = iter::once(default_route) + .filter_map(|r| r) + .map(|rid| (String::new(), rid.0)); + let routes = HashMap::from_iter( + routes + .into_iter() + .map(|(s, rid)| (s, rid.0)) + // Include the default route if there is one + .chain(default_route_iter) + // Having the static_routes inserted after ensures that they will overwrite + // any routes with the same prefix from the first set + .chain(static_routes.into_iter().map(|(s, rid)| (s, rid.0))), + ); + // TODO we may not want to print this because the routing table will be very big + // if the node has a lot of local accounts + trace!("Routing table is: {:?}", routes); + *routing_table.write() = Arc::new(routes); + Ok(()) } // Uuid does not implement ToRedisArgs and FromRedisValue. @@ -2193,23 +2074,23 @@ impl FromRedisValue for AccountWithEncryptedTokens { "ilp_over_http_incoming_token", &hash, )? - .map(SecretBytes::from), + .map(SecretBytesMut::from), ilp_over_http_outgoing_token: get_bytes_option( "ilp_over_http_outgoing_token", &hash, )? - .map(SecretBytes::from), + .map(SecretBytesMut::from), ilp_over_btp_url: get_url_option("ilp_over_btp_url", &hash)?, ilp_over_btp_incoming_token: get_bytes_option( "ilp_over_btp_incoming_token", &hash, )? - .map(SecretBytes::from), + .map(SecretBytesMut::from), ilp_over_btp_outgoing_token: get_bytes_option( "ilp_over_btp_outgoing_token", &hash, )? - .map(SecretBytes::from), + .map(SecretBytesMut::from), max_packet_amount: get_value("max_packet_amount", &hash)?, min_balance: get_value_option("min_balance", &hash)?, settle_threshold: get_value_option("settle_threshold", &hash)?, @@ -2250,10 +2131,13 @@ where } } -fn get_bytes_option(key: &str, map: &HashMap) -> Result, RedisError> { +fn get_bytes_option( + key: &str, + map: &HashMap, +) -> Result, RedisError> { if let Some(ref value) = map.get(key) { let vec: Vec = from_redis_value(value)?; - Ok(Some(Bytes::from(vec))) + Ok(Some(BytesMut::from(vec.as_slice()))) } else { Ok(None) } @@ -2275,29 +2159,16 @@ fn get_url_option(key: &str, map: &HashMap) -> Result #[cfg(test)] mod tests { use super::*; - use futures::future; use redis_crate::IntoConnectionInfo; - use tokio::runtime::Runtime; - - #[test] - fn connect_fails_if_db_unavailable() { - let mut runtime = Runtime::new().unwrap(); - runtime - .block_on(future::lazy( - || -> Box + Send> { - Box::new( - RedisStoreBuilder::new( - "redis://127.0.0.1:0".into_connection_info().unwrap() as ConnectionInfo, - [0; 32], - ) - .connect() - .then(|result| { - assert!(result.is_err()); - Ok(()) - }), - ) - }, - )) - .unwrap(); + + #[tokio::test] + async fn connect_fails_if_db_unavailable() { + let result = RedisStoreBuilder::new( + "redis://127.0.0.1:0".into_connection_info().unwrap() as ConnectionInfo, + [0; 32], + ) + .connect() + .await; + assert!(result.is_err()); } } diff --git a/crates/interledger-store/src/redis/reconnect.rs b/crates/interledger-store/src/redis/reconnect.rs index ed9680927..735e0dca7 100644 --- a/crates/interledger-store/src/redis/reconnect.rs +++ b/crates/interledger-store/src/redis/reconnect.rs @@ -1,43 +1,53 @@ -use futures::{ - future::{err, result, Either}, - Future, -}; +use futures::future::{FutureExt, TryFutureExt}; use log::{debug, error}; use parking_lot::RwLock; use redis_crate::{ - aio::{ConnectionLike, SharedConnection}, - Client, ConnectionInfo, RedisError, Value, + aio::{ConnectionLike, MultiplexedConnection}, + Client, Cmd, ConnectionInfo, Pipeline, RedisError, RedisFuture, Value, }; use std::sync::Arc; -/// Wrapper around a Redis SharedConnection that automatically +type Result = std::result::Result; + +/// Wrapper around a Redis MultiplexedConnection that automatically /// attempts to reconnect to the DB if the connection is dropped #[derive(Clone)] pub struct RedisReconnect { pub(crate) redis_info: Arc, - pub(crate) conn: Arc>, + pub(crate) conn: Arc>, +} + +async fn get_shared_connection(redis_info: Arc) -> Result { + let client = Client::open((*redis_info).clone())?; + client + .get_multiplexed_tokio_connection() + .map_err(|e| { + error!("Error connecting to Redis: {:?}", e); + e + }) + .await } impl RedisReconnect { - pub fn connect( - redis_info: ConnectionInfo, - ) -> impl Future { + /// Connects to redis with the provided [`ConnectionInfo`](redis_crate::ConnectionInfo) + pub async fn connect(redis_info: ConnectionInfo) -> Result { let redis_info = Arc::new(redis_info); - get_shared_connection(redis_info.clone()).map(move |conn| RedisReconnect { + let conn = get_shared_connection(redis_info.clone()).await?; + Ok(RedisReconnect { conn: Arc::new(RwLock::new(conn)), redis_info, }) } - pub fn reconnect(self) -> impl Future { - get_shared_connection(self.redis_info.clone()).and_then(move |shared_connection| { - (*self.conn.write()) = shared_connection; - debug!("Reconnected to Redis"); - Ok(self) - }) + /// Reconnects to redis + pub async fn reconnect(self) -> Result { + let shared_connection = get_shared_connection(self.redis_info.clone()).await?; + (*self.conn.write()) = shared_connection; + debug!("Reconnected to Redis"); + Ok(self) } - fn get_shared_connection(&self) -> SharedConnection { + fn get_shared_connection(&self) -> MultiplexedConnection { self.conn.read().clone() } } @@ -47,56 +57,46 @@ impl ConnectionLike for RedisReconnect { self.conn.read().get_db() } - fn req_packed_command( - self, - cmd: Vec, - ) -> Box + Send> { - let clone = self.clone(); - Box::new( - self.get_shared_connection() - .req_packed_command(cmd) - .or_else(move |error| { + fn req_packed_command<'a>(&'a mut self, cmd: &'a Cmd) -> RedisFuture<'a, Value> { + // This is how it is implemented in the redis-rs repository + (async move { + let mut connection = self.get_shared_connection(); + match connection.req_packed_command(cmd).await { + Ok(res) => Ok(res), + Err(error) => { if error.is_connection_dropped() { debug!("Redis connection was dropped, attempting to reconnect"); - Either::A(clone.reconnect().then(|_| Err(error))) - } else { - Either::B(err(error)) + // TODO: Is this correct syntax? Otherwise we get an unused result warning + let _ = self.clone().reconnect().await; } - }) - .map(move |(_conn, res)| (self, res)), - ) + Err(error) + } + } + }) + .boxed() } - fn req_packed_commands( - self, - cmd: Vec, + fn req_packed_commands<'a>( + &'a mut self, + cmd: &'a Pipeline, offset: usize, count: usize, - ) -> Box), Error = RedisError> + Send> { - let clone = self.clone(); - Box::new( - self.get_shared_connection() - .req_packed_commands(cmd, offset, count) - .or_else(move |error| { + ) -> RedisFuture<'a, Vec> { + // This is how it is implemented in the redis-rs repository + (async move { + let mut connection = self.get_shared_connection(); + match connection.req_packed_commands(cmd, offset, count).await { + Ok(res) => Ok(res), + Err(error) => { if error.is_connection_dropped() { debug!("Redis connection was dropped, attempting to reconnect"); - Either::A(clone.reconnect().then(|_| Err(error))) - } else { - Either::B(err(error)) + // TODO: Is this correct syntax? Otherwise we get an unused result warning + let _ = self.clone().reconnect().await; } - }) - .map(|(_conn, res)| (self, res)), - ) - } -} - -fn get_shared_connection( - redis_info: Arc, -) -> impl Future { - result(Client::open((*redis_info).clone())).and_then(|client| { - client.get_shared_async_connection().map_err(|e| { - error!("Error connecting to Redis: {:?}", e); - e + Err(error) + } + } }) - }) + .boxed() + } } diff --git a/crates/interledger-store/tests/redis/accounts_test.rs b/crates/interledger-store/tests/redis/accounts_test.rs index 31e4d25e4..16dfcd572 100644 --- a/crates/interledger-store/tests/redis/accounts_test.rs +++ b/crates/interledger-store/tests/redis/accounts_test.rs @@ -1,489 +1,281 @@ use super::{fixtures::*, redis_helpers::*, store_helpers::*}; -use futures::future::{result, Either, Future}; +use futures::future::Either; +use futures::TryFutureExt; use interledger_api::{AccountSettings, NodeStore}; -use interledger_btp::{BtpAccount, BtpStore}; +use interledger_btp::BtpAccount; use interledger_ccp::{CcpRoutingAccount, RoutingRelation}; -use interledger_http::{HttpAccount, HttpStore}; +use interledger_http::HttpAccount; use interledger_packet::Address; use interledger_service::Account as AccountTrait; use interledger_service::{AccountStore, AddressStore, Username}; use interledger_service_util::BalanceStore; use interledger_store::redis::RedisStoreBuilder; -use log::{debug, error}; use redis_crate::Client; use secrecy::ExposeSecret; use secrecy::SecretString; +use std::default::Default; use std::str::FromStr; use uuid::Uuid; -#[test] -fn picks_up_parent_during_initialization() { +#[tokio::test] +async fn picks_up_parent_during_initialization() { let context = TestContext::new(); - block_on( - result(Client::open(context.get_client_connection_info())) - .map_err(|err| error!("Error creating Redis client: {:?}", err)) - .and_then(|client| { - debug!("Connected to redis: {:?}", client); - client - .get_shared_async_connection() - .map_err(|err| error!("Error connecting to Redis: {:?}", err)) - }) - .and_then(move |connection| { - // we set a parent that was already configured via perhaps a - // previous account insertion. that means that when we connect - // to the store we will always get the configured parent (if - // there was one)) - redis_crate::cmd("SET") - .arg("parent_node_account_address") - .arg("example.bob.node") - .query_async(connection) - .map_err(|err| panic!(err)) - .and_then(move |(_, _): (_, redis_crate::Value)| { - RedisStoreBuilder::new(context.get_client_connection_info(), [0; 32]) - .connect() - .and_then(move |store| { - // the store's ilp address is the store's - // username appended to the parent's address - assert_eq!( - store.get_ilp_address(), - Address::from_str("example.bob.node").unwrap() - ); - let _ = context; - Ok(()) - }) - }) - }), - ) - .unwrap(); + let client = Client::open(context.get_client_connection_info()).unwrap(); + let mut connection = client.get_multiplexed_tokio_connection().await.unwrap(); + + // we set a parent that was already configured via perhaps a + // previous account insertion. that means that when we connect + // to the store we will always get the configured parent (if + // there was one)) + let _: redis_crate::Value = redis_crate::cmd("SET") + .arg("parent_node_account_address") + .arg("example.bob.node") + .query_async(&mut connection) + .await + .unwrap(); + + let store = RedisStoreBuilder::new(context.get_client_connection_info(), [0; 32]) + .connect() + .await + .unwrap(); + // the store's ilp address is the store's + // username appended to the parent's address + assert_eq!( + store.get_ilp_address(), + Address::from_str("example.bob.node").unwrap() + ); } -#[test] -fn insert_accounts() { - block_on(test_store().and_then(|(store, context, _accs)| { - store - .insert_account(ACCOUNT_DETAILS_2.clone()) - .and_then(move |account| { - assert_eq!( - *account.ilp_address(), - Address::from_str("example.alice.user1.charlie").unwrap() - ); - let _ = context; - Ok(()) - }) - })) - .unwrap(); +#[tokio::test] +async fn insert_accounts() { + let (store, _context, _) = test_store().await.unwrap(); + let account = store + .insert_account(ACCOUNT_DETAILS_2.clone()) + .await + .unwrap(); + assert_eq!( + *account.ilp_address(), + Address::from_str("example.alice.user1.charlie").unwrap() + ); } -#[test] -fn update_ilp_and_children_addresses() { - block_on(test_store().and_then(|(store, context, accs)| { - store - // Add a NonRoutingAccount to make sure its address - // gets updated as well - .insert_account(ACCOUNT_DETAILS_2.clone()) - .and_then(move |acc2| { - let mut accs = accs.clone(); - accs.push(acc2); - accs.sort_by_key(|a| a.username().clone()); - let ilp_address = Address::from_str("test.parent.our_address").unwrap(); - store - .set_ilp_address(ilp_address.clone()) - .and_then(move |_| { - let ret = store.get_ilp_address(); - assert_eq!(ilp_address, ret); - store.get_all_accounts().and_then(move |accounts: Vec<_>| { - let mut accounts = accounts.clone(); - accounts.sort_by(|a, b| { - a.username() - .as_bytes() - .partial_cmp(b.username().as_bytes()) - .unwrap() - }); - for (a, b) in accounts.into_iter().zip(&accs) { - if a.routing_relation() == RoutingRelation::Child - || a.routing_relation() == RoutingRelation::NonRoutingAccount - { - assert_eq!( - *a.ilp_address(), - ilp_address.with_suffix(a.username().as_bytes()).unwrap() - ); - } else { - assert_eq!(a.ilp_address(), b.ilp_address()); - } - } - let _ = context; - Ok(()) - }) - }) - }) - })) - .unwrap(); +#[tokio::test] +async fn update_ilp_and_children_addresses() { + let (store, _context, accs) = test_store().await.unwrap(); + // Add a NonRoutingAccount to make sure its address + // gets updated as well + let acc2 = store + .insert_account(ACCOUNT_DETAILS_2.clone()) + .await + .unwrap(); + let mut accs = accs.clone(); + accs.push(acc2); + accs.sort_by_key(|a| a.username().clone()); + let ilp_address = Address::from_str("test.parent.our_address").unwrap(); + + store.set_ilp_address(ilp_address.clone()).await.unwrap(); + let ret = store.get_ilp_address(); + assert_eq!(ilp_address, ret); + + let mut accounts = store.get_all_accounts().await.unwrap(); + accounts.sort_by(|a, b| { + a.username() + .as_bytes() + .partial_cmp(b.username().as_bytes()) + .unwrap() + }); + for (a, b) in accounts.into_iter().zip(&accs) { + if a.routing_relation() == RoutingRelation::Child + || a.routing_relation() == RoutingRelation::NonRoutingAccount + { + assert_eq!( + *a.ilp_address(), + ilp_address.with_suffix(a.username().as_bytes()).unwrap() + ); + } else { + assert_eq!(a.ilp_address(), b.ilp_address()); + } + } } -#[test] -fn only_one_parent_allowed() { +#[tokio::test] +async fn only_one_parent_allowed() { let mut acc = ACCOUNT_DETAILS_2.clone(); acc.routing_relation = Some("Parent".to_owned()); acc.username = Username::from_str("another_name").unwrap(); acc.ilp_address = Some(Address::from_str("example.another_name").unwrap()); - block_on(test_store().and_then(|(store, context, accs)| { - store.insert_account(acc.clone()).then(move |res| { - // This should fail - assert!(res.is_err()); - futures::future::join_all(vec![ - Either::A(store.delete_account(accs[0].id()).and_then(|_| Ok(()))), - // must also clear the ILP Address to indicate that we no longer - // have a parent account configured - Either::B(store.clear_ilp_address()), - ]) - .and_then(move |_| { - store.insert_account(acc).and_then(move |_| { - // the call was successful, so the parent was succesfully added - let _ = context; - Ok(()) - }) - }) - }) - })) - .unwrap(); + let (store, _context, accs) = test_store().await.unwrap(); + let res = store.insert_account(acc.clone()).await; + // This should fail + assert!(res.is_err()); + futures::future::join_all(vec![ + Either::Left(store.delete_account(accs[0].id()).map_ok(|_| ())), + // must also clear the ILP Address to indicate that we no longer + // have a parent account configured + Either::Right(store.clear_ilp_address()), + ]) + .await; + let res = store.insert_account(acc).await; + assert!(res.is_ok()); } -#[test] -fn delete_accounts() { - block_on(test_store().and_then(|(store, context, _accs)| { - store.get_all_accounts().and_then(move |accounts| { - let id = accounts[0].id(); - store.delete_account(id).and_then(move |_| { - store.get_all_accounts().and_then(move |accounts| { - for a in accounts { - assert_ne!(id, a.id()); - } - let _ = context; - Ok(()) - }) - }) - }) - })) - .unwrap(); +#[tokio::test] +async fn delete_accounts() { + let (store, _context, _) = test_store().await.unwrap(); + let accounts = store.get_all_accounts().await.unwrap(); + let id = accounts[0].id(); + store.delete_account(id).await.unwrap(); + let accounts = store.get_all_accounts().await.unwrap(); + for a in accounts { + assert_ne!(id, a.id()); + } } -#[test] -fn update_accounts() { - block_on(test_store().and_then(|(store, context, accounts)| { - context - .async_connection() - .map_err(|err| panic!(err)) - .and_then(move |connection| { - let id = accounts[0].id(); - redis_crate::cmd("HMSET") - .arg(format!("accounts:{}", id)) - .arg("balance") - .arg(600) - .arg("prepaid_amount") - .arg(400) - .query_async(connection) - .map_err(|err| panic!(err)) - .and_then(move |(_, _): (_, redis_crate::Value)| { - let mut new = ACCOUNT_DETAILS_0.clone(); - new.asset_code = String::from("TUV"); - store.update_account(id, new).and_then(move |account| { - assert_eq!(account.asset_code(), "TUV"); - store.get_balance(account).and_then(move |balance| { - assert_eq!(balance, 1000); - let _ = context; - Ok(()) - }) - }) - }) - }) - })) - .unwrap(); +#[tokio::test] +async fn update_accounts() { + let (store, context, accounts) = test_store().await.unwrap(); + let mut connection = context.async_connection().await.unwrap(); + let id = accounts[0].id(); + let _: redis_crate::Value = redis_crate::cmd("HMSET") + .arg(format!("accounts:{}", id)) + .arg("balance") + .arg(600u64) + .arg("prepaid_amount") + .arg(400u64) + .query_async(&mut connection) + .await + .unwrap(); + let mut new = ACCOUNT_DETAILS_0.clone(); + new.asset_code = String::from("TUV"); + let account = store.update_account(id, new).await.unwrap(); + assert_eq!(account.asset_code(), "TUV"); + let balance = store.get_balance(account).await.unwrap(); + assert_eq!(balance, 1000); } -#[test] -fn modify_account_settings_settle_to_overflow() { - block_on(test_store().and_then(|(store, context, accounts)| { - let mut settings = AccountSettings::default(); - // Redis.rs cannot save a value larger than i64::MAX - settings.settle_to = Some(std::i64::MAX as u64 + 1); - let account = accounts[0].clone(); - let id = account.id(); - store - .modify_account_settings(id, settings) - .then(move |ret| { - assert!(ret.is_err()); - let _ = context; - Ok(()) - }) - })) - .unwrap(); +#[tokio::test] +async fn modify_account_settings_settle_to_overflow() { + let (store, _context, accounts) = test_store().await.unwrap(); + let mut settings = AccountSettings::default(); + // Redis.rs cannot save a value larger than i64::MAX + settings.settle_to = Some(std::i64::MAX as u64 + 1); + let account = accounts[0].clone(); + let id = account.id(); + let ret = store.modify_account_settings(id, settings).await; + assert!(ret.is_err()); } -use std::default::Default; -#[test] -fn modify_account_settings_unchanged() { - block_on(test_store().and_then(|(store, context, accounts)| { - let settings = AccountSettings::default(); - let account = accounts[0].clone(); +#[tokio::test] +async fn modify_account_settings_unchanged() { + let (store, _context, accounts) = test_store().await.unwrap(); + let settings = AccountSettings::default(); + let account = accounts[0].clone(); - let id = account.id(); - store - .modify_account_settings(id, settings) - .and_then(move |ret| { - assert_eq!( - account.get_http_auth_token().unwrap().expose_secret(), - ret.get_http_auth_token().unwrap().expose_secret(), - ); - assert_eq!( - account.get_ilp_over_btp_outgoing_token().unwrap(), - ret.get_ilp_over_btp_outgoing_token().unwrap() - ); - // Cannot check other parameters since they are only pub(crate). - let _ = context; - Ok(()) - }) - })) - .unwrap(); -} + let id = account.id(); + let ret = store.modify_account_settings(id, settings).await.unwrap(); -#[test] -fn modify_account_settings() { - block_on(test_store().and_then(|(store, context, accounts)| { - let settings = AccountSettings { - ilp_over_http_outgoing_token: Some(SecretString::new("test_token".to_owned())), - ilp_over_http_incoming_token: Some(SecretString::new("http_in_new".to_owned())), - ilp_over_btp_outgoing_token: Some(SecretString::new("dylan:test".to_owned())), - ilp_over_btp_incoming_token: Some(SecretString::new("btp_in_new".to_owned())), - ilp_over_http_url: Some("http://example.com/accounts/dylan/ilp".to_owned()), - ilp_over_btp_url: Some("http://example.com/accounts/dylan/ilp/btp".to_owned()), - settle_threshold: Some(-50), - settle_to: Some(100), - }; - let account = accounts[0].clone(); - - let id = account.id(); - store - .modify_account_settings(id, settings) - .and_then(move |ret| { - assert_eq!( - ret.get_http_auth_token().unwrap().expose_secret(), - "test_token", - ); - assert_eq!( - ret.get_ilp_over_btp_outgoing_token().unwrap(), - &b"dylan:test"[..], - ); - // Cannot check other parameters since they are only pub(crate). - let _ = context; - Ok(()) - }) - })) - .unwrap(); -} - -#[test] -fn starts_with_zero_balance() { - block_on(test_store().and_then(|(store, context, accs)| { - let account0 = accs[0].clone(); - store.get_balance(account0).and_then(move |balance| { - assert_eq!(balance, 0); - let _ = context; - Ok(()) - }) - })) - .unwrap(); + assert_eq!( + account.get_http_auth_token().unwrap().expose_secret(), + ret.get_http_auth_token().unwrap().expose_secret(), + ); + assert_eq!( + account.get_ilp_over_btp_outgoing_token().unwrap(), + ret.get_ilp_over_btp_outgoing_token().unwrap() + ); } -#[test] -fn fetches_account_from_username() { - block_on(test_store().and_then(|(store, context, accs)| { - store - .get_account_id_from_username(&Username::from_str("alice").unwrap()) - .and_then(move |account_id| { - assert_eq!(account_id, accs[0].id()); - let _ = context; - Ok(()) - }) - })) - .unwrap(); -} - -#[test] -fn duplicate_http_incoming_auth_works() { - let mut duplicate = ACCOUNT_DETAILS_2.clone(); - duplicate.ilp_over_http_incoming_token = - Some(SecretString::new("incoming_auth_token".to_string())); - block_on(test_store().and_then(|(store, context, accs)| { - let original = accs[0].clone(); - let original_id = original.id(); - store.insert_account(duplicate).and_then(move |duplicate| { - let duplicate_id = duplicate.id(); - assert_ne!(original_id, duplicate_id); - futures::future::join_all(vec![ - store.get_account_from_http_auth( - &Username::from_str("alice").unwrap(), - "incoming_auth_token", - ), - store.get_account_from_http_auth( - &Username::from_str("charlie").unwrap(), - "incoming_auth_token", - ), - ]) - .and_then(move |accs| { - // Alice and Charlie had the same auth token, but they had a - // different username/account id, so no problem. - assert_ne!(accs[0].id(), accs[1].id()); - assert_eq!(accs[0].id(), original_id); - assert_eq!(accs[1].id(), duplicate_id); - let _ = context; - Ok(()) - }) - }) - })) - .unwrap(); -} +#[tokio::test] +async fn modify_account_settings() { + let (store, _context, accounts) = test_store().await.unwrap(); + let settings = AccountSettings { + ilp_over_http_outgoing_token: Some(SecretString::new("test_token".to_owned())), + ilp_over_http_incoming_token: Some(SecretString::new("http_in_new".to_owned())), + ilp_over_btp_outgoing_token: Some(SecretString::new("dylan:test".to_owned())), + ilp_over_btp_incoming_token: Some(SecretString::new("btp_in_new".to_owned())), + ilp_over_http_url: Some("http://example.com/accounts/dylan/ilp".to_owned()), + ilp_over_btp_url: Some("http://example.com/accounts/dylan/ilp/btp".to_owned()), + settle_threshold: Some(-50), + settle_to: Some(100), + }; + let account = accounts[0].clone(); -#[test] -fn gets_account_from_btp_auth() { - block_on(test_store().and_then(|(store, context, accs)| { - // alice's incoming btp token is the username/password to get her - // account's information - store - .get_account_from_btp_auth(&Username::from_str("alice").unwrap(), "btp_token") - .and_then(move |acc| { - assert_eq!(acc.id(), accs[0].id()); - let _ = context; - Ok(()) - }) - })) - .unwrap(); + let id = account.id(); + let ret = store.modify_account_settings(id, settings).await.unwrap(); + assert_eq!( + ret.get_http_auth_token().unwrap().expose_secret(), + "test_token", + ); + assert_eq!( + ret.get_ilp_over_btp_outgoing_token().unwrap(), + &b"dylan:test"[..], + ); } -#[test] -fn gets_account_from_http_auth() { - block_on(test_store().and_then(|(store, context, accs)| { - store - .get_account_from_http_auth( - &Username::from_str("alice").unwrap(), - "incoming_auth_token", - ) - .and_then(move |acc| { - assert_eq!(acc.id(), accs[0].id()); - let _ = context; - Ok(()) - }) - })) - .unwrap(); +#[tokio::test] +async fn starts_with_zero_balance() { + let (store, _context, accs) = test_store().await.unwrap(); + let account0 = accs[0].clone(); + let balance = store.get_balance(account0).await.unwrap(); + assert_eq!(balance, 0); } -#[test] -fn duplicate_btp_incoming_auth_works() { - let mut charlie = ACCOUNT_DETAILS_2.clone(); - charlie.ilp_over_btp_incoming_token = Some(SecretString::new("btp_token".to_string())); - block_on(test_store().and_then(|(store, context, accs)| { - let alice = accs[0].clone(); - let alice_id = alice.id(); - store.insert_account(charlie).and_then(move |charlie| { - let charlie_id = charlie.id(); - assert_ne!(alice_id, charlie_id); - futures::future::join_all(vec![ - store.get_account_from_btp_auth(&Username::from_str("alice").unwrap(), "btp_token"), - store.get_account_from_btp_auth( - &Username::from_str("charlie").unwrap(), - "btp_token", - ), - ]) - .and_then(move |accs| { - assert_ne!(accs[0].id(), accs[1].id()); - assert_eq!(accs[0].id(), alice_id); - assert_eq!(accs[1].id(), charlie_id); - let _ = context; - Ok(()) - }) - }) - })) - .unwrap(); +#[tokio::test] +async fn fetches_account_from_username() { + let (store, _context, accs) = test_store().await.unwrap(); + let account_id = store + .get_account_id_from_username(&Username::from_str("alice").unwrap()) + .await + .unwrap(); + assert_eq!(account_id, accs[0].id()); } -#[test] -fn get_all_accounts() { - block_on(test_store().and_then(|(store, context, _accs)| { - store.get_all_accounts().and_then(move |accounts| { - assert_eq!(accounts.len(), 2); - let _ = context; - Ok(()) - }) - })) - .unwrap(); +#[tokio::test] +async fn get_all_accounts() { + let (store, _context, _) = test_store().await.unwrap(); + let accounts = store.get_all_accounts().await.unwrap(); + assert_eq!(accounts.len(), 2); } -#[test] -fn gets_single_account() { - block_on(test_store().and_then(|(store, context, accs)| { - let store_clone = store.clone(); - let acc = accs[0].clone(); - store_clone - .get_accounts(vec![acc.id()]) - .and_then(move |accounts| { - assert_eq!(accounts[0].ilp_address(), acc.ilp_address()); - let _ = context; - Ok(()) - }) - })) - .unwrap(); +#[tokio::test] +async fn gets_single_account() { + let (store, _context, accs) = test_store().await.unwrap(); + let acc = accs[0].clone(); + let accounts = store.get_accounts(vec![acc.id()]).await.unwrap(); + assert_eq!(accounts[0].ilp_address(), acc.ilp_address()); } -#[test] -fn gets_multiple() { - block_on(test_store().and_then(|(store, context, accs)| { - let store_clone = store.clone(); - // set account ids in reverse order - let account_ids: Vec = accs.iter().rev().map(|a| a.id()).collect::<_>(); - store_clone - .get_accounts(account_ids) - .and_then(move |accounts| { - // note reverse order is intentional - assert_eq!(accounts[0].ilp_address(), accs[1].ilp_address()); - assert_eq!(accounts[1].ilp_address(), accs[0].ilp_address()); - let _ = context; - Ok(()) - }) - })) - .unwrap(); +#[tokio::test] +async fn gets_multiple() { + let (store, _context, accs) = test_store().await.unwrap(); + // set account ids in reverse order + let account_ids: Vec = accs.iter().rev().map(|a| a.id()).collect::<_>(); + let accounts = store.get_accounts(account_ids).await.unwrap(); + // note reverse order is intentional + assert_eq!(accounts[0].ilp_address(), accs[1].ilp_address()); + assert_eq!(accounts[1].ilp_address(), accs[0].ilp_address()); } -#[test] -fn decrypts_outgoing_tokens_acc() { - block_on(test_store().and_then(|(store, context, accs)| { - let acc = accs[0].clone(); - store - .get_accounts(vec![acc.id()]) - .and_then(move |accounts| { - let account = accounts[0].clone(); - assert_eq!( - account.get_http_auth_token().unwrap().expose_secret(), - acc.get_http_auth_token().unwrap().expose_secret(), - ); - assert_eq!( - account.get_ilp_over_btp_outgoing_token().unwrap(), - acc.get_ilp_over_btp_outgoing_token().unwrap(), - ); - let _ = context; - Ok(()) - }) - })) - .unwrap() +#[tokio::test] +async fn decrypts_outgoing_tokens_acc() { + let (store, _context, accs) = test_store().await.unwrap(); + let acc = accs[0].clone(); + let accounts = store.get_accounts(vec![acc.id()]).await.unwrap(); + let account = accounts[0].clone(); + assert_eq!( + account.get_http_auth_token().unwrap().expose_secret(), + acc.get_http_auth_token().unwrap().expose_secret(), + ); + assert_eq!( + account.get_ilp_over_btp_outgoing_token().unwrap(), + acc.get_ilp_over_btp_outgoing_token().unwrap(), + ); } -#[test] -fn errors_for_unknown_accounts() { - let result = block_on(test_store().and_then(|(store, context, _accs)| { - store - .get_accounts(vec![Uuid::new_v4(), Uuid::new_v4()]) - .then(move |result| { - let _ = context; - result - }) - })); +#[tokio::test] +async fn errors_for_unknown_accounts() { + let (store, _context, _) = test_store().await.unwrap(); + let result = store + .get_accounts(vec![Uuid::new_v4(), Uuid::new_v4()]) + .await; assert!(result.is_err()); } diff --git a/crates/interledger-store/tests/redis/balances_test.rs b/crates/interledger-store/tests/redis/balances_test.rs index 20f3fece5..88d18f1c4 100644 --- a/crates/interledger-store/tests/redis/balances_test.rs +++ b/crates/interledger-store/tests/redis/balances_test.rs @@ -1,5 +1,5 @@ use super::{fixtures::*, store_helpers::*}; -use futures::future::{self, Either, Future}; + use interledger_api::NodeStore; use interledger_packet::Address; use interledger_service::{Account as AccountTrait, AddressStore}; @@ -9,90 +9,64 @@ use interledger_store::account::Account; use std::str::FromStr; use uuid::Uuid; -#[test] -fn get_balance() { - block_on(test_store().and_then(|(store, context, _accs)| { - let account_id = Uuid::new_v4(); - context - .async_connection() - .map_err(move |err| panic!(err)) - .and_then(move |connection| { - redis_crate::cmd("HMSET") - .arg(format!("accounts:{}", account_id)) - .arg("balance") - .arg(600) - .arg("prepaid_amount") - .arg(400) - .query_async(connection) - .map_err(|err| panic!(err)) - .and_then(move |(_, _): (_, redis_crate::Value)| { - let account = Account::try_from( - account_id, - ACCOUNT_DETAILS_0.clone(), - store.get_ilp_address(), - ) - .unwrap(); - store.get_balance(account).and_then(move |balance| { - assert_eq!(balance, 1000); - let _ = context; - Ok(()) - }) - }) - }) - })) +#[tokio::test] +async fn get_balance() { + let (store, context, _accs) = test_store().await.unwrap(); + let account_id = Uuid::new_v4(); + let mut connection = context.async_connection().await.unwrap(); + let _: redis_crate::Value = redis_crate::cmd("HMSET") + .arg(format!("accounts:{}", account_id)) + .arg("balance") + .arg(600u64) + .arg("prepaid_amount") + .arg(400u64) + .query_async(&mut connection) + .await + .unwrap(); + let account = Account::try_from( + account_id, + ACCOUNT_DETAILS_0.clone(), + store.get_ilp_address(), + ) .unwrap(); + let balance = store.get_balance(account).await.unwrap(); + assert_eq!(balance, 1000); } -#[test] -fn prepare_then_fulfill_with_settlement() { - block_on(test_store().and_then(|(store, context, accs)| { - let store_clone_1 = store.clone(); - let store_clone_2 = store.clone(); - store - .clone() - .get_accounts(vec![accs[0].id(), accs[1].id()]) - .map_err(|_err| panic!("Unable to get accounts")) - .and_then(move |accounts| { - let account0 = accounts[0].clone(); - let account1 = accounts[1].clone(); - store - // reduce account 0's balance by 100 - .update_balances_for_prepare(accounts[0].clone(), 100) - .and_then(move |_| { - store_clone_1 - .clone() - .get_balance(accounts[0].clone()) - .join(store_clone_1.clone().get_balance(accounts[1].clone())) - .and_then(|(balance0, balance1)| { - assert_eq!(balance0, -100); - assert_eq!(balance1, 0); - Ok(()) - }) - }) - .and_then(move |_| { - store_clone_2 - .clone() - .update_balances_for_fulfill(account1.clone(), 100) - .and_then(move |_| { - store_clone_2 - .clone() - .get_balance(account0.clone()) - .join(store_clone_2.clone().get_balance(account1.clone())) - .and_then(move |(balance0, balance1)| { - assert_eq!(balance0, -100); - assert_eq!(balance1, -1000); // the account must be settled down to -1000 - let _ = context; - Ok(()) - }) - }) - }) - }) - })) - .unwrap(); +#[tokio::test] +async fn prepare_then_fulfill_with_settlement() { + let (store, _context, accs) = test_store().await.unwrap(); + let accounts = store + .get_accounts(vec![accs[0].id(), accs[1].id()]) + .await + .unwrap(); + let account0 = accounts[0].clone(); + let account1 = accounts[1].clone(); + // reduce account 0's balance by 100 + store + .update_balances_for_prepare(account0.clone(), 100) + .await + .unwrap(); + // TODO:Can we make get_balance take a reference to the account? + // Even better, we should make it just take the account uid/username! + let balance0 = store.get_balance(account0.clone()).await.unwrap(); + let balance1 = store.get_balance(account1.clone()).await.unwrap(); + assert_eq!(balance0, -100); + assert_eq!(balance1, 0); + + // Account 1 hits the settlement limit (?) TODO + store + .update_balances_for_fulfill(account1.clone(), 100) + .await + .unwrap(); + let balance0 = store.get_balance(account0).await.unwrap(); + let balance1 = store.get_balance(account1).await.unwrap(); + assert_eq!(balance0, -100); + assert_eq!(balance1, -1000); } -#[test] -fn process_fulfill_no_settle_to() { +#[tokio::test] +async fn process_fulfill_no_settle_to() { // account without a settle_to let acc = { let mut acc = ACCOUNT_DETAILS_1.clone(); @@ -104,31 +78,21 @@ fn process_fulfill_no_settle_to() { acc.settle_to = None; acc }; - block_on(test_store().and_then(|(store, context, _accs)| { - let store_clone = store.clone(); - store.clone().insert_account(acc).and_then(move |account| { - let id = account.id(); - store_clone - .get_accounts(vec![id]) - .and_then(move |accounts| { - let acc = accounts[0].clone(); - store_clone - .clone() - .update_balances_for_fulfill(acc.clone(), 100) - .and_then(move |(balance, amount_to_settle)| { - assert_eq!(balance, 100); - assert_eq!(amount_to_settle, 0); - let _ = context; - Ok(()) - }) - }) - }) - })) - .unwrap(); + let (store, _context, _accs) = test_store().await.unwrap(); + let account = store.insert_account(acc).await.unwrap(); + let id = account.id(); + let accounts = store.get_accounts(vec![id]).await.unwrap(); + let acc = accounts[0].clone(); + let (balance, amount_to_settle) = store + .update_balances_for_fulfill(acc.clone(), 100) + .await + .unwrap(); + assert_eq!(balance, 100); + assert_eq!(amount_to_settle, 0); } -#[test] -fn process_fulfill_settle_to_over_threshold() { +#[tokio::test] +async fn process_fulfill_settle_to_over_threshold() { // account misconfigured with settle_to >= settle_threshold does not get settlements let acc = { let mut acc = ACCOUNT_DETAILS_1.clone(); @@ -141,31 +105,21 @@ fn process_fulfill_settle_to_over_threshold() { acc.ilp_over_btp_incoming_token = None; acc }; - block_on(test_store().and_then(|(store, context, _accs)| { - let store_clone = store.clone(); - store.clone().insert_account(acc).and_then(move |acc| { - let id = acc.id(); - store_clone - .get_accounts(vec![id]) - .and_then(move |accounts| { - let acc = accounts[0].clone(); - store_clone - .clone() - .update_balances_for_fulfill(acc.clone(), 1000) - .and_then(move |(balance, amount_to_settle)| { - assert_eq!(balance, 1000); - assert_eq!(amount_to_settle, 0); - let _ = context; - Ok(()) - }) - }) - }) - })) - .unwrap(); + let (store, _context, _accs) = test_store().await.unwrap(); + let acc = store.insert_account(acc).await.unwrap(); + let id = acc.id(); + let accounts = store.get_accounts(vec![id]).await.unwrap(); + let acc = accounts[0].clone(); + let (balance, amount_to_settle) = store + .update_balances_for_fulfill(acc.clone(), 1000) + .await + .unwrap(); + assert_eq!(balance, 1000); + assert_eq!(amount_to_settle, 0); } -#[test] -fn process_fulfill_ok() { +#[tokio::test] +async fn process_fulfill_ok() { // account with settle to = 0 (not falsy) with settle_threshold > 0, gets settlements let acc = { let mut acc = ACCOUNT_DETAILS_1.clone(); @@ -178,160 +132,99 @@ fn process_fulfill_ok() { acc.ilp_over_btp_incoming_token = None; acc }; - block_on(test_store().and_then(|(store, context, _accs)| { - let store_clone = store.clone(); - store.clone().insert_account(acc).and_then(move |account| { - let id = account.id(); - store_clone - .get_accounts(vec![id]) - .and_then(move |accounts| { - let acc = accounts[0].clone(); - store_clone - .clone() - .update_balances_for_fulfill(acc.clone(), 101) - .and_then(move |(balance, amount_to_settle)| { - assert_eq!(balance, 0); - assert_eq!(amount_to_settle, 101); - let _ = context; - Ok(()) - }) - }) - }) - })) - .unwrap(); + let (store, _context, _accs) = test_store().await.unwrap(); + let account = store.insert_account(acc).await.unwrap(); + let id = account.id(); + let accounts = store.get_accounts(vec![id]).await.unwrap(); + let acc = accounts[0].clone(); + let (balance, amount_to_settle) = store + .update_balances_for_fulfill(acc.clone(), 101) + .await + .unwrap(); + assert_eq!(balance, 0); + assert_eq!(amount_to_settle, 101); } -#[test] -fn prepare_then_reject() { - block_on(test_store().and_then(|(store, context, accs)| { - let store_clone_1 = store.clone(); - let store_clone_2 = store.clone(); - store - .clone() - .get_accounts(vec![accs[0].id(), accs[1].id()]) - .map_err(|_err| panic!("Unable to get accounts")) - .and_then(move |accounts| { - let account0 = accounts[0].clone(); - let account1 = accounts[1].clone(); - store - .update_balances_for_prepare(accounts[0].clone(), 100) - .and_then(move |_| { - store_clone_1 - .clone() - .get_balance(accounts[0].clone()) - .join(store_clone_1.clone().get_balance(accounts[1].clone())) - .and_then(|(balance0, balance1)| { - assert_eq!(balance0, -100); - assert_eq!(balance1, 0); - Ok(()) - }) - }) - .and_then(move |_| { - store_clone_2 - .clone() - .update_balances_for_reject(account0.clone(), 100) - .and_then(move |_| { - store_clone_2 - .clone() - .get_balance(account0.clone()) - .join(store_clone_2.clone().get_balance(account1.clone())) - .and_then(move |(balance0, balance1)| { - assert_eq!(balance0, 0); - assert_eq!(balance1, 0); - let _ = context; - Ok(()) - }) - }) - }) - }) - })) - .unwrap(); +#[tokio::test] +async fn prepare_then_reject() { + let (store, _context, accs) = test_store().await.unwrap(); + let accounts = store + .get_accounts(vec![accs[0].id(), accs[1].id()]) + .await + .unwrap(); + let account0 = accounts[0].clone(); + let _account1 = accounts[1].clone(); + store + .update_balances_for_prepare(accounts[0].clone(), 100) + .await + .unwrap(); + let balance0 = store.get_balance(accounts[0].clone()).await.unwrap(); + let balance1 = store.get_balance(accounts[1].clone()).await.unwrap(); + assert_eq!(balance0, -100); + assert_eq!(balance1, 0); + store + .update_balances_for_reject(account0.clone(), 100) + .await + .unwrap(); + let balance0 = store.get_balance(accounts[0].clone()).await.unwrap(); + let balance1 = store.get_balance(accounts[1].clone()).await.unwrap(); + assert_eq!(balance0, 0); + assert_eq!(balance1, 0); } -#[test] -fn enforces_minimum_balance() { - block_on(test_store().and_then(|(store, context, accs)| { - store - .clone() - .get_accounts(vec![accs[0].id(), accs[1].id()]) - .map_err(|_err| panic!("Unable to get accounts")) - .and_then(move |accounts| { - store - .update_balances_for_prepare(accounts[0].clone(), 10000) - .then(move |result| { - assert!(result.is_err()); - let _ = context; - Ok(()) - }) - }) - })) - .unwrap() +#[tokio::test] +async fn enforces_minimum_balance() { + let (store, _context, accs) = test_store().await.unwrap(); + let accounts = store + .get_accounts(vec![accs[0].id(), accs[1].id()]) + .await + .unwrap(); + let result = store + .update_balances_for_prepare(accounts[0].clone(), 10000) + .await; + assert!(result.is_err()); } -#[test] +#[tokio::test] // Prepare and Fulfill a packet for 100 units from Account 0 to Account 1 // Then, Prepare and Fulfill a packet for 80 units from Account 1 to Account 0 -fn netting_fulfilled_balances() { - block_on(test_store().and_then(|(store, context, accs)| { - let store_clone1 = store.clone(); - let store_clone2 = store.clone(); - store - .clone() - .insert_account(ACCOUNT_DETAILS_2.clone()) - .and_then(move |acc| { - store - .clone() - .get_accounts(vec![accs[0].id(), acc.id()]) - .map_err(|_err| panic!("Unable to get accounts")) - .and_then(move |accounts| { - let account0 = accounts[0].clone(); - let account1 = accounts[1].clone(); - let account0_clone = account0.clone(); - let account1_clone = account1.clone(); - future::join_all(vec![ - Either::A(store.clone().update_balances_for_prepare( - account0.clone(), - 100, // decrement account 0 by 100 - )), - Either::B( - store - .clone() - .update_balances_for_fulfill( - account1.clone(), // increment account 1 by 100 - 100, - ) - .and_then(|_| Ok(())), - ), - ]) - .and_then(move |_| { - future::join_all(vec![ - Either::A( - store_clone1 - .clone() - .update_balances_for_prepare(account1.clone(), 80), - ), - Either::B( - store_clone1 - .clone() - .update_balances_for_fulfill(account0.clone(), 80) - .and_then(|_| Ok(())), - ), - ]) - }) - .and_then(move |_| { - store_clone2 - .clone() - .get_balance(account0_clone) - .join(store_clone2.get_balance(account1_clone)) - .and_then(move |(balance0, balance1)| { - assert_eq!(balance0, -20); - assert_eq!(balance1, 20); - let _ = context; - Ok(()) - }) - }) - }) - }) - })) - .unwrap(); +async fn netting_fulfilled_balances() { + let (store, _context, accs) = test_store().await.unwrap(); + let acc = store + .insert_account(ACCOUNT_DETAILS_2.clone()) + .await + .unwrap(); + let accounts = store + .get_accounts(vec![accs[0].id(), acc.id()]) + .await + .unwrap(); + let account0 = accounts[0].clone(); + let account1 = accounts[1].clone(); + + // decrement account 0 by 100 + store + .update_balances_for_prepare(account0.clone(), 100) + .await + .unwrap(); + // increment account 1 by 100 + store + .update_balances_for_fulfill(account1.clone(), 100) + .await + .unwrap(); + + // decrement account 1 by 80 + store + .update_balances_for_prepare(account1.clone(), 80) + .await + .unwrap(); + // increment account 0 by 80 + store + .update_balances_for_fulfill(account0.clone(), 80) + .await + .unwrap(); + + let balance0 = store.get_balance(accounts[0].clone()).await.unwrap(); + let balance1 = store.get_balance(accounts[1].clone()).await.unwrap(); + assert_eq!(balance0, -20); + assert_eq!(balance1, 20); } diff --git a/crates/interledger-store/tests/redis/btp_test.rs b/crates/interledger-store/tests/redis/btp_test.rs index 94af16200..72c0657cc 100644 --- a/crates/interledger-store/tests/redis/btp_test.rs +++ b/crates/interledger-store/tests/redis/btp_test.rs @@ -1,63 +1,79 @@ +use super::fixtures::*; + use super::store_helpers::*; -use futures::future::Future; + +use interledger_api::NodeStore; use interledger_btp::{BtpAccount, BtpStore}; use interledger_http::HttpAccount; use interledger_packet::Address; -use interledger_service::{Account, Username}; -use secrecy::ExposeSecret; +use interledger_service::{Account as AccountTrait, Username}; + +use secrecy::{ExposeSecret, SecretString}; use std::str::FromStr; -#[test] -fn gets_account_from_btp_auth() { - block_on(test_store().and_then(|(store, context, _accs)| { - store - .get_account_from_btp_auth(&Username::from_str("bob").unwrap(), "other_btp_token") - .and_then(move |account| { - assert_eq!( - *account.ilp_address(), - Address::from_str("example.alice.user1.bob").unwrap() - ); - let _ = context; - Ok(()) - }) - })) - .unwrap() +#[tokio::test] +async fn gets_account_from_btp_auth() { + let (store, _context, _) = test_store().await.unwrap(); + let account = store + .get_account_from_btp_auth(&Username::from_str("bob").unwrap(), "other_btp_token") + .await + .unwrap(); + assert_eq!( + *account.ilp_address(), + Address::from_str("example.alice.user1.bob").unwrap() + ); } -#[test] -fn decrypts_outgoing_tokens_btp() { - block_on(test_store().and_then(|(store, context, _accs)| { - store - .get_account_from_btp_auth(&Username::from_str("bob").unwrap(), "other_btp_token") - .and_then(move |account| { - // the account is created on Dylan's connector - assert_eq!( - account.get_http_auth_token().unwrap().expose_secret(), - "outgoing_auth_token", - ); - assert_eq!( - &account.get_ilp_over_btp_outgoing_token().unwrap(), - b"btp_token" - ); - let _ = context; - Ok(()) - }) - })) - .unwrap() +#[tokio::test] +async fn decrypts_outgoing_tokens_btp() { + let (store, _context, _) = test_store().await.unwrap(); + let account = store + .get_account_from_btp_auth(&Username::from_str("bob").unwrap(), "other_btp_token") + .await + .unwrap(); + + // the account is created on Dylan's connector + assert_eq!( + account.get_http_auth_token().unwrap().expose_secret(), + "outgoing_auth_token", + ); + assert_eq!( + &account.get_ilp_over_btp_outgoing_token().unwrap(), + b"btp_token" + ); } -#[test] -fn errors_on_unknown_btp_token() { - let result = block_on(test_store().and_then(|(store, context, _accs)| { - store - .get_account_from_btp_auth( - &Username::from_str("someuser").unwrap(), - "unknown_btp_token", - ) - .then(move |result| { - let _ = context; - result - }) - })); +#[tokio::test] +async fn errors_on_unknown_user_or_wrong_btp_token() { + let (store, _context, _) = test_store().await.unwrap(); + let result = store + .get_account_from_btp_auth(&Username::from_str("asdf").unwrap(), "other_btp_token") + .await; assert!(result.is_err()); + + let result = store + .get_account_from_btp_auth(&Username::from_str("bob").unwrap(), "wrong_token") + .await; + assert!(result.is_err()); +} + +#[tokio::test] +async fn duplicate_btp_incoming_auth_works() { + let mut charlie = ACCOUNT_DETAILS_2.clone(); + charlie.ilp_over_btp_incoming_token = Some(SecretString::new("btp_token".to_string())); + let (store, _context, accs) = test_store().await.unwrap(); + let alice = accs[0].clone(); + let alice_id = alice.id(); + let charlie = store.insert_account(charlie).await.unwrap(); + let charlie_id = charlie.id(); + assert_ne!(alice_id, charlie_id); + let result = futures::future::join_all(vec![ + store.get_account_from_btp_auth(&Username::from_str("alice").unwrap(), "btp_token"), + store.get_account_from_btp_auth(&Username::from_str("charlie").unwrap(), "btp_token"), + ]) + .await; + let accs: Vec<_> = result.into_iter().map(|r| r.unwrap()).collect(); + assert_ne!(accs[0].id(), accs[1].id()); + assert_eq!(accs[0].id(), alice_id); + assert_eq!(accs[1].id(), charlie_id); } diff --git a/crates/interledger-store/tests/redis/http_test.rs b/crates/interledger-store/tests/redis/http_test.rs index 3e3c2350f..a560f0670 100644 --- a/crates/interledger-store/tests/redis/http_test.rs +++ b/crates/interledger-store/tests/redis/http_test.rs @@ -1,74 +1,77 @@ +use super::fixtures::*; use super::store_helpers::*; -use futures::future::Future; + +use interledger_api::NodeStore; use interledger_btp::BtpAccount; use interledger_http::{HttpAccount, HttpStore}; use interledger_packet::Address; use interledger_service::{Account, Username}; -use secrecy::ExposeSecret; +use secrecy::{ExposeSecret, SecretString}; use std::str::FromStr; -#[test] -fn gets_account_from_http_bearer_token() { - block_on(test_store().and_then(|(store, context, _accs)| { - store - .get_account_from_http_auth( - &Username::from_str("alice").unwrap(), - "incoming_auth_token", - ) - .and_then(move |account| { - assert_eq!( - *account.ilp_address(), - Address::from_str("example.alice").unwrap() - ); - // this account is in Dylan's connector - assert_eq!( - account.get_http_auth_token().unwrap().expose_secret(), - "outgoing_auth_token", - ); - assert_eq!( - &account.get_ilp_over_btp_outgoing_token().unwrap(), - b"btp_token", - ); - let _ = context; - Ok(()) - }) - })) - .unwrap() +#[tokio::test] +async fn gets_account_from_http_bearer_token() { + let (store, _context, _) = test_store().await.unwrap(); + let account = store + .get_account_from_http_auth(&Username::from_str("alice").unwrap(), "incoming_auth_token") + .await + .unwrap(); + assert_eq!( + *account.ilp_address(), + Address::from_str("example.alice").unwrap() + ); + assert_eq!( + account.get_http_auth_token().unwrap().expose_secret(), + "outgoing_auth_token", + ); + assert_eq!( + &account.get_ilp_over_btp_outgoing_token().unwrap(), + b"btp_token", + ); } -#[test] -fn decrypts_outgoing_tokens_http() { - block_on(test_store().and_then(|(store, context, _accs)| { - store - .get_account_from_http_auth( - &Username::from_str("alice").unwrap(), - "incoming_auth_token", - ) - .and_then(move |account| { - assert_eq!( - account.get_http_auth_token().unwrap().expose_secret(), - "outgoing_auth_token", - ); - assert_eq!( - &account.get_ilp_over_btp_outgoing_token().unwrap(), - b"btp_token", - ); - let _ = context; - Ok(()) - }) - })) - .unwrap() -} +#[tokio::test] +async fn errors_on_unknown_user_or_wrong_http_token() { + let (store, _context, _) = test_store().await.unwrap(); + // wrong password + let result = store + .get_account_from_http_auth(&Username::from_str("alice").unwrap(), "unknown_token") + .await; + assert!(result.is_err()); -#[test] -fn errors_on_unknown_http_auth() { - let result = block_on(test_store().and_then(|(store, context, _accs)| { - store - .get_account_from_http_auth(&Username::from_str("someuser").unwrap(), "unknown_token") - .then(move |result| { - let _ = context; - result - }) - })); + // wrong user + let result = store + .get_account_from_http_auth(&Username::from_str("asdf").unwrap(), "incoming_auth_token") + .await; assert!(result.is_err()); } + +#[tokio::test] +async fn duplicate_http_incoming_auth_works() { + let mut duplicate = ACCOUNT_DETAILS_2.clone(); + duplicate.ilp_over_http_incoming_token = + Some(SecretString::new("incoming_auth_token".to_string())); + let (store, _context, accs) = test_store().await.unwrap(); + let original = accs[0].clone(); + let original_id = original.id(); + let duplicate = store.insert_account(duplicate).await.unwrap(); + let duplicate_id = duplicate.id(); + assert_ne!(original_id, duplicate_id); + let result = futures::future::join_all(vec![ + store.get_account_from_http_auth( + &Username::from_str("alice").unwrap(), + "incoming_auth_token", + ), + store.get_account_from_http_auth( + &Username::from_str("charlie").unwrap(), + "incoming_auth_token", + ), + ]) + .await; + let accs: Vec<_> = result.into_iter().map(|r| r.unwrap()).collect(); + // Alice and Charlie had the same auth token, but they had a + // different username/account id, so no problem. + assert_ne!(accs[0].id(), accs[1].id()); + assert_eq!(accs[0].id(), original_id); + assert_eq!(accs[1].id(), duplicate_id); +} diff --git a/crates/interledger-store/tests/redis/rate_limiting_test.rs b/crates/interledger-store/tests/redis/rate_limiting_test.rs index 178e4f0e0..6e6b1b5cf 100644 --- a/crates/interledger-store/tests/redis/rate_limiting_test.rs +++ b/crates/interledger-store/tests/redis/rate_limiting_test.rs @@ -1,98 +1,80 @@ use super::{fixtures::*, store_helpers::*}; -use futures::future::{join_all, Future}; +use futures::future::join_all; use interledger_service::AddressStore; use interledger_service_util::{RateLimitError, RateLimitStore}; use interledger_store::account::Account; use uuid::Uuid; -#[test] -fn rate_limits_number_of_packets() { - block_on(test_store().and_then(|(store, context, _accs)| { - let account = Account::try_from( - Uuid::new_v4(), - ACCOUNT_DETAILS_0.clone(), - store.get_ilp_address(), - ) - .unwrap(); - join_all(vec![ - store.clone().apply_rate_limits(account.clone(), 10), - store.clone().apply_rate_limits(account.clone(), 10), - store.clone().apply_rate_limits(account.clone(), 10), - ]) - .then(move |result| { - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), RateLimitError::PacketLimitExceeded); - let _ = context; - Ok(()) - }) - })) - .unwrap() +#[tokio::test] +async fn rate_limits_number_of_packets() { + let (store, _context, _) = test_store().await.unwrap(); + let account = Account::try_from( + Uuid::new_v4(), + ACCOUNT_DETAILS_0.clone(), + store.get_ilp_address(), + ) + .unwrap(); + let results = join_all(vec![ + store.clone().apply_rate_limits(account.clone(), 10), + store.clone().apply_rate_limits(account.clone(), 10), + store.clone().apply_rate_limits(account.clone(), 10), + ]) + .await; + // The first 2 calls succeed, while the 3rd one hits the rate limit error + // because the account is only allowed 2 packets per minute + assert_eq!( + results, + vec![Ok(()), Ok(()), Err(RateLimitError::PacketLimitExceeded)] + ); } -#[test] -fn limits_amount_throughput() { - block_on(test_store().and_then(|(store, context, _accs)| { - let account = Account::try_from( - Uuid::new_v4(), - ACCOUNT_DETAILS_1.clone(), - store.get_ilp_address(), - ) - .unwrap(); - join_all(vec![ - store.clone().apply_rate_limits(account.clone(), 500), - store.clone().apply_rate_limits(account.clone(), 500), - store.clone().apply_rate_limits(account.clone(), 1), - ]) - .then(move |result| { - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), RateLimitError::ThroughputLimitExceeded); - let _ = context; - Ok(()) - }) - })) - .unwrap() +#[tokio::test] +async fn limits_amount_throughput() { + let (store, _context, _) = test_store().await.unwrap(); + let account = Account::try_from( + Uuid::new_v4(), + ACCOUNT_DETAILS_1.clone(), + store.get_ilp_address(), + ) + .unwrap(); + let results = join_all(vec![ + store.clone().apply_rate_limits(account.clone(), 500), + store.clone().apply_rate_limits(account.clone(), 500), + store.clone().apply_rate_limits(account.clone(), 1), + ]) + .await; + // The first 2 calls succeed, while the 3rd one hits the rate limit error + // because the account is only allowed 1000 units of currency per minute + assert_eq!( + results, + vec![Ok(()), Ok(()), Err(RateLimitError::ThroughputLimitExceeded)] + ); } -#[test] -fn refunds_throughput_limit_for_rejected_packets() { - block_on(test_store().and_then(|(store, context, _accs)| { - let account = Account::try_from( - Uuid::new_v4(), - ACCOUNT_DETAILS_1.clone(), - store.get_ilp_address(), - ) +#[tokio::test] +async fn refunds_throughput_limit_for_rejected_packets() { + let (store, _context, _) = test_store().await.unwrap(); + let account = Account::try_from( + Uuid::new_v4(), + ACCOUNT_DETAILS_1.clone(), + store.get_ilp_address(), + ) + .unwrap(); + + join_all(vec![ + store.clone().apply_rate_limits(account.clone(), 500), + store.clone().apply_rate_limits(account.clone(), 500), + ]) + .await; + + // We refund the throughput limit once, meaning we can do 1 more call before + // the error + store + .refund_throughput_limit(account.clone(), 500) + .await .unwrap(); - join_all(vec![ - store.clone().apply_rate_limits(account.clone(), 500), - store.clone().apply_rate_limits(account.clone(), 500), - ]) - .map_err(|err| panic!(err)) - .and_then(move |_| { - let store_clone = store.clone(); - let account_clone = account.clone(); - store - .clone() - .refund_throughput_limit(account.clone(), 500) - .and_then(move |_| { - store - .clone() - .apply_rate_limits(account.clone(), 500) - .map_err(|err| panic!(err)) - }) - .and_then(move |_| { - store_clone - .apply_rate_limits(account_clone, 1) - .then(move |result| { - assert!(result.is_err()); - assert_eq!( - result.unwrap_err(), - RateLimitError::ThroughputLimitExceeded - ); - let _ = context; - Ok(()) - }) - }) - }) - })) - .unwrap() + store.apply_rate_limits(account.clone(), 500).await.unwrap(); + + let result = store.apply_rate_limits(account.clone(), 1).await; + assert_eq!(result.unwrap_err(), RateLimitError::ThroughputLimitExceeded); } diff --git a/crates/interledger-store/tests/redis/rates_test.rs b/crates/interledger-store/tests/redis/rates_test.rs index ce7a99d65..c0a52da41 100644 --- a/crates/interledger-store/tests/redis/rates_test.rs +++ b/crates/interledger-store/tests/redis/rates_test.rs @@ -1,27 +1,22 @@ use super::store_helpers::*; -use futures::future::Future; + use interledger_service_util::ExchangeRateStore; -#[test] -fn set_rates() { - block_on(test_store().and_then(|(store, context, _accs)| { - let store_clone = store.clone(); - let rates = store.get_exchange_rates(&["ABC", "XYZ"]); - assert!(rates.is_err()); - store - .set_exchange_rates( - [("ABC".to_string(), 500.0), ("XYZ".to_string(), 0.005)] - .iter() - .cloned() - .collect(), - ) - .and_then(move |_| { - let rates = store_clone.get_exchange_rates(&["XYZ", "ABC"]).unwrap(); - assert_eq!(rates[0].to_string(), "0.005"); - assert_eq!(rates[1].to_string(), "500"); - let _ = context; - Ok(()) - }) - })) - .unwrap(); +#[tokio::test] +async fn set_rates() { + let (store, _context, _) = test_store().await.unwrap(); + let rates = store.get_exchange_rates(&["ABC", "XYZ"]); + assert!(rates.is_err()); + store + .set_exchange_rates( + [("ABC".to_string(), 500.0), ("XYZ".to_string(), 0.005)] + .iter() + .cloned() + .collect(), + ) + .unwrap(); + + let rates = store.get_exchange_rates(&["XYZ", "ABC"]).unwrap(); + assert_eq!(rates[0].to_string(), "0.005"); + assert_eq!(rates[1].to_string(), "500"); } diff --git a/crates/interledger-store/tests/redis/redis_tests.rs b/crates/interledger-store/tests/redis/redis_tests.rs index c0910f487..b8f024943 100644 --- a/crates/interledger-store/tests/redis/redis_tests.rs +++ b/crates/interledger-store/tests/redis/redis_tests.rs @@ -8,6 +8,7 @@ mod routing_test; mod settlement_test; mod fixtures { + use interledger_api::AccountDetails; use interledger_packet::Address; use interledger_service::Username; @@ -88,7 +89,6 @@ mod redis_helpers { // Copied from https://github.com/mitsuhiko/redis-rs/blob/9a1777e8a90c82c315a481cdf66beb7d69e681a2/tests/support/mod.rs #![allow(dead_code)] - use futures::Future; use redis_crate::{self, RedisError}; use std::env; use std::fs; @@ -97,6 +97,8 @@ mod redis_helpers { use std::thread::sleep; use std::time::Duration; + use futures::future::TryFutureExt; + #[derive(PartialEq)] enum ServerType { Tcp, @@ -247,22 +249,21 @@ mod redis_helpers { self.client.get_connection().unwrap() } - pub fn async_connection( - &self, - ) -> impl Future { + pub async fn async_connection(&self) -> Result { self.client .get_async_connection() .map_err(|err| panic!(err)) + .await } pub fn stop_server(&mut self) { self.server.stop(); } - pub fn shared_async_connection( + pub async fn shared_async_connection( &self, - ) -> impl Future { - self.client.get_shared_async_connection() + ) -> Result { + self.client.get_multiplexed_tokio_connection().await } } } @@ -270,8 +271,7 @@ mod redis_helpers { mod store_helpers { use super::fixtures::*; use super::redis_helpers::*; - use env_logger; - use futures::Future; + use interledger_api::NodeStore; use interledger_packet::Address; use interledger_service::{Account as AccountTrait, AddressStore}; @@ -282,55 +282,37 @@ mod store_helpers { use lazy_static::lazy_static; use parking_lot::Mutex; use std::str::FromStr; - use tokio::runtime::Runtime; lazy_static! { static ref TEST_MUTEX: Mutex<()> = Mutex::new(()); } - pub fn test_store() -> impl Future), Error = ()> { + pub async fn test_store() -> Result<(RedisStore, TestContext, Vec), ()> { let context = TestContext::new(); - RedisStoreBuilder::new(context.get_client_connection_info(), [0; 32]) + let store = RedisStoreBuilder::new(context.get_client_connection_info(), [0; 32]) .node_ilp_address(Address::from_str("example.node").unwrap()) .connect() - .and_then(|store| { - let store_clone = store.clone(); - let mut accs = Vec::new(); - store - .clone() - .insert_account(ACCOUNT_DETAILS_0.clone()) - .and_then(move |acc| { - accs.push(acc.clone()); - // alice is a Parent, so the store's ilp address is updated to - // the value that would be received by the ILDCP request. here, - // we just assume alice appended some data to her address - store - .clone() - .set_ilp_address(acc.ilp_address().with_suffix(b"user1").unwrap()) - .and_then(move |_| { - store_clone - .insert_account(ACCOUNT_DETAILS_1.clone()) - .and_then(move |acc| { - accs.push(acc.clone()); - Ok((store, context, accs)) - }) - }) - }) - }) - } + .await + .unwrap(); + let mut accs = Vec::new(); + let acc = store + .insert_account(ACCOUNT_DETAILS_0.clone()) + .await + .unwrap(); + accs.push(acc.clone()); + // alice is a Parent, so the store's ilp address is updated to + // the value that would be received by the ILDCP request. here, + // we just assume alice appended some data to her address + store + .set_ilp_address(acc.ilp_address().with_suffix(b"user1").unwrap()) + .await + .unwrap(); - pub fn block_on(f: F) -> Result - where - F: Future + Send + 'static, - F::Item: Send, - F::Error: Send, - { - // Only run one test at a time - let _ = env_logger::try_init(); - let lock = TEST_MUTEX.lock(); - let mut runtime = Runtime::new().unwrap(); - let result = runtime.block_on(f); - drop(lock); - result + let acc = store + .insert_account(ACCOUNT_DETAILS_1.clone()) + .await + .unwrap(); + accs.push(acc); + Ok((store, context, accs)) } } diff --git a/crates/interledger-store/tests/redis/routing_test.rs b/crates/interledger-store/tests/redis/routing_test.rs index 46cadc1eb..480800798 100644 --- a/crates/interledger-store/tests/redis/routing_test.rs +++ b/crates/interledger-store/tests/redis/routing_test.rs @@ -1,5 +1,5 @@ use super::{fixtures::*, redis_helpers::*, store_helpers::*}; -use futures::future::Future; + use interledger_api::{AccountDetails, NodeStore}; use interledger_ccp::RouteManagerStore; use interledger_packet::Address; @@ -8,378 +8,266 @@ use interledger_service::{Account as AccountTrait, AddressStore, Username}; use interledger_store::{account::Account, redis::RedisStoreBuilder}; use std::str::FromStr; use std::{collections::HashMap, time::Duration}; -use tokio_timer::sleep; use uuid::Uuid; -#[test] -fn polls_for_route_updates() { +#[tokio::test] +async fn polls_for_route_updates() { let context = TestContext::new(); - block_on( - RedisStoreBuilder::new(context.get_client_connection_info(), [0; 32]) - .poll_interval(1) - .node_ilp_address(Address::from_str("example.node").unwrap()) - .connect() - .and_then(|store| { - let connection = context.async_connection(); - assert_eq!(store.routing_table().len(), 0); - let store_clone_1 = store.clone(); - let store_clone_2 = store.clone(); - store - .clone() - .insert_account(ACCOUNT_DETAILS_0.clone()) - .and_then(move |alice| { - let routing_table = store_clone_1.routing_table(); - assert_eq!(routing_table.len(), 1); - assert_eq!(*routing_table.get("example.alice").unwrap(), alice.id()); - store_clone_1 - .insert_account(AccountDetails { - ilp_address: Some(Address::from_str("example.bob").unwrap()), - username: Username::from_str("bob").unwrap(), - asset_scale: 6, - asset_code: "XYZ".to_string(), - max_packet_amount: 1000, - min_balance: Some(-1000), - ilp_over_http_url: None, - ilp_over_http_incoming_token: None, - ilp_over_http_outgoing_token: None, - ilp_over_btp_url: None, - ilp_over_btp_outgoing_token: None, - ilp_over_btp_incoming_token: None, - settle_threshold: None, - settle_to: None, - routing_relation: Some("Peer".to_owned()), - round_trip_time: None, - amount_per_minute_limit: None, - packets_per_minute_limit: None, - settlement_engine_url: None, - }) - .and_then(move |bob| { - let routing_table = store_clone_2.routing_table(); - assert_eq!(routing_table.len(), 2); - assert_eq!(*routing_table.get("example.bob").unwrap(), bob.id(),); - let alice_id = alice.id(); - let bob_id = bob.id(); - connection - .map_err(|err| panic!(err)) - .and_then(move |connection| { - redis_crate::cmd("HMSET") - .arg("routes:current") - .arg("example.alice") - .arg(bob_id.to_string()) - .arg("example.charlie") - .arg(alice_id.to_string()) - .query_async(connection) - .and_then( - |(_connection, _result): ( - _, - redis_crate::Value, - )| { - Ok(()) - }, - ) - .map_err(|err| panic!(err)) - .and_then(|_| { - sleep(Duration::from_millis(10)).then(|_| Ok(())) - }) - }) - .and_then(move |_| { - let routing_table = store_clone_2.routing_table(); - assert_eq!(routing_table.len(), 3); - assert_eq!( - *routing_table.get("example.alice").unwrap(), - bob_id - ); - assert_eq!( - *routing_table.get("example.bob").unwrap(), - bob.id(), - ); - assert_eq!( - *routing_table.get("example.charlie").unwrap(), - alice_id, - ); - assert!(routing_table.get("example.other").is_none()); - let _ = context; - Ok(()) - }) - }) - }) - }), - ) - .unwrap(); + let store = RedisStoreBuilder::new(context.get_client_connection_info(), [0; 32]) + .poll_interval(1) + .node_ilp_address(Address::from_str("example.node").unwrap()) + .connect() + .await + .unwrap(); + + let connection = context.async_connection(); + assert_eq!(store.routing_table().len(), 0); + let store_clone_1 = store.clone(); + let store_clone_2 = store.clone(); + let alice = store + .insert_account(ACCOUNT_DETAILS_0.clone()) + .await + .unwrap(); + let routing_table = store_clone_1.routing_table(); + assert_eq!(routing_table.len(), 1); + assert_eq!(*routing_table.get("example.alice").unwrap(), alice.id()); + let bob = store_clone_1 + .insert_account(AccountDetails { + ilp_address: Some(Address::from_str("example.bob").unwrap()), + username: Username::from_str("bob").unwrap(), + asset_scale: 6, + asset_code: "XYZ".to_string(), + max_packet_amount: 1000, + min_balance: Some(-1000), + ilp_over_http_url: None, + ilp_over_http_incoming_token: None, + ilp_over_http_outgoing_token: None, + ilp_over_btp_url: None, + ilp_over_btp_outgoing_token: None, + ilp_over_btp_incoming_token: None, + settle_threshold: None, + settle_to: None, + routing_relation: Some("Peer".to_owned()), + round_trip_time: None, + amount_per_minute_limit: None, + packets_per_minute_limit: None, + settlement_engine_url: None, + }) + .await + .unwrap(); + + let routing_table = store_clone_2.routing_table(); + assert_eq!(routing_table.len(), 2); + assert_eq!(*routing_table.get("example.bob").unwrap(), bob.id(),); + let alice_id = alice.id(); + let bob_id = bob.id(); + let mut connection = connection.await.unwrap(); + let _: redis_crate::Value = redis_crate::cmd("HMSET") + .arg("routes:current") + .arg("example.alice") + .arg(bob_id.to_string()) + .arg("example.charlie") + .arg(alice_id.to_string()) + .query_async(&mut connection) + .await + .unwrap(); + + tokio::time::delay_for(Duration::from_millis(10)).await; + let routing_table = store_clone_2.routing_table(); + assert_eq!(routing_table.len(), 3); + assert_eq!(*routing_table.get("example.alice").unwrap(), bob_id); + assert_eq!(*routing_table.get("example.bob").unwrap(), bob.id(),); + assert_eq!(*routing_table.get("example.charlie").unwrap(), alice_id,); + assert!(routing_table.get("example.other").is_none()); } -#[test] -fn gets_accounts_to_send_routes_to() { - block_on(test_store().and_then(|(store, context, _accs)| { - store - .get_accounts_to_send_routes_to(Vec::new()) - .and_then(move |accounts| { - // We send to child accounts but not parents - assert_eq!(accounts[0].username().as_ref(), "bob"); - assert_eq!(accounts.len(), 1); - let _ = context; - Ok(()) - }) - })) - .unwrap() +#[tokio::test] +async fn gets_accounts_to_send_routes_to() { + let (store, _context, _) = test_store().await.unwrap(); + let accounts = store + .get_accounts_to_send_routes_to(Vec::new()) + .await + .unwrap(); + // We send to child accounts but not parents + assert_eq!(accounts[0].username().as_ref(), "bob"); + assert_eq!(accounts.len(), 1); } -#[test] -fn gets_accounts_to_send_routes_to_and_skips_ignored() { - block_on(test_store().and_then(|(store, context, accs)| { - store - .get_accounts_to_send_routes_to(vec![accs[1].id()]) - .and_then(move |accounts| { - assert!(accounts.is_empty()); - let _ = context; - Ok(()) - }) - })) - .unwrap() +#[tokio::test] +async fn gets_accounts_to_send_routes_to_and_skips_ignored() { + let (store, _context, accs) = test_store().await.unwrap(); + let accounts = store + .get_accounts_to_send_routes_to(vec![accs[1].id()]) + .await + .unwrap(); + assert!(accounts.is_empty()); } -#[test] -fn gets_accounts_to_receive_routes_from() { - block_on(test_store().and_then(|(store, context, _accs)| { - store - .get_accounts_to_receive_routes_from() - .and_then(move |accounts| { - assert_eq!( - *accounts[0].ilp_address(), - Address::from_str("example.alice").unwrap() - ); - assert_eq!(accounts.len(), 1); - let _ = context; - Ok(()) - }) - })) - .unwrap() +#[tokio::test] +async fn gets_accounts_to_receive_routes_from() { + let (store, _context, _) = test_store().await.unwrap(); + let accounts = store.get_accounts_to_receive_routes_from().await.unwrap(); + assert_eq!( + *accounts[0].ilp_address(), + Address::from_str("example.alice").unwrap() + ); } -#[test] -fn gets_local_and_configured_routes() { - block_on(test_store().and_then(|(store, context, _accs)| { - store - .get_local_and_configured_routes() - .and_then(move |(local, configured)| { - assert_eq!(local.len(), 2); - assert!(configured.is_empty()); - let _ = context; - Ok(()) - }) - })) - .unwrap() +#[tokio::test] +async fn gets_local_and_configured_routes() { + let (store, _context, _) = test_store().await.unwrap(); + let (local, configured) = store.get_local_and_configured_routes().await.unwrap(); + assert_eq!(local.len(), 2); + assert!(configured.is_empty()); } -#[test] -fn saves_routes_to_db() { - block_on(test_store().and_then(|(mut store, context, _accs)| { - let get_connection = context.async_connection(); - let account0_id = Uuid::new_v4(); - let account1_id = Uuid::new_v4(); - let account0 = Account::try_from( - account0_id, - ACCOUNT_DETAILS_0.clone(), - store.get_ilp_address(), - ) +#[tokio::test] +async fn saves_routes_to_db() { + let (store, context, _) = test_store().await.unwrap(); + let get_connection = context.async_connection(); + let account0_id = Uuid::new_v4(); + let account1_id = Uuid::new_v4(); + let account0 = Account::try_from( + account0_id, + ACCOUNT_DETAILS_0.clone(), + store.get_ilp_address(), + ) + .unwrap(); + + let account1 = Account::try_from( + account1_id, + ACCOUNT_DETAILS_1.clone(), + store.get_ilp_address(), + ) + .unwrap(); + + store + .clone() + .set_routes(vec![ + ("example.a".to_string(), account0.clone()), + ("example.b".to_string(), account0.clone()), + ("example.c".to_string(), account1.clone()), + ]) + .await .unwrap(); - let account1 = Account::try_from( - account1_id, - ACCOUNT_DETAILS_1.clone(), - store.get_ilp_address(), - ) + let mut connection = get_connection.await.unwrap(); + let routes: HashMap = redis_crate::cmd("HGETALL") + .arg("routes:current") + .query_async(&mut connection) + .await .unwrap(); + assert_eq!(routes["example.a"], account0_id.to_string()); + assert_eq!(routes["example.b"], account0_id.to_string()); + assert_eq!(routes["example.c"], account1_id.to_string()); + assert_eq!(routes.len(), 3); - store - .set_routes(vec![ - ("example.a".to_string(), account0.clone()), - ("example.b".to_string(), account0.clone()), - ("example.c".to_string(), account1.clone()), - ]) - .and_then(move |_| { - get_connection.and_then(move |connection| { - redis_crate::cmd("HGETALL") - .arg("routes:current") - .query_async(connection) - .map_err(|err| panic!(err)) - .and_then(move |(_conn, routes): (_, HashMap)| { - assert_eq!(routes["example.a"], account0_id.to_string()); - assert_eq!(routes["example.b"], account0_id.to_string()); - assert_eq!(routes["example.c"], account1_id.to_string()); - assert_eq!(routes.len(), 3); - Ok(()) - }) - }) - }) - .and_then(move |_| { - let _ = context; - Ok(()) - }) - })) - .unwrap() + // local routing table routes are also updated + let routes = store.routing_table(); + assert_eq!(routes["example.a"], account0_id); + assert_eq!(routes["example.b"], account0_id); + assert_eq!(routes["example.c"], account1_id); + assert_eq!(routes.len(), 3); } -#[test] -fn updates_local_routes() { - block_on(test_store().and_then(|(store, context, _accs)| { - let account0_id = Uuid::new_v4(); - let account1_id = Uuid::new_v4(); - let account0 = Account::try_from( - account0_id, - ACCOUNT_DETAILS_0.clone(), - store.get_ilp_address(), - ) +#[tokio::test] +async fn adds_static_routes_to_redis() { + let (store, context, accs) = test_store().await.unwrap(); + let get_connection = context.async_connection(); + store + .set_static_routes(vec![ + ("example.a".to_string(), accs[0].id()), + ("example.b".to_string(), accs[0].id()), + ("example.c".to_string(), accs[1].id()), + ]) + .await .unwrap(); - let account1 = Account::try_from( - account1_id, - ACCOUNT_DETAILS_1.clone(), - store.get_ilp_address(), - ) + let mut connection = get_connection.await.unwrap(); + let routes: HashMap = redis_crate::cmd("HGETALL") + .arg("routes:static") + .query_async(&mut connection) + .await .unwrap(); - store - .clone() - .set_routes(vec![ - ("example.a".to_string(), account0.clone()), - ("example.b".to_string(), account0.clone()), - ("example.c".to_string(), account1.clone()), - ]) - .and_then(move |_| { - let routes = store.routing_table(); - assert_eq!(routes["example.a"], account0_id); - assert_eq!(routes["example.b"], account0_id); - assert_eq!(routes["example.c"], account1_id); - assert_eq!(routes.len(), 3); - Ok(()) - }) - .and_then(move |_| { - let _ = context; - Ok(()) - }) - })) - .unwrap() + assert_eq!(routes["example.a"], accs[0].id().to_string()); + assert_eq!(routes["example.b"], accs[0].id().to_string()); + assert_eq!(routes["example.c"], accs[1].id().to_string()); + assert_eq!(routes.len(), 3); } -#[test] -fn adds_static_routes_to_redis() { - block_on(test_store().and_then(|(store, context, accs)| { - let get_connection = context.async_connection(); - store - .clone() - .set_static_routes(vec![ - ("example.a".to_string(), accs[0].id()), - ("example.b".to_string(), accs[0].id()), - ("example.c".to_string(), accs[1].id()), - ]) - .and_then(move |_| { - get_connection.and_then(|connection| { - redis_crate::cmd("HGETALL") - .arg("routes:static") - .query_async(connection) - .map_err(|err| panic!(err)) - .and_then(move |(_, routes): (_, HashMap)| { - assert_eq!(routes["example.a"], accs[0].id().to_string()); - assert_eq!(routes["example.b"], accs[0].id().to_string()); - assert_eq!(routes["example.c"], accs[1].id().to_string()); - assert_eq!(routes.len(), 3); - let _ = context; - Ok(()) - }) - }) - }) - })) - .unwrap() -} +#[tokio::test] +async fn static_routes_override_others() { + let (store, _context, accs) = test_store().await.unwrap(); + store + .set_static_routes(vec![ + ("example.a".to_string(), accs[0].id()), + ("example.b".to_string(), accs[0].id()), + ]) + .await + .unwrap(); + + let account1_id = Uuid::new_v4(); + let account1 = Account::try_from( + account1_id, + ACCOUNT_DETAILS_1.clone(), + store.get_ilp_address(), + ) + .unwrap(); + store + .clone() + .set_routes(vec![ + ("example.a".to_string(), account1.clone()), + ("example.b".to_string(), account1.clone()), + ("example.c".to_string(), account1), + ]) + .await + .unwrap(); -#[test] -fn static_routes_override_others() { - block_on(test_store().and_then(|(store, context, accs)| { - let mut store_clone = store.clone(); - store - .clone() - .set_static_routes(vec![ - ("example.a".to_string(), accs[0].id()), - ("example.b".to_string(), accs[0].id()), - ]) - .and_then(move |_| { - let account1_id = Uuid::new_v4(); - let account1 = Account::try_from( - account1_id, - ACCOUNT_DETAILS_1.clone(), - store.get_ilp_address(), - ) - .unwrap(); - store_clone - .set_routes(vec![ - ("example.a".to_string(), account1.clone()), - ("example.b".to_string(), account1.clone()), - ("example.c".to_string(), account1), - ]) - .and_then(move |_| { - let routes = store.routing_table(); - assert_eq!(routes["example.a"], accs[0].id()); - assert_eq!(routes["example.b"], accs[0].id()); - assert_eq!(routes["example.c"], account1_id); - assert_eq!(routes.len(), 3); - let _ = context; - Ok(()) - }) - }) - })) - .unwrap() + let routes = store.routing_table(); + assert_eq!(routes["example.a"], accs[0].id()); + assert_eq!(routes["example.b"], accs[0].id()); + assert_eq!(routes["example.c"], account1_id); + assert_eq!(routes.len(), 3); } -#[test] -fn default_route() { - block_on(test_store().and_then(|(store, context, accs)| { - let mut store_clone = store.clone(); - store - .clone() - .set_default_route(accs[0].id()) - .and_then(move |_| { - let account1_id = Uuid::new_v4(); - let account1 = Account::try_from( - account1_id, - ACCOUNT_DETAILS_1.clone(), - store.get_ilp_address(), - ) - .unwrap(); - store_clone - .set_routes(vec![ - ("example.a".to_string(), account1.clone()), - ("example.b".to_string(), account1.clone()), - ]) - .and_then(move |_| { - let routes = store.routing_table(); - assert_eq!(routes[""], accs[0].id()); - assert_eq!(routes["example.a"], account1_id); - assert_eq!(routes["example.b"], account1_id); - assert_eq!(routes.len(), 3); - let _ = context; - Ok(()) - }) - }) - })) - .unwrap() +#[tokio::test] +async fn default_route() { + let (store, _context, accs) = test_store().await.unwrap(); + store.set_default_route(accs[0].id()).await.unwrap(); + let account1_id = Uuid::new_v4(); + let account1 = Account::try_from( + account1_id, + ACCOUNT_DETAILS_1.clone(), + store.get_ilp_address(), + ) + .unwrap(); + store + .clone() + .set_routes(vec![ + ("example.a".to_string(), account1.clone()), + ("example.b".to_string(), account1.clone()), + ]) + .await + .unwrap(); + + let routes = store.routing_table(); + assert_eq!(routes[""], accs[0].id()); + assert_eq!(routes["example.a"], account1_id); + assert_eq!(routes["example.b"], account1_id); + assert_eq!(routes.len(), 3); } -#[test] -fn returns_configured_routes_for_route_manager() { - block_on(test_store().and_then(|(store, context, accs)| { - store - .clone() - .set_static_routes(vec![ - ("example.a".to_string(), accs[0].id()), - ("example.b".to_string(), accs[1].id()), - ]) - .and_then(move |_| store.get_local_and_configured_routes()) - .and_then(move |(_local, configured)| { - assert_eq!(configured.len(), 2); - assert_eq!(configured["example.a"].id(), accs[0].id()); - assert_eq!(configured["example.b"].id(), accs[1].id()); - let _ = context; - Ok(()) - }) - })) - .unwrap() +#[tokio::test] +async fn returns_configured_routes_for_route_manager() { + let (store, _context, accs) = test_store().await.unwrap(); + store + .set_static_routes(vec![ + ("example.a".to_string(), accs[0].id()), + ("example.b".to_string(), accs[1].id()), + ]) + .await + .unwrap(); + let (_, configured) = store.get_local_and_configured_routes().await.unwrap(); + assert_eq!(configured.len(), 2); + assert_eq!(configured["example.a"].id(), accs[0].id()); + assert_eq!(configured["example.b"].id(), accs[1].id()); } diff --git a/crates/interledger-store/tests/redis/settlement_test.rs b/crates/interledger-store/tests/redis/settlement_test.rs index 0d1112215..15ea4c25c 100644 --- a/crates/interledger-store/tests/redis/settlement_test.rs +++ b/crates/interledger-store/tests/redis/settlement_test.rs @@ -1,6 +1,6 @@ use super::store_helpers::*; use bytes::Bytes; -use futures::future::{join_all, Future}; + use http::StatusCode; use interledger_api::NodeStore; use interledger_service::{Account, AccountStore}; @@ -10,7 +10,7 @@ use interledger_settlement::core::{ }; use lazy_static::lazy_static; use num_bigint::BigUint; -use redis_crate::{aio::SharedConnection, cmd}; +use redis_crate::cmd; use url::Url; use uuid::Uuid; @@ -18,387 +18,250 @@ lazy_static! { static ref IDEMPOTENCY_KEY: String = String::from("AJKJNUjM0oyiAN46"); } -#[test] -fn saves_and_gets_uncredited_settlement_amount_properly() { - block_on(test_store().and_then(|(store, context, _accs)| { - let amounts = vec![ - (BigUint::from(5u32), 11), // 5 - (BigUint::from(855u32), 12), // 905 - (BigUint::from(1u32), 10), // 1005 total - ]; - let acc = Uuid::new_v4(); - let mut f = Vec::new(); - for a in amounts { - let s = store.clone(); - f.push(s.save_uncredited_settlement_amount(acc, a)); - } - join_all(f) - .map_err(|err| eprintln!("Redis error: {:?}", err)) - .and_then(move |_| { - store - .load_uncredited_settlement_amount(acc, 9) - .map_err(|err| eprintln!("Redis error: {:?}", err)) - .and_then(move |ret| { - // 1 uncredited unit for scale 9 - assert_eq!(ret, BigUint::from(1u32)); - // rest should be in the leftovers store - store - .get_uncredited_settlement_amount(acc) - .map_err(|err| eprintln!("Redis error: {:?}", err)) - .and_then(move |ret| { - // 1 uncredited unit for scale 9 - assert_eq!(ret, (BigUint::from(5u32), 12)); - let _ = context; - Ok(()) - }) - }) - }) - })) - .unwrap() -} +#[tokio::test] +async fn saves_gets_clears_uncredited_settlement_amount_properly() { + let (store, _context, _accs) = test_store().await.unwrap(); + let amounts: Vec<(BigUint, u8)> = vec![ + (BigUint::from(5u32), 11), // 5 + (BigUint::from(855u32), 12), // 905 + (BigUint::from(1u32), 10), // 1005 total + ]; + let acc = Uuid::new_v4(); + for a in amounts { + let s = store.clone(); + s.save_uncredited_settlement_amount(acc, a).await.unwrap(); + } + let ret = store + .load_uncredited_settlement_amount(acc, 9u8) + .await + .unwrap(); + // 1 uncredited unit for scale 9 + assert_eq!(ret, BigUint::from(1u32)); + // rest should be in the leftovers store + let ret = store.get_uncredited_settlement_amount(acc).await.unwrap(); + // 1 uncredited unit for scale 9 + assert_eq!(ret, (BigUint::from(5u32), 12)); -#[test] -fn clears_uncredited_settlement_amount_properly() { - block_on(test_store().and_then(|(store, context, _accs)| { - let amounts = vec![ - (BigUint::from(5u32), 11), // 5 - (BigUint::from(855u32), 12), // 905 - (BigUint::from(1u32), 10), // 1005 total - ]; - let acc = Uuid::new_v4(); - let mut f = Vec::new(); - for a in amounts { - let s = store.clone(); - f.push(s.save_uncredited_settlement_amount(acc, a)); - } - join_all(f) - .map_err(|err| eprintln!("Redis error: {:?}", err)) - .and_then(move |_| { - store - .clear_uncredited_settlement_amount(acc) - .map_err(|err| eprintln!("Redis error: {:?}", err)) - .and_then(move |_| { - store - .get_uncredited_settlement_amount(acc) - .map_err(|err| eprintln!("Redis error: {:?}", err)) - .and_then(move |amount| { - assert_eq!(amount, (BigUint::from(0u32), 0)); - let _ = context; - Ok(()) - }) - }) - }) - })) - .unwrap() + // clears uncredited amount + store.clear_uncredited_settlement_amount(acc).await.unwrap(); + let ret = store.get_uncredited_settlement_amount(acc).await.unwrap(); + assert_eq!(ret, (BigUint::from(0u32), 0)); } -#[test] -fn credits_prepaid_amount() { - block_on(test_store().and_then(|(store, context, accs)| { - let id = accs[0].id(); - context.async_connection().and_then(move |conn| { - store - .update_balance_for_incoming_settlement(id, 100, Some(IDEMPOTENCY_KEY.clone())) - .and_then(move |_| { - cmd("HMGET") - .arg(format!("accounts:{}", id)) - .arg("balance") - .arg("prepaid_amount") - .query_async(conn) - .map_err(|err| eprintln!("Redis error: {:?}", err)) - .and_then(move |(_conn, (balance, prepaid_amount)): (_, (i64, i64))| { - assert_eq!(balance, 0); - assert_eq!(prepaid_amount, 100); - let _ = context; - Ok(()) - }) - }) - }) - })) - .unwrap() +#[tokio::test] +async fn credits_prepaid_amount() { + let (store, context, accs) = test_store().await.unwrap(); + let id = accs[0].id(); + let mut conn = context.async_connection().await.unwrap(); + store + .update_balance_for_incoming_settlement(id, 100, Some(IDEMPOTENCY_KEY.clone())) + .await + .unwrap(); + let (balance, prepaid_amount): (i64, i64) = cmd("HMGET") + .arg(format!("accounts:{}", id)) + .arg("balance") + .arg("prepaid_amount") + .query_async(&mut conn) + .await + .unwrap(); + assert_eq!(balance, 0); + assert_eq!(prepaid_amount, 100); } -#[test] -fn saves_and_loads_idempotency_key_data_properly() { - block_on(test_store().and_then(|(store, context, _accs)| { - let input_hash: [u8; 32] = Default::default(); - store - .save_idempotent_data( - IDEMPOTENCY_KEY.clone(), - input_hash, - StatusCode::OK, - Bytes::from("TEST"), - ) - .map_err(|err| eprintln!("Redis error: {:?}", err)) - .and_then(move |_| { - store - .load_idempotent_data(IDEMPOTENCY_KEY.clone()) - .map_err(|err| eprintln!("Redis error: {:?}", err)) - .and_then(move |data1| { - assert_eq!( - data1.unwrap(), - IdempotentData::new(StatusCode::OK, Bytes::from("TEST"), input_hash) - ); - let _ = context; +#[tokio::test] +async fn saves_and_loads_idempotency_key_data_properly() { + let (store, _context, _) = test_store().await.unwrap(); + let input_hash: [u8; 32] = Default::default(); + store + .save_idempotent_data( + IDEMPOTENCY_KEY.clone(), + input_hash, + StatusCode::OK, + Bytes::from("TEST"), + ) + .await + .unwrap(); + let data1 = store + .load_idempotent_data(IDEMPOTENCY_KEY.clone()) + .await + .unwrap(); + assert_eq!( + data1.unwrap(), + IdempotentData::new(StatusCode::OK, Bytes::from("TEST"), input_hash) + ); - store - .load_idempotent_data("asdf".to_string()) - .map_err(|err| eprintln!("Redis error: {:?}", err)) - .and_then(move |data2| { - assert!(data2.is_none()); - let _ = context; - Ok(()) - }) - }) - }) - })) - .unwrap(); + let data2 = store + .load_idempotent_data("asdf".to_string()) + .await + .unwrap(); + assert!(data2.is_none()); } -#[test] -fn idempotent_settlement_calls() { - block_on(test_store().and_then(|(store, context, accs)| { - let id = accs[0].id(); - context.async_connection().and_then(move |conn| { - store - .update_balance_for_incoming_settlement(id, 100, Some(IDEMPOTENCY_KEY.clone())) - .and_then(move |_| { - cmd("HMGET") - .arg(format!("accounts:{}", id)) - .arg("balance") - .arg("prepaid_amount") - .query_async(conn) - .map_err(|err| eprintln!("Redis error: {:?}", err)) - .and_then(move |(conn, (balance, prepaid_amount)): (_, (i64, i64))| { - assert_eq!(balance, 0); - assert_eq!(prepaid_amount, 100); +#[tokio::test] +async fn idempotent_settlement_calls() { + let (store, context, accs) = test_store().await.unwrap(); + let id = accs[0].id(); + let mut conn = context.async_connection().await.unwrap(); + store + .update_balance_for_incoming_settlement(id, 100, Some(IDEMPOTENCY_KEY.clone())) + .await + .unwrap(); + let (balance, prepaid_amount): (i64, i64) = cmd("HMGET") + .arg(format!("accounts:{}", id)) + .arg("balance") + .arg("prepaid_amount") + .query_async(&mut conn) + .await + .unwrap(); + assert_eq!(balance, 0); + assert_eq!(prepaid_amount, 100); - store - .update_balance_for_incoming_settlement( - id, - 100, - Some(IDEMPOTENCY_KEY.clone()), // Reuse key to make idempotent request. - ) - .and_then(move |_| { - cmd("HMGET") - .arg(format!("accounts:{}", id)) - .arg("balance") - .arg("prepaid_amount") - .query_async(conn) - .map_err(|err| eprintln!("Redis error: {:?}", err)) - .and_then( - move |(_conn, (balance, prepaid_amount)): ( - _, - (i64, i64), - )| { - // Since it's idempotent there - // will be no state update. - // Otherwise it'd be 200 (100 + 100) - assert_eq!(balance, 0); - assert_eq!(prepaid_amount, 100); - let _ = context; - Ok(()) - }, - ) - }) - }) - }) - }) - })) - .unwrap() + store + .update_balance_for_incoming_settlement( + id, + 100, + Some(IDEMPOTENCY_KEY.clone()), // Reuse key to make idempotent request. + ) + .await + .unwrap(); + let (balance, prepaid_amount): (i64, i64) = cmd("HMGET") + .arg(format!("accounts:{}", id)) + .arg("balance") + .arg("prepaid_amount") + .query_async(&mut conn) + .await + .unwrap(); + // Since it's idempotent there + // will be no state update. + // Otherwise it'd be 200 (100 + 100) + assert_eq!(balance, 0); + assert_eq!(prepaid_amount, 100); } -#[test] -fn credits_balance_owed() { - block_on(test_store().and_then(|(store, context, accs)| { - let id = accs[0].id(); - context - .shared_async_connection() - .map_err(|err| panic!(err)) - .and_then(move |conn| { - cmd("HSET") - .arg(format!("accounts:{}", id)) - .arg("balance") - .arg(-200) - .query_async(conn) - .map_err(|err| panic!(err)) - .and_then(move |(conn, _balance): (SharedConnection, i64)| { - store - .update_balance_for_incoming_settlement( - id, - 100, - Some(IDEMPOTENCY_KEY.clone()), - ) - .and_then(move |_| { - cmd("HMGET") - .arg(format!("accounts:{}", id)) - .arg("balance") - .arg("prepaid_amount") - .query_async(conn) - .map_err(|err| panic!(err)) - .and_then( - move |(_conn, (balance, prepaid_amount)): ( - _, - (i64, i64), - )| { - assert_eq!(balance, -100); - assert_eq!(prepaid_amount, 0); - let _ = context; - Ok(()) - }, - ) - }) - }) - }) - })) - .unwrap() +#[tokio::test] +async fn credits_balance_owed() { + let (store, context, accs) = test_store().await.unwrap(); + let id = accs[0].id(); + let mut conn = context.shared_async_connection().await.unwrap(); + let _balance: i64 = cmd("HSET") + .arg(format!("accounts:{}", id)) + .arg("balance") + .arg(-200i64) + .query_async(&mut conn) + .await + .unwrap(); + store + .update_balance_for_incoming_settlement(id, 100, Some(IDEMPOTENCY_KEY.clone())) + .await + .unwrap(); + let (balance, prepaid_amount): (i64, i64) = cmd("HMGET") + .arg(format!("accounts:{}", id)) + .arg("balance") + .arg("prepaid_amount") + .query_async(&mut conn) + .await + .unwrap(); + assert_eq!(balance, -100); + assert_eq!(prepaid_amount, 0); } -#[test] -fn clears_balance_owed() { - block_on(test_store().and_then(|(store, context, accs)| { - let id = accs[0].id(); - context - .shared_async_connection() - .map_err(|err| panic!(err)) - .and_then(move |conn| { - cmd("HSET") - .arg(format!("accounts:{}", id)) - .arg("balance") - .arg(-100) - .query_async(conn) - .map_err(|err| panic!(err)) - .and_then(move |(conn, _balance): (SharedConnection, i64)| { - store - .update_balance_for_incoming_settlement( - id, - 100, - Some(IDEMPOTENCY_KEY.clone()), - ) - .and_then(move |_| { - cmd("HMGET") - .arg(format!("accounts:{}", id)) - .arg("balance") - .arg("prepaid_amount") - .query_async(conn) - .map_err(|err| panic!(err)) - .and_then( - move |(_conn, (balance, prepaid_amount)): ( - _, - (i64, i64), - )| { - assert_eq!(balance, 0); - assert_eq!(prepaid_amount, 0); - let _ = context; - Ok(()) - }, - ) - }) - }) - }) - })) - .unwrap() +#[tokio::test] +async fn clears_balance_owed() { + let (store, context, accs) = test_store().await.unwrap(); + let id = accs[0].id(); + let mut conn = context.shared_async_connection().await.unwrap(); + let _balance: i64 = cmd("HSET") + .arg(format!("accounts:{}", id)) + .arg("balance") + .arg(-100i64) + .query_async(&mut conn) + .await + .unwrap(); + store + .update_balance_for_incoming_settlement(id, 100, Some(IDEMPOTENCY_KEY.clone())) + .await + .unwrap(); + let (balance, prepaid_amount): (i64, i64) = cmd("HMGET") + .arg(format!("accounts:{}", id)) + .arg("balance") + .arg("prepaid_amount") + .query_async(&mut conn) + .await + .unwrap(); + assert_eq!(balance, 0); + assert_eq!(prepaid_amount, 0); } -#[test] -fn clears_balance_owed_and_puts_remainder_as_prepaid() { - block_on(test_store().and_then(|(store, context, accs)| { - let id = accs[0].id(); - context - .shared_async_connection() - .map_err(|err| panic!(err)) - .and_then(move |conn| { - cmd("HSET") - .arg(format!("accounts:{}", id)) - .arg("balance") - .arg(-40) - .query_async(conn) - .map_err(|err| panic!(err)) - .and_then(move |(conn, _balance): (SharedConnection, i64)| { - store - .update_balance_for_incoming_settlement( - id, - 100, - Some(IDEMPOTENCY_KEY.clone()), - ) - .and_then(move |_| { - cmd("HMGET") - .arg(format!("accounts:{}", id)) - .arg("balance") - .arg("prepaid_amount") - .query_async(conn) - .map_err(|err| panic!(err)) - .and_then( - move |(_conn, (balance, prepaid_amount)): ( - _, - (i64, i64), - )| { - assert_eq!(balance, 0); - assert_eq!(prepaid_amount, 60); - let _ = context; - Ok(()) - }, - ) - }) - }) - }) - })) - .unwrap() +#[tokio::test] +async fn clears_balance_owed_and_puts_remainder_as_prepaid() { + let (store, context, accs) = test_store().await.unwrap(); + let id = accs[0].id(); + let mut conn = context.shared_async_connection().await.unwrap(); + let _balance: i64 = cmd("HSET") + .arg(format!("accounts:{}", id)) + .arg("balance") + .arg(-40) + .query_async(&mut conn) + .await + .unwrap(); + store + .update_balance_for_incoming_settlement(id, 100, Some(IDEMPOTENCY_KEY.clone())) + .await + .unwrap(); + let (balance, prepaid_amount): (i64, i64) = cmd("HMGET") + .arg(format!("accounts:{}", id)) + .arg("balance") + .arg("prepaid_amount") + .query_async(&mut conn) + .await + .unwrap(); + assert_eq!(balance, 0); + assert_eq!(prepaid_amount, 60); } -#[test] -fn loads_globally_configured_settlement_engine_url() { - block_on(test_store().and_then(|(store, context, accs)| { - assert!(accs[0].settlement_engine_details().is_some()); - assert!(accs[1].settlement_engine_details().is_none()); - let account_ids = vec![accs[0].id(), accs[1].id()]; - store - .clone() - .get_accounts(account_ids.clone()) - .and_then(move |accounts| { - assert!(accounts[0].settlement_engine_details().is_some()); - assert!(accounts[1].settlement_engine_details().is_none()); +#[tokio::test] +async fn loads_globally_configured_settlement_engine_url() { + let (store, _context, accs) = test_store().await.unwrap(); + assert!(accs[0].settlement_engine_details().is_some()); + assert!(accs[1].settlement_engine_details().is_none()); + let account_ids = vec![accs[0].id(), accs[1].id()]; + let accounts = store.get_accounts(account_ids.clone()).await.unwrap(); + assert!(accounts[0].settlement_engine_details().is_some()); + assert!(accounts[1].settlement_engine_details().is_none()); - store - .clone() - .set_settlement_engines(vec![ - ( - "ABC".to_string(), - Url::parse("http://settle-abc.example").unwrap(), - ), - ( - "XYZ".to_string(), - Url::parse("http://settle-xyz.example").unwrap(), - ), - ]) - .and_then(move |_| { - store.get_accounts(account_ids).and_then(move |accounts| { - // It should not overwrite the one that was individually configured - assert_eq!( - accounts[0] - .settlement_engine_details() - .unwrap() - .url - .as_str(), - "http://settlement.example/" - ); + store + .clone() + .set_settlement_engines(vec![ + ( + "ABC".to_string(), + Url::parse("http://settle-abc.example").unwrap(), + ), + ( + "XYZ".to_string(), + Url::parse("http://settle-xyz.example").unwrap(), + ), + ]) + .await + .unwrap(); + let accounts = store.get_accounts(account_ids).await.unwrap(); + // It should not overwrite the one that was individually configured + assert_eq!( + accounts[0] + .settlement_engine_details() + .unwrap() + .url + .as_str(), + "http://settlement.example/" + ); - // It should set the URL for the account that did not have one configured - assert!(accounts[1].settlement_engine_details().is_some()); - assert_eq!( - accounts[1] - .settlement_engine_details() - .unwrap() - .url - .as_str(), - "http://settle-abc.example/" - ); - let _ = context; - Ok(()) - }) - }) - // store.set_settlement_engines - }) - })) - .unwrap() + // It should set the URL for the account that did not have one configured + assert!(accounts[1].settlement_engine_details().is_some()); + assert_eq!( + accounts[1] + .settlement_engine_details() + .unwrap() + .url + .as_str(), + "http://settle-abc.example/" + ); } diff --git a/crates/interledger-stream/Cargo.toml b/crates/interledger-stream/Cargo.toml index 62c8fb064..e06d87fda 100644 --- a/crates/interledger-stream/Cargo.toml +++ b/crates/interledger-stream/Cargo.toml @@ -18,7 +18,7 @@ byteorder = { version = "1.3.2", default-features = false } chrono = { version = "0.4.9", default-features = false, features = ["clock"] } csv = { version = "1.1.1", default-features = false, optional = true } failure = { version = "0.1.5", default-features = false, features = ["derive"] } -futures = { version = "0.1.29", default-features = false } +futures = { version = "0.3.1", default-features = false } hex = { version = "0.4.0", default-features = false } interledger-ildcp = { path = "../interledger-ildcp", version = "^0.4.0", default-features = false } interledger-packet = { path = "../interledger-packet", version = "^0.4.0", features = ["serde"], default-features = false } @@ -27,8 +27,10 @@ log = { version = "0.4.8", default-features = false } parking_lot = { version = "0.9.0", default-features = false } ring = { version = "0.16.9", default-features = false } serde = { version = "1.0.101", default-features = false } -tokio = { version = "0.1.22", default-features = false, features = ["rt-full"] } +tokio = { version = "^0.2.6", default-features = false, features = ["rt-core", "macros"] } uuid = { version = "0.8.1", default-features = false, features = ["v4"] } +async-trait = "0.1.22" +pin-project = "0.4.7" [dev-dependencies] interledger-router = { path = "../interledger-router", version = "^0.4.0", default-features = false } diff --git a/crates/interledger-stream/src/client.rs b/crates/interledger-stream/src/client.rs index 4a5502316..2e4e68281 100644 --- a/crates/interledger-stream/src/client.rs +++ b/crates/interledger-stream/src/client.rs @@ -4,7 +4,7 @@ use super::error::Error; use super::packet::*; use bytes::Bytes; use bytes::BytesMut; -use futures::{Async, Future, Poll}; +use futures::{ready, TryFutureExt}; use interledger_ildcp::get_ildcp_info; use interledger_packet::{ Address, ErrorClass, ErrorCode as IlpErrorCode, Fulfill, PacketType as IlpPacketType, @@ -12,6 +12,7 @@ use interledger_packet::{ }; use interledger_service::*; use log::{debug, error, warn}; +use pin_project::{pin_project, project}; use serde::{Deserialize, Serialize}; use std::{ cell::Cell, @@ -19,27 +20,43 @@ use std::{ str, time::{Duration, Instant, SystemTime}, }; +use std::{ + future::Future, + pin::Pin, + task::{Context, Poll}, +}; -// Maximum time we should wait since last fulfill before we error out to avoid -// getting into an infinite loop of sending packets and effectively DoSing ourselves +/// Maximum time we should wait since last fulfill before we error out to avoid +/// getting into an infinite loop of sending packets and effectively DoSing ourselves const MAX_TIME_SINCE_LAST_FULFILL: Duration = Duration::from_secs(30); +/// Metadata about a completed STREAM payment #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] pub struct StreamDelivery { + /// The sender's ILP Address pub from: Address, + /// The receiver's ILP Address pub to: Address, // StreamDelivery variables which we know ahead of time + /// The amount sent by the sender pub sent_amount: u64, + /// The sender's asset scale pub sent_asset_scale: u8, + /// The sender's asset code pub sent_asset_code: String, + /// The amount delivered to the receiver pub delivered_amount: u64, // StreamDelivery variables which may get updated if the receiver sends us a // ConnectionAssetDetails frame. + /// The asset scale delivered to the receiver + /// (this may change depending on the granularity of accounts across nodes) pub delivered_asset_scale: Option, + /// The asset code delivered to the receiver (this may happen in cross-currency payments) pub delivered_asset_code: Option, } impl StreamDelivery { + /// Increases the `StreamDelivery`'s [`delivered_amount`](./struct.StreamDelivery.html#structfield.delivered_amount) by `amount` fn increment_delivered_amount(&mut self, amount: u64) { self.delivered_amount += amount; } @@ -48,111 +65,134 @@ impl StreamDelivery { /// Send a given amount of money using the STREAM transport protocol. /// /// This returns the amount delivered, as reported by the receiver and in the receiver's asset's units. -pub fn send_money( +pub async fn send_money( service: S, from_account: &A, destination_account: Address, shared_secret: &[u8], source_amount: u64, -) -> impl Future +) -> Result<(StreamDelivery, S), Error> where - S: IncomingService + Clone, - A: Account, + S: IncomingService + Send + Sync + Clone + 'static, + A: Account + Send + Sync + 'static, { let shared_secret = Bytes::from(shared_secret); let from_account = from_account.clone(); // TODO can/should we avoid cloning the account? - get_ildcp_info(&mut service.clone(), from_account.clone()) + let account_details = get_ildcp_info(&mut service.clone(), from_account.clone()) .map_err(|_err| Error::ConnectionError("Unable to get ILDCP info: {:?}".to_string())) - .and_then(move |account_details| { - let source_account = account_details.ilp_address(); - if source_account.scheme() != destination_account.scheme() { - warn!("Destination ILP address starts with a different scheme prefix (\"{}\') than ours (\"{}\'), this probably isn't going to work", - destination_account.scheme(), - source_account.scheme()); - } + .await?; - SendMoneyFuture { - state: SendMoneyFutureState::SendMoney, - next: Some(service), - from_account: from_account.clone(), - source_account, - destination_account: destination_account.clone(), - shared_secret, - source_amount, - // Try sending the full amount first - // TODO make this configurable -- in different scenarios you might prioritize - // sending as much as possible per packet vs getting money flowing ASAP differently - congestion_controller: CongestionController::new(source_amount, source_amount / 10, 2.0), - pending_requests: Cell::new(Vec::new()), - receipt: StreamDelivery { - from: from_account.ilp_address().clone(), - to: destination_account, - sent_amount: source_amount, - sent_asset_scale: from_account.asset_scale(), - sent_asset_code: from_account.asset_code().to_string(), - delivered_asset_scale: None, - delivered_asset_code: None, - delivered_amount: 0, - }, - should_send_source_account: true, - sequence: 1, - rejected_packets: 0, - error: None, - last_fulfill_time: Instant::now(), - } - }) + let source_account = account_details.ilp_address(); + if source_account.scheme() != destination_account.scheme() { + warn!("Destination ILP address starts with a different scheme prefix (\"{}\') than ours (\"{}\'), this probably isn't going to work", + destination_account.scheme(), + source_account.scheme()); + } + + SendMoneyFuture { + state: SendMoneyFutureState::SendMoney, + next: Some(service), + from_account: from_account.clone(), + source_account, + destination_account: destination_account.clone(), + shared_secret, + source_amount, + // Try sending the full amount first + // TODO make this configurable -- in different scenarios you might prioritize + // sending as much as possible per packet vs getting money flowing ASAP differently + congestion_controller: CongestionController::new(source_amount, source_amount / 10, 2.0), + pending_requests: Cell::new(Vec::new()), + receipt: StreamDelivery { + from: from_account.ilp_address().clone(), + to: destination_account, + sent_amount: source_amount, + sent_asset_scale: from_account.asset_scale(), + sent_asset_code: from_account.asset_code().to_string(), + delivered_asset_scale: None, + delivered_asset_code: None, + delivered_amount: 0, + }, + should_send_source_account: true, + sequence: 1, + rejected_packets: 0, + error: None, + last_fulfill_time: Instant::now(), + } + .await } +#[pin_project] +/// Helper data type used to track a streaming payment struct SendMoneyFuture, A: Account> { + /// The future's [state](./enum.SendMoneyFutureState.html) state: SendMoneyFutureState, + /// The next service which will receive the Stream packet next: Option, + /// The account sending the STREAM payment from_account: A, + /// The ILP Address of the account sending the payment source_account: Address, + /// The ILP Address of the account receiving the payment destination_account: Address, + /// The shared secret generated by the sender and the receiver shared_secret: Bytes, + /// The amount sent by the sender source_amount: u64, + /// The [congestion controller](./../congestion/struct.CongestionController.html) for this stream congestion_controller: CongestionController, + /// STREAM packets we have sent and have not received responses yet for pending_requests: Cell>, + /// The [StreamDelivery](./struct.StreamDelivery.html) receipt of this stream receipt: StreamDelivery, + /// Boolean indicating if the source account should also be sent to the receiver should_send_source_account: bool, + /// The sequence number of this stream sequence: u64, + /// The amount of rejected packets by the stream rejected_packets: u64, + /// The STREAM error for this stream error: Option, + /// The last time a packet was fulfilled for this stream. last_fulfill_time: Instant, } struct PendingRequest { sequence: u64, amount: u64, - future: BoxedIlpFuture, + future: Pin + Send>>, } +/// The state of the send money future #[derive(PartialEq)] enum SendMoneyFutureState { - SendMoney, + /// Initial state of the future + SendMoney = 0, + /// Once the stream has been finished, it transitions to this state and tries to send a + /// ConnectionCloseFrame Closing, - // RemoteClosed, + /// The connection is now closed and the send_money function can return Closed, } +#[project] impl SendMoneyFuture where - S: IncomingService, - A: Account, + S: IncomingService + Send + Sync + Clone + 'static, + A: Account + Send + Sync + 'static, { + /// Fire off requests until the congestion controller tells us to stop or we've sent the total amount or maximum time since last fulfill has elapsed fn try_send_money(&mut self) -> Result { - // Fire off requests until the congestion controller tells us to stop or we've sent the total amount or maximum time since last fulfill has elapsed let mut sent_packets = false; loop { let amount = min( - self.source_amount, + *self.source_amount, self.congestion_controller.get_max_amount(), ); if amount == 0 { break; } - self.source_amount -= amount; + *self.source_amount -= amount; // Load up the STREAM packet let sequence = self.next_sequence(); @@ -160,7 +200,7 @@ where stream_id: 1, shares: 1, })]; - if self.should_send_source_account { + if *self.should_send_source_account { frames.push(Frame::ConnectionNewAddress(ConnectionNewAddressFrame { source_account: self.source_account.clone(), })); @@ -193,24 +233,27 @@ where // Send it! self.congestion_controller.prepare(amount); - if let Some(ref mut next) = self.next { - let send_request = next.handle_request(IncomingRequest { - from: self.from_account.clone(), - prepare, + if let Some(ref next) = self.next { + let mut next = next.clone(); + let from = self.from_account.clone(); + let request = Box::pin(async move { + next.handle_request(IncomingRequest { from, prepare }).await }); self.pending_requests.get_mut().push(PendingRequest { sequence, amount, - future: Box::new(send_request), + future: request, }); sent_packets = true; } else { panic!("Polled after finish"); } } + Ok(sent_packets) } + /// Sends a STREAM inside a Prepare packet with a ConnectionClose frame to the peer fn try_send_connection_close(&mut self) -> Result<(), Error> { let sequence = self.next_sequence(); let stream_packet = StreamPacketBuilder { @@ -236,15 +279,17 @@ where // Send it! debug!("Closing connection"); - if let Some(ref mut next) = self.next { - let send_request = next.handle_request(IncomingRequest { - from: self.from_account.clone(), - prepare, - }); + if let Some(ref next) = self.next { + let mut next = next.clone(); + let from = self.from_account.clone(); + let request = + Box::pin( + async move { next.handle_request(IncomingRequest { from, prepare }).await }, + ); self.pending_requests.get_mut().push(PendingRequest { sequence, amount: 0, - future: Box::new(send_request), + future: request, }); } else { panic!("Polled after finish"); @@ -252,39 +297,57 @@ where Ok(()) } - fn poll_pending_requests(&mut self) -> Poll<(), Error> { + fn poll_pending_requests(&mut self, cx: &mut Context<'_>) -> Poll> { let pending_requests = self.pending_requests.take(); let pending_requests = pending_requests .into_iter() - .filter_map(|mut pending_request| match pending_request.future.poll() { - Ok(Async::NotReady) => Some(pending_request), - Ok(Async::Ready(fulfill)) => { - self.handle_fulfill(pending_request.sequence, pending_request.amount, fulfill); - None - } - Err(reject) => { - self.handle_reject(pending_request.sequence, pending_request.amount, reject); - None - } - }) + .filter_map( + |mut pending_request| match pending_request.future.as_mut().poll(cx) { + Poll::Pending => Some(pending_request), + Poll::Ready(result) => { + match result { + Ok(fulfill) => { + self.handle_fulfill( + pending_request.sequence, + pending_request.amount, + fulfill, + ); + } + Err(reject) => { + self.handle_reject( + pending_request.sequence, + pending_request.amount, + reject, + ); + } + }; + None + } + }, + ) .collect(); self.pending_requests.set(pending_requests); if let Some(error) = self.error.take() { error!("Send money stopped because of error: {:?}", error); - Err(error) + Poll::Ready(Err(error)) } else if self.pending_requests.get_mut().is_empty() { - Ok(Async::Ready(())) + Poll::Ready(Ok(())) } else { - Ok(Async::NotReady) + Poll::Pending } } + /// Parses the provided Fulfill packet. + /// 1. Logs the fulfill in the congestion controller + /// 1. Updates the last fulfill time of the send money future + /// 1. It tries to aprse a Stream Packet inside the fulfill packet's data field + /// If successful, it increments the delivered amount by the Stream packet's prepare amount fn handle_fulfill(&mut self, sequence: u64, amount: u64, fulfill: Fulfill) { // TODO should we check the fulfillment and expiry or can we assume the plugin does that? self.congestion_controller.fulfill(amount); - self.should_send_source_account = false; - self.last_fulfill_time = Instant::now(); + *self.should_send_source_account = false; + *self.last_fulfill_time = Instant::now(); if let Ok(packet) = StreamPacket::from_encrypted(&self.shared_secret, fulfill.into_data()) { if packet.ilp_packet_type() == IlpPacketType::Fulfill { @@ -319,10 +382,19 @@ where ); } + /// Parses the provided Reject packet. + /// 1. Increases the source-amount which was deducted at the start of the [send_money](./fn.send_money.html) loop + /// 1. Logs the reject in the congestion controller + /// 1. Increments the rejected packets counter + /// 1. If the receipt's `delivered_asset` fields are not populated, it tries to parse + /// a Stream Packet inside the reject packet's data field to check if + /// there is a [`ConnectionAssetDetailsFrame`](./../packet/struct.ConnectionAssetDetailsFrame.html) frame. + /// If one is found, then it updates the receipt's `delivered_asset_scale` and `delivered_asset_code` + /// to them. fn handle_reject(&mut self, sequence: u64, amount: u64, reject: Reject) { - self.source_amount += amount; + *self.source_amount += amount; self.congestion_controller.reject(amount, &reject); - self.rejected_packets += 1; + *self.rejected_packets += 1; debug!( "Prepare {} with amount {} was rejected with code: {} ({} left to send)", sequence, @@ -363,7 +435,7 @@ where // TODO handle STREAM errors } _ => { - self.error = Some(Error::SendMoneyError(format!( + *self.error = Some(Error::SendMoneyError(format!( "Packet was rejected with error: {} {}", reject.code(), str::from_utf8(reject.message()).unwrap_or_default(), @@ -372,48 +444,47 @@ where } } + /// Increments the stream's sequence number and returns the updated value fn next_sequence(&mut self) -> u64 { - let seq = self.sequence; - self.sequence += 1; + let seq = *self.sequence; + *self.sequence += 1; seq } } impl Future for SendMoneyFuture where - S: IncomingService, - A: Account, + S: IncomingService + Send + Sync + Clone + 'static, + A: Account + Send + Sync + 'static, { - type Item = (StreamDelivery, S); - type Error = Error; + type Output = Result<(StreamDelivery, S), Error>; - fn poll(&mut self) -> Poll { + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { // TODO maybe don't have loops here and in try_send_money + let mut this = self.project(); + loop { - self.poll_pending_requests()?; - if self.last_fulfill_time.elapsed() >= MAX_TIME_SINCE_LAST_FULFILL { - return Err(Error::TimeoutError(format!( + ready!(this.poll_pending_requests(cx)?); + if this.last_fulfill_time.elapsed() >= MAX_TIME_SINCE_LAST_FULFILL { + return Poll::Ready(Err(Error::TimeoutError(format!( "Time since last fulfill exceeded the maximum time limit of {:?} secs", - self.last_fulfill_time.elapsed().as_secs() - ))); + this.last_fulfill_time.elapsed().as_secs() + )))); } - if self.source_amount == 0 && self.pending_requests.get_mut().is_empty() { - if self.state == SendMoneyFutureState::SendMoney { - self.state = SendMoneyFutureState::Closing; - self.try_send_connection_close()?; + if *this.source_amount == 0 && this.pending_requests.get_mut().is_empty() { + if *this.state == SendMoneyFutureState::SendMoney { + *this.state = SendMoneyFutureState::Closing; + this.try_send_connection_close()?; } else { - self.state = SendMoneyFutureState::Closed; + *this.state = SendMoneyFutureState::Closed; debug!( - "Send money future finished. Delivered: {} ({} packets fulfilled, {} packets rejected)", self.receipt.delivered_amount, self.sequence - 1, self.rejected_packets, + "Send money future finished. Delivered: {} ({} packets fulfilled, {} packets rejected)", this.receipt.delivered_amount, *this.sequence - 1, this.rejected_packets, ); - return Ok(Async::Ready(( - self.receipt.clone(), - self.next.take().unwrap(), - ))); + return Poll::Ready(Ok((this.receipt.clone(), this.next.take().unwrap()))); } - } else if !self.try_send_money()? { - return Ok(Async::NotReady); + } else if !this.try_send_money()? { + return Poll::Pending; } } } @@ -431,8 +502,8 @@ mod send_money_tests { use std::sync::Arc; use uuid::Uuid; - #[test] - fn stops_at_final_errors() { + #[tokio::test] + async fn stops_at_final_errors() { let account = TestAccount { id: Uuid::new_v4(), asset_code: "XYZ".to_string(), @@ -457,7 +528,7 @@ mod send_money_tests { &[0; 32][..], 100, ) - .wait(); + .await; assert!(result.is_err()); assert_eq!(requests.lock().len(), 1); } diff --git a/crates/interledger-stream/src/congestion.rs b/crates/interledger-stream/src/congestion.rs index 3138c55a6..2f9eb676d 100644 --- a/crates/interledger-stream/src/congestion.rs +++ b/crates/interledger-stream/src/congestion.rs @@ -17,11 +17,19 @@ use std::io; /// control algorithms. pub struct CongestionController { state: CongestionState, + /// Amount which is added to `max_in_flight` per fulfill increase_amount: u64, + /// Divide `max_in_flight` by this factor per reject with code for insufficient liquidity + /// or if there is no `max_packet_amount` specified decrease_factor: f64, + /// The maximum amount we are allowed to add in a packet. This gets automatically set if + /// we receive a reject packet with a `F08_AMOUNT_TOO_LARGE` error max_packet_amount: Option, + /// The current amount in flight amount_in_flight: u64, + /// The maximum allowed amount to be in flight max_in_flight: u64, + /// Writer object to write our metrics to a csv #[cfg(feature = "metrics_csv")] csv_writer: csv::Writer, } @@ -33,6 +41,7 @@ enum CongestionState { } impl CongestionController { + /// Constructs a new congestion controller pub fn new(start_amount: u64, increase_amount: u64, decrease_factor: f64) -> Self { #[cfg(feature = "metrics_csv")] let mut csv_writer = csv::Writer::from_writer(io::stdout()); @@ -53,6 +62,7 @@ impl CongestionController { } } + /// The maximum amount which can be sent is the maximum amount in flight minus the current amount in flight pub fn get_max_amount(&mut self) -> u64 { if self.amount_in_flight > self.max_in_flight { return 0; @@ -66,6 +76,7 @@ impl CongestionController { } } + /// Increments the amount in flight by the provided amount pub fn prepare(&mut self, amount: u64) { if amount > 0 { self.amount_in_flight += amount; @@ -76,6 +87,8 @@ impl CongestionController { } } + /// Decrements the amount in flight by the provided amount + /// Increases the allowed max in flight amount cap pub fn fulfill(&mut self, prepare_amount: u64) { self.amount_in_flight -= prepare_amount; @@ -111,6 +124,8 @@ impl CongestionController { self.log_stats(prepare_amount); } + /// Decrements the amount in flight by the provided amount + /// Decreases the allowed max in flight amount cap pub fn reject(&mut self, prepare_amount: u64, reject: &Reject) { self.amount_in_flight -= prepare_amount; diff --git a/crates/interledger-stream/src/crypto.rs b/crates/interledger-stream/src/crypto.rs index 03c9727aa..657c8142c 100644 --- a/crates/interledger-stream/src/crypto.rs +++ b/crates/interledger-stream/src/crypto.rs @@ -8,9 +8,12 @@ use ring::{aead, digest, hmac}; const NONCE_LENGTH: usize = 12; const AUTH_TAG_LENGTH: usize = 16; +/// Protocol specific string for encryption static ENCRYPTION_KEY_STRING: &[u8] = b"ilp_stream_encryption"; +/// Protocol specific string for generating fulfillments static FULFILLMENT_GENERATION_STRING: &[u8] = b"ilp_stream_fulfillment"; +/// Returns the HMAC-SHA256 of the provided message using the provided **secret** key pub fn hmac_sha256(key: &[u8], message: &[u8]) -> [u8; 32] { let key = hmac::Key::new(hmac::HMAC_SHA256, key); let output = hmac::sign(&key, message); @@ -19,11 +22,17 @@ pub fn hmac_sha256(key: &[u8], message: &[u8]) -> [u8; 32] { to_return } +/// The fulfillment is generated by HMAC-256'ing the data with a secret key. +/// The secret key is generated deterministically by HMAC-256'ing the shared secret +/// and the hardcoded string "ilp_stream_fulfillment" pub fn generate_fulfillment(shared_secret: &[u8], data: &[u8]) -> [u8; 32] { + // generate the key as defined in the specificatoin let key = hmac_sha256(&shared_secret[..], &FULFILLMENT_GENERATION_STRING); + // return the hmac-sha256 of the data based on the generated key hmac_sha256(&key[..], &data[..]) } +/// Returns a 32-byte sha256 digest of the provided preimage pub fn hash_sha256(preimage: &[u8]) -> [u8; 32] { let output = digest::digest(&digest::SHA256, &preimage[..]); let mut to_return: [u8; 32] = [0; 32]; @@ -31,11 +40,15 @@ pub fn hash_sha256(preimage: &[u8]) -> [u8; 32] { to_return } +/// The fulfillment condition is the 32-byte sha256 of the fulfillment +/// generated by the provided shared secret and data via the +/// [generate_fulfillment](./fn.generate_fulfillment.html) function pub fn generate_condition(shared_secret: &[u8], data: &[u8]) -> [u8; 32] { let fulfillment = generate_fulfillment(&shared_secret, &data); hash_sha256(&fulfillment) } +/// Returns a random 32 byte number using [SystemRandom::new()](../../ring/rand/struct.SystemRandom.html#method.new) pub fn random_condition() -> [u8; 32] { let mut condition_slice: [u8; 32] = [0; 32]; SystemRandom::new() @@ -44,6 +57,8 @@ pub fn random_condition() -> [u8; 32] { condition_slice } +/// Returns a random 18 byte number using +/// [SystemRandom::new()](../../ring/rand/struct.SystemRandom.html#method.new) pub fn generate_token() -> [u8; 18] { let mut token: [u8; 18] = [0; 18]; SystemRandom::new() @@ -52,6 +67,9 @@ pub fn generate_token() -> [u8; 18] { token } +/// Encrypts a plaintext by calling [encrypt_with_nonce](./fn.encrypt_with_nonce.html) +/// with a random nonce of [`NONCE_LENGTH`](./constant.NONCE_LENGTH.html) generated using +/// [SystemRandom::new()](../../ring/rand/struct.SystemRandom.html#method.new) pub fn encrypt(shared_secret: &[u8], plaintext: BytesMut) -> BytesMut { // Generate a random nonce or IV let mut nonce: [u8; NONCE_LENGTH] = [0; NONCE_LENGTH]; @@ -62,6 +80,15 @@ pub fn encrypt(shared_secret: &[u8], plaintext: BytesMut) -> BytesMut { encrypt_with_nonce(shared_secret, plaintext, nonce) } +/// Encrypts a plaintext with a nonce by using AES256-GCM. +/// +/// A secret key is generated deterministically by HMAC-256'ing the `shared_secret` +/// and the hardcoded string "ilp_stream_encryption" +/// +/// The `additional_data` field is left empty. +/// +/// The ciphertext can be decrypted by calling the [`decrypt`](./fn.decrypt.html) function with the +/// same `shared_secret`. fn encrypt_with_nonce( shared_secret: &[u8], mut plaintext: BytesMut, @@ -96,6 +123,15 @@ fn encrypt_with_nonce( nonce_tag_data } +/// Decrypts a AES256-GCM encrypted ciphertext. +/// +/// The secret key is generated deterministically by HMAC-256'ing the `shared_secret` +/// and the hardcoded string "ilp_stream_encryption" +/// +/// The `additional_data` field is left empty. +/// +/// The nonce and auth tag are extracted from the first 12 and 16 bytes +/// of the ciphertext. pub fn decrypt(shared_secret: &[u8], mut ciphertext: BytesMut) -> Result { // ciphertext must include at least a nonce and tag, if ciphertext.len() < AUTH_TAG_LENGTH { diff --git a/crates/interledger-stream/src/error.rs b/crates/interledger-stream/src/error.rs index 60d92ba2d..8ef6ff586 100644 --- a/crates/interledger-stream/src/error.rs +++ b/crates/interledger-stream/src/error.rs @@ -1,5 +1,6 @@ use failure::Fail; +/// Stream Errors #[derive(Fail, Debug)] pub enum Error { #[fail(display = "Error connecting: {}", _0)] diff --git a/crates/interledger-stream/src/lib.rs b/crates/interledger-stream/src/lib.rs index 7e0c048ac..c0337705c 100644 --- a/crates/interledger-stream/src/lib.rs +++ b/crates/interledger-stream/src/lib.rs @@ -4,11 +4,17 @@ //! //! STREAM is responsible for splitting larger payments and messages into smaller chunks of money and data, and sending them over ILP. +/// Stream client mod client; +/// Congestion controller consumed by the [stream client](./client/fn.send_money.html) mod congestion; +/// Cryptographic utilities for generating fulfillments and encrypting/decrypting STREAM packets mod crypto; +/// Stream errors mod error; +/// Stream Packet implementation, [as specified in the RFC](https://interledger.org/rfcs/0029-stream/#5-packet-and-frame-specification) mod packet; +/// A stream server implementing an [Outgoing Service](../interledger_service/trait.OutgoingService.html) for receiving STREAM payments from peers mod server; pub use client::{send_money, StreamDelivery}; @@ -20,7 +26,8 @@ pub use server::{ #[cfg(test)] pub mod test_helpers { use super::*; - use futures::{future::ok, sync::mpsc::UnboundedSender, Future}; + use async_trait::async_trait; + use futures::channel::mpsc::UnboundedSender; use interledger_packet::Address; use interledger_router::RouterStore; use interledger_service::{Account, AccountStore, AddressStore, Username}; @@ -88,22 +95,17 @@ pub mod test_helpers { pub route: (String, TestAccount), } + #[async_trait] impl AccountStore for TestStore { type Account = TestAccount; - fn get_accounts( - &self, - _account_ids: Vec, - ) -> Box, Error = ()> + Send> { - Box::new(ok(vec![self.route.1.clone()])) + async fn get_accounts(&self, _account_ids: Vec) -> Result, ()> { + Ok(vec![self.route.1.clone()]) } // stub implementation (not used in these tests) - fn get_account_id_from_username( - &self, - _username: &Username, - ) -> Box + Send> { - Box::new(ok(Uuid::new_v4())) + async fn get_account_id_from_username(&self, _username: &Username) -> Result { + Ok(Uuid::new_v4()) } } @@ -115,16 +117,14 @@ pub mod test_helpers { } } + #[async_trait] impl AddressStore for TestStore { /// Saves the ILP Address in the store's memory and database - fn set_ilp_address( - &self, - _ilp_address: Address, - ) -> Box + Send> { + async fn set_ilp_address(&self, _ilp_address: Address) -> Result<(), ()> { unimplemented!() } - fn clear_ilp_address(&self) -> Box + Send> { + async fn clear_ilp_address(&self) -> Result<(), ()> { unimplemented!() } @@ -140,18 +140,16 @@ mod send_money_to_receiver { use super::test_helpers::*; use super::*; use bytes::Bytes; - use futures::Future; use interledger_ildcp::IldcpService; use interledger_packet::Address; use interledger_packet::{ErrorCode, RejectBuilder}; use interledger_router::Router; use interledger_service::outgoing_service_fn; use std::str::FromStr; - use tokio::runtime::Runtime; use uuid::Uuid; - #[test] - fn send_money_test() { + #[tokio::test] + async fn send_money_test() { let server_secret = Bytes::from(&[0; 32][..]); let destination_address = Address::from_str("example.receiver").unwrap(); let account = TestAccount { @@ -184,7 +182,7 @@ mod send_money_to_receiver { connection_generator.generate_address_and_secret(&destination_address); let destination_address = Address::from_str("example.receiver").unwrap(); - let run = send_money( + let (receipt, _service) = send_money( server, &test_helpers::TestAccount { id: Uuid::new_v4(), @@ -196,12 +194,9 @@ mod send_money_to_receiver { &shared_secret[..], 100, ) - .and_then(|(receipt, _service)| { - assert_eq!(receipt.delivered_amount, 100); - Ok(()) - }) - .map_err(|err| panic!(err)); - let runtime = Runtime::new().unwrap(); - runtime.block_on_all(run).unwrap(); + .await + .unwrap(); + + assert_eq!(receipt.delivered_amount, 100); } } diff --git a/crates/interledger-stream/src/packet.rs b/crates/interledger-stream/src/packet.rs index 59a78f5f7..cc77d0bb1 100644 --- a/crates/interledger-stream/src/packet.rs +++ b/crates/interledger-stream/src/packet.rs @@ -10,16 +10,25 @@ use lazy_static::lazy_static; use log::warn; use std::{convert::TryFrom, fmt, str, u64}; +/// The Stream Protocol's version const STREAM_VERSION: u8 = 1; +/// Builder for [Stream Packets](https://interledger.org/rfcs/0029-stream/#52-stream-packet) pub struct StreamPacketBuilder<'a> { + /// The stream packet's sequence number pub sequence: u64, + /// The [ILP Packet Type](../interledger_packet/enum.PacketType.html) pub ilp_packet_type: IlpPacketType, + /// Destination amount of the ILP Prepare, used for enforcing minimum exchange rates and congestion control. + /// Within an ILP Prepare, represents the minimum amount the recipient needs to receive in order to fulfill the packet. + /// Within an ILP Fulfill or ILP Reject, represents the amount received by the recipient. pub prepare_amount: u64, + /// The stream frames pub frames: &'a [Frame<'a>], } impl<'a> StreamPacketBuilder<'a> { + /// Serializes the builder into a Stream Packet pub fn build(&self) -> StreamPacket { // TODO predict length first let mut buffer_unencrypted = Vec::new(); @@ -96,7 +105,7 @@ impl<'a> StreamPacketBuilder<'a> { } StreamPacket { - buffer_unencrypted: BytesMut::from(buffer_unencrypted), + buffer_unencrypted: BytesMut::from(&buffer_unencrypted[..]), sequence: self.sequence, ilp_packet_type: self.ilp_packet_type, prepare_amount: self.prepare_amount, @@ -105,16 +114,31 @@ impl<'a> StreamPacketBuilder<'a> { } } +/// A Stream Packet as specified in its [ASN.1 definition](https://interledger.org/rfcs/asn1/Stream.asn) #[derive(PartialEq, Clone)] pub struct StreamPacket { + /// The cleartext serialized packet pub(crate) buffer_unencrypted: BytesMut, + /// The packet's sequence number sequence: u64, + /// The [ILP Packet Type](../interledger_packet/enum.PacketType.html) ilp_packet_type: IlpPacketType, + /// Destination amount of the ILP Prepare, used for enforcing minimum exchange rates and congestion control. + /// Within an ILP Prepare, represents the minimum amount the recipient needs to receive in order to fulfill the packet. + /// Within an ILP Fulfill or ILP Reject, represents the amount received by the recipient. prepare_amount: u64, + /// The offset after which frames can be found inside the `buffer_unencrypted` field frames_offset: usize, } impl StreamPacket { + /// Constructs a [Stream Packet](./struct.StreamPacket.html) from an encrypted buffer + /// and a shared secret + /// + /// # Errors + /// 1. If the version of Stream Protocol doesn't match the hardcoded [stream version](constant.STREAM_VERSION.html) + /// 1. If the decryption fails + /// 1. If the decrypted bytes cannot be parsed to an unencrypted [Stream Packet](./struct.StreamPacket.html) pub fn from_encrypted(shared_secret: &[u8], ciphertext: BytesMut) -> Result { // TODO handle decryption failure let decrypted = decrypt(shared_secret, ciphertext) @@ -122,6 +146,11 @@ impl StreamPacket { StreamPacket::from_bytes_unencrypted(decrypted) } + /// Constructs a [Stream Packet](./struct.StreamPacket.html) from a buffer + /// + /// # Errors + /// 1. If the version of Stream Protocol doesn't match the hardcoded [stream version](constant.STREAM_VERSION.html) + /// 1. If the decrypted bytes cannot be parsed to an unencrypted [Stream Packet](./struct.StreamPacket.html) fn from_bytes_unencrypted(buffer_unencrypted: BytesMut) -> Result { // TODO don't copy the whole packet again let mut reader = &buffer_unencrypted[..]; @@ -161,22 +190,28 @@ impl StreamPacket { } } + /// Consumes the packet and a shared secret and returns a serialized encrypted + /// Stream packet pub fn into_encrypted(self, shared_secret: &[u8]) -> BytesMut { encrypt(shared_secret, self.buffer_unencrypted) } + /// The packet's sequence number pub fn sequence(&self) -> u64 { self.sequence } + /// The packet's [type](../interledger_packet/enum.PacketType.html) pub fn ilp_packet_type(&self) -> IlpPacketType { self.ilp_packet_type } + /// Destination amount of the ILP Prepare, used for enforcing minimum exchange rates and congestion control. pub fn prepare_amount(&self) -> u64 { self.prepare_amount } + /// Returns a [FrameIterator](./struct.FrameIterator.html) over the packet's [frames](./enum.Frame.html) pub fn frames(&self) -> FrameIterator { FrameIterator { buffer: &self.buffer_unencrypted[self.frames_offset..], @@ -197,11 +232,14 @@ impl fmt::Debug for StreamPacket { } } +/// Iterator over a serialized Frame to support zero-copy deserialization pub struct FrameIterator<'a> { buffer: &'a [u8], } impl<'a> FrameIterator<'a> { + /// Reads a u8 from the iterator's buffer, and depending on the type it returns + /// a [`Frame`](./enum.Frame.html) fn try_read_next_frame(&mut self) -> Result, ParseError> { let frame_type = self.buffer.read_u8()?; let contents: &'a [u8] = self.buffer.read_var_octet_string()?; @@ -291,6 +329,7 @@ impl<'a> fmt::Debug for FrameIterator<'a> { } } +/// Enum around the different Stream Frame types #[derive(PartialEq, Clone)] pub enum Frame<'a> { ConnectionClose(ConnectionCloseFrame<'a>), @@ -332,6 +371,7 @@ impl<'a> fmt::Debug for Frame<'a> { } } +/// The Stream Frame types [as defined in the RFC](https://interledger.org/rfcs/0029-stream/#53-frames) #[derive(Debug, PartialEq, Clone)] #[repr(u8)] pub enum FrameType { @@ -351,6 +391,7 @@ pub enum FrameType { StreamDataBlocked = 0x16, Unknown, } + impl From for FrameType { fn from(num: u8) -> Self { match num { @@ -373,6 +414,7 @@ impl From for FrameType { } } +/// The STREAM Error Codes [as defined in the RFC](https://interledger.org/rfcs/0029-stream/#54-error-codes) #[derive(Debug, PartialEq, Clone)] #[repr(u8)] pub enum ErrorCode { @@ -404,15 +446,20 @@ impl From for ErrorCode { } } +/// Helper trait for having a common interface to read/write on Frames pub trait SerializableFrame<'a>: Sized { fn put_contents(&self, buf: &mut impl MutBufOerExt) -> (); fn read_contents(reader: &'a [u8]) -> Result; } +/// Frame after which a connection must be closed. +/// If implementations allow half-open connections, an endpoint may continue sending packets after receiving a ConnectionClose frame. #[derive(Debug, PartialEq, Clone)] pub struct ConnectionCloseFrame<'a> { + /// Machine-readable [Error Code](./enum.ErrorCode.html) indicating why the connection was closed. pub code: ErrorCode, + /// Human-readable string intended to give more information helpful for debugging purposes. pub message: &'a str, } @@ -431,8 +478,10 @@ impl<'a> SerializableFrame<'a> for ConnectionCloseFrame<'a> { } } +/// Frame which contains the sender of the Stream payment #[derive(PartialEq, Clone)] pub struct ConnectionNewAddressFrame { + /// New ILP address of the endpoint that sent the frame. pub source_account: Address, } @@ -460,9 +509,13 @@ impl<'a> fmt::Debug for ConnectionNewAddressFrame { } } +/// The assets being transported in this Stream payment +/// Asset details exposed by this frame MUST NOT change during the lifetime of a Connection. #[derive(Debug, PartialEq, Clone)] pub struct ConnectionAssetDetailsFrame<'a> { + /// Asset code of endpoint that sent the frame. pub source_asset_code: &'a str, + /// Asset scale of endpoint that sent the frame. pub source_asset_scale: u8, } @@ -483,8 +536,10 @@ impl<'a> SerializableFrame<'a> for ConnectionAssetDetailsFrame<'a> { } } +/// Endpoints MUST NOT exceed the total number of bytes the other endpoint is willing to accept. #[derive(Debug, PartialEq, Clone)] pub struct ConnectionMaxDataFrame { + /// The total number of bytes the endpoint is willing to receive on this connection. pub max_offset: u64, } @@ -500,8 +555,10 @@ impl<'a> SerializableFrame<'a> for ConnectionMaxDataFrame { } } +/// Frame specifying the amount of data which is going to be sent #[derive(Debug, PartialEq, Clone)] pub struct ConnectionDataBlockedFrame { + /// The total number of bytes the endpoint wants to send. pub max_offset: u64, } @@ -517,8 +574,10 @@ impl<'a> SerializableFrame<'a> for ConnectionDataBlockedFrame { } } +/// Frame specifying the maximum stream ID the endpoint is willing to accept. #[derive(Debug, PartialEq, Clone)] pub struct ConnectionMaxStreamIdFrame { + /// The maximum stream ID the endpoint is willing to accept. pub max_stream_id: u64, } @@ -534,8 +593,10 @@ impl<'a> SerializableFrame<'a> for ConnectionMaxStreamIdFrame { } } +/// Frame specifying the maximum stream ID the endpoint wishes to open. #[derive(Debug, PartialEq, Clone)] pub struct ConnectionStreamIdBlockedFrame { + /// The maximum stream ID the endpoint wishes to open. pub max_stream_id: u64, } @@ -551,10 +612,16 @@ impl<'a> SerializableFrame<'a> for ConnectionStreamIdBlockedFrame { } } +/// Endpoints MUST close the stream after receiving this stream immediately. +/// If implementations allow half-open streams, an endpoint MAY continue sending +/// money or data for this stream after receiving a StreamClose frame. #[derive(Debug, PartialEq, Clone)] pub struct StreamCloseFrame<'a> { + /// Identifier of the stream this frame refers to. pub stream_id: u64, + /// Machine-readable [Error Code](./enum.ErrorCode.html) indicating why the connection was closed. pub code: ErrorCode, + /// Human-readable string intended to give more information helpful for debugging purposes. pub message: &'a str, } @@ -579,9 +646,25 @@ impl<'a> SerializableFrame<'a> for StreamCloseFrame<'a> { } } +/// Frame specifying the amount of money that should go to each stream +/// +/// The amount of money that should go to each stream is calculated by +/// dividing the number of shares for the given stream by the total number +/// of shares in all of the StreamMoney frames in the packet. +/// +/// For example, if an ILP Prepare packet has an amount of 100 and three +/// StreamMoney frames with 5, 15, and 30 shares for streams 2, 4, and 6, +/// respectively, that would indicate that stream 2 should get 10 units, +/// stream 4 gets 30 units, and stream 6 gets 60 units. +/// If the Prepare amount is not divisible by the total number of shares, +/// stream amounts are rounded down. +/// +/// The remainder is be allocated to the lowest-numbered open stream that has not reached its maximum receive amount. #[derive(Debug, PartialEq, Clone)] pub struct StreamMoneyFrame { + /// Identifier of the stream this frame refers to. pub stream_id: u64, + /// Proportion of the ILP Prepare amount destined for the stream specified. pub shares: u64, } @@ -599,10 +682,21 @@ impl<'a> SerializableFrame<'a> for StreamMoneyFrame { } } +/// Specifies the max amount of money the endpoint wants to send +/// +/// The amounts in this frame are denominated in the units of the +/// endpoint sending the frame, so the other endpoint must use their +/// calculated exchange rate to determine how much more they can send +/// for this stream. #[derive(Debug, PartialEq, Clone)] pub struct StreamMaxMoneyFrame { + /// Identifier of the stream this frame refers to. pub stream_id: u64, + /// Total amount, denominated in the units of the endpoint + /// sending this frame, that the endpoint is willing to receive on this stream. pub receive_max: u64, + /// Total amount, denominated in the units of the endpoint + /// sending this frame, that the endpoint has received thus far. pub total_received: u64, } @@ -626,10 +720,16 @@ impl<'a> SerializableFrame<'a> for StreamMaxMoneyFrame { } } +/// Frame specifying the maximum amount of money the sending endpoint will send #[derive(Debug, PartialEq, Clone)] pub struct StreamMoneyBlockedFrame { + /// Identifier of the stream this frame refers to. pub stream_id: u64, + /// Total amount, denominated in the units of the endpoint + /// sending this frame, that the endpoint wants to send. pub send_max: u64, + /// Total amount, denominated in the units of the endpoint + /// sending this frame, that the endpoint has sent already. pub total_sent: u64, } @@ -653,10 +753,29 @@ impl<'a> SerializableFrame<'a> for StreamMoneyBlockedFrame { } } +/// Packets may be received out of order so the Offset is used to +/// indicate the correct position of the byte segment in the overall stream. +/// The first StreamData frame sent for a given stream MUST start with an Offset of zero. + +/// Fragments of data provided by a stream's StreamData frames +/// MUST NOT ever overlap with one another. For example, the following combination +/// of frames is forbidden because bytes 15-19 were provided twice: +/// +/// ```ignore +/// StreamData { StreamID: 1, Offset: 10, Data: "1234567890" } +/// StreamData { StreamID: 1, Offset: 15, Data: "67890" } +/// ``` +/// +/// In other words, if a sender resends data (e.g. because a packet was lost), +/// it MUST resend the exact frames — offset and data. +/// This rule exists to simplify data reassembly for the receiver #[derive(Debug, PartialEq, Clone)] pub struct StreamDataFrame<'a> { + /// Identifier of the stream this frame refers to. pub stream_id: u64, + /// Position of this data in the byte stream. pub offset: u64, + /// Application data pub data: &'a [u8], } @@ -680,9 +799,12 @@ impl<'a> SerializableFrame<'a> for StreamDataFrame<'a> { } } +/// The maximum amount of data the endpoint is willing to receive on this stream #[derive(Debug, PartialEq, Clone)] pub struct StreamMaxDataFrame { + /// Identifier of the stream this frame refers to. pub stream_id: u64, + /// The total number of bytes the endpoint is willing to receive on this stream. pub max_offset: u64, } @@ -703,9 +825,12 @@ impl<'a> SerializableFrame<'a> for StreamMaxDataFrame { } } +/// The maximum amount of data the endpoint is willing to send on this stream #[derive(Debug, PartialEq, Clone)] pub struct StreamDataBlockedFrame { + /// Identifier of the stream this frame refers to. pub stream_id: u64, + /// The total number of bytes the endpoint wants to send on this stream. pub max_offset: u64, } @@ -801,14 +926,16 @@ mod serialization { ] } .build(); - static ref SERIALIZED: BytesMut = BytesMut::from(vec![ - 1, 12, 1, 1, 1, 99, 1, 14, 1, 5, 1, 3, 111, 111, 112, 2, 13, 12, 101, 120, 97, 109, - 112, 108, 101, 46, 98, 108, 97, 104, 3, 3, 2, 3, 232, 4, 3, 2, 7, 208, 5, 3, 2, 11, - 184, 6, 3, 2, 15, 160, 7, 5, 3, 88, 89, 90, 9, 16, 8, 1, 76, 2, 4, 98, 108, 97, 104, - 17, 4, 1, 88, 1, 99, 18, 8, 1, 11, 2, 3, 219, 2, 1, 244, 19, 8, 1, 66, 2, 78, 32, 2, - 23, 112, 20, 11, 1, 34, 2, 35, 40, 5, 104, 101, 108, 108, 111, 21, 5, 1, 35, 2, 34, 62, - 22, 6, 2, 3, 120, 2, 173, 156 - ]); + static ref SERIALIZED: BytesMut = BytesMut::from( + &vec![ + 1, 12, 1, 1, 1, 99, 1, 14, 1, 5, 1, 3, 111, 111, 112, 2, 13, 12, 101, 120, 97, 109, + 112, 108, 101, 46, 98, 108, 97, 104, 3, 3, 2, 3, 232, 4, 3, 2, 7, 208, 5, 3, 2, 11, + 184, 6, 3, 2, 15, 160, 7, 5, 3, 88, 89, 90, 9, 16, 8, 1, 76, 2, 4, 98, 108, 97, + 104, 17, 4, 1, 88, 1, 99, 18, 8, 1, 11, 2, 3, 219, 2, 1, 244, 19, 8, 1, 66, 2, 78, + 32, 2, 23, 112, 20, 11, 1, 34, 2, 35, 40, 5, 104, 101, 108, 108, 111, 21, 5, 1, 35, + 2, 34, 62, 22, 6, 2, 3, 120, 2, 173, 156 + ][..] + ); } #[test] diff --git a/crates/interledger-stream/src/server.rs b/crates/interledger-stream/src/server.rs index 890489969..0ba842bee 100644 --- a/crates/interledger-stream/src/server.rs +++ b/crates/interledger-stream/src/server.rs @@ -1,15 +1,16 @@ use super::crypto::*; use super::packet::*; +use async_trait::async_trait; use base64; use bytes::{Bytes, BytesMut}; use chrono::{DateTime, Utc}; -use futures::{future::result, sync::mpsc::UnboundedSender}; +use futures::channel::mpsc::UnboundedSender; use hex; use interledger_packet::{ Address, ErrorCode, Fulfill, FulfillBuilder, PacketType as IlpPacketType, Prepare, Reject, RejectBuilder, }; -use interledger_service::{Account, BoxedIlpFuture, OutgoingRequest, OutgoingService, Username}; +use interledger_service::{Account, IlpResult, OutgoingRequest, OutgoingService, Username}; use log::debug; use serde::{Deserialize, Serialize}; use std::marker::PhantomData; @@ -58,7 +59,7 @@ impl ConnectionGenerator { // is valid and adding base64-url characters will always be valid let destination_account = base_address.with_suffix(&token.as_ref()).unwrap(); - debug!("Generated address: {}", destination_account,); + debug!("Generated address: {}", destination_account); (destination_account, shared_secret) } @@ -84,12 +85,18 @@ impl ConnectionGenerator { } } +/// Notification that STREAM fulfilled a packet and received a single Interledger payment, used by Pubsub API consumers #[derive(Debug, Deserialize, Serialize)] pub struct PaymentNotification { + /// The username of the account that received the Interledger payment pub to_username: Username, + /// The username of the account that routed the Interledger payment to this node pub from_username: Username, + /// The ILP Address of the receiver of the payment notification pub destination: Address, + /// The amount received pub amount: u64, + /// The time this payment notification was fired in RFC3339 format pub timestamp: String, } @@ -97,12 +104,15 @@ pub struct PaymentNotification { pub trait StreamNotificationsStore { type Account: Account; + /// *Synchronously* saves the sending side of the provided account id's websocket channel to the store's memory fn add_payment_notification_subscription( &self, account_id: Uuid, sender: UnboundedSender, ); + /// Instructs the store to publish the provided payment notification object + /// via its Pubsub interface fn publish_payment_notification(&self, _payment: PaymentNotification); } @@ -137,17 +147,16 @@ where } } +#[async_trait] impl OutgoingService for StreamReceiverService where S: StreamNotificationsStore + Send + Sync + 'static + Clone, - O: OutgoingService, - A: Account, + O: OutgoingService + Send + Sync + Clone, + A: Account + Send + Sync + Clone, { - type Future = BoxedIlpFuture; - /// Try fulfilling the request if it is for this STREAM server or pass it to the next /// outgoing handler if not. - fn send_request(&mut self, request: OutgoingRequest) -> Self::Future { + async fn send_request(&mut self, request: OutgoingRequest) -> IlpResult { let to_username = request.to.username().clone(); let from_username = request.from.username().clone(); let amount = request.prepare.amount(); @@ -182,14 +191,14 @@ where // the sender will likely see an error like F02: Unavailable (this is // a bit confusing but the packet data should not be modified at all // under normal circumstances). - return Box::new(self.next.send_request(request)); + return self.next.send_request(request).await; } } }; - return Box::new(result(response)); + return response; } } - Box::new(self.next.send_request(request)) + self.next.send_request(request).await } } @@ -367,7 +376,7 @@ mod receiving_money { fn fulfills_valid_packet() { let ilp_address = Address::from_str("example.destination").unwrap(); let server_secret = Bytes::from(&[1; 32][..]); - let connection_generator = ConnectionGenerator::new(server_secret.clone()); + let connection_generator = ConnectionGenerator::new(server_secret); let (destination_account, shared_secret) = connection_generator.generate_address_and_secret(&ilp_address); let stream_packet = test_stream_packet(); @@ -395,7 +404,7 @@ mod receiving_money { fn fulfills_valid_packet_without_connection_tag() { let ilp_address = Address::from_str("example.destination").unwrap(); let server_secret = Bytes::from(&[1; 32][..]); - let connection_generator = ConnectionGenerator::new(server_secret.clone()); + let connection_generator = ConnectionGenerator::new(server_secret); let (destination_account, shared_secret) = connection_generator.generate_address_and_secret(&ilp_address); let stream_packet = test_stream_packet(); @@ -423,7 +432,7 @@ mod receiving_money { fn rejects_modified_data() { let ilp_address = Address::from_str("example.destination").unwrap(); let server_secret = Bytes::from(&[1; 32][..]); - let connection_generator = ConnectionGenerator::new(server_secret.clone()); + let connection_generator = ConnectionGenerator::new(server_secret); let (destination_account, shared_secret) = connection_generator.generate_address_and_secret(&ilp_address); let stream_packet = test_stream_packet(); @@ -452,7 +461,7 @@ mod receiving_money { fn rejects_too_little_money() { let ilp_address = Address::from_str("example.destination").unwrap(); let server_secret = Bytes::from(&[1; 32][..]); - let connection_generator = ConnectionGenerator::new(server_secret.clone()); + let connection_generator = ConnectionGenerator::new(server_secret); let (destination_account, shared_secret) = connection_generator.generate_address_and_secret(&ilp_address); @@ -491,7 +500,7 @@ mod receiving_money { fn fulfills_packets_sent_to_javascript_receiver() { // This was created by the JS ilp-protocol-stream library let ilp_address = Address::from_str("test.peerB").unwrap(); - let prepare = Prepare::try_from(bytes::BytesMut::from(hex::decode("0c819900000000000001f43230313931303238323134313533383338f31a96346c613011947f39a0f1f4e573c2fc3e7e53797672b01d2898e90c9a0723746573742e70656572422e4e6a584430754a504275477a353653426d4933755836682d3b6cc484c0d4e9282275d4b37c6ae18f35b497ddbfcbce6d9305b9451b4395c3158aa75e05bf27582a237109ec6ca0129d840da7abd96826c8147d0d").unwrap())).unwrap(); + let prepare = Prepare::try_from(bytes::BytesMut::from(&hex::decode("0c819900000000000001f43230313931303238323134313533383338f31a96346c613011947f39a0f1f4e573c2fc3e7e53797672b01d2898e90c9a0723746573742e70656572422e4e6a584430754a504275477a353653426d4933755836682d3b6cc484c0d4e9282275d4b37c6ae18f35b497ddbfcbce6d9305b9451b4395c3158aa75e05bf27582a237109ec6ca0129d840da7abd96826c8147d0d").unwrap()[..])).unwrap(); let condition = prepare.execution_condition().to_vec(); let server_secret = Bytes::from(vec![0u8; 32]); let connection_generator = ConnectionGenerator::new(server_secret); @@ -519,15 +528,15 @@ mod receiving_money { mod stream_receiver_service { use super::*; use crate::test_helpers::*; - use futures::Future; use interledger_packet::PrepareBuilder; use interledger_service::outgoing_service_fn; use std::convert::TryFrom; use std::str::FromStr; use std::time::UNIX_EPOCH; - #[test] - fn fulfills_correct_packets() { + + #[tokio::test] + async fn fulfills_correct_packets() { let ilp_address = Address::from_str("example.destination").unwrap(); let server_secret = Bytes::from(&[1; 32][..]); let connection_generator = ConnectionGenerator::new(server_secret.clone()); @@ -550,7 +559,7 @@ mod stream_receiver_service { let mut service = StreamReceiverService::new( server_secret.clone(), DummyStore, - outgoing_service_fn(|_: OutgoingRequest| -> BoxedIlpFuture { + outgoing_service_fn(|_: OutgoingRequest| -> IlpResult { panic!("shouldn't get here") }), ); @@ -572,12 +581,12 @@ mod stream_receiver_service { original_amount: prepare.amount(), prepare, }) - .wait(); + .await; assert!(result.is_ok()); } - #[test] - fn rejects_invalid_packets() { + #[tokio::test] + async fn rejects_invalid_packets() { let ilp_address = Address::from_str("example.destination").unwrap(); let server_secret = Bytes::from(&[1; 32][..]); let connection_generator = ConnectionGenerator::new(server_secret.clone()); @@ -632,12 +641,12 @@ mod stream_receiver_service { original_amount: prepare.amount(), prepare, }) - .wait(); + .await; assert!(result.is_err()); } - #[test] - fn passes_on_packets_not_for_it() { + #[tokio::test] + async fn passes_on_packets_not_for_it() { let ilp_address = Address::from_str("example.destination").unwrap(); let server_secret = Bytes::from(&[1; 32][..]); let connection_generator = ConnectionGenerator::new(server_secret.clone()); @@ -690,7 +699,7 @@ mod stream_receiver_service { }, prepare, }) - .wait(); + .await; assert!(result.is_err()); assert_eq!( result.unwrap_err().triggered_by().unwrap(),