├── .circleci ├── config.yml └── release │ ├── build_musl_binary.sh │ ├── get_crate_name.sh │ ├── get_docker_image_tag.sh │ ├── github_release.sh │ └── github_tag.sh ├── .dockerignore ├── .github ├── config.yml └── workflows │ ├── ci.yml │ ├── commits.yml │ └── docker.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── crates ├── ilp-cli │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── interpreter.rs │ │ ├── main.rs │ │ └── parser.rs ├── ilp-node │ ├── Cargo.toml │ ├── README.md │ ├── benches │ │ ├── multiple_payments.rs │ │ ├── redis_helpers.rs │ │ └── test_helpers.rs │ ├── build.rs │ ├── src │ │ ├── instrumentation │ │ │ ├── google_pubsub.rs │ │ │ ├── metrics.rs │ │ │ ├── mod.rs │ │ │ ├── prometheus.rs │ │ │ └── trace.rs │ │ ├── lib.rs │ │ ├── main.rs │ │ ├── node.rs │ │ └── redis_store.rs │ └── tests │ │ └── redis │ │ ├── btp.rs │ │ ├── exchange_rates.rs │ │ ├── payments_incoming.rs │ │ ├── prometheus.rs │ │ ├── redis_helpers.rs │ │ ├── redis_tests.rs │ │ ├── test_helpers.rs │ │ ├── three_nodes.rs │ │ └── time_based_settlement.rs ├── interledger-api │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── lib.rs │ │ └── routes │ │ ├── accounts.rs │ │ ├── mod.rs │ │ ├── node_settings.rs │ │ └── test_helpers.rs ├── interledger-btp │ ├── Cargo.toml │ ├── README.md │ ├── fuzz │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ └── fuzz_targets │ │ │ └── fuzz_target_1.rs │ └── src │ │ ├── client.rs │ │ ├── errors.rs │ │ ├── lib.rs │ │ ├── packet.rs │ │ ├── server.rs │ │ ├── service.rs │ │ └── wrapped_ws.rs ├── interledger-ccp │ ├── Cargo.toml │ ├── README.md │ ├── fuzz │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ └── fuzz_targets │ │ │ ├── fuzz_target_1.rs │ │ │ └── fuzz_target_2.rs │ └── src │ │ ├── fixtures.rs │ │ ├── lib.rs │ │ ├── packet.rs │ │ ├── routing_table.rs │ │ ├── server.rs │ │ └── test_helpers.rs ├── interledger-errors │ ├── Cargo.toml │ └── src │ │ ├── account_store_error.rs │ │ ├── address_store_error.rs │ │ ├── balance_store_error.rs │ │ ├── btp_store_error.rs │ │ ├── ccprouting_store_error.rs │ │ ├── create_account_error.rs │ │ ├── error │ │ ├── error_types.rs │ │ └── mod.rs │ │ ├── exchange_rate_store_error.rs │ │ ├── http_store_error.rs │ │ ├── lib.rs │ │ ├── node_store_error.rs │ │ └── settlement_errors.rs ├── interledger-http │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── client.rs │ │ ├── lib.rs │ │ └── server.rs ├── interledger-ildcp │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── client.rs │ │ ├── lib.rs │ │ ├── packet.rs │ │ └── server.rs ├── interledger-packet │ ├── Cargo.toml │ ├── README.md │ ├── benches │ │ └── packets.rs │ ├── fuzz │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ ├── README.md │ │ └── fuzz_targets │ │ │ ├── address.rs │ │ │ └── packet.rs │ └── src │ │ ├── address.rs │ │ ├── error.rs │ │ ├── errors.rs │ │ ├── fixtures.rs │ │ ├── hex.rs │ │ ├── lib.rs │ │ ├── oer.rs │ │ └── packet.rs ├── interledger-rates │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── coincap.rs │ │ ├── cryptocompare.rs │ │ └── lib.rs ├── interledger-router │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── lib.rs │ │ └── router.rs ├── interledger-service-util │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── balance_service.rs │ │ ├── echo_service.rs │ │ ├── exchange_rates_service.rs │ │ ├── expiry_shortener_service.rs │ │ ├── lib.rs │ │ ├── max_packet_amount_service.rs │ │ ├── rate_limit_service.rs │ │ └── validator_service.rs ├── interledger-service │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── lib.rs │ │ ├── trace.rs │ │ └── username.rs ├── interledger-settlement │ ├── Cargo.toml │ └── src │ │ ├── api │ │ ├── fixtures.rs │ │ ├── message_service.rs │ │ ├── mod.rs │ │ ├── node_api.rs │ │ └── test_helpers.rs │ │ ├── core │ │ ├── backends_common │ │ │ ├── mod.rs │ │ │ └── redis │ │ │ │ ├── mod.rs │ │ │ │ └── test_helpers │ │ │ │ ├── mod.rs │ │ │ │ ├── redis_helpers.rs │ │ │ │ └── store_helpers.rs │ │ ├── engines_api.rs │ │ ├── idempotency.rs │ │ ├── mod.rs │ │ ├── settlement_client.rs │ │ └── types.rs │ │ └── lib.rs ├── interledger-spsp │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── client.rs │ │ ├── lib.rs │ │ └── server.rs ├── interledger-store │ ├── Cargo.toml │ ├── README.md │ ├── external │ │ ├── libredis_cell.dylib │ │ └── libredis_cell.so │ ├── redis-example.conf │ ├── src │ │ ├── account.rs │ │ ├── crypto.rs │ │ ├── lib.rs │ │ └── redis │ │ │ ├── lua │ │ │ ├── account_from_username.lua │ │ │ ├── load_accounts.lua │ │ │ ├── process_fulfill.lua │ │ │ ├── process_incoming_settlement.lua │ │ │ ├── process_prepare.lua │ │ │ ├── process_reject.lua │ │ │ ├── process_settle.lua │ │ │ └── refund_settlement.lua │ │ │ ├── mod.rs │ │ │ └── reconnect.rs │ └── tests │ │ └── redis │ │ ├── accounts_test.rs │ │ ├── balances_test.rs │ │ ├── btp_test.rs │ │ ├── http_test.rs │ │ ├── notifications.rs │ │ ├── rate_limiting_test.rs │ │ ├── rates_test.rs │ │ ├── redis_tests.rs │ │ ├── routing_test.rs │ │ └── settlement_test.rs ├── interledger-stream │ ├── Cargo.toml │ ├── README.md │ ├── fuzz │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ └── fuzz_targets │ │ │ └── stream_packet.rs │ └── src │ │ ├── client.rs │ │ ├── congestion.rs │ │ ├── crypto.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── packet.rs │ │ └── server.rs └── interledger │ ├── Cargo.toml │ ├── README.md │ └── src │ └── lib.rs ├── docker ├── Dockerfile ├── docker-build.sh ├── ilp-cli.dockerfile ├── ilp-node.dockerfile ├── redis.conf ├── run-services-in-docker.sh └── run-testnet-bundle.js ├── docs ├── CONTRIBUTING.md ├── api.md ├── api.yml ├── architecture.md ├── configuration.md ├── interledger-rs.svg ├── logging.md ├── manual-config.md ├── peering.md ├── prometheus.md └── testnet.md ├── examples ├── README.md ├── eth-settlement │ ├── README.md │ └── images │ │ ├── materials │ │ └── overview.graffle │ │ └── overview.svg ├── eth-xrp-three-nodes │ ├── README.md │ └── images │ │ ├── materials │ │ └── overview.graffle │ │ └── overview.svg ├── simple │ ├── README.md │ └── images │ │ ├── materials │ │ └── overview.graffle │ │ └── overview.svg └── xrp-settlement │ ├── README.md │ └── images │ ├── materials │ └── overview.graffle │ └── overview.svg └── scripts ├── parse-md.sh ├── release.sh ├── run-md-lib.sh ├── run-md-test.sh └── run-md.sh /.circleci/release/build_musl_binary.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Build musl binary inside a Docker container and copy it to local disk. 4 | # This script requires docker command. 5 | # `crate_name` ($1) is expected to be the same as bin name. 6 | # 7 | # 1) Spin up a new container of "clux/muslrust:stable" 8 | # 2) Copy source code etc. to the container 9 | # 3) Build a musl binary of the specified crate ($1) inside the container 10 | # 4) Copy the built binary to local disk space ($2) 11 | 12 | crate_name=$1 13 | artifacts_path=$2 14 | docker_image_name="clux/muslrust:stable" 15 | 16 | if [ -z "${crate_name}" ]; then 17 | printf "%s\n" "crate_name is required." 18 | exit 1 19 | fi 20 | if [ -z "${artifacts_path}" ]; then 21 | printf "%s\n" "artifacts_path is required." 22 | exit 1 23 | fi 24 | 25 | docker run -dt --name builder "${docker_image_name}" 26 | 27 | docker cp ./Cargo.toml builder:/usr/src/Cargo.toml 28 | docker cp ./Cargo.lock builder:/usr/src/Cargo.lock 29 | docker cp ./crates builder:/usr/src/crates 30 | 31 | # "--workdir" requires API version 1.35, but the Docker daemon API version of CircleCI is 1.32 32 | docker exec builder "/bin/bash" "-c" "cd /usr/src && cargo build --release --package \"${crate_name}\" --bin \"${crate_name}\" --target x86_64-unknown-linux-musl" 33 | docker cp "builder:/usr/src/target/x86_64-unknown-linux-musl/release/${crate_name}" "${artifacts_path}" 34 | 35 | docker stop builder 36 | -------------------------------------------------------------------------------- /.circleci/release/get_crate_name.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Returns a crate name to build from a git tag name. 4 | # The tag name is assumed to be output by `cargo release` or be tagged manually. 5 | 6 | # The regex means "(something)-(semantic version)" 7 | if [[ $1 =~ ^(.*)-v([0-9]+)\.([0-9]+)\.([0-9]+)(-([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?(\+[0-9A-Za-z-]+)?$ ]]; then 8 | echo ${BASH_REMATCH[1]} 9 | elif [[ $1 =~ ^ilp-node-.*$ ]]; then 10 | echo "ilp-node" 11 | elif [[ $1 =~ ^ilp-cli-.*$ ]]; then 12 | echo "ilp-cli" 13 | fi 14 | -------------------------------------------------------------------------------- /.circleci/release/get_docker_image_tag.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Returns `latest` or crate version. If git_tag ($2) argument is given, it is considered as that the new version 4 | # is published. If the tag is the form of semantic version like "ilp-node-0.4.1-beta.3", this script returns 5 | # the version of crate like "0.4.1-beta.3" because tag names generated by `cargo release` look a bit redundant. 6 | # If the tag is not one of semantic version, just returns the tag itself and the docker image will be tagged 7 | # with the git tag. 8 | # If no tag is given, it is considered as a `latest` (or possibly could be said `nightly`) build. 9 | # This script requires `jq` 10 | 11 | crate_name=$1 12 | git_tag=$2 13 | 14 | if [ -n "$git_tag" ]; then 15 | # If it is a tag of semantic version expression 16 | if [[ "$git_tag" =~ ^.*v([0-9]+)\.([0-9]+)\.([0-9]+)(-([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?(\+[0-9A-Za-z-]+)?$ ]] ; then 17 | cargo read-manifest --manifest-path crates/${crate_name}/Cargo.toml | jq -r .version 18 | else 19 | printf "$git_tag" 20 | fi 21 | else 22 | printf "latest" 23 | fi 24 | -------------------------------------------------------------------------------- /.circleci/release/github_release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Intended to be run in the top directory. 4 | # 5 | # This script creates a GitHub release and attach specified asset files to the release. 6 | # [WARNING] If there is any release that has the same name, it will be replaced. 7 | # 8 | # Env var: 9 | # GITHUB_OAUTH_TOKEN: OAuth token for GitHub 10 | # Arguments: 11 | # [tag_name] [release_name] [release_note_path] [asset_path].. 12 | # Note: 13 | # - We need to set Authorization and User-Agent headers. 14 | # - You can generate OAuth tokens from https://github.com/settings/tokens 15 | 16 | function push_release() { 17 | local repository="interledger-rs/interledger-rs" 18 | local user_agent="curl-on-CircleCI" 19 | local tag_name="$1" 20 | local release_name="$2" 21 | local release_note_path="$3" 22 | local log_dir=logs/${release_name} 23 | shift 3 24 | 25 | if [ -z "${tag_name}" ]; then 26 | printf "%s\n" "tag name is required." 27 | exit 1 28 | fi 29 | if [ -z "${release_name}" ]; then 30 | printf "%s\n" "release name is required." 31 | exit 1 32 | fi 33 | if [ -z "${release_note_path}" ]; then 34 | printf "%s\n" "release note path is required." 35 | exit 1 36 | fi 37 | if [ ! -e "${release_note_path}" ] || [ ! -f "${release_note_path}" ]; then 38 | printf "%s\n" "release note file was not found." 39 | exit 1 40 | fi 41 | if [ ! $# -ge 1 ]; then 42 | printf "%s\n" "asset path(s) is required." 43 | exit 1 44 | fi 45 | 46 | mkdir -p ${log_dir} 47 | 48 | # check if there is any release of the same name 49 | curl \ 50 | -X GET \ 51 | -H "User-Agent: ${user_agent}" \ 52 | -H "Authorization: token ${GITHUB_OAUTH_TOKEN}" \ 53 | -H "Accept: application/vnd.github.v3+json" \ 54 | https://api.github.com/repos/${repository}/releases/tags/${release_name} 2>/dev/null >${log_dir}/prev_release.json || exit 2 55 | local release_id=$(jq -r .id < "${log_dir}/prev_release.json") 56 | 57 | # delete it if found 58 | if [ "${release_id}" != "null" ]; then 59 | printf "%s%d%s\n" "Found a release of the same name: " "${release_id}" ", deleting..." 60 | curl \ 61 | -X DELETE \ 62 | -H "User-Agent: ${user_agent}" \ 63 | -H "Authorization: token ${GITHUB_OAUTH_TOKEN}" \ 64 | -H "Accept: application/vnd.github.v3+json" \ 65 | https://api.github.com/repos/${repository}/releases/${release_id} 2>/dev/null >${log_dir}/delete_release.json || exit 2 66 | fi 67 | 68 | # create a release 69 | json=$(printf '{ 70 | "tag_name": "%s", 71 | "name": "%s", 72 | "body": "" 73 | }' "${tag_name}" "${release_name}" | jq --arg release_note "$(cat ${release_note_path})" '.body=$release_note') 74 | 75 | printf "%s" "Creating a release: ${release_name}..." 76 | curl \ 77 | -X POST \ 78 | -H "User-Agent: ${user_agent}" \ 79 | -H "Authorization: token ${GITHUB_OAUTH_TOKEN}" \ 80 | -H "Accept: application/vnd.github.v3+json" \ 81 | -d "${json}" \ 82 | https://api.github.com/repos/${repository}/releases 2>/dev/null >${log_dir}/release.json || exit 2 83 | printf "%s\n" "done" 84 | 85 | asset_upload_url=$(jq -r ".upload_url" < "${log_dir}/release.json") 86 | asset_upload_url=${asset_upload_url/\{\?name,label\}/} 87 | 88 | for asset_path in $@ 89 | do 90 | file_name=$(basename "${asset_path}") 91 | content_type=$(file -b --mime-type "${asset_path}") 92 | printf "%s" "Uploading an asset: ${file_name}..." 93 | curl \ 94 | -X POST \ 95 | -H "User-Agent: curl-on-CircleCI" \ 96 | -H "Authorization: token ${GITHUB_OAUTH_TOKEN}" \ 97 | -H "Content-Type: $(file -b --mime-type ${content_type})" \ 98 | --data-binary @${asset_path} \ 99 | ${asset_upload_url}?name=${file_name} 2>/dev/null >${log_dir}/asset_${file_name}.json || exit 2 100 | printf "%s\n" "done" 101 | done 102 | } 103 | 104 | if [ ! $# -ge 3 ]; then 105 | printf "%s\n" "missing parameter(s)." 106 | exit 1 107 | fi 108 | 109 | mkdir -p logs 110 | 111 | push_release "$@" 112 | -------------------------------------------------------------------------------- /.circleci/release/github_tag.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function create_tag() { 4 | local repository="interledger-rs/interledger-rs" 5 | local user_agent="curl-on-CircleCI" 6 | local tag_name=$1 7 | local message=$2 8 | local object=$3 9 | local type=$4 10 | local log_dir=${LOG_DIR:-logs} 11 | 12 | if [ -z "${tag_name}" ]; then 13 | printf "%s\n" "tag name is required." 14 | exit 1 15 | fi 16 | if [ -z "${message}" ]; then 17 | printf "%s\n" "message name is required." 18 | exit 1 19 | fi 20 | if [ -z "${object}" ]; then 21 | printf "%s\n" "object is required." 22 | exit 1 23 | fi 24 | if [ -z "${type}" ]; then 25 | printf "%s\n" "type is required." 26 | exit 1 27 | fi 28 | 29 | mkdir -p "${log_dir}" 30 | 31 | # check if there is any release of the same tag 32 | curl \ 33 | -X GET \ 34 | -H "User-Agent: ${user_agent}" \ 35 | -H "Authorization: token ${GITHUB_OAUTH_TOKEN}" \ 36 | -H "Accept: application/vnd.github.v3+json" \ 37 | https://api.github.com/repos/${repository}/releases/tags/${tag_name} 2>/dev/null >${log_dir}/prev_tag.json || exit 2 38 | local release_id=$(jq -r .id < "${log_dir}/prev_tag.json") 39 | 40 | # delete it if found 41 | if [ "${release_id}" != "null" ]; then 42 | printf "%s%d%s\n" "Found a tag of the same name: " "${release_id}" ", deleting..." 43 | curl \ 44 | -X DELETE \ 45 | -H "User-Agent: ${user_agent}" \ 46 | -H "Authorization: token ${GITHUB_OAUTH_TOKEN}" \ 47 | -H "Accept: application/vnd.github.v3+json" \ 48 | https://api.github.com/repos/${repository}/releases/${tag_name} 2>/dev/null >${log_dir}/delete_tag.json || exit 2 49 | fi 50 | 51 | # create a new tag 52 | json=$(printf '{ 53 | "tag": "%s", 54 | "message": "%s", 55 | "object": "%s", 56 | "type": "%s" 57 | }' "${tag_name}" "${message}" "${object}" "${type}") 58 | 59 | printf "%s\n" "Creating a tag: ${tag_name}..." 60 | curl \ 61 | -X POST \ 62 | -H "User-Agent: ${user_agent}" \ 63 | -H "Authorization: token ${GITHUB_OAUTH_TOKEN}" \ 64 | -H "Accept: application/vnd.github.v3+json" \ 65 | -d "${json}" \ 66 | https://api.github.com/repos/${repository}/git/tags 2>/dev/null >${log_dir}/tag.json || exit 2 67 | } 68 | 69 | create_tag "$@" 70 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | target 2 | docs 3 | examples 4 | -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | # Use the commitizen / AngularJS commit message style 2 | # See https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines for more information 3 | COMMIT_MESSAGE_REGEX: /(?:^(feat|fix|docs|style|refactor|perf|test|chore|ci|build)(\(.{1,30}\))?:[ ].{5,100}$)(:?\n(?:\n{1,2}^.{1,100}$)+)?/m -------------------------------------------------------------------------------- /.github/workflows/commits.yml: -------------------------------------------------------------------------------- 1 | # Checks all commits in a PR follow the repo rules: 2 | # 3 | # 1. conventional commit messages 4 | # 2. used `git commit --signoff` 5 | # 3. no extra merge commits 6 | name: Commits 7 | 8 | on: 9 | workflow_dispatch: 10 | # Perform these checks on PRs into *any* branch. 11 | # 12 | # Motivation: 13 | # Commits which are not --signoff but merged into other branches 14 | # will likely make their way into PRs for master. At which 15 | # point it will be difficult to get the original author to --signoff. 16 | pull_request: 17 | 18 | jobs: 19 | commit-checks: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v2 23 | with: 24 | fetch-depth: 0 25 | 26 | - name: Set commit range variables 27 | # Finding the commit range is not as trivial as it may seem. 28 | # 29 | # At this stage git's HEAD does not refer to the latest commit in the PR, 30 | # but rather to the merge commit inserted by the PR. So instead we have 31 | # to get 'HEAD' from the PR event. 32 | # 33 | # One cannot use the number of commits (github.event.pull_request.commits) 34 | # to find the start commit i.e. HEAD~N does not work, this breaks if there 35 | # are merge commits. 36 | run: | 37 | echo "PR_HEAD=${{ github.event.pull_request.head.sha }}" >> $GITHUB_ENV 38 | echo "PR_BASE=${{ github.event.pull_request.base.sha }}" >> $GITHUB_ENV 39 | 40 | - name: Install conventional-commit linter 41 | run: | 42 | # Intall the linter and the conventional commits config 43 | npm install @commitlint/config-conventional @commitlint/cli 44 | 45 | # Extend the conventional commits config with the `--signoff` 46 | # requirement. 47 | echo "module.exports = { 48 | extends: ['@commitlint/config-conventional'], 49 | rules: { 50 | 'signed-off-by': [2, 'always', 'Signed-off-by:'], 51 | } 52 | }" > commitlint.config.js 53 | 54 | - name: Conventional commit check 55 | run: | 56 | npx commitlint --from $PR_BASE --to $PR_HEAD 57 | 58 | - name: No merge commits 59 | run: | 60 | # This will list any merge commits in the PR commit path 61 | MERGE=$(git log --merges --ancestry-path $PR_BASE..$PR_HEAD) 62 | 63 | # The merge list should be empty 64 | [[ ! -z "$MERGE" ]] && { 65 | echo "PR contains merge commits:"; 66 | echo $MERGE; 67 | exit 1; 68 | } 69 | exit 0; 70 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | tags: 8 | - 'ilp-node-*' 9 | - 'ilp-cli-*' 10 | 11 | jobs: 12 | update-docker-images: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout sources 16 | uses: actions/checkout@v2 17 | 18 | - name: Install dependencies 19 | run: | 20 | sudo apt-get update 21 | sudo apt-get install -y redis-server redis-tools libssl-dev 22 | 23 | - name: Install node 24 | uses: actions/setup-node@v2 25 | with: 26 | node-version: 'v12.18.4' 27 | 28 | - name: Get tags 29 | id: tags 30 | run: | 31 | TAG=${GITHUB_REF#refs/tags/} 32 | if [ $TAG = "refs/heads/master" ]; then 33 | TAG="" 34 | fi 35 | echo ::set-output name=tag::${TAG} 36 | 37 | ILP_NODE_IMAGE_TAG=$(./.circleci/release/get_docker_image_tag.sh ilp-node ${TAG}) 38 | echo ::set-output name=ilp_node_image_tag::${ILP_NODE_IMAGE_TAG} 39 | echo "ilp node image tag: ${ILP_NODE_IMAGE_TAG}" 40 | 41 | ILP_NODE_CLI_IMAGE_TAG=$(./.circleci/release/get_docker_image_tag.sh ilp-cli ${TAG}) 42 | echo ::set-output name=ilp_node_cli_image_tag::${ILP_NODE_CLI_IMAGE_TAG} 43 | 44 | - name: Login to Docker Hub 45 | uses: docker/login-action@v1 46 | with: 47 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 48 | password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} 49 | 50 | # Build and push ilp-node in the case of push to master or tag with 'ilp-node-*' 51 | - name: Build ilp-node 52 | if: startsWith(steps.tags.outputs.tag, 'ilp-cli-') != true 53 | uses: docker/build-push-action@v2 54 | with: 55 | context: . 56 | file: ./docker/ilp-node.dockerfile 57 | push: true 58 | tags: interledgerrs/ilp-node:${{steps.tags.outputs.ilp_node_image_tag}} 59 | build-args: | 60 | CARGO_BUILD_OPTION=--release 61 | RUST_BIN_DIR_NAME=release 62 | 63 | # Build and push together with ilp-node in the case of push to master or tag with 'ilp-node-*' 64 | - name: Build ilp-testnet 65 | if: startsWith(steps.tags.outputs.tag, 'ilp-cli-') != true 66 | uses: docker/build-push-action@v2 67 | with: 68 | context: . 69 | file: ./docker/Dockerfile 70 | push: true 71 | tags: interledgerrs/testnet-bundle:${{steps.tags.outputs.ilp_node_image_tag}} 72 | 73 | # Build and push in the case of push to master or tag with `ilp-cli-*` 74 | - name: Build ilp-cli 75 | if: startsWith(steps.tags.outputs.tag, 'ilp-node-') != true 76 | uses: docker/build-push-action@v2 77 | with: 78 | context: . 79 | file: ./docker/ilp-cli.dockerfile 80 | push: true 81 | tags: interledgerrs/ilp-cli:${{steps.tags.outputs.ilp_node_cli_image_tag}} 82 | 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | **/*.rs.bk 3 | .vscode 4 | .idea 5 | *.code-workspace 6 | *.csv* 7 | *.ods* 8 | *.xls* 9 | *.rdb 10 | *.aof 11 | cmake-build-debug/ 12 | **/*.log 13 | .DS_Store 14 | 15 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "./crates/ilp-cli", 5 | "./crates/ilp-node", 6 | "./crates/interledger", 7 | "./crates/interledger-api", 8 | "./crates/interledger-btp", 9 | "./crates/interledger-ccp", 10 | "./crates/interledger-http", 11 | "./crates/interledger-ildcp", 12 | "./crates/interledger-packet", 13 | "./crates/interledger-router", 14 | "./crates/interledger-rates", 15 | "./crates/interledger-service", 16 | "./crates/interledger-service-util", 17 | "./crates/interledger-settlement", 18 | "./crates/interledger-spsp", 19 | "./crates/interledger-store", 20 | "./crates/interledger-stream", 21 | "./crates/interledger-errors", 22 | ] 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018-2019 Evan Schwartz and contributors 2 | Copyright 2017-2018 Evan Schwartz 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Interledger.rs 3 |

