├── .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 |
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?&&&