├── .cargo └── config.toml ├── .dockerignore ├── .env.sample ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── action-rs │ └── grcov.yml ├── landing_page │ ├── gnosis_safe_logo.png │ └── index.html ├── pull_request_template.md └── workflows │ ├── cla.yml │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── README.md ├── add_rustfmt_git_hook.sh ├── docker-compose.yml ├── nginx └── templates │ └── nginx.conf.template ├── rust-toolchain.toml ├── rustfmt.toml ├── scripts ├── autodeploy.sh ├── deploy_docker.sh └── generate_test_tx.sh └── src ├── cache ├── cache_op_executors.rs ├── cache_operations.rs ├── inner_cache.rs ├── manager.rs ├── mod.rs ├── redis.rs └── tests │ ├── cache_inner.rs │ ├── cache_op_executors.rs │ ├── cache_operations.rs │ └── mod.rs ├── common ├── converters │ ├── data_decoded.rs │ ├── mod.rs │ ├── page_metadata.rs │ ├── tests │ │ ├── balances.rs │ │ ├── balances_v2.rs │ │ ├── data_decoded.rs │ │ ├── get_address_ex_from_any_source.rs │ │ ├── get_transfer_direction.rs │ │ ├── mod.rs │ │ ├── page_metadata.rs │ │ ├── safe_app.rs │ │ ├── transfer_erc20.rs │ │ ├── transfer_erc721.rs │ │ ├── transfer_ether.rs │ │ └── transfers.rs │ └── transfers.rs ├── mod.rs ├── models │ ├── addresses.rs │ ├── backend │ │ ├── about.rs │ │ ├── balances.rs │ │ ├── balances_v2.rs │ │ ├── chains.rs │ │ ├── hooks.rs │ │ ├── mod.rs │ │ ├── notifications.rs │ │ ├── safe_apps.rs │ │ ├── safes.rs │ │ ├── transactions.rs │ │ └── transfers.rs │ ├── data_decoded.rs │ ├── mod.rs │ └── page.rs ├── routes │ ├── authorization.rs │ └── mod.rs └── tests │ ├── common.rs │ └── mod.rs ├── config ├── mod.rs └── tests │ └── mod.rs ├── macros.rs ├── main.rs ├── monitoring ├── mod.rs ├── performance.rs └── tests │ ├── mod.rs │ └── path_patterns.rs ├── providers ├── address_info.rs ├── ext.rs ├── fiat.rs ├── info.rs ├── mod.rs └── tests │ ├── fiat.rs │ ├── info.rs │ └── mod.rs ├── routes ├── about │ ├── handlers.rs │ ├── mod.rs │ ├── models.rs │ ├── routes.rs │ └── tests │ │ ├── mod.rs │ │ └── routes.rs ├── balances │ ├── converters.rs │ ├── converters_v2.rs │ ├── handlers.rs │ ├── handlers_v2.rs │ ├── mod.rs │ ├── models.rs │ └── routes.rs ├── chains │ ├── converters.rs │ ├── handlers.rs │ ├── mod.rs │ ├── models.rs │ ├── routes.rs │ └── tests │ │ ├── chains.rs │ │ ├── json │ │ ├── backend_chains_info_page.json │ │ └── expected_chains_info_page.json │ │ ├── mod.rs │ │ └── routes.rs ├── collectibles │ ├── handlers.rs │ ├── mod.rs │ ├── models.rs │ ├── routes.rs │ └── tests │ │ ├── mod.rs │ │ └── routes.rs ├── contracts │ ├── handlers.rs │ ├── mod.rs │ ├── models.rs │ ├── routes.rs │ └── tests │ │ ├── mod.rs │ │ └── routes.rs ├── delegates │ ├── handlers.rs │ ├── mod.rs │ ├── models.rs │ ├── routes.rs │ └── tests │ │ ├── json │ │ ├── backend_create_delegate.json │ │ ├── backend_delete_delegate.json │ │ ├── backend_delete_delegate_safe.json │ │ ├── backend_list_delegates_of_safe.json │ │ └── expected_list_delegates_of_safe.json │ │ ├── mod.rs │ │ └── routes.rs ├── health │ ├── mod.rs │ ├── routes.rs │ └── tests │ │ ├── mod.rs │ │ └── routes.rs ├── hooks │ ├── handlers.rs │ ├── mod.rs │ ├── routes.rs │ └── tests │ │ ├── invalidate_caches.rs │ │ ├── mod.rs │ │ ├── routes.rs │ │ └── safes.rs ├── messages │ ├── backend_models.rs │ ├── create_message.rs │ ├── frontend_models.rs │ ├── get_message.rs │ ├── get_messages.rs │ ├── message_mapper.rs │ ├── mod.rs │ └── update_message.rs ├── mod.rs ├── notifications │ ├── handlers.rs │ ├── mod.rs │ ├── models.rs │ ├── routes.rs │ └── tests │ │ ├── mod.rs │ │ └── routes.rs ├── safe_apps │ ├── converters.rs │ ├── handlers.rs │ ├── mod.rs │ ├── models.rs │ ├── routes.rs │ └── tests │ │ ├── json │ │ ├── response_safe_apps.json │ │ └── response_safe_apps_url_query.json │ │ ├── mod.rs │ │ └── routes.rs ├── safes │ ├── converters.rs │ ├── handlers │ │ ├── estimations.rs │ │ ├── mod.rs │ │ └── safes.rs │ ├── mod.rs │ ├── models.rs │ ├── routes.rs │ └── tests │ │ ├── json │ │ ├── last_collectible_transfer.json │ │ ├── last_history_tx.json │ │ ├── last_queued_tx.json │ │ └── safe_state.json │ │ ├── mod.rs │ │ └── routes.rs └── transactions │ ├── converters │ ├── details.rs │ ├── mod.rs │ ├── safe_app_info.rs │ ├── summary.rs │ ├── tests │ │ ├── check_sender_or_receiver.rs │ │ ├── data_size_calculation.rs │ │ ├── details.rs │ │ ├── is_cancellation.rs │ │ ├── map_status.rs │ │ ├── missing_signers.rs │ │ ├── mod.rs │ │ ├── safe_app_info.rs │ │ ├── summary.rs │ │ ├── transaction_id.rs │ │ ├── transaction_types.rs │ │ └── transfer_type_checks.rs │ └── transaction_id.rs │ ├── filters │ ├── mod.rs │ ├── module.rs │ ├── multisig.rs │ ├── tests │ │ └── mod.rs │ └── transfer.rs │ ├── handlers │ ├── commons.rs │ ├── details.rs │ ├── history.rs │ ├── mod.rs │ ├── module.rs │ ├── multisig.rs │ ├── preview.rs │ ├── proposal.rs │ ├── queued.rs │ ├── tests │ │ ├── mod.rs │ │ ├── parse_id.rs │ │ ├── transactions_history.rs │ │ ├── transactions_queued.rs │ │ └── transfers.rs │ └── transfers.rs │ ├── mod.rs │ ├── models │ ├── details.rs │ ├── mod.rs │ ├── requests.rs │ └── summary.rs │ ├── routes.rs │ └── tests │ ├── json │ ├── chain_response.json │ ├── contract_info_BID.json │ ├── contracts_response.json │ ├── multisig_tx_details.json │ ├── post_confirmation_result.json │ ├── preview_response.json │ └── preview_response_data_decoded_error.json │ ├── mod.rs │ ├── preview.rs │ └── routes.rs ├── tests ├── backend_url.rs ├── json │ ├── balances │ │ ├── balance_compound_ether.json │ │ └── balance_ether.json │ ├── chains │ │ ├── polygon.json │ │ ├── rinkeby.json │ │ ├── rinkeby_disabled_wallets.json │ │ ├── rinkeby_enabled_features.json │ │ ├── rinkeby_fixed_gas_price.json │ │ ├── rinkeby_multiple_gas_price.json │ │ ├── rinkeby_no_gas_price.json │ │ ├── rinkeby_rpc_auth_unknown.json │ │ ├── rinkeby_rpc_no_auth.json │ │ └── rinkeby_unknown_gas_price.json │ ├── collectibles │ │ ├── collectibles_page.json │ │ ├── collectibles_paginated_empty_cgw.json │ │ ├── collectibles_paginated_empty_txs.json │ │ ├── collectibles_paginated_page_1_cgw.json │ │ ├── collectibles_paginated_page_1_txs.json │ │ ├── collectibles_paginated_page_2_cgw.json │ │ └── collectibles_paginated_page_2_txs.json │ ├── commons │ │ ├── DOCTORED_data_decoded_multi_send_nested_delegate.json │ │ ├── DOCTORED_data_decoded_nested_multi_sends.json │ │ ├── data_decoded_add_owner_with_threshold.json │ │ ├── data_decoded_approve.json │ │ ├── data_decoded_change_master_copy.json │ │ ├── data_decoded_change_threshold.json │ │ ├── data_decoded_delete_guard.json │ │ ├── data_decoded_disable_module.json │ │ ├── data_decoded_enable_module.json │ │ ├── data_decoded_exec_transaction_from_module.json │ │ ├── data_decoded_multi_send.json │ │ ├── data_decoded_multi_send_single_inner_transaction.json │ │ ├── data_decoded_nested_safe_interaction.json │ │ ├── data_decoded_remove_owner.json │ │ ├── data_decoded_set_fallback_handler.json │ │ ├── data_decoded_set_guard.json │ │ ├── data_decoded_swap_array_values.json │ │ ├── data_decoded_swap_owner.json │ │ └── empty_page.json │ ├── contracts │ │ └── contract_info_BID.json │ ├── exchange │ │ └── currency_rates.json │ ├── master_copies │ │ └── polygon_master_copies.json │ ├── mod.rs │ ├── results │ │ └── tx_details_with_origin.json │ ├── safe_apps │ │ ├── polygon_safe_app_url_query.json │ │ └── polygon_safe_apps.json │ ├── safes │ │ ├── with_guard_safe_v130_l2.json │ │ ├── with_module_transactions.json │ │ ├── with_modules.json │ │ ├── with_modules_and_high_nonce.json │ │ └── with_threshold_two.json │ ├── tokens │ │ ├── bat.json │ │ ├── crypto_kitties.json │ │ ├── dai.json │ │ ├── pv_memorial_token.json │ │ └── usdt.json │ ├── transactions │ │ ├── backend_history_transaction_list_page.json │ │ ├── backend_multisig_transfer_tx.json │ │ ├── backend_queued_transaction_list_page_conflicts_393.json │ │ ├── backend_queued_transaction_list_page_conflicts_394.json │ │ ├── backend_queued_transaction_list_page_no_conflicts.json │ │ ├── creation_transaction.json │ │ ├── ethereum_inconsistent_token_types.json │ │ ├── module_addOwnerWithThreshold_settings_change.json │ │ ├── module_erc20_transfer.json │ │ ├── module_erc721_transfer.json │ │ ├── module_ether_transfer.json │ │ ├── module_newAndDifferentAddOwnerWithThreshold_settings_change.json │ │ ├── module_transaction.json │ │ ├── module_transaction_failed.json │ │ ├── multisig_addOwnerWithThreshold_settings_change.json │ │ ├── multisig_approve_custom.json │ │ ├── multisig_awaiting_confirmations.json │ │ ├── multisig_awaiting_confirmations_empty.json │ │ ├── multisig_awaiting_confirmations_null.json │ │ ├── multisig_awaiting_confirmations_required_null.json │ │ ├── multisig_awaiting_execution.json │ │ ├── multisig_cancellation_transaction.json │ │ ├── multisig_confirmations_null.json │ │ ├── multisig_erc20_transfer.json │ │ ├── multisig_erc20_transfer_delegate.json │ │ ├── multisig_erc20_transfer_invalid_to_and_from.json │ │ ├── multisig_erc20_transfer_with_value.json │ │ ├── multisig_erc721_transfer.json │ │ ├── multisig_erc721_transfer_cancelled.json │ │ ├── multisig_erc721_transfer_invalid_to_and_from.json │ │ ├── multisig_ether_transfer.json │ │ ├── multisig_failed_transfer.json │ │ ├── multisig_newAndDifferentAddOwnerWithThreshold_settings_change.json │ │ └── multisig_with_origin.json │ └── transfers │ │ ├── erc20_transfer_with_erc721_token_info.json │ │ ├── erc_20_transfer_unexpected_param_names.json │ │ ├── erc_20_transfer_with_token_info_incoming.json │ │ ├── erc_20_transfer_with_token_info_outgoing.json │ │ ├── erc_20_transfer_without_token_info.json │ │ ├── erc_721_transfer_with_token_info_incoming.json │ │ ├── erc_721_transfer_with_token_info_outgoing.json │ │ ├── erc_721_transfer_without_token_info.json │ │ ├── ether_transfer_incoming.json │ │ └── ether_transfer_outgoing.json ├── main.rs └── mod.rs └── utils ├── context.rs ├── cors.rs ├── errors.rs ├── http_client.rs ├── json.rs ├── mod.rs ├── tests ├── data_decoded_utils.rs ├── errors.rs ├── json.rs ├── macros.rs ├── method_names.rs ├── mod.rs └── transactions.rs ├── transactions.rs └── urls.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.aarch64-unknown-linux-gnu] 2 | linker = "aarch64-linux-gnu-gcc" 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | ### Rust ### 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # These are backup files generated by rustfmt 6 | **/*.rs.bk 7 | 8 | .gitignore 9 | .git -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in 2 | # the repo. Unless a later match takes precedence, 3 | * @safe-global/safe-client-gateway-maintainers 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | - Call url `...` 16 | - Provide `json` you are submitting to the service (if it applies) 17 | - Links to issues in other repos (if possible) 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Environment (please complete the following information):** 23 | - Staging or production? 24 | - Which chain? 25 | - OS: [e.g. iOS] 26 | - Browser [e.g. chrome, safari] 27 | - Version [e.g. 22] 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Provide references to the feature you are implementing that requires this change** 14 | Provide at least one of the following: 15 | - Links to epics in your repository 16 | - Images taken from mocks 17 | - Gitbook or any form of written documentation links, etc. Any of these alternatives will help us contextualise your request. 18 | 19 | **Describe the solution you'd like** 20 | A clear and concise description of what you want to happen. 21 | 22 | **Describe alternatives you've considered** 23 | A clear and concise description of any alternative solutions or features you've considered. 24 | 25 | **Additional context** 26 | Add any other context or screenshots about the feature request here. 27 | -------------------------------------------------------------------------------- /.github/action-rs/grcov.yml: -------------------------------------------------------------------------------- 1 | branch: true 2 | ignore-not-existing: true 3 | llvm: true 4 | filter: covered 5 | output-type: lcov 6 | output-path: ./lcov.info 7 | coveralls-token: ${{ secrets.COVERALLS_TOKEN }} 8 | prefix-dir: . 9 | ignore: 10 | - "/*" 11 | - "/**/tests/**" 12 | - "target/debug/build/**" 13 | 14 | excl-line: "#\\[cfg\\(test\\)\\]|#\\[derive|#\\[serde" 15 | excl-br-line: "#\\[derive\\(" 16 | excl-start: "mod tests \\{" 17 | excl-br-start: "mod tests \\{" 18 | -------------------------------------------------------------------------------- /.github/landing_page/gnosis_safe_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5afe/safe-client-gateway/9a016db82fe7038db73fb41981dbd15bc89770f5/.github/landing_page/gnosis_safe_logo.png -------------------------------------------------------------------------------- /.github/landing_page/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Safe Client Gateway 6 | 10 | 11 | 12 |
13 |
14 |
15 | 16 |