4 | 5 | --- 6 | > Interledger implementation in Rust :money_with_wings: 7 | 8 | [![crates.io](https://img.shields.io/crates/v/interledger.svg)](https://crates.io/crates/interledger) 9 | [![Interledger.rs Documentation](https://docs.rs/interledger/badge.svg)](https://docs.rs/interledger) 10 | [![CircleCI](https://circleci.com/gh/interledger-rs/interledger-rs.svg?style=shield)](https://circleci.com/gh/interledger-rs/interledger-rs) 11 | ![rustc](https://img.shields.io/badge/rustc-1.39+-red.svg) 12 | ![Rust](https://img.shields.io/badge/rust-stable-Success) 13 | [![Docker Image](https://img.shields.io/docker/pulls/interledgerrs/ilp-node.svg?maxAge=2592000)](https://hub.docker.com/r/interledgerrs/ilp-node/) 14 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 15 | 16 | ## Requirements 17 | 18 | All crates require Rust 2018 edition and are tested on the following channels: 19 | 20 | - `stable` 21 | 22 | ## Connecting to the Testnet 23 | 24 | See the [testnet instructions](./docs/testnet.md) to quickly connect to the testnet with a bundle that includes the Interledger.rs node and settlement engines. 25 | 26 | ## Understanding Interledger.rs 27 | - [HTTP API](./docs/api.md) 28 | - [Rust API](https://docs.rs/interledger) 29 | - [Interledger.rs Architecture](./docs/architecture.md) 30 | - [Interledger Forum](https://forum.interledger.org) for general questions about the Interledger Protocol and Project 31 | 32 | ## Installation and Usage 33 | 34 | To run the Interledger.rs components by themselves (rather than the `testnet-bundle`), you can follow these instructions: 35 | 36 | ### Using Docker 37 | 38 | #### Prerequisites 39 | 40 | - Docker 41 | 42 | #### Install 43 | 44 | ```bash # 45 | docker pull interledgerrs/ilp-node 46 | docker pull interledgerrs/ilp-cli 47 | docker pull interledgerrs/ilp-settlement-ethereum 48 | ``` 49 | 50 | #### Run 51 | 52 | ```bash # 53 | # This runs the sender / receiver / router bundle 54 | docker run -it interledgerrs/ilp-node 55 | 56 | # This is a simple CLI for interacting with the node's HTTP API 57 | docker run -it --rm interledgerrs/ilp-cli 58 | 59 | # This includes the Ethereum Settlement Engines written in Rust 60 | docker run -it interledgerrs/ilp-settlement-ethereum 61 | ``` 62 | 63 | ### Building From Source 64 | 65 | #### Prerequisites 66 | 67 | - Git 68 | - [Redis](https://redis.io/) 69 | - [Rust](https://www.rust-lang.org/tools/install) - latest stable version 70 | 71 | #### Install 72 | 73 | ```bash # 74 | # 1. Clone the repsitory and change the working directory 75 | git clone https://github.com/interledger-rs/interledger-rs && cd interledger-rs 76 | 77 | # 2. Build interledger-rs (add `--release` to compile the release version, which is slower to compile but faster to run) 78 | cargo build 79 | ``` 80 | 81 | You can find the Interledger Settlement Engines in a [separate repository](https://github.com/interledger-rs/settlement-engines). 82 | 83 | #### Run 84 | 85 | ```bash # 86 | # This runs the ilp-node 87 | cargo run --bin ilp-node -- # Put CLI args after the "--" 88 | 89 | cargo run --bin ilp-cli -- # Put CLI args after the "--" 90 | ``` 91 | 92 | Append the `--help` flag to see available options. 93 | 94 | See [configuration](./docs/configuration.md) for more details on how the node is configured. 95 | 96 | #### Configuring Redis 97 | 98 | We have some account settings such as `amount_per_minute_limit` or `packets_per_minute_limit`. In order to enable these options, you need to load the [redis-cell](https://github.com/brandur/redis-cell) module as follows. *You don't need to load this module unless you use the rate-limit options.* 99 | 100 | ``` 101 | # in your redis config file 102 | # libredis_cell.so file will be found in crates/interledger-store/external 103 | loadmodule /path/to/modules/libredis_cell.so 104 | ``` 105 | 106 | or you can specify an argument when you start up the redis instance as follows. 107 | 108 | ``` 109 | redis-server --loadmodule /path/to/modules/libredis_cell.so 110 | ``` 111 | 112 | ## Examples 113 | 114 | See the [examples](./examples/README.md) for demos of Interledger functionality and how to use the Interledger.rs implementation. 115 | 116 | ## Contributing 117 | 118 | Contributions are very welcome and if you're interested in getting involved, see [CONTRIBUTING.md](docs/CONTRIBUTING.md). We're more than happy to answer questions and mentor you in making your first contributions to Interledger.rs (even if you've never written in Rust before)! 119 | -------------------------------------------------------------------------------- /crates/ilp-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ilp-cli" 3 | version = "1.0.0" 4 | authors = ["Ben Striegel "] 5 | description = "Interledger.rs Command-Line Interface" 6 | license = "Apache-2.0" 7 | edition = "2018" 8 | repository = "https://github.com/interledger-rs/interledger-rs" 9 | 10 | [dependencies] 11 | clap = { version = "2.33.0", default-features = false } 12 | thiserror = { version = "1.0.10", default-features = false } 13 | http = { version = "0.2", default-features = false } 14 | reqwest = { version = "0.11.4", default-features = false, features = ["default-tls", "blocking", "json"] } 15 | serde = { version = "1.0.101", default-features = false, features = ["derive"] } 16 | serde_json = { version = "1.0.41", default-features = false } 17 | tokio-tungstenite = { version = "0.15.0", default-features = false, features = ["connect", "native-tls"] } 18 | url = { version = "2.1.1", default-features = false } 19 | -------------------------------------------------------------------------------- /crates/ilp-cli/README.md: -------------------------------------------------------------------------------- 1 | # Interledger CLI 2 | 3 | Command Line Interface which makes calls over HTTP to the Interledger node. 4 | 5 | Build yourself: 6 | ```bash 7 | cargo build --bin ilp-cli 8 | ``` 9 | 10 | Run via docker: 11 | 12 | ```bash 13 | docker pull interledgerrs/ilp-cli 14 | docker run -it interledgerrs/ilp-cli 15 | ``` 16 | 17 | 18 | Example output: 19 | 20 | ```bash 21 | $ cargo run --bin ilp-cli --help 22 | 23 | ilp-cli 0.0.1 24 | Interledger.rs Command-Line Interface 25 | 26 | USAGE: 27 | ilp-cli [FLAGS] [OPTIONS] [SUBCOMMAND] 28 | 29 | FLAGS: 30 | -h, --help Prints help information 31 | -q, --quiet Disable printing the bodies of successful HTTP responses upon receipt 32 | -V, --version Prints version information 33 | 34 | OPTIONS: 35 | --node The base URL of the node to connect to [env: ILP_CLI_NODE_URL=] [default: 36 | http://localhost:7770] 37 | 38 | SUBCOMMANDS: 39 | accounts Operations for interacting with accounts 40 | help Prints this message or the help of the given subcommand(s) 41 | pay Send a payment from an account on this node 42 | rates Operations for interacting with exchange rates 43 | routes Operations for interacting with the routing table 44 | settlement-engines Interact with the settlement engine configurations 45 | status Query the status of the server 46 | testnet Easily access the testnet 47 | ``` -------------------------------------------------------------------------------- /crates/ilp-node/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ilp-node" 3 | version = "1.0.0" 4 | authors = ["Evan Schwartz "] 5 | description = "Interledger node (sender, connector, receiver bundle)" 6 | license = "Apache-2.0" 7 | edition = "2018" 8 | repository = "https://github.com/interledger-rs/interledger-rs" 9 | default-run = "ilp-node" 10 | build = "build.rs" 11 | 12 | [features] 13 | default = ["balance-tracking", "redis", "monitoring"] 14 | balance-tracking = [] 15 | redis = ["redis_crate", "interledger/redis"] 16 | 17 | # This is an experimental feature that enables submitting packet 18 | # records to Google Cloud PubSub. This may be removed in the future. 19 | google-pubsub = ["base64", "chrono", "parking_lot", "reqwest", "serde_json", "yup-oauth2"] 20 | # This enables monitoring and tracing related features 21 | monitoring = [ 22 | "metrics", 23 | "metrics-core", 24 | "metrics-runtime", 25 | "tracing-futures", 26 | "tracing-subscriber", 27 | "tracing-appender", 28 | ] 29 | 30 | [[test]] 31 | name = "redis_tests" 32 | path = "tests/redis/redis_tests.rs" 33 | required-features = ["redis"] 34 | 35 | [build-dependencies] 36 | # vergen allows to get the VERGEN_BUILD_TIMESTAMP etc environment variables 37 | vergen = { version = "4.2" } 38 | 39 | [dependencies] 40 | interledger = { path = "../interledger", version = "1.0.0", default-features = false, features = ["node"] } 41 | 42 | bytes = { package = "bytes", version = "1.0.1" } 43 | cfg-if = { version = "0.1.10", default-features = false } 44 | clap = { version = "2.33.0", default-features = false } 45 | config = { version = "0.10.1", default-features = false, features = ["json", "yaml"] } 46 | futures = { version = "0.3.7", default-features = false, features = ["compat"] } 47 | hex = { version = "0.4.0" } 48 | once_cell = { version = "1.3.1", default-features = false } 49 | num-bigint = { version = "0.2.3", default-features = false, features = ["std"] } 50 | redis_crate = { package = "redis", version = "0.21.0", optional = true, default-features = false, features = ["tokio-comp"] } 51 | ring = { version = "0.16.9", default-features = false } 52 | serde = { version = "1.0.101", default-features = false } 53 | tokio = { version = "1.9.0", default-features = false, features = ["rt-multi-thread", "macros", "time", "sync"] } 54 | tokio-stream = { version = "0.1.7", features = ["sync"] } 55 | tracing = { version = "0.1.12", default-features = false, features = ["log"] } 56 | url = { version = "2.1.1", default-features = false } 57 | libc = { version = "0.2.62", default-features = false } 58 | warp = { version = "0.3.1", default-features = false, features = ["websocket"] } 59 | secrecy = { version = "0.8", default-features = false, features = ["alloc", "serde"] } 60 | uuid = { version = "0.8.1", default-features = false, features = ["v4"] } 61 | 62 | # For google-pubsub 63 | base64 = { version = "0.13.0", default-features = false, optional = true } 64 | chrono = { version = "0.4.20", default-features = false, optional = true} 65 | parking_lot = { version = "0.10.0", default-features = false, optional = true } 66 | reqwest = { version = "0.11.4", default-features = false, features = ["default-tls", "json"], optional = true } 67 | serde_json = { version = "1.0.41", default-features = false, optional = true } 68 | yup-oauth2 = { version = "5.1.0", optional = true } 69 | 70 | # Tracing / metrics / prometheus for instrumentation 71 | tracing-futures = { version = "0.2", default-features = false, features = ["std", "futures-03"], optional = true } 72 | tracing-subscriber = { version = "0.2.0", default-features = false, features = ["tracing-log", "fmt", "env-filter", "chrono"], optional = true } 73 | tracing-appender = { version = "0.1", optional = true } 74 | metrics = { version = "0.12.0", default-features = false, features = ["std"], optional = true } 75 | metrics-core = { version = "0.5.1", default-features = false, optional = true } 76 | metrics-runtime = { version = "0.13.0", default-features = false, features = ["metrics-observer-prometheus"], optional = true } 77 | 78 | [dev-dependencies] 79 | base64 = { version = "0.13.0", default-features = false } 80 | socket2 = "0.4.0" 81 | rand = { version = "0.7.2", default-features = false } 82 | reqwest = { version = "0.11.4", default-features = false, features = ["default-tls", "json"] } 83 | serde_json = { version = "1.0.41", default-features = false } 84 | tokio-tungstenite = { version = "0.15.0" } 85 | criterion = { version = "0.3", default-features = false , features = ["cargo_bench_support", 'html_reports']} 86 | tempfile = "3" 87 | 88 | [badges] 89 | circle-ci = { repository = "interledger-rs/interledger-rs" } 90 | codecov = { repository = "interledger-rs/interledger-rs" } 91 | 92 | [[bench]] 93 | name = "multiple_payments" 94 | harness = false 95 | -------------------------------------------------------------------------------- /crates/ilp-node/README.md: -------------------------------------------------------------------------------- 1 | # ilp-node 2 | 3 | The Interledger.rs node bundles all the functionality necessary to send, receive, and forward 4 | Interledger packets. See the examples for how to configure and use the `ilp-node`. 5 | 6 | #### Benchmark 7 | 8 | ```bash # 9 | # This runs the process payment benchmark 10 | cargo bench -- process_payment 11 | ``` -------------------------------------------------------------------------------- /crates/ilp-node/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | { 3 | use vergen::{vergen, Config, ShaKind}; 4 | let mut config = Config::default(); 5 | *config.git_mut().sha_mut() = true; 6 | *config.git_mut().sha_kind_mut() = ShaKind::Short; 7 | vergen(config).expect("Unable to generate the cargo keys! Do you have git installed?"); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /crates/ilp-node/src/instrumentation/metrics.rs: -------------------------------------------------------------------------------- 1 | use interledger::{ 2 | ccp::CcpRoutingAccount, 3 | service::{ 4 | Account, IlpResult, IncomingRequest, IncomingService, OutgoingRequest, OutgoingService, 5 | }, 6 | }; 7 | use metrics::{self, labels, recorder, Key}; 8 | use std::time::Instant; 9 | 10 | pub async fn incoming_metrics( 11 | request: IncomingRequest, 12 | mut next: Box + Send>, 13 | ) -> IlpResult { 14 | let labels = labels!( 15 | "from_asset_code" => request.from.asset_code().to_string(), 16 | "from_routing_relation" => request.from.routing_relation().to_string(), 17 | ); 18 | recorder().increment_counter( 19 | Key::from_name_and_labels("requests.incoming.prepare", labels.clone()), 20 | 1, 21 | ); 22 | let start_time = Instant::now(); 23 | 24 | let result = next.handle_request(request).await; 25 | if result.is_ok() { 26 | recorder().increment_counter( 27 | Key::from_name_and_labels("requests.incoming.fulfill", labels.clone()), 28 | 1, 29 | ); 30 | } else { 31 | recorder().increment_counter( 32 | Key::from_name_and_labels("requests.incoming.reject", labels.clone()), 33 | 1, 34 | ); 35 | } 36 | 37 | recorder().record_histogram( 38 | Key::from_name_and_labels("requests.incoming.duration", labels), 39 | (Instant::now() - start_time).as_nanos() as u64, 40 | ); 41 | result 42 | } 43 | 44 | pub async fn outgoing_metrics( 45 | request: OutgoingRequest, 46 | mut next: Box + Send>, 47 | ) -> IlpResult { 48 | let labels = labels!( 49 | "from_asset_code" => request.from.asset_code().to_string(), 50 | "to_asset_code" => request.to.asset_code().to_string(), 51 | "from_routing_relation" => request.from.routing_relation().to_string(), 52 | "to_routing_relation" => request.to.routing_relation().to_string(), 53 | ); 54 | 55 | // TODO replace these calls with the counter! macro if there's a way to easily pass in the already-created labels 56 | // right now if you pass the labels into one of the other macros, it gets a recursion limit error while expanding the macro 57 | recorder().increment_counter( 58 | Key::from_name_and_labels("requests.outgoing.prepare", labels.clone()), 59 | 1, 60 | ); 61 | let start_time = Instant::now(); 62 | 63 | let result = next.send_request(request).await; 64 | if result.is_ok() { 65 | recorder().increment_counter( 66 | Key::from_name_and_labels("requests.outgoing.fulfill", labels.clone()), 67 | 1, 68 | ); 69 | } else { 70 | recorder().increment_counter( 71 | Key::from_name_and_labels("requests.outgoing.reject", labels.clone()), 72 | 1, 73 | ); 74 | } 75 | 76 | recorder().record_histogram( 77 | Key::from_name_and_labels("requests.outgoing.duration", labels), 78 | (Instant::now() - start_time).as_nanos() as u64, 79 | ); 80 | 81 | result 82 | } 83 | -------------------------------------------------------------------------------- /crates/ilp-node/src/instrumentation/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "monitoring")] 2 | pub mod metrics; 3 | #[cfg(feature = "monitoring")] 4 | pub mod trace; 5 | 6 | #[cfg(feature = "monitoring")] 7 | pub mod prometheus; 8 | 9 | #[cfg(feature = "google-pubsub")] 10 | pub mod google_pubsub; 11 | -------------------------------------------------------------------------------- /crates/ilp-node/src/instrumentation/prometheus.rs: -------------------------------------------------------------------------------- 1 | use crate::InterledgerNode; 2 | use metrics_core::{Builder, Drain, Observe}; 3 | use serde::Deserialize; 4 | use std::{net::SocketAddr, sync::Arc, time::Duration}; 5 | use tracing::{error, info}; 6 | use warp::{ 7 | http::{Response, StatusCode}, 8 | Filter, 9 | }; 10 | 11 | /// Configuration for [Prometheus](https://prometheus.io) metrics collection. 12 | #[derive(Deserialize, Clone, PartialEq, Eq, Debug)] 13 | pub struct PrometheusConfig { 14 | /// IP address and port to host the Prometheus endpoint on. 15 | pub bind_address: SocketAddr, 16 | /// Amount of time, in milliseconds, that the node will collect data points for the 17 | /// Prometheus histograms. Defaults to 300000ms (5 minutes). 18 | #[serde(default = "PrometheusConfig::default_histogram_window")] 19 | pub histogram_window: u64, 20 | /// Granularity, in milliseconds, that the node will use to roll off old data. 21 | /// For example, a value of 1000ms (1 second) would mean that the node forgets the oldest 22 | /// 1 second of histogram data points every second. Defaults to 10000ms (10 seconds). 23 | #[serde(default = "PrometheusConfig::default_histogram_granularity")] 24 | pub histogram_granularity: u64, 25 | } 26 | 27 | impl PrometheusConfig { 28 | fn default_histogram_window() -> u64 { 29 | 300_000 30 | } 31 | 32 | fn default_histogram_granularity() -> u64 { 33 | 10_000 34 | } 35 | } 36 | 37 | /// Starts a Prometheus metrics server that will listen on the configured address. 38 | /// 39 | /// # Errors 40 | /// This will fail if another Prometheus server is already running in this 41 | /// process or on the configured port. 42 | #[allow(clippy::cognitive_complexity)] 43 | pub async fn serve_prometheus(node: InterledgerNode) -> Result<(), ()> { 44 | let prometheus = if let Some(ref prometheus) = node.prometheus { 45 | prometheus 46 | } else { 47 | error!(target: "interledger-node", "No prometheus configuration provided"); 48 | return Err(()); 49 | }; 50 | 51 | // Set up the metrics collector 52 | let receiver = metrics_runtime::Builder::default() 53 | .histogram( 54 | Duration::from_millis(prometheus.histogram_window), 55 | Duration::from_millis(prometheus.histogram_granularity), 56 | ) 57 | .build() 58 | .expect("Failed to create metrics Receiver"); 59 | 60 | let controller = receiver.controller(); 61 | // Try installing the global recorder 62 | match metrics::set_boxed_recorder(Box::new(receiver)) { 63 | Ok(_) => { 64 | let observer = Arc::new(metrics_runtime::observers::PrometheusBuilder::default()); 65 | 66 | let filter = warp::get().and(warp::path::end()).map(move || { 67 | let mut observer = observer.build(); 68 | controller.observe(&mut observer); 69 | let prometheus_response = observer.drain(); 70 | Response::builder() 71 | .status(StatusCode::OK) 72 | .header("Content-Type", "text/plain; version=0.0.4") 73 | .body(prometheus_response) 74 | }); 75 | 76 | info!(target: "interledger-node", 77 | "Prometheus metrics server listening on: {}", 78 | prometheus.bind_address 79 | ); 80 | 81 | tokio::spawn(warp::serve(filter).bind(prometheus.bind_address)); 82 | Ok(()) 83 | } 84 | Err(e) => { 85 | 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); 86 | Err(()) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /crates/ilp-node/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![type_length_limit = "10000000"] 2 | mod instrumentation; 3 | mod node; 4 | 5 | #[cfg(feature = "redis")] 6 | mod redis_store; 7 | 8 | pub use node::*; 9 | -------------------------------------------------------------------------------- /crates/ilp-node/src/redis_store.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "redis")] 2 | 3 | use crate::node::{InterledgerNode, LogWriter}; 4 | use futures::TryFutureExt; 5 | pub use interledger::{ 6 | api::{AccountDetails, NodeStore}, 7 | packet::Address, 8 | service::Account, 9 | store::redis::RedisStoreBuilder, 10 | }; 11 | pub use redis_crate::{ConnectionInfo, IntoConnectionInfo}; 12 | use ring::hmac; 13 | use tracing::error; 14 | 15 | static REDIS_SECRET_GENERATION_STRING: &str = "ilp_redis_secret"; 16 | 17 | pub fn default_redis_url() -> String { 18 | String::from("redis://127.0.0.1:6379") 19 | } 20 | 21 | // This function could theoretically be defined as an inherent method on InterledgerNode itself. 22 | // However, we define it in this module in order to consolidate conditionally-compiled code 23 | // into as few discrete units as possible. 24 | pub async fn serve_redis_node( 25 | node: InterledgerNode, 26 | ilp_address: Address, 27 | log_writer: Option, 28 | ) -> Result<(), ()> { 29 | let redis_connection_info = node.database_url.clone().into_connection_info().unwrap(); 30 | let redis_addr = redis_connection_info.addr.clone(); 31 | let redis_secret = generate_redis_secret(&node.secret_seed); 32 | let store = RedisStoreBuilder::new(redis_connection_info, redis_secret) 33 | .with_db_prefix(node.database_prefix.as_str()) 34 | .node_ilp_address(ilp_address.clone()) 35 | .connect() 36 | .map_err(move |err| error!(target: "interledger-node", "Error connecting to Redis: {:?} {:?}", redis_addr, err)) 37 | .await?; 38 | node.chain_services(store, ilp_address, log_writer).await 39 | } 40 | 41 | pub fn generate_redis_secret(secret_seed: &[u8; 32]) -> [u8; 32] { 42 | let mut redis_secret: [u8; 32] = [0; 32]; 43 | let sig = hmac::sign( 44 | &hmac::Key::new(hmac::HMAC_SHA256, secret_seed), 45 | REDIS_SECRET_GENERATION_STRING.as_bytes(), 46 | ); 47 | redis_secret.copy_from_slice(sig.as_ref()); 48 | redis_secret 49 | } 50 | -------------------------------------------------------------------------------- /crates/ilp-node/tests/redis/exchange_rates.rs: -------------------------------------------------------------------------------- 1 | use crate::redis_helpers::*; 2 | use crate::test_helpers::*; 3 | use ilp_node::InterledgerNode; 4 | use reqwest::Client; 5 | use serde_json::{self, json, Value}; 6 | use std::env; 7 | use std::time::Duration; 8 | 9 | #[tokio::test] 10 | async fn coincap() { 11 | let context = TestContext::new(); 12 | 13 | let http_port = get_open_port(None); 14 | 15 | let node: InterledgerNode = serde_json::from_value(json!({ 16 | "ilp_address": "example.one", 17 | "default_spsp_account": "one", 18 | "admin_auth_token": "admin", 19 | "database_url": connection_info_to_string(context.get_client_connection_info()), 20 | "http_bind_address": format!("127.0.0.1:{}", http_port), 21 | "settlement_api_bind_address": format!("127.0.0.1:{}", get_open_port(None)), 22 | "secret_seed": random_secret(), 23 | "route_broadcast_interval": 200, 24 | "exchange_rate": { 25 | "poll_interval": 100, 26 | "provider": "coincap", 27 | }, 28 | })) 29 | .unwrap(); 30 | node.serve(None).await.unwrap(); 31 | 32 | // Wait a few seconds so our node can poll the API 33 | tokio::time::sleep(Duration::from_millis(1000)).await; 34 | 35 | let ret = Client::new() 36 | .get(&format!("http://localhost:{}/rates", http_port)) 37 | .send() 38 | .await 39 | .unwrap(); 40 | let txt = ret.text().await.unwrap(); 41 | let obj: Value = serde_json::from_str(&txt).unwrap(); 42 | 43 | assert_eq!( 44 | format!("{}", obj.get("USD").expect("Should have USD rate")).as_str(), 45 | "1.0" 46 | ); 47 | 48 | // since coinbase sometimes suspends some exchange rates we would consider the test correct if 70% of the following rates are available 49 | let mut count = 0; 50 | let expected_rates = ["EUR", "JPY", "BTC", "ETH", "XRP"]; 51 | for r in &expected_rates { 52 | if obj.get(r).is_some() { 53 | count += 1; 54 | } 55 | } 56 | 57 | assert!(count as f32 >= 0.7 * expected_rates.len() as f32) 58 | } 59 | 60 | // TODO can we disable this with conditional compilation? 61 | #[tokio::test] 62 | async fn cryptocompare() { 63 | let context = TestContext::new(); 64 | 65 | let api_key = match env::var("ILP_TEST_CRYPTOCOMPARE_API_KEY") { 66 | Ok(value) => value, 67 | Err(_) => { 68 | eprintln!("Skipping cryptocompare test. Must configure an API key by setting ILP_TEST_CRYPTOCOMPARE_API_KEY to run this test"); 69 | return; 70 | } 71 | }; 72 | 73 | let http_port = get_open_port(Some(3011)); 74 | 75 | let node: InterledgerNode = serde_json::from_value(json!({ 76 | "ilp_address": "example.one", 77 | "default_spsp_account": "one", 78 | "admin_auth_token": "admin", 79 | "database_url": connection_info_to_string(context.get_client_connection_info()), 80 | "http_bind_address": format!("127.0.0.1:{}", http_port), 81 | "settlement_api_bind_address": format!("127.0.0.1:{}", get_open_port(None)), 82 | "secret_seed": random_secret(), 83 | "route_broadcast_interval": 200, 84 | "exchange_rate": { 85 | "poll_interval": 100, 86 | "provider": { 87 | "cryptocompare": api_key 88 | }, 89 | "spread": 0.0, 90 | }, 91 | })) 92 | .unwrap(); 93 | node.serve(None).await.unwrap(); 94 | 95 | // Wait a few seconds so our node can poll the API 96 | tokio::time::sleep(Duration::from_millis(1000)).await; 97 | 98 | let ret = Client::new() 99 | .get(&format!("http://localhost:{}/rates", http_port)) 100 | .send() 101 | .await 102 | .unwrap(); 103 | let txt = ret.text().await.unwrap(); 104 | let obj: Value = serde_json::from_str(&txt).unwrap(); 105 | 106 | assert_eq!( 107 | format!("{}", obj.get("USD").expect("Should have USD rate")).as_str(), 108 | "1.0" 109 | ); 110 | assert!(obj.get("BTC").is_some()); 111 | assert!(obj.get("ETH").is_some()); 112 | assert!(obj.get("XRP").is_some()); 113 | } 114 | -------------------------------------------------------------------------------- /crates/ilp-node/tests/redis/redis_tests.rs: -------------------------------------------------------------------------------- 1 | #![type_length_limit = "10000000"] 2 | mod btp; 3 | mod exchange_rates; 4 | mod payments_incoming; 5 | mod three_nodes; 6 | mod time_based_settlement; 7 | 8 | // Only run prometheus tests if the monitoring feature is turned on 9 | #[cfg(feature = "monitoring")] 10 | mod prometheus; 11 | 12 | mod redis_helpers; 13 | mod test_helpers; 14 | -------------------------------------------------------------------------------- /crates/ilp-node/tests/redis/test_helpers.rs: -------------------------------------------------------------------------------- 1 | use bytes::Bytes; 2 | use futures::TryFutureExt; 3 | use interledger::stream::StreamDelivery; 4 | use interledger::{packet::Address, service::Account as AccountTrait, store::account::Account}; 5 | use ring::rand::{SecureRandom, SystemRandom}; 6 | use serde::Serialize; 7 | use serde_json::json; 8 | use std::collections::HashMap; 9 | use std::fmt::{Debug, Display}; 10 | use std::str; 11 | use uuid::Uuid; 12 | 13 | #[allow(unused)] 14 | pub fn random_secret() -> String { 15 | let mut bytes: [u8; 32] = [0; 32]; 16 | SystemRandom::new().fill(&mut bytes).unwrap(); 17 | hex::encode(bytes) 18 | } 19 | 20 | #[derive(serde::Deserialize, Debug, PartialEq)] 21 | pub struct BalanceData { 22 | pub balance: f64, 23 | pub asset_code: String, 24 | } 25 | 26 | #[allow(unused)] 27 | pub async fn create_account_on_node( 28 | api_port: u16, 29 | data: T, 30 | auth: &str, 31 | ) -> Result { 32 | let client = reqwest::Client::new(); 33 | let res = client 34 | .post(&format!("http://localhost:{}/accounts", api_port)) 35 | .header("Content-Type", "application/json") 36 | .header("Authorization", format!("Bearer {}", auth)) 37 | .json(&data) 38 | .send() 39 | .map_err(|_| ()) 40 | .await?; 41 | 42 | let res = res.error_for_status().map_err(|_| ())?; 43 | 44 | Ok(res.json::().map_err(|_| ()).await.unwrap()) 45 | } 46 | 47 | #[allow(unused)] 48 | pub async fn create_account_on_engine( 49 | engine_port: u16, 50 | account_id: T, 51 | ) -> Result { 52 | let client = reqwest::Client::new(); 53 | let res = client 54 | .post(&format!("http://localhost:{}/accounts", engine_port)) 55 | .header("Content-Type", "application/json") 56 | .json(&json!({ "id": account_id })) 57 | .send() 58 | .map_err(|_| ()) 59 | .await?; 60 | 61 | let res = res.error_for_status().map_err(|_| ())?; 62 | 63 | let data: Bytes = res.bytes().map_err(|_| ()).await?; 64 | 65 | Ok(str::from_utf8(&data).unwrap().to_string()) 66 | } 67 | 68 | #[allow(unused)] 69 | pub async fn send_money_to_username( 70 | from_port: u16, 71 | to_port: u16, 72 | amount: u64, 73 | to_username: T, 74 | from_username: &str, 75 | from_auth: &str, 76 | ) -> Result { 77 | let client = reqwest::Client::new(); 78 | let res = client 79 | .post(&format!( 80 | "http://localhost:{}/accounts/{}/payments", 81 | from_port, from_username 82 | )) 83 | .header("Authorization", format!("Bearer {}", from_auth)) 84 | .json(&json!({ 85 | "receiver": format!("http://localhost:{}/accounts/{}/spsp", to_port, to_username), 86 | "source_amount": amount, 87 | "slippage": 0.025 // allow up to 2.5% slippage 88 | })) 89 | .send() 90 | .map_err(|_| ()) 91 | .await?; 92 | 93 | let res = res.error_for_status().map_err(|_| ())?; 94 | Ok(res.json::().await.unwrap()) 95 | } 96 | 97 | #[allow(unused)] 98 | pub async fn get_all_accounts(node_port: u16, admin_token: &str) -> Result, ()> { 99 | let client = reqwest::Client::new(); 100 | let res = client 101 | .get(&format!("http://localhost:{}/accounts", node_port)) 102 | .header("Authorization", format!("Bearer {}", admin_token)) 103 | .send() 104 | .map_err(|_| ()) 105 | .await?; 106 | 107 | let res = res.error_for_status().map_err(|_| ())?; 108 | let body: Bytes = res.bytes().map_err(|_| ()).await?; 109 | let ret: Vec = serde_json::from_slice(&body).unwrap(); 110 | Ok(ret) 111 | } 112 | 113 | #[allow(unused)] 114 | #[allow(clippy::mutable_key_type)] 115 | pub fn accounts_to_ids(accounts: Vec) -> HashMap { 116 | let mut map = HashMap::new(); 117 | for a in accounts { 118 | map.insert(a.ilp_address().clone(), a.id()); 119 | } 120 | map 121 | } 122 | 123 | #[allow(unused)] 124 | pub async fn get_balance( 125 | account_id: T, 126 | node_port: u16, 127 | admin_token: &str, 128 | ) -> Result { 129 | let client = reqwest::Client::new(); 130 | let res = client 131 | .get(&format!( 132 | "http://localhost:{}/accounts/{}/balance", 133 | node_port, account_id 134 | )) 135 | .header("Authorization", format!("Bearer {}", admin_token)) 136 | .send() 137 | .map_err(|_| ()) 138 | .await?; 139 | 140 | let res = res.error_for_status().map_err(|_| ())?; 141 | let body: Bytes = res.bytes().map_err(|_| ()).await?; 142 | let ret: BalanceData = serde_json::from_slice(&body).unwrap(); 143 | Ok(ret) 144 | } 145 | -------------------------------------------------------------------------------- /crates/interledger-api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "interledger-api" 3 | version = "1.0.0" 4 | authors = ["Evan Schwartz "] 5 | description = "API for managing an Interledger node" 6 | license = "Apache-2.0" 7 | edition = "2018" 8 | repository = "https://github.com/interledger-rs/interledger-rs" 9 | 10 | [dependencies] 11 | interledger-packet = { path = "../interledger-packet", version = "1.0.0", default-features = false } 12 | interledger-http = { path = "../interledger-http", version = "1.0.0", default-features = false } 13 | interledger-ildcp = { path = "../interledger-ildcp", version = "1.0.0", default-features = false } 14 | interledger-rates = { path = "../interledger-rates", version = "1.0.0", default-features = false } 15 | interledger-router = { path = "../interledger-router", version = "1.0.0", default-features = false } 16 | interledger-service = { path = "../interledger-service", version = "1.0.0", default-features = false } 17 | interledger-service-util = { path = "../interledger-service-util", version = "1.0.0", default-features = false } 18 | interledger-settlement = { path = "../interledger-settlement", version = "1.0.0", default-features = false } 19 | interledger-spsp = { path = "../interledger-spsp", version = "1.0.0", default-features = false } 20 | interledger-stream = { path = "../interledger-stream", version = "1.0.0", default-features = false } 21 | interledger-ccp = { path = "../interledger-ccp", version = "1.0.0", default-features = false } 22 | interledger-btp = { path = "../interledger-btp", version = "1.0.0", default-features = false } 23 | interledger-errors = { path = "../interledger-errors", version = "1.0.0", default-features = false, features = ["warp_errors"] } 24 | 25 | bytes = { version = "1.0.1", default-features = false } 26 | futures = { version = "0.3.7", default-features = false } 27 | futures-retry = { version = "0.6.0", default-features = false } 28 | http = { version = "0.2", default-features = false } 29 | tracing = { version = "0.1.12", default-features = false, features = ["log"] } 30 | serde = { version = "1.0.101", default-features = false, features = ["derive"] } 31 | serde_json = { version = "1.0.41", default-features = false } 32 | reqwest = { version = "0.11.4", default-features = false, features = ["default-tls", "json"] } 33 | url = { version = "2.1.1", default-features = false, features = ["serde"] } 34 | uuid = { version = "0.8.1", default-features = false} 35 | warp = { version = "0.3.1", default-features = false } 36 | secrecy = { version = "0.8", default-features = false, features = ["serde"] } 37 | once_cell = "1.3.1" 38 | async-trait = "0.1.22" 39 | tokio = { version = "1.9.0", default-features = false, features = ["rt", "macros"] } 40 | tokio-stream = { version = "0.1.7", features = ["sync"] } 41 | 42 | 43 | [dev-dependencies] 44 | 45 | [badges] 46 | circle-ci = { repository = "interledger-rs/interledger-rs" } 47 | codecov = { repository = "interledger-rs/interledger-rs" } 48 | -------------------------------------------------------------------------------- /crates/interledger-api/README.md: -------------------------------------------------------------------------------- 1 | # interledger-api 2 | 3 | This crate defines HTTP endpoints for working with Interledger. 4 | See also [the API documentation](https://github.com/interledger-rs/interledger-rs/blob/master/docs/api.md). 5 | -------------------------------------------------------------------------------- /crates/interledger-api/src/routes/mod.rs: -------------------------------------------------------------------------------- 1 | mod accounts; 2 | mod node_settings; 3 | 4 | pub use accounts::accounts_api; 5 | pub use node_settings::node_settings_api; 6 | 7 | #[cfg(test)] 8 | pub mod test_helpers; 9 | -------------------------------------------------------------------------------- /crates/interledger-btp/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "interledger-btp" 3 | version = "1.0.0" 4 | authors = ["Evan Schwartz "] 5 | description = "Bilateral Transfer Protocol (BTP) client and server services for Interledger.rs" 6 | license = "Apache-2.0" 7 | edition = "2018" 8 | repository = "https://github.com/interledger-rs/interledger-rs" 9 | 10 | [features] 11 | strict = ["interledger-packet/strict"] 12 | 13 | [dependencies] 14 | interledger-errors = { path = "../interledger-errors", version = "1.0.0", default-features = false } 15 | interledger-packet = { path = "../interledger-packet", version = "1.0.0", default-features = false } 16 | interledger-service = { path = "../interledger-service", version = "1.0.0", default-features = false } 17 | 18 | bytes = { version = "1.0.1" } 19 | chrono = { version = "0.4.20", default-features = false } 20 | futures = { version = "0.3.7", default-features = false, features = ["std"] } 21 | tracing = { version = "0.1.12", default-features = false, features = ["log"] } 22 | parking_lot = { version = "0.10.0", default-features = false } 23 | thiserror = { version = "1.0.10", default-features = false } 24 | rand = { version = "0.7.2", default-features = false, features = ["std"] } 25 | stream-cancel = { version = "0.8.1", default-features = false } 26 | tokio-tungstenite = { version = "0.15.0", default-features = false, features = ["native-tls", "connect"] } 27 | url = { version = "2.1.1", default-features = false } 28 | uuid = { version = "0.8.1", default-features = false, features = ["v4"]} 29 | warp = { version = "0.3.1", default-features = false, features = ["websocket"] } 30 | secrecy = { version = "0.8", default-features = false, features = ["alloc"] } 31 | async-trait = { version = "0.1.22", default-features = false } 32 | tokio = { version = "1.9.0", default-features = false, features = ["rt", "time", "macros"] } 33 | tokio-stream = { version = "0.1.7" } 34 | once_cell = { version = "1.3.1", default-features = false } 35 | pin-project = { version = "0.4.6", default-features = false } 36 | 37 | [dev-dependencies] 38 | hex-literal = "0.3" 39 | socket2 = "0.4.0" 40 | -------------------------------------------------------------------------------- /crates/interledger-btp/README.md: -------------------------------------------------------------------------------- 1 | # interledger-btp 2 | 3 | This crate provides an implementation of [Bilateral Transfer Protocol](https://interledger.org/rfcs/0023-bilateral-transfer-protocol/) 4 | (BTP), an implementation of the [data link layer](https://en.wikipedia.org/wiki/Data_link_layer) 5 | of the Interledger Protocol stack, roughly analogous to [Ethernet](https://en.wikipedia.org/wiki/Ethernet). 6 | 7 | BTP utilizes websockets, which makes it suitable for users who 8 | do not have a public internet server. 9 | Users who do not need such functionality may prefer the alternative, 10 | simpler data link layer protocol provided by [the interledger-http crate](https://github.com/interledger-rs/interledger-rs/tree/master/crates/interledger-http). 11 | -------------------------------------------------------------------------------- /crates/interledger-btp/fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | target 3 | corpus 4 | artifacts 5 | Cargo.lock 6 | -------------------------------------------------------------------------------- /crates/interledger-btp/fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "interledger-btp-fuzz" 3 | version = "0.0.0" 4 | authors = ["Automatically generated"] 5 | publish = false 6 | edition = "2018" 7 | 8 | [package.metadata] 9 | cargo-fuzz = true 10 | 11 | [dependencies] 12 | libfuzzer-sys = "0.4" 13 | 14 | [dependencies.interledger-btp] 15 | path = ".." 16 | features = ["strict"] 17 | 18 | # Prevent this from interfering with workspaces 19 | [workspace] 20 | members = ["."] 21 | 22 | [[bin]] 23 | name = "fuzz_target_1" 24 | path = "fuzz_targets/fuzz_target_1.rs" 25 | test = false 26 | doc = false 27 | -------------------------------------------------------------------------------- /crates/interledger-btp/fuzz/fuzz_targets/fuzz_target_1.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | 4 | fuzz_target!(|data: &[u8]| { 5 | interledger_btp::fuzzing::roundtrip_btppacket(data); 6 | }); 7 | -------------------------------------------------------------------------------- /crates/interledger-btp/src/errors.rs: -------------------------------------------------------------------------------- 1 | use interledger_packet::OerError; 2 | use std::str::Utf8Error; 3 | 4 | #[derive(Debug, thiserror::Error)] 5 | pub enum BtpPacketError { 6 | #[error("extra trailing bytes")] 7 | TrailingBytesErr, 8 | #[error("UTF-8 Error: {0}")] 9 | Utf8Err(#[from] Utf8Error), 10 | #[error("Chrono Error: {0}")] 11 | ChronoErr(#[from] chrono::ParseError), 12 | #[error("Invalid Packet: {0}")] 13 | PacketType(#[from] PacketTypeError), 14 | #[error("Invalid Packet: {0}")] 15 | Oer(#[from] OerError), 16 | } 17 | 18 | #[derive(Debug, thiserror::Error)] 19 | pub enum PacketTypeError { 20 | #[error("PacketType {0} is not supported")] 21 | Unknown(u8), 22 | #[error("Cannot parse Message from packet of type {0}, expected type {1}")] 23 | Unexpected(u8, u8), 24 | } 25 | -------------------------------------------------------------------------------- /crates/interledger-btp/src/wrapped_ws.rs: -------------------------------------------------------------------------------- 1 | use futures::stream::Stream; 2 | use futures::Sink; 3 | use pin_project::pin_project; 4 | use std::pin::Pin; 5 | use std::task::{Context, Poll}; 6 | use tokio_tungstenite::tungstenite; 7 | use tracing::warn; 8 | use warp::ws::Message; 9 | 10 | /// Wrapper struct to unify the Tungstenite WebSocket connection from connect_async 11 | /// with the Warp websocket connection from ws.upgrade. Stream and Sink are re-implemented 12 | /// for this struct, normalizing it to use Tungstenite's messages and a wrapped error type 13 | #[pin_project] 14 | #[derive(Clone)] 15 | pub(crate) struct WsWrap { 16 | #[pin] 17 | pub(crate) connection: W, 18 | } 19 | 20 | impl Stream for WsWrap 21 | where 22 | W: Stream, 23 | { 24 | type Item = tokio_tungstenite::tungstenite::Message; 25 | 26 | fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { 27 | let this = self.project(); 28 | match this.connection.poll_next(cx) { 29 | Poll::Pending => Poll::Pending, 30 | Poll::Ready(val) => match val { 31 | Some(v) => { 32 | let v = convert_msg(v); 33 | Poll::Ready(Some(v)) 34 | } 35 | None => Poll::Ready(None), 36 | }, 37 | } 38 | } 39 | } 40 | 41 | impl Sink for WsWrap 42 | where 43 | W: Sink, 44 | { 45 | type Error = W::Error; 46 | 47 | fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 48 | let this = self.project(); 49 | this.connection.poll_ready(cx) 50 | } 51 | 52 | fn start_send(self: Pin<&mut Self>, item: tungstenite::Message) -> Result<(), Self::Error> { 53 | let this = self.project(); 54 | let item = match item { 55 | tungstenite::Message::Binary(data) => Message::binary(data), 56 | tungstenite::Message::Text(data) => Message::text(data), 57 | // Ignore other message types because warp's WebSocket type doesn't 58 | // allow us to send any other types of messages 59 | // TODO make sure warp's websocket responds to pings and/or sends them to keep the 60 | // connection alive 61 | _ => return Ok(()), 62 | }; 63 | this.connection.start_send(item) 64 | } 65 | 66 | fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 67 | let this = self.project(); 68 | this.connection.poll_flush(cx) 69 | } 70 | 71 | fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 72 | let this = self.project(); 73 | this.connection.poll_close(cx) 74 | } 75 | } 76 | 77 | fn convert_msg(message: Message) -> tungstenite::Message { 78 | if message.is_ping() { 79 | tungstenite::Message::Ping(message.into_bytes()) 80 | } else if message.is_binary() { 81 | tungstenite::Message::Binary(message.into_bytes()) 82 | } else if message.is_text() { 83 | tungstenite::Message::Text(message.to_str().unwrap_or_default().to_string()) 84 | } else if message.is_close() { 85 | tungstenite::Message::Close(None) 86 | } else { 87 | warn!( 88 | "Got unexpected websocket message, closing connection: {:?}", 89 | message 90 | ); 91 | tungstenite::Message::Close(None) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /crates/interledger-ccp/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "interledger-ccp" 3 | version = "1.0.0" 4 | authors = ["Evan Schwartz "] 5 | description = "Implementation of the Interledger Dynamic Configuration Protocol (ILDCP)" 6 | license = "Apache-2.0" 7 | edition = "2018" 8 | repository = "https://github.com/interledger-rs/interledger-rs" 9 | 10 | [dependencies] 11 | interledger-errors = { path = "../interledger-errors", version = "1.0.0", default-features = false } 12 | interledger-packet = { path = "../interledger-packet", version = "1.0.0", default-features = false } 13 | interledger-service = { path = "../interledger-service", version = "1.0.0", default-features = false } 14 | 15 | bytes = { version = "1.0.1" } 16 | futures = { version = "0.3.7", default-features = false } 17 | once_cell = { version = "1.3.1", default-features = false } 18 | tracing = { version = "0.1.12", default-features = false, features = ["log"] } 19 | parking_lot = { version = "0.10.0", default-features = false } 20 | ring = { version = "0.16.9", default-features = false } 21 | uuid = { version = "0.8.1", default-features = false, features = ["v4"]} 22 | serde = { version = "1.0.101", default-features = false, features = ["derive"] } 23 | async-trait = { version = "0.1.22", default-features = false } 24 | tokio = { version = "1.9.0", default-features = false, features = ["time", "rt", "macros"] } 25 | 26 | [dev-dependencies] 27 | hex-literal = "0.3" 28 | -------------------------------------------------------------------------------- /crates/interledger-ccp/README.md: -------------------------------------------------------------------------------- 1 | # interledger-ccp 2 | This crate implements the Connector-to-Connector Protocol (CCP) for exchanging routing 3 | information with peers. The `CcpRouteManager` processes Route Update and Route Control 4 | messages from accounts that we are configured to receive routes from and sends route 5 | updates to accounts that we are configured to send updates to. 6 | 7 | This populates the routing table implemented in [the interledger-router crate](https://github.com/interledger-rs/interledger-rs/tree/master/crates/interledger-router). 8 | -------------------------------------------------------------------------------- /crates/interledger-ccp/fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | target 3 | corpus 4 | artifacts 5 | Cargo.lock 6 | -------------------------------------------------------------------------------- /crates/interledger-ccp/fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | 2 | [package] 3 | name = "interledger-ccp-fuzz" 4 | version = "0.0.0" 5 | authors = ["Automatically generated"] 6 | publish = false 7 | edition = "2018" 8 | 9 | [package.metadata] 10 | cargo-fuzz = true 11 | 12 | [dependencies] 13 | libfuzzer-sys = "0.4" 14 | 15 | [dependencies.interledger-ccp] 16 | path = ".." 17 | 18 | # Prevent this from interfering with workspaces 19 | [workspace] 20 | members = ["."] 21 | 22 | [[bin]] 23 | name = "fuzz_target_1" 24 | path = "fuzz_targets/fuzz_target_1.rs" 25 | test = false 26 | doc = false 27 | 28 | [[bin]] 29 | name = "fuzz_target_2" 30 | path = "fuzz_targets/fuzz_target_2.rs" 31 | test = false 32 | doc = false 33 | -------------------------------------------------------------------------------- /crates/interledger-ccp/fuzz/fuzz_targets/fuzz_target_1.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | 4 | fuzz_target!(|data: &[u8]| { 5 | interledger_ccp::fuzz_control_request(data); 6 | }); 7 | -------------------------------------------------------------------------------- /crates/interledger-ccp/fuzz/fuzz_targets/fuzz_target_2.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | 4 | fuzz_target!(|data: &[u8]| { 5 | interledger_ccp::fuzz_update_request(data); 6 | }); 7 | -------------------------------------------------------------------------------- /crates/interledger-ccp/src/fixtures.rs: -------------------------------------------------------------------------------- 1 | /* kcov-ignore-start */ 2 | use crate::packet::*; 3 | use bytes::Bytes; 4 | use hex_literal::hex; 5 | use interledger_packet::Address; 6 | #[cfg(test)] 7 | use once_cell::sync::Lazy; 8 | use std::str::FromStr; 9 | 10 | pub static CONTROL_REQUEST_SERIALIZED: &[u8] = &hex!("0c6c0000000000000000323031353036313630303031303030303066687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f292512706565722e726f7574652e636f6e74726f6c1f0170d1a134a0df4f47964f6e19e2ab379000000020010203666f6f03626172"); 11 | 12 | pub static CONTROL_REQUEST: Lazy = Lazy::new(|| RouteControlRequest { 13 | mode: Mode::Sync, 14 | last_known_routing_table_id: [ 15 | 112, 209, 161, 52, 160, 223, 79, 71, 150, 79, 110, 25, 226, 171, 55, 144, 16 | ], // "70d1a134-a0df-4f47-964f-6e19e2ab3790" 17 | last_known_epoch: 32, 18 | features: vec!["foo".to_string(), "bar".to_string()], 19 | }); 20 | 21 | pub static UPDATE_REQUEST_SIMPLE_SERIALIZED: &[u8] = &hex!("0c7e0000000000000000323031353036313630303031303030303066687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f292511706565722e726f7574652e7570646174653221e55f8eabcd4e979ab9bf0ff00a224c000000340000003400000034000075300d6578616d706c652e616c69636501000100"); 22 | 23 | pub static UPDATE_REQUEST_SIMPLE: Lazy = Lazy::new(|| RouteUpdateRequest { 24 | routing_table_id: [ 25 | 33, 229, 95, 142, 171, 205, 78, 151, 154, 185, 191, 15, 240, 10, 34, 76, 26 | ], // '21e55f8e-abcd-4e97-9ab9-bf0ff00a224c' 27 | current_epoch_index: 52, 28 | from_epoch_index: 52, 29 | to_epoch_index: 52, 30 | hold_down_time: 30000, 31 | speaker: Address::from_str("example.alice").unwrap(), 32 | new_routes: Vec::new(), 33 | withdrawn_routes: Vec::new(), 34 | }); 35 | 36 | pub static UPDATE_REQUEST_COMPLEX_SERIALIZED: &[u8] = &hex!("0c8201520000000000000000323031353036313630303031303030303066687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f292511706565722e726f7574652e757064617465820104bffbf6ad0ddc4d3ba1e5b4f0537365bd000000340000002e00000032000075300d6578616d706c652e616c69636501020f6578616d706c652e7072656669783101010f6578616d706c652e707265666978317a6c7d85867c46a2fabfad1afa7a4a5e229ce574fcce63f5edeedfc03f8468ea01000f6578616d706c652e707265666978320102126578616d706c652e636f6e6e6563746f72310f6578616d706c652e707265666978322b08e53fbcc17c5f1bd54ae0d9ad7ba39a5f9a7b126ca9b5c0945609a35324cc01025000000b68656c6c6f20776f726c64e0000104a0a0a0a001020f6578616d706c652e707265666978330f6578616d706c652e70726566697834"); 37 | 38 | pub static UPDATE_REQUEST_COMPLEX: Lazy = Lazy::new(|| RouteUpdateRequest { 39 | routing_table_id: [ 40 | 191, 251, 246, 173, 13, 220, 77, 59, 161, 229, 180, 240, 83, 115, 101, 189, 41 | ], // 'bffbf6ad-0ddc-4d3b-a1e5-b4f0537365bd' 42 | current_epoch_index: 52, 43 | from_epoch_index: 46, 44 | to_epoch_index: 50, 45 | hold_down_time: 30000, 46 | speaker: Address::from_str("example.alice").unwrap(), 47 | new_routes: vec![ 48 | Route { 49 | prefix: "example.prefix1".to_string(), 50 | path: vec!["example.prefix1".to_string()], 51 | auth: [ 52 | 122, 108, 125, 133, 134, 124, 70, 162, 250, 191, 173, 26, 250, 122, 74, 94, 34, 53 | 156, 229, 116, 252, 206, 99, 245, 237, 238, 223, 192, 63, 132, 104, 234, 54 | ], 55 | props: Vec::new(), 56 | }, 57 | Route { 58 | prefix: "example.prefix2".to_string(), 59 | path: vec![ 60 | "example.connector1".to_string(), 61 | "example.prefix2".to_string(), 62 | ], 63 | auth: [ 64 | 43, 8, 229, 63, 188, 193, 124, 95, 27, 213, 74, 224, 217, 173, 123, 163, 154, 95, 65 | 154, 123, 18, 108, 169, 181, 192, 148, 86, 9, 163, 83, 36, 204, 66 | ], 67 | props: vec![ 68 | RouteProp { 69 | is_optional: false, 70 | is_transitive: true, 71 | is_partial: false, 72 | is_utf8: true, 73 | id: 0, 74 | value: Bytes::from("hello world"), 75 | }, 76 | RouteProp { 77 | is_optional: true, 78 | is_transitive: true, 79 | is_partial: true, 80 | is_utf8: false, 81 | id: 1, 82 | value: Bytes::from(&hex!("a0a0a0a0")[..]), 83 | }, 84 | ], 85 | }, 86 | ], 87 | withdrawn_routes: vec!["example.prefix3".to_string(), "example.prefix4".to_string()], 88 | }); 89 | /* kcov-ignore-end */ 90 | -------------------------------------------------------------------------------- /crates/interledger-errors/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "interledger-errors" 3 | version = "1.0.0" 4 | authors = ["Georgios Konstantopoulos "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | interledger-packet = { path = "../interledger-packet", version = "1.0.0", default-features = false } 11 | 12 | once_cell = { version = "1.3.1", default-features = false } 13 | thiserror = { version = "1.0.10", default-features = false } 14 | serde = { version = "1.0.101", default-features = false, features = ["derive"] } 15 | serde_json = { version = "1.0.41", default-features = false } 16 | serde_path_to_error = { version = "0.1", default-features = false } 17 | http = { version = "0.2.0", default-features = false } 18 | chrono = { version = "0.4.20", default-features = false, features = ["clock"] } 19 | regex = { version ="1.5", default-features = false, features = ["std"] } 20 | warp = { version = "0.3.1", default-features = false } 21 | redis = { package = "redis", version = "0.21.0", optional = true, default-features = false, features = ["tokio-comp"] } 22 | url = { version = "2.1.1", default-features = false } 23 | 24 | [features] 25 | warp_errors = [] 26 | redis_errors = ["redis"] 27 | -------------------------------------------------------------------------------- /crates/interledger-errors/src/account_store_error.rs: -------------------------------------------------------------------------------- 1 | use super::BtpStoreError; 2 | use crate::error::ApiError; 3 | use std::error::Error as StdError; 4 | use thiserror::Error; 5 | 6 | /// Errors for the AccountStore 7 | #[derive(Error, Debug)] 8 | #[non_exhaustive] 9 | pub enum AccountStoreError { 10 | #[error("{0}")] 11 | Other(#[from] Box), 12 | #[error("account `{0}` was not found")] 13 | AccountNotFound(String), 14 | #[error("account `{0}` already exists")] 15 | AccountExists(String), 16 | #[error("wrong account length (expected {expected}, got {actual})")] 17 | WrongLength { expected: usize, actual: usize }, 18 | } 19 | 20 | impl From for BtpStoreError { 21 | fn from(src: AccountStoreError) -> Self { 22 | match src { 23 | AccountStoreError::AccountNotFound(s) => BtpStoreError::AccountNotFound(s), 24 | _ => BtpStoreError::Other(Box::new(src)), 25 | } 26 | } 27 | } 28 | 29 | impl From for ApiError { 30 | fn from(src: AccountStoreError) -> Self { 31 | match src { 32 | AccountStoreError::AccountNotFound(_) => { 33 | ApiError::account_not_found().detail(src.to_string()) 34 | } 35 | AccountStoreError::AccountExists(_) => ApiError::conflict().detail(src.to_string()), 36 | _ => ApiError::internal_server_error().detail(src.to_string()), 37 | } 38 | } 39 | } 40 | 41 | #[cfg(feature = "warp_errors")] 42 | impl From for warp::Rejection { 43 | fn from(src: AccountStoreError) -> Self { 44 | ApiError::from(src).into() 45 | } 46 | } 47 | 48 | #[cfg(feature = "redis_errors")] 49 | use redis::RedisError; 50 | 51 | #[cfg(feature = "redis_errors")] 52 | impl From for AccountStoreError { 53 | fn from(err: RedisError) -> Self { 54 | AccountStoreError::Other(Box::new(err)) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /crates/interledger-errors/src/address_store_error.rs: -------------------------------------------------------------------------------- 1 | use super::{ApiError, NodeStoreError}; 2 | use interledger_packet::Address; 3 | use std::error::Error as StdError; 4 | use thiserror::Error; 5 | 6 | /// Errors for the AddressStore 7 | #[derive(Error, Debug)] 8 | #[non_exhaustive] 9 | pub enum AddressStoreError { 10 | #[error("{0}")] 11 | Other(#[from] Box), 12 | #[error("Could not save address: {0}")] 13 | SetAddress(Address), 14 | #[error("Could not save address: {0}")] 15 | ClearAddress(Address), 16 | } 17 | 18 | impl From for AddressStoreError { 19 | fn from(src: NodeStoreError) -> Self { 20 | AddressStoreError::Other(Box::new(src)) 21 | } 22 | } 23 | 24 | #[cfg(feature = "redis_errors")] 25 | use redis::RedisError; 26 | #[cfg(feature = "redis_errors")] 27 | impl From for AddressStoreError { 28 | fn from(src: RedisError) -> Self { 29 | AddressStoreError::Other(Box::new(src)) 30 | } 31 | } 32 | 33 | impl From for ApiError { 34 | fn from(src: AddressStoreError) -> Self { 35 | // AddressStore erroring is always an internal server error 36 | ApiError::internal_server_error().detail(src.to_string()) 37 | } 38 | } 39 | 40 | #[cfg(feature = "warp_errors")] 41 | impl From for warp::Rejection { 42 | fn from(src: AddressStoreError) -> Self { 43 | ApiError::from(src).into() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /crates/interledger-errors/src/balance_store_error.rs: -------------------------------------------------------------------------------- 1 | use crate::error::ApiError; 2 | use std::error::Error as StdError; 3 | use thiserror::Error; 4 | 5 | /// Errors for the BalanceStore 6 | #[derive(Error, Debug)] 7 | #[non_exhaustive] 8 | pub enum BalanceStoreError { 9 | #[error("{0}")] 10 | Other(#[from] Box), 11 | } 12 | 13 | impl From for ApiError { 14 | fn from(src: BalanceStoreError) -> Self { 15 | ApiError::internal_server_error().detail(src.to_string()) 16 | } 17 | } 18 | 19 | #[cfg(feature = "warp_errors")] 20 | impl From for warp::Rejection { 21 | fn from(src: BalanceStoreError) -> Self { 22 | ApiError::from(src).into() 23 | } 24 | } 25 | 26 | #[cfg(feature = "redis_errors")] 27 | use redis::RedisError; 28 | 29 | #[cfg(feature = "redis_errors")] 30 | impl From for BalanceStoreError { 31 | fn from(src: RedisError) -> BalanceStoreError { 32 | BalanceStoreError::Other(Box::new(src)) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /crates/interledger-errors/src/btp_store_error.rs: -------------------------------------------------------------------------------- 1 | use crate::error::ApiError; 2 | use std::error::Error as StdError; 3 | use thiserror::Error; 4 | 5 | /// Errors for the BtpStore 6 | #[derive(Error, Debug)] 7 | #[non_exhaustive] 8 | pub enum BtpStoreError { 9 | #[error("{0}")] 10 | Other(#[from] Box), 11 | #[error("account `{0}` was not found")] 12 | AccountNotFound(String), 13 | #[error("account `{0}` is not authorized for this action")] 14 | Unauthorized(String), 15 | } 16 | 17 | impl From for ApiError { 18 | fn from(src: BtpStoreError) -> Self { 19 | match src { 20 | BtpStoreError::AccountNotFound(_) => { 21 | ApiError::account_not_found().detail(src.to_string()) 22 | } 23 | BtpStoreError::Unauthorized(_) => ApiError::unauthorized().detail(src.to_string()), 24 | _ => ApiError::internal_server_error().detail(src.to_string()), 25 | } 26 | } 27 | } 28 | 29 | #[cfg(feature = "warp_errors")] 30 | impl From for warp::Rejection { 31 | fn from(src: BtpStoreError) -> Self { 32 | ApiError::from(src).into() 33 | } 34 | } 35 | 36 | #[cfg(feature = "redis_errors")] 37 | use redis::RedisError; 38 | 39 | #[cfg(feature = "redis_errors")] 40 | impl From for BtpStoreError { 41 | fn from(src: RedisError) -> BtpStoreError { 42 | BtpStoreError::Other(Box::new(src)) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /crates/interledger-errors/src/ccprouting_store_error.rs: -------------------------------------------------------------------------------- 1 | use super::{AccountStoreError, NodeStoreError}; 2 | use crate::error::ApiError; 3 | use std::error::Error as StdError; 4 | use thiserror::Error; 5 | 6 | /// Errors for the CcpRoutingStore 7 | #[derive(Error, Debug)] 8 | #[non_exhaustive] 9 | pub enum CcpRoutingStoreError { 10 | #[error("{0}")] 11 | Other(#[from] Box), 12 | } 13 | 14 | impl From for CcpRoutingStoreError { 15 | fn from(src: AccountStoreError) -> Self { 16 | CcpRoutingStoreError::Other(Box::new(src)) 17 | } 18 | } 19 | 20 | impl From for CcpRoutingStoreError { 21 | fn from(src: NodeStoreError) -> Self { 22 | CcpRoutingStoreError::Other(Box::new(src)) 23 | } 24 | } 25 | 26 | impl From for ApiError { 27 | fn from(_src: CcpRoutingStoreError) -> Self { 28 | ApiError::method_not_allowed() 29 | } 30 | } 31 | 32 | #[cfg(feature = "warp_errors")] 33 | impl From for warp::Rejection { 34 | fn from(src: CcpRoutingStoreError) -> Self { 35 | ApiError::from(src).into() 36 | } 37 | } 38 | 39 | #[cfg(feature = "redis_errors")] 40 | use redis::RedisError; 41 | 42 | #[cfg(feature = "redis_errors")] 43 | impl From for CcpRoutingStoreError { 44 | fn from(src: RedisError) -> CcpRoutingStoreError { 45 | CcpRoutingStoreError::Other(Box::new(src)) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /crates/interledger-errors/src/create_account_error.rs: -------------------------------------------------------------------------------- 1 | use crate::error::ApiError; 2 | use interledger_packet::ParseError; 3 | use std::error::Error as StdError; 4 | use thiserror::Error; 5 | use url::ParseError as UrlParseError; 6 | 7 | /// Errors which can happen when creating an account 8 | #[derive(Error, Debug)] 9 | #[non_exhaustive] 10 | pub enum CreateAccountError { 11 | #[error("{0}")] 12 | Other(#[from] Box), 13 | #[error("the provided suffix is not valid: {0}")] 14 | InvalidSuffix(ParseError), 15 | #[error("the provided http url is not valid: {0}")] 16 | InvalidHttpUrl(UrlParseError), 17 | #[error("the provided btp url is not valid: {0}")] 18 | InvalidBtpUrl(UrlParseError), 19 | #[error("the provided routing relation is not valid: {0}")] 20 | InvalidRoutingRelation(String), 21 | #[error("the provided value for parameter `{0}` was too large")] 22 | ParamTooLarge(String), 23 | } 24 | 25 | impl From for ApiError { 26 | fn from(src: CreateAccountError) -> Self { 27 | ApiError::bad_request().detail(src.to_string()) 28 | } 29 | } 30 | 31 | #[cfg(feature = "warp_errors")] 32 | impl From for warp::Rejection { 33 | fn from(src: CreateAccountError) -> Self { 34 | ApiError::from(src).into() 35 | } 36 | } 37 | 38 | #[cfg(feature = "redis_errors")] 39 | use redis::RedisError; 40 | 41 | #[cfg(feature = "redis_errors")] 42 | impl From for CreateAccountError { 43 | fn from(err: RedisError) -> Self { 44 | CreateAccountError::Other(Box::new(err)) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /crates/interledger-errors/src/exchange_rate_store_error.rs: -------------------------------------------------------------------------------- 1 | use crate::error::ApiError; 2 | use std::error::Error as StdError; 3 | use thiserror::Error; 4 | 5 | /// Errors for the ExchangeRateStore 6 | #[derive(Error, Debug)] 7 | #[non_exhaustive] 8 | pub enum ExchangeRateStoreError { 9 | #[error("{0}")] 10 | Other(#[from] Box), 11 | #[error("Pair {from}/{to} not found")] 12 | PairNotFound { from: String, to: String }, 13 | } 14 | 15 | impl From for ApiError { 16 | fn from(src: ExchangeRateStoreError) -> Self { 17 | ApiError::internal_server_error().detail(src.to_string()) 18 | } 19 | } 20 | 21 | #[cfg(feature = "warp_errors")] 22 | impl From for warp::Rejection { 23 | fn from(src: ExchangeRateStoreError) -> Self { 24 | ApiError::from(src).into() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /crates/interledger-errors/src/http_store_error.rs: -------------------------------------------------------------------------------- 1 | use crate::error::ApiError; 2 | use std::error::Error as StdError; 3 | use thiserror::Error; 4 | 5 | /// Errors for the HttpStore 6 | #[derive(Error, Debug)] 7 | #[non_exhaustive] 8 | pub enum HttpStoreError { 9 | #[error("{0}")] 10 | Other(#[from] Box), 11 | #[error("account `{0}` was not found")] 12 | AccountNotFound(String), 13 | #[error("account `{0}` is not authorized for this action")] 14 | Unauthorized(String), 15 | } 16 | 17 | impl From for ApiError { 18 | fn from(src: HttpStoreError) -> Self { 19 | match src { 20 | HttpStoreError::AccountNotFound(_) => { 21 | ApiError::account_not_found().detail(src.to_string()) 22 | } 23 | HttpStoreError::Unauthorized(_) => ApiError::unauthorized().detail(src.to_string()), 24 | _ => ApiError::internal_server_error().detail(src.to_string()), 25 | } 26 | } 27 | } 28 | 29 | #[cfg(feature = "warp_errors")] 30 | impl From for warp::Rejection { 31 | fn from(src: HttpStoreError) -> Self { 32 | ApiError::from(src).into() 33 | } 34 | } 35 | 36 | #[cfg(feature = "redis_errors")] 37 | use redis::RedisError; 38 | 39 | #[cfg(feature = "redis_errors")] 40 | impl From for HttpStoreError { 41 | fn from(src: RedisError) -> HttpStoreError { 42 | HttpStoreError::Other(Box::new(src)) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /crates/interledger-errors/src/lib.rs: -------------------------------------------------------------------------------- 1 | /// [RFC7807](https://tools.ietf.org/html/rfc7807) compliant errors 2 | mod error; 3 | pub use error::*; 4 | 5 | mod account_store_error; 6 | pub use account_store_error::AccountStoreError; 7 | 8 | mod address_store_error; 9 | pub use address_store_error::AddressStoreError; 10 | 11 | mod http_store_error; 12 | pub use http_store_error::HttpStoreError; 13 | 14 | mod btp_store_error; 15 | pub use btp_store_error::BtpStoreError; 16 | 17 | mod ccprouting_store_error; 18 | pub use ccprouting_store_error::CcpRoutingStoreError; 19 | 20 | mod balance_store_error; 21 | pub use balance_store_error::BalanceStoreError; 22 | 23 | mod node_store_error; 24 | pub use node_store_error::NodeStoreError; 25 | 26 | mod exchange_rate_store_error; 27 | pub use exchange_rate_store_error::ExchangeRateStoreError; 28 | 29 | mod settlement_errors; 30 | pub use settlement_errors::{IdempotentStoreError, LeftoversStoreError, SettlementStoreError}; 31 | 32 | mod create_account_error; 33 | pub use create_account_error::CreateAccountError; 34 | -------------------------------------------------------------------------------- /crates/interledger-errors/src/node_store_error.rs: -------------------------------------------------------------------------------- 1 | use super::{AccountStoreError, BtpStoreError, CreateAccountError}; 2 | use crate::error::ApiError; 3 | use std::error::Error as StdError; 4 | use thiserror::Error; 5 | 6 | /// Errors for the NodeStore 7 | #[derive(Error, Debug)] 8 | #[non_exhaustive] 9 | pub enum NodeStoreError { 10 | #[error("{0}")] 11 | Other(#[from] Box), 12 | #[error("Settlement engine URL loaded was not a valid url: {0}")] 13 | InvalidEngineUrl(String), 14 | #[error("account `{0}` was not found")] 15 | AccountNotFound(String), 16 | #[error("account `{0}` already exists")] 17 | AccountExists(String), 18 | #[error("not all of the given accounts exist")] 19 | MissingAccounts, 20 | #[error("invalid account: {0}")] 21 | InvalidAccount(CreateAccountError), 22 | } 23 | 24 | impl From for BtpStoreError { 25 | fn from(src: NodeStoreError) -> Self { 26 | match src { 27 | NodeStoreError::AccountNotFound(s) => BtpStoreError::AccountNotFound(s), 28 | _ => BtpStoreError::Other(Box::new(src)), 29 | } 30 | } 31 | } 32 | 33 | impl From for NodeStoreError { 34 | fn from(src: AccountStoreError) -> Self { 35 | match src { 36 | AccountStoreError::AccountNotFound(s) => NodeStoreError::AccountNotFound(s), 37 | _ => NodeStoreError::Other(Box::new(src)), 38 | } 39 | } 40 | } 41 | 42 | impl From for ApiError { 43 | fn from(src: NodeStoreError) -> Self { 44 | match src { 45 | NodeStoreError::AccountNotFound(_) => { 46 | ApiError::account_not_found().detail(src.to_string()) 47 | } 48 | NodeStoreError::InvalidAccount(_) | NodeStoreError::InvalidEngineUrl(_) => { 49 | ApiError::bad_request().detail(src.to_string()) 50 | } 51 | _ => ApiError::internal_server_error().detail(src.to_string()), 52 | } 53 | } 54 | } 55 | 56 | #[cfg(feature = "warp_errors")] 57 | impl From for warp::Rejection { 58 | fn from(src: NodeStoreError) -> Self { 59 | ApiError::from(src).into() 60 | } 61 | } 62 | 63 | #[cfg(feature = "redis_errors")] 64 | use redis::RedisError; 65 | #[cfg(feature = "redis_errors")] 66 | impl From for NodeStoreError { 67 | fn from(src: RedisError) -> Self { 68 | NodeStoreError::Other(Box::new(src)) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /crates/interledger-errors/src/settlement_errors.rs: -------------------------------------------------------------------------------- 1 | use crate::error::ApiError; 2 | use std::error::Error as StdError; 3 | use thiserror::Error; 4 | 5 | /// Errors for the LeftoversStore 6 | #[derive(Error, Debug)] 7 | #[non_exhaustive] 8 | pub enum LeftoversStoreError { 9 | #[error("{0}")] 10 | Other(#[from] Box), 11 | } 12 | 13 | impl From for ApiError { 14 | fn from(_src: LeftoversStoreError) -> Self { 15 | ApiError::method_not_allowed() 16 | } 17 | } 18 | 19 | #[cfg(feature = "warp_errors")] 20 | impl From for warp::Rejection { 21 | fn from(src: LeftoversStoreError) -> Self { 22 | ApiError::from(src).into() 23 | } 24 | } 25 | 26 | /// Errors for the IdempotentStore 27 | #[derive(Error, Debug)] 28 | #[non_exhaustive] 29 | pub enum IdempotentStoreError { 30 | #[error("{0}")] 31 | Other(#[from] Box), 32 | } 33 | 34 | impl From for ApiError { 35 | fn from(_src: IdempotentStoreError) -> Self { 36 | ApiError::method_not_allowed() 37 | } 38 | } 39 | 40 | #[cfg(feature = "warp_errors")] 41 | impl From for warp::Rejection { 42 | fn from(src: IdempotentStoreError) -> Self { 43 | ApiError::from(src).into() 44 | } 45 | } 46 | 47 | /// Errors for the SettlementStore 48 | #[derive(Error, Debug)] 49 | #[non_exhaustive] 50 | pub enum SettlementStoreError { 51 | #[error("{0}")] 52 | Other(#[from] Box), 53 | #[error("could not update balance for incoming settlement")] 54 | BalanceUpdateFailure, 55 | #[error("could not refund settlement")] 56 | RefundFailure, 57 | } 58 | 59 | impl From for ApiError { 60 | fn from(_src: SettlementStoreError) -> Self { 61 | ApiError::method_not_allowed() 62 | } 63 | } 64 | 65 | impl From for SettlementStoreError { 66 | fn from(src: LeftoversStoreError) -> SettlementStoreError { 67 | SettlementStoreError::Other(Box::new(src)) 68 | } 69 | } 70 | 71 | #[cfg(feature = "warp_errors")] 72 | impl From for warp::Rejection { 73 | fn from(src: SettlementStoreError) -> Self { 74 | ApiError::from(src).into() 75 | } 76 | } 77 | 78 | #[cfg(feature = "redis_errors")] 79 | use redis::RedisError; 80 | 81 | #[cfg(feature = "redis_errors")] 82 | impl From for SettlementStoreError { 83 | fn from(src: RedisError) -> SettlementStoreError { 84 | SettlementStoreError::Other(Box::new(src)) 85 | } 86 | } 87 | 88 | #[cfg(feature = "redis_errors")] 89 | impl From for LeftoversStoreError { 90 | fn from(src: RedisError) -> LeftoversStoreError { 91 | LeftoversStoreError::Other(Box::new(src)) 92 | } 93 | } 94 | 95 | #[cfg(feature = "redis_errors")] 96 | impl From for IdempotentStoreError { 97 | fn from(src: RedisError) -> IdempotentStoreError { 98 | IdempotentStoreError::Other(Box::new(src)) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /crates/interledger-http/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "interledger-http" 3 | version = "1.0.0" 4 | authors = ["Evan Schwartz "] 5 | description = "HTTP client and server services for Interledger.rs" 6 | license = "Apache-2.0" 7 | edition = "2018" 8 | repository = "https://github.com/interledger-rs/interledger-rs" 9 | 10 | [dependencies] 11 | interledger-errors = { path = "../interledger-errors", version = "1.0.0", default-features = false } 12 | interledger-packet = { path = "../interledger-packet", version = "1.0.0", default-features = false } 13 | interledger-service = { path = "../interledger-service", version = "1.0.0", default-features = false } 14 | 15 | bytes = { version = "1.0.1", default-features = false } 16 | futures = { version = "0.3.7", default-features = false } 17 | tracing = { version = "0.1.12", default-features = false, features = ["log"] } 18 | reqwest = { version = "0.11.4", default-features = false, features = ["default-tls"] } 19 | url = { version = "2.1.1", default-features = false } 20 | warp = { version = "0.3.1", default-features = false } 21 | serde = { version = "1.0.101", default-features = false, features = ["derive"] } 22 | serde_json = { version = "1.0.41", default-features = false } 23 | serde_path_to_error = { version = "0.1", default-features = false } 24 | http = { version = "0.2.0", default-features = false } 25 | once_cell = { version = "1.3.1", default-features = false } 26 | mime = { version ="0.3.14", default-features = false } 27 | secrecy = { version = "0.8", default-features = false, features = ["alloc"] } 28 | async-trait = { version = "0.1.22", default-features = false } 29 | 30 | [dev-dependencies] 31 | uuid = { version = "0.8.1", default-features = false, features=["v4"]} 32 | tokio = { version = "1.9.0", default-features = false, features = ["rt", "macros"]} 33 | -------------------------------------------------------------------------------- /crates/interledger-http/README.md: -------------------------------------------------------------------------------- 1 | # interledger-http 2 | 3 | This crate provides an implementation of [ILP over HTTP](https://interledger.org/rfcs/0035-ilp-over-http/), 4 | an implementation of the [data link layer](https://en.wikipedia.org/wiki/Data_link_layer) 5 | of the Interledger Protocol stack, roughly analogous to [Ethernet](https://en.wikipedia.org/wiki/Ethernet). 6 | 7 | This is an alternative to the protocol implemented by the 8 | interledger-btp crate, whose main distinguishing feature 9 | is the use of HTTP rather than websockets. 10 | This protocol is intended primarily for server-to-server 11 | communication between peers on the Interledger network. 12 | -------------------------------------------------------------------------------- /crates/interledger-ildcp/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "interledger-ildcp" 3 | version = "1.0.0" 4 | authors = ["Evan Schwartz "] 5 | description = "Implementation of the Interledger Dynamic Configuration Protocol (ILDCP)" 6 | license = "Apache-2.0" 7 | edition = "2018" 8 | repository = "https://github.com/interledger-rs/interledger-rs" 9 | 10 | [dependencies] 11 | interledger-packet = { path = "../interledger-packet", version = "1.0.0", default-features = false } 12 | interledger-service = { path = "../interledger-service", version = "1.0.0", default-features = false } 13 | 14 | bytes = { version = "1.0.1" } 15 | futures = { version = "0.3.7", default-features = false } 16 | once_cell = { version = "1.3.1", default-features = false } 17 | tracing = { version = "0.1.12", default-features = false, features = ["log"] } 18 | async-trait = { version = "0.1.22", default-features = false } 19 | 20 | [dev-dependencies] 21 | tokio = { version = "1.9.0", default-features = false, features = ["macros","rt"]} 22 | uuid = { version = "0.8.1", default-features = false, features = ["v4"] } 23 | -------------------------------------------------------------------------------- /crates/interledger-ildcp/README.md: -------------------------------------------------------------------------------- 1 | # interledger-ildcp 2 | 3 | This crate provides an implementation of the [Interledger Dynamic 4 | Configuration Protocol](https://interledger.org/rfcs/0031-dynamic-configuration-protocol/) 5 | (ILDCP), used by clients to query for their ILP address 6 | and asset details such as asset code and scale. 7 | By way of analogy with the internet, this serves a similar purpose to [DHCP](https://en.wikipedia.org/wiki/Dynamic_Host_Configuration_Protocol). 8 | -------------------------------------------------------------------------------- /crates/interledger-ildcp/src/client.rs: -------------------------------------------------------------------------------- 1 | use super::packet::*; 2 | use futures::future::TryFutureExt; 3 | use interledger_service::*; 4 | use std::convert::TryFrom; 5 | use tracing::{debug, error}; 6 | 7 | /// Sends an ILDCP Request to the provided service from the provided account 8 | /// and receives the account's ILP address and asset details 9 | pub async fn get_ildcp_info(service: &mut S, account: A) -> Result 10 | where 11 | S: IncomingService, 12 | A: Account, 13 | { 14 | let prepare = IldcpRequest {}.to_prepare(); 15 | let fulfill = service 16 | .handle_request(IncomingRequest { 17 | from: account, 18 | prepare, 19 | }) 20 | .map_err(|err| error!("Error getting ILDCP info: {:?}", err)) 21 | .await?; 22 | 23 | let response = IldcpResponse::try_from(fulfill.into_data().freeze()).map_err(|err| { 24 | error!( 25 | "Unable to parse ILDCP response from fulfill packet: {:?}", 26 | err 27 | ); 28 | })?; 29 | debug!("Got ILDCP response: {:?}", response); 30 | Ok(response) 31 | } 32 | -------------------------------------------------------------------------------- /crates/interledger-ildcp/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # interledger-ildcp 2 | //! 3 | //! Client and server implementations of the [Interledger Dynamic Configuration Protocol (ILDCP)](https://github.com/interledger/rfcs/blob/master/0031-dynamic-configuration-protocol/0031-dynamic-configuration-protocol.md). 4 | //! 5 | //! This is used by clients to query for their ILP address and asset details such as asset code and scale. 6 | 7 | use interledger_service::Account; 8 | 9 | mod client; 10 | mod packet; 11 | mod server; 12 | 13 | pub use client::get_ildcp_info; 14 | pub use packet::*; 15 | pub use server::IldcpService; 16 | -------------------------------------------------------------------------------- /crates/interledger-ildcp/src/server.rs: -------------------------------------------------------------------------------- 1 | use super::packet::*; 2 | use super::Account; 3 | use async_trait::async_trait; 4 | use interledger_packet::*; 5 | use interledger_service::*; 6 | use std::marker::PhantomData; 7 | use tracing::debug; 8 | 9 | /// A simple service that intercepts incoming ILDCP requests 10 | /// and responds using the information in the Account struct. 11 | #[derive(Clone)] 12 | pub struct IldcpService { 13 | next: I, 14 | account_type: PhantomData, 15 | } 16 | 17 | impl IldcpService 18 | where 19 | I: IncomingService, 20 | A: Account, 21 | { 22 | pub fn new(next: I) -> Self { 23 | IldcpService { 24 | next, 25 | account_type: PhantomData, 26 | } 27 | } 28 | } 29 | 30 | #[async_trait] 31 | impl IncomingService for IldcpService 32 | where 33 | I: IncomingService + Send, 34 | A: Account, 35 | { 36 | async fn handle_request(&mut self, request: IncomingRequest) -> IlpResult { 37 | if is_ildcp_request(&request.prepare) { 38 | let from = request.from.ilp_address(); 39 | let builder = IldcpResponseBuilder { 40 | ilp_address: from, 41 | asset_code: request.from.asset_code(), 42 | asset_scale: request.from.asset_scale(), 43 | }; 44 | debug!("Responding to query for ildcp info by account: {:?}", from); 45 | let response = builder.build(); 46 | Ok(Fulfill::from(response)) 47 | } else { 48 | self.next.handle_request(request).await 49 | } 50 | } 51 | } 52 | 53 | #[cfg(test)] 54 | mod tests { 55 | use super::*; 56 | use crate::get_ildcp_info; 57 | use once_cell::sync::Lazy; 58 | use std::str::FromStr; 59 | use uuid::Uuid; 60 | 61 | pub static ALICE: Lazy = Lazy::new(|| Username::from_str("alice").unwrap()); 62 | pub static EXAMPLE_ADDRESS: Lazy
= 63 | Lazy::new(|| Address::from_str("example.alice").unwrap()); 64 | 65 | #[derive(Clone, Debug, Copy)] 66 | struct TestAccount; 67 | 68 | impl Account for TestAccount { 69 | fn id(&self) -> Uuid { 70 | Uuid::new_v4() 71 | } 72 | 73 | fn username(&self) -> &Username { 74 | &ALICE 75 | } 76 | 77 | fn asset_scale(&self) -> u8 { 78 | 9 79 | } 80 | 81 | fn asset_code(&self) -> &str { 82 | "XYZ" 83 | } 84 | 85 | fn ilp_address(&self) -> &Address { 86 | &EXAMPLE_ADDRESS 87 | } 88 | } 89 | 90 | #[tokio::test] 91 | async fn handles_request() { 92 | let from = TestAccount; 93 | let prepare = IldcpRequest {}.to_prepare(); 94 | let req = IncomingRequest { from, prepare }; 95 | let mut service = IldcpService::new(incoming_service_fn(|_| { 96 | Err(RejectBuilder { 97 | code: ErrorCode::F02_UNREACHABLE, 98 | message: b"No other incoming handler!", 99 | data: &[], 100 | triggered_by: None, 101 | } 102 | .build()) 103 | })); 104 | 105 | let result = service.handle_request(req).await.unwrap(); 106 | assert_eq!(result.data().len(), 19); 107 | 108 | let ildpc_info = get_ildcp_info(&mut service, from).await.unwrap(); 109 | assert_eq!(ildpc_info.ilp_address(), EXAMPLE_ADDRESS.clone()); 110 | assert_eq!(ildpc_info.asset_code(), b"XYZ"); 111 | assert_eq!(ildpc_info.asset_scale(), 9); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /crates/interledger-packet/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "interledger-packet" 3 | version = "1.0.0" 4 | authors = ["Evan Schwartz "] 5 | description = "Interledger packet serialization/deserialization" 6 | license = "Apache-2.0" 7 | edition = "2018" 8 | repository = "https://github.com/interledger-rs/interledger-rs" 9 | 10 | [features] 11 | # strict adherence to the rfcs, but not taking any "roundtrip-only" shortcuts 12 | strict = [] 13 | # used when fuzzing; accepts only roundtripping input 14 | roundtrip-only = ["strict"] 15 | 16 | [dependencies] 17 | bytes = { package = "bytes", version = "1.0.1", features = ["serde"] } 18 | chrono = { version = "0.4.20", default-features = false, features = ["std"] } 19 | thiserror = { version = "1.0.10", default-features = false } 20 | serde = { version = "1.0.101", default-features = false, features = ["derive"], optional = true } 21 | regex = { version ="1.5", default-features = false, features = ["std"] } 22 | once_cell = { version = "1.3.1", default-features = false, features = ["std"] } 23 | 24 | [dev-dependencies] 25 | criterion = { version = "0.3.0", default-features = false } 26 | # "serde" is both here and in `[dependencies]` to ensure it is included during 27 | # testing, but optional otherwise. 28 | serde = { version = "1.0.99", default-features = false, features = ["derive"] } 29 | serde_test = { version = "1.0", default-features = false } 30 | 31 | [[bench]] 32 | name = "packets" 33 | harness = false 34 | -------------------------------------------------------------------------------- /crates/interledger-packet/README.md: -------------------------------------------------------------------------------- 1 | # interledger-packet 2 | 3 | This crate provides an implementation of [the Interledger Protocol](https://interledger.org/rfcs/0027-interledger-protocol-4/), 4 | the core component of the Interledger Protocol Suite. 5 | By way of analogy with the internet, this layer is roughly analogous to [IP](https://en.wikipedia.org/wiki/Internet_Protocol). 6 | -------------------------------------------------------------------------------- /crates/interledger-packet/benches/packets.rs: -------------------------------------------------------------------------------- 1 | //! Benchmark packet serialization and deserialization. 2 | 3 | use bytes::BytesMut; 4 | use chrono::{DateTime, Utc}; 5 | use criterion::{criterion_group, criterion_main, Criterion}; 6 | use once_cell::sync::Lazy; 7 | use std::convert::TryFrom; 8 | 9 | use ilp::Address; 10 | use ilp::{ErrorCode, Fulfill, Prepare, Reject}; 11 | use ilp::{FulfillBuilder, PrepareBuilder, RejectBuilder}; 12 | use interledger_packet as ilp; 13 | use std::str::FromStr; 14 | 15 | static PREPARE: Lazy> = Lazy::new(|| PrepareBuilder { 16 | amount: 107, 17 | expires_at: DateTime::parse_from_rfc3339("2017-12-23T01:21:40.549Z") 18 | .unwrap() 19 | .with_timezone(&Utc) 20 | .into(), 21 | execution_condition: b"\ 22 | \x74\xe1\x13\x6d\xc7\x1c\x9e\x5f\x28\x3b\xec\x83\x46\x1c\xbf\x12\ 23 | \x61\xc4\x01\x4f\x72\xd4\x8f\x8d\xd6\x54\x53\xa0\xb8\x4e\x7d\xe1\ 24 | ", 25 | destination: Address::from_str("example.alice").unwrap(), 26 | data: b"\ 27 | \x5d\xb3\x43\xfd\xc4\x18\x98\xf6\xdf\x42\x02\x32\x91\x39\xdc\x24\ 28 | \x2d\xd0\xf5\x58\xa8\x11\xb4\x6b\x28\x91\x8f\xda\xb3\x7c\x6c\xb0\ 29 | ", 30 | }); 31 | static FULFILL: Lazy> = Lazy::new(|| FulfillBuilder { 32 | fulfillment: b"\ 33 | \x11\x7b\x43\x4f\x1a\x54\xe9\x04\x4f\x4f\x54\x92\x3b\x2c\xff\x9e\ 34 | \x4a\x6d\x42\x0a\xe2\x81\xd5\x02\x5d\x7b\xb0\x40\xc4\xb4\xc0\x4a\ 35 | ", 36 | data: b"\ 37 | \x5d\xb3\x43\xfd\xc4\x18\x98\xf6\xdf\x42\x02\x32\x91\x39\xdc\x24\ 38 | \x2d\xd0\xf5\x58\xa8\x11\xb4\x6b\x28\x91\x8f\xda\xb3\x7c\x6c\xb0\ 39 | ", 40 | }); 41 | static EXAMPLE_CONNECTOR: Lazy
= 42 | Lazy::new(|| Address::from_str("example.connector").unwrap()); 43 | static REJECT: Lazy> = Lazy::new(|| RejectBuilder { 44 | code: ErrorCode::F99_APPLICATION_ERROR, 45 | message: b"Some error", 46 | triggered_by: Some(&*EXAMPLE_CONNECTOR), 47 | data: b"\ 48 | \x5d\xb3\x43\xfd\xc4\x18\x98\xf6\xdf\x42\x02\x32\x91\x39\xdc\x24\ 49 | \x2d\xd0\xf5\x58\xa8\x11\xb4\x6b\x28\x91\x8f\xda\xb3\x7c\x6c\xb0\ 50 | ", 51 | }); 52 | 53 | fn benchmark_serialize(c: &mut Criterion) { 54 | let prepare_bytes = BytesMut::from(PREPARE.build()); 55 | c.bench_function("Prepare (serialize)", move |b| { 56 | b.iter(|| { 57 | assert_eq!(BytesMut::from(PREPARE.build()), prepare_bytes); 58 | }); 59 | }); 60 | 61 | let fulfill_bytes = BytesMut::from(FULFILL.build()); 62 | c.bench_function("Fulfill (serialize)", move |b| { 63 | b.iter(|| { 64 | assert_eq!(BytesMut::from(FULFILL.build()), fulfill_bytes); 65 | }); 66 | }); 67 | 68 | let reject_bytes = BytesMut::from(REJECT.build()); 69 | c.bench_function("Reject (serialize)", move |b| { 70 | b.iter(|| { 71 | assert_eq!(BytesMut::from(REJECT.build()), reject_bytes); 72 | }); 73 | }); 74 | } 75 | 76 | fn benchmark_deserialize(c: &mut Criterion) { 77 | let prepare_bytes = BytesMut::from(PREPARE.build()); 78 | c.bench_function("Prepare (deserialize)", move |b| { 79 | b.iter(|| { 80 | let parsed = Prepare::try_from(prepare_bytes.clone()).unwrap(); 81 | assert_eq!(parsed.amount(), PREPARE.amount); 82 | assert_eq!(parsed.destination(), PREPARE.destination); 83 | }); 84 | }); 85 | 86 | let fulfill_bytes = BytesMut::from(FULFILL.build()); 87 | c.bench_function("Fulfill (deserialize)", move |b| { 88 | b.iter(|| { 89 | let parsed = Fulfill::try_from(fulfill_bytes.clone()).unwrap(); 90 | assert_eq!(parsed.fulfillment(), FULFILL.fulfillment); 91 | }); 92 | }); 93 | 94 | let reject_bytes = BytesMut::from(REJECT.build()); 95 | c.bench_function("Reject (deserialize)", move |b| { 96 | b.iter(|| { 97 | let parsed = Reject::try_from(reject_bytes.clone()).unwrap(); 98 | assert_eq!(parsed.code(), REJECT.code); 99 | }); 100 | }); 101 | } 102 | 103 | criterion_group! { 104 | name = benches; 105 | config = Criterion::default() 106 | .sample_size(1000); 107 | targets = 108 | benchmark_serialize, 109 | benchmark_deserialize, 110 | } 111 | 112 | criterion_main!(benches); 113 | -------------------------------------------------------------------------------- /crates/interledger-packet/fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | target 3 | corpus 4 | artifacts 5 | afl_out 6 | Cargo.lock 7 | -------------------------------------------------------------------------------- /crates/interledger-packet/fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | 2 | [package] 3 | name = "interledger-packet-fuzz" 4 | version = "0.0.0" 5 | authors = ["Automatically generated"] 6 | publish = false 7 | edition = "2018" 8 | 9 | [package.metadata] 10 | cargo-fuzz = true 11 | 12 | [dependencies] 13 | libfuzzer-sys = "0.4" 14 | bytes = "0.5" 15 | 16 | [dependencies.interledger-packet] 17 | path = ".." 18 | features = ["roundtrip-only"] 19 | 20 | # Prevent this from interfering with workspaces 21 | [workspace] 22 | members = ["."] 23 | 24 | [[bin]] 25 | name = "packet" 26 | path = "fuzz_targets/packet.rs" 27 | test = false 28 | doc = false 29 | 30 | [[bin]] 31 | name = "address" 32 | path = "fuzz_targets/address.rs" 33 | test = false 34 | doc = false 35 | -------------------------------------------------------------------------------- /crates/interledger-packet/fuzz/README.md: -------------------------------------------------------------------------------- 1 | # interledger-packet fuzzing 2 | 3 | ## Quickstart (cargo-fuzz) 4 | 5 | See book for more information: https://rust-fuzz.github.io/book/cargo-fuzz.html 6 | 7 | ``` 8 | cargo install cargo-fuzz 9 | ``` 10 | 11 | Then under the interledger-packet root: 12 | 13 | ``` 14 | cargo +nightly fuzz run prepare 15 | ``` 16 | -------------------------------------------------------------------------------- /crates/interledger-packet/fuzz/fuzz_targets/address.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use interledger_packet::{oer, Address}; 3 | use libfuzzer_sys::fuzz_target; 4 | use std::convert::TryFrom; 5 | 6 | fuzz_target!(|data: &[u8]| { 7 | if let Ok(address) = Address::try_from(data) { 8 | assert!(oer::predict_var_octet_string(address.len()) >= Address::MIN_LEN); 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /crates/interledger-packet/fuzz/fuzz_targets/packet.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | 4 | fuzz_target!(|data: &[u8]| { 5 | let _ = interledger_packet::lenient_packet_roundtrips(data); 6 | }); 7 | -------------------------------------------------------------------------------- /crates/interledger-packet/src/errors.rs: -------------------------------------------------------------------------------- 1 | use super::AddressError; 2 | 3 | #[derive(Debug, thiserror::Error)] 4 | pub enum ParseError { 5 | #[error("Invalid Packet: {0}")] 6 | Oer(#[from] OerError), 7 | #[error("Chrono Error: {0}")] 8 | ChronoErr(#[from] chrono::ParseError), 9 | #[error("Invalid Packet: {0}")] 10 | PacketType(#[from] PacketTypeError), 11 | #[error("Invalid Packet: Reject.ErrorCode was not IA5String")] 12 | ErrorCodeConversion, 13 | #[error("Invalid Packet: DateTime must be numeric")] 14 | TimestampConversion, 15 | #[error("Invalid Address: {0}")] 16 | InvalidAddress(#[from] AddressError), 17 | #[error("Invalid Packet: {0}")] 18 | TrailingBytes(#[from] TrailingBytesError), 19 | #[cfg(feature = "roundtrip-only")] 20 | #[cfg_attr(feature = "roundtrip-only", error("Timestamp not roundtrippable"))] 21 | NonRoundtrippableTimestamp, 22 | } 23 | 24 | #[derive(Debug, thiserror::Error)] 25 | pub enum PacketTypeError { 26 | #[error("Unknown packet type")] 27 | Eof, 28 | #[error("PacketType {0} is not supported")] 29 | Unknown(u8), 30 | #[error("PacketType {1} expected, found {0}")] 31 | Unexpected(u8, u8), 32 | } 33 | 34 | #[derive(Debug, thiserror::Error)] 35 | pub enum TrailingBytesError { 36 | #[error("Unexpected outer trailing bytes")] 37 | Outer, 38 | #[error("Unexpected inner trailing bytes")] 39 | Inner, 40 | } 41 | 42 | /// Object Encoding Rules errors happen with any low level representation reading. 43 | /// 44 | /// See the [RFC-0030] for details. 45 | /// [RFC-0030]: https://github.com/interledger/rfcs/blob/master/0030-notes-on-oer-encoding/0030-notes-on-oer-encoding.md 46 | #[derive(PartialEq, Eq, Debug, thiserror::Error)] 47 | pub enum OerError { 48 | #[error("buffer too small")] 49 | UnexpectedEof, 50 | #[error("{0}")] 51 | LengthPrefix(#[from] LengthPrefixError), 52 | #[error("{0}")] 53 | VarUint(#[from] VarUintError), 54 | #[error("{0}")] 55 | VariableLengthTimestamp(#[from] VariableLengthTimestampError), 56 | } 57 | 58 | #[derive(PartialEq, Eq, Debug, thiserror::Error)] 59 | pub enum LengthPrefixError { 60 | #[error("indefinite lengths are not allowed")] 61 | IndefiniteLength, 62 | #[error("length prefix too large")] 63 | TooLarge, 64 | #[error("length prefix overflow")] 65 | UsizeOverflow, 66 | #[error("variable length prefix with unnecessary multibyte length")] 67 | LeadingZeros, 68 | #[cfg(feature = "strict")] 69 | #[cfg_attr(feature = "strict", error("length prefix with leading zero"))] 70 | StrictLeadingZeros, 71 | } 72 | 73 | #[derive(PartialEq, Eq, Debug, thiserror::Error)] 74 | pub enum VarUintError { 75 | #[error("var uint has zero length")] 76 | ZeroLength, 77 | #[error("var uint too large")] 78 | TooLarge, 79 | } 80 | 81 | #[derive(PartialEq, Eq, Debug, thiserror::Error)] 82 | pub enum VariableLengthTimestampError { 83 | #[error("Invalid length for variable length timestamp: {0}")] 84 | InvalidLength(usize), 85 | #[error("Input failed to parse as timestamp")] 86 | InvalidTimestamp, 87 | } 88 | -------------------------------------------------------------------------------- /crates/interledger-packet/src/hex.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | /// Slice as hex string debug formatter, doesn't require allocating a string. 4 | #[derive(PartialEq, Eq)] 5 | pub struct HexString<'a>(pub &'a [u8]); 6 | 7 | impl<'a> fmt::Debug for HexString<'a> { 8 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { 9 | for b in self.0 { 10 | write!(fmt, "{:02x}", b)?; 11 | } 12 | Ok(()) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /crates/interledger-packet/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # interledger-packet 2 | //! 3 | //! Interledger packet serialization/deserialization. 4 | 5 | mod address; 6 | 7 | mod error; 8 | mod errors; 9 | #[cfg(test)] 10 | mod fixtures; 11 | pub mod hex; 12 | pub mod oer; 13 | mod packet; 14 | 15 | pub use self::address::{Address, AddressError}; 16 | pub use self::error::{ErrorClass, ErrorCode}; 17 | pub use self::errors::{OerError, PacketTypeError, ParseError, TrailingBytesError}; 18 | 19 | pub use self::packet::MaxPacketAmountDetails; 20 | pub use self::packet::{Fulfill, Packet, PacketType, Prepare, Reject}; 21 | pub use self::packet::{FulfillBuilder, PrepareBuilder, RejectBuilder}; 22 | 23 | #[cfg(any(fuzzing, test))] 24 | pub fn lenient_packet_roundtrips(data: &[u8]) -> Result<(), ParseError> { 25 | use bytes::BytesMut; 26 | use hex::HexString; 27 | use std::convert::{TryFrom, TryInto}; 28 | 29 | let pkt = Packet::try_from(BytesMut::from(data))?; 30 | 31 | // try to create a corresponding builder and a new set of bytes 32 | let other = match pkt { 33 | Packet::Prepare(p) => { 34 | let other = PrepareBuilder { 35 | amount: p.amount(), 36 | expires_at: p.expires_at(), 37 | destination: p.destination(), 38 | execution_condition: p 39 | .execution_condition() 40 | .try_into() 41 | .expect("wrong length slice"), 42 | data: p.data(), 43 | } 44 | .build(); 45 | 46 | if p == other { 47 | // if the packet roundtripped, great, we are done 48 | return Ok(()); 49 | } 50 | 51 | BytesMut::from(other) 52 | } 53 | Packet::Fulfill(f) => { 54 | let other = FulfillBuilder { 55 | fulfillment: f.fulfillment().try_into().expect("wrong length slice"), 56 | data: f.data(), 57 | } 58 | .build(); 59 | 60 | if f == other { 61 | return Ok(()); 62 | } 63 | 64 | BytesMut::from(other) 65 | } 66 | Packet::Reject(r) => { 67 | let other = RejectBuilder { 68 | code: r.code(), 69 | message: r.message(), 70 | triggered_by: r.triggered_by().as_ref(), 71 | data: r.data(), 72 | } 73 | .build(); 74 | 75 | if r == other { 76 | return Ok(()); 77 | } 78 | 79 | BytesMut::from(other) 80 | } 81 | }; 82 | 83 | assert_eq!(HexString(data), HexString(&other[..])); 84 | 85 | Ok(()) 86 | } 87 | -------------------------------------------------------------------------------- /crates/interledger-rates/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "interledger-rates" 3 | version = "1.0.0" 4 | authors = ["Evan Schwartz "] 5 | description = "Exchange rate utilities" 6 | license = "Apache-2.0" 7 | edition = "2018" 8 | repository = "https://github.com/interledger-rs/interledger-rs" 9 | 10 | [dependencies] 11 | interledger-errors = { path = "../interledger-errors", version = "1.0.0" } 12 | 13 | futures = { version = "0.3.7", default-features = false } 14 | tracing = { version = "0.1.12", default-features = false, features = ["log"] } 15 | once_cell = { version = "1.3.1", default-features = false } 16 | reqwest = { version = "0.11.4", default-features = false, features = ["default-tls", "json"] } 17 | secrecy = { version = "0.8", default-features = false, features = ["alloc", "serde"] } 18 | serde = { version = "1.0.101", default-features = false, features = ["derive"]} 19 | tokio = { version = "1.9.0", default-features = false, features = ["macros", "time", "sync"] } 20 | -------------------------------------------------------------------------------- /crates/interledger-rates/README.md: -------------------------------------------------------------------------------- 1 | # interledger-rates 2 | 3 | Utilities for fetching and caching exchange rates from external APIs, which supports CoinCap and CryptoCompare rate backends. 4 | -------------------------------------------------------------------------------- /crates/interledger-rates/src/coincap.rs: -------------------------------------------------------------------------------- 1 | use futures::TryFutureExt; 2 | use once_cell::sync::Lazy; 3 | use reqwest::{Client, Url}; 4 | use serde::Deserialize; 5 | use std::{collections::HashMap, str::FromStr}; 6 | use tracing::{error, warn}; 7 | 8 | // We use both endpoints because they contain different sets of rates 9 | // This one has more cryptocurrencies 10 | static COINCAP_ASSETS_URL: Lazy = 11 | Lazy::new(|| Url::parse("https://api.coincap.io/v2/assets").unwrap()); 12 | // This one has more fiat currencies 13 | static COINCAP_RATES_URL: Lazy = 14 | Lazy::new(|| Url::parse("https://api.coincap.io/v2/rates").unwrap()); 15 | 16 | #[derive(Deserialize, Debug)] 17 | struct Rate { 18 | symbol: String, 19 | #[serde(alias = "rateUsd", alias = "priceUsd")] 20 | rate_usd: String, 21 | } 22 | 23 | #[derive(Deserialize, Debug)] 24 | struct RateResponse { 25 | data: Vec, 26 | } 27 | 28 | pub async fn query_coincap(client: &Client) -> Result, ()> { 29 | let (assets, rates) = futures::future::join( 30 | query_coincap_endpoint(client, COINCAP_ASSETS_URL.clone()), 31 | query_coincap_endpoint(client, COINCAP_RATES_URL.clone()), 32 | ) 33 | .await; 34 | 35 | let all_rates: HashMap = assets? 36 | .data 37 | .into_iter() 38 | .chain(rates?.data.into_iter()) 39 | .filter_map(|record| match f64::from_str(record.rate_usd.as_str()) { 40 | Ok(rate) => Some((record.symbol.to_uppercase(), rate)), 41 | Err(err) => { 42 | warn!( 43 | "Unable to parse {} rate as an f64: {} {:?}", 44 | record.symbol, record.rate_usd, err 45 | ); 46 | None 47 | } 48 | }) 49 | .collect(); 50 | Ok(all_rates) 51 | } 52 | 53 | async fn query_coincap_endpoint(client: &Client, url: Url) -> Result { 54 | let res = client 55 | .get(url) 56 | .send() 57 | .map_err(|err| { 58 | error!("Error fetching exchange rates from CoinCap: {:?}", err); 59 | }) 60 | .await?; 61 | 62 | let res = res.error_for_status().map_err(|err| { 63 | error!("HTTP error getting exchange rates from CoinCap: {:?}", err); 64 | })?; 65 | 66 | res.json() 67 | .map_err(|err| { 68 | error!( 69 | "Error getting exchange rate response body from CoinCap, incorrect type: {:?}", 70 | err 71 | ); 72 | }) 73 | .await 74 | } 75 | -------------------------------------------------------------------------------- /crates/interledger-rates/src/cryptocompare.rs: -------------------------------------------------------------------------------- 1 | use futures::TryFutureExt; 2 | use once_cell::sync::Lazy; 3 | use reqwest::{Client, Url}; 4 | use secrecy::{ExposeSecret, SecretString}; 5 | use serde::Deserialize; 6 | use std::{collections::HashMap, iter::once}; 7 | use tracing::error; 8 | 9 | static CRYPTOCOMPARE_URL: Lazy = Lazy::new(|| { 10 | Url::parse("https://min-api.cryptocompare.com/data/top/mktcapfull?limit=100&tsym=USD").unwrap() 11 | }); 12 | 13 | #[derive(Deserialize, Debug)] 14 | struct Price { 15 | #[serde(rename = "PRICE")] 16 | price: f64, 17 | } 18 | 19 | #[derive(Deserialize, Debug)] 20 | struct Raw { 21 | #[serde(rename = "USD")] 22 | usd: Price, 23 | } 24 | 25 | #[derive(Deserialize, Debug)] 26 | struct CoinInfo { 27 | #[serde(rename(deserialize = "Name"))] 28 | name: String, 29 | } 30 | 31 | #[derive(Deserialize, Debug)] 32 | struct Record { 33 | #[serde(rename = "CoinInfo")] 34 | coin_info: CoinInfo, 35 | #[serde(rename = "RAW")] 36 | raw: Option, 37 | } 38 | 39 | #[derive(Deserialize, Debug)] 40 | struct Response { 41 | #[serde(rename = "Data")] 42 | data: Vec, 43 | } 44 | 45 | pub async fn query_cryptocompare( 46 | client: &Client, 47 | api_key: &SecretString, 48 | ) -> Result, ()> { 49 | // ref: https://github.com/rust-lang/rust/pull/64856 50 | let header = format!("Apikey {}", api_key.expose_secret()); 51 | let res = client 52 | .get(CRYPTOCOMPARE_URL.clone()) 53 | // TODO don't copy the api key on every request 54 | .header("Authorization", header) 55 | .send() 56 | .map_err(|err| { 57 | error!( 58 | "Error fetching exchange rates from CryptoCompare: {:?}", 59 | err 60 | ); 61 | }) 62 | .await?; 63 | 64 | let res = res.error_for_status().map_err(|err| { 65 | error!( 66 | "HTTP error getting exchange rates from CryptoCompare: {:?}", 67 | err 68 | ); 69 | })?; 70 | 71 | let res: Response = res 72 | .json() 73 | .map_err(|err| { 74 | error!( 75 | "Error getting exchange rate response body from CryptoCompare, incorrect type: {:?}", 76 | err 77 | ); 78 | }) 79 | .await?; 80 | 81 | let rates = res 82 | .data 83 | .into_iter() 84 | .filter_map(|asset| { 85 | if let Some(raw) = asset.raw { 86 | Some((asset.coin_info.name.to_uppercase(), raw.usd.price)) 87 | } else { 88 | None 89 | } 90 | }) 91 | .chain(once(("USD".to_string(), 1.0))); 92 | Ok(rates.collect()) 93 | } 94 | -------------------------------------------------------------------------------- /crates/interledger-router/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "interledger-router" 3 | version = "1.0.0" 4 | authors = ["Evan Schwartz "] 5 | description = "Router for Interledger requests" 6 | license = "Apache-2.0" 7 | edition = "2018" 8 | repository = "https://github.com/interledger-rs/interledger-rs" 9 | 10 | [dependencies] 11 | interledger-packet = { path = "../interledger-packet", version = "1.0.0", default-features = false } 12 | interledger-service = { path = "../interledger-service", version = "1.0.0", default-features = false } 13 | 14 | tracing = { version = "0.1.12", default-features = false, features = ["log"] } 15 | parking_lot = { version = "0.10.0", default-features = false } 16 | uuid = { version = "0.8.1", default-features = false, features = ["v4"]} 17 | async-trait = { version = "0.1.22", default-features = false } 18 | 19 | [dev-dependencies] 20 | once_cell = { version = "1.3.1", default-features = false } 21 | tokio = { version = "1.9.0", default-features = false, features = ["rt", "macros"]} 22 | interledger-errors = { path = "../interledger-errors", version = "1.0.0", default-features = false } 23 | -------------------------------------------------------------------------------- /crates/interledger-router/README.md: -------------------------------------------------------------------------------- 1 | # Interledger Router 2 | 3 | The router implements an incoming service and includes an outgoing service. 4 | 5 | It determines the next account to forward to and passes it on. Both incoming and outgoing services can respond to requests but many just pass the request on. It stores a RouterStore which stores the entire routing table. 6 | 7 | Once it receives a Prepare, it checks its destination in its routing table. If the destination exists in the routing table it forwards it there, otherwise it searches for a route where the prefix matches the address and forwards it there. 8 | -------------------------------------------------------------------------------- /crates/interledger-router/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # interledger-router 2 | //! 3 | //! A service that routes ILP Prepare packets to the correct next 4 | //! account based on the ILP address in the Prepare packet based 5 | //! on the routing table. 6 | //! 7 | //! A routing table could be as simple as a single entry for the empty prefix 8 | //! ("") that will route all requests to a specific outgoing account. 9 | //! 10 | //! Note that the Router is not responsible for building the routing table, 11 | //! only using the information provided by the store. The routing table in the 12 | //! store can either be configured or populated using the `CcpRouteManager` 13 | //! (see the `interledger-ccp` crate for more details). 14 | 15 | use interledger_service::AccountStore; 16 | use std::{collections::HashMap, sync::Arc}; 17 | use uuid::Uuid; 18 | 19 | mod router; 20 | 21 | pub use self::router::Router; 22 | 23 | /// A trait for Store implmentations that have ILP routing tables. 24 | pub trait RouterStore: AccountStore + Clone + Send + Sync + 'static { 25 | /// **Synchronously** return the routing table. 26 | /// Note that this is synchronous because it assumes that Stores should 27 | /// keep the routing table in memory and use PubSub or polling to keep it updated. 28 | /// This ensures that individual packets can be routed without hitting the underlying store. 29 | /// An Arc is returned to avoid copying the underlying data while processing each packet. 30 | fn routing_table(&self) -> Arc>; 31 | } 32 | -------------------------------------------------------------------------------- /crates/interledger-service-util/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "interledger-service-util" 3 | version = "1.0.0" 4 | authors = ["Evan Schwartz "] 5 | description = "Small, commonly used Interledger services that don't really fit anywhere else" 6 | license = "Apache-2.0" 7 | edition = "2018" 8 | repository = "https://github.com/interledger-rs/interledger-rs" 9 | 10 | [dependencies] 11 | interledger-errors = { path = "../interledger-errors", version = "1.0.0", default-features = false } 12 | interledger-packet = { path = "../interledger-packet", version = "1.0.0", default-features = false } 13 | interledger-rates = { path = "../interledger-rates", version = "1.0.0", default-features = false } 14 | interledger-service = { path = "../interledger-service", version = "1.0.0", default-features = false } 15 | interledger-settlement = { path = "../interledger-settlement", version = "1.0.0", default-features = false, features = ["settlement_api"] } 16 | 17 | bytes = { version = "1.0.1", default-features = false } 18 | chrono = { version = "0.4.20", default-features = false, features = ["clock"] } 19 | futures = { version = "0.3.7", default-features = false } 20 | once_cell = { version = "1.3.1", default-features = false, features = ["std"] } 21 | tracing = { version = "0.1.12", default-features = false, features = ["log"] } 22 | reqwest = { version = "0.11.4", default-features = false, features = ["default-tls"] } 23 | ring = { version = "0.16.9", default-features = false } 24 | secrecy = { version = "0.8", default-features = false, features = ["alloc", "serde"] } 25 | serde = { version = "1.0.101", default-features = false, features = ["derive"]} 26 | tokio = { version = "1.9.0", default-features = false, features = ["macros", "time", "sync"] } 27 | tokio-util = { version = "0.6.7", features = ["time"]} 28 | async-trait = { version = "0.1.22", default-features = false } 29 | uuid = { version = "0.8.1", default-features = false } 30 | 31 | [dev-dependencies] 32 | uuid = { version = "0.8.1", default-features = false} 33 | once_cell = { version = "1.3.1", default-features = false } 34 | parking_lot = { version = "0.10.0", default-features = false } 35 | mockito = { version = "0.23.0", default-features = false } 36 | url = { version = "2.1.1", default-features = false } 37 | -------------------------------------------------------------------------------- /crates/interledger-service-util/README.md: -------------------------------------------------------------------------------- 1 | # Interledger Service Utilities 2 | 3 | Contains a few interledger services which are too small to be their own crate. 4 | 5 | TODO: UML Sequence charts for each service 6 | 7 | ## Currently supported services 8 | 9 | - Balance 10 | - Echo 11 | - Exchange Rates 12 | - Expiry Shortener 13 | - Max Packet Amount 14 | - Rate Limit 15 | - Validator 16 | 17 | -------------------------------------------------------------------------------- /crates/interledger-service-util/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # interledger-service-util 2 | //! 3 | //! Miscellaneous, small Interledger Services. 4 | 5 | /// Balance tracking service 6 | mod balance_service; 7 | /// Service which implements the echo protocol 8 | mod echo_service; 9 | /// Service responsible for setting and fetching dollar denominated exchange rates 10 | mod exchange_rates_service; 11 | /// Service responsible for shortening the expiry time of packets, 12 | /// to take into account for network latency 13 | mod expiry_shortener_service; 14 | /// Service responsible for capping the amount an account can send in a packet 15 | mod max_packet_amount_service; 16 | /// Service responsible for capping the amount of packets and amount in packets an account can send 17 | mod rate_limit_service; 18 | /// Service responsible for checking that packets are not expired and that prepare packets' fulfillment conditions 19 | /// match the fulfillment inside the incoming fulfills 20 | mod validator_service; 21 | 22 | pub use self::balance_service::{start_delayed_settlement, BalanceService, BalanceStore}; 23 | pub use self::echo_service::EchoService; 24 | pub use self::exchange_rates_service::ExchangeRateService; 25 | pub use self::expiry_shortener_service::{ 26 | ExpiryShortenerService, RoundTripTimeAccount, DEFAULT_ROUND_TRIP_TIME, 27 | }; 28 | pub use self::max_packet_amount_service::{MaxPacketAmountAccount, MaxPacketAmountService}; 29 | pub use self::rate_limit_service::{ 30 | RateLimitAccount, RateLimitError, RateLimitService, RateLimitStore, 31 | }; 32 | pub use self::validator_service::ValidatorService; 33 | -------------------------------------------------------------------------------- /crates/interledger-service/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "interledger-service" 3 | version = "1.0.0" 4 | authors = ["Evan Schwartz "] 5 | description = "The core abstraction for the Interledger.rs implementation" 6 | license = "Apache-2.0" 7 | edition = "2018" 8 | repository = "https://github.com/interledger-rs/interledger-rs" 9 | 10 | [features] 11 | default = [] 12 | trace = ["tracing-futures"] 13 | 14 | [dependencies] 15 | interledger-errors = { path = "../interledger-errors", version = "1.0.0", default-features = false } 16 | interledger-packet = { path = "../interledger-packet", version = "1.0.0", default-features = false } 17 | 18 | futures = { version = "0.3.7", default-features = false } 19 | serde = { version = "1.0.101", default-features = false, features = ["derive"] } 20 | regex = { version = "1.5", default-features = false, features = ["std", "unicode-perl"] } 21 | once_cell = { version = "1.3.1", default-features = false, features = ["std"] } 22 | unicase = { version = "2.5.1", default-features = false } 23 | unicode-normalization = { version = "0.1.8", default-features = false } 24 | uuid = { version = "0.8.1", default-features = false} 25 | async-trait = { version = "0.1.22", default-features = false } 26 | 27 | #trace feature 28 | tracing-futures = { version = "0.2.1", default-features = false, features = ["std", "futures-03"], optional = true } 29 | 30 | [dev-dependencies] 31 | serde_json = { version = "1.0.41", default-features = false } 32 | -------------------------------------------------------------------------------- /crates/interledger-service/README.md: -------------------------------------------------------------------------------- 1 | # interledger-service 2 | 3 | This is the core abstraction used across the Interledger.rs implementation. 4 | 5 | Inspired by [tower](https://github.com/tower-rs), all of the components 6 | of this implementation are "services" 7 | that take a request type and asynchronously return a result. 8 | Every component uses the same interface so that 9 | services can be reused and combined into different bundles of functionality. 10 | 11 | The Interledger service traits use requests that contain 12 | ILP Prepare packets and the related `from`/`to` Accounts 13 | and asynchronously return either an ILP Fullfill or Reject packet. 14 | Implementations of Stores (wrappers around 15 | databases) can attach additional information to the Account records, 16 | which are then passed through the service chain. 17 | 18 | ## Example Service Bundles 19 | 20 | The following examples illustrate how different Services can be 21 | chained together to create different bundles of functionality. 22 | 23 | ### SPSP Sender 24 | 25 | `SPSP Client --> ValidatorService --> RouterService --> HttpOutgoingService` 26 | 27 | ### Connector 28 | 29 | `HttpServerService --> ValidatorService --> RouterService --> BalanceAndExchangeRateService --> ValidatorService --> HttpOutgoingService` 30 | 31 | ### STREAM Receiver 32 | 33 | `HttpServerService --> ValidatorService --> StreamReceiverService` 34 | -------------------------------------------------------------------------------- /crates/interledger-service/src/trace.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use async_trait::async_trait; 3 | use tracing_futures::{Instrument, Instrumented}; 4 | 5 | // TODO see if we can replace this with the tower tracing later 6 | #[async_trait] 7 | impl IncomingService for Instrumented 8 | where 9 | IO: IncomingService + Clone + Send, 10 | A: Account + 'static, 11 | { 12 | async fn handle_request(&mut self, request: IncomingRequest) -> IlpResult { 13 | self.inner_mut() 14 | .handle_request(request) 15 | .in_current_span() 16 | .await 17 | } 18 | } 19 | 20 | #[async_trait] 21 | impl OutgoingService for Instrumented 22 | where 23 | IO: OutgoingService + Clone + Send, 24 | A: Account + 'static, 25 | { 26 | async fn send_request(&mut self, request: OutgoingRequest) -> IlpResult { 27 | self.inner_mut() 28 | .send_request(request) 29 | .in_current_span() 30 | .await 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /crates/interledger-settlement/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "interledger-settlement" 3 | version = "1.0.0" 4 | authors = ["Evan Schwartz "] 5 | description = "Settlement-related components for Interledger.rs" 6 | license = "Apache-2.0" 7 | edition = "2018" 8 | repository = "https://github.com/interledger-rs/interledger-rs" 9 | 10 | [dependencies] 11 | interledger-errors = { path = "../interledger-errors", version = "1.0.0", default-features = false, features = ["redis_errors"] } 12 | interledger-packet = { path = "../interledger-packet", version = "1.0.0", default-features = false } 13 | interledger-service = { path = "../interledger-service", version = "1.0.0", default-features = false } 14 | 15 | bytes = { version = "1.0.1", default-features = false, features = ["serde"] } 16 | futures = { version = "0.3.7", default-features = false } 17 | hyper = { version = "0.14.11", default-features = false } 18 | tracing = { version = "0.1.12", default-features = false, features = ["log"] } 19 | reqwest = { version = "0.11.4", default-features = false, features = ["default-tls", "json"] } 20 | serde = { version = "1.0.101", default-features = false } 21 | serde_json = { version = "1.0.41", default-features = false } 22 | url = { version = "2.1.1", default-features = false } 23 | once_cell = { version = "1.3.1", default-features = false, features = ["std"] } 24 | uuid = { version = "0.8.1", default-features = false, features = ["v4"] } 25 | ring = { version = "0.16.9", default-features = false } 26 | tokio = { version = "1.9.0", default-features = false, features = ["macros", "rt"] } 27 | num-bigint = { version = "0.2.3", default-features = false, features = ["std"] } 28 | num-traits = { version = "0.2.8", default-features = false } 29 | warp = { version = "0.3.1", default-features = false } 30 | http = { version = "0.2.0", default-features = false } 31 | redis_crate = { package = "redis", version = "0.21.0", optional = true, default-features = false, features = ["tokio-comp"] } 32 | async-trait = { version = "0.1.22", default-features = false } 33 | futures-retry = { version = "0.6.0", default-features = false } 34 | 35 | [dev-dependencies] 36 | parking_lot = { version = "0.10.0", default-features = false } 37 | mockito = { version = "0.23.1", default-features = false } 38 | socket2 = "0.4.0" 39 | rand = { version = "0.7.2", default-features = false } 40 | 41 | [features] 42 | settlement_api = [] 43 | backends_common = ["redis"] 44 | redis = ["redis_crate"] 45 | -------------------------------------------------------------------------------- /crates/interledger-settlement/src/api/fixtures.rs: -------------------------------------------------------------------------------- 1 | use interledger_packet::Address; 2 | use mockito::Matcher; 3 | #[cfg(test)] 4 | use once_cell::sync::Lazy; 5 | use std::str::FromStr; 6 | use uuid::Uuid; 7 | 8 | use super::test_helpers::TestAccount; 9 | 10 | pub static DATA: &str = "DATA_FOR_SETTLEMENT_ENGINE"; 11 | pub static BODY: &str = "hi"; 12 | pub static IDEMPOTENCY: &str = "AJKJNUjM0oyiAN46"; 13 | 14 | pub static TEST_ACCOUNT_0: Lazy = Lazy::new(|| { 15 | TestAccount::new( 16 | Uuid::from_slice(&[0; 16]).unwrap(), 17 | "http://localhost:1234", 18 | "example.account", 19 | ) 20 | }); 21 | pub static SERVICE_ADDRESS: Lazy
= 22 | Lazy::new(|| Address::from_str("example.connector").unwrap()); 23 | pub static MESSAGES_API: Lazy = Lazy::new(|| { 24 | Matcher::Regex(r"^/accounts/[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}/messages$".to_string()) 25 | }); 26 | -------------------------------------------------------------------------------- /crates/interledger-settlement/src/api/mod.rs: -------------------------------------------------------------------------------- 1 | /// [`IncomingService`](../../interledger_service/trait.IncomingService.html) which catches 2 | /// incoming requests which are sent to `peer.settle` (the node's settlement engine ILP address) 3 | mod message_service; 4 | /// The Warp API exposed by the connector 5 | mod node_api; 6 | 7 | #[cfg(test)] 8 | mod fixtures; 9 | #[cfg(test)] 10 | mod test_helpers; 11 | 12 | pub use message_service::SettlementMessageService; 13 | pub use node_api::create_settlements_filter; 14 | -------------------------------------------------------------------------------- /crates/interledger-settlement/src/core/backends_common/mod.rs: -------------------------------------------------------------------------------- 1 | /// This module holds baseline implementations for the idempotency and leftover-related features 2 | /// which should be shared across engines which use the same store backend. An engine backend's 3 | /// imlpementation could directly use the provided store and not have to worry about any 4 | /// idempotency or leftover-related functionality. 5 | #[cfg(feature = "redis")] 6 | pub mod redis; 7 | -------------------------------------------------------------------------------- /crates/interledger-settlement/src/core/backends_common/redis/test_helpers/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod redis_helpers; 3 | #[cfg(test)] 4 | mod store_helpers; 5 | #[cfg(test)] 6 | pub use store_helpers::{test_store, IDEMPOTENCY_KEY}; 7 | -------------------------------------------------------------------------------- /crates/interledger-settlement/src/core/backends_common/redis/test_helpers/store_helpers.rs: -------------------------------------------------------------------------------- 1 | use super::redis_helpers::TestContext; 2 | use crate::core::backends_common::redis::{EngineRedisStore, EngineRedisStoreBuilder}; 3 | 4 | use once_cell::sync::Lazy; 5 | 6 | pub static IDEMPOTENCY_KEY: Lazy = Lazy::new(|| String::from("abcd")); 7 | 8 | pub async fn test_store() -> Result<(EngineRedisStore, TestContext), ()> { 9 | let context = TestContext::new(); 10 | let store = EngineRedisStoreBuilder::new(context.get_client_connection_info()) 11 | .connect() 12 | .await?; 13 | Ok((store, context)) 14 | } 15 | -------------------------------------------------------------------------------- /crates/interledger-settlement/src/core/mod.rs: -------------------------------------------------------------------------------- 1 | /// Common backend utils for the IdempotentStore and LeftoversStore traits 2 | /// Only exported if the `backends_common` feature flag is enabled 3 | #[cfg(feature = "backends_common")] 4 | pub mod backends_common; 5 | 6 | /// Web service which exposes settlement related endpoints as described in the [RFC](https://interledger.org/rfcs/0038-settlement-engines/), 7 | /// All endpoints are idempotent. 8 | pub mod engines_api; 9 | 10 | mod settlement_client; 11 | pub use settlement_client::SettlementClient; 12 | 13 | /// Expose useful utilities for implementing idempotent functionalities 14 | pub mod idempotency; 15 | 16 | /// Expose useful traits 17 | pub mod types; 18 | 19 | use num_bigint::BigUint; 20 | use num_traits::Zero; 21 | use ring::digest::{digest, SHA256}; 22 | use types::{Convert, ConvertDetails}; 23 | 24 | /// Converts a number from a precision to another while taking precision loss into account 25 | /// 26 | /// # Examples 27 | /// ```rust 28 | /// # use num_bigint::BigUint; 29 | /// # use interledger_settlement::core::scale_with_precision_loss; 30 | /// assert_eq!( 31 | /// scale_with_precision_loss(BigUint::from(905u32), 9, 11), 32 | /// (BigUint::from(9u32), BigUint::from(5u32)) 33 | /// ); 34 | /// 35 | /// assert_eq!( 36 | /// scale_with_precision_loss(BigUint::from(8053u32), 9, 12), 37 | /// (BigUint::from(8u32), BigUint::from(53u32)) 38 | /// ); 39 | /// 40 | /// assert_eq!( 41 | /// scale_with_precision_loss(BigUint::from(1u32), 9, 6), 42 | /// (BigUint::from(1000u32), BigUint::from(0u32)) 43 | /// ); 44 | /// ``` 45 | pub fn scale_with_precision_loss( 46 | amount: BigUint, 47 | local_scale: u8, 48 | remote_scale: u8, 49 | ) -> (BigUint, BigUint) { 50 | // It's safe to unwrap here since BigUint's normalize_scale cannot fail. 51 | let scaled = amount 52 | .normalize_scale(ConvertDetails { 53 | from: remote_scale, 54 | to: local_scale, 55 | }) 56 | .unwrap(); 57 | 58 | if local_scale < remote_scale { 59 | // If we ended up downscaling, scale the value back up back, 60 | // and return any precision loss 61 | // note that `from` and `to` are reversed compared to the previous call 62 | let upscaled = scaled 63 | .normalize_scale(ConvertDetails { 64 | from: local_scale, 65 | to: remote_scale, 66 | }) 67 | .unwrap(); 68 | let precision_loss = if upscaled < amount { 69 | amount - upscaled 70 | } else { 71 | Zero::zero() 72 | }; 73 | (scaled, precision_loss) 74 | } else { 75 | // there is no need to do anything further if we upscaled 76 | (scaled, Zero::zero()) 77 | } 78 | } 79 | 80 | /// Returns the 32-bytes SHA256 hash of the provided preimage 81 | pub fn get_hash_of(preimage: &[u8]) -> [u8; 32] { 82 | let mut hash = [0; 32]; 83 | hash.copy_from_slice(digest(&SHA256, preimage).as_ref()); 84 | hash 85 | } 86 | -------------------------------------------------------------------------------- /crates/interledger-settlement/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "settlement_api")] 2 | /// Settlement API exposed by the Interledger Node 3 | /// This is only available if the `settlement_api` feature is enabled 4 | pub mod api; 5 | /// Core module including types, common store implementations for settlement 6 | pub mod core; 7 | -------------------------------------------------------------------------------- /crates/interledger-spsp/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "interledger-spsp" 3 | version = "1.0.0" 4 | authors = ["Evan Schwartz "] 5 | description = "Client and server implementations of the Simple Payment Setup Protocol (SPSP)" 6 | license = "Apache-2.0" 7 | edition = "2018" 8 | repository = "https://github.com/interledger-rs/interledger-rs" 9 | 10 | [dependencies] 11 | interledger-packet = { path = "../interledger-packet", version = "1.0.0", features = ["serde"], default-features = false } 12 | interledger-rates = { path = "../interledger-rates", version = "1.0.0", default-features = false } 13 | interledger-service = { path = "../interledger-service", version = "1.0.0", default-features = false } 14 | interledger-stream = { path = "../interledger-stream", version = "1.0.0", default-features = false } 15 | 16 | base64 = { version = "0.13.0", default-features = false } 17 | bytes = { version = "1.0.1", default-features = false } 18 | futures = { version = "0.3.7", default-features = false } 19 | hyper = { version = "0.14.11", default-features = false } 20 | tracing = { version = "0.1.12", default-features = false, features = ["log"] } 21 | reqwest = { version = "0.11.4", default-features = false, features = ["default-tls", "json"] } 22 | serde = { version = "1.0.101", default-features = false } 23 | serde_json = { version = "1.0.41", default-features = false } 24 | thiserror = { version = "1.0.10", default-features = false } 25 | 26 | [dev-dependencies] 27 | tokio = { version = "1.9.0", default-features = false, features = ["macros"] } 28 | -------------------------------------------------------------------------------- /crates/interledger-spsp/README.md: -------------------------------------------------------------------------------- 1 | # interledger-spsp 2 | 3 | Client and server implementations of the [Simple Payment Setup Protocol (SPSP)](https://interledger.org/rfcs/0009-simple-payment-setup-protocol/), 4 | an application-layer protocol within the Interledger Protocol Suite. 5 | 6 | This uses a simple HTTPS request to establish a shared key 7 | between the sender and receiver that is used to 8 | authenticate ILP packets sent between them. 9 | SPSP uses the STREAM transport protocol for sending money and data over ILP. 10 | -------------------------------------------------------------------------------- /crates/interledger-spsp/src/client.rs: -------------------------------------------------------------------------------- 1 | use super::{Error, SpspResponse}; 2 | use futures::TryFutureExt; 3 | use interledger_rates::ExchangeRateStore; 4 | use interledger_service::{Account, IncomingService}; 5 | use interledger_stream::{send_money, StreamDelivery}; 6 | use reqwest::Client; 7 | use tracing::{debug, error, trace}; 8 | 9 | /// Get an ILP Address and shared secret by the receiver of this payment for this connection 10 | pub async fn query(server: &str) -> Result { 11 | let server = payment_pointer_to_url(server); 12 | trace!("Querying receiver: {}", server); 13 | 14 | let client = Client::new(); 15 | let res = client 16 | .get(&server) 17 | .header("Accept", "application/spsp4+json") 18 | .send() 19 | .map_err(|err| Error::HttpError(format!("Error querying SPSP receiver: {:?}", err))) 20 | .await?; 21 | 22 | let res = res 23 | .error_for_status() 24 | .map_err(|err| Error::HttpError(format!("Error querying SPSP receiver: {:?}", err)))?; 25 | 26 | res.json::() 27 | .map_err(|err| Error::InvalidSpspServerResponseError(format!("{:?}", err))) 28 | .await 29 | } 30 | 31 | /// Query the details of the given Payment Pointer and send a payment using the STREAM protocol. 32 | /// 33 | /// This returns the amount delivered, as reported by the receiver and in the receiver's asset's units. 34 | pub async fn pay( 35 | service: I, 36 | from_account: A, 37 | store: S, 38 | receiver: &str, 39 | source_amount: u64, 40 | slippage: f64, 41 | ) -> Result 42 | where 43 | I: IncomingService + Clone + Send + Sync + 'static, 44 | A: Account + Send + Sync + 'static, 45 | S: ExchangeRateStore + Send + Sync + 'static, 46 | { 47 | let spsp = query(receiver).await?; 48 | let shared_secret = spsp.shared_secret; 49 | let addr = spsp.destination_account; 50 | debug!("Sending SPSP payment to address: {}", addr); 51 | 52 | let receipt = send_money( 53 | service, 54 | &from_account, 55 | store, 56 | addr, 57 | shared_secret, 58 | source_amount, 59 | slippage, 60 | ) 61 | .map_err(move |err| { 62 | error!("Error sending payment: {:?}", err); 63 | Error::SendMoneyError(source_amount) 64 | }) 65 | .await?; 66 | 67 | debug!("Sent SPSP payment. StreamDelivery: {:?}", receipt); 68 | Ok(receipt) 69 | } 70 | 71 | fn payment_pointer_to_url(payment_pointer: &str) -> String { 72 | let mut url: String = if let Some(suffix) = payment_pointer.strip_prefix('$') { 73 | let prefix = "https://"; 74 | let mut url = String::with_capacity(prefix.len() + suffix.len()); 75 | url.push_str(prefix); 76 | url.push_str(suffix); 77 | url 78 | } else { 79 | payment_pointer.to_string() 80 | }; 81 | 82 | let num_slashes = url.matches('/').count(); 83 | if num_slashes == 2 { 84 | url.push_str("/.well-known/pay"); 85 | } else if num_slashes == 1 && url.ends_with('/') { 86 | url.push_str(".well-known/pay"); 87 | } 88 | trace!( 89 | "Converted payment pointer: {} to URL: {}", 90 | payment_pointer, 91 | url 92 | ); 93 | url 94 | } 95 | 96 | #[cfg(test)] 97 | mod payment_pointer { 98 | use super::*; 99 | 100 | #[test] 101 | fn converts_pointer() { 102 | let pointer = "$subdomain.domain.example"; 103 | assert_eq!( 104 | payment_pointer_to_url(pointer), 105 | "https://subdomain.domain.example/.well-known/pay" 106 | ); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /crates/interledger-spsp/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # interledger-spsp 2 | //! 3 | //! Client and server implementations of the [Simple Payment Setup Protocol (SPSP)](https://github.com/interledger/rfcs/blob/master/0009-simple-payment-setup-protocol/0009-simple-payment-setup-protocol.md). 4 | //! 5 | //! This uses a simple HTTPS request to establish a shared key between the sender and receiver that is used to 6 | //! authenticate ILP packets sent between them. SPSP uses the STREAM transport protocol for sending money and data over ILP. 7 | 8 | use interledger_packet::Address; 9 | use interledger_stream::Error as StreamError; 10 | use serde::{Deserialize, Serialize}; 11 | 12 | /// An SPSP client which can query an SPSP Server's payment pointer and initiate a STREAM payment 13 | mod client; 14 | /// An SPSP Server implementing an HTTP Service which generates ILP Addresses and Shared Secrets 15 | mod server; 16 | 17 | pub use client::{pay, query}; 18 | pub use server::SpspResponder; 19 | 20 | #[derive(Debug, thiserror::Error)] 21 | pub enum Error { 22 | #[error("Unable to query SPSP server: {0}")] 23 | HttpError(String), 24 | #[error("Got invalid SPSP response from server: {0}")] 25 | InvalidSpspServerResponseError(String), 26 | #[error("STREAM error: {0}")] 27 | StreamError(#[from] StreamError), 28 | #[error("Error sending money: {0}")] 29 | SendMoneyError(u64), 30 | #[error("Error listening: {0}")] 31 | ListenError(String), 32 | #[error("Invalid Payment Pointer: {0}")] 33 | InvalidPaymentPointerError(String), 34 | } 35 | 36 | /// An SPSP Response returned by the SPSP server 37 | #[derive(Debug, Deserialize, Serialize)] 38 | pub struct SpspResponse { 39 | /// The generated ILP Address for this SPSP connection 40 | destination_account: Address, 41 | /// Base-64 encoded shared secret between SPSP client and server 42 | /// to be consumed for the STREAM connection 43 | #[serde(with = "serde_base64")] 44 | shared_secret: Vec, 45 | } 46 | 47 | // From https://github.com/serde-rs/json/issues/360#issuecomment-330095360 48 | #[doc(hidden)] 49 | mod serde_base64 { 50 | use serde::{de, Deserialize, Deserializer, Serializer}; 51 | 52 | pub fn serialize(bytes: &[u8], serializer: S) -> Result 53 | where 54 | S: Serializer, 55 | { 56 | serializer.serialize_str(&base64::encode(bytes)) 57 | } 58 | 59 | pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> 60 | where 61 | D: Deserializer<'de>, 62 | { 63 | let s = <&str>::deserialize(deserializer)?; 64 | // TODO also accept non-URL safe 65 | base64::decode(s).map_err(de::Error::custom) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /crates/interledger-spsp/src/server.rs: -------------------------------------------------------------------------------- 1 | use super::SpspResponse; 2 | use bytes::Bytes; 3 | use hyper::{service::Service as HttpService, Body, Error, Request, Response}; 4 | use interledger_packet::Address; 5 | use interledger_stream::ConnectionGenerator; 6 | use std::error::Error as StdError; 7 | use std::{ 8 | fmt, str, 9 | task::{Context, Poll}, 10 | }; 11 | use tracing::debug; 12 | 13 | /// A Hyper::Service that responds to incoming SPSP Query requests with newly generated 14 | /// details for a STREAM connection. 15 | #[derive(Clone)] 16 | pub struct SpspResponder { 17 | ilp_address: Address, 18 | connection_generator: ConnectionGenerator, 19 | } 20 | 21 | impl SpspResponder { 22 | /// Constructs a new SPSP Responder by receiving an ILP Address and a server **secret** 23 | pub fn new(ilp_address: Address, server_secret: Bytes) -> Self { 24 | let connection_generator = ConnectionGenerator::new(server_secret); 25 | SpspResponder { 26 | ilp_address, 27 | connection_generator, 28 | } 29 | } 30 | 31 | /// Returns an HTTP Response containing the destination account 32 | /// and shared secret for this connection 33 | /// These fields are generated via [Stream's `ConnectionGenerator`](../interledger_stream/struct.ConnectionGenerator.html#method.generate_address_and_secret) 34 | pub fn generate_http_response(&self) -> Response { 35 | let (destination_account, shared_secret) = self 36 | .connection_generator 37 | .generate_address_and_secret(&self.ilp_address); 38 | debug!( 39 | "Generated address and secret for: {:?}", 40 | destination_account 41 | ); 42 | let response = SpspResponse { 43 | destination_account, 44 | shared_secret: shared_secret.to_vec(), 45 | }; 46 | 47 | Response::builder() 48 | .header("Content-Type", "application/spsp4+json") 49 | .header("Cache-Control", "max-age=60") 50 | .status(200) 51 | .body(Body::from(serde_json::to_string(&response).unwrap())) 52 | .unwrap() 53 | } 54 | } 55 | 56 | impl HttpService> for SpspResponder { 57 | type Response = Response; 58 | type Error = Error; 59 | type Future = futures::future::Ready>; 60 | 61 | fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { 62 | Ok(()).into() 63 | } 64 | 65 | fn call(&mut self, _request: Request) -> Self::Future { 66 | futures::future::ok(self.generate_http_response()) 67 | } 68 | } 69 | 70 | // copied from https://github.com/hyperium/hyper/blob/master/src/common/never.rs 71 | #[derive(Debug)] 72 | pub enum Never {} 73 | 74 | impl fmt::Display for Never { 75 | fn fmt(&self, _: &mut fmt::Formatter) -> fmt::Result { 76 | match *self {} 77 | } 78 | } 79 | 80 | impl StdError for Never { 81 | fn description(&self) -> &str { 82 | match *self {} 83 | } 84 | } 85 | 86 | #[cfg(test)] 87 | mod spsp_server_test { 88 | use super::*; 89 | use std::str::FromStr; 90 | 91 | #[tokio::test] 92 | async fn spsp_response_headers() { 93 | let addr = Address::from_str("example.receiver").unwrap(); 94 | let mut responder = SpspResponder::new(addr, Bytes::from(&[0; 32][..])); 95 | let response = responder 96 | .call( 97 | Request::builder() 98 | .method("GET") 99 | .uri("http://example.com") 100 | .header("Accept", "application/spsp4+json") 101 | .body(Body::empty()) 102 | .unwrap(), 103 | ) 104 | .await 105 | .unwrap(); 106 | assert_eq!( 107 | response.headers().get("Content-Type").unwrap(), 108 | "application/spsp4+json" 109 | ); 110 | assert_eq!( 111 | response.headers().get("Cache-Control").unwrap(), 112 | "max-age=60" 113 | ); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /crates/interledger-store/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "interledger-store" 3 | version = "1.0.0" 4 | authors = ["Evan Schwartz "] 5 | description = "Data stores for Interledger.rs" 6 | license = "Apache-2.0" 7 | edition = "2018" 8 | repository = "https://github.com/interledger-rs/interledger-rs" 9 | 10 | [features] 11 | default = [] 12 | redis = ["redis_crate"] 13 | 14 | [lib] 15 | name = "interledger_store" 16 | path = "src/lib.rs" 17 | 18 | [[test]] 19 | name = "redis_tests" 20 | path = "tests/redis/redis_tests.rs" 21 | required-features = ["redis"] 22 | 23 | [dependencies] 24 | interledger-api = { path = "../interledger-api", version = "1.0.0", default-features = false } 25 | interledger-packet = { path = "../interledger-packet", version = "1.0.0", default-features = false } 26 | interledger-btp = { path = "../interledger-btp", version = "1.0.0", default-features = false } 27 | interledger-ccp = { path = "../interledger-ccp", version = "1.0.0", default-features = false } 28 | interledger-http = { path = "../interledger-http", version = "1.0.0", default-features = false } 29 | interledger-rates = { path = "../interledger-rates", version = "1.0.0", default-features = false } 30 | interledger-router = { path = "../interledger-router", version = "1.0.0", default-features = false } 31 | interledger-service = { path = "../interledger-service", version = "1.0.0", default-features = false } 32 | interledger-service-util = { path = "../interledger-service-util", version = "1.0.0", default-features = false } 33 | interledger-settlement = { path = "../interledger-settlement", version = "1.0.0", default-features = false } 34 | interledger-stream = { path = "../interledger-stream", version = "1.0.0", default-features = false } 35 | interledger-errors = { path = "../interledger-errors", version = "1.0.0", default-features = false, features = ["redis_errors"] } 36 | 37 | bytes = { version = "1.0.1", default-features = false } 38 | futures = { version = "0.3.7", default-features = false } 39 | once_cell = { version = "1.3.1", default-features = false } 40 | tracing = { version = "0.1.12", default-features = false, features = ["log"] } 41 | parking_lot = { version = "0.10.0", default-features = false } 42 | ring = { version = "0.16.9", default-features = false } 43 | serde = { version = "1.0.101", default-features = false, features = ["derive"] } 44 | serde_json = { version = "1.0.41", default-features = false } 45 | tokio = { version = "1.9.0", default-features = false, features = ["macros", "rt"] } 46 | url = { version = "2.1.1", default-features = false, features = ["serde"] } 47 | http = { version = "0.2", default-features = false } 48 | secrecy = { version = "0.8", default-features = false, features = ["serde", "bytes"] } 49 | zeroize = { version = "1.0.0", default-features = false } 50 | num-bigint = { version = "0.2.3", default-features = false, features = ["std"]} 51 | uuid = { version = "0.8.1", default-features = false, features = ["serde"] } 52 | async-trait = { version = "0.1.22", default-features = false } 53 | thiserror = { version = "1.0.10", default-features = false } 54 | 55 | # redis feature 56 | redis_crate = { package = "redis", version = "0.21.0", optional = true, default-features = false, features = ["tokio-comp", "script"] } 57 | 58 | [dev-dependencies] 59 | rand = { version = "0.7.2", default-features = false } 60 | socket2 = "0.4.0" 61 | os_type = { version = "2.2", default-features = false } 62 | tokio-stream = { version = "0.1.7", features = ["sync"] } 63 | -------------------------------------------------------------------------------- /crates/interledger-store/README.md: -------------------------------------------------------------------------------- 1 | # Redis Store 2 | > An Interledger.rs store backed by Redis 3 | 4 | ## Recommended Configuration 5 | 6 | See [./redis-example.conf]. 7 | 8 | ## Internal Organization 9 | 10 | ### Account Details 11 | 12 | Account IDs are unsigned 64-bit integers. The `next_account_id` stores the integer that should be used for the next account added to the store. 13 | 14 | Static account details as well as balances are stored as hash maps under the keys `accounts:X`, where X is the account ID. 15 | 16 | #### Balances 17 | 18 | For each account, the store tracks a `balance` (as a signed 64-bit integer) that represents the **net** position with that account holder. 19 | A positive balance indicates the operator of the store has an outstanding liability (owes money) to that account holder. 20 | A negative balance represents an asset (the account holder owes money to the operator). 21 | 22 | The store also tracks `prepaid_amount`, which represents the amount that the account holder has pre-funded (in incoming settlements) above what they owe for ILP packets they have sent. 23 | This is tracked separately from the `balance` to avoid the ["settling back and forth forever" problem](https://forum.interledger.org/t/what-should-positive-negative-balances-represent/501/26). 24 | 25 | The `asset_code` and `asset_scale` for each of the accounts' balances can be found in the Account Details hash map. 26 | Note that this means that accounts' balances are not directly comparable (for example if account 1's `balance` is 100 and account 2's `balance` is 1000, this does not necessarily mean that we owe accountholder 2 more than accountholder 1, because these values represent completely different assets). 27 | 28 | #### Incoming / Outgoing Auth Tokens 29 | 30 | Auth tokens are encrypted in the following manner: 31 | - The encryption/decryption key is generated as `hmac_sha256(store_secret, "ilp_store_redis_encryption_key")` 32 | - Tokens are encrypted using the AES-256-GCM symmetric encryption scheme using 12-byte randomly generated nonces 33 | - The nonce is appended to the encrypted output (which includes the auth tag) and stored in the DB 34 | 35 | ### Routing Table 36 | 37 | The current routing table is stored as a hash map under the key `routes:current`. The routing table maps ILP address prefixes to the account ID of the "next hop" that the packet should be forwarded to. 38 | 39 | Statically configured routes are stored as a hash map of prefix to account ID under the key `routes:static`. These will take precedence over any routes added directly to the current routing table. 40 | 41 | ### Exchange Rates 42 | 43 | Exchange rates are stored as a hash map of currency code to rate under the key `rates:current`. 44 | 45 | ### Rate Limiting 46 | 47 | This store uses [`redis-cell`](https://github.com/brandur/redis-cell) for rate limiting. This means that the module MUST be loaded when the Redis server is started. 48 | 49 | `redis-cell` is used for both packet- and value throughput-based rate limiting. The limits are set on each account in the Account Details. 50 | -------------------------------------------------------------------------------- /crates/interledger-store/external/libredis_cell.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interledger/interledger-rs/544b6dbcd299087d16babc3377e4e014f87d68ec/crates/interledger-store/external/libredis_cell.dylib -------------------------------------------------------------------------------- /crates/interledger-store/external/libredis_cell.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interledger/interledger-rs/544b6dbcd299087d16babc3377e4e014f87d68ec/crates/interledger-store/external/libredis_cell.so -------------------------------------------------------------------------------- /crates/interledger-store/redis-example.conf: -------------------------------------------------------------------------------- 1 | # Use a unix socket because it is more performant than TCP 2 | unixsocket /tmp/redis.sock 3 | unixsocketperm 777' 4 | 5 | # Save redis data using append-only log of commands 6 | # Note this is in addition to the RDB snapshots that are on by default 7 | appendonly yes 8 | # This saves the data every second, which is faster than after 9 | # each command but means that up to 1 second of transactions 10 | # can be lost if the server crashes 11 | # Change this value to "always" to make it save transactions to 12 | # the file before applying them 13 | appendfsync everysec 14 | 15 | # Load redis-cell module, which is used for rate limiting 16 | loadmodule ./external/libredis_cell.so 17 | 18 | # Change this to set a different working directory 19 | dir ./ 20 | -------------------------------------------------------------------------------- /crates/interledger-store/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # interledger-store 2 | //! 3 | //! Backend databases for storing account details, balances, the routing table, etc. 4 | 5 | /// A module to define the primitive `Account` struct which implements `Account` related traits. 6 | pub mod account; 7 | /// Cryptographic utilities for encrypting/decrypting data as well as clearing data from memory 8 | pub mod crypto; 9 | /// A redis backend using [redis-rs](https://github.com/mitsuhiko/redis-rs/) 10 | #[cfg(feature = "redis")] 11 | pub mod redis; 12 | -------------------------------------------------------------------------------- /crates/interledger-store/src/redis/lua/account_from_username.lua: -------------------------------------------------------------------------------- 1 | local usernames_key = ARGV[1] 2 | local accounts_key = ARGV[2] 3 | local username = ARGV[3] 4 | local id_from_username = redis.call('HGET', usernames_key, username) 5 | if id_from_username then 6 | return redis.call('HGETALL', accounts_key .. ':' .. id_from_username) 7 | else 8 | return nil 9 | end 10 | -------------------------------------------------------------------------------- /crates/interledger-store/src/redis/lua/load_accounts.lua: -------------------------------------------------------------------------------- 1 | -- borrowed from https://stackoverflow.com/a/34313599 2 | local function into_dictionary(flat_map) 3 | local result = {} 4 | for i = 1, #flat_map, 2 do 5 | result[flat_map[i]] = flat_map[i + 1] 6 | end 7 | return result 8 | end 9 | 10 | 11 | local accounts_key = ARGV[1] 12 | local settlement_engines_key = ARGV[2] 13 | local settlement_engines = into_dictionary(redis.call('HGETALL', settlement_engines_key)) 14 | local accounts = {} 15 | 16 | -- TODO get rid of the two representations of account 17 | -- For some reason, the result from HGETALL returns 18 | -- a bulk value type that we can return but that 19 | -- we cannot index into with string keys. In contrast, 20 | -- the result from into_dictionary can be indexed into 21 | -- but if we try to return it, redis thinks it is a 22 | -- '(empty list or set)'. There _should_ be some better way to do 23 | -- this simple operation and a less janky way to insert the 24 | -- settlement_engine_url into the account we are going to return 25 | local account 26 | local account_dict 27 | 28 | for index, id in ipairs(ARGV) do 29 | -- skip first two arguments 30 | if index > 2 then 31 | account = redis.call('HGETALL', accounts_key .. ':' .. id) 32 | 33 | if account ~= nil then 34 | account_dict = into_dictionary(account) 35 | 36 | -- If the account does not have a settlement_engine_url specified 37 | -- but there is one configured for that currency, set the 38 | -- account to use that url 39 | if account_dict.settlement_engine_url == nil then 40 | local url = settlement_engines[account_dict.asset_code] 41 | if url ~= nil then 42 | table.insert(account, 'settlement_engine_url') 43 | table.insert(account, url) 44 | end 45 | end 46 | 47 | table.insert(accounts, account) 48 | end 49 | end 50 | end 51 | return accounts 52 | -------------------------------------------------------------------------------- /crates/interledger-store/src/redis/lua/process_fulfill.lua: -------------------------------------------------------------------------------- 1 | local accounts_key = ARGV[1] 2 | local to_account = accounts_key .. ':' .. ARGV[2] 3 | local to_amount = tonumber(ARGV[3]) 4 | 5 | local balance = redis.call('HINCRBY', to_account, 'balance', to_amount) 6 | local prepaid_amount, settle_threshold, settle_to = unpack(redis.call('HMGET', to_account, 'prepaid_amount', 'settle_threshold', 'settle_to')) 7 | 8 | -- The logic for trigerring settlement is as follows: 9 | -- 1. settle_threshold must be non-nil (if it's nil, then settlement was perhaps disabled on the account). 10 | -- 2. balance must be greater than settle_threshold (this is the core of the 'should I settle logic') 11 | -- 3. settle_threshold must be greater than settle_to (e.g., settleTo=5, settleThreshold=6) 12 | local settle_amount = 0 13 | if (settle_threshold and settle_to) and (balance >= tonumber(settle_threshold)) and (tonumber(settle_threshold) > tonumber(settle_to)) then 14 | settle_amount = balance - tonumber(settle_to) 15 | 16 | -- Update the balance _before_ sending the settlement so that we don't accidentally send 17 | -- multiple settlements for the same balance. If the settlement fails we'll roll back 18 | -- the balance change by re-adding the amount back to the balance 19 | balance = settle_to 20 | redis.call('HSET', to_account, 'balance', balance) 21 | end 22 | 23 | return {balance + prepaid_amount, settle_amount} 24 | -------------------------------------------------------------------------------- /crates/interledger-store/src/redis/lua/process_incoming_settlement.lua: -------------------------------------------------------------------------------- 1 | local accounts_key = ARGV[1] 2 | local account = accounts_key .. ':' .. ARGV[2] 3 | local amount = tonumber(ARGV[3]) 4 | local idempotency_key = ARGV[4] 5 | 6 | local balance, prepaid_amount = unpack(redis.call('HMGET', account, 'balance', 'prepaid_amount')) 7 | 8 | -- If idempotency key has been used, then do not perform any operations 9 | if redis.call('EXISTS', idempotency_key) == 1 then 10 | return balance + prepaid_amount 11 | end 12 | 13 | -- Otherwise, set it to true and make it expire after 24h (86400 sec) 14 | redis.call('SET', idempotency_key, 'true', 'EX', 86400) 15 | 16 | -- Credit the incoming settlement to the balance and/or prepaid amount, 17 | -- depending on whether that account currently owes money or not 18 | if tonumber(balance) >= 0 then 19 | prepaid_amount = redis.call('HINCRBY', account, 'prepaid_amount', amount) 20 | elseif math.abs(balance) >= amount then 21 | balance = redis.call('HINCRBY', account, 'balance', amount) 22 | else 23 | prepaid_amount = redis.call('HINCRBY', account, 'prepaid_amount', amount + balance) 24 | balance = 0 25 | redis.call('HSET', account, 'balance', 0) 26 | end 27 | 28 | return balance + prepaid_amount -------------------------------------------------------------------------------- /crates/interledger-store/src/redis/lua/process_prepare.lua: -------------------------------------------------------------------------------- 1 | local accounts_key = ARGV[1] 2 | local from_id = ARGV[2] 3 | local from_account = accounts_key .. ':' .. from_id 4 | local from_amount = tonumber(ARGV[3]) 5 | local min_balance, balance, prepaid_amount = unpack(redis.call('HMGET', from_account, 'min_balance', 'balance', 'prepaid_amount')) 6 | balance = tonumber(balance) 7 | prepaid_amount = tonumber(prepaid_amount) 8 | 9 | -- Check that the prepare wouldn't go under the account's minimum balance 10 | if min_balance then 11 | min_balance = tonumber(min_balance) 12 | if balance + prepaid_amount - from_amount < min_balance then 13 | error('Incoming prepare of ' .. from_amount .. ' would bring account ' .. from_id .. ' under its minimum balance. Current balance: ' .. balance .. ', min balance: ' .. min_balance) 14 | end 15 | end 16 | 17 | -- Deduct the from_amount from the prepaid_amount and/or the balance 18 | if prepaid_amount >= from_amount then 19 | prepaid_amount = redis.call('HINCRBY', from_account, 'prepaid_amount', 0 - from_amount) 20 | elseif prepaid_amount > 0 then 21 | local sub_from_balance = from_amount - prepaid_amount 22 | prepaid_amount = 0 23 | redis.call('HSET', from_account, 'prepaid_amount', 0) 24 | balance = redis.call('HINCRBY', from_account, 'balance', 0 - sub_from_balance) 25 | else 26 | balance = redis.call('HINCRBY', from_account, 'balance', 0 - from_amount) 27 | end 28 | 29 | return balance + prepaid_amount -------------------------------------------------------------------------------- /crates/interledger-store/src/redis/lua/process_reject.lua: -------------------------------------------------------------------------------- 1 | local accounts_key = ARGV[1] 2 | local from_account = accounts_key .. ':' .. ARGV[2] 3 | local from_amount = tonumber(ARGV[3]) 4 | 5 | local prepaid_amount = redis.call('HGET', from_account, 'prepaid_amount') 6 | local balance = redis.call('HINCRBY', from_account, 'balance', from_amount) 7 | return balance + prepaid_amount 8 | -------------------------------------------------------------------------------- /crates/interledger-store/src/redis/lua/process_settle.lua: -------------------------------------------------------------------------------- 1 | -- almost the same as process_fulfill.lua which is used to settle when balance is over the settlement threshold: 2 | -- returns similarly the balance and the amount to settle, but is called with only the account id as the only argument. 3 | -- 4 | -- upon completion the `balance` is at the level of `settle_to` 5 | local accounts_key = ARGV[1] 6 | local to_account = accounts_key .. ':' .. ARGV[2] 7 | local balance, prepaid_amount, settle_threshold, settle_to = unpack(redis.call('HMGET', to_account, 'balance', 'prepaid_amount', 'settle_threshold', 'settle_to')) 8 | local settle_amount = 0 9 | 10 | if (settle_threshold and settle_to) and (tonumber(settle_threshold) > tonumber(settle_to)) and tonumber(balance) >= tonumber(settle_to) then 11 | settle_amount = tonumber(balance) - tonumber(settle_to) 12 | balance = settle_to 13 | redis.call('HSET', to_account, 'balance', balance) 14 | end 15 | 16 | return {balance + prepaid_amount, settle_amount} 17 | -------------------------------------------------------------------------------- /crates/interledger-store/src/redis/lua/refund_settlement.lua: -------------------------------------------------------------------------------- 1 | local accounts_key = ARGV[1] 2 | local account = accounts_key .. ':' .. ARGV[2] 3 | local settle_amount = tonumber(ARGV[3]) 4 | 5 | local balance = redis.call('HINCRBY', account, 'balance', settle_amount) 6 | return balance -------------------------------------------------------------------------------- /crates/interledger-store/src/redis/reconnect.rs: -------------------------------------------------------------------------------- 1 | use futures::future::{FutureExt, TryFutureExt}; 2 | use parking_lot::RwLock; 3 | use redis_crate::{ 4 | aio::{ConnectionLike, MultiplexedConnection}, 5 | Client, Cmd, ConnectionInfo, Pipeline, RedisError, RedisFuture, Value, 6 | }; 7 | use std::sync::Arc; 8 | use tracing::{debug, error}; 9 | 10 | type Result = std::result::Result; 11 | 12 | /// Wrapper around a Redis MultiplexedConnection that automatically 13 | /// attempts to reconnect to the DB if the connection is dropped 14 | #[derive(Clone)] 15 | pub struct RedisReconnect { 16 | pub(crate) redis_info: Arc, 17 | pub(crate) conn: Arc>, 18 | } 19 | 20 | async fn get_shared_connection(redis_info: Arc) -> Result { 21 | let client = Client::open((*redis_info).clone())?; 22 | client 23 | .get_multiplexed_tokio_connection() 24 | .map_err(|e| { 25 | error!("Error connecting to Redis: {:?}", e); 26 | e 27 | }) 28 | .await 29 | } 30 | 31 | impl RedisReconnect { 32 | /// Connects to redis with the provided [`ConnectionInfo`](redis_crate::ConnectionInfo) 33 | pub async fn connect(redis_info: ConnectionInfo) -> Result { 34 | let redis_info = Arc::new(redis_info); 35 | let conn = get_shared_connection(redis_info.clone()).await?; 36 | Ok(RedisReconnect { 37 | conn: Arc::new(RwLock::new(conn)), 38 | redis_info, 39 | }) 40 | } 41 | 42 | /// Reconnects to redis 43 | pub async fn reconnect(&self) -> Result<()> { 44 | let shared_connection = get_shared_connection(self.redis_info.clone()).await?; 45 | (*self.conn.write()) = shared_connection; 46 | debug!("Reconnected to Redis"); 47 | Ok(()) 48 | } 49 | 50 | fn get_shared_connection(&self) -> MultiplexedConnection { 51 | self.conn.read().clone() 52 | } 53 | } 54 | 55 | impl ConnectionLike for RedisReconnect { 56 | fn get_db(&self) -> i64 { 57 | self.conn.read().get_db() 58 | } 59 | 60 | fn req_packed_command<'a>(&'a mut self, cmd: &'a Cmd) -> RedisFuture<'a, Value> { 61 | // This is how it is implemented in the redis-rs repository 62 | (async move { 63 | let mut connection = self.get_shared_connection(); 64 | match connection.req_packed_command(cmd).await { 65 | Ok(res) => Ok(res), 66 | Err(error) => { 67 | if error.is_connection_dropped() { 68 | debug!("Redis connection was dropped, attempting to reconnect"); 69 | // FIXME: this conceals potential reconnect errors 70 | let _ = self.reconnect().await; 71 | } 72 | Err(error) 73 | } 74 | } 75 | }) 76 | .boxed() 77 | } 78 | 79 | fn req_packed_commands<'a>( 80 | &'a mut self, 81 | cmd: &'a Pipeline, 82 | offset: usize, 83 | count: usize, 84 | ) -> RedisFuture<'a, Vec> { 85 | // This is how it is implemented in the redis-rs repository 86 | (async move { 87 | let mut connection = self.get_shared_connection(); 88 | match connection.req_packed_commands(cmd, offset, count).await { 89 | Ok(res) => Ok(res), 90 | Err(error) => { 91 | if error.is_connection_dropped() { 92 | debug!("Redis connection was dropped, attempting to reconnect"); 93 | // FIXME: this conceals potential reconnect errors 94 | let _ = self.reconnect().await; 95 | } 96 | Err(error) 97 | } 98 | } 99 | }) 100 | .boxed() 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /crates/interledger-store/tests/redis/btp_test.rs: -------------------------------------------------------------------------------- 1 | use super::fixtures::*; 2 | 3 | use super::store_helpers::*; 4 | 5 | use interledger_api::NodeStore; 6 | use interledger_btp::{BtpAccount, BtpStore}; 7 | use interledger_http::HttpAccount; 8 | use interledger_packet::Address; 9 | use interledger_service::{Account as AccountTrait, Username}; 10 | 11 | use secrecy::{ExposeSecret, SecretString}; 12 | use std::str::FromStr; 13 | 14 | #[tokio::test] 15 | async fn gets_account_from_btp_auth() { 16 | let (store, _context, _) = test_store().await.unwrap(); 17 | let account = store 18 | .get_account_from_btp_auth(&Username::from_str("bob").unwrap(), "other_btp_token") 19 | .await 20 | .unwrap(); 21 | assert_eq!( 22 | *account.ilp_address(), 23 | Address::from_str("example.alice.user1.bob").unwrap() 24 | ); 25 | 26 | assert_eq!( 27 | account.get_http_auth_token().unwrap().expose_secret(), 28 | "outgoing_auth_token", 29 | ); 30 | assert_eq!( 31 | &account.get_ilp_over_btp_outgoing_token().unwrap(), 32 | b"btp_token" 33 | ); 34 | } 35 | 36 | #[tokio::test] 37 | async fn errors_on_unknown_user() { 38 | let (store, _context, _) = test_store().await.unwrap(); 39 | let err = store 40 | .get_account_from_btp_auth(&Username::from_str("asdf").unwrap(), "other_btp_token") 41 | .await 42 | .unwrap_err(); 43 | assert_eq!(err.to_string(), "account `asdf` was not found"); 44 | } 45 | 46 | #[tokio::test] 47 | async fn errors_on_wrong_btp_token() { 48 | let (store, _context, _) = test_store().await.unwrap(); 49 | let err = store 50 | .get_account_from_btp_auth(&Username::from_str("bob").unwrap(), "wrong_token") 51 | .await 52 | .unwrap_err(); 53 | assert_eq!( 54 | err.to_string(), 55 | "account `bob` is not authorized for this action" 56 | ); 57 | } 58 | 59 | #[tokio::test] 60 | async fn duplicate_btp_incoming_auth_works() { 61 | let mut charlie = ACCOUNT_DETAILS_2.clone(); 62 | charlie.ilp_over_btp_incoming_token = Some(SecretString::new("btp_token".to_string())); 63 | let (store, _context, accs) = test_store().await.unwrap(); 64 | let alice = accs[0].clone(); 65 | let alice_id = alice.id(); 66 | let charlie = store.insert_account(charlie).await.unwrap(); 67 | let charlie_id = charlie.id(); 68 | assert_ne!(alice_id, charlie_id); 69 | let result = futures::future::join_all(vec![ 70 | store.get_account_from_btp_auth(&Username::from_str("alice").unwrap(), "btp_token"), 71 | store.get_account_from_btp_auth(&Username::from_str("charlie").unwrap(), "btp_token"), 72 | ]) 73 | .await; 74 | let accs: Vec<_> = result.into_iter().map(|r| r.unwrap()).collect(); 75 | assert_ne!(accs[0].id(), accs[1].id()); 76 | assert_eq!(accs[0].id(), alice_id); 77 | assert_eq!(accs[1].id(), charlie_id); 78 | } 79 | -------------------------------------------------------------------------------- /crates/interledger-store/tests/redis/http_test.rs: -------------------------------------------------------------------------------- 1 | use super::fixtures::*; 2 | use super::store_helpers::*; 3 | 4 | use interledger_api::NodeStore; 5 | use interledger_btp::BtpAccount; 6 | use interledger_http::{HttpAccount, HttpStore}; 7 | use interledger_packet::Address; 8 | use interledger_service::{Account, Username}; 9 | use secrecy::{ExposeSecret, SecretString}; 10 | use std::str::FromStr; 11 | 12 | #[tokio::test] 13 | async fn gets_account_from_http_bearer_token() { 14 | let (store, _context, _) = test_store().await.unwrap(); 15 | let account = store 16 | .get_account_from_http_auth(&Username::from_str("alice").unwrap(), "incoming_auth_token") 17 | .await 18 | .unwrap(); 19 | assert_eq!( 20 | *account.ilp_address(), 21 | Address::from_str("example.alice").unwrap() 22 | ); 23 | assert_eq!( 24 | account.get_http_auth_token().unwrap().expose_secret(), 25 | "outgoing_auth_token", 26 | ); 27 | assert_eq!( 28 | &account.get_ilp_over_btp_outgoing_token().unwrap(), 29 | b"btp_token", 30 | ); 31 | } 32 | 33 | #[tokio::test] 34 | async fn errors_on_wrong_http_token() { 35 | let (store, _context, _) = test_store().await.unwrap(); 36 | // wrong password 37 | let err = store 38 | .get_account_from_http_auth(&Username::from_str("alice").unwrap(), "unknown_token") 39 | .await 40 | .unwrap_err(); 41 | assert_eq!( 42 | err.to_string(), 43 | "account `alice` is not authorized for this action" 44 | ); 45 | } 46 | 47 | #[tokio::test] 48 | async fn errors_on_unknown_user() { 49 | let (store, _context, _) = test_store().await.unwrap(); 50 | // wrong user 51 | let err = store 52 | .get_account_from_http_auth(&Username::from_str("asdf").unwrap(), "incoming_auth_token") 53 | .await 54 | .unwrap_err(); 55 | assert_eq!(err.to_string(), "account `asdf` was not found"); 56 | } 57 | 58 | #[tokio::test] 59 | async fn duplicate_http_incoming_auth_works() { 60 | let mut duplicate = ACCOUNT_DETAILS_2.clone(); 61 | duplicate.ilp_over_http_incoming_token = 62 | Some(SecretString::new("incoming_auth_token".to_string())); 63 | let (store, _context, accs) = test_store().await.unwrap(); 64 | let original = accs[0].clone(); 65 | let original_id = original.id(); 66 | let duplicate = store.insert_account(duplicate).await.unwrap(); 67 | let duplicate_id = duplicate.id(); 68 | assert_ne!(original_id, duplicate_id); 69 | let result = futures::future::join_all(vec![ 70 | store.get_account_from_http_auth( 71 | &Username::from_str("alice").unwrap(), 72 | "incoming_auth_token", 73 | ), 74 | store.get_account_from_http_auth( 75 | &Username::from_str("charlie").unwrap(), 76 | "incoming_auth_token", 77 | ), 78 | ]) 79 | .await; 80 | let accs: Vec<_> = result.into_iter().map(|r| r.unwrap()).collect(); 81 | // Alice and Charlie had the same auth token, but they had a 82 | // different username/account id, so no problem. 83 | assert_ne!(accs[0].id(), accs[1].id()); 84 | assert_eq!(accs[0].id(), original_id); 85 | assert_eq!(accs[1].id(), duplicate_id); 86 | } 87 | -------------------------------------------------------------------------------- /crates/interledger-store/tests/redis/notifications.rs: -------------------------------------------------------------------------------- 1 | use super::{fixtures::*, redis_helpers::*}; 2 | use futures::{FutureExt, StreamExt}; 3 | use interledger_api::NodeStore; 4 | use interledger_packet::Address; 5 | use interledger_service::Account as AccountTrait; 6 | use interledger_store::redis::RedisStoreBuilder; 7 | use interledger_stream::{PaymentNotification, StreamNotificationsStore}; 8 | use std::str::FromStr; 9 | use tokio_stream::wrappers::BroadcastStream; 10 | 11 | #[tokio::test] 12 | async fn notifications_on_multitenant_config() { 13 | let context = TestContext::new(); 14 | 15 | let first = RedisStoreBuilder::new(context.get_client_connection_info(), [0; 32]) 16 | .with_db_prefix("first") 17 | .connect() 18 | .await 19 | .unwrap(); 20 | 21 | let second = RedisStoreBuilder::new(context.get_client_connection_info(), [1; 32]) 22 | .with_db_prefix("second") 23 | .connect() 24 | .await 25 | .unwrap(); 26 | 27 | let firstuser = first 28 | .insert_account(ACCOUNT_DETAILS_0.clone()) 29 | .await 30 | .unwrap(); 31 | 32 | let seconduser = second 33 | .insert_account({ 34 | let mut details = ACCOUNT_DETAILS_1.clone(); 35 | details.ilp_address = Some(Address::from_str("example.charlie").unwrap()); 36 | details 37 | }) 38 | .await 39 | .unwrap(); 40 | 41 | let first_pmt = PaymentNotification { 42 | from_username: seconduser.username().to_owned(), 43 | to_username: firstuser.username().to_owned(), 44 | destination: firstuser.ilp_address().to_owned(), 45 | amount: 1, 46 | timestamp: String::from("2021-04-04T12:11:11.987+00:00"), 47 | sequence: 2, 48 | connection_closed: false, 49 | }; 50 | 51 | let second_pmt = PaymentNotification { 52 | from_username: firstuser.username().to_owned(), 53 | to_username: seconduser.username().to_owned(), 54 | destination: seconduser.ilp_address().to_owned(), 55 | amount: 1, 56 | timestamp: String::from("2021-04-04T12:11:10.987+00:00"), 57 | sequence: 1, 58 | connection_closed: false, 59 | }; 60 | 61 | // do the test in a loop since sometimes the psubscribe functionality just isn't ready 62 | for _ in 0..10 { 63 | // we recreate these on the start of every attempt in order to get a fresh start; the 64 | // channel will not forward us messages which have come before. 65 | let mut rx1 = BroadcastStream::new(first.all_payment_subscription()); 66 | let mut rx2 = BroadcastStream::new(second.all_payment_subscription()); 67 | 68 | first.publish_payment_notification(first_pmt.clone()); 69 | second.publish_payment_notification(second_pmt.clone()); 70 | 71 | // these used to only log before #700: 72 | // 73 | // WARN interledger_store::redis: Ignoring unexpected message from Redis subscription for channel: first:stream_notifications:... 74 | // WARN interledger_store::redis: Ignoring unexpected message from Redis subscription for channel: second:stream_notifications:... 75 | // 76 | // after fixing this, there will still be: 77 | // 78 | // TRACE interledger_store::redis: Ignoring message for account ... because there were no open subscriptions 79 | // TRACE interledger_store::redis: Ignoring message for account ... because there were no open subscriptions 80 | // 81 | // even though the subscription to all exists. this tests uses the all_payment_subscription() 82 | // and that should be ok, since the trigger still comes through PSUBSCRIBE. 83 | // 84 | let deadline = std::time::Duration::from_millis(1000); 85 | let read_both = futures::future::join(rx1.next(), rx2.next()); 86 | 87 | let (msg1, msg2) = match tokio::time::timeout(deadline, read_both).await { 88 | Ok(messages) => messages, 89 | Err(tokio::time::error::Elapsed { .. }) => { 90 | // failure is most likely because of redis, or publishing to it 91 | // see issue #711. 92 | continue; 93 | } 94 | }; 95 | assert_eq!(msg1.unwrap().expect("cannot lag yet").sequence, 2); 96 | assert_eq!(msg2.unwrap().expect("cannot lag yet").sequence, 1); 97 | 98 | let (msg1, msg2) = (rx1.next().now_or_never(), rx2.next().now_or_never()); 99 | assert!(msg1.is_none(), "{:?}", msg1); 100 | assert!(msg2.is_none(), "{:?}", msg2); 101 | return; 102 | } 103 | 104 | unreachable!("did not complete with retries"); 105 | } 106 | -------------------------------------------------------------------------------- /crates/interledger-store/tests/redis/rate_limiting_test.rs: -------------------------------------------------------------------------------- 1 | use super::{fixtures::*, store_helpers::*}; 2 | use futures::future::join_all; 3 | use interledger_service::AddressStore; 4 | use interledger_service_util::{RateLimitError, RateLimitStore}; 5 | use interledger_store::account::Account; 6 | use uuid::Uuid; 7 | 8 | #[tokio::test] 9 | async fn rate_limits_number_of_packets() { 10 | let (store, _context, _) = test_store().await.unwrap(); 11 | let account = Account::try_from( 12 | Uuid::new_v4(), 13 | ACCOUNT_DETAILS_0.clone(), 14 | store.get_ilp_address(), 15 | ) 16 | .unwrap(); 17 | let results = join_all(vec![ 18 | store.clone().apply_rate_limits(account.clone(), 10), 19 | store.clone().apply_rate_limits(account.clone(), 10), 20 | store.clone().apply_rate_limits(account.clone(), 10), 21 | ]) 22 | .await; 23 | // The first 2 calls succeed, while the 3rd one hits the rate limit error 24 | // because the account is only allowed 2 packets per minute 25 | assert_eq!( 26 | results, 27 | vec![Ok(()), Ok(()), Err(RateLimitError::PacketLimitExceeded)] 28 | ); 29 | } 30 | 31 | #[tokio::test] 32 | async fn limits_amount_throughput() { 33 | let (store, _context, _) = test_store().await.unwrap(); 34 | let account = Account::try_from( 35 | Uuid::new_v4(), 36 | ACCOUNT_DETAILS_1.clone(), 37 | store.get_ilp_address(), 38 | ) 39 | .unwrap(); 40 | let results = join_all(vec![ 41 | store.clone().apply_rate_limits(account.clone(), 500), 42 | store.clone().apply_rate_limits(account.clone(), 500), 43 | store.clone().apply_rate_limits(account.clone(), 1), 44 | ]) 45 | .await; 46 | // The first 2 calls succeed, while the 3rd one hits the rate limit error 47 | // because the account is only allowed 1000 units of currency per minute 48 | assert_eq!( 49 | results, 50 | vec![Ok(()), Ok(()), Err(RateLimitError::ThroughputLimitExceeded)] 51 | ); 52 | } 53 | 54 | #[tokio::test] 55 | async fn refunds_throughput_limit_for_rejected_packets() { 56 | let (store, _context, _) = test_store().await.unwrap(); 57 | let account = Account::try_from( 58 | Uuid::new_v4(), 59 | ACCOUNT_DETAILS_1.clone(), 60 | store.get_ilp_address(), 61 | ) 62 | .unwrap(); 63 | 64 | join_all(vec![ 65 | store.clone().apply_rate_limits(account.clone(), 500), 66 | store.clone().apply_rate_limits(account.clone(), 500), 67 | ]) 68 | .await; 69 | 70 | // We refund the throughput limit once, meaning we can do 1 more call before 71 | // the error 72 | store 73 | .refund_throughput_limit(account.clone(), 500) 74 | .await 75 | .unwrap(); 76 | store.apply_rate_limits(account.clone(), 500).await.unwrap(); 77 | 78 | let result = store.apply_rate_limits(account.clone(), 1).await; 79 | assert_eq!(result.unwrap_err(), RateLimitError::ThroughputLimitExceeded); 80 | } 81 | -------------------------------------------------------------------------------- /crates/interledger-store/tests/redis/rates_test.rs: -------------------------------------------------------------------------------- 1 | use super::store_helpers::*; 2 | 3 | use interledger_rates::ExchangeRateStore; 4 | 5 | #[tokio::test] 6 | async fn set_rates() { 7 | let (store, _context, _) = test_store().await.unwrap(); 8 | let rates = store.get_exchange_rates(&["ABC", "XYZ"]); 9 | assert!(rates.is_err()); 10 | store 11 | .set_exchange_rates( 12 | [("ABC".to_string(), 500.0), ("XYZ".to_string(), 0.005)] 13 | .iter() 14 | .cloned() 15 | .collect(), 16 | ) 17 | .unwrap(); 18 | 19 | let rates = store.get_exchange_rates(&["XYZ", "ABC"]).unwrap(); 20 | assert_eq!(rates[0].to_string(), "0.005"); 21 | assert_eq!(rates[1].to_string(), "500"); 22 | } 23 | -------------------------------------------------------------------------------- /crates/interledger-stream/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "interledger-stream" 3 | version = "1.0.0" 4 | authors = ["Evan Schwartz "] 5 | description = "Client and server implementations of the STREAM transport protocol" 6 | license = "Apache-2.0" 7 | edition = "2018" 8 | repository = "https://github.com/interledger-rs/interledger-rs" 9 | 10 | # Optional feature to log connection statistics using a CSV file 11 | [features] 12 | strict = ["interledger-packet/strict"] 13 | # Only applicable for roundtripping in fuzzing 14 | # Deliberate error for valid replacement of data, such as `saturating_read_var_uint`. 15 | roundtrip-only = ["strict"] 16 | 17 | [dependencies] 18 | interledger-packet = { path = "../interledger-packet", version = "1.0.0", default-features = false, features = ["serde"] } 19 | interledger-rates = { path = "../interledger-rates", version = "1.0.0", default-features = false } 20 | interledger-service = { path = "../interledger-service", version = "1.0.0", default-features = false } 21 | 22 | base64 = { version = "0.13.0", default-features = false, features = ["std"] } 23 | bytes = { version = "1.0.1" } 24 | chrono = { version = "0.4.20", default-features = false, features = ["clock"] } 25 | futures = { version = "0.3.7", default-features = false, features = ["std"] } 26 | tracing = { version = "0.1.12", default-features = false, features = ["log"] } 27 | num = { version = "0.2.1" } 28 | ring = { version = "0.16.9", default-features = false } 29 | serde = { version = "1.0.101", default-features = false } 30 | tokio = { version = "1.9.0", default-features = false, features = ["rt", "time", "macros"] } 31 | uuid = { version = "0.8.1", default-features = false, features = ["v4"] } 32 | async-trait = { version = "0.1.22", default-features = false } 33 | pin-project = { version = "0.4.7", default-features = false } 34 | thiserror = { version = "1.0.10", default-features = false } 35 | 36 | [dev-dependencies] 37 | interledger-errors = { path = "../interledger-errors", version = "1.0.0", default-features = false } 38 | interledger-router = { path = "../interledger-router", version = "1.0.0", default-features = false } 39 | interledger-service-util = { path = "../interledger-service-util", version = "1.0.0", default-features = false } 40 | hex-literal = "0.3" 41 | parking_lot = { version = "0.10.0", default-features = false } 42 | 43 | once_cell = { version = "1.3.1", default-features = false } 44 | -------------------------------------------------------------------------------- /crates/interledger-stream/README.md: -------------------------------------------------------------------------------- 1 | # interledger-stream 2 | 3 | Client and server implementations of [STREAM](https://interledger.org/rfcs/0029-stream/), 4 | a transport-layer protocol of the Interledger Protocol Suite. 5 | 6 | STREAM is responsible for splitting larger payments and 7 | messages into smaller chunks of money and data, and sending them over ILP. 8 | -------------------------------------------------------------------------------- /crates/interledger-stream/fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | target 3 | corpus 4 | artifacts 5 | Cargo.lock 6 | -------------------------------------------------------------------------------- /crates/interledger-stream/fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | 2 | [package] 3 | name = "interledger-stream-fuzz" 4 | version = "0.0.0" 5 | authors = ["Automatically generated"] 6 | publish = false 7 | edition = "2018" 8 | 9 | [package.metadata] 10 | cargo-fuzz = true 11 | 12 | [dependencies] 13 | libfuzzer-sys = "0.4" 14 | 15 | [dependencies.interledger-stream] 16 | path = ".." 17 | # roundtrip enables strict features as well 18 | features = ["roundtrip-only"] 19 | 20 | # Prevent this from interfering with workspaces 21 | [workspace] 22 | members = ["."] 23 | 24 | [[bin]] 25 | name = "stream_packet" 26 | path = "fuzz_targets/stream_packet.rs" 27 | test = false 28 | doc = false 29 | -------------------------------------------------------------------------------- /crates/interledger-stream/fuzz/fuzz_targets/stream_packet.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | 4 | fuzz_target!(|data: &[u8]| { 5 | interledger_stream::fuzz_decrypted_stream_packet(data); 6 | }); 7 | -------------------------------------------------------------------------------- /crates/interledger-stream/src/error.rs: -------------------------------------------------------------------------------- 1 | use interledger_packet::{ 2 | AddressError, ErrorCode, OerError, PacketTypeError as IlpPacketTypeError, 3 | }; 4 | /// Stream Errors 5 | use std::str::Utf8Error; 6 | 7 | #[derive(Debug, thiserror::Error)] 8 | pub enum Error { 9 | #[error("Terminating payment since too many packets are rejected ({0} packets fulfilled, {1} packets rejected)")] 10 | PaymentFailFast(u64, u64), 11 | #[error("Packet was rejected with ErrorCode: {0} {1:?}")] 12 | UnexpectedRejection(ErrorCode, String), 13 | #[error( 14 | "Error maximum time exceeded: Time since last fulfill exceeded the maximum time limit" 15 | )] 16 | Timeout, 17 | } 18 | 19 | #[derive(Debug, thiserror::Error)] 20 | pub enum StreamPacketError { 21 | #[error("Unable to decrypt packet")] 22 | FailedToDecrypt, 23 | #[error("Unsupported STREAM version: {0}")] 24 | UnsupportedVersion(u8), 25 | #[error("Invalid Packet: Incorrect number of frames or unable to parse all frames")] 26 | NotEnoughValidFrames, 27 | #[error("Trailing bytes error: Inner")] 28 | TrailingInnerBytes, 29 | #[error("Invalid Packet: {0}")] 30 | Oer(#[from] OerError), 31 | #[error("Ilp PacketType Error: {0}")] 32 | IlpPacketType(#[from] IlpPacketTypeError), 33 | #[error("Address Error: {0}")] 34 | Address(#[from] AddressError), 35 | #[error("UTF-8 Error: {0}")] 36 | Utf8Err(#[from] Utf8Error), 37 | #[cfg(feature = "roundtrip-only")] 38 | #[cfg_attr( 39 | feature = "roundtrip-only", 40 | error("Roundtrip only: Error expected for roundtrip fuzzing") 41 | )] 42 | NonRoundtrippableSaturatingAmount, 43 | } 44 | -------------------------------------------------------------------------------- /crates/interledger/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "interledger" 3 | version = "1.0.0" 4 | authors = ["Evan Schwartz "] 5 | description = "Interledger client library" 6 | license = "Apache-2.0" 7 | edition = "2018" 8 | repository = "https://github.com/interledger-rs/interledger-rs" 9 | 10 | [features] 11 | default = ["node"] 12 | node = [ 13 | "api", 14 | "btp", 15 | "ccp", 16 | "http", 17 | "ildcp", 18 | "rates", 19 | "router", 20 | "service-util", 21 | "settlement", 22 | "spsp", 23 | "store", 24 | "stream", 25 | "trace", 26 | ] 27 | api = ["interledger-api"] 28 | btp = ["interledger-btp"] 29 | ccp = ["interledger-ccp"] 30 | http = ["interledger-http"] 31 | ildcp = ["interledger-ildcp"] 32 | rates = ["interledger-rates"] 33 | router = ["interledger-router"] 34 | service-util = ["interledger-service-util"] 35 | settlement = ["interledger-settlement" ] 36 | spsp = ["interledger-spsp", "stream"] 37 | store = ["interledger-store"] 38 | stream = ["interledger-stream", "ildcp"] 39 | trace = ["interledger-service/trace"] 40 | redis = ["interledger-store/redis"] 41 | 42 | [dependencies] 43 | interledger-api = { path = "../interledger-api", version = "1.0.0", optional = true, default-features = false } 44 | interledger-btp = { path = "../interledger-btp", version = "1.0.0", optional = true, default-features = false } 45 | interledger-ccp = { path = "../interledger-ccp", version = "1.0.0", optional = true, default-features = false } 46 | interledger-http = { path = "../interledger-http", version = "1.0.0", optional = true, default-features = false } 47 | interledger-ildcp = { path = "../interledger-ildcp", version = "1.0.0", optional = true, default-features = false } 48 | interledger-packet = { path = "../interledger-packet", version = "1.0.0", default-features = false } 49 | interledger-errors = { path = "../interledger-errors", version = "1.0.0", default-features = false } 50 | interledger-rates = { path = "../interledger-rates", version = "1.0.0", optional = true, default-features = false } 51 | interledger-router = { path = "../interledger-router", version = "1.0.0", optional = true, default-features = false } 52 | interledger-service = { path = "../interledger-service", version = "1.0.0", default-features = false } 53 | interledger-service-util = { path = "../interledger-service-util", version = "1.0.0", optional = true, default-features = false } 54 | interledger-settlement = { path = "../interledger-settlement", version = "1.0.0", optional = true, default-features = false } 55 | interledger-spsp = { path = "../interledger-spsp", version = "1.0.0", optional = true, default-features = false } 56 | interledger-stream = { path = "../interledger-stream", version = "1.0.0", optional = true, default-features = false } 57 | interledger-store = { path = "../interledger-store", version = "1.0.0", optional = true, default-features = false, features = ["redis"] } 58 | 59 | [badges] 60 | circle-ci = { repository = "interledger-rs/interledger-rs" } 61 | codecov = { repository = "interledger-rs/interledger-rs" } 62 | -------------------------------------------------------------------------------- /crates/interledger/README.md: -------------------------------------------------------------------------------- 1 | # Interledger.rs 2 | 3 | This crate bundles all libraries that comprise the Rust implementation of [the Interledger Protocol Suite](https://interledger.org/rfcs/0001-interledger-architecture/). 4 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build Interledger node into standalone binary 2 | FROM clux/muslrust:stable as rust 3 | 4 | WORKDIR /usr/src/interledger-rs 5 | COPY ./Cargo.toml /usr/src/interledger-rs/Cargo.toml 6 | COPY ./Cargo.lock /usr/src/interledger-rs/Cargo.lock 7 | COPY ./crates /usr/src/interledger-rs/crates 8 | COPY ./.git /usr/src/interledger-rs/ 9 | 10 | # TODO: investigate using a method like https://whitfin.io/speeding-up-rust-docker-builds/ 11 | # to ensure that the dependencies are cached so the build doesn't take as long 12 | # RUN cargo build --all-features --package ilp-node --package ilp-cli 13 | RUN cargo build --release --all-features --package ilp-node --package ilp-cli 14 | 15 | WORKDIR /usr/src/ 16 | RUN git clone https://github.com/interledger-rs/settlement-engines.git 17 | WORKDIR /usr/src/settlement-engines 18 | RUN cargo build --release --all-features --package ilp-settlement-ethereum 19 | 20 | FROM node:12-alpine 21 | 22 | # Expose ports for HTTP server 23 | EXPOSE 7770 24 | 25 | # To save the node's data across runs, mount a volume called "/data". 26 | # You can do this by adding the option `-v data-volume-name:/data` 27 | # when calling `docker run`. 28 | 29 | VOLUME [ "/data" ] 30 | 31 | # Install SSL certs and Redis 32 | RUN apk --no-cache add \ 33 | ca-certificates \ 34 | redis 35 | 36 | # Copy Interledger binary 37 | COPY --from=rust \ 38 | /usr/src/interledger-rs/target/x86_64-unknown-linux-musl/release/ilp-node \ 39 | /usr/local/bin/ilp-node 40 | COPY --from=rust \ 41 | /usr/src/interledger-rs/target/x86_64-unknown-linux-musl/release/ilp-cli \ 42 | /usr/local/bin/ilp-cli 43 | COPY --from=rust \ 44 | /usr/src/settlement-engines/target/x86_64-unknown-linux-musl/release/ilp-settlement-ethereum \ 45 | /usr/local/bin/ilp-settlement-ethereum 46 | # COPY --from=rust \ 47 | # /usr/src/interledger-rs/target/x86_64-unknown-linux-musl/debug/ilp-node \ 48 | # /usr/local/bin/ilp-node 49 | # COPY --from=rust \ 50 | # /usr/src/interledger-rs/target/x86_64-unknown-linux-musl/debug/ilp-cli \ 51 | # /usr/local/bin/ilp-cli 52 | # COPY --from=rust \ 53 | # /usr/src/settlement-engines/target/x86_64-unknown-linux-musl/debug/ilp-settlement-ethereum \ 54 | # /usr/local/bin/ilp-settlement-ethereum 55 | 56 | WORKDIR /opt/app 57 | 58 | RUN npm install -g ilp-settlement-xrp localtunnel 59 | 60 | COPY ./docker/redis.conf redis.conf 61 | COPY ./docker/run-testnet-bundle.js run-testnet-bundle.js 62 | 63 | # ENV RUST_BACKTRACE=1 64 | ENV RUST_LOG=interledger=debug,ilp_settlement_ethereum=debug 65 | 66 | # In order for the node to access the config file, you need to mount 67 | # the directory with the node's config.yml file as a Docker volume 68 | # called "/config". You can do this by adding the option 69 | # `-v /path/to/config.yml:/config` when calling `docker run`. 70 | VOLUME [ "/config" ] 71 | 72 | ENTRYPOINT [ "node", "run-testnet-bundle.js" ] 73 | -------------------------------------------------------------------------------- /docker/docker-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # intended to be run in the top directory 4 | docker_image_tag=${DOCKER_IMAGE_TAG:-latest} 5 | 6 | build_targets=() 7 | 8 | if [ "$PROFILE" = "release" ]; then 9 | printf "\e[32;1mBuilding profile: release\e[m\n" 10 | CARGO_BUILD_OPTION=--release 11 | RUST_BIN_DIR_NAME=release 12 | else 13 | printf "\e[32;1mBuilding profile: dev\e[m\n" 14 | CARGO_BUILD_OPTION= 15 | RUST_BIN_DIR_NAME=debug 16 | fi 17 | 18 | if [ $# -eq 0 ]; then 19 | printf "\e[33mNo build target is given, building all targets.\e[m\n" 20 | build_targets+=(ilp-cli) 21 | build_targets+=(ilp-node) 22 | build_targets+=(testnet-bundle) 23 | fi 24 | 25 | # if arguments are given, add it as build targets 26 | build_targets+=($@) 27 | 28 | printf "\e[32;1mBuilding docker image with tag: ${docker_image_tag}\e[m\n" 29 | 30 | for build_target in "${build_targets[@]}"; do 31 | case $build_target in 32 | "ilp-cli") docker build -f ./docker/ilp-cli.dockerfile -t interledgerrs/ilp-cli:${docker_image_tag} . ;; 33 | "ilp-node") docker build -f ./docker/ilp-node.dockerfile -t interledgerrs/ilp-node:${docker_image_tag} \ 34 | --build-arg CARGO_BUILD_OPTION="${CARGO_BUILD_OPTION}" \ 35 | --build-arg RUST_BIN_DIR_NAME="${RUST_BIN_DIR_NAME}" \ 36 | . ;; 37 | "testnet-bundle") docker build -f ./docker/Dockerfile -t interledgerrs/testnet-bundle:${docker_image_tag} . ;; 38 | esac 39 | done 40 | -------------------------------------------------------------------------------- /docker/ilp-cli.dockerfile: -------------------------------------------------------------------------------- 1 | # Build Interledger node into standalone binary 2 | FROM clux/muslrust:stable as rust 3 | 4 | WORKDIR /usr/src 5 | COPY ./Cargo.toml /usr/src/Cargo.toml 6 | COPY ./Cargo.lock /usr/src/Cargo.lock 7 | COPY ./crates /usr/src/crates 8 | COPY ./.git /usr/src/ 9 | 10 | # TODO: investigate using a method like https://whitfin.io/speeding-up-rust-docker-builds/ 11 | # to ensure that the dependencies are cached so the build doesn't take as long 12 | # RUN cargo build --all-features --package ilp-node --package interledger-settlement-engines --package ilp-cli 13 | RUN cargo build --release --all-features --package ilp-cli 14 | 15 | FROM alpine 16 | 17 | # Expose ports for HTTP server 18 | EXPOSE 7770 19 | 20 | # Install SSL certs 21 | RUN apk --no-cache add \ 22 | ca-certificates 23 | 24 | # Copy Interledger binary 25 | COPY --from=rust \ 26 | /usr/src/target/x86_64-unknown-linux-musl/release/ilp-cli \ 27 | /usr/local/bin/ilp-cli 28 | 29 | ENTRYPOINT [ "ilp-cli" ] 30 | -------------------------------------------------------------------------------- /docker/ilp-node.dockerfile: -------------------------------------------------------------------------------- 1 | # Build Interledger node into standalone binary 2 | FROM clux/muslrust:stable as rust 3 | ARG CARGO_BUILD_OPTION="" 4 | ARG RUST_BIN_DIR_NAME="debug" 5 | 6 | RUN echo "Building profile: ${CARGO_BUILD_OPTION}, output dir: ${RUST_BIN_DIR_NAME}" 7 | 8 | WORKDIR /usr/src 9 | COPY ./Cargo.toml /usr/src/Cargo.toml 10 | COPY ./Cargo.lock /usr/src/Cargo.lock 11 | COPY ./crates /usr/src/crates 12 | COPY ./.git /usr/src/ 13 | 14 | RUN cargo build ${CARGO_BUILD_OPTION} --package ilp-node --bin ilp-node 15 | 16 | # Deploy compiled binary to another container 17 | FROM alpine 18 | ARG CARGO_BUILD_OPTION="" 19 | ARG RUST_BIN_DIR_NAME="debug" 20 | 21 | # Expose ports for HTTP and BTP 22 | # - 7770: HTTP - ILP over HTTP, API, BTP 23 | # - 7771: HTTP - settlement 24 | EXPOSE 7770 25 | EXPOSE 7771 26 | 27 | # Install SSL certs 28 | RUN apk --no-cache add ca-certificates 29 | 30 | # Copy Interledger binary 31 | COPY --from=rust \ 32 | /usr/src/target/x86_64-unknown-linux-musl/${RUST_BIN_DIR_NAME}/ilp-node \ 33 | /usr/local/bin/ilp-node 34 | 35 | WORKDIR /opt/app 36 | 37 | # ENV RUST_BACKTRACE=1 38 | ENV RUST_LOG=ilp,interledger=debug 39 | 40 | ENTRYPOINT [ "/usr/local/bin/ilp-node" ] 41 | -------------------------------------------------------------------------------- /docker/redis.conf: -------------------------------------------------------------------------------- 1 | # Listen for TCP connections on this port 2 | port 6379 3 | # Also listen for unix socket connections, because they are faster than TCP 4 | unixsocket /tmp/redis.sock 5 | unixsocketperm 777 6 | 7 | # Save redis data using append-only log of commands 8 | # Note this is in addition to the RDB snapshots that are on by default 9 | appendonly yes 10 | # This saves the data every second, which is faster than after 11 | # each command but means that up to 1 second of transactions 12 | # can be lost if the server crashes 13 | # Change this value to "always" to make it save transactions to 14 | # the file before applying them 15 | appendfsync everysec 16 | 17 | # Load redis-cell module, which is used for rate limiting 18 | loadmodule ./interledger-store/external/libredis_cell.so 19 | 20 | daemonize yes 21 | -------------------------------------------------------------------------------- /docker/run-services-in-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # turn on bash's job control 4 | set -m 5 | 6 | redis-server --dir /data & 7 | lt -s test -p 7770 & 8 | DEBUG=settlement* ilp-settlement-xrp & 9 | # interledger-settlement-engines & 10 | sleep 1 11 | ilp-node $@ 12 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # ILP Node HTTP API 2 | 3 | For instructions on running the ILP Node, see the [Readme](../README.md). 4 | 5 | ## Authorization 6 | 7 | The ILP Node uses [HTTP Bearer](https://tools.ietf.org/html/rfc6750) tokens for authorization. 8 | 9 | Your HTTP requests should look like: 10 | 11 | ``` 12 | GET /accounts HTTP/1.1 13 | Authorization: Bearer BEARER-TOKEN-HERE 14 | ``` 15 | 16 | For administrative functionalities, the value of the token must be the value of `admin_auth_token` when the node was launched. When authorizing as a user, it must be the `ilp_over_http_incoming_token` which was specified during that user's account creation. 17 | 18 | ## HTTP REST API 19 | 20 | ### **By default, the API is available on port `7770` and it exposes endpoints as specified in [this OpenAPIv3 specification](https://app.swaggerhub.com/apis/interledger-rs/Interledger/1.0) ([corresponding yml file](./api.yml)).** 21 | 22 | ## WebSockets API 23 | 24 | ### `/accounts/:username/payments/incoming` 25 | 26 | Admin or account-holder only. 27 | 28 | #### Message 29 | 30 | In the format of text message of WebSocket, the endpoint will send the following JSON as a payment notification when receiving payments: 31 | 32 | ```json 33 | { 34 | "to_username": "Receiving account username", 35 | "from_username": "Sending account username", 36 | "destination": "Destination ILP address", 37 | "amount": 1000, 38 | "timestamp": "Receiving time in RFC3339 format", 39 | "sequence": 2, 40 | "connection_closed": false 41 | } 42 | ``` 43 | 44 | Note that the `from_username` corresponds to the account that received the packet _on this node_, not the original sender. 45 | 46 | The `sequence` field reports the sequence number of each received packet carrying a payment amount. 47 | 48 | A payment notification with `amount: 0` and `connection_closed: true` will be sent when the last packet (which has a `ConnectionClose` frame) has been received. All other payment notifications report an actual payment amount and `connection_closed: false`. 49 | 50 | 51 | ### `/accounts/:username/ilp/btp` - Bilateral Transfer Protocol (BTP) 52 | 53 | Account-holder only. 54 | 55 | This endpoint implements BTP, a WebSocket-based protocol for sending and receiving ILP packets. This protocol is specified in [IL-RFC 22: Bilateral Transfer Protocol 2.0 (BTP/2.0)](https://github.com/interledger/rfcs/blob/master/0023-bilateral-transfer-protocol/0023-bilateral-transfer-protocol.md). 56 | 57 | Note this endpoint is the one referred to as `ilp_over_btp_url` in the `AccountSettings`. 58 | -------------------------------------------------------------------------------- /docs/logging.md: -------------------------------------------------------------------------------- 1 | # Logging 2 | 3 | Logs are created via the `tracing` crates. We define various _scopes_ depending on the operation we want to trace at various debug levels. The log level can be set via the `RUST_LOG` environment variable, and via the `/tracing-level` at runtime by the node operator. 4 | 5 | For each request we track various information depending on the error log lvel: 6 | - **Incoming**: 7 | - `ERROR`: 8 | - `request.id`: a randomly generated uuid for that specific request 9 | - `prepare.destination`: the destination of the prepare packet inside the request 10 | - `prepare.amount`: the amount in the prepare packet inside the request 11 | - `from.id`: the request sender's account uuid 12 | - `DEBUG`: 13 | - `from.username`: the request sender's username 14 | - `from.ilp_address`: the request sender's ilp address 15 | - `from.asset_code`: the request sender's asset code 16 | - `from.asset_scale`: the request sender's asset scale 17 | - **Forwarding** (this is shown when an incoming request is turned into an outgoing request and is being forwarded to a peer): 18 | - `ERROR`: 19 | - `prepare.amount`: the amount in the prepare packet inside the request 20 | - `from.id`: the request sender's account uuid 21 | - `DEBUG`: 22 | - `to.username`: the request receiver's username 23 | - `to.ilp_address`: the request receiver's ilp address 24 | - `to.asset_code`: the request receiver's asset code 25 | - `to.asset_scale`: the request receiver's asset scale 26 | - **Outgoing**: 27 | - `ERROR`: 28 | - `request.id`: a randomly generated uuid for that specific request 29 | - `prepare.destination`: the destination of the prepare packet inside the request 30 | - `from.id`: the request sender's account uuid 31 | - `to.id`: the request receiver's account uuid 32 | - `DEBUG`: 33 | - `from.username`: the request sender's username 34 | - `from.ilp_address`: the request sender's ilp address 35 | - `from.asset_code`: the request sender's asset code 36 | - `from.asset_scale`: the request sender's asset scale 37 | - `to.username`: the request receiver's username 38 | - `to.ilp_address`: the request receiver's ilp address 39 | - `to.asset_code`: the request receiver's asset code 40 | - `to.asset_scale`: the request receiver's asset scale 41 | 42 | Then, depending on the response received for the request, we add additional information to that log: 43 | - `Fulfill`: We add a scope `"result = fulfill"` at the `DEBUG` level 44 | - `fulfillment`: the fulfill packet's fulfillment condition 45 | - `Reject`: We add a scope `"result = "reject"` at the INFO level 46 | - `reject.code`: the reject packet's error code field 47 | - `reject.message`: the reject packet's message field 48 | - `reject.triggered_by`: the reject packet's triggered_by field -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Interledger.rs Examples 2 | 3 | Here you can find various demos of Interledger.rs' functionality: 4 | 5 | 1. [Simple Two-Node Payment](./simple/README.md) 6 | 1. [Two-Node Payment with Ethereum On-Ledger Settlement](./eth-settlement/README.md) 7 | 1. [Two-Node Payment with XRP On-Ledger Settlement](./xrp-settlement/README.md) 8 | 1. [Three-Node Payment with Ethereum and XRP On-Ledger Settlement](./eth-xrp-three-nodes/README.md) 9 | 1. Integrating Interledger Into Your App (Coming Soon!) 10 | 11 | Have questions? Feel free to [open an issue](https://github.com/interledger-rs/interledger-rs/issues/new) or ask a question [on the forum](https://forum.interledger.org/)! 12 | 13 | ## Running the Examples 14 | The README of each example provides step-by-step instructions on how to run the example. 15 | 16 | If you want to run all of the steps automatically, you can use the provided [`run-md.sh`](../scripts/run-md.sh) script to parse and execute the shell commands from the Markdown file: 17 | 18 | ```bash # 19 | # Under the example directory, for example, "simple" 20 | $ ../../scripts/run-md.sh README.md 21 | 22 | # It also accepts STDIN: 23 | $ (some command) | ../../scripts/run-md.sh 24 | ``` 25 | 26 | By default, the script will download compiled binaries to use with the examples. If you want it to build the project from the source code instead, you can set `SOURCE_MODE=1` as follows: 27 | 28 | ```bash # 29 | SOURCE_MODE=1 ../../scripts/run-md.sh README.md 30 | ``` 31 | -------------------------------------------------------------------------------- /examples/eth-settlement/images/materials/overview.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interledger/interledger-rs/544b6dbcd299087d16babc3377e4e014f87d68ec/examples/eth-settlement/images/materials/overview.graffle -------------------------------------------------------------------------------- /examples/eth-xrp-three-nodes/images/materials/overview.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interledger/interledger-rs/544b6dbcd299087d16babc3377e4e014f87d68ec/examples/eth-xrp-three-nodes/images/materials/overview.graffle -------------------------------------------------------------------------------- /examples/simple/images/materials/overview.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interledger/interledger-rs/544b6dbcd299087d16babc3377e4e014f87d68ec/examples/simple/images/materials/overview.graffle -------------------------------------------------------------------------------- /examples/xrp-settlement/images/materials/overview.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interledger/interledger-rs/544b6dbcd299087d16babc3377e4e014f87d68ec/examples/xrp-settlement/images/materials/overview.graffle -------------------------------------------------------------------------------- /scripts/parse-md.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Just the awk md parser taken out so that it can be tested. 3 | 4 | set -eu 5 | 6 | # [ -t 0 ] is true when the stdin is terminal, sort of isatty 7 | if [[ $# -ne 0 ]] || [[ "${1:-}" == "--help" ]] || [[ "${1:-}" == "-h" ]] || [ -t 0 ]; then 8 | echo "USAGE: ${0:-parse-md.sh} < input_file" 1>&2 9 | echo "This program will strip the input out of markdown and output the executable script." 1>&2 10 | echo "Example: scripts/parse-md.sh < examples/simple/README.md" 1>&2 11 | exit 1 12 | fi 13 | 14 | # Based on mdlp.awk from https://gist.github.com/trauber/4955706 15 | # Originally written by @trauber Rich Traube 16 | 17 | # This script parses and executes code blocks found in Markdown files. 18 | # In addition to parsing code found between ```bash ... ```, 19 | # it also parses code from special HTML comments that start with $" } 25 | { 26 | # cc = code block count 27 | # hc = html comment count 28 | # ps = print script 29 | # codeblock starts only if the block is of bash, and ends if the line starts with ``` 30 | if (hc % 2 == 0 && /^```/) { if (/^```(bash)$/) { ps = 1; } else { ps = 0; } cc++; next } 31 | 32 | # html comment starts if the line starts with ) 33 | else if (cc % 2 == 0 && /^ and it is in html comment context 36 | else if (cc % 2 == 0 && hc % 2 == 1 && /^-->/) { hc++; next } 37 | 38 | # if the line is in either html comment section or code block section, print the line 39 | else if ((hc % 2 == 1 || cc % 2 == 1) && ps == 1) { print } 40 | 41 | # if the line matches one line comment (), just print it 42 | else if ($0 ~ one_line_comment) { p = $0; sub("^$", "", p); print p } 43 | }' 44 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Just not to publish crates 4 | # $ ./scripts/release.sh --dry-run 5 | cargo release --skip-publish $@ 6 | -------------------------------------------------------------------------------- /scripts/run-md-lib.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # initialize global variables 4 | function init() { 5 | if [ -n "$SOURCE_MODE" ] && [ "$SOURCE_MODE" -ne "0" ]; then 6 | SOURCE_MODE=1 7 | else 8 | SOURCE_MODE=0 9 | fi 10 | 11 | if [ -n "$TEST_MODE" ] && [ "$TEST_MODE" -ne "0" ]; then 12 | TEST_MODE=1 13 | else 14 | TEST_MODE=0 15 | fi 16 | } 17 | 18 | # run pre_test_hook function only if it exists 19 | function run_pre_test_hook { 20 | # only when the hook is defined 21 | type pre_test_hook &>/dev/null 22 | if [ $? -eq 0 ]; then 23 | pre_test_hook 24 | fi 25 | } 26 | 27 | # run post_test_hook function only if it exists 28 | function run_post_test_hook { 29 | # only when the hook is defined 30 | type post_test_hook &>/dev/null 31 | if [ $? -eq 0 ]; then 32 | post_test_hook 33 | fi 34 | } 35 | 36 | # $1 = error message 37 | # 38 | # error_and_exit "Error! Try again." 39 | function error_and_exit() { 40 | printf "\e[31m%b\e[m\n" "$1" 1>&2 41 | exit 1 42 | } 43 | 44 | # $1 = url 45 | # $2 = timeout, default: -1 = don't timeout 46 | # returns 0 if succeeds 47 | # returns 1 if timeouts 48 | # 49 | # wait_to_serve "http://localhost:7770" 10 50 | function wait_to_serve() { 51 | local timeout=${2:--1} 52 | local start=$SECONDS 53 | while : 54 | do 55 | printf "." 56 | curl $1 &> /dev/null 57 | if [ $? -eq 0 ]; then 58 | break 59 | fi 60 | if [ $timeout -ge 0 ] && [ $(($SECONDS - $start)) -ge $timeout ]; then 61 | return 1 62 | fi 63 | sleep 1 64 | done 65 | return 0 66 | } 67 | 68 | # $1 = expected body 69 | # $2 = timeout, -1 = don't timeout 70 | # $3.. = curl arguments (excludes curl itself) 71 | # 72 | # wait_to_get '{"balance":0, "asset_code": "ABC"}' -1 -H "Authorization: Bearer xxx" "http://localhost/" 73 | function wait_to_get_http_response_body() { 74 | local expected=$1 75 | local timeout=$2 76 | local start=$SECONDS 77 | shift 78 | while : 79 | do 80 | printf "." 81 | local json=$(curl "$@" 2> /dev/null) 82 | if [ "$json" = "$expected" ]; then 83 | break 84 | fi 85 | if [ $timeout -ge 0 ] && [ $(($SECONDS - $start)) -ge $timeout ]; then 86 | return 1 87 | fi 88 | sleep 1 89 | done 90 | return 0 91 | } 92 | 93 | # $1 = prompt text 94 | # $2 = default value in [yn] 95 | # sets PROMPT_ANSWER in [yn] 96 | # 97 | # prompt_yn "Quit? [y/N]" n 98 | function prompt_yn() { 99 | if [ $# -ne 2 ]; then 100 | return 1 101 | fi 102 | local text=$1 103 | local default=$2 104 | if ! [[ $default =~ [yn] ]]; then 105 | return 2 106 | fi 107 | read -p "$text" -n 1 answer 108 | if [[ "$answer" =~ ^[^yYnN]+ ]]; then 109 | printf "\n" 110 | prompt_yn "$text" "$default" 111 | return $? 112 | fi 113 | case "$answer" in 114 | [Yy]) PROMPT_ANSWER=y;; 115 | [Nn]) PROMPT_ANSWER=n;; 116 | *) PROMPT_ANSWER=$default;; 117 | esac 118 | } 119 | 120 | # $1.. = curl arguments (excludes curl itself) 121 | # sets TEST_RESULT to http response body 122 | # 123 | # test_http_response_body -H "Authorization: Bearer xxx" "http://localhost/" 124 | function test_http_response_body() { 125 | TEST_RESULT=$(curl "$@" 2> /dev/null) 126 | return $? 127 | } 128 | 129 | # $1 expected value 130 | # $2.. test function and its arguments 131 | # 132 | # test_equals_or_exit '{"value":true}' test_http_response_body -H "Authorization: Bearer xxx" "http://localhost/" 133 | function test_equals_or_exit() { 134 | local expected_value="$1" 135 | shift 136 | "$@" 137 | if [ "$TEST_RESULT" = "$expected_value" ]; then 138 | return 0 139 | else 140 | error_and_exit "Test failed. Expected: $expected_value, Got: $TEST_RESULT" 141 | fi 142 | } 143 | 144 | function is_linux() { 145 | if [[ $(uname) =~ Linux ]]; then 146 | echo 1 147 | else 148 | echo 0 149 | fi 150 | } 151 | 152 | function is_macos() { 153 | if [[ $(uname) =~ Darwin ]]; then 154 | echo 1 155 | else 156 | echo 0 157 | fi 158 | } 159 | -------------------------------------------------------------------------------- /scripts/run-md.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TMP_SCRIPT=$(mktemp) 4 | export RUN_MD_LIB="$(dirname $0)/run-md-lib.sh" 5 | 6 | if [ -n "$1" ]; then 7 | # if the first argument is a file, run it 8 | if [ -f "$1" ]; then 9 | MD_FILE="$1" 10 | elif [ -d "$1" ]; then 11 | # if the argument is a directory, run the README.md file in it 12 | MD_FILE="$1/README.md" 13 | fi 14 | else 15 | MD_FILE=- 16 | fi 17 | 18 | # run tcpdump in the same directory where other artifacts to be uploaded reside 19 | PARENT_DIR_NAME="${PWD##*/}" 20 | TCPDUMP_OUTPUT_FILENAME="$PARENT_DIR_NAME".pcap 21 | echo "saving packet capture for $PARENT_DIR_NAME/${MD_FILE##*/} as $TCPDUMP_OUTPUT_FILENAME" 22 | sudo tcpdump -i lo -s 65535 -w "/tmp/run-md-test/${TCPDUMP_OUTPUT_FILENAME}" & 23 | TCPDUMP_PID=$! 24 | 25 | cat "$MD_FILE" | "$(dirname $0)/parse-md.sh" > "$TMP_SCRIPT" 26 | bash -x -O expand_aliases "$TMP_SCRIPT" 27 | 28 | sudo kill -2 $TCPDUMP_PID 29 | if [ $? -eq 0 ]; then 30 | rm "$TMP_SCRIPT" 31 | exit 0 32 | else 33 | printf "\e[31;1mError running markdown file: $MD_FILE (parsed bash script $TMP_SCRIPT)\e[m\n" 1>&2 34 | exit 1 35 | fi 36 | --------------------------------------------------------------------------------