├── .buildkite ├── docker-build-push.nix ├── pipeline.yml └── stack-cabal-sync.sh ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── failing_test.md │ └── task.md ├── PULL_REQUEST_TEMPLATE.md ├── RELEASE_TEMPLATE.md ├── images │ ├── badge-new.png │ └── cardano-logo.png ├── iohk-signature.gif └── workflows │ ├── main.yml │ └── release.yaml ├── .gitignore ├── .hlint.yaml ├── .stylish-haskell.yaml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bors.toml ├── cabal.project ├── cabal.project.freeze ├── config ├── .gitignore ├── postgres_db.example ├── postgres_password.example └── postgres_user.example ├── default.nix ├── docker-compose.yml ├── explorer-api ├── CHANGELOG.md ├── LICENSE ├── Setup.hs ├── app │ ├── Explorer │ │ └── Web │ │ │ ├── Validate.hs │ │ │ └── Validate │ │ │ ├── Address.hs │ │ │ ├── BlocksTxs.hs │ │ │ ├── ErrorHandling.hs │ │ │ ├── GenesisAddress.hs │ │ │ └── Random.hs │ ├── cardano-explorer-api-compare.hs │ ├── cardano-explorer-api-validate.hs │ └── cardano-explorer-api.hs ├── cardano-explorer-api.cabal ├── shell.nix ├── src │ └── Explorer │ │ ├── Web.hs │ │ └── Web │ │ ├── Api.hs │ │ ├── Api │ │ ├── HttpBridge.hs │ │ ├── HttpBridge │ │ │ └── AddressBalance.hs │ │ ├── Legacy.hs │ │ └── Legacy │ │ │ ├── AddressSummary.hs │ │ │ ├── BlockAddress.hs │ │ │ ├── BlockPagesTotal.hs │ │ │ ├── BlocksPages.hs │ │ │ ├── BlocksTxs.hs │ │ │ ├── EpochPage.hs │ │ │ ├── EpochSlot.hs │ │ │ ├── GenesisAddress.hs │ │ │ ├── GenesisPages.hs │ │ │ ├── GenesisSummary.hs │ │ │ ├── RedeemSummary.hs │ │ │ ├── StatsTxs.hs │ │ │ ├── TxLast.hs │ │ │ ├── TxsSummary.hs │ │ │ ├── Types.hs │ │ │ └── Util.hs │ │ ├── ClientTypes.hs │ │ ├── Error.hs │ │ ├── Query.hs │ │ └── Server.hs ├── swagger.yaml └── test │ ├── Main.hs │ └── Test │ └── Explorer │ └── Web │ ├── Api │ └── Legacy │ │ └── UtilSpec.hs │ └── ClientTypesSpec.hs ├── migration-guide ├── README.md ├── mkdocs.yml ├── requirements.txt └── src │ ├── explorer-api.md │ ├── index.md │ └── submit-api.md ├── nix-shell.project ├── nix ├── default.nix ├── docker.nix ├── haskell.nix ├── nixos │ ├── cardano-explorer-api-service.nix │ ├── cardano-submit-api-service.nix │ ├── default.nix │ └── module-list.nix ├── pkgs.nix ├── regenerate.sh ├── scripts.nix ├── sources.json ├── sources.nix ├── stack-shell.nix └── util.nix ├── release.nix ├── rest-common ├── LICENSE ├── cardano-rest-common.cabal └── src │ └── Cardano │ └── Rest │ ├── Parsers.hs │ ├── Types.hs │ └── Web.hs ├── shell.nix ├── stack.yaml ├── submit-api ├── CHANGELOG.md ├── LICENSE ├── app │ └── cardano-submit-api.hs ├── cardano-submit-api.cabal ├── config │ └── tx-submit-mainnet-config.yaml ├── src │ └── Cardano │ │ ├── TxSubmit.hs │ │ └── TxSubmit │ │ ├── CLI │ │ ├── Parsers.hs │ │ └── Types.hs │ │ ├── Config.hs │ │ ├── ErrorRender.hs │ │ ├── JsonOrphans.hs │ │ ├── Metrics.hs │ │ ├── Tracing │ │ └── ToObjectOrphans.hs │ │ ├── Tx.hs │ │ ├── Types.hs │ │ ├── Util.hs │ │ └── Web.hs ├── swagger.yaml ├── test │ ├── run.sh │ └── test.hs └── testing-tx-submit.md └── tests ├── Dockerfile ├── README.md ├── cardano-rest-tests.iml ├── conf.d ├── docker-compose.yml ├── pom.xml ├── src └── test │ ├── README.md │ ├── resources │ ├── config.properties │ ├── data │ │ ├── mainnet │ │ │ ├── addresses.txt │ │ │ ├── blocks.txt │ │ │ ├── blocks_and_addresses.txt │ │ │ └── transactions.txt │ │ └── testnet │ │ │ ├── addresses.txt │ │ │ ├── blocks.txt │ │ │ ├── blocks_and_addresses.txt │ │ │ └── transactions.txt │ ├── example_queries_to_get_data.sql │ └── schemas │ │ ├── valid-addresses-summary-address-schema.json │ │ ├── valid-block-blockhash-address-address-schema.json │ │ ├── valid-blocks-pages-schema.json │ │ ├── valid-blocks-pages-total-schema.json │ │ ├── valid-blocks-summary-blockhash-schema.json │ │ ├── valid-blocks-txs-blockhash-schema.json │ │ ├── valid-epochs-epoch-schema.json │ │ ├── valid-epochs-epoch-slot-schema.json │ │ ├── valid-genesis-address-pages-total-schema.json │ │ ├── valid-genesis-address-schema.json │ │ ├── valid-genesis-summary-schema.json │ │ ├── valid-stats-txs-schema.json │ │ ├── valid-supply-ada-schema.json │ │ ├── valid-txs-last-schema.json │ │ └── valid-txs-summary-txid-schema.json │ └── scala │ └── com │ └── cardano │ └── rest │ └── tests │ ├── DataStore.scala │ ├── functional │ ├── ApiResponseComparison.java │ ├── BaseTest.java │ ├── data_intensive │ │ ├── AddressesTest.java │ │ ├── BlocksTest.java │ │ ├── EpochsTest.java │ │ └── TransactionsTest.java │ ├── data_validation │ │ └── AddressesTest.java │ ├── oracle │ │ ├── AddressesTest.java │ │ ├── BlocksTest.java │ │ ├── EpochsTest.java │ │ ├── GenesisTest.java │ │ └── TransactionsTest.java │ └── smoke_tests │ │ ├── AddressesTest.java │ │ ├── BlocksTest.java │ │ ├── EpochsTest.java │ │ ├── GenesisTest.java │ │ └── TransactionsTest.java │ └── simulations │ └── performance │ ├── addresses │ ├── AddressesSummaryAddressSimulation.scala │ └── BlockBlockhashAddressAddressSimulation.scala │ ├── blocks │ ├── BlocksPagesSimulation.scala │ ├── BlocksPagesTotalSimulation.scala │ ├── BlocksSummaryBlockhashSimulation.scala │ └── BlocksTxsBlockhashSimulation.scala │ ├── epochs │ ├── EpochsEpochSimulation.scala │ └── EpochsEpochSlotsSlotSimulation.scala │ ├── genesis │ ├── GenesisAddressPagesTotalSimulation.scala │ ├── GenesisAddressSimulation.scala │ ├── GenesisSummarySimulation.scala │ └── SupplyAdaSimulation.scala │ └── transactions │ ├── StatsTxsSimulation.scala │ ├── TxsLastSimulation.scala │ └── TxsSummaryTxSimulation.scala ├── tests_functional-data-intensive.xml ├── tests_functional-data-validation.xml ├── tests_functional-smoke.xml ├── tests_oracle.xml ├── tests_performance-runner.sh └── tests_run-all.xml /.buildkite/docker-build-push.nix: -------------------------------------------------------------------------------- 1 | # This script will load nix-built docker image of cardano-explorer-api application 2 | # into the Docker daemon (must be running), and then push to the Docker Hub. 3 | # Credentials for the hub must already be installed with # "docker login". 4 | # 5 | # There is a little bit of bash logic to replace the default repo and 6 | # tag from the nix-build (../nix/docker.nix). 7 | 8 | { restPackages ? import ../. {} 9 | 10 | # Build system's Nixpkgs. We use this so that we have the same docker 11 | # version as the docker daemon. 12 | , hostPkgs ? import {} 13 | }: 14 | 15 | with hostPkgs; 16 | with hostPkgs.lib; 17 | 18 | let 19 | images = map impureCreated [ 20 | restPackages.dockerImages.explorerApi 21 | restPackages.dockerImages.submitApi 22 | ]; 23 | 24 | # Override Docker image, setting its creation date to the current time rather than the UNIX epoch. 25 | impureCreated = image: image.overrideAttrs (oldAttrs: { created = "now"; }) // { inherit (image) imageName; }; 26 | 27 | in 28 | writeScript "docker-build-push" ('' 29 | #!${runtimeShell} 30 | 31 | set -euo pipefail 32 | 33 | export PATH=${lib.makeBinPath [ docker gnused ]} 34 | 35 | '' + concatMapStringsSep "\n" (image: '' 36 | fullrepo="${image.imageName}" 37 | branch="''${BUILDKITE_BRANCH:-}" 38 | event="''${GITHUB_EVENT_NAME:-}" 39 | 40 | gitrev="${image.imageTag}" 41 | 42 | echo "Loading $fullrepo:$gitrev" 43 | docker load -i ${image} 44 | 45 | # If there is a release, it needs to be tagged with the release 46 | # version (e.g. "v0.0.28") AND the "latest" tag 47 | if [[ "$event" = release ]]; then 48 | ref="''${GITHUB_REF:-}" 49 | version="$(echo $ref | sed -e 's/refs\/tags\///')" 50 | 51 | echo "Tagging with a version number: $fullrepo:$version" 52 | docker tag $fullrepo:$gitrev $fullrepo:$version 53 | echo "Pushing $fullrepo:$version" 54 | docker push "$fullrepo:$version" 55 | 56 | echo "Tagging as latest" 57 | docker tag $fullrepo:$version $fullrepo:latest 58 | echo "Pushing $fullrepo:latest" 59 | docker push "$fullrepo:latest" 60 | # Every commit to master needs to be tagged with master 61 | elif [[ "$branch" = master ]]; then 62 | echo "Tagging as master" 63 | docker tag $fullrepo:$gitrev $fullrepo:$branch 64 | echo "Pushing $fullrepo:$branch" 65 | docker push "$fullrepo:$branch" 66 | fi 67 | 68 | echo "Cleaning up with docker system prune" 69 | docker system prune -f 70 | '') images) 71 | -------------------------------------------------------------------------------- /.buildkite/pipeline.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - label: 'check-cabal-project' 3 | command: 'nix-build ./nix -A iohkNix.checkCabalProject -o check-cabal-project.sh && ./check-cabal-project.sh' 4 | agents: 5 | system: x86_64-linux 6 | 7 | - label: 'stack-cabal-sync' 8 | command: 'nix-shell ./nix -A iohkNix.stack-cabal-sync-shell --run .buildkite/stack-cabal-sync.sh' 9 | agents: 10 | system: x86_64-linux 11 | 12 | - label: 'Docker Images' 13 | command: 14 | - "nix-build .buildkite/docker-build-push.nix -o docker-build-push" 15 | - "./docker-build-push" 16 | branches: master 17 | agents: 18 | system: x86_64-linux 19 | -------------------------------------------------------------------------------- /.buildkite/stack-cabal-sync.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eu 3 | 4 | # This script checks that the `stack.yaml` and `cabal.project` files have 5 | # consistent git hashes for the packages they depend on. We use 6 | # `cardano-repo-tool`'s `update-cabal-project` command which modifies 7 | # `cabal.project` to be consistent with `stack.yaml`s versions. If the 8 | # diff is non-empty, we know they're out of sync. 9 | 10 | # Check that functions are defined. 11 | HELP_TEXT="cardano-repo-tool not found." 12 | type cardano-repo-tool > /dev/null 2>&1 || { echo "${HELP_TEXT}"; exit 1; } 13 | HELP_TEXT="git not found." 14 | type git > /dev/null 2>&1 || { echo "${HELP_TEXT}"; exit 1; } 15 | 16 | # Update `cabal.project` from the `stack.yaml` file. 17 | cardano-repo-tool update-cabal-project 18 | 19 | git diff cabal.project | tee stack-cabal.patch 20 | 21 | if test "$(wc -l < stack-cabal.patch)" -gt 0 ; then 22 | buildkite-agent artifact upload stack-cabal.patch --job "$BUILDKITE_JOB_ID" 23 | exit 1 24 | fi 25 | 26 | exit 0 27 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [Makefile] 12 | indent_style = tab 13 | indent_size = 8 14 | 15 | [*.hs] 16 | indent_size = 4 17 | max_line_length = 80 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a bug report 4 | 5 | --- 6 | 7 | # Context 8 | 9 | 14 | 15 | 16 | | Information | - | 17 | | --- | --- | 18 | | Version | | 19 | | Platform | | 20 | | Installation | | 21 | 22 | 23 | # Steps to Reproduce 24 | 25 | 28 | 29 | 1. ... 30 | 31 | ## Expected behavior 32 | 33 | 36 | 37 | ## Actual behavior 38 | 39 | 43 | 44 | 45 | --- 46 | 47 | # Resolution 48 | 49 | 56 | 57 | 58 | --- 59 | 60 | # QA 61 | 62 | 66 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/failing_test.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Failing Test Case 3 | about: A failing test case scenario found locally or in CI 4 | 5 | --- 6 | 7 | # Context 8 | 9 | 14 | 15 | # Test Case 16 | 17 | 21 | 22 | 23 | # Failure / Counter-example 24 | 25 | 29 | 30 | 31 | --- 32 | 33 | # Resolution 34 | 35 | 42 | 43 | 44 | --- 45 | 46 | # QA 47 | 48 | 52 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/task.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Task 3 | about: Create a new Task 4 | 5 | --- 6 | 7 | # Context 8 | 9 | 14 | 15 | 16 | # Decision 17 | 18 | 23 | 24 | 25 | # Acceptance Criteria 26 | 27 | 34 | 35 | 36 | 37 | --- 38 | 39 | # Development 40 | 41 | 52 | 53 | # QA 54 | 55 | 59 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Issue Number 2 | 3 | 4 | 5 | 6 | # Overview 7 | 8 | 9 | 10 | - [ ] I have ... 11 | 12 | 13 | # Comments 14 | 15 | 16 | 17 | 27 | -------------------------------------------------------------------------------- /.github/images/badge-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/input-output-hk/cardano-rest/040b123b45af06060aae04479d92fada68820f12/.github/images/badge-new.png -------------------------------------------------------------------------------- /.github/images/cardano-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/input-output-hk/cardano-rest/040b123b45af06060aae04479d92fada68820f12/.github/images/cardano-logo.png -------------------------------------------------------------------------------- /.github/iohk-signature.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/input-output-hk/cardano-rest/040b123b45af06060aae04479d92fada68820f12/.github/iohk-signature.gif -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | test-job: 10 | name: "Tag / Build / Push Container" 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Trigger Buildkite Pipeline 14 | uses: buildkite/trigger-pipeline-action@v1.2.0 15 | env: 16 | BUILDKITE_API_ACCESS_TOKEN: ${{ secrets.Buildkite_Token }} 17 | PIPELINE: ${{ github.repository }} 18 | COMMIT: ${{ github.sha }} 19 | BRANCH: "master" 20 | MESSAGE: ":github: Triggered from a GitHub Action" 21 | BUILD_ENV_VARS: "{\"GITHUB_REF\": \"${{ github.ref }}\", \"GITHUB_EVENT_NAME\": \"${{ github.event_name }}\" }" 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Haskell ### 2 | dist 3 | dist-* 4 | cabal-dev 5 | *.o 6 | *.hi 7 | *.chi 8 | *.chs.h 9 | *.dyn_o 10 | *.dump-hi 11 | *.dyn_hi 12 | .hpc 13 | .hsenv 14 | .cabal-sandbox/ 15 | cabal.sandbox.config 16 | *.prof 17 | *.aux 18 | *.hp 19 | *.eventlog 20 | .stack-work/ 21 | cabal.project.local 22 | cabal.project.local~ 23 | .HTF/ 24 | .ghc.environment.* 25 | .ghci 26 | *.tix 27 | stack.yaml.lock 28 | 29 | ### Nix ### 30 | result* 31 | .stack-to-nix.cache 32 | 33 | ### Scala ### 34 | target/ 35 | .idea/ -------------------------------------------------------------------------------- /.hlint.yaml: -------------------------------------------------------------------------------- 1 | # HLint configuration file 2 | # https://github.com/ndmitchell/hlint 3 | ########################## 4 | 5 | # This file contains a template configuration file, which is typically 6 | # placed as .hlint.yaml in the root of your project 7 | 8 | 9 | # Specify additional command line arguments 10 | # 11 | # - arguments: [--color, --cpp-simple, -XQuasiQuotes] 12 | 13 | 14 | # Control which extensions/flags/modules/functions can be used 15 | # 16 | # - extensions: 17 | # - default: false # all extension are banned by default 18 | # - name: [PatternGuards, ViewPatterns] # only these listed extensions can be used 19 | # - {name: CPP, within: CrossPlatform} # CPP can only be used in a given module 20 | # 21 | # - flags: 22 | # - {name: -w, within: []} # -w is allowed nowhere 23 | # 24 | # - modules: 25 | # - {name: [Data.Set, Data.HashSet], as: Set} # if you import Data.Set qualified, it must be as 'Set' 26 | # - {name: Control.Arrow, within: []} # Certain modules are banned entirely 27 | # 28 | # - functions: 29 | # - {name: unsafePerformIO, within: []} # unsafePerformIO can only appear in no modules 30 | 31 | - modules: 32 | # Enforce some common qualified imports aliases across the codebase 33 | - {name: [Data.Aeson, Data.Aeson.Types], as: Aeson} 34 | - {name: [Data.ByteArray], as: BA} 35 | - {name: [Data.ByteString.Base16], as: B16} 36 | - {name: [Data.ByteString.Char8], as: B8} 37 | - {name: [Data.ByteString.Lazy], as: BL} 38 | - {name: [Data.ByteString], as: BS} 39 | - {name: [Data.Foldable], as: F} 40 | - {name: [Data.List.NonEmpty], as: NE} 41 | - {name: [Data.List], as: L} 42 | - {name: [Data.Map.Strict], as: Map} 43 | - {name: [Data.Sequence], as: Seq} 44 | - {name: [Data.Set, Data.HashSet], as: Set} 45 | - {name: [Data.Text, Data.Text.Encoding], as: T} 46 | - {name: [Data.Vector], as: V} 47 | 48 | # Ignore some build-in rules 49 | - ignore: {name: "Reduce duplication"} # This is a decision left to developers and reviewers 50 | - ignore: {name: "Redundant bracket"} # Not everyone knows precedences of every operators in Haskell. Brackets help readability. 51 | - ignore: {name: "Redundant do"} # Just an annoying hlint built-in, GHC may remove redundant do if he wants 52 | - ignore: {name: "Monoid law, left identity"} # Using 'mempty' can be useful to vertically-align elements 53 | - ignore: {name: "Use ?~"} # It's actually much clearer to do (.~ Just ...) than having to load yet-another-lens-operator 54 | 55 | # Add custom hints for this project 56 | # 57 | # Will suggest replacing "wibbleMany [myvar]" with "wibbleOne myvar" 58 | # - error: {lhs: "wibbleMany [x]", rhs: wibbleOne x} 59 | 60 | 61 | # Turn on hints that are off by default 62 | # 63 | # Ban "module X(module X) where", to require a real export list 64 | # - warn: {name: Use explicit module export list} 65 | # 66 | # Replace a $ b $ c with a . b $ c 67 | # - group: {name: dollar, enabled: true} 68 | # 69 | # Generalise map to fmap, ++ to <> 70 | # - group: {name: generalise, enabled: true} 71 | 72 | 73 | # Ignore some builtin hints 74 | # - ignore: {name: Use let} 75 | # - ignore: {name: Use const, within: SpecialModule} # Only within certain modules 76 | 77 | 78 | # Define some custom infix operators 79 | # - fixity: infixr 3 ~^#^~ 80 | 81 | 82 | # To generate a suitable file for HLint do: 83 | # $ hlint --default > .hlint.yaml 84 | -------------------------------------------------------------------------------- /.stylish-haskell.yaml: -------------------------------------------------------------------------------- 1 | # Stylish-haskell configuration file 2 | # 3 | # See `stylish-haskell --defaults` or 4 | # https://github.com/jaspervdj/stylish-haskell/blob/master/data/stylish-haskell.yaml 5 | # for usage. 6 | 7 | columns: 80 # Should match .editorconfig 8 | steps: 9 | - imports: 10 | align: none 11 | empty_list_align: inherit 12 | list_align: new_line 13 | list_padding: 4 14 | long_list_align: new_line_multiline 15 | pad_module_names: false 16 | separate_lists: true 17 | space_surround: true 18 | 19 | - language_pragmas: 20 | align: false 21 | remove_redundant: true 22 | style: vertical 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | - [cardano-explorer-api](./explorer-api/CHANGELOG.md) 4 | - [cardano-submit-api](./submit-api/CHANGELOG.md) 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Cardano REST 3 |

4 | 5 |

6 | 7 |

8 | 9 |

10 | 11 | 12 |

13 | 14 |
15 | 16 | ## Overview 17 | 18 | Cardano REST provides a set of APIs for interacting with on-chain data through JSON over HTTP. 19 | 20 | ## Deprecation Notice 21 | 22 | :warning: **cardano-rest is now DEPRECATED. The `explorer-api` and `submit-api` will NOT survive the Alonzo hard-fork.** :warning: 23 | 24 | As a replacement, the following tools are at your disposal: 25 | 26 | - [cardano-graphql](https://github.com/input-output-hk/cardano-graphql) 27 | - [cardano-rosetta](https://github.com/input-output-hk/cardano-rosetta) 28 | - [cardano-submit-api](https://github.com/input-output-hk/cardano-node/tree/master/cardano-submit-api) 29 | - [cardano-wallet](https://github.com/input-output-hk/cardano-wallet) 30 | 31 | Users with an existing integration are encouraged to look at the [migration guide](https://input-output-hk.github.io/cardano-rest/migration-guide/). 32 | 33 | ## Documentation 34 | 35 | - [API Documentation (cardano-explorer-api)](https://input-output-hk.github.io/cardano-rest/explorer-api) 36 | - [API Documentation (cardano-submit-api)](https://input-output-hk.github.io/cardano-rest/submit-api) 37 | 38 |
39 | 40 |

41 | 42 |