Safe Client Gateway

17 |
18 |
19 |
20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Make sure these boxes are checked! 📦✅ 2 | 3 | - [ ] You have a nightly compatible version of `rustfmt` installed 4 | ```bash 5 | rustup component add rustfmt --toolchain nightly 6 | ``` 7 | - [ ] You ran `cargo +nightly fmt` on the code base before submitting 8 | -------------------------------------------------------------------------------- /.github/workflows/cla.yml: -------------------------------------------------------------------------------- 1 | name: "CLA Assistant" 2 | on: 3 | issue_comment: 4 | types: [ created ] 5 | pull_request_target: 6 | types: [ opened,closed,synchronize ] 7 | 8 | jobs: 9 | CLAssistant: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: "CLA Assistant" 13 | if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' 14 | # Beta Release 15 | uses: cla-assistant/github-action@v2.1.3-beta 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | # the below token should have repo scope and must be manually added by you in the repository's secret 19 | PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} 20 | with: 21 | path-to-signatures: 'signatures/version1/cla.json' 22 | path-to-document: 'https://safe.global/cla/' 23 | # branch should not be protected 24 | branch: 'cla-signatures' 25 | allowlist: hectorgomezv,moisses89,luarx,fmrsabino,rmeissner,Uxio0,*bot # may need to update this expression if we add new bots 26 | 27 | #below are the optional inputs - If the optional inputs are not given, then default values will be taken 28 | #remote-organization-name: enter the remote organization name where the signatures should be stored (Default is storing the signatures in the same repository) 29 | #remote-repository-name: enter the remote repository name where the signatures should be stored (Default is storing the signatures in the same repository) 30 | #create-file-commit-message: 'For example: Creating file for storing CLA Signatures' 31 | #signed-commit-message: 'For example: $contributorName has signed the CLA in #$pullRequestNo' 32 | #custom-notsigned-prcomment: 'pull request comment with Introductory message to ask new contributors to sign' 33 | #custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA' 34 | #custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.' 35 | #lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true) 36 | #use-dco-flag: true - If you are using DCO instead of CLA 37 | 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | # Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | .idea 13 | 14 | .env 15 | 16 | dump.rdb 17 | 18 | **/venv 19 | 20 | *.DS_Store* 21 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "safe-client-gateway" 3 | version = "3.53.0" 4 | authors = ["jpalvarezl ", "rmeissner ", "fmrsabino "] 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 | bb8-redis = "0.12.0" 11 | bigdecimal = { version = "0.3.0", features = ["serde"] } 12 | chrono = { version = "0.4.24", features = ["serde"] } 13 | derivative = "2.2.0" 14 | dotenv = "0.15.0" 15 | env_logger = "0.10.0" 16 | ethcontract-common = "0.23.0" 17 | ethabi = "18.0.0" 18 | itertools = "0.10.5" 19 | lazy_static = "1.4.0" 20 | log = "0.4.17" 21 | mockall = "0.11.3" 22 | openssl = { version = "0.10", features = ["vendored"] } 23 | rand = "0.8.5" 24 | r2d2 = "0.8.10" 25 | regex = "1.7.1" 26 | reqwest = { version = "0.11.16", features = ["json"] } 27 | rocket = { version = "0.5.0-rc.2", features = ["tls", "json"] } 28 | rocket_codegen = { version = "0.5.0-rc.2" } 29 | rocket_okapi = { version = "0.8.0-rc.2", features = ["swagger"] } 30 | semver = "1.0.17" 31 | serde = { version = "1.0.159", features = ["derive"] } 32 | serde_json = { version = "1.0.96", features = ["raw_value"] } 33 | serde_repr = "0.1.12" 34 | thiserror = "1.0.40" 35 | tokio = "1.16.1" 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Safe Ecosystem Foundation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /add_rustfmt_git_hook.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # check that rustfmt installed, or else this hook doesn't make much sense 4 | command -v rustfmt >/dev/null 2>&1 || { echo >&2 "Rustfmt is required but it's not installed. Aborting."; exit 1; } 5 | 6 | # write a whole script to pre-commit hook 7 | # NOTE: it will overwrite pre-commit file! 8 | cat > .git/hooks/pre-commit <<'EOF' 9 | #!/bin/bash -e 10 | declare -a rust_files=() 11 | files=$(git diff-index --name-only HEAD) 12 | echo 'Formatting source files' 13 | for file in $files; do 14 | if [ ! -f "${file}" ]; then 15 | continue 16 | fi 17 | if [[ "${file}" == *.rs ]]; then 18 | rust_files+=("${file}") 19 | fi 20 | done 21 | if [ ${#rust_files[@]} -ne 0 ]; then 22 | command -v rustfmt >/dev/null 2>&1 || { echo >&2 "Rustfmt is required but it's not installed. Aborting."; exit 1; } 23 | $(command -v rustfmt) --edition 2018 --unstable-features --skip-children ${rust_files[@]} & 24 | fi 25 | wait 26 | if [ ${#rust_files[@]} -ne 0 ]; then 27 | git add ${rust_files[@]} 28 | echo "Formatting done, changed files: ${rust_files[@]}" 29 | else 30 | echo "No changes, formatting skipped" 31 | fi 32 | EOF 33 | 34 | chmod +x .git/hooks/pre-commit 35 | 36 | echo "Hooks updated" 37 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | nginx: 5 | image: nginx:1.21-alpine 6 | env_file: 7 | - .env 8 | ports: 9 | - "${NGINX_HOST_PORT}:80" 10 | volumes: 11 | - ./nginx/templates:/etc/nginx/templates 12 | depends_on: 13 | - web 14 | 15 | web: 16 | build: . 17 | env_file: 18 | - .env 19 | depends_on: 20 | - redis 21 | 22 | redis: 23 | image: redis:6-alpine 24 | env_file: 25 | - .env 26 | ports: 27 | - "${REDIS_PORT}:${REDIS_PORT}" 28 | -------------------------------------------------------------------------------- /nginx/templates/nginx.conf.template: -------------------------------------------------------------------------------- 1 | worker_processes auto; 2 | 3 | events { 4 | worker_connections 10000; # increase if you have lots of clients 5 | # accept_mutex on; # set to 'on' if nginx worker_processes > 1 6 | use epoll; # to enable for Linux 2.6+ 7 | } 8 | 9 | http { 10 | include mime.types; 11 | # fallback in case we can't determine a type 12 | default_type application/octet-stream; 13 | sendfile on; 14 | 15 | upstream app_server { 16 | ip_hash; # For load-balancing 17 | server web:${ROCKET_PORT} fail_timeout=0; 18 | keepalive 32; 19 | } 20 | 21 | server { 22 | access_log off; 23 | listen 80; 24 | charset utf-8; 25 | 26 | keepalive_timeout 75s; 27 | keepalive_requests 100000; 28 | sendfile on; 29 | tcp_nopush on; 30 | tcp_nodelay on; 31 | 32 | gzip on; 33 | gzip_min_length 10000; 34 | gzip_comp_level 6; 35 | 36 | # text/html is always included by default 37 | gzip_types text/plain text/css application/json application/javascript application/x-javascript text/javascript text/xml application/xml application/rss+xml application/atom+xml application/rdf+xml; 38 | gzip_disable "MSIE [1-6]\."; 39 | 40 | # allow the server to close connection on non responding client, this will free up memory 41 | reset_timedout_connection on; 42 | 43 | # Redirect http to https 44 | if ($http_x_forwarded_proto = 'http') { 45 | return 301 https://$host$request_uri; 46 | } 47 | 48 | location / { 49 | proxy_pass http://app_server/; 50 | proxy_set_header Host $host; 51 | proxy_set_header X-Forwarded-Host $server_name; 52 | proxy_set_header X-Real-IP $remote_addr; 53 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 54 | proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; 55 | add_header Front-End-Https on; 56 | # we don't want nginx trying to do something clever with 57 | # redirects, we set the Host: header above already. 58 | proxy_redirect off; 59 | # They default to 60s. Increase to avoid WORKER TIMEOUT in web container 60 | proxy_connect_timeout 60s; 61 | proxy_read_timeout 60s; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | # match with version in DockerFile 3 | channel = "1.65.0" 4 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # Enable unstable features on the unstable channel. 2 | unstable_features = true 3 | 4 | # Reorder impl items. type and const are put first, then macros and methods. 5 | reorder_impl_items = true 6 | 7 | # Convert /* */ comments to // comments where possible 8 | normalize_comments = true 9 | 10 | # Merge imports from the same module into a single use statement. Conversely, imports from different modules are split into separate statements. 11 | imports_granularity = "Module" 12 | -------------------------------------------------------------------------------- /scripts/autodeploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ev 4 | 5 | curl -s --output /dev/null --write-out "%{http_code}" \ 6 | -H "Content-Type: application/json" \ 7 | -X POST \ 8 | -u "$AUTODEPLOY_TOKEN" \ 9 | -d '{"push_data": {"tag": "'$TARGET_ENV'" }}' \ 10 | $AUTODEPLOY_URL 11 | -------------------------------------------------------------------------------- /scripts/deploy_docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | # Workflow run number 6 | export BUILD_NUMBER=$GITHUB_RUN_NUMBER 7 | # strip the first char as that should always be "v" (as tags should be in the format "vX.X.X") 8 | description="$(git describe --tags --always)" 9 | export VERSION=${description:1} 10 | 11 | echo "Trigger docker build and upload for version $VERSION ($BUILD_NUMBER)" 12 | 13 | if [ "$1" = "develop" -o "$1" = "main" ]; then 14 | cache_tag="$1" 15 | else 16 | cache_tag="staging" 17 | fi 18 | 19 | cached_builder_image_id="$DOCKERHUB_ORG/$DOCKERHUB_PROJECT:$cache_tag-builder" 20 | cached_runtime_image_id="$DOCKERHUB_ORG/$DOCKERHUB_PROJECT:$cache_tag" 21 | runtime_image_id="$DOCKERHUB_ORG/$DOCKERHUB_PROJECT:$1" 22 | 23 | # Load cached builder image 24 | docker pull $cached_builder_image_id || true 25 | # Rebuild builder image if required 26 | docker buildx build \ 27 | --target builder \ 28 | --cache-from $cached_builder_image_id \ 29 | -t $cached_builder_image_id \ 30 | -f Dockerfile \ 31 | --build-arg VERSION --build-arg BUILD_NUMBER \ 32 | . 33 | 34 | # Load cached runtime image 35 | docker pull $cached_runtime_image_id || true 36 | # Rebuild runtime image if required 37 | docker buildx build \ 38 | --cache-from $cached_builder_image_id \ 39 | --cache-from $cached_runtime_image_id \ 40 | -t $runtime_image_id \ 41 | -f Dockerfile \ 42 | --build-arg VERSION --build-arg BUILD_NUMBER \ 43 | . 44 | 45 | # Push runtime images to remote repository 46 | docker push $runtime_image_id 47 | 48 | # Push builder image to remote repository for next build 49 | docker push $cached_builder_image_id 50 | 51 | # If release, set latest docker tag 52 | case $1 in v*) 53 | latest_image_id="$DOCKERHUB_ORG/$DOCKERHUB_PROJECT:latest" 54 | docker tag $runtime_image_id $latest_image_id 55 | docker push $latest_image_id 56 | esac 57 | -------------------------------------------------------------------------------- /src/cache/inner_cache.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::errors::ApiError; 2 | 3 | #[derive(Debug, PartialEq)] 4 | pub(super) struct CachedWithCode { 5 | pub(super) code: u16, 6 | pub(super) data: String, 7 | } 8 | 9 | impl CachedWithCode { 10 | const SEPARATOR: &'static str = ";"; 11 | 12 | pub(super) fn split(cached: &str) -> Self { 13 | let cached_with_code: Vec<&str> = cached.splitn(2, CachedWithCode::SEPARATOR).collect(); 14 | CachedWithCode { 15 | code: cached_with_code 16 | .get(0) 17 | .expect("Must have a status code") 18 | .parse() 19 | .expect("Not a valid Http code"), 20 | data: cached_with_code.get(1).expect("Must have data").to_string(), 21 | } 22 | } 23 | 24 | pub(super) fn join(code: u16, data: &str) -> String { 25 | format!("{}{}{}", code, CachedWithCode::SEPARATOR, data) 26 | } 27 | 28 | pub(super) fn is_error(&self) -> bool { 29 | 200 > self.code || self.code >= 400 30 | } 31 | 32 | pub(super) fn to_result(&self) -> Result { 33 | if self.is_error() { 34 | Err(ApiError::from_backend_error(self.code, &self.data)) 35 | } else { 36 | Ok(String::from(&self.data)) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/cache/manager.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use crate::cache::redis::{new_service_cache, new_service_cache_mainnet}; 4 | use crate::cache::Cache; 5 | 6 | pub enum ChainCache { 7 | Mainnet, 8 | Other, 9 | } 10 | 11 | impl From<&str> for ChainCache { 12 | fn from(id: &str) -> Self { 13 | match id { 14 | "1" => ChainCache::Mainnet, 15 | _ => ChainCache::Other, 16 | } 17 | } 18 | } 19 | 20 | #[rocket::async_trait] 21 | pub trait RedisCacheManager: Send + Sync { 22 | fn cache_for_chain(&self, chain_cache: ChainCache) -> Arc; 23 | } 24 | 25 | pub struct DefaultRedisCacheManager { 26 | mainnet_cache: Arc, 27 | default_cache: Arc, 28 | } 29 | 30 | pub async fn create_cache_manager() -> DefaultRedisCacheManager { 31 | DefaultRedisCacheManager { 32 | mainnet_cache: Arc::new(new_service_cache_mainnet().await), 33 | default_cache: Arc::new(new_service_cache().await), 34 | } 35 | } 36 | 37 | #[rocket::async_trait] 38 | impl RedisCacheManager for DefaultRedisCacheManager { 39 | fn cache_for_chain(&self, chain_cache: ChainCache) -> Arc { 40 | match chain_cache { 41 | ChainCache::Mainnet => self.mainnet_cache.clone(), 42 | ChainCache::Other => self.default_cache.clone(), 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/cache/mod.rs: -------------------------------------------------------------------------------- 1 | use mockall::automock; 2 | 3 | mod cache_op_executors; 4 | pub mod cache_operations; 5 | mod inner_cache; 6 | pub mod manager; 7 | pub mod redis; 8 | 9 | #[cfg(test)] 10 | mod tests; 11 | 12 | const CACHE_REQS_PREFIX: &'static str = "c_reqs"; 13 | const CACHE_RESP_PREFIX: &'static str = "c_resp"; 14 | const CACHE_REQS_RESP_PREFIX: &'static str = "c_re"; 15 | 16 | #[automock] 17 | #[rocket::async_trait] 18 | pub trait Cache: Send + Sync { 19 | async fn fetch(&self, id: &str) -> Option; 20 | async fn create(&self, id: &str, dest: &str, timeout: usize); 21 | async fn insert_in_hash(&self, hash: &str, id: &str, dest: &str); 22 | async fn get_from_hash(&self, hash: &str, id: &str) -> Option; 23 | async fn has_key(&self, id: &str) -> bool; 24 | async fn expire_entity(&self, id: &str, timeout: usize); 25 | async fn invalidate_pattern(&self, pattern: &str); 26 | async fn invalidate(&self, id: &str); 27 | async fn info(&self) -> Option; 28 | } 29 | -------------------------------------------------------------------------------- /src/cache/tests/mod.rs: -------------------------------------------------------------------------------- 1 | mod cache_inner; 2 | mod cache_op_executors; 3 | mod cache_operations; 4 | -------------------------------------------------------------------------------- /src/common/converters/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod data_decoded; 2 | pub mod page_metadata; 3 | pub mod transfers; 4 | 5 | #[cfg(test)] 6 | mod tests; 7 | 8 | use crate::common::models::addresses::AddressEx; 9 | use crate::providers::info::InfoProvider; 10 | use crate::routes::transactions::models::TransferDirection; 11 | 12 | pub(crate) fn get_transfer_direction(safe: &str, from: &str, to: &str) -> TransferDirection { 13 | if safe == from { 14 | TransferDirection::Outgoing 15 | } else if safe == to { 16 | TransferDirection::Incoming 17 | } else { 18 | TransferDirection::Unknown 19 | } 20 | } 21 | 22 | // This method is required to prevent polluting the cache with all the safe requests 23 | // This is done to prevent that every user that queries a transfer transaction, doesn't 24 | // leave a mark in our cache. 25 | pub(crate) async fn get_address_ex_from_any_source( 26 | safe: &str, 27 | address: &str, 28 | info_provider: &impl InfoProvider, 29 | ) -> AddressEx { 30 | if safe != address { 31 | info_provider 32 | .address_ex_from_any_source(address) 33 | .await 34 | .unwrap_or(AddressEx::address_only(address)) 35 | } else { 36 | AddressEx::address_only(address) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/common/converters/page_metadata.rs: -------------------------------------------------------------------------------- 1 | use crate::common::models::page::PageMetadata; 2 | use std::ops::Deref; 3 | 4 | impl PageMetadata { 5 | pub fn to_url_string(&self) -> String { 6 | return format!("limit={}&offset={}", self.limit, self.offset); 7 | } 8 | 9 | pub fn from_cursor(encoded_cursor: &str) -> Self { 10 | let mut output = Self::default(); 11 | 12 | let chunked: Vec> = encoded_cursor 13 | .split("&") 14 | .map(|it| it.split("=").collect()) 15 | .collect(); 16 | 17 | chunked.into_iter().for_each(|it| { 18 | let first = it.first().unwrap_or(&"").deref(); 19 | if first == "limit" { 20 | output.limit = it.get(1).unwrap_or(&"0").parse::().unwrap_or(20); 21 | } else if first == "offset" { 22 | output.offset = it.get(1).unwrap_or(&"0").parse::().unwrap_or(0); 23 | } 24 | }); 25 | 26 | output 27 | } 28 | } 29 | impl Default for PageMetadata { 30 | fn default() -> Self { 31 | Self { 32 | offset: 0, 33 | limit: 20, 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/common/converters/tests/get_address_ex_from_any_source.rs: -------------------------------------------------------------------------------- 1 | use crate::common::converters::get_address_ex_from_any_source; 2 | use crate::common::models::addresses::AddressEx; 3 | use crate::providers::info::*; 4 | 5 | #[rocket::async_test] 6 | async fn get_address_info_address_diff_than_safe() { 7 | let address = "0x1234"; 8 | let safe = "0x4321"; 9 | 10 | let mut mock_info_provider = MockInfoProvider::new(); 11 | mock_info_provider 12 | .expect_address_ex_from_any_source() 13 | .times(1) 14 | .return_once(move |_| { 15 | Ok(AddressEx { 16 | value: address.to_string(), 17 | name: Some("".to_string()), 18 | logo_uri: None, 19 | }) 20 | }); 21 | 22 | let expected = AddressEx { 23 | value: address.to_string(), 24 | name: Some("".to_string()), 25 | logo_uri: None, 26 | }; 27 | 28 | let actual = get_address_ex_from_any_source(safe, address, &mut mock_info_provider).await; 29 | 30 | assert_eq!(expected, actual); 31 | } 32 | 33 | #[rocket::async_test] 34 | async fn get_address_info_address_diff_than_safe_error() { 35 | let address = "0x1234"; 36 | let safe = "0x4321"; 37 | 38 | let mut mock_info_provider = MockInfoProvider::new(); 39 | mock_info_provider 40 | .expect_address_ex_from_any_source() 41 | .times(1) 42 | .return_once(move |_| bail!("No address info")); 43 | 44 | let actual = get_address_ex_from_any_source(safe, address, &mut mock_info_provider).await; 45 | assert_eq!(AddressEx::address_only(address), actual); 46 | } 47 | 48 | #[rocket::async_test] 49 | async fn get_address_info_address_equal_to_safe() { 50 | let address = "0x1234"; 51 | let safe = "0x1234"; 52 | 53 | let mut mock_info_provider = MockInfoProvider::new(); 54 | mock_info_provider 55 | .expect_address_ex_from_contracts() 56 | .times(0); 57 | 58 | let actual = get_address_ex_from_any_source(safe, address, &mut mock_info_provider).await; 59 | assert_eq!(AddressEx::address_only(address), actual); 60 | } 61 | -------------------------------------------------------------------------------- /src/common/converters/tests/get_transfer_direction.rs: -------------------------------------------------------------------------------- 1 | use crate::common::converters::get_transfer_direction; 2 | use crate::routes::transactions::models::TransferDirection; 3 | 4 | #[test] 5 | fn get_transfer_direction_incoming() { 6 | let safe = "0x1230B3d59858296A31053C1b8562Ecf89A2f888b"; 7 | let to = "0x1230B3d59858296A31053C1b8562Ecf89A2f888b"; 8 | let from = "0x65F8236309e5A99Ff0d129d04E486EBCE20DC7B0"; 9 | 10 | let actual = get_transfer_direction(safe, from, to); 11 | 12 | assert_eq!(actual, TransferDirection::Incoming); 13 | } 14 | 15 | #[test] 16 | fn get_transfer_direction_outgoing() { 17 | let safe = "0x1230B3d59858296A31053C1b8562Ecf89A2f888b"; 18 | let to = "0x65F8236309e5A99Ff0d129d04E486EBCE20DC7B0"; 19 | let from = "0x1230B3d59858296A31053C1b8562Ecf89A2f888b"; 20 | 21 | let actual = get_transfer_direction(safe, from, to); 22 | 23 | assert_eq!(actual, TransferDirection::Outgoing); 24 | } 25 | 26 | #[test] 27 | fn get_transfer_direction_unknown() { 28 | let safe = "0xBEA2F9227230976d2813a2f8b922c22bE1DE1B23"; 29 | let to = "0x65F8236309e5A99Ff0d129d04E486EBCE20DC7B0"; 30 | let from = "0x1230B3d59858296A31053C1b8562Ecf89A2f888b"; 31 | 32 | let actual = get_transfer_direction(safe, from, to); 33 | 34 | assert_eq!(actual, TransferDirection::Unknown); 35 | } 36 | -------------------------------------------------------------------------------- /src/common/converters/tests/mod.rs: -------------------------------------------------------------------------------- 1 | pub(super) mod balances; 2 | pub(super) mod balances_v2; 3 | mod data_decoded; 4 | mod get_address_ex_from_any_source; 5 | mod get_transfer_direction; 6 | mod page_metadata; 7 | mod safe_app; 8 | mod transfer_erc20; 9 | mod transfer_erc721; 10 | mod transfer_ether; 11 | mod transfers; 12 | -------------------------------------------------------------------------------- /src/common/converters/tests/page_metadata.rs: -------------------------------------------------------------------------------- 1 | use crate::common::models::page::PageMetadata; 2 | 3 | #[test] 4 | fn page_metadata_with_valid_non_zero_data() { 5 | let input = "limit=20&offset=20&queued=false"; 6 | 7 | let actual = PageMetadata::from_cursor(input); 8 | let expected = PageMetadata { 9 | offset: 20, 10 | limit: 20, 11 | }; 12 | assert_eq!(expected, actual); 13 | } 14 | 15 | #[test] 16 | fn page_metadata_with_zeros() { 17 | let input = "limit=0&offset=0"; 18 | 19 | let actual = PageMetadata::from_cursor(input); 20 | let expected = PageMetadata { 21 | offset: 0, 22 | limit: 0, 23 | }; 24 | assert_eq!(expected, actual); 25 | } 26 | 27 | #[test] 28 | fn page_metadata_with_missing_optional_args() { 29 | let input = "offset=50"; 30 | 31 | let actual = PageMetadata::from_cursor(input); 32 | let expected = PageMetadata { 33 | offset: 50, 34 | limit: 20, 35 | }; 36 | assert_eq!(expected, actual); 37 | } 38 | -------------------------------------------------------------------------------- /src/common/mod.rs: -------------------------------------------------------------------------------- 1 | #[doc(hidden)] 2 | pub mod converters; 3 | pub mod models; 4 | 5 | pub mod routes; 6 | #[cfg(test)] 7 | mod tests; 8 | -------------------------------------------------------------------------------- /src/common/models/addresses.rs: -------------------------------------------------------------------------------- 1 | use crate::providers::info::InfoProvider; 2 | use serde::Serialize; 3 | 4 | #[derive(Serialize, Debug, PartialEq)] 5 | #[serde(rename_all = "camelCase")] 6 | #[cfg_attr(test, derive(serde::Deserialize))] 7 | pub struct AddressEx { 8 | pub value: String, 9 | #[serde(skip_serializing_if = "Option::is_none")] 10 | pub name: Option, 11 | #[serde(skip_serializing_if = "Option::is_none")] 12 | pub logo_uri: Option, 13 | } 14 | 15 | impl AddressEx { 16 | pub fn zero() -> Self { 17 | AddressEx { 18 | value: "0x0000000000000000000000000000000000000000".to_owned(), 19 | name: None, 20 | logo_uri: None, 21 | } 22 | } 23 | 24 | pub fn address_only(address: &str) -> Self { 25 | AddressEx { 26 | value: address.to_owned(), 27 | name: None, 28 | logo_uri: None, 29 | } 30 | } 31 | 32 | pub async fn any_source(address: &str, info_provider: &(impl InfoProvider + Sync)) -> Self { 33 | info_provider 34 | .address_ex_from_any_source(address) 35 | .await 36 | .unwrap_or(AddressEx::address_only(address)) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/common/models/backend/about.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Deserialize, Debug)] 4 | pub struct About { 5 | pub name: String, 6 | pub version: String, 7 | pub api_version: String, 8 | pub secure: bool, 9 | pub settings: SettingsDto, 10 | } 11 | 12 | #[derive(Deserialize, Debug)] 13 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 14 | pub struct SettingsDto { 15 | ethereum_node_url: String, 16 | ethereum_tracing_node_url: String, 17 | eth_internal_txs_block_process_limit: Option, 18 | eth_reorg_blocks: usize, 19 | eth_uniswap_factory_address: String, 20 | } 21 | -------------------------------------------------------------------------------- /src/common/models/backend/balances.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Deserialize, Debug, Hash)] 4 | #[serde(rename_all = "camelCase")] 5 | pub struct Balance { 6 | pub token_address: Option, 7 | pub token: Option, 8 | pub balance: String, 9 | pub fiat_balance: String, 10 | pub fiat_conversion: String, 11 | } 12 | 13 | #[derive(Deserialize, Debug, Hash)] 14 | #[serde(rename_all = "camelCase")] 15 | pub struct BalanceToken { 16 | pub name: String, 17 | pub symbol: String, 18 | pub decimals: u64, 19 | pub logo_uri: String, 20 | } 21 | -------------------------------------------------------------------------------- /src/common/models/backend/balances_v2.rs: -------------------------------------------------------------------------------- 1 | use bigdecimal::BigDecimal; 2 | use serde::Deserialize; 3 | 4 | #[derive(Deserialize, Debug)] 5 | #[serde(rename_all = "camelCase")] 6 | pub struct Balance { 7 | pub token_address: Option, 8 | pub token: Option, 9 | pub balance: String, 10 | } 11 | 12 | #[derive(Deserialize, Debug)] 13 | #[serde(rename_all = "camelCase")] 14 | pub struct BalanceToken { 15 | pub name: String, 16 | pub symbol: String, 17 | pub decimals: u64, 18 | pub logo_uri: String, 19 | } 20 | 21 | #[derive(Deserialize, Debug)] 22 | #[serde(rename_all = "camelCase")] 23 | pub struct TokenPrice { 24 | pub fiat_code: String, 25 | pub fiat_price: BigDecimal, 26 | pub timestamp: String, 27 | } 28 | -------------------------------------------------------------------------------- /src/common/models/backend/chains.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Deserialize, Debug, PartialEq, Clone)] 4 | #[serde(rename_all = "camelCase")] 5 | pub struct ChainInfo { 6 | pub recommended_master_copy_version: String, 7 | pub transaction_service: String, 8 | pub vpc_transaction_service: String, 9 | pub chain_id: String, 10 | pub chain_name: String, 11 | pub short_name: String, 12 | pub l2: bool, 13 | pub description: String, 14 | pub rpc_uri: RpcUri, 15 | pub safe_apps_rpc_uri: RpcUri, 16 | pub public_rpc_uri: RpcUri, 17 | pub block_explorer_uri_template: BlockExplorerUriTemplate, 18 | pub native_currency: NativeCurrency, 19 | pub theme: Theme, 20 | pub ens_registry_address: Option, 21 | pub gas_price: Vec, 22 | pub disabled_wallets: Vec, 23 | pub features: Vec, 24 | } 25 | 26 | #[derive(Deserialize, Debug, PartialEq, Clone)] 27 | #[serde(rename_all = "camelCase")] 28 | pub struct NativeCurrency { 29 | pub name: String, 30 | pub symbol: String, 31 | pub decimals: u64, 32 | pub logo_uri: String, 33 | } 34 | 35 | #[derive(Deserialize, Debug, PartialEq, Clone)] 36 | #[serde(rename_all = "camelCase")] 37 | pub struct Theme { 38 | pub text_color: String, 39 | pub background_color: String, 40 | } 41 | 42 | #[derive(Deserialize, Debug, PartialEq, Clone)] 43 | #[serde(tag = "type")] 44 | #[serde(rename_all = "lowercase")] 45 | pub enum GasPrice { 46 | #[serde(rename_all = "camelCase")] 47 | Oracle { 48 | uri: String, 49 | gas_parameter: String, 50 | gwei_factor: String, 51 | }, 52 | #[serde(rename_all = "camelCase")] 53 | Fixed { wei_value: String }, 54 | #[serde(other)] 55 | Unknown, 56 | } 57 | 58 | #[derive(Deserialize, Debug, PartialEq, Clone)] 59 | #[serde(rename_all = "camelCase")] 60 | pub struct RpcUri { 61 | pub authentication: RpcAuthentication, 62 | pub value: String, 63 | } 64 | 65 | #[derive(Deserialize, Debug, PartialEq, Clone)] 66 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 67 | pub enum RpcAuthentication { 68 | ApiKeyPath, 69 | NoAuthentication, 70 | #[serde(other)] 71 | Unknown, 72 | } 73 | 74 | #[derive(Deserialize, Debug, PartialEq, Clone)] 75 | #[serde(rename_all = "camelCase")] 76 | pub struct BlockExplorerUriTemplate { 77 | pub address: String, 78 | pub tx_hash: String, 79 | pub api: String, 80 | } 81 | -------------------------------------------------------------------------------- /src/common/models/backend/hooks.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Deserialize, Debug, Hash)] 4 | #[serde(tag = "type")] 5 | pub struct Payload { 6 | pub address: String, 7 | #[serde(rename(deserialize = "chainId"))] 8 | pub chain_id: String, 9 | #[serde(flatten)] 10 | pub details: Option, 11 | } 12 | 13 | #[derive(Deserialize, Debug, Hash)] 14 | #[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")] 15 | pub enum PayloadDetails { 16 | NewConfirmation(NewConfirmation), 17 | ExecutedMultisigTransaction(ExecutedMultisigTransaction), 18 | PendingMultisigTransaction(PendingMultisigTransaction), 19 | IncomingEther(IncomingEther), 20 | IncomingToken(IncomingToken), 21 | #[serde(other)] 22 | Unknown, 23 | } 24 | 25 | #[derive(Deserialize, Debug, Hash)] 26 | #[serde(rename_all = "camelCase")] 27 | pub struct NewConfirmation { 28 | pub owner: String, 29 | pub safe_tx_hash: String, 30 | } 31 | 32 | #[derive(Deserialize, Debug, Hash)] 33 | #[serde(rename_all = "camelCase")] 34 | pub struct ExecutedMultisigTransaction { 35 | pub safe_tx_hash: String, 36 | pub tx_hash: String, 37 | } 38 | 39 | #[derive(Deserialize, Debug, Hash)] 40 | #[serde(rename_all = "camelCase")] 41 | pub struct PendingMultisigTransaction { 42 | pub safe_tx_hash: String, 43 | } 44 | 45 | #[derive(Deserialize, Debug, Hash)] 46 | #[serde(rename_all = "camelCase")] 47 | pub struct IncomingEther { 48 | pub tx_hash: String, 49 | pub value: String, 50 | } 51 | 52 | #[derive(Deserialize, Debug, Hash)] 53 | #[serde(rename_all = "camelCase")] 54 | pub struct IncomingToken { 55 | pub tx_hash: String, 56 | pub token_address: String, 57 | pub token_id: Option, 58 | pub value: Option, 59 | } 60 | -------------------------------------------------------------------------------- /src/common/models/backend/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod about; 2 | pub mod balances; 3 | pub mod balances_v2; 4 | pub mod chains; 5 | pub mod hooks; 6 | pub mod notifications; 7 | pub mod safe_apps; 8 | pub mod safes; 9 | pub mod transactions; 10 | pub mod transfers; 11 | -------------------------------------------------------------------------------- /src/common/models/backend/notifications.rs: -------------------------------------------------------------------------------- 1 | use crate::routes::notifications::models::DeviceData; 2 | use serde::Serialize; 3 | 4 | #[derive(Serialize, Debug, Clone, PartialEq)] 5 | #[serde(rename_all = "camelCase")] 6 | pub struct NotificationRegistrationRequest { 7 | #[serde(flatten)] 8 | pub notification_device_data: DeviceData, 9 | pub safes: Vec, 10 | pub signatures: Vec, 11 | } 12 | -------------------------------------------------------------------------------- /src/common/models/backend/safe_apps.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Deserialize, Debug, PartialEq, Clone)] 4 | #[serde(rename_all = "camelCase")] 5 | pub struct SafeApp { 6 | pub id: u64, 7 | pub url: String, 8 | pub name: String, 9 | pub icon_url: String, 10 | pub description: String, 11 | pub chain_ids: Vec, 12 | pub provider: Option, 13 | pub access_control: SafeAppAccessControlPolicies, 14 | #[serde(default)] 15 | pub tags: Vec, 16 | pub features: Vec, 17 | pub developer_website: Option, 18 | pub social_profiles: Vec, 19 | } 20 | 21 | #[derive(Deserialize, Debug, PartialEq, Clone)] 22 | #[serde(rename_all = "camelCase")] 23 | pub struct SafeAppSocialProfile { 24 | pub platform: String, 25 | pub url: String, 26 | } 27 | 28 | #[derive(Deserialize, Debug, PartialEq, Clone)] 29 | #[serde(rename_all = "camelCase")] 30 | pub struct SafeAppProvider { 31 | pub url: String, 32 | pub name: String, 33 | } 34 | 35 | #[derive(Deserialize, Debug, PartialEq, Clone)] 36 | #[serde(tag = "type")] 37 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 38 | pub enum SafeAppAccessControlPolicies { 39 | NoRestrictions, 40 | DomainAllowlist(SafeAppDomainAllowlistPolicy), 41 | #[serde(other)] 42 | Unknown, 43 | } 44 | 45 | #[derive(Deserialize, Debug, PartialEq, Clone)] 46 | #[serde(rename_all = "camelCase")] 47 | pub struct SafeAppDomainAllowlistPolicy { 48 | pub value: Vec, 49 | } 50 | -------------------------------------------------------------------------------- /src/common/models/backend/safes.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Deserialize, Debug)] 4 | #[serde(rename_all = "camelCase")] 5 | pub struct MasterCopy { 6 | pub address: String, 7 | pub version: String, 8 | pub deployer: String, 9 | pub deployed_block_number: u64, 10 | pub last_indexed_block_number: u64, 11 | } 12 | -------------------------------------------------------------------------------- /src/common/models/backend/transfers.rs: -------------------------------------------------------------------------------- 1 | use crate::providers::info::TokenInfo; 2 | use chrono::{DateTime, Utc}; 3 | use derivative::Derivative; 4 | use serde::Deserialize; 5 | 6 | #[derive(Deserialize, Debug, Clone, Hash)] 7 | #[serde(tag = "type")] 8 | pub enum Transfer { 9 | #[serde(rename(deserialize = "ERC721_TRANSFER"))] 10 | Erc721(Erc721Transfer), 11 | #[serde(rename(deserialize = "ERC20_TRANSFER"))] 12 | Erc20(Erc20Transfer), 13 | #[serde(rename(deserialize = "ETHER_TRANSFER"))] 14 | Ether(EtherTransfer), 15 | #[serde(other)] 16 | Unknown, 17 | } 18 | 19 | #[derive(Derivative, Deserialize, Debug, PartialEq, Clone)] 20 | #[derivative(Hash)] 21 | #[serde(rename_all = "camelCase")] 22 | pub struct Erc721Transfer { 23 | pub execution_date: DateTime, 24 | pub block_number: u64, 25 | pub transaction_hash: String, 26 | pub to: String, 27 | pub token_id: String, 28 | pub token_address: String, 29 | #[derivative(Hash = "ignore")] 30 | pub token_info: Option, 31 | pub from: String, 32 | } 33 | 34 | #[derive(Derivative, Deserialize, Debug, Clone)] 35 | #[derivative(Hash)] 36 | #[serde(rename_all = "camelCase")] 37 | pub struct Erc20Transfer { 38 | pub execution_date: DateTime, 39 | pub block_number: u64, 40 | pub transaction_hash: String, 41 | pub to: String, 42 | pub value: String, 43 | pub token_address: String, 44 | #[derivative(Hash = "ignore")] 45 | pub token_info: Option, 46 | pub from: String, 47 | } 48 | 49 | #[derive(Derivative, Deserialize, Debug, Clone)] 50 | #[derivative(Hash)] 51 | #[serde(rename_all = "camelCase")] 52 | pub struct EtherTransfer { 53 | pub execution_date: DateTime, 54 | pub block_number: u64, 55 | pub transaction_hash: String, 56 | pub to: String, 57 | pub value: String, 58 | pub from: String, 59 | } 60 | -------------------------------------------------------------------------------- /src/common/models/data_decoded.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::json; 2 | use serde::{Deserialize, Serialize}; 3 | use serde_repr::{Deserialize_repr, Serialize_repr}; 4 | 5 | #[derive(Serialize_repr, Deserialize_repr, PartialEq, Debug, Clone, Copy, Hash)] 6 | #[repr(u8)] 7 | pub enum Operation { 8 | CALL = 0, 9 | DELEGATE = 1, 10 | } 11 | 12 | #[derive(Serialize, Deserialize, Debug, Clone, Hash, PartialEq)] 13 | #[serde(rename_all = "camelCase")] 14 | pub struct DataDecoded { 15 | pub method: String, 16 | pub parameters: Option>, 17 | } 18 | 19 | #[derive(Serialize, Deserialize, Debug, Clone, Hash, PartialEq)] 20 | #[serde(rename_all = "camelCase")] 21 | pub struct Parameter { 22 | pub name: String, 23 | #[serde(rename = "type")] 24 | pub param_type: String, 25 | pub value: ParamValue, 26 | #[serde(skip_serializing_if = "Option::is_none")] 27 | #[serde(deserialize_with = "json::try_deserialize")] 28 | #[serde(default)] 29 | pub value_decoded: Option, 30 | } 31 | 32 | #[derive(Serialize, Deserialize, Debug, Clone, Hash, PartialEq)] 33 | #[serde(untagged)] 34 | pub enum ParamValue { 35 | SingleValue(String), 36 | ArrayValue(Vec), 37 | } 38 | 39 | #[derive(Serialize, Deserialize, Debug, Clone, Hash, PartialEq)] 40 | #[serde(untagged)] 41 | pub enum ValueDecodedType { 42 | InternalTransaction(Vec), 43 | } 44 | 45 | #[derive(Serialize, Deserialize, Debug, Clone, Hash, PartialEq)] 46 | #[serde(rename_all = "camelCase")] 47 | pub struct InternalTransaction { 48 | pub operation: Operation, 49 | pub to: String, // TODO: Address that will not be mapped to AddressEx for now 50 | pub value: Option, 51 | pub data: Option, 52 | pub data_decoded: Option, 53 | } 54 | 55 | impl From for ParamValue { 56 | fn from(item: String) -> Self { 57 | ParamValue::SingleValue(item) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/common/models/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod addresses; 2 | #[doc(hidden)] 3 | pub mod backend; 4 | pub mod data_decoded; 5 | pub mod page; 6 | -------------------------------------------------------------------------------- /src/common/models/page.rs: -------------------------------------------------------------------------------- 1 | use rocket_okapi::okapi::schemars; 2 | use rocket_okapi::okapi::schemars::JsonSchema; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Serialize, Deserialize, Debug, JsonSchema)] 6 | #[serde(rename_all = "camelCase")] 7 | #[cfg_attr(test, derive(PartialEq))] 8 | pub struct Page { 9 | pub next: Option, 10 | pub previous: Option, 11 | pub results: Vec, 12 | } 13 | 14 | #[derive(Debug, PartialEq)] 15 | pub struct PageMetadata { 16 | pub offset: u64, 17 | pub limit: u64, 18 | } 19 | 20 | #[derive(Serialize, Deserialize, Debug)] 21 | #[cfg_attr(test, derive(PartialEq))] 22 | pub struct SafeList { 23 | safes: Vec, 24 | } 25 | 26 | impl Page { 27 | pub fn map_inner(self, link_mapper: impl Fn(Option) -> Option) -> Page 28 | where 29 | U: From, 30 | { 31 | Page { 32 | next: link_mapper(self.next), 33 | previous: link_mapper(self.previous), 34 | results: self.results.into_iter().map(|it| U::from(it)).collect(), 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/common/routes/authorization.rs: -------------------------------------------------------------------------------- 1 | use crate::config::webhook_token; 2 | use rocket::http::Status; 3 | use rocket::request::{FromRequest, Outcome}; 4 | use rocket::Request; 5 | 6 | pub struct AuthorizationToken { 7 | value: String, 8 | } 9 | 10 | #[derive(Debug)] 11 | pub enum AuthorizationError { 12 | Missing, 13 | Invalid, 14 | } 15 | 16 | #[rocket::async_trait] 17 | impl<'r> FromRequest<'r> for AuthorizationToken { 18 | type Error = AuthorizationError; 19 | 20 | async fn from_request(request: &'r Request<'_>) -> Outcome { 21 | match request.headers().get_one("Authorization") { 22 | // Require the header to be present 23 | None => Outcome::Failure((Status::BadRequest, AuthorizationError::Missing)), 24 | Some(key) if key == format!("Basic {}", webhook_token()) => { 25 | Outcome::Success(AuthorizationToken { 26 | value: key.to_string(), 27 | }) 28 | } 29 | // If the Authorization header didn't match with "Basic " we consider it to be 30 | // an invalid token 31 | Some(_) => Outcome::Failure((Status::Unauthorized, AuthorizationError::Invalid)), 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/common/routes/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod authorization; 2 | -------------------------------------------------------------------------------- /src/common/tests/mod.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![deny(unused_must_use)] 2 | #![deny(rustdoc::broken_intra_doc_links)] 3 | 4 | #[macro_use] 5 | extern crate rocket; 6 | 7 | use std::sync::Arc; 8 | 9 | use dotenv::dotenv; 10 | use rocket::{Build, Rocket}; 11 | 12 | use routes::active_routes; 13 | use utils::cors::CORS; 14 | 15 | use crate::cache::manager::{create_cache_manager, RedisCacheManager}; 16 | use crate::routes::error_catchers; 17 | use crate::utils::http_client::{setup_http_client, HttpClient}; 18 | use rocket_okapi::swagger_ui::{make_swagger_ui, SwaggerUIConfig}; 19 | 20 | #[doc(hidden)] 21 | #[macro_use] 22 | pub mod macros; 23 | 24 | #[doc(hidden)] 25 | mod cache; 26 | mod common; 27 | #[doc(hidden)] 28 | mod config; 29 | 30 | #[doc(hidden)] 31 | mod monitoring; 32 | mod providers; 33 | 34 | /// Collection of all endpoints all endpoints 35 | mod routes; 36 | #[doc(hidden)] 37 | mod utils; 38 | 39 | #[cfg(test)] 40 | mod tests; 41 | 42 | #[doc(hidden)] 43 | #[launch] 44 | async fn rocket() -> Rocket { 45 | dotenv().ok(); 46 | setup_logger(); 47 | 48 | let client = setup_http_client(); 49 | let cache_manager = create_cache_manager().await; 50 | 51 | rocket::build() 52 | .mount("/", active_routes()) 53 | .mount( 54 | "/", 55 | make_swagger_ui(&SwaggerUIConfig { 56 | url: "../openapi.json".to_owned(), 57 | ..Default::default() 58 | }), 59 | ) 60 | .register("/", error_catchers()) 61 | .manage(Arc::new(cache_manager) as Arc) 62 | .manage(Arc::new(client) as Arc) 63 | .attach(monitoring::performance::PerformanceMonitor()) 64 | .attach(CORS()) 65 | } 66 | 67 | #[cfg(test)] 68 | fn setup_logger() { 69 | // noop: no need to set the logger for tests 70 | } 71 | 72 | #[doc(hidden)] 73 | #[cfg(not(test))] 74 | fn setup_logger() { 75 | env_logger::init(); 76 | } 77 | -------------------------------------------------------------------------------- /src/monitoring/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod performance; 2 | 3 | #[cfg(test)] 4 | mod tests; 5 | -------------------------------------------------------------------------------- /src/monitoring/performance.rs: -------------------------------------------------------------------------------- 1 | use crate::config; 2 | use chrono::Utc; 3 | use rocket::fairing::{Fairing, Info, Kind}; 4 | use rocket::http::uri::Path; 5 | use rocket::{Data, Request, Response}; 6 | 7 | pub struct PerformanceMonitor(); 8 | 9 | const INVALID_IP_ADDRESS: &str = "-1"; 10 | 11 | #[rocket::async_trait] 12 | impl Fairing for PerformanceMonitor { 13 | fn info(&self) -> Info { 14 | Info { 15 | name: "PerformanceMonitor", 16 | kind: Kind::Request | Kind::Response, 17 | } 18 | } 19 | 20 | async fn on_request(&self, request: &mut Request<'_>, _data: &mut Data<'_>) { 21 | request.local_cache(|| Utc::now().timestamp_millis()); 22 | } 23 | 24 | async fn on_response<'r>(&self, request: &'r Request<'_>, response: &mut Response<'r>) { 25 | if rand::random::() <= config::log_threshold() { 26 | let request_path = request.uri().path(); 27 | 28 | let chain_id = extract_chain_id(&request_path); 29 | 30 | let route = request 31 | .route() 32 | .map(|route| route.uri.to_string()) 33 | .unwrap_or(request.uri().path().to_string()); 34 | let cached = request 35 | .local_cache(|| Utc::now().timestamp_millis()) 36 | .to_owned(); 37 | let method = request.method().as_str(); 38 | let status_code = response.status().code; 39 | let delta = Utc::now().timestamp_millis() - cached; 40 | 41 | // Reads the client IP from the "X-Real-IP" header 42 | // If the IP is invalid or not present uses the remote connection instead 43 | // If the IP is still invalid, we set the value to "-1" 44 | let client_ip: String = request 45 | .client_ip() 46 | .and_then(|ip_addr| Some(ip_addr.to_string())) 47 | .unwrap_or(INVALID_IP_ADDRESS.to_string()); 48 | 49 | log::info!( 50 | "MT::{}::{}::{}::{}::{}::{}::{}", 51 | method, 52 | route, 53 | delta, 54 | status_code, 55 | request.uri().to_string(), // full path with query params 56 | chain_id, 57 | client_ip 58 | ); 59 | } 60 | } 61 | } 62 | 63 | pub(super) fn extract_chain_id(path: &Path) -> String { 64 | let chain_id = path.segments().get(2); 65 | let contains_chains = path.segments().get(1).map_or(false, |it| it == "chains"); 66 | if contains_chains && chain_id.is_some() { 67 | chain_id.unwrap().to_string() 68 | } else { 69 | String::from("-1") 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/monitoring/tests/mod.rs: -------------------------------------------------------------------------------- 1 | mod path_patterns; 2 | -------------------------------------------------------------------------------- /src/monitoring/tests/path_patterns.rs: -------------------------------------------------------------------------------- 1 | use crate::monitoring::performance::extract_chain_id; 2 | 3 | #[test] 4 | fn chain_dependent_endpoint() { 5 | let uri = uri!("/v1/chains/1/safes/0x1230B3d59858296A31053C1b8562Ecf89A2f888b"); 6 | let actual = extract_chain_id(&uri.path()); 7 | 8 | assert_eq!("1", actual); 9 | } 10 | 11 | #[test] 12 | fn chain_info_endpoint_single() { 13 | let uri = uri!("/v1/chains/1337"); 14 | let actual = extract_chain_id(&uri.path()); 15 | 16 | assert_eq!("1337", actual); 17 | } 18 | 19 | #[test] 20 | fn chain_info_all() { 21 | let uri = uri!("/v1/chains"); 22 | 23 | let actual = extract_chain_id(&uri.path()); 24 | assert_eq!("-1", actual); 25 | } 26 | 27 | #[test] 28 | fn chain_independent_endpoint() { 29 | let uri = uri!("/about/redis/"); 30 | 31 | let actual = extract_chain_id(&uri.path()); 32 | assert_eq!("-1", actual); 33 | } 34 | -------------------------------------------------------------------------------- /src/providers/address_info.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::json::default_if_null; 2 | use serde::{Deserialize, Serialize}; 3 | use serde_json::value::Value; 4 | 5 | #[derive(Serialize, Deserialize, Debug)] 6 | #[serde(rename_all = "camelCase")] 7 | #[cfg_attr(test, derive(PartialEq))] 8 | pub struct ContractInfo { 9 | pub address: String, 10 | #[serde(deserialize_with = "default_if_null")] 11 | pub name: String, 12 | #[serde(deserialize_with = "default_if_null")] 13 | pub display_name: String, 14 | pub logo_uri: Option, 15 | pub contract_abi: Option, 16 | pub trusted_for_delegate_call: bool, 17 | } 18 | -------------------------------------------------------------------------------- /src/providers/ext.rs: -------------------------------------------------------------------------------- 1 | use crate::common::models::addresses::AddressEx; 2 | use crate::providers::info::{InfoProvider, TokenInfo}; 3 | use rocket::futures::future::OptionFuture; 4 | 5 | // Using the pattern here: 6 | // use rocket::futures::stream::StreamExt; 7 | impl InfoProviderExt for T where T: InfoProvider {} 8 | 9 | #[rocket::async_trait] 10 | pub trait InfoProviderExt: InfoProvider { 11 | async fn address_to_token_info(&self, address: &Option) -> Option { 12 | let address = address.as_ref()?; 13 | self.token_info(address).await.ok() 14 | } 15 | 16 | async fn address_ex_from_contracts_or_default(&self, address: &String) -> AddressEx { 17 | self.address_ex_from_contracts(&address) 18 | .await 19 | .unwrap_or(AddressEx::address_only(address)) 20 | } 21 | 22 | async fn multiple_address_ex_from_contracts( 23 | &self, 24 | addresses: &Option>, 25 | ) -> Option> { 26 | let addresses = addresses.as_ref()?; 27 | if addresses.is_empty() { 28 | return None; 29 | } 30 | let mut results = Vec::with_capacity(addresses.len()); 31 | for address in addresses { 32 | results.push(self.address_ex_from_contracts_or_default(address).await) 33 | } 34 | Some(results) 35 | } 36 | 37 | async fn address_ex_from_contracts_optional(&self, address: &String) -> Option { 38 | if address != "0x0000000000000000000000000000000000000000" { 39 | Some(self.address_ex_from_contracts_or_default(address).await) 40 | } else { 41 | None 42 | } 43 | } 44 | 45 | async fn optional_address_ex_from_contracts( 46 | &self, 47 | address: &Option, 48 | ) -> Option { 49 | OptionFuture::from( 50 | address.as_ref().map(|address| async move { 51 | self.address_ex_from_contracts_or_default(address).await 52 | }), 53 | ) 54 | .await 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/providers/fiat.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::Arc; 3 | 4 | use bigdecimal::BigDecimal; 5 | use serde::Deserialize; 6 | 7 | use crate::cache::cache_operations::RequestCached; 8 | use crate::cache::manager::ChainCache; 9 | use crate::cache::Cache; 10 | use crate::config::{base_exchange_api_uri, exchange_api_cache_duration, short_error_duration}; 11 | use crate::utils::context::RequestContext; 12 | use crate::utils::errors::ApiResult; 13 | use crate::utils::http_client::HttpClient; 14 | 15 | #[derive(Deserialize, Clone, Debug)] 16 | pub struct Exchange { 17 | pub rates: Option>, 18 | pub base: String, 19 | } 20 | 21 | pub struct FiatInfoProvider { 22 | client: Arc, 23 | cache: Arc, 24 | } 25 | 26 | impl FiatInfoProvider { 27 | pub fn new(context: &RequestContext) -> Self { 28 | FiatInfoProvider { 29 | client: context.http_client(), 30 | cache: context.cache(ChainCache::Other), 31 | } 32 | } 33 | 34 | pub async fn exchange_usd_to(&self, currency_code: &str) -> ApiResult { 35 | if ¤cy_code.to_lowercase() == "usd" { 36 | return Ok(BigDecimal::from(1)); 37 | } 38 | 39 | let currency_code = currency_code.to_uppercase(); 40 | let exchange = self.fetch_exchange().await?; 41 | match exchange.rates { 42 | Some(rates) => { 43 | let base_to_usd = rates.get("USD").unwrap_or(&BigDecimal::from(0)).to_owned(); 44 | rates 45 | .get(¤cy_code) 46 | .cloned() 47 | .map(|base_to_requested_code| base_to_requested_code / base_to_usd) 48 | .ok_or(client_error!(422, "Currency not found")) 49 | } 50 | None => Err(client_error!(422, "Currency not found")), 51 | } 52 | } 53 | 54 | pub async fn available_currency_codes(&self) -> ApiResult> { 55 | let exchange = self.fetch_exchange().await?; 56 | Ok(exchange 57 | .rates 58 | .map_or(vec![], |s| s.keys().cloned().collect::>())) 59 | } 60 | 61 | async fn fetch_exchange(&self) -> ApiResult { 62 | let url = base_exchange_api_uri(); 63 | let body = RequestCached::new(url, &self.client, &self.cache) 64 | .cache_duration(exchange_api_cache_duration()) 65 | .error_cache_duration(short_error_duration()) 66 | .execute() 67 | .await?; 68 | serde_json::from_str::(&body) 69 | .map_err(|_| api_error!("Unknown 'Exchange' json structure")) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/providers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod address_info; 2 | #[doc(hidden)] 3 | pub mod ext; 4 | #[doc(hidden)] 5 | pub mod fiat; 6 | #[doc(hidden)] 7 | pub mod info; 8 | 9 | #[cfg(test)] 10 | mod tests; 11 | -------------------------------------------------------------------------------- /src/providers/tests/mod.rs: -------------------------------------------------------------------------------- 1 | mod fiat; 2 | mod info; 3 | -------------------------------------------------------------------------------- /src/routes/about/handlers.rs: -------------------------------------------------------------------------------- 1 | use crate::config::{build_number, version}; 2 | use crate::providers::info::{DefaultInfoProvider, InfoProvider}; 3 | use crate::routes::about::models::{About, ChainAbout}; 4 | use crate::routes::safes::models::Implementation; 5 | use crate::utils::context::RequestContext; 6 | use crate::utils::errors::ApiResult; 7 | 8 | pub async fn chains_about(context: &RequestContext, chain_id: &str) -> ApiResult { 9 | let info_provider = DefaultInfoProvider::new(chain_id, &context); 10 | let chain_info = info_provider.chain_info().await?; 11 | let about = about(); 12 | Ok(ChainAbout { 13 | transaction_service_base_uri: chain_info.transaction_service, 14 | about: About { 15 | name: about.name, 16 | version: about.version, 17 | build_number: about.build_number, 18 | }, 19 | }) 20 | } 21 | 22 | pub fn about() -> About { 23 | About { 24 | name: env!("CARGO_PKG_NAME").to_string(), 25 | version: version(), 26 | build_number: build_number(), 27 | } 28 | } 29 | 30 | pub async fn get_master_copies( 31 | context: &RequestContext, 32 | chain_id: &str, 33 | ) -> ApiResult> { 34 | let info_provider = DefaultInfoProvider::new(chain_id, &context); 35 | Ok(info_provider 36 | .master_copies() 37 | .await? 38 | .into_iter() 39 | .map(|master_copy| master_copy.into()) 40 | .collect()) 41 | } 42 | -------------------------------------------------------------------------------- /src/routes/about/mod.rs: -------------------------------------------------------------------------------- 1 | #[doc(hidden)] 2 | pub mod handlers; 3 | pub mod models; 4 | pub mod routes; 5 | 6 | #[cfg(test)] 7 | mod tests; 8 | -------------------------------------------------------------------------------- /src/routes/about/models.rs: -------------------------------------------------------------------------------- 1 | use rocket_okapi::okapi::schemars; 2 | use rocket_okapi::okapi::schemars::JsonSchema; 3 | use serde::Serialize; 4 | /// ChainAbout 5 | /// 6 | ///
7 | /// Sample 8 | /// 9 | /// ```json 10 | /// { 11 | /// "transactionServiceBaseUri": "https://safe-transaction.mainnet.staging.gnosisdev.com", 12 | /// "name": "safe-client-gateway", 13 | /// "version": "3.0.0", 14 | /// "buildNumber": "48" 15 | /// } 16 | /// ``` 17 | ///
18 | #[derive(Serialize, Debug, JsonSchema)] 19 | #[serde(rename_all = "camelCase")] 20 | pub struct ChainAbout { 21 | /// base URI string used for backend requests 22 | pub transaction_service_base_uri: String, 23 | #[serde(flatten)] 24 | pub about: About, 25 | } 26 | 27 | /// About 28 | /// 29 | ///
30 | /// Sample 31 | /// 32 | /// ```json 33 | /// { 34 | /// "name": "safe-client-gateway", 35 | /// "version": "3.0.0", 36 | /// "buildNumber": "48" 37 | /// } 38 | /// ``` 39 | ///
40 | #[derive(Serialize, Debug, JsonSchema)] 41 | #[serde(rename_all = "camelCase")] 42 | pub struct About { 43 | /// crate name 44 | pub name: String, 45 | /// env variable `VERSION`, defaults to crate version 46 | pub version: String, 47 | /// Build number from github action 48 | pub build_number: Option, 49 | } 50 | -------------------------------------------------------------------------------- /src/routes/about/tests/mod.rs: -------------------------------------------------------------------------------- 1 | mod routes; 2 | -------------------------------------------------------------------------------- /src/routes/balances/converters.rs: -------------------------------------------------------------------------------- 1 | use crate::common::models::backend::balances::Balance as BalanceDto; 2 | use crate::common::models::backend::chains::NativeCurrency; 3 | use crate::providers::info::{TokenInfo, TokenType}; 4 | use crate::routes::balances::models::Balance; 5 | 6 | const SCALE: f64 = 1_0000_f64; 7 | 8 | impl BalanceDto { 9 | pub fn to_balance(&self, usd_to_fiat: f64, native_coin: &NativeCurrency) -> Balance { 10 | let fiat_conversion = self.fiat_conversion.parse::().unwrap_or(0.0) * usd_to_fiat; 11 | let fiat_balance = self.fiat_balance.parse::().unwrap_or(0.0) * usd_to_fiat; 12 | let token_type = self 13 | .token_address 14 | .as_ref() 15 | .map(|_| TokenType::Erc20) 16 | .unwrap_or(TokenType::NativeToken); 17 | 18 | let logo_uri = if token_type == TokenType::NativeToken { 19 | Some(native_coin.logo_uri.to_string()) 20 | } else { 21 | self.token.as_ref().map(|it| it.logo_uri.to_string()) 22 | }; 23 | Balance { 24 | token_info: TokenInfo { 25 | token_type, 26 | address: self 27 | .token_address 28 | .to_owned() 29 | .unwrap_or(String::from("0x0000000000000000000000000000000000000000")), 30 | decimals: self 31 | .token 32 | .as_ref() 33 | .map(|it| it.decimals) 34 | .unwrap_or(native_coin.decimals), 35 | symbol: self 36 | .token 37 | .as_ref() 38 | .map(|it| it.symbol.to_string()) 39 | .unwrap_or(native_coin.symbol.to_string()), 40 | name: self 41 | .token 42 | .as_ref() 43 | .map(|it| it.name.to_string()) 44 | .unwrap_or(native_coin.name.to_string()), 45 | logo_uri, 46 | }, 47 | balance: self.balance.to_owned(), 48 | fiat_balance: ((fiat_balance * SCALE).floor() / SCALE).to_string(), 49 | fiat_conversion: ((fiat_conversion * SCALE).floor() / SCALE).to_string(), 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/routes/balances/mod.rs: -------------------------------------------------------------------------------- 1 | #[doc(hidden)] 2 | pub mod converters; 3 | #[doc(hidden)] 4 | pub mod converters_v2; 5 | #[doc(hidden)] 6 | pub mod handlers; 7 | #[doc(hidden)] 8 | pub mod handlers_v2; 9 | pub mod models; 10 | pub mod routes; 11 | -------------------------------------------------------------------------------- /src/routes/balances/models.rs: -------------------------------------------------------------------------------- 1 | use crate::providers::info::TokenInfo; 2 | use bigdecimal::BigDecimal; 3 | use serde::Serialize; 4 | 5 | #[derive(Serialize, Debug, PartialEq)] 6 | #[serde(rename_all = "camelCase")] 7 | pub struct Balance { 8 | pub token_info: TokenInfo, 9 | pub balance: String, 10 | pub fiat_balance: String, 11 | pub fiat_conversion: String, 12 | } 13 | 14 | #[derive(Serialize, Debug, PartialEq)] 15 | #[serde(rename_all = "camelCase")] 16 | pub struct Balances { 17 | /// Aggregated fiat balance 18 | pub fiat_total: String, 19 | /// Individual [Balance] entries for each ERC20 in the Safe 20 | pub items: Vec, 21 | } 22 | 23 | #[derive(Serialize, Debug)] 24 | #[serde(rename_all = "camelCase")] 25 | pub struct TokenPrice { 26 | pub address: String, 27 | pub fiat_code: String, 28 | pub fiat_price: BigDecimal, 29 | pub timestamp: String, 30 | } 31 | -------------------------------------------------------------------------------- /src/routes/chains/handlers.rs: -------------------------------------------------------------------------------- 1 | use crate::cache::cache_operations::RequestCached; 2 | use crate::cache::manager::ChainCache; 3 | use crate::common::models::backend::chains::ChainInfo as BackendChainInfo; 4 | use crate::common::models::page::{Page, PageMetadata}; 5 | use crate::config::{chain_info_cache_duration, chain_info_request_timeout}; 6 | use crate::providers::info::{DefaultInfoProvider, InfoProvider}; 7 | use crate::routes::chains::models::ChainInfo as ServiceChainInfo; 8 | use crate::utils::context::RequestContext; 9 | use crate::utils::errors::ApiResult; 10 | use crate::utils::urls::build_absolute_uri; 11 | 12 | pub async fn get_chains_paginated( 13 | context: &RequestContext, 14 | cursor: &Option, 15 | ) -> ApiResult> { 16 | let page_metadata = cursor 17 | .as_ref() 18 | .map(|cursor| PageMetadata::from_cursor(cursor)); 19 | let url = config_uri!( 20 | "/v1/chains/?{}", 21 | page_metadata 22 | .as_ref() 23 | .unwrap_or(&PageMetadata::default()) 24 | .to_url_string() 25 | ); 26 | 27 | let body = RequestCached::new_from_context(url, context, ChainCache::Other) 28 | .request_timeout(chain_info_request_timeout()) 29 | .cache_duration(chain_info_cache_duration()) 30 | .execute() 31 | .await?; 32 | 33 | let page = serde_json::from_str::>(&body)?; 34 | 35 | Ok(page.map_inner(|link| map_link(context, link))) 36 | } 37 | 38 | pub async fn get_single_chain( 39 | context: &RequestContext, 40 | chain_id: &str, 41 | ) -> ApiResult { 42 | let info_provider = DefaultInfoProvider::new(&chain_id, &context); 43 | Ok(info_provider.chain_info().await?.into()) 44 | } 45 | 46 | fn map_link(context: &RequestContext, original_link: Option) -> Option { 47 | original_link.as_ref().map(|link| { 48 | let cursor = 49 | PageMetadata::from_cursor(link.split("?").collect::>().get(1).unwrap_or(&"")) 50 | .to_url_string(); 51 | let uri = build_absolute_uri( 52 | context, 53 | uri!(crate::routes::chains::routes::get_chains(Some(cursor))), 54 | ); 55 | String::from(uri) 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /src/routes/chains/mod.rs: -------------------------------------------------------------------------------- 1 | #[doc(hidden)] 2 | pub mod converters; 3 | #[doc(hidden)] 4 | pub mod handlers; 5 | pub mod models; 6 | pub mod routes; 7 | 8 | #[cfg(test)] 9 | mod tests; 10 | -------------------------------------------------------------------------------- /src/routes/chains/models.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | 3 | #[derive(Serialize, Debug, PartialEq, Clone)] 4 | #[serde(rename_all = "camelCase")] 5 | #[cfg_attr(test, derive(serde::Deserialize))] 6 | pub struct ChainInfo { 7 | pub transaction_service: String, 8 | // do we need to expose this? 9 | pub chain_id: String, 10 | pub chain_name: String, 11 | pub short_name: String, 12 | pub l2: bool, 13 | pub description: String, 14 | pub rpc_uri: RpcUri, 15 | pub safe_apps_rpc_uri: RpcUri, 16 | pub public_rpc_uri: RpcUri, 17 | pub block_explorer_uri_template: BlockExplorerUriTemplate, 18 | pub native_currency: NativeCurrency, 19 | pub theme: Theme, 20 | #[serde(skip_serializing_if = "Option::is_none")] 21 | pub ens_registry_address: Option, 22 | pub gas_price: Vec, 23 | pub disabled_wallets: Vec, 24 | pub features: Vec, 25 | } 26 | 27 | #[derive(Serialize, Debug, PartialEq, Clone)] 28 | #[serde(rename_all = "camelCase")] 29 | #[cfg_attr(test, derive(serde::Deserialize))] 30 | pub struct NativeCurrency { 31 | pub name: String, 32 | pub symbol: String, 33 | pub decimals: u64, 34 | pub logo_uri: String, 35 | } 36 | 37 | #[derive(Serialize, Debug, PartialEq, Clone)] 38 | #[serde(rename_all = "camelCase")] 39 | #[cfg_attr(test, derive(serde::Deserialize))] 40 | pub struct Theme { 41 | pub text_color: String, 42 | pub background_color: String, 43 | } 44 | 45 | #[derive(Serialize, Debug, PartialEq, Clone)] 46 | #[serde(tag = "type")] 47 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 48 | #[cfg_attr(test, derive(serde::Deserialize))] 49 | pub enum GasPrice { 50 | #[serde(rename_all = "camelCase")] 51 | Oracle { 52 | uri: String, 53 | gas_parameter: String, 54 | gwei_factor: String, 55 | }, 56 | #[serde(rename_all = "camelCase")] 57 | Fixed { 58 | wei_value: String, 59 | }, 60 | Unknown, 61 | } 62 | 63 | #[derive(Serialize, Debug, PartialEq, Clone)] 64 | #[serde(rename_all = "camelCase")] 65 | #[cfg_attr(test, derive(serde::Deserialize))] 66 | pub struct RpcUri { 67 | pub authentication: RpcAuthentication, 68 | pub value: String, 69 | } 70 | 71 | #[derive(Serialize, Debug, PartialEq, Clone)] 72 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 73 | #[cfg_attr(test, derive(serde::Deserialize))] 74 | pub enum RpcAuthentication { 75 | ApiKeyPath, 76 | NoAuthentication, 77 | #[serde(other)] 78 | Unknown, 79 | } 80 | 81 | #[derive(Serialize, Debug, PartialEq, Clone)] 82 | #[serde(rename_all = "camelCase")] 83 | #[cfg_attr(test, derive(serde::Deserialize))] 84 | pub struct BlockExplorerUriTemplate { 85 | pub address: String, 86 | pub tx_hash: String, 87 | pub api: String, 88 | } 89 | -------------------------------------------------------------------------------- /src/routes/chains/routes.rs: -------------------------------------------------------------------------------- 1 | use crate::cache::cache_operations::CacheResponse; 2 | use crate::cache::manager::ChainCache; 3 | use crate::config::chain_info_response_cache_duration; 4 | use crate::routes::chains::handlers::{get_chains_paginated, get_single_chain}; 5 | use crate::utils::context::RequestContext; 6 | use crate::utils::errors::ApiResult; 7 | use rocket::response::content; 8 | use rocket_okapi::openapi; 9 | 10 | /// `/v1/chains//`
11 | /// Returns [ChainInfo](crate::routes::chains::models::ChainInfo) 12 | /// 13 | /// # Chains 14 | /// 15 | /// This endpoint returns the [ChainInfo](crate::routes::chains::models::ChainInfo) for a given `chain_id` 16 | /// 17 | /// ## Path 18 | /// 19 | /// - `/v1/chains//`returns the `ChainInfo` for `` 20 | #[openapi(tag = "Chains")] 21 | #[get("/v1/chains/")] 22 | pub async fn get_chain( 23 | context: RequestContext, 24 | chain_id: String, 25 | ) -> ApiResult> { 26 | CacheResponse::new(&context, ChainCache::from(chain_id.as_str())) 27 | .duration(chain_info_response_cache_duration()) 28 | .resp_generator(|| get_single_chain(&context, &chain_id)) 29 | .execute() 30 | .await 31 | } 32 | 33 | /// `/v1/chains/`
34 | /// Returns a [Page](crate::common::models::page::Page) of [ChainInfo](crate::routes::chains::models::ChainInfo) 35 | /// 36 | /// # Chains 37 | /// 38 | /// Returns a paginated list of all the supported [ChainInfo](crate::routes::chains::models::ChainInfo) 39 | /// 40 | /// ## Path 41 | /// 42 | /// - `/v1/chains/` Returns the `ChainInfo` for our services supported networks 43 | #[openapi(tag = "Chains")] 44 | #[get("/v1/chains?")] 45 | pub async fn get_chains( 46 | context: RequestContext, 47 | cursor: Option, 48 | ) -> ApiResult> { 49 | CacheResponse::new(&context, ChainCache::Other) 50 | .duration(chain_info_response_cache_duration()) 51 | .resp_generator(|| get_chains_paginated(&context, &cursor)) 52 | .execute() 53 | .await 54 | } 55 | -------------------------------------------------------------------------------- /src/routes/chains/tests/mod.rs: -------------------------------------------------------------------------------- 1 | mod chains; 2 | mod routes; 3 | 4 | pub(super) const BACKEND_CHAINS_INFO_PAGE: &str = 5 | include_str!("json/backend_chains_info_page.json"); 6 | pub(super) const EXPECTED_CHAINS_INFO_PAGE: &str = 7 | include_str!("json/expected_chains_info_page.json"); 8 | -------------------------------------------------------------------------------- /src/routes/collectibles/mod.rs: -------------------------------------------------------------------------------- 1 | #[doc(hidden)] 2 | pub mod handlers; 3 | pub mod models; 4 | pub mod routes; 5 | 6 | #[cfg(test)] 7 | mod tests; 8 | -------------------------------------------------------------------------------- /src/routes/collectibles/models.rs: -------------------------------------------------------------------------------- 1 | use rocket_okapi::okapi::schemars; 2 | use rocket_okapi::okapi::schemars::JsonSchema; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Deserialize, Serialize, Debug, PartialEq, JsonSchema)] 6 | #[serde(rename_all = "camelCase")] 7 | pub struct Collectible { 8 | address: String, 9 | token_name: String, 10 | token_symbol: String, 11 | logo_uri: String, 12 | id: String, 13 | uri: Option, 14 | name: Option, 15 | description: Option, 16 | image_uri: Option, 17 | metadata: Option, 18 | } 19 | -------------------------------------------------------------------------------- /src/routes/collectibles/tests/mod.rs: -------------------------------------------------------------------------------- 1 | mod routes; 2 | -------------------------------------------------------------------------------- /src/routes/contracts/handlers.rs: -------------------------------------------------------------------------------- 1 | use crate::common::models::data_decoded::DataDecoded; 2 | use crate::providers::address_info::ContractInfo; 3 | use crate::providers::info::{DefaultInfoProvider, InfoProvider}; 4 | use crate::routes::contracts::models::DataDecoderRequest; 5 | use crate::utils::context::RequestContext; 6 | use crate::utils::errors::ApiResult; 7 | use crate::utils::http_client::Request; 8 | use serde_json::json; 9 | 10 | pub async fn request_data_decoded( 11 | context: &RequestContext, 12 | chain_id: &str, 13 | data_decoder_request: &DataDecoderRequest, 14 | ) -> ApiResult { 15 | let info_provider = DefaultInfoProvider::new(chain_id, context); 16 | let client = context.http_client(); 17 | let url = core_uri!(info_provider, "/v1/data-decoder/")?; 18 | let body = json!({"data": &data_decoder_request.data}); 19 | 20 | let request = { 21 | let mut request = Request::new(url); 22 | request.body(Some(body.to_string())); 23 | request 24 | }; 25 | 26 | let response_body = client.post(request).await?.body; 27 | Ok(serde_json::from_str::(&response_body)?) 28 | } 29 | 30 | pub async fn get_contract( 31 | context: &RequestContext, 32 | chain_id: &str, 33 | contract_address: &str, 34 | ) -> ApiResult { 35 | let info_provider = DefaultInfoProvider::new(chain_id, context); 36 | info_provider.contract_info(contract_address).await 37 | } 38 | -------------------------------------------------------------------------------- /src/routes/contracts/mod.rs: -------------------------------------------------------------------------------- 1 | #[doc(hidden)] 2 | pub mod handlers; 3 | pub mod models; 4 | pub mod routes; 5 | 6 | #[cfg(test)] 7 | mod tests; 8 | -------------------------------------------------------------------------------- /src/routes/contracts/models.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Deserialize, Serialize, Debug)] 4 | #[serde(rename_all = "camelCase")] 5 | pub struct DataDecoderRequest { 6 | pub data: String, 7 | pub to: Option, 8 | } 9 | -------------------------------------------------------------------------------- /src/routes/contracts/tests/mod.rs: -------------------------------------------------------------------------------- 1 | mod routes; 2 | -------------------------------------------------------------------------------- /src/routes/delegates/mod.rs: -------------------------------------------------------------------------------- 1 | #[doc(hidden)] 2 | mod handlers; 3 | mod models; 4 | pub mod routes; 5 | 6 | #[cfg(test)] 7 | mod tests; 8 | -------------------------------------------------------------------------------- /src/routes/delegates/models.rs: -------------------------------------------------------------------------------- 1 | use rocket_okapi::okapi::schemars; 2 | use rocket_okapi::okapi::schemars::JsonSchema; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Serialize, Deserialize, Debug)] 6 | #[serde(rename_all = "camelCase")] 7 | #[cfg_attr(test, derive(PartialEq))] 8 | pub struct Delegate { 9 | safe: Option, 10 | delegate: String, 11 | delegator: String, 12 | label: String, 13 | } 14 | 15 | #[derive(Serialize, Deserialize, JsonSchema)] 16 | #[serde(rename_all = "camelCase")] 17 | pub struct DelegateCreate { 18 | safe: Option, 19 | delegate: String, 20 | delegator: String, 21 | signature: String, 22 | label: String, 23 | } 24 | 25 | #[derive(Serialize, Deserialize, JsonSchema)] 26 | #[serde(rename_all = "camelCase")] 27 | pub struct DelegateDelete { 28 | delegate: String, 29 | delegator: String, 30 | signature: String, 31 | } 32 | 33 | #[derive(Serialize, Deserialize, JsonSchema)] 34 | #[serde(rename_all = "camelCase")] 35 | pub struct SafeDelegateDelete { 36 | safe: String, 37 | delegate: String, 38 | signature: String, 39 | } 40 | -------------------------------------------------------------------------------- /src/routes/delegates/routes.rs: -------------------------------------------------------------------------------- 1 | use crate::routes::delegates::handlers; 2 | use crate::routes::delegates::models::{DelegateCreate, DelegateDelete, SafeDelegateDelete}; 3 | use crate::utils::context::RequestContext; 4 | use crate::utils::errors::ApiResult; 5 | use rocket::response::content; 6 | use rocket::serde::json::Json; 7 | use rocket_okapi::openapi; 8 | 9 | #[openapi(tag = "Delegates")] 10 | #[get("/v1/chains//delegates?&&&