43 | -------------------------------------------------------------------------------- /bors.toml: -------------------------------------------------------------------------------- 1 | status = [ 2 | "buildkite/cardano-rest", 3 | "ci/hydra:Cardano:cardano-rest:required", 4 | ] 5 | timeout_sec = 7200 6 | required_approvals = 1 7 | block_labels = [ "WIP", "DO NOT MERGE" ] 8 | delete_merged_branches = true 9 | -------------------------------------------------------------------------------- /config/.gitignore: -------------------------------------------------------------------------------- 1 | postgres_db 2 | postgres_password 3 | postgres_user 4 | -------------------------------------------------------------------------------- /config/postgres_db.example: -------------------------------------------------------------------------------- 1 | cexplorer 2 | -------------------------------------------------------------------------------- /config/postgres_password.example: -------------------------------------------------------------------------------- 1 | Yanrg6mM 2 | -------------------------------------------------------------------------------- /config/postgres_user.example: -------------------------------------------------------------------------------- 1 | cardano 2 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { system ? builtins.currentSystem 2 | , crossSystem ? null 3 | # allows to cutomize haskellNix (ghc and profiling, see ./nix/haskell.nix) 4 | , config ? {} 5 | # override scripts with custom configuration 6 | , customConfig ? {} 7 | # allows to override dependencies of the project without modifications, 8 | # eg. to test build against local checkout of nixpkgs and iohk-nix: 9 | # nix build -f default.nix cardano-explorer-api --arg sourcesOverride '{ 10 | # iohk-nix = ../iohk-nix; 11 | # }' 12 | , sourcesOverride ? {} 13 | # pinned version of nixpkgs augmented with overlays (iohk-nix and our packages). 14 | , pkgs ? import ./nix { inherit system crossSystem config sourcesOverride; } 15 | , gitrev ? pkgs.iohkNix.commitIdFromGitRepoOrZero ./.git 16 | }: 17 | with pkgs; with commonLib; 18 | let 19 | 20 | haskellPackages = recRecurseIntoAttrs 21 | # the Haskell.nix package set, reduced to local packages. 22 | (selectProjectPackages cardanoRestHaskellPackages); 23 | 24 | scripts = callPackage ./nix/scripts.nix { inherit customConfig; }; 25 | 26 | # NixOS tests 27 | #nixosTests = import ./nix/nixos/tests { 28 | # inherit pkgs; 29 | #}; 30 | 31 | dockerImages = let 32 | defaultConfig = rec { 33 | services.cardano-submit-api.socketPath = "/node-ipc/node.socket"; 34 | services.cardano-explorer-api.listenAddress = "0.0.0.0"; 35 | services.cardano-explorer-api.pgpassFile = "/configuration/pgpass"; 36 | services.cardano-submit-api.listenAddress = "0.0.0.0"; 37 | }; 38 | customConfig' = lib.mkMerge [ defaultConfig customConfig ]; 39 | in pkgs.callPackage ./nix/docker.nix { 40 | inherit (self) cardano-explorer-api; 41 | inherit (self) cardano-submit-api; 42 | inherit customConfig; 43 | scripts = callPackage ./nix/scripts.nix { customConfig = customConfig'; }; 44 | }; 45 | 46 | self = { 47 | inherit haskellPackages 48 | scripts 49 | dockerImages 50 | #nixosTests 51 | ; 52 | 53 | # Grab the executable component of our package. 54 | inherit (haskellPackages.cardano-explorer-api.components.exes) 55 | cardano-explorer-api cardano-explorer-api-compare cardano-explorer-api-validate; 56 | inherit (haskellPackages.cardano-submit-api.components.exes) 57 | cardano-submit-api; 58 | 59 | # `tests` are the test suites which have been built. 60 | tests = collectComponents' "tests" haskellPackages; 61 | # `benchmarks` (only built, not run). 62 | benchmarks = collectComponents' "benchmarks" haskellPackages; 63 | 64 | checks = recurseIntoAttrs { 65 | # `checks.tests` collect results of executing the tests: 66 | tests = collectChecks haskellPackages; 67 | }; 68 | 69 | shell = import ./shell.nix { 70 | inherit pkgs; 71 | withHoogle = true; 72 | }; 73 | }; 74 | in self 75 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.5" 2 | 3 | services: 4 | postgres: 5 | image: postgres:11.5-alpine 6 | command: postgres -c 'max_pred_locks_per_transaction=128' -c 'max_locks_per_transaction=128' 7 | environment: 8 | - POSTGRES_LOGGING=true 9 | - POSTGRES_DB_FILE=/run/secrets/postgres_db 10 | - POSTGRES_PASSWORD_FILE=/run/secrets/postgres_password 11 | - POSTGRES_USER_FILE=/run/secrets/postgres_user 12 | secrets: 13 | - postgres_password 14 | - postgres_user 15 | - postgres_db 16 | volumes: 17 | - postgres:/var/lib/postgresql/data 18 | ports: 19 | - 5432:5432 20 | restart: on-failure 21 | logging: 22 | driver: "json-file" 23 | options: 24 | max-size: "400k" 25 | max-file: "20" 26 | 27 | cardano-node: 28 | image: inputoutput/cardano-node:${CARDANO_NODE_VERSION:-1.25.1} 29 | environment: 30 | - NETWORK=${NETWORK:-mainnet} 31 | volumes: 32 | - node-db:/data/db 33 | - node-ipc:/ipc 34 | logging: 35 | driver: "json-file" 36 | options: 37 | max-size: "200k" 38 | max-file: "10" 39 | 40 | cardano-db-sync: 41 | image: inputoutput/cardano-db-sync:${CARDANO_DB_SYNC_VERSION:-8.0.0} 42 | environment: 43 | - NETWORK=${NETWORK:-mainnet} 44 | - POSTGRES_HOST=postgres 45 | - POSTGRES_PORT=5432 46 | depends_on: 47 | - cardano-node 48 | - postgres 49 | secrets: 50 | - postgres_password 51 | - postgres_user 52 | - postgres_db 53 | volumes: 54 | - db-sync-data:/var/lib/cdbsync 55 | - node-ipc:/node-ipc 56 | restart: on-failure 57 | logging: 58 | driver: "json-file" 59 | options: 60 | max-size: "200k" 61 | max-file: "10" 62 | 63 | cardano-explorer-api: 64 | image: inputoutput/cardano-explorer-api:${CARDANO_EXPLORER_API_VERSION:-3.1.2} 65 | depends_on: 66 | - postgres 67 | - cardano-db-sync 68 | environment: 69 | - NETWORK=${NETWORK:-mainnet} 70 | - POSTGRES_HOST=postgres 71 | - POSTGRES_PORT=5432 72 | secrets: 73 | - postgres_password 74 | - postgres_user 75 | - postgres_db 76 | ports: 77 | - 8100:8100 78 | restart: on-failure 79 | logging: 80 | driver: "json-file" 81 | options: 82 | max-size: "200k" 83 | max-file: "10" 84 | 85 | cardano-submit-api: 86 | image: inputoutput/cardano-submit-api:${CARDANO_SUBMIT_API_VERSION:-3.1.2} 87 | environment: 88 | - NETWORK=${NETWORK:-mainnet} 89 | depends_on: 90 | - cardano-node 91 | volumes: 92 | - node-ipc:/node-ipc 93 | ports: 94 | - 8090:8090 95 | restart: on-failure 96 | logging: 97 | driver: "json-file" 98 | options: 99 | max-size: "200k" 100 | max-file: "10" 101 | 102 | secrets: 103 | postgres_db: 104 | file: ./config/postgres_db 105 | postgres_password: 106 | file: ./config/postgres_password 107 | postgres_user: 108 | file: ./config/postgres_user 109 | 110 | volumes: 111 | db-sync-data: 112 | postgres: 113 | node-db: 114 | node-ipc: 115 | -------------------------------------------------------------------------------- /explorer-api/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 3.1.2 -- January 2021 4 | 5 | * Upgrade dependencies to more recent versions of the Cardano eco-system (cardano-node 1.25.1, cardano-db-sync 8.0.0) 6 | 7 | ## 3.1.1 -- December 2020 8 | 9 | * ø (patch in submit-api). 10 | 11 | ## 3.1.0 -- December 2020 12 | 13 | * Fix an internal error with 'InnerJoin' requests on block's transaction summary. 14 | * Fix inconsistency in the OpenAPI 2.0 specification (and consequently, the documentation). 15 | * Upgrade dependencies to more recent version of the Cardano eco-system (cardano-db-sync-7.1.0 & cardano-node 1.24.2) 16 | 17 | ## 3.0.0 -- October 2020 18 | 19 | * Upgrade dependencies to more recent version of the Cardano eco-system (cardano-db-sync-6.0.0) 20 | 21 | ## 2.1.3 -- August 2020 22 | 23 | * Fix bug regarding Shelley addresses not being decoded correctly (#75) 24 | * Fix block number overflow (#79) 25 | 26 | ## 2.1.2 -- July 2020 27 | 28 | * Fix bug with cbeSlot (#77) 29 | * Update dependencies for compatibility with `cardano-db-sync` 3.1.0. 30 | 31 | ## 2.1.1 -- July 2020 32 | 33 | * Update dependencies for compatibility with `cardano-node` 1.18.0. 34 | 35 | ## 2.1.0 -- July 2020 36 | 37 | * You can now specify the webserver settings on the command line. 38 | * Moving some sharable code into a cardano-rest-common/ package. 39 | * Tidyied the way the explorer prints its connection details on startup. 40 | * Adding a '--random-port' flag.. 41 | * Internal refactoring. 42 | * Bugfixes for cardano-explorer-api-validate. 43 | * Fixed defaults for the command line flags. 44 | 45 | ## 2.0.0 -- March 2020 46 | 47 | * Moved 'cardano-explorer-api' into its own repository 48 | * Renamed service and build artifacts 49 | 50 | ## 1.3.0 -- January 2020 51 | 52 | * Update dependencies to latest versions. 53 | * Docker image: log all runit services to stdout 54 | * Initial documentation on how to use build and run the components in docker 55 | 56 | ## 1.2.2 -- January 2020 57 | 58 | * Swagger docs https://input-output-hk.github.io/cardano-explorer/ 59 | * Fix /api/blocks/txs/{blkHash} endpoint (#195) 60 | * Fix Ada/Lovelace denomination bug (#197) 61 | * Fix JSON rendering for addresses to match old API 62 | * Add validation for genesis address paging (#219) 63 | * Add additional tests (#222, #227) 64 | * Update dependencies to latest versions. 65 | 66 | ## 1.2.1 -- January 2020 67 | 68 | * Update dependencies to latest versions. 69 | 70 | ## 1.2.0 -- December 2019 71 | 72 | * Update dependencies to latest versions. 73 | 74 | ## 1.1.0 -- December 2019 75 | 76 | * Renamed from cardano-explorer to cardano-explorer-webapi (#193). 77 | 78 | * Remove unused/unsupported endpoints (#198) 79 | 80 | * Provide more info in /api/txs/summary/{txHash} endpoint (#174). 81 | 82 | Specifically, for each transaction replace '(address, coin)' with 83 | a struct that also contains the transaction '(hash, index)' pair. 84 | 85 | * Provide more info about transactions in some endpoints (#174, #131). 86 | 87 | Specifically: 88 | * /api/blocks/txs/{blockHash} 89 | * /api/txs/summary/{txHash} 90 | 91 | * Add ChainTip info to address endpoints (#177, #130) 92 | 93 | * Run all transactions at an isolation level of Serializable (#189, #133) 94 | 95 | * Document atomicity of API interactions 96 | 97 | * Fix validation error (#191) 98 | 99 | * Add additional tests to validiate against the old API (#190) 100 | 101 | ## 1.0.0 -- November 2019 102 | 103 | * First release of new explorer API server based on new cardano-explorer-node. 104 | * New modular design, no longer integrated with the cardano-sl node. 105 | * Gets all data from a local PostgreSQL DB. 106 | * Compatible with the old explorer HTTP API and old web frontend. 107 | * Some small compatible API extensions 108 | -------------------------------------------------------------------------------- /explorer-api/Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /explorer-api/app/Explorer/Web/Validate.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | module Explorer.Web.Validate 4 | ( runValidation 5 | ) where 6 | 7 | import Cardano.Db 8 | ( readPGPassFileEnv, toConnectionString ) 9 | import Control.Monad.IO.Class 10 | ( liftIO ) 11 | import Control.Monad.Logger 12 | ( runNoLoggingT ) 13 | import Data.Text.ANSI 14 | ( yellow ) 15 | import Database.Persist.Postgresql 16 | ( withPostgresqlConn ) 17 | import Database.Persist.Sql 18 | ( SqlBackend ) 19 | import Explorer.Web.Api.Legacy.Util 20 | ( runQuery, textShow ) 21 | import Explorer.Web.Validate.Address 22 | ( validateAddressSummary, validateRedeemAddressSummary ) 23 | import Explorer.Web.Validate.BlocksTxs 24 | ( validateBlocksTxs ) 25 | import Explorer.Web.Validate.GenesisAddress 26 | ( validateGenesisAddressPaging ) 27 | 28 | import qualified Data.Text.IO as Text 29 | 30 | runValidation :: Word -> IO () 31 | runValidation count = do 32 | pgconfig <- readPGPassFileEnv 33 | putStrLn "" 34 | runNoLoggingT . 35 | withPostgresqlConn (toConnectionString pgconfig) $ \backend -> 36 | liftIO $ loop backend 1 37 | where 38 | loop :: SqlBackend -> Word -> IO () 39 | loop backend n 40 | | n > count = pure () 41 | | otherwise = do 42 | Text.putStrLn $ yellow ("Test #" <> textShow n <> ":") 43 | validate backend 44 | loop backend (n + 1) 45 | 46 | validate :: SqlBackend -> IO () 47 | validate backend = runQuery backend $ do 48 | validateRedeemAddressSummary 49 | validateAddressSummary 50 | validateGenesisAddressPaging 51 | validateBlocksTxs 52 | liftIO $ putStrLn "" 53 | -------------------------------------------------------------------------------- /explorer-api/app/Explorer/Web/Validate/BlocksTxs.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE TupleSections #-} 3 | 4 | module Explorer.Web.Validate.BlocksTxs 5 | ( validateBlocksTxs 6 | ) where 7 | 8 | import Control.Monad.IO.Class 9 | ( MonadIO, liftIO ) 10 | import qualified Data.List as List 11 | import Data.Text 12 | ( Text ) 13 | import qualified Data.Text as Text 14 | import Data.Text.ANSI 15 | ( green, red ) 16 | import qualified Data.Text.IO as Text 17 | 18 | import Database.Persist.Sql 19 | ( SqlPersistT ) 20 | 21 | import Explorer.Web 22 | ( CTxAddressBrief (..), CTxBrief (..), queryBlocksTxs ) 23 | import Explorer.Web.Api.Legacy.Util 24 | ( bsBase16Encode ) 25 | import Explorer.Web.Validate.ErrorHandling 26 | ( handleExplorerError, handleLookupFail ) 27 | import Explorer.Web.Validate.Random 28 | ( queryRandomBlockHash ) 29 | 30 | import System.Exit 31 | ( exitFailure ) 32 | 33 | import Text.Show.Pretty 34 | ( ppShow ) 35 | 36 | validateBlocksTxs :: MonadIO m => SqlPersistT m () 37 | validateBlocksTxs = do 38 | (blkHash, txs) <- do 39 | blkHash <- handleLookupFail =<< queryRandomBlockHash 40 | (blkHash,) <$> (handleExplorerError =<< queryBlocksTxs blkHash 100 0) 41 | liftIO $ do 42 | validateInputsUnique (bsBase16Encode blkHash) $ List.sortOn ctaAddress (concatMap ctbInputs txs) 43 | validateOutputsUnique (bsBase16Encode blkHash) $ List.sortOn ctaAddress (concatMap ctbOutputs txs) 44 | 45 | 46 | -- ------------------------------------------------------------------------------------------------- 47 | 48 | validateInputsUnique :: Text -> [CTxAddressBrief] -> IO () 49 | validateInputsUnique blkHash tabs = do 50 | mapM_ Text.putStr [ " Inputs for block " , shortenTxHash blkHash, " are unique: " ] 51 | if length tabs == length (List.nub tabs) 52 | then Text.putStrLn $ green "ok" 53 | else do 54 | Text.putStrLn $ red "validateInputsUnique failed" 55 | exitFailure 56 | 57 | -- https://github.com/input-output-hk/cardano-explorer/issues/195 58 | validateOutputsUnique :: Text -> [CTxAddressBrief] -> IO () 59 | validateOutputsUnique blkHash tabs = do 60 | mapM_ Text.putStr [ " Outputs for block " , shortenTxHash blkHash, " are unique: " ] 61 | if length tabs == length (List.nub tabs) 62 | then Text.putStrLn $ green "ok" 63 | else do 64 | Text.putStrLn $ red "failed\n Duplicate entries in:" 65 | mapM_ (\x -> putStrLn $ " " ++ x) $ lines (ppShow tabs) 66 | exitFailure 67 | 68 | -- ------------------------------------------------------------------------------------------------- 69 | 70 | shortenTxHash :: Text -> Text 71 | shortenTxHash txh = 72 | mconcat [Text.take 10 txh, "...", Text.drop (Text.length txh - 10) txh] 73 | -------------------------------------------------------------------------------- /explorer-api/app/Explorer/Web/Validate/ErrorHandling.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | module Explorer.Web.Validate.ErrorHandling 4 | ( handleLookupFail 5 | , handleExplorerError 6 | ) where 7 | 8 | import Cardano.Db 9 | ( LookupFail, renderLookupFail ) 10 | import Control.Monad.IO.Class 11 | ( MonadIO, liftIO ) 12 | import Data.Text.ANSI 13 | ( red ) 14 | import Explorer.Web.Error 15 | ( ExplorerError (..), renderExplorerError ) 16 | import System.Exit 17 | ( exitFailure ) 18 | 19 | import qualified Data.Text.IO as Text 20 | 21 | handleLookupFail :: MonadIO m => Either LookupFail a -> m a 22 | handleLookupFail ela = 23 | case ela of 24 | Left err -> liftIO $ do 25 | Text.putStrLn $ red (renderLookupFail err) 26 | exitFailure 27 | Right v -> pure v 28 | 29 | handleExplorerError :: MonadIO m => Either ExplorerError a -> m a 30 | handleExplorerError eea = 31 | case eea of 32 | Left err -> liftIO $ do 33 | Text.putStrLn $ red (renderExplorerError err) 34 | exitFailure 35 | Right v -> pure v 36 | -------------------------------------------------------------------------------- /explorer-api/app/Explorer/Web/Validate/GenesisAddress.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | module Explorer.Web.Validate.GenesisAddress 4 | ( validateGenesisAddressPaging 5 | ) where 6 | 7 | import Cardano.Db 8 | ( EntityField (..), LookupFail (..), listToMaybe ) 9 | import Control.Monad.IO.Class 10 | ( MonadIO, liftIO ) 11 | import Data.Text.ANSI 12 | ( green, red ) 13 | import Database.Esqueleto 14 | ( InnerJoin (..) 15 | , Value (..) 16 | , countRows 17 | , from 18 | , isNothing 19 | , on 20 | , select 21 | , where_ 22 | , (==.) 23 | , (^.) 24 | ) 25 | import Database.Persist.Sql 26 | ( SqlPersistT ) 27 | import Explorer.Web 28 | ( CAddress (..), CGenesisAddressInfo (..), queryAllGenesisAddresses ) 29 | import Explorer.Web.Api.Legacy.Types 30 | ( PageNo (..), PageSize (..) ) 31 | import Explorer.Web.Validate.ErrorHandling 32 | ( handleLookupFail ) 33 | import System.Exit 34 | ( exitFailure ) 35 | import System.Random 36 | ( randomRIO ) 37 | 38 | import qualified Data.List as List 39 | import qualified Data.Text.IO as Text 40 | 41 | -- | Validate that all address have a balance >= 0. 42 | validateGenesisAddressPaging :: MonadIO m => SqlPersistT m () 43 | validateGenesisAddressPaging = do 44 | (addr1, addr2) <- do 45 | pageSize <- genRandomPageSize 46 | pageNo <- handleLookupFail =<< genRandomPageNo pageSize 47 | page1 <- queryAllGenesisAddresses pageNo pageSize 48 | page2 <- queryAllGenesisAddresses (nextPageNo pageNo) pageSize 49 | pure (extractAddresses page1, extractAddresses page2) 50 | liftIO $ if length (List.nub $ addr1 ++ addr2) == length addr1 + length addr2 51 | then Text.putStrLn $ " Adjacent pages for Genesis addresses do not overlap: " <> green "ok" 52 | else reportIntersectFail addr1 addr2 53 | 54 | 55 | extractAddresses :: [CGenesisAddressInfo] -> [CAddress] 56 | extractAddresses = List.map cgaiCardanoAddress 57 | 58 | genRandomPageNo :: MonadIO m => PageSize -> SqlPersistT m (Either LookupFail PageNo) 59 | genRandomPageNo (PageSize pageSize) = do 60 | res <- select . from $ \ (txOut `InnerJoin` tx `InnerJoin` blk) -> do 61 | on (blk ^. BlockId ==. tx ^. TxBlockId) 62 | on (tx ^. TxId ==. txOut ^. TxOutTxId) 63 | where_ (isNothing (blk ^. BlockPreviousId)) 64 | pure countRows 65 | case listToMaybe res of 66 | Nothing -> pure $ Left (DbLookupMessage "genRandomPageNo: Empty Block table") 67 | Just (Value addrCount) 68 | | addrCount <= 3 * pageSize -> 69 | pure $ Left (DbLookupMessage "genRandomPageNo: Genesis address count is too low") 70 | | otherwise -> do 71 | offset <- max addrCount <$> liftIO (randomRIO (1, addrCount - 3 * pageSize)) 72 | pure $ Right (PageNo $ offset `div` pageSize) 73 | 74 | genRandomPageSize :: MonadIO m => SqlPersistT m PageSize 75 | genRandomPageSize = PageSize <$> liftIO (randomRIO (2, 50)) 76 | 77 | nextPageNo :: PageNo -> PageNo 78 | nextPageNo (PageNo x) = PageNo (x + 1) 79 | 80 | reportIntersectFail :: [CAddress] -> [CAddress] -> IO () 81 | reportIntersectFail _addr1 _addr2 = do 82 | Text.putStrLn $ " Adjacent pages for Genesis addresses do not overlap: " <> red "fail" 83 | exitFailure 84 | -------------------------------------------------------------------------------- /explorer-api/app/Explorer/Web/Validate/Random.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE ScopedTypeVariables #-} 4 | {-# LANGUAGE TypeFamilies #-} 5 | 6 | module Explorer.Web.Validate.Random 7 | ( queryRandomAddress 8 | , queryRandomBlockHash 9 | , queryRandomRedeemAddress 10 | ) where 11 | 12 | import Cardano.Db 13 | ( BlockId, EntityField (..), Key (..), LookupFail (..), maybeToEither ) 14 | import Control.Monad.IO.Class 15 | ( MonadIO ) 16 | import Control.Monad.Trans.Reader 17 | ( ReaderT ) 18 | import Data.ByteString.Char8 19 | ( ByteString ) 20 | import Data.Maybe 21 | ( listToMaybe ) 22 | import Data.Text 23 | ( Text ) 24 | import Database.Esqueleto 25 | ( InnerJoin (..) 26 | , SqlBackend 27 | , SqlExpr 28 | , SqlQuery 29 | , Value (..) 30 | , asc 31 | , from 32 | , limit 33 | , on 34 | , orderBy 35 | , select 36 | , val 37 | , where_ 38 | , (==.) 39 | , (>.) 40 | , (^.) 41 | ) 42 | import Database.Esqueleto.PostgreSQL 43 | ( random_ ) 44 | 45 | -- | Get a random address. 46 | queryRandomAddress :: MonadIO m => ReaderT SqlBackend m (Either LookupFail Text) 47 | queryRandomAddress = do 48 | res <- select . from $ \ txOut -> do 49 | firstRandomRow 50 | pure (txOut ^. TxOutAddress) 51 | pure $ maybeToEither errMsg unValue (listToMaybe res) 52 | where 53 | errMsg :: LookupFail 54 | errMsg = DbLookupMessage "queryRandomAddress: Lookup random address failed" 55 | 56 | queryRandomBlockHash :: MonadIO m => ReaderT SqlBackend m (Either LookupFail ByteString) 57 | queryRandomBlockHash = do 58 | res <- select . from $ \ blk -> do 59 | where_ (blk ^. BlockTxCount >. val 0) 60 | firstRandomRow 61 | limit 1 62 | pure (blk ^. BlockHash) 63 | pure $ maybeToEither errMsg unValue (listToMaybe res) 64 | where 65 | errMsg :: LookupFail 66 | errMsg = DbLookupMessage "queryRandomBlockHash: Lookup random block failed" 67 | 68 | queryRandomRedeemAddress :: MonadIO m => ReaderT SqlBackend m (Either LookupFail Text) 69 | queryRandomRedeemAddress = do 70 | res <- select . from $ \ (tx `InnerJoin` txOut) -> do 71 | -- Block 1 contains all the TxOuts from the Genesis Distribution. 72 | where_ (tx ^. TxBlockId ==. val (mkBlockId 1)) 73 | on (tx ^. TxId ==. txOut ^. TxOutTxId) 74 | firstRandomRow 75 | pure (txOut ^. TxOutAddress) 76 | pure $ maybeToEither errMsg unValue (listToMaybe res) 77 | where 78 | errMsg :: LookupFail 79 | errMsg = DbLookupMessage "queryRandomRedeemAddress: Lookup random address failed" 80 | 81 | mkBlockId :: Word -> BlockId 82 | mkBlockId = BlockKey . fromIntegral 83 | 84 | -- | Filter a table to pick a row at random row from the database. 85 | -- 86 | -- Note: This will be fine for ad-hoc queries, but if you use it for 87 | -- anything high-performance, you should test it thoroughly first and 88 | -- consider alternative approaches. 89 | firstRandomRow :: SqlQuery () 90 | firstRandomRow = do 91 | orderBy [asc (random_ :: SqlExpr (Value Int))] 92 | limit 1 93 | -------------------------------------------------------------------------------- /explorer-api/app/cardano-explorer-api-compare.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | import Control.Concurrent.Async (concurrently) 4 | 5 | import Data.Algorithm.Diff (Diff, PolyDiff (..), getDiff) 6 | import qualified Data.ByteString.Char8 as BS 7 | import Data.Char (isSpace) 8 | import Data.Text.ANSI (green, red) 9 | import Data.Text (Text) 10 | import qualified Data.Text as Text 11 | import qualified Data.Text.Encoding as Text 12 | import qualified Data.Text.IO as Text 13 | 14 | import Network.HTTP.Simple (getResponseBody, httpBS, parseRequest) 15 | 16 | import System.Environment (getArgs, getProgName) 17 | 18 | 19 | -- A quick and dirty tool to submit the same query to the old and new cardano webapis 20 | -- and display the differences between the two. 21 | 22 | main :: IO () 23 | main = do 24 | uri <- getArgs 25 | case uri of 26 | [path] -> compareOutputs path 27 | _ -> usageError 28 | 29 | 30 | usageError :: IO () 31 | usageError = do 32 | pn <- getProgName 33 | mapM_ putStrLn 34 | [ "Usage: \n" 35 | , " " ++ pn ++ " \n" 36 | , "where is something like '/api/blocks/pages'" 37 | ] 38 | 39 | compareOutputs :: String -> IO () 40 | compareOutputs path = do 41 | (old, new) <- concurrently 42 | (httpLines $ "http://cardanoexplorer.com" ++ path) 43 | (httpLines $ "http://127.0.0.1:8100" ++ path) 44 | let diff = getDiff old new 45 | if any isDiff diff 46 | then reportDiff diff 47 | else do 48 | Text.putStrLn $ green "Outputs are the same:" 49 | mapM_ Text.putStrLn old 50 | 51 | reportDiff :: [Diff Text] -> IO () 52 | reportDiff xs = do 53 | Text.putStrLn $ Text.concat [ "Explorer web API differences: ", green "old", " vs ", red "new", "." ] 54 | mapM_ display xs 55 | where 56 | display :: Diff Text -> IO () 57 | display db = 58 | case db of 59 | First a -> Text.putStrLn $ green a 60 | Second a -> Text.putStrLn $ red a 61 | Both a _ -> Text.putStrLn a 62 | 63 | isDiff :: Diff a -> Bool 64 | isDiff d = 65 | case d of 66 | First _ -> True 67 | Second _ -> True 68 | Both _ _ -> False 69 | 70 | httpLines :: String -> IO [Text] 71 | httpLines url = do 72 | req <- parseRequest url 73 | -- Remove whitespace, convert to Text and split into lines on commas. 74 | Text.lines . Text.replace "," "\n," 75 | . Text.decodeUtf8 . BS.filter (not . isSpace) 76 | . getResponseBody <$> httpBS req 77 | -------------------------------------------------------------------------------- /explorer-api/app/cardano-explorer-api-validate.hs: -------------------------------------------------------------------------------- 1 | 2 | import Data.Monoid ((<>)) 3 | 4 | import Explorer.Web.Validate 5 | 6 | import Options.Applicative (Parser, ParserInfo, ParserPrefs) 7 | import qualified Options.Applicative as Opt 8 | 9 | 10 | main :: IO () 11 | main = do 12 | Opt.customExecParser p opts >>= runCommand 13 | where 14 | opts :: ParserInfo Command 15 | opts = Opt.info (Opt.helper <*> pVersion <*> pCommand) 16 | ( Opt.fullDesc 17 | <> Opt.header "cardano-webapi-validate - Run validations on the webapi" 18 | ) 19 | 20 | p :: ParserPrefs 21 | p = Opt.prefs Opt.showHelpOnEmpty 22 | 23 | -- ----------------------------------------------------------------------------- 24 | 25 | data Command 26 | = Validate Word 27 | 28 | runCommand :: Command -> IO () 29 | runCommand cmd = do 30 | case cmd of 31 | Validate count -> runValidation count 32 | 33 | pVersion :: Parser (a -> a) 34 | pVersion = 35 | Opt.infoOption "cardano-webapi-validate version 0.1.0.0" 36 | ( Opt.long "version" 37 | <> Opt.short 'v' 38 | <> Opt.help "Print the version and exit" 39 | ) 40 | 41 | pCommand :: Parser Command 42 | pCommand = 43 | Opt.subparser 44 | ( Opt.command "validate" 45 | ( Opt.info pValidate 46 | $ Opt.progDesc "Run validation checks against the webapi." 47 | ) 48 | ) 49 | where 50 | pValidate :: Parser Command 51 | pValidate = 52 | Validate <$> pCount 53 | 54 | pCount :: Parser Word 55 | pCount = 56 | Opt.option Opt.auto 57 | ( Opt.long "count" 58 | <> Opt.help "The number of validations to run." 59 | <> Opt.value 10 60 | <> Opt.showDefault 61 | ) 62 | -------------------------------------------------------------------------------- /explorer-api/app/cardano-explorer-api.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | import Cardano.Db 4 | ( readPGPassFileEnv ) 5 | import Cardano.Rest.Parsers 6 | ( pWebserverConfig ) 7 | import Explorer.Web 8 | ( runServer ) 9 | import Options.Applicative 10 | ( ParserPrefs ) 11 | import qualified Options.Applicative as Opt 12 | 13 | main :: IO () 14 | main = do 15 | webserverConfig <- Opt.customExecParser prefs opts 16 | pgConfig <- readPGPassFileEnv 17 | runServer webserverConfig pgConfig 18 | where 19 | opts = 20 | Opt.info 21 | (Opt.helper <*> pWebserverConfig 8100) 22 | (Opt.fullDesc <> 23 | Opt.header 24 | "cardano-explorer-api - A block explorer for the cardano network") 25 | prefs :: ParserPrefs 26 | prefs = Opt.prefs $ Opt.showHelpOnEmpty <> Opt.showHelpOnError 27 | -------------------------------------------------------------------------------- /explorer-api/shell.nix: -------------------------------------------------------------------------------- 1 | let 2 | self = import ../. {}; 3 | in self.haskellPackages.shellFor { 4 | name = "cardano-explorer-api"; 5 | packages = ps: [ ps.cardano-explorer-api ]; 6 | buildInputs = with self.pkgs.haskellPackages; [ hlint stylish-haskell ghcid ]; 7 | shellHook = '' 8 | compare_api() { 9 | curl http://localhost:8100/api/$1 -o /tmp/compare-local 10 | curl https://cardanoexplorer.com/api/$1 -o /tmp/compare-remote 11 | echo "old explorer:" 12 | ${self.pkgs.jq}/bin/jq < /tmp/compare-remote "$2" 13 | ${self.pkgs.jq}/bin/jq < /tmp/compare-local "$2" 14 | ${self.pkgs.haskellPackages.aeson-diff}/bin/json-diff /tmp/compare-remote /tmp/compare-local | jq . 15 | } 16 | ''; 17 | } 18 | -------------------------------------------------------------------------------- /explorer-api/src/Explorer/Web.hs: -------------------------------------------------------------------------------- 1 | module Explorer.Web 2 | ( runServer 3 | , WebserverConfig(..) 4 | 5 | -- For testing. 6 | , CAddress (..) 7 | , CTxAddressBrief (..) 8 | , CAddressSummary (..) 9 | , CCoin (..) 10 | , CGenesisAddressInfo (..) 11 | , CHash (..) 12 | , CTxBrief (..) 13 | , CTxHash (..) 14 | , queryAddressSummary 15 | , queryAllGenesisAddresses 16 | , queryBlocksTxs 17 | , queryChainTip 18 | , runQuery 19 | ) where 20 | 21 | import Explorer.Web.Api.Legacy.AddressSummary (queryAddressSummary) 22 | import Explorer.Web.Api.Legacy.BlocksTxs (queryBlocksTxs) 23 | import Explorer.Web.Api.Legacy.GenesisAddress (queryAllGenesisAddresses) 24 | import Explorer.Web.Api.Legacy.Util (runQuery) 25 | import Explorer.Web.ClientTypes (CAddress (..), CTxAddressBrief (..), CAddressSummary (..), 26 | CCoin (..), CGenesisAddressInfo (..), CHash (..), CTxBrief (..), CTxHash (..)) 27 | import Explorer.Web.Query (queryChainTip) 28 | import Explorer.Web.Server (runServer, WebserverConfig(..)) 29 | -------------------------------------------------------------------------------- /explorer-api/src/Explorer/Web/Api.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DataKinds #-} 2 | {-# LANGUAGE TypeOperators #-} 3 | 4 | module Explorer.Web.Api 5 | ( ExplorerApi 6 | , explorerApi 7 | ) where 8 | 9 | import Data.Proxy (Proxy (..)) 10 | 11 | import Explorer.Web.Api.HttpBridge (HttpBridgeApi) 12 | import Explorer.Web.Api.Legacy (ExplorerApiRecord) 13 | 14 | import Servant.API ((:>), (:<|>)) 15 | import Servant.API.Generic (ToServantApi) 16 | 17 | -- | Servant API which provides access to explorer 18 | type ExplorerApi 19 | = "api" :> ToServantApi ExplorerApiRecord 20 | -- This one needs to be last in the list because it does not start with 21 | -- as static string. 22 | :<|> ToServantApi HttpBridgeApi 23 | 24 | -- | Helper Proxy 25 | explorerApi :: Proxy ExplorerApi 26 | explorerApi = Proxy 27 | 28 | -------------------------------------------------------------------------------- /explorer-api/src/Explorer/Web/Api/HttpBridge.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE TypeOperators #-} 2 | {-# LANGUAGE DataKinds #-} 3 | {-# LANGUAGE DeriveGeneric #-} 4 | 5 | module Explorer.Web.Api.HttpBridge 6 | ( HttpBridgeApi (..) 7 | ) where 8 | 9 | import GHC.Generics (Generic) 10 | 11 | import Explorer.Web.ClientTypes (CAddress, CNetwork, CAddressBalanceError) 12 | 13 | import Servant.API ((:>), Capture, Get, JSON, Summary) 14 | import Servant.API.Generic ((:-)) 15 | 16 | data HttpBridgeApi route = HttpBridgeApi 17 | { _addressBalance :: route 18 | :- Summary "Get current balance of provided address." 19 | :> Capture "network" CNetwork 20 | :> "utxos" 21 | :> Capture "address" CAddress 22 | :> Get '[JSON] CAddressBalanceError 23 | } deriving Generic 24 | -------------------------------------------------------------------------------- /explorer-api/src/Explorer/Web/Api/HttpBridge/AddressBalance.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DataKinds #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | module Explorer.Web.Api.HttpBridge.AddressBalance 5 | ( addressBalance 6 | ) where 7 | 8 | import Cardano.Db 9 | ( DbLovelace (..), EntityField (..), queryNetworkName, txOutUnspentP ) 10 | import Control.Monad.IO.Class 11 | ( MonadIO ) 12 | import Data.ByteString.Char8 13 | ( ByteString ) 14 | import Data.Text 15 | ( Text ) 16 | import Data.Word 17 | ( Word16 ) 18 | import Database.Esqueleto 19 | ( InnerJoin (..), Value (..), from, on, select, val, where_, (==.), (^.) ) 20 | import Database.Persist.Sql 21 | ( SqlPersistT ) 22 | import Explorer.Web.Api.Legacy.Util 23 | ( bsBase16Encode, decodeTextAddress ) 24 | import Explorer.Web.ClientTypes 25 | ( CAddress (..) 26 | , CAddressBalance (..) 27 | , CAddressBalanceError (..) 28 | , CNetwork (..) 29 | ) 30 | 31 | -- This endpoint emulates the Rust cardano-http-bridge endpoint: 32 | -- 33 | -- GET: /:network/utxos/:address 34 | -- 35 | -- and returns the current Utxo output details: 36 | -- 37 | -- [ { "address": "2cWKMJemoBamE3kYCuVLq6pwWwNBJVZmv471Zcb2ok8cH9NjJC4JUkq5rV5ss9ALXWCKN" 38 | -- , "coin": 310025 39 | -- , "index": 0 40 | -- , "txid": "89eb0d6a8a691dae2cd15ed0369931ce0a949ecafa5c3f93f8121833646e15c3" 41 | -- } 42 | -- ] 43 | 44 | 45 | -- This endpoint always returns a list (which may be empty). 46 | -- There are a number of potential failures and wyat 47 | addressBalance :: 48 | MonadIO m => CNetwork -> CAddress -> SqlPersistT m CAddressBalanceError 49 | addressBalance (CNetwork networkName) (CAddress addrTxt) = 50 | -- Currently ignore the 'CNetwork' parameter (eg mainnet, testnet etc) as the explorer only 51 | -- supports a single network and returns a result for whichever network its running on. 52 | case decodeTextAddress addrTxt of 53 | Left _ -> pure $ CABError "Invalid address" 54 | Right _ -> do 55 | mNetName <- queryNetworkName 56 | case mNetName of 57 | Nothing -> pure $ CABError "Invalid network name" 58 | Just name -> if name /= networkName 59 | then pure $ CABError "Network name mismatch" 60 | else CABValue <$> queryAddressBalance addrTxt 61 | 62 | -- ------------------------------------------------------------------------------------------------- 63 | 64 | queryAddressBalance :: MonadIO m => Text -> SqlPersistT m [CAddressBalance] 65 | queryAddressBalance addrTxt = do 66 | rows <- select . from $ \ (tx `InnerJoin` txOut) -> do 67 | on (tx ^. TxId ==. txOut ^. TxOutTxId) 68 | txOutUnspentP txOut 69 | where_ (txOut ^. TxOutAddress ==. val addrTxt) 70 | pure (txOut ^. TxOutAddress, tx ^. TxHash, txOut ^. TxOutIndex, txOut ^. TxOutValue) 71 | pure $ map convert rows 72 | where 73 | convert :: (Value Text, Value ByteString, Value Word16, Value DbLovelace) -> CAddressBalance 74 | convert (Value addr, Value txhash, Value index, Value coin) = 75 | CAddressBalance 76 | { cuaAddress = addr 77 | , cuaTxHash = bsBase16Encode txhash 78 | , cuaIndex = index 79 | , cuaCoin = unDbLovelace coin 80 | } 81 | -------------------------------------------------------------------------------- /explorer-api/src/Explorer/Web/Api/Legacy/BlockPagesTotal.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE MultiWayIf #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | module Explorer.Web.Api.Legacy.BlockPagesTotal 4 | ( blockPagesTotal 5 | ) where 6 | 7 | import Cardano.Db 8 | ( EntityField (..), isJust ) 9 | import Control.Monad.IO.Class 10 | ( MonadIO ) 11 | import Data.Maybe 12 | ( listToMaybe ) 13 | import Database.Esqueleto 14 | ( countRows, from, select, unValue, where_, (^.) ) 15 | import Database.Persist.Sql 16 | ( SqlPersistT ) 17 | import Explorer.Web.Api.Legacy 18 | ( PageNumber ) 19 | import Explorer.Web.Api.Legacy.Types 20 | ( PageSize (..) ) 21 | import Explorer.Web.Api.Legacy.Util 22 | ( divRoundUp, toPageSize ) 23 | import Explorer.Web.Error 24 | ( ExplorerError (..) ) 25 | 26 | blockPagesTotal 27 | :: MonadIO m 28 | => Maybe PageSize 29 | -> SqlPersistT m (Either ExplorerError PageNumber) 30 | blockPagesTotal mPageSize = do 31 | blockCount <- queryMainBlockCount 32 | if | blockCount < 1 -> pure $ Left (Internal "There are currently no block to display.") 33 | | pageSize < 1 -> pure $ Left (Internal "Page size must be greater than 1 if you want to display blocks.") 34 | | otherwise -> pure $ Right $ divRoundUp blockCount pageSize 35 | where 36 | pageSize = unPageSize $ toPageSize mPageSize 37 | 38 | queryMainBlockCount :: MonadIO m => SqlPersistT m Word 39 | queryMainBlockCount = do 40 | res <- select . from $ \ blk -> do 41 | where_ (isJust $ blk ^. BlockBlockNo) 42 | pure countRows 43 | pure $ maybe 0 unValue (listToMaybe res) 44 | -------------------------------------------------------------------------------- /explorer-api/src/Explorer/Web/Api/Legacy/GenesisPages.hs: -------------------------------------------------------------------------------- 1 | module Explorer.Web.Api.Legacy.GenesisPages 2 | ( genesisPages 3 | ) where 4 | 5 | import Cardano.Db 6 | ( EntityField (..), txOutSpentP, txOutUnspentP ) 7 | import Control.Monad.IO.Class 8 | ( MonadIO ) 9 | import Data.Maybe 10 | ( listToMaybe ) 11 | import Database.Esqueleto 12 | ( InnerJoin (..) 13 | , Value 14 | , countRows 15 | , from 16 | , on 17 | , select 18 | , unValue 19 | , val 20 | , where_ 21 | , (==.) 22 | , (^.) 23 | ) 24 | import Database.Persist.Sql 25 | ( SqlPersistT ) 26 | import Explorer.Web.Api.Legacy 27 | ( PageNumber ) 28 | import Explorer.Web.Api.Legacy.Types 29 | ( PageSize (..) ) 30 | import Explorer.Web.Api.Legacy.Util 31 | ( divRoundUp, toPageSize ) 32 | import Explorer.Web.ClientTypes 33 | ( CAddressesFilter (..) ) 34 | import Explorer.Web.Error 35 | ( ExplorerError (..) ) 36 | 37 | 38 | genesisPages 39 | :: MonadIO m 40 | => Maybe PageSize 41 | -> Maybe CAddressesFilter 42 | -> SqlPersistT m (Either ExplorerError PageNumber) 43 | genesisPages mPageSize mAddrFilter = 44 | case mAddrFilter of 45 | Just RedeemedAddresses -> Right <$> queryRedeemedGenesisAddressCount pageSize 46 | Just NonRedeemedAddresses -> Right <$> queryUnRedeemedGenesisAddressCount pageSize 47 | _ -> Right <$> queryGenesisAddressCount pageSize 48 | where 49 | pageSize = toPageSize mPageSize 50 | 51 | queryGenesisAddressCount :: MonadIO m => PageSize -> SqlPersistT m Word 52 | queryGenesisAddressCount (PageSize pageSize) = do 53 | res <- select . from $ \ (blk `InnerJoin` tx `InnerJoin` txOut) -> do 54 | on (tx ^. TxId ==. txOut ^. TxOutTxId) 55 | on (blk ^. BlockId ==. tx ^. TxBlockId) 56 | -- Only the initial genesis block has a size of 0. 57 | where_ (blk ^. BlockSize ==. val 0) 58 | pure countRows 59 | pure $ maybe 0 (dividePageSize pageSize) (listToMaybe res) 60 | 61 | queryRedeemedGenesisAddressCount :: MonadIO m => PageSize -> SqlPersistT m Word 62 | queryRedeemedGenesisAddressCount (PageSize pageSize) = do 63 | res <- select . from $ \ (blk `InnerJoin` tx `InnerJoin` txOut) -> do 64 | on (tx ^. TxId ==. txOut ^. TxOutTxId) 65 | on (blk ^. BlockId ==. tx ^. TxBlockId) 66 | txOutSpentP txOut 67 | -- Only the initial genesis block has a size of 0. 68 | where_ (blk ^. BlockSize ==. val 0) 69 | pure countRows 70 | pure $ maybe 0 (dividePageSize pageSize) (listToMaybe res) 71 | 72 | queryUnRedeemedGenesisAddressCount :: MonadIO m => PageSize -> SqlPersistT m Word 73 | queryUnRedeemedGenesisAddressCount (PageSize pageSize) = do 74 | res <- select . from $ \ (blk `InnerJoin` tx `InnerJoin` txOut) -> do 75 | on (tx ^. TxId ==. txOut ^. TxOutTxId) 76 | on (blk ^. BlockId ==. tx ^. TxBlockId) 77 | txOutUnspentP txOut 78 | -- Only the initial genesis block has a size of 0. 79 | where_ (blk ^. BlockSize ==. val 0) 80 | pure countRows 81 | pure $ maybe 0 (dividePageSize pageSize) (listToMaybe res) 82 | 83 | 84 | dividePageSize :: Word -> Value Word -> Word 85 | dividePageSize pageSize vw = 86 | divRoundUp (unValue vw) pageSize 87 | -------------------------------------------------------------------------------- /explorer-api/src/Explorer/Web/Api/Legacy/GenesisSummary.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DataKinds #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | module Explorer.Web.Api.Legacy.GenesisSummary 5 | ( genesisSummary 6 | ) where 7 | 8 | import Cardano.Db 9 | ( EntityField (..), txOutSpentP ) 10 | import Control.Monad.IO.Class 11 | ( MonadIO ) 12 | import Data.Fixed 13 | ( Fixed (..), Uni ) 14 | import Data.Maybe 15 | ( listToMaybe ) 16 | import Database.Esqueleto 17 | ( InnerJoin (..) 18 | , Value 19 | , countRows 20 | , from 21 | , on 22 | , select 23 | , sum_ 24 | , unValue 25 | , val 26 | , where_ 27 | , (==.) 28 | , (^.) 29 | ) 30 | import Database.Persist.Sql 31 | ( SqlPersistT ) 32 | import Explorer.Web.ClientTypes 33 | ( CGenesisSummary (..) ) 34 | import Explorer.Web.Error 35 | ( ExplorerError (..) ) 36 | 37 | genesisSummary :: MonadIO m => SqlPersistT m (Either ExplorerError CGenesisSummary) 38 | genesisSummary = Right <$> do 39 | (numTotal,valTotal) <- queryInitialGenesis 40 | (redTotal, valRedeemed) <- queryGenesisRedeemed 41 | pure $ CGenesisSummary 42 | { cgsNumTotal = numTotal 43 | , cgsNumRedeemed = redTotal 44 | , cgsNumNotRedeemed = numTotal - redTotal 45 | , cgsRedeemedAmountTotal = fromIntegral valRedeemed 46 | , cgsNonRedeemedAmountTotal = fromIntegral $ valTotal - valRedeemed 47 | } 48 | 49 | queryInitialGenesis :: MonadIO m => SqlPersistT m (Word, Integer) 50 | queryInitialGenesis = do 51 | res <- select . from $ \ (blk `InnerJoin` tx `InnerJoin` txOut) -> do 52 | on (tx ^. TxId ==. txOut ^. TxOutTxId) 53 | on (blk ^. BlockId ==. tx ^. TxBlockId) 54 | -- Only the initial genesis block has a size of 0. 55 | where_ (blk ^. BlockSize ==. val 0) 56 | pure (countRows, sum_ (txOut ^. TxOutValue)) 57 | pure $ maybe (0, 0) convertPair (listToMaybe res) 58 | 59 | queryGenesisRedeemed :: MonadIO m => SqlPersistT m (Word, Integer) 60 | queryGenesisRedeemed = do 61 | res <- select . from $ \ (blk `InnerJoin` tx `InnerJoin` txOut) -> do 62 | on (tx ^. TxId ==. txOut ^. TxOutTxId) 63 | on (blk ^. BlockId ==. tx ^. TxBlockId) 64 | txOutSpentP txOut 65 | -- Only the initial genesis block has a size of 0. 66 | where_ (blk ^. BlockSize ==. val 0) 67 | pure (countRows, sum_ (txOut ^. TxOutValue)) 68 | pure $ maybe (0, 0) convertPair (listToMaybe res) 69 | 70 | convertPair :: (Value Word, Value (Maybe Uni)) -> (Word, Integer) 71 | convertPair (vcount, vtotal) = (unValue vcount, unTotal vtotal) 72 | 73 | unTotal :: Value (Maybe Uni) -> Integer 74 | unTotal mvi = 75 | case unValue mvi of 76 | Just (MkFixed x) -> x 77 | _ -> 0 78 | -------------------------------------------------------------------------------- /explorer-api/src/Explorer/Web/Api/Legacy/StatsTxs.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE ScopedTypeVariables #-} 3 | {-# LANGUAGE TupleSections #-} 4 | module Explorer.Web.Api.Legacy.StatsTxs 5 | ( statsTxs 6 | ) where 7 | 8 | import Cardano.Db 9 | ( BlockId, EntityField (..), isJust, queryBlockHeight ) 10 | import Control.Monad.Extra 11 | ( concatMapM ) 12 | import Control.Monad.IO.Class 13 | ( MonadIO ) 14 | import Data.ByteString.Char8 15 | ( ByteString ) 16 | import Data.Word 17 | ( Word64 ) 18 | import Database.Esqueleto 19 | ( InnerJoin (..) 20 | , Value (..) 21 | , asc 22 | , desc 23 | , from 24 | , limit 25 | , offset 26 | , on 27 | , orderBy 28 | , select 29 | , val 30 | , where_ 31 | , (==.) 32 | , (^.) 33 | ) 34 | import Database.Persist.Sql 35 | ( SqlPersistT ) 36 | import Explorer.Web.Api.Legacy 37 | ( PageNumber, TxsStats ) 38 | import Explorer.Web.Api.Legacy.Types 39 | ( PageNo (..) ) 40 | import Explorer.Web.Api.Legacy.Util 41 | ( bsBase16Encode ) 42 | import Explorer.Web.ClientTypes 43 | ( CHash (..), CTxHash (..) ) 44 | import Explorer.Web.Error 45 | ( ExplorerError (..) ) 46 | 47 | import qualified Data.List as List 48 | 49 | -- Example queries: 50 | -- 51 | -- /api/stats/txs 52 | -- /api/stats/txs?page=1 53 | -- /api/stats/txs?page=4000 54 | -- /api/stats/txs?page=10000 55 | -- /api/stats/txs?page=100000 56 | 57 | 58 | -- type TxsStats = (PageNumber, [(CTxHash, Word64)]) 59 | statsTxs 60 | :: MonadIO m 61 | => Maybe PageNo 62 | -> SqlPersistT m (Either ExplorerError TxsStats) 63 | statsTxs mPageNo = do 64 | blockHeight <- queryBlockHeight 65 | let currentPageNo = toPageNo blockHeight 66 | case mPageNo of 67 | Nothing -> Right . (currentPageNo,) <$> queryLatestBlockTx (calculatePageEntries blockHeight) 68 | Just (PageNo 0) -> pure $ Left (Internal "Page number must be greater than 0") 69 | Just pn -> 70 | if unPageNo pn > currentPageNo 71 | then pure $ Left (Internal "Number of pages exceeds total page count.") 72 | else Right . (currentPageNo,) <$> queryBlockTxPageNo pn 73 | where 74 | toPageNo :: Word64 -> PageNumber 75 | toPageNo x = 76 | case fromIntegral x `divMod` 10 of 77 | (y, 0) -> y 78 | (y, _) -> y + 1 79 | 80 | calculatePageEntries :: Word64 -> Int 81 | calculatePageEntries blockHeight = 82 | case blockHeight `mod` 10 of 83 | 0 -> 10 84 | y -> fromIntegral y 85 | 86 | 87 | queryLatestBlockTx :: MonadIO m => Int -> SqlPersistT m [(CTxHash, Word64)] 88 | queryLatestBlockTx count = do 89 | rows <- select . from $ \ blk -> do 90 | where_ (isJust (blk ^. BlockBlockNo)) 91 | orderBy [desc (blk ^. BlockBlockNo)] 92 | limit (fromIntegral count) 93 | pure (blk ^. BlockId) 94 | concatMapM queryBlockTx rows 95 | 96 | queryBlockTxPageNo :: MonadIO m => PageNo -> SqlPersistT m [(CTxHash, Word64)] 97 | queryBlockTxPageNo (PageNo page) = do 98 | rows <- select . from $ \ blk -> do 99 | where_ (isJust (blk ^. BlockBlockNo)) 100 | orderBy [asc (blk ^. BlockBlockNo)] 101 | offset (fromIntegral $ (page - 1) * 10) 102 | limit 10 103 | pure (blk ^. BlockId) 104 | concatMapM queryBlockTx $ List.reverse rows 105 | 106 | queryBlockTx :: MonadIO m => Value BlockId -> SqlPersistT m [(CTxHash, Word64)] 107 | queryBlockTx (Value blkid) = do 108 | rows <- select . from $ \ (blk `InnerJoin` tx) -> do 109 | on (blk ^. BlockId ==. tx ^. TxBlockId) 110 | where_ (blk ^. BlockId ==. val blkid) 111 | pure (tx ^. TxHash, tx ^. TxSize) 112 | pure $ map convert rows 113 | where 114 | convert :: (Value ByteString, Value Word64) -> (CTxHash, Word64) 115 | convert (Value h, Value s) = (CTxHash $ CHash (bsBase16Encode h), s) 116 | -------------------------------------------------------------------------------- /explorer-api/src/Explorer/Web/Api/Legacy/TxLast.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | module Explorer.Web.Api.Legacy.TxLast 4 | ( getLastTxs 5 | ) where 6 | 7 | import Cardano.Db 8 | ( EntityField (..), Tx, isJust ) 9 | import Control.Monad.IO.Class 10 | ( MonadIO ) 11 | import Data.ByteString.Char8 12 | ( ByteString ) 13 | import Data.Fixed 14 | ( Fixed (..), Uni ) 15 | import Data.Time.Clock 16 | ( UTCTime ) 17 | import Data.Time.Clock.POSIX 18 | ( utcTimeToPOSIXSeconds ) 19 | import Database.Esqueleto 20 | ( Entity 21 | , InnerJoin (..) 22 | , SqlExpr 23 | , Value 24 | , desc 25 | , from 26 | , limit 27 | , on 28 | , orderBy 29 | , select 30 | , subSelectUnsafe 31 | , sum_ 32 | , unValue 33 | , where_ 34 | , (==.) 35 | , (^.) 36 | ) 37 | import Database.Persist.Sql 38 | ( SqlPersistT ) 39 | import Explorer.Web.Api.Legacy.Util 40 | import Explorer.Web.ClientTypes 41 | ( CHash (..), CTxEntry (..), CTxHash (..) ) 42 | import Explorer.Web.Error 43 | ( ExplorerError (..) ) 44 | 45 | 46 | getLastTxs :: MonadIO m => SqlPersistT m (Either ExplorerError [CTxEntry]) 47 | getLastTxs = Right <$> queryCTxEntry 48 | 49 | 50 | queryCTxEntry :: MonadIO m => SqlPersistT m [CTxEntry] 51 | queryCTxEntry = do 52 | txRows <- select . from $ \ (blk `InnerJoin` tx) -> do 53 | on (blk ^. BlockId ==. tx ^. TxBlockId) 54 | where_ (isJust $ blk ^. BlockSlotNo) 55 | orderBy [desc (blk ^. BlockSlotNo)] 56 | limit 20 57 | pure (blk ^. BlockTime, tx ^. TxHash, txOutValue tx) 58 | pure $ map convert txRows 59 | where 60 | convert :: (Value UTCTime, Value ByteString, Value (Maybe Uni)) -> CTxEntry 61 | convert (vtime, vhash, vtotal) = 62 | CTxEntry 63 | { cteId = CTxHash . CHash $ bsBase16Encode (unValue vhash) 64 | , cteTimeIssued = Just $ utcTimeToPOSIXSeconds (unValue vtime) 65 | , cteAmount = unTotal vtotal 66 | } 67 | 68 | txOutValue :: SqlExpr (Entity Tx) -> SqlExpr (Value (Maybe Uni)) 69 | txOutValue tx = 70 | -- This actually is safe. 71 | subSelectUnsafe . from $ \ txOut -> do 72 | where_ (txOut ^. TxOutTxId ==. tx ^. TxId) 73 | pure $ sum_ (txOut ^. TxOutValue) 74 | 75 | unTotal :: Num a => Value (Maybe Uni) -> a 76 | unTotal mvi = 77 | fromIntegral $ 78 | case unValue mvi of 79 | Just (MkFixed x) -> x 80 | _ -> 0 81 | -------------------------------------------------------------------------------- /explorer-api/src/Explorer/Web/Api/Legacy/Types.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 2 | {-# LANGUAGE StandaloneDeriving #-} 3 | module Explorer.Web.Api.Legacy.Types 4 | ( PageNo (..) 5 | , PageSize (..) 6 | ) where 7 | 8 | import Servant.API (FromHttpApiData) 9 | 10 | 11 | newtype PageNo = PageNo 12 | { unPageNo :: Word 13 | } deriving Show 14 | 15 | deriving instance FromHttpApiData PageNo 16 | 17 | newtype PageSize = PageSize 18 | { unPageSize :: Word 19 | } deriving Show 20 | 21 | deriving instance FromHttpApiData PageSize 22 | -------------------------------------------------------------------------------- /explorer-api/src/Explorer/Web/Error.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveGeneric #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE TemplateHaskell #-} 4 | 5 | -- | Types describing runtime errors related to Explorer 6 | 7 | module Explorer.Web.Error 8 | ( ExplorerError (..) 9 | , renderExplorerError 10 | ) where 11 | 12 | import Cardano.Db 13 | ( LookupFail (..), renderLookupFail ) 14 | import Data.Aeson 15 | ( ToJSON (..), Value (..) ) 16 | import Data.Text 17 | ( Text ) 18 | import Formatting 19 | ( bprint, stext, (%) ) 20 | import Formatting.Buildable 21 | ( Buildable ) 22 | import GHC.Generics 23 | ( Generic ) 24 | 25 | import qualified Formatting.Buildable 26 | 27 | data ExplorerError 28 | = Internal Text -- Stupid error constructor from the old code base. 29 | | EELookupFail !LookupFail 30 | deriving (Generic, Show) 31 | 32 | instance Buildable ExplorerError where 33 | build ee = 34 | case ee of 35 | Internal msg -> bprint ("Internal explorer error ("%stext%")") msg 36 | EELookupFail err -> bprint stext $ renderLookupFail err 37 | 38 | renderExplorerError :: ExplorerError -> Text 39 | renderExplorerError ee = 40 | case ee of 41 | Internal msg -> mconcat [ "Internal explorer error: ", msg ] 42 | EELookupFail err -> renderLookupFail err 43 | 44 | 45 | instance ToJSON ExplorerError where 46 | toJSON ee = 47 | case ee of 48 | Internal msg -> String msg 49 | EELookupFail err -> String $ renderLookupFail err 50 | -------------------------------------------------------------------------------- /explorer-api/test/Main.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -F -pgmF hspec-discover #-} 2 | -------------------------------------------------------------------------------- /explorer-api/test/Test/Explorer/Web/Api/Legacy/UtilSpec.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | module Test.Explorer.Web.Api.Legacy.UtilSpec 4 | ( spec 5 | ) where 6 | 7 | import Prelude 8 | 9 | import Data.Either 10 | ( isLeft, isRight ) 11 | import Explorer.Web.Api.Legacy.Util 12 | ( decodeTextAddress ) 13 | import Test.Hspec 14 | ( Spec, describe, it, shouldSatisfy ) 15 | 16 | spec :: Spec 17 | spec = do 18 | describe "decodeTextAddress" $ do 19 | it "x cannot decode arbitrary unicode" $ do 20 | decodeTextAddress 21 | "💩" 22 | `shouldSatisfy` isLeft 23 | 24 | it "x cannot decode gibberish base58" $ do 25 | decodeTextAddress 26 | "FpqXJLkgVhRTYkf5F7mh3q6bAv5hWYjhSV1gekjEJE8XFeZSganv" 27 | `shouldSatisfy` isLeft 28 | 29 | it "x cannot decode Jörmungandr address" $ do 30 | decodeTextAddress 31 | "addr1qdaa2wrvxxkrrwnsw6zk2qx0ymu96354hq83s0r6203l9pqe6677z5t3m7d" 32 | `shouldSatisfy` isLeft 33 | 34 | it "✓ can decode Base58 Byron address (Legacy Mainnet)" $ do 35 | decodeTextAddress 36 | "DdzFFzCqrhstkaXBhux3ALL9wqvP3Nkz8QE5qKwFbqkmTL6zyKpc\ 37 | \FpqXJLkgVhRTYkf5F7mh3q6bAv5hWYjhSV1gekjEJE8XFeZSganv" 38 | `shouldSatisfy` isRight 39 | 40 | it "✓ can decode Base58 Byron address (Legacy Testnet)" $ do 41 | decodeTextAddress 42 | "37btjrVyb4KEgoGCHJ7XFaJRLBRiVuvcrQWPpp4HeaxdTxhKwQjXHNKL4\ 43 | \3NhXaQNa862BmxSFXZFKqPqbxRc3kCUeTRMwjJevFeCKokBG7A7num5Wh" 44 | `shouldSatisfy` isRight 45 | 46 | it "✓ can decode Base58 Byron address (Icarus)" $ do 47 | decodeTextAddress 48 | "Ae2tdPwUPEZ4Gs4s2recjNjQHBKfuBTkeuqbHJJrC6CuyjGyUD44cCTq4sJ" 49 | `shouldSatisfy` isRight 50 | 51 | it "✓ can decode Bech32 Shelley address (basic)" $ do 52 | decodeTextAddress 53 | "addr1vpu5vlrf4xkxv2qpwngf6cjhtw542ayty80v8dyr49rf5eg0yu80w" 54 | `shouldSatisfy` isRight 55 | 56 | it "✓ can decode Bech32 Shelley address (stake by value)" $ do 57 | decodeTextAddress 58 | "addr1qrejymax5dh6srcj3sehf6lt0czdj9uklffzhc3fqgglnl\ 59 | \0nyfh6dgm04q839rpnwn47klsymyted7jj903zjqs3l87sutspf7" 60 | `shouldSatisfy` isRight 61 | 62 | it "✓ can decode Bech32 Shelley address (pointer)" $ do 63 | decodeTextAddress 64 | "addr1g8ejymax5dh6srcj3sehf6lt0czdj9uklffzhc3fqgglnlf2pcqqc69etp" 65 | `shouldSatisfy` isRight 66 | 67 | it "✓ can decode Base16 Byron address" $ do 68 | decodeTextAddress 69 | "82d818582183581c4ad651d1c6afe6b3483eac69ab9\ 70 | \6575519376f136f024c35e5d27071a0001a453d0d11" 71 | `shouldSatisfy` isRight 72 | 73 | it "✓ can decode Base16 Shelley address" $ do 74 | decodeTextAddress 75 | "6079467c69a9ac66280174d09d62575ba955748b21dec3b483a9469a65" 76 | `shouldSatisfy` isRight 77 | -------------------------------------------------------------------------------- /explorer-api/test/Test/Explorer/Web/ClientTypesSpec.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | module Test.Explorer.Web.ClientTypesSpec 4 | ( spec 5 | ) where 6 | 7 | import Cardano.Chain.Common 8 | ( maxLovelaceVal ) 9 | import Cardano.Db 10 | ( Ada (..), word64ToAda ) 11 | import Data.Word 12 | ( Word64 ) 13 | import Explorer.Web.ClientTypes 14 | ( adaToCCoin, cCoinToAda ) 15 | import Hedgehog 16 | ( Gen, PropertyT ) 17 | import Test.Hspec 18 | ( Spec, describe, it, shouldBe ) 19 | 20 | import qualified Hedgehog as H 21 | import qualified Hedgehog.Gen as Gen 22 | import qualified Hedgehog.Range as Range 23 | 24 | prop_roundtrip_ada_to_ccoin :: PropertyT IO () 25 | prop_roundtrip_ada_to_ccoin = do 26 | ada <- H.forAll genAda 27 | H.tripping ada adaToCCoin (Just . cCoinToAda) 28 | 29 | genAda :: Gen Ada 30 | genAda = 31 | word64ToAda <$> genWord64Ada 32 | where 33 | genWord64Ada :: Gen Word64 34 | genWord64Ada = 35 | Gen.choice 36 | [ Gen.word64 (Range.linear 0 maxLovelaceVal) -- Full range 37 | , Gen.word64 (Range.linear 0 5000) -- Small values 38 | , Gen.word64 (Range.linear (maxLovelaceVal - 5000) maxLovelaceVal) -- Near max. 39 | ] 40 | 41 | -- ----------------------------------------------------------------------------- 42 | 43 | spec :: Spec 44 | spec = describe "ClientTypesSpec" $ do 45 | it "properties" $ do 46 | result <- H.checkParallel $ H.Group "ClientTypesSpec" 47 | [ ( "adaToCCoin . cCoinToAda" 48 | , H.property prop_roundtrip_ada_to_ccoin 49 | ) 50 | ] 51 | result `shouldBe` True 52 | -------------------------------------------------------------------------------- /migration-guide/README.md: -------------------------------------------------------------------------------- 1 | # cardano-rest migration guide 2 | 3 | ## Installing 4 | 5 | ```console 6 | $ pip install -r requirements.txt 7 | ``` 8 | 9 | ## Building 10 | 11 | ```console 12 | $ mkdocs build 13 | ``` 14 | 15 | ## Working 16 | 17 | ```console 18 | $ mkdocs serve 19 | ``` 20 | -------------------------------------------------------------------------------- /migration-guide/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: cardano-rest Migration Guide 2 | site_url: https://input-output-hk.github.io/cardano-rest/migration-guide/ 3 | repo_url: https://github.com/input-output-hk/cardano-rest 4 | edit_uri: tree/master/migration-guide/src 5 | site_description: A migration guide to help moving away from cardano-rest to other more sensible solutions after deprecation. 6 | copyright: | 7 | Copyright © 2020 - present | Input Output 8 | docs_dir: src 9 | theme: 10 | name: material 11 | language: en 12 | palette: 13 | primary: deep-purple 14 | accent: deep-purple 15 | include_search_page: false 16 | search_index_only: true 17 | nav: 18 | - Getting Started: index.md 19 | - explorer-api: cardano-api.md 20 | - submmit-api: submit-api.md 21 | 22 | markdown_extensions: 23 | - markdown.extensions.admonition 24 | - markdown.extensions.codehilite: 25 | guess_lang: false 26 | - markdown.extensions.toc: 27 | permalink: true 28 | - pymdownx.betterem: 29 | smart_enable: all 30 | - pymdownx.details 31 | - pymdownx.emoji: 32 | emoji_generator: !!python/name:pymdownx.emoji.to_svg 33 | - pymdownx.inlinehilite 34 | - pymdownx.magiclink 35 | - pymdownx.smartsymbols 36 | - pymdownx.superfences 37 | - pymdownx.tabbed 38 | - pymdownx.tasklist: 39 | custom_checkbox: true 40 | - admonition 41 | -------------------------------------------------------------------------------- /migration-guide/requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs-material>=3.0 2 | -------------------------------------------------------------------------------- /migration-guide/src/index.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## End of life 4 | 5 | After several years of service, cardano-rest is now considered deprecated. There will be no further development nor support done on this component stack after the 31st of March 2021. 6 | Note that it **will** however survive the Mary hardfork going happening soon. Clients currently integrated with this component are therefore encouraged to upgrade their integration to 7 | use either [cardano-graphql](https://github.com/input-output-hk/cardano-graphql) or [cardano-rosetta](https://github.com/input-output-hk/cardano-rosetta) which are more capable, 8 | production-ready and better documented components still under active development. 9 | 10 | !!! info 11 | This guide is meant to help existing customers migrating from cardano-rest to new components by showing how information from cardano-rest can be retrieved from each components. Note that 12 | this guide does not cover new components installation and build instructions. For these, refer to each component's README and wiki page on their respective Github repositories. 13 | 14 | ### cardano-graphql 15 | 16 | `Cardano-graphql` is **cross-platform**, **typed**, and **queryable API** for Cardano. The project contains multiple packages for composing GraphQL services to meet specific application demands, and a docker-compose stack serving the included cardano-graphql-server Dockerfile and the extended hasura Dockerfile. The schema is defined in native .graphql, and used to generate a TypeScript package for client-side static typing. 17 | 18 | ### cardano-rosetta 19 | 20 | `Cardano-rosetta` is an implementation of the [Rosetta](https://www.rosetta-api.org/docs/1.4.4/welcome.html) specification for Cardano. Rosetta is an open-source specification and set of tools that makes integrating with blockchains **simpler**, **faster**, and **more reliable**. 21 | 22 | ### cardano-submit-api 23 | 24 | [`cardano-submit-api`](https://github.com/input-output-hk/cardano-node/tree/master/cardano-submit-api) is a fresh implementation of the `submit-api` from `cardano-rest`. The API has a compatible interface so that existing users will be already familiar with it, though it includes new features and will be maintained in the future. The API is also now located closer to `cardano-node` and share the same development resources. 25 | -------------------------------------------------------------------------------- /migration-guide/src/submit-api.md: -------------------------------------------------------------------------------- 1 | # submit-api 2 | 3 | ## Transactions 4 | 5 | #### [/api/submit/tx](https://input-output-hk.github.io/cardano-rest/submit-api/#operation/postTransaction) 6 | 7 | Submit an already serialized transaction to the network. 8 | 9 | === "cardano-rest" 10 | 11 | Assuming `data` is a serialized transaction on the file-system. 12 | ```console 13 | $ curl -X POST --header "Content-Type: application/cbor" --data-binary @data http://localhost:8101/api/submit/tx 14 | ``` 15 | 16 |
17 | see JSON response 18 | ```json 19 | 92bcd06b25dfbd89b578d536b4d3b7dd269b7c2aa206ed518012cffe0444d67f 20 | ``` 21 |
22 | 23 | === "cardano-graphql" 24 | 25 | ```graphql 26 | mutation submitTransaction($transaction) { 27 | submitTransaction(transaction: $transaction) { 28 | hash 29 | } 30 | } 31 | ``` 32 | 33 | Where `$transaction` corresponds to a CBOR-serialized transaction, e.g. as output from `cardano-cli`. 34 | 35 | === "cardano-rosetta" 36 | 37 | This can be done via the [/construction/submit](https://www.rosetta-api.org/docs/ConstructionApi.html#constructionsubmit) endpoint. 38 | 39 | === "cardano-submit-api" 40 | 41 | Follow the OpenAPI specification defined [here](https://github.com/input-output-hk/cardano-node/blob/master/cardano-submit-api/swagger.yaml). For any question, please open a ticket 42 | on input-output-hk/cardano-node. 43 | -------------------------------------------------------------------------------- /nix-shell.project: -------------------------------------------------------------------------------- 1 | index-state: 2020-02-11T00:00:00Z 2 | 3 | packages: 4 | rest-common 5 | explorer-api 6 | submit-api 7 | 8 | package cardano-rest-common 9 | tests: True 10 | ghc-options: -Wall -Werror -fwarn-redundant-constraints -Wno-error=missing-local-signatures 11 | 12 | package cardano-explorer-api 13 | tests: True 14 | ghc-options: -Wall -Werror -fwarn-redundant-constraints -Wno-error=missing-local-signatures 15 | 16 | package cardano-submit-api 17 | tests: True 18 | ghc-options: -Wall -Werror -fwarn-redundant-constraints -Wno-error=missing-local-signatures 19 | 20 | package cardano-db-sync 21 | tests: False 22 | 23 | package cardano-shell 24 | tests: False 25 | 26 | package ouroboros-consensus 27 | tests: False 28 | 29 | package ouroboros-consensus-cardano 30 | tests: False 31 | 32 | package ouroboros-consensus-byron 33 | tests: False 34 | 35 | package ouroboros-consensus-shelley 36 | tests: False 37 | 38 | package ouroboros-consensus-mock 39 | tests: False 40 | -------------------------------------------------------------------------------- /nix/default.nix: -------------------------------------------------------------------------------- 1 | { system ? builtins.currentSystem 2 | , crossSystem ? null 3 | , config ? {} 4 | , sourcesOverride ? {} 5 | }: 6 | let 7 | sources = import ./sources.nix { inherit pkgs; } 8 | // sourcesOverride; 9 | iohkNix = import sources.iohk-nix {}; 10 | haskellNix = (import sources."haskell.nix" {}).nixpkgsArgs; 11 | # use our own nixpkgs if it exists in our sources, 12 | # otherwise use iohkNix default nixpkgs. 13 | nixpkgs = if (sources ? nixpkgs) 14 | then (builtins.trace "Not using IOHK default nixpkgs (use 'niv drop nixpkgs' to use default for better sharing)" 15 | sources.nixpkgs) 16 | else (builtins.trace "Using IOHK default nixpkgs" 17 | iohkNix.nixpkgs); 18 | 19 | # for inclusion in pkgs: 20 | overlays = 21 | # Haskell.nix (https://github.com/input-output-hk/haskell.nix) 22 | haskellNix.overlays 23 | # haskell-nix.haskellLib.extra: some useful extra utility functions for haskell.nix 24 | ++ iohkNix.overlays.haskell-nix-extra 25 | # iohkNix: nix utilities and niv: 26 | ++ iohkNix.overlays.iohkNix 27 | # libsodium fork 28 | ++ iohkNix.overlays.crypto 29 | # our own overlays: 30 | ++ [ 31 | (pkgs: _: with pkgs; { 32 | 33 | # commonLib: mix pkgs.lib with iohk-nix utils and our own: 34 | commonLib = lib // iohkNix // iohkNix.cardanoLib 35 | // import ./util.nix { inherit haskell-nix; } 36 | # also expose our sources and overlays 37 | // { inherit overlays sources; }; 38 | }) 39 | # And, of course, our haskell-nix-ified cabal project: 40 | (import ./pkgs.nix) 41 | ]; 42 | 43 | pkgs = import nixpkgs { 44 | inherit system crossSystem overlays; 45 | config = haskellNix.config // config; 46 | }; 47 | 48 | in pkgs 49 | -------------------------------------------------------------------------------- /nix/haskell.nix: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Builds Haskell packages with Haskell.nix 3 | ############################################################################ 4 | { lib 5 | , stdenv 6 | , haskell-nix 7 | , buildPackages 8 | , config ? {} 9 | # GHC attribute name 10 | , compiler ? config.haskellNix.compiler or "ghc865" 11 | # Enable profiling 12 | , profiling ? config.haskellNix.profiling or false 13 | }: 14 | let 15 | # This creates the Haskell package set. 16 | # https://input-output-hk.github.io/haskell.nix/user-guide/projects/ 17 | pkgSet = haskell-nix.cabalProject { 18 | src = haskell-nix.haskellLib.cleanGit { src = ../.; name = "cardano-rest"; }; 19 | compiler-nix-name = compiler; 20 | modules = [ 21 | { 22 | # Packages we wish to ignore version bounds of. 23 | # This is similar to jailbreakCabal, however it 24 | # does not require any messing with cabal files. 25 | packages.katip.doExactConfig = true; 26 | 27 | # split data output for ekg to reduce closure size 28 | packages.ekg.components.library.enableSeparateDataOutput = true; 29 | packages.cardano-explorer-api.configureFlags = [ "--ghc-option=-Wall" "--ghc-option=-Werror" ]; 30 | packages.cardano-submit-api.configureFlags = [ "--ghc-option=-Wall" "--ghc-option=-Werror" ]; 31 | enableLibraryProfiling = profiling; 32 | } 33 | ]; 34 | pkg-def-extras = [ 35 | # Workaround for https://github.com/input-output-hk/haskell.nix/issues/214 36 | (hackage: { 37 | packages = { 38 | "hsc2hs" = (((hackage.hsc2hs)."0.68.4").revisions).default; 39 | }; 40 | }) 41 | ]; 42 | }; 43 | in 44 | pkgSet 45 | -------------------------------------------------------------------------------- /nix/nixos/cardano-explorer-api-service.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, ... }: 2 | 3 | let 4 | cfg = config.services.cardano-explorer-api; 5 | in { 6 | options = { 7 | services.cardano-explorer-api = { 8 | enable = lib.mkEnableOption "enable the cardano-explorer web api"; 9 | script = lib.mkOption { 10 | internal = true; 11 | type = lib.types.package; 12 | }; 13 | port = lib.mkOption { 14 | type = lib.types.port; 15 | default = 8100; 16 | }; 17 | listenAddress = lib.mkOption { 18 | type = lib.types.str; 19 | default = "127.0.0.1"; 20 | }; 21 | pgpass = lib.mkOption { 22 | internal = true; 23 | type = lib.types.path; 24 | }; 25 | pgpassFile = lib.mkOption { 26 | type = lib.types.path; 27 | default = builtins.toFile "pgpass" "${cfg.postgres.socketdir}:${toString cfg.postgres.port}:${cfg.postgres.database}:${cfg.postgres.user}:*"; 28 | }; 29 | package = lib.mkOption { 30 | type = lib.types.package; 31 | default = (import ../. {}).cardanoRestHaskellPackages.cardano-explorer-api.components.exes.cardano-explorer-api; 32 | }; 33 | postgres = { 34 | socketdir = lib.mkOption { 35 | type = lib.types.str; 36 | default = "/run/postgresql"; 37 | description = "the path to the postgresql socket"; 38 | }; 39 | port = lib.mkOption { 40 | type = lib.types.int; 41 | default = 5432; 42 | description = "the postgresql port"; 43 | }; 44 | database = lib.mkOption { 45 | type = lib.types.str; 46 | default = "cexplorer"; 47 | description = "the postgresql database to use"; 48 | }; 49 | user = lib.mkOption { 50 | type = lib.types.str; 51 | default = "cexplorer"; 52 | description = "the postgresql user to use"; 53 | }; 54 | }; 55 | }; 56 | }; 57 | config = lib.mkIf cfg.enable { 58 | services.cardano-explorer-api = { 59 | script = pkgs.writeShellScript "cardano-explorer-api" '' 60 | export PGPASSFILE=${cfg.pgpassFile} 61 | exec ${cfg.package}/bin/cardano-explorer-api \ 62 | --port ${toString cfg.port} \ 63 | --listen-address ${cfg.listenAddress} 64 | ''; 65 | }; 66 | systemd.services.cardano-explorer-api = { 67 | wantedBy = [ "multi-user.target" ]; 68 | requires = [ "postgresql.service" ]; 69 | path = [ pkgs.netcat ]; 70 | preStart = '' 71 | for x in {1..10}; do 72 | nc -z localhost ${toString cfg.postgres.port} && break 73 | echo loop $x: waiting for postgresql 2 sec... 74 | sleep 2 75 | done 76 | ''; 77 | serviceConfig = { 78 | ExecStart = cfg.script; 79 | User = "cexplorer"; 80 | }; 81 | }; 82 | }; 83 | } 84 | -------------------------------------------------------------------------------- /nix/nixos/cardano-submit-api-service.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, ... }: 2 | 3 | # notes: 4 | # this service exposes an http port, and connects to a cardano-node over a UNIX socket 5 | let 6 | cfg = config.services.cardano-submit-api; 7 | envConfig = cfg.environment; 8 | localPkgs = import ../. {}; 9 | in { 10 | options = { 11 | services.cardano-submit-api = { 12 | enable = lib.mkEnableOption "enable the cardano-submit-api api"; 13 | script = lib.mkOption { 14 | internal = true; 15 | type = lib.types.package; 16 | }; 17 | package = lib.mkOption { 18 | type = lib.types.package; 19 | default = localPkgs.cardanoRestHaskellPackages.cardano-submit-api.components.exes.cardano-submit-api; 20 | }; 21 | port = lib.mkOption { 22 | type = lib.types.port; 23 | default = 8090; 24 | }; 25 | listenAddress = lib.mkOption { 26 | type = lib.types.str; 27 | default = "127.0.0.1"; 28 | }; 29 | socketPath = lib.mkOption { 30 | type = lib.types.nullOr lib.types.path; 31 | default = null; 32 | }; 33 | config = lib.mkOption { 34 | type = lib.types.nullOr lib.types.attrs; 35 | default = localPkgs.iohkNix.cardanoLib.defaultExplorerLogConfig; 36 | }; 37 | network = lib.mkOption { 38 | type = lib.types.nullOr lib.types.str; 39 | description = "network name"; 40 | default = null; 41 | }; 42 | environment = lib.mkOption { 43 | type = lib.types.nullOr lib.types.attrs; 44 | default = localPkgs.iohkNix.cardanoLib.environments.${cfg.network}; 45 | }; 46 | }; 47 | }; 48 | config = let 49 | envNodeCfg = cfg.environment.nodeConfig; 50 | shelleyGenesisParams = __fromJSON (__readFile envNodeCfg.ShelleyGenesisFile); 51 | envFlag = if cfg.network == "mainnet" then "--mainnet" else "--testnet-magic ${toString shelleyGenesisParams.networkMagic}"; 52 | in lib.mkIf cfg.enable { 53 | services.cardano-submit-api.script = pkgs.writeShellScript "cardano-submit-api" '' 54 | ${if (cfg.socketPath == null) then ''if [ -z "$CARDANO_NODE_SOCKET_PATH" ] 55 | then 56 | echo "You must set \$CARDANO_NODE_SOCKET_PATH" 57 | exit 1 58 | fi'' else "export \"CARDANO_NODE_SOCKET_PATH=${cfg.socketPath}\""} 59 | exec ${cfg.package}/bin/cardano-submit-api --socket-path "$CARDANO_NODE_SOCKET_PATH" ${envFlag} \ 60 | --port ${toString cfg.port} \ 61 | --listen-address ${cfg.listenAddress} \ 62 | --config ${builtins.toFile "submit-api.json" (builtins.toJSON cfg.config)} 63 | ''; 64 | systemd.services.cardano-submit-api = { 65 | serviceConfig = { 66 | ExecStart = config.services.cardano-submit-api.script; 67 | DynamicUser = true; 68 | }; 69 | wantedBy = [ "multi-user.target" ]; 70 | after = [ "cardano-node.service" ]; 71 | }; 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /nix/nixos/default.nix: -------------------------------------------------------------------------------- 1 | 2 | { 3 | imports = import ./module-list.nix; 4 | } 5 | -------------------------------------------------------------------------------- /nix/nixos/module-list.nix: -------------------------------------------------------------------------------- 1 | [ 2 | ./cardano-explorer-api-service.nix 3 | ./cardano-submit-api-service.nix 4 | ] 5 | -------------------------------------------------------------------------------- /nix/pkgs.nix: -------------------------------------------------------------------------------- 1 | # our packages overlay 2 | pkgs: _: with pkgs; { 3 | cardanoRestHaskellPackages = import ./haskell.nix { 4 | inherit config 5 | lib 6 | stdenv 7 | haskell-nix 8 | buildPackages 9 | ; 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /nix/regenerate.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | cd $(git rev-parse --show-toplevel) 4 | 5 | exec $(nix-build `dirname $0`/. -A iohkNix.cabalProjectRegenerate --no-out-link --option substituters "https://hydra.iohk.io https://cache.nixos.org" --option trusted-substituters "" --option trusted-public-keys "hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=")/bin/cabal-project-regenerate 6 | -------------------------------------------------------------------------------- /nix/scripts.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib, iohkNix, customConfig }: 2 | let 3 | blacklistedEnvs = [ "selfnode" "shelley_selfnode" "latency-tests" "mainnet-ci" "shelley_testnet" ]; 4 | environments = lib.filterAttrs (k: v: (!builtins.elem k blacklistedEnvs)) iohkNix.cardanoLib.environments; 5 | mkStartScripts = envConfig: let 6 | extraModule = { 7 | services.cardano-explorer-api = { 8 | enable = true; 9 | postgres.user = "*"; 10 | }; 11 | services.cardano-submit-api = { 12 | enable = true; 13 | environment = envConfig; 14 | network = envConfig.name; 15 | }; 16 | }; 17 | systemdCompat.options = { 18 | systemd.services = lib.mkOption {}; 19 | services.postgresql = lib.mkOption {}; 20 | users = lib.mkOption {}; 21 | }; 22 | eval = lib.evalModules { 23 | prefix = []; 24 | modules = import nixos/module-list.nix ++ [ systemdCompat extraModule customConfig ]; 25 | args = { inherit pkgs; }; 26 | }; 27 | in { 28 | explorer-api = eval.config.services.cardano-explorer-api.script; 29 | submit-api = eval.config.services.cardano-submit-api.script; 30 | }; 31 | in iohkNix.cardanoLib.forEnvironmentsCustom mkStartScripts environments // { inherit environments; } 32 | -------------------------------------------------------------------------------- /nix/sources.json: -------------------------------------------------------------------------------- 1 | { 2 | "haskell.nix": { 3 | "branch": "master", 4 | "description": "Alternative Haskell Infrastructure for Nixpkgs", 5 | "homepage": "https://input-output-hk.github.io/haskell.nix", 6 | "owner": "input-output-hk", 7 | "repo": "haskell.nix", 8 | "rev": "69dcebb5fae7b5ccdf5756d0d097e9bb8dd880f1", 9 | "sha256": "18jvl3mj0hvy4jx04y6mihwpfslaqj0qcz1wxjj7ryb8vmz4cz6r", 10 | "type": "tarball", 11 | "url": "https://github.com/input-output-hk/haskell.nix/archive/69dcebb5fae7b5ccdf5756d0d097e9bb8dd880f1.tar.gz", 12 | "url_template": "https://github.com///archive/.tar.gz" 13 | }, 14 | "iohk-nix": { 15 | "branch": "master", 16 | "description": "nix scripts shared across projects", 17 | "homepage": null, 18 | "owner": "input-output-hk", 19 | "repo": "iohk-nix", 20 | "rev": "f345d26508966e434d8db57d6c3b4ce4202c9fb5", 21 | "sha256": "1xcar2r6jm68dp5s8kc2rii0rcg47w41qc1akazbngkj1x6hf1r2", 22 | "type": "tarball", 23 | "url": "https://github.com/input-output-hk/iohk-nix/archive/f345d26508966e434d8db57d6c3b4ce4202c9fb5.tar.gz", 24 | "url_template": "https://github.com///archive/.tar.gz" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /nix/stack-shell.nix: -------------------------------------------------------------------------------- 1 | 2 | with import ./. {}; 3 | 4 | haskell.lib.buildStackProject { 5 | name = "stack-env"; 6 | buildInputs = with pkgs; [ zlib openssl gmp libffi git systemd haskellPackages.happy ]; 7 | ghc = (import ../shell.nix {inherit pkgs;}).ghc.baseGhc; 8 | } 9 | -------------------------------------------------------------------------------- /nix/util.nix: -------------------------------------------------------------------------------- 1 | { haskell-nix }: 2 | 3 | with haskell-nix.haskellLib; 4 | { 5 | 6 | inherit 7 | selectProjectPackages 8 | collectComponents'; 9 | 10 | inherit (extra) 11 | recRecurseIntoAttrs 12 | collectChecks; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /rest-common/cardano-rest-common.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: >= 1.10 2 | 3 | -- http://haskell.org/cabal/users-guide/ 4 | 5 | name: cardano-rest-common 6 | version: 3.1.2 7 | synopsis: A Block Explorer for the Cardano network 8 | description: 9 | homepage: https://github.com/input-output-hk/cardano-explorer 10 | bug-reports: https://github.com/input-output-hk/cardano-explorer/issues 11 | license: Apache-2.0 12 | license-file: LICENSE 13 | author: IOHK Engineering Team 14 | maintainer: operations@iohk.io 15 | copyright: (c) 2019-2021 IOHK 16 | category: Language 17 | build-type: Simple 18 | extra-source-files: CHANGELOG.md 19 | 20 | library 21 | default-language: Haskell2010 22 | hs-source-dirs: src 23 | ghc-options: -Wall 24 | -Werror 25 | -Wcompat 26 | -fwarn-redundant-constraints 27 | -fwarn-incomplete-patterns 28 | -fwarn-unused-imports 29 | -Wincomplete-record-updates 30 | -Wincomplete-uni-patterns 31 | -Weverything 32 | -Wno-safe 33 | -Wno-unsafe 34 | -Wno-implicit-prelude 35 | -Wno-all-missed-specialisations 36 | -Wno-missing-import-lists 37 | 38 | exposed-modules: Cardano.Rest.Parsers 39 | , Cardano.Rest.Types 40 | , Cardano.Rest.Web 41 | 42 | -- other-modules: 43 | 44 | build-depends: base >= 4.12 && < 4.13 45 | , optparse-applicative 46 | , monad-logger 47 | , text 48 | , streaming-commons 49 | , network 50 | , servant-server 51 | , warp 52 | -------------------------------------------------------------------------------- /rest-common/src/Cardano/Rest/Parsers.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE ApplicativeDo #-} 2 | {-# LANGUAGE NamedFieldPuns #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | 5 | module Cardano.Rest.Parsers 6 | ( pWebserverConfig 7 | ) where 8 | 9 | import Cardano.Rest.Types 10 | ( WebserverConfig (..) ) 11 | import Data.String 12 | ( fromString ) 13 | import Network.Wai.Handler.Warp 14 | ( HostPreference, Port ) 15 | import Options.Applicative 16 | ( Parser ) 17 | import Options.Applicative 18 | ( auto 19 | , help 20 | , long 21 | , metavar 22 | , option 23 | , showDefault 24 | , strOption 25 | , switch 26 | , value 27 | ) 28 | 29 | pWebserverConfig :: Port -> Parser WebserverConfig 30 | pWebserverConfig defaultPort = do 31 | wcHost <- pHostPreferenceOption 32 | isRandom <- pRandomPortOption 33 | wcPort <- pPortOption defaultPort 34 | pure 35 | WebserverConfig 36 | { wcHost 37 | , wcPort = 38 | if isRandom 39 | then 0 40 | else wcPort 41 | } 42 | 43 | pHostPreferenceOption :: Parser HostPreference 44 | pHostPreferenceOption = 45 | fromString <$> 46 | strOption 47 | (long "listen-address" <> 48 | metavar "HOST" <> 49 | help 50 | ("Specification of which host to the bind API server to. " <> 51 | "Can be an IPv[46] address, hostname, or '*'.") <> 52 | value "127.0.0.1" <> showDefault) 53 | 54 | pPortOption :: Port -> Parser Port 55 | pPortOption defaultPort = 56 | option auto $ 57 | long "port" <> 58 | metavar "INT" <> 59 | help "Port used for the API server." <> value defaultPort <> showDefault 60 | 61 | pRandomPortOption :: Parser Bool 62 | pRandomPortOption = 63 | switch $ 64 | long "random-port" <> 65 | help "Serve API on any available port (overrides --port)" 66 | -------------------------------------------------------------------------------- /rest-common/src/Cardano/Rest/Types.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE NamedFieldPuns #-} 2 | {-# LANGUAGE StrictData #-} 3 | 4 | module Cardano.Rest.Types 5 | ( WebserverConfig(..) 6 | , toWarpSettings 7 | ) where 8 | 9 | import Data.Function 10 | ( (&) ) 11 | import qualified Network.Wai.Handler.Warp as Warp 12 | 13 | ------------------------------------------------------------ 14 | data WebserverConfig = 15 | WebserverConfig 16 | { wcHost :: Warp.HostPreference 17 | , wcPort :: Warp.Port 18 | } 19 | 20 | instance Show WebserverConfig where 21 | show (WebserverConfig {wcHost, wcPort}) = show wcHost <> ":" <> show wcPort 22 | 23 | toWarpSettings :: WebserverConfig -> Warp.Settings 24 | toWarpSettings (WebserverConfig {wcHost, wcPort}) = 25 | Warp.defaultSettings & Warp.setHost wcHost & Warp.setPort wcPort 26 | -------------------------------------------------------------------------------- /rest-common/src/Cardano/Rest/Web.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | module Cardano.Rest.Web 4 | ( runSettings 5 | ) where 6 | 7 | import Control.Exception 8 | ( bracket ) 9 | import Control.Monad.IO.Class 10 | ( liftIO ) 11 | import Control.Monad.Logger 12 | ( logInfoN, runStdoutLoggingT ) 13 | import Data.Streaming.Network 14 | ( bindPortTCP ) 15 | import Data.Text 16 | ( pack ) 17 | import Network.Socket 18 | ( close, getSocketName, withSocketsDo ) 19 | import Network.Wai.Handler.Warp 20 | ( Settings, getHost, getPort, runSettingsSocket ) 21 | import Servant 22 | ( Application ) 23 | 24 | -- | Like 'Network.Wai.Handler.Warp.runSettings', except with better logging. 25 | runSettings :: Settings -> Application -> IO () 26 | runSettings settings app = 27 | withSocketsDo $ do 28 | bracket 29 | (bindPortTCP (getPort settings) (getHost settings)) 30 | close 31 | (\socket -> 32 | runStdoutLoggingT $ do 33 | addr <- liftIO $ getSocketName socket 34 | logInfoN $ "Running server on " <> pack (show addr) 35 | liftIO $ runSettingsSocket settings socket app) 36 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | # This file is used by nix-shell. 2 | # It just takes the shell attribute from default.nix. 3 | { config ? {} 4 | , sourcesOverride ? {} 5 | , withHoogle ? true 6 | , pkgs ? import ./nix { 7 | inherit config sourcesOverride; 8 | } 9 | }: 10 | with pkgs; 11 | let 12 | # This provides a development environment that can be used with nix-shell or 13 | # lorri. See https://input-output-hk.github.io/haskell.nix/user-guide/development/ 14 | shell = cardanoRestHaskellPackages.shellFor { 15 | name = "cabal-dev-shell"; 16 | 17 | packages = ps: with ps; [ 18 | ps.cardano-explorer-api 19 | ps.cardano-submit-api 20 | ]; 21 | 22 | # These programs will be available inside the nix-shell. 23 | buildInputs = with haskellPackages; [ 24 | cabal-install 25 | ghcid 26 | hlint 27 | weeder 28 | nix 29 | niv 30 | pkgconfig 31 | sqlite-interactive 32 | tmux 33 | git 34 | ]; 35 | 36 | # Prevents cabal from choosing alternate plans, so that 37 | # *all* dependencies are provided by Nix. 38 | exactDeps = true; 39 | 40 | inherit withHoogle; 41 | }; 42 | 43 | devops = pkgs.stdenv.mkDerivation { 44 | name = "devops-shell"; 45 | buildInputs = [ 46 | niv 47 | ]; 48 | shellHook = '' 49 | echo "DevOps Tools" \ 50 | | ${figlet}/bin/figlet -f banner -c \ 51 | | ${lolcat}/bin/lolcat 52 | 53 | echo "NOTE: you may need to export GITHUB_TOKEN if you hit rate limits with niv" 54 | echo "Commands: 55 | * niv update - update package 56 | 57 | " 58 | ''; 59 | }; 60 | 61 | in 62 | 63 | shell // { inherit devops; } 64 | -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | resolver: https://raw.githubusercontent.com/input-output-hk/cardano-haskell/578c5777b8f4db1f8f5d4d8c07b1945dc0cbaab7/snapshots/cardano-1.25.1.yaml 2 | compiler: ghc-8.6.5 3 | 4 | allow-newer: true 5 | 6 | packages: 7 | - explorer-api 8 | - rest-common 9 | - submit-api 10 | 11 | extra-deps: 12 | - esqueleto-3.2.0 13 | - persistent-2.10.5.1 14 | - persistent-postgresql-2.10.1.2 15 | - persistent-template-2.8.2.3 16 | - Diff-0.4.0 17 | 18 | - git: https://github.com/input-output-hk/cardano-db-sync 19 | commit: 1c7dbdac93e4d75118f1592dbb411f4f6a617128 20 | # Author: Erik de Castro Lopo 21 | # Date: Wed Jan 27 08:22:21 2021 +1100 22 | # 23 | # Version 8.0.0 24 | subdirs: 25 | - cardano-db 26 | 27 | ghc-options: 28 | $locals: -ddump-to-file -ddump-hi 29 | cardano-explorer-api: -Wall -Werror -fwarn-redundant-constraints 30 | cardano-submit-api: -Wall -Werror -fwarn-redundant-constraints 31 | 32 | nix: 33 | shell-file: nix/stack-shell.nix 34 | 35 | flags: 36 | # Bundle VRF crypto in libsodium and do not rely on an external fork to have it. 37 | # This still requires the host system to have the 'standard' libsodium installed. 38 | cardano-crypto-praos: 39 | external-libsodium-vrf: false 40 | -------------------------------------------------------------------------------- /submit-api/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 3.1.2 -- January 2021 4 | 5 | * Upgrade dependencies to more recent versions of the Cardano eco-system (cardano-node 1.25.1, cardano-db-sync 8.0.0) 6 | 7 | ## 3.1.1 -- December 2020 8 | 9 | * Revert the MIME-type `encoding` addition from 3.1.0. Turns out there's a bug with an underlying dependency 10 | causing the feature to misbehave. Removing for now. Will rework later if time allows. 11 | 12 | ## 3.1.0 -- December 2020 13 | 14 | * Allow clients to define an extra outer-encoding for the submit endpoint using a MIME-type parameter `encoding`. 15 | When set, the parameter must be set to either `base16` or `base64` and the data payload must be encoded accordingly. 16 | When not set, then the present behaviour remains applicable and a raw octet stream is expected. 17 | 18 | Examples: 19 | - `Content-Type: application/cbor;encoding=base16` 20 | - `Content-Type: application/cbor;encoding=base64` 21 | - `Content-Type: application/cbor` 22 | 23 | * Upgrade dependencies to more recent version of the Cardano eco-system (cardano-db-sync-7.1.0 & cardano-node 1.24.2) 24 | 25 | ## 3.0.0 -- October 2020 26 | 27 | * Upgrade dependencies to more recent version of the Cardano eco-system (cardano-db-sync-6.0.0) 28 | * Interface **breaking change**: the command-line no longer accept a `--security-param` option. 29 | 30 | ## 2.1.3 -- August 2020 31 | 32 | * No changes. Bumping version to keep consistency with explorer-api. 33 | 34 | ## 2.1.1 -- July 2020 35 | 36 | * Update dependencies for compatibility with `cardano-node` 1.18.0. 37 | 38 | ## 2.1.0 -- July 2020 39 | 40 | * Fixing a cabal warning caused by flags in the wrong place. 41 | * Adding a '--random-port' flag.. 42 | * You can now specify the webserver settings on the command line. 43 | * Add support for multiple protocol modes, defaulting to Cardano 44 | * Internal refactoring. 45 | 46 | ## 2.0.0 -- March 2020 47 | 48 | * Moved 'cardano-explorer-api' into its own repository 49 | * Renamed service and build artifacts 50 | * Add swagger documentation for cardano-submit-api 51 | * API **breaking** changes: 52 | * returns 400 Bad Request on errors, and 202 for successful submission. 53 | * returns plain text transaction id on success, and plain text error on errors (cf API documentation) 54 | 55 | ## 1.3.0 -- January 2020 56 | 57 | * API change: require content-type `application/cbor` for posted transactions. 58 | * API change: require raw transaction binary format for posted transactions. 59 | * Add more specific error message when posted transaction is hex encoded. 60 | * Include example testnet configuration and update README on using it. 61 | * Documentation for submission API and how to generate example transactions. 62 | * Update dependencies to latest versions. 63 | * Docker image: log all runit services to stdout 64 | * Initial documentation on how to use build and run the components in docker 65 | 66 | ## 1.2.? -- ? 67 | 68 | * Require content-type `application/cbor` for posted transaction. 69 | * Add more specific error message when posted transaction is hex encoded. 70 | 71 | ## 1.2.2 -- January 2020 72 | 73 | * Update dependencies to latest versions. 74 | * Service added to docker files. 75 | 76 | ## 1.2.1 -- January 2020 77 | 78 | * Update dependencies to latest versions. 79 | * Improve logging of tx submit responses. 80 | * Add a README. 81 | * Add QA document on how to test the component. 82 | 83 | ## 1.2.0 -- December 2019 84 | 85 | * First release of the Tx Submission endpoint 86 | -------------------------------------------------------------------------------- /submit-api/app/cardano-submit-api.hs: -------------------------------------------------------------------------------- 1 | import Cardano.TxSubmit (opts, runTxSubmitWebapi) 2 | 3 | import qualified Options.Applicative as Opt 4 | 5 | main :: IO () 6 | main = 7 | runTxSubmitWebapi =<< Opt.execParser opts 8 | -------------------------------------------------------------------------------- /submit-api/config/tx-submit-mainnet-config.yaml: -------------------------------------------------------------------------------- 1 | # Tx Submission Server Configuration 2 | 3 | EnableLogMetrics: False 4 | EnableLogging: True 5 | 6 | # ------------------------------------------------------------------------------ 7 | # Logging configuration follows. 8 | 9 | # global filter; messages must have at least this severity to pass: 10 | minSeverity: Info 11 | 12 | # global file rotation settings: 13 | rotation: 14 | rpLogLimitBytes: 5000000 15 | rpKeepFilesNum: 10 16 | rpMaxAgeHours: 24 17 | 18 | # these backends are initialized: 19 | setupBackends: 20 | - AggregationBK 21 | - KatipBK 22 | # - EditorBK 23 | # - EKGViewBK 24 | 25 | # if not indicated otherwise, then messages are passed to these backends: 26 | defaultBackends: 27 | - KatipBK 28 | 29 | # if wanted, the GUI is listening on this port: 30 | # hasGUI: 12787 31 | 32 | # if wanted, the EKG interface is listening on this port: 33 | # hasEKG: 12788 34 | 35 | # here we set up outputs of logging in 'katip': 36 | setupScribes: 37 | - scKind: StdoutSK 38 | scName: stdout 39 | scFormat: ScText 40 | scRotation: null 41 | 42 | # if not indicated otherwise, then log output is directed to this: 43 | defaultScribes: 44 | - - StdoutSK 45 | - stdout 46 | 47 | # more options which can be passed as key-value pairs: 48 | options: 49 | cfokey: 50 | value: "Release-1.0.0" 51 | mapSubtrace: 52 | benchmark: 53 | contents: 54 | - GhcRtsStats 55 | - MonotonicClock 56 | subtrace: ObservableTrace 57 | '#ekgview': 58 | contents: 59 | - - tag: Contains 60 | contents: 'cardano.epoch-validation.benchmark' 61 | - - tag: Contains 62 | contents: .monoclock.basic. 63 | - - tag: Contains 64 | contents: 'cardano.epoch-validation.benchmark' 65 | - - tag: Contains 66 | contents: diff.RTS.cpuNs.timed. 67 | - - tag: StartsWith 68 | contents: '#ekgview.#aggregation.cardano.epoch-validation.benchmark' 69 | - - tag: Contains 70 | contents: diff.RTS.gcNum.timed. 71 | subtrace: FilterTrace 72 | 'cardano.epoch-validation.utxo-stats': 73 | # Change the `subtrace` value to `Neutral` in order to log 74 | # `UTxO`-related messages during epoch validation. 75 | subtrace: NoTrace 76 | '#messagecounters.aggregation': 77 | subtrace: NoTrace 78 | '#messagecounters.ekgview': 79 | subtrace: NoTrace 80 | '#messagecounters.switchboard': 81 | subtrace: NoTrace 82 | '#messagecounters.katip': 83 | subtrace: NoTrace 84 | '#messagecounters.monitoring': 85 | subtrace: NoTrace 86 | 'cardano.#messagecounters.aggregation': 87 | subtrace: NoTrace 88 | 'cardano.#messagecounters.ekgview': 89 | subtrace: NoTrace 90 | 'cardano.#messagecounters.switchboard': 91 | subtrace: NoTrace 92 | 'cardano.#messagecounters.katip': 93 | subtrace: NoTrace 94 | 'cardano.#messagecounters.monitoring': 95 | subtrace: NoTrace 96 | mapBackends: 97 | cardano.epoch-validation.benchmark: 98 | - AggregationBK 99 | '#aggregation.cardano.epoch-validation.benchmark': 100 | - EKGViewBK 101 | -------------------------------------------------------------------------------- /submit-api/src/Cardano/TxSubmit.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE NamedFieldPuns #-} 2 | {-# LANGUAGE NoImplicitPrelude #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | 5 | module Cardano.TxSubmit 6 | ( module X 7 | , runTxSubmitWebapi 8 | ) where 9 | 10 | import qualified Cardano.BM.Setup as Logging 11 | import qualified Cardano.BM.Trace as Logging 12 | import Cardano.BM.Trace (Trace, logInfo) 13 | 14 | import Cardano.Prelude 15 | 16 | import Cardano.TxSubmit.CLI.Parsers as X 17 | import Cardano.TxSubmit.CLI.Types as X 18 | import Cardano.TxSubmit.Config as X 19 | import Cardano.TxSubmit.Metrics (registerMetricsServer) 20 | import Cardano.TxSubmit.Tx as X 21 | import Cardano.TxSubmit.Types as X 22 | import Cardano.TxSubmit.Util as X 23 | import Cardano.TxSubmit.Web as X 24 | 25 | import qualified Control.Concurrent.Async as Async 26 | import Control.Monad.IO.Class (liftIO) 27 | 28 | import Data.Text (Text) 29 | 30 | 31 | runTxSubmitWebapi :: TxSubmitNodeParams -> IO () 32 | runTxSubmitWebapi tsnp = do 33 | tsnc <- readTxSubmitNodeConfig (unConfigFile $ tspConfigFile tsnp) 34 | trce <- mkTracer tsnc 35 | (metrics, metricsServer) <- registerMetricsServer 36 | txSubmitServer <- Async.async $ 37 | runTxSubmitServer trce metrics tspWebserverConfig tspProtocol tspNetworkId tspSocketPath 38 | void $ Async.waitAnyCancel 39 | [ txSubmitServer 40 | , metricsServer 41 | ] 42 | logInfo trce "runTxSubmitWebapi: Async.waitAnyCancel returned" 43 | where 44 | TxSubmitNodeParams 45 | { tspProtocol 46 | , tspNetworkId 47 | , tspSocketPath 48 | , tspWebserverConfig 49 | } = tsnp 50 | 51 | mkTracer :: TxSubmitNodeConfig -> IO (Trace IO Text) 52 | mkTracer enc = 53 | if not (tscEnableLogging enc) 54 | then pure Logging.nullTracer 55 | else liftIO $ Logging.setupTrace (Right $ tscLoggingConfig enc) "cardano-tx-submit" 56 | -------------------------------------------------------------------------------- /submit-api/src/Cardano/TxSubmit/CLI/Types.hs: -------------------------------------------------------------------------------- 1 | module Cardano.TxSubmit.CLI.Types 2 | ( ConfigFile (..) 3 | , GenesisFile (..) 4 | , SocketPath (..) 5 | , TxSubmitNodeParams (..) 6 | ) where 7 | 8 | import Cardano.Api.Protocol 9 | ( Protocol (..) ) 10 | import Cardano.Api.Typed 11 | ( NetworkId (..) ) 12 | import Cardano.Rest.Types 13 | ( WebserverConfig ) 14 | 15 | -- | The product type of all command line arguments 16 | data TxSubmitNodeParams = TxSubmitNodeParams 17 | { tspConfigFile :: !ConfigFile 18 | , tspProtocol :: !Protocol 19 | , tspNetworkId :: !NetworkId 20 | , tspSocketPath :: !SocketPath 21 | , tspWebserverConfig :: !WebserverConfig 22 | } 23 | 24 | newtype ConfigFile = ConfigFile 25 | { unConfigFile :: FilePath 26 | } 27 | 28 | newtype GenesisFile = GenesisFile 29 | { unGenesisFile :: FilePath 30 | } 31 | 32 | newtype SocketPath = SocketPath 33 | { unSocketPath :: FilePath 34 | } 35 | -------------------------------------------------------------------------------- /submit-api/src/Cardano/TxSubmit/Config.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleInstances #-} 2 | {-# LANGUAGE NoImplicitPrelude #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-# LANGUAGE ScopedTypeVariables #-} 5 | 6 | module Cardano.TxSubmit.Config 7 | ( TxSubmitNodeConfig 8 | , GenTxSubmitNodeConfig (..) 9 | , readTxSubmitNodeConfig 10 | ) where 11 | 12 | import qualified Cardano.BM.Configuration as Logging 13 | import qualified Cardano.BM.Configuration.Model as Logging 14 | import qualified Cardano.BM.Data.Configuration as Logging 15 | 16 | import Cardano.Prelude 17 | 18 | import Cardano.TxSubmit.Util 19 | 20 | import Data.Aeson (FromJSON (..), Object, Value (..), (.:)) 21 | import Data.Aeson.Types (Parser) 22 | import qualified Data.Aeson as Aeson 23 | import qualified Data.ByteString.Char8 as BS 24 | import qualified Data.Text as Text 25 | import qualified Data.Yaml as Yaml 26 | 27 | type TxSubmitNodeConfig = GenTxSubmitNodeConfig Logging.Configuration 28 | 29 | data GenTxSubmitNodeConfig a = GenTxSubmitNodeConfig 30 | { tscLoggingConfig :: !a 31 | , tscEnableLogging :: !Bool 32 | , tscEnableMetrics :: !Bool 33 | } 34 | 35 | readTxSubmitNodeConfig :: FilePath -> IO TxSubmitNodeConfig 36 | readTxSubmitNodeConfig fp = do 37 | res <- Yaml.decodeEither' <$> readLoggingConfig 38 | case res of 39 | Left err -> panic $ "readTxSubmitNodeConfig: Error parsing config: " <> textShow err 40 | Right icr -> convertLogging icr 41 | where 42 | readLoggingConfig :: IO ByteString 43 | readLoggingConfig = 44 | catch (BS.readFile fp) $ \(_ :: IOException) -> 45 | panic $ "Cannot find the logging configuration file at : " <> Text.pack fp 46 | 47 | convertLogging :: GenTxSubmitNodeConfig Logging.Representation -> IO TxSubmitNodeConfig 48 | convertLogging tsc = do 49 | lc <- Logging.setupFromRepresentation $ tscLoggingConfig tsc 50 | pure $ tsc { tscLoggingConfig = lc } 51 | 52 | -- ------------------------------------------------------------------------------------------------- 53 | 54 | instance FromJSON (GenTxSubmitNodeConfig Logging.Representation) where 55 | parseJSON o = 56 | Aeson.withObject "top-level" parseGenTxSubmitNodeConfig o 57 | 58 | parseGenTxSubmitNodeConfig :: Object -> Parser (GenTxSubmitNodeConfig Logging.Representation) 59 | parseGenTxSubmitNodeConfig o = 60 | GenTxSubmitNodeConfig 61 | <$> parseJSON (Object o) 62 | <*> o .: "EnableLogging" 63 | <*> o .: "EnableLogMetrics" 64 | -------------------------------------------------------------------------------- /submit-api/src/Cardano/TxSubmit/ErrorRender.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE NamedFieldPuns #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | module Cardano.TxSubmit.ErrorRender 5 | ( renderApplyMempoolPayloadErr 6 | , renderEraMismatch 7 | ) where 8 | 9 | -- This file contains error renders. The should hve defined at a lower level, with the error 10 | -- type definitions, but for some reason have not been. 11 | -- They will be defined here for now and then moved where they are supposed to be once they 12 | -- are working. 13 | 14 | import Cardano.Chain.Byron.API 15 | ( ApplyMempoolPayloadErr (..) ) 16 | import Cardano.Chain.UTxO.UTxO 17 | ( UTxOError (..) ) 18 | import Cardano.Chain.UTxO.Validation 19 | ( TxValidationError (..), UTxOValidationError (..) ) 20 | import Data.Text 21 | ( Text ) 22 | import Formatting 23 | ( build, sformat, stext, (%) ) 24 | import Ouroboros.Consensus.Cardano.Block 25 | ( EraMismatch (..) ) 26 | 27 | import qualified Data.Text as Text 28 | 29 | renderApplyMempoolPayloadErr :: ApplyMempoolPayloadErr -> Text 30 | renderApplyMempoolPayloadErr err = 31 | case err of 32 | MempoolTxErr ve -> renderValidationError ve 33 | MempoolDlgErr {} -> "Delegation error" 34 | MempoolUpdateProposalErr {} -> "Update proposal error" 35 | MempoolUpdateVoteErr {} -> "Update vote error" 36 | 37 | 38 | renderValidationError :: UTxOValidationError -> Text 39 | renderValidationError ve = 40 | case ve of 41 | UTxOValidationTxValidationError tve -> renderTxValidationError tve 42 | UTxOValidationUTxOError ue -> renderUTxOError ue 43 | 44 | 45 | renderTxValidationError :: TxValidationError -> Text 46 | renderTxValidationError tve = 47 | "Tx Validation: " <> 48 | case tve of 49 | TxValidationLovelaceError txt e -> 50 | sformat ("Lovelace error "% stext %": "% build) txt e 51 | TxValidationFeeTooSmall tx expected actual -> 52 | sformat ("Tx "% build %" fee "% build %"too low, expected "% build) tx actual expected 53 | TxValidationWitnessWrongSignature wit pmid sig -> 54 | sformat ("Bad witness "% build %" for signature "% stext %" protocol magic id "% stext) wit (textShow sig) (textShow pmid) 55 | TxValidationWitnessWrongKey wit addr -> 56 | sformat ("Bad witness "% build %" for address "% build) wit addr 57 | TxValidationMissingInput tx -> 58 | sformat ("Validation cannot find input tx "% build) tx 59 | -- Fields are 60 | TxValidationNetworkMagicMismatch expected actual -> 61 | mconcat [ "Bad network magic ", textShow actual, ", expected ", textShow expected ] 62 | TxValidationTxTooLarge expected actual -> 63 | mconcat [ "Tx is ", textShow actual, " bytes, but expected < ", textShow expected, " bytes" ] 64 | TxValidationUnknownAddressAttributes -> 65 | "Unknown address attributes" 66 | TxValidationUnknownAttributes -> 67 | "Unknown attributes" 68 | 69 | renderUTxOError :: UTxOError -> Text 70 | renderUTxOError ue = 71 | "UTxOError: " <> 72 | case ue of 73 | UTxOMissingInput tx -> sformat ("Lookup of tx "% build %" failed") tx 74 | UTxOOverlappingUnion -> "Union or two overlapping UTxO sets" 75 | 76 | renderEraMismatch :: EraMismatch -> Text 77 | renderEraMismatch EraMismatch{ledgerEraName, otherEraName} = 78 | "The era of the node and the tx do not match. " <> 79 | "The node is running in the " <> ledgerEraName <> 80 | " era, but the transaction is for the " <> otherEraName <> " era." 81 | 82 | textShow :: Show a => a -> Text 83 | textShow = Text.pack . show 84 | -------------------------------------------------------------------------------- /submit-api/src/Cardano/TxSubmit/JsonOrphans.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DerivingStrategies #-} 2 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 3 | {-# LANGUAGE StandaloneDeriving #-} 4 | 5 | {-# OPTIONS_GHC -Wno-orphans #-} 6 | 7 | module Cardano.TxSubmit.JsonOrphans () where 8 | 9 | import Cardano.Api.Typed 10 | ( TxId (..) ) 11 | import Cardano.Crypto.Hash.Class as Cryptos 12 | import Data.Aeson 13 | 14 | import qualified Shelley.Spec.Ledger.TxBody as Shelley 15 | 16 | deriving newtype instance ToJSON TxId 17 | deriving newtype instance ToJSON (Shelley.TxId c) 18 | -------------------------------------------------------------------------------- /submit-api/src/Cardano/TxSubmit/Metrics.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE NoImplicitPrelude #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | module Cardano.TxSubmit.Metrics 5 | ( TxSubmitMetrics (..) 6 | , makeMetrics 7 | , registerMetricsServer 8 | ) where 9 | 10 | import Cardano.Prelude 11 | 12 | import System.Metrics.Prometheus.Concurrent.RegistryT (RegistryT (..), registerGauge, 13 | runRegistryT, unRegistryT) 14 | import System.Metrics.Prometheus.Metric.Gauge (Gauge) 15 | import System.Metrics.Prometheus.Http.Scrape (serveHttpTextMetricsT) 16 | 17 | 18 | data TxSubmitMetrics = TxSubmitMetrics 19 | { tsmCount :: !Gauge 20 | } 21 | 22 | registerMetricsServer :: IO (TxSubmitMetrics, Async ()) 23 | registerMetricsServer = 24 | runRegistryT $ do 25 | metrics <- makeMetrics 26 | registry <- RegistryT ask 27 | server <- liftIO . async $ runReaderT (unRegistryT $ serveHttpTextMetricsT 8081 []) registry 28 | pure (metrics, server) 29 | 30 | makeMetrics :: RegistryT IO TxSubmitMetrics 31 | makeMetrics = 32 | TxSubmitMetrics 33 | <$> registerGauge "tx_submit_count" mempty 34 | -------------------------------------------------------------------------------- /submit-api/src/Cardano/TxSubmit/Tracing/ToObjectOrphans.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleInstances #-} 2 | {-# LANGUAGE MultiParamTypeClasses #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-# LANGUAGE TupleSections #-} 5 | 6 | {-# OPTIONS_GHC -fno-warn-orphans #-} 7 | 8 | module Cardano.TxSubmit.Tracing.ToObjectOrphans () where 9 | 10 | import Cardano.BM.Data.Severity 11 | import Cardano.BM.Data.Tracer 12 | import Data.Aeson 13 | ( (.=) ) 14 | import Data.Text 15 | import Ouroboros.Network.NodeToClient 16 | ( ErrorPolicyTrace (..), WithAddr (..) ) 17 | 18 | import qualified Network.Socket as Socket 19 | 20 | instance HasPrivacyAnnotation (WithAddr Socket.SockAddr ErrorPolicyTrace) 21 | instance HasSeverityAnnotation (WithAddr Socket.SockAddr ErrorPolicyTrace) where 22 | getSeverityAnnotation (WithAddr _ ev) = case ev of 23 | ErrorPolicySuspendPeer {} -> Warning -- peer misbehaved 24 | ErrorPolicySuspendConsumer {} -> Notice -- peer temporarily not useful 25 | ErrorPolicyLocalNodeError {} -> Error 26 | ErrorPolicyResumePeer {} -> Debug 27 | ErrorPolicyKeepSuspended {} -> Debug 28 | ErrorPolicyResumeConsumer {} -> Debug 29 | ErrorPolicyResumeProducer {} -> Debug 30 | ErrorPolicyUnhandledApplicationException {} -> Error 31 | ErrorPolicyUnhandledConnectionException {} -> Error 32 | ErrorPolicyAcceptException {} -> Error 33 | 34 | 35 | instance HasTextFormatter (WithAddr Socket.SockAddr ErrorPolicyTrace) where 36 | 37 | -- transform @ErrorPolicyTrace@ 38 | instance Transformable Text IO (WithAddr Socket.SockAddr ErrorPolicyTrace) where 39 | trTransformer verb tr = trStructured verb tr 40 | 41 | instance ToObject (WithAddr Socket.SockAddr ErrorPolicyTrace) where 42 | toObject _verb (WithAddr addr ev) = 43 | mkObject [ "kind" .= ("ErrorPolicyTrace" :: String) 44 | , "address" .= show addr 45 | , "event" .= show ev ] 46 | -------------------------------------------------------------------------------- /submit-api/src/Cardano/TxSubmit/Types.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DataKinds #-} 2 | {-# LANGUAGE DeriveGeneric #-} 3 | {-# LANGUAGE MultiParamTypeClasses #-} 4 | {-# LANGUAGE OverloadedStrings #-} 5 | {-# LANGUAGE TypeOperators #-} 6 | module Cardano.TxSubmit.Types 7 | ( TxSubmitApi 8 | , TxSubmitApiRecord (..) 9 | , TxSubmitWebApiError (..) 10 | , TxSubmitPort (..) 11 | , renderTxSubmitWebApiError 12 | ) where 13 | 14 | import Cardano.Api.Typed 15 | ( TxId ) 16 | import Cardano.Binary 17 | ( DecoderError ) 18 | import Cardano.TxSubmit.Tx 19 | ( TxSubmitError, renderTxSubmitError ) 20 | import Data.Aeson 21 | ( ToJSON (..), Value (..) ) 22 | import Data.ByteString.Char8 23 | ( ByteString ) 24 | import Data.Text 25 | ( Text ) 26 | import Formatting 27 | ( build, sformat ) 28 | import GHC.Generics 29 | ( Generic ) 30 | import Network.HTTP.Media 31 | ( (//) ) 32 | import Servant 33 | ( (:>) 34 | , Accept (..) 35 | , JSON 36 | , MimeRender (..) 37 | , MimeUnrender (..) 38 | , PostAccepted 39 | , ReqBody 40 | ) 41 | import Servant.API.Generic 42 | ( (:-), ToServantApi ) 43 | 44 | import qualified Data.ByteString.Lazy.Char8 as LBS 45 | 46 | newtype TxSubmitPort 47 | = TxSubmitPort Int 48 | 49 | -- | An error that can occur in the transaction submission web API. 50 | data TxSubmitWebApiError 51 | = TxSubmitDecodeHex 52 | | TxSubmitEmpty 53 | | TxSubmitDecodeFail !DecoderError 54 | | TxSubmitBadTx !Text 55 | | TxSubmitFail !TxSubmitError 56 | deriving Eq 57 | 58 | instance ToJSON TxSubmitWebApiError where 59 | toJSON = convertJson 60 | 61 | convertJson :: TxSubmitWebApiError -> Value 62 | convertJson = String . renderTxSubmitWebApiError 63 | 64 | renderTxSubmitWebApiError :: TxSubmitWebApiError -> Text 65 | renderTxSubmitWebApiError st = 66 | case st of 67 | TxSubmitDecodeHex -> "Provided data was hex encoded and this webapi expects raw binary" 68 | TxSubmitEmpty -> "Provided transaction has zero length" 69 | TxSubmitDecodeFail err -> sformat build err 70 | TxSubmitBadTx tt -> mconcat ["Transactions of type '", tt, "' not supported"] 71 | TxSubmitFail err -> renderTxSubmitError err 72 | 73 | -- | Servant API which provides access to tx submission webapi 74 | type TxSubmitApi 75 | = "api" :> ToServantApi TxSubmitApiRecord 76 | 77 | -- | A servant-generic record with all the methods of the API 78 | data TxSubmitApiRecord route = TxSubmitApiRecord 79 | { _txSubmitPost :: route 80 | :- "submit" 81 | :> "tx" 82 | :> ReqBody '[CBORStream] ByteString 83 | :> PostAccepted '[JSON] TxId 84 | } deriving (Generic) 85 | 86 | data CBORStream 87 | 88 | instance Accept CBORStream where 89 | contentType _ = "application" // "cbor" 90 | 91 | instance MimeRender CBORStream ByteString where 92 | mimeRender _ = LBS.fromStrict 93 | 94 | instance MimeRender CBORStream LBS.ByteString where 95 | mimeRender _ = id 96 | 97 | instance MimeUnrender CBORStream ByteString where 98 | mimeUnrender _ = Right . LBS.toStrict 99 | 100 | instance MimeUnrender CBORStream LBS.ByteString where 101 | mimeUnrender _ = Right . id 102 | -------------------------------------------------------------------------------- /submit-api/src/Cardano/TxSubmit/Util.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE NoImplicitPrelude #-} 2 | 3 | module Cardano.TxSubmit.Util 4 | ( logException 5 | , textShow 6 | ) where 7 | 8 | import Cardano.BM.Trace (Trace, logError) 9 | 10 | import Cardano.Prelude 11 | 12 | import Data.Text (Text) 13 | import qualified Data.Text as Text 14 | 15 | 16 | -- | ouroboros-network catches 'SomeException' and if a 'nullTracer' is passed into that 17 | -- code, the caught exception will not be logged. Therefore wrap all tx submission code that 18 | -- is called from network with an exception logger so at least the exception will be 19 | -- logged (instead of silently swallowed) and then rethrown. 20 | logException :: Trace IO Text -> Text -> IO a -> IO a 21 | logException tracer txt action = 22 | action `catch` logger 23 | where 24 | logger :: SomeException -> IO a 25 | logger e = do 26 | logError tracer $ txt <> textShow e 27 | throwIO e 28 | 29 | textShow :: Show a => a -> Text 30 | textShow = Text.pack . show 31 | -------------------------------------------------------------------------------- /submit-api/swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: '2.0' 2 | schemes: ["http"] 3 | host: localhost 4 | basePath: / 5 | info: 6 | title: cardano-submit-api 7 | version: 3.1.0 8 | license: 9 | name: Apache-2.0 10 | url: https://github.com/input-output-hk/cardano-rest/blob/master/submit-api/LICENSE 11 | description: | 12 |

13 | 14 | x-tagGroups: [] 15 | 16 | definitions: {} 17 | 18 | paths: 19 | /api/submit/tx: 20 | post: 21 | operationId: postTransaction 22 | summary: Submit Tx 23 | description: Submit an already serialized transaction to the network. 24 | parameters: 25 | - in: header 26 | name: Content-Type 27 | required: true 28 | type: string 29 | enum: ["application/cbor"] 30 | 31 | x-code-samples: 32 | - lang: "Shell" 33 | label: "cURL" 34 | source: | 35 | # Assuming `data` is a serialized transaction on the file-system. 36 | curl -X POST \ 37 | --header "Content-Type: application/cbor" \ 38 | --data-binary @data http://localhost:8101/api/submit/tx 39 | produces: 40 | - "application/json" 41 | responses: 42 | 202: 43 | description: Ok 44 | schema: 45 | description: The transaction id. 46 | type: string 47 | format: hex 48 | minLength: 64 49 | maxLength: 64 50 | example: 92bcd06b25dfbd89b578d536b4d3b7dd269b7c2aa206ed518012cffe0444d67f 51 | 52 | 400: 53 | description: Bad Request 54 | schema: 55 | type: string 56 | description: An error message. 57 | -------------------------------------------------------------------------------- /submit-api/test/test.hs: -------------------------------------------------------------------------------- 1 | 2 | main :: IO () 3 | main = putStrLn "cardano-tx-submit test" 4 | -------------------------------------------------------------------------------- /tests/Dockerfile: -------------------------------------------------------------------------------- 1 | ##################### 2 | # INITIAL CONTAINER # 3 | # Run tests # 4 | ##################### 5 | FROM maven:3.6.3-jdk-14 AS builder 6 | 7 | # Set-up working directory 8 | ENV HOME=/home/usr/app 9 | RUN mkdir -p $HOME 10 | WORKDIR $HOME 11 | 12 | # Lets Docker use a cache so it doesn't install the internet 13 | ADD pom.xml $HOME 14 | RUN ["/usr/local/bin/mvn-entrypoint.sh", "mvn", "verify", "clean", "--fail-never"] 15 | 16 | # Copy all files from host computer to container 17 | ADD . $HOME 18 | 19 | # Run specified test suites 20 | RUN mvn test -Dsurefire.suiteXmlFiles=tests_functional-smoke.xml 21 | RUN mvn test -Dsurefile.suiteXmlFiles=tests_oracle.xml 22 | RUN mvn test -Dsurefire.suiteXmlFiles=tests_functional-data-validation.xml 23 | RUN mvn test -Dsurefire.suiteXmlFiles=tests_functional-data-intensive.xml 24 | 25 | # Run performance tests and edit the folder names 26 | RUN ./tests_performance-runner.sh # CHANGE ME TO INVALIDATE CACHE: 0 27 | 28 | 29 | ############################ 30 | # INTERMEDIATE CONTAINER # 31 | # Generate Allure reports # 32 | ############################ 33 | FROM ubuntu:latest AS report-generator 34 | 35 | # Set-up working directory 36 | ENV HOME=/home/usr/app 37 | RUN mkdir -p $HOME 38 | WORKDIR $HOME 39 | 40 | # Copy report logs over from the previous container 41 | COPY --from=builder /home/usr/app/target/allure-results/ /allure-results/ 42 | 43 | # Needed for Allure 44 | RUN apt-get update && apt-get install -y curl tar 45 | 46 | # Needed for Java 47 | RUN apt-get update && \ 48 | apt-get install -y openjdk-8-jdk && \ 49 | apt-get install -y ant && \ 50 | apt-get clean; 51 | RUN apt-get update && \ 52 | apt-get install ca-certificates-java && \ 53 | apt-get clean && \ 54 | update-ca-certificates -f; 55 | ENV JAVA_HOME /usr/lib/jvm/java-8-openjdk-amd64/ 56 | RUN export JAVA_HOME 57 | 58 | # Download and install Allure 59 | RUN curl -o allure-2.6.0.tgz -Ls https://dl.bintray.com/qameta/generic/io/qameta/allure/allure/2.6.0/allure-2.6.0.tgz && \ 60 | tar -zxvf allure-2.6.0.tgz -C /opt/ && \ 61 | ln -s /opt/allure-2.6.0/bin/allure /usr/bin/allure && \ 62 | allure --version 63 | 64 | # Generate reports from the logs 65 | RUN allure generate /allure-results/ --clean -o allure/ 66 | 67 | 68 | ################### 69 | # FINAL CONTAINER # 70 | # Serve reports # 71 | ################### 72 | FROM nginx 73 | 74 | # Get the files we want to serve including the nginx config file 75 | COPY --from=report-generator /home/usr/app/allure/ /usr/share/nginx/html/ 76 | COPY --from=builder /home/usr/app/target/gatling/ /usr/share/nginx/html/gatling 77 | COPY --from=builder /home/usr/app/conf.d /etc/nginx/conf.d 78 | 79 | # Expose http and https of nginx web server 80 | EXPOSE 80 443 81 | CMD ["nginx", "-g", "daemon off;"] 82 | -------------------------------------------------------------------------------- /tests/cardano-rest-tests.iml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/conf.d: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80 default_server; 3 | listen [::]:80 default_server; 4 | server_name _; 5 | root /var/share/nginx/html; 6 | index index.html 7 | } -------------------------------------------------------------------------------- /tests/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | services: 3 | rest-tests: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | network: host 8 | ports: 9 | - "80:80" 10 | -------------------------------------------------------------------------------- /tests/src/test/README.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | ## Functional 4 | 5 | This package contains functional tests against `cardano-rest`. 6 | 7 | ### Smoke Tests 8 | 9 | Tests to run basic response and schema validation against all endpoints. Estimated time to run ~20s. 10 | 11 | ### Data Validation 12 | 13 | Tests to check expected values returned against hardcoded data. The values set in `tests_functional-data-validation.xml` 14 | are used as parameters. 15 | 16 | ### Data Intensive 17 | 18 | Tests to run schema validation against all endpoints but with `n` values. This takes some time to complete because of 19 | a 1s delay between each call in order to prevent accidental load testing! 20 | 21 | ### Oracle 22 | 23 | Oracle tests compare two versions of the RestAPI to ensure Data Parity. Note, this won't identify legacy errors but will identify regressions. 24 | Known breaking changes will cause these tests to fail, and they should be ignored or updated accordingly. You can set the 25 | Oracle in `config.properties`. 26 | 27 | ## Simulations 28 | 29 | This package contains non-functional performance tests against `cardano-rest`. 30 | 31 | ### Performance 32 | 33 | Gatling performance tests are ran against each endpoint. The configuration for these tests can be found in `config.properties`. 34 | 35 | ## Data Used 36 | 37 | Data is available for both testnet and mainnet networks. DBSync was queried for a maximum of 2500 random values which 38 | are stored in the `resources` folder. You can specify the network in the `config.properties` file or get more test data using the 39 | example SQL queries (`resources/example_queries_to_get_data.sql`). 40 | 41 | ## Default Configuration 42 | 43 | - host=https://explorer.cardano-testnet.iohkdev.io/api/ 44 | - oracle=https://explorer.cardano-testnet.iohkdev.io/api/ 45 | - network=testnet 46 | - pauseBetweenTests=5 47 | - pauseBetweenRequests=5 48 | - startingUsers=1 49 | - maximumUsers=10 50 | - timeFrameToIncreaseUsers=15 51 | - maxTestDuration=30 52 | -------------------------------------------------------------------------------- /tests/src/test/resources/config.properties: -------------------------------------------------------------------------------- 1 | host=http://localhost:8100/api/ 2 | oracle=https://explorer.cardano-testnet.iohkdev.io/api/ 3 | network=testnet 4 | pauseBetweenTests=5 5 | pauseBetweenRequests=5 6 | startingUsers=1 7 | maximumUsers=10 8 | timeFrameToIncreaseUsers=15 9 | maxTestDuration=30 10 | -------------------------------------------------------------------------------- /tests/src/test/resources/example_queries_to_get_data.sql: -------------------------------------------------------------------------------- 1 | -- Remember to copy the values out into a text file! 2 | -- Also, you can change the limit to get more/less data. 3 | 4 | -- It'd be a good idea to automate refreshing data over time so we don't get stale :) 5 | \copy (select address from tx_out where random() < 0.01 limit 2500) to 'addresses.csv' csv; 6 | \copy (select hash from block where random() < 0.01 limit 2500) to 'blocks.csv' csv; 7 | \copy (select hash from tx where random() < 0.01 limit 2500) to 'transactions.csv' csv; 8 | \copy (select block.hash, tx_out.address from tx_out left join tx on tx.id = tx_out.tx_id left join block on block.id = tx.block where random() < 0.01 limit 2500) to 'blocks_and_addresses.csv' csv; 9 | -------------------------------------------------------------------------------- /tests/src/test/resources/schemas/valid-addresses-summary-address-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "Right": { 3 | "caAddress": "Ae2tdPwUPEZK72eZZqulakkhaUfTCcoaGepvQP718aYBczw5uZmp47h1k14", 4 | "caType": "CPubKeyAddress", 5 | "caChainTip": { 6 | "ctBlockNo": 0, 7 | "ctSlotNo": 0, 8 | "ctBlockHash": "3c89f7d9ff6c06468e32fd916d153b033264f780e11fca7750cb85f56d4f31d0" 9 | }, 10 | "caTxNum": 0, 11 | "caBalance": { 12 | "getCoin": 0 13 | }, 14 | "caTotalInput": { 15 | "getCoin": 0 16 | }, 17 | "caTotalOutput": { 18 | "getCoin": 0 19 | }, 20 | "caTotalFee": { 21 | "getCoin": 0 22 | }, 23 | "caTxList": [ 24 | { 25 | "ctbId": "3c89f7d9ff6c06468e32fd916d153b033264f780e11fca7750cb85f56d4f31d0", 26 | "ctbTimeIssued": 0, 27 | "ctbInputs": [ 28 | { 29 | "ctaAddress": "Ae2tdPwUPEZK72eZZqulakkhaUfTCcoaGepvQP718aYBczw5uZmp47h1k14", 30 | "ctaAmount": { 31 | "getCoin": 0 32 | }, 33 | "ctaTxHash": "3c89f7d9ff6c06468e32fd916d153b033264f780e11fca7750cb85f56d4f31d0", 34 | "ctaTxIndex": 0 35 | } 36 | ], 37 | "ctbOutputs": [ 38 | { 39 | "ctaAddress": "Ae2tdPwUPEZK72eZZqulakkhaUfTCcoaGepvQP718aYBczw5uZmp47h1k14", 40 | "ctaAmount": { 41 | "getCoin": 0 42 | }, 43 | "ctaTxHash": "3c89f7d9ff6c06468e32fd916d153b033264f780e11fca7750cb85f56d4f31d0", 44 | "ctaTxIndex": 0 45 | } 46 | ], 47 | "ctbInputSum": { 48 | "getCoin": 0 49 | }, 50 | "ctbOutputSum": { 51 | "getCoin": 0 52 | }, 53 | "ctbFees": { 54 | "getCoin": 0 55 | } 56 | } 57 | ] 58 | } 59 | } -------------------------------------------------------------------------------- /tests/src/test/resources/schemas/valid-blocks-pages-total-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "Right": { 6 | "type": "integer" 7 | } 8 | }, 9 | "required": [ 10 | "Right" 11 | ] 12 | } -------------------------------------------------------------------------------- /tests/src/test/resources/schemas/valid-blocks-summary-blockhash-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "Right": { 6 | "type": "object", 7 | "properties": { 8 | "cbsEntry": { 9 | "type": "object", 10 | "properties": { 11 | "cbeEpoch": { 12 | "type": "integer" 13 | }, 14 | "cbeSlot": { 15 | "type": "integer" 16 | }, 17 | "cbeBlkHeight": { 18 | "type": "integer" 19 | }, 20 | "cbeBlkHash": { 21 | "type": "string" 22 | }, 23 | "cbeTimeIssued": { 24 | "type": "integer" 25 | }, 26 | "cbeTxNum": { 27 | "type": "integer" 28 | }, 29 | "cbeTotalSent": { 30 | "type": "object", 31 | "properties": { 32 | "getCoin": { 33 | "type": "string" 34 | } 35 | }, 36 | "required": [ 37 | "getCoin" 38 | ] 39 | }, 40 | "cbeSize": { 41 | "type": "integer" 42 | }, 43 | "cbeBlockLead": { 44 | "type": "string" 45 | }, 46 | "cbeFees": { 47 | "type": "object", 48 | "properties": { 49 | "getCoin": { 50 | "type": "string" 51 | } 52 | }, 53 | "required": [ 54 | "getCoin" 55 | ] 56 | } 57 | }, 58 | "required": [ 59 | "cbeEpoch", 60 | "cbeSlot", 61 | "cbeBlkHeight", 62 | "cbeBlkHash", 63 | "cbeTimeIssued", 64 | "cbeTxNum", 65 | "cbeTotalSent", 66 | "cbeSize", 67 | "cbeBlockLead", 68 | "cbeFees" 69 | ] 70 | }, 71 | "cbsPrevHash": { 72 | "type": "string" 73 | }, 74 | "cbsNextHash": { 75 | "type": "string" 76 | }, 77 | "cbsMerkleRoot": { 78 | "type": "string" 79 | } 80 | }, 81 | "required": [ 82 | "cbsEntry", 83 | "cbsPrevHash", 84 | "cbsNextHash", 85 | "cbsMerkleRoot" 86 | ] 87 | } 88 | }, 89 | "required": [ 90 | "Right" 91 | ] 92 | } 93 | -------------------------------------------------------------------------------- /tests/src/test/resources/schemas/valid-epochs-epoch-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "Right": { 6 | "type": "array", 7 | "items": [ 8 | { 9 | "type": "integer" 10 | }, 11 | { 12 | "type": "array", 13 | "items": [ 14 | { 15 | "type": "object", 16 | "properties": { 17 | "cbeEpoch": { 18 | "type": "integer" 19 | }, 20 | "cbeSlot": { 21 | "type": "integer" 22 | }, 23 | "cbeBlkHeight": { 24 | "type": "integer" 25 | }, 26 | "cbeBlkHash": { 27 | "type": "string" 28 | }, 29 | "cbeTimeIssued": { 30 | "type": "integer" 31 | }, 32 | "cbeTxNum": { 33 | "type": "integer" 34 | }, 35 | "cbeTotalSent": { 36 | "type": "object", 37 | "properties": { 38 | "getCoin": { 39 | "type": "string" 40 | } 41 | }, 42 | "required": [ 43 | "getCoin" 44 | ] 45 | }, 46 | "cbeSize": { 47 | "type": "integer" 48 | }, 49 | "cbeBlockLead": { 50 | "type": "string" 51 | }, 52 | "cbeFees": { 53 | "type": "object", 54 | "properties": { 55 | "getCoin": { 56 | "type": "string" 57 | } 58 | }, 59 | "required": [ 60 | "getCoin" 61 | ] 62 | } 63 | }, 64 | "required": [ 65 | "cbeEpoch", 66 | "cbeSlot", 67 | "cbeBlkHeight", 68 | "cbeBlkHash", 69 | "cbeTimeIssued", 70 | "cbeTxNum", 71 | "cbeTotalSent", 72 | "cbeSize", 73 | "cbeBlockLead", 74 | "cbeFees" 75 | ] 76 | } 77 | ] 78 | } 79 | ] 80 | } 81 | }, 82 | "required": [ 83 | "Right" 84 | ] 85 | } 86 | -------------------------------------------------------------------------------- /tests/src/test/resources/schemas/valid-epochs-epoch-slot-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "Right": { 6 | "type": "array", 7 | "items": [ 8 | { 9 | "type": "object", 10 | "properties": { 11 | "cbeEpoch": { 12 | "type": "integer" 13 | }, 14 | "cbeSlot": { 15 | "type": "integer" 16 | }, 17 | "cbeBlkHeight": { 18 | "type": "integer" 19 | }, 20 | "cbeBlkHash": { 21 | "type": "string" 22 | }, 23 | "cbeTimeIssued": { 24 | "type": "integer" 25 | }, 26 | "cbeTxNum": { 27 | "type": "integer" 28 | }, 29 | "cbeTotalSent": { 30 | "type": "object", 31 | "properties": { 32 | "getCoin": { 33 | "type": "string" 34 | } 35 | }, 36 | "required": [ 37 | "getCoin" 38 | ] 39 | }, 40 | "cbeSize": { 41 | "type": "integer" 42 | }, 43 | "cbeBlockLead": { 44 | "type": "string" 45 | }, 46 | "cbeFees": { 47 | "type": "object", 48 | "properties": { 49 | "getCoin": { 50 | "type": "string" 51 | } 52 | }, 53 | "required": [ 54 | "getCoin" 55 | ] 56 | } 57 | }, 58 | "required": [ 59 | "cbeEpoch", 60 | "cbeSlot", 61 | "cbeBlkHeight", 62 | "cbeBlkHash", 63 | "cbeTimeIssued", 64 | "cbeTxNum", 65 | "cbeTotalSent", 66 | "cbeSize", 67 | "cbeBlockLead", 68 | "cbeFees" 69 | ] 70 | } 71 | ] 72 | } 73 | }, 74 | "required": [ 75 | "Right" 76 | ] 77 | } 78 | -------------------------------------------------------------------------------- /tests/src/test/resources/schemas/valid-genesis-address-pages-total-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "Right": { 6 | "type": "integer" 7 | } 8 | }, 9 | "required": [ 10 | "Right" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /tests/src/test/resources/schemas/valid-genesis-address-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "Right": { 6 | "type": "array", 7 | "properties": { 8 | "cgaiCardanoAddress": { 9 | "type": "string" 10 | }, 11 | "cgaiGenesisAmount": { 12 | "type": "object", 13 | "properties": { 14 | "getCoin": { 15 | "type": "string" 16 | } 17 | }, 18 | "required": [ 19 | "getCoin" 20 | ] 21 | }, 22 | "cgaiIsRedeemed": { 23 | "type": "boolean" 24 | } 25 | }, 26 | "required": [ 27 | "cgaiCardanoAddress", 28 | "cgaiGenesisAmount", 29 | "cgaiIsRedeemed" 30 | ] 31 | } 32 | }, 33 | "required": [ 34 | "Right" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /tests/src/test/resources/schemas/valid-genesis-summary-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "Right": { 6 | "type": "object", 7 | "properties": { 8 | "cgsNumTotal": { 9 | "type": "integer" 10 | }, 11 | "cgsNumRedeemed": { 12 | "type": "integer" 13 | }, 14 | "cgsNumNotRedeemed": { 15 | "type": "integer" 16 | }, 17 | "cgsRedeemedAmountTotal": { 18 | "type": "object", 19 | "properties": { 20 | "getCoin": { 21 | "type": "string" 22 | } 23 | }, 24 | "required": [ 25 | "getCoin" 26 | ] 27 | }, 28 | "cgsNonRedeemedAmountTotal": { 29 | "type": "object", 30 | "properties": { 31 | "getCoin": { 32 | "type": "string" 33 | } 34 | }, 35 | "required": [ 36 | "getCoin" 37 | ] 38 | } 39 | }, 40 | "required": [ 41 | "cgsNumTotal", 42 | "cgsNumRedeemed", 43 | "cgsNumNotRedeemed", 44 | "cgsRedeemedAmountTotal", 45 | "cgsNonRedeemedAmountTotal" 46 | ] 47 | } 48 | }, 49 | "required": [ 50 | "Right" 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /tests/src/test/resources/schemas/valid-stats-txs-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "Right": { 6 | "type": "array", 7 | "items": [ 8 | { 9 | "type": "integer" 10 | }, 11 | { 12 | "type": "array", 13 | "items": [ 14 | { 15 | "type": "array", 16 | "items": [ 17 | { 18 | "type": "string" 19 | }, 20 | { 21 | "type": "integer" 22 | } 23 | ] 24 | }, 25 | { 26 | "type": "array", 27 | "items": [ 28 | { 29 | "type": "string" 30 | }, 31 | { 32 | "type": "integer" 33 | } 34 | ] 35 | } 36 | ] 37 | } 38 | ] 39 | } 40 | }, 41 | "required": [ 42 | "Right" 43 | ] 44 | } -------------------------------------------------------------------------------- /tests/src/test/resources/schemas/valid-supply-ada-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "Right": { 6 | "type": "number" 7 | } 8 | }, 9 | "required": [ 10 | "Right" 11 | ] 12 | } -------------------------------------------------------------------------------- /tests/src/test/resources/schemas/valid-txs-last-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "Right": { 6 | "type": "array", 7 | "properties": { 8 | "cteId": { 9 | "type": "string" 10 | }, 11 | "cteTimeIssued": { 12 | "type": "integer" 13 | }, 14 | "cteAmount": { 15 | "type": "object", 16 | "properties": { 17 | "getCoin": { 18 | "type": "string" 19 | } 20 | }, 21 | "required": [ 22 | "getCoin" 23 | ] 24 | } 25 | }, 26 | "required": [ 27 | "cteId", 28 | "cteTimeIssued", 29 | "cteAmount" 30 | ] 31 | } 32 | }, 33 | "required": [ 34 | "Right" 35 | ] 36 | } -------------------------------------------------------------------------------- /tests/src/test/scala/com/cardano/rest/tests/functional/ApiResponseComparison.java: -------------------------------------------------------------------------------- 1 | package com.cardano.rest.tests.functional; 2 | 3 | 4 | public class ApiResponseComparison { 5 | public String Response; 6 | public String OracleResponse; 7 | 8 | public ApiResponseComparison(String response, String oracleResponse) { 9 | this.Response = response; 10 | this.OracleResponse = oracleResponse; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/src/test/scala/com/cardano/rest/tests/functional/BaseTest.java: -------------------------------------------------------------------------------- 1 | package com.cardano.rest.tests.functional; 2 | 3 | import io.restassured.RestAssured; 4 | import io.restassured.response.Response; 5 | import io.restassured.specification.RequestSpecification; 6 | import org.testng.annotations.BeforeSuite; 7 | import org.testng.annotations.BeforeTest; 8 | 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.util.Properties; 12 | 13 | import com.cardano.rest.tests.DataStore; 14 | 15 | import static org.testng.AssertJUnit.assertEquals; 16 | 17 | public class BaseTest { 18 | 19 | protected String host; 20 | protected String oracle; 21 | protected DataStore dataStore; 22 | 23 | @BeforeSuite 24 | public void setupProperties() { 25 | } 26 | 27 | @BeforeTest 28 | public void setupTests() { 29 | this.dataStore = new DataStore(); 30 | 31 | String resourceName = "config.properties"; // could also be a constant 32 | ClassLoader loader = Thread.currentThread().getContextClassLoader(); 33 | Properties props = new Properties(); 34 | try(InputStream resourceStream = loader.getResourceAsStream(resourceName)) { 35 | props.load(resourceStream); 36 | } catch (IOException e) { 37 | e.printStackTrace(); 38 | } 39 | 40 | this.host = props.getProperty("host"); 41 | this.oracle = props.getProperty("oracle"); 42 | } 43 | 44 | public ApiResponseComparison compareTwoResponses(String endpoint) { 45 | String host_url = this.host + endpoint; 46 | String oracle_url = this.oracle + endpoint; 47 | 48 | RequestSpecification httpRequest = RestAssured.given(); 49 | Response response = httpRequest.get(host_url); 50 | Response oracleResponse = httpRequest.get(oracle_url); 51 | 52 | String responseJson = response.getBody().asString(); 53 | String oracleResponseJson = oracleResponse.getBody().asString(); 54 | 55 | return new ApiResponseComparison(responseJson, oracleResponseJson); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/src/test/scala/com/cardano/rest/tests/functional/data_intensive/AddressesTest.java: -------------------------------------------------------------------------------- 1 | package com.cardano.rest.tests.functional.data_intensive; 2 | 3 | import com.cardano.rest.tests.functional.BaseTest; 4 | import io.qameta.allure.Description; 5 | import io.restassured.http.ContentType; 6 | import org.testng.annotations.Test; 7 | 8 | import java.util.concurrent.TimeUnit; 9 | 10 | import static io.restassured.RestAssured.given; 11 | import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath; 12 | 13 | public class AddressesTest extends BaseTest { 14 | 15 | @Test(invocationCount = 100, threadPoolSize = 2) 16 | @Description("addresses/summary/{address} matches expected JSON schema") 17 | public void addressesSummaryAddress_validSchema_test() throws InterruptedException { 18 | String endpoint = String.format("addresses/summary/%s", this.dataStore.getAddressHash()); 19 | String url = this.host + endpoint; 20 | 21 | given(). 22 | get(url). 23 | then(). 24 | assertThat(). 25 | body(matchesJsonSchemaInClasspath("schemas/valid-addresses-summary-address-schema.json")); 26 | 27 | TimeUnit.SECONDS.sleep(1); 28 | } 29 | 30 | @Test(invocationCount = 100, threadPoolSize = 2) 31 | @Description("block/{blockHash}/address/{address} matches expected JSON schema") 32 | public void blockBlockhashAddressAddress_validSchema_test() throws InterruptedException { 33 | String[] testData = this.dataStore.getBlockHashAndAddress().split(" "); 34 | String endpoint = String.format("block/%s/address/%s", testData[0], testData[1]); 35 | String url = this.host + endpoint; 36 | 37 | given(). 38 | get(url). 39 | then(). 40 | assertThat(). 41 | body(matchesJsonSchemaInClasspath("schemas/valid-block-blockhash-address-address-schema.json")); 42 | 43 | TimeUnit.SECONDS.sleep(1); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/src/test/scala/com/cardano/rest/tests/functional/data_intensive/BlocksTest.java: -------------------------------------------------------------------------------- 1 | package com.cardano.rest.tests.functional.data_intensive; 2 | 3 | import com.cardano.rest.tests.functional.BaseTest; 4 | import io.qameta.allure.Description; 5 | import io.restassured.http.ContentType; 6 | import org.testng.annotations.Test; 7 | 8 | import java.util.concurrent.TimeUnit; 9 | 10 | import static io.restassured.RestAssured.given; 11 | import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath; 12 | 13 | public class BlocksTest extends BaseTest { 14 | 15 | @Test(invocationCount = 100, threadPoolSize = 2) 16 | @Description("blocks/summary/{blockHash} matches expected JSON schema") 17 | public void blocksSummaryBlockhash_validSchema_test() throws InterruptedException { 18 | String endpoint = String.format("blocks/summary/%s", this.dataStore.getBlockHash()); 19 | String url = this.host + endpoint; 20 | 21 | given(). 22 | get(url). 23 | then(). 24 | assertThat(). 25 | body(matchesJsonSchemaInClasspath("schemas/valid-blocks-summary-blockhash-schema.json")); 26 | 27 | TimeUnit.SECONDS.sleep(1); 28 | } 29 | 30 | @Test(invocationCount = 100, threadPoolSize = 2) 31 | @Description("blocks/txs/{blockHash} matches expected JSON schema") 32 | public void blocksTxsBlockhash_validSchema_test() throws InterruptedException { 33 | String endpoint = String.format("blocks/summary/%s", this.dataStore.getBlockHash()); 34 | String url = this.host + endpoint; 35 | 36 | given(). 37 | get(url). 38 | then(). 39 | assertThat(). 40 | body(matchesJsonSchemaInClasspath("schemas/valid-blocks-txs-blockhash-schema.json")); 41 | 42 | TimeUnit.SECONDS.sleep(1); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/src/test/scala/com/cardano/rest/tests/functional/data_intensive/EpochsTest.java: -------------------------------------------------------------------------------- 1 | package com.cardano.rest.tests.functional.data_intensive; 2 | 3 | import com.cardano.rest.tests.functional.BaseTest; 4 | import io.qameta.allure.Description; 5 | import io.restassured.http.ContentType; 6 | import org.testng.annotations.Test; 7 | 8 | import java.util.concurrent.TimeUnit; 9 | 10 | import static io.restassured.RestAssured.given; 11 | import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath; 12 | 13 | public class EpochsTest extends BaseTest { 14 | 15 | @Test(invocationCount = 100, threadPoolSize = 2) 16 | @Description("epochs/{epoch} matches expected JSON schema") 17 | public void epochsEpoch_validSchema_test() throws InterruptedException { 18 | String endpoint = String.format("epochs/%s", this.dataStore.getEpoch()); 19 | String url = this.host + endpoint; 20 | 21 | given(). 22 | get(url). 23 | then(). 24 | assertThat(). 25 | body(matchesJsonSchemaInClasspath("schemas/valid-epochs-epoch-schema.json")); 26 | 27 | TimeUnit.SECONDS.sleep(1); 28 | } 29 | 30 | @Test(invocationCount = 100, threadPoolSize = 2) 31 | @Description("epochs/{epoch}/{slot} matches expected JSON schema") 32 | public void epochsEpochSlot_validSchema_test() throws InterruptedException { 33 | String endpoint = String.format("epochs/%s/%s", this.dataStore.getEpoch(), this.dataStore.getSlot()); 34 | String url = this.host + endpoint; 35 | 36 | given(). 37 | get(url) 38 | .then() 39 | .assertThat() 40 | .body(matchesJsonSchemaInClasspath("schemas/valid-epochs-epoch-slot-schema.json")); 41 | 42 | TimeUnit.SECONDS.sleep(1); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/src/test/scala/com/cardano/rest/tests/functional/data_intensive/TransactionsTest.java: -------------------------------------------------------------------------------- 1 | package com.cardano.rest.tests.functional.data_intensive; 2 | 3 | import com.cardano.rest.tests.functional.BaseTest; 4 | import io.qameta.allure.Description; 5 | import io.restassured.http.ContentType; 6 | import org.testng.annotations.Test; 7 | 8 | import java.util.concurrent.TimeUnit; 9 | 10 | import static io.restassured.RestAssured.given; 11 | import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath; 12 | 13 | public class TransactionsTest extends BaseTest { 14 | 15 | @Test(invocationCount = 100, threadPoolSize = 2) 16 | @Description("txs/summary/{tx} matches expected JSON schema") 17 | public void txsSummaryTxid_validSchema_test() throws InterruptedException { 18 | String endpoint = String.format("txs/summary/%s", this.dataStore.getTransactionHash()); 19 | String url = this.host + endpoint; 20 | 21 | given(). 22 | get(url). 23 | then(). 24 | assertThat(). 25 | body(matchesJsonSchemaInClasspath("schemas/valid-txs-summary-txid-schema.json")); 26 | 27 | TimeUnit.SECONDS.sleep(1); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/src/test/scala/com/cardano/rest/tests/functional/data_validation/AddressesTest.java: -------------------------------------------------------------------------------- 1 | package com.cardano.rest.tests.functional.data_validation; 2 | 3 | import com.cardano.rest.tests.functional.BaseTest; 4 | import io.qameta.allure.Description; 5 | import org.testng.annotations.Parameters; 6 | import org.testng.annotations.Test; 7 | 8 | import static org.hamcrest.Matchers.equalTo; 9 | import static io.restassured.RestAssured.given; 10 | import static org.hamcrest.Matchers.is; 11 | 12 | public class AddressesTest extends BaseTest { 13 | 14 | @Test 15 | @Parameters({"address", "caAddress"}) 16 | @Description("addresses/summary/{address} returns the expected address") 17 | public void addressesSummaryAddress_returnsCorrectAddress_test(String address, String caAddress) { 18 | String endpoint = String.format("addresses/summary/%s", address); 19 | String url = this.host + endpoint; 20 | 21 | given(). 22 | get(url). 23 | then(). 24 | statusCode(200). 25 | body("Right.caAddress", equalTo(caAddress)); 26 | } 27 | 28 | @Test 29 | @Parameters({"address", "caType"}) 30 | @Description("addresses/summary/{address} returns the expected type") 31 | public void addressesSummaryAddress_returnsCorrectType_test(String address, String caType) { 32 | String endpoint = String.format("addresses/summary/%s", address); 33 | String url = this.host + endpoint; 34 | 35 | given(). 36 | get(url). 37 | then(). 38 | statusCode(200). 39 | body("Right.caType", equalTo(caType)); 40 | } 41 | 42 | @Test 43 | @Parameters({"address", "ctbId"}) 44 | @Description("addresses/summary/{address} returns the expected Id") 45 | public void addressesSummaryAddress_returnsExpectedIdInTxList_test(String address, String ctbId) { 46 | String endpoint = String.format("addresses/summary/%s", address); 47 | String url = this.host + endpoint; 48 | 49 | given(). 50 | get(url). 51 | then(). 52 | statusCode(200). 53 | body("Right.caTxList[0].ctbId", is(ctbId)); 54 | } 55 | 56 | @Test 57 | @Parameters({"address", "ctaAmount"}) 58 | @Description("addresses/summary/{address} returns the expected Amount") 59 | public void addressesSummaryAddress_returnsExpectedAmountInTxList_test(String address, String ctaAmount) { 60 | String endpoint = String.format("addresses/summary/%s", address); 61 | String url = this.host + endpoint; 62 | 63 | given(). 64 | get(url). 65 | then(). 66 | statusCode(200). 67 | body("Right.caTxList[0].ctbInputs.ctaAmount.getCoin[0]", equalTo(ctaAmount)); 68 | } 69 | 70 | @Test 71 | @Parameters({"address", "ctaTxHash"}) 72 | @Description("addresses/summary/{address} returns the expected Tx Hash") 73 | public void addressesSummaryAddress_returnsExpectedTxHashInTxList_test(String address, String ctaTxHash) { 74 | String endpoint = String.format("addresses/summary/%s", address); 75 | String url = this.host + endpoint; 76 | 77 | given(). 78 | get(url). 79 | then(). 80 | statusCode(200). 81 | body("Right.caTxList[0].ctbInputs.ctaTxHash[0]", equalTo(ctaTxHash)); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/src/test/scala/com/cardano/rest/tests/functional/oracle/AddressesTest.java: -------------------------------------------------------------------------------- 1 | package com.cardano.rest.tests.functional.oracle; 2 | 3 | import com.cardano.rest.tests.functional.ApiResponseComparison; 4 | import com.cardano.rest.tests.functional.BaseTest; 5 | import io.qameta.allure.Description; 6 | import org.testng.annotations.Test; 7 | 8 | import static org.testng.AssertJUnit.assertEquals; 9 | 10 | public class AddressesTest extends BaseTest { 11 | 12 | @Test 13 | @Description("addresses/summary/{address} has not changed against the oracle") 14 | public void addressesSummaryAddress_oracleTest() { 15 | String addressHash = this.dataStore.getAddressHash(); 16 | ApiResponseComparison responses = compareTwoResponses("addresses/summary/" + addressHash); 17 | 18 | assertEquals(responses.Response, responses.OracleResponse); 19 | } 20 | 21 | @Test 22 | @Description("block/{blockHash}/address/{address} has not changed against the oracle") 23 | public void blockBlockhashAddressAddress_oracleTest() { 24 | String[] testData = this.dataStore.getBlockHashAndAddress().split(" "); 25 | ApiResponseComparison responses = compareTwoResponses("block/" + testData[0] + "/address/" + testData[1]); 26 | 27 | assertEquals(responses.Response, responses.OracleResponse); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/src/test/scala/com/cardano/rest/tests/functional/oracle/BlocksTest.java: -------------------------------------------------------------------------------- 1 | package com.cardano.rest.tests.functional.oracle; 2 | 3 | import com.cardano.rest.tests.functional.ApiResponseComparison; 4 | import com.cardano.rest.tests.functional.BaseTest; 5 | import io.qameta.allure.Description; 6 | import io.restassured.http.ContentType; 7 | import org.testng.annotations.Test; 8 | 9 | import static io.restassured.RestAssured.given; 10 | import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath; 11 | import static org.testng.AssertJUnit.assertEquals; 12 | 13 | public class BlocksTest extends BaseTest { 14 | 15 | @Test 16 | @Description("blocks/pages has not changed against the oracle") 17 | public void blocksPages_basicResponse_test() { 18 | ApiResponseComparison responses = compareTwoResponses("blocks/pages"); 19 | 20 | assertEquals(responses.Response, responses.OracleResponse); 21 | } 22 | 23 | @Test 24 | @Description("blocks/pages/total has not changed against the oracle") 25 | public void blocksPagesTotal_basicResponse_test() { 26 | ApiResponseComparison responses = compareTwoResponses("blocks/pages/total"); 27 | 28 | assertEquals(responses.Response, responses.OracleResponse); 29 | } 30 | 31 | @Test 32 | @Description("blocks/summary/{blockHash} has not changed against the oracle") 33 | public void blocksSummaryBlockhash_basicResponse_test() { 34 | String blockHash = this.dataStore.getBlockHash(); 35 | ApiResponseComparison responses = compareTwoResponses("blocks/summary/" + blockHash); 36 | 37 | assertEquals(responses.Response, responses.OracleResponse); 38 | } 39 | 40 | @Test 41 | @Description("blocks/txs/{blockHash} has not changed against the oracle") 42 | public void blocksTxsBlockhash_basicResponse_test() { 43 | String blockHash = this.dataStore.getBlockHash(); 44 | ApiResponseComparison responses = compareTwoResponses("blocks/txs/" + blockHash); 45 | 46 | assertEquals(responses.Response, responses.OracleResponse); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/src/test/scala/com/cardano/rest/tests/functional/oracle/EpochsTest.java: -------------------------------------------------------------------------------- 1 | package com.cardano.rest.tests.functional.oracle; 2 | 3 | import com.cardano.rest.tests.functional.ApiResponseComparison; 4 | import com.cardano.rest.tests.functional.BaseTest; 5 | import io.qameta.allure.Description; 6 | import io.restassured.http.ContentType; 7 | import org.testng.annotations.Test; 8 | 9 | import static io.restassured.RestAssured.given; 10 | import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath; 11 | import static org.testng.AssertJUnit.assertEquals; 12 | 13 | public class EpochsTest extends BaseTest { 14 | 15 | @Test 16 | @Description("epochs/{epoch} has not changed against the oracle") 17 | public void epochsEpoch_basicResponse_test() { 18 | String epoch = this.dataStore.getEpoch(); 19 | ApiResponseComparison responses = compareTwoResponses("epochs/" + epoch); 20 | 21 | assertEquals(responses.Response, responses.OracleResponse); 22 | } 23 | 24 | @Test 25 | @Description("epochs/{epoch}/{slot} has not changed against the oracle") 26 | public void epochsEpochSlot_basicResponse_test() { 27 | String epoch = this.dataStore.getEpoch(); 28 | String slot = this.dataStore.getSlot(); 29 | ApiResponseComparison responses = compareTwoResponses("epochs/" + epoch + "/" + slot); 30 | 31 | assertEquals(responses.Response, responses.OracleResponse); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/src/test/scala/com/cardano/rest/tests/functional/oracle/GenesisTest.java: -------------------------------------------------------------------------------- 1 | package com.cardano.rest.tests.functional.oracle; 2 | 3 | import com.cardano.rest.tests.functional.ApiResponseComparison; 4 | import com.cardano.rest.tests.functional.BaseTest; 5 | import io.qameta.allure.Description; 6 | import io.restassured.http.ContentType; 7 | import org.testng.annotations.Test; 8 | 9 | import static io.restassured.RestAssured.given; 10 | import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath; 11 | import static org.testng.AssertJUnit.assertEquals; 12 | 13 | public class GenesisTest extends BaseTest { 14 | 15 | @Test 16 | @Description("genesis/summary has not changed against the oracle") 17 | public void genesisSummary_basicResponse_test() { 18 | ApiResponseComparison responses = compareTwoResponses("genesis/summary"); 19 | 20 | assertEquals(responses.Response, responses.OracleResponse); 21 | } 22 | 23 | @Test 24 | @Description("genesis/address/pages/total has not changed against the oracle") 25 | public void genesisAddressPagesTotal_basicResponse_test() { 26 | ApiResponseComparison responses = compareTwoResponses("genesis/address/pages/total"); 27 | 28 | assertEquals(responses.Response, responses.OracleResponse); 29 | } 30 | 31 | @Test 32 | @Description("genesis/address has not changed against the oracle") 33 | public void genesisAddress_basicResponse_test() { 34 | ApiResponseComparison responses = compareTwoResponses("genesis/address"); 35 | 36 | assertEquals(responses.Response, responses.OracleResponse); 37 | } 38 | 39 | @Test 40 | @Description("supply/ada has not changed against the oracle") 41 | public void supplyAda_basicResponse_test() { 42 | ApiResponseComparison responses = compareTwoResponses("supply/ada"); 43 | 44 | assertEquals(responses.Response, responses.OracleResponse); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/src/test/scala/com/cardano/rest/tests/functional/oracle/TransactionsTest.java: -------------------------------------------------------------------------------- 1 | package com.cardano.rest.tests.functional.oracle; 2 | 3 | import com.cardano.rest.tests.functional.ApiResponseComparison; 4 | import com.cardano.rest.tests.functional.BaseTest; 5 | import io.qameta.allure.Description; 6 | import io.restassured.http.ContentType; 7 | import org.testng.annotations.Test; 8 | 9 | import static io.restassured.RestAssured.given; 10 | import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath; 11 | import static org.testng.AssertJUnit.assertEquals; 12 | 13 | public class TransactionsTest extends BaseTest { 14 | 15 | @Test 16 | @Description("txs/last has not changed against the oracle") 17 | public void txsLast_basicResponse_test() { 18 | ApiResponseComparison responses = compareTwoResponses("txs/last"); 19 | 20 | assertEquals(responses.Response, responses.OracleResponse); 21 | } 22 | 23 | @Test 24 | @Description("txs/summary/{tx} has not changed against the oracle") 25 | public void txsSummaryTxid_basicResponse_test() { 26 | String transaction = this.dataStore.getTransactionHash(); 27 | ApiResponseComparison responses = compareTwoResponses("txs/summary/" + transaction); 28 | 29 | assertEquals(responses.Response, responses.OracleResponse); 30 | } 31 | 32 | @Test 33 | @Description("stats/txs has not changed against the oracle") 34 | public void statsTxs_basicResponse_test() { 35 | ApiResponseComparison responses = compareTwoResponses("stats/txs"); 36 | 37 | assertEquals(responses.Response, responses.OracleResponse); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/src/test/scala/com/cardano/rest/tests/functional/smoke_tests/AddressesTest.java: -------------------------------------------------------------------------------- 1 | package com.cardano.rest.tests.functional.smoke_tests; 2 | 3 | import com.cardano.rest.tests.functional.BaseTest; 4 | import io.qameta.allure.Description; 5 | import io.restassured.http.ContentType; 6 | import org.testng.annotations.Test; 7 | 8 | import static io.restassured.RestAssured.given; 9 | import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath; 10 | 11 | public class AddressesTest extends BaseTest { 12 | 13 | @Test 14 | @Description("addresses/summary/{address} responds") 15 | public void addressesSummaryAddress_basicResponse_test() { 16 | String endpoint = String.format("addresses/summary/%s", this.dataStore.getAddressHash()); 17 | String url = this.host + endpoint; 18 | 19 | given(). 20 | when(). 21 | get(url). 22 | then(). 23 | assertThat(). 24 | statusCode(200). 25 | and(). 26 | contentType(ContentType.JSON); 27 | } 28 | 29 | @Test 30 | @Description("addresses/summary/{address} matches expected JSON schema") 31 | public void addressesSummaryAddress_validSchema_test() { 32 | String endpoint = String.format("addresses/summary/%s", this.dataStore.getAddressHash()); 33 | String url = this.host + endpoint; 34 | 35 | given(). 36 | get(url). 37 | then(). 38 | assertThat(). 39 | body(matchesJsonSchemaInClasspath("schemas/valid-addresses-summary-address-schema.json")); 40 | } 41 | 42 | @Test 43 | @Description("block/{blockHash}/address/{address} responds") 44 | public void blockBlockhashAddressAddress_basicResponse_test() { 45 | String[] testData = this.dataStore.getBlockHashAndAddress().split(" "); 46 | String endpoint = String.format("block/%s/address/%s", testData[0], testData[1]); 47 | String url = this.host + endpoint; 48 | 49 | given(). 50 | when(). 51 | get(url). 52 | then(). 53 | assertThat(). 54 | statusCode(200). 55 | and(). 56 | contentType(ContentType.JSON); 57 | } 58 | 59 | @Test 60 | @Description("block/{blockHash}/address/{address} matches expected JSON schema") 61 | public void blockBlockhashAddressAddress_validSchema_test() { 62 | String[] testData = this.dataStore.getBlockHashAndAddress().split(" "); 63 | String endpoint = String.format("block/%s/address/%s", testData[0], testData[1]); 64 | String url = this.host + endpoint; 65 | 66 | given(). 67 | get(url). 68 | then(). 69 | assertThat(). 70 | body(matchesJsonSchemaInClasspath("schemas/valid-block-blockhash-address-address-schema.json")); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/src/test/scala/com/cardano/rest/tests/functional/smoke_tests/EpochsTest.java: -------------------------------------------------------------------------------- 1 | package com.cardano.rest.tests.functional.smoke_tests; 2 | 3 | import com.cardano.rest.tests.functional.BaseTest; 4 | import io.qameta.allure.Description; 5 | import io.restassured.http.ContentType; 6 | import org.testng.annotations.Test; 7 | 8 | import static io.restassured.RestAssured.given; 9 | import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath; 10 | 11 | public class EpochsTest extends BaseTest { 12 | 13 | @Test 14 | @Description("epochs/{epoch} responds") 15 | public void epochsEpoch_basicResponse_test() { 16 | String endpoint = String.format("epochs/%s", this.dataStore.getEpoch()); 17 | String url = this.host + endpoint; 18 | 19 | given(). 20 | when(). 21 | get(url). 22 | then(). 23 | assertThat(). 24 | statusCode(200). 25 | and(). 26 | contentType(ContentType.JSON); 27 | } 28 | 29 | @Test 30 | @Description("epochs/{epoch} matches expected JSON schema") 31 | public void epochsEpoch_validSchema_test() { 32 | String endpoint = String.format("epochs/%s", this.dataStore.getEpoch()); 33 | String url = this.host + endpoint; 34 | 35 | given(). 36 | get(url) 37 | .then() 38 | .assertThat() 39 | .body(matchesJsonSchemaInClasspath("schemas/valid-epochs-epoch-schema.json")); 40 | } 41 | 42 | @Test 43 | @Description("epochs/{epoch}/{slot} responds") 44 | public void epochsEpochSlot_basicResponse_test() { 45 | String endpoint = String.format("epochs/%s/%s", this.dataStore.getEpoch(), this.dataStore.getSlot()); 46 | String url = this.host + endpoint; 47 | 48 | given(). 49 | when(). 50 | get(url). 51 | then(). 52 | assertThat(). 53 | statusCode(200). 54 | and(). 55 | contentType(ContentType.JSON); 56 | } 57 | 58 | @Test 59 | @Description("epochs/{epoch}/{slot} matches expected JSON schema") 60 | public void epochsEpochSlot_validSchema_test() { 61 | String endpoint = String.format("epochs/%s/%s", this.dataStore.getEpoch(), this.dataStore.getSlot()); 62 | String url = this.host + endpoint; 63 | 64 | given(). 65 | get(url) 66 | .then() 67 | .assertThat() 68 | .body(matchesJsonSchemaInClasspath("schemas/valid-epochs-epoch-slot-schema.json")); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/src/test/scala/com/cardano/rest/tests/functional/smoke_tests/TransactionsTest.java: -------------------------------------------------------------------------------- 1 | package com.cardano.rest.tests.functional.smoke_tests; 2 | 3 | import com.cardano.rest.tests.functional.BaseTest; 4 | import io.qameta.allure.Description; 5 | import io.restassured.http.ContentType; 6 | import org.testng.annotations.Test; 7 | 8 | import static io.restassured.RestAssured.given; 9 | import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath; 10 | 11 | public class TransactionsTest extends BaseTest { 12 | 13 | @Test 14 | @Description("txs/last responds") 15 | public void txsLast_basicResponse_test() { 16 | String endpoint = "txs/last"; 17 | String url = this.host + endpoint; 18 | 19 | given(). 20 | when(). 21 | get(url). 22 | then(). 23 | assertThat(). 24 | statusCode(200). 25 | and(). 26 | contentType(ContentType.JSON); 27 | } 28 | 29 | @Test(dependsOnMethods = "txsLast_basicResponse_test") 30 | @Description("txs/last matches expected JSON schema") 31 | public void txsLast_validSchema_test() { 32 | String endpoint = "txs/last"; 33 | String url = this.host + endpoint; 34 | 35 | given(). 36 | get(url). 37 | then(). 38 | assertThat(). 39 | body(matchesJsonSchemaInClasspath("schemas/valid-txs-last-schema.json")); 40 | } 41 | 42 | @Test 43 | @Description("txs/summary/{tx} responds") 44 | public void txsSummaryTxid_basicResponse_test() { 45 | String endpoint = String.format("txs/summary/%s", this.dataStore.getTransactionHash()); 46 | String url = this.host + endpoint; 47 | 48 | given(). 49 | when(). 50 | get(url). 51 | then(). 52 | assertThat(). 53 | statusCode(200). 54 | and(). 55 | contentType(ContentType.JSON); 56 | } 57 | 58 | @Test 59 | @Description("txs/summary/{tx} matches expected JSON schema") 60 | public void txsSummaryTxid_validSchema_test() { 61 | String endpoint = String.format("txs/summary/%s", this.dataStore.getTransactionHash()); 62 | String url = this.host + endpoint; 63 | 64 | given(). 65 | get(url). 66 | then(). 67 | assertThat(). 68 | body(matchesJsonSchemaInClasspath("schemas/valid-txs-summary-txid-schema.json")); 69 | } 70 | 71 | @Test 72 | @Description("stats/txs responds") 73 | public void statsTxs_basicResponse_test() { 74 | String endpoint = "stats/txs"; 75 | String url = this.host + endpoint; 76 | 77 | given(). 78 | when(). 79 | get(url). 80 | then(). 81 | assertThat(). 82 | statusCode(200). 83 | and(). 84 | contentType(ContentType.JSON); 85 | } 86 | 87 | @Test(dependsOnMethods = "statsTxs_basicResponse_test") 88 | @Description("stats/txs matches expected JSON schema") 89 | public void statsTxs_validSchema_test() { 90 | String endpoint = "stats/txs"; 91 | String url = this.host + endpoint; 92 | 93 | given(). 94 | get(url). 95 | then(). 96 | assertThat(). 97 | body(matchesJsonSchemaInClasspath("schemas/valid-stats-txs-schema.json")); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /tests/src/test/scala/com/cardano/rest/tests/simulations/performance/addresses/AddressesSummaryAddressSimulation.scala: -------------------------------------------------------------------------------- 1 | package com.cardano.rest.tests.simulations.performance.addresses 2 | 3 | 4 | import java.net.URL 5 | import java.util.Properties 6 | 7 | import com.cardano.rest.tests.DataStore 8 | import io.gatling.core.Predef._ 9 | import io.gatling.core.structure.{ChainBuilder, ScenarioBuilder} 10 | import io.gatling.http.Predef._ 11 | import io.gatling.http.protocol.HttpProtocolBuilder 12 | 13 | import scala.concurrent.duration.DurationInt 14 | import scala.io.Source 15 | import scala.language.postfixOps 16 | 17 | 18 | class AddressesSummaryAddressSimulation extends Simulation { 19 | 20 | var properties : Properties = _ 21 | val url: URL = getClass.getResource("/config.properties") 22 | if (url != null) { 23 | val source = Source.fromURL(url) 24 | 25 | properties = new Properties() 26 | properties.load(source.bufferedReader()) 27 | } 28 | 29 | val host: String = properties.getProperty("host") 30 | val pauseBetweenTests: Int = properties.getProperty("pauseBetweenTests").toInt 31 | val pauseBetweenRequests: Int = properties.getProperty("pauseBetweenRequests").toInt 32 | val startingUsers: Int = properties.getProperty("startingUsers").toInt 33 | val maximumUsers: Int = properties.getProperty("maximumUsers").toInt 34 | val timeFrameToIncreaseUsers: Int = properties.getProperty("timeFrameToIncreaseUsers").toInt 35 | val maxTestDuration: Int = properties.getProperty("maxTestDuration").toInt 36 | 37 | val dataStore = new DataStore 38 | 39 | // Set-up the URL 40 | val httpConf: HttpProtocolBuilder = http.baseUrl(host) 41 | .header("Accept", "application/json") 42 | 43 | // Call the URL and set assertions 44 | def getAddressesSummaryAddress: ChainBuilder = { 45 | exec ( 46 | http("Get addresses/summary/{address}") 47 | .get(String.format("addresses/summary/%s", dataStore.getAddressHash)) 48 | .check(status.is(200)) 49 | ) 50 | } 51 | 52 | // Create the scenario 53 | val scn: ScenarioBuilder = scenario("performance test: addresses/summary/{address}") 54 | .forever( 55 | exec(getAddressesSummaryAddress) 56 | .pause(pauseBetweenRequests seconds) 57 | ) 58 | 59 | setUp( 60 | scn.inject( 61 | nothingFor(pauseBetweenTests seconds), 62 | atOnceUsers(startingUsers), 63 | rampUsers(maximumUsers) during (timeFrameToIncreaseUsers seconds) 64 | ).protocols(httpConf) 65 | ).maxDuration(maxTestDuration seconds) 66 | } 67 | -------------------------------------------------------------------------------- /tests/src/test/scala/com/cardano/rest/tests/simulations/performance/addresses/BlockBlockhashAddressAddressSimulation.scala: -------------------------------------------------------------------------------- 1 | package com.cardano.rest.tests.simulations.performance.addresses 2 | 3 | 4 | import java.net.URL 5 | import java.util.Properties 6 | 7 | import com.cardano.rest.tests.DataStore 8 | import io.gatling.core.Predef._ 9 | import io.gatling.core.structure.{ChainBuilder, ScenarioBuilder} 10 | import io.gatling.http.Predef._ 11 | import io.gatling.http.protocol.HttpProtocolBuilder 12 | 13 | import scala.concurrent.duration.DurationInt 14 | import scala.io.Source 15 | 16 | 17 | class BlockBlockhashAddressAddressSimulation extends Simulation { 18 | 19 | var properties : Properties = _ 20 | val url: URL = getClass.getResource("/config.properties") 21 | if (url != null) { 22 | val source = Source.fromURL(url) 23 | 24 | properties = new Properties() 25 | properties.load(source.bufferedReader()) 26 | } 27 | 28 | val host: String = properties.getProperty("host") 29 | val pauseBetweenTests: Int = properties.getProperty("pauseBetweenTests").toInt 30 | val pauseBetweenRequests: Int = properties.getProperty("pauseBetweenRequests").toInt 31 | val startingUsers: Int = properties.getProperty("startingUsers").toInt 32 | val maximumUsers: Int = properties.getProperty("maximumUsers").toInt 33 | val timeFrameToIncreaseUsers: Int = properties.getProperty("timeFrameToIncreaseUsers").toInt 34 | val maxTestDuration: Int = properties.getProperty("maxTestDuration").toInt 35 | 36 | 37 | val dataStore = new DataStore 38 | 39 | val httpConf: HttpProtocolBuilder = http.baseUrl(host) 40 | .header("Accept", "application/json") 41 | 42 | def getBlockBlockhashAddressAddress: ChainBuilder = { 43 | exec ( 44 | http("Get addresses/summary/{address}") 45 | .get(String.format("block/%s/address/%s", dataStore.getBlockHash, dataStore.getAddressHash)) 46 | .check(status.is(200)) 47 | ) 48 | } 49 | 50 | val scn: ScenarioBuilder = scenario("performance test: block/{blockhash}/address/{address}") 51 | .forever( 52 | exec(getBlockBlockhashAddressAddress) 53 | .pause(pauseBetweenRequests seconds) 54 | ) 55 | 56 | setUp( 57 | scn.inject( 58 | nothingFor(pauseBetweenTests seconds), 59 | atOnceUsers(startingUsers), 60 | rampUsers(maximumUsers) during (timeFrameToIncreaseUsers seconds) 61 | ).protocols(httpConf) 62 | ).maxDuration(maxTestDuration seconds) 63 | } 64 | -------------------------------------------------------------------------------- /tests/src/test/scala/com/cardano/rest/tests/simulations/performance/blocks/BlocksPagesSimulation.scala: -------------------------------------------------------------------------------- 1 | package com.cardano.rest.tests.simulations.performance.blocks 2 | 3 | import java.net.URL 4 | import java.util.Properties 5 | 6 | import io.gatling.core.Predef._ 7 | import io.gatling.core.structure.{ChainBuilder, ScenarioBuilder} 8 | import io.gatling.http.Predef._ 9 | import io.gatling.http.protocol.HttpProtocolBuilder 10 | 11 | import scala.concurrent.duration.DurationInt 12 | import scala.io.Source 13 | 14 | 15 | class BlocksPagesSimulation extends Simulation { 16 | 17 | var properties : Properties = _ 18 | val url: URL = getClass.getResource("/config.properties") 19 | if (url != null) { 20 | val source = Source.fromURL(url) 21 | 22 | properties = new Properties() 23 | properties.load(source.bufferedReader()) 24 | } 25 | 26 | val host: String = properties.getProperty("host") 27 | val pauseBetweenTests: Int = properties.getProperty("pauseBetweenTests").toInt 28 | val pauseBetweenRequests: Int = properties.getProperty("pauseBetweenRequests").toInt 29 | val startingUsers: Int = properties.getProperty("startingUsers").toInt 30 | val maximumUsers: Int = properties.getProperty("maximumUsers").toInt 31 | val timeFrameToIncreaseUsers: Int = properties.getProperty("timeFrameToIncreaseUsers").toInt 32 | val maxTestDuration: Int = properties.getProperty("maxTestDuration").toInt 33 | 34 | 35 | val httpConf: HttpProtocolBuilder = http.baseUrl(host) 36 | .header("Accept", "application/json") 37 | 38 | def getBlocksPages: ChainBuilder = { 39 | exec ( 40 | http("Get blocks/pages") 41 | .get("blocks/pages") 42 | .check(status.is(200)) 43 | ) 44 | } 45 | 46 | val scn: ScenarioBuilder = scenario("performance test: blocks/pages") 47 | .forever( 48 | exec(getBlocksPages) 49 | .pause(pauseBetweenRequests seconds) 50 | ) 51 | 52 | setUp( 53 | scn.inject( 54 | nothingFor(pauseBetweenTests seconds), 55 | atOnceUsers(startingUsers), 56 | rampUsers(maximumUsers) during (timeFrameToIncreaseUsers seconds) 57 | ).protocols(httpConf) 58 | ).maxDuration(maxTestDuration seconds) 59 | } 60 | -------------------------------------------------------------------------------- /tests/src/test/scala/com/cardano/rest/tests/simulations/performance/blocks/BlocksPagesTotalSimulation.scala: -------------------------------------------------------------------------------- 1 | package com.cardano.rest.tests.simulations.performance.blocks 2 | 3 | import java.net.URL 4 | import java.util.Properties 5 | 6 | import io.gatling.core.Predef._ 7 | import io.gatling.core.structure.{ChainBuilder, ScenarioBuilder} 8 | import io.gatling.http.Predef._ 9 | import io.gatling.http.protocol.HttpProtocolBuilder 10 | 11 | import scala.concurrent.duration.DurationInt 12 | import scala.io.Source 13 | 14 | 15 | class BlocksPagesTotalSimulation extends Simulation { 16 | 17 | var properties : Properties = _ 18 | val url: URL = getClass.getResource("/config.properties") 19 | if (url != null) { 20 | val source = Source.fromURL(url) 21 | 22 | properties = new Properties() 23 | properties.load(source.bufferedReader()) 24 | } 25 | 26 | val host: String = properties.getProperty("host") 27 | val pauseBetweenTests: Int = properties.getProperty("pauseBetweenTests").toInt 28 | val pauseBetweenRequests: Int = properties.getProperty("pauseBetweenRequests").toInt 29 | val startingUsers: Int = properties.getProperty("startingUsers").toInt 30 | val maximumUsers: Int = properties.getProperty("maximumUsers").toInt 31 | val timeFrameToIncreaseUsers: Int = properties.getProperty("timeFrameToIncreaseUsers").toInt 32 | val maxTestDuration: Int = properties.getProperty("maxTestDuration").toInt 33 | 34 | 35 | val httpConf: HttpProtocolBuilder = http.baseUrl(host) 36 | .header("Accept", "application/json") 37 | 38 | def getBlocksPagesTotal: ChainBuilder = { 39 | exec ( 40 | http("Get blocks/pages/total") 41 | .get("blocks/pages/total") 42 | .check(status.is(200)) 43 | ) 44 | } 45 | 46 | val scn: ScenarioBuilder = scenario("performance test: blocks/pages/total") 47 | .forever( 48 | exec(getBlocksPagesTotal) 49 | .pause(pauseBetweenRequests seconds) 50 | ) 51 | 52 | setUp( 53 | scn.inject( 54 | nothingFor(pauseBetweenTests seconds), 55 | atOnceUsers(startingUsers), 56 | rampUsers(maximumUsers) during (timeFrameToIncreaseUsers seconds) 57 | ).protocols(httpConf) 58 | ).maxDuration(maxTestDuration seconds) 59 | } 60 | -------------------------------------------------------------------------------- /tests/src/test/scala/com/cardano/rest/tests/simulations/performance/blocks/BlocksSummaryBlockhashSimulation.scala: -------------------------------------------------------------------------------- 1 | package com.cardano.rest.tests.simulations.performance.blocks 2 | 3 | import java.net.URL 4 | import java.util.Properties 5 | 6 | import com.cardano.rest.tests.DataStore 7 | import io.gatling.core.Predef._ 8 | import io.gatling.core.structure.{ChainBuilder, ScenarioBuilder} 9 | import io.gatling.http.Predef._ 10 | import io.gatling.http.protocol.HttpProtocolBuilder 11 | 12 | import scala.concurrent.duration.DurationInt 13 | import scala.io.Source 14 | 15 | 16 | class BlocksSummaryBlockhashSimulation extends Simulation { 17 | 18 | var properties : Properties = _ 19 | val url: URL = getClass.getResource("/config.properties") 20 | if (url != null) { 21 | val source = Source.fromURL(url) 22 | 23 | properties = new Properties() 24 | properties.load(source.bufferedReader()) 25 | } 26 | 27 | val host: String = properties.getProperty("host") 28 | val pauseBetweenTests: Int = properties.getProperty("pauseBetweenTests").toInt 29 | val pauseBetweenRequests: Int = properties.getProperty("pauseBetweenRequests").toInt 30 | val startingUsers: Int = properties.getProperty("startingUsers").toInt 31 | val maximumUsers: Int = properties.getProperty("maximumUsers").toInt 32 | val timeFrameToIncreaseUsers: Int = properties.getProperty("timeFrameToIncreaseUsers").toInt 33 | val maxTestDuration: Int = properties.getProperty("maxTestDuration").toInt 34 | 35 | val dataStore = new DataStore 36 | 37 | 38 | val httpConf: HttpProtocolBuilder = http.baseUrl(host) 39 | .header("Accept", "application/json") 40 | 41 | def getBlocksSummaryBlockhashTotal: ChainBuilder = { 42 | exec ( 43 | http("Get blocks/summary/{blockhash}") 44 | .get(String.format("blocks/summary/%s", dataStore.getBlockHash)) 45 | .check(status.is(200)) 46 | ) 47 | } 48 | 49 | val scn: ScenarioBuilder = scenario("performance test: blocks/summary/{blockhash}") 50 | .forever( 51 | exec(getBlocksSummaryBlockhashTotal) 52 | .pause(pauseBetweenRequests seconds) 53 | ) 54 | 55 | setUp( 56 | scn.inject( 57 | nothingFor(pauseBetweenTests seconds), 58 | atOnceUsers(startingUsers), 59 | rampUsers(maximumUsers) during (timeFrameToIncreaseUsers seconds) 60 | ).protocols(httpConf) 61 | ).maxDuration(maxTestDuration seconds) 62 | } 63 | -------------------------------------------------------------------------------- /tests/src/test/scala/com/cardano/rest/tests/simulations/performance/blocks/BlocksTxsBlockhashSimulation.scala: -------------------------------------------------------------------------------- 1 | package com.cardano.rest.tests.simulations.performance.blocks 2 | 3 | import java.net.URL 4 | import java.util.Properties 5 | 6 | import com.cardano.rest.tests.DataStore 7 | import io.gatling.core.Predef._ 8 | import io.gatling.core.structure.{ChainBuilder, ScenarioBuilder} 9 | import io.gatling.http.Predef._ 10 | import io.gatling.http.protocol.HttpProtocolBuilder 11 | 12 | import scala.concurrent.duration.DurationInt 13 | import scala.io.Source 14 | 15 | 16 | class BlocksTxsBlockhashSimulation extends Simulation { 17 | 18 | var properties : Properties = _ 19 | val url: URL = getClass.getResource("/config.properties") 20 | if (url != null) { 21 | val source = Source.fromURL(url) 22 | 23 | properties = new Properties() 24 | properties.load(source.bufferedReader()) 25 | } 26 | 27 | val host: String = properties.getProperty("host") 28 | val pauseBetweenTests: Int = properties.getProperty("pauseBetweenTests").toInt 29 | val pauseBetweenRequests: Int = properties.getProperty("pauseBetweenRequests").toInt 30 | val startingUsers: Int = properties.getProperty("startingUsers").toInt 31 | val maximumUsers: Int = properties.getProperty("maximumUsers").toInt 32 | val timeFrameToIncreaseUsers: Int = properties.getProperty("timeFrameToIncreaseUsers").toInt 33 | val maxTestDuration: Int = properties.getProperty("maxTestDuration").toInt 34 | 35 | val dataStore = new DataStore 36 | 37 | val httpConf: HttpProtocolBuilder = http.baseUrl(host) 38 | .header("Accept", "application/json") 39 | 40 | def getBlocksTxsBlockhashTotal: ChainBuilder = { 41 | exec ( 42 | http("Get blocks/txs/{blockhash}") 43 | .get(String.format("blocks/txs/%s", dataStore.getBlockHash)) 44 | .check(status.is(200)) 45 | ) 46 | } 47 | 48 | val scn: ScenarioBuilder = scenario("performance test: blocks/txs/{blockhash}") 49 | .forever( 50 | exec(getBlocksTxsBlockhashTotal) 51 | .pause(pauseBetweenRequests seconds) 52 | ) 53 | 54 | setUp( 55 | scn.inject( 56 | nothingFor(pauseBetweenTests seconds), 57 | atOnceUsers(startingUsers), 58 | rampUsers(maximumUsers) during (timeFrameToIncreaseUsers seconds) 59 | ).protocols(httpConf) 60 | ).maxDuration(maxTestDuration seconds) 61 | } 62 | -------------------------------------------------------------------------------- /tests/src/test/scala/com/cardano/rest/tests/simulations/performance/epochs/EpochsEpochSimulation.scala: -------------------------------------------------------------------------------- 1 | package com.cardano.rest.tests.simulations.performance.epochs 2 | 3 | import java.net.URL 4 | import java.util.Properties 5 | 6 | import io.gatling.core.Predef._ 7 | import io.gatling.core.structure.{ChainBuilder, ScenarioBuilder} 8 | import io.gatling.http.Predef._ 9 | import io.gatling.http.protocol.HttpProtocolBuilder 10 | 11 | import scala.concurrent.duration.DurationInt 12 | import scala.io.Source 13 | import scala.language.postfixOps 14 | 15 | import com.cardano.rest.tests.DataStore 16 | 17 | class EpochsEpochSimulation extends Simulation { 18 | 19 | var properties : Properties = _ 20 | val url: URL = getClass.getResource("/config.properties") 21 | if (url != null) { 22 | val source = Source.fromURL(url) 23 | 24 | properties = new Properties() 25 | properties.load(source.bufferedReader()) 26 | } 27 | 28 | val host: String = properties.getProperty("host") 29 | val pauseBetweenTests: Int = properties.getProperty("pauseBetweenTests").toInt 30 | val pauseBetweenRequests: Int = properties.getProperty("pauseBetweenRequests").toInt 31 | val startingUsers: Int = properties.getProperty("startingUsers").toInt 32 | val maximumUsers: Int = properties.getProperty("maximumUsers").toInt 33 | val timeFrameToIncreaseUsers: Int = properties.getProperty("timeFrameToIncreaseUsers").toInt 34 | val maxTestDuration: Int = properties.getProperty("maxTestDuration").toInt 35 | 36 | val dataStore = new DataStore 37 | 38 | val httpConf: HttpProtocolBuilder = http.baseUrl(host) 39 | .header("Accept", "application/json") 40 | 41 | def getEpochsEpoch: ChainBuilder = { 42 | exec ( 43 | http("Get epochs/{epoch}") 44 | .get(String.format("epochs/%s", dataStore.getEpoch)) 45 | .check(status.is(200)) 46 | ) 47 | } 48 | 49 | val scn: ScenarioBuilder = scenario("performance test: epochs/{epoch}") 50 | .forever( 51 | exec(getEpochsEpoch) 52 | .pause(pauseBetweenRequests seconds) 53 | ) 54 | 55 | setUp( 56 | scn.inject( 57 | nothingFor(pauseBetweenTests seconds), 58 | atOnceUsers(startingUsers), 59 | rampUsers(maximumUsers) during (timeFrameToIncreaseUsers seconds) 60 | ).protocols(httpConf) 61 | ).maxDuration(maxTestDuration seconds) 62 | } 63 | -------------------------------------------------------------------------------- /tests/src/test/scala/com/cardano/rest/tests/simulations/performance/epochs/EpochsEpochSlotsSlotSimulation.scala: -------------------------------------------------------------------------------- 1 | package com.cardano.rest.tests.simulations.performance.epochs 2 | 3 | import java.net.URL 4 | import java.util.Properties 5 | 6 | import com.cardano.rest.tests.DataStore 7 | import io.gatling.core.Predef._ 8 | import io.gatling.core.structure.{ChainBuilder, ScenarioBuilder} 9 | import io.gatling.http.Predef._ 10 | import io.gatling.http.protocol.HttpProtocolBuilder 11 | 12 | import scala.concurrent.duration.DurationInt 13 | import scala.io.Source 14 | 15 | 16 | class EpochsEpochSlotsSlotSimulation extends Simulation { 17 | 18 | var properties : Properties = _ 19 | val url: URL = getClass.getResource("/config.properties") 20 | if (url != null) { 21 | val source = Source.fromURL(url) 22 | 23 | properties = new Properties() 24 | properties.load(source.bufferedReader()) 25 | } 26 | 27 | val host: String = properties.getProperty("host") 28 | val pauseBetweenTests: Int = properties.getProperty("pauseBetweenTests").toInt 29 | val pauseBetweenRequests: Int = properties.getProperty("pauseBetweenRequests").toInt 30 | val startingUsers: Int = properties.getProperty("startingUsers").toInt 31 | val maximumUsers: Int = properties.getProperty("maximumUsers").toInt 32 | val timeFrameToIncreaseUsers: Int = properties.getProperty("timeFrameToIncreaseUsers").toInt 33 | val maxTestDuration: Int = properties.getProperty("maxTestDuration").toInt 34 | 35 | 36 | val dataStore = new DataStore 37 | 38 | val httpConf: HttpProtocolBuilder = http.baseUrl(host) 39 | .header("Accept", "application/json") 40 | 41 | def getEpochsEpochSlotsSlot: ChainBuilder = { 42 | exec ( 43 | http("Get epochs/{epoch}/slots/{slot}") 44 | .get(String.format("epochs/%s/slots/%s", dataStore.getEpoch, dataStore.getSlot)) 45 | .check(status.is(200)) 46 | ) 47 | } 48 | 49 | val scn: ScenarioBuilder = scenario("performance test: epochs/{epoch}/slots/{slot}") 50 | .forever( 51 | exec(getEpochsEpochSlotsSlot) 52 | .pause(pauseBetweenRequests seconds) 53 | ) 54 | 55 | setUp( 56 | scn.inject( 57 | nothingFor(pauseBetweenTests seconds), 58 | atOnceUsers(startingUsers), 59 | rampUsers(maximumUsers) during (timeFrameToIncreaseUsers seconds) 60 | ).protocols(httpConf) 61 | ).maxDuration(maxTestDuration seconds) 62 | } 63 | -------------------------------------------------------------------------------- /tests/src/test/scala/com/cardano/rest/tests/simulations/performance/genesis/GenesisAddressPagesTotalSimulation.scala: -------------------------------------------------------------------------------- 1 | package com.cardano.rest.tests.simulations.performance.genesis 2 | 3 | import java.net.URL 4 | import java.util.Properties 5 | 6 | import io.gatling.core.Predef._ 7 | import io.gatling.core.structure.{ChainBuilder, ScenarioBuilder} 8 | import io.gatling.http.Predef._ 9 | import io.gatling.http.protocol.HttpProtocolBuilder 10 | 11 | import scala.concurrent.duration.DurationInt 12 | import scala.io.Source 13 | 14 | 15 | class GenesisAddressPagesTotalSimulation extends Simulation { 16 | 17 | var properties : Properties = _ 18 | val url: URL = getClass.getResource("/config.properties") 19 | if (url != null) { 20 | val source = Source.fromURL(url) 21 | 22 | properties = new Properties() 23 | properties.load(source.bufferedReader()) 24 | } 25 | 26 | val host: String = properties.getProperty("host") 27 | val pauseBetweenTests: Int = properties.getProperty("pauseBetweenTests").toInt 28 | val pauseBetweenRequests: Int = properties.getProperty("pauseBetweenRequests").toInt 29 | val startingUsers: Int = properties.getProperty("startingUsers").toInt 30 | val maximumUsers: Int = properties.getProperty("maximumUsers").toInt 31 | val timeFrameToIncreaseUsers: Int = properties.getProperty("timeFrameToIncreaseUsers").toInt 32 | val maxTestDuration: Int = properties.getProperty("maxTestDuration").toInt 33 | 34 | 35 | val httpConf: HttpProtocolBuilder = http.baseUrl(host) 36 | .header("Accept", "application/json") 37 | 38 | def getGenesisAddressPagesSummary: ChainBuilder = { 39 | exec ( 40 | http("Get genesis/address/pages") 41 | .get("genesis/address/pages") 42 | .check(status.is(200)) 43 | ) 44 | } 45 | 46 | val scn: ScenarioBuilder = scenario("performance test: genesis/address/pages") 47 | .forever( 48 | exec(getGenesisAddressPagesSummary) 49 | .pause(pauseBetweenRequests seconds) 50 | ) 51 | 52 | setUp( 53 | scn.inject( 54 | nothingFor(pauseBetweenTests seconds), 55 | atOnceUsers(startingUsers), 56 | rampUsers(maximumUsers) during (timeFrameToIncreaseUsers seconds) 57 | ).protocols(httpConf) 58 | ).maxDuration(maxTestDuration seconds) 59 | } 60 | -------------------------------------------------------------------------------- /tests/src/test/scala/com/cardano/rest/tests/simulations/performance/genesis/GenesisAddressSimulation.scala: -------------------------------------------------------------------------------- 1 | package com.cardano.rest.tests.simulations.performance.genesis 2 | 3 | import java.net.URL 4 | import java.util.Properties 5 | 6 | import io.gatling.core.Predef._ 7 | import io.gatling.core.structure.{ChainBuilder, ScenarioBuilder} 8 | import io.gatling.http.Predef._ 9 | import io.gatling.http.protocol.HttpProtocolBuilder 10 | 11 | import scala.concurrent.duration.DurationInt 12 | import scala.io.Source 13 | 14 | 15 | class GenesisAddressSimulation extends Simulation { 16 | 17 | var properties : Properties = _ 18 | val url: URL = getClass.getResource("/config.properties") 19 | if (url != null) { 20 | val source = Source.fromURL(url) 21 | 22 | properties = new Properties() 23 | properties.load(source.bufferedReader()) 24 | } 25 | 26 | val host: String = properties.getProperty("host") 27 | val pauseBetweenTests: Int = properties.getProperty("pauseBetweenTests").toInt 28 | val pauseBetweenRequests: Int = properties.getProperty("pauseBetweenRequests").toInt 29 | val startingUsers: Int = properties.getProperty("startingUsers").toInt 30 | val maximumUsers: Int = properties.getProperty("maximumUsers").toInt 31 | val timeFrameToIncreaseUsers: Int = properties.getProperty("timeFrameToIncreaseUsers").toInt 32 | val maxTestDuration: Int = properties.getProperty("maxTestDuration").toInt 33 | 34 | 35 | val httpConf: HttpProtocolBuilder = http.baseUrl(host) 36 | .header("Accept", "application/json") 37 | 38 | def getGenesisAddress: ChainBuilder = { 39 | exec ( 40 | http("Get genesis/address") 41 | .get("genesis/address") 42 | .check(status.is(200)) 43 | ) 44 | } 45 | 46 | val scn: ScenarioBuilder = scenario("performance test: genesis/address") 47 | .forever( 48 | exec(getGenesisAddress) 49 | .pause(pauseBetweenRequests seconds) 50 | ) 51 | 52 | setUp( 53 | scn.inject( 54 | nothingFor(pauseBetweenTests seconds), 55 | atOnceUsers(startingUsers), 56 | rampUsers(maximumUsers) during (timeFrameToIncreaseUsers seconds) 57 | ).protocols(httpConf) 58 | ).maxDuration(maxTestDuration seconds) 59 | } 60 | -------------------------------------------------------------------------------- /tests/src/test/scala/com/cardano/rest/tests/simulations/performance/genesis/GenesisSummarySimulation.scala: -------------------------------------------------------------------------------- 1 | package com.cardano.rest.tests.simulations.performance.genesis 2 | 3 | import java.net.URL 4 | import java.util.Properties 5 | 6 | import io.gatling.core.Predef._ 7 | import io.gatling.core.structure.{ChainBuilder, ScenarioBuilder} 8 | import io.gatling.http.Predef._ 9 | import io.gatling.http.protocol.HttpProtocolBuilder 10 | 11 | import scala.concurrent.duration.DurationInt 12 | import scala.io.Source 13 | 14 | 15 | class GenesisSummarySimulation extends Simulation { 16 | 17 | var properties : Properties = _ 18 | val url: URL = getClass.getResource("/config.properties") 19 | if (url != null) { 20 | val source = Source.fromURL(url) 21 | 22 | properties = new Properties() 23 | properties.load(source.bufferedReader()) 24 | } 25 | 26 | val host: String = properties.getProperty("host") 27 | val pauseBetweenTests: Int = properties.getProperty("pauseBetweenTests").toInt 28 | val pauseBetweenRequests: Int = properties.getProperty("pauseBetweenRequests").toInt 29 | val startingUsers: Int = properties.getProperty("startingUsers").toInt 30 | val maximumUsers: Int = properties.getProperty("maximumUsers").toInt 31 | val timeFrameToIncreaseUsers: Int = properties.getProperty("timeFrameToIncreaseUsers").toInt 32 | val maxTestDuration: Int = properties.getProperty("maxTestDuration").toInt 33 | 34 | 35 | val httpConf: HttpProtocolBuilder = http.baseUrl(host) 36 | .header("Accept", "application/json") 37 | 38 | def getGenesisSummary: ChainBuilder = { 39 | exec ( 40 | http("Get genesis/summary") 41 | .get("genesis/summary") 42 | .check(status.is(200)) 43 | ) 44 | } 45 | 46 | val scn: ScenarioBuilder = scenario("performance test: genesis/summary") 47 | .forever( 48 | exec(getGenesisSummary) 49 | .pause(pauseBetweenRequests seconds) 50 | ) 51 | 52 | setUp( 53 | scn.inject( 54 | nothingFor(pauseBetweenTests seconds), 55 | atOnceUsers(startingUsers), 56 | rampUsers(maximumUsers) during (timeFrameToIncreaseUsers seconds) 57 | ).protocols(httpConf) 58 | ).maxDuration(maxTestDuration seconds) 59 | } 60 | -------------------------------------------------------------------------------- /tests/src/test/scala/com/cardano/rest/tests/simulations/performance/genesis/SupplyAdaSimulation.scala: -------------------------------------------------------------------------------- 1 | package com.cardano.rest.tests.simulations.performance.genesis 2 | 3 | import java.net.URL 4 | import java.util.Properties 5 | 6 | import io.gatling.core.Predef._ 7 | import io.gatling.core.structure.{ChainBuilder, ScenarioBuilder} 8 | import io.gatling.http.Predef._ 9 | import io.gatling.http.protocol.HttpProtocolBuilder 10 | 11 | import scala.concurrent.duration.DurationInt 12 | import scala.io.Source 13 | 14 | 15 | class SupplyAdaSimulation extends Simulation { 16 | 17 | var properties : Properties = _ 18 | val url: URL = getClass.getResource("/config.properties") 19 | if (url != null) { 20 | val source = Source.fromURL(url) 21 | 22 | properties = new Properties() 23 | properties.load(source.bufferedReader()) 24 | } 25 | 26 | val host: String = properties.getProperty("host") 27 | val pauseBetweenTests: Int = properties.getProperty("pauseBetweenTests").toInt 28 | val pauseBetweenRequests: Int = properties.getProperty("pauseBetweenRequests").toInt 29 | val startingUsers: Int = properties.getProperty("startingUsers").toInt 30 | val maximumUsers: Int = properties.getProperty("maximumUsers").toInt 31 | val timeFrameToIncreaseUsers: Int = properties.getProperty("timeFrameToIncreaseUsers").toInt 32 | val maxTestDuration: Int = properties.getProperty("maxTestDuration").toInt 33 | 34 | 35 | val httpConf: HttpProtocolBuilder = http.baseUrl(host) 36 | .header("Accept", "application/json") 37 | 38 | def getSupplyAda: ChainBuilder = { 39 | exec ( 40 | http("Get supply/ada") 41 | .get("supply/ada") 42 | .check(status.is(200)) 43 | ) 44 | } 45 | 46 | val scn: ScenarioBuilder = scenario("performance test: supply/ada") 47 | .forever( 48 | exec(getSupplyAda) 49 | .pause(pauseBetweenRequests seconds) 50 | ) 51 | 52 | setUp( 53 | scn.inject( 54 | nothingFor(pauseBetweenTests seconds), 55 | atOnceUsers(startingUsers), 56 | rampUsers(maximumUsers) during (timeFrameToIncreaseUsers seconds) 57 | ).protocols(httpConf) 58 | ).maxDuration(maxTestDuration seconds) 59 | } 60 | -------------------------------------------------------------------------------- /tests/src/test/scala/com/cardano/rest/tests/simulations/performance/transactions/StatsTxsSimulation.scala: -------------------------------------------------------------------------------- 1 | package com.cardano.rest.tests.simulations.performance.transactions 2 | 3 | import java.net.URL 4 | import java.util.Properties 5 | 6 | import io.gatling.core.Predef._ 7 | import io.gatling.core.structure.{ChainBuilder, ScenarioBuilder} 8 | import io.gatling.http.Predef._ 9 | import io.gatling.http.protocol.HttpProtocolBuilder 10 | 11 | import scala.concurrent.duration.DurationInt 12 | import scala.io.Source 13 | 14 | 15 | class StatsTxsSimulation extends Simulation { 16 | 17 | var properties : Properties = _ 18 | val url: URL = getClass.getResource("/config.properties") 19 | if (url != null) { 20 | val source = Source.fromURL(url) 21 | 22 | properties = new Properties() 23 | properties.load(source.bufferedReader()) 24 | } 25 | 26 | val host: String = properties.getProperty("host") 27 | val pauseBetweenTests: Int = properties.getProperty("pauseBetweenTests").toInt 28 | val pauseBetweenRequests: Int = properties.getProperty("pauseBetweenRequests").toInt 29 | val startingUsers: Int = properties.getProperty("startingUsers").toInt 30 | val maximumUsers: Int = properties.getProperty("maximumUsers").toInt 31 | val timeFrameToIncreaseUsers: Int = properties.getProperty("timeFrameToIncreaseUsers").toInt 32 | val maxTestDuration: Int = properties.getProperty("maxTestDuration").toInt 33 | 34 | 35 | val httpConf: HttpProtocolBuilder = http.baseUrl(host) 36 | .header("Accept", "application/json") 37 | 38 | def getSupplyAda: ChainBuilder = { 39 | exec ( 40 | http("Get stats/txs") 41 | .get("stats/txs") 42 | .check(status.is(200)) 43 | ) 44 | } 45 | 46 | val scn: ScenarioBuilder = scenario("performance test: stats/txs") 47 | .forever( 48 | exec(getSupplyAda) 49 | .pause(pauseBetweenRequests seconds) 50 | ) 51 | 52 | setUp( 53 | scn.inject( 54 | nothingFor(pauseBetweenTests seconds), 55 | atOnceUsers(startingUsers), 56 | rampUsers(maximumUsers) during (timeFrameToIncreaseUsers seconds) 57 | ).protocols(httpConf) 58 | ).maxDuration(maxTestDuration seconds) 59 | } 60 | -------------------------------------------------------------------------------- /tests/src/test/scala/com/cardano/rest/tests/simulations/performance/transactions/TxsLastSimulation.scala: -------------------------------------------------------------------------------- 1 | package com.cardano.rest.tests.simulations.performance.transactions 2 | 3 | import java.net.URL 4 | import java.util.Properties 5 | 6 | import io.gatling.core.Predef._ 7 | import io.gatling.core.structure.{ChainBuilder, ScenarioBuilder} 8 | import io.gatling.http.Predef._ 9 | import io.gatling.http.protocol.HttpProtocolBuilder 10 | 11 | import scala.concurrent.duration.DurationInt 12 | import scala.io.Source 13 | 14 | 15 | class TxsLastSimulation extends Simulation { 16 | 17 | var properties : Properties = _ 18 | val url: URL = getClass.getResource("/config.properties") 19 | if (url != null) { 20 | val source = Source.fromURL(url) 21 | 22 | properties = new Properties() 23 | properties.load(source.bufferedReader()) 24 | } 25 | 26 | val host: String = properties.getProperty("host") 27 | val pauseBetweenTests: Int = properties.getProperty("pauseBetweenTests").toInt 28 | val pauseBetweenRequests: Int = properties.getProperty("pauseBetweenRequests").toInt 29 | val startingUsers: Int = properties.getProperty("startingUsers").toInt 30 | val maximumUsers: Int = properties.getProperty("maximumUsers").toInt 31 | val timeFrameToIncreaseUsers: Int = properties.getProperty("timeFrameToIncreaseUsers").toInt 32 | val maxTestDuration: Int = properties.getProperty("maxTestDuration").toInt 33 | 34 | val httpConf: HttpProtocolBuilder = http.baseUrl(host) 35 | .header("Accept", "application/json") 36 | 37 | def getTxsLast: ChainBuilder = { 38 | exec ( 39 | http("Get txs/last") 40 | .get("txs/last") 41 | .check(status.is(200)) 42 | ) 43 | } 44 | 45 | val scn: ScenarioBuilder = scenario("performance test: txs/last") 46 | .forever( 47 | exec(getTxsLast) 48 | .pause(pauseBetweenRequests seconds) 49 | ) 50 | 51 | setUp( 52 | scn.inject( 53 | nothingFor(pauseBetweenTests seconds), 54 | atOnceUsers(startingUsers), 55 | rampUsers(maximumUsers) during (timeFrameToIncreaseUsers seconds) 56 | ).protocols(httpConf) 57 | ).maxDuration(maxTestDuration seconds) 58 | } 59 | -------------------------------------------------------------------------------- /tests/src/test/scala/com/cardano/rest/tests/simulations/performance/transactions/TxsSummaryTxSimulation.scala: -------------------------------------------------------------------------------- 1 | package com.cardano.rest.tests.simulations.performance.transactions 2 | 3 | import java.net.URL 4 | import java.util.Properties 5 | 6 | import com.cardano.rest.tests.DataStore 7 | import com.typesafe.config.{Config, ConfigFactory} 8 | import io.gatling.core.Predef._ 9 | import io.gatling.core.structure.{ChainBuilder, ScenarioBuilder} 10 | import io.gatling.http.Predef._ 11 | import io.gatling.http.protocol.HttpProtocolBuilder 12 | 13 | import scala.concurrent.duration.DurationInt 14 | import scala.io.Source 15 | 16 | 17 | class TxsSummaryTxSimulation extends Simulation { 18 | 19 | var properties : Properties = _ 20 | val url: URL = getClass.getResource("/config.properties") 21 | if (url != null) { 22 | val source = Source.fromURL(url) 23 | 24 | properties = new Properties() 25 | properties.load(source.bufferedReader()) 26 | } 27 | 28 | val host: String = properties.getProperty("host") 29 | val pauseBetweenTests: Int = properties.getProperty("pauseBetweenTests").toInt 30 | val pauseBetweenRequests: Int = properties.getProperty("pauseBetweenRequests").toInt 31 | val startingUsers: Int = properties.getProperty("startingUsers").toInt 32 | val maximumUsers: Int = properties.getProperty("maximumUsers").toInt 33 | val timeFrameToIncreaseUsers: Int = properties.getProperty("timeFrameToIncreaseUsers").toInt 34 | val maxTestDuration: Int = properties.getProperty("maxTestDuration").toInt 35 | 36 | val dataStore = new DataStore 37 | 38 | val httpConf: HttpProtocolBuilder = http.baseUrl(host) 39 | .header("Accept", "application/json") 40 | 41 | def getTxsSummaryTx: ChainBuilder = { 42 | exec ( 43 | http("Get txs/summary/{tx}") 44 | .get(String.format("txs/summary/%s", dataStore.getTransactionHash)) 45 | .check(status.is(200)) 46 | ) 47 | } 48 | 49 | val scn: ScenarioBuilder = scenario("performance test: txs/summary/{tx}") 50 | .forever( 51 | exec(getTxsSummaryTx) 52 | .pause(pauseBetweenRequests seconds) 53 | ) 54 | 55 | setUp( 56 | scn.inject( 57 | nothingFor(pauseBetweenTests seconds), 58 | atOnceUsers(startingUsers), 59 | rampUsers(maximumUsers) during (timeFrameToIncreaseUsers seconds) 60 | ).protocols(httpConf) 61 | ).maxDuration(maxTestDuration seconds) 62 | } 63 | -------------------------------------------------------------------------------- /tests/tests_functional-data-intensive.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/tests_functional-data-validation.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/tests_functional-smoke.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/tests_oracle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/tests_performance-runner.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | HOST=http://localhost:8100/api/ 4 | REPORT_DIR=./target/gatling 5 | 6 | rm -fr ${REPORT_DIR} 7 | 8 | mvn gatling:test -Dhost=${HOST} 9 | 10 | for dir in "${REPORT_DIR}"/*/ 11 | do 12 | mv "${dir}" "$(echo "${dir}" | cut -d- -f1)" 13 | done 14 | -------------------------------------------------------------------------------- /tests/tests_run-all.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | --------------------------------------------------------------------------------