diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index a10966c273..3cbac56978 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -12,6 +12,9 @@ env: MIX_ENV: test OTP_VERSION: '24.3.4.1' ELIXIR_VERSION: '1.13.4' + ACCOUNT_AUTH0_DOMAIN: 'blockscoutcom.us.auth0.com' + ACCOUNT_AUTH0_LOGOUT_URL: 'https://blockscoutcom.us.auth0.com/v2/logout' + ACCOUNT_AUTH0_LOGOUT_RETURN_URL: 'https://blockscout.com/auth/logout' jobs: build-and-cache: @@ -34,7 +37,7 @@ jobs: uses: actions/cache@v2 id: deps-cache with: - path: | + path: | deps _build key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_12-${{ hashFiles('mix.lock') }} @@ -89,12 +92,12 @@ jobs: with: otp-version: ${{ env.OTP_VERSION }} elixir-version: ${{ env.ELIXIR_VERSION }} - + - name: Restore Mix Deps Cache uses: actions/cache@v2 id: deps-cache with: - path: | + path: | deps _build key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_12-${{ hashFiles('mix.lock') }} @@ -118,7 +121,7 @@ jobs: uses: actions/cache@v2 id: deps-cache with: - path: | + path: | deps _build key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_12-${{ hashFiles('mix.lock') }} @@ -141,7 +144,7 @@ jobs: uses: actions/cache@v2 id: deps-cache with: - path: | + path: | deps _build key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_12-${{ hashFiles('mix.lock') }} @@ -181,7 +184,7 @@ jobs: uses: actions/cache@v2 id: deps-cache with: - path: | + path: | deps _build key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_12-${{ hashFiles('mix.lock') }} @@ -207,7 +210,7 @@ jobs: uses: actions/cache@v2 id: deps-cache with: - path: | + path: | deps _build key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_12-${{ hashFiles('mix.lock') }} @@ -235,7 +238,7 @@ jobs: uses: actions/cache@v2 id: deps-cache with: - path: | + path: | deps _build key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_12-${{ hashFiles('mix.lock') }} @@ -281,7 +284,7 @@ jobs: uses: actions/cache@v2 id: deps-cache with: - path: | + path: | deps _build key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_12-${{ hashFiles('mix.lock') }} @@ -340,7 +343,7 @@ jobs: uses: actions/cache@v2 id: deps-cache with: - path: | + path: | deps _build key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_12-${{ hashFiles('mix.lock') }} @@ -396,7 +399,7 @@ jobs: uses: actions/cache@v2 id: deps-cache with: - path: | + path: | deps _build key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_12-${{ hashFiles('mix.lock') }} @@ -463,7 +466,7 @@ jobs: uses: actions/cache@v2 id: deps-cache with: - path: | + path: | deps _build key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_12-${{ hashFiles('mix.lock') }} @@ -493,6 +496,11 @@ jobs: runs-on: ubuntu-latest needs: build-and-cache services: + redis_db: + image: 'redis:alpine' + ports: + - 6379:6379 + postgres: image: postgres env: @@ -524,7 +532,7 @@ jobs: uses: actions/cache@v2 id: deps-cache with: - path: | + path: | deps _build key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_12-${{ hashFiles('mix.lock') }} @@ -571,4 +579,6 @@ jobs: ETHEREUM_JSONRPC_CASE: "EthereumJSONRPC.Case.Parity.Mox" ETHEREUM_JSONRPC_WEB_SOCKET_CASE: "EthereumJSONRPC.WebSocket.Case.Mox" CHAIN_ID: "77" - ADMIN_PANEL_ENABLED: "true" \ No newline at end of file + ADMIN_PANEL_ENABLED: "true" + ACCOUNT_ENABLED: "true" + ACCOUNT_REDIS_URL: "redis://localhost:6379" diff --git a/.gitignore b/.gitignore index a069fb5fd6..2f86dc1f3f 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,9 @@ /*.ez /logs +# mix dialyzer artifacts +/priv/plts + # Generated on crash by the VM erl_crash.dump diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c2dac2b90..9c9ab72b71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ## Current ### Features + +- [#6092](https://github.com/blockscout/blockscout/pull/6092) - Blockscout Account functionality - [#6073](https://github.com/blockscout/blockscout/pull/6073) - Add vyper support for rust verifier microservice integration ### Fixes diff --git a/apps/block_scout_web/API blueprint.md b/apps/block_scout_web/API blueprint.md new file mode 100644 index 0000000000..9cbc20a71d --- /dev/null +++ b/apps/block_scout_web/API blueprint.md @@ -0,0 +1,2228 @@ +FORMAT: 1A +HOST:http://blockscout.com/xdai/testnet +# + + +# API Documentation + + +# Group BlockScoutWeb.Account.Api.V1.UserController +## BlockScoutWeb.Account.Api.V1.UserController [/api/account/v1/user/info] +### BlockScoutWeb.Account.Api.V1.UserController info [GET /api/account/v1/user/info] + + + + + ++ Request Get info about user +**GET**  `/api/account/v1/user/info` + + ++ Response 200 + + + Headers + + set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMTBkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTEzQGJsb2Nrc2NvdXQuY29tZAACaWRh42QABG5hbWVtAAAAC1VzZXIgVGVzdDEwZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjEwZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDEwZAAMd2F0Y2hsaXN0X2lkYeM.d_nsIdBT4zP1sObizRp2ufpZ2-HDGFD1puY3eNSvftY; path=/; HttpOnly + content-type: application/json; charset=utf-8 + cache-control: max-age=0, private, must-revalidate + x-request-id: FxF1gur6Ap5Rc1YAAAYC + access-control-allow-credentials: true + access-control-allow-origin: * + access-control-expose-headers: + + Body + + { + "nickname": "test_user10", + "name": "User Test10", + "email": "test_user-13@blockscout.com", + "avatar": "https://example.com/avatar/test_user10" + } +### BlockScoutWeb.Account.Api.V1.UserController create_tag_address [POST /api/account/v1/user/tags/address] + + + + + ++ Request Add private address tag +**POST**  `/api/account/v1/user/tags/address` + + + Headers + + content-type: multipart/mixed; boundary=plug_conn_test + + Body + + { + "name": "MyName", + "address_hash": "0x3e9ac8f16c92bc4f093357933b5befbf1e16987b" + } + ++ Response 200 + + + Headers + + set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAlaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMmQABWVtYWlsbQAAABp0ZXN0X3VzZXItMkBibG9ja3Njb3V0LmNvbWQAAmlkYdtkAARuYW1lbQAAAApVc2VyIFRlc3QyZAAIbmlja25hbWVtAAAACnRlc3RfdXNlcjJkAAN1aWRtAAAAD2Jsb2Nrc2NvdXR8MDAwMmQADHdhdGNobGlzdF9pZGHb.XPfo6e6fTpCgSOVWcAgze_SHHkf_6UVp-SfOi2EVKcM; path=/; HttpOnly + content-type: application/json; charset=utf-8 + cache-control: max-age=0, private, must-revalidate + x-request-id: FxF1gt7Hha-gjLUAABDh + access-control-allow-credentials: true + access-control-allow-origin: * + access-control-expose-headers: + + Body + + { + "name": "MyName", + "id": 65, + "address_hash": "0x3e9ac8f16c92bc4f093357933b5befbf1e16987b" + } + +# Group BlockScoutWeb.Account.Api.V1.TagsController +## BlockScoutWeb.Account.Api.V1.TagsController [/api/account/v1/tags/address/0x3e9ac8f16c92bc4f093357933b5befbf1e16987b] +### BlockScoutWeb.Account.Api.V1.TagsController tags_address [GET /api/account/v1/tags/address/{address_hash}] + + + + ++ Parameters + + address_hash: `0x3e9ac8f16c92bc4f093357933b5befbf1e16987b` + address_hash: 0x3e9ac8f16c92bc4f093357933b5befbf1e16987b + + ++ Request Get tags for address +**GET**  `/api/account/v1/tags/address/0x3e9ac8f16c92bc4f093357933b5befbf1e16987b` + + ++ Response 200 + + + Headers + + set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAlaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMmQABWVtYWlsbQAAABp0ZXN0X3VzZXItMkBibG9ja3Njb3V0LmNvbWQAAmlkYdtkAARuYW1lbQAAAApVc2VyIFRlc3QyZAAIbmlja25hbWVtAAAACnRlc3RfdXNlcjJkAAN1aWRtAAAAD2Jsb2Nrc2NvdXR8MDAwMmQADHdhdGNobGlzdF9pZGHb.XPfo6e6fTpCgSOVWcAgze_SHHkf_6UVp-SfOi2EVKcM; path=/; HttpOnly + content-type: application/json; charset=utf-8 + cache-control: max-age=0, private, must-revalidate + x-request-id: FxF1gt8j_62gjLUAABFB + access-control-allow-credentials: true + access-control-allow-origin: * + access-control-expose-headers: + + Body + + { + "watchlist_names": [], + "personal_tags": [ + { + "label": "MyName", + "display_name": "MyName", + "address_hash": "0x3e9ac8f16c92bc4f093357933b5befbf1e16987b" + } + ], + "common_tags": [] + } + +# Group BlockScoutWeb.Account.Api.V1.UserController +## BlockScoutWeb.Account.Api.V1.UserController [/api/account/v1/user/tags/address/72] +### BlockScoutWeb.Account.Api.V1.UserController update_tag_address [PUT /api/account/v1/user/tags/address/{id}] + + + + ++ Parameters + + id: `72` + id: 72 + + ++ Request Edit private address tag +**PUT**  `/api/account/v1/user/tags/address/72` + + + Headers + + content-type: multipart/mixed; boundary=plug_conn_test + + Body + + { + "name": "name3", + "address_hash": "0x0000000000000000000000000000000000000054" + } + ++ Response 200 + + + Headers + + set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMTdkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTIxQGJsb2Nrc2NvdXQuY29tZAACaWRh6mQABG5hbWVtAAAAC1VzZXIgVGVzdDE3ZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjE3ZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDE3ZAAMd2F0Y2hsaXN0X2lkYeo.SwNPw9upySrwQX8GCp62J924WYWbJY-WNA31fMLjUas; path=/; HttpOnly + content-type: application/json; charset=utf-8 + cache-control: max-age=0, private, must-revalidate + x-request-id: FxF1gvKquVfUECUAAB4B + access-control-allow-credentials: true + access-control-allow-origin: * + access-control-expose-headers: + + Body + + { + "name": "name3", + "id": 72, + "address_hash": "0x0000000000000000000000000000000000000054" + } +### BlockScoutWeb.Account.Api.V1.UserController tags_address [GET /api/account/v1/user/tags/address] + + + + + ++ Request Get private addresses tags +**GET**  `/api/account/v1/user/tags/address` + + ++ Response 200 + + + Headers + + set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMTFkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTE0QGJsb2Nrc2NvdXQuY29tZAACaWRh5GQABG5hbWVtAAAAC1VzZXIgVGVzdDExZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjExZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDExZAAMd2F0Y2hsaXN0X2lkYeQ.YOpB44xZNsuC9o5OZZQWpH-ijPijlYkT_fApVrfNuhs; path=/; HttpOnly + content-type: application/json; charset=utf-8 + cache-control: max-age=0, private, must-revalidate + x-request-id: FxF1guwn5VVeZtAAABdh + access-control-allow-credentials: true + access-control-allow-origin: * + access-control-expose-headers: + + Body + + [ + { + "name": "name2", + "id": 71, + "address_hash": "0x000000000000000000000000000000000000003a" + }, + { + "name": "name1", + "id": 70, + "address_hash": "0x0000000000000000000000000000000000000039" + }, + { + "name": "name0", + "id": 69, + "address_hash": "0x0000000000000000000000000000000000000038" + } + ] +### BlockScoutWeb.Account.Api.V1.UserController delete_tag_address [DELETE /api/account/v1/user/tags/address/{id}] + + + + ++ Parameters + + id: `66` + id: 66 + + ++ Request Delete private address tag +**DELETE**  `/api/account/v1/user/tags/address/66` + + ++ Response 200 + + + Headers + + set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAlaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyNmQABWVtYWlsbQAAABp0ZXN0X3VzZXItN0BibG9ja3Njb3V0LmNvbWQAAmlkYd9kAARuYW1lbQAAAApVc2VyIFRlc3Q2ZAAIbmlja25hbWVtAAAACnRlc3RfdXNlcjZkAAN1aWRtAAAAD2Jsb2Nrc2NvdXR8MDAwNmQADHdhdGNobGlzdF9pZGHf.2gy24vcTMAaovCIPA7q8PYmlv1ojuZGzgHCkQ6n_W70; path=/; HttpOnly + content-type: application/json; charset=utf-8 + cache-control: max-age=0, private, must-revalidate + x-request-id: FxF1guUM2L0cz9IAABXh + access-control-allow-credentials: true + access-control-allow-origin: * + access-control-expose-headers: + + Body + + { + "message": "OK" + } +### BlockScoutWeb.Account.Api.V1.UserController create_tag_transaction [POST /api/account/v1/user/tags/transaction] + + + + + ++ Request Create private transaction tag +**POST**  `/api/account/v1/user/tags/transaction` + + + Headers + + content-type: multipart/mixed; boundary=plug_conn_test + + Body + + { + "transaction_hash": "0x0000000000000000000000000000000000000000000000000000000000000009", + "name": "MyName" + } + ++ Response 200 + + + Headers + + set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMjFkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTI1QGJsb2Nrc2NvdXQuY29tZAACaWRh7mQABG5hbWVtAAAAC1VzZXIgVGVzdDIxZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjIxZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDIxZAAMd2F0Y2hsaXN0X2lkYe4.OALg_k0K4kFbxlwrk2_wILKz3Ojtx5g-lwqsQWUvTHE; path=/; HttpOnly + content-type: application/json; charset=utf-8 + cache-control: max-age=0, private, must-revalidate + x-request-id: FxF1gvV7jRTkLOwAACCB + access-control-allow-credentials: true + access-control-allow-origin: * + access-control-expose-headers: + + Body + + { + "transaction_hash": "0x0000000000000000000000000000000000000000000000000000000000000009", + "name": "MyName", + "id": 72 + } + + ++ Request Error on try to create private transaction tag for tx does not exist +**POST**  `/api/account/v1/user/tags/transaction` + + + Headers + + content-type: multipart/mixed; boundary=plug_conn_test + + Body + + { + "transaction_hash": "0x0000000000000000000000000000000000000000000000000000000000000008", + "name": "MyName" + } + ++ Response 422 + + + Headers + + set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMjFkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTI1QGJsb2Nrc2NvdXQuY29tZAACaWRh7mQABG5hbWVtAAAAC1VzZXIgVGVzdDIxZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjIxZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDIxZAAMd2F0Y2hsaXN0X2lkYe4.OALg_k0K4kFbxlwrk2_wILKz3Ojtx5g-lwqsQWUvTHE; path=/; HttpOnly + content-type: application/json; charset=utf-8 + cache-control: max-age=0, private, must-revalidate + x-request-id: FxF1gvVV0ZPkLOwAACBh + access-control-allow-credentials: true + access-control-allow-origin: * + access-control-expose-headers: + + Body + + { + "errors": { + "tx_hash": [ + "Transaction does not exist" + ] + } + } + +# Group BlockScoutWeb.Account.Api.V1.TagsController +## BlockScoutWeb.Account.Api.V1.TagsController [/api/account/v1/tags/transaction/0x0000000000000000000000000000000000000000000000000000000000000009] +### BlockScoutWeb.Account.Api.V1.TagsController tags_transaction [GET /api/account/v1/tags/transaction/{transaction_hash}] + + + + ++ Parameters + + transaction_hash: `0x0000000000000000000000000000000000000000000000000000000000000009` + transaction_hash: 0x0000000000000000000000000000000000000000000000000000000000000009 + + ++ Request Get tags for transaction +**GET**  `/api/account/v1/tags/transaction/0x0000000000000000000000000000000000000000000000000000000000000009` + + ++ Response 200 + + + Headers + + set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMjFkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTI1QGJsb2Nrc2NvdXQuY29tZAACaWRh7mQABG5hbWVtAAAAC1VzZXIgVGVzdDIxZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjIxZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDIxZAAMd2F0Y2hsaXN0X2lkYe4.OALg_k0K4kFbxlwrk2_wILKz3Ojtx5g-lwqsQWUvTHE; path=/; HttpOnly + content-type: application/json; charset=utf-8 + cache-control: max-age=0, private, must-revalidate + x-request-id: FxF1gvWZkx3kLOwAACCh + access-control-allow-credentials: true + access-control-allow-origin: * + access-control-expose-headers: + + Body + + { + "watchlist_names": [], + "personal_tx_tag": { + "label": "MyName" + }, + "personal_tags": [], + "common_tags": [] + } + +# Group BlockScoutWeb.Account.Api.V1.UserController +## BlockScoutWeb.Account.Api.V1.UserController [/api/account/v1/user/tags/transaction/65] +### BlockScoutWeb.Account.Api.V1.UserController update_tag_transaction [PUT /api/account/v1/user/tags/transaction/{id}] + + + + ++ Parameters + + id: `65` + id: 65 + + ++ Request Edit private transaction tag +**PUT**  `/api/account/v1/user/tags/transaction/65` + + + Headers + + content-type: multipart/mixed; boundary=plug_conn_test + + Body + + { + "transaction_hash": "0x0000000000000000000000000000000000000000000000000000000000000001", + "name": "name1" + } + ++ Response 200 + + + Headers + + set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAlaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyOGQABWVtYWlsbQAAABp0ZXN0X3VzZXItOUBibG9ja3Njb3V0LmNvbWQAAmlkYeFkAARuYW1lbQAAAApVc2VyIFRlc3Q4ZAAIbmlja25hbWVtAAAACnRlc3RfdXNlcjhkAAN1aWRtAAAAD2Jsb2Nrc2NvdXR8MDAwOGQADHdhdGNobGlzdF9pZGHh.CybEtb6DRCGrUsJ2qnEERIZwD6pRhUfUSwFugOLA9kg; path=/; HttpOnly + content-type: application/json; charset=utf-8 + cache-control: max-age=0, private, must-revalidate + x-request-id: FxF1gunOuMiiGZsAAASi + access-control-allow-credentials: true + access-control-allow-origin: * + access-control-expose-headers: + + Body + + { + "transaction_hash": "0x0000000000000000000000000000000000000000000000000000000000000001", + "name": "name1", + "id": 65 + } +### BlockScoutWeb.Account.Api.V1.UserController tags_transaction [GET /api/account/v1/user/tags/transaction] + + + + + ++ Request Get private transactions tags +**GET**  `/api/account/v1/user/tags/transaction` + + ++ Response 200 + + + Headers + + set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMTRkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTE4QGJsb2Nrc2NvdXQuY29tZAACaWRh52QABG5hbWVtAAAAC1VzZXIgVGVzdDE0ZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjE0ZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDE0ZAAMd2F0Y2hsaXN0X2lkYec.CDHGLjvSgiNStdl55exaXgWiuAWfGw65IX3_vK5h5dU; path=/; HttpOnly + content-type: application/json; charset=utf-8 + cache-control: max-age=0, private, must-revalidate + x-request-id: FxF1gu9MDrtpGp0AABnh + access-control-allow-credentials: true + access-control-allow-origin: * + access-control-expose-headers: + + Body + + [ + { + "transaction_hash": "0x0000000000000000000000000000000000000000000000000000000000000004", + "name": "name2", + "id": 68 + }, + { + "transaction_hash": "0x0000000000000000000000000000000000000000000000000000000000000003", + "name": "name1", + "id": 67 + }, + { + "transaction_hash": "0x0000000000000000000000000000000000000000000000000000000000000002", + "name": "name0", + "id": 66 + } + ] +### BlockScoutWeb.Account.Api.V1.UserController delete_tag_transaction [DELETE /api/account/v1/user/tags/transaction/{id}] + + + + ++ Parameters + + id: `69` + id: 69 + + ++ Request Delete private transaction tag +**DELETE**  `/api/account/v1/user/tags/transaction/69` + + ++ Response 200 + + + Headers + + set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMTZkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTIwQGJsb2Nrc2NvdXQuY29tZAACaWRh6WQABG5hbWVtAAAAC1VzZXIgVGVzdDE2ZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjE2ZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDE2ZAAMd2F0Y2hsaXN0X2lkYek.LsY5H_7VsGeJ-WoDRIReTCTZmPTJNCTjme7ZshEuEpQ; path=/; HttpOnly + content-type: application/json; charset=utf-8 + cache-control: max-age=0, private, must-revalidate + x-request-id: FxF1gvGE13QyfYIAAByB + access-control-allow-credentials: true + access-control-allow-origin: * + access-control-expose-headers: + + Body + + { + "message": "OK" + } +### BlockScoutWeb.Account.Api.V1.UserController create_watchlist [POST /api/account/v1/user/watchlist] + + + + + ++ Request Add address to watch list +**POST**  `/api/account/v1/user/watchlist` + + + Headers + + content-type: multipart/mixed; boundary=plug_conn_test + + Body + + { + "notification_settings": { + "native": { + "outcoming": true, + "incoming": true + }, + "ERC-721": { + "outcoming": true, + "incoming": false + }, + "ERC-20": { + "outcoming": true, + "incoming": true + } + }, + "notification_methods": { + "email": true + }, + "name": "test16", + "address_hash": "0x0000000000000000000000000000000000000011" + } + ++ Response 200 + + + Headers + + set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAlaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyM2QABWVtYWlsbQAAABp0ZXN0X3VzZXItM0BibG9ja3Njb3V0LmNvbWQAAmlkYdxkAARuYW1lbQAAAApVc2VyIFRlc3QzZAAIbmlja25hbWVtAAAACnRlc3RfdXNlcjNkAAN1aWRtAAAAD2Jsb2Nrc2NvdXR8MDAwM2QADHdhdGNobGlzdF9pZGHc.ujumccFj98DtF6Rf_O0i31DGgry0eHmykzCC1xvjVfY; path=/; HttpOnly + content-type: application/json; charset=utf-8 + cache-control: max-age=0, private, must-revalidate + x-request-id: FxF1gt-4UWemyBYAABJB + access-control-allow-credentials: true + access-control-allow-origin: * + access-control-expose-headers: + + Body + + { + "notification_settings": { + "native": { + "outcoming": true, + "incoming": true + }, + "ERC-721": { + "outcoming": true, + "incoming": false + }, + "ERC-20": { + "outcoming": true, + "incoming": true + } + }, + "notification_methods": { + "email": true + }, + "name": "test16", + "id": 75, + "exchange_rate": null, + "address_hash": "0x0000000000000000000000000000000000000011", + "address_balance": null + } +### BlockScoutWeb.Account.Api.V1.UserController watchlist [GET /api/account/v1/user/watchlist] + + + + + ++ Request Get addresses from watchlists +**GET**  `/api/account/v1/user/watchlist` + + ++ Response 200 + + + Headers + + set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAlaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyM2QABWVtYWlsbQAAABp0ZXN0X3VzZXItM0BibG9ja3Njb3V0LmNvbWQAAmlkYdxkAARuYW1lbQAAAApVc2VyIFRlc3QzZAAIbmlja25hbWVtAAAACnRlc3RfdXNlcjNkAAN1aWRtAAAAD2Jsb2Nrc2NvdXR8MDAwM2QADHdhdGNobGlzdF9pZGHc.ujumccFj98DtF6Rf_O0i31DGgry0eHmykzCC1xvjVfY; path=/; HttpOnly + content-type: application/json; charset=utf-8 + cache-control: max-age=0, private, must-revalidate + x-request-id: FxF1guCYRuamyBYAAANj + access-control-allow-credentials: true + access-control-allow-origin: * + access-control-expose-headers: + + Body + + [ + { + "notification_settings": { + "native": { + "outcoming": true, + "incoming": false + }, + "ERC-721": { + "outcoming": true, + "incoming": false + }, + "ERC-20": { + "outcoming": false, + "incoming": true + } + }, + "notification_methods": { + "email": false + }, + "name": "test17", + "id": 76, + "exchange_rate": null, + "address_hash": "0x0000000000000000000000000000000000000012", + "address_balance": null + }, + { + "notification_settings": { + "native": { + "outcoming": true, + "incoming": true + }, + "ERC-721": { + "outcoming": true, + "incoming": false + }, + "ERC-20": { + "outcoming": true, + "incoming": true + } + }, + "notification_methods": { + "email": true + }, + "name": "test16", + "id": 75, + "exchange_rate": null, + "address_hash": "0x0000000000000000000000000000000000000011", + "address_balance": null + } + ] +### BlockScoutWeb.Account.Api.V1.UserController delete_watchlist [DELETE /api/account/v1/user/watchlist/{id}] + + + + ++ Parameters + + id: `82` + id: 82 + + ++ Request Delete address from watchlist by id +**DELETE**  `/api/account/v1/user/watchlist/82` + + ++ Response 200 + + + Headers + + set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMTlkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTIzQGJsb2Nrc2NvdXQuY29tZAACaWRh7GQABG5hbWVtAAAAC1VzZXIgVGVzdDE5ZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjE5ZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDE5ZAAMd2F0Y2hsaXN0X2lkYew.slyWFXgdvd78Pwp3lyrU5tmgCtF7VNIPHxnFkfAQ-YQ; path=/; HttpOnly + content-type: application/json; charset=utf-8 + cache-control: max-age=0, private, must-revalidate + x-request-id: FxF1gvR861_DWHcAAAhC + access-control-allow-credentials: true + access-control-allow-origin: * + access-control-expose-headers: + + Body + + { + "message": "OK" + } +### BlockScoutWeb.Account.Api.V1.UserController update_watchlist [PUT /api/account/v1/user/watchlist/{id}] + + + + ++ Parameters + + id: `80` + id: 80 + + ++ Request Edit watchlist address +**PUT**  `/api/account/v1/user/watchlist/80` + + + Headers + + content-type: multipart/mixed; boundary=plug_conn_test + + Body + + { + "notification_settings": { + "native": { + "outcoming": false, + "incoming": false + }, + "ERC-721": { + "outcoming": true, + "incoming": true + }, + "ERC-20": { + "outcoming": false, + "incoming": false + } + }, + "notification_methods": { + "email": false + }, + "name": "test21", + "address_hash": "0x0000000000000000000000000000000000000023" + } + ++ Response 200 + + + Headers + + set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAlaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyN2QABWVtYWlsbQAAABp0ZXN0X3VzZXItOEBibG9ja3Njb3V0LmNvbWQAAmlkYeBkAARuYW1lbQAAAApVc2VyIFRlc3Q3ZAAIbmlja25hbWVtAAAACnRlc3RfdXNlcjdkAAN1aWRtAAAAD2Jsb2Nrc2NvdXR8MDAwN2QADHdhdGNobGlzdF9pZGHg.2IaE2naK_o4H_guVwcTb0JZIp2hs2c4fvtASxCmIWHM; path=/; HttpOnly + content-type: application/json; charset=utf-8 + cache-control: max-age=0, private, must-revalidate + x-request-id: FxF1gugvkSj5PXEAAANi + access-control-allow-credentials: true + access-control-allow-origin: * + access-control-expose-headers: + + Body + + { + "notification_settings": { + "native": { + "outcoming": false, + "incoming": false + }, + "ERC-721": { + "outcoming": true, + "incoming": true + }, + "ERC-20": { + "outcoming": false, + "incoming": false + } + }, + "notification_methods": { + "email": false + }, + "name": "test21", + "id": 80, + "exchange_rate": null, + "address_hash": "0x0000000000000000000000000000000000000023", + "address_balance": null + } +### BlockScoutWeb.Account.Api.V1.UserController create_watchlist [POST /api/account/v1/user/watchlist] + + + + + ++ Request Example of error on creating watchlist address +**POST**  `/api/account/v1/user/watchlist` + + + Headers + + content-type: multipart/mixed; boundary=plug_conn_test + + Body + + { + "notification_settings": { + "native": { + "outcoming": false, + "incoming": false + }, + "ERC-721": { + "outcoming": true, + "incoming": false + }, + "ERC-20": { + "outcoming": true, + "incoming": true + } + }, + "notification_methods": { + "email": false + }, + "name": "test18", + "address_hash": "0x0000000000000000000000000000000000000013" + } + ++ Response 422 + + + Headers + + set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAlaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyNGQABWVtYWlsbQAAABp0ZXN0X3VzZXItNEBibG9ja3Njb3V0LmNvbWQAAmlkYd1kAARuYW1lbQAAAApVc2VyIFRlc3Q0ZAAIbmlja25hbWVtAAAACnRlc3RfdXNlcjRkAAN1aWRtAAAAD2Jsb2Nrc2NvdXR8MDAwNGQADHdhdGNobGlzdF9pZGHd.jCNAb9dB6WGIZv9wIVL9tpikIPr056ChTYcDeSWdnG4; path=/; HttpOnly + content-type: application/json; charset=utf-8 + cache-control: max-age=0, private, must-revalidate + x-request-id: FxF1guGsUmFGrIUAABMB + access-control-allow-credentials: true + access-control-allow-origin: * + access-control-expose-headers: + + Body + + { + "errors": { + "watchlist_id": [ + "Address already added to the watch list" + ] + } + } +### BlockScoutWeb.Account.Api.V1.UserController update_watchlist [PUT /api/account/v1/user/watchlist/{id}] + + + + ++ Parameters + + id: `79` + id: 79 + + ++ Request Example of error on editing watchlist address +**PUT**  `/api/account/v1/user/watchlist/79` + + + Headers + + content-type: multipart/mixed; boundary=plug_conn_test + + Body + + { + "notification_settings": { + "native": { + "outcoming": false, + "incoming": false + }, + "ERC-721": { + "outcoming": true, + "incoming": false + }, + "ERC-20": { + "outcoming": true, + "incoming": true + } + }, + "notification_methods": { + "email": false + }, + "name": "test18", + "address_hash": "0x0000000000000000000000000000000000000013" + } + ++ Response 422 + + + Headers + + set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAlaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyNGQABWVtYWlsbQAAABp0ZXN0X3VzZXItNEBibG9ja3Njb3V0LmNvbWQAAmlkYd1kAARuYW1lbQAAAApVc2VyIFRlc3Q0ZAAIbmlja25hbWVtAAAACnRlc3RfdXNlcjRkAAN1aWRtAAAAD2Jsb2Nrc2NvdXR8MDAwNGQADHdhdGNobGlzdF9pZGHd.jCNAb9dB6WGIZv9wIVL9tpikIPr056ChTYcDeSWdnG4; path=/; HttpOnly + content-type: application/json; charset=utf-8 + cache-control: max-age=0, private, must-revalidate + x-request-id: FxF1guIKk8ZGrIUAABNB + access-control-allow-credentials: true + access-control-allow-origin: * + access-control-expose-headers: + + Body + + { + "errors": { + "watchlist_id": [ + "Address already added to the watch list" + ] + } + } +### BlockScoutWeb.Account.Api.V1.UserController create_api_key [POST /api/account/v1/user/api_keys] + + + + + ++ Request Add api key +**POST**  `/api/account/v1/user/api_keys` + + + Headers + + content-type: multipart/mixed; boundary=plug_conn_test + + Body + + { + "name": "test" + } + ++ Response 200 + + + Headers + + set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMjBkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTI0QGJsb2Nrc2NvdXQuY29tZAACaWRh7WQABG5hbWVtAAAAC1VzZXIgVGVzdDIwZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjIwZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDIwZAAMd2F0Y2hsaXN0X2lkYe0.hIRgUayy_NKWZARAIxD2-TPy3PaP5kQSHuKGOLxxwz0; path=/; HttpOnly + content-type: application/json; charset=utf-8 + cache-control: max-age=0, private, must-revalidate + x-request-id: FxF1gvTjkbFZ2PwAACBB + access-control-allow-credentials: true + access-control-allow-origin: * + access-control-expose-headers: + + Body + + { + "name": "test", + "api_key": "05b65dfd-0d08-4aa1-b22b-95e3fc8a55e5" + } + + ++ Request Example of error on creating api key +**POST**  `/api/account/v1/user/api_keys` + + + Headers + + content-type: multipart/mixed; boundary=plug_conn_test + + Body + + { + "name": "test" + } + ++ Response 422 + + + Headers + + set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMTVkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTE5QGJsb2Nrc2NvdXQuY29tZAACaWRh6GQABG5hbWVtAAAAC1VzZXIgVGVzdDE1ZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjE1ZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDE1ZAAMd2F0Y2hsaXN0X2lkYeg.M4suuaCnSncg5sgQepwyEGrDqMcSle2BvUjGq5qw0Q8; path=/; HttpOnly + content-type: application/json; charset=utf-8 + cache-control: max-age=0, private, must-revalidate + x-request-id: FxF1gu_KXoEIU2IAABrh + access-control-allow-credentials: true + access-control-allow-origin: * + access-control-expose-headers: + + Body + + { + "errors": { + "name": [ + "Max 3 keys per account" + ] + } + } +### BlockScoutWeb.Account.Api.V1.UserController api_keys [GET /api/account/v1/user/api_keys] + + + + + ++ Request Get api keys list +**GET**  `/api/account/v1/user/api_keys` + + ++ Response 200 + + + Headers + + set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMTVkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTE5QGJsb2Nrc2NvdXQuY29tZAACaWRh6GQABG5hbWVtAAAAC1VzZXIgVGVzdDE1ZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjE1ZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDE1ZAAMd2F0Y2hsaXN0X2lkYeg.M4suuaCnSncg5sgQepwyEGrDqMcSle2BvUjGq5qw0Q8; path=/; HttpOnly + content-type: application/json; charset=utf-8 + cache-control: max-age=0, private, must-revalidate + x-request-id: FxF1gu_ZqjIIU2IAABsB + access-control-allow-credentials: true + access-control-allow-origin: * + access-control-expose-headers: + + Body + + [ + { + "name": "test", + "api_key": "3d07da0e-428e-4410-bc54-43ab544e20f4" + }, + { + "name": "test", + "api_key": "92036fb5-a22a-418d-ac3a-0415e731d55a" + }, + { + "name": "test", + "api_key": "0262ffe5-6d6a-4f79-8444-479e8be85d0e" + } + ] +### BlockScoutWeb.Account.Api.V1.UserController update_api_key [PUT /api/account/v1/user/api_keys/{api_key}] + + + + ++ Parameters + + api_key: `6bcec727-d945-4785-99b6-c6094bbf0452` + api_key: 6bcec727-d945-4785-99b6-c6094bbf0452 + + ++ Request Edit api key +**PUT**  `/api/account/v1/user/api_keys/6bcec727-d945-4785-99b6-c6094bbf0452` + + + Headers + + content-type: multipart/mixed; boundary=plug_conn_test + + Body + + { + "name": "test_1" + } + ++ Response 200 + + + Headers + + set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAlaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMGQABWVtYWlsbQAAABp0ZXN0X3VzZXItMEBibG9ja3Njb3V0LmNvbWQAAmlkYdlkAARuYW1lbQAAAApVc2VyIFRlc3QwZAAIbmlja25hbWVtAAAACnRlc3RfdXNlcjBkAAN1aWRtAAAAD2Jsb2Nrc2NvdXR8MDAwMGQADHdhdGNobGlzdF9pZGHZ.eNhiwGmTdeNAVqQGfVgtac9gGTsoXnysChIBQN75BQk; path=/; HttpOnly + content-type: application/json; charset=utf-8 + cache-control: max-age=0, private, must-revalidate + x-request-id: FxF1gtunEs8BJMYAABCE + access-control-allow-credentials: true + access-control-allow-origin: * + access-control-expose-headers: + + Body + + { + "name": "test_1", + "api_key": "6bcec727-d945-4785-99b6-c6094bbf0452" + } +### BlockScoutWeb.Account.Api.V1.UserController delete_api_key [DELETE /api/account/v1/user/api_keys/{api_key}] + + + + ++ Parameters + + api_key: `0e26955f-5431-4652-84da-d08aded97a28` + api_key: 0e26955f-5431-4652-84da-d08aded97a28 + + ++ Request Delete api key +**DELETE**  `/api/account/v1/user/api_keys/0e26955f-5431-4652-84da-d08aded97a28` + + ++ Response 200 + + + Headers + + set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMThkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTIyQGJsb2Nrc2NvdXQuY29tZAACaWRh62QABG5hbWVtAAAAC1VzZXIgVGVzdDE4ZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjE4ZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDE4ZAAMd2F0Y2hsaXN0X2lkYes.NYp71-Be73f-HTquq2QWWCa70c169Rd9GXDOOSCdC34; path=/; HttpOnly + content-type: application/json; charset=utf-8 + cache-control: max-age=0, private, must-revalidate + x-request-id: FxF1gvMpP3rEvHcAAAei + access-control-allow-credentials: true + access-control-allow-origin: * + access-control-expose-headers: + + Body + + { + "message": "OK" + } +### BlockScoutWeb.Account.Api.V1.UserController create_custom_abi [POST /api/account/v1/user/custom_abis] + + + + + ++ Request Add custom abi +**POST**  `/api/account/v1/user/custom_abis` + + + Headers + + content-type: multipart/mixed; boundary=plug_conn_test + + Body + + { + "name": "test26", + "contract_address_hash": "0x0000000000000000000000000000000000000089", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] + } + ++ Response 200 + + + Headers + + set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMjNkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTM3QGJsb2Nrc2NvdXQuY29tZAACaWRh8GQABG5hbWVtAAAAC1VzZXIgVGVzdDIzZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjIzZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDIzZAAMd2F0Y2hsaXN0X2lkYfA.EgDkDw8R9zBMVjqsTcEWr77klYQVx6QOCcxXyN7EAqg; path=/; HttpOnly + content-type: application/json; charset=utf-8 + cache-control: max-age=0, private, must-revalidate + x-request-id: FxF1gvk62Sj0d-gAAArC + access-control-allow-credentials: true + access-control-allow-origin: * + access-control-expose-headers: + + Body + + { + "name": "test26", + "id": 161, + "contract_address_hash": "0x0000000000000000000000000000000000000089", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] + } + + ++ Request Example of error on creating custom abi +**POST**  `/api/account/v1/user/custom_abis` + + + Headers + + content-type: multipart/mixed; boundary=plug_conn_test + + Body + + { + "name": "test15", + "contract_address_hash": "0x0000000000000000000000000000000000000010", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] + } + ++ Response 422 + + + Headers + + set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAlaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMWQABWVtYWlsbQAAABp0ZXN0X3VzZXItMUBibG9ja3Njb3V0LmNvbWQAAmlkYdpkAARuYW1lbQAAAApVc2VyIFRlc3QxZAAIbmlja25hbWVtAAAACnRlc3RfdXNlcjFkAAN1aWRtAAAAD2Jsb2Nrc2NvdXR8MDAwMWQADHdhdGNobGlzdF9pZGHa.ynGrz6gad7RIkTh1lopco9xXNhiI-y6Bm6ecAnv3Usg; path=/; HttpOnly + content-type: application/json; charset=utf-8 + cache-control: max-age=0, private, must-revalidate + x-request-id: FxF1gt5BIL0fpssAABCB + access-control-allow-credentials: true + access-control-allow-origin: * + access-control-expose-headers: + + Body + + { + "errors": { + "name": [ + "Max 15 ABIs per account" + ] + } + } +### BlockScoutWeb.Account.Api.V1.UserController custom_abis [GET /api/account/v1/user/custom_abis] + + + + + ++ Request Get custom abis list +**GET**  `/api/account/v1/user/custom_abis` + + ++ Response 200 + + + Headers + + set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAlaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMWQABWVtYWlsbQAAABp0ZXN0X3VzZXItMUBibG9ja3Njb3V0LmNvbWQAAmlkYdpkAARuYW1lbQAAAApVc2VyIFRlc3QxZAAIbmlja25hbWVtAAAACnRlc3RfdXNlcjFkAAN1aWRtAAAAD2Jsb2Nrc2NvdXR8MDAwMWQADHdhdGNobGlzdF9pZGHa.ynGrz6gad7RIkTh1lopco9xXNhiI-y6Bm6ecAnv3Usg; path=/; HttpOnly + content-type: application/json; charset=utf-8 + cache-control: max-age=0, private, must-revalidate + x-request-id: FxF1gt5U3pwfpssAABCh + access-control-allow-credentials: true + access-control-allow-origin: * + access-control-expose-headers: + + Body + + [ + { + "name": "test14", + "id": 159, + "contract_address_hash": "0x000000000000000000000000000000000000000f", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] + }, + { + "name": "test13", + "id": 158, + "contract_address_hash": "0x000000000000000000000000000000000000000e", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] + }, + { + "name": "test12", + "id": 157, + "contract_address_hash": "0x000000000000000000000000000000000000000d", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] + }, + { + "name": "test11", + "id": 156, + "contract_address_hash": "0x000000000000000000000000000000000000000c", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] + }, + { + "name": "test10", + "id": 155, + "contract_address_hash": "0x000000000000000000000000000000000000000b", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] + }, + { + "name": "test9", + "id": 154, + "contract_address_hash": "0x000000000000000000000000000000000000000a", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] + }, + { + "name": "test8", + "id": 153, + "contract_address_hash": "0x0000000000000000000000000000000000000009", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] + }, + { + "name": "test7", + "id": 152, + "contract_address_hash": "0x0000000000000000000000000000000000000008", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] + }, + { + "name": "test6", + "id": 151, + "contract_address_hash": "0x0000000000000000000000000000000000000007", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] + }, + { + "name": "test5", + "id": 150, + "contract_address_hash": "0x0000000000000000000000000000000000000006", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] + }, + { + "name": "test4", + "id": 149, + "contract_address_hash": "0x0000000000000000000000000000000000000005", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] + }, + { + "name": "test3", + "id": 148, + "contract_address_hash": "0x0000000000000000000000000000000000000004", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] + }, + { + "name": "test2", + "id": 147, + "contract_address_hash": "0x0000000000000000000000000000000000000003", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] + }, + { + "name": "test1", + "id": 146, + "contract_address_hash": "0x0000000000000000000000000000000000000002", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] + }, + { + "name": "test0", + "id": 145, + "contract_address_hash": "0x0000000000000000000000000000000000000001", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] + } + ] +### BlockScoutWeb.Account.Api.V1.UserController update_custom_abi [PUT /api/account/v1/user/custom_abis/{id}] + + + + ++ Parameters + + id: `160` + id: 160 + + ++ Request Edit custom abi +**PUT**  `/api/account/v1/user/custom_abis/160` + + + Headers + + content-type: multipart/mixed; boundary=plug_conn_test + + Body + + { + "name": "test23", + "contract_address_hash": "0x0000000000000000000000000000000000000046", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] + } + ++ Response 200 + + + Headers + + set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMTNkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTE3QGJsb2Nrc2NvdXQuY29tZAACaWRh5mQABG5hbWVtAAAAC1VzZXIgVGVzdDEzZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjEzZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDEzZAAMd2F0Y2hsaXN0X2lkYeY.sl0nMtxBkMGt3aK7ohM3AYMcNEI-l37Xvqvl9qZ2Tso; path=/; HttpOnly + content-type: application/json; charset=utf-8 + cache-control: max-age=0, private, must-revalidate + x-request-id: FxF1gu0y0bFQlB0AAAbi + access-control-allow-credentials: true + access-control-allow-origin: * + access-control-expose-headers: + + Body + + { + "name": "test23", + "id": 160, + "contract_address_hash": "0x0000000000000000000000000000000000000046", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] + } +### BlockScoutWeb.Account.Api.V1.UserController delete_custom_abi [DELETE /api/account/v1/user/custom_abis/{id}] + + + + ++ Parameters + + id: `162` + id: 162 + + ++ Request Delete custom abi +**DELETE**  `/api/account/v1/user/custom_abis/162` + + ++ Response 200 + + + Headers + + set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMjRkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTM4QGJsb2Nrc2NvdXQuY29tZAACaWRh8WQABG5hbWVtAAAAC1VzZXIgVGVzdDI0ZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjI0ZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDI0ZAAMd2F0Y2hsaXN0X2lkYfE.i0XOrEfBULTfd08Ig4nhy_veB1sWxl2UWYT9kkveABw; path=/; HttpOnly + content-type: application/json; charset=utf-8 + cache-control: max-age=0, private, must-revalidate + x-request-id: FxF1gvnkpEhLN3QAACMB + access-control-allow-credentials: true + access-control-allow-origin: * + access-control-expose-headers: + + Body + + { + "message": "OK" + } +### BlockScoutWeb.Account.Api.V1.UserController create_public_tags_request [POST /api/account/v1/user/public_tags] + + + + + ++ Request Submit request to add a public tag +**POST**  `/api/account/v1/user/public_tags` + + + Headers + + content-type: multipart/mixed; boundary=plug_conn_test + + Body + + { + "website": "website3", + "tags": "Tag5;Tag6", + "is_owner": false, + "full_name": "full name3", + "email": "test_user-16@blockscout.com", + "company": "company3", + "addresses": [ + "0x000000000000000000000000000000000000003b", + "0x000000000000000000000000000000000000003c", + "0x000000000000000000000000000000000000003d", + "0x000000000000000000000000000000000000003e", + "0x000000000000000000000000000000000000003f", + "0x0000000000000000000000000000000000000040", + "0x0000000000000000000000000000000000000041", + "0x0000000000000000000000000000000000000042", + "0x0000000000000000000000000000000000000043", + "0x0000000000000000000000000000000000000044" + ], + "additional_comment": "additional_comment3" + } + ++ Response 200 + + + Headers + + set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMTJkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTE1QGJsb2Nrc2NvdXQuY29tZAACaWRh5WQABG5hbWVtAAAAC1VzZXIgVGVzdDEyZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjEyZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDEyZAAMd2F0Y2hsaXN0X2lkYeU.8B0VERlCeTBlp1w0Zys_ZGaVIKj0VYi6pV2wMnCjeac; path=/; HttpOnly + content-type: application/json; charset=utf-8 + cache-control: max-age=0, private, must-revalidate + x-request-id: FxF1guxmyw_F-rUAAATj + access-control-allow-credentials: true + access-control-allow-origin: * + access-control-expose-headers: + + Body + + { + "website": "website3", + "tags": "Tag5;Tag6", + "submission_date": "2022-09-03T21:02:22.651943Z", + "is_owner": false, + "id": 146, + "full_name": "full name3", + "email": "test_user-16@blockscout.com", + "company": "company3", + "addresses": [ + "0x000000000000000000000000000000000000003b", + "0x000000000000000000000000000000000000003c", + "0x000000000000000000000000000000000000003d", + "0x000000000000000000000000000000000000003e", + "0x000000000000000000000000000000000000003f", + "0x0000000000000000000000000000000000000040", + "0x0000000000000000000000000000000000000041", + "0x0000000000000000000000000000000000000042", + "0x0000000000000000000000000000000000000043", + "0x0000000000000000000000000000000000000044" + ], + "additional_comment": "additional_comment3" + } +### BlockScoutWeb.Account.Api.V1.UserController public_tags_requests [GET /api/account/v1/user/public_tags] + + + + + ++ Request Get list of requests to add a public tag +**GET**  `/api/account/v1/user/public_tags` + + ++ Response 200 + + + Headers + + set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMjJkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTI2QGJsb2Nrc2NvdXQuY29tZAACaWRh72QABG5hbWVtAAAAC1VzZXIgVGVzdDIyZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjIyZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDIyZAAMd2F0Y2hsaXN0X2lkYe8.oZY96LW6ZLfw1aK-C5TYkrK2GRNQEJCapnUSkd5OjXU; path=/; HttpOnly + content-type: application/json; charset=utf-8 + cache-control: max-age=0, private, must-revalidate + x-request-id: FxF1gvdQvQ8r6iIAAAki + access-control-allow-credentials: true + access-control-allow-origin: * + access-control-expose-headers: + + Body + + [ + { + "website": "website13", + "tags": "Tag18;Tag19", + "submission_date": "2022-09-03T21:02:23.000000Z", + "is_owner": false, + "id": 156, + "full_name": "full name13", + "email": "test_user-36@blockscout.com", + "company": "company13", + "addresses": [ + "0x0000000000000000000000000000000000000084", + "0x0000000000000000000000000000000000000085", + "0x0000000000000000000000000000000000000086", + "0x0000000000000000000000000000000000000087", + "0x0000000000000000000000000000000000000088" + ], + "additional_comment": "additional_comment13" + }, + { + "website": "website12", + "tags": "Tag17", + "submission_date": "2022-09-03T21:02:23.000000Z", + "is_owner": true, + "id": 155, + "full_name": "full name12", + "email": "test_user-35@blockscout.com", + "company": "company12", + "addresses": [ + "0x0000000000000000000000000000000000000083" + ], + "additional_comment": "additional_comment12" + }, + { + "website": "website11", + "tags": "Tag16", + "submission_date": "2022-09-03T21:02:23.000000Z", + "is_owner": false, + "id": 154, + "full_name": "full name11", + "email": "test_user-34@blockscout.com", + "company": "company11", + "addresses": [ + "0x000000000000000000000000000000000000007b", + "0x000000000000000000000000000000000000007c", + "0x000000000000000000000000000000000000007d", + "0x000000000000000000000000000000000000007e", + "0x000000000000000000000000000000000000007f", + "0x0000000000000000000000000000000000000080", + "0x0000000000000000000000000000000000000081", + "0x0000000000000000000000000000000000000082" + ], + "additional_comment": "additional_comment11" + }, + { + "website": "website10", + "tags": "Tag15", + "submission_date": "2022-09-03T21:02:23.000000Z", + "is_owner": false, + "id": 153, + "full_name": "full name10", + "email": "test_user-33@blockscout.com", + "company": "company10", + "addresses": [ + "0x0000000000000000000000000000000000000073", + "0x0000000000000000000000000000000000000074", + "0x0000000000000000000000000000000000000075", + "0x0000000000000000000000000000000000000076", + "0x0000000000000000000000000000000000000077", + "0x0000000000000000000000000000000000000078", + "0x0000000000000000000000000000000000000079", + "0x000000000000000000000000000000000000007a" + ], + "additional_comment": "additional_comment10" + }, + { + "website": "website9", + "tags": "Tag14", + "submission_date": "2022-09-03T21:02:23.000000Z", + "is_owner": false, + "id": 152, + "full_name": "full name9", + "email": "test_user-32@blockscout.com", + "company": "company9", + "addresses": [ + "0x000000000000000000000000000000000000006d", + "0x000000000000000000000000000000000000006e", + "0x000000000000000000000000000000000000006f", + "0x0000000000000000000000000000000000000070", + "0x0000000000000000000000000000000000000071", + "0x0000000000000000000000000000000000000072" + ], + "additional_comment": "additional_comment9" + }, + { + "website": "website8", + "tags": "Tag13", + "submission_date": "2022-09-03T21:02:23.000000Z", + "is_owner": false, + "id": 151, + "full_name": "full name8", + "email": "test_user-31@blockscout.com", + "company": "company8", + "addresses": [ + "0x0000000000000000000000000000000000000064", + "0x0000000000000000000000000000000000000065", + "0x0000000000000000000000000000000000000066", + "0x0000000000000000000000000000000000000067", + "0x0000000000000000000000000000000000000068", + "0x0000000000000000000000000000000000000069", + "0x000000000000000000000000000000000000006a", + "0x000000000000000000000000000000000000006b", + "0x000000000000000000000000000000000000006c" + ], + "additional_comment": "additional_comment8" + }, + { + "website": "website7", + "tags": "Tag11;Tag12", + "submission_date": "2022-09-03T21:02:23.000000Z", + "is_owner": true, + "id": 150, + "full_name": "full name7", + "email": "test_user-30@blockscout.com", + "company": "company7", + "addresses": [ + "0x0000000000000000000000000000000000000063" + ], + "additional_comment": "additional_comment7" + }, + { + "website": "website6", + "tags": "Tag9;Tag10", + "submission_date": "2022-09-03T21:02:23.000000Z", + "is_owner": false, + "id": 149, + "full_name": "full name6", + "email": "test_user-29@blockscout.com", + "company": "company6", + "addresses": [ + "0x0000000000000000000000000000000000000060", + "0x0000000000000000000000000000000000000061", + "0x0000000000000000000000000000000000000062" + ], + "additional_comment": "additional_comment6" + }, + { + "website": "website5", + "tags": "Tag8", + "submission_date": "2022-09-03T21:02:23.000000Z", + "is_owner": true, + "id": 148, + "full_name": "full name5", + "email": "test_user-28@blockscout.com", + "company": "company5", + "addresses": [ + "0x000000000000000000000000000000000000005e", + "0x000000000000000000000000000000000000005f" + ], + "additional_comment": "additional_comment5" + }, + { + "website": "website4", + "tags": "Tag7", + "submission_date": "2022-09-03T21:02:23.000000Z", + "is_owner": false, + "id": 147, + "full_name": "full name4", + "email": "test_user-27@blockscout.com", + "company": "company4", + "addresses": [ + "0x000000000000000000000000000000000000005b", + "0x000000000000000000000000000000000000005c", + "0x000000000000000000000000000000000000005d" + ], + "additional_comment": "additional_comment4" + } + ] +### BlockScoutWeb.Account.Api.V1.UserController delete_public_tags_request [DELETE /api/account/v1/user/public_tags/{id}] + + + + ++ Parameters + + id: `156` + id: 156 + + ++ Request Delete public tags request +**DELETE**  `/api/account/v1/user/public_tags/156` + + + Headers + + content-type: multipart/mixed; boundary=plug_conn_test + + Body + + { + "remove_reason": "reason" + } + ++ Response 200 + + + Headers + + set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMjJkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTI2QGJsb2Nrc2NvdXQuY29tZAACaWRh72QABG5hbWVtAAAAC1VzZXIgVGVzdDIyZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjIyZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDIyZAAMd2F0Y2hsaXN0X2lkYe8.oZY96LW6ZLfw1aK-C5TYkrK2GRNQEJCapnUSkd5OjXU; path=/; HttpOnly + content-type: application/json; charset=utf-8 + cache-control: max-age=0, private, must-revalidate + x-request-id: FxF1gvdm8H0r6iIAAAlC + access-control-allow-credentials: true + access-control-allow-origin: * + access-control-expose-headers: + + Body + + { + "message": "OK" + } +### BlockScoutWeb.Account.Api.V1.UserController update_public_tags_request [PUT /api/account/v1/user/public_tags/{id}] + + + + ++ Parameters + + id: `145` + id: 145 + + ++ Request Edit request to add a public tag +**PUT**  `/api/account/v1/user/public_tags/145` + + + Headers + + content-type: multipart/mixed; boundary=plug_conn_test + + Body + + { + "website": "website2", + "tags": "Tag3;Tag4", + "is_owner": false, + "full_name": "full name2", + "email": "test_user-12@blockscout.com", + "company": "company2", + "addresses": [ + "0x000000000000000000000000000000000000002f", + "0x0000000000000000000000000000000000000030", + "0x0000000000000000000000000000000000000031", + "0x0000000000000000000000000000000000000032", + "0x0000000000000000000000000000000000000033", + "0x0000000000000000000000000000000000000034", + "0x0000000000000000000000000000000000000035", + "0x0000000000000000000000000000000000000036", + "0x0000000000000000000000000000000000000037" + ], + "additional_comment": "additional_comment2" + } + ++ Response 200 + + + Headers + + set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAlaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyOWQABWVtYWlsbQAAABt0ZXN0X3VzZXItMTBAYmxvY2tzY291dC5jb21kAAJpZGHiZAAEbmFtZW0AAAAKVXNlciBUZXN0OWQACG5pY2tuYW1lbQAAAAp0ZXN0X3VzZXI5ZAADdWlkbQAAAA9ibG9ja3Njb3V0fDAwMDlkAAx3YXRjaGxpc3RfaWRh4g.cM2caeO_bvTyojrTAKD7Tt4WEPEIsHwTMmWkTEVgSLo; path=/; HttpOnly + content-type: application/json; charset=utf-8 + cache-control: max-age=0, private, must-revalidate + x-request-id: FxF1guqVaODqqc8AAAUi + access-control-allow-credentials: true + access-control-allow-origin: * + access-control-expose-headers: + + Body + + { + "website": "website2", + "tags": "Tag3;Tag4", + "submission_date": "2022-09-03T21:02:23.000000Z", + "is_owner": false, + "id": 145, + "full_name": "full name2", + "email": "test_user-12@blockscout.com", + "company": "company2", + "addresses": [ + "0x000000000000000000000000000000000000002f", + "0x0000000000000000000000000000000000000030", + "0x0000000000000000000000000000000000000031", + "0x0000000000000000000000000000000000000032", + "0x0000000000000000000000000000000000000033", + "0x0000000000000000000000000000000000000034", + "0x0000000000000000000000000000000000000035", + "0x0000000000000000000000000000000000000036", + "0x0000000000000000000000000000000000000037" + ], + "additional_comment": "additional_comment2" + } + diff --git a/apps/block_scout_web/API.md b/apps/block_scout_web/API.md new file mode 100644 index 0000000000..afc68c19e6 --- /dev/null +++ b/apps/block_scout_web/API.md @@ -0,0 +1,2227 @@ +# API Documentation + + * [BlockScoutWeb.Account.Api.V1.UserController](#blockscoutweb-account-api-v1-usercontroller) + * [info](#blockscoutweb-account-api-v1-usercontroller-info) + * [create_tag_address](#blockscoutweb-account-api-v1-usercontroller-create_tag_address) + * [BlockScoutWeb.Account.Api.V1.TagsController](#blockscoutweb-account-api-v1-tagscontroller) + * [tags_address](#blockscoutweb-account-api-v1-tagscontroller-tags_address) + * [BlockScoutWeb.Account.Api.V1.UserController](#blockscoutweb-account-api-v1-usercontroller) + * [update_tag_address](#blockscoutweb-account-api-v1-usercontroller-update_tag_address) + * [tags_address](#blockscoutweb-account-api-v1-usercontroller-tags_address) + * [delete_tag_address](#blockscoutweb-account-api-v1-usercontroller-delete_tag_address) + * [create_tag_transaction](#blockscoutweb-account-api-v1-usercontroller-create_tag_transaction) + * [BlockScoutWeb.Account.Api.V1.TagsController](#blockscoutweb-account-api-v1-tagscontroller) + * [tags_transaction](#blockscoutweb-account-api-v1-tagscontroller-tags_transaction) + * [BlockScoutWeb.Account.Api.V1.UserController](#blockscoutweb-account-api-v1-usercontroller) + * [update_tag_transaction](#blockscoutweb-account-api-v1-usercontroller-update_tag_transaction) + * [tags_transaction](#blockscoutweb-account-api-v1-usercontroller-tags_transaction) + * [delete_tag_transaction](#blockscoutweb-account-api-v1-usercontroller-delete_tag_transaction) + * [create_watchlist](#blockscoutweb-account-api-v1-usercontroller-create_watchlist) + * [watchlist](#blockscoutweb-account-api-v1-usercontroller-watchlist) + * [delete_watchlist](#blockscoutweb-account-api-v1-usercontroller-delete_watchlist) + * [update_watchlist](#blockscoutweb-account-api-v1-usercontroller-update_watchlist) + * [create_watchlist](#blockscoutweb-account-api-v1-usercontroller-create_watchlist) + * [update_watchlist](#blockscoutweb-account-api-v1-usercontroller-update_watchlist) + * [create_api_key](#blockscoutweb-account-api-v1-usercontroller-create_api_key) + * [api_keys](#blockscoutweb-account-api-v1-usercontroller-api_keys) + * [update_api_key](#blockscoutweb-account-api-v1-usercontroller-update_api_key) + * [delete_api_key](#blockscoutweb-account-api-v1-usercontroller-delete_api_key) + * [create_custom_abi](#blockscoutweb-account-api-v1-usercontroller-create_custom_abi) + * [custom_abis](#blockscoutweb-account-api-v1-usercontroller-custom_abis) + * [update_custom_abi](#blockscoutweb-account-api-v1-usercontroller-update_custom_abi) + * [delete_custom_abi](#blockscoutweb-account-api-v1-usercontroller-delete_custom_abi) + * [create_public_tags_request](#blockscoutweb-account-api-v1-usercontroller-create_public_tags_request) + * [public_tags_requests](#blockscoutweb-account-api-v1-usercontroller-public_tags_requests) + * [delete_public_tags_request](#blockscoutweb-account-api-v1-usercontroller-delete_public_tags_request) + * [update_public_tags_request](#blockscoutweb-account-api-v1-usercontroller-update_public_tags_request) + +## BlockScoutWeb.Account.Api.V1.UserController +### info +#### Get info about user + +##### Request +* __Method:__ GET +* __Path:__ /api/account/v1/user/info + +##### Response +* __Status__: 200 +* __Response headers:__ +``` +set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAlaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyNGQABWVtYWlsbQAAABp0ZXN0X3VzZXItNEBibG9ja3Njb3V0LmNvbWQAAmlkYcRkAARuYW1lbQAAAApVc2VyIFRlc3Q0ZAAIbmlja25hbWVtAAAACnRlc3RfdXNlcjRkAAN1aWRtAAAAD2Jsb2Nrc2NvdXR8MDAwNGQADHdhdGNobGlzdF9pZGHE.Ovcc2Vzzv4fhFzmirtQjJ06gcqQwUHMMlju7VX24fyo; path=/; HttpOnly +content-type: application/json; charset=utf-8 +cache-control: max-age=0, private, must-revalidate +x-request-id: FxF1Y1_QfU9-YaIAAGdh +access-control-allow-credentials: true +access-control-allow-origin: * +access-control-expose-headers: +``` +* __Response body:__ +```json +{ + "nickname": "test_user4", + "name": "User Test4", + "email": "test_user-4@blockscout.com", + "avatar": "https://example.com/avatar/test_user4" +} +``` + +### create_tag_address +#### Add private address tag + +##### Request +* __Method:__ POST +* __Path:__ /api/account/v1/user/tags/address +* __Request headers:__ +``` +content-type: multipart/mixed; boundary=plug_conn_test +``` +* __Request body:__ +```json +{ + "name": "MyName", + "address_hash": "0x3e9ac8f16c92bc4f093357933b5befbf1e16987b" +} +``` + +##### Response +* __Status__: 200 +* __Response headers:__ +``` +set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMThkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTIyQGJsb2Nrc2NvdXQuY29tZAACaWRh0mQABG5hbWVtAAAAC1VzZXIgVGVzdDE4ZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjE4ZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDE4ZAAMd2F0Y2hsaXN0X2lkYdI.tFFJ387fBBdBFuMzzeaWcMTeapzMHnbuEfnqTdq5lJ8; path=/; HttpOnly +content-type: application/json; charset=utf-8 +cache-control: max-age=0, private, must-revalidate +x-request-id: FxF1Y3ALw8xSCMAAAHAC +access-control-allow-credentials: true +access-control-allow-origin: * +access-control-expose-headers: +``` +* __Response body:__ +```json +{ + "name": "MyName", + "id": 61, + "address_hash": "0x3e9ac8f16c92bc4f093357933b5befbf1e16987b" +} +``` + +## BlockScoutWeb.Account.Api.V1.TagsController +### tags_address +#### Get tags for address + +##### Request +* __Method:__ GET +* __Path:__ /api/account/v1/tags/address/0x3e9ac8f16c92bc4f093357933b5befbf1e16987b + +##### Response +* __Status__: 200 +* __Response headers:__ +``` +set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMThkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTIyQGJsb2Nrc2NvdXQuY29tZAACaWRh0mQABG5hbWVtAAAAC1VzZXIgVGVzdDE4ZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjE4ZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDE4ZAAMd2F0Y2hsaXN0X2lkYdI.tFFJ387fBBdBFuMzzeaWcMTeapzMHnbuEfnqTdq5lJ8; path=/; HttpOnly +content-type: application/json; charset=utf-8 +cache-control: max-age=0, private, must-revalidate +x-request-id: FxF1Y3BIWjdSCMAAAG4B +access-control-allow-credentials: true +access-control-allow-origin: * +access-control-expose-headers: +``` +* __Response body:__ +```json +{ + "watchlist_names": [], + "personal_tags": [ + { + "label": "MyName", + "display_name": "MyName", + "address_hash": "0x3e9ac8f16c92bc4f093357933b5befbf1e16987b" + } + ], + "common_tags": [] +} +``` + +## BlockScoutWeb.Account.Api.V1.UserController +### update_tag_address +#### Edit private address tag + +##### Request +* __Method:__ PUT +* __Path:__ /api/account/v1/user/tags/address/57 +* __Request headers:__ +``` +content-type: multipart/mixed; boundary=plug_conn_test +``` +* __Request body:__ +```json +{ + "name": "name3", + "address_hash": "0x0000000000000000000000000000000000000016" +} +``` + +##### Response +* __Status__: 200 +* __Response headers:__ +``` +set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAlaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyN2QABWVtYWlsbQAAABt0ZXN0X3VzZXItMTBAYmxvY2tzY291dC5jb21kAAJpZGHHZAAEbmFtZW0AAAAKVXNlciBUZXN0N2QACG5pY2tuYW1lbQAAAAp0ZXN0X3VzZXI3ZAADdWlkbQAAAA9ibG9ja3Njb3V0fDAwMDdkAAx3YXRjaGxpc3RfaWRhxw.Bn03yTZrlP0m6amYLQVeI-pvhvUf1F6d9SGAkDTLEck; path=/; HttpOnly +content-type: application/json; charset=utf-8 +cache-control: max-age=0, private, must-revalidate +x-request-id: FxF1Y2IdgOjzsTkAAGYC +access-control-allow-credentials: true +access-control-allow-origin: * +access-control-expose-headers: +``` +* __Response body:__ +```json +{ + "name": "name3", + "id": 57, + "address_hash": "0x0000000000000000000000000000000000000016" +} +``` + +### tags_address +#### Get private addresses tags + +##### Request +* __Method:__ GET +* __Path:__ /api/account/v1/user/tags/address + +##### Response +* __Status__: 200 +* __Response headers:__ +``` +set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMTVkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTE5QGJsb2Nrc2NvdXQuY29tZAACaWRhz2QABG5hbWVtAAAAC1VzZXIgVGVzdDE1ZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjE1ZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDE1ZAAMd2F0Y2hsaXN0X2lkYc8.AoYBq7uUH9JOt11vL4-71qtsXMzpPDFsx8BV97n1Y-o; path=/; HttpOnly +content-type: application/json; charset=utf-8 +cache-control: max-age=0, private, must-revalidate +x-request-id: FxF1Y2ynKDFWAsYAAG5C +access-control-allow-credentials: true +access-control-allow-origin: * +access-control-expose-headers: +``` +* __Response body:__ +```json +[ + { + "name": "name2", + "id": 60, + "address_hash": "0x000000000000000000000000000000000000003f" + }, + { + "name": "name1", + "id": 59, + "address_hash": "0x000000000000000000000000000000000000003e" + }, + { + "name": "name0", + "id": 58, + "address_hash": "0x000000000000000000000000000000000000003d" + } +] +``` + +### delete_tag_address +#### Delete private address tag + +##### Request +* __Method:__ DELETE +* __Path:__ /api/account/v1/user/tags/address/62 + +##### Response +* __Status__: 200 +* __Response headers:__ +``` +set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMjRkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTM4QGJsb2Nrc2NvdXQuY29tZAACaWRh2GQABG5hbWVtAAAAC1VzZXIgVGVzdDI0ZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjI0ZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDI0ZAAMd2F0Y2hsaXN0X2lkYdg.x6Qf5zC5gCGQrKy2MbTqd3Xt7S_2oUYaCnO-pbZwRMI; path=/; HttpOnly +content-type: application/json; charset=utf-8 +cache-control: max-age=0, private, must-revalidate +x-request-id: FxF1Y3biZmVZE0MAAHKC +access-control-allow-credentials: true +access-control-allow-origin: * +access-control-expose-headers: +``` +* __Response body:__ +```json +{ + "message": "OK" +} +``` + +### create_tag_transaction +#### Error on try to create private transaction tag for tx does not exist + +##### Request +* __Method:__ POST +* __Path:__ /api/account/v1/user/tags/transaction +* __Request headers:__ +``` +content-type: multipart/mixed; boundary=plug_conn_test +``` +* __Request body:__ +```json +{ + "transaction_hash": "0x0000000000000000000000000000000000000000000000000000000000000008", + "name": "MyName" +} +``` + +##### Response +* __Status__: 422 +* __Response headers:__ +``` +set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMTlkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTIzQGJsb2Nrc2NvdXQuY29tZAACaWRh02QABG5hbWVtAAAAC1VzZXIgVGVzdDE5ZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjE5ZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDE5ZAAMd2F0Y2hsaXN0X2lkYdM.zuwR-sOIcF7Xpo97W6G9Szzi_BPlu6Pu9_4kn7T2c10; path=/; HttpOnly +content-type: application/json; charset=utf-8 +cache-control: max-age=0, private, must-revalidate +x-request-id: FxF1Y3DXWVBu-HUAAG6h +access-control-allow-credentials: true +access-control-allow-origin: * +access-control-expose-headers: +``` +* __Response body:__ +```json +{ + "errors": { + "tx_hash": [ + "Transaction does not exist" + ] + } +} +``` + +#### Create private transaction tag + +##### Request +* __Method:__ POST +* __Path:__ /api/account/v1/user/tags/transaction +* __Request headers:__ +``` +content-type: multipart/mixed; boundary=plug_conn_test +``` +* __Request body:__ +```json +{ + "transaction_hash": "0x0000000000000000000000000000000000000000000000000000000000000009", + "name": "MyName" +} +``` + +##### Response +* __Status__: 200 +* __Response headers:__ +``` +set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMTlkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTIzQGJsb2Nrc2NvdXQuY29tZAACaWRh02QABG5hbWVtAAAAC1VzZXIgVGVzdDE5ZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjE5ZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDE5ZAAMd2F0Y2hsaXN0X2lkYdM.zuwR-sOIcF7Xpo97W6G9Szzi_BPlu6Pu9_4kn7T2c10; path=/; HttpOnly +content-type: application/json; charset=utf-8 +cache-control: max-age=0, private, must-revalidate +x-request-id: FxF1Y3EB0Ytu-HUAAG7B +access-control-allow-credentials: true +access-control-allow-origin: * +access-control-expose-headers: +``` +* __Response body:__ +```json +{ + "transaction_hash": "0x0000000000000000000000000000000000000000000000000000000000000009", + "name": "MyName", + "id": 64 +} +``` + +## BlockScoutWeb.Account.Api.V1.TagsController +### tags_transaction +#### Get tags for transaction + +##### Request +* __Method:__ GET +* __Path:__ /api/account/v1/tags/transaction/0x0000000000000000000000000000000000000000000000000000000000000009 + +##### Response +* __Status__: 200 +* __Response headers:__ +``` +set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMTlkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTIzQGJsb2Nrc2NvdXQuY29tZAACaWRh02QABG5hbWVtAAAAC1VzZXIgVGVzdDE5ZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjE5ZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDE5ZAAMd2F0Y2hsaXN0X2lkYdM.zuwR-sOIcF7Xpo97W6G9Szzi_BPlu6Pu9_4kn7T2c10; path=/; HttpOnly +content-type: application/json; charset=utf-8 +cache-control: max-age=0, private, must-revalidate +x-request-id: FxF1Y3Efe0tu-HUAAG7h +access-control-allow-credentials: true +access-control-allow-origin: * +access-control-expose-headers: +``` +* __Response body:__ +```json +{ + "watchlist_names": [], + "personal_tx_tag": { + "label": "MyName" + }, + "personal_tags": [], + "common_tags": [] +} +``` + +## BlockScoutWeb.Account.Api.V1.UserController +### update_tag_transaction +#### Edit private transaction tag + +##### Request +* __Method:__ PUT +* __Path:__ /api/account/v1/user/tags/transaction/57 +* __Request headers:__ +``` +content-type: multipart/mixed; boundary=plug_conn_test +``` +* __Request body:__ +```json +{ + "transaction_hash": "0x0000000000000000000000000000000000000000000000000000000000000001", + "name": "name1" +} +``` + +##### Response +* __Status__: 200 +* __Response headers:__ +``` +set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAlaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMGQABWVtYWlsbQAAABp0ZXN0X3VzZXItMEBibG9ja3Njb3V0LmNvbWQAAmlkYcBkAARuYW1lbQAAAApVc2VyIFRlc3QwZAAIbmlja25hbWVtAAAACnRlc3RfdXNlcjBkAAN1aWRtAAAAD2Jsb2Nrc2NvdXR8MDAwMGQADHdhdGNobGlzdF9pZGHA.-aMP6TTEeEfxopoeChJPvTvjkSRD9_ZgaeLDlOC21gU; path=/; HttpOnly +content-type: application/json; charset=utf-8 +cache-control: max-age=0, private, must-revalidate +x-request-id: FxF1Y1xoENHeIlkAAGEi +access-control-allow-credentials: true +access-control-allow-origin: * +access-control-expose-headers: +``` +* __Response body:__ +```json +{ + "transaction_hash": "0x0000000000000000000000000000000000000000000000000000000000000001", + "name": "name1", + "id": 57 +} +``` + +### tags_transaction +#### Get private transactions tags + +##### Request +* __Method:__ GET +* __Path:__ /api/account/v1/user/tags/transaction + +##### Response +* __Status__: 200 +* __Response headers:__ +``` +set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMTRkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTE4QGJsb2Nrc2NvdXQuY29tZAACaWRhzmQABG5hbWVtAAAAC1VzZXIgVGVzdDE0ZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjE0ZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDE0ZAAMd2F0Y2hsaXN0X2lkYc4.8SGhlMOY4aB444Afz1VajofmGp9YZbrfbVkZ4BTyaBI; path=/; HttpOnly +content-type: application/json; charset=utf-8 +cache-control: max-age=0, private, must-revalidate +x-request-id: FxF1Y2tEsVp5P30AAGzi +access-control-allow-credentials: true +access-control-allow-origin: * +access-control-expose-headers: +``` +* __Response body:__ +```json +[ + { + "transaction_hash": "0x0000000000000000000000000000000000000000000000000000000000000004", + "name": "name2", + "id": 60 + }, + { + "transaction_hash": "0x0000000000000000000000000000000000000000000000000000000000000003", + "name": "name1", + "id": 59 + }, + { + "transaction_hash": "0x0000000000000000000000000000000000000000000000000000000000000002", + "name": "name0", + "id": 58 + } +] +``` + +### delete_tag_transaction +#### Delete private transaction tag + +##### Request +* __Method:__ DELETE +* __Path:__ /api/account/v1/user/tags/transaction/61 + +##### Response +* __Status__: 200 +* __Response headers:__ +``` +set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMTZkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTIwQGJsb2Nrc2NvdXQuY29tZAACaWRh0GQABG5hbWVtAAAAC1VzZXIgVGVzdDE2ZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjE2ZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDE2ZAAMd2F0Y2hsaXN0X2lkYdA.YfL9L7-UIBleRbWWhHNvutNuw8Y4SadvwGFmGwakxQA; path=/; HttpOnly +content-type: application/json; charset=utf-8 +cache-control: max-age=0, private, must-revalidate +x-request-id: FxF1Y26c9UuC4TcAAGwh +access-control-allow-credentials: true +access-control-allow-origin: * +access-control-expose-headers: +``` +* __Response body:__ +```json +{ + "message": "OK" +} +``` + +### create_watchlist +#### Add address to watch list + +##### Request +* __Method:__ POST +* __Path:__ /api/account/v1/user/watchlist +* __Request headers:__ +``` +content-type: multipart/mixed; boundary=plug_conn_test +``` +* __Request body:__ +```json +{ + "notification_settings": { + "native": { + "outcoming": false, + "incoming": true + }, + "ERC-721": { + "outcoming": false, + "incoming": true + }, + "ERC-20": { + "outcoming": false, + "incoming": false + } + }, + "notification_methods": { + "email": true + }, + "name": "test2", + "address_hash": "0x0000000000000000000000000000000000000007" +} +``` + +##### Response +* __Status__: 200 +* __Response headers:__ +``` +set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAlaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyM2QABWVtYWlsbQAAABp0ZXN0X3VzZXItM0BibG9ja3Njb3V0LmNvbWQAAmlkYcNkAARuYW1lbQAAAApVc2VyIFRlc3QzZAAIbmlja25hbWVtAAAACnRlc3RfdXNlcjNkAAN1aWRtAAAAD2Jsb2Nrc2NvdXR8MDAwM2QADHdhdGNobGlzdF9pZGHD.kv5nnz8sVGLaopoZs9ppOfu0hfpFi58yuisPDN6PtPI; path=/; HttpOnly +content-type: application/json; charset=utf-8 +cache-control: max-age=0, private, must-revalidate +x-request-id: FxF1Y16Kv_0GzWcAAGKi +access-control-allow-credentials: true +access-control-allow-origin: * +access-control-expose-headers: +``` +* __Response body:__ +```json +{ + "notification_settings": { + "native": { + "outcoming": false, + "incoming": true + }, + "ERC-721": { + "outcoming": false, + "incoming": true + }, + "ERC-20": { + "outcoming": false, + "incoming": false + } + }, + "notification_methods": { + "email": true + }, + "name": "test2", + "id": 68, + "exchange_rate": null, + "address_hash": "0x0000000000000000000000000000000000000007", + "address_balance": null +} +``` + +### watchlist +#### Get addresses from watchlists + +##### Request +* __Method:__ GET +* __Path:__ /api/account/v1/user/watchlist + +##### Response +* __Status__: 200 +* __Response headers:__ +``` +set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAlaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyM2QABWVtYWlsbQAAABp0ZXN0X3VzZXItM0BibG9ja3Njb3V0LmNvbWQAAmlkYcNkAARuYW1lbQAAAApVc2VyIFRlc3QzZAAIbmlja25hbWVtAAAACnRlc3RfdXNlcjNkAAN1aWRtAAAAD2Jsb2Nrc2NvdXR8MDAwM2QADHdhdGNobGlzdF9pZGHD.kv5nnz8sVGLaopoZs9ppOfu0hfpFi58yuisPDN6PtPI; path=/; HttpOnly +content-type: application/json; charset=utf-8 +cache-control: max-age=0, private, must-revalidate +x-request-id: FxF1Y19FyIUGzWcAAGMC +access-control-allow-credentials: true +access-control-allow-origin: * +access-control-expose-headers: +``` +* __Response body:__ +```json +[ + { + "notification_settings": { + "native": { + "outcoming": false, + "incoming": false + }, + "ERC-721": { + "outcoming": true, + "incoming": false + }, + "ERC-20": { + "outcoming": true, + "incoming": false + } + }, + "notification_methods": { + "email": false + }, + "name": "test3", + "id": 69, + "exchange_rate": null, + "address_hash": "0x0000000000000000000000000000000000000008", + "address_balance": null + }, + { + "notification_settings": { + "native": { + "outcoming": false, + "incoming": true + }, + "ERC-721": { + "outcoming": false, + "incoming": true + }, + "ERC-20": { + "outcoming": false, + "incoming": false + } + }, + "notification_methods": { + "email": true + }, + "name": "test2", + "id": 68, + "exchange_rate": null, + "address_hash": "0x0000000000000000000000000000000000000007", + "address_balance": null + } +] +``` + +### delete_watchlist +#### Delete address from watchlist by id + +##### Request +* __Method:__ DELETE +* __Path:__ /api/account/v1/user/watchlist/74 + +##### Response +* __Status__: 200 +* __Response headers:__ +``` +set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMTFkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTE0QGJsb2Nrc2NvdXQuY29tZAACaWRhy2QABG5hbWVtAAAAC1VzZXIgVGVzdDExZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjExZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDExZAAMd2F0Y2hsaXN0X2lkYcs.YjW8nzuA66id0ADg2qpyjTMGfKJ7BHhjU_HdVq8w8vk; path=/; HttpOnly +content-type: application/json; charset=utf-8 +cache-control: max-age=0, private, must-revalidate +x-request-id: FxF1Y2f5j2WpY30AAGuC +access-control-allow-credentials: true +access-control-allow-origin: * +access-control-expose-headers: +``` +* __Response body:__ +```json +{ + "message": "OK" +} +``` + +### update_watchlist +#### Edit watchlist address + +##### Request +* __Method:__ PUT +* __Path:__ /api/account/v1/user/watchlist/67 +* __Request headers:__ +``` +content-type: multipart/mixed; boundary=plug_conn_test +``` +* __Request body:__ +```json +{ + "notification_settings": { + "native": { + "outcoming": false, + "incoming": true + }, + "ERC-721": { + "outcoming": true, + "incoming": true + }, + "ERC-20": { + "outcoming": true, + "incoming": true + } + }, + "notification_methods": { + "email": true + }, + "name": "test1", + "address_hash": "0x0000000000000000000000000000000000000006" +} +``` + +##### Response +* __Status__: 200 +* __Response headers:__ +``` +set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAlaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMWQABWVtYWlsbQAAABp0ZXN0X3VzZXItMUBibG9ja3Njb3V0LmNvbWQAAmlkYcFkAARuYW1lbQAAAApVc2VyIFRlc3QxZAAIbmlja25hbWVtAAAACnRlc3RfdXNlcjFkAAN1aWRtAAAAD2Jsb2Nrc2NvdXR8MDAwMWQADHdhdGNobGlzdF9pZGHB.3KOkZkPrcMrRXfooQckn-zi6xmax1LJMBGBSjmGM8ww; path=/; HttpOnly +content-type: application/json; charset=utf-8 +cache-control: max-age=0, private, must-revalidate +x-request-id: FxF1Y12FoNKu97sAAGch +access-control-allow-credentials: true +access-control-allow-origin: * +access-control-expose-headers: +``` +* __Response body:__ +```json +{ + "notification_settings": { + "native": { + "outcoming": false, + "incoming": true + }, + "ERC-721": { + "outcoming": true, + "incoming": true + }, + "ERC-20": { + "outcoming": true, + "incoming": true + } + }, + "notification_methods": { + "email": true + }, + "name": "test1", + "id": 67, + "exchange_rate": null, + "address_hash": "0x0000000000000000000000000000000000000006", + "address_balance": null +} +``` + +### create_watchlist +#### Example of error on creating watchlist address + +##### Request +* __Method:__ POST +* __Path:__ /api/account/v1/user/watchlist +* __Request headers:__ +``` +content-type: multipart/mixed; boundary=plug_conn_test +``` +* __Request body:__ +```json +{ + "notification_settings": { + "native": { + "outcoming": false, + "incoming": true + }, + "ERC-721": { + "outcoming": false, + "incoming": false + }, + "ERC-20": { + "outcoming": true, + "incoming": false + } + }, + "notification_methods": { + "email": false + }, + "name": "test4", + "address_hash": "0x0000000000000000000000000000000000000017" +} +``` + +##### Response +* __Status__: 422 +* __Response headers:__ +``` +set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAlaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyOGQABWVtYWlsbQAAABt0ZXN0X3VzZXItMTFAYmxvY2tzY291dC5jb21kAAJpZGHIZAAEbmFtZW0AAAAKVXNlciBUZXN0OGQACG5pY2tuYW1lbQAAAAp0ZXN0X3VzZXI4ZAADdWlkbQAAAA9ibG9ja3Njb3V0fDAwMDhkAAx3YXRjaGxpc3RfaWRhyA.q1Rmte0qLd31GbmpA46bE8rXo2okwzX8aD_oDHn8CIQ; path=/; HttpOnly +content-type: application/json; charset=utf-8 +cache-control: max-age=0, private, must-revalidate +x-request-id: FxF1Y2MCqHvooPMAAGbi +access-control-allow-credentials: true +access-control-allow-origin: * +access-control-expose-headers: +``` +* __Response body:__ +```json +{ + "errors": { + "watchlist_id": [ + "Address already added to the watch list" + ] + } +} +``` + +### update_watchlist +#### Example of error on editing watchlist address + +##### Request +* __Method:__ PUT +* __Path:__ /api/account/v1/user/watchlist/72 +* __Request headers:__ +``` +content-type: multipart/mixed; boundary=plug_conn_test +``` +* __Request body:__ +```json +{ + "notification_settings": { + "native": { + "outcoming": false, + "incoming": true + }, + "ERC-721": { + "outcoming": false, + "incoming": false + }, + "ERC-20": { + "outcoming": true, + "incoming": false + } + }, + "notification_methods": { + "email": false + }, + "name": "test4", + "address_hash": "0x0000000000000000000000000000000000000017" +} +``` + +##### Response +* __Status__: 422 +* __Response headers:__ +``` +set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAlaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyOGQABWVtYWlsbQAAABt0ZXN0X3VzZXItMTFAYmxvY2tzY291dC5jb21kAAJpZGHIZAAEbmFtZW0AAAAKVXNlciBUZXN0OGQACG5pY2tuYW1lbQAAAAp0ZXN0X3VzZXI4ZAADdWlkbQAAAA9ibG9ja3Njb3V0fDAwMDhkAAx3YXRjaGxpc3RfaWRhyA.q1Rmte0qLd31GbmpA46bE8rXo2okwzX8aD_oDHn8CIQ; path=/; HttpOnly +content-type: application/json; charset=utf-8 +cache-control: max-age=0, private, must-revalidate +x-request-id: FxF1Y2Nh1eHooPMAAGci +access-control-allow-credentials: true +access-control-allow-origin: * +access-control-expose-headers: +``` +* __Response body:__ +```json +{ + "errors": { + "watchlist_id": [ + "Address already added to the watch list" + ] + } +} +``` + +### create_api_key +#### Add api key + +##### Request +* __Method:__ POST +* __Path:__ /api/account/v1/user/api_keys +* __Request headers:__ +``` +content-type: multipart/mixed; boundary=plug_conn_test +``` +* __Request body:__ +```json +{ + "name": "test" +} +``` + +##### Response +* __Status__: 200 +* __Response headers:__ +``` +set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAlaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMmQABWVtYWlsbQAAABp0ZXN0X3VzZXItMkBibG9ja3Njb3V0LmNvbWQAAmlkYcJkAARuYW1lbQAAAApVc2VyIFRlc3QyZAAIbmlja25hbWVtAAAACnRlc3RfdXNlcjJkAAN1aWRtAAAAD2Jsb2Nrc2NvdXR8MDAwMmQADHdhdGNobGlzdF9pZGHC.ULESD1_sOySz8eEVGnagUzGw6eMIx_8Pwoyr_5S3K0M; path=/; HttpOnly +content-type: application/json; charset=utf-8 +cache-control: max-age=0, private, must-revalidate +x-request-id: FxF1Y14XlMBqXaQAAGHi +access-control-allow-credentials: true +access-control-allow-origin: * +access-control-expose-headers: +``` +* __Response body:__ +```json +{ + "name": "test", + "api_key": "de9ef457-3f47-48d3-affa-79ad9d3b27b9" +} +``` + +#### Example of error on creating api key + +##### Request +* __Method:__ POST +* __Path:__ /api/account/v1/user/api_keys +* __Request headers:__ +``` +content-type: multipart/mixed; boundary=plug_conn_test +``` +* __Request body:__ +```json +{ + "name": "test" +} +``` + +##### Response +* __Status__: 422 +* __Response headers:__ +``` +set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMjJkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTI2QGJsb2Nrc2NvdXQuY29tZAACaWRh1mQABG5hbWVtAAAAC1VzZXIgVGVzdDIyZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjIyZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDIyZAAMd2F0Y2hsaXN0X2lkYdY.P37J2lZZdHaT4P-RatVaXCx77UcSH3s_TMx-FieaYk0; path=/; HttpOnly +content-type: application/json; charset=utf-8 +cache-control: max-age=0, private, must-revalidate +x-request-id: FxF1Y3LmuuofZKYAAG_h +access-control-allow-credentials: true +access-control-allow-origin: * +access-control-expose-headers: +``` +* __Response body:__ +```json +{ + "errors": { + "name": [ + "Max 3 keys per account" + ] + } +} +``` + +### api_keys +#### Get api keys list + +##### Request +* __Method:__ GET +* __Path:__ /api/account/v1/user/api_keys + +##### Response +* __Status__: 200 +* __Response headers:__ +``` +set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMjJkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTI2QGJsb2Nrc2NvdXQuY29tZAACaWRh1mQABG5hbWVtAAAAC1VzZXIgVGVzdDIyZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjIyZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDIyZAAMd2F0Y2hsaXN0X2lkYdY.P37J2lZZdHaT4P-RatVaXCx77UcSH3s_TMx-FieaYk0; path=/; HttpOnly +content-type: application/json; charset=utf-8 +cache-control: max-age=0, private, must-revalidate +x-request-id: FxF1Y3LyOSIfZKYAAHAB +access-control-allow-credentials: true +access-control-allow-origin: * +access-control-expose-headers: +``` +* __Response body:__ +```json +[ + { + "name": "test", + "api_key": "2ac16688-34e6-4fa4-8983-a9bc34c912f6" + }, + { + "name": "test", + "api_key": "a55426db-04f0-40be-a146-1ced4558aa0c" + }, + { + "name": "test", + "api_key": "d73fc23b-59f0-4e6f-a739-f4de30995101" + } +] +``` + +### update_api_key +#### Edit api key + +##### Request +* __Method:__ PUT +* __Path:__ /api/account/v1/user/api_keys/2b1d400d-713e-4bfc-8ef0-710555693138 +* __Request headers:__ +``` +content-type: multipart/mixed; boundary=plug_conn_test +``` +* __Request body:__ +```json +{ + "name": "test_1" +} +``` + +##### Response +* __Status__: 200 +* __Response headers:__ +``` +set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMTdkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTIxQGJsb2Nrc2NvdXQuY29tZAACaWRh0WQABG5hbWVtAAAAC1VzZXIgVGVzdDE3ZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjE3ZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDE3ZAAMd2F0Y2hsaXN0X2lkYdE.bLJKM3-kFm04mMC-4-3b2mjrig_lmQYt5C2tg-9q9so; path=/; HttpOnly +content-type: application/json; charset=utf-8 +cache-control: max-age=0, private, must-revalidate +x-request-id: FxF1Y2-0eR7T2BMAAG0B +access-control-allow-credentials: true +access-control-allow-origin: * +access-control-expose-headers: +``` +* __Response body:__ +```json +{ + "name": "test_1", + "api_key": "2b1d400d-713e-4bfc-8ef0-710555693138" +} +``` + +### delete_api_key +#### Delete api key + +##### Request +* __Method:__ DELETE +* __Path:__ /api/account/v1/user/api_keys/3bd44c0d-290f-4dfc-9283-5f674080f8ef + +##### Response +* __Status__: 200 +* __Response headers:__ +``` +set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMjBkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTI0QGJsb2Nrc2NvdXQuY29tZAACaWRh1GQABG5hbWVtAAAAC1VzZXIgVGVzdDIwZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjIwZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDIwZAAMd2F0Y2hsaXN0X2lkYdQ.WgjMmOxwwBGcTZZscpLA8EXErwL8ITCvoIXPLIQAhtw; path=/; HttpOnly +content-type: application/json; charset=utf-8 +cache-control: max-age=0, private, must-revalidate +x-request-id: FxF1Y3HQdpa0710AAHBi +access-control-allow-credentials: true +access-control-allow-origin: * +access-control-expose-headers: +``` +* __Response body:__ +```json +{ + "message": "OK" +} +``` + +### create_custom_abi +#### Add custom abi + +##### Request +* __Method:__ POST +* __Path:__ /api/account/v1/user/custom_abis +* __Request headers:__ +``` +content-type: multipart/mixed; boundary=plug_conn_test +``` +* __Request body:__ +```json +{ + "name": "test25", + "contract_address_hash": "0x000000000000000000000000000000000000002c", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] +} +``` + +##### Response +* __Status__: 200 +* __Response headers:__ +``` +set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMTJkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTE1QGJsb2Nrc2NvdXQuY29tZAACaWRhzGQABG5hbWVtAAAAC1VzZXIgVGVzdDEyZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjEyZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDEyZAAMd2F0Y2hsaXN0X2lkYcw.7cCOt6SVrOb5VLYplBzwZ03FWMo9jQpAV7cNroY4txY; path=/; HttpOnly +content-type: application/json; charset=utf-8 +cache-control: max-age=0, private, must-revalidate +x-request-id: FxF1Y2iZJWbZgfgAAGwC +access-control-allow-credentials: true +access-control-allow-origin: * +access-control-expose-headers: +``` +* __Response body:__ +```json +{ + "name": "test25", + "id": 143, + "contract_address_hash": "0x000000000000000000000000000000000000002c", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] +} +``` + +#### Example of error on creating custom abi + +##### Request +* __Method:__ POST +* __Path:__ /api/account/v1/user/custom_abis +* __Request headers:__ +``` +content-type: multipart/mixed; boundary=plug_conn_test +``` +* __Request body:__ +```json +{ + "name": "test21", + "contract_address_hash": "0x0000000000000000000000000000000000000028", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] +} +``` + +##### Response +* __Status__: 422 +* __Response headers:__ +``` +set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAlaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyOWQABWVtYWlsbQAAABt0ZXN0X3VzZXItMTJAYmxvY2tzY291dC5jb21kAAJpZGHJZAAEbmFtZW0AAAAKVXNlciBUZXN0OWQACG5pY2tuYW1lbQAAAAp0ZXN0X3VzZXI5ZAADdWlkbQAAAA9ibG9ja3Njb3V0fDAwMDlkAAx3YXRjaGxpc3RfaWRhyQ.MCpJsS-nb95ccHRtzOk7DbIRjEcTG34ONq4PrC5hOcU; path=/; HttpOnly +content-type: application/json; charset=utf-8 +cache-control: max-age=0, private, must-revalidate +x-request-id: FxF1Y2Ypm-ny0swAAGiB +access-control-allow-credentials: true +access-control-allow-origin: * +access-control-expose-headers: +``` +* __Response body:__ +```json +{ + "errors": { + "name": [ + "Max 15 ABIs per account" + ] + } +} +``` + +### custom_abis +#### Get custom abis list + +##### Request +* __Method:__ GET +* __Path:__ /api/account/v1/user/custom_abis + +##### Response +* __Status__: 200 +* __Response headers:__ +``` +set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAlaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyOWQABWVtYWlsbQAAABt0ZXN0X3VzZXItMTJAYmxvY2tzY291dC5jb21kAAJpZGHJZAAEbmFtZW0AAAAKVXNlciBUZXN0OWQACG5pY2tuYW1lbQAAAAp0ZXN0X3VzZXI5ZAADdWlkbQAAAA9ibG9ja3Njb3V0fDAwMDlkAAx3YXRjaGxpc3RfaWRhyQ.MCpJsS-nb95ccHRtzOk7DbIRjEcTG34ONq4PrC5hOcU; path=/; HttpOnly +content-type: application/json; charset=utf-8 +cache-control: max-age=0, private, must-revalidate +x-request-id: FxF1Y2Y-qjXy0swAAGnC +access-control-allow-credentials: true +access-control-allow-origin: * +access-control-expose-headers: +``` +* __Response body:__ +```json +[ + { + "name": "test20", + "id": 141, + "contract_address_hash": "0x0000000000000000000000000000000000000027", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] + }, + { + "name": "test19", + "id": 140, + "contract_address_hash": "0x0000000000000000000000000000000000000026", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] + }, + { + "name": "test18", + "id": 139, + "contract_address_hash": "0x0000000000000000000000000000000000000025", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] + }, + { + "name": "test17", + "id": 138, + "contract_address_hash": "0x0000000000000000000000000000000000000024", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] + }, + { + "name": "test16", + "id": 137, + "contract_address_hash": "0x0000000000000000000000000000000000000023", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] + }, + { + "name": "test15", + "id": 136, + "contract_address_hash": "0x0000000000000000000000000000000000000022", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] + }, + { + "name": "test14", + "id": 135, + "contract_address_hash": "0x0000000000000000000000000000000000000021", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] + }, + { + "name": "test13", + "id": 134, + "contract_address_hash": "0x0000000000000000000000000000000000000020", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] + }, + { + "name": "test12", + "id": 133, + "contract_address_hash": "0x000000000000000000000000000000000000001f", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] + }, + { + "name": "test11", + "id": 132, + "contract_address_hash": "0x000000000000000000000000000000000000001e", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] + }, + { + "name": "test10", + "id": 131, + "contract_address_hash": "0x000000000000000000000000000000000000001d", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] + }, + { + "name": "test9", + "id": 130, + "contract_address_hash": "0x000000000000000000000000000000000000001c", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] + }, + { + "name": "test8", + "id": 129, + "contract_address_hash": "0x000000000000000000000000000000000000001b", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] + }, + { + "name": "test7", + "id": 128, + "contract_address_hash": "0x000000000000000000000000000000000000001a", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] + }, + { + "name": "test6", + "id": 127, + "contract_address_hash": "0x0000000000000000000000000000000000000019", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] + } +] +``` + +### update_custom_abi +#### Edit custom abi + +##### Request +* __Method:__ PUT +* __Path:__ /api/account/v1/user/custom_abis/144 +* __Request headers:__ +``` +content-type: multipart/mixed; boundary=plug_conn_test +``` +* __Request body:__ +```json +{ + "name": "test27", + "contract_address_hash": "0x000000000000000000000000000000000000004b", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] +} +``` + +##### Response +* __Status__: 200 +* __Response headers:__ +``` +set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMjFkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTI1QGJsb2Nrc2NvdXQuY29tZAACaWRh1WQABG5hbWVtAAAAC1VzZXIgVGVzdDIxZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjIxZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDIxZAAMd2F0Y2hsaXN0X2lkYdU.SEUqq9ZiSD79HIzwKvwTspmBKKU87m_Xwu5gw2pX1e0; path=/; HttpOnly +content-type: application/json; charset=utf-8 +cache-control: max-age=0, private, must-revalidate +x-request-id: FxF1Y3JcHmB4X2AAAHDC +access-control-allow-credentials: true +access-control-allow-origin: * +access-control-expose-headers: +``` +* __Response body:__ +```json +{ + "name": "test27", + "id": 144, + "contract_address_hash": "0x000000000000000000000000000000000000004b", + "abi": [ + { + "type": "function", + "stateMutability": "nonpayable", + "payable": false, + "outputs": [], + "name": "set", + "inputs": [ + { + "type": "uint256", + "name": "x" + } + ], + "constant": false + }, + { + "type": "function", + "stateMutability": "view", + "payable": false, + "outputs": [ + { + "type": "uint256", + "name": "" + } + ], + "name": "get", + "inputs": [], + "constant": true + } + ] +} +``` + +### delete_custom_abi +#### Delete custom abi + +##### Request +* __Method:__ DELETE +* __Path:__ /api/account/v1/user/custom_abis/142 + +##### Response +* __Status__: 200 +* __Response headers:__ +``` +set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMTBkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTEzQGJsb2Nrc2NvdXQuY29tZAACaWRhymQABG5hbWVtAAAAC1VzZXIgVGVzdDEwZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjEwZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDEwZAAMd2F0Y2hsaXN0X2lkYco.x_6dmEjpZ1o8_ct-M7pWWP0LkI66xhwl8gWeQt9XzHA; path=/; HttpOnly +content-type: application/json; charset=utf-8 +cache-control: max-age=0, private, must-revalidate +x-request-id: FxF1Y2b1jJGBaO4AAGrC +access-control-allow-credentials: true +access-control-allow-origin: * +access-control-expose-headers: +``` +* __Response body:__ +```json +{ + "message": "OK" +} +``` + +### create_public_tags_request +#### Submit request to add a public tag + +##### Request +* __Method:__ POST +* __Path:__ /api/account/v1/user/public_tags +* __Request headers:__ +``` +content-type: multipart/mixed; boundary=plug_conn_test +``` +* __Request body:__ +```json +{ + "website": "website0", + "tags": "Tag0", + "is_owner": true, + "full_name": "full name0", + "email": "test_user-6@blockscout.com", + "company": "company0", + "addresses": [ + "0x0000000000000000000000000000000000000009", + "0x000000000000000000000000000000000000000a", + "0x000000000000000000000000000000000000000b", + "0x000000000000000000000000000000000000000c", + "0x000000000000000000000000000000000000000d" + ], + "additional_comment": "additional_comment0" +} +``` + +##### Response +* __Status__: 200 +* __Response headers:__ +``` +set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAlaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyNWQABWVtYWlsbQAAABp0ZXN0X3VzZXItNUBibG9ja3Njb3V0LmNvbWQAAmlkYcVkAARuYW1lbQAAAApVc2VyIFRlc3Q1ZAAIbmlja25hbWVtAAAACnRlc3RfdXNlcjVkAAN1aWRtAAAAD2Jsb2Nrc2NvdXR8MDAwNWQADHdhdGNobGlzdF9pZGHF.kXAMBaL9a7aYjPDgZ9Llxe1etUCPH3vEvQe9Fq2May4; path=/; HttpOnly +content-type: application/json; charset=utf-8 +cache-control: max-age=0, private, must-revalidate +x-request-id: FxF1Y2BIESA-ecUAAGgB +access-control-allow-credentials: true +access-control-allow-origin: * +access-control-expose-headers: +``` +* __Response body:__ +```json +{ + "website": "website0", + "tags": "Tag0", + "submission_date": "2022-09-03T21:00:07.156465Z", + "is_owner": true, + "id": 131, + "full_name": "full name0", + "email": "test_user-6@blockscout.com", + "company": "company0", + "addresses": [ + "0x0000000000000000000000000000000000000009", + "0x000000000000000000000000000000000000000a", + "0x000000000000000000000000000000000000000b", + "0x000000000000000000000000000000000000000c", + "0x000000000000000000000000000000000000000d" + ], + "additional_comment": "additional_comment0" +} +``` + +### public_tags_requests +#### Get list of requests to add a public tag + +##### Request +* __Method:__ GET +* __Path:__ /api/account/v1/user/public_tags + +##### Response +* __Status__: 200 +* __Response headers:__ +``` +set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMjNkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTI3QGJsb2Nrc2NvdXQuY29tZAACaWRh12QABG5hbWVtAAAAC1VzZXIgVGVzdDIzZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjIzZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDIzZAAMd2F0Y2hsaXN0X2lkYdc._6gJnvzjA6VEztgoIdpp7chhmhsdFrJImlcdrp4-pW0; path=/; HttpOnly +content-type: application/json; charset=utf-8 +cache-control: max-age=0, private, must-revalidate +x-request-id: FxF1Y3SaPVCdkicAAHIi +access-control-allow-credentials: true +access-control-allow-origin: * +access-control-expose-headers: +``` +* __Response body:__ +```json +[ + { + "website": "website13", + "tags": "Tag17", + "submission_date": "2022-09-03T21:00:07.000000Z", + "is_owner": false, + "id": 143, + "full_name": "full name13", + "email": "test_user-37@blockscout.com", + "company": "company13", + "addresses": [ + "0x000000000000000000000000000000000000007e", + "0x000000000000000000000000000000000000007f", + "0x0000000000000000000000000000000000000080", + "0x0000000000000000000000000000000000000081", + "0x0000000000000000000000000000000000000082", + "0x0000000000000000000000000000000000000083", + "0x0000000000000000000000000000000000000084" + ], + "additional_comment": "additional_comment13" + }, + { + "website": "website12", + "tags": "Tag16", + "submission_date": "2022-09-03T21:00:07.000000Z", + "is_owner": false, + "id": 142, + "full_name": "full name12", + "email": "test_user-36@blockscout.com", + "company": "company12", + "addresses": [ + "0x0000000000000000000000000000000000000075", + "0x0000000000000000000000000000000000000076", + "0x0000000000000000000000000000000000000077", + "0x0000000000000000000000000000000000000078", + "0x0000000000000000000000000000000000000079", + "0x000000000000000000000000000000000000007a", + "0x000000000000000000000000000000000000007b", + "0x000000000000000000000000000000000000007c", + "0x000000000000000000000000000000000000007d" + ], + "additional_comment": "additional_comment12" + }, + { + "website": "website11", + "tags": "Tag15", + "submission_date": "2022-09-03T21:00:07.000000Z", + "is_owner": false, + "id": 141, + "full_name": "full name11", + "email": "test_user-35@blockscout.com", + "company": "company11", + "addresses": [ + "0x000000000000000000000000000000000000006d", + "0x000000000000000000000000000000000000006e", + "0x000000000000000000000000000000000000006f", + "0x0000000000000000000000000000000000000070", + "0x0000000000000000000000000000000000000071", + "0x0000000000000000000000000000000000000072", + "0x0000000000000000000000000000000000000073", + "0x0000000000000000000000000000000000000074" + ], + "additional_comment": "additional_comment11" + }, + { + "website": "website10", + "tags": "Tag14", + "submission_date": "2022-09-03T21:00:07.000000Z", + "is_owner": false, + "id": 140, + "full_name": "full name10", + "email": "test_user-34@blockscout.com", + "company": "company10", + "addresses": [ + "0x0000000000000000000000000000000000000067", + "0x0000000000000000000000000000000000000068", + "0x0000000000000000000000000000000000000069", + "0x000000000000000000000000000000000000006a", + "0x000000000000000000000000000000000000006b", + "0x000000000000000000000000000000000000006c" + ], + "additional_comment": "additional_comment10" + }, + { + "website": "website9", + "tags": "Tag13", + "submission_date": "2022-09-03T21:00:07.000000Z", + "is_owner": true, + "id": 139, + "full_name": "full name9", + "email": "test_user-33@blockscout.com", + "company": "company9", + "addresses": [ + "0x0000000000000000000000000000000000000061", + "0x0000000000000000000000000000000000000062", + "0x0000000000000000000000000000000000000063", + "0x0000000000000000000000000000000000000064", + "0x0000000000000000000000000000000000000065", + "0x0000000000000000000000000000000000000066" + ], + "additional_comment": "additional_comment9" + }, + { + "website": "website8", + "tags": "Tag12", + "submission_date": "2022-09-03T21:00:07.000000Z", + "is_owner": false, + "id": 138, + "full_name": "full name8", + "email": "test_user-32@blockscout.com", + "company": "company8", + "addresses": [ + "0x0000000000000000000000000000000000000060" + ], + "additional_comment": "additional_comment8" + }, + { + "website": "website7", + "tags": "Tag11", + "submission_date": "2022-09-03T21:00:07.000000Z", + "is_owner": true, + "id": 137, + "full_name": "full name7", + "email": "test_user-31@blockscout.com", + "company": "company7", + "addresses": [ + "0x000000000000000000000000000000000000005f" + ], + "additional_comment": "additional_comment7" + }, + { + "website": "website6", + "tags": "Tag9;Tag10", + "submission_date": "2022-09-03T21:00:07.000000Z", + "is_owner": true, + "id": 136, + "full_name": "full name6", + "email": "test_user-30@blockscout.com", + "company": "company6", + "addresses": [ + "0x000000000000000000000000000000000000005a", + "0x000000000000000000000000000000000000005b", + "0x000000000000000000000000000000000000005c", + "0x000000000000000000000000000000000000005d", + "0x000000000000000000000000000000000000005e" + ], + "additional_comment": "additional_comment6" + }, + { + "website": "website5", + "tags": "Tag8", + "submission_date": "2022-09-03T21:00:07.000000Z", + "is_owner": false, + "id": 135, + "full_name": "full name5", + "email": "test_user-29@blockscout.com", + "company": "company5", + "addresses": [ + "0x0000000000000000000000000000000000000051", + "0x0000000000000000000000000000000000000052", + "0x0000000000000000000000000000000000000053", + "0x0000000000000000000000000000000000000054", + "0x0000000000000000000000000000000000000055", + "0x0000000000000000000000000000000000000056", + "0x0000000000000000000000000000000000000057", + "0x0000000000000000000000000000000000000058", + "0x0000000000000000000000000000000000000059" + ], + "additional_comment": "additional_comment5" + }, + { + "website": "website4", + "tags": "Tag6;Tag7", + "submission_date": "2022-09-03T21:00:07.000000Z", + "is_owner": true, + "id": 134, + "full_name": "full name4", + "email": "test_user-28@blockscout.com", + "company": "company4", + "addresses": [ + "0x000000000000000000000000000000000000004c", + "0x000000000000000000000000000000000000004d", + "0x000000000000000000000000000000000000004e", + "0x000000000000000000000000000000000000004f", + "0x0000000000000000000000000000000000000050" + ], + "additional_comment": "additional_comment4" + } +] +``` + +### delete_public_tags_request +#### Delete public tags request + +##### Request +* __Method:__ DELETE +* __Path:__ /api/account/v1/user/public_tags/143 +* __Request headers:__ +``` +content-type: multipart/mixed; boundary=plug_conn_test +``` +* __Request body:__ +```json +{ + "remove_reason": "reason" +} +``` + +##### Response +* __Status__: 200 +* __Response headers:__ +``` +set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMjNkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTI3QGJsb2Nrc2NvdXQuY29tZAACaWRh12QABG5hbWVtAAAAC1VzZXIgVGVzdDIzZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjIzZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDIzZAAMd2F0Y2hsaXN0X2lkYdc._6gJnvzjA6VEztgoIdpp7chhmhsdFrJImlcdrp4-pW0; path=/; HttpOnly +content-type: application/json; charset=utf-8 +cache-control: max-age=0, private, must-revalidate +x-request-id: FxF1Y3SwObudkicAAHBB +access-control-allow-credentials: true +access-control-allow-origin: * +access-control-expose-headers: +``` +* __Response body:__ +```json +{ + "message": "OK" +} +``` + +### update_public_tags_request +#### Edit request to add a public tag + +##### Request +* __Method:__ PUT +* __Path:__ /api/account/v1/user/public_tags/132 +* __Request headers:__ +``` +content-type: multipart/mixed; boundary=plug_conn_test +``` +* __Request body:__ +```json +{ + "website": "website2", + "tags": "Tag2;Tag3", + "is_owner": true, + "full_name": "full name2", + "email": "test_user-9@blockscout.com", + "company": "company2", + "addresses": [ + "0x000000000000000000000000000000000000000f", + "0x0000000000000000000000000000000000000010", + "0x0000000000000000000000000000000000000011", + "0x0000000000000000000000000000000000000012", + "0x0000000000000000000000000000000000000013", + "0x0000000000000000000000000000000000000014" + ], + "additional_comment": "additional_comment2" +} +``` + +##### Response +* __Status__: 200 +* __Response headers:__ +``` +set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAlaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyNmQABWVtYWlsbQAAABp0ZXN0X3VzZXItN0BibG9ja3Njb3V0LmNvbWQAAmlkYcZkAARuYW1lbQAAAApVc2VyIFRlc3Q2ZAAIbmlja25hbWVtAAAACnRlc3RfdXNlcjZkAAN1aWRtAAAAD2Jsb2Nrc2NvdXR8MDAwNmQADHdhdGNobGlzdF9pZGHG.86gruprPiLE-Nf9xkOzjEcW2wfSnCCPly5fHTwHrF6c; path=/; HttpOnly +content-type: application/json; charset=utf-8 +cache-control: max-age=0, private, must-revalidate +x-request-id: FxF1Y2E03jhU4u4AAGSi +access-control-allow-credentials: true +access-control-allow-origin: * +access-control-expose-headers: +``` +* __Response body:__ +```json +{ + "website": "website2", + "tags": "Tag2;Tag3", + "submission_date": "2022-09-03T21:00:07.000000Z", + "is_owner": true, + "id": 132, + "full_name": "full name2", + "email": "test_user-9@blockscout.com", + "company": "company2", + "addresses": [ + "0x000000000000000000000000000000000000000f", + "0x0000000000000000000000000000000000000010", + "0x0000000000000000000000000000000000000011", + "0x0000000000000000000000000000000000000012", + "0x0000000000000000000000000000000000000013", + "0x0000000000000000000000000000000000000014" + ], + "additional_comment": "additional_comment2" +} +``` + diff --git a/apps/block_scout_web/assets/css/app.scss b/apps/block_scout_web/assets/css/app.scss index 330b99c4e0..e3468b6358 100644 --- a/apps/block_scout_web/assets/css/app.scss +++ b/apps/block_scout_web/assets/css/app.scss @@ -124,6 +124,8 @@ $fa-font-path: "~@fortawesome/fontawesome-free/webfonts"; @import "components/_dropzone"; @import "components/_search"; @import "components/_ad"; +@import "components/_account"; + // Font Awesome @import "components/_fontawesome_icon"; diff --git a/apps/block_scout_web/assets/css/components/_account.scss b/apps/block_scout_web/assets/css/components/_account.scss new file mode 100644 index 0000000000..8b1fc68a13 --- /dev/null +++ b/apps/block_scout_web/assets/css/components/_account.scss @@ -0,0 +1,143 @@ +div.divider { + height: inherit; + width: 1px; + background: #828ba0; + margin-left: 10px; + margin-right: 10px; +} + +input.profile-item { + margin-left: 10px; + color: #828ba0; + outline: none; +} + +.header-account { + font-size: 18px; +} + +.label-account { + font-size: 1rem; +} + +.white { + color: #fff; +} + +.card-body-account { + max-width: none !important; +} + +.form-input { + display: flex; + margin-bottom: 1rem; +} + +.form-checkbox { + margin-right: 0.5rem; + align-self: center; +} + +.label-checkbox { + font-size: 14px; + margin-bottom: 0; +} + +.o-flow-x { + overflow-x: auto; +} + +.fs-14 { + font-size: 14px; +} + +.acc-link-active { + &:hover, &:focus { + background-color: $primary !important; + color: #fff !important; + } + background-color: $primary; + color: #fff; +} + +.table-watchlist { + margin: 30px; + + @include media-breakpoint-down(md) { + margin: 0; + } +} + +.form-error { + display: block; + font-size: 13px; + line-height: 1.2; + padding-top: 10px; +} + +.navbar-account { + @include media-breakpoint-down(sm) { + max-width: 100%; + flex-grow: 1; + } +} + +.nav-item.account { + @include media-breakpoint-down(sm) { + width: auto; + display: inline-grid; + } +} + +.nav.account { + @include media-breakpoint-down(sm) { + display: inline; + } +} + +li.public-tags-address { + list-style: none; + display: flex; +} + +input.public-tags-address { + flex-basis: 557px; + width: auto; +} + +.add-form-field { + background: none; + border: none; + margin-left: 0.5px; +} + +.remove-form-field { + background: none; + border: none; + + svg { + margin-top: 3px; + } +} + +.multiple-input-fields-container { + padding-inline-start: 0; + display: inline-block; + margin-bottom: 0; +} + +.line-input { + @include media-breakpoint-up(md) { + display: flex; + justify-content: space-between; + + .form-group { + flex-grow: 1; + margin-right: 4rem; + } + } +} + +.mr-4-rem { + margin-right: 4rem; +} \ No newline at end of file diff --git a/apps/block_scout_web/assets/css/components/_btn_line.scss b/apps/block_scout_web/assets/css/components/_btn_line.scss index b62b345db3..854ab5b65d 100644 --- a/apps/block_scout_web/assets/css/components/_btn_line.scss +++ b/apps/block_scout_web/assets/css/components/_btn_line.scss @@ -4,3 +4,13 @@ $btn-line-color: $secondary !default; .btn-line { @include btn-line($btn-line-bg, $btn-line-color); } + +.btn-line-inversed { + @include btn-line($btn-line-color, $btn-line-bg); +} + +.btn-line-inversed:hover { + background-color: $btn-line-color; + color: $btn-line-bg; + text-decoration: none; +} \ No newline at end of file diff --git a/apps/block_scout_web/assets/css/components/_card.scss b/apps/block_scout_web/assets/css/components/_card.scss index 109b5c47d0..bcbe9dce58 100644 --- a/apps/block_scout_web/assets/css/components/_card.scss +++ b/apps/block_scout_web/assets/css/components/_card.scss @@ -326,4 +326,22 @@ $card-tab-icon-color-active: #fff !default; .function-output { margin-left: -1rem; +} + +.functions-tabs input[type="radio"] { + display: none; +} + +.card-misc-container { + padding-left: $card-horizontal-padding; + padding-top: $card-horizontal-padding; + + .btn-line-inversed, + .btn-line { + display: inline-flex; + } +} + +.nav-pills .nav-link.active { + background-color: $primary; } \ No newline at end of file diff --git a/apps/block_scout_web/assets/js/lib/public_tags_request_form.js b/apps/block_scout_web/assets/js/lib/public_tags_request_form.js new file mode 100644 index 0000000000..a1f88a3f11 --- /dev/null +++ b/apps/block_scout_web/assets/js/lib/public_tags_request_form.js @@ -0,0 +1,43 @@ +import $ from 'jquery' + +const $removeButton = $('.remove-form-field')[0] +const $container = $('#' + $removeButton.dataset.container) +const index = parseInt($container[0].dataset.index) + +if (index <= 1) { + $('.remove-form-field').hide() +} + +$('.add-form-field').on('click', (event) => { + event.preventDefault() + console.log(event) + const $container = $('#' + event.currentTarget.dataset.container) + const index = parseInt($container[0].dataset.index) + if (index < 10) { + $container.append($.parseHTML(event.currentTarget.dataset.prototype)) + $container[0].dataset.index = index + 1 + } + if (index >= 9) { + $('.add-form-field').hide() + } + if (index <= 1) { + $('.remove-form-field').show() + } +}) + +$('[data-multiple-input-field-container]').on('click', '.remove-form-field', (event) => { + event.preventDefault() + console.log(event) + const $container = $('#' + event.currentTarget.dataset.container) + const index = parseInt($container[0].dataset.index) + if (index > 1) { + $container[0].dataset.index = index - 1 + event.currentTarget.parentElement.remove() + } + if (index >= 10) { + $('.add-form-field').show() + } + if (index <= 2) { + $('.remove-form-field').hide() + } +}) diff --git a/apps/block_scout_web/assets/js/lib/smart_contract/functions.js b/apps/block_scout_web/assets/js/lib/smart_contract/functions.js index bf23ea03fb..6230972ba7 100644 --- a/apps/block_scout_web/assets/js/lib/smart_contract/functions.js +++ b/apps/block_scout_web/assets/js/lib/smart_contract/functions.js @@ -4,7 +4,7 @@ import { queryMethod, callMethod } from './interact' import { walletEnabled, connectToWallet, disconnectWallet, web3ModalInit } from './connect.js' import '../../pages/address' -const loadFunctions = (element) => { +const loadFunctions = (element, isCustomABI) => { const $element = $(element) const url = $element.data('url') const hash = $element.data('hash') @@ -13,7 +13,7 @@ const loadFunctions = (element) => { $.get( url, - { hash, type, action }, + { hash, type, action, is_custom_abi: isCustomABI }, response => $element.html(response) ) .done(function () { @@ -21,7 +21,9 @@ const loadFunctions = (element) => { document.querySelector(disconnectSelector) && document.querySelector(disconnectSelector).addEventListener('click', disconnectWallet) web3ModalInit(connectToWallet) - $('[data-function]').each((_, element) => { + const selector = isCustomABI ? '[data-function-custom]' : '[data-function]' + + $(selector).each((_, element) => { readWriteFunction(element) }) @@ -79,9 +81,10 @@ const readWriteFunction = (element) => { return } const type = $('[data-smart-contract-functions]').data('type') + const isCustomABI = $form.data('custom-abi') walletEnabled() - .then((isWalletEnabled) => queryMethod(isWalletEnabled, url, $methodId, args, type, functionName, $responseContainer)) + .then((isWalletEnabled) => queryMethod(isWalletEnabled, url, $methodId, args, type, functionName, $responseContainer, isCustomABI)) } else if (action === 'write') { const explorerChainId = $form.data('chainId') walletEnabled() @@ -93,5 +96,11 @@ const readWriteFunction = (element) => { const container = $('[data-smart-contract-functions]') if (container.length) { - loadFunctions(container) + loadFunctions(container, false) +} + +const customABIContainer = $('[data-smart-contract-functions-custom]') + +if (customABIContainer.length) { + loadFunctions(customABIContainer, true) } diff --git a/apps/block_scout_web/assets/js/lib/smart_contract/interact.js b/apps/block_scout_web/assets/js/lib/smart_contract/interact.js index 960cf4ef04..eaacbfe814 100644 --- a/apps/block_scout_web/assets/js/lib/smart_contract/interact.js +++ b/apps/block_scout_web/assets/js/lib/smart_contract/interact.js @@ -3,11 +3,12 @@ import { openErrorModal, openWarningModal, openSuccessModal, openModalWithMessag import { compareChainIDs, formatError, formatTitleAndError, getContractABI, getCurrentAccountPromise, getMethodInputs, prepareMethodArgs } from './common_helpers' import BigNumber from 'bignumber.js' -export const queryMethod = (isWalletEnabled, url, $methodId, args, type, functionName, $responseContainer) => { +export const queryMethod = (isWalletEnabled, url, $methodId, args, type, functionName, $responseContainer, isCustomABI) => { const data = { function_name: functionName, method_id: $methodId.val(), - type + type, + is_custom_abi: isCustomABI } data.args_count = args.length @@ -62,7 +63,7 @@ export const callMethod = (isWalletEnabled, $functionInputs, explorerChainId, $f openErrorModal(titleAndError.title.length ? titleAndError.title : `Error in sending transaction for method "${functionName}"`, message, false) }) .on('transactionHash', function (txHash) { - onTransactionHash(txHash, $element, functionName) + onTransactionHash(txHash, functionName) }) } else { const txParams = { @@ -75,7 +76,7 @@ export const callMethod = (isWalletEnabled, $functionInputs, explorerChainId, $f params: [txParams] }) .then(function (txHash) { - onTransactionHash(txHash, $element, functionName) + onTransactionHash(txHash, functionName) }) .catch(function (error) { openErrorModal('Error in sending transaction for fallback method', formatError(error), false) @@ -88,8 +89,8 @@ export const callMethod = (isWalletEnabled, $functionInputs, explorerChainId, $f }) } -function onTransactionHash (txHash, $element, functionName) { - openModalWithMessage($element.find('#pending-contract-write'), true, txHash) +function onTransactionHash (txHash, functionName) { + openModalWithMessage($('#pending-contract-write'), true, txHash) const getTxReceipt = (txHash) => { window.ethereum.request({ method: 'eth_getTransactionReceipt', diff --git a/apps/block_scout_web/assets/js/pages/account/delete_item_handler.js b/apps/block_scout_web/assets/js/pages/account/delete_item_handler.js new file mode 100644 index 0000000000..0e357e037e --- /dev/null +++ b/apps/block_scout_web/assets/js/pages/account/delete_item_handler.js @@ -0,0 +1,19 @@ +import $ from 'jquery' + +$('[data-delete-item]').on('click', (event) => { + event.preventDefault() + + if (confirm('Are you sure you want to delete item?')) { + $(event.currentTarget.parentElement).find('form').trigger('submit') + } +}) + +$('[data-delete-request]').on('click', (event) => { + event.preventDefault() + + const result = prompt('Public tags: "' + event.currentTarget.dataset.tags.replace(';', '" and "') + '" will be removed.\nWhy do you want to remove tags?') + if (result) { + $(event.currentTarget.parentElement).find('[name="remove_reason"]').val(result) + $(event.currentTarget.parentElement).find('form').trigger('submit') + } +}) diff --git a/apps/block_scout_web/assets/webpack.config.js b/apps/block_scout_web/assets/webpack.config.js index 3b95893c76..6c422fedc3 100644 --- a/apps/block_scout_web/assets/webpack.config.js +++ b/apps/block_scout_web/assets/webpack.config.js @@ -70,7 +70,9 @@ const appJs = 'token-overview': './js/pages/token/overview.js', 'export-csv': './css/export-csv.scss', 'csv-download': './js/lib/csv_download.js', - 'dropzone': './js/lib/dropzone.js' + 'dropzone': './js/lib/dropzone.js', + 'delete-item-handler': './js/pages/account/delete_item_handler.js', + 'public-tags-request-form': './js/lib/public_tags_request_form.js' }, output: { filename: '[name].js', diff --git a/apps/block_scout_web/config/config.exs b/apps/block_scout_web/config/config.exs index dc2add39a8..87804da868 100644 --- a/apps/block_scout_web/config/config.exs +++ b/apps/block_scout_web/config/config.exs @@ -8,7 +8,7 @@ import Config # General application configuration config :block_scout_web, namespace: BlockScoutWeb, - ecto_repos: [Explorer.Repo] + ecto_repos: [Explorer.Repo, Explorer.Repo.Account] config :block_scout_web, admin_panel_enabled: System.get_env("ADMIN_PANEL_ENABLED", "") == "true" @@ -78,6 +78,15 @@ config :block_scout_web, BlockScoutWeb.ApiRouter, config :block_scout_web, BlockScoutWeb.WebRouter, enabled: System.get_env("DISABLE_WEBAPP") != "true" +# Configures Ueberauth local settings +config :ueberauth, Ueberauth, + providers: [ + auth0: { + Ueberauth.Strategy.Auth0, + [callback_path: "/auth/auth0/callback"] + } + ] + config :hammer, backend: {Hammer.Backend.ETS, [expiry_ms: 60_000 * 60 * 4, cleanup_interval_ms: 60_000 * 10]} diff --git a/apps/block_scout_web/config/runtime/test.exs b/apps/block_scout_web/config/runtime/test.exs index 29e4e71f6d..d7100466c1 100644 --- a/apps/block_scout_web/config/runtime/test.exs +++ b/apps/block_scout_web/config/runtime/test.exs @@ -4,6 +4,15 @@ config :explorer, Explorer.ExchangeRates, enabled: false, store: :none config :explorer, Explorer.KnownTokens, enabled: false, store: :none +config :ueberauth, Ueberauth.Strategy.Auth0.OAuth, + domain: "example.com", + client_id: "clien_id", + client_secret: "secrets" + +config :ueberauth, Ueberauth, + logout_url: "example.com/logout", + logout_return_to_url: "example.com/return" + variant = if is_nil(System.get_env("ETHEREUM_JSONRPC_VARIANT")) do "parity" diff --git a/apps/block_scout_web/config/test.exs b/apps/block_scout_web/config/test.exs index 69ff45e1da..c18e31b20d 100644 --- a/apps/block_scout_web/config/test.exs +++ b/apps/block_scout_web/config/test.exs @@ -23,3 +23,11 @@ config :wallaby, screenshot_on_failure: true, driver: Wallaby.Chrome, js_errors: config :block_scout_web, BlockScoutWeb.Counters.BlocksIndexedCounter, enabled: false config :block_scout_web, :captcha_helper, BlockScoutWeb.TestCaptchaHelper + +config :ueberauth, Ueberauth, + providers: [ + auth0: { + Ueberauth.Strategy.Auth0, + [callback_url: "example.com/callback"] + } + ] diff --git a/apps/block_scout_web/lib/block_scout_web/api_router.ex b/apps/block_scout_web/lib/block_scout_web/api_router.ex index 6063941f69..9e1b414f3d 100644 --- a/apps/block_scout_web/lib/block_scout_web/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/api_router.ex @@ -13,11 +13,76 @@ defmodule BlockScoutWeb.ApiRouter do Router for API """ use BlockScoutWeb, :router + alias BlockScoutWeb.Plug.CheckAccountAPI pipeline :api do plug(:accepts, ["json"]) end + pipeline :account_api do + plug(:fetch_session) + plug(:protect_from_forgery) + plug(CheckAccountAPI) + end + + alias BlockScoutWeb.Account.Api.V1.{TagsController, UserController} + + scope "/account/v1" do + pipe_through(:api) + pipe_through(:account_api) + + get("/get_csrf", UserController, :get_csrf) + + scope "/user" do + get("/info", UserController, :info) + + get("/watchlist", UserController, :watchlist) + delete("/watchlist/:id", UserController, :delete_watchlist) + post("/watchlist", UserController, :create_watchlist) + put("/watchlist/:id", UserController, :update_watchlist) + + get("/api_keys", UserController, :api_keys) + delete("/api_keys/:api_key", UserController, :delete_api_key) + post("/api_keys", UserController, :create_api_key) + put("/api_keys/:api_key", UserController, :update_api_key) + + get("/custom_abis", UserController, :custom_abis) + delete("/custom_abis/:id", UserController, :delete_custom_abi) + post("/custom_abis", UserController, :create_custom_abi) + put("/custom_abis/:id", UserController, :update_custom_abi) + + get("/public_tags", UserController, :public_tags_requests) + delete("/public_tags/:id", UserController, :delete_public_tags_request) + post("/public_tags", UserController, :create_public_tags_request) + put("/public_tags/:id", UserController, :update_public_tags_request) + + scope "/tags" do + get("/address/", UserController, :tags_address) + get("/address/:id", UserController, :tags_address) + delete("/address/:id", UserController, :delete_tag_address) + post("/address/", UserController, :create_tag_address) + put("/address/:id", UserController, :update_tag_address) + + get("/transaction/", UserController, :tags_transaction) + get("/transaction/:id", UserController, :tags_transaction) + delete("/transaction/:id", UserController, :delete_tag_transaction) + post("/transaction/", UserController, :create_tag_transaction) + put("/transaction/:id", UserController, :update_tag_transaction) + end + end + end + + scope "/account/v1" do + pipe_through(:api) + pipe_through(:account_api) + + scope "/tags" do + get("/address/:address_hash", TagsController, :tags_address) + + get("/transaction/:transaction_hash", TagsController, :tags_transaction) + end + end + scope "/v1", as: :api_v1 do pipe_through(:api) alias BlockScoutWeb.API.{EthRPC, RPC, V1} diff --git a/apps/block_scout_web/lib/block_scout_web/chain.ex b/apps/block_scout_web/lib/block_scout_web/chain.ex index d2b4a78ece..927e5f2546 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -32,10 +32,10 @@ defmodule BlockScoutWeb.Chain do alias Explorer.PagingOptions - defimpl Poison.Encoder, for: Poison.Encoder.Decimal do + defimpl Poison.Encoder, for: Decimal do def encode(value, _opts) do # silence the xref warning - decimal = Poison.Encoder.Decimal + decimal = Decimal [?\", decimal.to_string(value), ?\"] end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v1/fallback_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v1/fallback_controller.ex new file mode 100644 index 0000000000..56c3e44229 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v1/fallback_controller.ex @@ -0,0 +1,83 @@ +defmodule BlockScoutWeb.Account.Api.V1.FallbackController do + use Phoenix.Controller + + alias BlockScoutWeb.Account.Api.V1.UserView + alias Ecto.Changeset + + def call(conn, {:identity, _}) do + conn + |> put_status(:not_found) + |> put_view(UserView) + |> render(:message, %{message: "User not found"}) + end + + def call(conn, {:watchlist, _}) do + conn + |> put_status(:not_found) + |> put_view(UserView) + |> render(:message, %{message: "Watchlist not found"}) + end + + def call(conn, {:error, %{reason: :item_not_found}}) do + conn + |> put_status(:not_found) + |> put_view(UserView) + |> render(:message, %{message: "Item not found"}) + end + + def call(conn, {:error, %Changeset{} = changeset}) do + conn + |> put_status(:unprocessable_entity) + |> put_view(UserView) + |> render(:changeset_errors, changeset: changeset) + end + + def call(conn, {:create_tag, {:error, message}}) do + conn + |> put_status(:unprocessable_entity) + |> put_view(UserView) + |> render(:message, %{message: message}) + end + + def call(conn, {:watchlist_delete, false}) do + conn + |> put_status(:not_found) + |> put_view(UserView) + |> render(:message, %{message: "Watchlist address not found"}) + end + + def call(conn, {:tag_delete, false}) do + conn + |> put_status(:not_found) + |> put_view(UserView) + |> render(:message, %{message: "Tag not found"}) + end + + def call(conn, {:api_key_delete, false}) do + conn + |> put_status(:not_found) + |> put_view(UserView) + |> render(:message, %{message: "Api key not found"}) + end + + def call(conn, {:custom_abi_delete, false}) do + conn + |> put_status(:not_found) + |> put_view(UserView) + |> render(:message, %{message: "Custom ABI not found"}) + end + + def call(conn, {:public_tag_delete, false}) do + conn + |> put_status(:not_found) + |> put_view(UserView) + |> render(:message, %{message: "Error"}) + end + + def call(conn, {:auth, _}) do + conn + |> put_status(:unauthorized) + |> put_view(UserView) + |> render(:message, %{message: "Unauthorized"}) + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v1/tags_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v1/tags_controller.ex new file mode 100644 index 0000000000..91f8ab5833 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v1/tags_controller.ex @@ -0,0 +1,87 @@ +defmodule BlockScoutWeb.Account.Api.V1.TagsController do + use BlockScoutWeb, :controller + + import BlockScoutWeb.Account.AuthController, only: [current_user: 1] + + alias BlockScoutWeb.Models.{GetAddressTags, GetTransactionTags, UserFromAuth} + alias Explorer.Account.Identity + alias Explorer.{Chain, Repo} + alias Explorer.Chain.Hash.{Address, Full} + + action_fallback(BlockScoutWeb.Account.Api.V1.FallbackController) + + def tags_address(conn, %{"address_hash" => address_hash}) do + personal_tags = + if is_nil(current_user(conn)) do + %{personal_tags: [], watchlist_names: []} + else + uid = current_user(conn).id + + with {:identity, [%Identity{} = identity]} <- {:identity, UserFromAuth.find_identity(uid)}, + {:watchlist, %{watchlists: [watchlist | _]}} <- + {:watchlist, Repo.account_repo().preload(identity, :watchlists)}, + {:address_hash, {:ok, address_hash}} <- {:address_hash, Address.cast(address_hash)} do + GetAddressTags.get_address_tags(address_hash, %{id: identity.id, watchlist_id: watchlist.id}) + else + _ -> + %{personal_tags: [], watchlist_names: []} + end + end + + public_tags = + case Address.cast(address_hash) do + {:ok, address_hash} -> + GetAddressTags.get_public_tags(address_hash) + + _ -> + %{common_tags: []} + end + + conn + |> put_status(200) + |> render(:address_tags, %{tags_map: Map.merge(personal_tags, public_tags)}) + end + + def tags_transaction(conn, %{"transaction_hash" => transaction_hash}) do + transaction = + with {:ok, transaction_hash} <- Full.cast(transaction_hash), + {:ok, transaction} <- Chain.hash_to_transaction(transaction_hash) do + transaction + else + _ -> + nil + end + + personal_tags = + if is_nil(current_user(conn)) do + %{personal_tags: [], watchlist_names: [], personal_tx_tag: nil} + else + uid = current_user(conn).id + + with {:identity, [%Identity{} = identity]} <- {:identity, UserFromAuth.find_identity(uid)}, + {:watchlist, %{watchlists: [watchlist | _]}} <- + {:watchlist, Repo.account_repo().preload(identity, :watchlists)}, + false <- is_nil(transaction) do + GetTransactionTags.get_transaction_with_addresses_tags(transaction, %{ + id: identity.id, + watchlist_id: watchlist.id + }) + else + _ -> + %{personal_tags: [], watchlist_names: [], personal_tx_tag: nil} + end + end + + public_tags_from = + if is_nil(transaction), do: [], else: GetAddressTags.get_public_tags(transaction.from_address_hash).common_tags + + public_tags_to = + if is_nil(transaction), do: [], else: GetAddressTags.get_public_tags(transaction.to_address_hash).common_tags + + public_tags = %{common_tags: public_tags_from ++ public_tags_to} + + conn + |> put_status(200) + |> render(:transaction_tags, %{tags_map: Map.merge(personal_tags, public_tags)}) + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v1/user_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v1/user_controller.ex new file mode 100644 index 0000000000..22ab4e2696 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v1/user_controller.ex @@ -0,0 +1,476 @@ +defmodule BlockScoutWeb.Account.Api.V1.UserController do + use BlockScoutWeb, :controller + + import BlockScoutWeb.Account.AuthController, only: [current_user: 1] + import Ecto.Query, only: [from: 2] + + alias BlockScoutWeb.Models.UserFromAuth + alias Explorer.Account.Api.Key, as: ApiKey + alias Explorer.Account.CustomABI + alias Explorer.Account.{Identity, PublicTagsRequest, TagAddress, TagTransaction, WatchlistAddress} + alias Explorer.ExchangeRates.Token + alias Explorer.{Market, Repo} + alias Plug.CSRFProtection + + action_fallback(BlockScoutWeb.Account.Api.V1.FallbackController) + + @ok_message "OK" + + def info(conn, _params) do + with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, + {:identity, [%Identity{} = identity]} <- {:identity, UserFromAuth.find_identity(uid)} do + conn + |> put_status(200) + |> render(:user_info, %{identity: identity}) + end + end + + def watchlist(conn, _params) do + with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, + {:identity, [%Identity{} = identity]} <- {:identity, UserFromAuth.find_identity(uid)}, + {:watchlist, %{watchlists: [watchlist | _]}} <- + {:watchlist, Repo.account_repo().preload(identity, :watchlists)}, + watchlist_with_addresses <- preload_watchlist_address_fetched_coin_balance(watchlist) do + conn + |> put_status(200) + |> render(:watchlist_addresses, %{ + exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), + watchlist_addresses: watchlist_with_addresses.watchlist_addresses + }) + end + end + + def delete_watchlist(conn, %{"id" => watchlist_address_id}) do + with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, + {:identity, [%Identity{} = identity]} <- {:identity, UserFromAuth.find_identity(uid)}, + {:watchlist, %{watchlists: [watchlist | _]}} <- + {:watchlist, Repo.account_repo().preload(identity, :watchlists)}, + {count, _} <- WatchlistAddress.delete(watchlist_address_id, watchlist.id), + {:watchlist_delete, true} <- {:watchlist_delete, count > 0} do + conn + |> put_status(200) + |> render(:message, %{message: @ok_message}) + end + end + + def create_watchlist(conn, %{ + "address_hash" => address_hash, + "name" => name, + "notification_settings" => %{ + "native" => %{ + "incoming" => watch_coin_input, + "outcoming" => watch_coin_output + }, + "ERC-20" => %{ + "incoming" => watch_erc_20_input, + "outcoming" => watch_erc_20_output + }, + "ERC-721" => %{ + "incoming" => watch_erc_721_input, + "outcoming" => watch_erc_721_output + } + # , + # "ERC-1155" => %{ + # "incoming" => watch_erc_1155_input, + # "outcoming" => watch_erc_1155_output + # } + }, + "notification_methods" => %{ + "email" => notify_email + } + }) do + watchlist_params = %{ + name: name, + watch_coin_input: watch_coin_input, + watch_coin_output: watch_coin_output, + watch_erc_20_input: watch_erc_20_input, + watch_erc_20_output: watch_erc_20_output, + watch_erc_721_input: watch_erc_721_input, + watch_erc_721_output: watch_erc_721_output, + watch_erc_1155_input: watch_erc_721_input, + watch_erc_1155_output: watch_erc_721_output, + notify_email: notify_email, + address_hash: address_hash + } + + with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, + {:identity, [%Identity{} = identity]} <- {:identity, UserFromAuth.find_identity(uid)}, + {:watchlist, %{watchlists: [watchlist | _]}} <- + {:watchlist, Repo.account_repo().preload(identity, :watchlists)}, + {:ok, watchlist_address} <- + WatchlistAddress.create(Map.put(watchlist_params, :watchlist_id, watchlist.id)), + watchlist_address_preloaded <- WatchlistAddress.preload_address_fetched_coin_balance(watchlist_address) do + conn + |> put_status(200) + |> render(:watchlist_address, %{ + watchlist_address: watchlist_address_preloaded, + exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null() + }) + end + end + + def update_watchlist(conn, %{ + "id" => watchlist_address_id, + "address_hash" => address_hash, + "name" => name, + "notification_settings" => %{ + "native" => %{ + "incoming" => watch_coin_input, + "outcoming" => watch_coin_output + }, + "ERC-20" => %{ + "incoming" => watch_erc_20_input, + "outcoming" => watch_erc_20_output + }, + "ERC-721" => %{ + "incoming" => watch_erc_721_input, + "outcoming" => watch_erc_721_output + } + # , + # "ERC-1155" => %{ + # "incoming" => watch_erc_1155_input, + # "outcoming" => watch_erc_1155_output + # } + }, + "notification_methods" => %{ + "email" => notify_email + } + }) do + watchlist_params = %{ + id: watchlist_address_id, + name: name, + watch_coin_input: watch_coin_input, + watch_coin_output: watch_coin_output, + watch_erc_20_input: watch_erc_20_input, + watch_erc_20_output: watch_erc_20_output, + watch_erc_721_input: watch_erc_721_input, + watch_erc_721_output: watch_erc_721_output, + watch_erc_1155_input: watch_erc_721_input, + watch_erc_1155_output: watch_erc_721_output, + notify_email: notify_email, + address_hash: address_hash + } + + with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, + {:identity, [%Identity{} = identity]} <- {:identity, UserFromAuth.find_identity(uid)}, + {:watchlist, %{watchlists: [watchlist | _]}} <- + {:watchlist, Repo.account_repo().preload(identity, :watchlists)}, + {:ok, watchlist_address} <- + WatchlistAddress.update(Map.put(watchlist_params, :watchlist_id, watchlist.id)), + watchlist_address_preloaded <- WatchlistAddress.preload_address_fetched_coin_balance(watchlist_address) do + conn + |> put_status(200) + |> render(:watchlist_address, %{ + watchlist_address: watchlist_address_preloaded, + exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null() + }) + end + end + + def tags_address(conn, _params) do + with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, + {:identity, [%Identity{} = identity]} <- {:identity, UserFromAuth.find_identity(uid)}, + address_tags <- TagAddress.get_tags_address_by_identity_id(identity.id) do + conn + |> put_status(200) + |> render(:address_tags, %{address_tags: address_tags}) + end + end + + def delete_tag_address(conn, %{"id" => tag_id}) do + with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, + {:identity, [%Identity{} = identity]} <- {:identity, UserFromAuth.find_identity(uid)}, + {count, _} <- TagAddress.delete(tag_id, identity.id), + {:tag_delete, true} <- {:tag_delete, count > 0} do + conn + |> put_status(200) + |> render(:message, %{message: @ok_message}) + end + end + + def create_tag_address(conn, %{"address_hash" => address_hash, "name" => name}) do + with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, + {:identity, [%Identity{} = identity]} <- {:identity, UserFromAuth.find_identity(uid)}, + {:ok, address_tag} <- + TagAddress.create(%{ + name: name, + address_hash: address_hash, + identity_id: identity.id + }) do + conn + |> put_status(200) + |> render(:address_tag, %{address_tag: address_tag}) + end + end + + def update_tag_address(conn, %{"id" => tag_id} = attrs) do + with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, + {:identity, [%Identity{} = identity]} <- {:identity, UserFromAuth.find_identity(uid)}, + {:ok, address_tag} <- + TagAddress.update( + reject_nil_map_values(%{ + id: tag_id, + name: attrs["name"], + address_hash: attrs["address_hash"], + identity_id: identity.id + }) + ) do + conn + |> put_status(200) + |> render(:address_tag, %{address_tag: address_tag}) + end + end + + def tags_transaction(conn, _params) do + with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, + {:identity, [%Identity{} = identity]} <- {:identity, UserFromAuth.find_identity(uid)}, + transaction_tags <- TagTransaction.get_tags_transaction_by_identity_id(identity.id) do + conn + |> put_status(200) + |> render(:transaction_tags, %{transaction_tags: transaction_tags}) + end + end + + def delete_tag_transaction(conn, %{"id" => tag_id}) do + with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, + {:identity, [%Identity{} = identity]} <- {:identity, UserFromAuth.find_identity(uid)}, + {count, _} <- TagTransaction.delete(tag_id, identity.id), + {:tag_delete, true} <- {:tag_delete, count > 0} do + conn + |> put_status(200) + |> render(:message, %{message: @ok_message}) + end + end + + def create_tag_transaction(conn, %{"transaction_hash" => tx_hash, "name" => name}) do + with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, + {:identity, [%Identity{} = identity]} <- {:identity, UserFromAuth.find_identity(uid)}, + {:ok, transaction_tag} <- + TagTransaction.create(%{ + name: name, + tx_hash: tx_hash, + identity_id: identity.id + }) do + conn + |> put_status(200) + |> render(:transaction_tag, %{transaction_tag: transaction_tag}) + end + end + + def update_tag_transaction(conn, %{"id" => tag_id} = attrs) do + with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, + {:identity, [%Identity{} = identity]} <- {:identity, UserFromAuth.find_identity(uid)}, + {:ok, transaction_tag} <- + TagTransaction.update( + reject_nil_map_values(%{ + id: tag_id, + name: attrs["name"], + tx_hash: attrs["transaction_hash"], + identity_id: identity.id + }) + ) do + conn + |> put_status(200) + |> render(:transaction_tag, %{transaction_tag: transaction_tag}) + end + end + + def api_keys(conn, _params) do + with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, + {:identity, [%Identity{} = identity]} <- {:identity, UserFromAuth.find_identity(uid)}, + api_keys <- ApiKey.get_api_keys_by_identity_id(identity.id) do + conn + |> put_status(200) + |> render(:api_keys, %{api_keys: api_keys}) + end + end + + def delete_api_key(conn, %{"api_key" => api_key_uuid}) do + with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, + {:identity, [%Identity{} = identity]} <- {:identity, UserFromAuth.find_identity(uid)}, + {count, _} <- ApiKey.delete(api_key_uuid, identity.id), + {:api_key_delete, true} <- {:api_key_delete, count > 0} do + conn + |> put_status(200) + |> render(:message, %{message: @ok_message}) + end + end + + def create_api_key(conn, %{"name" => api_key_name}) do + with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, + {:identity, [%Identity{} = identity]} <- {:identity, UserFromAuth.find_identity(uid)}, + {:ok, api_key} <- + ApiKey.create(%{name: api_key_name, identity_id: identity.id}) do + conn + |> put_status(200) + |> render(:api_key, %{api_key: api_key}) + end + end + + def update_api_key(conn, %{"name" => api_key_name, "api_key" => api_key_value}) do + with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, + {:identity, [%Identity{} = identity]} <- {:identity, UserFromAuth.find_identity(uid)}, + {:ok, api_key} <- + ApiKey.update(%{value: api_key_value, name: api_key_name, identity_id: identity.id}) do + conn + |> put_status(200) + |> render(:api_key, %{api_key: api_key}) + end + end + + def custom_abis(conn, _params) do + with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, + {:identity, [%Identity{} = identity]} <- {:identity, UserFromAuth.find_identity(uid)}, + custom_abis <- CustomABI.get_custom_abis_by_identity_id(identity.id) do + conn + |> put_status(200) + |> render(:custom_abis, %{custom_abis: custom_abis}) + end + end + + def delete_custom_abi(conn, %{"id" => id}) do + with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, + {:identity, [%Identity{} = identity]} <- {:identity, UserFromAuth.find_identity(uid)}, + {count, _} <- CustomABI.delete(id, identity.id), + {:custom_abi_delete, true} <- {:custom_abi_delete, count > 0} do + conn + |> put_status(200) + |> render(:message, %{message: @ok_message}) + end + end + + def create_custom_abi(conn, %{"contract_address_hash" => contract_address_hash, "name" => name, "abi" => abi}) do + with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, + {:identity, [%Identity{} = identity]} <- {:identity, UserFromAuth.find_identity(uid)}, + {:ok, custom_abi} <- + CustomABI.create(%{ + name: name, + address_hash: contract_address_hash, + abi: abi, + identity_id: identity.id + }) do + conn + |> put_status(200) + |> render(:custom_abi, %{custom_abi: custom_abi}) + end + end + + def update_custom_abi( + conn, + %{ + "id" => id + } = params + ) do + with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, + {:identity, [%Identity{} = identity]} <- {:identity, UserFromAuth.find_identity(uid)}, + {:ok, custom_abi} <- + CustomABI.update( + reject_nil_map_values(%{ + id: id, + name: params["name"], + address_hash: params["contract_address_hash"], + abi: params["abi"], + identity_id: identity.id + }) + ) do + conn + |> put_status(200) + |> render(:custom_abi, %{custom_abi: custom_abi}) + end + end + + def public_tags_requests(conn, _params) do + with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, + {:identity, [%Identity{} = identity]} <- {:identity, UserFromAuth.find_identity(uid)}, + public_tags_requests <- PublicTagsRequest.get_public_tags_requests_by_identity_id(identity.id) do + conn + |> put_status(200) + |> render(:public_tags_requests, %{public_tags_requests: public_tags_requests}) + end + end + + def delete_public_tags_request(conn, %{"id" => id, "remove_reason" => remove_reason}) do + with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, + {:identity, [%Identity{} = identity]} <- {:identity, UserFromAuth.find_identity(uid)}, + {:public_tag_delete, true} <- + {:public_tag_delete, + PublicTagsRequest.mark_as_deleted_public_tags_request(%{ + id: id, + identity_id: identity.id, + remove_reason: remove_reason + })} do + conn + |> put_status(200) + |> render(:message, %{message: @ok_message}) + end + end + + def create_public_tags_request(conn, params) do + with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, + {:identity, [%Identity{} = identity]} <- {:identity, UserFromAuth.find_identity(uid)}, + {:ok, public_tags_request} <- + PublicTagsRequest.create(%{ + full_name: params["full_name"], + email: params["email"], + tags: params["tags"], + website: params["website"], + additional_comment: params["additional_comment"], + addresses: params["addresses"], + company: params["company"], + is_owner: params["is_owner"], + identity_id: identity.id + }) do + conn + |> put_status(200) + |> render(:public_tags_request, %{public_tags_request: public_tags_request}) + end + end + + def update_public_tags_request( + conn, + %{ + "id" => id + } = params + ) do + with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, + {:identity, [%Identity{} = identity]} <- {:identity, UserFromAuth.find_identity(uid)}, + {:ok, public_tags_request} <- + PublicTagsRequest.update( + reject_nil_map_values(%{ + id: id, + full_name: params["full_name"], + email: params["email"], + tags: params["tags"], + website: params["website"], + additional_comment: params["additional_comment"], + addresses: params["addresses"], + company: params["company"], + is_owner: params["is_owner"], + identity_id: identity.id + }) + ) do + conn + |> put_status(200) + |> render(:public_tags_request, %{public_tags_request: public_tags_request}) + end + end + + def get_csrf(conn, _) do + with {:auth, %{id: _}} <- {:auth, current_user(conn)} do + conn + |> put_resp_header("x-bs-account-csrf", CSRFProtection.get_csrf_token()) + |> put_status(200) + |> render(:message, %{message: "ok"}) + end + end + + defp reject_nil_map_values(map) when is_map(map) do + Map.reject(map, fn {_k, v} -> is_nil(v) end) + end + + defp preload_watchlist_address_fetched_coin_balance(watchlist) do + watchlist + |> Repo.account_repo().preload(watchlist_addresses: from(wa in WatchlistAddress, order_by: [desc: wa.id])) + |> WatchlistAddress.preload_address_fetched_coin_balance() + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/account/api_key_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/account/api_key_controller.ex new file mode 100644 index 0000000000..55c0b05283 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/account/api_key_controller.ex @@ -0,0 +1,65 @@ +defmodule BlockScoutWeb.Account.ApiKeyController do + use BlockScoutWeb, :controller + + alias Explorer.Account.Api.Key, as: ApiKey + + import BlockScoutWeb.Account.AuthController, only: [authenticate!: 1] + + def new(conn, _params) do + authenticate!(conn) + + render(conn, "form.html", method: :create, api_key: empty_api_key()) + end + + def create(conn, %{"key" => api_key}) do + current_user = authenticate!(conn) + + case ApiKey.create(%{name: api_key["name"], identity_id: current_user.id}) do + {:ok, _} -> + redirect(conn, to: api_key_path(conn, :index)) + + {:error, invalid_api_key} -> + render(conn, "form.html", method: :create, api_key: invalid_api_key) + end + end + + def create(conn, _) do + redirect(conn, to: api_key_path(conn, :index)) + end + + def index(conn, _params) do + current_user = authenticate!(conn) + + render(conn, "index.html", api_keys: ApiKey.get_api_keys_by_identity_id(current_user.id)) + end + + def edit(conn, %{"id" => api_key}) do + current_user = authenticate!(conn) + + case ApiKey.get_api_key_by_value_and_identity_id(api_key, current_user.id) do + nil -> + not_found(conn) + + %ApiKey{} = api_key -> + render(conn, "form.html", method: :update, api_key: ApiKey.changeset(api_key)) + end + end + + def update(conn, %{"id" => api_key, "key" => %{"value" => api_key, "name" => name}}) do + current_user = authenticate!(conn) + + ApiKey.update(%{value: api_key, identity_id: current_user.id, name: name}) + + redirect(conn, to: api_key_path(conn, :index)) + end + + def delete(conn, %{"id" => api_key}) do + current_user = authenticate!(conn) + + ApiKey.delete(api_key, current_user.id) + + redirect(conn, to: api_key_path(conn, :index)) + end + + defp empty_api_key, do: ApiKey.changeset() +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/account/auth_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/account/auth_controller.ex new file mode 100644 index 0000000000..2691ac643a --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/account/auth_controller.ex @@ -0,0 +1,73 @@ +defmodule BlockScoutWeb.Account.AuthController do + use BlockScoutWeb, :controller + + alias BlockScoutWeb.Models.UserFromAuth + alias Explorer.Account + alias Plug.CSRFProtection + + plug(Ueberauth) + + def request(conn, _) do + not_found(conn) + end + + def logout(conn, _params) do + conn + |> configure_session(drop: true) + |> redirect(to: root()) + end + + def profile(conn, _params), + do: conn |> get_session(:current_user) |> do_profile(conn) + + defp do_profile(nil, conn), + do: redirect(conn, to: root()) + + defp do_profile(%{} = user, conn), + do: render(conn, :profile, user: user) + + def callback(%{assigns: %{ueberauth_failure: _fails}} = conn, _params) do + conn + |> put_flash(:error, "Failed to authenticate.") + |> redirect(to: root()) + end + + def callback(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do + case UserFromAuth.find_or_create(auth) do + {:ok, user} -> + CSRFProtection.get_csrf_token() + + conn + |> put_session(:current_user, user) + |> redirect(to: root()) + + {:error, reason} -> + conn + |> put_flash(:error, reason) + |> redirect(to: root()) + end + end + + def callback(conn, _) do + not_found(conn) + end + + # for importing in other controllers + def authenticate!(conn) do + current_user(conn) || redirect(conn, to: root()) + end + + def current_user(%{private: %{plug_session: %{"current_user" => _}}} = conn) do + if Account.enabled?() do + get_session(conn, :current_user) + else + nil + end + end + + def current_user(_), do: nil + + defp root do + System.get_env("NETWORK_PATH") || "/" + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/account/custom_abi_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/account/custom_abi_controller.ex new file mode 100644 index 0000000000..dcd8fca985 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/account/custom_abi_controller.ex @@ -0,0 +1,87 @@ +defmodule BlockScoutWeb.Account.CustomABIController do + use BlockScoutWeb, :controller + + alias Ecto.Changeset + alias Explorer.Account.CustomABI + + import BlockScoutWeb.Account.AuthController, only: [authenticate!: 1] + + def new(conn, _params) do + authenticate!(conn) + + render(conn, "form.html", method: :create, custom_abi: empty_custom_abi()) + end + + def create(conn, %{"custom_abi" => custom_abi}) do + current_user = authenticate!(conn) + + case CustomABI.create(%{ + name: custom_abi["name"], + address_hash: custom_abi["address_hash"], + abi: custom_abi["abi"], + identity_id: current_user.id + }) do + {:ok, _} -> + redirect(conn, to: custom_abi_path(conn, :index)) + + {:error, invalid_custom_abi} -> + render(conn, "form.html", method: :create, custom_abi: invalid_custom_abi) + end + end + + def create(conn, _) do + redirect(conn, to: custom_abi_path(conn, :index)) + end + + def index(conn, _params) do + current_user = authenticate!(conn) + + render(conn, "index.html", custom_abis: CustomABI.get_custom_abis_by_identity_id(current_user.id)) + end + + def edit(conn, %{"id" => id}) do + current_user = authenticate!(conn) + + case CustomABI.get_custom_abi_by_id_and_identity_id(id, current_user.id) do + nil -> + not_found(conn) + + %CustomABI{} = custom_abi -> + render(conn, "form.html", method: :update, custom_abi: CustomABI.changeset_without_constraints(custom_abi)) + end + end + + def update(conn, %{"id" => id, "custom_abi" => %{"abi" => abi, "name" => name, "address_hash" => address_hash}}) do + current_user = authenticate!(conn) + + case CustomABI.update(%{ + id: id, + name: name, + address_hash: address_hash, + abi: abi, + identity_id: current_user.id + }) do + {:error, %Changeset{} = custom_abi} -> + render(conn, "form.html", method: :update, custom_abi: custom_abi) + + _ -> + redirect(conn, to: custom_abi_path(conn, :index)) + end + end + + def update(conn, _) do + authenticate!(conn) + + redirect(conn, to: custom_abi_path(conn, :index)) + end + + def delete(conn, %{"id" => id}) do + current_user = authenticate!(conn) + + CustomABI.delete(id, current_user.id) + + redirect(conn, to: custom_abi_path(conn, :index)) + end + + defp empty_custom_abi, do: CustomABI.changeset_without_constraints() +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/account/public_tags_request_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/account/public_tags_request_controller.ex new file mode 100644 index 0000000000..a380a2cc3c --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/account/public_tags_request_controller.ex @@ -0,0 +1,114 @@ +defmodule BlockScoutWeb.Account.PublicTagsRequestController do + use BlockScoutWeb, :controller + + alias Ecto.Changeset + alias Explorer.Account.PublicTagsRequest + + import BlockScoutWeb.Account.AuthController, only: [authenticate!: 1] + + def index(conn, _params) do + current_user = authenticate!(conn) + + render(conn, "index.html", + public_tags_requests: PublicTagsRequest.get_public_tags_requests_by_identity_id(current_user.id) + ) + end + + def new(conn, _params) do + current_user = authenticate!(conn) + + render(conn, "form.html", + method: :create, + public_tags_request: + PublicTagsRequest.changeset_without_constraints(%PublicTagsRequest{}, %{ + full_name: current_user.name, + email: current_user.email + }) + ) + end + + def create(conn, %{"public_tags_request" => public_tags_request}) do + current_user = authenticate!(conn) + + case PublicTagsRequest.create(%{ + full_name: public_tags_request["full_name"], + email: public_tags_request["email"], + tags: public_tags_request["tags"], + website: public_tags_request["website"], + additional_comment: public_tags_request["additional_comment"], + addresses: public_tags_request["addresses"], + company: public_tags_request["company"], + is_owner: public_tags_request["is_owner"], + identity_id: current_user.id + }) do + {:ok, _} -> + redirect(conn, to: public_tags_request_path(conn, :index)) + + {:error, invalid_public_tags_request} -> + render(conn, "form.html", method: :create, public_tags_request: invalid_public_tags_request) + end + end + + def create(conn, _) do + redirect(conn, to: public_tags_request_path(conn, :index)) + end + + def edit(conn, %{"id" => id}) do + current_user = authenticate!(conn) + + case PublicTagsRequest.get_public_tags_request_by_id_and_identity_id(id, current_user.id) do + nil -> + not_found(conn) + + %PublicTagsRequest{} = public_tags_request -> + render(conn, "form.html", + method: :update, + public_tags_request: PublicTagsRequest.changeset_without_constraints(public_tags_request) + ) + end + end + + def update(conn, %{ + "id" => id, + "public_tags_request" => public_tags_request + }) do + current_user = authenticate!(conn) + + case PublicTagsRequest.update(%{ + id: id, + full_name: public_tags_request["full_name"], + email: public_tags_request["email"], + tags: public_tags_request["tags"], + website: public_tags_request["website"], + additional_comment: public_tags_request["additional_comment"], + addresses: public_tags_request["addresses"], + company: public_tags_request["company"], + is_owner: public_tags_request["is_owner"], + identity_id: current_user.id + }) do + {:error, %Changeset{} = public_tags_request} -> + render(conn, "form.html", method: :update, public_tags_request: public_tags_request) + + _ -> + redirect(conn, to: public_tags_request_path(conn, :index)) + end + end + + def update(conn, _) do + authenticate!(conn) + + redirect(conn, to: public_tags_request_path(conn, :index)) + end + + def delete(conn, %{"id" => id, "remove_reason" => remove_reason}) do + current_user = authenticate!(conn) + + PublicTagsRequest.mark_as_deleted_public_tags_request(%{ + id: id, + identity_id: current_user.id, + remove_reason: remove_reason + }) + + redirect(conn, to: public_tags_request_path(conn, :index)) + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/account/tag_address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/account/tag_address_controller.ex new file mode 100644 index 0000000000..8ca30a959b --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/account/tag_address_controller.ex @@ -0,0 +1,49 @@ +defmodule BlockScoutWeb.Account.TagAddressController do + use BlockScoutWeb, :controller + + alias Explorer.Account.TagAddress + + import BlockScoutWeb.Account.AuthController, only: [authenticate!: 1] + + def index(conn, _params) do + current_user = authenticate!(conn) + + render(conn, "index.html", address_tags: TagAddress.get_tags_address_by_identity_id(current_user.id)) + end + + def new(conn, _params) do + authenticate!(conn) + + render(conn, "form.html", tag_address: new_tag()) + end + + def create(conn, %{"tag_address" => tag_address}) do + current_user = authenticate!(conn) + + case TagAddress.create(%{ + name: tag_address["name"], + address_hash: tag_address["address_hash"], + identity_id: current_user.id + }) do + {:ok, _} -> + redirect(conn, to: tag_address_path(conn, :index)) + + {:error, invalid_tag_address} -> + render(conn, "form.html", tag_address: invalid_tag_address) + end + end + + def create(conn, _) do + redirect(conn, to: tag_address_path(conn, :index)) + end + + def delete(conn, %{"id" => id}) do + current_user = authenticate!(conn) + + TagAddress.delete(id, current_user.id) + + redirect(conn, to: tag_address_path(conn, :index)) + end + + defp new_tag, do: TagAddress.changeset() +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/account/tag_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/account/tag_transaction_controller.ex new file mode 100644 index 0000000000..cba3dea142 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/account/tag_transaction_controller.ex @@ -0,0 +1,49 @@ +defmodule BlockScoutWeb.Account.TagTransactionController do + use BlockScoutWeb, :controller + + alias Explorer.Account.TagTransaction + + import BlockScoutWeb.Account.AuthController, only: [authenticate!: 1] + + def index(conn, _params) do + current_user = authenticate!(conn) + + render(conn, "index.html", tx_tags: TagTransaction.get_tags_transaction_by_identity_id(current_user.id)) + end + + def new(conn, _params) do + authenticate!(conn) + + render(conn, "form.html", tag_transaction: new_tag()) + end + + def create(conn, %{"tag_transaction" => tag_address}) do + current_user = authenticate!(conn) + + case TagTransaction.create(%{ + name: tag_address["name"], + tx_hash: tag_address["tx_hash"], + identity_id: current_user.id + }) do + {:ok, _} -> + redirect(conn, to: tag_transaction_path(conn, :index)) + + {:error, invalid_tag_transaction} -> + render(conn, "form.html", tag_transaction: invalid_tag_transaction) + end + end + + def create(conn, _) do + redirect(conn, to: tag_transaction_path(conn, :index)) + end + + def delete(conn, %{"id" => id}) do + current_user = authenticate!(conn) + + TagTransaction.delete(id, current_user.id) + + redirect(conn, to: tag_transaction_path(conn, :index)) + end + + defp new_tag, do: TagTransaction.changeset() +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/account/watchlist_address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/account/watchlist_address_controller.ex new file mode 100644 index 0000000000..55236895a4 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/account/watchlist_address_controller.ex @@ -0,0 +1,92 @@ +defmodule BlockScoutWeb.Account.WatchlistAddressController do + use BlockScoutWeb, :controller + + alias Explorer.Account.WatchlistAddress + + import BlockScoutWeb.Account.AuthController, only: [authenticate!: 1] + + def new(conn, _params) do + authenticate!(conn) + + render(conn, "form.html", method: :create, watchlist_address: empty_watchlist_address()) + end + + def create(conn, %{"watchlist_address" => wa_params}) do + current_user = authenticate!(conn) + + case WatchlistAddress.create(params_to_attributes(wa_params, current_user.watchlist_id)) do + {:ok, _watchlist_address} -> + redirect(conn, to: watchlist_path(conn, :show)) + + {:error, changeset} -> + render(conn, "form.html", method: :create, watchlist_address: changeset) + end + end + + def edit(conn, %{"id" => id}) do + current_user = authenticate!(conn) + + case WatchlistAddress.get_watchlist_address_by_id_and_watchlist_id(id, current_user.watchlist_id) do + nil -> + not_found(conn) + + %WatchlistAddress{} = watchlist_address -> + render(conn, "form.html", method: :update, watchlist_address: WatchlistAddress.changeset(watchlist_address)) + end + end + + def update(conn, %{"id" => id, "watchlist_address" => wa_params}) do + current_user = authenticate!(conn) + + case wa_params + |> params_to_attributes(current_user.watchlist_id) + |> Map.put(:id, id) + |> WatchlistAddress.update() do + {:ok, _watchlist_address} -> + redirect(conn, to: watchlist_path(conn, :show)) + + {:error, changeset} -> + render(conn, "form.html", method: :update, watchlist_address: changeset) + end + end + + def delete(conn, %{"id" => id}) do + current_user = authenticate!(conn) + + WatchlistAddress.delete(id, current_user.watchlist_id) + + redirect(conn, to: watchlist_path(conn, :show)) + end + + defp empty_watchlist_address, do: WatchlistAddress.changeset() + + defp params_to_attributes( + %{ + "address_hash" => address_hash, + "name" => name, + "watch_coin_input" => watch_coin_input, + "watch_coin_output" => watch_coin_output, + "watch_erc_20_input" => watch_erc_20_input, + "watch_erc_20_output" => watch_erc_20_output, + "watch_erc_721_input" => watch_nft_input, + "watch_erc_721_output" => watch_nft_output, + "notify_email" => notify_email + }, + watchlist_id + ) do + %{ + address_hash: address_hash, + name: name, + watch_coin_input: watch_coin_input, + watch_coin_output: watch_coin_output, + watch_erc_20_input: watch_erc_20_input, + watch_erc_20_output: watch_erc_20_output, + watch_erc_721_input: watch_nft_input, + watch_erc_721_output: watch_nft_output, + watch_erc_1155_input: watch_nft_input, + watch_erc_1155_output: watch_nft_output, + notify_email: notify_email, + watchlist_id: watchlist_id + } + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/account/watchlist_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/account/watchlist_controller.ex new file mode 100644 index 0000000000..f8c548322f --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/account/watchlist_controller.ex @@ -0,0 +1,26 @@ +defmodule BlockScoutWeb.Account.WatchlistController do + use BlockScoutWeb, :controller + + import BlockScoutWeb.Account.AuthController, only: [authenticate!: 1] + import Ecto.Query, only: [from: 2] + + alias Explorer.Account.{Watchlist, WatchlistAddress} + alias Explorer.Repo + + def show(conn, _params) do + current_user = authenticate!(conn) + + render( + conn, + "show.html", + watchlist: watchlist_with_addresses(current_user) + ) + end + + defp watchlist_with_addresses(user) do + Watchlist + |> Repo.account_repo().get(user.watchlist_id) + |> Repo.account_repo().preload(watchlist_addresses: from(wa in WatchlistAddress, order_by: [desc: wa.id])) + |> WatchlistAddress.preload_address_fetched_coin_balance() + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_coin_balance_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_coin_balance_controller.ex index b024c102a0..bde343dd12 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_coin_balance_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_coin_balance_controller.ex @@ -5,7 +5,9 @@ defmodule BlockScoutWeb.AddressCoinBalanceController do use BlockScoutWeb, :controller + import BlockScoutWeb.Account.AuthController, only: [current_user: 1] import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1] + import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2] alias BlockScoutWeb.{AccessHelpers, AddressCoinBalanceView, Controller} alias Explorer.{Chain, Market} @@ -76,7 +78,8 @@ defmodule BlockScoutWeb.AddressCoinBalanceController do coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address), exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), current_path: Controller.current_full_path(conn), - counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)}) + counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)}), + tags: get_address_tags(address_hash, current_user(conn)) ) else {:restricted_access, _} -> diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_controller.ex index a2f1d30cbb..ffd65b2608 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_controller.ex @@ -2,6 +2,9 @@ defmodule BlockScoutWeb.AddressContractController do use BlockScoutWeb, :controller + import BlockScoutWeb.Account.AuthController, only: [current_user: 1] + import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2] + alias BlockScoutWeb.AccessHelpers alias BlockScoutWeb.AddressContractVerificationController, as: VerificationController alias Explorer.{Chain, Market} @@ -29,7 +32,8 @@ defmodule BlockScoutWeb.AddressContractController do address: address, coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address), exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), - counters_path: address_path(conn, :address_counters, %{"id" => address_hash_string}) + counters_path: address_path(conn, :address_counters, %{"id" => address_hash_string}), + tags: get_address_tags(address_hash, current_user(conn)) ) else {:restricted_access, _} -> diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex index 13cf9c20d2..b90321d16a 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex @@ -1,8 +1,12 @@ defmodule BlockScoutWeb.AddressController do use BlockScoutWeb, :controller + import BlockScoutWeb.Account.AuthController, only: [current_user: 1] + import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1] + import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2] + alias BlockScoutWeb.{ AccessHelpers, AddressTransactionController, @@ -101,7 +105,8 @@ defmodule BlockScoutWeb.AddressController do exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), filter: params["filter"], counters_path: address_path(conn, :address_counters, %{"id" => address_hash_string}), - current_path: Controller.current_full_path(conn) + current_path: Controller.current_full_path(conn), + tags: get_address_tags(address_hash, current_user(conn)) ) else :error -> @@ -130,7 +135,8 @@ defmodule BlockScoutWeb.AddressController do exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), filter: params["filter"], counters_path: address_path(conn, :address_counters, %{"id" => address_hash_string}), - current_path: Controller.current_full_path(conn) + current_path: Controller.current_full_path(conn), + tags: get_address_tags(address_hash, current_user(conn)) ) _ -> diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_decompiled_contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_decompiled_contract_controller.ex index 079d5d9093..968326cbf2 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_decompiled_contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_decompiled_contract_controller.ex @@ -1,6 +1,9 @@ defmodule BlockScoutWeb.AddressDecompiledContractController do use BlockScoutWeb, :controller + import BlockScoutWeb.Account.AuthController, only: [current_user: 1] + import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2] + alias BlockScoutWeb.AccessHelpers alias Explorer.{Chain, Market} alias Explorer.ExchangeRates.Token @@ -16,7 +19,8 @@ defmodule BlockScoutWeb.AddressDecompiledContractController do address: address, coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address), exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), - counters_path: address_path(conn, :address_counters, %{"id" => address_hash_string}) + counters_path: address_path(conn, :address_counters, %{"id" => address_hash_string}), + tags: get_address_tags(address_hash, current_user(conn)) ) else {:restricted_access, _} -> diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_internal_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_internal_transaction_controller.ex index a72ba8e256..bfc8ce84fb 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_internal_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_internal_transaction_controller.ex @@ -5,7 +5,9 @@ defmodule BlockScoutWeb.AddressInternalTransactionController do use BlockScoutWeb, :controller + import BlockScoutWeb.Account.AuthController, only: [current_user: 1] import BlockScoutWeb.Chain, only: [current_filter: 1, paging_options: 1, next_page_params: 3, split_list_by_page: 1] + import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2] alias BlockScoutWeb.{AccessHelpers, Controller, InternalTransactionView} alias Explorer.{Chain, Market} @@ -86,7 +88,8 @@ defmodule BlockScoutWeb.AddressInternalTransactionController do current_path: Controller.current_full_path(conn), exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), filter: params["filter"], - counters_path: address_path(conn, :address_counters, %{"id" => address_hash_string}) + counters_path: address_path(conn, :address_counters, %{"id" => address_hash_string}), + tags: get_address_tags(address_hash, current_user(conn)) ) else {:restricted_access, _} -> @@ -112,7 +115,8 @@ defmodule BlockScoutWeb.AddressInternalTransactionController do coin_balance_status: nil, exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)}), - current_path: Controller.current_full_path(conn) + current_path: Controller.current_full_path(conn), + tags: get_address_tags(address_hash, current_user(conn)) ) _ -> diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_logs_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_logs_controller.ex index bcb138ca08..742c58cc08 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_logs_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_logs_controller.ex @@ -3,8 +3,12 @@ defmodule BlockScoutWeb.AddressLogsController do Manages events logs tab. """ + import BlockScoutWeb.Account.AuthController, only: [current_user: 1] + import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1] + import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2] + alias BlockScoutWeb.{AccessHelpers, AddressLogsView, Controller} alias Explorer.{Chain, Market} alias Explorer.ExchangeRates.Token @@ -64,7 +68,8 @@ defmodule BlockScoutWeb.AddressLogsController do current_path: Controller.current_full_path(conn), coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address), exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), - counters_path: address_path(conn, :address_counters, %{"id" => address_hash_string}) + counters_path: address_path(conn, :address_counters, %{"id" => address_hash_string}), + tags: get_address_tags(address_hash, current_user(conn)) ) else _ -> diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_read_contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_read_contract_controller.ex index 4734b8f0f3..2751e69dd0 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_read_contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_read_contract_controller.ex @@ -8,10 +8,15 @@ defmodule BlockScoutWeb.AddressReadContractController do use BlockScoutWeb, :controller + import BlockScoutWeb.Account.AuthController, only: [current_user: 1] + import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2] + alias BlockScoutWeb.AccessHelpers + alias BlockScoutWeb.AddressView alias Explorer.{Chain, Market} alias Explorer.Chain.Address alias Explorer.ExchangeRates.Token + alias Explorer.SmartContract.Reader alias Indexer.Fetcher.CoinBalanceOnDemand def index(conn, %{"address_id" => address_hash_string} = params) do @@ -25,23 +30,63 @@ defmodule BlockScoutWeb.AddressReadContractController do } ] + custom_abi = AddressView.fetch_custom_abi(conn, address_hash_string) + custom_abi? = AddressView.check_custom_abi_for_having_read_functions(custom_abi) + + need_wallet_custom_abi? = + !is_nil(custom_abi) && Reader.read_functions_required_wallet_from_abi(custom_abi.abi) != [] + + base_params = [ + type: :regular, + action: :read, + custom_abi: custom_abi?, + exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null() + ] + with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), {:ok, address} <- Chain.find_contract_address(address_hash, address_options, true), false <- is_nil(address.smart_contract), + need_wallet? <- Reader.read_functions_required_wallet_from_abi(address.smart_contract.abi) != [], {:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params) do render( conn, "index.html", - address: address, - type: :regular, - action: :read, - coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address), - exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), - counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)}) + base_params ++ + [ + address: address, + non_custom_abi: true, + need_wallet: need_wallet? || need_wallet_custom_abi?, + coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address), + counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)}), + tags: get_address_tags(address_hash, current_user(conn)) + ] ) else _ -> - not_found(conn) + if custom_abi? do + with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), + {:ok, address} <- Chain.find_contract_address(address_hash, address_options, false), + {:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params) do + render( + conn, + "index.html", + base_params ++ + [ + address: address, + non_custom_abi: false, + need_wallet: need_wallet_custom_abi?, + coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address), + counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)}), + tags: get_address_tags(address_hash, current_user(conn)) + ] + ) + else + _ -> + not_found(conn) + end + else + not_found(conn) + end end end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_read_proxy_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_read_proxy_controller.ex index 7ed05ef4b4..564655a0c1 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_read_proxy_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_read_proxy_controller.ex @@ -2,6 +2,9 @@ defmodule BlockScoutWeb.AddressReadProxyController do use BlockScoutWeb, :controller + import BlockScoutWeb.Account.AuthController, only: [current_user: 1] + import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2] + alias BlockScoutWeb.AccessHelpers alias Explorer.{Chain, Market} alias Explorer.Chain.Address @@ -31,7 +34,8 @@ defmodule BlockScoutWeb.AddressReadProxyController do action: :read, coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address), exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), - counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)}) + counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)}), + tags: get_address_tags(address_hash, current_user(conn)) ) else _ -> diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_token_controller.ex index 99b29f4082..5470034f89 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_token_controller.ex @@ -2,6 +2,8 @@ defmodule BlockScoutWeb.AddressTokenController do use BlockScoutWeb, :controller import BlockScoutWeb.Chain, only: [next_page_params: 3, paging_options: 1, split_list_by_page: 1] + import BlockScoutWeb.Account.AuthController, only: [current_user: 1] + import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2] alias BlockScoutWeb.{AccessHelpers, AddressTokenView, Controller} alias Explorer.{Chain, Market} @@ -74,7 +76,8 @@ defmodule BlockScoutWeb.AddressTokenController do current_path: Controller.current_full_path(conn), coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address), exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), - counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)}) + counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)}), + tags: get_address_tags(address_hash, current_user(conn)) ) else {:restricted_access, _} -> diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_token_transfer_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_token_transfer_controller.ex index 3cfee0da7d..eeebc3236a 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_token_transfer_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_token_transfer_controller.ex @@ -1,6 +1,9 @@ defmodule BlockScoutWeb.AddressTokenTransferController do use BlockScoutWeb, :controller + import BlockScoutWeb.Account.AuthController, only: [current_user: 1] + import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2] + alias BlockScoutWeb.{AccessHelpers, Controller, TransactionView} alias Explorer.ExchangeRates.Token alias Explorer.{Chain, Market} @@ -109,7 +112,8 @@ defmodule BlockScoutWeb.AddressTokenTransferController do exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), current_path: Controller.current_full_path(conn), token: token, - counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)}) + counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)}), + tags: get_address_tags(address_hash, current_user(conn)) ) else {:restricted_access, _} -> @@ -200,7 +204,8 @@ defmodule BlockScoutWeb.AddressTokenTransferController do exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), filter: params["filter"], current_path: Controller.current_full_path(conn), - counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)}) + counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)}), + tags: get_address_tags(address_hash, current_user(conn)) ) else {:restricted_access, _} -> diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex index 649bd8f4b4..679b0f62e9 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex @@ -5,8 +5,12 @@ defmodule BlockScoutWeb.AddressTransactionController do use BlockScoutWeb, :controller + import BlockScoutWeb.Account.AuthController, only: [current_user: 1] + import BlockScoutWeb.Chain, only: [current_filter: 1, paging_options: 1, next_page_params: 3, split_list_by_page: 1] + import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2] + alias BlockScoutWeb.{AccessHelpers, Controller, TransactionView} alias Explorer.{Chain, Market} @@ -122,7 +126,8 @@ defmodule BlockScoutWeb.AddressTransactionController do exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), filter: params["filter"], counters_path: address_path(conn, :address_counters, %{"id" => address_hash_string}), - current_path: Controller.current_full_path(conn) + current_path: Controller.current_full_path(conn), + tags: get_address_tags(address_hash, current_user(conn)) ) else :error -> @@ -151,7 +156,8 @@ defmodule BlockScoutWeb.AddressTransactionController do exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), filter: params["filter"], counters_path: address_path(conn, :address_counters, %{"id" => address_hash_string}), - current_path: Controller.current_full_path(conn) + current_path: Controller.current_full_path(conn), + tags: get_address_tags(address_hash, current_user(conn)) ) _ -> diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_validation_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_validation_controller.ex index 6f6ccdcad7..3963c26289 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_validation_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_validation_controller.ex @@ -4,9 +4,13 @@ defmodule BlockScoutWeb.AddressValidationController do """ use BlockScoutWeb, :controller + import BlockScoutWeb.Account.AuthController, only: [current_user: 1] + import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1] + import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2] + alias BlockScoutWeb.{AccessHelpers, BlockView, Controller} alias Explorer.ExchangeRates.Token alias Explorer.{Chain, Market} @@ -79,7 +83,8 @@ defmodule BlockScoutWeb.AddressValidationController do coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address), current_path: Controller.current_full_path(conn), counters_path: address_path(conn, :address_counters, %{"id" => address_hash_string}), - exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null() + exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), + tags: get_address_tags(address_hash, current_user(conn)) ) else {:restricted_access, _} -> diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_write_contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_write_contract_controller.ex index f3b50dd4a2..1accce5f5c 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_write_contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_write_contract_controller.ex @@ -8,7 +8,11 @@ defmodule BlockScoutWeb.AddressWriteContractController do use BlockScoutWeb, :controller + import BlockScoutWeb.Account.AuthController, only: [current_user: 1] + import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2] + alias BlockScoutWeb.AccessHelpers + alias BlockScoutWeb.AddressView alias Explorer.{Chain, Market} alias Explorer.Chain.Address alias Explorer.ExchangeRates.Token @@ -25,6 +29,15 @@ defmodule BlockScoutWeb.AddressWriteContractController do } ] + custom_abi? = AddressView.has_address_custom_abi_with_write_functions?(conn, address_hash_string) + + base_params = [ + type: :regular, + action: :write, + custom_abi: custom_abi?, + exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null() + ] + with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), {:ok, address} <- Chain.find_contract_address(address_hash, address_options, true), false <- is_nil(address.smart_contract), @@ -32,16 +45,40 @@ defmodule BlockScoutWeb.AddressWriteContractController do render( conn, "index.html", - address: address, - type: :regular, - action: :write, - coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address), - exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), - counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)}) + base_params ++ + [ + address: address, + non_custom_abi: true, + coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address), + counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)}), + tags: get_address_tags(address_hash, current_user(conn)) + ] ) else _ -> - not_found(conn) + if custom_abi? do + with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), + {:ok, address} <- Chain.find_contract_address(address_hash, address_options, false), + {:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params) do + render( + conn, + "index.html", + base_params ++ + [ + address: address, + non_custom_abi: false, + coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address), + counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)}), + tags: get_address_tags(address_hash, current_user(conn)) + ] + ) + else + _ -> + not_found(conn) + end + else + not_found(conn) + end end end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_write_proxy_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_write_proxy_controller.ex index e82fc17c29..fc5a7c7133 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_write_proxy_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_write_proxy_controller.ex @@ -2,6 +2,9 @@ defmodule BlockScoutWeb.AddressWriteProxyController do use BlockScoutWeb, :controller + import BlockScoutWeb.Account.AuthController, only: [current_user: 1] + import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2] + alias BlockScoutWeb.AccessHelpers alias Explorer.{Chain, Market} alias Explorer.Chain.Address @@ -31,7 +34,8 @@ defmodule BlockScoutWeb.AddressWriteProxyController do action: :write, coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address), exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), - counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)}) + counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)}), + tags: get_address_tags(address_hash, current_user(conn)) ) else _ -> diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/smart_contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/smart_contract_controller.ex index c0a64d8d43..0b3a9883b2 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/smart_contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/smart_contract_controller.ex @@ -1,19 +1,25 @@ defmodule BlockScoutWeb.SmartContractController do use BlockScoutWeb, :controller + alias BlockScoutWeb.AddressView alias Explorer.Chain alias Explorer.SmartContract.{Reader, Writer} + import Explorer.SmartContract.Solidity.Verifier, only: [parse_boolean: 1] + @burn_address "0x0000000000000000000000000000000000000000" - def index(conn, %{"hash" => address_hash_string, "type" => contract_type, "action" => action}) do + def index(conn, %{"hash" => address_hash_string, "type" => contract_type, "action" => action} = params) do address_options = [ necessity_by_association: %{ :smart_contract => :optional } ] + is_custom_abi = parse_boolean(params["is_custom_abi"]) + with true <- ajax?(conn), + {:custom_abi, false} <- {:custom_abi, is_custom_abi}, {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), {:ok, address} <- Chain.find_contract_address(address_hash, address_options, true) do implementation_address_hash_string = @@ -78,6 +84,9 @@ defmodule BlockScoutWeb.SmartContractController do action: action ) else + {:custom_abi, true} -> + custom_abi_render(conn, params) + :error -> unprocessable_entity(conn) @@ -91,6 +100,51 @@ defmodule BlockScoutWeb.SmartContractController do def index(conn, _), do: not_found(conn) + defp custom_abi_render(conn, %{"hash" => address_hash_string, "type" => contract_type, "action" => action}) do + with custom_abi <- AddressView.fetch_custom_abi(conn, address_hash_string), + false <- is_nil(custom_abi), + abi <- custom_abi.abi, + {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string) do + functions = + if action == "write" do + Writer.filter_write_functions(abi) + else + Reader.read_only_functions_from_abi(abi, address_hash) + end + + read_functions_required_wallet = + if action == "read" do + Reader.read_functions_required_wallet_from_abi(abi) + else + [] + end + + contract_abi = Poison.encode!(abi) + + conn + |> put_status(200) + |> put_layout(false) + |> render( + "_functions.html", + read_functions_required_wallet: read_functions_required_wallet, + read_only_functions: functions, + address: %{hash: address_hash}, + custom_abi: true, + contract_abi: contract_abi, + implementation_address: @burn_address, + implementation_abi: [], + contract_type: contract_type, + action: action + ) + else + :error -> + unprocessable_entity(conn) + + _ -> + not_found(conn) + end + end + def show(conn, params) do address_options = [ necessity_by_association: %{ @@ -102,6 +156,9 @@ defmodule BlockScoutWeb.SmartContractController do } ] + custom_abi = + if parse_boolean(params["is_custom_abi"]), do: AddressView.fetch_custom_abi(conn, params["id"]), else: nil + with true <- ajax?(conn), {:ok, address_hash} <- Chain.string_to_address_hash(params["id"]), {:ok, _address} <- Chain.find_contract_address(address_hash, address_options, true) do @@ -115,18 +172,19 @@ defmodule BlockScoutWeb.SmartContractController do else: for(x <- 0..(args_count - 1), do: params["arg_" <> to_string(x)] |> convert_map_to_array()) %{output: outputs, names: names} = - if params["from"] do - Reader.query_function_with_names( + if custom_abi do + Reader.query_function_with_names_custom_abi( address_hash, %{method_id: params["method_id"], args: args}, - contract_type, - params["from"] + params["from"], + custom_abi.abi ) else Reader.query_function_with_names( address_hash, %{method_id: params["method_id"], args: args}, - contract_type + contract_type, + params["from"] ) end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/contract_controller.ex index e16d9bf470..9a6bee2686 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/contract_controller.ex @@ -1,6 +1,9 @@ defmodule BlockScoutWeb.Tokens.ContractController do use BlockScoutWeb, :controller + import BlockScoutWeb.Account.AuthController, only: [current_user: 1] + import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2] + alias BlockScoutWeb.{AccessHelpers, TabHelpers} alias Explorer.{Chain, Market} alias Explorer.Chain.Address @@ -33,7 +36,8 @@ defmodule BlockScoutWeb.Tokens.ContractController do type: type, action: action, token: Market.add_price(token), - counters_path: token_path(conn, :token_counters, %{"id" => Address.checksum(address_hash)}) + counters_path: token_path(conn, :token_counters, %{"id" => Address.checksum(address_hash)}), + tags: get_address_tags(address_hash, current_user(conn)) ) else {:restricted_access, _} -> diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/holder_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/holder_controller.ex index 806badc195..710ff74e75 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/holder_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/holder_controller.ex @@ -1,6 +1,9 @@ defmodule BlockScoutWeb.Tokens.HolderController do use BlockScoutWeb, :controller + import BlockScoutWeb.Account.AuthController, only: [current_user: 1] + import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2] + alias BlockScoutWeb.{AccessHelpers, Controller} alias BlockScoutWeb.Tokens.HolderView alias Explorer.{Chain, Market} @@ -66,7 +69,8 @@ defmodule BlockScoutWeb.Tokens.HolderController do "index.html", current_path: Controller.current_full_path(conn), token: Market.add_price(token), - counters_path: token_path(conn, :token_counters, %{"id" => Address.checksum(address_hash)}) + counters_path: token_path(conn, :token_counters, %{"id" => Address.checksum(address_hash)}), + tags: get_address_tags(address_hash, current_user(conn)) ) else {:restricted_access, _} -> diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/transfer_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/transfer_controller.ex index 9c3ce0278e..ad7db933c0 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/transfer_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/transfer_controller.ex @@ -1,6 +1,9 @@ defmodule BlockScoutWeb.Tokens.TransferController do use BlockScoutWeb, :controller + import BlockScoutWeb.Account.AuthController, only: [current_user: 1] + import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2] + alias BlockScoutWeb.{AccessHelpers, Controller} alias BlockScoutWeb.Tokens.TransferView alias Explorer.{Chain, Market} @@ -71,7 +74,8 @@ defmodule BlockScoutWeb.Tokens.TransferController do counters_path: token_path(conn, :token_counters, %{"id" => Address.checksum(address_hash)}), current_path: Controller.current_full_path(conn), token: Market.add_price(token), - token_total_supply_status: TokenTotalSupplyOnDemand.trigger_fetch(address_hash) + token_total_supply_status: TokenTotalSupplyOnDemand.trigger_fetch(address_hash), + tags: get_address_tags(address_hash, current_user(conn)) ) else {:restricted_access, _} -> diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex index 695d961b7e..858b7dc115 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex @@ -1,6 +1,8 @@ defmodule BlockScoutWeb.TransactionController do use BlockScoutWeb, :controller + import BlockScoutWeb.Account.AuthController, only: [current_user: 1] + import BlockScoutWeb.Chain, only: [ fetch_page_number: 1, @@ -10,6 +12,9 @@ defmodule BlockScoutWeb.TransactionController do split_list_by_page: 1 ] + import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2] + import BlockScoutWeb.Models.GetTransactionTags, only: [get_transaction_with_addresses_tags: 2] + alias BlockScoutWeb.{ AccessHelpers, Controller, @@ -159,8 +164,16 @@ defmodule BlockScoutWeb.TransactionController do exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), block_height: Chain.block_height(), current_path: Controller.current_full_path(conn), + current_user: current_user(conn), show_token_transfers: true, - transaction: transaction + transaction: transaction, + from_tags: get_address_tags(transaction.from_address_hash, current_user(conn)), + to_tags: get_address_tags(transaction.to_address_hash, current_user(conn)), + tx_tags: + get_transaction_with_addresses_tags( + transaction, + current_user(conn) + ) ) else :not_found -> @@ -188,9 +201,17 @@ defmodule BlockScoutWeb.TransactionController do "show_internal_transactions.html", exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), current_path: Controller.current_full_path(conn), + current_user: current_user(conn), block_height: Chain.block_height(), show_token_transfers: Chain.transaction_has_token_transfers?(transaction_hash), - transaction: transaction + transaction: transaction, + from_tags: get_address_tags(transaction.from_address_hash, current_user(conn)), + to_tags: get_address_tags(transaction.to_address_hash, current_user(conn)), + tx_tags: + get_transaction_with_addresses_tags( + transaction, + current_user(conn) + ) ) else :not_found -> diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_internal_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_internal_transaction_controller.ex index 61b7122180..5cc1af7f6f 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_internal_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_internal_transaction_controller.ex @@ -1,7 +1,10 @@ defmodule BlockScoutWeb.TransactionInternalTransactionController do use BlockScoutWeb, :controller + import BlockScoutWeb.Account.AuthController, only: [current_user: 1] import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1] + import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2] + import BlockScoutWeb.Models.GetTransactionTags, only: [get_transaction_with_addresses_tags: 2] alias BlockScoutWeb.{AccessHelpers, Controller, InternalTransactionView, TransactionController} alias Explorer.{Chain, Market} @@ -101,9 +104,17 @@ defmodule BlockScoutWeb.TransactionInternalTransactionController do "index.html", exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), current_path: Controller.current_full_path(conn), + current_user: current_user(conn), block_height: Chain.block_height(), show_token_transfers: Chain.transaction_has_token_transfers?(transaction_hash), - transaction: transaction + transaction: transaction, + from_tags: get_address_tags(transaction.from_address_hash, current_user(conn)), + to_tags: get_address_tags(transaction.to_address_hash, current_user(conn)), + tx_tags: + get_transaction_with_addresses_tags( + transaction, + current_user(conn) + ) ) else {:restricted_access, _} -> diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_log_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_log_controller.ex index f9abbf207b..bb29f7818f 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_log_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_log_controller.ex @@ -1,7 +1,10 @@ defmodule BlockScoutWeb.TransactionLogController do use BlockScoutWeb, :controller + import BlockScoutWeb.Account.AuthController, only: [current_user: 1] import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1] + import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2] + import BlockScoutWeb.Models.GetTransactionTags, only: [get_transaction_with_addresses_tags: 2] alias BlockScoutWeb.{AccessHelpers, Controller, TransactionController, TransactionLogView} alias Explorer.{Chain, Market} @@ -95,8 +98,16 @@ defmodule BlockScoutWeb.TransactionLogController do block_height: Chain.block_height(), show_token_transfers: Chain.transaction_has_token_transfers?(transaction_hash), current_path: Controller.current_full_path(conn), + current_user: current_user(conn), transaction: transaction, - exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null() + exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), + from_tags: get_address_tags(transaction.from_address_hash, current_user(conn)), + to_tags: get_address_tags(transaction.to_address_hash, current_user(conn)), + tx_tags: + get_transaction_with_addresses_tags( + transaction, + current_user(conn) + ) ) else {:restricted_access, _} -> diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_raw_trace_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_raw_trace_controller.ex index 44584b795f..49447c639b 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_raw_trace_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_raw_trace_controller.ex @@ -1,6 +1,10 @@ defmodule BlockScoutWeb.TransactionRawTraceController do use BlockScoutWeb, :controller + import BlockScoutWeb.Account.AuthController, only: [current_user: 1] + import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2] + import BlockScoutWeb.Models.GetTransactionTags, only: [get_transaction_with_addresses_tags: 2] + alias BlockScoutWeb.{AccessHelpers, TransactionController} alias EthereumJSONRPC alias Explorer.{Chain, Market} @@ -92,8 +96,16 @@ defmodule BlockScoutWeb.TransactionRawTraceController do exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), internal_transactions: internal_transactions, block_height: Chain.block_height(), + current_user: current_user(conn), show_token_transfers: Chain.transaction_has_token_transfers?(hash), - transaction: transaction + transaction: transaction, + from_tags: get_address_tags(transaction.from_address_hash, current_user(conn)), + to_tags: get_address_tags(transaction.to_address_hash, current_user(conn)), + tx_tags: + get_transaction_with_addresses_tags( + transaction, + current_user(conn) + ) ) end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_token_transfer_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_token_transfer_controller.ex index 95e5b34d0a..931df3ce94 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_token_transfer_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_token_transfer_controller.ex @@ -1,7 +1,10 @@ defmodule BlockScoutWeb.TransactionTokenTransferController do use BlockScoutWeb, :controller + import BlockScoutWeb.Account.AuthController, only: [current_user: 1] import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1] + import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2] + import BlockScoutWeb.Models.GetTransactionTags, only: [get_transaction_with_addresses_tags: 2] alias BlockScoutWeb.{AccessHelpers, Controller, TransactionController, TransactionTokenTransferView} alias Explorer.{Chain, Market} @@ -106,8 +109,16 @@ defmodule BlockScoutWeb.TransactionTokenTransferController do exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), block_height: Chain.block_height(), current_path: Controller.current_full_path(conn), + current_user: current_user(conn), show_token_transfers: true, - transaction: transaction + transaction: transaction, + from_tags: get_address_tags(transaction.from_address_hash, current_user(conn)), + to_tags: get_address_tags(transaction.to_address_hash, current_user(conn)), + tx_tags: + get_transaction_with_addresses_tags( + transaction, + current_user(conn) + ) ) else :not_found -> diff --git a/apps/block_scout_web/lib/block_scout_web/endpoint.ex b/apps/block_scout_web/lib/block_scout_web/endpoint.ex index f53445bc05..7b83088c18 100644 --- a/apps/block_scout_web/lib/block_scout_web/endpoint.ex +++ b/apps/block_scout_web/lib/block_scout_web/endpoint.ex @@ -59,11 +59,13 @@ defmodule BlockScoutWeb.Endpoint do # The session will be stored in the cookie and signed, # this means its contents can be read but not tampered with. # Set :encryption_salt if you would also like to encrypt it. + plug( Plug.Session, - store: :cookie, + store: BlockScoutWeb.Plug.RedisCookie, key: "_explorer_key", - signing_salt: "iC2ksJHS" + signing_salt: "iC2ksJHS", + same_site: "Lax" ) use SpandexPhoenix diff --git a/apps/block_scout_web/lib/block_scout_web/models/get_address_tags.ex b/apps/block_scout_web/lib/block_scout_web/models/get_address_tags.ex new file mode 100644 index 0000000000..459d5dffdb --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/models/get_address_tags.ex @@ -0,0 +1,75 @@ +defmodule BlockScoutWeb.Models.GetAddressTags do + @moduledoc """ + Get various types of tags associated with the address + """ + + import Ecto.Query, only: [from: 2] + + alias Explorer.Account.{TagAddress, WatchlistAddress} + alias Explorer.Chain.Hash + alias Explorer.Repo + alias Explorer.Tags.{AddressTag, AddressToTag} + + def get_address_tags(nil, nil), + do: %{personal_tags: [], watchlist_names: []} + + def get_address_tags(%Hash{} = address_hash, current_user) do + %{ + # common_tags: get_tags_on_address(address_hash), + personal_tags: get_personal_tags(address_hash, current_user), + watchlist_names: get_watchlist_names_on_address(address_hash, current_user) + } + end + + def get_address_tags(_, _), do: %{personal_tags: [], watchlist_names: []} + + def get_public_tags(%Hash{} = address_hash) do + %{ + common_tags: get_tags_on_address(address_hash) + } + end + + def get_tags_on_address(%Hash{} = address_hash) do + query = + from( + tt in AddressTag, + left_join: att in AddressToTag, + on: tt.id == att.tag_id, + where: att.address_hash == ^address_hash, + where: tt.label != ^"validator", + select: %{label: tt.label, display_name: tt.display_name, address_hash: att.address_hash} + ) + + Repo.all(query) + end + + def get_tags_on_address(_), do: [] + + def get_personal_tags(%Hash{} = address_hash, %{id: id}) do + query = + from( + ta in TagAddress, + where: ta.address_hash_hash == ^address_hash, + where: ta.identity_id == ^id, + select: %{label: ta.name, display_name: ta.name, address_hash: ta.address_hash} + ) + + Repo.account_repo().all(query) + end + + def get_personal_tags(_, _), do: [] + + def get_watchlist_names_on_address(%Hash{} = address_hash, %{watchlist_id: watchlist_id}) do + query = + from( + wa in WatchlistAddress, + where: wa.address_hash_hash == ^address_hash, + where: wa.watchlist_id == ^watchlist_id, + select: %{label: wa.name, display_name: wa.name} + ) + + Repo.account_repo().all(query) + end + + def get_watchlist_names_on_address(_, _), do: [] +end diff --git a/apps/block_scout_web/lib/block_scout_web/models/get_transaction_tags.ex b/apps/block_scout_web/lib/block_scout_web/models/get_transaction_tags.ex new file mode 100644 index 0000000000..c173a43a33 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/models/get_transaction_tags.ex @@ -0,0 +1,41 @@ +defmodule BlockScoutWeb.Models.GetTransactionTags do + @moduledoc """ + Get various types of tags associated with the transaction + """ + + import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2] + + alias Explorer.Account.TagTransaction + alias Explorer.Chain.Transaction + alias Explorer.Repo + + def get_transaction_with_addresses_tags( + %Transaction{} = transaction, + %{id: identity_id, watchlist_id: watchlist_id} + ) do + tx_tag = get_transaction_tags(transaction.hash, %{id: identity_id}) + addresses_tags = get_addresses_tags_for_transaction(transaction, %{id: identity_id, watchlist_id: watchlist_id}) + Map.put(addresses_tags, :personal_tx_tag, tx_tag) + end + + def get_transaction_with_addresses_tags(_, _), do: %{personal_tags: [], watchlist_names: [], personal_tx_tag: nil} + + def get_transaction_tags(transaction_hash, %{id: identity_id}) do + Repo.account_repo().get_by(TagTransaction, tx_hash_hash: transaction_hash, identity_id: identity_id) + end + + def get_transaction_tags(_, _), do: nil + + def get_addresses_tags_for_transaction( + %Transaction{} = transaction, + %{id: identity_id, watchlist_id: watchlist_id} + ) do + from_tags = get_address_tags(transaction.from_address_hash, %{id: identity_id, watchlist_id: watchlist_id}) + to_tags = get_address_tags(transaction.to_address_hash, %{id: identity_id, watchlist_id: watchlist_id}) + + %{ + personal_tags: Enum.dedup(from_tags.personal_tags ++ to_tags.personal_tags), + watchlist_names: Enum.dedup(from_tags.watchlist_names ++ to_tags.watchlist_names) + } + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/models/user_from_auth.ex b/apps/block_scout_web/lib/block_scout_web/models/user_from_auth.ex new file mode 100644 index 0000000000..058b160540 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/models/user_from_auth.ex @@ -0,0 +1,136 @@ +defmodule BlockScoutWeb.Models.UserFromAuth do + @moduledoc """ + Retrieve the user information from an auth request + """ + require Logger + require Poison + + alias Explorer.Account.Identity + alias Explorer.Repo + alias Ueberauth.Auth + + import Ecto.Query, only: [from: 2] + + def find_or_create(%Auth{} = auth, api_call? \\ false) do + case find_identity(auth) do + [] -> + case create_identity(auth) do + %Identity{} = identity -> + {:ok, return_value(identity, auth, api_call?)} + + {:error, changeset} -> + {:error, changeset} + end + + [%{} = identity | _] -> + update_identity(identity, update_identity_map(auth)) + {:ok, return_value(identity, auth, api_call?)} + end + end + + defp return_value(identity, _auth, true) do + identity + end + + defp return_value(identity, auth, false) do + basic_info(auth, identity) + end + + defp create_identity(auth) do + with {:ok, %Identity{} = identity} <- Repo.account_repo().insert(new_identity(auth)), + {:ok, _watchlist} <- add_watchlist(identity) do + identity + end + end + + defp update_identity(identity, attrs) do + identity + |> Identity.changeset(attrs) + |> Repo.account_repo().update() + end + + defp new_identity(auth) do + %Identity{ + uid: auth.uid, + uid_hash: auth.uid, + email: email_from_auth(auth), + name: name_from_auth(auth), + nickname: nickname_from_auth(auth), + avatar: avatar_from_auth(auth) + } + end + + defp add_watchlist(identity) do + watchlist = Ecto.build_assoc(identity, :watchlists, %{}) + + with {:ok, _} <- Repo.account_repo().insert(watchlist), + do: {:ok, identity} + end + + def find_identity(auth_or_uid) do + Repo.account_repo().all(query_identity(auth_or_uid)) + end + + def query_identity(%Auth{} = auth) do + from(i in Identity, where: i.uid_hash == ^auth.uid) + end + + def query_identity(id) do + from(i in Identity, where: i.id == ^id) + end + + defp basic_info(auth, identity) do + %{watchlists: [watchlist | _]} = Repo.account_repo().preload(identity, :watchlists) + + %{ + id: identity.id, + uid: auth.uid, + email: email_from_auth(auth), + name: name_from_auth(auth), + nickname: nickname_from_auth(auth), + avatar: avatar_from_auth(auth), + watchlist_id: watchlist.id + } + end + + defp update_identity_map(auth) do + %{ + email: email_from_auth(auth), + name: name_from_auth(auth), + nickname: nickname_from_auth(auth), + avatar: avatar_from_auth(auth) + } + end + + # github does it this way + defp avatar_from_auth(%{info: %{urls: %{avatar_url: image}}}), do: image + + # facebook does it this way + defp avatar_from_auth(%{info: %{image: image}}), do: image + + # default case if nothing matches + defp avatar_from_auth(auth) do + Logger.warn(auth.provider <> " needs to find an avatar URL!") + Logger.debug(Poison.encode!(auth)) + nil + end + + defp email_from_auth(%{info: %{email: email}}), do: email + + defp nickname_from_auth(%{info: %{nickname: nickname}}), do: nickname + + defp name_from_auth(%{info: %{name: name}}) + when name != "" and not is_nil(name), + do: name + + defp name_from_auth(%{info: info}) do + [info.first_name, info.last_name, info.nickname] + |> Enum.map(&(&1 |> to_string() |> String.trim())) + |> case do + ["", "", nick] -> nick + ["", lastname, _] -> lastname + [name, "", _] -> name + [name, lastname, _] -> name <> " " <> lastname + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/plug/check_account_api.ex b/apps/block_scout_web/lib/block_scout_web/plug/check_account_api.ex new file mode 100644 index 0000000000..80c5301bce --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/plug/check_account_api.ex @@ -0,0 +1,21 @@ +defmodule BlockScoutWeb.Plug.CheckAccountAPI do + @moduledoc """ + Checks if the Account functionality enabled for API level. + """ + import Plug.Conn + + alias Explorer.Account + + def init(opts), do: opts + + def call(conn, _opts) do + if Account.enabled?() do + conn + else + conn + |> put_resp_content_type("application/json") + |> send_resp(404, Jason.encode!(%{message: "Account functionality is disabled"})) + |> halt() + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/plug/check_account_web.ex b/apps/block_scout_web/lib/block_scout_web/plug/check_account_web.ex new file mode 100644 index 0000000000..00a3af4e00 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/plug/check_account_web.ex @@ -0,0 +1,31 @@ +defmodule BlockScoutWeb.Plug.CheckAccountWeb do + @moduledoc """ + Checks if the Account functionality enabled for web interface. + """ + import Phoenix.Controller + alias Phoenix.View + import Plug.Conn + + alias Explorer.Account + + def init(opts), do: opts + + def call(conn, _opts) do + if Account.enabled?() do + conn + else + inner_view = + View.render( + BlockScoutWeb.PageNotFoundView, + "index.html", + token: nil + ) + + conn + |> put_status(404) + |> put_view(BlockScoutWeb.LayoutView) + |> render(:app, inner_content: inner_view) + |> halt() + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/plug/redis_cookie.ex b/apps/block_scout_web/lib/block_scout_web/plug/redis_cookie.ex new file mode 100644 index 0000000000..ff8d457f09 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/plug/redis_cookie.ex @@ -0,0 +1,223 @@ +defmodule BlockScoutWeb.Plug.RedisCookie do + @moduledoc """ + Extended version of Plug.Session.COOKIE from https://github.com/elixir-plug/plug/blob/main/lib/plug/session/cookie.ex + Added Redis to have a possibility to invalidate session + """ + + require Logger + @behaviour Plug.Session.Store + + alias Plug.Crypto + alias Plug.Crypto.{KeyGenerator, MessageEncryptor, MessageVerifier} + + @impl true + def init(opts) do + opts + |> build_opts() + |> build_rotating_opts(opts[:rotating_options]) + |> Map.delete(:secret_key_base) + end + + @impl true + def get(conn, raw_cookie, opts) do + opts = Map.put(opts, :secret_key_base, conn.secret_key_base) + + [opts | opts.rotating_options] + |> Enum.find_value(:error, &read_raw_cookie(raw_cookie, &1)) + |> decode(opts.serializer, opts.log) + |> check_in_redis(raw_cookie) + end + + @impl true + def put(conn, _sid, term, opts) do + %{serializer: serializer, key_opts: key_opts, signing_salt: signing_salt} = opts + binary = encode(term, serializer) + + opts + |> case do + %{encryption_salt: nil} -> + MessageVerifier.sign(binary, derive(conn.secret_key_base, signing_salt, key_opts)) + + %{encryption_salt: encryption_salt} -> + MessageEncryptor.encrypt( + binary, + derive(conn.secret_key_base, encryption_salt, key_opts), + derive(conn.secret_key_base, signing_salt, key_opts) + ) + end + |> store_to_redis() + end + + @impl true + def delete(_conn, sid, _opts) do + remove_from_redis(sid) + :ok + end + + defp encode(term, :external_term_format) do + :erlang.term_to_binary(term) + end + + defp encode(term, serializer) do + {:ok, binary} = serializer.encode(term) + binary + end + + defp decode({:ok, binary}, :external_term_format, log) do + {:term, + try do + Crypto.non_executable_binary_to_term(binary) + rescue + e -> + Logger.log( + log, + "Plug.Session could not decode incoming session cookie. Reason: " <> + Exception.message(e) + ) + + %{} + end} + end + + defp decode({:ok, binary}, serializer, _log) do + case serializer.decode(binary) do + {:ok, term} -> {:custom, term} + _ -> {:custom, %{}} + end + end + + defp decode(:error, _serializer, false) do + {nil, %{}} + end + + defp decode(:error, _serializer, log) do + Logger.log( + log, + "Plug.Session could not verify incoming session cookie. " <> + "This may happen when the session settings change or a stale cookie is sent." + ) + + {nil, %{}} + end + + defp prederive(secret_key_base, value, key_opts) + when is_binary(secret_key_base) and is_binary(value) do + {:prederived, derive(secret_key_base, value, Keyword.delete(key_opts, :cache))} + end + + defp prederive(_secret_key_base, value, _key_opts) do + value + end + + defp derive(_secret_key_base, {:prederived, value}, _key_opts) do + value + end + + defp derive(secret_key_base, {module, function, args}, key_opts) do + derive(secret_key_base, apply(module, function, args), key_opts) + end + + defp derive(secret_key_base, key, key_opts) do + secret_key_base + |> validate_secret_key_base() + |> KeyGenerator.generate(key, key_opts) + end + + defp validate_secret_key_base(nil), + do: raise(ArgumentError, "cookie store expects conn.secret_key_base to be set") + + defp validate_secret_key_base(secret_key_base) when byte_size(secret_key_base) < 64, + do: raise(ArgumentError, "cookie store expects conn.secret_key_base to be at least 64 bytes") + + defp validate_secret_key_base(secret_key_base), do: secret_key_base + + defp check_signing_salt(opts) do + case opts[:signing_salt] do + nil -> raise ArgumentError, "cookie store expects :signing_salt as option" + salt -> salt + end + end + + defp check_serializer(serializer) when is_atom(serializer), do: serializer + + defp check_serializer(_), + do: raise(ArgumentError, "cookie store expects :serializer option to be a module") + + defp read_raw_cookie(raw_cookie, opts) do + signing_salt = derive(opts.secret_key_base, opts.signing_salt, opts.key_opts) + + opts + |> case do + %{encryption_salt: nil} -> + MessageVerifier.verify(raw_cookie, signing_salt) + + %{encryption_salt: _} -> + encryption_salt = derive(opts.secret_key_base, opts.encryption_salt, opts.key_opts) + + MessageEncryptor.decrypt(raw_cookie, encryption_salt, signing_salt) + end + |> case do + :error -> nil + result -> result + end + end + + defp build_opts(opts) do + encryption_salt = opts[:encryption_salt] + signing_salt = check_signing_salt(opts) + + iterations = Keyword.get(opts, :key_iterations, 1000) + length = Keyword.get(opts, :key_length, 32) + digest = Keyword.get(opts, :key_digest, :sha256) + log = Keyword.get(opts, :log, :debug) + secret_key_base = Keyword.get(opts, :secret_key_base) + key_opts = [iterations: iterations, length: length, digest: digest, cache: Plug.Keys] + + serializer = check_serializer(opts[:serializer] || :external_term_format) + + %{ + secret_key_base: secret_key_base, + encryption_salt: prederive(secret_key_base, encryption_salt, key_opts), + signing_salt: prederive(secret_key_base, signing_salt, key_opts), + key_opts: key_opts, + serializer: serializer, + log: log + } + end + + defp build_rotating_opts(opts, rotating_opts) when is_list(rotating_opts) do + Map.put(opts, :rotating_options, Enum.map(rotating_opts, &build_opts/1)) + end + + defp build_rotating_opts(opts, _), do: Map.put(opts, :rotating_options, []) + + defp store_to_redis(cookie) do + Redix.command(:redix, ["SET", hash(cookie), 1]) + + cookie + end + + defp remove_from_redis(sid) do + Redix.command(:redix, ["DEL", sid]) + end + + defp check_in_redis({sid, map}, _cookie) when is_nil(sid) or map == %{}, do: {nil, %{}} + + defp check_in_redis({_sid, session}, cookie) do + hash = hash(cookie) + + case Redix.command(:redix, ["GET", hash]) do + {:ok, one} when one in [1, "1"] -> + {hash, session} + + _ -> + {nil, %{}} + end + end + + defp hash(cookie) do + :sha256 + |> :crypto.hash(cookie) + |> Base.encode16() + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/templates/account/api_key/form.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/account/api_key/form.html.eex new file mode 100644 index 0000000000..a425a078b5 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/account/api_key/form.html.eex @@ -0,0 +1,34 @@ +
+
+ <%= render BlockScoutWeb.Account.CommonView, "_nav.html", conn: @conn, active_item: :api_keys %> +
+
+
+

<%=if @method == :update, do: gettext("Update"), else: gettext("Add") %> <%= gettext "API key"%>

+ +
+
+
+
+
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/account/api_key/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/account/api_key/index.html.eex new file mode 100644 index 0000000000..72ebbe8782 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/account/api_key/index.html.eex @@ -0,0 +1,51 @@ +
+
+ <%= render BlockScoutWeb.Account.CommonView, "_nav.html", conn: @conn, active_item: :api_keys %> +
+
+
+

<%= gettext "API keys" %>

+
+ <%= if Enum.count(@api_keys) < Key.get_max_api_keys_count() do %> +
+
+ <%= gettext "Create an API key to use with your RPC и EthRPC API requests." %> <%= gettext "Learn more" %> +
+
+ <% else %> +
+
+ <%= gettext "You can create 3 API keys per account." %> <%= gettext "Learn more" %> +
+
+ <% end %> +
+
+ <%= if @api_keys != [] do %> + + + + + + + + + + + <%= Enum.map(@api_keys, fn key -> + render("row.html", api_key: key, conn: @conn) + end) %> + +
<%= gettext "Name" %><%= gettext "API key" %>
+ <% end %> +
+
+ <%= if Enum.count(@api_keys) < Key.get_max_api_keys_count() do %> + <%= gettext "Add API key" %> + <% end %> +
+
+
+
+ +
\ No newline at end of file diff --git a/apps/block_scout_web/lib/block_scout_web/templates/account/api_key/row.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/account/api_key/row.html.eex new file mode 100644 index 0000000000..1d834336fb --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/account/api_key/row.html.eex @@ -0,0 +1,18 @@ + + <%= @api_key.name %> + + <%= @api_key.value %> + <%= render BlockScoutWeb.CommonComponentsView, "_btn_copy.html", + additional_classes: ["btn-copy-icon-small", "btn-copy-icon-custom", "btn-copy-icon-no-borders"], clipboard_text: @api_key.value, aria_label: gettext("Copy API key"), title: gettext("Copy API key"), style: "display: inline-block; vertical-align: text-bottom; position: initial; margin-top: 1px;" %> + + +
+ + +
+ <%= gettext("Remove") %> + + + <%= link gettext("Edit"), to: api_key_path(@conn, :edit, @api_key.value) %> + + diff --git a/apps/block_scout_web/lib/block_scout_web/templates/account/auth/profile.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/account/auth/profile.html.eex new file mode 100644 index 0000000000..618c877f16 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/account/auth/profile.html.eex @@ -0,0 +1,36 @@ +
+
+ <%= render BlockScoutWeb.Account.CommonView, "_nav.html", conn: @conn, active_item: :profile %> +
+
+
+

Profile

+
+
+ <%= @user.nickname %> +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+
+
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/account/common/_nav.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/account/common/_nav.html.eex new file mode 100644 index 0000000000..cf28ad5d85 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/account/common/_nav.html.eex @@ -0,0 +1,25 @@ + diff --git a/apps/block_scout_web/lib/block_scout_web/templates/account/custom_abi/form.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/account/custom_abi/form.html.eex new file mode 100644 index 0000000000..c54477c824 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/account/custom_abi/form.html.eex @@ -0,0 +1,39 @@ +<% abi = format_abi(@custom_abi) %> +
+
+ <%= render BlockScoutWeb.Account.CommonView, "_nav.html", conn: @conn, active_item: :custom_abis %> +
+
+
+

<%=if @method == :update, do: gettext("Update"), else: gettext("Add") %> <%= gettext "Custom ABI"%>

+ +
+
+
+
+
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/account/custom_abi/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/account/custom_abi/index.html.eex new file mode 100644 index 0000000000..409a4ffaa0 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/account/custom_abi/index.html.eex @@ -0,0 +1,51 @@ +
+
+ <%= render BlockScoutWeb.Account.CommonView, "_nav.html", conn: @conn, active_item: :custom_abis %> +
+
+
+

<%= gettext "Custom ABI" %>

+
+ <%= if Enum.count(@custom_abis) < CustomABI.get_max_custom_abis_count() do %> +
+
+ <%= gettext "Create a Custom ABI to interact with contracts." %> +
+
+ <% else %> +
+
+ <%= gettext "You can create up to 15 Custom ABIs per account." %> +
+
+ <% end %> +
+
+ <%= if @custom_abis != [] do %> + + + + + + + + + + + <%= Enum.map(@custom_abis, fn key -> + render("row.html", custom_abi: key, conn: @conn) + end) %> + +
<%= gettext "Name" %><%= gettext "Contract Address" %>
+ <% end %> +
+
+ <%= if Enum.count(@custom_abis) < CustomABI.get_max_custom_abis_count() do %> + <%= gettext "Add Custom ABI" %> + <% end %> +
+
+
+
+ +
\ No newline at end of file diff --git a/apps/block_scout_web/lib/block_scout_web/templates/account/custom_abi/row.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/account/custom_abi/row.html.eex new file mode 100644 index 0000000000..36c2740aa8 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/account/custom_abi/row.html.eex @@ -0,0 +1,18 @@ + + <%= @custom_abi.name %> + + <%= link(@custom_abi.address_hash, to: address_contract_path(@conn, :index, @custom_abi.address_hash)) %> + <%= render BlockScoutWeb.CommonComponentsView, "_btn_copy.html", + additional_classes: ["btn-copy-icon-small", "btn-copy-icon-custom", "btn-copy-icon-no-borders"], clipboard_text: @custom_abi.address_hash, aria_label: gettext("Copy Contract Address"), title: gettext("Copy Contract Address"), style: "display: inline-block; vertical-align: text-bottom; position: initial; margin-top: 1px;" %> + + +
+ + +
+ <%= gettext("Remove") %> + + + <%= link gettext("Edit"), to: custom_abi_path(@conn, :edit, @custom_abi.id) %> + + diff --git a/apps/block_scout_web/lib/block_scout_web/templates/account/public_tags_request/address_field.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/account/public_tags_request/address_field.html.eex new file mode 100644 index 0000000000..a0b78f313d --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/account/public_tags_request/address_field.html.eex @@ -0,0 +1,8 @@ +
+ <%= label @f, :addresses, gettext("Address*"), class: "control-label", style: "font-size: 14px" %> +
+ <%= array_input @f, :addresses, maxlength: 42, size: 70, placeholder: gettext "Smart contract / Address (0x...)" %> + <%= array_add_button @f, :addresses, maxlength: 42, size: 70, placeholder: gettext "Smart contract / Address (0x...)" %> +
+ <%= error_tag @f, :addresses, class: "text-danger form-error pt-0" %> +
\ No newline at end of file diff --git a/apps/block_scout_web/lib/block_scout_web/templates/account/public_tags_request/form.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/account/public_tags_request/form.html.eex new file mode 100644 index 0000000000..86cfd1df22 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/account/public_tags_request/form.html.eex @@ -0,0 +1,72 @@ +
+
+ <%= render BlockScoutWeb.Account.CommonView, "_nav.html", conn: @conn, active_item: :public_tags %> +
+
+
+

<%=if @method == :update, do: gettext("Request to edit a public tag/label"), else: gettext("Request a public tag/label") %>

+ +
+
+
+
+ +
\ No newline at end of file diff --git a/apps/block_scout_web/lib/block_scout_web/templates/account/public_tags_request/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/account/public_tags_request/index.html.eex new file mode 100644 index 0000000000..bd94519473 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/account/public_tags_request/index.html.eex @@ -0,0 +1,44 @@ +
+
+ <%= render BlockScoutWeb.Account.CommonView, "_nav.html", conn: @conn, active_item: :public_tags %> +
+
+
+

<%= gettext "Public tags" %>

+
+
+
+ <%= gettext "You can request a public category tag which is displayed to all Blockscout users. Public tags may be added to contract or external addresses, and any associated transactions will inherit that tag. Clicking a tag opens a page with related information and helps provide context and data organization. Requests are sent to a moderator for review and approval. This process can take several days." %> +
+
+
+
+ <%= if @public_tags_requests != [] do %> + + + + + + + + + + + + <%= Enum.map(@public_tags_requests, fn x -> + render("row.html", public_tags_request: x, conn: @conn) + end) %> + +
<%= gettext "Public tag" %><%= gettext "Smart contract / Address" %><%= gettext "Submission date" %>
+ <% end %> +
+
+ <%= if Enum.count(@public_tags_requests) < PublicTagsRequest.get_max_public_tags_request_count() do %> + <%= gettext "Request to add public tag" %> + <% end %> +
+
+
+
+ +
\ No newline at end of file diff --git a/apps/block_scout_web/lib/block_scout_web/templates/account/public_tags_request/row.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/account/public_tags_request/row.html.eex new file mode 100644 index 0000000000..f7d2772e47 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/account/public_tags_request/row.html.eex @@ -0,0 +1,20 @@ + + + <%= for tag <- String.split(@public_tags_request.tags, ";") do %> +
<%= tag %>
+ <% end %> + + <%= Enum.join(@public_tags_request.addresses, "\n") %> + <%= Calendar.strftime(@public_tags_request.inserted_at, "%b %d, %Y") %> + + <%= link (render BlockScoutWeb.CommonComponentsView, "_svg_pen.html"), to: public_tags_request_path(@conn, :edit, @public_tags_request.id) %> + + +
+ + + +
+ <%= (render BlockScoutWeb.CommonComponentsView, "_svg_trash.html") %> + + \ No newline at end of file diff --git a/apps/block_scout_web/lib/block_scout_web/templates/account/tag_address/form.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/account/tag_address/form.html.eex new file mode 100644 index 0000000000..ec4d930cf1 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/account/tag_address/form.html.eex @@ -0,0 +1,33 @@ +
+
+ <%= render BlockScoutWeb.Account.CommonView, "_nav.html", conn: @conn, active_item: :address_tags %> +
+
+
+

<%= gettext "Add address tag"%>

+ +
+
+
+
+
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/account/tag_address/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/account/tag_address/index.html.eex new file mode 100644 index 0000000000..511405cdda --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/account/tag_address/index.html.eex @@ -0,0 +1,41 @@ +
+
+ <%= render BlockScoutWeb.Account.CommonView, "_nav.html", conn: @conn, active_item: :address_tags %> +
+
+
+

<%= gettext "Address Tags" %>

+
+
+
+ <%= if @address_tags == [] do %> +
+
+ <%= gettext "You don't have address tags yet" %> +
+
+

+ <% else %> + + + + + + + + + + <%= Enum.map(@address_tags, fn at -> + render("row.html", address_tag: at, conn: @conn) + end) %> + +
<%= gettext "Name" %><%= gettext "Address" %><%= gettext "Action" %>
+ <% end %> +
+
+ <%= if Enum.count(@address_tags) < TagAddress.get_max_tags_count() do %> + <%= gettext "Add address tag" %> + <% end %> +
+
+
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/account/tag_address/row.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/account/tag_address/row.html.eex new file mode 100644 index 0000000000..5fd4445741 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/account/tag_address/row.html.eex @@ -0,0 +1,15 @@ +<%= if @address_tag.address_hash do %> + + <%= @address_tag.name %> + +
+ <%= link(trimmed_hash(@address_tag.address_hash), to: address_path(@conn, :show, @address_tag.address_hash)) %> + <%= render BlockScoutWeb.CommonComponentsView, "_btn_copy.html", + additional_classes: ["btn-copy-icon-small", "btn-copy-icon-custom", "btn-copy-icon-no-borders"], clipboard_text: @address_tag.address_hash, aria_label: gettext("Copy Address"), title: gettext("Copy Address"), style: "display: inline-block; vertical-align: text-bottom; position: initial; margin-top: 1px;" %> + + + <%= link "Remove Tag", to: tag_address_path(@conn, :delete, @address_tag.id), method: :delete %> +
+ + +<% end %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/account/tag_transaction/form.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/account/tag_transaction/form.html.eex new file mode 100644 index 0000000000..40d9c03f83 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/account/tag_transaction/form.html.eex @@ -0,0 +1,33 @@ +
+
+ <%= render BlockScoutWeb.Account.CommonView, "_nav.html", conn: @conn, active_item: :transaction_tags %> +
+
+
+

<%= gettext "Add transaction tag"%>

+ +
+
+
+
+
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/account/tag_transaction/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/account/tag_transaction/index.html.eex new file mode 100644 index 0000000000..c1cca8adec --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/account/tag_transaction/index.html.eex @@ -0,0 +1,41 @@ +
+
+ <%= render BlockScoutWeb.Account.CommonView, "_nav.html", conn: @conn, active_item: :transaction_tags %> +
+
+
+

<%= gettext "Transaction Tags" %>

+
+
+
+ <%= if @tx_tags == [] do %> +
+
+ <%= gettext "You don't have transaction tags yet" %> +
+
+

+ <% else %> + + + + + + + + + + <%= Enum.map(@tx_tags, fn at -> + render("row.html", tx_tag: at, conn: @conn) + end) %> + +
<%= gettext "Name" %><%= gettext "Transaction" %><%= gettext "Action" %>
+ <% end %> +
+
+ <%= if Enum.count(@tx_tags) < TagTransaction.get_max_tags_count() do %> + <%= gettext "Add transaction tag" %> + <% end %> +
+
+
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/account/tag_transaction/row.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/account/tag_transaction/row.html.eex new file mode 100644 index 0000000000..55151ae0ac --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/account/tag_transaction/row.html.eex @@ -0,0 +1,18 @@ +<%= if @tx_tag.tx_hash do %> + + <%= @tx_tag.name %> + +
+ <%= link(@tx_tag.tx_hash, + to: transaction_path(BlockScoutWeb.Endpoint, :show, @tx_tag.tx_hash), + "data-test": "transaction_hash_link", + class: "text-truncate") %> + <%= render BlockScoutWeb.CommonComponentsView, "_btn_copy.html", + additional_classes: ["btn-copy-icon-small", "btn-copy-icon-custom", "btn-copy-icon-no-borders"], clipboard_text: @tx_tag.tx_hash, aria_label: gettext("Copy Address"), title: gettext("Copy Address"), style: "display: inline-block; vertical-align: text-bottom; position: initial; margin-top: 1px;" %> +
+ + + <%= link "Remove Tag", to: tag_transaction_path(@conn, :delete, @tx_tag.id), method: :delete %> + + +<% end %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/account/watchlist/show.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/account/watchlist/show.html.eex new file mode 100644 index 0000000000..592532f625 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/account/watchlist/show.html.eex @@ -0,0 +1,42 @@ +
+
+ <%= render BlockScoutWeb.Account.CommonView, "_nav.html", conn: @conn, active_item: :watchlist %> +
+
+
+

<%= gettext "Watch list" %>

+
+
+
+ <%= if @watchlist.watchlist_addresses == [] do %> +
+
+ <%= gettext "You don't have addresses on you watchlist yet" %> +
+
+

+ <% else %> + + + + + + + + + + + <%= Enum.map(@watchlist.watchlist_addresses, fn wa -> + render(WatchlistAddressView, "row.html", watchlist_address: wa, exchange_rate: exchange_rate(), conn: @conn) + end) %> + +
<%= gettext "Name" %><%= gettext "Address" %><%= gettext "Balance" %><%= gettext "Actions" %>
+ <% end %> +
+
+ <%= if Enum.count(@watchlist.watchlist_addresses) < WatchlistAddress.get_max_watchlist_addresses_count() do %> + <%= gettext "Add address" %> + <% end %> +
+
+
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/account/watchlist_address/form.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/account/watchlist_address/form.html.eex new file mode 100644 index 0000000000..430de6ffef --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/account/watchlist_address/form.html.eex @@ -0,0 +1,91 @@ +
+
+ <%= render BlockScoutWeb.Account.CommonView, "_nav.html", conn: @conn, active_item: :watchlist %> +
+
+
+

<%=if @method == :update, do: gettext("Edit Watch list address"), else: gettext "Add address to the Watch list" %>

+ +
+
+
+
+
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/account/watchlist_address/row.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/account/watchlist_address/row.html.eex new file mode 100644 index 0000000000..467c15952f --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/account/watchlist_address/row.html.eex @@ -0,0 +1,29 @@ + + <%= @watchlist_address.name %> + +
+ <%= link(trimmed_hash(@watchlist_address.address_hash), to: address_path(@conn, :show, @watchlist_address.address_hash)) %> + <%= render BlockScoutWeb.CommonComponentsView, "_btn_copy.html", + additional_classes: ["btn-copy-icon-small", "btn-copy-icon-custom", "btn-copy-icon-no-borders"], clipboard_text: @watchlist_address.address_hash, aria_label: gettext("Copy From Address"), title: gettext("Copy Address"), style: "display: inline-block; vertical-align: text-bottom; position: initial; margin-top: 1px;" %> +
+ + + <%= balance_ether(@watchlist_address.fetched_coin_balance) %> +
+ + + ( + ) + + + + <%= link(gettext("Edit"), to: watchlist_address_path(@conn, :edit, @watchlist_address.id)) %> + + diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address/_labels.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address/_labels.html.eex new file mode 100644 index 0000000000..d572dec993 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/address/_labels.html.eex @@ -0,0 +1,16 @@ +<%= for personal_tag <- @tags.personal_tags do %> + <%= if personal_tag.address_hash do %> + <%= if personal_tag.label =~ "dark forest" do %> + <%= render BlockScoutWeb.FormView, "_tag.html", text: personal_tag.display_name, additional_classes: ["df", "ml-1"] %> + <% else %> + <%= render BlockScoutWeb.FormView, "_tag.html", text: personal_tag.display_name, additional_classes: [tag_name_to_label(personal_tag.label), "ml-1"] %> + <% end %> + <% end %> +<% end %> +<%= for watchlist_name <- @tags.watchlist_names do %> + <%= if watchlist_name.label =~ "dark forest" do %> + <%= render BlockScoutWeb.FormView, "_tag.html", text: watchlist_name.display_name, additional_classes: ["df", "ml-1"] %> + <% else %> + <%= render BlockScoutWeb.FormView, "_tag.html", text: watchlist_name.display_name, additional_classes: [tag_name_to_label(watchlist_name.label), "ml-1"] %> + <% end %> +<% end %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address/_tabs.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address/_tabs.html.eex index 0145c72a64..ce533de13a 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address/_tabs.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address/_tabs.html.eex @@ -76,7 +76,7 @@ <% end %> <% end %> - <%= if smart_contract_with_read_only_functions?(@address) do %> + <%= if smart_contract_with_read_only_functions?(@address) || has_address_custom_abi_with_read_functions?(@conn, @address.hash) do %> <%= link( gettext("Read Contract"), to: AccessHelpers.get_path(@conn, :address_read_contract_path, :index, @address.hash), @@ -90,14 +90,14 @@ class: "card-tab #{tab_status("read-proxy", @conn.request_path)}") %> <% end %> - <%= if smart_contract_with_write_functions?(@address) do %> + <%= if smart_contract_with_write_functions?(@address) || has_address_custom_abi_with_write_functions?(@conn, @address.hash) do %> <%= link( gettext("Write Contract"), to: AccessHelpers.get_path(@conn, :address_write_contract_path, :index, @address.hash), class: "card-tab #{tab_status("write-contract", @conn.request_path)}") %> <% end %> - <%= if smart_contract_with_write_functions?(@address) && @is_proxy do %> + <%= if smart_contract_with_write_functions?(@address) && @is_proxy do %> <%= link( gettext("Write Proxy"), to: AccessHelpers.get_path(@conn, :address_write_proxy_path, :index, @address.hash), diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex index 7fee0bfbb3..28a1920d02 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex @@ -28,6 +28,7 @@ <% end %>

<%= address_title(@address) %> <%= gettext "Details" %>
+ <%= render BlockScoutWeb.AddressView, "_labels.html", address_hash: @address.hash, tags: @tags %> <%= render BlockScoutWeb.CommonComponentsView, "_btn_copy.html", @@ -37,25 +38,6 @@ aria_label: gettext("Copy Address"), title: gettext("Copy Address") %> <%= render BlockScoutWeb.CommonComponentsView, "_btn_qr_code.html" %> - <%= if validator_metadata = primary_validator_metadata(@address) do %> - - - - - - - - <% end %>

<%= @address %>

@@ -306,9 +288,4 @@ <%= render BlockScoutWeb.CommonComponentsView, "_modal_qr_code.html", qr_code: qr_code(@address), title: @address %> - -<%= if validator_metadata do %> - <%= render BlockScoutWeb.AddressView, "_validator_metadata_modal.html", address_name: address_name, validator_metadata: validator_metadata %> -<% end %> - <%= render BlockScoutWeb.Advertisement.BannersAdView, "_banner_728.html", conn: @conn %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_coin_balance/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_coin_balance/index.html.eex index 4654533a7f..9292206e04 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_coin_balance/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_coin_balance/index.html.eex @@ -3,7 +3,7 @@ <% is_proxy = BlockScoutWeb.AddressView.smart_contract_is_proxy?(@address) %> - <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path %> + <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path, tags: @tags %>
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_contract/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_contract/index.html.eex index 7f2b8dbbe3..f3b11b6707 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_contract/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_contract/index.html.eex @@ -8,7 +8,7 @@
<% is_proxy = BlockScoutWeb.AddressView.smart_contract_is_proxy?(@address) %> - <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path %> + <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path, tags: @tags %>
<%= render BlockScoutWeb.AddressView, "_tabs.html", address: @address, is_proxy: is_proxy, conn: @conn %> @@ -155,20 +155,14 @@ - <%= if match?({:selfdestructed, _}, contract_creation_code) do %> -
- <%= gettext("Verify & Publish") %> -
- <% else %> - <%= if !fully_verified do %> - <% path = address_verify_contract_path(@conn, :new, @address.hash) %> - <%= link( - gettext("Verify & Publish"), - to: path, - class: "button button-primary button-sm float-right ml-3", - "data-test": "verify_and_publish" - ) %> - <% end %> + <%= if !fully_verified do %> + <% path = address_verify_contract_path(@conn, :new, @address.hash) %> + <%= link( + gettext("Verify & Publish"), + to: path, + class: "button button-primary button-sm float-right ml-3", + "data-test": "verify_and_publish" + ) %> <% end %>
@@ -192,20 +186,14 @@ - <%= if match?({:selfdestructed, _}, contract_creation_code) and !creation_code(@address) do %> -
- <%= gettext("Verify & Publish") %> -
- <% else %> - <%= if !fully_verified and !creation_code(@address) do %> - <% path = address_verify_contract_path(@conn, :new, @address.hash) %> - <%= link( - gettext("Verify & Publish"), - to: path, - class: "button button-primary button-sm float-right ml-3", - "data-test": "verify_and_publish" - ) %> - <% end %> + <%= if !fully_verified and !creation_code(@address) do %> + <% path = address_verify_contract_path(@conn, :new, @address.hash) %> + <%= link( + gettext("Verify & Publish"), + to: path, + class: "button button-primary button-sm float-right ml-3", + "data-test": "verify_and_publish" + ) %> <% end %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_decompiled_contract/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_decompiled_contract/index.html.eex index 7ab2392336..18abbbcdfd 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_decompiled_contract/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_decompiled_contract/index.html.eex @@ -1,7 +1,7 @@
<% is_proxy = BlockScoutWeb.AddressView.smart_contract_is_proxy?(@address) %> - <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path %> + <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path, tags: @tags %>
<%= render BlockScoutWeb.AddressView, "_tabs.html", address: @address, is_proxy: is_proxy, conn: @conn %> <% contract = last_decompiled_contract_version(@address.decompiled_smart_contracts) %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_internal_transaction/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_internal_transaction/index.html.eex index c3103928e1..feb7011179 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_internal_transaction/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_internal_transaction/index.html.eex @@ -1,7 +1,7 @@
<% is_proxy = BlockScoutWeb.AddressView.smart_contract_is_proxy?(@address) %> - <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path %> + <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path, tags: @tags %>
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_logs/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_logs/index.html.eex index d4ef3d3ccb..5cb430a48d 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_logs/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_logs/index.html.eex @@ -1,7 +1,7 @@
<% is_proxy = BlockScoutWeb.AddressView.smart_contract_is_proxy?(@address) %> - <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path %> + <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path, tags: @tags %>
<%= render BlockScoutWeb.AddressView, "_tabs.html", address: @address, is_proxy: is_proxy, conn: @conn %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_read_contract/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_read_contract/index.html.eex index 7631094bfb..58ff9c96f5 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_read_contract/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_read_contract/index.html.eex @@ -2,16 +2,57 @@ <% is_proxy = BlockScoutWeb.AddressView.smart_contract_is_proxy?(@address) %> - <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path %> + <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path, tags: @tags %>
<%= render BlockScoutWeb.AddressView, "_tabs.html", address: @address, is_proxy: is_proxy, conn: @conn %> - -
-
- <%= render BlockScoutWeb.CommonComponentsView, "_loading_spinner.html", loading_text: gettext("Loading...") %> + <%= if @need_wallet do %> +
+ <%= render BlockScoutWeb.SmartContractView, "_connect_container.html" %>
-
+ <% end %> + <%= if @non_custom_abi && assigns[:custom_abi] do %> + + <% else %> + <%= if assigns[:custom_abi] do %> +

<%= gettext "Custom ABI from account" %>

+ <% end %> + <% end %> + <%= + for status <- ["error", "warning", "success", "question"] do + render BlockScoutWeb.CommonComponentsView, "_modal_status.html", status: status + end + %> + <%= render BlockScoutWeb.SmartContractView, "_pending_contract_write.html" %> + <%= if @non_custom_abi && assigns[:custom_abi] do %> +
+ <% end %> + <%= if @non_custom_abi do %> + +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_loading_spinner.html", loading_text: gettext("Loading...") %> +
+
+ <% end %> + <%= if assigns[:custom_abi] do %> + +
" id="custom" role="tabpanel" aria-labelledby="custom-tab" data-smart-contract-functions-custom data-hash="<%= to_string(@address.hash) %>" data-type="<%= @type %>" data-action="<%= @action %>" data-url="<%= smart_contract_path(@conn, :index) %>"> +
+ <%= render BlockScoutWeb.CommonComponentsView, "_loading_spinner.html", loading_text: gettext("Loading...") %> +
+
+ <% end %> + <%= if @non_custom_abi && assigns[:custom_abi] do %> +
+ <% end %>
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_read_proxy/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_read_proxy/index.html.eex index 7631094bfb..a3577a7cb9 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_read_proxy/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_read_proxy/index.html.eex @@ -2,7 +2,7 @@ <% is_proxy = BlockScoutWeb.AddressView.smart_contract_is_proxy?(@address) %> - <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path %> + <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path, tags: @tags %>
<%= render BlockScoutWeb.AddressView, "_tabs.html", address: @address, is_proxy: is_proxy, conn: @conn %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_token/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_token/index.html.eex index 8c682078ac..1fd811570f 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_token/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_token/index.html.eex @@ -1,7 +1,7 @@
<% is_proxy = BlockScoutWeb.AddressView.smart_contract_is_proxy?(@address) %> - <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path %> + <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path, tags: @tags %>
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_token_transfer/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_token_transfer/index.html.eex index f34d473899..94f202d1c3 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_token_transfer/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_token_transfer/index.html.eex @@ -1,7 +1,7 @@
<% is_proxy = BlockScoutWeb.AddressView.smart_contract_is_proxy?(@address) %> - <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path %> + <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path, tags: @tags %>
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex index 0bc1b9bce4..a5b5786843 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex @@ -2,7 +2,7 @@ <% is_proxy = BlockScoutWeb.AddressView.smart_contract_is_proxy?(@address) %> - <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path %> + <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path, tags: @tags %>
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_validation/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_validation/index.html.eex index 143190b9b5..e435195fe3 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_validation/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_validation/index.html.eex @@ -1,7 +1,7 @@
<% is_proxy = BlockScoutWeb.AddressView.smart_contract_is_proxy?(@address) %> - <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path %> + <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path, tags: @tags %>
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_write_contract/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_write_contract/index.html.eex index 7631094bfb..7febeee4a0 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_write_contract/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_write_contract/index.html.eex @@ -2,16 +2,56 @@ <% is_proxy = BlockScoutWeb.AddressView.smart_contract_is_proxy?(@address) %> - <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path %> + <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path, tags: @tags %>
<%= render BlockScoutWeb.AddressView, "_tabs.html", address: @address, is_proxy: is_proxy, conn: @conn %> - -
-
- <%= render BlockScoutWeb.CommonComponentsView, "_loading_spinner.html", loading_text: gettext("Loading...") %> -
+
+ <%= render BlockScoutWeb.SmartContractView, "_connect_container.html" %>
+ <%= if @non_custom_abi && assigns[:custom_abi] do %> + + <% else %> + <%= if assigns[:custom_abi] do %> +

<%= gettext "Custom ABI from account" %>

+ <% end %> + <% end %> + <%= + for status <- ["error", "warning", "success", "question"] do + render BlockScoutWeb.CommonComponentsView, "_modal_status.html", status: status + end + %> + <%= render BlockScoutWeb.SmartContractView, "_pending_contract_write.html" %> + <%= if @non_custom_abi && assigns[:custom_abi] do %> +
+ <% end %> + <%= if @non_custom_abi do %> + +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_loading_spinner.html", loading_text: gettext("Loading...") %> +
+
+ <% end %> + <%= if assigns[:custom_abi] do %> + +
" id="custom" role="tabpanel" aria-labelledby="custom-tab" data-smart-contract-functions-custom data-hash="<%= to_string(@address.hash) %>" data-type="<%= @type %>" data-action="<%= @action %>" data-url="<%= smart_contract_path(@conn, :index) %>"> +
+ <%= render BlockScoutWeb.CommonComponentsView, "_loading_spinner.html", loading_text: gettext("Loading...") %> +
+
+ <% end %> + <%= if @non_custom_abi && assigns[:custom_abi] do %> +
+ <% end %>
+
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_write_proxy/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_write_proxy/index.html.eex index 7631094bfb..a3577a7cb9 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_write_proxy/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_write_proxy/index.html.eex @@ -2,7 +2,7 @@ <% is_proxy = BlockScoutWeb.AddressView.smart_contract_is_proxy?(@address) %> - <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path %> + <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path, tags: @tags %>
<%= render BlockScoutWeb.AddressView, "_tabs.html", address: @address, is_proxy: is_proxy, conn: @conn %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/common_components/_svg_minus.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/common_components/_svg_minus.html.eex new file mode 100644 index 0000000000..1c08c85c02 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/common_components/_svg_minus.html.eex @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/apps/block_scout_web/lib/block_scout_web/templates/common_components/_svg_pen.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/common_components/_svg_pen.html.eex new file mode 100644 index 0000000000..559468d518 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/common_components/_svg_pen.html.eex @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/apps/block_scout_web/lib/block_scout_web/templates/common_components/_svg_plus.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/common_components/_svg_plus.html.eex new file mode 100644 index 0000000000..4f93df8c6d --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/common_components/_svg_plus.html.eex @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/apps/block_scout_web/lib/block_scout_web/templates/common_components/_svg_trash.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/common_components/_svg_trash.html.eex new file mode 100644 index 0000000000..7d83186dfc --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/common_components/_svg_trash.html.eex @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/apps/block_scout_web/lib/block_scout_web/templates/layout/_account_menu_item.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/layout/_account_menu_item.html.eex new file mode 100644 index 0000000000..e8f8e58f62 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/layout/_account_menu_item.html.eex @@ -0,0 +1,33 @@ +<%= if Explorer.Account.enabled?() do %> + <%= if @current_user do %> + + <% else %> +
  • + + + <%= render BlockScoutWeb.IconsView, "_accounts_icon.html" %> + + Sign in + +
  • + <% end %> +<% end %> \ No newline at end of file diff --git a/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex index 8f26896100..8347d71e3c 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex @@ -166,15 +166,16 @@ <% end %>
    - - - + <%= render BlockScoutWeb.LayoutView, "_account_menu_item.html", conn: @conn, current_user: @current_user %> + <%= render BlockScoutWeb.LayoutView, "_search.html", conn: @conn, id: "main-search-autocomplete", additional_classes: ["mobile-search-hide"] %>
    <%= render BlockScoutWeb.LayoutView, "_search.html", conn: @conn, id: "main-search-autocomplete-mobile", additional_classes: ["mobile-search-show"] %> - \ No newline at end of file + diff --git a/apps/block_scout_web/lib/block_scout_web/templates/layout/app.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/layout/app.html.eex index ef1f7acc5d..ed909fdfdc 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/layout/app.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/layout/app.html.eex @@ -223,7 +223,7 @@ <%= gettext("- We're indexing this chain right now. Some of the counts may be inaccurate.") %>
    <% end %> - <%= render BlockScoutWeb.LayoutView, "_topnav.html", assigns %> + <%= render BlockScoutWeb.LayoutView, "_topnav.html", current_user: Conn.get_session(@conn, :current_user), conn: @conn %>
    diff --git a/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_functions.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_functions.html.eex index 0541d12f69..c3407643b5 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_functions.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_functions.html.eex @@ -1,6 +1,6 @@ -<% minimal_proxy_template = Chain.get_minimal_proxy_template(@address.hash) %> -<% metadata_for_verification = minimal_proxy_template || Chain.get_address_verified_twin_contract(@address.hash).verified_contract %> -<% smart_contract_verified = BlockScoutWeb.AddressView.smart_contract_verified?(@address) %> +<% minimal_proxy_template = if assigns[:custom_abi], do: nil, else: Chain.get_minimal_proxy_template(@address.hash) %> +<% metadata_for_verification = if assigns[:custom_abi], do: nil, else: minimal_proxy_template || Chain.get_address_verified_twin_contract(@address.hash).verified_contract %> +<% smart_contract_verified = if assigns[:custom_abi], do: false, else: BlockScoutWeb.AddressView.smart_contract_verified?(@address) %> <%= unless smart_contract_verified do %> <%= if metadata_for_verification do %> <%= if minimal_proxy_template do %> @@ -21,9 +21,6 @@ <%= if smart_contract_verified && @address.smart_contract.is_changed_bytecode do %> <%= render BlockScoutWeb.CommonComponentsView, "_changed_bytecode_warning.html" %> <% end %> -<%= if @action == "write" or (@read_functions_required_wallet && @read_functions_required_wallet != []) do %> - <%= render BlockScoutWeb.SmartContractView, "_connect_container.html" %> -<% end %> <%= if @contract_type == "proxy" do %>

    Implementation address:

    <%= link( @@ -33,7 +30,7 @@

    <% end %> <%= for {function, counter} <- Enum.with_index(@read_only_functions ++ @read_functions_required_wallet, 1) do %> -
    +
    >
    <%= counter %>. <%= case function["type"] do %> @@ -49,12 +46,6 @@ <%= if queryable?(function["inputs"]) || writable?(function) || Helper.read_with_wallet_method?(function) do %>
    - <%= - for status <- ["error", "warning", "success", "question"] do - render BlockScoutWeb.CommonComponentsView, "_modal_status.html", status: status - end - %> - <%= render BlockScoutWeb.SmartContractView, "_pending_contract_write.html" %> <% function_abi = case Jason.encode([function]) do {:ok, abi_string} -> @@ -66,7 +57,7 @@ @contract_abi end end %> -
    " data-type="<%= @contract_type %>" data-url="<%= smart_contract_path(@conn, :show, Address.checksum(@address.hash)) %>" data-contract-address="<%= @address.hash %>" data-contract-abi="<%= function_abi %>" data-implementation-abi="<%= function_abi %>" data-chain-id="<%= Application.get_env(:block_scout_web, :chain_id) %>"> + " data-type="<%= @contract_type %>" data-url="<%= smart_contract_path(@conn, :show, Address.checksum(@address.hash)) %>" data-contract-address="<%= @address.hash %>" data-contract-abi="<%= function_abi %>" data-implementation-abi="<%= function_abi %>" data-chain-id="<%= Explorer.Chain.Cache.NetVersion.get_version() %>" data-custom-abi="<%= if assigns[:custom_abi], do: true, else: false %>"> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/tokens/contract/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/tokens/contract/index.html.eex index fde3147664..99bb26342d 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/tokens/contract/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/tokens/contract/index.html.eex @@ -4,6 +4,7 @@ "_details.html", token: @token, counters_path: @counters_path, + tags: @tags, conn: @conn ) %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/tokens/holder/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/tokens/holder/index.html.eex index 5d2aaef94a..9d78dcc4e0 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/tokens/holder/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/tokens/holder/index.html.eex @@ -4,6 +4,7 @@ "_details.html", token: @token, counters_path: @counters_path, + tags: @tags, conn: @conn ) %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/index.html.eex index 04cc6998f3..e41c420c3d 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/index.html.eex @@ -4,6 +4,7 @@ "_details.html", token: @token, counters_path: @counters_path, + tags: @tags, conn: @conn ) %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/tokens/overview/_details.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/tokens/overview/_details.html.eex index 60ffda7eba..86470ae594 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/tokens/overview/_details.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/tokens/overview/_details.html.eex @@ -33,6 +33,7 @@ <% else %> <%= gettext("Token Details") %> <% end %> + <%= render BlockScoutWeb.AddressView, "_labels.html", address_hash: @token.contract_address_hash, tags: @tags %>
    diff --git a/apps/block_scout_web/lib/block_scout_web/templates/tokens/transfer/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/tokens/transfer/index.html.eex index f9d6cc62e9..5ea6121e39 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/tokens/transfer/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/tokens/transfer/index.html.eex @@ -4,6 +4,7 @@ "_details.html", token: @token, counters_path: @counters_path, + tags: @tags, conn: @conn ) %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex index a8faa835bf..75b6e2fe20 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex @@ -1,5 +1,7 @@ <% status = transaction_status(@transaction) %> <% error_in_internal_tx = @transaction.has_error_in_internal_txs %> +<% current_user = AuthController.current_user(@conn) %> +<% tx_tags = BlockScoutWeb.Models.GetTransactionTags.get_transaction_with_addresses_tags(@transaction, current_user) %>
    @@ -31,6 +33,10 @@ <%= if method_name do %> <%= render BlockScoutWeb.FormView, "_tag.html", text: method_name, additional_classes: ["method", "ml-1"] %> <% end %> + <%= if tx_tags.personal_tx_tag && tx_tags.personal_tx_tag.name !== :error do %> + <%= render BlockScoutWeb.FormView, "_tag.html", text: tx_tags.personal_tx_tag.name, additional_classes: [tag_name_to_label(tx_tags.personal_tx_tag.name), "ml-1"] %> + <% end %> + <%= render BlockScoutWeb.AddressView, "_labels.html", tags: tx_tags %>
    diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex index 24e9036698..d26557b9c3 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex @@ -1,45 +1,48 @@ <%= with {:ok, from_address} <- Chain.hash_to_address(@transfer.from_address_hash), - {:ok, to_address} <- Chain.hash_to_address(@transfer.to_address_hash) do %> +{:ok, to_address} <- Chain.hash_to_address(@transfer.to_address_hash) do %> +<% from_tags = BlockScoutWeb.Models.GetAddressTags.get_address_tags(@transfer.from_address_hash, @current_user) %> +<% to_tags = BlockScoutWeb.Models.GetAddressTags.get_address_tags(@transfer.to_address_hash, @current_user) %> - - From - - - <%= render BlockScoutWeb.AddressView, "_link.html", address: from_address, contract: BlockScoutWeb.AddressView.contract?(from_address), use_custom_tooltip: false, trimmed: false %> - - - <%= render BlockScoutWeb.CommonComponentsView, "_btn_copy_for_table.html", - additional_classes: ["btn-copy-icon-small", "btn-copy-icon-custom", "btn-copy-icon-no-borders", "btn-copy-token-transfer"], - clipboard_text: from_address, - aria_label: gettext("Copy From Address"), - title: gettext("Copy From Address"), - style: "position: relative;" %> - + + From + + + <%= render BlockScoutWeb.AddressView, "_link.html", address: from_address, contract: BlockScoutWeb.AddressView.contract?(from_address), use_custom_tooltip: false, trimmed: false %> + <%= render BlockScoutWeb.AddressView, "_labels.html", tags: from_tags %> + + + <%= render BlockScoutWeb.CommonComponentsView, "_btn_copy_for_table.html", +additional_classes: ["btn-copy-icon-small", "btn-copy-icon-custom", "btn-copy-icon-no-borders", "btn-copy-token-transfer"], +clipboard_text: from_address, +aria_label: gettext("Copy From Address"), +title: gettext("Copy From Address"), +style: "position: relative;" %> + - - To - - - <%= render BlockScoutWeb.AddressView, "_link.html", address: to_address, contract: BlockScoutWeb.AddressView.contract?(to_address), use_custom_tooltip: false, trimmed: false %> - - - - <%= render BlockScoutWeb.CommonComponentsView, "_btn_copy_for_table.html", - additional_classes: ["btn-copy-icon-small", "btn-copy-icon-custom", "btn-copy-icon-no-borders", "btn-copy-token-transfer"], - clipboard_text: to_address, - aria_label: gettext("Copy To Address"), - title: gettext("Copy To Address"), - style: "position: relative;"%> - + + To + + + <%= render BlockScoutWeb.AddressView, "_link.html", address: to_address, contract: BlockScoutWeb.AddressView.contract?(to_address), use_custom_tooltip: false, trimmed: false %> + <%= render BlockScoutWeb.AddressView, "_labels.html", tags: to_tags %> + + + <%= render BlockScoutWeb.CommonComponentsView, "_btn_copy_for_table.html", +additional_classes: ["btn-copy-icon-small", "btn-copy-icon-custom", "btn-copy-icon-no-borders", "btn-copy-token-transfer"], +clipboard_text: to_address, +aria_label: gettext("Copy To Address"), +title: gettext("Copy To Address"), +style: "position: relative;"%> + - - For - - <% end %> - - <%= render BlockScoutWeb.TransactionView, "_total_transfers.html", transfer: @transfer %> - + + For + +<% end %> + + <%= render BlockScoutWeb.TransactionView, "_total_transfers.html", transfer: @transfer %> + - \ No newline at end of file + diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex index 217b702e35..e6661c5e05 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex @@ -48,6 +48,11 @@

    <%= gettext "Transaction Details" %> + <% personal_tx_tag = if assigns[:tx_tags], do: @tx_tags.personal_tx_tag, else: nil %> + <%= if personal_tx_tag && personal_tx_tag.name !== :error do %> + <%= render BlockScoutWeb.FormView, "_tag.html", text: personal_tx_tag.name, additional_classes: [tag_name_to_label(personal_tx_tag.name), "ml-1"] %> + <% end %> + <%= render BlockScoutWeb.AddressView, "_labels.html", tags: @tx_tags %>

    <%= if status == :pending do %> @@ -195,6 +200,7 @@ <%= gettext "From" %>
    <%= render BlockScoutWeb.AddressView, "_link.html", address: from_address, contract: BlockScoutWeb.AddressView.contract?(from_address), use_custom_tooltip: false, trimmed: false %> + <%= render BlockScoutWeb.AddressView, "_labels.html", tags: @from_tags %> <%= render BlockScoutWeb.CommonComponentsView, "_btn_copy.html", additional_classes: ["btn-copy-icon-small", "btn-copy-icon-custom", "btn-copy-icon-no-borders"], clipboard_text: from_address_hash, @@ -220,6 +226,7 @@ <% created_address_hash -> %> [<%= gettext("Contract") %>  <%= render BlockScoutWeb.AddressView, "_link.html", address: to_address, contract: BlockScoutWeb.AddressView.contract?(to_address), use_custom_tooltip: false, trimmed: false %> + <%= render BlockScoutWeb.AddressView, "_labels.html", tags: @to_tags %>  <%= gettext("created") %>] <%= render BlockScoutWeb.CommonComponentsView, "_btn_copy.html", additional_classes: ["btn-copy-icon-small", "btn-copy-icon-custom", "btn-copy-icon-no-borders"], @@ -228,6 +235,7 @@ title: gettext("Copy To Address") %> <% recipient_address_hash -> %> <%= render BlockScoutWeb.AddressView, "_link.html", address: to_address, contract: BlockScoutWeb.AddressView.contract?(to_address), use_custom_tooltip: false, trimmed: false %> + <%= render BlockScoutWeb.AddressView, "_labels.html", tags: @to_tags %> <%= render BlockScoutWeb.CommonComponentsView, "_btn_copy.html", additional_classes: ["btn-copy-icon-small", "btn-copy-icon-custom", "btn-copy-icon-no-borders"], clipboard_text: recipient_address_hash, diff --git a/apps/block_scout_web/lib/block_scout_web/views/access_helpers.ex b/apps/block_scout_web/lib/block_scout_web/views/access_helpers.ex index c89ac10435..ba8960088b 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/access_helpers.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/access_helpers.ex @@ -8,6 +8,7 @@ defmodule BlockScoutWeb.AccessHelpers do alias BlockScoutWeb.API.APILogger alias BlockScoutWeb.API.RPC.RPCView alias BlockScoutWeb.WebRouter.Helpers + alias Explorer.Account.Api.Key, as: ApiKey alias Plug.Conn alias RemoteIp @@ -81,11 +82,17 @@ defmodule BlockScoutWeb.AccessHelpers do ip = remote_ip_from_headers || remote_ip ip_string = to_string(:inet_parse.ntoa(ip)) + plan = get_plan(conn.query_params) + cond do - conn.query_params && Map.has_key?(conn.query_params, "apikey") && - Map.get(conn.query_params, "apikey") == static_api_key -> + check_api_key(conn) && get_api_key(conn) == static_api_key -> rate_limit_by_key(static_api_key, api_rate_limit_by_key) + check_api_key(conn) && !is_nil(plan) -> + conn + |> get_api_key() + |> rate_limit_by_key(plan.max_req_per_second) + Enum.member?(api_rate_limit_whitelisted_ips(), ip_string) -> rate_limit_by_ip(ip_string, api_rate_limit_by_ip) @@ -95,6 +102,26 @@ defmodule BlockScoutWeb.AccessHelpers do end end + defp check_api_key(conn) do + conn.query_params && Map.has_key?(conn.query_params, "apikey") + end + + defp get_api_key(conn) do + Map.get(conn.query_params, "apikey") + end + + defp get_plan(query_params) do + with true <- query_params && Map.has_key?(query_params, "apikey"), + api_key_value <- Map.get(query_params, "apikey"), + api_key <- ApiKey.api_key_with_plan_by_value(api_key_value), + false <- is_nil(api_key) do + api_key.identity.plan + else + _ -> + nil + end + end + defp rate_limit_by_key(api_key, api_rate_limit_by_key) do case Hammer.check_rate("api-#{api_key}", 1_000, api_rate_limit_by_key) do {:allow, _count} -> diff --git a/apps/block_scout_web/lib/block_scout_web/views/account/api/v1/account_view.ex b/apps/block_scout_web/lib/block_scout_web/views/account/api/v1/account_view.ex new file mode 100644 index 0000000000..0e3a65e616 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/account/api/v1/account_view.ex @@ -0,0 +1,7 @@ +defmodule BlockScoutWeb.Account.Api.V1.AccountView do + def render("message.json", %{message: message}) do + %{ + "message" => message + } + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/account/api/v1/tags_view.ex b/apps/block_scout_web/lib/block_scout_web/views/account/api/v1/tags_view.ex new file mode 100644 index 0000000000..d97e35f914 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/account/api/v1/tags_view.ex @@ -0,0 +1,27 @@ +defmodule BlockScoutWeb.Account.Api.V1.TagsView do + def render("address_tags.json", %{tags_map: tags_map}) do + tags_map + end + + def render("transaction_tags.json", %{ + tags_map: %{ + personal_tags: personal_tags, + watchlist_names: watchlist_names, + personal_tx_tag: personal_tx_tag, + common_tags: common_tags + } + }) do + %{ + personal_tx_tag: prepare_transaction_tag(personal_tx_tag), + personal_tags: personal_tags, + watchlist_names: watchlist_names, + common_tags: common_tags + } + end + + def prepare_transaction_tag(nil), do: nil + + def prepare_transaction_tag(transaction_tag) do + %{"label" => transaction_tag.name} + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/account/api/v1/user_view.ex b/apps/block_scout_web/lib/block_scout_web/views/account/api/v1/user_view.ex new file mode 100644 index 0000000000..91299034a1 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/account/api/v1/user_view.ex @@ -0,0 +1,141 @@ +defmodule BlockScoutWeb.Account.Api.V1.UserView do + alias BlockScoutWeb.Account.Api.V1.AccountView + alias Ecto.Changeset + + def render("message.json", assigns) do + AccountView.render("message.json", assigns) + end + + def render("user_info.json", %{identity: identity}) do + %{"name" => identity.name, "email" => identity.email, "avatar" => identity.avatar, "nickname" => identity.nickname} + end + + def render("watchlist_addresses.json", %{watchlist_addresses: watchlist_addresses, exchange_rate: exchange_rate}) do + Enum.map(watchlist_addresses, &prepare_watchlist_address(&1, exchange_rate)) + end + + def render("watchlist_address.json", %{watchlist_address: watchlist_address, exchange_rate: exchange_rate}) do + prepare_watchlist_address(watchlist_address, exchange_rate) + end + + def render("address_tags.json", %{address_tags: address_tags}) do + Enum.map(address_tags, &prepare_address_tag/1) + end + + def render("address_tag.json", %{address_tag: address_tag}) do + prepare_address_tag(address_tag) + end + + def render("transaction_tags.json", %{transaction_tags: transaction_tags}) do + Enum.map(transaction_tags, &prepare_transaction_tag/1) + end + + def render("transaction_tag.json", %{transaction_tag: transaction_tag}) do + prepare_transaction_tag(transaction_tag) + end + + def render("api_keys.json", %{api_keys: api_keys}) do + Enum.map(api_keys, &prepare_api_key/1) + end + + def render("api_key.json", %{api_key: api_key}) do + prepare_api_key(api_key) + end + + def render("custom_abis.json", %{custom_abis: custom_abis}) do + Enum.map(custom_abis, &prepare_custom_abi/1) + end + + def render("custom_abi.json", %{custom_abi: custom_abi}) do + prepare_custom_abi(custom_abi) + end + + def render("public_tags_requests.json", %{public_tags_requests: public_tags_requests}) do + Enum.map(public_tags_requests, &prepare_public_tags_request/1) + end + + def render("public_tags_request.json", %{public_tags_request: public_tags_request}) do + prepare_public_tags_request(public_tags_request) + end + + def render("changeset_errors.json", %{changeset: changeset}) do + %{ + "errors" => + Changeset.traverse_errors(changeset, fn {msg, opts} -> + Regex.replace(~r"%{(\w+)}", msg, fn _, key -> + opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string() + end) + end) + } + end + + def prepare_watchlist_address(watchlist, exchange_rate) do + %{ + "id" => watchlist.id, + "address_hash" => watchlist.address_hash, + "name" => watchlist.name, + "address_balance" => if(watchlist.fetched_coin_balance, do: watchlist.fetched_coin_balance.value), + "exchange_rate" => exchange_rate.usd_value, + "notification_settings" => %{ + "native" => %{ + "incoming" => watchlist.watch_coin_input, + "outcoming" => watchlist.watch_coin_output + }, + "ERC-20" => %{ + "incoming" => watchlist.watch_erc_20_input, + "outcoming" => watchlist.watch_erc_20_output + }, + "ERC-721" => %{ + "incoming" => watchlist.watch_erc_721_input, + "outcoming" => watchlist.watch_erc_721_output + } + # , + # "ERC-1155" => %{ + # "incoming" => watchlist.watch_erc_1155_input, + # "outcoming" => watchlist.watch_erc_1155_output + # } + }, + "notification_methods" => %{ + "email" => watchlist.notify_email + } + } + end + + def prepare_custom_abi(custom_abi) do + %{ + "id" => custom_abi.id, + "contract_address_hash" => custom_abi.address_hash, + "name" => custom_abi.name, + "abi" => custom_abi.abi + } + end + + def prepare_api_key(api_key) do + %{"api_key" => api_key.value, "name" => api_key.name} + end + + def prepare_address_tag(address_tag) do + %{"id" => address_tag.id, "address_hash" => address_tag.address_hash, "name" => address_tag.name} + end + + def prepare_transaction_tag(nil), do: nil + + def prepare_transaction_tag(transaction_tag) do + %{"id" => transaction_tag.id, "transaction_hash" => transaction_tag.tx_hash, "name" => transaction_tag.name} + end + + def prepare_public_tags_request(public_tags_request) do + %{ + "id" => public_tags_request.id, + "full_name" => public_tags_request.full_name, + "email" => public_tags_request.email, + "company" => public_tags_request.company, + "website" => public_tags_request.website, + "tags" => public_tags_request.tags, + "addresses" => public_tags_request.addresses, + "additional_comment" => public_tags_request.additional_comment, + "is_owner" => public_tags_request.is_owner, + "submission_date" => public_tags_request.inserted_at + } + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/account/api_key_view.ex b/apps/block_scout_web/lib/block_scout_web/views/account/api_key_view.ex new file mode 100644 index 0000000000..a0b21b79da --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/account/api_key_view.ex @@ -0,0 +1,5 @@ +defmodule BlockScoutWeb.Account.ApiKeyView do + use BlockScoutWeb, :view + + alias Explorer.Account.Api.Key +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/account/auth_view.ex b/apps/block_scout_web/lib/block_scout_web/views/account/auth_view.ex new file mode 100644 index 0000000000..cfbeb001e0 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/account/auth_view.ex @@ -0,0 +1,3 @@ +defmodule BlockScoutWeb.Account.AuthView do + use BlockScoutWeb, :view +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/account/common_view.ex b/apps/block_scout_web/lib/block_scout_web/views/account/common_view.ex new file mode 100644 index 0000000000..e296ca90d6 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/account/common_view.ex @@ -0,0 +1,11 @@ +defmodule BlockScoutWeb.Account.CommonView do + use BlockScoutWeb, :view + + def nav_class(active_item, item) do + if active_item == item do + "dropdown-item active fs-14" + else + "dropdown-item fs-14" + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/account/custom_abi_view.ex b/apps/block_scout_web/lib/block_scout_web/views/account/custom_abi_view.ex new file mode 100644 index 0000000000..3b38effa41 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/account/custom_abi_view.ex @@ -0,0 +1,22 @@ +defmodule BlockScoutWeb.Account.CustomABIView do + use BlockScoutWeb, :view + + alias Ecto.Changeset + alias Explorer.Account.CustomABI + + def format_abi(custom_abi) do + with {_type, abi} <- Changeset.fetch_field(custom_abi, :abi), + false <- is_nil(abi), + {:binary, false} <- {:binary, is_binary(abi)}, + {:ok, encoded_abi} <- Poison.encode(abi) do + encoded_abi + else + {:binary, true} -> + {_type, abi} = Changeset.fetch_field(custom_abi, :abi) + abi + + _ -> + "" + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/account/public_tags_request_view.ex b/apps/block_scout_web/lib/block_scout_web/views/account/public_tags_request_view.ex new file mode 100644 index 0000000000..2a13dd8d7e --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/account/public_tags_request_view.ex @@ -0,0 +1,70 @@ +defmodule BlockScoutWeb.Account.PublicTagsRequestView do + use BlockScoutWeb, :view + use Phoenix.HTML + + alias Explorer.Account.PublicTagsRequest + alias Phoenix.HTML.Form + + def array_input(form, field, attrs \\ []) do + values = Form.input_value(form, field) || [""] + id = Form.input_id(form, field) + + content_tag :ul, + id: container_id(id), + data: [index: Enum.count(values), multiple_input_field_container: ""], + class: "multiple-input-fields-container" do + values + |> Enum.map(fn v -> + form_elements(form, field, to_string(v), attrs) + end) + end + end + + def array_add_button(form, field, attrs \\ []) do + id = Form.input_id(form, field) + + content = + form + |> form_elements(field, "", attrs) + |> safe_to_string + + data = [ + prototype: content, + container: container_id(id) + ] + + content_tag(:button, render(BlockScoutWeb.CommonComponentsView, "_svg_plus.html"), + data: data, + class: "add-form-field" + ) + end + + defp form_elements(form, field, k, attrs) do + type = Form.input_type(form, field) + id = Form.input_id(form, field) + + input_opts = + [ + name: new_field_name(form, field), + value: k, + id: id, + class: "form-control public-tags-address" + ] ++ attrs + + content_tag :li, class: "public-tags-address form-group" do + [ + apply(Form, type, [form, field, input_opts]), + content_tag(:button, render(BlockScoutWeb.CommonComponentsView, "_svg_minus.html"), + data: [container: container_id(id)], + class: "remove-form-field ml-1" + ) + ] + end + end + + defp container_id(id), do: id <> "_container" + + defp new_field_name(form, field) do + Form.input_name(form, field) <> "[]" + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/account/tag_address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/account/tag_address_view.ex new file mode 100644 index 0000000000..74886c33e9 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/account/tag_address_view.ex @@ -0,0 +1,7 @@ +defmodule BlockScoutWeb.Account.TagAddressView do + use BlockScoutWeb, :view + + import BlockScoutWeb.AddressView, only: [trimmed_hash: 1] + + alias Explorer.Account.TagAddress +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/account/tag_transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/account/tag_transaction_view.ex new file mode 100644 index 0000000000..7edfa1e340 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/account/tag_transaction_view.ex @@ -0,0 +1,5 @@ +defmodule BlockScoutWeb.Account.TagTransactionView do + use BlockScoutWeb, :view + + alias Explorer.Account.TagTransaction +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/account/watchlist_address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/account/watchlist_address_view.ex new file mode 100644 index 0000000000..f3f538399e --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/account/watchlist_address_view.ex @@ -0,0 +1,11 @@ +defmodule BlockScoutWeb.Account.WatchlistAddressView do + use BlockScoutWeb, :view + import BlockScoutWeb.AddressView, only: [trimmed_hash: 1] + import BlockScoutWeb.WeiHelpers, only: [format_wei_value: 2] + + def balance_ether(nil), do: "" + + def balance_ether(balance) do + format_wei_value(balance, :ether) + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/account/watchlist_view.ex b/apps/block_scout_web/lib/block_scout_web/views/account/watchlist_view.ex new file mode 100644 index 0000000000..fa39663236 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/account/watchlist_view.ex @@ -0,0 +1,17 @@ +defmodule BlockScoutWeb.Account.WatchlistView do + use BlockScoutWeb, :view + + alias BlockScoutWeb.Account.WatchlistAddressView + alias Explorer.Account.WatchlistAddress + alias Explorer.ExchangeRates.Token + alias Explorer.Market + alias Indexer.Fetcher.CoinBalanceOnDemand + + def coin_balance_status(address) do + CoinBalanceOnDemand.trigger_fetch(address) + end + + def exchange_rate do + Market.get_exchange_rate(Explorer.coin()) || Token.null() + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_view.ex index f0b262d291..8fff82fb89 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/address_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/address_view.ex @@ -4,12 +4,15 @@ defmodule BlockScoutWeb.AddressView do require Logger alias BlockScoutWeb.{AccessHelpers, LayoutView} + alias Explorer.Account.CustomABI alias Explorer.{Chain, CustomContractsHelpers, Repo} alias Explorer.Chain.{Address, Hash, InternalTransaction, SmartContract, Token, TokenTransfer, Transaction, Wei} alias Explorer.Chain.Block.Reward alias Explorer.ExchangeRates.Token, as: TokenExchangeRate alias Explorer.SmartContract.{Helper, Writer} + import BlockScoutWeb.Account.AuthController, only: [current_user: 1] + @dialyzer :no_match @tabs [ @@ -446,4 +449,33 @@ defmodule BlockScoutWeb.AddressView do end def smart_contract_is_gnosis_safe_proxy?(_address), do: false + + def tag_name_to_label(tag_name) do + tag_name + |> String.replace(" ", "-") + end + + def fetch_custom_abi(conn, address_hash) do + if current_user = current_user(conn) do + CustomABI.get_custom_abi_by_identity_id_and_address_hash(address_hash, current_user.id) + end + end + + def has_address_custom_abi_with_read_functions?(conn, address_hash) do + custom_abi = fetch_custom_abi(conn, address_hash) + + check_custom_abi_for_having_read_functions(custom_abi) + end + + def check_custom_abi_for_having_read_functions(custom_abi), + do: !is_nil(custom_abi) && Enum.any?(custom_abi.abi, &is_read_function?(&1)) + + def has_address_custom_abi_with_write_functions?(conn, address_hash) do + custom_abi = fetch_custom_abi(conn, address_hash) + + check_custom_abi_for_having_write_functions(custom_abi) + end + + def check_custom_abi_for_having_write_functions(custom_abi), + do: !is_nil(custom_abi) && Enum.any?(custom_abi.abi, &Writer.write_function?(&1)) end diff --git a/apps/block_scout_web/lib/block_scout_web/views/error_view.ex b/apps/block_scout_web/lib/block_scout_web/views/error_view.ex index 22da72d0a2..090159d946 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/error_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/error_view.ex @@ -1,15 +1,28 @@ defmodule BlockScoutWeb.ErrorView do use BlockScoutWeb, :view - def render("404.html", _assigns) do + # when type in ["json", "html"] + def render("404." <> _type, _assigns) do "Page not found" end - def render("422.html", _assigns) do + def render("400." <> _type, _assigns) do + "Bad request" + end + + def render("401." <> _type, _assigns) do + "Unauthorized" + end + + def render("403." <> _type, _assigns) do + "Forbidden" + end + + def render("422." <> _type, _assigns) do "Unprocessable entity" end - def render("500.html", _assigns) do + def render("500." <> _type, _assigns) do "Internal server error" end diff --git a/apps/block_scout_web/lib/block_scout_web/views/layout_view.ex b/apps/block_scout_web/lib/block_scout_web/views/layout_view.ex index 16907be092..f60152575b 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/layout_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/layout_view.ex @@ -252,4 +252,29 @@ defmodule BlockScoutWeb.LayoutView do end defp validate_url(_), do: :error + + def sign_in_link do + if Mix.env() == :test do + "/auth/auth0" + else + Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:path] <> "auth/auth0" + end + end + + def sign_out_link do + client_id = Application.get_env(:ueberauth, Ueberauth.Strategy.Auth0.OAuth)[:client_id] + return_to = Application.get_env(:ueberauth, Ueberauth)[:logout_return_to_url] + logout_url = Application.get_env(:ueberauth, Ueberauth)[:logout_url] + + if client_id && return_to && logout_url do + params = [ + client_id: client_id, + returnTo: return_to + ] + + [logout_url, "?", URI.encode_query(params)] + else + [] + end + end end diff --git a/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex index 31d9feffff..72f300bc4c 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex @@ -2,6 +2,7 @@ defmodule BlockScoutWeb.TransactionView do use BlockScoutWeb, :view alias BlockScoutWeb.{AccessHelpers, AddressView, BlockView, TabHelpers} + alias BlockScoutWeb.Account.AuthController alias BlockScoutWeb.Cldr.Number alias Explorer.{Chain, CustomContractsHelpers, Repo} alias Explorer.Chain.Block.Reward @@ -11,7 +12,7 @@ defmodule BlockScoutWeb.TransactionView do alias Timex.Duration import BlockScoutWeb.Gettext - import BlockScoutWeb.AddressView, only: [from_address_hash: 1, short_token_id: 2] + import BlockScoutWeb.AddressView, only: [from_address_hash: 1, short_token_id: 2, tag_name_to_label: 1] import BlockScoutWeb.Tokens.Helpers @tabs ["token-transfers", "internal-transactions", "logs", "raw-trace"] diff --git a/apps/block_scout_web/lib/block_scout_web/web_router.ex b/apps/block_scout_web/lib/block_scout_web/web_router.ex index c3ad18e43d..4350a4fc6f 100644 --- a/apps/block_scout_web/lib/block_scout_web/web_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/web_router.ex @@ -3,6 +3,9 @@ defmodule BlockScoutWeb.WebRouter do Router for web app """ use BlockScoutWeb, :router + require Ueberauth + + alias BlockScoutWeb.Plug.CheckAccountWeb pipeline :browser do plug(:accepts, ["html"]) @@ -13,6 +16,69 @@ defmodule BlockScoutWeb.WebRouter do plug(BlockScoutWeb.ChecksumAddress) end + pipeline :account do + plug(:accepts, ["html"]) + plug(:fetch_session) + plug(:fetch_flash) + plug(CheckAccountWeb) + plug(:protect_from_forgery) + plug(BlockScoutWeb.CSPHeader) + plug(BlockScoutWeb.ChecksumAddress) + end + + if Mix.env() == :dev do + forward("/sent_emails", Bamboo.SentEmailViewerPlug) + end + + scope "/auth", BlockScoutWeb do + pipe_through(:account) + + get("/profile", Account.AuthController, :profile) + get("/logout", Account.AuthController, :logout) + get("/:provider", Account.AuthController, :request) + get("/:provider/callback", Account.AuthController, :callback) + end + + scope "/account", BlockScoutWeb do + pipe_through(:account) + + resources("/tag_address", Account.TagAddressController, + only: [:index, :new, :create, :delete], + as: :tag_address + ) + + resources("/tag_transaction", Account.TagTransactionController, + only: [:index, :new, :create, :delete], + as: :tag_transaction + ) + + resources("/watchlist", Account.WatchlistController, + only: [:show], + singleton: true, + as: :watchlist + ) + + resources("/watchlist_address", Account.WatchlistAddressController, + only: [:new, :create, :edit, :update, :delete], + as: :watchlist_address + ) + + resources("/api_key", Account.ApiKeyController, + only: [:new, :create, :edit, :update, :delete, :index], + as: :api_key + ) + + resources("/custom_abi", Account.CustomABIController, + only: [:new, :create, :edit, :update, :delete, :index], + as: :custom_abi + ) + + resources("/public_tags_request", Account.PublicTagsRequestController, + only: [:new, :create, :edit, :update, :delete, :index], + as: :public_tags_request + ) + end + # Disallows Iframes (write routes) scope "/", BlockScoutWeb do pipe_through(:browser) @@ -40,7 +106,10 @@ defmodule BlockScoutWeb.WebRouter do resources("/blocks", BlockController, as: :blocks, only: [:index]) - resources "/blocks", BlockController, as: :block_secondary, only: [:show], param: "hash_or_number" do + resources "/blocks", BlockController, + as: :block_secondary, + only: [:show], + param: "hash_or_number" do resources("/transactions", BlockTransactionController, only: [:index], as: :transaction) end diff --git a/apps/block_scout_web/mix.exs b/apps/block_scout_web/mix.exs index 6de44d043e..459eca37d7 100644 --- a/apps/block_scout_web/mix.exs +++ b/apps/block_scout_web/mix.exs @@ -44,6 +44,7 @@ defmodule BlockScoutWeb.Mixfile do defp extra_applications, do: [ + :ueberauth_auth0, :logger, :runtime_tools ] @@ -103,7 +104,7 @@ defmodule BlockScoutWeb.Mixfile do {:plug_cowboy, "~> 2.2"}, # Waiting for the Pretty Print to be implemented at the Jason lib # https://github.com/michalmuskala/jason/issues/15 - {:poison, "~> 5.0.0"}, + {:poison, "~> 4.0.1"}, {:postgrex, ">= 0.0.0"}, # For compatibility with `prometheus_process_collector`, which hasn't been updated yet {:prometheus, "~> 4.0", override: true}, @@ -127,7 +128,11 @@ defmodule BlockScoutWeb.Mixfile do # `:cowboy` `~> 2.0` and Phoenix 1.4 compatibility {:websocket_client, "~> 1.3"}, {:wobserver, "~> 0.2.0", github: "poanetwork/wobserver", branch: "support-https"}, - {:ex_json_schema, "~> 0.9.1"} + {:ex_json_schema, "~> 0.9.1"}, + {:ueberauth, "~> 0.7"}, + {:ueberauth_auth0, "~> 2.0"}, + {:bureaucrat, "~> 0.2.9", only: :test}, + {:poison, "~> 4.0.0"} ] end diff --git a/apps/block_scout_web/priv/gettext/default.pot b/apps/block_scout_web/priv/gettext/default.pot index c49c49c37a..92fdcb21ae 100644 --- a/apps/block_scout_web/priv/gettext/default.pot +++ b/apps/block_scout_web/priv/gettext/default.pot @@ -63,7 +63,7 @@ msgstr "" msgid "%{subnetwork} Explorer - BlockScout" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:349 +#: lib/block_scout_web/views/transaction_view.ex:350 #, elixir-autogen, elixir-format msgid "(Awaiting internal transactions for status)" msgstr "" @@ -101,6 +101,11 @@ msgstr "" msgid "A string with the name of the module to be invoked." msgstr "" +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:24 +#, elixir-autogen, elixir-format +msgid "ABI" +msgstr "" + #: lib/block_scout_web/templates/address_contract_verification_common_fields/_constructor_args.html.eex:3 #, elixir-autogen, elixir-format msgid "ABI-encoded Constructor Arguments (if required by the contract)" @@ -126,48 +131,83 @@ msgstr "" msgid "APIs" msgstr "" +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:24 +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:24 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:69 #, elixir-autogen, elixir-format msgid "Action" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:417 +#: lib/block_scout_web/templates/transaction/overview.html.eex:425 #, elixir-autogen, elixir-format msgid "Actual gas amount used by the transaction." msgstr "" +#: lib/block_scout_web/templates/account/api_key/form.html.eex:7 +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:8 #: lib/block_scout_web/templates/layout/_add_chain_to_mm.html.eex:11 #, elixir-autogen, elixir-format msgid "Add" msgstr "" +#: lib/block_scout_web/templates/account/api_key/index.html.eex:44 +#, elixir-autogen, elixir-format +msgid "Add API key" +msgstr "" + +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:44 +#, elixir-autogen, elixir-format +msgid "Add Custom ABI" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:7 +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:37 +#, elixir-autogen, elixir-format +msgid "Add address tag" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:7 +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:37 +#, elixir-autogen, elixir-format +msgid "Add transaction tag" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:11 +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:23 +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:23 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:12 #: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:16 #: lib/block_scout_web/templates/transaction_log/_logs.html.eex:20 -#: lib/block_scout_web/views/address_view.ex:104 +#: lib/block_scout_web/views/address_view.ex:107 #, elixir-autogen, elixir-format msgid "Address" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:211 +#: lib/block_scout_web/templates/transaction/overview.html.eex:217 #, elixir-autogen, elixir-format msgid "Address (external or contract) receiving the transaction." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:194 +#: lib/block_scout_web/templates/transaction/overview.html.eex:199 #, elixir-autogen, elixir-format msgid "Address (external or contract) sending the transaction." msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:167 +#: lib/block_scout_web/templates/address/overview.html.eex:149 #, elixir-autogen, elixir-format msgid "Address balance in" msgstr "" -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:50 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:51 #, elixir-autogen, elixir-format msgid "Address of the token contract" msgstr "" +#: lib/block_scout_web/templates/account/public_tags_request/address_field.html.eex:2 +#, elixir-autogen, elixir-format +msgid "Address*" +msgstr "" + #: lib/block_scout_web/templates/address/index.html.eex:5 #, elixir-autogen, elixir-format msgid "Addresses" @@ -194,12 +234,12 @@ msgstr "" msgid "All metadata displayed below is from that contract. In order to verify current contract, click" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:192 +#: lib/block_scout_web/templates/address/overview.html.eex:174 #, elixir-autogen, elixir-format msgid "All tokens in the account and total value." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:403 +#: lib/block_scout_web/templates/transaction/overview.html.eex:411 #, elixir-autogen, elixir-format msgid "Amount of" msgstr "" @@ -235,7 +275,8 @@ msgstr "" msgid "Back Home" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:168 +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:24 +#: lib/block_scout_web/templates/address/overview.html.eex:150 #: lib/block_scout_web/templates/address_token/overview.html.eex:51 #, elixir-autogen, elixir-format msgid "Balance" @@ -257,14 +298,14 @@ msgstr "" msgid "Base URL:" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:438 +#: lib/block_scout_web/templates/transaction/overview.html.eex:446 #, elixir-autogen, elixir-format msgid "Binary data included with the transaction. See input / logs below for additional info." msgstr "" #: lib/block_scout_web/templates/address_coin_balance/_coin_balances.html.eex:8 #: lib/block_scout_web/templates/block/overview.html.eex:29 -#: lib/block_scout_web/templates/transaction/overview.html.eex:153 +#: lib/block_scout_web/templates/transaction/overview.html.eex:158 #, elixir-autogen, elixir-format msgid "Block" msgstr "" @@ -296,7 +337,7 @@ msgstr "" msgid "Block Mined, awaiting import..." msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:33 +#: lib/block_scout_web/views/transaction_view.ex:34 #, elixir-autogen, elixir-format msgid "Block Pending" msgstr "" @@ -311,12 +352,12 @@ msgstr "" msgid "Block not found, please try again later." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:152 +#: lib/block_scout_web/templates/transaction/overview.html.eex:157 #, elixir-autogen, elixir-format msgid "Block number containing the transaction." msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:275 +#: lib/block_scout_web/templates/address/overview.html.eex:257 #, elixir-autogen, elixir-format msgid "Block number in which the address was updated." msgstr "" @@ -339,9 +380,9 @@ msgid "Blocks Indexed" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:48 -#: lib/block_scout_web/templates/address/overview.html.eex:293 +#: lib/block_scout_web/templates/address/overview.html.eex:275 #: lib/block_scout_web/templates/address_validation/index.html.eex:11 -#: lib/block_scout_web/views/address_view.ex:371 +#: lib/block_scout_web/views/address_view.ex:374 #, elixir-autogen, elixir-format msgid "Blocks Validated" msgstr "" @@ -378,6 +419,7 @@ msgstr "" msgid "Call Code" msgstr "" +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:62 #: lib/block_scout_web/templates/address_contract_verification/new.html.eex:120 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:145 #: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:41 @@ -420,13 +462,13 @@ msgstr "" #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:187 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:126 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:149 -#: lib/block_scout_web/views/address_view.ex:364 +#: lib/block_scout_web/views/address_view.ex:367 #, elixir-autogen, elixir-format msgid "Code" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:34 -#: lib/block_scout_web/views/address_view.ex:370 +#: lib/block_scout_web/views/address_view.ex:373 #, elixir-autogen, elixir-format msgid "Coin Balance History" msgstr "" @@ -436,6 +478,16 @@ msgstr "" msgid "Collapse" msgstr "" +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:20 +#, elixir-autogen, elixir-format +msgid "Company name" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:32 +#, elixir-autogen, elixir-format +msgid "Company website" +msgstr "" + #: lib/block_scout_web/templates/address_contract_verification_common_fields/_compiler_field.html.eex:3 #, elixir-autogen, elixir-format msgid "Compiler" @@ -446,17 +498,17 @@ msgstr "" msgid "Compiler version" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:342 +#: lib/block_scout_web/views/transaction_view.ex:343 #, elixir-autogen, elixir-format msgid "Confirmed" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:119 +#: lib/block_scout_web/templates/transaction/overview.html.eex:124 #, elixir-autogen, elixir-format msgid "Confirmed by " msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:185 +#: lib/block_scout_web/templates/transaction/overview.html.eex:190 #, elixir-autogen, elixir-format msgid "Confirmed within" msgstr "" @@ -467,7 +519,7 @@ msgstr "" #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:4 #: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:6 #: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:4 -#: lib/block_scout_web/templates/tokens/holder/index.html.eex:15 +#: lib/block_scout_web/templates/tokens/holder/index.html.eex:16 #, elixir-autogen, elixir-format msgid "Connection Lost" msgstr "" @@ -500,8 +552,8 @@ msgstr "" msgid "Constructor Arguments" msgstr "" -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:51 -#: lib/block_scout_web/templates/transaction/overview.html.eex:221 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:52 +#: lib/block_scout_web/templates/transaction/overview.html.eex:227 #, elixir-autogen, elixir-format msgid "Contract" msgstr "" @@ -511,25 +563,27 @@ msgstr "" msgid "Contract ABI" msgstr "" +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:18 +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:29 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_contract_address_field.html.eex:3 -#: lib/block_scout_web/views/address_view.ex:102 +#: lib/block_scout_web/views/address_view.ex:105 #, elixir-autogen, elixir-format msgid "Contract Address" msgstr "" #: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:16 -#: lib/block_scout_web/views/address_view.ex:42 -#: lib/block_scout_web/views/address_view.ex:76 +#: lib/block_scout_web/views/address_view.ex:45 +#: lib/block_scout_web/views/address_view.ex:79 #, elixir-autogen, elixir-format msgid "Contract Address Pending" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:457 +#: lib/block_scout_web/views/transaction_view.ex:458 #, elixir-autogen, elixir-format msgid "Contract Call" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:454 +#: lib/block_scout_web/views/transaction_view.ex:455 #, elixir-autogen, elixir-format msgid "Contract Creation" msgstr "" @@ -546,7 +600,7 @@ msgstr "" msgid "Contract Libraries" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:93 +#: lib/block_scout_web/templates/address/overview.html.eex:75 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_contract_name_field.html.eex:3 #, elixir-autogen, elixir-format msgid "Contract Name" @@ -583,16 +637,33 @@ msgstr "" msgid "Copy ABI" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:37 +#: lib/block_scout_web/templates/account/api_key/row.html.eex:6 +#: lib/block_scout_web/templates/account/api_key/row.html.eex:6 +#, elixir-autogen, elixir-format +msgid "Copy API key" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_address/row.html.eex:8 +#: lib/block_scout_web/templates/account/tag_address/row.html.eex:8 +#: lib/block_scout_web/templates/account/tag_transaction/row.html.eex:11 +#: lib/block_scout_web/templates/account/tag_transaction/row.html.eex:11 +#: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:7 #: lib/block_scout_web/templates/address/overview.html.eex:38 +#: lib/block_scout_web/templates/address/overview.html.eex:39 #: lib/block_scout_web/templates/block/overview.html.eex:104 #: lib/block_scout_web/templates/block/overview.html.eex:105 -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:42 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:43 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:44 #, elixir-autogen, elixir-format msgid "Copy Address" msgstr "" +#: lib/block_scout_web/templates/account/custom_abi/row.html.eex:6 +#: lib/block_scout_web/templates/account/custom_abi/row.html.eex:6 +#, elixir-autogen, elixir-format +msgid "Copy Contract Address" +msgstr "" + #: lib/block_scout_web/templates/address_contract/index.html.eex:140 #: lib/block_scout_web/templates/address_contract/index.html.eex:156 #, elixir-autogen, elixir-format @@ -604,16 +675,17 @@ msgstr "" msgid "Copy Decompiled Contract Code" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:183 -#: lib/block_scout_web/templates/address_contract/index.html.eex:193 +#: lib/block_scout_web/templates/address_contract/index.html.eex:177 +#: lib/block_scout_web/templates/address_contract/index.html.eex:187 #, elixir-autogen, elixir-format msgid "Copy Deployed ByteCode" msgstr "" -#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:14 -#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:15 -#: lib/block_scout_web/templates/transaction/overview.html.eex:201 -#: lib/block_scout_web/templates/transaction/overview.html.eex:202 +#: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:7 +#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:17 +#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:18 +#: lib/block_scout_web/templates/transaction/overview.html.eex:207 +#: lib/block_scout_web/templates/transaction/overview.html.eex:208 #, elixir-autogen, elixir-format msgid "Copy From Address" msgstr "" @@ -646,12 +718,12 @@ msgstr "" msgid "Copy Source Code" msgstr "" -#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:31 -#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:32 -#: lib/block_scout_web/templates/transaction/overview.html.eex:227 -#: lib/block_scout_web/templates/transaction/overview.html.eex:228 +#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:34 +#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:35 #: lib/block_scout_web/templates/transaction/overview.html.eex:234 #: lib/block_scout_web/templates/transaction/overview.html.eex:235 +#: lib/block_scout_web/templates/transaction/overview.html.eex:242 +#: lib/block_scout_web/templates/transaction/overview.html.eex:243 #, elixir-autogen, elixir-format msgid "Copy To Address" msgstr "" @@ -662,30 +734,30 @@ msgstr "" msgid "Copy Token ID" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:82 +#: lib/block_scout_web/templates/transaction/overview.html.eex:87 #, elixir-autogen, elixir-format msgid "Copy Transaction Hash" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:83 +#: lib/block_scout_web/templates/transaction/overview.html.eex:88 #, elixir-autogen, elixir-format msgid "Copy Txn Hash" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:464 +#: lib/block_scout_web/templates/transaction/overview.html.eex:472 #, elixir-autogen, elixir-format msgid "Copy Txn Hex Input" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:470 +#: lib/block_scout_web/templates/transaction/overview.html.eex:478 #, elixir-autogen, elixir-format msgid "Copy Txn UTF-8 Input" msgstr "" #: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:20 #: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:41 -#: lib/block_scout_web/templates/transaction/overview.html.eex:463 -#: lib/block_scout_web/templates/transaction/overview.html.eex:469 +#: lib/block_scout_web/templates/transaction/overview.html.eex:471 +#: lib/block_scout_web/templates/transaction/overview.html.eex:477 #: lib/block_scout_web/templates/transaction_raw_trace/index.html.eex:14 #, elixir-autogen, elixir-format msgid "Copy Value" @@ -696,12 +768,22 @@ msgstr "" msgid "Create" msgstr "" +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:12 +#, elixir-autogen, elixir-format +msgid "Create a Custom ABI to interact with contracts." +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/index.html.eex:12 +#, elixir-autogen, elixir-format +msgid "Create an API key to use with your RPC и EthRPC API requests." +msgstr "" + #: lib/block_scout_web/views/internal_transaction_view.ex:26 #, elixir-autogen, elixir-format msgid "Create2" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:120 +#: lib/block_scout_web/templates/address/overview.html.eex:102 #, elixir-autogen, elixir-format msgid "Creator" msgstr "" @@ -712,11 +794,31 @@ msgstr "" msgid "Curl" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:92 +#: lib/block_scout_web/templates/transaction/overview.html.eex:97 #, elixir-autogen, elixir-format msgid "Current transaction state: Success, Failed (Error), or Pending (In Process)" msgstr "" +#: lib/block_scout_web/templates/address_read_contract/index.html.eex:20 +#: lib/block_scout_web/templates/address_write_contract/index.html.eex:18 +#, elixir-autogen, elixir-format +msgid "Custom" +msgstr "" + +#: lib/block_scout_web/templates/account/common/_nav.html.eex:19 +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:8 +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:7 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:18 +#, elixir-autogen, elixir-format +msgid "Custom ABI" +msgstr "" + +#: lib/block_scout_web/templates/address_read_contract/index.html.eex:25 +#: lib/block_scout_web/templates/address_write_contract/index.html.eex:23 +#, elixir-autogen, elixir-format +msgid "Custom ABI from account" +msgstr "" + #: lib/block_scout_web/templates/chain/show.html.eex:69 #, elixir-autogen, elixir-format msgid "Daily Transactions" @@ -735,13 +837,13 @@ msgstr "" msgid "Date & time at which block was produced." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:171 +#: lib/block_scout_web/templates/transaction/overview.html.eex:176 #, elixir-autogen, elixir-format msgid "Date & time of transaction inclusion, including length of time for confirmation." msgstr "" #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:52 -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:130 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:131 #, elixir-autogen, elixir-format msgid "Decimals" msgstr "" @@ -757,7 +859,7 @@ msgstr "" msgid "Decoded" msgstr "" -#: lib/block_scout_web/views/address_view.ex:365 +#: lib/block_scout_web/views/address_view.ex:368 #, elixir-autogen, elixir-format msgid "Decompiled Code" msgstr "" @@ -782,8 +884,8 @@ msgstr "" msgid "Delegate Call" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:181 -#: lib/block_scout_web/templates/address_contract/index.html.eex:189 +#: lib/block_scout_web/templates/address_contract/index.html.eex:175 +#: lib/block_scout_web/templates/address_contract/index.html.eex:183 #, elixir-autogen, elixir-format msgid "Deployed ByteCode" msgstr "" @@ -796,6 +898,11 @@ msgstr "" msgid "Description" msgstr "" +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:56 +#, elixir-autogen, elixir-format +msgid "Description*" +msgstr "" + #: lib/block_scout_web/templates/address/overview.html.eex:30 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:166 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:127 @@ -838,30 +945,45 @@ msgstr "" msgid "During times when the network is busy (i.e during ICOs) it can take a while for your transaction to propagate through the network and for us to index it." msgstr "" +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:27 +#, elixir-autogen, elixir-format +msgid "E-mail*" +msgstr "" + #: lib/block_scout_web/templates/common_components/_minimal_proxy_pattern.html.eex:6 #, elixir-autogen, elixir-format msgid "EIP-1167" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:214 +#: lib/block_scout_web/views/transaction_view.ex:215 #, elixir-autogen, elixir-format msgid "ERC-1155 " msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:212 +#: lib/block_scout_web/views/transaction_view.ex:213 #, elixir-autogen, elixir-format msgid "ERC-20 " msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:213 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:40 +#, elixir-autogen, elixir-format +msgid "ERC-20 tokens (beta)" +msgstr "" + +#: lib/block_scout_web/views/transaction_view.ex:214 #, elixir-autogen, elixir-format msgid "ERC-721 " msgstr "" +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:53 +#, elixir-autogen, elixir-format +msgid "ERC-721, ERC-1155 tokens (NFT) (beta)" +msgstr "" + #: lib/block_scout_web/templates/address_token/overview.html.eex:1 -#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:104 -#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:104 -#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:146 +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:95 +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:95 +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:137 #, elixir-autogen, elixir-format msgid "ETH" msgstr "" @@ -883,6 +1005,18 @@ msgstr "" msgid "Easy Cowboy! This block does not exist yet!" msgstr "" +#: lib/block_scout_web/templates/account/api_key/row.html.eex:16 +#: lib/block_scout_web/templates/account/custom_abi/row.html.eex:16 +#: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:27 +#, elixir-autogen, elixir-format +msgid "Edit" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:71 +#, elixir-autogen, elixir-format +msgid "Email notifications" +msgstr "" + #: lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex:5 #, elixir-autogen, elixir-format msgid "Emission Contract" @@ -909,7 +1043,7 @@ msgstr "" msgid "Error" msgstr "" -#: lib/block_scout_web/templates/transaction/_tile.html.eex:9 +#: lib/block_scout_web/templates/transaction/_tile.html.eex:11 #, elixir-autogen, elixir-format msgid "Error in internal transactions" msgstr "" @@ -924,17 +1058,17 @@ msgstr "" msgid "Error trying to fetch balances." msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:353 +#: lib/block_scout_web/views/transaction_view.ex:354 #, elixir-autogen, elixir-format msgid "Error: %{reason}" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:351 +#: lib/block_scout_web/views/transaction_view.ex:352 #, elixir-autogen, elixir-format msgid "Error: (Awaiting internal transactions for reason)" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:137 +#: lib/block_scout_web/templates/address/overview.html.eex:119 #, elixir-autogen, elixir-format msgid "Error: Could not determine contract creator." msgstr "" @@ -944,16 +1078,18 @@ msgstr "" msgid "Eth RPC" msgstr "" +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:27 +#: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:21 #: lib/block_scout_web/templates/address/_current_coin_balance.html.eex:11 #: lib/block_scout_web/templates/address/index.html.eex:5 -#: lib/block_scout_web/templates/address/overview.html.eex:181 +#: lib/block_scout_web/templates/address/overview.html.eex:163 #: lib/block_scout_web/templates/block/overview.html.eex:215 #: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:24 #: lib/block_scout_web/templates/layout/_topnav.html.eex:82 #: lib/block_scout_web/templates/layout/app.html.eex:48 #: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:20 -#: lib/block_scout_web/templates/transaction/_tile.html.eex:43 -#: lib/block_scout_web/templates/transaction/overview.html.eex:403 +#: lib/block_scout_web/templates/transaction/_tile.html.eex:49 +#: lib/block_scout_web/templates/transaction/overview.html.eex:411 #: lib/block_scout_web/views/wei_helpers.ex:78 #, elixir-autogen, elixir-format msgid "Ether" @@ -981,7 +1117,7 @@ msgstr "" msgid "Export Data" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:224 +#: lib/block_scout_web/templates/address_contract/index.html.eex:212 #, elixir-autogen, elixir-format msgid "External libraries" msgstr "" @@ -1002,12 +1138,12 @@ msgstr "" msgid "Fast" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:265 +#: lib/block_scout_web/templates/address/overview.html.eex:247 #, elixir-autogen, elixir-format msgid "Fetching gas used..." msgstr "" -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:111 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:112 #, elixir-autogen, elixir-format msgid "Fetching holders..." msgstr "" @@ -1017,15 +1153,15 @@ msgstr "" msgid "Fetching tokens..." msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:212 -#: lib/block_scout_web/templates/address/overview.html.eex:220 +#: lib/block_scout_web/templates/address/overview.html.eex:194 +#: lib/block_scout_web/templates/address/overview.html.eex:202 #, elixir-autogen, elixir-format msgid "Fetching transactions..." msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:239 -#: lib/block_scout_web/templates/address/overview.html.eex:247 -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:122 +#: lib/block_scout_web/templates/address/overview.html.eex:221 +#: lib/block_scout_web/templates/address/overview.html.eex:229 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:123 #, elixir-autogen, elixir-format msgid "Fetching transfers..." msgstr "" @@ -1048,7 +1184,7 @@ msgstr "" #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:38 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:40 #: lib/block_scout_web/templates/address_transaction/index.html.eex:34 -#: lib/block_scout_web/templates/transaction/overview.html.eex:195 +#: lib/block_scout_web/templates/transaction/overview.html.eex:200 #: lib/block_scout_web/views/address_internal_transaction_view.ex:10 #: lib/block_scout_web/views/address_token_transfer_view.ex:10 #: lib/block_scout_web/views/address_transaction_view.ex:10 @@ -1063,24 +1199,24 @@ msgstr "" #: lib/block_scout_web/templates/block/_tile.html.eex:67 #: lib/block_scout_web/templates/block/overview.html.eex:187 -#: lib/block_scout_web/templates/transaction/overview.html.eex:365 +#: lib/block_scout_web/templates/transaction/overview.html.eex:373 #, elixir-autogen, elixir-format msgid "Gas Limit" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:345 +#: lib/block_scout_web/templates/transaction/overview.html.eex:353 #, elixir-autogen, elixir-format msgid "Gas Price" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:258 +#: lib/block_scout_web/templates/address/overview.html.eex:240 #: lib/block_scout_web/templates/block/_tile.html.eex:73 #: lib/block_scout_web/templates/block/overview.html.eex:178 #, elixir-autogen, elixir-format msgid "Gas Used" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:418 +#: lib/block_scout_web/templates/transaction/overview.html.eex:426 #, elixir-autogen, elixir-format msgid "Gas Used by Transaction" msgstr "" @@ -1091,7 +1227,7 @@ msgstr "" msgid "Gas tracker" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:257 +#: lib/block_scout_web/templates/address/overview.html.eex:239 #, elixir-autogen, elixir-format msgid "Gas used by the address." msgstr "" @@ -1132,13 +1268,13 @@ msgstr "" msgid "Hash" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:446 -#: lib/block_scout_web/templates/transaction/overview.html.eex:450 +#: lib/block_scout_web/templates/transaction/overview.html.eex:454 +#: lib/block_scout_web/templates/transaction/overview.html.eex:458 #, elixir-autogen, elixir-format msgid "Hex (Default)" msgstr "" -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:107 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:108 #, elixir-autogen, elixir-format msgid "Holders" msgstr "" @@ -1154,7 +1290,7 @@ msgid "IMPORTANT: This information is a best guess based on similar functions fr msgstr "" #: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:42 -#: lib/block_scout_web/templates/transaction/_tile.html.eex:86 +#: lib/block_scout_web/templates/transaction/_tile.html.eex:92 #, elixir-autogen, elixir-format msgid "IN" msgstr "" @@ -1169,17 +1305,17 @@ msgstr "" msgid "If you have just submitted this transaction please wait for at least 30 seconds before refreshing this page." msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:150 +#: lib/block_scout_web/templates/address/overview.html.eex:132 #, elixir-autogen, elixir-format msgid "Implementation" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:149 +#: lib/block_scout_web/templates/address/overview.html.eex:131 #, elixir-autogen, elixir-format msgid "Implementation address of the proxy contract." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:430 +#: lib/block_scout_web/templates/transaction/overview.html.eex:438 #, elixir-autogen, elixir-format msgid "Index position of Transaction in the block." msgstr "" @@ -1204,7 +1340,7 @@ msgstr "" msgid "Input" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:213 +#: lib/block_scout_web/templates/transaction/overview.html.eex:219 #, elixir-autogen, elixir-format msgid "Interacted With (To)" msgstr "" @@ -1218,8 +1354,8 @@ msgstr "" #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:17 #: lib/block_scout_web/templates/transaction/_tabs.html.eex:11 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:6 -#: lib/block_scout_web/views/address_view.ex:361 -#: lib/block_scout_web/views/transaction_view.ex:512 +#: lib/block_scout_web/views/address_view.ex:364 +#: lib/block_scout_web/views/transaction_view.ex:513 #, elixir-autogen, elixir-format msgid "Internal Transactions" msgstr "" @@ -1229,7 +1365,7 @@ msgstr "" msgid "Invalid Transaction Hash" msgstr "" -#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:15 +#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:16 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:19 #: lib/block_scout_web/views/tokens/overview_view.ex:42 #, elixir-autogen, elixir-format @@ -1241,11 +1377,17 @@ msgstr "" msgid "It could still be in the TX Pool of a different node, waiting to be broadcasted." msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:276 +#: lib/block_scout_web/templates/address/overview.html.eex:258 #, elixir-autogen, elixir-format msgid "Last Balance Update" msgstr "" +#: lib/block_scout_web/templates/account/api_key/index.html.eex:12 +#: lib/block_scout_web/templates/account/api_key/index.html.eex:18 +#, elixir-autogen, elixir-format +msgid "Learn more" +msgstr "" + #: lib/block_scout_web/templates/layout/app.html.eex:45 #, elixir-autogen, elixir-format msgid "Less than" @@ -1271,22 +1413,22 @@ msgstr "" msgid "License ID" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:297 +#: lib/block_scout_web/templates/transaction/overview.html.eex:305 #, elixir-autogen, elixir-format msgid "List of ERC-1155 tokens created in the transaction." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:281 +#: lib/block_scout_web/templates/transaction/overview.html.eex:289 #, elixir-autogen, elixir-format msgid "List of token burnt in the transaction." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:264 +#: lib/block_scout_web/templates/transaction/overview.html.eex:272 #, elixir-autogen, elixir-format msgid "List of token minted in the transaction." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:248 +#: lib/block_scout_web/templates/transaction/overview.html.eex:256 #, elixir-autogen, elixir-format msgid "List of token transferred in the transaction." msgstr "" @@ -1302,11 +1444,13 @@ msgstr "" #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:133 #: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:49 #: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:45 -#: lib/block_scout_web/templates/address_read_contract/index.html.eex:12 +#: lib/block_scout_web/templates/address_read_contract/index.html.eex:41 +#: lib/block_scout_web/templates/address_read_contract/index.html.eex:49 #: lib/block_scout_web/templates/address_read_proxy/index.html.eex:12 -#: lib/block_scout_web/templates/address_write_contract/index.html.eex:12 +#: lib/block_scout_web/templates/address_write_contract/index.html.eex:39 +#: lib/block_scout_web/templates/address_write_contract/index.html.eex:47 #: lib/block_scout_web/templates/address_write_proxy/index.html.eex:12 -#: lib/block_scout_web/templates/tokens/contract/index.html.eex:16 +#: lib/block_scout_web/templates/tokens/contract/index.html.eex:17 #, elixir-autogen, elixir-format msgid "Loading..." msgstr "" @@ -1325,8 +1469,8 @@ msgstr "" #: lib/block_scout_web/templates/address_logs/index.html.eex:10 #: lib/block_scout_web/templates/transaction/_tabs.html.eex:17 #: lib/block_scout_web/templates/transaction_log/index.html.eex:8 -#: lib/block_scout_web/views/address_view.ex:372 -#: lib/block_scout_web/views/transaction_view.ex:513 +#: lib/block_scout_web/views/address_view.ex:375 +#: lib/block_scout_web/views/transaction_view.ex:514 #, elixir-autogen, elixir-format msgid "Logs" msgstr "" @@ -1338,33 +1482,33 @@ msgstr "" #: lib/block_scout_web/templates/chain/show.html.eex:52 #: lib/block_scout_web/templates/layout/app.html.eex:46 -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:83 -#: lib/block_scout_web/views/address_view.ex:142 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:84 +#: lib/block_scout_web/views/address_view.ex:145 #, elixir-autogen, elixir-format msgid "Market Cap" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:374 +#: lib/block_scout_web/templates/transaction/overview.html.eex:382 #, elixir-autogen, elixir-format msgid "Max Fee per Gas" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:384 +#: lib/block_scout_web/templates/transaction/overview.html.eex:392 #, elixir-autogen, elixir-format msgid "Max Priority Fee per Gas" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:319 +#: lib/block_scout_web/views/transaction_view.ex:320 #, elixir-autogen, elixir-format msgid "Max of" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:364 +#: lib/block_scout_web/templates/transaction/overview.html.eex:372 #, elixir-autogen, elixir-format msgid "Maximum gas amount approved for the transaction." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:373 +#: lib/block_scout_web/templates/transaction/overview.html.eex:381 #, elixir-autogen, elixir-format msgid "Maximum total amount per unit of gas a user is willing to pay for a transaction, including base fee and priority fee." msgstr "" @@ -1438,6 +1582,16 @@ msgstr "" msgid "N/A bytes" msgstr "" +#: lib/block_scout_web/templates/account/api_key/form.html.eex:19 +#: lib/block_scout_web/templates/account/api_key/index.html.eex:28 +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:13 +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:28 +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:18 +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:22 +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:18 +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:22 +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:22 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:19 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:52 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:59 #: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:4 @@ -1446,6 +1600,27 @@ msgstr "" msgid "Name" msgstr "" +#: lib/block_scout_web/templates/account/api_key/form.html.eex:20 +#, elixir-autogen, elixir-format +msgid "Name this API key" +msgstr "" + +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:14 +#, elixir-autogen, elixir-format +msgid "Name this Custom ABI" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:19 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:20 +#, elixir-autogen, elixir-format +msgid "Name this address" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:19 +#, elixir-autogen, elixir-format +msgid "Name this transaction" +msgstr "" + #: lib/block_scout_web/templates/address_token/overview.html.eex:44 #, elixir-autogen, elixir-format msgid "Net Worth" @@ -1487,7 +1662,7 @@ msgid "No" msgstr "" #: lib/block_scout_web/templates/block/overview.html.eex:196 -#: lib/block_scout_web/templates/transaction/overview.html.eex:428 +#: lib/block_scout_web/templates/transaction/overview.html.eex:436 #, elixir-autogen, elixir-format msgid "Nonce" msgstr "" @@ -1497,38 +1672,38 @@ msgstr "" msgid "Not unique Token" msgstr "" -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:106 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:107 #, elixir-autogen, elixir-format msgid "Number of accounts holding the token" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:292 +#: lib/block_scout_web/templates/address/overview.html.eex:274 #, elixir-autogen, elixir-format msgid "Number of blocks validated by this validator." msgstr "" -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:129 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:130 #, elixir-autogen, elixir-format msgid "Number of digits that come after the decimal place when displaying token value" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:203 +#: lib/block_scout_web/templates/address/overview.html.eex:185 #, elixir-autogen, elixir-format msgid "Number of transactions related to this address." msgstr "" -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:117 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:118 #, elixir-autogen, elixir-format msgid "Number of transfers for the token" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:230 +#: lib/block_scout_web/templates/address/overview.html.eex:212 #, elixir-autogen, elixir-format msgid "Number of transfers to/from this address." msgstr "" #: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:40 -#: lib/block_scout_web/templates/transaction/_tile.html.eex:82 +#: lib/block_scout_web/templates/transaction/_tile.html.eex:88 #, elixir-autogen, elixir-format msgid "OUT" msgstr "" @@ -1555,6 +1730,13 @@ msgstr "" msgid "Other Explorers" msgstr "" +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:35 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:48 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:61 +#, elixir-autogen, elixir-format +msgid "Outgoing" +msgstr "" + #: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:24 #, elixir-autogen, elixir-format msgid "Owner Address" @@ -1583,8 +1765,8 @@ msgid "Parent Hash" msgstr "" #: lib/block_scout_web/templates/layout/_topnav.html.eex:60 -#: lib/block_scout_web/views/transaction_view.ex:348 -#: lib/block_scout_web/views/transaction_view.ex:386 +#: lib/block_scout_web/views/transaction_view.ex:349 +#: lib/block_scout_web/views/transaction_view.ex:387 #, elixir-autogen, elixir-format msgid "Pending" msgstr "" @@ -1600,7 +1782,7 @@ msgstr "" msgid "Play" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:430 +#: lib/block_scout_web/templates/transaction/overview.html.eex:438 #, elixir-autogen, elixir-format msgid "Position" msgstr "" @@ -1623,23 +1805,23 @@ msgstr "" #: lib/block_scout_web/templates/chain/show.html.eex:41 #: lib/block_scout_web/templates/layout/app.html.eex:47 -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:94 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:95 #, elixir-autogen, elixir-format msgid "Price" msgstr "" -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:93 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:94 #, elixir-autogen, elixir-format msgid "Price per token on the exchanges" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:344 +#: lib/block_scout_web/templates/transaction/overview.html.eex:352 #, elixir-autogen, elixir-format msgid "Price per unit of gas specified by the sender. Higher gas prices can prioritize transaction inclusion during times of high usage." msgstr "" #: lib/block_scout_web/templates/block/overview.html.eex:225 -#: lib/block_scout_web/templates/transaction/overview.html.eex:394 +#: lib/block_scout_web/templates/transaction/overview.html.eex:402 #, elixir-autogen, elixir-format msgid "Priority Fee / Tip" msgstr "" @@ -1649,6 +1831,33 @@ msgstr "" msgid "Priority Fees" msgstr "" +#: lib/block_scout_web/templates/account/common/_nav.html.eex:4 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:13 +#, elixir-autogen, elixir-format +msgid "Profile" +msgstr "" + +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:19 +#, elixir-autogen, elixir-format +msgid "Public Tags" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:20 +#, elixir-autogen, elixir-format +msgid "Public tag" +msgstr "" + +#: lib/block_scout_web/templates/account/common/_nav.html.eex:22 +#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Public tags" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:50 +#, elixir-autogen, elixir-format +msgid "Public tags* (2 tags maximum, please use \";\" as a divider)" +msgstr "" + #: lib/block_scout_web/templates/common_components/_btn_qr_code.html.eex:10 #: lib/block_scout_web/templates/common_components/_modal_qr_code.html.eex:5 #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:83 @@ -1656,7 +1865,7 @@ msgstr "" msgid "QR Code" msgstr "" -#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:109 +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:100 #, elixir-autogen, elixir-format msgid "Query" msgstr "" @@ -1666,21 +1875,21 @@ msgstr "" msgid "RPC" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:439 +#: lib/block_scout_web/templates/transaction/overview.html.eex:447 #, elixir-autogen, elixir-format msgid "Raw Input" msgstr "" #: lib/block_scout_web/templates/transaction/_tabs.html.eex:24 #: lib/block_scout_web/templates/transaction_raw_trace/index.html.eex:7 -#: lib/block_scout_web/views/transaction_view.ex:514 +#: lib/block_scout_web/views/transaction_view.ex:515 #, elixir-autogen, elixir-format msgid "Raw Trace" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:81 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:27 -#: lib/block_scout_web/views/address_view.ex:366 +#: lib/block_scout_web/views/address_view.ex:369 #: lib/block_scout_web/views/tokens/overview_view.ex:41 #, elixir-autogen, elixir-format msgid "Read Contract" @@ -1688,7 +1897,7 @@ msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:88 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:41 -#: lib/block_scout_web/views/address_view.ex:367 +#: lib/block_scout_web/views/address_view.ex:370 #, elixir-autogen, elixir-format msgid "Read Proxy" msgstr "" @@ -1698,11 +1907,32 @@ msgstr "" msgid "Records" msgstr "" +#: lib/block_scout_web/templates/account/api_key/row.html.eex:13 +#: lib/block_scout_web/templates/account/custom_abi/row.html.eex:13 +#, elixir-autogen, elixir-format +msgid "Remove" +msgstr "" + #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:155 #, elixir-autogen, elixir-format msgid "Request URL" msgstr "" +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Request a public tag/label" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:37 +#, elixir-autogen, elixir-format +msgid "Request to add public tag" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Request to edit a public tag/label" +msgstr "" + #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:142 #: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:38 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:138 @@ -1724,12 +1954,12 @@ msgstr "" msgid "Responses" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:93 +#: lib/block_scout_web/templates/transaction/overview.html.eex:98 #, elixir-autogen, elixir-format msgid "Result" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:130 +#: lib/block_scout_web/templates/transaction/overview.html.eex:135 #, elixir-autogen, elixir-format msgid "Revert reason" msgstr "" @@ -1746,6 +1976,15 @@ msgstr "" msgid "Run" msgstr "" +#: lib/block_scout_web/templates/account/api_key/form.html.eex:26 +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:31 +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:25 +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:25 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:83 +#, elixir-autogen, elixir-format +msgid "Save" +msgstr "" + #: lib/block_scout_web/templates/address_logs/index.html.eex:16 #: lib/block_scout_web/templates/layout/_search.html.eex:34 #, elixir-autogen, elixir-format @@ -1772,6 +2011,11 @@ msgstr "" msgid "Self-Destruct" msgstr "" +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:63 +#, elixir-autogen, elixir-format +msgid "Send request" +msgstr "" + #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:163 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:124 #, elixir-autogen, elixir-format @@ -1788,11 +2032,6 @@ msgstr "" msgid "Show QR Code" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:47 -#, elixir-autogen, elixir-format -msgid "Show Validator Info" -msgstr "" - #: lib/block_scout_web/templates/address_token/overview.html.eex:52 #, elixir-autogen, elixir-format msgid "Shows the current" @@ -1813,6 +2052,11 @@ msgstr "" msgid "Shows total assets held in the address" msgstr "" +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:20 +#, elixir-autogen, elixir-format +msgid "Sign out" +msgstr "" + #: lib/block_scout_web/templates/block/overview.html.eex:114 #, elixir-autogen, elixir-format msgid "Size" @@ -1828,6 +2072,17 @@ msgstr "" msgid "Slow" msgstr "" +#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:21 +#, elixir-autogen, elixir-format +msgid "Smart contract / Address" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/address_field.html.eex:4 +#: lib/block_scout_web/templates/account/public_tags_request/address_field.html.eex:5 +#, elixir-autogen, elixir-format +msgid "Smart contract / Address (0x...)" +msgstr "" + #: lib/block_scout_web/templates/address_coin_balance/index.html.eex:30 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:50 #: lib/block_scout_web/templates/address_logs/index.html.eex:23 @@ -1838,11 +2093,11 @@ msgstr "" #: lib/block_scout_web/templates/block_transaction/index.html.eex:22 #: lib/block_scout_web/templates/chain/show.html.eex:157 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:18 -#: lib/block_scout_web/templates/tokens/holder/index.html.eex:23 +#: lib/block_scout_web/templates/tokens/holder/index.html.eex:24 #: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:23 #: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:23 -#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:22 -#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:21 +#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:23 +#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:22 #: lib/block_scout_web/templates/transaction/index.html.eex:25 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:13 #: lib/block_scout_web/templates/transaction_log/index.html.eex:15 @@ -1886,24 +2141,29 @@ msgstr "" msgid "Static Call" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:105 +#: lib/block_scout_web/templates/transaction/overview.html.eex:110 #, elixir-autogen, elixir-format msgid "Status" msgstr "" +#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:22 +#, elixir-autogen, elixir-format +msgid "Submission date" +msgstr "" + #: lib/block_scout_web/templates/layout/_footer.html.eex:39 #, elixir-autogen, elixir-format msgid "Submit an Issue" msgstr "" #: lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex:8 -#: lib/block_scout_web/views/transaction_view.ex:350 +#: lib/block_scout_web/views/transaction_view.ex:351 #, elixir-autogen, elixir-format msgid "Success" msgstr "" #: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:21 -#: lib/block_scout_web/templates/transaction/_tile.html.eex:46 +#: lib/block_scout_web/templates/transaction/_tile.html.eex:52 #, elixir-autogen, elixir-format msgid "TX Fee" msgstr "" @@ -1928,7 +2188,7 @@ msgstr "" msgid "The block height of a particular block is defined as the number of blocks preceding it in the blockchain." msgstr "" -#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:41 +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:38 #, elixir-autogen, elixir-format msgid "The fallback function is executed on a call to the contract if none of the other functions match the given function signature, or if no data was supplied at all and there is no receive Ether function. The fallback function always receives data, but in order to also receive Ether it must be marked payable." msgstr "" @@ -1938,12 +2198,12 @@ msgstr "" msgid "The hash of the block from which this block was generated." msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:92 +#: lib/block_scout_web/templates/address/overview.html.eex:74 #, elixir-autogen, elixir-format msgid "The name found in the source code of the Contract." msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:103 +#: lib/block_scout_web/templates/address/overview.html.eex:85 #, elixir-autogen, elixir-format msgid "The name of the validator." msgstr "" @@ -1953,22 +2213,22 @@ msgstr "" msgid "The number of transactions in the block." msgstr "" -#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:43 +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:40 #, elixir-autogen, elixir-format msgid "The receive function is executed on a call to the contract with empty calldata. This is the function that is executed on plain Ether transfers (e.g. via .send() or .transfer()). If no such function exists, but a payable fallback function exists, the fallback function will be called on a plain Ether transfer. If neither a receive Ether nor a payable fallback function is present, the contract cannot receive Ether through regular transactions and throws an exception." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:129 +#: lib/block_scout_web/templates/transaction/overview.html.eex:134 #, elixir-autogen, elixir-format msgid "The revert reason of the transaction." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:104 +#: lib/block_scout_web/templates/transaction/overview.html.eex:109 #, elixir-autogen, elixir-format msgid "The status of the transaction: Confirmed or Unconfirmed." msgstr "" -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:67 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:68 #, elixir-autogen, elixir-format msgid "The total amount of tokens issued" msgstr "" @@ -1988,7 +2248,7 @@ msgstr "" msgid "There are no blocks." msgstr "" -#: lib/block_scout_web/templates/tokens/holder/index.html.eex:28 +#: lib/block_scout_web/templates/tokens/holder/index.html.eex:29 #, elixir-autogen, elixir-format msgid "There are no holders for this Token." msgstr "" @@ -2033,7 +2293,7 @@ msgstr "" msgid "There are no tokens for this address." msgstr "" -#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:27 +#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:28 #, elixir-autogen, elixir-format msgid "There are no tokens." msgstr "" @@ -2055,7 +2315,7 @@ msgstr "" #: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:28 #: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:28 -#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:26 +#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:27 #, elixir-autogen, elixir-format msgid "There are no transfers for this Token." msgstr "" @@ -2106,13 +2366,13 @@ msgstr "" msgid "This is useful to allow sending requests to blockscout without having to change anything about the request." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:59 +#: lib/block_scout_web/templates/transaction/overview.html.eex:64 #, elixir-autogen, elixir-format msgid "This transaction is pending confirmation." msgstr "" #: lib/block_scout_web/templates/block/overview.html.eex:71 -#: lib/block_scout_web/templates/transaction/overview.html.eex:172 +#: lib/block_scout_web/templates/transaction/overview.html.eex:177 #, elixir-autogen, elixir-format msgid "Timestamp" msgstr "" @@ -2120,7 +2380,7 @@ msgstr "" #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:32 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:34 #: lib/block_scout_web/templates/address_transaction/index.html.eex:28 -#: lib/block_scout_web/templates/transaction/overview.html.eex:215 +#: lib/block_scout_web/templates/transaction/overview.html.eex:221 #: lib/block_scout_web/views/address_internal_transaction_view.ex:9 #: lib/block_scout_web/views/address_token_transfer_view.ex:9 #: lib/block_scout_web/views/address_transaction_view.ex:9 @@ -2145,19 +2405,19 @@ msgstr "" msgid "Toggle navigation" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:73 +#: lib/block_scout_web/templates/address/overview.html.eex:55 #, elixir-autogen, elixir-format msgid "Token" msgstr "" #: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:3 -#: lib/block_scout_web/views/transaction_view.ex:448 +#: lib/block_scout_web/views/transaction_view.ex:449 #, elixir-autogen, elixir-format msgid "Token Burning" msgstr "" #: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:7 -#: lib/block_scout_web/views/transaction_view.ex:449 +#: lib/block_scout_web/views/transaction_view.ex:450 #, elixir-autogen, elixir-format msgid "Token Creation" msgstr "" @@ -2168,7 +2428,7 @@ msgstr "" msgid "Token Details" msgstr "" -#: lib/block_scout_web/templates/tokens/holder/index.html.eex:16 +#: lib/block_scout_web/templates/tokens/holder/index.html.eex:17 #: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:16 #: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:17 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:11 @@ -2185,14 +2445,14 @@ msgid "Token ID" msgstr "" #: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:5 -#: lib/block_scout_web/views/transaction_view.ex:447 +#: lib/block_scout_web/views/transaction_view.ex:448 #, elixir-autogen, elixir-format msgid "Token Minting" msgstr "" #: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:9 #: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:11 -#: lib/block_scout_web/views/transaction_view.ex:450 +#: lib/block_scout_web/views/transaction_view.ex:451 #, elixir-autogen, elixir-format msgid "Token Transfer" msgstr "" @@ -2202,54 +2462,54 @@ msgstr "" #: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:3 #: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:16 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:5 -#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:14 +#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:15 #: lib/block_scout_web/templates/transaction/_tabs.html.eex:4 #: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:7 -#: lib/block_scout_web/views/address_view.ex:363 +#: lib/block_scout_web/views/address_view.ex:366 #: lib/block_scout_web/views/tokens/instance/overview_view.ex:195 #: lib/block_scout_web/views/tokens/overview_view.ex:39 -#: lib/block_scout_web/views/transaction_view.ex:511 +#: lib/block_scout_web/views/transaction_view.ex:512 #, elixir-autogen, elixir-format msgid "Token Transfers" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:72 +#: lib/block_scout_web/templates/address/overview.html.eex:54 #, elixir-autogen, elixir-format msgid "Token name and symbol." msgstr "" -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:141 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:142 #, elixir-autogen, elixir-format msgid "Token type" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:21 -#: lib/block_scout_web/templates/address/overview.html.eex:193 +#: lib/block_scout_web/templates/address/overview.html.eex:175 #: lib/block_scout_web/templates/address_token/overview.html.eex:58 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:13 #: lib/block_scout_web/templates/layout/_topnav.html.eex:73 #: lib/block_scout_web/templates/tokens/index.html.eex:10 -#: lib/block_scout_web/views/address_view.ex:360 +#: lib/block_scout_web/views/address_view.ex:363 #, elixir-autogen, elixir-format msgid "Tokens" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:282 +#: lib/block_scout_web/templates/transaction/overview.html.eex:290 #, elixir-autogen, elixir-format msgid "Tokens Burnt" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:298 +#: lib/block_scout_web/templates/transaction/overview.html.eex:306 #, elixir-autogen, elixir-format msgid "Tokens Created" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:265 +#: lib/block_scout_web/templates/transaction/overview.html.eex:273 #, elixir-autogen, elixir-format msgid "Tokens Minted" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:249 +#: lib/block_scout_web/templates/transaction/overview.html.eex:257 #, elixir-autogen, elixir-format msgid "Tokens Transferred" msgstr "" @@ -2275,7 +2535,7 @@ msgstr "" msgid "Total Difficulty" msgstr "" -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:82 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:83 #, elixir-autogen, elixir-format msgid "Total Supply * Price" msgstr "" @@ -2295,12 +2555,12 @@ msgstr "" msgid "Total gas limit provided by all transactions in the block." msgstr "" -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:68 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:69 #, elixir-autogen, elixir-format msgid "Total supply" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:329 +#: lib/block_scout_web/templates/transaction/overview.html.eex:337 #, elixir-autogen, elixir-format msgid "Total transaction fee." msgstr "" @@ -2310,8 +2570,10 @@ msgstr "" msgid "Total transactions" msgstr "" +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:11 +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:23 #: lib/block_scout_web/templates/address_logs/_logs.html.eex:19 -#: lib/block_scout_web/views/transaction_view.ex:460 +#: lib/block_scout_web/views/transaction_view.ex:461 #, elixir-autogen, elixir-format msgid "Transaction" msgstr "" @@ -2326,7 +2588,7 @@ msgstr "" msgid "Transaction %{transaction}, %{subnetwork} %{transaction}" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:404 +#: lib/block_scout_web/templates/transaction/overview.html.eex:412 #, elixir-autogen, elixir-format msgid "Transaction Burnt Fee" msgstr "" @@ -2336,12 +2598,12 @@ msgstr "" msgid "Transaction Details" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:330 +#: lib/block_scout_web/templates/transaction/overview.html.eex:338 #, elixir-autogen, elixir-format msgid "Transaction Fee" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:75 +#: lib/block_scout_web/templates/transaction/overview.html.eex:80 #, elixir-autogen, elixir-format msgid "Transaction Hash" msgstr "" @@ -2352,36 +2614,36 @@ msgstr "" msgid "Transaction Inputs" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:354 +#: lib/block_scout_web/templates/transaction/overview.html.eex:362 #, elixir-autogen, elixir-format msgid "Transaction Type" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:427 +#: lib/block_scout_web/templates/transaction/overview.html.eex:435 #, elixir-autogen, elixir-format msgid "Transaction number from the sending address. Each transaction sent from an address increments the nonce by 1." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:353 +#: lib/block_scout_web/templates/transaction/overview.html.eex:361 #, elixir-autogen, elixir-format msgid "Transaction type, introduced in EIP-2718." msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:7 -#: lib/block_scout_web/templates/address/overview.html.eex:204 -#: lib/block_scout_web/templates/address/overview.html.eex:210 -#: lib/block_scout_web/templates/address/overview.html.eex:218 +#: lib/block_scout_web/templates/address/overview.html.eex:186 +#: lib/block_scout_web/templates/address/overview.html.eex:192 +#: lib/block_scout_web/templates/address/overview.html.eex:200 #: lib/block_scout_web/templates/address_transaction/index.html.eex:13 #: lib/block_scout_web/templates/block/overview.html.eex:80 #: lib/block_scout_web/templates/block_transaction/index.html.eex:10 #: lib/block_scout_web/templates/chain/show.html.eex:213 #: lib/block_scout_web/templates/layout/_topnav.html.eex:48 -#: lib/block_scout_web/views/address_view.ex:362 +#: lib/block_scout_web/views/address_view.ex:365 #, elixir-autogen, elixir-format msgid "Transactions" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:119 +#: lib/block_scout_web/templates/address/overview.html.eex:101 #, elixir-autogen, elixir-format msgid "Transactions and address of creation." msgstr "" @@ -2391,11 +2653,11 @@ msgstr "" msgid "Transactions sent" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:231 -#: lib/block_scout_web/templates/address/overview.html.eex:237 -#: lib/block_scout_web/templates/address/overview.html.eex:245 +#: lib/block_scout_web/templates/address/overview.html.eex:213 +#: lib/block_scout_web/templates/address/overview.html.eex:219 +#: lib/block_scout_web/templates/address/overview.html.eex:227 #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:50 -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:118 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:119 #, elixir-autogen, elixir-format msgid "Transfers" msgstr "" @@ -2422,12 +2684,12 @@ msgstr "" msgid "Type" msgstr "" -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:140 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:141 #, elixir-autogen, elixir-format msgid "Type of the token standard" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:453 +#: lib/block_scout_web/templates/transaction/overview.html.eex:461 #, elixir-autogen, elixir-format msgid "UTF-8" msgstr "" @@ -2443,7 +2705,7 @@ msgstr "" msgid "Uncles" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:341 +#: lib/block_scout_web/views/transaction_view.ex:342 #, elixir-autogen, elixir-format msgid "Unconfirmed" msgstr "" @@ -2453,17 +2715,17 @@ msgstr "" msgid "Unique Token" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:74 +#: lib/block_scout_web/templates/transaction/overview.html.eex:79 #, elixir-autogen, elixir-format msgid "Unique character string (TxID) assigned to every verified transaction." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:383 +#: lib/block_scout_web/templates/transaction/overview.html.eex:391 #, elixir-autogen, elixir-format msgid "User defined maximum fee (tip) per unit of gas paid to validator for transaction prioritization." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:393 +#: lib/block_scout_web/templates/transaction/overview.html.eex:401 #, elixir-autogen, elixir-format msgid "User-defined tip sent to validator for transaction priority/inclusion." msgstr "" @@ -2493,26 +2755,27 @@ msgstr "" msgid "Validator Data" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:51 -#, elixir-autogen, elixir-format -msgid "Validator Info" -msgstr "" - -#: lib/block_scout_web/templates/address/overview.html.eex:104 +#: lib/block_scout_web/templates/address/overview.html.eex:86 #, elixir-autogen, elixir-format msgid "Validator Name" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:315 +#: lib/block_scout_web/templates/transaction/overview.html.eex:323 #, elixir-autogen, elixir-format msgid "Value" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:314 +#: lib/block_scout_web/templates/transaction/overview.html.eex:322 #, elixir-autogen, elixir-format msgid "Value sent in the native token (and USD) if applicable." msgstr "" +#: lib/block_scout_web/templates/address_read_contract/index.html.eex:17 +#: lib/block_scout_web/templates/address_write_contract/index.html.eex:15 +#, elixir-autogen, elixir-format +msgid "Verified" +msgstr "" + #: lib/block_scout_web/templates/address_contract/index.html.eex:82 #, elixir-autogen, elixir-format msgid "Verified at" @@ -2520,10 +2783,8 @@ msgstr "" #: lib/block_scout_web/templates/address_contract/index.html.eex:27 #: lib/block_scout_web/templates/address_contract/index.html.eex:29 -#: lib/block_scout_web/templates/address_contract/index.html.eex:160 -#: lib/block_scout_web/templates/address_contract/index.html.eex:166 -#: lib/block_scout_web/templates/address_contract/index.html.eex:197 -#: lib/block_scout_web/templates/address_contract/index.html.eex:203 +#: lib/block_scout_web/templates/address_contract/index.html.eex:161 +#: lib/block_scout_web/templates/address_contract/index.html.eex:192 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:14 #, elixir-autogen, elixir-format msgid "Verify & Publish" @@ -2586,12 +2847,12 @@ msgstr "" msgid "View Contract" msgstr "" -#: lib/block_scout_web/templates/transaction/_tile.html.eex:67 +#: lib/block_scout_web/templates/transaction/_tile.html.eex:73 #, elixir-autogen, elixir-format msgid "View Less Transfers" msgstr "" -#: lib/block_scout_web/templates/transaction/_tile.html.eex:66 +#: lib/block_scout_web/templates/transaction/_tile.html.eex:72 #, elixir-autogen, elixir-format msgid "View More Transfers" msgstr "" @@ -2626,7 +2887,7 @@ msgstr "" msgid "Vyper contract" msgstr "" -#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:145 +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:136 #, elixir-autogen, elixir-format msgid "WEI" msgstr "" @@ -2646,26 +2907,33 @@ msgstr "" msgid "Warning! Contract bytecode has been changed and doesn't match the verified one. Therefore, interaction with this smart contract may be risky." msgstr "" +#: lib/block_scout_web/templates/account/common/_nav.html.eex:7 +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:7 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:14 +#, elixir-autogen, elixir-format +msgid "Watch list" +msgstr "" + #: lib/block_scout_web/views/wei_helpers.ex:76 #, elixir-autogen, elixir-format msgid "Wei" msgstr "" -#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:109 +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:100 #, elixir-autogen, elixir-format msgid "Write" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:95 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:34 -#: lib/block_scout_web/views/address_view.ex:368 +#: lib/block_scout_web/views/address_view.ex:371 #, elixir-autogen, elixir-format msgid "Write Contract" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:102 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:48 -#: lib/block_scout_web/views/address_view.ex:369 +#: lib/block_scout_web/views/address_view.ex:372 #, elixir-autogen, elixir-format msgid "Write Proxy" msgstr "" @@ -2678,7 +2946,32 @@ msgstr "" msgid "Yes" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:129 +#: lib/block_scout_web/templates/account/api_key/index.html.eex:18 +#, elixir-autogen, elixir-format +msgid "You can create 3 API keys per account." +msgstr "" + +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:18 +#, elixir-autogen, elixir-format +msgid "You can create up to 15 Custom ABIs per account." +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:11 +#, elixir-autogen, elixir-format +msgid "You can request a public category tag which is displayed to all Blockscout users. Public tags may be added to contract or external addresses, and any associated transactions will inherit that tag. Clicking a tag opens a page with related information and helps provide context and data organization. Requests are sent to a moderator for review and approval. This process can take several days." +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:15 +#, elixir-autogen, elixir-format +msgid "Your name" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:14 +#, elixir-autogen, elixir-format +msgid "Your name*" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:111 #, elixir-autogen, elixir-format msgid "at" msgstr "" @@ -2688,7 +2981,7 @@ msgstr "" msgid "balance of the address" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:403 +#: lib/block_scout_web/templates/transaction/overview.html.eex:411 #, elixir-autogen, elixir-format msgid "burned for this transaction. Equals Block Base Fee per Gas * Gas Used." msgstr "" @@ -2703,7 +2996,7 @@ msgstr "" msgid "button" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:223 +#: lib/block_scout_web/templates/transaction/overview.html.eex:230 #, elixir-autogen, elixir-format msgid "created" msgstr "" @@ -2713,7 +3006,7 @@ msgstr "" msgid "custom RPC" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:167 +#: lib/block_scout_web/templates/address/overview.html.eex:149 #, elixir-autogen, elixir-format msgid "doesn't include ERC20, ERC721, ERC1155 tokens)." msgstr "" @@ -2723,7 +3016,7 @@ msgstr "" msgid "elements are displayed" msgstr "" -#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:41 +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:38 #, elixir-autogen, elixir-format msgid "fallback" msgstr "" @@ -2765,7 +3058,7 @@ msgstr "" msgid "page" msgstr "" -#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:43 +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:40 #, elixir-autogen, elixir-format msgid "receive" msgstr "" @@ -2876,3 +3169,120 @@ msgstr "" #, elixir-autogen, elixir-format msgid "The requested path was not found on BlockScout." msgstr "" + +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:25 +#, elixir-autogen, elixir-format +msgid "Actions" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:38 +#, elixir-autogen, elixir-format +msgid "Add address" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Add address to the Watch list" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:81 +#, elixir-autogen, elixir-format +msgid "Back to Watch list (Cancel)" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Edit Watch list address" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:77 +#, elixir-autogen, elixir-format +msgid "Remove from Watch list" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:14 +#, elixir-autogen, elixir-format +msgid "You don't have address tags yet" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:14 +#, elixir-autogen, elixir-format +msgid "You don't have addresses on you watchlist yet" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:14 +#, elixir-autogen, elixir-format +msgid "You don't have transaction tags yet" +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/form.html.eex:7 +#: lib/block_scout_web/templates/account/api_key/form.html.eex:13 +#: lib/block_scout_web/templates/account/api_key/form.html.eex:14 +#: lib/block_scout_web/templates/account/api_key/index.html.eex:29 +#, elixir-autogen, elixir-format +msgid "API key" +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/index.html.eex:7 +#: lib/block_scout_web/templates/account/common/_nav.html.eex:16 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:17 +#, elixir-autogen, elixir-format +msgid "API keys" +msgstr "" + +#: lib/block_scout_web/templates/account/common/_nav.html.eex:10 +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:7 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:15 +#, elixir-autogen, elixir-format +msgid "Address Tags" +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/form.html.eex:25 +#, elixir-autogen, elixir-format +msgid "Back to API keys (Cancel)" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:24 +#, elixir-autogen, elixir-format +msgid "Back to Address Tags (Cancel)" +msgstr "" + +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:30 +#, elixir-autogen, elixir-format +msgid "Back to Custom ABI (Cancel)" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:24 +#, elixir-autogen, elixir-format +msgid "Back to Transaction Tags (Cancel)" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:30 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:43 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:56 +#, elixir-autogen, elixir-format +msgid "Incoming" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:68 +#, elixir-autogen, elixir-format +msgid "Please select notification methods:" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:24 +#, elixir-autogen, elixir-format +msgid "Please select what types of notifications you will receive:" +msgstr "" + +#: lib/block_scout_web/templates/account/common/_nav.html.eex:13 +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:7 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:16 +#, elixir-autogen, elixir-format +msgid "Transaction Tags" +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/form.html.eex:7 +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:8 +#, elixir-autogen, elixir-format +msgid "Update" +msgstr "" diff --git a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po index be81fc4f99..df1c48c61c 100644 --- a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po +++ b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po @@ -63,7 +63,7 @@ msgstr "" msgid "%{subnetwork} Explorer - BlockScout" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:349 +#: lib/block_scout_web/views/transaction_view.ex:350 #, elixir-autogen, elixir-format msgid "(Awaiting internal transactions for status)" msgstr "" @@ -101,6 +101,11 @@ msgstr "" msgid "A string with the name of the module to be invoked." msgstr "" +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:24 +#, elixir-autogen, elixir-format +msgid "ABI" +msgstr "" + #: lib/block_scout_web/templates/address_contract_verification_common_fields/_constructor_args.html.eex:3 #, elixir-autogen, elixir-format msgid "ABI-encoded Constructor Arguments (if required by the contract)" @@ -126,48 +131,83 @@ msgstr "" msgid "APIs" msgstr "" +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:24 +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:24 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:69 #, elixir-autogen, elixir-format msgid "Action" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:417 +#: lib/block_scout_web/templates/transaction/overview.html.eex:425 #, elixir-autogen, elixir-format msgid "Actual gas amount used by the transaction." msgstr "" +#: lib/block_scout_web/templates/account/api_key/form.html.eex:7 +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:8 #: lib/block_scout_web/templates/layout/_add_chain_to_mm.html.eex:11 #, elixir-autogen, elixir-format msgid "Add" msgstr "" +#: lib/block_scout_web/templates/account/api_key/index.html.eex:44 +#, elixir-autogen, elixir-format +msgid "Add API key" +msgstr "" + +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:44 +#, elixir-autogen, elixir-format +msgid "Add Custom ABI" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:7 +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:37 +#, elixir-autogen, elixir-format +msgid "Add address tag" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:7 +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:37 +#, elixir-autogen, elixir-format +msgid "Add transaction tag" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:11 +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:23 +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:23 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:12 #: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:16 #: lib/block_scout_web/templates/transaction_log/_logs.html.eex:20 -#: lib/block_scout_web/views/address_view.ex:104 +#: lib/block_scout_web/views/address_view.ex:107 #, elixir-autogen, elixir-format msgid "Address" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:211 +#: lib/block_scout_web/templates/transaction/overview.html.eex:217 #, elixir-autogen, elixir-format msgid "Address (external or contract) receiving the transaction." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:194 +#: lib/block_scout_web/templates/transaction/overview.html.eex:199 #, elixir-autogen, elixir-format msgid "Address (external or contract) sending the transaction." msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:167 +#: lib/block_scout_web/templates/address/overview.html.eex:149 #, elixir-autogen, elixir-format msgid "Address balance in" msgstr "" -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:50 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:51 #, elixir-autogen, elixir-format msgid "Address of the token contract" msgstr "" +#: lib/block_scout_web/templates/account/public_tags_request/address_field.html.eex:2 +#, elixir-autogen, elixir-format +msgid "Address*" +msgstr "" + #: lib/block_scout_web/templates/address/index.html.eex:5 #, elixir-autogen, elixir-format msgid "Addresses" @@ -194,12 +234,12 @@ msgstr "" msgid "All metadata displayed below is from that contract. In order to verify current contract, click" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:192 +#: lib/block_scout_web/templates/address/overview.html.eex:174 #, elixir-autogen, elixir-format msgid "All tokens in the account and total value." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:403 +#: lib/block_scout_web/templates/transaction/overview.html.eex:411 #, elixir-autogen, elixir-format msgid "Amount of" msgstr "" @@ -235,7 +275,8 @@ msgstr "" msgid "Back Home" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:168 +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:24 +#: lib/block_scout_web/templates/address/overview.html.eex:150 #: lib/block_scout_web/templates/address_token/overview.html.eex:51 #, elixir-autogen, elixir-format msgid "Balance" @@ -257,14 +298,14 @@ msgstr "" msgid "Base URL:" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:438 +#: lib/block_scout_web/templates/transaction/overview.html.eex:446 #, elixir-autogen, elixir-format msgid "Binary data included with the transaction. See input / logs below for additional info." msgstr "" #: lib/block_scout_web/templates/address_coin_balance/_coin_balances.html.eex:8 #: lib/block_scout_web/templates/block/overview.html.eex:29 -#: lib/block_scout_web/templates/transaction/overview.html.eex:153 +#: lib/block_scout_web/templates/transaction/overview.html.eex:158 #, elixir-autogen, elixir-format msgid "Block" msgstr "" @@ -296,7 +337,7 @@ msgstr "" msgid "Block Mined, awaiting import..." msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:33 +#: lib/block_scout_web/views/transaction_view.ex:34 #, elixir-autogen, elixir-format msgid "Block Pending" msgstr "" @@ -311,12 +352,12 @@ msgstr "" msgid "Block not found, please try again later." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:152 +#: lib/block_scout_web/templates/transaction/overview.html.eex:157 #, elixir-autogen, elixir-format msgid "Block number containing the transaction." msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:275 +#: lib/block_scout_web/templates/address/overview.html.eex:257 #, elixir-autogen, elixir-format msgid "Block number in which the address was updated." msgstr "" @@ -339,9 +380,9 @@ msgid "Blocks Indexed" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:48 -#: lib/block_scout_web/templates/address/overview.html.eex:293 +#: lib/block_scout_web/templates/address/overview.html.eex:275 #: lib/block_scout_web/templates/address_validation/index.html.eex:11 -#: lib/block_scout_web/views/address_view.ex:371 +#: lib/block_scout_web/views/address_view.ex:374 #, elixir-autogen, elixir-format msgid "Blocks Validated" msgstr "" @@ -378,6 +419,7 @@ msgstr "" msgid "Call Code" msgstr "" +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:62 #: lib/block_scout_web/templates/address_contract_verification/new.html.eex:120 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:145 #: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:41 @@ -420,13 +462,13 @@ msgstr "" #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:187 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:126 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:149 -#: lib/block_scout_web/views/address_view.ex:364 +#: lib/block_scout_web/views/address_view.ex:367 #, elixir-autogen, elixir-format msgid "Code" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:34 -#: lib/block_scout_web/views/address_view.ex:370 +#: lib/block_scout_web/views/address_view.ex:373 #, elixir-autogen, elixir-format msgid "Coin Balance History" msgstr "" @@ -436,6 +478,16 @@ msgstr "" msgid "Collapse" msgstr "" +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:20 +#, elixir-autogen, elixir-format +msgid "Company name" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:32 +#, elixir-autogen, elixir-format +msgid "Company website" +msgstr "" + #: lib/block_scout_web/templates/address_contract_verification_common_fields/_compiler_field.html.eex:3 #, elixir-autogen, elixir-format msgid "Compiler" @@ -446,17 +498,17 @@ msgstr "" msgid "Compiler version" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:342 +#: lib/block_scout_web/views/transaction_view.ex:343 #, elixir-autogen, elixir-format msgid "Confirmed" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:119 +#: lib/block_scout_web/templates/transaction/overview.html.eex:124 #, elixir-autogen, elixir-format msgid "Confirmed by " msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:185 +#: lib/block_scout_web/templates/transaction/overview.html.eex:190 #, elixir-autogen, elixir-format msgid "Confirmed within" msgstr "" @@ -467,7 +519,7 @@ msgstr "" #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:4 #: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:6 #: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:4 -#: lib/block_scout_web/templates/tokens/holder/index.html.eex:15 +#: lib/block_scout_web/templates/tokens/holder/index.html.eex:16 #, elixir-autogen, elixir-format msgid "Connection Lost" msgstr "" @@ -500,8 +552,8 @@ msgstr "" msgid "Constructor Arguments" msgstr "" -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:51 -#: lib/block_scout_web/templates/transaction/overview.html.eex:221 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:52 +#: lib/block_scout_web/templates/transaction/overview.html.eex:227 #, elixir-autogen, elixir-format msgid "Contract" msgstr "" @@ -511,25 +563,27 @@ msgstr "" msgid "Contract ABI" msgstr "" +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:18 +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:29 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_contract_address_field.html.eex:3 -#: lib/block_scout_web/views/address_view.ex:102 +#: lib/block_scout_web/views/address_view.ex:105 #, elixir-autogen, elixir-format msgid "Contract Address" msgstr "" #: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:16 -#: lib/block_scout_web/views/address_view.ex:42 -#: lib/block_scout_web/views/address_view.ex:76 +#: lib/block_scout_web/views/address_view.ex:45 +#: lib/block_scout_web/views/address_view.ex:79 #, elixir-autogen, elixir-format msgid "Contract Address Pending" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:457 +#: lib/block_scout_web/views/transaction_view.ex:458 #, elixir-autogen, elixir-format msgid "Contract Call" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:454 +#: lib/block_scout_web/views/transaction_view.ex:455 #, elixir-autogen, elixir-format msgid "Contract Creation" msgstr "" @@ -546,7 +600,7 @@ msgstr "" msgid "Contract Libraries" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:93 +#: lib/block_scout_web/templates/address/overview.html.eex:75 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_contract_name_field.html.eex:3 #, elixir-autogen, elixir-format msgid "Contract Name" @@ -583,16 +637,33 @@ msgstr "" msgid "Copy ABI" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:37 +#: lib/block_scout_web/templates/account/api_key/row.html.eex:6 +#: lib/block_scout_web/templates/account/api_key/row.html.eex:6 +#, elixir-autogen, elixir-format +msgid "Copy API key" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_address/row.html.eex:8 +#: lib/block_scout_web/templates/account/tag_address/row.html.eex:8 +#: lib/block_scout_web/templates/account/tag_transaction/row.html.eex:11 +#: lib/block_scout_web/templates/account/tag_transaction/row.html.eex:11 +#: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:7 #: lib/block_scout_web/templates/address/overview.html.eex:38 +#: lib/block_scout_web/templates/address/overview.html.eex:39 #: lib/block_scout_web/templates/block/overview.html.eex:104 #: lib/block_scout_web/templates/block/overview.html.eex:105 -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:42 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:43 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:44 #, elixir-autogen, elixir-format msgid "Copy Address" msgstr "" +#: lib/block_scout_web/templates/account/custom_abi/row.html.eex:6 +#: lib/block_scout_web/templates/account/custom_abi/row.html.eex:6 +#, elixir-autogen, elixir-format +msgid "Copy Contract Address" +msgstr "" + #: lib/block_scout_web/templates/address_contract/index.html.eex:140 #: lib/block_scout_web/templates/address_contract/index.html.eex:156 #, elixir-autogen, elixir-format @@ -604,16 +675,17 @@ msgstr "" msgid "Copy Decompiled Contract Code" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:183 -#: lib/block_scout_web/templates/address_contract/index.html.eex:193 +#: lib/block_scout_web/templates/address_contract/index.html.eex:177 +#: lib/block_scout_web/templates/address_contract/index.html.eex:187 #, elixir-autogen, elixir-format msgid "Copy Deployed ByteCode" msgstr "" -#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:14 -#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:15 -#: lib/block_scout_web/templates/transaction/overview.html.eex:201 -#: lib/block_scout_web/templates/transaction/overview.html.eex:202 +#: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:7 +#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:17 +#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:18 +#: lib/block_scout_web/templates/transaction/overview.html.eex:207 +#: lib/block_scout_web/templates/transaction/overview.html.eex:208 #, elixir-autogen, elixir-format msgid "Copy From Address" msgstr "" @@ -646,12 +718,12 @@ msgstr "" msgid "Copy Source Code" msgstr "" -#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:31 -#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:32 -#: lib/block_scout_web/templates/transaction/overview.html.eex:227 -#: lib/block_scout_web/templates/transaction/overview.html.eex:228 +#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:34 +#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:35 #: lib/block_scout_web/templates/transaction/overview.html.eex:234 #: lib/block_scout_web/templates/transaction/overview.html.eex:235 +#: lib/block_scout_web/templates/transaction/overview.html.eex:242 +#: lib/block_scout_web/templates/transaction/overview.html.eex:243 #, elixir-autogen, elixir-format msgid "Copy To Address" msgstr "" @@ -662,30 +734,30 @@ msgstr "" msgid "Copy Token ID" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:82 +#: lib/block_scout_web/templates/transaction/overview.html.eex:87 #, elixir-autogen, elixir-format msgid "Copy Transaction Hash" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:83 +#: lib/block_scout_web/templates/transaction/overview.html.eex:88 #, elixir-autogen, elixir-format msgid "Copy Txn Hash" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:464 +#: lib/block_scout_web/templates/transaction/overview.html.eex:472 #, elixir-autogen, elixir-format msgid "Copy Txn Hex Input" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:470 +#: lib/block_scout_web/templates/transaction/overview.html.eex:478 #, elixir-autogen, elixir-format msgid "Copy Txn UTF-8 Input" msgstr "" #: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:20 #: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:41 -#: lib/block_scout_web/templates/transaction/overview.html.eex:463 -#: lib/block_scout_web/templates/transaction/overview.html.eex:469 +#: lib/block_scout_web/templates/transaction/overview.html.eex:471 +#: lib/block_scout_web/templates/transaction/overview.html.eex:477 #: lib/block_scout_web/templates/transaction_raw_trace/index.html.eex:14 #, elixir-autogen, elixir-format msgid "Copy Value" @@ -696,12 +768,22 @@ msgstr "" msgid "Create" msgstr "" +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:12 +#, elixir-autogen, elixir-format +msgid "Create a Custom ABI to interact with contracts." +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/index.html.eex:12 +#, elixir-autogen, elixir-format +msgid "Create an API key to use with your RPC и EthRPC API requests." +msgstr "" + #: lib/block_scout_web/views/internal_transaction_view.ex:26 #, elixir-autogen, elixir-format msgid "Create2" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:120 +#: lib/block_scout_web/templates/address/overview.html.eex:102 #, elixir-autogen, elixir-format msgid "Creator" msgstr "" @@ -712,11 +794,31 @@ msgstr "" msgid "Curl" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:92 +#: lib/block_scout_web/templates/transaction/overview.html.eex:97 #, elixir-autogen, elixir-format msgid "Current transaction state: Success, Failed (Error), or Pending (In Process)" msgstr "" +#: lib/block_scout_web/templates/address_read_contract/index.html.eex:20 +#: lib/block_scout_web/templates/address_write_contract/index.html.eex:18 +#, elixir-autogen, elixir-format +msgid "Custom" +msgstr "" + +#: lib/block_scout_web/templates/account/common/_nav.html.eex:19 +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:8 +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:7 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:18 +#, elixir-autogen, elixir-format +msgid "Custom ABI" +msgstr "" + +#: lib/block_scout_web/templates/address_read_contract/index.html.eex:25 +#: lib/block_scout_web/templates/address_write_contract/index.html.eex:23 +#, elixir-autogen, elixir-format +msgid "Custom ABI from account" +msgstr "" + #: lib/block_scout_web/templates/chain/show.html.eex:69 #, elixir-autogen, elixir-format msgid "Daily Transactions" @@ -735,13 +837,13 @@ msgstr "" msgid "Date & time at which block was produced." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:171 +#: lib/block_scout_web/templates/transaction/overview.html.eex:176 #, elixir-autogen, elixir-format msgid "Date & time of transaction inclusion, including length of time for confirmation." msgstr "" #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:52 -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:130 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:131 #, elixir-autogen, elixir-format msgid "Decimals" msgstr "" @@ -757,7 +859,7 @@ msgstr "" msgid "Decoded" msgstr "" -#: lib/block_scout_web/views/address_view.ex:365 +#: lib/block_scout_web/views/address_view.ex:368 #, elixir-autogen, elixir-format msgid "Decompiled Code" msgstr "" @@ -782,8 +884,8 @@ msgstr "" msgid "Delegate Call" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:181 -#: lib/block_scout_web/templates/address_contract/index.html.eex:189 +#: lib/block_scout_web/templates/address_contract/index.html.eex:175 +#: lib/block_scout_web/templates/address_contract/index.html.eex:183 #, elixir-autogen, elixir-format msgid "Deployed ByteCode" msgstr "" @@ -796,6 +898,11 @@ msgstr "" msgid "Description" msgstr "" +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:56 +#, elixir-autogen, elixir-format +msgid "Description*" +msgstr "" + #: lib/block_scout_web/templates/address/overview.html.eex:30 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:166 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:127 @@ -838,30 +945,45 @@ msgstr "" msgid "During times when the network is busy (i.e during ICOs) it can take a while for your transaction to propagate through the network and for us to index it." msgstr "" +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:27 +#, elixir-autogen, elixir-format +msgid "E-mail*" +msgstr "" + #: lib/block_scout_web/templates/common_components/_minimal_proxy_pattern.html.eex:6 #, elixir-autogen, elixir-format msgid "EIP-1167" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:214 +#: lib/block_scout_web/views/transaction_view.ex:215 #, elixir-autogen, elixir-format msgid "ERC-1155 " msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:212 +#: lib/block_scout_web/views/transaction_view.ex:213 #, elixir-autogen, elixir-format msgid "ERC-20 " msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:213 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:40 +#, elixir-autogen, elixir-format +msgid "ERC-20 tokens (beta)" +msgstr "" + +#: lib/block_scout_web/views/transaction_view.ex:214 #, elixir-autogen, elixir-format msgid "ERC-721 " msgstr "" +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:53 +#, elixir-autogen, elixir-format +msgid "ERC-721, ERC-1155 tokens (NFT) (beta)" +msgstr "" + #: lib/block_scout_web/templates/address_token/overview.html.eex:1 -#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:104 -#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:104 -#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:146 +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:95 +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:95 +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:137 #, elixir-autogen, elixir-format msgid "ETH" msgstr "" @@ -883,6 +1005,18 @@ msgstr "" msgid "Easy Cowboy! This block does not exist yet!" msgstr "" +#: lib/block_scout_web/templates/account/api_key/row.html.eex:16 +#: lib/block_scout_web/templates/account/custom_abi/row.html.eex:16 +#: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:27 +#, elixir-autogen, elixir-format +msgid "Edit" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:71 +#, elixir-autogen, elixir-format +msgid "Email notifications" +msgstr "" + #: lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex:5 #, elixir-autogen, elixir-format msgid "Emission Contract" @@ -909,7 +1043,7 @@ msgstr "" msgid "Error" msgstr "" -#: lib/block_scout_web/templates/transaction/_tile.html.eex:9 +#: lib/block_scout_web/templates/transaction/_tile.html.eex:11 #, elixir-autogen, elixir-format msgid "Error in internal transactions" msgstr "" @@ -924,17 +1058,17 @@ msgstr "" msgid "Error trying to fetch balances." msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:353 +#: lib/block_scout_web/views/transaction_view.ex:354 #, elixir-autogen, elixir-format msgid "Error: %{reason}" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:351 +#: lib/block_scout_web/views/transaction_view.ex:352 #, elixir-autogen, elixir-format msgid "Error: (Awaiting internal transactions for reason)" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:137 +#: lib/block_scout_web/templates/address/overview.html.eex:119 #, elixir-autogen, elixir-format msgid "Error: Could not determine contract creator." msgstr "" @@ -944,16 +1078,18 @@ msgstr "" msgid "Eth RPC" msgstr "" +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:27 +#: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:21 #: lib/block_scout_web/templates/address/_current_coin_balance.html.eex:11 #: lib/block_scout_web/templates/address/index.html.eex:5 -#: lib/block_scout_web/templates/address/overview.html.eex:181 +#: lib/block_scout_web/templates/address/overview.html.eex:163 #: lib/block_scout_web/templates/block/overview.html.eex:215 #: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:24 #: lib/block_scout_web/templates/layout/_topnav.html.eex:82 #: lib/block_scout_web/templates/layout/app.html.eex:48 #: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:20 -#: lib/block_scout_web/templates/transaction/_tile.html.eex:43 -#: lib/block_scout_web/templates/transaction/overview.html.eex:403 +#: lib/block_scout_web/templates/transaction/_tile.html.eex:49 +#: lib/block_scout_web/templates/transaction/overview.html.eex:411 #: lib/block_scout_web/views/wei_helpers.ex:78 #, elixir-autogen, elixir-format msgid "Ether" @@ -981,7 +1117,7 @@ msgstr "" msgid "Export Data" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:224 +#: lib/block_scout_web/templates/address_contract/index.html.eex:212 #, elixir-autogen, elixir-format msgid "External libraries" msgstr "" @@ -1002,12 +1138,12 @@ msgstr "" msgid "Fast" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:265 +#: lib/block_scout_web/templates/address/overview.html.eex:247 #, elixir-autogen, elixir-format msgid "Fetching gas used..." msgstr "" -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:111 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:112 #, elixir-autogen, elixir-format msgid "Fetching holders..." msgstr "" @@ -1017,15 +1153,15 @@ msgstr "" msgid "Fetching tokens..." msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:212 -#: lib/block_scout_web/templates/address/overview.html.eex:220 +#: lib/block_scout_web/templates/address/overview.html.eex:194 +#: lib/block_scout_web/templates/address/overview.html.eex:202 #, elixir-autogen, elixir-format msgid "Fetching transactions..." msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:239 -#: lib/block_scout_web/templates/address/overview.html.eex:247 -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:122 +#: lib/block_scout_web/templates/address/overview.html.eex:221 +#: lib/block_scout_web/templates/address/overview.html.eex:229 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:123 #, elixir-autogen, elixir-format msgid "Fetching transfers..." msgstr "" @@ -1048,7 +1184,7 @@ msgstr "" #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:38 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:40 #: lib/block_scout_web/templates/address_transaction/index.html.eex:34 -#: lib/block_scout_web/templates/transaction/overview.html.eex:195 +#: lib/block_scout_web/templates/transaction/overview.html.eex:200 #: lib/block_scout_web/views/address_internal_transaction_view.ex:10 #: lib/block_scout_web/views/address_token_transfer_view.ex:10 #: lib/block_scout_web/views/address_transaction_view.ex:10 @@ -1063,24 +1199,24 @@ msgstr "" #: lib/block_scout_web/templates/block/_tile.html.eex:67 #: lib/block_scout_web/templates/block/overview.html.eex:187 -#: lib/block_scout_web/templates/transaction/overview.html.eex:365 +#: lib/block_scout_web/templates/transaction/overview.html.eex:373 #, elixir-autogen, elixir-format msgid "Gas Limit" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:345 +#: lib/block_scout_web/templates/transaction/overview.html.eex:353 #, elixir-autogen, elixir-format msgid "Gas Price" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:258 +#: lib/block_scout_web/templates/address/overview.html.eex:240 #: lib/block_scout_web/templates/block/_tile.html.eex:73 #: lib/block_scout_web/templates/block/overview.html.eex:178 #, elixir-autogen, elixir-format msgid "Gas Used" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:418 +#: lib/block_scout_web/templates/transaction/overview.html.eex:426 #, elixir-autogen, elixir-format msgid "Gas Used by Transaction" msgstr "" @@ -1091,7 +1227,7 @@ msgstr "" msgid "Gas tracker" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:257 +#: lib/block_scout_web/templates/address/overview.html.eex:239 #, elixir-autogen, elixir-format msgid "Gas used by the address." msgstr "" @@ -1132,13 +1268,13 @@ msgstr "" msgid "Hash" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:446 -#: lib/block_scout_web/templates/transaction/overview.html.eex:450 +#: lib/block_scout_web/templates/transaction/overview.html.eex:454 +#: lib/block_scout_web/templates/transaction/overview.html.eex:458 #, elixir-autogen, elixir-format msgid "Hex (Default)" msgstr "" -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:107 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:108 #, elixir-autogen, elixir-format msgid "Holders" msgstr "" @@ -1154,7 +1290,7 @@ msgid "IMPORTANT: This information is a best guess based on similar functions fr msgstr "" #: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:42 -#: lib/block_scout_web/templates/transaction/_tile.html.eex:86 +#: lib/block_scout_web/templates/transaction/_tile.html.eex:92 #, elixir-autogen, elixir-format msgid "IN" msgstr "" @@ -1169,17 +1305,17 @@ msgstr "" msgid "If you have just submitted this transaction please wait for at least 30 seconds before refreshing this page." msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:150 +#: lib/block_scout_web/templates/address/overview.html.eex:132 #, elixir-autogen, elixir-format msgid "Implementation" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:149 +#: lib/block_scout_web/templates/address/overview.html.eex:131 #, elixir-autogen, elixir-format msgid "Implementation address of the proxy contract." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:430 +#: lib/block_scout_web/templates/transaction/overview.html.eex:438 #, elixir-autogen, elixir-format msgid "Index position of Transaction in the block." msgstr "" @@ -1204,7 +1340,7 @@ msgstr "" msgid "Input" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:213 +#: lib/block_scout_web/templates/transaction/overview.html.eex:219 #, elixir-autogen, elixir-format msgid "Interacted With (To)" msgstr "" @@ -1218,8 +1354,8 @@ msgstr "" #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:17 #: lib/block_scout_web/templates/transaction/_tabs.html.eex:11 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:6 -#: lib/block_scout_web/views/address_view.ex:361 -#: lib/block_scout_web/views/transaction_view.ex:512 +#: lib/block_scout_web/views/address_view.ex:364 +#: lib/block_scout_web/views/transaction_view.ex:513 #, elixir-autogen, elixir-format msgid "Internal Transactions" msgstr "" @@ -1229,7 +1365,7 @@ msgstr "" msgid "Invalid Transaction Hash" msgstr "" -#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:15 +#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:16 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:19 #: lib/block_scout_web/views/tokens/overview_view.ex:42 #, elixir-autogen, elixir-format @@ -1241,11 +1377,17 @@ msgstr "" msgid "It could still be in the TX Pool of a different node, waiting to be broadcasted." msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:276 +#: lib/block_scout_web/templates/address/overview.html.eex:258 #, elixir-autogen, elixir-format msgid "Last Balance Update" msgstr "" +#: lib/block_scout_web/templates/account/api_key/index.html.eex:12 +#: lib/block_scout_web/templates/account/api_key/index.html.eex:18 +#, elixir-autogen, elixir-format +msgid "Learn more" +msgstr "" + #: lib/block_scout_web/templates/layout/app.html.eex:45 #, elixir-autogen, elixir-format msgid "Less than" @@ -1271,22 +1413,22 @@ msgstr "" msgid "License ID" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:297 +#: lib/block_scout_web/templates/transaction/overview.html.eex:305 #, elixir-autogen, elixir-format msgid "List of ERC-1155 tokens created in the transaction." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:281 +#: lib/block_scout_web/templates/transaction/overview.html.eex:289 #, elixir-autogen, elixir-format msgid "List of token burnt in the transaction." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:264 +#: lib/block_scout_web/templates/transaction/overview.html.eex:272 #, elixir-autogen, elixir-format msgid "List of token minted in the transaction." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:248 +#: lib/block_scout_web/templates/transaction/overview.html.eex:256 #, elixir-autogen, elixir-format msgid "List of token transferred in the transaction." msgstr "" @@ -1302,11 +1444,13 @@ msgstr "" #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:133 #: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:49 #: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:45 -#: lib/block_scout_web/templates/address_read_contract/index.html.eex:12 +#: lib/block_scout_web/templates/address_read_contract/index.html.eex:41 +#: lib/block_scout_web/templates/address_read_contract/index.html.eex:49 #: lib/block_scout_web/templates/address_read_proxy/index.html.eex:12 -#: lib/block_scout_web/templates/address_write_contract/index.html.eex:12 +#: lib/block_scout_web/templates/address_write_contract/index.html.eex:39 +#: lib/block_scout_web/templates/address_write_contract/index.html.eex:47 #: lib/block_scout_web/templates/address_write_proxy/index.html.eex:12 -#: lib/block_scout_web/templates/tokens/contract/index.html.eex:16 +#: lib/block_scout_web/templates/tokens/contract/index.html.eex:17 #, elixir-autogen, elixir-format msgid "Loading..." msgstr "" @@ -1325,8 +1469,8 @@ msgstr "" #: lib/block_scout_web/templates/address_logs/index.html.eex:10 #: lib/block_scout_web/templates/transaction/_tabs.html.eex:17 #: lib/block_scout_web/templates/transaction_log/index.html.eex:8 -#: lib/block_scout_web/views/address_view.ex:372 -#: lib/block_scout_web/views/transaction_view.ex:513 +#: lib/block_scout_web/views/address_view.ex:375 +#: lib/block_scout_web/views/transaction_view.ex:514 #, elixir-autogen, elixir-format msgid "Logs" msgstr "" @@ -1338,33 +1482,33 @@ msgstr "" #: lib/block_scout_web/templates/chain/show.html.eex:52 #: lib/block_scout_web/templates/layout/app.html.eex:46 -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:83 -#: lib/block_scout_web/views/address_view.ex:142 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:84 +#: lib/block_scout_web/views/address_view.ex:145 #, elixir-autogen, elixir-format msgid "Market Cap" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:374 +#: lib/block_scout_web/templates/transaction/overview.html.eex:382 #, elixir-autogen, elixir-format msgid "Max Fee per Gas" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:384 +#: lib/block_scout_web/templates/transaction/overview.html.eex:392 #, elixir-autogen, elixir-format msgid "Max Priority Fee per Gas" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:319 +#: lib/block_scout_web/views/transaction_view.ex:320 #, elixir-autogen, elixir-format msgid "Max of" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:364 +#: lib/block_scout_web/templates/transaction/overview.html.eex:372 #, elixir-autogen, elixir-format msgid "Maximum gas amount approved for the transaction." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:373 +#: lib/block_scout_web/templates/transaction/overview.html.eex:381 #, elixir-autogen, elixir-format msgid "Maximum total amount per unit of gas a user is willing to pay for a transaction, including base fee and priority fee." msgstr "" @@ -1438,6 +1582,16 @@ msgstr "" msgid "N/A bytes" msgstr "" +#: lib/block_scout_web/templates/account/api_key/form.html.eex:19 +#: lib/block_scout_web/templates/account/api_key/index.html.eex:28 +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:13 +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:28 +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:18 +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:22 +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:18 +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:22 +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:22 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:19 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:52 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:59 #: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:4 @@ -1446,6 +1600,27 @@ msgstr "" msgid "Name" msgstr "" +#: lib/block_scout_web/templates/account/api_key/form.html.eex:20 +#, elixir-autogen, elixir-format +msgid "Name this API key" +msgstr "" + +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:14 +#, elixir-autogen, elixir-format +msgid "Name this Custom ABI" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:19 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:20 +#, elixir-autogen, elixir-format +msgid "Name this address" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:19 +#, elixir-autogen, elixir-format +msgid "Name this transaction" +msgstr "" + #: lib/block_scout_web/templates/address_token/overview.html.eex:44 #, elixir-autogen, elixir-format msgid "Net Worth" @@ -1487,7 +1662,7 @@ msgid "No" msgstr "" #: lib/block_scout_web/templates/block/overview.html.eex:196 -#: lib/block_scout_web/templates/transaction/overview.html.eex:428 +#: lib/block_scout_web/templates/transaction/overview.html.eex:436 #, elixir-autogen, elixir-format msgid "Nonce" msgstr "" @@ -1497,38 +1672,38 @@ msgstr "" msgid "Not unique Token" msgstr "" -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:106 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:107 #, elixir-autogen, elixir-format msgid "Number of accounts holding the token" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:292 +#: lib/block_scout_web/templates/address/overview.html.eex:274 #, elixir-autogen, elixir-format msgid "Number of blocks validated by this validator." msgstr "" -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:129 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:130 #, elixir-autogen, elixir-format msgid "Number of digits that come after the decimal place when displaying token value" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:203 +#: lib/block_scout_web/templates/address/overview.html.eex:185 #, elixir-autogen, elixir-format msgid "Number of transactions related to this address." msgstr "" -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:117 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:118 #, elixir-autogen, elixir-format msgid "Number of transfers for the token" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:230 +#: lib/block_scout_web/templates/address/overview.html.eex:212 #, elixir-autogen, elixir-format msgid "Number of transfers to/from this address." msgstr "" #: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:40 -#: lib/block_scout_web/templates/transaction/_tile.html.eex:82 +#: lib/block_scout_web/templates/transaction/_tile.html.eex:88 #, elixir-autogen, elixir-format msgid "OUT" msgstr "" @@ -1555,6 +1730,13 @@ msgstr "" msgid "Other Explorers" msgstr "" +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:35 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:48 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:61 +#, elixir-autogen, elixir-format +msgid "Outgoing" +msgstr "" + #: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:24 #, elixir-autogen, elixir-format msgid "Owner Address" @@ -1583,8 +1765,8 @@ msgid "Parent Hash" msgstr "" #: lib/block_scout_web/templates/layout/_topnav.html.eex:60 -#: lib/block_scout_web/views/transaction_view.ex:348 -#: lib/block_scout_web/views/transaction_view.ex:386 +#: lib/block_scout_web/views/transaction_view.ex:349 +#: lib/block_scout_web/views/transaction_view.ex:387 #, elixir-autogen, elixir-format msgid "Pending" msgstr "" @@ -1600,7 +1782,7 @@ msgstr "" msgid "Play" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:430 +#: lib/block_scout_web/templates/transaction/overview.html.eex:438 #, elixir-autogen, elixir-format msgid "Position" msgstr "" @@ -1623,23 +1805,23 @@ msgstr "" #: lib/block_scout_web/templates/chain/show.html.eex:41 #: lib/block_scout_web/templates/layout/app.html.eex:47 -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:94 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:95 #, elixir-autogen, elixir-format msgid "Price" msgstr "" -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:93 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:94 #, elixir-autogen, elixir-format msgid "Price per token on the exchanges" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:344 +#: lib/block_scout_web/templates/transaction/overview.html.eex:352 #, elixir-autogen, elixir-format msgid "Price per unit of gas specified by the sender. Higher gas prices can prioritize transaction inclusion during times of high usage." msgstr "" #: lib/block_scout_web/templates/block/overview.html.eex:225 -#: lib/block_scout_web/templates/transaction/overview.html.eex:394 +#: lib/block_scout_web/templates/transaction/overview.html.eex:402 #, elixir-autogen, elixir-format msgid "Priority Fee / Tip" msgstr "" @@ -1649,6 +1831,33 @@ msgstr "" msgid "Priority Fees" msgstr "" +#: lib/block_scout_web/templates/account/common/_nav.html.eex:4 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:13 +#, elixir-autogen, elixir-format +msgid "Profile" +msgstr "" + +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:19 +#, elixir-autogen, elixir-format +msgid "Public Tags" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:20 +#, elixir-autogen, elixir-format +msgid "Public tag" +msgstr "" + +#: lib/block_scout_web/templates/account/common/_nav.html.eex:22 +#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Public tags" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:50 +#, elixir-autogen, elixir-format +msgid "Public tags* (2 tags maximum, please use \";\" as a divider)" +msgstr "" + #: lib/block_scout_web/templates/common_components/_btn_qr_code.html.eex:10 #: lib/block_scout_web/templates/common_components/_modal_qr_code.html.eex:5 #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:83 @@ -1656,7 +1865,7 @@ msgstr "" msgid "QR Code" msgstr "" -#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:109 +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:100 #, elixir-autogen, elixir-format msgid "Query" msgstr "" @@ -1666,21 +1875,21 @@ msgstr "" msgid "RPC" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:439 +#: lib/block_scout_web/templates/transaction/overview.html.eex:447 #, elixir-autogen, elixir-format msgid "Raw Input" msgstr "" #: lib/block_scout_web/templates/transaction/_tabs.html.eex:24 #: lib/block_scout_web/templates/transaction_raw_trace/index.html.eex:7 -#: lib/block_scout_web/views/transaction_view.ex:514 +#: lib/block_scout_web/views/transaction_view.ex:515 #, elixir-autogen, elixir-format msgid "Raw Trace" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:81 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:27 -#: lib/block_scout_web/views/address_view.ex:366 +#: lib/block_scout_web/views/address_view.ex:369 #: lib/block_scout_web/views/tokens/overview_view.ex:41 #, elixir-autogen, elixir-format msgid "Read Contract" @@ -1688,7 +1897,7 @@ msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:88 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:41 -#: lib/block_scout_web/views/address_view.ex:367 +#: lib/block_scout_web/views/address_view.ex:370 #, elixir-autogen, elixir-format msgid "Read Proxy" msgstr "" @@ -1698,11 +1907,32 @@ msgstr "" msgid "Records" msgstr "" +#: lib/block_scout_web/templates/account/api_key/row.html.eex:13 +#: lib/block_scout_web/templates/account/custom_abi/row.html.eex:13 +#, elixir-autogen, elixir-format +msgid "Remove" +msgstr "" + #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:155 #, elixir-autogen, elixir-format msgid "Request URL" msgstr "" +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Request a public tag/label" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:37 +#, elixir-autogen, elixir-format +msgid "Request to add public tag" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Request to edit a public tag/label" +msgstr "" + #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:142 #: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:38 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:138 @@ -1724,12 +1954,12 @@ msgstr "" msgid "Responses" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:93 +#: lib/block_scout_web/templates/transaction/overview.html.eex:98 #, elixir-autogen, elixir-format msgid "Result" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:130 +#: lib/block_scout_web/templates/transaction/overview.html.eex:135 #, elixir-autogen, elixir-format msgid "Revert reason" msgstr "" @@ -1746,6 +1976,15 @@ msgstr "" msgid "Run" msgstr "" +#: lib/block_scout_web/templates/account/api_key/form.html.eex:26 +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:31 +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:25 +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:25 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:83 +#, elixir-autogen, elixir-format +msgid "Save" +msgstr "" + #: lib/block_scout_web/templates/address_logs/index.html.eex:16 #: lib/block_scout_web/templates/layout/_search.html.eex:34 #, elixir-autogen, elixir-format @@ -1772,6 +2011,11 @@ msgstr "" msgid "Self-Destruct" msgstr "" +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:63 +#, elixir-autogen, elixir-format +msgid "Send request" +msgstr "" + #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:163 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:124 #, elixir-autogen, elixir-format @@ -1788,11 +2032,6 @@ msgstr "" msgid "Show QR Code" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:47 -#, elixir-autogen, elixir-format -msgid "Show Validator Info" -msgstr "" - #: lib/block_scout_web/templates/address_token/overview.html.eex:52 #, elixir-autogen, elixir-format msgid "Shows the current" @@ -1813,6 +2052,11 @@ msgstr "" msgid "Shows total assets held in the address" msgstr "" +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:20 +#, elixir-autogen, elixir-format +msgid "Sign out" +msgstr "" + #: lib/block_scout_web/templates/block/overview.html.eex:114 #, elixir-autogen, elixir-format msgid "Size" @@ -1828,6 +2072,17 @@ msgstr "" msgid "Slow" msgstr "" +#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:21 +#, elixir-autogen, elixir-format +msgid "Smart contract / Address" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/address_field.html.eex:4 +#: lib/block_scout_web/templates/account/public_tags_request/address_field.html.eex:5 +#, elixir-autogen, elixir-format +msgid "Smart contract / Address (0x...)" +msgstr "" + #: lib/block_scout_web/templates/address_coin_balance/index.html.eex:30 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:50 #: lib/block_scout_web/templates/address_logs/index.html.eex:23 @@ -1838,11 +2093,11 @@ msgstr "" #: lib/block_scout_web/templates/block_transaction/index.html.eex:22 #: lib/block_scout_web/templates/chain/show.html.eex:157 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:18 -#: lib/block_scout_web/templates/tokens/holder/index.html.eex:23 +#: lib/block_scout_web/templates/tokens/holder/index.html.eex:24 #: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:23 #: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:23 -#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:22 -#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:21 +#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:23 +#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:22 #: lib/block_scout_web/templates/transaction/index.html.eex:25 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:13 #: lib/block_scout_web/templates/transaction_log/index.html.eex:15 @@ -1886,24 +2141,29 @@ msgstr "" msgid "Static Call" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:105 +#: lib/block_scout_web/templates/transaction/overview.html.eex:110 #, elixir-autogen, elixir-format msgid "Status" msgstr "" +#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:22 +#, elixir-autogen, elixir-format +msgid "Submission date" +msgstr "" + #: lib/block_scout_web/templates/layout/_footer.html.eex:39 #, elixir-autogen, elixir-format msgid "Submit an Issue" msgstr "" #: lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex:8 -#: lib/block_scout_web/views/transaction_view.ex:350 +#: lib/block_scout_web/views/transaction_view.ex:351 #, elixir-autogen, elixir-format msgid "Success" msgstr "" #: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:21 -#: lib/block_scout_web/templates/transaction/_tile.html.eex:46 +#: lib/block_scout_web/templates/transaction/_tile.html.eex:52 #, elixir-autogen, elixir-format msgid "TX Fee" msgstr "" @@ -1928,7 +2188,7 @@ msgstr "" msgid "The block height of a particular block is defined as the number of blocks preceding it in the blockchain." msgstr "" -#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:41 +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:38 #, elixir-autogen, elixir-format msgid "The fallback function is executed on a call to the contract if none of the other functions match the given function signature, or if no data was supplied at all and there is no receive Ether function. The fallback function always receives data, but in order to also receive Ether it must be marked payable." msgstr "" @@ -1938,12 +2198,12 @@ msgstr "" msgid "The hash of the block from which this block was generated." msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:92 +#: lib/block_scout_web/templates/address/overview.html.eex:74 #, elixir-autogen, elixir-format msgid "The name found in the source code of the Contract." msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:103 +#: lib/block_scout_web/templates/address/overview.html.eex:85 #, elixir-autogen, elixir-format msgid "The name of the validator." msgstr "" @@ -1953,22 +2213,22 @@ msgstr "" msgid "The number of transactions in the block." msgstr "" -#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:43 +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:40 #, elixir-autogen, elixir-format msgid "The receive function is executed on a call to the contract with empty calldata. This is the function that is executed on plain Ether transfers (e.g. via .send() or .transfer()). If no such function exists, but a payable fallback function exists, the fallback function will be called on a plain Ether transfer. If neither a receive Ether nor a payable fallback function is present, the contract cannot receive Ether through regular transactions and throws an exception." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:129 +#: lib/block_scout_web/templates/transaction/overview.html.eex:134 #, elixir-autogen, elixir-format msgid "The revert reason of the transaction." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:104 +#: lib/block_scout_web/templates/transaction/overview.html.eex:109 #, elixir-autogen, elixir-format msgid "The status of the transaction: Confirmed or Unconfirmed." msgstr "" -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:67 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:68 #, elixir-autogen, elixir-format msgid "The total amount of tokens issued" msgstr "" @@ -1988,7 +2248,7 @@ msgstr "" msgid "There are no blocks." msgstr "" -#: lib/block_scout_web/templates/tokens/holder/index.html.eex:28 +#: lib/block_scout_web/templates/tokens/holder/index.html.eex:29 #, elixir-autogen, elixir-format msgid "There are no holders for this Token." msgstr "" @@ -2033,7 +2293,7 @@ msgstr "" msgid "There are no tokens for this address." msgstr "" -#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:27 +#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:28 #, elixir-autogen, elixir-format msgid "There are no tokens." msgstr "" @@ -2055,7 +2315,7 @@ msgstr "" #: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:28 #: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:28 -#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:26 +#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:27 #, elixir-autogen, elixir-format msgid "There are no transfers for this Token." msgstr "" @@ -2106,13 +2366,13 @@ msgstr "" msgid "This is useful to allow sending requests to blockscout without having to change anything about the request." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:59 +#: lib/block_scout_web/templates/transaction/overview.html.eex:64 #, elixir-autogen, elixir-format msgid "This transaction is pending confirmation." msgstr "" #: lib/block_scout_web/templates/block/overview.html.eex:71 -#: lib/block_scout_web/templates/transaction/overview.html.eex:172 +#: lib/block_scout_web/templates/transaction/overview.html.eex:177 #, elixir-autogen, elixir-format msgid "Timestamp" msgstr "" @@ -2120,7 +2380,7 @@ msgstr "" #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:32 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:34 #: lib/block_scout_web/templates/address_transaction/index.html.eex:28 -#: lib/block_scout_web/templates/transaction/overview.html.eex:215 +#: lib/block_scout_web/templates/transaction/overview.html.eex:221 #: lib/block_scout_web/views/address_internal_transaction_view.ex:9 #: lib/block_scout_web/views/address_token_transfer_view.ex:9 #: lib/block_scout_web/views/address_transaction_view.ex:9 @@ -2145,19 +2405,19 @@ msgstr "" msgid "Toggle navigation" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:73 +#: lib/block_scout_web/templates/address/overview.html.eex:55 #, elixir-autogen, elixir-format msgid "Token" msgstr "" #: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:3 -#: lib/block_scout_web/views/transaction_view.ex:448 +#: lib/block_scout_web/views/transaction_view.ex:449 #, elixir-autogen, elixir-format msgid "Token Burning" msgstr "" #: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:7 -#: lib/block_scout_web/views/transaction_view.ex:449 +#: lib/block_scout_web/views/transaction_view.ex:450 #, elixir-autogen, elixir-format msgid "Token Creation" msgstr "" @@ -2168,7 +2428,7 @@ msgstr "" msgid "Token Details" msgstr "" -#: lib/block_scout_web/templates/tokens/holder/index.html.eex:16 +#: lib/block_scout_web/templates/tokens/holder/index.html.eex:17 #: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:16 #: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:17 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:11 @@ -2185,14 +2445,14 @@ msgid "Token ID" msgstr "" #: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:5 -#: lib/block_scout_web/views/transaction_view.ex:447 +#: lib/block_scout_web/views/transaction_view.ex:448 #, elixir-autogen, elixir-format msgid "Token Minting" msgstr "" #: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:9 #: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:11 -#: lib/block_scout_web/views/transaction_view.ex:450 +#: lib/block_scout_web/views/transaction_view.ex:451 #, elixir-autogen, elixir-format msgid "Token Transfer" msgstr "" @@ -2202,54 +2462,54 @@ msgstr "" #: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:3 #: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:16 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:5 -#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:14 +#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:15 #: lib/block_scout_web/templates/transaction/_tabs.html.eex:4 #: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:7 -#: lib/block_scout_web/views/address_view.ex:363 +#: lib/block_scout_web/views/address_view.ex:366 #: lib/block_scout_web/views/tokens/instance/overview_view.ex:195 #: lib/block_scout_web/views/tokens/overview_view.ex:39 -#: lib/block_scout_web/views/transaction_view.ex:511 +#: lib/block_scout_web/views/transaction_view.ex:512 #, elixir-autogen, elixir-format msgid "Token Transfers" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:72 +#: lib/block_scout_web/templates/address/overview.html.eex:54 #, elixir-autogen, elixir-format msgid "Token name and symbol." msgstr "" -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:141 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:142 #, elixir-autogen, elixir-format msgid "Token type" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:21 -#: lib/block_scout_web/templates/address/overview.html.eex:193 +#: lib/block_scout_web/templates/address/overview.html.eex:175 #: lib/block_scout_web/templates/address_token/overview.html.eex:58 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:13 #: lib/block_scout_web/templates/layout/_topnav.html.eex:73 #: lib/block_scout_web/templates/tokens/index.html.eex:10 -#: lib/block_scout_web/views/address_view.ex:360 +#: lib/block_scout_web/views/address_view.ex:363 #, elixir-autogen, elixir-format msgid "Tokens" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:282 +#: lib/block_scout_web/templates/transaction/overview.html.eex:290 #, elixir-autogen, elixir-format msgid "Tokens Burnt" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:298 +#: lib/block_scout_web/templates/transaction/overview.html.eex:306 #, elixir-autogen, elixir-format msgid "Tokens Created" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:265 +#: lib/block_scout_web/templates/transaction/overview.html.eex:273 #, elixir-autogen, elixir-format msgid "Tokens Minted" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:249 +#: lib/block_scout_web/templates/transaction/overview.html.eex:257 #, elixir-autogen, elixir-format msgid "Tokens Transferred" msgstr "" @@ -2275,7 +2535,7 @@ msgstr "" msgid "Total Difficulty" msgstr "" -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:82 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:83 #, elixir-autogen, elixir-format msgid "Total Supply * Price" msgstr "" @@ -2295,12 +2555,12 @@ msgstr "" msgid "Total gas limit provided by all transactions in the block." msgstr "" -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:68 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:69 #, elixir-autogen, elixir-format msgid "Total supply" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:329 +#: lib/block_scout_web/templates/transaction/overview.html.eex:337 #, elixir-autogen, elixir-format msgid "Total transaction fee." msgstr "" @@ -2310,8 +2570,10 @@ msgstr "" msgid "Total transactions" msgstr "" +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:11 +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:23 #: lib/block_scout_web/templates/address_logs/_logs.html.eex:19 -#: lib/block_scout_web/views/transaction_view.ex:460 +#: lib/block_scout_web/views/transaction_view.ex:461 #, elixir-autogen, elixir-format msgid "Transaction" msgstr "" @@ -2326,7 +2588,7 @@ msgstr "" msgid "Transaction %{transaction}, %{subnetwork} %{transaction}" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:404 +#: lib/block_scout_web/templates/transaction/overview.html.eex:412 #, elixir-autogen, elixir-format msgid "Transaction Burnt Fee" msgstr "" @@ -2336,12 +2598,12 @@ msgstr "" msgid "Transaction Details" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:330 +#: lib/block_scout_web/templates/transaction/overview.html.eex:338 #, elixir-autogen, elixir-format msgid "Transaction Fee" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:75 +#: lib/block_scout_web/templates/transaction/overview.html.eex:80 #, elixir-autogen, elixir-format msgid "Transaction Hash" msgstr "" @@ -2352,36 +2614,36 @@ msgstr "" msgid "Transaction Inputs" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:354 +#: lib/block_scout_web/templates/transaction/overview.html.eex:362 #, elixir-autogen, elixir-format msgid "Transaction Type" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:427 +#: lib/block_scout_web/templates/transaction/overview.html.eex:435 #, elixir-autogen, elixir-format msgid "Transaction number from the sending address. Each transaction sent from an address increments the nonce by 1." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:353 +#: lib/block_scout_web/templates/transaction/overview.html.eex:361 #, elixir-autogen, elixir-format msgid "Transaction type, introduced in EIP-2718." msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:7 -#: lib/block_scout_web/templates/address/overview.html.eex:204 -#: lib/block_scout_web/templates/address/overview.html.eex:210 -#: lib/block_scout_web/templates/address/overview.html.eex:218 +#: lib/block_scout_web/templates/address/overview.html.eex:186 +#: lib/block_scout_web/templates/address/overview.html.eex:192 +#: lib/block_scout_web/templates/address/overview.html.eex:200 #: lib/block_scout_web/templates/address_transaction/index.html.eex:13 #: lib/block_scout_web/templates/block/overview.html.eex:80 #: lib/block_scout_web/templates/block_transaction/index.html.eex:10 #: lib/block_scout_web/templates/chain/show.html.eex:213 #: lib/block_scout_web/templates/layout/_topnav.html.eex:48 -#: lib/block_scout_web/views/address_view.ex:362 +#: lib/block_scout_web/views/address_view.ex:365 #, elixir-autogen, elixir-format msgid "Transactions" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:119 +#: lib/block_scout_web/templates/address/overview.html.eex:101 #, elixir-autogen, elixir-format msgid "Transactions and address of creation." msgstr "" @@ -2391,11 +2653,11 @@ msgstr "" msgid "Transactions sent" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:231 -#: lib/block_scout_web/templates/address/overview.html.eex:237 -#: lib/block_scout_web/templates/address/overview.html.eex:245 +#: lib/block_scout_web/templates/address/overview.html.eex:213 +#: lib/block_scout_web/templates/address/overview.html.eex:219 +#: lib/block_scout_web/templates/address/overview.html.eex:227 #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:50 -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:118 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:119 #, elixir-autogen, elixir-format msgid "Transfers" msgstr "" @@ -2422,12 +2684,12 @@ msgstr "" msgid "Type" msgstr "" -#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:140 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:141 #, elixir-autogen, elixir-format msgid "Type of the token standard" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:453 +#: lib/block_scout_web/templates/transaction/overview.html.eex:461 #, elixir-autogen, elixir-format msgid "UTF-8" msgstr "" @@ -2443,7 +2705,7 @@ msgstr "" msgid "Uncles" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:341 +#: lib/block_scout_web/views/transaction_view.ex:342 #, elixir-autogen, elixir-format msgid "Unconfirmed" msgstr "" @@ -2453,17 +2715,17 @@ msgstr "" msgid "Unique Token" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:74 +#: lib/block_scout_web/templates/transaction/overview.html.eex:79 #, elixir-autogen, elixir-format msgid "Unique character string (TxID) assigned to every verified transaction." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:383 +#: lib/block_scout_web/templates/transaction/overview.html.eex:391 #, elixir-autogen, elixir-format msgid "User defined maximum fee (tip) per unit of gas paid to validator for transaction prioritization." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:393 +#: lib/block_scout_web/templates/transaction/overview.html.eex:401 #, elixir-autogen, elixir-format msgid "User-defined tip sent to validator for transaction priority/inclusion." msgstr "" @@ -2493,26 +2755,27 @@ msgstr "" msgid "Validator Data" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:51 -#, elixir-autogen, elixir-format -msgid "Validator Info" -msgstr "" - -#: lib/block_scout_web/templates/address/overview.html.eex:104 +#: lib/block_scout_web/templates/address/overview.html.eex:86 #, elixir-autogen, elixir-format msgid "Validator Name" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:315 +#: lib/block_scout_web/templates/transaction/overview.html.eex:323 #, elixir-autogen, elixir-format msgid "Value" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:314 +#: lib/block_scout_web/templates/transaction/overview.html.eex:322 #, elixir-autogen, elixir-format msgid "Value sent in the native token (and USD) if applicable." msgstr "" +#: lib/block_scout_web/templates/address_read_contract/index.html.eex:17 +#: lib/block_scout_web/templates/address_write_contract/index.html.eex:15 +#, elixir-autogen, elixir-format +msgid "Verified" +msgstr "" + #: lib/block_scout_web/templates/address_contract/index.html.eex:82 #, elixir-autogen, elixir-format msgid "Verified at" @@ -2520,10 +2783,8 @@ msgstr "" #: lib/block_scout_web/templates/address_contract/index.html.eex:27 #: lib/block_scout_web/templates/address_contract/index.html.eex:29 -#: lib/block_scout_web/templates/address_contract/index.html.eex:160 -#: lib/block_scout_web/templates/address_contract/index.html.eex:166 -#: lib/block_scout_web/templates/address_contract/index.html.eex:197 -#: lib/block_scout_web/templates/address_contract/index.html.eex:203 +#: lib/block_scout_web/templates/address_contract/index.html.eex:161 +#: lib/block_scout_web/templates/address_contract/index.html.eex:192 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:14 #, elixir-autogen, elixir-format msgid "Verify & Publish" @@ -2586,12 +2847,12 @@ msgstr "" msgid "View Contract" msgstr "" -#: lib/block_scout_web/templates/transaction/_tile.html.eex:67 +#: lib/block_scout_web/templates/transaction/_tile.html.eex:73 #, elixir-autogen, elixir-format msgid "View Less Transfers" msgstr "" -#: lib/block_scout_web/templates/transaction/_tile.html.eex:66 +#: lib/block_scout_web/templates/transaction/_tile.html.eex:72 #, elixir-autogen, elixir-format msgid "View More Transfers" msgstr "" @@ -2626,7 +2887,7 @@ msgstr "" msgid "Vyper contract" msgstr "" -#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:145 +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:136 #, elixir-autogen, elixir-format msgid "WEI" msgstr "" @@ -2646,26 +2907,33 @@ msgstr "" msgid "Warning! Contract bytecode has been changed and doesn't match the verified one. Therefore, interaction with this smart contract may be risky." msgstr "" +#: lib/block_scout_web/templates/account/common/_nav.html.eex:7 +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:7 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:14 +#, elixir-autogen, elixir-format +msgid "Watch list" +msgstr "" + #: lib/block_scout_web/views/wei_helpers.ex:76 #, elixir-autogen, elixir-format msgid "Wei" msgstr "" -#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:109 +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:100 #, elixir-autogen, elixir-format msgid "Write" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:95 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:34 -#: lib/block_scout_web/views/address_view.ex:368 +#: lib/block_scout_web/views/address_view.ex:371 #, elixir-autogen, elixir-format msgid "Write Contract" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:102 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:48 -#: lib/block_scout_web/views/address_view.ex:369 +#: lib/block_scout_web/views/address_view.ex:372 #, elixir-autogen, elixir-format msgid "Write Proxy" msgstr "" @@ -2678,7 +2946,32 @@ msgstr "" msgid "Yes" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:129 +#: lib/block_scout_web/templates/account/api_key/index.html.eex:18 +#, elixir-autogen, elixir-format +msgid "You can create 3 API keys per account." +msgstr "" + +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:18 +#, elixir-autogen, elixir-format +msgid "You can create up to 15 Custom ABIs per account." +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:11 +#, elixir-autogen, elixir-format +msgid "You can request a public category tag which is displayed to all Blockscout users. Public tags may be added to contract or external addresses, and any associated transactions will inherit that tag. Clicking a tag opens a page with related information and helps provide context and data organization. Requests are sent to a moderator for review and approval. This process can take several days." +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:15 +#, elixir-autogen, elixir-format +msgid "Your name" +msgstr "" + +#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:14 +#, elixir-autogen, elixir-format +msgid "Your name*" +msgstr "" + +#: lib/block_scout_web/templates/address/overview.html.eex:111 #, elixir-autogen, elixir-format msgid "at" msgstr "" @@ -2688,7 +2981,7 @@ msgstr "" msgid "balance of the address" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:403 +#: lib/block_scout_web/templates/transaction/overview.html.eex:411 #, elixir-autogen, elixir-format msgid "burned for this transaction. Equals Block Base Fee per Gas * Gas Used." msgstr "" @@ -2703,7 +2996,7 @@ msgstr "" msgid "button" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:223 +#: lib/block_scout_web/templates/transaction/overview.html.eex:230 #, elixir-autogen, elixir-format msgid "created" msgstr "" @@ -2713,7 +3006,7 @@ msgstr "" msgid "custom RPC" msgstr "" -#: lib/block_scout_web/templates/address/overview.html.eex:167 +#: lib/block_scout_web/templates/address/overview.html.eex:149 #, elixir-autogen, elixir-format msgid "doesn't include ERC20, ERC721, ERC1155 tokens)." msgstr "" @@ -2723,7 +3016,7 @@ msgstr "" msgid "elements are displayed" msgstr "" -#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:41 +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:38 #, elixir-autogen, elixir-format msgid "fallback" msgstr "" @@ -2765,7 +3058,7 @@ msgstr "" msgid "page" msgstr "" -#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:43 +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:40 #, elixir-autogen, elixir-format msgid "receive" msgstr "" @@ -2876,3 +3169,120 @@ msgstr "" #, elixir-autogen, elixir-format msgid "The requested path was not found on BlockScout." msgstr "" + +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:25 +#, elixir-autogen, elixir-format, fuzzy +msgid "Actions" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:38 +#, elixir-autogen, elixir-format, fuzzy +msgid "Add address" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:7 +#, elixir-autogen, elixir-format, fuzzy +msgid "Add address to the Watch list" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:81 +#, elixir-autogen, elixir-format, fuzzy +msgid "Back to Watch list (Cancel)" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:7 +#, elixir-autogen, elixir-format, fuzzy +msgid "Edit Watch list address" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:77 +#, elixir-autogen, elixir-format +msgid "Remove from Watch list" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:14 +#, elixir-autogen, elixir-format +msgid "You don't have address tags yet" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:14 +#, elixir-autogen, elixir-format +msgid "You don't have addresses on you watchlist yet" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:14 +#, elixir-autogen, elixir-format +msgid "You don't have transaction tags yet" +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/form.html.eex:7 +#: lib/block_scout_web/templates/account/api_key/form.html.eex:13 +#: lib/block_scout_web/templates/account/api_key/form.html.eex:14 +#: lib/block_scout_web/templates/account/api_key/index.html.eex:29 +#, elixir-autogen, elixir-format, fuzzy +msgid "API key" +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/index.html.eex:7 +#: lib/block_scout_web/templates/account/common/_nav.html.eex:16 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:17 +#, elixir-autogen, elixir-format +msgid "API keys" +msgstr "" + +#: lib/block_scout_web/templates/account/common/_nav.html.eex:10 +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:7 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:15 +#, elixir-autogen, elixir-format, fuzzy +msgid "Address Tags" +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/form.html.eex:25 +#, elixir-autogen, elixir-format +msgid "Back to API keys (Cancel)" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:24 +#, elixir-autogen, elixir-format +msgid "Back to Address Tags (Cancel)" +msgstr "" + +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:30 +#, elixir-autogen, elixir-format +msgid "Back to Custom ABI (Cancel)" +msgstr "" + +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:24 +#, elixir-autogen, elixir-format +msgid "Back to Transaction Tags (Cancel)" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:30 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:43 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:56 +#, elixir-autogen, elixir-format +msgid "Incoming" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:68 +#, elixir-autogen, elixir-format +msgid "Please select notification methods:" +msgstr "" + +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:24 +#, elixir-autogen, elixir-format +msgid "Please select what types of notifications you will receive:" +msgstr "" + +#: lib/block_scout_web/templates/account/common/_nav.html.eex:13 +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:7 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:16 +#, elixir-autogen, elixir-format, fuzzy +msgid "Transaction Tags" +msgstr "" + +#: lib/block_scout_web/templates/account/api_key/form.html.eex:7 +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:8 +#, elixir-autogen, elixir-format +msgid "Update" +msgstr "" diff --git a/apps/block_scout_web/test/block_scout_web/chain_test.exs b/apps/block_scout_web/test/block_scout_web/chain_test.exs index c215da886a..7656ff2259 100644 --- a/apps/block_scout_web/test/block_scout_web/chain_test.exs +++ b/apps/block_scout_web/test/block_scout_web/chain_test.exs @@ -76,7 +76,7 @@ defmodule BlockScoutWeb.ChainTest do test "correctly encodes decimal values" do val = Decimal.from_float(5.55) - assert "5.55" == Poison.encode!(val) + assert "\"5.55\"" == Poison.encode!(val) end end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/account/api/v1/user_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/account/api/v1/user_controller_test.exs new file mode 100644 index 0000000000..2ce2a92d62 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/account/api/v1/user_controller_test.exs @@ -0,0 +1,919 @@ +defmodule BlockScoutWeb.Account.Api.V1.UserControllerTest do + use BlockScoutWeb.ConnCase + + alias BlockScoutWeb.Models.UserFromAuth + + setup %{conn: conn} do + auth = build(:auth) + + {:ok, user} = UserFromAuth.find_or_create(auth) + + {:ok, user: user, conn: Plug.Test.init_test_session(conn, current_user: user)} + end + + describe "Test account/api/v1/user" do + test "get user info", %{conn: conn, user: user} do + result_conn = + conn + |> get("/api/account/v1/user/info") + |> doc(description: "Get info about user") + + assert json_response(result_conn, 200) == %{ + "nickname" => user.nickname, + "name" => user.name, + "email" => user.email, + "avatar" => user.avatar + } + end + + test "post private address tag", %{conn: conn} do + tag_address_response = + conn + |> post("/api/account/v1/user/tags/address", %{ + "address_hash" => "0x3e9ac8f16c92bc4f093357933b5befbf1e16987b", + "name" => "MyName" + }) + |> doc(description: "Add private address tag") + |> json_response(200) + + conn + |> get("/api/account/v1/tags/address/0x3e9ac8f16c92bc4f093357933b5befbf1e16987b") + |> doc(description: "Get tags for address") + |> json_response(200) + + assert tag_address_response["address_hash"] == "0x3e9ac8f16c92bc4f093357933b5befbf1e16987b" + assert tag_address_response["name"] == "MyName" + assert tag_address_response["id"] + end + + test "edit private address tag", %{conn: conn} do + address_tag = build(:tag_address) + + tag_address_response = + conn + |> post("/api/account/v1/user/tags/address", address_tag) + |> json_response(200) + + response = + conn + |> get("/api/account/v1/user/tags/address") + |> json_response(200) == [tag_address_response] + + assert tag_address_response["address_hash"] == address_tag["address_hash"] + assert tag_address_response["name"] == address_tag["name"] + assert tag_address_response["id"] + + new_address_tag = build(:tag_address) + + new_tag_address_response = + conn + |> put("/api/account/v1/user/tags/address/#{tag_address_response["id"]}", new_address_tag) + |> doc(description: "Edit private address tag") + |> json_response(200) + + assert new_tag_address_response["address_hash"] == new_address_tag["address_hash"] + assert new_tag_address_response["name"] == new_address_tag["name"] + assert new_tag_address_response["id"] == tag_address_response["id"] + end + + test "get address tags after inserting one private tags", %{conn: conn} do + addresses = Enum.map(0..2, fn _x -> to_string(build(:address).hash) end) + names = Enum.map(0..2, fn x -> "name#{x}" end) + zipped = Enum.zip(addresses, names) + + created = + Enum.map(zipped, fn {addr, name} -> + id = + (conn + |> post("/api/account/v1/user/tags/address", %{ + "address_hash" => addr, + "name" => name + }) + |> json_response(200))["id"] + + {addr, %{"display_name" => name, "label" => name, "address_hash" => addr}, + %{"address_hash" => addr, "id" => id, "name" => name}} + end) + + assert Enum.all?(created, fn {addr, map_tag, _} -> + response = + conn + |> get("/api/account/v1/tags/address/#{addr}") + |> json_response(200) + + response["personal_tags"] == [map_tag] + end) + + response = + conn + |> get("/api/account/v1/user/tags/address") + |> doc(description: "Get private addresses tags") + |> json_response(200) + + assert Enum.all?(created, fn {_, _, map} -> map in response end) + end + + test "delete address tag", %{conn: conn} do + addresses = Enum.map(0..2, fn _x -> to_string(build(:address).hash) end) + names = Enum.map(0..2, fn x -> "name#{x}" end) + zipped = Enum.zip(addresses, names) + + created = + Enum.map(zipped, fn {addr, name} -> + id = + (conn + |> post("/api/account/v1/user/tags/address", %{ + "address_hash" => addr, + "name" => name + }) + |> json_response(200))["id"] + + {addr, %{"display_name" => name, "label" => name, "address_hash" => addr}, + %{"address_hash" => addr, "id" => id, "name" => name}} + end) + + assert Enum.all?(created, fn {addr, map_tag, _} -> + response = + conn + |> get("/api/account/v1/tags/address/#{addr}") + |> json_response(200) + + response["personal_tags"] == [map_tag] + end) + + response = + conn + |> get("/api/account/v1/user/tags/address") + |> json_response(200) + + assert Enum.all?(created, fn {_, _, map} -> map in response end) + + {_, _, %{"id" => id}} = Enum.at(created, 0) + + assert conn + |> delete("/api/account/v1/user/tags/address/#{id}") + |> doc("Delete private address tag") + |> json_response(200) == %{"message" => "OK"} + + assert Enum.all?(Enum.drop(created, 1), fn {_, _, %{"id" => id}} -> + conn + |> delete("/api/account/v1/user/tags/address/#{id}") + |> json_response(200) == %{"message" => "OK"} + end) + + assert conn + |> get("/api/account/v1/user/tags/address") + |> json_response(200) == [] + + assert Enum.all?(created, fn {addr, _, _} -> + response = + conn + |> get("/api/account/v1/tags/address/#{addr}") + |> json_response(200) + + response["personal_tags"] == [] + end) + end + + test "post private transaction tag", %{conn: conn} do + tx_hash_non_existing = to_string(build(:transaction).hash) + tx_hash = to_string(insert(:transaction).hash) + + assert conn + |> post("/api/account/v1/user/tags/transaction", %{ + "transaction_hash" => tx_hash_non_existing, + "name" => "MyName" + }) + |> doc(description: "Error on try to create private transaction tag for tx does not exist") + |> json_response(422) == %{"errors" => %{"tx_hash" => ["Transaction does not exist"]}} + + tag_transaction_response = + conn + |> post("/api/account/v1/user/tags/transaction", %{ + "transaction_hash" => tx_hash, + "name" => "MyName" + }) + |> doc(description: "Create private transaction tag") + |> json_response(200) + + conn + |> get("/api/account/v1/tags/transaction/#{tx_hash}") + |> doc(description: "Get tags for transaction") + |> json_response(200) + + assert tag_transaction_response["transaction_hash"] == tx_hash + assert tag_transaction_response["name"] == "MyName" + assert tag_transaction_response["id"] + end + + test "edit private transaction tag", %{conn: conn} do + tx_tag = build(:tag_transaction) + + tag_response = + conn + |> post("/api/account/v1/user/tags/transaction", tx_tag) + |> json_response(200) + + response = + conn + |> get("/api/account/v1/user/tags/transaction") + |> json_response(200) == [tag_response] + + assert tag_response["address_hash"] == tx_tag["address_hash"] + assert tag_response["name"] == tx_tag["name"] + assert tag_response["id"] + + new_tx_tag = build(:tag_transaction) + + new_tag_response = + conn + |> put("/api/account/v1/user/tags/transaction/#{tag_response["id"]}", new_tx_tag) + |> doc(description: "Edit private transaction tag") + |> json_response(200) + + assert new_tag_response["address_hash"] == new_tx_tag["address_hash"] + assert new_tag_response["name"] == new_tx_tag["name"] + assert new_tag_response["id"] == tag_response["id"] + end + + test "get transaction tags after inserting one private tags", %{conn: conn} do + transactions = Enum.map(0..2, fn _x -> to_string(insert(:transaction).hash) end) + names = Enum.map(0..2, fn x -> "name#{x}" end) + zipped = Enum.zip(transactions, names) + + created = + Enum.map(zipped, fn {tx_hash, name} -> + id = + (conn + |> post("/api/account/v1/user/tags/transaction", %{ + "transaction_hash" => tx_hash, + "name" => name + }) + |> json_response(200))["id"] + + {tx_hash, %{"label" => name}, %{"transaction_hash" => tx_hash, "id" => id, "name" => name}} + end) + + assert Enum.all?(created, fn {tx_hash, map_tag, _} -> + response = + conn + |> get("/api/account/v1/tags/transaction/#{tx_hash}") + |> json_response(200) + + response["personal_tx_tag"] == map_tag + end) + + response = + conn + |> get("/api/account/v1/user/tags/transaction") + |> doc(description: "Get private transactions tags") + |> json_response(200) + + assert Enum.all?(created, fn {_, _, map} -> map in response end) + end + + test "delete transaction tag", %{conn: conn} do + transactions = Enum.map(0..2, fn _x -> to_string(insert(:transaction).hash) end) + names = Enum.map(0..2, fn x -> "name#{x}" end) + zipped = Enum.zip(transactions, names) + + created = + Enum.map(zipped, fn {tx_hash, name} -> + id = + (conn + |> post("/api/account/v1/user/tags/transaction", %{ + "transaction_hash" => tx_hash, + "name" => name + }) + |> json_response(200))["id"] + + {tx_hash, %{"label" => name}, %{"transaction_hash" => tx_hash, "id" => id, "name" => name}} + end) + + assert Enum.all?(created, fn {tx_hash, map_tag, _} -> + response = + conn + |> get("/api/account/v1/tags/transaction/#{tx_hash}") + |> json_response(200) + + response["personal_tx_tag"] == map_tag + end) + + response = + conn + |> get("/api/account/v1/user/tags/transaction") + |> json_response(200) + + assert Enum.all?(created, fn {_, _, map} -> map in response end) + + {_, _, %{"id" => id}} = Enum.at(created, 0) + + assert conn + |> delete("/api/account/v1/user/tags/transaction/#{id}") + |> doc("Delete private transaction tag") + |> json_response(200) == %{"message" => "OK"} + + assert Enum.all?(Enum.drop(created, 1), fn {_, _, %{"id" => id}} -> + conn + |> delete("/api/account/v1/user/tags/transaction/#{id}") + |> json_response(200) == %{"message" => "OK"} + end) + + assert conn + |> get("/api/account/v1/user/tags/transaction") + |> json_response(200) == [] + + assert Enum.all?(created, fn {addr, _, _} -> + response = + conn + |> get("/api/account/v1/tags/transaction/#{addr}") + |> json_response(200) + + response["personal_tx_tag"] == nil + end) + end + + test "post && get watchlist address", %{conn: conn} do + watchlist_address_map = build(:watchlist_address) + + post_watchlist_address_response = + conn + |> post( + "/api/account/v1/user/watchlist", + watchlist_address_map + ) + |> doc(description: "Add address to watch list") + |> json_response(200) + + assert post_watchlist_address_response["notification_settings"] == watchlist_address_map["notification_settings"] + assert post_watchlist_address_response["name"] == watchlist_address_map["name"] + assert post_watchlist_address_response["notification_methods"] == watchlist_address_map["notification_methods"] + assert post_watchlist_address_response["address_hash"] == watchlist_address_map["address_hash"] + + get_watchlist_address_response = conn |> get("/api/account/v1/user/watchlist") |> json_response(200) |> Enum.at(0) + + assert get_watchlist_address_response["notification_settings"] == watchlist_address_map["notification_settings"] + assert get_watchlist_address_response["name"] == watchlist_address_map["name"] + assert get_watchlist_address_response["notification_methods"] == watchlist_address_map["notification_methods"] + assert get_watchlist_address_response["address_hash"] == watchlist_address_map["address_hash"] + assert get_watchlist_address_response["id"] == post_watchlist_address_response["id"] + + watchlist_address_map_1 = build(:watchlist_address) + + post_watchlist_address_response_1 = + conn + |> post( + "/api/account/v1/user/watchlist", + watchlist_address_map_1 + ) + |> json_response(200) + + get_watchlist_address_response_1_0 = + conn + |> get("/api/account/v1/user/watchlist") + |> doc(description: "Get addresses from watchlists") + |> json_response(200) + |> Enum.at(1) + + get_watchlist_address_response_1_1 = + conn |> get("/api/account/v1/user/watchlist") |> json_response(200) |> Enum.at(0) + + assert get_watchlist_address_response_1_0 == get_watchlist_address_response + + assert get_watchlist_address_response_1_1["notification_settings"] == + watchlist_address_map_1["notification_settings"] + + assert get_watchlist_address_response_1_1["name"] == watchlist_address_map_1["name"] + + assert get_watchlist_address_response_1_1["notification_methods"] == + watchlist_address_map_1["notification_methods"] + + assert get_watchlist_address_response_1_1["address_hash"] == watchlist_address_map_1["address_hash"] + assert get_watchlist_address_response_1_1["id"] == post_watchlist_address_response_1["id"] + end + + test "delete watchlist address", %{conn: conn} do + watchlist_address_map = build(:watchlist_address) + + post_watchlist_address_response = + conn + |> post( + "/api/account/v1/user/watchlist", + watchlist_address_map + ) + |> json_response(200) + + assert post_watchlist_address_response["notification_settings"] == watchlist_address_map["notification_settings"] + assert post_watchlist_address_response["name"] == watchlist_address_map["name"] + assert post_watchlist_address_response["notification_methods"] == watchlist_address_map["notification_methods"] + assert post_watchlist_address_response["address_hash"] == watchlist_address_map["address_hash"] + + get_watchlist_address_response = conn |> get("/api/account/v1/user/watchlist") |> json_response(200) |> Enum.at(0) + + assert get_watchlist_address_response["notification_settings"] == watchlist_address_map["notification_settings"] + assert get_watchlist_address_response["name"] == watchlist_address_map["name"] + assert get_watchlist_address_response["notification_methods"] == watchlist_address_map["notification_methods"] + assert get_watchlist_address_response["address_hash"] == watchlist_address_map["address_hash"] + assert get_watchlist_address_response["id"] == post_watchlist_address_response["id"] + + watchlist_address_map_1 = build(:watchlist_address) + + post_watchlist_address_response_1 = + conn + |> post( + "/api/account/v1/user/watchlist", + watchlist_address_map_1 + ) + |> json_response(200) + + get_watchlist_address_response_1_0 = + conn |> get("/api/account/v1/user/watchlist") |> json_response(200) |> Enum.at(1) + + get_watchlist_address_response_1_1 = + conn |> get("/api/account/v1/user/watchlist") |> json_response(200) |> Enum.at(0) + + assert get_watchlist_address_response_1_0 == get_watchlist_address_response + + assert get_watchlist_address_response_1_1["notification_settings"] == + watchlist_address_map_1["notification_settings"] + + assert get_watchlist_address_response_1_1["name"] == watchlist_address_map_1["name"] + + assert get_watchlist_address_response_1_1["notification_methods"] == + watchlist_address_map_1["notification_methods"] + + assert get_watchlist_address_response_1_1["address_hash"] == watchlist_address_map_1["address_hash"] + assert get_watchlist_address_response_1_1["id"] == post_watchlist_address_response_1["id"] + + assert conn + |> delete("/api/account/v1/user/watchlist/#{get_watchlist_address_response_1_1["id"]}") + |> doc(description: "Delete address from watchlist by id") + |> json_response(200) == %{"message" => "OK"} + + assert conn + |> delete("/api/account/v1/user/watchlist/#{get_watchlist_address_response_1_0["id"]}") + |> json_response(200) == %{"message" => "OK"} + + assert conn |> get("/api/account/v1/user/watchlist") |> json_response(200) == [] + end + + test "put watchlist address", %{conn: conn} do + watchlist_address_map = build(:watchlist_address) + + post_watchlist_address_response = + conn + |> post( + "/api/account/v1/user/watchlist", + watchlist_address_map + ) + |> json_response(200) + + assert post_watchlist_address_response["notification_settings"] == watchlist_address_map["notification_settings"] + assert post_watchlist_address_response["name"] == watchlist_address_map["name"] + assert post_watchlist_address_response["notification_methods"] == watchlist_address_map["notification_methods"] + assert post_watchlist_address_response["address_hash"] == watchlist_address_map["address_hash"] + + get_watchlist_address_response = conn |> get("/api/account/v1/user/watchlist") |> json_response(200) |> Enum.at(0) + + assert get_watchlist_address_response["notification_settings"] == watchlist_address_map["notification_settings"] + assert get_watchlist_address_response["name"] == watchlist_address_map["name"] + assert get_watchlist_address_response["notification_methods"] == watchlist_address_map["notification_methods"] + assert get_watchlist_address_response["address_hash"] == watchlist_address_map["address_hash"] + assert get_watchlist_address_response["id"] == post_watchlist_address_response["id"] + + new_watchlist_address_map = build(:watchlist_address) + + put_watchlist_address_response = + conn + |> put( + "/api/account/v1/user/watchlist/#{post_watchlist_address_response["id"]}", + new_watchlist_address_map + ) + |> doc(description: "Edit watchlist address") + |> json_response(200) + + assert put_watchlist_address_response["notification_settings"] == + new_watchlist_address_map["notification_settings"] + + assert put_watchlist_address_response["name"] == new_watchlist_address_map["name"] + assert put_watchlist_address_response["notification_methods"] == new_watchlist_address_map["notification_methods"] + assert put_watchlist_address_response["address_hash"] == new_watchlist_address_map["address_hash"] + assert get_watchlist_address_response["id"] == put_watchlist_address_response["id"] + end + + test "cannot create duplicate of watchlist address", %{conn: conn} do + watchlist_address_map = build(:watchlist_address) + + post_watchlist_address_response = + conn + |> post( + "/api/account/v1/user/watchlist", + watchlist_address_map + ) + |> json_response(200) + + assert post_watchlist_address_response["notification_settings"] == watchlist_address_map["notification_settings"] + assert post_watchlist_address_response["name"] == watchlist_address_map["name"] + assert post_watchlist_address_response["notification_methods"] == watchlist_address_map["notification_methods"] + assert post_watchlist_address_response["address_hash"] == watchlist_address_map["address_hash"] + + assert conn + |> post( + "/api/account/v1/user/watchlist", + watchlist_address_map + ) + |> doc(description: "Example of error on creating watchlist address") + |> json_response(422) == %{"errors" => %{"watchlist_id" => ["Address already added to the watch list"]}} + + new_watchlist_address_map = build(:watchlist_address) + + post_watchlist_address_response_1 = + conn + |> post( + "/api/account/v1/user/watchlist", + new_watchlist_address_map + ) + |> json_response(200) + + assert conn + |> put( + "/api/account/v1/user/watchlist/#{post_watchlist_address_response_1["id"]}", + watchlist_address_map + ) + |> doc(description: "Example of error on editing watchlist address") + |> json_response(422) == %{"errors" => %{"watchlist_id" => ["Address already added to the watch list"]}} + end + + test "post api key", %{conn: conn} do + post_api_key_response = + conn + |> post( + "/api/account/v1/user/api_keys", + %{"name" => "test"} + ) + |> doc(description: "Add api key") + |> json_response(200) + + assert post_api_key_response["name"] == "test" + assert post_api_key_response["api_key"] + end + + test "can create not more than 3 api keys + get api keys", %{conn: conn} do + Enum.each(0..2, fn _x -> + conn + |> post( + "/api/account/v1/user/api_keys", + %{"name" => "test"} + ) + |> json_response(200) + end) + + assert conn + |> post( + "/api/account/v1/user/api_keys", + %{"name" => "test"} + ) + |> doc(description: "Example of error on creating api key") + |> json_response(422) == %{"errors" => %{"name" => ["Max 3 keys per account"]}} + + assert conn + |> get("/api/account/v1/user/api_keys") + |> doc(description: "Get api keys list") + |> json_response(200) + |> Enum.count() == 3 + end + + test "edit api key", %{conn: conn} do + post_api_key_response = + conn + |> post( + "/api/account/v1/user/api_keys", + %{"name" => "test"} + ) + |> json_response(200) + + assert post_api_key_response["name"] == "test" + assert post_api_key_response["api_key"] + + put_api_key_response = + conn + |> put( + "/api/account/v1/user/api_keys/#{post_api_key_response["api_key"]}", + %{"name" => "test_1"} + ) + |> doc(description: "Edit api key") + |> json_response(200) + + assert put_api_key_response["api_key"] == post_api_key_response["api_key"] + assert put_api_key_response["name"] == "test_1" + + assert conn + |> get("/api/account/v1/user/api_keys") + |> json_response(200) == [put_api_key_response] + end + + test "delete api key", %{conn: conn} do + post_api_key_response = + conn + |> post( + "/api/account/v1/user/api_keys", + %{"name" => "test"} + ) + |> json_response(200) + + assert post_api_key_response["name"] == "test" + assert post_api_key_response["api_key"] + + assert conn + |> get("/api/account/v1/user/api_keys") + |> json_response(200) + |> Enum.count() == 1 + + assert conn + |> delete("/api/account/v1/user/api_keys/#{post_api_key_response["api_key"]}") + |> doc(description: "Delete api key") + |> json_response(200) == %{"message" => "OK"} + + assert conn + |> get("/api/account/v1/user/api_keys") + |> json_response(200) == [] + end + + test "post custom abi", %{conn: conn} do + custom_abi = build(:custom_abi) + + post_custom_abi_response = + conn + |> post( + "/api/account/v1/user/custom_abis", + custom_abi + ) + |> doc(description: "Add custom abi") + |> json_response(200) + + assert post_custom_abi_response["name"] == custom_abi["name"] + assert post_custom_abi_response["abi"] == custom_abi["abi"] + assert post_custom_abi_response["contract_address_hash"] == custom_abi["contract_address_hash"] + assert post_custom_abi_response["id"] + end + + test "can create not more than 15 custom abis + get custom abi", %{conn: conn} do + Enum.each(0..14, fn _x -> + conn + |> post( + "/api/account/v1/user/custom_abis", + build(:custom_abi) + ) + |> json_response(200) + end) + + assert conn + |> post( + "/api/account/v1/user/custom_abis", + build(:custom_abi) + ) + |> doc(description: "Example of error on creating custom abi") + |> json_response(422) == %{"errors" => %{"name" => ["Max 15 ABIs per account"]}} + + assert conn + |> get("/api/account/v1/user/custom_abis") + |> doc(description: "Get custom abis list") + |> json_response(200) + |> Enum.count() == 15 + end + + test "edit custom abi", %{conn: conn} do + custom_abi = build(:custom_abi) + + post_custom_abi_response = + conn + |> post( + "/api/account/v1/user/custom_abis", + custom_abi + ) + |> json_response(200) + + assert post_custom_abi_response["name"] == custom_abi["name"] + assert post_custom_abi_response["abi"] == custom_abi["abi"] + assert post_custom_abi_response["contract_address_hash"] == custom_abi["contract_address_hash"] + assert post_custom_abi_response["id"] + + custom_abi_1 = build(:custom_abi) + + put_custom_abi_response = + conn + |> put( + "/api/account/v1/user/custom_abis/#{post_custom_abi_response["id"]}", + custom_abi_1 + ) + |> doc(description: "Edit custom abi") + |> json_response(200) + + assert put_custom_abi_response["name"] == custom_abi_1["name"] + assert put_custom_abi_response["id"] == post_custom_abi_response["id"] + assert put_custom_abi_response["contract_address_hash"] == custom_abi_1["contract_address_hash"] + assert put_custom_abi_response["abi"] == custom_abi_1["abi"] + + assert conn + |> get("/api/account/v1/user/custom_abis") + |> json_response(200) == [put_custom_abi_response] + end + + test "delete custom abi", %{conn: conn} do + custom_abi = build(:custom_abi) + + post_custom_abi_response = + conn + |> post( + "/api/account/v1/user/custom_abis", + custom_abi + ) + |> json_response(200) + + assert post_custom_abi_response["name"] == custom_abi["name"] + assert post_custom_abi_response["id"] + + assert conn + |> get("/api/account/v1/user/custom_abis") + |> json_response(200) + |> Enum.count() == 1 + + assert conn + |> delete("/api/account/v1/user/custom_abis/#{post_custom_abi_response["id"]}") + |> doc(description: "Delete custom abi") + |> json_response(200) == %{"message" => "OK"} + + assert conn + |> get("/api/account/v1/user/custom_abis") + |> json_response(200) == [] + end + end + + describe "public tags" do + test "create public tags reuqest", %{conn: conn} do + public_tags_request = build(:public_tags_request) + + post_public_tasg_request_response = + conn + |> post( + "/api/account/v1/user/public_tags", + public_tags_request + ) + |> doc(description: "Submit request to add a public tag") + |> json_response(200) + + assert post_public_tasg_request_response["full_name"] == public_tags_request["full_name"] + assert post_public_tasg_request_response["email"] == public_tags_request["email"] + assert post_public_tasg_request_response["tags"] == public_tags_request["tags"] + assert post_public_tasg_request_response["website"] == public_tags_request["website"] + assert post_public_tasg_request_response["additional_comment"] == public_tags_request["additional_comment"] + assert post_public_tasg_request_response["addresses"] == public_tags_request["addresses"] + assert post_public_tasg_request_response["company"] == public_tags_request["company"] + assert post_public_tasg_request_response["is_owner"] == public_tags_request["is_owner"] + assert post_public_tasg_request_response["id"] + end + + test "get one public tags requests", %{conn: conn} do + public_tags_request = build(:public_tags_request) + + post_public_tasg_request_response = + conn + |> post( + "/api/account/v1/user/public_tags", + public_tags_request + ) + |> json_response(200) + + assert post_public_tasg_request_response["full_name"] == public_tags_request["full_name"] + assert post_public_tasg_request_response["email"] == public_tags_request["email"] + assert post_public_tasg_request_response["tags"] == public_tags_request["tags"] + assert post_public_tasg_request_response["website"] == public_tags_request["website"] + assert post_public_tasg_request_response["additional_comment"] == public_tags_request["additional_comment"] + assert post_public_tasg_request_response["addresses"] == public_tags_request["addresses"] + assert post_public_tasg_request_response["company"] == public_tags_request["company"] + assert post_public_tasg_request_response["is_owner"] == public_tags_request["is_owner"] + assert post_public_tasg_request_response["id"] + + assert conn + |> get("/api/account/v1/user/public_tags") + |> json_response(200) + |> Enum.map(&convert_date/1) == + [post_public_tasg_request_response] + |> Enum.map(&convert_date/1) + end + + test "get and delete several public tags requests", %{conn: conn} do + public_tags_list = build_list(10, :public_tags_request) + + final_list = + public_tags_list + |> Enum.map(fn request -> + response = + conn + |> post( + "/api/account/v1/user/public_tags", + request + ) + |> json_response(200) + + assert response["full_name"] == request["full_name"] + assert response["email"] == request["email"] + assert response["tags"] == request["tags"] + assert response["website"] == request["website"] + assert response["additional_comment"] == request["additional_comment"] + assert response["addresses"] == request["addresses"] + assert response["company"] == request["company"] + assert response["is_owner"] == request["is_owner"] + assert response["id"] + + convert_date(response) + end) + |> Enum.reverse() + + assert conn + |> get("/api/account/v1/user/public_tags") + |> doc(description: "Get list of requests to add a public tag") + |> json_response(200) + |> Enum.map(&convert_date/1) == final_list + + %{"id" => id} = Enum.at(final_list, 0) + + assert conn + |> delete("/api/account/v1/user/public_tags/#{id}", %{"remove_reason" => "reason"}) + |> doc(description: "Delete public tags request") + |> json_response(200) == %{"message" => "OK"} + + Enum.each(Enum.drop(final_list, 1), fn request -> + assert conn + |> delete("/api/account/v1/user/public_tags/#{request["id"]}", %{"remove_reason" => "reason"}) + |> json_response(200) == %{"message" => "OK"} + end) + + assert conn + |> get("/api/account/v1/user/public_tags") + |> json_response(200) == [] + end + + test "edit public tags request", %{conn: conn} do + public_tags_request = build(:public_tags_request) + + post_public_tasg_request_response = + conn + |> post( + "/api/account/v1/user/public_tags", + public_tags_request + ) + |> json_response(200) + + assert post_public_tasg_request_response["full_name"] == public_tags_request["full_name"] + assert post_public_tasg_request_response["email"] == public_tags_request["email"] + assert post_public_tasg_request_response["tags"] == public_tags_request["tags"] + assert post_public_tasg_request_response["website"] == public_tags_request["website"] + assert post_public_tasg_request_response["additional_comment"] == public_tags_request["additional_comment"] + assert post_public_tasg_request_response["addresses"] == public_tags_request["addresses"] + assert post_public_tasg_request_response["company"] == public_tags_request["company"] + assert post_public_tasg_request_response["is_owner"] == public_tags_request["is_owner"] + assert post_public_tasg_request_response["id"] + + assert conn + |> get("/api/account/v1/user/public_tags") + |> json_response(200) + |> Enum.map(&convert_date/1) == + [post_public_tasg_request_response] + |> Enum.map(&convert_date/1) + + new_public_tags_request = build(:public_tags_request) + + put_public_tasg_request_response = + conn + |> put( + "/api/account/v1/user/public_tags/#{post_public_tasg_request_response["id"]}", + new_public_tags_request + ) + |> doc(description: "Edit request to add a public tag") + |> json_response(200) + + assert put_public_tasg_request_response["full_name"] == new_public_tags_request["full_name"] + assert put_public_tasg_request_response["email"] == new_public_tags_request["email"] + assert put_public_tasg_request_response["tags"] == new_public_tags_request["tags"] + assert put_public_tasg_request_response["website"] == new_public_tags_request["website"] + assert put_public_tasg_request_response["additional_comment"] == new_public_tags_request["additional_comment"] + assert put_public_tasg_request_response["addresses"] == new_public_tags_request["addresses"] + assert put_public_tasg_request_response["company"] == new_public_tags_request["company"] + assert put_public_tasg_request_response["is_owner"] == new_public_tags_request["is_owner"] + assert put_public_tasg_request_response["id"] == post_public_tasg_request_response["id"] + + assert conn + |> get("/api/account/v1/user/public_tags") + |> json_response(200) + |> Enum.map(&convert_date/1) == + [put_public_tasg_request_response] + |> Enum.map(&convert_date/1) + end + end + + def convert_date(request) do + {:ok, time, _} = DateTime.from_iso8601(request["submission_date"]) + %{request | "submission_date" => Calendar.strftime(time, "%b %d, %Y")} + end +end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/account/custom_abi_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/account/custom_abi_controller_test.exs new file mode 100644 index 0000000000..89ff276f69 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/account/custom_abi_controller_test.exs @@ -0,0 +1,186 @@ +defmodule BlockScoutWeb.Account.CustomABIControllerTest do + use BlockScoutWeb.ConnCase + + alias BlockScoutWeb.Models.UserFromAuth + + @custom_abi "[{\"type\":\"function\",\"outputs\":[{\"type\":\"string\",\"name\":\"\"}],\"name\":\"name\",\"inputs\":[],\"constant\":true}]" + + setup %{conn: conn} do + auth = build(:auth) + + {:ok, user} = UserFromAuth.find_or_create(auth) + + {:ok, conn: Plug.Test.init_test_session(conn, current_user: user)} + end + + describe "test custom ABI functionality" do + test "custom ABI page opens correctly", %{conn: conn} do + result_conn = + conn + |> get(custom_abi_path(conn, :index)) + + assert html_response(result_conn, 200) =~ "Create a Custom ABI to interact with contracts." + end + + test "do not add custom ABI with wrong ABI", %{conn: conn} do + contract_address = insert(:address, contract_code: "0x0102") + + custom_abi = %{ + "name" => "1", + "address_hash" => to_string(contract_address), + "abi" => "" + } + + result_conn = + conn + |> post(custom_abi_path(conn, :create, %{"custom_abi" => custom_abi})) + + assert html_response(result_conn, 200) =~ "Add Custom ABI" + assert html_response(result_conn, 200) =~ to_string(contract_address.hash) + assert html_response(result_conn, 200) =~ "Required" + + result_conn_1 = + conn + |> post(custom_abi_path(conn, :create, %{"custom_abi" => Map.put(custom_abi, "abi", "123")})) + + assert html_response(result_conn_1, 200) =~ "Add Custom ABI" + assert html_response(result_conn_1, 200) =~ to_string(contract_address.hash) + assert html_response(result_conn_1, 200) =~ "Invalid format" + + result_conn_2 = + conn + |> get(custom_abi_path(conn, :index)) + + assert html_response(result_conn_2, 200) =~ "Create a Custom ABI to interact with contracts." + refute html_response(result_conn_2, 200) =~ to_string(contract_address.hash) + end + + test "add one custom abi and do not allow to create duplicates", %{conn: conn} do + contract_address = insert(:contract_address, contract_code: "0x0102") + + custom_abi = %{ + "name" => "1", + "address_hash" => to_string(contract_address), + "abi" => @custom_abi + } + + result_conn = + conn + |> post(custom_abi_path(conn, :create, %{"custom_abi" => custom_abi})) + + assert redirected_to(result_conn) == custom_abi_path(conn, :index) + + result_conn_2 = get(result_conn, custom_abi_path(conn, :index)) + assert html_response(result_conn_2, 200) =~ to_string(contract_address.hash) + assert html_response(result_conn_2, 200) =~ "Create a Custom ABI to interact with contracts." + + result_conn_1 = + conn + |> post(custom_abi_path(conn, :create, %{"custom_abi" => custom_abi})) + + assert html_response(result_conn_1, 200) =~ "Add Custom ABI" + assert html_response(result_conn_1, 200) =~ to_string(contract_address.hash) + assert html_response(result_conn_1, 200) =~ "Custom ABI for this address has already been added before" + end + + test "show error on address which is not smart contract", %{conn: conn} do + contract_address = insert(:address) + + custom_abi = %{ + "name" => "1", + "address_hash" => to_string(contract_address), + "abi" => @custom_abi + } + + result_conn = + conn + |> post(custom_abi_path(conn, :create, %{"custom_abi" => custom_abi})) + + assert html_response(result_conn, 200) =~ "Add Custom ABI" + assert html_response(result_conn, 200) =~ to_string(contract_address.hash) + assert html_response(result_conn, 200) =~ "Address is not a smart contract" + end + + test "user can add up to 15 custom ABIs", %{conn: conn} do + addresses = + Enum.map(1..15, fn _x -> + address = insert(:contract_address, contract_code: "0x0102") + + custom_abi = %{ + "name" => "1", + "address_hash" => to_string(address), + "abi" => @custom_abi + } + + assert conn + |> post(custom_abi_path(conn, :create, %{"custom_abi" => custom_abi})) + |> redirected_to() == custom_abi_path(conn, :index) + + to_string(address.hash) + end) + + assert abi_list = + conn + |> get(custom_abi_path(conn, :index)) + |> html_response(200) + + Enum.each(addresses, fn address -> assert abi_list =~ address end) + + address = insert(:contract_address, contract_code: "0x0102") + + custom_abi = %{ + "name" => "1", + "address_hash" => to_string(address), + "abi" => @custom_abi + } + + assert error_form = + conn + |> post(custom_abi_path(conn, :create, %{"custom_abi" => custom_abi})) + |> html_response(200) + + assert error_form =~ "Add Custom ABI" + assert error_form =~ "Max 15 ABIs per account" + assert error_form =~ to_string(address.hash) + + assert abi_list_new = + conn + |> get(custom_abi_path(conn, :index)) + |> html_response(200) + + Enum.each(addresses, fn address -> assert abi_list_new =~ address end) + + refute abi_list_new =~ to_string(address.hash) + assert abi_list_new =~ "You can create up to 15 Custom ABIs per account." + end + + test "after adding custom ABI on address page appear Read/Write Contract tab", %{conn: conn} do + contract_address = insert(:contract_address, contract_code: "0x0102") + + custom_abi = %{ + "name" => "1", + "address_hash" => to_string(contract_address), + "abi" => + "[{\"type\":\"function\",\"outputs\":[{\"type\":\"string\",\"name\":\"\"}],\"name\":\"name\",\"inputs\":[],\"constant\":true},{\"type\":\"function\",\"outputs\":[{\"type\":\"bool\",\"name\":\"success\"}],\"name\":\"approve\",\"inputs\":[{\"type\":\"address\",\"name\":\"_spender\"},{\"type\":\"uint256\",\"name\":\"_value\"}],\"constant\":false}]" + } + + result_conn = + conn + |> post(custom_abi_path(conn, :create, %{"custom_abi" => custom_abi})) + + assert redirected_to(result_conn) == custom_abi_path(conn, :index) + + result_conn_2 = get(result_conn, custom_abi_path(conn, :index)) + assert html_response(result_conn_2, 200) =~ to_string(contract_address.hash) + assert html_response(result_conn_2, 200) =~ "Create a Custom ABI to interact with contracts." + + assert contract_page = + result_conn + |> get(address_contract_path(result_conn, :index, to_string(contract_address))) + |> html_response(200) + + assert contract_page =~ "Write Contract" + assert contract_page =~ "Read Contract" + end + end +end diff --git a/apps/block_scout_web/test/block_scout_web/models/user_from_auth_test.exs b/apps/block_scout_web/test/block_scout_web/models/user_from_auth_test.exs new file mode 100644 index 0000000000..2d13f11600 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/models/user_from_auth_test.exs @@ -0,0 +1,107 @@ +defmodule UserFromAuthTest do + use Explorer.DataCase + + alias BlockScoutWeb.Models.UserFromAuth + alias Explorer.Account.Identity + alias Explorer.Account.Watchlist + alias Explorer.Repo + alias Ueberauth.Auth + alias Ueberauth.Auth.Info + alias Ueberauth.Strategy.Auth0 + + describe "get user info" do + test "from github" do + auth = %Auth{ + info: %Info{ + birthday: nil, + description: nil, + email: "john@blockscout.com", + first_name: nil, + image: "https://avatars.githubusercontent.com/u/666666=4", + last_name: nil, + location: nil, + name: "John Snow", + nickname: "johnnny", + phone: nil, + urls: %{profile: nil, website: nil} + }, + provider: :auth0, + strategy: Auth0, + uid: "github|666666" + } + + user_data = UserFromAuth.find_or_create(auth) + + %{ + id: identity_id, + email: "john@blockscout.com", + name: "John Snow", + uid: "github|666666" + } = Identity |> first |> Repo.account_repo().one() + + %{ + id: watchlist_id, + identity_id: ^identity_id, + name: "default" + } = Watchlist |> first |> Repo.account_repo().one() + + assert {:ok, + %{ + avatar: "https://avatars.githubusercontent.com/u/666666=4", + email: "john@blockscout.com", + id: ^identity_id, + name: "John Snow", + nickname: "johnnny", + uid: "github|666666", + watchlist_id: ^watchlist_id + }} = user_data + end + + test "from google" do + auth = %Auth{ + info: %Info{ + birthday: nil, + description: nil, + email: "john@blockscout.com", + first_name: "John", + image: "https://lh3.googleusercontent.com/a/xxx666-yyy777=s99-c", + last_name: "Snow", + location: nil, + name: "John Snow", + nickname: "johnnny", + phone: nil, + urls: %{profile: nil, website: nil} + }, + provider: :auth0, + strategy: Auth0, + uid: "google-oauth2|666666" + } + + user_data = UserFromAuth.find_or_create(auth) + + %{ + id: identity_id, + email: "john@blockscout.com", + name: "John Snow", + uid: "google-oauth2|666666" + } = Identity |> first |> Repo.account_repo().one() + + %{ + id: watchlist_id, + identity_id: ^identity_id, + name: "default" + } = Watchlist |> first |> Repo.account_repo().one() + + assert {:ok, + %{ + avatar: "https://lh3.googleusercontent.com/a/xxx666-yyy777=s99-c", + email: "john@blockscout.com", + id: ^identity_id, + name: "John Snow", + nickname: "johnnny", + uid: "google-oauth2|666666", + watchlist_id: ^watchlist_id + }} = user_data + end + end +end diff --git a/apps/block_scout_web/test/support/conn_case.ex b/apps/block_scout_web/test/support/conn_case.ex index f9ae650bf9..5ae6216f0c 100644 --- a/apps/block_scout_web/test/support/conn_case.ex +++ b/apps/block_scout_web/test/support/conn_case.ex @@ -22,6 +22,7 @@ defmodule BlockScoutWeb.ConnCase do import Phoenix.ConnTest import BlockScoutWeb.Router.Helpers import BlockScoutWeb.WebRouter.Helpers, except: [static_path: 2] + import Bureaucrat.Helpers # The default endpoint for testing @endpoint BlockScoutWeb.Endpoint @@ -36,9 +37,11 @@ defmodule BlockScoutWeb.ConnCase do @dialyzer {:nowarn_function, __ex_unit_setup_0: 1} setup tags do :ok = Ecto.Adapters.SQL.Sandbox.checkout(Explorer.Repo) + :ok = Ecto.Adapters.SQL.Sandbox.checkout(Explorer.Repo.Account) unless tags[:async] do Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo, {:shared, self()}) + Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Account, {:shared, self()}) end Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Transactions.child_id()) diff --git a/apps/block_scout_web/test/test_helper.exs b/apps/block_scout_web/test/test_helper.exs index 9944181688..a99ba9a234 100644 --- a/apps/block_scout_web/test/test_helper.exs +++ b/apps/block_scout_web/test/test_helper.exs @@ -11,12 +11,21 @@ Application.put_env(:wallaby, :base_url, BlockScoutWeb.Endpoint.url()) {:ok, _} = Application.ensure_all_started(:ex_machina) -ExUnit.configure(formatters: [JUnitFormatter, ExUnit.CLIFormatter]) +Bureaucrat.start( + writer: Bureaucrat.ApiBlueprintWriter, + default_path: "API blueprint.md", + env_var: "DOC" +) + +# Bureaucrat.start() + +ExUnit.configure(formatters: [JUnitFormatter, ExUnit.CLIFormatter, Bureaucrat.Formatter]) ExUnit.start() Mox.defmock(Explorer.ExchangeRates.Source.TestSource, for: Explorer.ExchangeRates.Source) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo, :manual) +Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Account, :manual) Absinthe.Test.prime(BlockScoutWeb.Schema) diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs index cc58587a54..c58d3bfd37 100644 --- a/apps/explorer/config/config.exs +++ b/apps/explorer/config/config.exs @@ -7,7 +7,7 @@ import Config # General application configuration config :explorer, - ecto_repos: [Explorer.Repo], + ecto_repos: [Explorer.Repo, Explorer.Repo.Account], token_functions_reader_max_retries: 3 config :explorer, Explorer.Counters.AverageBlockTime, diff --git a/apps/explorer/config/dev.exs b/apps/explorer/config/dev.exs index 7d7bd024a7..1ccfb34b6d 100644 --- a/apps/explorer/config/dev.exs +++ b/apps/explorer/config/dev.exs @@ -6,6 +6,9 @@ config :explorer, Explorer.Repo, timeout: :timer.seconds(80) # Configure API database config :explorer, Explorer.Repo.Replica1, timeout: :timer.seconds(80) +# Configure Account database +config :explorer, Explorer.Repo.Account, timeout: :timer.seconds(80) + config :explorer, Explorer.Tracer, env: "dev", disabled?: true config :logger, :explorer, diff --git a/apps/explorer/config/prod.exs b/apps/explorer/config/prod.exs index 8b42271e33..2d389e59d6 100644 --- a/apps/explorer/config/prod.exs +++ b/apps/explorer/config/prod.exs @@ -10,6 +10,11 @@ config :explorer, Explorer.Repo.Replica1, prepare: :unnamed, timeout: :timer.seconds(60) +# Configures Account database +config :explorer, Explorer.Repo.Account, + prepare: :unnamed, + timeout: :timer.seconds(60) + config :explorer, Explorer.Tracer, env: "production", disabled?: true config :logger, :explorer, diff --git a/apps/explorer/config/test.exs b/apps/explorer/config/test.exs index 3c7fc40fec..1b3b160791 100644 --- a/apps/explorer/config/test.exs +++ b/apps/explorer/config/test.exs @@ -23,6 +23,16 @@ config :explorer, Explorer.Repo.Replica1, timeout: :timer.seconds(60), queue_target: 1000 +# Configure API database +config :explorer, Explorer.Repo.Account, + database: "explorer_test_account", + hostname: "localhost", + pool: Ecto.Adapters.SQL.Sandbox, + # Default of `5_000` was too low for `BlockFetcher` test + ownership_timeout: :timer.minutes(1), + timeout: :timer.seconds(60), + queue_target: 1000 + config :logger, :explorer, level: :warn, path: Path.absname("logs/test/explorer.log") diff --git a/apps/explorer/lib/encrypt.ex b/apps/explorer/lib/encrypt.ex new file mode 100644 index 0000000000..e105feb11d --- /dev/null +++ b/apps/explorer/lib/encrypt.ex @@ -0,0 +1,116 @@ +defmodule Mix.Tasks.Encrypt do + @moduledoc "The encrypt mix task: `mix help encrypt`" + use Mix.Task + + alias Ecto.Changeset + + alias Explorer.Account.{ + CustomABI, + Identity, + PublicTagsRequest, + TagAddress, + TagTransaction, + WatchlistAddress, + WatchlistNotification + } + + alias Explorer.Repo.Account + alias Mix.Task + + @shortdoc "Encrypt" + def run(_) do + Task.run("app.start") + + Identity + |> Account.all() + |> Enum.each(fn element -> + element + |> Changeset.change(%{ + encrypted_uid: element.uid, + encrypted_email: element.email, + encrypted_name: element.name, + encrypted_nickname: element.nickname, + encrypted_avatar: element.avatar, + uid_hash: element.uid + }) + |> Account.update!() + end) + + TagAddress + |> Account.all() + |> Enum.each(fn element -> + element + |> Changeset.change(%{ + encrypted_name: element.name, + encrypted_address_hash: element.address_hash, + address_hash_hash: element.address_hash |> to_string() |> String.downcase() + }) + |> Account.update!() + end) + + TagTransaction + |> Account.all() + |> Enum.each(fn element -> + element + |> Changeset.change(%{ + encrypted_name: element.name, + encrypted_tx_hash: element.tx_hash, + tx_hash_hash: element.tx_hash |> to_string() |> String.downcase() + }) + |> Account.update!() + end) + + CustomABI + |> Account.all() + |> Enum.each(fn element -> + element + |> Changeset.change(%{ + encrypted_name: element.name, + encrypted_address_hash: element.address_hash, + address_hash_hash: element.address_hash |> to_string() |> String.downcase() + }) + |> Account.update!() + end) + + WatchlistAddress + |> Account.all() + |> Enum.each(fn element -> + element + |> Changeset.change(%{ + encrypted_name: element.name, + encrypted_address_hash: element.address_hash, + address_hash_hash: element.address_hash |> to_string() |> String.downcase() + }) + |> Account.update!() + end) + + WatchlistNotification + |> Account.all() + |> Enum.each(fn element -> + element + |> Changeset.change(%{ + encrypted_name: element.name, + encrypted_from_address_hash: element.from_address_hash, + encrypted_to_address_hash: element.to_address_hash, + encrypted_transaction_hash: element.transaction_hash, + encrypted_subject: element.subject, + from_address_hash_hash: element.from_address_hash |> to_string() |> String.downcase(), + to_address_hash_hash: element.to_address_hash |> to_string() |> String.downcase(), + transaction_hash_hash: element.transaction_hash |> to_string() |> String.downcase(), + subject_hash: element.subject + }) + |> Account.update!() + end) + + PublicTagsRequest + |> Account.all() + |> Enum.each(fn element -> + element + |> Changeset.change(%{ + encrypted_full_name: element.full_name, + encrypted_email: element.email + }) + |> Account.update!() + end) + end +end diff --git a/apps/explorer/lib/explorer/account.ex b/apps/explorer/lib/explorer/account.ex new file mode 100644 index 0000000000..5f2155b1f4 --- /dev/null +++ b/apps/explorer/lib/explorer/account.ex @@ -0,0 +1,9 @@ +defmodule Explorer.Account do + @moduledoc """ + Context for Account module. + """ + + def enabled? do + Application.get_env(:explorer, __MODULE__)[:enabled] + end +end diff --git a/apps/explorer/lib/explorer/account/api/key.ex b/apps/explorer/lib/explorer/account/api/key.ex new file mode 100644 index 0000000000..ab80d41ec2 --- /dev/null +++ b/apps/explorer/lib/explorer/account/api/key.ex @@ -0,0 +1,129 @@ +defmodule Explorer.Account.Api.Key do + @moduledoc """ + Module is responsible for schema for API keys, keys is used to track number of requests to the API endpoints + """ + use Explorer.Schema + + alias Explorer.Account.Identity + alias Ecto.{Changeset, UUID} + alias Explorer.Repo + + import Ecto.Changeset + + @max_key_per_account 3 + + @primary_key false + schema "account_api_keys" do + field(:name, :string) + field(:value, UUID, primary_key: true) + belongs_to(:identity, Identity) + + timestamps() + end + + @attrs ~w(value name identity_id)a + + def changeset do + %__MODULE__{} + |> cast(%{}, @attrs) + end + + def changeset(%__MODULE__{} = api_key, attrs \\ %{}) do + api_key + |> cast(attrs, @attrs) + |> validate_required(@attrs, message: "Required") + |> validate_length(:name, min: 1, max: 255) + |> unique_constraint(:value, message: "API key already exists") + |> foreign_key_constraint(:identity_id, message: "User not found") + |> api_key_count_constraint() + end + + def create(attrs) do + %__MODULE__{} + |> changeset(Map.put(attrs, :value, generate_api_key())) + |> Repo.account_repo().insert() + end + + def api_key_count_constraint(%Changeset{changes: %{identity_id: identity_id}} = api_key) do + if identity_id + |> api_keys_by_identity_id_query() + |> limit(@max_key_per_account) + |> Repo.account_repo().aggregate(:count, :value) >= @max_key_per_account do + api_key + |> add_error(:name, "Max #{@max_key_per_account} keys per account") + else + api_key + end + end + + def api_key_count_constraint(changeset), do: changeset + + def generate_api_key do + UUID.generate() + end + + def api_keys_by_identity_id_query(id) when not is_nil(id) do + __MODULE__ + |> where([api_key], api_key.identity_id == ^id) + |> order_by([api_key], desc: api_key.inserted_at) + end + + def api_keys_by_identity_id_query(_), do: nil + + def api_key_by_value_and_identity_id_query(api_key_value, identity_id) + when not is_nil(api_key_value) and not is_nil(identity_id) do + __MODULE__ + |> where([api_key], api_key.identity_id == ^identity_id and api_key.value == ^api_key_value) + end + + def api_key_by_value_and_identity_id_query(_, _), do: nil + + def get_api_key_by_value_and_identity_id(value, identity_id) when not is_nil(value) and not is_nil(identity_id) do + value + |> api_key_by_value_and_identity_id_query(identity_id) + |> Repo.account_repo().one() + end + + def get_api_key_by_value_and_identity_id(_, _), do: nil + + def update(%{value: api_key_value, identity_id: identity_id} = attrs) do + with api_key <- get_api_key_by_value_and_identity_id(api_key_value, identity_id), + false <- is_nil(api_key) do + api_key |> changeset(attrs) |> Repo.account_repo().update() + else + true -> + {:error, %{reason: :item_not_found}} + end + end + + def delete(api_key_value, identity_id) when not is_nil(api_key_value) and not is_nil(identity_id) do + api_key_value + |> api_key_by_value_and_identity_id_query(identity_id) + |> Repo.account_repo().delete_all() + end + + def delete(_, _), do: nil + + def get_api_keys_by_identity_id(id) when not is_nil(id) do + id + |> api_keys_by_identity_id_query() + |> Repo.account_repo().all() + end + + def get_api_keys_by_identity_id(_), do: nil + + def api_key_with_plan_by_value(api_key_value) when not is_nil(api_key_value) do + if match?({:ok, _casted_api_key}, UUID.cast(api_key_value)) do + __MODULE__ + |> where([api_key], api_key.value == ^api_key_value) + |> Repo.account_repo().one() + |> Repo.account_repo().preload(identity: :plan) + else + nil + end + end + + def api_key_with_plan_by_value(_), do: nil + + def get_max_api_keys_count, do: @max_key_per_account +end diff --git a/apps/explorer/lib/explorer/account/api/plan.ex b/apps/explorer/lib/explorer/account/api/plan.ex new file mode 100644 index 0000000000..b89b33bc9c --- /dev/null +++ b/apps/explorer/lib/explorer/account/api/plan.ex @@ -0,0 +1,13 @@ +defmodule Explorer.Account.Api.Plan do + @moduledoc """ + Module is responsible for schema for API plans, each plan contains its name and maximum number of requests per second + """ + use Explorer.Schema + + schema "account_api_plans" do + field(:name, :string) + field(:max_req_per_second, :integer) + + timestamps() + end +end diff --git a/apps/explorer/lib/explorer/account/custom_abi.ex b/apps/explorer/lib/explorer/account/custom_abi.ex new file mode 100644 index 0000000000..94f62f482a --- /dev/null +++ b/apps/explorer/lib/explorer/account/custom_abi.ex @@ -0,0 +1,226 @@ +defmodule Explorer.Account.CustomABI do + @moduledoc """ + Module is responsible for schema for API keys, keys is used to track number of requests to the API endpoints + """ + use Explorer.Schema + + alias ABI.FunctionSelector + alias Ecto.Changeset + alias Explorer.Account.Identity + alias Explorer.{Chain, Repo} + + import Explorer.Chain, only: [hash_to_lower_case_string: 1] + import Ecto.Changeset + + @max_abis_per_account 15 + + schema "account_custom_abis" do + field(:abi, {:array, :map}) + field(:given_abi, :string, virtual: true) + field(:abi_validating_error, :string, virtual: true) + field(:address_hash_hash, Cloak.Ecto.SHA256) + field(:address_hash, Explorer.Encrypted.AddressHash, null: false) + field(:name, Explorer.Encrypted.Binary) + + belongs_to(:identity, Identity) + + timestamps() + end + + @attrs ~w(name abi identity_id address_hash)a + + def changeset(%__MODULE__{} = custom_abi \\ %__MODULE__{}, attrs \\ %{}) do + custom_abi + |> cast(check_is_abi_valid?(attrs), @attrs ++ [:id, :given_abi, :abi_validating_error]) + |> validate_required(@attrs, message: "Required") + |> validate_custom_abi() + |> check_smart_contract_address() + |> foreign_key_constraint(:identity_id, message: "User not found") + |> put_hashed_fields() + |> unique_constraint([:identity_id, :address_hash_hash], + message: "Custom ABI for this address has already been added before" + ) + |> custom_abi_count_constraint() + end + + def changeset_without_constraints(%__MODULE__{} = custom_abi \\ %__MODULE__{}, attrs \\ %{}) do + custom_abi + |> cast(attrs, [:id | @attrs]) + |> validate_required(@attrs, message: "Required") + end + + defp put_hashed_fields(changeset) do + changeset + |> put_change(:address_hash_hash, hash_to_lower_case_string(get_field(changeset, :address_hash))) + end + + defp check_smart_contract_address(%Changeset{changes: %{address_hash: address_hash}} = custom_abi) do + check_smart_contract_address_inner(custom_abi, address_hash) + end + + defp check_smart_contract_address(%Changeset{data: %{address_hash: address_hash}} = custom_abi) do + check_smart_contract_address_inner(custom_abi, address_hash) + end + + defp check_smart_contract_address(custom_abi), do: custom_abi + + defp check_smart_contract_address_inner(changeset, address_hash) do + if Chain.is_address_hash_is_smart_contract?(address_hash) do + changeset + else + add_error(changeset, :address_hash, "Address is not a smart contract") + end + end + + defp validate_custom_abi(%Changeset{changes: %{given_abi: given_abi, abi_validating_error: error}} = custom_abi) do + custom_abi + |> add_error(:abi, error) + |> force_change(:abi, given_abi) + end + + defp validate_custom_abi(custom_abi), do: custom_abi + + defp check_is_abi_valid?(%{abi: abi} = custom_abi) when is_binary(abi) do + with {:ok, decoded} <- Jason.decode(abi), + true <- is_list(decoded) do + custom_abi + |> Map.put(:abi, decoded) + |> check_is_abi_valid?(abi) + else + _ -> + custom_abi + |> Map.put(:abi, "") + |> Map.put(:given_abi, abi) + |> Map.put(:abi_validating_error, "Invalid format") + end + end + + defp check_is_abi_valid?(custom_abi, given_abi \\ nil) + + defp check_is_abi_valid?(%{abi: abi} = custom_abi, given_abi) when is_list(abi) do + with true <- length(abi) > 0, + filtered_abi <- filter_abi(abi), + true <- Enum.count(filtered_abi) > 0 do + Map.put(custom_abi, :abi, filtered_abi) + else + _ -> + custom_abi + |> Map.put(:abi, "") + |> (&if(is_nil(given_abi), + do: Map.put(&1, :given_abi, Jason.encode!(abi)), + else: Map.put(&1, :given_abi, given_abi) + )).() + |> Map.put(:abi_validating_error, "ABI must contain functions") + end + end + + defp check_is_abi_valid?(custom_abi, _), do: custom_abi + + defp filter_abi(abi_list) when is_list(abi_list) do + Enum.filter(abi_list, &is_abi_function(&1)) + end + + defp is_abi_function(abi_item) when is_map(abi_item) do + case ABI.parse_specification([abi_item], include_events?: false) do + [%FunctionSelector{type: :constructor}] -> + false + + [_] -> + true + + _ -> + false + end + end + + def custom_abi_count_constraint(%Changeset{changes: %{identity_id: identity_id}} = custom_abi) do + if identity_id + |> custom_abis_by_identity_id_query() + |> limit(@max_abis_per_account) + |> Repo.account_repo().aggregate(:count, :id) >= @max_abis_per_account do + add_error(custom_abi, :name, "Max #{@max_abis_per_account} ABIs per account") + else + custom_abi + end + end + + def custom_abi_count_constraint(%Changeset{} = custom_abi), do: custom_abi + + def create(attrs) do + %__MODULE__{} + |> changeset(attrs) + |> Repo.account_repo().insert() + end + + def custom_abis_by_identity_id_query(id) when not is_nil(id) do + __MODULE__ + |> where([abi], abi.identity_id == ^id) + |> order_by([abi], desc: abi.id) + end + + def custom_abis_by_identity_id_query(_), do: nil + + def custom_abi_by_id_and_identity_id_query(id, identity_id) + when not is_nil(id) and not is_nil(identity_id) do + __MODULE__ + |> where([custom_abi], custom_abi.identity_id == ^identity_id and custom_abi.id == ^id) + end + + def custom_abi_by_id_and_identity_id_query(_, _), do: nil + + def custom_abi_by_identity_id_and_address_hash_query(address_hash, identity_id) + when not is_nil(identity_id) and not is_nil(address_hash) do + __MODULE__ + |> where([custom_abi], custom_abi.identity_id == ^identity_id and custom_abi.address_hash_hash == ^address_hash) + end + + def custom_abi_by_identity_id_and_address_hash_query(_, _), do: nil + + def get_custom_abi_by_identity_id_and_address_hash(address_hash, identity_id) + when not is_nil(identity_id) and not is_nil(address_hash) do + address_hash + |> hash_to_lower_case_string() + |> custom_abi_by_identity_id_and_address_hash_query(identity_id) + |> Repo.account_repo().one() + end + + def get_custom_abi_by_identity_id_and_address_hash(_, _), do: nil + + def get_custom_abi_by_id_and_identity_id(id, identity_id) when not is_nil(id) and not is_nil(identity_id) do + id + |> custom_abi_by_id_and_identity_id_query(identity_id) + |> Repo.account_repo().one() + end + + def get_custom_abi_by_id_and_identity_id(_, _), do: nil + + def get_custom_abis_by_identity_id(id) when not is_nil(id) do + id + |> custom_abis_by_identity_id_query() + |> Repo.account_repo().all() + end + + def get_custom_abis_by_identity_id(_), do: nil + + def delete(id, identity_id) when not is_nil(id) and not is_nil(identity_id) do + id + |> custom_abi_by_id_and_identity_id_query(identity_id) + |> Repo.account_repo().delete_all() + end + + def delete(_, _), do: nil + + def update(%{id: id, identity_id: identity_id} = attrs) do + with custom_abi <- get_custom_abi_by_id_and_identity_id(id, identity_id), + false <- is_nil(custom_abi) do + custom_abi + |> changeset(attrs) + |> Repo.account_repo().update() + else + true -> + {:error, %{reason: :item_not_found}} + end + end + + def get_max_custom_abis_count, do: @max_abis_per_account +end diff --git a/apps/explorer/lib/explorer/account/identity.ex b/apps/explorer/lib/explorer/account/identity.ex new file mode 100644 index 0000000000..d0766d7d35 --- /dev/null +++ b/apps/explorer/lib/explorer/account/identity.ex @@ -0,0 +1,41 @@ +defmodule Explorer.Account.Identity do + @moduledoc """ + Identity of user fetched via Oauth + """ + + use Explorer.Schema + + import Ecto.Changeset + + alias Explorer.Account.Api.Plan + alias Explorer.Account.{TagAddress, Watchlist} + + schema "account_identities" do + field(:uid_hash, Cloak.Ecto.SHA256) + field(:uid, Explorer.Encrypted.Binary) + field(:email, Explorer.Encrypted.Binary) + field(:name, Explorer.Encrypted.Binary) + field(:nickname, Explorer.Encrypted.Binary) + field(:avatar, Explorer.Encrypted.Binary) + + has_many(:tag_addresses, TagAddress) + has_many(:watchlists, Watchlist) + + belongs_to(:plan, Plan) + + timestamps() + end + + @doc false + def changeset(identity, attrs) do + identity + |> cast(attrs, [:uid, :email, :name, :nickname, :avatar]) + |> validate_required([:uid, :email, :name]) + |> put_hashed_fields() + end + + defp put_hashed_fields(changeset) do + changeset + |> put_change(:uid_hash, get_field(changeset, :uid)) + end +end diff --git a/apps/explorer/lib/explorer/account/notifier/email.ex b/apps/explorer/lib/explorer/account/notifier/email.ex new file mode 100644 index 0000000000..35555e3169 --- /dev/null +++ b/apps/explorer/lib/explorer/account/notifier/email.ex @@ -0,0 +1,155 @@ +defmodule Explorer.Account.Notifier.Email do + @moduledoc """ + Composing an email to sendgrid + """ + + require Logger + + alias BlockScoutWeb.WebRouter.Helpers + alias Explorer.Account.{Identity, Watchlist, WatchlistAddress, WatchlistNotification} + alias Explorer.Repo + + import Bamboo.{Email, SendGridHelper} + + def compose(notification, %{notify_email: notify}) when notify do + notification = preload(notification) + + email = compose_email(notification) + Logger.debug("--- composed email", fetcher: :account) + Logger.debug(email, fetcher: :account) + email + end + + def compose(_, _), do: nil + + defp compose_email(notification) do + email = new_email(from: sender(), to: email(notification)) + + email + |> with_template(template()) + |> add_dynamic_field("username", username(notification)) + |> add_dynamic_field("address_hash", address_hash_string(notification)) + |> add_dynamic_field("address_name", notification.watchlist_address.name) + |> add_dynamic_field("transaction_hash", hash_string(notification.transaction_hash)) + |> add_dynamic_field("from_address_hash", hash_string(notification.from_address_hash)) + |> add_dynamic_field("to_address_hash", hash_string(notification.to_address_hash)) + |> add_dynamic_field("block_number", notification.block_number) + |> add_dynamic_field("amount", amount(notification)) + |> add_dynamic_field("name", notification.name) + |> add_dynamic_field("tx_fee", notification.tx_fee) + |> add_dynamic_field("direction", direction(notification)) + |> add_dynamic_field("method", notification.method) + |> add_dynamic_field("transaction_url", transaction_url(notification)) + |> add_dynamic_field("address_url", address_url(notification.watchlist_address.address_hash)) + |> add_dynamic_field("from_url", address_url(notification.from_address_hash)) + |> add_dynamic_field("to_url", address_url(notification.to_address_hash)) + |> add_dynamic_field("block_url", block_url(notification)) + end + + defp amount(%WatchlistNotification{amount: amount, subject: subject, type: type}) do + case type do + "COIN" -> + amount + + "ERC-20" -> + amount + + "ERC-721" -> + "Token ID: " <> subject <> " of " + + "ERC-1155" -> + "Token ID: " <> subject <> " of " + end + end + + defp email(%WatchlistNotification{ + watchlist_address: %WatchlistAddress{ + watchlist: %Watchlist{ + identity: %Identity{ + email: email + } + } + } + }), + do: email + + defp username(%WatchlistNotification{ + watchlist_address: %WatchlistAddress{ + watchlist: %Watchlist{ + identity: %Identity{ + name: name + } + } + } + }), + do: name + + defp address_hash_string(%WatchlistNotification{ + watchlist_address: %WatchlistAddress{address_hash: address_hash} + }), + do: hash_string(address_hash) + + defp hash_string(hash) do + "0x" <> Base.encode16(hash.bytes, case: :lower) + end + + defp direction(notification) do + affect(notification) <> " " <> place(notification) + end + + defp place(%WatchlistNotification{direction: direction}) do + case direction do + "incoming" -> "at" + "outgoing" -> "from" + _ -> "unknown" + end + end + + defp affect(%WatchlistNotification{direction: direction}) do + case direction do + "incoming" -> "received" + "outgoing" -> "sent" + _ -> "unknown" + end + end + + defp preload(notification) do + Repo.account_repo().preload(notification, watchlist_address: [watchlist: :identity]) + end + + defp address_url(address_hash) do + Helpers.address_url(uri(), :show, address_hash) + end + + defp block_url(notification) do + URI.to_string(uri()) <> "block/" <> Integer.to_string(notification.block_number) + end + + defp transaction_url(notification) do + Helpers.transaction_url(uri(), :show, notification.transaction_hash) + end + + defp uri do + %URI{scheme: "https", host: host(), path: path()} + end + + defp host do + if System.get_env("MIX_ENV") == "prod" do + "blockscout.com" + else + Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] + end + end + + defp path do + Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:path] + end + + defp sender do + Application.get_env(:explorer, Explorer.Account)[:sendgrid][:sender] + end + + defp template do + Application.get_env(:explorer, Explorer.Account)[:sendgrid][:template] + end +end diff --git a/apps/explorer/lib/explorer/account/notifier/forbidden_address.ex b/apps/explorer/lib/explorer/account/notifier/forbidden_address.ex new file mode 100644 index 0000000000..f873864ff1 --- /dev/null +++ b/apps/explorer/lib/explorer/account/notifier/forbidden_address.ex @@ -0,0 +1,67 @@ +defmodule Explorer.Account.Notifier.ForbiddenAddress do + @moduledoc """ + Check if address is forbidden to notify + """ + + @blacklist [ + "0x0000000000000000000000000000000000000000", + "0x000000000000000000000000000000000000dEaD" + ] + + alias Explorer.Chain.Token + alias Explorer.Repo + + import Ecto.Query, only: [from: 2] + import Explorer.Chain, only: [string_to_address_hash: 1] + + def check(address_string) when is_bitstring(address_string) do + case format_address(address_string) do + {:error, message} -> + {:error, message} + + address_hash -> + check(address_hash) + end + end + + def check(%Explorer.Chain.Hash{} = address_hash) do + cond do + address_hash in blacklist() -> + {:error, "This address is blacklisted"} + + is_contract(address_hash) -> + {:error, "This address isn't personal"} + + address_hash -> + {:ok, address_hash} + end + end + + defp is_contract(%Explorer.Chain.Hash{} = address_hash) do + query = + from( + token in Token, + where: token.contract_address_hash == ^address_hash + ) + + contract_addresses = Repo.all(query) + List.first(contract_addresses) + end + + defp format_address(address_hash_string) do + case string_to_address_hash(address_hash_string) do + {:ok, address_hash} -> + address_hash + + :error -> + {:error, "Address is invalid"} + end + end + + defp blacklist do + Enum.map( + @blacklist, + &format_address(&1) + ) + end +end diff --git a/apps/explorer/lib/explorer/account/notifier/notify.ex b/apps/explorer/lib/explorer/account/notifier/notify.ex new file mode 100644 index 0000000000..b64b4bd47a --- /dev/null +++ b/apps/explorer/lib/explorer/account/notifier/notify.ex @@ -0,0 +1,153 @@ +defmodule Explorer.Account.Notifier.Notify do + @moduledoc """ + Composing notification, store and send it to email + """ + + alias Explorer.Account.Notifier.{Email, ForbiddenAddress, Summary} + alias Explorer.Account.{WatchlistAddress, WatchlistNotification} + alias Explorer.Chain.{TokenTransfer, Transaction} + alias Explorer.{Mailer, Repo} + + require Logger + + import Ecto.Query, only: [from: 2] + import Explorer.Chain, only: [hash_to_lower_case_string: 1] + + def call(nil), do: nil + def call([]), do: nil + + def call(transactions) when is_list(transactions) do + Enum.map(transactions, fn transaction -> process(transaction) end) + end + + defp process(%TokenTransfer{} = transfer) do + Logger.debug(transfer, fetcher: :account) + + transfer + |> Summary.process() + |> Enum.map(fn summary -> notify_watchlists(summary) end) + end + + defp process(%Transaction{} = transaction) do + Logger.debug(transaction, fetcher: :account) + + transaction + |> Summary.process() + |> Enum.map(fn summary -> notify_watchlists(summary) end) + end + + defp process(_), do: nil + + defp notify_watchlists(%Summary{from_address_hash: nil}), do: nil + defp notify_watchlists(%Summary{to_address_hash: nil}), do: nil + + defp notify_watchlists(%Summary{} = summary) do + incoming_addresses = find_watchlists_addresses(summary.to_address_hash) + outgoing_addresses = find_watchlists_addresses(summary.from_address_hash) + + Logger.debug("--- filled summary", fetcher: :account) + Logger.debug(summary, fetcher: :account) + + Enum.each(incoming_addresses, fn address -> notify_watchlist(address, summary, :incoming) end) + Enum.each(outgoing_addresses, fn address -> notify_watchlist(address, summary, :outgoing) end) + end + + defp notify_watchlists(nil), do: nil + + defp notify_watchlist(%WatchlistAddress{} = address, summary, direction) do + case ForbiddenAddress.check(address.address_hash) do + {:ok, _address_hash} -> + with %WatchlistNotification{} = notification <- + build_watchlist_notification( + address, + summary, + direction + ) do + notification + |> query_notification(address) + |> Repo.account_repo().all() + |> case do + [] -> save_and_send_notification(notification, address) + _ -> :ok + end + end + + {:error, _message} -> + nil + end + end + + defp query_notification(notification, watchlist_address) do + from(wn in WatchlistNotification, + where: + wn.watchlist_address_id == ^watchlist_address.id and + wn.from_address_hash_hash == ^notification.from_address_hash and + wn.to_address_hash_hash == ^notification.to_address_hash and + wn.transaction_hash_hash == ^notification.transaction_hash and + wn.block_number == ^notification.block_number and + wn.direction == ^notification.direction and + wn.subject_hash == ^notification.subject and + wn.amount == ^notification.amount + ) + end + + defp save_and_send_notification(%WatchlistNotification{} = notification, %WatchlistAddress{} = address) do + Repo.account_repo().insert(notification) + + email = Email.compose(notification, address) + + case Mailer.deliver_now(email, response: true) do + {:ok, _email, response} -> + Logger.info("--- email delivery response: SUCCESS", fetcher: :account) + Logger.info(response, fetcher: :account) + + {:error, error} -> + Logger.info("--- email delivery response: FAILED", fetcher: :account) + Logger.info(error, fetcher: :account) + end + end + + @doc """ + direction = :incoming || :outgoing + """ + def build_watchlist_notification(%Explorer.Account.WatchlistAddress{} = address, summary, direction) do + if is_watched(address, summary, direction) do + %WatchlistNotification{ + watchlist_address_id: address.id, + transaction_hash: summary.transaction_hash, + from_address_hash: summary.from_address_hash, + to_address_hash: summary.to_address_hash, + direction: to_string(direction), + method: summary.method, + block_number: summary.block_number, + amount: summary.amount, + subject: summary.subject, + tx_fee: summary.tx_fee, + name: summary.name, + type: summary.type, + from_address_hash_hash: hash_to_lower_case_string(summary.from_address_hash), + to_address_hash_hash: hash_to_lower_case_string(summary.to_address_hash), + transaction_hash_hash: hash_to_lower_case_string(summary.transaction_hash), + subject_hash: summary.subject + } + end + end + + defp is_watched(%WatchlistAddress{} = address, %{type: type}, direction) do + case {type, direction} do + {"COIN", :incoming} -> address.watch_coin_input + {"COIN", :outgoing} -> address.watch_coin_output + {"ERC-20", :incoming} -> address.watch_erc_20_input + {"ERC-20", :outgoing} -> address.watch_erc_20_output + {"ERC-721", :incoming} -> address.watch_erc_721_input + {"ERC-721", :outgoing} -> address.watch_erc_721_output + {"ERC-1155", :incoming} -> address.watch_erc_1155_input + {"ERC-1155", :outgoing} -> address.watch_erc_1155_output + end + end + + defp find_watchlists_addresses(%Explorer.Chain.Hash{} = address_hash) do + query = from(wa in WatchlistAddress, where: wa.address_hash_hash == ^address_hash) + Repo.account_repo().all(query) + end +end diff --git a/apps/explorer/lib/explorer/account/notifier/summary.ex b/apps/explorer/lib/explorer/account/notifier/summary.ex new file mode 100644 index 0000000000..a327074492 --- /dev/null +++ b/apps/explorer/lib/explorer/account/notifier/summary.ex @@ -0,0 +1,226 @@ +defmodule Explorer.Account.Notifier.Summary do + @moduledoc """ + Compose a summary from transactions + """ + + require Logger + + alias Explorer.Account.Notifier.Summary + alias Explorer.{Chain, Repo} + alias Explorer.Chain.Wei + + defstruct [ + :transaction_hash, + :from_address_hash, + :to_address_hash, + :method, + :block_number, + :amount, + :tx_fee, + :name, + :subject, + :type + ] + + def process(%Chain.Transaction{} = transaction) do + preloaded_transaction = preload(transaction) + + transfers_summaries = + handle_collection( + transaction, + preloaded_transaction.token_transfers + ) + + transaction_summary = fetch_summary(transaction) + + [transaction_summary | transfers_summaries] + |> Enum.filter(fn summary -> + not (is_nil(summary) or + summary == :nothing or + is_nil(summary.amount) or + summary.amount == Decimal.new(0)) + end) + end + + def process(%Chain.TokenTransfer{} = transfer) do + preloaded_transfer = preload(transfer) + + summary = fetch_summary(preloaded_transfer.transaction, preloaded_transfer) + + if summary != :nothing do + [summary] + else + [] + end + end + + def process(_), do: nil + + def handle_collection(_transaction, []), do: [] + + def handle_collection(transaction, transfers_list) do + Enum.map( + transfers_list, + fn transfer -> + transaction + |> fetch_summary(transfer) + end + ) + end + + def fetch_summary(%Chain.Transaction{block_number: nil}), do: :nothing + + def fetch_summary(%Chain.Transaction{created_contract_address_hash: nil} = transaction) do + %Summary{ + transaction_hash: transaction.hash, + method: method(transaction), + from_address_hash: transaction.from_address_hash, + to_address_hash: transaction.to_address_hash, + block_number: transaction.block_number, + amount: amount(transaction), + tx_fee: fee(transaction), + name: Application.get_env(:explorer, :coin_name), + subject: "Coin transaction", + type: "COIN" + } + end + + def fetch_summary(%Chain.Transaction{to_address_hash: nil} = transaction) do + %Summary{ + transaction_hash: transaction.hash, + method: "contract_creation", + from_address_hash: transaction.from_address_hash, + to_address_hash: transaction.created_contract_address_hash, + block_number: transaction.block_number, + amount: amount(transaction), + tx_fee: fee(transaction), + name: Application.get_env(:explorer, :coin_name), + subject: "Contract creation", + type: "COIN" + } + end + + def fetch_summary(_), do: :nothing + + def fetch_summary(%Chain.Transaction{block_number: nil}, _), do: :nothing + + def fetch_summary( + %Chain.Transaction{} = transaction, + %Chain.TokenTransfer{} = transfer + ) do + case transfer.token.type do + "ERC-20" -> + %Summary{ + transaction_hash: transaction.hash, + method: method(transfer), + from_address_hash: transfer.from_address_hash, + to_address_hash: transfer.to_address_hash, + block_number: transfer.block_number, + amount: amount(transfer), + subject: transfer.token.type, + tx_fee: fee(transaction), + name: transfer.token.name, + type: transfer.token.type + } + + "ERC-721" -> + %Summary{ + amount: 0, + transaction_hash: transaction.hash, + method: method(transfer), + from_address_hash: transfer.from_address_hash, + to_address_hash: transfer.to_address_hash, + block_number: transfer.block_number, + subject: to_string(transfer.token_id), + tx_fee: fee(transaction), + name: transfer.token.name, + type: transfer.token.type + } + + "ERC-1155" -> + %Summary{ + amount: 0, + transaction_hash: transaction.hash, + method: method(transfer), + from_address_hash: transfer.from_address_hash, + to_address_hash: transfer.to_address_hash, + block_number: transfer.block_number, + subject: token_ids(transfer), + tx_fee: fee(transaction), + name: transfer.token.name, + type: transfer.token.type + } + end + end + + def fetch_summary(_, _), do: :nothing + + @burn_address "0x0000000000000000000000000000000000000000" + + def method(%{from_address_hash: from, to_address_hash: to}) do + {:ok, burn_address} = format_address(@burn_address) + + cond do + burn_address == from -> "mint" + burn_address == to -> "burn" + true -> "transfer" + end + end + + def format_address(address_hash_string) do + Chain.string_to_address_hash(address_hash_string) + end + + def amount(%Chain.Transaction{} = transaction) do + Wei.to(transaction.value, :ether) + end + + def amount(%Chain.TokenTransfer{amount: amount}) when is_nil(amount), do: nil + + def amount(%Chain.TokenTransfer{amount: amount} = transfer) do + decimals = + Decimal.new( + Integer.pow( + 10, + Decimal.to_integer(token_decimals(transfer)) + ) + ) + + Decimal.div( + amount, + decimals + ) + end + + def token_ids(%Chain.TokenTransfer{token_id: token_id, token_ids: token_ids}) do + case token_id do + nil -> + Enum.map_join(token_ids, ", ", fn id -> to_string(id) end) + + _ -> + to_string(token_id) + end + end + + def token_decimals(%Chain.TokenTransfer{} = transfer) do + transfer.token.decimals || Decimal.new(1) + end + + def type(%Chain.Transaction{}), do: :coin + def type(%Chain.InternalTransaction{}), do: :coin + + def fee(%Chain.Transaction{} = transaction) do + {_, fee} = Chain.fee(transaction, :gwei) + fee + end + + def preload(%Chain.Transaction{} = transaction) do + Repo.preload(transaction, [:internal_transactions, token_transfers: :token]) + end + + def preload(%Chain.TokenTransfer{} = transfer) do + Repo.preload(transfer, [:transaction, :token]) + end + + def preload(_), do: nil +end diff --git a/apps/explorer/lib/explorer/account/notify.ex b/apps/explorer/lib/explorer/account/notify.ex new file mode 100644 index 0000000000..fc10579294 --- /dev/null +++ b/apps/explorer/lib/explorer/account/notify.ex @@ -0,0 +1,44 @@ +defmodule Explorer.Account.Notify do + @moduledoc """ + Interface for notifier, for import and call from other modules + """ + + alias Explorer.Account + alias Explorer.Account.Notifier.Notify + + require Logger + + def async(transactions) do + Task.async(fn -> process(transactions) end) + end + + defp process(transactions) do + if Account.enabled?() do + check_envs() + Notify.call(transactions) + end + rescue + err -> + Logger.info("--- Notifier error", fetcher: :account) + Logger.info(err, fetcher: :account) + end + + defp check_envs do + check_auth0() + check_sendgrid() + end + + defp check_auth0 do + (Application.get_env(:ueberauth, Ueberauth.Strategy.Auth0.OAuth)[:client_id] && + Application.get_env(:ueberauth, Ueberauth.Strategy.Auth0.OAuth)[:client_secret] && + Application.get_env(:ueberauth, Ueberauth)[:logout_return_to_url] && + Application.get_env(:ueberauth, Ueberauth)[:logout_url]) || + raise "Auth0 not configured" + end + + defp check_sendgrid do + (Application.get_env(:explorer, Explorer.Account)[:sendgrid][:sender] && + Application.get_env(:explorer, Explorer.Account)[:sendgrid][:template]) || + raise "SendGrid not configured" + end +end diff --git a/apps/explorer/lib/explorer/account/public_tags_request.ex b/apps/explorer/lib/explorer/account/public_tags_request.ex new file mode 100644 index 0000000000..eb989e5e76 --- /dev/null +++ b/apps/explorer/lib/explorer/account/public_tags_request.ex @@ -0,0 +1,255 @@ +defmodule Explorer.Account.PublicTagsRequest do + @moduledoc """ + Module is responsible for requests for public tags + """ + use Explorer.Schema + + alias Ecto.Changeset + alias Explorer.Account.Identity + alias Explorer.Chain.Hash + alias Explorer.Repo + alias Explorer.ThirdPartyIntegrations.AirTable + + import Ecto.Changeset + + @distance_between_same_addresses 24 * 3600 + + @max_public_tags_request_per_account 15 + @max_addresses_per_request 10 + @max_tags_per_request 2 + @max_tag_length 35 + + schema("account_public_tags_requests") do + field(:company, :string) + field(:website, :string) + field(:tags, :string) + field(:addresses, {:array, Hash.Address}) + field(:description, :string) + field(:additional_comment, :string) + field(:request_type, :string) + field(:is_owner, :boolean, default: true) + field(:remove_reason, :string) + field(:request_id, :string) + field(:full_name, Explorer.Encrypted.Binary) + field(:email, Explorer.Encrypted.Binary) + + belongs_to(:identity, Identity) + + timestamps() + end + + @local_fields [:__meta__, :inserted_at, :updated_at, :id, :request_id] + + def to_map(%__MODULE__{} = request) do + association_fields = request.__struct__.__schema__(:associations) + waste_fields = association_fields ++ @local_fields + + network = + Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] <> + Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:path] + + request |> Map.from_struct() |> Map.drop(waste_fields) |> Map.put(:network, network) + end + + @attrs ~w(company website description remove_reason request_id)a + @required_attrs ~w(full_name email tags addresses additional_comment request_type is_owner identity_id)a + + def changeset(%__MODULE__{} = public_tags_request, attrs \\ %{}) do + public_tags_request + |> cast(trim_empty_addresses(attrs), @attrs ++ @required_attrs) + |> validate_tags() + |> validate_required(@required_attrs, message: "Required") + |> validate_format(:email, ~r/^[A-Z0-9._%+-]+@[A-Z0-9-]+.+.[A-Z]{2,4}$/i, message: "is invalid") + |> validate_length(:addresses, min: 1, max: @max_addresses_per_request) + |> extract_and_validate_addresses() + |> foreign_key_constraint(:identity_id) + |> public_tags_request_count_constraint() + |> public_tags_request_time_interval_uniqueness() + end + + def changeset_without_constraints(%__MODULE__{} = public_tags_request \\ %__MODULE__{}, attrs \\ %{}) do + public_tags_request + |> cast(attrs, @attrs ++ @required_attrs) + end + + def create(attrs) do + %__MODULE__{} + |> changeset(Map.put(attrs, :request_type, "add")) + |> Repo.account_repo().insert() + |> AirTable.submit() + end + + defp trim_empty_addresses(%{addresses: addresses} = attrs) when is_list(addresses) do + filtered_addresses = Enum.filter(addresses, fn addr -> addr != "" and !is_nil(addr) end) + Map.put(attrs, :addresses, if(filtered_addresses == [], do: [""], else: filtered_addresses)) + end + + defp trim_empty_addresses(attrs), do: attrs + + def public_tags_request_count_constraint(%Changeset{changes: %{identity_id: identity_id}} = request) do + if identity_id + |> public_tags_requests_by_identity_id_query() + |> limit(@max_public_tags_request_per_account) + |> Repo.account_repo().aggregate(:count, :id) >= @max_public_tags_request_per_account do + request + |> add_error(:tags, "Max #{@max_public_tags_request_per_account} public tags requests per account") + else + request + end + end + + def public_tags_request_count_constraint(changeset), do: changeset + + defp public_tags_request_time_interval_uniqueness(%Changeset{changes: %{addresses: addresses}} = request) do + prepared_addresses = + if request.data && request.data.addresses, do: addresses -- request.data.addresses, else: addresses + + public_tags_request = + request + |> fetch_field!(:identity_id) + |> public_tags_requests_by_identity_id_query() + |> where( + [public_tags_request], + fragment("? && ?", public_tags_request.addresses, ^Enum.map(prepared_addresses, fn x -> x.bytes end)) + ) + |> limit(1) + |> Repo.account_repo().one() + + now = DateTime.utc_now() + + if !is_nil(public_tags_request) && + public_tags_request.inserted_at + |> DateTime.add(@distance_between_same_addresses, :second) + |> DateTime.compare(now) == :gt do + request + |> add_error(:addresses, "You have already submitted the same public tag address in the last 24 hours") + else + request + end + end + + defp public_tags_request_time_interval_uniqueness(changeset), do: changeset + + defp extract_and_validate_addresses(%Changeset{} = changeset) do + with {:fetch, {_src, addresses}} <- {:fetch, fetch_field(changeset, :addresses)}, + false <- is_nil(addresses), + {:uniqueness, true} <- {:uniqueness, Enum.count(Enum.uniq(addresses)) == Enum.count(addresses)} do + changeset + else + {:uniqueness, false} -> + add_error(changeset, :addresses, "All addresses should be unique") + + _ -> + add_error(changeset, :addresses, "No addresses") + end + end + + defp validate_tags(%Changeset{} = changeset) do + with {:fetch, {_src, tags}} <- {:fetch, fetch_field(changeset, :tags)}, + false <- is_nil(tags), + trimmed_tags <- String.trim(tags), + tags_list <- String.split(trimmed_tags, ";"), + {:filter_empty, [_ | _] = filtered_tags} <- {:filter_empty, Enum.filter(tags_list, fn tag -> tag != "" end)}, + trimmed_spaces_tags <- Enum.map(filtered_tags, fn tag -> String.trim(tag) end), + {:validate, false} <- {:validate, Enum.any?(tags_list, fn tag -> String.length(tag) > @max_tag_length end)}, + {:uniqueness, true} <- + {:uniqueness, + Enum.count(Enum.uniq_by(trimmed_spaces_tags, &String.downcase(&1))) == Enum.count(trimmed_spaces_tags)}, + trimmed_tags_list <- Enum.take(trimmed_spaces_tags, @max_tags_per_request) do + force_change(changeset, :tags, Enum.join(trimmed_tags_list, ";")) + else + {:uniqueness, false} -> + add_error(changeset, :tags, "All tags should be unique") + + {:filter_empty, _} -> + add_error(changeset, :tags, "All tags are empty strings") + + {:validate, _} -> + add_error(changeset, :tags, "Tags should contain less than #{@max_tag_length} characters") + + _ -> + add_error(changeset, :tags, "No tags") + end + end + + def public_tags_requests_by_identity_id_query(id) when not is_nil(id) do + __MODULE__ + |> where( + [request], + request.identity_id == ^id and request.request_type != "delete" and not is_nil(request.request_id) + ) + |> order_by([request], desc: request.id) + end + + def public_tags_requests_by_identity_id_query(_), do: nil + + def public_tags_request_by_id_and_identity_id_query(id, identity_id) + when not is_nil(id) and not is_nil(identity_id) do + __MODULE__ + |> where([public_tags_request], public_tags_request.identity_id == ^identity_id and public_tags_request.id == ^id) + end + + def public_tags_request_by_id_and_identity_id_query(_, _), do: nil + + def get_public_tags_request_by_id_and_identity_id(id, identity_id) when not is_nil(id) and not is_nil(identity_id) do + id |> public_tags_request_by_id_and_identity_id_query(identity_id) |> Repo.account_repo().one() + end + + def get_public_tags_request_by_id_and_identity_id(_, _), do: nil + + def get_public_tags_requests_by_identity_id(id) when not is_nil(id) do + id + |> public_tags_requests_by_identity_id_query() + |> Repo.account_repo().all() + end + + def get_public_tags_requests_by_identity_id(_), do: nil + + def delete_public_tags_request(identity_id, id) when not is_nil(id) and not is_nil(identity_id) do + id + |> public_tags_request_by_id_and_identity_id_query(identity_id) + |> Repo.account_repo().delete_all() + end + + def delete_public_tags_request(_, _), do: nil + + def update(%{id: id, identity_id: identity_id} = attrs) do + with public_tags_request <- get_public_tags_request_by_id_and_identity_id(id, identity_id), + false <- is_nil(public_tags_request), + {:ok, changeset} <- + public_tags_request |> changeset(Map.put(attrs, :request_type, "edit")) |> Repo.account_repo().update() do + AirTable.submit({:ok, changeset}) + else + true -> + {:error, %{reason: :item_not_found}} + + other -> + other + end + end + + def mark_as_deleted_public_tags_request(%{id: id, identity_id: identity_id, remove_reason: remove_reason}) do + with public_tags_request <- get_public_tags_request_by_id_and_identity_id(id, identity_id), + false <- is_nil(public_tags_request), + {:ok, changeset} <- + public_tags_request + |> changeset_without_constraints(%{request_type: "delete", remove_reason: remove_reason}) + |> Repo.account_repo().update() do + case AirTable.submit({:ok, changeset}) do + {:error, changeset} -> + changeset + + _ -> + true + end + else + {:error, changeset} -> + changeset + + _ -> + false + end + end + + def get_max_public_tags_request_count, do: @max_public_tags_request_per_account +end diff --git a/apps/explorer/lib/explorer/account/tag_address.ex b/apps/explorer/lib/explorer/account/tag_address.ex new file mode 100644 index 0000000000..75d4e8d6c8 --- /dev/null +++ b/apps/explorer/lib/explorer/account/tag_address.ex @@ -0,0 +1,156 @@ +defmodule Explorer.Account.TagAddress do + @moduledoc """ + Watchlist is root entity for WatchlistAddresses + """ + + use Explorer.Schema + + import Ecto.Changeset + + alias Ecto.Changeset + alias Explorer.Account.Identity + alias Explorer.{Chain, Repo} + alias Explorer.Chain.{Address, Hash} + + import Explorer.Chain, only: [hash_to_lower_case_string: 1] + + @max_tag_address_per_account 15 + + schema "account_tag_addresses" do + field(:address_hash_hash, Cloak.Ecto.SHA256) + field(:name, Explorer.Encrypted.Binary) + field(:address_hash, Explorer.Encrypted.AddressHash, null: false) + + belongs_to(:identity, Identity) + + timestamps() + end + + @attrs ~w(name identity_id address_hash)a + + def changeset do + %__MODULE__{} + |> cast(%{}, @attrs) + end + + @doc false + def changeset(tag, attrs) do + tag + |> cast(attrs, @attrs) + |> validate_required(@attrs, message: "Required") + |> validate_length(:name, min: 1, max: 35) + |> put_hashed_fields() + |> unique_constraint([:identity_id, :address_hash_hash], message: "Address tag already exists") + |> check_existance_or_create_address() + |> tag_address_count_constraint() + end + + def create(attrs) do + %__MODULE__{} + |> changeset(attrs) + |> Repo.account_repo().insert() + end + + defp put_hashed_fields(changeset) do + changeset + |> put_change(:address_hash_hash, hash_to_lower_case_string(get_field(changeset, :address_hash))) + end + + defp check_existance_or_create_address(%Changeset{changes: %{address_hash: address_hash}, valid?: true} = changeset) do + check_existance_or_create_address_inner(changeset, address_hash) + end + + defp check_existance_or_create_address(changeset), do: changeset + + defp check_existance_or_create_address_inner(changeset, address_hash) do + with {:ok, hash} <- Hash.Address.cast(address_hash), + {:ok, %Address{}} <- Chain.find_or_insert_address_from_hash(hash, []) do + changeset + end + end + + def tag_address_count_constraint(%Changeset{changes: %{identity_id: identity_id}} = tag_address) do + if identity_id + |> tags_address_by_identity_id_query() + |> limit(@max_tag_address_per_account) + |> Repo.account_repo().aggregate(:count, :id) >= @max_tag_address_per_account do + tag_address + |> add_error(:name, "Max #{@max_tag_address_per_account} tags per account") + else + tag_address + end + end + + def tag_address_count_constraint(changeset), do: changeset + + def tags_address_by_identity_id_query(id) when not is_nil(id) do + __MODULE__ + |> where([tag], tag.identity_id == ^id) + |> order_by([tag], desc: tag.id) + end + + def tags_address_by_identity_id_query(_), do: nil + + def get_tags_address_by_identity_id(id) when not is_nil(id) do + id + |> tags_address_by_identity_id_query() + |> Repo.account_repo().all() + end + + def get_tags_address_by_identity_id(_), do: nil + + def tag_address_by_address_hash_and_identity_id_query(address_hash, identity_id) + when not is_nil(address_hash) and not is_nil(identity_id) do + __MODULE__ + |> where([tag], tag.identity_id == ^identity_id and tag.address_hash == ^address_hash) + end + + def tag_address_by_address_hash_and_identity_id_query(_, _), do: nil + + def get_tag_address_by_address_hash_and_identity_id(address_hash, identity_id) + when not is_nil(address_hash) and not is_nil(identity_id) do + address_hash + |> hash_to_lower_case_string() + |> tag_address_by_address_hash_and_identity_id_query(identity_id) + |> Repo.account_repo().one() + end + + def get_tag_address_by_address_hash_and_identity_id(_, _), do: nil + + def tag_address_by_id_and_identity_id_query(tag_id, identity_id) + when not is_nil(tag_id) and not is_nil(identity_id) do + __MODULE__ + |> where([tag], tag.identity_id == ^identity_id and tag.id == ^tag_id) + end + + def tag_address_by_id_and_identity_id_query(_, _), do: nil + + def get_tag_address_by_id_and_identity_id_query(tag_id, identity_id) + when not is_nil(tag_id) and not is_nil(identity_id) do + tag_id + |> tag_address_by_id_and_identity_id_query(identity_id) + |> Repo.account_repo().one() + end + + def get_tag_address_by_id_and_identity_id_query(_, _), do: nil + + def delete(tag_id, identity_id) when not is_nil(tag_id) and not is_nil(identity_id) do + tag_id + |> tag_address_by_id_and_identity_id_query(identity_id) + |> Repo.account_repo().delete_all() + end + + def delete(_, _), do: nil + + def update(%{id: tag_id, identity_id: identity_id} = attrs) do + with tag <- get_tag_address_by_id_and_identity_id_query(tag_id, identity_id), + false <- is_nil(tag) do + tag |> changeset(attrs) |> Repo.account_repo().update() + else + true -> + {:error, %{reason: :item_not_found}} + end + end + + def get_max_tags_count, do: @max_tag_address_per_account +end diff --git a/apps/explorer/lib/explorer/account/tag_transaction.ex b/apps/explorer/lib/explorer/account/tag_transaction.ex new file mode 100644 index 0000000000..495dd1779a --- /dev/null +++ b/apps/explorer/lib/explorer/account/tag_transaction.ex @@ -0,0 +1,155 @@ +defmodule Explorer.Account.TagTransaction do + @moduledoc """ + This is a personal tag for transaction + """ + + use Explorer.Schema + + import Ecto.Changeset + + alias Ecto.Changeset + alias Explorer.Account.Identity + alias Explorer.{Chain, Repo} + import Explorer.Chain, only: [hash_to_lower_case_string: 1] + + @max_tag_transaction_per_account 15 + + schema "account_tag_transactions" do + field(:tx_hash_hash, Cloak.Ecto.SHA256) + field(:name, Explorer.Encrypted.Binary) + field(:tx_hash, Explorer.Encrypted.TransactionHash, null: false) + + belongs_to(:identity, Identity) + + timestamps() + end + + @attrs ~w(name identity_id tx_hash)a + + def changeset do + %__MODULE__{} + |> cast(%{}, @attrs) + end + + @doc false + def changeset(tag, attrs) do + tag + |> cast(attrs, @attrs) + |> validate_required(@attrs, message: "Required") + |> validate_length(:name, min: 1, max: 35) + |> put_hashed_fields() + |> unique_constraint([:identity_id, :tx_hash_hash], message: "Transaction tag already exists") + |> tag_transaction_count_constraint() + |> check_transaction_existance() + end + + def create(attrs) do + %__MODULE__{} + |> changeset(attrs) + |> Repo.account_repo().insert() + end + + defp put_hashed_fields(changeset) do + changeset + |> put_change(:tx_hash_hash, hash_to_lower_case_string(get_field(changeset, :tx_hash))) + end + + defp check_transaction_existance(%Changeset{changes: %{tx_hash: tx_hash}} = changeset) do + check_transaction_existance_inner(changeset, tx_hash) + end + + defp check_transaction_existance(changeset), do: changeset + + defp check_transaction_existance_inner(changeset, tx_hash) do + if match?({:ok, _}, Chain.hash_to_transaction(tx_hash)) do + changeset + else + add_error(changeset, :tx_hash, "Transaction does not exist") + end + end + + def tag_transaction_count_constraint(%Changeset{changes: %{identity_id: identity_id}} = tag_transaction) do + if identity_id + |> tags_transaction_by_identity_id_query() + |> limit(@max_tag_transaction_per_account) + |> Repo.account_repo().aggregate(:count, :id) >= @max_tag_transaction_per_account do + tag_transaction + |> add_error(:name, "Max #{@max_tag_transaction_per_account} tags per account") + else + tag_transaction + end + end + + def tag_transaction_count_constraint(changeset), do: changeset + + def tags_transaction_by_identity_id_query(id) when not is_nil(id) do + __MODULE__ + |> where([tag], tag.identity_id == ^id) + |> order_by([tag], desc: tag.id) + end + + def tags_transaction_by_identity_id_query(_), do: nil + + def get_tags_transaction_by_identity_id(id) when not is_nil(id) do + id + |> tags_transaction_by_identity_id_query() + |> Repo.account_repo().all() + end + + def get_tags_transaction_by_identity_id(_), do: nil + + def tag_transaction_by_transaction_hash_and_identity_id_query(tx_hash, identity_id) + when not is_nil(tx_hash) and not is_nil(identity_id) do + __MODULE__ + |> where([tag], tag.identity_id == ^identity_id and tag.tx_hash == ^tx_hash) + end + + def tag_transaction_by_transaction_hash_and_identity_id_query(_, _), do: nil + + def get_tag_transaction_by_transaction_hash_and_identity_id(tx_hash, identity_id) + when not is_nil(tx_hash) and not is_nil(identity_id) do + tx_hash + |> hash_to_lower_case_string() + |> tag_transaction_by_transaction_hash_and_identity_id_query(identity_id) + |> Repo.account_repo().one() + end + + def get_tag_transaction_by_transaction_hash_and_identity_id(_, _), do: nil + + def tag_transaction_by_id_and_identity_id_query(tag_id, identity_id) + when not is_nil(tag_id) and not is_nil(identity_id) do + __MODULE__ + |> where([tag], tag.identity_id == ^identity_id and tag.id == ^tag_id) + end + + def tag_transaction_by_id_and_identity_id_query(_, _), do: nil + + def get_tag_transaction_by_id_and_identity_id_query(tag_id, identity_id) + when not is_nil(tag_id) and not is_nil(identity_id) do + tag_id + |> tag_transaction_by_id_and_identity_id_query(identity_id) + |> Repo.account_repo().one() + end + + def get_tag_transaction_by_id_and_identity_id_query(_, _), do: nil + + def delete(tag_id, identity_id) when not is_nil(tag_id) and not is_nil(identity_id) do + tag_id + |> tag_transaction_by_id_and_identity_id_query(identity_id) + |> Repo.account_repo().delete_all() + end + + def delete(_, _), do: nil + + def update(%{id: tag_id, identity_id: identity_id} = attrs) do + with tag <- get_tag_transaction_by_id_and_identity_id_query(tag_id, identity_id), + false <- is_nil(tag) do + tag |> changeset(attrs) |> Repo.account_repo().update() + else + true -> + {:error, %{reason: :item_not_found}} + end + end + + def get_max_tags_count, do: @max_tag_transaction_per_account +end diff --git a/apps/explorer/lib/explorer/account/watchlist.ex b/apps/explorer/lib/explorer/account/watchlist.ex new file mode 100644 index 0000000000..cd6998b83f --- /dev/null +++ b/apps/explorer/lib/explorer/account/watchlist.ex @@ -0,0 +1,27 @@ +defmodule Explorer.Account.Watchlist do + @moduledoc """ + Watchlist is root entity for WatchlistAddresses + """ + + use Explorer.Schema + + import Ecto.Changeset + + alias Explorer.Account.{Identity, WatchlistAddress} + + @derive {Jason.Encoder, only: [:name, :watchlist_addresses]} + schema "account_watchlists" do + field(:name, :string) + belongs_to(:identity, Identity) + has_many(:watchlist_addresses, WatchlistAddress) + + timestamps() + end + + @doc false + def changeset(watchlist, attrs) do + watchlist + |> cast(attrs, [:name]) + |> validate_required([:name]) + end +end diff --git a/apps/explorer/lib/explorer/account/watchlist_address.ex b/apps/explorer/lib/explorer/account/watchlist_address.ex new file mode 100644 index 0000000000..cce6c7935e --- /dev/null +++ b/apps/explorer/lib/explorer/account/watchlist_address.ex @@ -0,0 +1,176 @@ +defmodule Explorer.Account.WatchlistAddress do + @moduledoc """ + WatchlistAddress entity + """ + + use Explorer.Schema + + import Ecto.Changeset + + alias Ecto.Changeset + alias Explorer.Account.Notifier.ForbiddenAddress + alias Explorer.Account.Watchlist + alias Explorer.{Chain, Repo} + alias Explorer.Chain.{Address, Wei} + + import Explorer.Chain, only: [hash_to_lower_case_string: 1] + + @max_watchlist_addresses_per_account 10 + + schema "account_watchlist_addresses" do + field(:address_hash_hash, Cloak.Ecto.SHA256) + field(:name, Explorer.Encrypted.Binary) + field(:address_hash, Explorer.Encrypted.AddressHash, null: false) + + belongs_to(:watchlist, Watchlist) + + field(:watch_coin_input, :boolean, default: true) + field(:watch_coin_output, :boolean, default: true) + field(:watch_erc_20_input, :boolean, default: true) + field(:watch_erc_20_output, :boolean, default: true) + field(:watch_erc_721_input, :boolean, default: true) + field(:watch_erc_721_output, :boolean, default: true) + field(:watch_erc_1155_input, :boolean, default: true) + field(:watch_erc_1155_output, :boolean, default: true) + field(:notify_email, :boolean, default: true) + field(:notify_epns, :boolean) + field(:notify_feed, :boolean) + field(:notify_inapp, :boolean) + + field(:fetched_coin_balance, Wei, virtual: true) + + timestamps() + end + + @attrs ~w(name address_hash watch_coin_input watch_coin_output watch_erc_20_input watch_erc_20_output watch_erc_721_input watch_erc_721_output watch_erc_1155_input watch_erc_1155_output notify_email notify_epns notify_feed notify_inapp watchlist_id)a + + def changeset do + %__MODULE__{} + |> cast(%{}, @attrs) + end + + @doc false + def changeset(watchlist_address, attrs \\ %{}) do + watchlist_address + |> cast(attrs, @attrs) + |> validate_length(:name, min: 1, max: 35) + |> validate_required([:name, :address_hash, :watchlist_id], message: "Required") + |> put_hashed_fields() + |> unique_constraint([:watchlist_id, :address_hash_hash], + name: "unique_watchlist_id_address_hash_hash_index", + message: "Address already added to the watch list" + ) + |> check_address() + |> watchlist_address_count_constraint() + end + + defp put_hashed_fields(changeset) do + changeset + |> put_change(:address_hash_hash, hash_to_lower_case_string(get_field(changeset, :address_hash))) + end + + def create(attrs) do + %__MODULE__{} + |> changeset(attrs) + |> Repo.account_repo().insert() + end + + def watchlist_address_count_constraint(%Changeset{changes: %{watchlist_id: watchlist_id}} = watchlist_address) do + if watchlist_id + |> watchlist_addresses_by_watchlist_id_query() + |> limit(@max_watchlist_addresses_per_account) + |> Repo.account_repo().aggregate(:count, :id) >= @max_watchlist_addresses_per_account do + watchlist_address + |> add_error(:name, "Max #{@max_watchlist_addresses_per_account} watch list addresses per account") + else + watchlist_address + end + end + + def watchlist_address_count_constraint(changeset), do: changeset + + defp check_address(%Changeset{changes: %{address_hash: address_hash}, valid?: true} = changeset) do + check_address_inner(changeset, address_hash) + end + + defp check_address(%Changeset{data: %{address_hash: address_hash}, valid?: true} = changeset) do + check_address_inner(changeset, address_hash) + end + + defp check_address(changeset), do: changeset + + defp check_address_inner(changeset, address_hash) do + with {:ok, address_hash} <- ForbiddenAddress.check(address_hash), + {:ok, %Address{}} <- Chain.find_or_insert_address_from_hash(address_hash, []) do + changeset + else + {:error, reason} -> + add_error(changeset, :address_hash, reason) + + _ -> + add_error(changeset, :address_hash, "Address error") + end + end + + def watchlist_addresses_by_watchlist_id_query(watchlist_id) when not is_nil(watchlist_id) do + __MODULE__ + |> where([wl_address], wl_address.watchlist_id == ^watchlist_id) + end + + def watchlist_addresses_by_watchlist_id_query(_), do: nil + + def watchlist_address_by_id_and_watchlist_id_query(watchlist_address_id, watchlist_id) + when not is_nil(watchlist_address_id) and not is_nil(watchlist_id) do + __MODULE__ + |> where([wl_address], wl_address.watchlist_id == ^watchlist_id and wl_address.id == ^watchlist_address_id) + end + + def watchlist_address_by_id_and_watchlist_id_query(_, _), do: nil + + def get_watchlist_address_by_id_and_watchlist_id(watchlist_address_id, watchlist_id) + when not is_nil(watchlist_address_id) and not is_nil(watchlist_id) do + watchlist_address_id + |> watchlist_address_by_id_and_watchlist_id_query(watchlist_id) + |> Repo.account_repo().one() + end + + def get_watchlist_address_by_id_and_watchlist_id(_, _), do: nil + + def delete(watchlist_address_id, watchlist_id) + when not is_nil(watchlist_address_id) and not is_nil(watchlist_id) do + watchlist_address_id + |> watchlist_address_by_id_and_watchlist_id_query(watchlist_id) + |> Repo.account_repo().delete_all() + end + + def delete(_, _), do: nil + + def update(%{id: id, watchlist_id: watchlist_id} = attrs) do + with watchlist_address <- get_watchlist_address_by_id_and_watchlist_id(id, watchlist_id), + false <- is_nil(watchlist_address) do + watchlist_address + |> changeset(attrs) + |> Repo.account_repo().update() + else + true -> + {:error, %{reason: :item_not_found}} + end + end + + def get_max_watchlist_addresses_count, do: @max_watchlist_addresses_per_account + + def preload_address_fetched_coin_balance(%Watchlist{watchlist_addresses: watchlist_addresses} = watchlist) do + w_addresses = + Enum.map(watchlist_addresses, fn wa -> + preload_address_fetched_coin_balance(wa) + end) + + %Watchlist{watchlist | watchlist_addresses: w_addresses} + end + + def preload_address_fetched_coin_balance(%__MODULE__{address_hash: address_hash} = watchlist_address) do + %__MODULE__{watchlist_address | fetched_coin_balance: address_hash |> Address.fetched_coin_balance() |> Repo.one()} + end + + def preload_address_fetched_coin_balance(watchlist), do: watchlist +end diff --git a/apps/explorer/lib/explorer/account/watchlist_notification.ex b/apps/explorer/lib/explorer/account/watchlist_notification.ex new file mode 100644 index 0000000000..6c909be3ed --- /dev/null +++ b/apps/explorer/lib/explorer/account/watchlist_notification.ex @@ -0,0 +1,65 @@ +defmodule Explorer.Account.WatchlistNotification do + @moduledoc """ + Strored notification about event + related to WatchlistAddress + """ + + use Explorer.Schema + + import Ecto.Changeset + import Explorer.Chain, only: [hash_to_lower_case_string: 1] + + alias Explorer.Account.WatchlistAddress + + schema "account_watchlist_notifications" do + field(:amount, :decimal) + field(:block_number, :integer) + field(:direction, :string) + field(:method, :string) + field(:tx_fee, :decimal) + field(:type, :string) + field(:viewed_at, :integer) + field(:name, Explorer.Encrypted.Binary) + field(:subject, Explorer.Encrypted.Binary) + field(:subject_hash, Cloak.Ecto.SHA256) + + belongs_to(:watchlist_address, WatchlistAddress) + + field(:from_address_hash, Explorer.Encrypted.AddressHash) + field(:to_address_hash, Explorer.Encrypted.AddressHash) + field(:transaction_hash, Explorer.Encrypted.TransactionHash) + + field(:from_address_hash_hash, Cloak.Ecto.SHA256) + field(:to_address_hash_hash, Cloak.Ecto.SHA256) + field(:transaction_hash_hash, Cloak.Ecto.SHA256) + + timestamps() + end + + @doc false + def changeset(watchlist_notifications, attrs) do + watchlist_notifications + |> cast(attrs, [:amount, :direction, :name, :type, :method, :block_number, :tx_fee, :value, :decimals, :viewed_at]) + |> validate_required([ + :amount, + :direction, + :name, + :type, + :method, + :block_number, + :tx_fee, + :value, + :decimals, + :viewed_at + ]) + |> put_hashed_fields() + end + + defp put_hashed_fields(changeset) do + changeset + |> put_change(:from_address_hash_hash, hash_to_lower_case_string(get_field(changeset, :from_address_hash))) + |> put_change(:to_address_hash_hash, hash_to_lower_case_string(get_field(changeset, :to_address_hash))) + |> put_change(:transaction_hash_hash, hash_to_lower_case_string(get_field(changeset, :transaction_hash))) + |> put_change(:subject_hash, get_field(changeset, :subject)) + end +end diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index c42370c0e5..1fd3a6cd2e 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -43,6 +43,8 @@ defmodule Explorer.Application do base_children = [ Explorer.Repo, Explorer.Repo.Replica1, + Explorer.Repo.Account, + Explorer.Vault, Supervisor.child_spec({SpandexDatadog.ApiServer, datadog_opts()}, id: SpandexDatadog.ApiServer), Supervisor.child_spec({Task.Supervisor, name: Explorer.HistoryTaskSupervisor}, id: Explorer.HistoryTaskSupervisor), Supervisor.child_spec({Task.Supervisor, name: Explorer.MarketTaskSupervisor}, id: Explorer.MarketTaskSupervisor), @@ -65,7 +67,8 @@ defmodule Explorer.Application do con_cache_child_spec(RSK.cache_name(), ttl_check_interval: :timer.minutes(1), global_ttl: :timer.minutes(30)), Transactions, Accounts, - Uncles + Uncles, + {Redix, redix_opts()} ] children = base_children ++ configurable_children() @@ -174,4 +177,8 @@ defmodule Explorer.Application do id: {ConCache, name} ) end + + defp redix_opts do + {System.get_env("ACCOUNT_REDIS_URL") || "redis://127.0.0.1:6379", [name: :redix]} + end end diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 7b0c65c263..e80f66a4ba 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -6183,4 +6183,22 @@ defmodule Explorer.Chain do query |> Repo.one() end + + def is_address_hash_is_smart_contract?(nil), do: false + + def is_address_hash_is_smart_contract?(address_hash) do + with %Address{contract_code: bytecode} <- Repo.get_by(Address, hash: address_hash), + false <- is_nil(bytecode) do + true + else + _ -> + false + end + end + + def hash_to_lower_case_string(hash) do + hash + |> to_string() + |> String.downcase() + end end diff --git a/apps/explorer/lib/explorer/chain/address.ex b/apps/explorer/lib/explorer/chain/address.ex index 1a71d3e224..163dd66f8d 100644 --- a/apps/explorer/lib/explorer/chain/address.ex +++ b/apps/explorer/lib/explorer/chain/address.ex @@ -259,6 +259,12 @@ defmodule Explorer.Chain.Address do ) end + def fetched_coin_balance(address_hash) when not is_nil(address_hash) do + Address + |> where([address], address.hash == ^address_hash) + |> select([address], address.fetched_coin_balance) + end + defimpl String.Chars do @doc """ Uses `hash` as string representation, formatting it according to the eip-55 specification diff --git a/apps/explorer/lib/explorer/chain/import.ex b/apps/explorer/lib/explorer/chain/import.ex index b3aa19ac42..8640d47e3a 100644 --- a/apps/explorer/lib/explorer/chain/import.ex +++ b/apps/explorer/lib/explorer/chain/import.ex @@ -4,10 +4,13 @@ defmodule Explorer.Chain.Import do """ alias Ecto.Changeset + alias Explorer.Account.Notify alias Explorer.Chain.Events.Publisher alias Explorer.Chain.Import alias Explorer.Repo + require Logger + @stages [ Import.Stage.Addresses, Import.Stage.AddressReferencing, @@ -126,6 +129,7 @@ defmodule Explorer.Chain.Import do {:ok, valid_runner_option_pairs} <- validate_runner_options_pairs(runner_options_pairs), {:ok, runner_to_changes_list} <- runner_to_changes_list(valid_runner_option_pairs), {:ok, data} <- insert_runner_to_changes_list(runner_to_changes_list, options) do + Notify.async(data[:transactions]) Publisher.broadcast(data, Map.get(options, :broadcast, false)) {:ok, data} end diff --git a/apps/explorer/lib/explorer/chain/import/runner/block/second_degree_relations.ex b/apps/explorer/lib/explorer/chain/import/runner/block/second_degree_relations.ex index 18b8eb089c..014bef4a04 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/block/second_degree_relations.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/block/second_degree_relations.ex @@ -57,7 +57,7 @@ defmodule Explorer.Chain.Import.Runner.Block.SecondDegreeRelations do optional(:on_conflict) => Import.Runner.on_conflict(), required(:timeout) => timeout }) :: - {:ok, %{nephew_hash: Hash.Full.t(), uncle_hash: Hash.Full.t(), index: non_neg_integer()}} + {:ok, nil | %{nephew_hash: Hash.Full.t(), uncle_hash: Hash.Full.t(), index: non_neg_integer()}} | {:error, [Changeset.t()]} defp insert(repo, changes_list, %{timeout: timeout} = options) when is_atom(repo) and is_list(changes_list) do on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) diff --git a/apps/explorer/lib/explorer/chain/import/runner/token_transfers.ex b/apps/explorer/lib/explorer/chain/import/runner/token_transfers.ex index 3935fb53b5..2b39a32fd7 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/token_transfers.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/token_transfers.ex @@ -57,7 +57,7 @@ defmodule Explorer.Chain.Import.Runner.TokenTransfers do # Enforce TokenTransfer ShareLocks order (see docs: sharelocks.md) ordered_changes_list = Enum.sort_by(changes_list, &{&1.transaction_hash, &1.block_hash, &1.log_index}) - {:ok, _} = + {:ok, inserted} = Import.insert_changes_list( repo, ordered_changes_list, @@ -68,6 +68,8 @@ defmodule Explorer.Chain.Import.Runner.TokenTransfers do timeout: timeout, timestamps: timestamps ) + + {:ok, inserted} end defp default_on_conflict do diff --git a/apps/explorer/lib/explorer/chain/wei.ex b/apps/explorer/lib/explorer/chain/wei.ex index b77f71f90f..8fa3a69b22 100644 --- a/apps/explorer/lib/explorer/chain/wei.ex +++ b/apps/explorer/lib/explorer/chain/wei.ex @@ -266,3 +266,9 @@ defimpl Inspect, for: Explorer.Chain.Wei do "#Explorer.Chain.Wei<#{Decimal.to_string(wei.value)}>" end end + +defimpl Jason.Encoder, for: Explorer.Chain.Wei do + def encode(wei, _) do + Decimal.to_string(wei.value) + end +end diff --git a/apps/explorer/lib/explorer/encrypted/address_hash.ex b/apps/explorer/lib/explorer/encrypted/address_hash.ex new file mode 100644 index 0000000000..4518951298 --- /dev/null +++ b/apps/explorer/lib/explorer/encrypted/address_hash.ex @@ -0,0 +1,5 @@ +defmodule Explorer.Encrypted.AddressHash do + @moduledoc false + + use Explorer.Encrypted.Types.AddressHash, vault: Explorer.Vault +end diff --git a/apps/explorer/lib/explorer/encrypted/binary.ex b/apps/explorer/lib/explorer/encrypted/binary.ex new file mode 100644 index 0000000000..6de296ded6 --- /dev/null +++ b/apps/explorer/lib/explorer/encrypted/binary.ex @@ -0,0 +1,5 @@ +defmodule Explorer.Encrypted.Binary do + @moduledoc false + + use Cloak.Ecto.Binary, vault: Explorer.Vault +end diff --git a/apps/explorer/lib/explorer/encrypted/transaction_hash.ex b/apps/explorer/lib/explorer/encrypted/transaction_hash.ex new file mode 100644 index 0000000000..a783cb899b --- /dev/null +++ b/apps/explorer/lib/explorer/encrypted/transaction_hash.ex @@ -0,0 +1,5 @@ +defmodule Explorer.Encrypted.TransactionHash do + @moduledoc false + + use Explorer.Encrypted.Types.TransactionHash, vault: Explorer.Vault +end diff --git a/apps/explorer/lib/explorer/encrypted/types/address_hash.ex b/apps/explorer/lib/explorer/encrypted/types/address_hash.ex new file mode 100644 index 0000000000..f9ca332c53 --- /dev/null +++ b/apps/explorer/lib/explorer/encrypted/types/address_hash.ex @@ -0,0 +1,27 @@ +defmodule Explorer.Encrypted.Types.AddressHash do + @moduledoc """ + An `Ecto.Type` to encrypt address_hash fields. + """ + + @doc false + defmacro __using__(opts) do + opts = Keyword.merge(opts, vault: Keyword.fetch!(opts, :vault)) + + quote do + use Cloak.Ecto.Type, unquote(opts) + + def cast(value) do + Explorer.Chain.Hash.Address.cast(value) + end + + def after_decrypt(nil), do: nil + def after_decrypt(""), do: nil + def after_decrypt(:error), do: nil + + def after_decrypt(value) do + {:ok, address_hash} = Explorer.Chain.Hash.Address.cast(value) + address_hash + end + end + end +end diff --git a/apps/explorer/lib/explorer/encrypted/types/transaction_hash.ex b/apps/explorer/lib/explorer/encrypted/types/transaction_hash.ex new file mode 100644 index 0000000000..7c39a9aeca --- /dev/null +++ b/apps/explorer/lib/explorer/encrypted/types/transaction_hash.ex @@ -0,0 +1,27 @@ +defmodule Explorer.Encrypted.Types.TransactionHash do + @moduledoc """ + An `Ecto.Type` to encrypt transaction_hash fields. + """ + + @doc false + defmacro __using__(opts) do + opts = Keyword.merge(opts, vault: Keyword.fetch!(opts, :vault)) + + quote do + use Cloak.Ecto.Type, unquote(opts) + + def cast(value) do + Explorer.Chain.Hash.Full.cast(value) + end + + def after_decrypt(nil), do: nil + def after_decrypt(""), do: nil + def after_decrypt(:error), do: nil + + def after_decrypt(value) do + {:ok, transaction_hash} = Explorer.Chain.Hash.Full.cast(value) + transaction_hash + end + end + end +end diff --git a/apps/explorer/lib/explorer/env_var_translator.ex b/apps/explorer/lib/explorer/env_var_translator.ex new file mode 100644 index 0000000000..62f3da9003 --- /dev/null +++ b/apps/explorer/lib/explorer/env_var_translator.ex @@ -0,0 +1,24 @@ +defmodule Explorer.EnvVarTranslator do + @moduledoc """ + The module for transaformation of environment variables + """ + + alias Poison.Parser + + @spec map_array_env_var_to_list(atom()) :: list() + def map_array_env_var_to_list(config_name) do + env_var = Application.get_env(:block_scout_web, config_name) + + if env_var do + try do + env_var + |> Parser.parse!(%{keys: :atoms!}) + rescue + _ -> + [] + end + else + [] + end + end +end diff --git a/apps/explorer/lib/explorer/exchange_rates/token.ex b/apps/explorer/lib/explorer/exchange_rates/token.ex index 6521181c12..8aa6d3b721 100644 --- a/apps/explorer/lib/explorer/exchange_rates/token.ex +++ b/apps/explorer/lib/explorer/exchange_rates/token.ex @@ -30,6 +30,7 @@ defmodule Explorer.ExchangeRates.Token do volume_24h_usd: Decimal.t() } + @derive Jason.Encoder @enforce_keys ~w(available_supply total_supply btc_value id last_updated market_cap_usd name symbol usd_value volume_24h_usd)a defstruct ~w(available_supply total_supply btc_value id last_updated market_cap_usd name symbol usd_value volume_24h_usd)a diff --git a/apps/explorer/lib/explorer/mailer.ex b/apps/explorer/lib/explorer/mailer.ex new file mode 100644 index 0000000000..ba5c095714 --- /dev/null +++ b/apps/explorer/lib/explorer/mailer.ex @@ -0,0 +1,12 @@ +defmodule Explorer.Mailer do + @moduledoc """ + Base module for mail sending + + add in your module: + alias Explorer.Mailer + + and call + Mailer.deliver_now!(email) + """ + use Bamboo.Mailer, otp_app: :explorer +end diff --git a/apps/explorer/lib/explorer/repo.ex b/apps/explorer/lib/explorer/repo.ex index 14a48d9d33..c64db8b97d 100644 --- a/apps/explorer/lib/explorer/repo.ex +++ b/apps/explorer/lib/explorer/repo.ex @@ -128,10 +128,54 @@ defmodule Explorer.Repo do def replica, do: Explorer.Repo.Replica1 end + def account_repo, do: Explorer.Repo.Account + defmodule Replica1 do use Ecto.Repo, otp_app: :explorer, adapter: Ecto.Adapters.Postgres, read_only: true + + def init(_, opts) do + db_url = Application.get_env(:explorer, Explorer.Repo.Replica1)[:url] + repo_conf = Application.get_env(:explorer, Explorer.Repo.Replica1) + + merged = + %{url: db_url} + |> ConfigHelper.get_db_config() + |> Keyword.merge(repo_conf, fn + _key, v1, nil -> v1 + _key, nil, v2 -> v2 + _, _, v2 -> v2 + end) + + Application.put_env(:explorer, Explorer.Repo.Replica1, merged) + + {:ok, Keyword.put(opts, :url, db_url)} + end + end + + defmodule Account do + use Ecto.Repo, + otp_app: :explorer, + adapter: Ecto.Adapters.Postgres + + def init(_, opts) do + db_url = Application.get_env(:explorer, Explorer.Repo.Account)[:url] + repo_conf = Application.get_env(:explorer, Explorer.Repo.Account) + + merged = + %{url: db_url} + |> ConfigHelper.get_db_config() + |> Keyword.merge(repo_conf, fn + _key, v1, nil -> v1 + _key, nil, v2 -> v2 + _, _, v2 -> v2 + end) + + Application.put_env(:explorer, Explorer.Repo.Account, merged) + + {:ok, Keyword.put(opts, :url, db_url)} + end end end diff --git a/apps/explorer/lib/explorer/smart_contract/reader.ex b/apps/explorer/lib/explorer/smart_contract/reader.ex index b9f4410a1b..d83cdc2c4a 100644 --- a/apps/explorer/lib/explorer/smart_contract/reader.ex +++ b/apps/explorer/lib/explorer/smart_contract/reader.ex @@ -228,11 +228,7 @@ defmodule Explorer.SmartContract.Reader do [] _ -> - abi_with_method_id = get_abi_with_method_id(abi) - - abi_with_method_id - |> Enum.filter(&Helper.queriable_method?(&1)) - |> Enum.map(&fetch_current_value_from_blockchain(&1, abi_with_method_id, contract_address_hash, false)) + read_only_functions_from_abi(abi, contract_address_hash) end end @@ -244,13 +240,7 @@ defmodule Explorer.SmartContract.Reader do [] _ -> - implementation_abi_with_method_id = get_abi_with_method_id(implementation_abi) - - implementation_abi_with_method_id - |> Enum.filter(&Helper.queriable_method?(&1)) - |> Enum.map( - &fetch_current_value_from_blockchain(&1, implementation_abi_with_method_id, contract_address_hash, false) - ) + read_only_functions_from_abi(implementation_abi, contract_address_hash) end end @@ -266,10 +256,7 @@ defmodule Explorer.SmartContract.Reader do [] _ -> - implementation_abi_with_method_id = get_abi_with_method_id(implementation_abi) - - implementation_abi_with_method_id - |> Enum.filter(&Helper.read_with_wallet_method?(&1)) + read_functions_required_wallet_from_abi(implementation_abi) end end @@ -288,13 +275,29 @@ defmodule Explorer.SmartContract.Reader do [] _ -> - abi_with_method_id = get_abi_with_method_id(abi) - - abi_with_method_id - |> Enum.filter(&Helper.read_with_wallet_method?(&1)) + read_functions_required_wallet_from_abi(abi) end end + def read_only_functions_from_abi([_ | _] = abi, contract_address_hash) do + abi_with_method_id = get_abi_with_method_id(abi) + + abi_with_method_id + |> Enum.filter(&Helper.queriable_method?(&1)) + |> Enum.map(&fetch_current_value_from_blockchain(&1, abi_with_method_id, contract_address_hash, false)) + end + + def read_only_functions_from_abi(_, _), do: [] + + def read_functions_required_wallet_from_abi([_ | _] = abi) do + abi_with_method_id = get_abi_with_method_id(abi) + + abi_with_method_id + |> Enum.filter(&Helper.read_with_wallet_method?(&1)) + end + + def read_functions_required_wallet_from_abi(_), do: [] + def get_abi_with_method_id(abi) do abi |> Enum.map(fn method -> @@ -365,14 +368,16 @@ defmodule Explorer.SmartContract.Reader do @doc """ Method performs query of read functions of a smart contract. `type` could be :proxy or :reqular - if ethereumJSONRPC will return some errors it will represented as map + `from` is a address of a function caller """ - @spec query_function_with_names(Hash.t(), %{method_id: String.t(), args: [term()] | nil}, atom()) :: %{ - :names => [any()], - :output => [%{}] - } - def query_function_with_names(contract_address_hash, %{method_id: method_id, args: args}, type) do - outputs = query_function(contract_address_hash, %{method_id: method_id, args: args}, type, true) + @spec query_function_with_names( + Hash.t(), + %{method_id: String.t(), args: [term()] | nil}, + atom(), + String.t() + ) :: %{:names => [any()], :output => [%{}]} + def query_function_with_names(contract_address_hash, %{method_id: method_id, args: args}, type, from) do + outputs = query_function(contract_address_hash, %{method_id: method_id, args: args}, type, from, true) names = parse_names_from_abi(get_abi(contract_address_hash, type), method_id) %{output: outputs, names: names} end @@ -382,15 +387,17 @@ defmodule Explorer.SmartContract.Reader do `type` could be :proxy or :reqular `from` is a address of a function caller """ - @spec query_function_with_names( + @spec query_function_with_names_custom_abi( Hash.t(), %{method_id: String.t(), args: [term()] | nil}, - atom(), - String.t() + String.t(), + [%{}] ) :: %{:names => [any()], :output => [%{}]} - def query_function_with_names(contract_address_hash, %{method_id: method_id, args: args}, type, from) do - outputs = query_function(contract_address_hash, %{method_id: method_id, args: args}, type, from, true) - names = parse_names_from_abi(get_abi(contract_address_hash, type), method_id) + def query_function_with_names_custom_abi(contract_address_hash, %{method_id: method_id, args: args}, from, custom_abi) do + outputs = + query_function_with_custom_abi(contract_address_hash, %{method_id: method_id, args: args}, from, true, custom_abi) + + names = parse_names_from_abi(custom_abi, method_id) %{output: outputs, names: names} end @@ -432,7 +439,77 @@ defmodule Explorer.SmartContract.Reader do query_contract_and_link_outputs(contract_address_hash, args, from, abi, outputs, method_id, leave_error_as_map) end - defp proccess_abi(nil, _method_id), do: nil + @spec query_function_with_custom_abi( + String.t(), + %{method_id: String.t(), args: nil}, + String.t() | nil, + true | false, + [%{}] + ) :: [%{}] + def query_function_with_custom_abi( + contract_address_hash, + %{method_id: method_id, args: nil}, + from, + leave_error_as_map, + custom_abi + ) do + query_function_with_custom_abi( + contract_address_hash, + %{method_id: method_id, args: []}, + from, + leave_error_as_map, + custom_abi + ) + end + + @spec query_function_with_custom_abi( + Hash.t(), + %{method_id: String.t(), args: [term()]}, + String.t() | nil, + true | false, + [%{}] + ) :: [ + %{} + ] + def query_function_with_custom_abi( + contract_address_hash, + %{method_id: method_id, args: args}, + from, + leave_error_as_map, + custom_abi + ) do + query_function_with_custom_abi_inner(contract_address_hash, method_id, args, from, leave_error_as_map, custom_abi) + end + + @spec query_function_with_custom_abi_inner(Hash.t(), String.t(), [term()], String.t() | nil, true | false, [%{}]) :: [ + %{} + ] + defp query_function_with_custom_abi_inner( + contract_address_hash, + method_id, + args, + from, + leave_error_as_map, + custom_abi + ) do + parsed_abi = + custom_abi + |> ABI.parse_specification() + + %{outputs: outputs, method_id: method_id} = proccess_abi(parsed_abi, method_id) + + query_contract_and_link_outputs( + contract_address_hash, + args, + from, + custom_abi, + outputs, + method_id, + leave_error_as_map + ) + end + + defp proccess_abi([], _method_id), do: nil defp proccess_abi(abi, method_id) do function_object = find_function_by_method(abi, method_id) diff --git a/apps/explorer/lib/explorer/smart_contract/solidity/verifier.ex b/apps/explorer/lib/explorer/smart_contract/solidity/verifier.ex index 25ff8cf129..bf8e9e6780 100644 --- a/apps/explorer/lib/explorer/smart_contract/solidity/verifier.ex +++ b/apps/explorer/lib/explorer/smart_contract/solidity/verifier.ex @@ -525,9 +525,11 @@ defmodule Explorer.SmartContract.Solidity.Verifier do Enum.any?(abi, fn el -> el["type"] == "constructor" && el["inputs"] != [] end) end - defp parse_boolean("true"), do: true - defp parse_boolean("false"), do: false + def parse_boolean("true"), do: true + def parse_boolean("false"), do: false - defp parse_boolean(true), do: true - defp parse_boolean(false), do: false + def parse_boolean(true), do: true + def parse_boolean(false), do: false + + def parse_boolean(_), do: false end diff --git a/apps/explorer/lib/explorer/smart_contract/writer.ex b/apps/explorer/lib/explorer/smart_contract/writer.ex index ba231a6551..2d6c7484be 100644 --- a/apps/explorer/lib/explorer/smart_contract/writer.ex +++ b/apps/explorer/lib/explorer/smart_contract/writer.ex @@ -43,8 +43,10 @@ defmodule Explorer.SmartContract.Writer do (Helper.payable?(function) || Helper.nonpayable?(function)) end - defp filter_write_functions(abi) do + def filter_write_functions(abi) when is_list(abi) do abi |> Enum.filter(&write_function?(&1)) end + + def filter_write_functions(_), do: [] end diff --git a/apps/explorer/lib/explorer/tags/address_tag.ex b/apps/explorer/lib/explorer/tags/address_tag.ex new file mode 100644 index 0000000000..6174235608 --- /dev/null +++ b/apps/explorer/lib/explorer/tags/address_tag.ex @@ -0,0 +1,98 @@ +defmodule Explorer.Tags.AddressTag do + @moduledoc """ + Represents a Tag object. + """ + + use Explorer.Schema + + import Ecto.Changeset + + import Ecto.Query, + only: [ + from: 2 + ] + + alias Explorer.Chain.Address + alias Explorer.Repo + alias Explorer.Tags.{AddressTag, AddressToTag} + + @typedoc """ + * `:id` - id of Tag + * `:label` - Tag's label + * `:label` - Label's display name + """ + @type t :: %AddressTag{ + label: String.t() + } + + schema "address_tags" do + field(:label, :string) + field(:display_name, :string) + has_many(:addresses, Address, foreign_key: :hash) + has_many(:tag_id, AddressToTag, foreign_key: :id) + + timestamps() + end + + @required_attrs ~w(label display_name)a + + @doc false + def changeset(struct, params \\ %{}) do + struct + |> cast(params, @required_attrs) + |> validate_required(@required_attrs) + |> unique_constraint(:label, name: :address_tags_label_index) + end + + def set_tag(name, display_name) do + tag = get_tag(name) + + if tag do + tag + |> AddressTag.changeset(%{display_name: display_name}) + |> Repo.update() + else + %AddressTag{} + |> AddressTag.changeset(%{label: name, display_name: display_name}) + |> Repo.insert() + end + end + + def get_tag_id(nil), do: nil + + def get_tag_id(label) do + query = + from( + tag in AddressTag, + where: tag.label == ^label, + select: tag.id + ) + + query + |> Repo.one() + end + + def get_tag(nil), do: nil + + def get_tag(label) do + query = + from( + tag in AddressTag, + where: tag.label == ^label + ) + + query + |> Repo.one() + end + + def get_all_tags do + query = + from( + tag in AddressTag, + select: tag + ) + + query + |> Repo.all() + end +end diff --git a/apps/explorer/lib/explorer/tags/address_tag_cataloger.ex b/apps/explorer/lib/explorer/tags/address_tag_cataloger.ex new file mode 100644 index 0000000000..3256f3f3c2 --- /dev/null +++ b/apps/explorer/lib/explorer/tags/address_tag_cataloger.ex @@ -0,0 +1,198 @@ +defmodule Explorer.Tags.AddressTag.Cataloger do + @moduledoc """ + Actualizes address tags. + """ + + use GenServer + + alias Explorer.EnvVarTranslator + alias Explorer.Tags.{AddressTag, AddressToTag} + alias Explorer.Validator.MetadataRetriever + alias Poison.Parser + + def start_link(_) do + GenServer.start_link(__MODULE__, :ok, name: __MODULE__) + end + + @impl GenServer + def init(args) do + send(self(), :fetch_tags) + + {:ok, args} + end + + @impl GenServer + def handle_info(:fetch_tags, state) do + # set tag for every chainlink oracle + create_chainlink_oracle_tag() + + create_new_tags() + + send(self(), :bind_addresses) + + {:noreply, state} + end + + def handle_info(:bind_addresses, state) do + # set validator tag + set_validator_tag() + + # set amb bridge mediators tag + set_amb_mediators_tag() + + # set omni bridge tag + set_omni_tag() + + # set L2 tag + set_l2_tag() + + all_tags = AddressTag.get_all_tags() + + all_tags + |> Enum.each(fn %{label: tag_name} -> + if tag_name !== "validator" && tag_name !== "amb bridge mediators" && tag_name !== "omni bridge" && + tag_name !== "l2" && !String.contains?(tag_name, "chainlink") do + env_var_name = "CUSTOM_CONTRACT_ADDRESSES_#{tag_name_to_env_var_part(tag_name)}" + set_tag_for_env_var_multiple_addresses(env_var_name, tag_name) + end + end) + + {:noreply, state} + end + + defp tag_name_to_env_var_part(tag_name) do + tag_name + |> String.upcase() + |> String.replace(" ", "_") + |> String.replace(".", "_") + end + + def create_chainlink_oracle_tag do + chainlink_oracles_config = Application.get_env(:block_scout_web, :chainlink_oracles) + + if chainlink_oracles_config do + chainlink_oracles_config + |> Parser.parse!(%{keys: :atoms!}) + |> Enum.each(fn %{:name => name, :address => address} -> + chainlink_tag_name = "chainlink oracle #{String.downcase(name)}" + AddressTag.set_tag(chainlink_tag_name, chainlink_tag_name) + tag_id = AddressTag.get_tag_id(chainlink_tag_name) + AddressToTag.set_tag_to_addresses(tag_id, [address]) + end) + end + end + + defp set_tag_for_multiple_env_var_addresses(env_vars, tag) do + addresses = + env_vars + |> Enum.map(fn env_var -> + env_var + |> System.get_env("") + |> String.downcase() + end) + + tag_id = AddressTag.get_tag_id(tag) + AddressToTag.set_tag_to_addresses(tag_id, addresses) + end + + defp set_tag_for_multiple_env_var_array_addresses(env_vars, tag) do + addresses = + env_vars + |> Enum.reduce([], fn env_var, acc -> + env_var + |> System.get_env("") + |> String.split(",") + |> Enum.reduce(acc, fn env_var, acc_inner -> + addr = + env_var + |> String.downcase() + + [addr | acc_inner] + end) + end) + + tag_id = AddressTag.get_tag_id(tag) + AddressToTag.set_tag_to_addresses(tag_id, addresses) + end + + def create_new_tags do + tags = EnvVarTranslator.map_array_env_var_to_list(:new_tags) + + tags + |> Enum.each(fn %{tag: tag_name, title: tag_display_name} -> + AddressTag.set_tag(tag_name, tag_display_name) + end) + end + + defp set_tag_for_env_var_multiple_addresses(env_var, tag) do + addresses = env_var_string_array_to_list(env_var) + + tag_id = AddressTag.get_tag_id(tag) + AddressToTag.set_tag_to_addresses(tag_id, addresses) + end + + defp env_var_string_array_to_list(env_var_array_string) do + env_var = + env_var_array_string + |> System.get_env(nil) + + if env_var do + env_var + |> String.split(",") + |> Enum.map(fn env_var_array_string_item -> + env_var_array_string_item + |> String.downcase() + end) + else + [] + end + end + + defp set_validator_tag do + validators = MetadataRetriever.fetch_validators_list() + tag_id = AddressTag.get_tag_id("validator") + AddressToTag.set_tag_to_addresses(tag_id, validators) + end + + defp set_amb_mediators_tag do + set_tag_for_multiple_env_var_array_addresses( + ["AMB_BRIDGE_MEDIATORS", "CUSTOM_CONTRACT_ADDRESSES_AMB_BRIDGE_MEDIATORS"], + "amb bridge mediators" + ) + end + + defp set_omni_tag do + set_tag_for_multiple_env_var_addresses( + ["ETH_OMNI_BRIDGE_MEDIATOR", "BSC_OMNI_BRIDGE_MEDIATOR", "POA_OMNI_BRIDGE_MEDIATOR"], + "omni bridge" + ) + end + + defp set_l2_tag do + set_tag_for_multiple_env_var_addresses(["CUSTOM_CONTRACT_ADDRESSES_AOX"], "l2") + end + + def set_chainlink_oracle_tag do + chainlink_oracles = chainlink_oracles_list() + + tag_id = AddressTag.get_tag_id("chainlink oracle") + AddressToTag.set_tag_to_addresses(tag_id, chainlink_oracles) + end + + defp chainlink_oracles_list do + chainlink_oracles_config = Application.get_env(:block_scout_web, :chainlink_oracles) + + if chainlink_oracles_config do + try do + chainlink_oracles_config + |> Parser.parse!(%{keys: :atoms!}) + |> Enum.map(fn %{:name => _name, :address => address} -> address end) + rescue + _ -> + [] + end + else + [] + end + end +end diff --git a/apps/explorer/lib/explorer/tags/address_to_tag.ex b/apps/explorer/lib/explorer/tags/address_to_tag.ex new file mode 100644 index 0000000000..26468cf29a --- /dev/null +++ b/apps/explorer/lib/explorer/tags/address_to_tag.ex @@ -0,0 +1,145 @@ +defmodule Explorer.Tags.AddressToTag do + @moduledoc """ + Represents ann Address to Tag relation. + """ + + use Explorer.Schema + + import Ecto.Changeset + + alias Explorer.{Chain, Repo} + alias Explorer.Chain.{Address, Hash} + alias Explorer.Tags.{AddressTag, AddressToTag} + + # Notation.import_types(BlockScoutWeb.Schema.Types) + + @typedoc """ + * `:tag_id` - id of Tag + * `:address_hash` - hash of Address + """ + @type t :: %AddressToTag{ + tag_id: Decimal.t(), + address_hash: Hash.Address.t() + } + + schema "address_to_tags" do + belongs_to( + :tag, + AddressTag, + foreign_key: :tag_id, + references: :id, + type: :integer + ) + + belongs_to( + :address, + Address, + foreign_key: :address_hash, + references: :hash, + type: Hash.Address + ) + + timestamps() + end + + @required_attrs ~w(address_hash tag_id)a + + @doc false + def changeset(struct, params \\ %{}) do + struct + |> cast(params, @required_attrs) + |> unique_constraint([:address_hash, :tag_id], name: :address_to_tags_address_hash_tag_id_index) + end + + defp get_address_hashes_mapped_to_tag(nil), do: nil + + defp get_address_hashes_mapped_to_tag(tag_id) do + query = + from( + att in AddressToTag, + where: att.tag_id == ^tag_id, + select: att.address_hash + ) + + query + |> Repo.all() + end + + def set_tag_to_addresses(tag_id, address_hash_string_list) do + current_address_hashes = get_address_hashes_mapped_to_tag(tag_id) + + if current_address_hashes do + current_address_hashes_strings = + current_address_hashes + |> Enum.map(fn address_hash -> + "0x" <> Base.encode16(address_hash.bytes, case: :lower) + end) + + current_address_hashes_strings_tuples = MapSet.new(current_address_hashes_strings) + new_address_hashes_strings_tuples = MapSet.new(address_hash_string_list) + + all_tuples = MapSet.union(current_address_hashes_strings_tuples, new_address_hashes_strings_tuples) + + addresses_to_delete = + all_tuples + |> MapSet.difference(new_address_hashes_strings_tuples) + |> MapSet.to_list() + + addresses_to_add = + all_tuples + |> MapSet.difference(current_address_hashes_strings_tuples) + |> MapSet.to_list() + + changeset_to_add_list = + addresses_to_add + |> Enum.map(fn address_hash_string -> + with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), + :ok <- Chain.check_address_exists(address_hash) do + %{ + tag_id: tag_id, + address_hash: address_hash, + inserted_at: DateTime.utc_now(), + updated_at: DateTime.utc_now() + } + else + _ -> + nil + end + end) + |> Enum.filter(&(!is_nil(&1))) + + if Enum.count(addresses_to_delete) > 0 do + delete_query_base = + from( + att in AddressToTag, + where: att.tag_id == ^tag_id + ) + + delete_query = + delete_query_base + |> where_addresses(addresses_to_delete) + + Repo.delete_all(delete_query) + end + + Repo.insert_all(AddressToTag, changeset_to_add_list, + on_conflict: :nothing, + conflict_target: [:address_hash, :tag_id] + ) + end + end + + defp where_addresses(query, addresses_to_delete) do + addresses_to_delete + |> Enum.reduce(query, fn address_hash_string, acc -> + case Chain.string_to_address_hash(address_hash_string) do + {:ok, address_hash} -> + acc + |> where(address_hash: ^address_hash) + + _ -> + acc + end + end) + end +end diff --git a/apps/explorer/lib/explorer/third_party_integrations/airtable.ex b/apps/explorer/lib/explorer/third_party_integrations/airtable.ex new file mode 100644 index 0000000000..a2aba9c664 --- /dev/null +++ b/apps/explorer/lib/explorer/third_party_integrations/airtable.ex @@ -0,0 +1,56 @@ +defmodule Explorer.ThirdPartyIntegrations.AirTable do + @moduledoc """ + Module is responsible for submitting requests for public tags to AirTable + """ + require Logger + + alias Ecto.Changeset + alias Explorer.Account.PublicTagsRequest + alias Explorer.Repo + alias HTTPoison.Response + + def submit({:ok, %PublicTagsRequest{} = new_request} = input) do + if Mix.env() == :test do + new_request + |> PublicTagsRequest.changeset(%{request_id: "123"}) + |> Repo.account_repo().update() + + input + else + api_key = Application.get_env(:explorer, __MODULE__)[:api_key] + headers = [{"Authorization", "Bearer #{api_key}"}, {"Content-Type", "application/json"}] + url = Application.get_env(:explorer, __MODULE__)[:table_url] + + body = %{ + "typecast" => true, + "records" => [%{"fields" => PublicTagsRequest.to_map(new_request)}] + } + + request = HTTPoison.post(url, Jason.encode!(body), headers, []) + + case request do + {:ok, %Response{body: body, status_code: 200}} -> + request_id = Enum.at(Jason.decode!(body)["records"], 0)["fields"]["request_id"] + + new_request + |> PublicTagsRequest.changeset(%{request_id: request_id}) + |> Repo.account_repo().update() + + input + + error -> + Logger.error(fn -> ["Error while submitting AirTable entry", inspect(error)] end) + + {:error, + %{ + (%PublicTagsRequest{} + |> PublicTagsRequest.changeset_without_constraints(PublicTagsRequest.to_map(new_request)) + |> Changeset.add_error(:full_name, "AirTable error. Please try again later")) + | action: :insert + }} + end + end + end + + def submit(error), do: error +end diff --git a/apps/explorer/lib/explorer/validator/metadata_retriever.ex b/apps/explorer/lib/explorer/validator/metadata_retriever.ex index 99fb83db79..3a5d13eefa 100644 --- a/apps/explorer/lib/explorer/validator/metadata_retriever.ex +++ b/apps/explorer/lib/explorer/validator/metadata_retriever.ex @@ -15,7 +15,7 @@ defmodule Explorer.Validator.MetadataRetriever do end) end - defp fetch_validators_list do + def fetch_validators_list do # b7ab4db5 = keccak256(getValidators()) case Reader.query_contract( config(:validators_contract_address), diff --git a/apps/explorer/lib/explorer/vault.ex b/apps/explorer/lib/explorer/vault.ex new file mode 100644 index 0000000000..70fc1ed17d --- /dev/null +++ b/apps/explorer/lib/explorer/vault.ex @@ -0,0 +1,22 @@ +defmodule Explorer.Vault do + @moduledoc """ + Module responsible for encrypt/decrypt GenServer initialization + """ + use Cloak.Vault, otp_app: :explorer + + @impl GenServer + def init(config) do + config = + Keyword.put(config, :ciphers, + default: {Cloak.Ciphers.AES.GCM, tag: "AES.GCM.V1", key: decode_env!("ACCOUNT_CLOAK_KEY")} + ) + + {:ok, config} + end + + defp decode_env!(var) do + env = if Mix.env() == :test, do: "+fh7IElJfA61+vMMw8rW9SBJFHmhVL1DLpKE22qUJgw=", else: System.get_env(var) + + Base.decode64!(env || "") + end +end diff --git a/apps/explorer/lib/release_tasks.ex b/apps/explorer/lib/release_tasks.ex index 8d5a1b486c..1fcdc7fca8 100644 --- a/apps/explorer/lib/release_tasks.ex +++ b/apps/explorer/lib/release_tasks.ex @@ -14,7 +14,7 @@ defmodule Explorer.ReleaseTasks do :ecto_sql ] - @repos Application.compile_env(:blockscout, :ecto_repos, [Explorer.Repo]) + @repos Application.compile_env(:blockscout, :ecto_repos, [Explorer.Repo, Explorer.Repo.Account]) def create_and_migrate do start_services() diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs index 8f27ecc6b7..b9217ddd0a 100644 --- a/apps/explorer/mix.exs +++ b/apps/explorer/mix.exs @@ -24,7 +24,8 @@ defmodule Explorer.Mixfile do dialyzer: :test ], start_permanent: Mix.env() == :prod, - version: "4.1.8" + version: "4.1.8", + xref: [exclude: [BlockScoutWeb.WebRouter.Helpers]] ] end @@ -56,6 +57,8 @@ defmodule Explorer.Mixfile do # Type `mix help deps` for examples and options. defp deps do [ + {:bamboo, "~> 2.2.0"}, + {:mime, "~> 1.4"}, {:bcrypt_elixir, "~> 3.0"}, # benchmark optimizations {:benchee, "~> 1.1.0", only: :test}, @@ -87,7 +90,7 @@ defmodule Explorer.Mixfile do {:mock, "~> 0.3.0", only: [:test], runtime: false}, {:mox, "~> 1.0", only: [:test]}, {:phoenix_html, "== 3.0.4"}, - {:poison, "~> 5.0.0"}, + {:poison, "~> 4.0.1"}, {:nimble_csv, "~> 1.1"}, {:postgrex, ">= 0.0.0"}, # For compatibility with `prometheus_process_collector`, which hasn't been updated yet @@ -110,7 +113,9 @@ defmodule Explorer.Mixfile do {:timex, "~> 3.7.1"}, {:con_cache, "~> 1.0"}, {:tesla, "~> 1.4.4"}, - {:cbor, "~> 1.0"} + {:cbor, "~> 1.0"}, + {:cloak_ecto, "~> 1.2.0"}, + {:redix, "~> 1.1"} ] end diff --git a/apps/explorer/priv/account/migrations/20211031164954_create_account_identities.exs b/apps/explorer/priv/account/migrations/20211031164954_create_account_identities.exs new file mode 100644 index 0000000000..fb571247f0 --- /dev/null +++ b/apps/explorer/priv/account/migrations/20211031164954_create_account_identities.exs @@ -0,0 +1,13 @@ +defmodule Explorer.Repo.Account.Migrations.CreateAccountIdentities do + use Ecto.Migration + + def change do + create table(:account_identities) do + add(:uid, :string) + + timestamps() + end + + create(unique_index(:account_identities, [:uid])) + end +end diff --git a/apps/explorer/priv/account/migrations/20211105114502_create_account_watchlists.exs b/apps/explorer/priv/account/migrations/20211105114502_create_account_watchlists.exs new file mode 100644 index 0000000000..2e7c93f3cc --- /dev/null +++ b/apps/explorer/priv/account/migrations/20211105114502_create_account_watchlists.exs @@ -0,0 +1,14 @@ +defmodule Explorer.Repo.Account.Migrations.CreateAccountWatchlists do + use Ecto.Migration + + def change do + create table(:account_watchlists) do + add(:name, :string, default: "default") + add(:identity_id, references(:account_identities, on_delete: :delete_all)) + + timestamps() + end + + create(index(:account_watchlists, [:identity_id])) + end +end diff --git a/apps/explorer/priv/account/migrations/20211105130907_create_account_watchlist_addresses.exs b/apps/explorer/priv/account/migrations/20211105130907_create_account_watchlist_addresses.exs new file mode 100644 index 0000000000..ef51c1850e --- /dev/null +++ b/apps/explorer/priv/account/migrations/20211105130907_create_account_watchlist_addresses.exs @@ -0,0 +1,28 @@ +defmodule Explorer.Repo.Account.Migrations.CreateAccountWatchlistAddresses do + use Ecto.Migration + + def change do + create table(:account_watchlist_addresses) do + add(:name, :string) + add(:address_hash, :bytea, null: false) + add(:watchlist_id, references(:account_watchlists, on_delete: :delete_all)) + add(:watch_coin_input, :boolean, default: true) + add(:watch_coin_output, :boolean, default: true) + add(:watch_erc_20_input, :boolean, default: true) + add(:watch_erc_20_output, :boolean, default: true) + add(:watch_erc_721_input, :boolean, default: true) + add(:watch_erc_721_output, :boolean, default: true) + add(:watch_erc_1155_input, :boolean, default: true) + add(:watch_erc_1155_output, :boolean, default: true) + add(:notify_email, :boolean, default: true) + add(:notify_epns, :boolean, default: false) + add(:notify_feed, :boolean, default: true) + add(:notify_inapp, :boolean, default: false) + + timestamps() + end + + create(index(:account_watchlist_addresses, [:watchlist_id])) + create(index(:account_watchlist_addresses, [:address_hash])) + end +end diff --git a/apps/explorer/priv/account/migrations/20211127212336_create_account_watchlist_notifications.exs b/apps/explorer/priv/account/migrations/20211127212336_create_account_watchlist_notifications.exs new file mode 100644 index 0000000000..7d4af566fe --- /dev/null +++ b/apps/explorer/priv/account/migrations/20211127212336_create_account_watchlist_notifications.exs @@ -0,0 +1,31 @@ +defmodule Explorer.Repo.Account.Migrations.CreateAccountWatchlistNotifications do + use Ecto.Migration + + def change do + create table(:account_watchlist_notifications) do + add(:watchlist_address_id, references(:account_watchlist_addresses, on_delete: :delete_all)) + + add(:transaction_hash, :bytea) + + add(:from_address_hash, :bytea) + + add(:to_address_hash, :bytea) + + add(:direction, :string) + add(:name, :string) + add(:type, :string) + add(:method, :string) + add(:block_number, :integer) + add(:amount, :decimal) + add(:tx_fee, :decimal) + add(:viewed_at, :utc_datetime_usec) + + timestamps(null: false, type: :utc_datetime_usec) + end + + create(index(:account_watchlist_notifications, [:watchlist_address_id])) + create(index(:account_watchlist_notifications, [:transaction_hash])) + create(index(:account_watchlist_notifications, [:from_address_hash])) + create(index(:account_watchlist_notifications, [:to_address_hash])) + end +end diff --git a/apps/explorer/priv/account/migrations/20211205220414_add_email_and_name_to_account_identity.exs b/apps/explorer/priv/account/migrations/20211205220414_add_email_and_name_to_account_identity.exs new file mode 100644 index 0000000000..0633e0dbae --- /dev/null +++ b/apps/explorer/priv/account/migrations/20211205220414_add_email_and_name_to_account_identity.exs @@ -0,0 +1,10 @@ +defmodule Explorer.Repo.Account.Migrations.AddEmailToAccountIdentity do + use Ecto.Migration + + def change do + alter table(:account_identities) do + add(:email, :string) + add(:name, :string) + end + end +end diff --git a/apps/explorer/priv/account/migrations/20220212222222_create_account_tag_addresses.exs b/apps/explorer/priv/account/migrations/20220212222222_create_account_tag_addresses.exs new file mode 100644 index 0000000000..df0074041e --- /dev/null +++ b/apps/explorer/priv/account/migrations/20220212222222_create_account_tag_addresses.exs @@ -0,0 +1,17 @@ +defmodule Explorer.Repo.Account.Migrations.CreateAccountTagAddresses do + use Ecto.Migration + + def change do + create table(:account_tag_addresses) do + add(:name, :string) + add(:identity_id, references(:account_identities, on_delete: :delete_all)) + + add(:address_hash, :bytea, null: false) + + timestamps() + end + + create(index(:account_tag_addresses, [:identity_id])) + create(index(:account_tag_addresses, [:address_hash])) + end +end diff --git a/apps/explorer/priv/account/migrations/20220313133333_create_account_tag_transactions.exs b/apps/explorer/priv/account/migrations/20220313133333_create_account_tag_transactions.exs new file mode 100644 index 0000000000..ba6fa338b9 --- /dev/null +++ b/apps/explorer/priv/account/migrations/20220313133333_create_account_tag_transactions.exs @@ -0,0 +1,17 @@ +defmodule Explorer.Repo.Account.Migrations.CreateAccountTagTransactions do + use Ecto.Migration + + def change do + create table(:account_tag_transactions) do + add(:name, :string) + add(:identity_id, references(:account_identities, on_delete: :delete_all)) + + add(:tx_hash, :bytea, null: false) + + timestamps() + end + + create(index(:account_tag_transactions, [:identity_id])) + create(index(:account_tag_transactions, [:tx_hash])) + end +end diff --git a/apps/explorer/priv/account/migrations/20220324213333_add_subject_to_watchlist_notifications.exs b/apps/explorer/priv/account/migrations/20220324213333_add_subject_to_watchlist_notifications.exs new file mode 100644 index 0000000000..6ff8a359ba --- /dev/null +++ b/apps/explorer/priv/account/migrations/20220324213333_add_subject_to_watchlist_notifications.exs @@ -0,0 +1,9 @@ +defmodule Explorer.Repo.Account.Migrations.AddSubjectToWatchlistNotifications do + use Ecto.Migration + + def change do + alter table(:account_watchlist_notifications) do + add(:subject, :string, null: true) + end + end +end diff --git a/apps/explorer/priv/account/migrations/20220407134152_add_api_keys_and_plans_tables.exs b/apps/explorer/priv/account/migrations/20220407134152_add_api_keys_and_plans_tables.exs new file mode 100644 index 0000000000..11f23ffd61 --- /dev/null +++ b/apps/explorer/priv/account/migrations/20220407134152_add_api_keys_and_plans_tables.exs @@ -0,0 +1,33 @@ +defmodule Explorer.Repo.Account.Migrations.AddApiKeysAndPlansTables do + use Ecto.Migration + + def change do + create table(:account_api_plans, primary_key: false) do + add(:id, :serial, null: false, primary_key: true) + add(:max_req_per_second, :smallint) + add(:name, :string, null: false) + + timestamps() + end + + create(unique_index(:account_api_plans, [:id, :max_req_per_second, :name])) + + execute( + "INSERT INTO account_api_plans (id, max_req_per_second, name, inserted_at, updated_at) VALUES (1, 10, 'Free Plan', NOW(), NOW());" + ) + + create table(:account_api_keys, primary_key: false) do + add(:identity_id, references(:account_identities, column: :id, on_delete: :delete_all), null: false) + add(:name, :string, null: false) + add(:value, :uuid, null: false, primary_key: true) + + timestamps() + end + + alter table(:account_identities) do + add(:plan_id, references(:account_api_plans, column: :id), default: 1) + end + + create(index(:account_api_keys, [:identity_id])) + end +end diff --git a/apps/explorer/priv/account/migrations/20220510094118_add_custom_abis_table.exs b/apps/explorer/priv/account/migrations/20220510094118_add_custom_abis_table.exs new file mode 100644 index 0000000000..a248c79458 --- /dev/null +++ b/apps/explorer/priv/account/migrations/20220510094118_add_custom_abis_table.exs @@ -0,0 +1,18 @@ +defmodule Explorer.Repo.Account.Migrations.AddCustomAbisTable do + use Ecto.Migration + + def change do + create table(:account_custom_abis, primary_key: false) do + add(:id, :serial, null: false, primary_key: true) + add(:identity_id, references(:account_identities, column: :id, on_delete: :delete_all), null: false) + add(:name, :string, null: false) + add(:address_hash, :bytea, null: false) + add(:abi, :jsonb, null: false) + + timestamps() + end + + create(unique_index(:account_custom_abis, [:identity_id, :address_hash])) + create(index(:account_custom_abis, [:identity_id])) + end +end diff --git a/apps/explorer/priv/account/migrations/20220606194836_add_account_public_tags_requests.exs b/apps/explorer/priv/account/migrations/20220606194836_add_account_public_tags_requests.exs new file mode 100644 index 0000000000..dd0a9896b0 --- /dev/null +++ b/apps/explorer/priv/account/migrations/20220606194836_add_account_public_tags_requests.exs @@ -0,0 +1,23 @@ +defmodule Explorer.Repo.Account.Migrations.AddAccountPublicTagsRequests do + use Ecto.Migration + + def change do + create table(:account_public_tags_requests) do + add(:identity_id, references(:account_identities)) + add(:full_name, :string) + add(:email, :string) + add(:company, :string) + add(:website, :string) + add(:tags, :string) + add(:addresses, :text) + add(:description, :text) + add(:additional_comment, :string) + add(:request_type, :string) + add(:is_owner, :boolean) + add(:remove_reason, :text) + add(:request_id, :string) + + timestamps() + end + end +end diff --git a/apps/explorer/priv/account/migrations/20220620182600_add_account_identity_fields.exs b/apps/explorer/priv/account/migrations/20220620182600_add_account_identity_fields.exs new file mode 100644 index 0000000000..1c7b13535f --- /dev/null +++ b/apps/explorer/priv/account/migrations/20220620182600_add_account_identity_fields.exs @@ -0,0 +1,10 @@ +defmodule Explorer.Repo.Account.Migrations.AddAccountIdentityFields do + use Ecto.Migration + + def change do + alter table("account_identities") do + add(:nickname, :string, null: true) + add(:avatar, :text, null: true) + end + end +end diff --git a/apps/explorer/priv/account/migrations/20220624142547_add_unique_constraints.exs b/apps/explorer/priv/account/migrations/20220624142547_add_unique_constraints.exs new file mode 100644 index 0000000000..53293983d4 --- /dev/null +++ b/apps/explorer/priv/account/migrations/20220624142547_add_unique_constraints.exs @@ -0,0 +1,9 @@ +defmodule Explorer.Repo.Account.Migrations.AddUniqueConstraints do + use Ecto.Migration + + def change do + create(unique_index(:account_tag_addresses, [:identity_id, :address_hash])) + create(unique_index(:account_tag_transactions, [:identity_id, :tx_hash])) + create(unique_index(:account_watchlist_addresses, [:watchlist_id, :address_hash])) + end +end diff --git a/apps/explorer/priv/account/migrations/20220705195240_migrate_public_tags_addresses_to_array.exs b/apps/explorer/priv/account/migrations/20220705195240_migrate_public_tags_addresses_to_array.exs new file mode 100644 index 0000000000..1282e2166c --- /dev/null +++ b/apps/explorer/priv/account/migrations/20220705195240_migrate_public_tags_addresses_to_array.exs @@ -0,0 +1,34 @@ +defmodule Explorer.Repo.Account.Migrations.MigratePublicTagsAddressesToArray do + use Ecto.Migration + + def change do + alter table(:account_public_tags_requests) do + add(:addresses_duplicate, {:array, :bytea}) + end + + execute(""" + CREATE OR REPLACE FUNCTION convert(text[]) RETURNS bytea[] AS $$ + DECLARE + s bytea[] := ARRAY[]::bytea[]; + x text; + BEGIN + FOREACH x IN ARRAY $1 + LOOP + s := array_append(s, decode(replace(x, '0x', ''), 'hex')); + END LOOP; + RETURN s; + END; + $$ LANGUAGE plpgsql; + """) + + execute(""" + UPDATE account_public_tags_requests set addresses_duplicate = convert(string_to_array(addresses, ';')) + """) + + alter table(:account_public_tags_requests) do + remove(:addresses) + end + + rename(table(:account_public_tags_requests), :addresses_duplicate, to: :addresses) + end +end diff --git a/apps/explorer/priv/account/migrations/20220706114430_encrypt_account_data.exs b/apps/explorer/priv/account/migrations/20220706114430_encrypt_account_data.exs new file mode 100644 index 0000000000..0328025982 --- /dev/null +++ b/apps/explorer/priv/account/migrations/20220706114430_encrypt_account_data.exs @@ -0,0 +1,60 @@ +defmodule Explorer.Repo.Account.Migrations.EncryptAccountData do + use Ecto.Migration + + def change do + alter table(:account_identities) do + add(:encrypted_uid, :binary) + add(:uid_hash, :binary) + add(:encrypted_email, :binary) + add(:encrypted_name, :binary) + add(:encrypted_nickname, :binary, null: true) + add(:encrypted_avatar, :binary, null: true) + end + + # unused because we dont have personal watchlists, only autogenerated `default` for each identity + # alter table(:account_watchlists) do + # add(:encrypted_name, :binary) + # end + + alter table(:account_custom_abis) do + add(:address_hash_hash, :binary) + add(:encrypted_address_hash, :binary) + add(:encrypted_name, :binary) + end + + alter table(:account_tag_addresses) do + add(:address_hash_hash, :binary) + add(:encrypted_name, :binary) + add(:encrypted_address_hash, :binary) + end + + alter table(:account_tag_transactions) do + add(:tx_hash_hash, :binary) + add(:encrypted_name, :binary) + add(:encrypted_tx_hash, :binary) + end + + alter table(:account_watchlist_addresses) do + add(:address_hash_hash, :binary) + add(:encrypted_name, :binary) + add(:encrypted_address_hash, :binary) + end + + alter table(:account_watchlist_notifications) do + add(:encrypted_name, :binary) + add(:encrypted_subject, :binary, null: true) + add(:encrypted_from_address_hash, :binary) + add(:encrypted_to_address_hash, :binary) + add(:encrypted_transaction_hash, :binary) + add(:subject_hash, :binary, null: true) + add(:from_address_hash_hash, :binary, null: true) + add(:to_address_hash_hash, :binary, null: true) + add(:transaction_hash_hash, :binary, null: true) + end + + alter table(:account_public_tags_requests) do + add(:encrypted_email, :binary) + add(:encrypted_full_name, :binary) + end + end +end diff --git a/apps/explorer/priv/account/migrations/20220706153506_remove_unencrypted_fields.exs b/apps/explorer/priv/account/migrations/20220706153506_remove_unencrypted_fields.exs new file mode 100644 index 0000000000..b887ac9f79 --- /dev/null +++ b/apps/explorer/priv/account/migrations/20220706153506_remove_unencrypted_fields.exs @@ -0,0 +1,79 @@ +defmodule Explorer.Repo.Account.Migrations.RemoveUnencryptedFields do + use Ecto.Migration + + def change do + alter table(:account_identities) do + remove(:uid) + remove(:email) + remove(:name) + remove(:nickname) + remove(:avatar) + end + + rename(table(:account_identities), :encrypted_uid, to: :uid) + rename(table(:account_identities), :encrypted_email, to: :email) + rename(table(:account_identities), :encrypted_name, to: :name) + rename(table(:account_identities), :encrypted_nickname, to: :nickname) + rename(table(:account_identities), :encrypted_avatar, to: :avatar) + + # unused because we dont have personal watchlists, only autogenerated `default` for each identity + # alter table(:account_watchlists) do + # remove(:name) + # end + # rename(table(:account_watchlists), :encrypted_name, to: :name) + + alter table(:account_custom_abis) do + remove(:address_hash) + remove(:name) + end + + rename(table(:account_custom_abis), :encrypted_address_hash, to: :address_hash) + rename(table(:account_custom_abis), :encrypted_name, to: :name) + + alter table(:account_tag_addresses) do + remove(:address_hash) + remove(:name) + end + + rename(table(:account_tag_addresses), :encrypted_address_hash, to: :address_hash) + rename(table(:account_tag_addresses), :encrypted_name, to: :name) + + alter table(:account_tag_transactions) do + remove(:tx_hash) + remove(:name) + end + + rename(table(:account_tag_transactions), :encrypted_tx_hash, to: :tx_hash) + rename(table(:account_tag_transactions), :encrypted_name, to: :name) + + alter table(:account_watchlist_addresses) do + remove(:address_hash) + remove(:name) + end + + rename(table(:account_watchlist_addresses), :encrypted_address_hash, to: :address_hash) + rename(table(:account_watchlist_addresses), :encrypted_name, to: :name) + + alter table(:account_watchlist_notifications) do + remove(:to_address_hash) + remove(:from_address_hash) + remove(:transaction_hash) + remove(:subject) + remove(:name) + end + + rename(table(:account_watchlist_notifications), :encrypted_name, to: :name) + rename(table(:account_watchlist_notifications), :encrypted_subject, to: :subject) + rename(table(:account_watchlist_notifications), :encrypted_from_address_hash, to: :from_address_hash) + rename(table(:account_watchlist_notifications), :encrypted_to_address_hash, to: :to_address_hash) + rename(table(:account_watchlist_notifications), :encrypted_transaction_hash, to: :transaction_hash) + + alter table(:account_public_tags_requests) do + remove(:full_name) + remove(:email) + end + + rename(table(:account_public_tags_requests), :encrypted_full_name, to: :full_name) + rename(table(:account_public_tags_requests), :encrypted_email, to: :email) + end +end diff --git a/apps/explorer/priv/account/migrations/20220706211444_set_new_indexes.exs b/apps/explorer/priv/account/migrations/20220706211444_set_new_indexes.exs new file mode 100644 index 0000000000..26abb92abe --- /dev/null +++ b/apps/explorer/priv/account/migrations/20220706211444_set_new_indexes.exs @@ -0,0 +1,43 @@ +defmodule Explorer.Repo.Account.Migrations.SetNewIndexes do + use Ecto.Migration + + def change do + drop_if_exists(unique_index(:account_tag_addresses, [:identity_id, :address_hash])) + drop_if_exists(unique_index(:account_tag_transactions, [:identity_id, :tx_hash])) + drop_if_exists(unique_index(:account_watchlist_addresses, [:watchlist_id, :address_hash])) + drop_if_exists(unique_index(:account_custom_abis, [:identity_id, :address_hash])) + + drop_if_exists(index(:account_watchlist_notifications, [:transaction_hash])) + drop_if_exists(index(:account_watchlist_notifications, [:from_address_hash])) + drop_if_exists(index(:account_watchlist_notifications, [:to_address_hash])) + + drop_if_exists(unique_index(:account_identities, [:uid])) + + drop_if_exists(index(:account_tag_addresses, [:address_hash])) + drop_if_exists(index(:account_tag_transactions, [:tx_hash])) + + drop_if_exists(index(:account_watchlist_addresses, [:address_hash])) + + create(unique_index(:account_tag_addresses, [:identity_id, :address_hash_hash])) + create(unique_index(:account_tag_transactions, [:identity_id, :tx_hash_hash])) + + create( + unique_index(:account_watchlist_addresses, [:watchlist_id, :address_hash_hash], + name: "unique_watchlist_id_address_hash_hash_index" + ) + ) + + create(unique_index(:account_custom_abis, [:identity_id, :address_hash_hash])) + + create(index(:account_watchlist_notifications, [:transaction_hash_hash])) + create(index(:account_watchlist_notifications, [:from_address_hash_hash])) + create(index(:account_watchlist_notifications, [:to_address_hash_hash])) + + create(unique_index(:account_identities, [:uid_hash])) + + create(index(:account_tag_addresses, [:address_hash_hash])) + create(index(:account_tag_transactions, [:tx_hash_hash])) + + create(index(:account_watchlist_addresses, [:address_hash_hash])) + end +end diff --git a/apps/explorer/priv/account/migrations/20220905195203_remove_guardian_tokens.exs b/apps/explorer/priv/account/migrations/20220905195203_remove_guardian_tokens.exs new file mode 100644 index 0000000000..2c76df8099 --- /dev/null +++ b/apps/explorer/priv/account/migrations/20220905195203_remove_guardian_tokens.exs @@ -0,0 +1,7 @@ +defmodule Explorer.Repo.Account.Migrations.RemoveGuardianTokens do + use Ecto.Migration + + def change do + drop_if_exists(table("guardian_tokens")) + end +end diff --git a/apps/explorer/priv/repo/migrations/20210219080523_add_tags.exs b/apps/explorer/priv/repo/migrations/20210219080523_add_tags.exs new file mode 100644 index 0000000000..79e83f808c --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20210219080523_add_tags.exs @@ -0,0 +1,24 @@ +defmodule Explorer.Repo.Migrations.AddTags do + use Ecto.Migration + + def change do + create table(:address_tags, primary_key: false) do + add(:id, :serial, null: false) + add(:label, :string, null: false) + + timestamps() + end + + create(unique_index(:address_tags, [:id])) + create(unique_index(:address_tags, [:label])) + + create table(:address_to_tags) do + add(:address_hash, references(:addresses, column: :hash, type: :bytea), null: false) + add(:tag_id, references(:address_tags, column: :id, type: :serial), null: false) + + timestamps() + end + + create(unique_index(:address_to_tags, [:address_hash, :tag_id])) + end +end diff --git a/apps/explorer/priv/repo/migrations/20211210184136_add_display_name_to_address_tag.exs b/apps/explorer/priv/repo/migrations/20211210184136_add_display_name_to_address_tag.exs new file mode 100644 index 0000000000..f07af29e10 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20211210184136_add_display_name_to_address_tag.exs @@ -0,0 +1,15 @@ +defmodule Explorer.Repo.Migrations.AddDisplayNameToAddressTag do + use Ecto.Migration + + def up do + alter table(:address_tags) do + add(:display_name, :string, null: true) + end + end + + def down do + alter table(:address_tags) do + remove(:display_name) + end + end +end diff --git a/apps/explorer/test/explorer/account/notify/email_test.exs b/apps/explorer/test/explorer/account/notify/email_test.exs new file mode 100644 index 0000000000..34e5a57ad6 --- /dev/null +++ b/apps/explorer/test/explorer/account/notify/email_test.exs @@ -0,0 +1,124 @@ +defmodule Explorer.Account.Notify.EmailTest do + use ExUnit.Case + + alias Explorer.Chain.Address + alias Explorer.Chain.Transaction + + alias Explorer.Account.{ + Identity, + Watchlist, + WatchlistAddress, + WatchlistNotification + } + + import Explorer.Chain, + only: [ + string_to_address_hash: 1, + string_to_transaction_hash: 1 + ] + + import Explorer.Account.Notifier.Email, + only: [compose: 2] + + setup do + host = Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] + path = Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:path] + + Application.put_env(:block_scout_web, BlockScoutWeb.Endpoint, url: [host: "localhost", path: "/"]) + + Application.put_env(:explorer, Explorer.Account, + sendgrid: [ + sender: "noreply@blockscout.com", + template: "d-666" + ] + ) + + :ok + + on_exit(fn -> + Application.put_env(:block_scout_web, BlockScoutWeb.Endpoint, url: [host: host, path: path]) + end) + end + + describe "composing email" do + test "compose_email" do + {:ok, tx_hash} = string_to_transaction_hash("0x5d5ff210261f1b2d6e4af22ea494f428f9997d4ab614a629d4f1390004b3e80d") + + {:ok, from_hash} = string_to_address_hash("0x092D537737E767Dae48c28aE509f34094496f030") + + {:ok, to_hash} = string_to_address_hash("0xE1F4dd38f00B0D8D4d2b4B5010bE53F2A0b934E5") + to_address = %Address{hash: to_hash} + + identity = %Identity{ + uid: "foo|bar", + name: "John Snow", + email: "john@blockscout.com" + } + + watchlist = %Watchlist{identity: identity} + + watchlist_address = %WatchlistAddress{ + name: "wallet", + watchlist: watchlist, + address_hash: to_hash, + watch_coin_input: true, + watch_coin_output: true, + notify_email: true + } + + watchlist_notification = %WatchlistNotification{ + watchlist_address: watchlist_address, + transaction_hash: tx_hash, + from_address_hash: from_hash, + to_address_hash: to_hash, + direction: "incoming", + method: "transfer", + block_number: 24_121_177, + amount: Decimal.new(1), + tx_fee: Decimal.new(210_000), + name: "wallet", + type: "COIN" + } + + assert compose(watchlist_notification, watchlist_address) == + %Bamboo.Email{ + assigns: %{}, + attachments: [], + bcc: nil, + blocked: false, + cc: nil, + from: "noreply@blockscout.com", + headers: %{}, + html_body: nil, + private: %{ + send_grid_template: %{ + dynamic_template_data: %{ + "address_hash" => "0xe1f4dd38f00b0d8d4d2b4b5010be53f2a0b934e5", + "address_name" => "wallet", + "address_url" => "https://localhost//address/0xe1f4dd38f00b0d8d4d2b4b5010be53f2a0b934e5", + "amount" => Decimal.new(1), + "block_number" => 24_121_177, + "block_url" => "https://localhost/block/24121177", + "direction" => "received at", + "from_address_hash" => "0x092d537737e767dae48c28ae509f34094496f030", + "from_url" => "https://localhost//address/0x092d537737e767dae48c28ae509f34094496f030", + "method" => "transfer", + "name" => "wallet", + "to_address_hash" => "0xe1f4dd38f00b0d8d4d2b4b5010be53f2a0b934e5", + "to_url" => "https://localhost//address/0xe1f4dd38f00b0d8d4d2b4b5010be53f2a0b934e5", + "transaction_hash" => "0x5d5ff210261f1b2d6e4af22ea494f428f9997d4ab614a629d4f1390004b3e80d", + "transaction_url" => + "https://localhost//tx/0x5d5ff210261f1b2d6e4af22ea494f428f9997d4ab614a629d4f1390004b3e80d", + "tx_fee" => Decimal.new(210_000), + "username" => "John Snow" + }, + template_id: "d-666" + } + }, + subject: nil, + text_body: nil, + to: "john@blockscout.com" + } + end + end +end diff --git a/apps/explorer/test/explorer/account/notify/notify_test.exs b/apps/explorer/test/explorer/account/notify/notify_test.exs new file mode 100644 index 0000000000..4d169f04d6 --- /dev/null +++ b/apps/explorer/test/explorer/account/notify/notify_test.exs @@ -0,0 +1,91 @@ +defmodule Explorer.Account.Notify.NotifyTest do + # use ExUnit.Case + use Explorer.DataCase + + import Explorer.Factory + + alias Explorer.Account.Notifier.Notify + alias Explorer.Account.{WatchlistAddress, WatchlistNotification} + alias Explorer.Chain + alias Explorer.Chain.{Transaction, Wei} + alias Explorer.Repo + + setup do + Application.put_env(:explorer, Explorer.Account, + sendgrid: [ + sender: "noreply@blockscout.com", + template: "d-666" + ] + ) + + Application.put_env(:explorer, Explorer.Mailer, + adapter: Bamboo.SendGridAdapter, + api_key: "SENDGRID_API_KEY" + ) + + Application.put_env( + :ueberauth, + Ueberauth, + providers: [ + auth0: { + Ueberauth.Strategy.Auth0, + [callback_url: "callback.url"] + } + ], + logout_url: "logout.url", + logout_return_to_url: "return.url" + ) + end + + describe "notify" do + test "when address not in any watchlist" do + tx = with_block(insert(:transaction)) + + notify = Notify.call([tx]) + + wn = + WatchlistNotification + |> first + |> Repo.account_repo().one() + + assert notify == [[:ok]] + + assert wn == nil + end + + test "when address apears in watchlist" do + wa = + %WatchlistAddress{address_hash: address_hash} = + build(:account_watchlist_address) + |> Repo.account_repo().insert!() + + _watchlist_address = Repo.preload(wa, watchlist: :identity) + + tx = + %Transaction{ + from_address: _from_address, + to_address: _to_address, + block_number: _block_number, + hash: _tx_hash + } = with_block(insert(:transaction, to_address: %Chain.Address{hash: address_hash})) + + {_, fee} = Chain.fee(tx, :gwei) + amount = Wei.to(tx.value, :ether) + notify = Notify.call([tx]) + + wn = + WatchlistNotification + |> first + |> Repo.account_repo().one() + + assert notify == [[:ok]] + + assert wn.amount == amount + assert wn.direction == "incoming" + assert wn.method == "transfer" + assert wn.subject == "Coin transaction" + assert wn.tx_fee == fee + assert wn.type == "COIN" + end + end +end diff --git a/apps/explorer/test/explorer/account/notify/summary_test.exs b/apps/explorer/test/explorer/account/notify/summary_test.exs new file mode 100644 index 0000000000..611316f193 --- /dev/null +++ b/apps/explorer/test/explorer/account/notify/summary_test.exs @@ -0,0 +1,273 @@ +defmodule Explorer.Account.Notify.SummaryTest do + use Explorer.DataCase + + import Explorer.Factory + + alias Explorer.Account.Notifier.Summary + alias Explorer.Chain + alias Explorer.Chain.{TokenTransfer, Transaction, Wei} + alias Explorer.Repo + + describe "call" do + test "Coin transaction" do + tx = + %Transaction{ + from_address: from_address, + to_address: to_address, + block_number: block_number, + hash: tx_hash + } = with_block(insert(:transaction)) + + {_, fee} = Chain.fee(tx, :gwei) + amount = Wei.to(tx.value, :ether) + + assert Summary.process(tx) == [ + %Summary{ + amount: amount, + block_number: block_number, + from_address_hash: from_address.hash, + method: "transfer", + name: "POA", + subject: "Coin transaction", + to_address_hash: to_address.hash, + transaction_hash: tx_hash, + tx_fee: fee, + type: "COIN" + } + ] + end + + test "Pending Coin transaction (w/o block)" do + tx = + %Transaction{ + from_address: _from_address, + to_address: _to_address, + hash: _tx_hash + } = insert(:transaction) + + assert Summary.process(tx) == [] + end + + test "Contract creation transaction" do + address = insert(:address) + contract_address = insert(:contract_address) + + block = insert(:block) + + tx = + %Transaction{ + from_address: _from_address, + block_number: _block_number, + hash: tx_hash + } = + :transaction + |> insert(from_address: address, to_address: nil) + |> with_contract_creation(contract_address) + |> with_block(block) + + {_, fee} = Chain.fee(tx, :gwei) + amount = Wei.to(tx.value, :ether) + + assert Summary.process(tx) == [ + %Summary{ + amount: amount, + block_number: block.number, + from_address_hash: address.hash, + method: "contract_creation", + name: "POA", + subject: "Contract creation", + to_address_hash: contract_address.hash, + transaction_hash: tx_hash, + tx_fee: fee, + type: "COIN" + } + ] + end + + test "ERC-20 Token transfer" do + tx = + %Transaction{ + from_address: _from_address, + to_address: _to_address, + block_number: _block_number, + hash: _tx_hash + } = with_block(insert(:transaction)) + + transfer = + %TokenTransfer{ + amount: _amount, + block_number: block_number, + from_address: from_address, + to_address: to_address, + token: token + } = + :token_transfer + |> insert(transaction: tx) + |> Repo.preload([ + :token + ]) + + {_, fee} = Chain.fee(tx, :gwei) + + token_decimals = Decimal.to_integer(token.decimals) + + decimals = Decimal.new(Integer.pow(10, token_decimals)) + + amount = Decimal.div(transfer.amount, decimals) + + assert Summary.process(transfer) == [ + %Summary{ + amount: amount, + block_number: block_number, + from_address_hash: from_address.hash, + method: "transfer", + name: "Infinite Token", + subject: "ERC-20", + to_address_hash: to_address.hash, + transaction_hash: tx.hash, + tx_fee: fee, + type: "ERC-20" + } + ] + end + + test "ERC-721 Token transfer" do + token = insert(:token, type: "ERC-721") + + tx = + %Transaction{ + from_address: _from_address, + to_address: _to_address, + block_number: _block_number, + hash: _tx_hash + } = with_block(insert(:transaction)) + + transfer = + %TokenTransfer{ + amount: _amount, + block_number: block_number, + from_address: from_address, + to_address: to_address + } = + :token_transfer + |> insert( + transaction: tx, + token_id: 42, + token_contract_address: token.contract_address + ) + |> Repo.preload([ + :token + ]) + + {_, fee} = Chain.fee(tx, :gwei) + + assert Summary.process(transfer) == [ + %Summary{ + amount: 0, + block_number: block_number, + from_address_hash: from_address.hash, + method: "transfer", + name: "Infinite Token", + subject: "42", + to_address_hash: to_address.hash, + transaction_hash: tx.hash, + tx_fee: fee, + type: "ERC-721" + } + ] + end + + test "ERC-1155 single Token transfer" do + token = insert(:token, type: "ERC-1155") + + tx = + %Transaction{ + from_address: _from_address, + to_address: _to_address, + block_number: _block_number, + hash: _tx_hash + } = with_block(insert(:transaction)) + + transfer = + %TokenTransfer{ + amount: _amount, + block_number: block_number, + from_address: from_address, + to_address: to_address + } = + :token_transfer + |> insert( + transaction: tx, + token_id: 42, + token_contract_address: token.contract_address + ) + |> Repo.preload([ + :token + ]) + + {_, fee} = Chain.fee(tx, :gwei) + + assert Summary.process(transfer) == [ + %Summary{ + amount: 0, + block_number: block_number, + from_address_hash: from_address.hash, + method: "transfer", + name: "Infinite Token", + subject: "42", + to_address_hash: to_address.hash, + transaction_hash: tx.hash, + tx_fee: fee, + type: "ERC-1155" + } + ] + end + + test "ERC-1155 multiple Token transfer" do + token = insert(:token, type: "ERC-1155") + + tx = + %Transaction{ + from_address: _from_address, + to_address: _to_address, + block_number: _block_number, + hash: _tx_hash + } = with_block(insert(:transaction)) + + transfer = + %TokenTransfer{ + amount: _amount, + block_number: block_number, + from_address: from_address, + to_address: to_address + } = + :token_transfer + |> insert( + transaction: tx, + token_id: nil, + token_ids: [23, 42], + token_contract_address: token.contract_address + ) + |> Repo.preload([ + :token + ]) + + {_, fee} = Chain.fee(tx, :gwei) + + assert Summary.process(transfer) == [ + %Summary{ + amount: 0, + block_number: block_number, + from_address_hash: from_address.hash, + method: "transfer", + name: "Infinite Token", + subject: "23, 42", + to_address_hash: to_address.hash, + transaction_hash: tx.hash, + tx_fee: fee, + type: "ERC-1155" + } + ] + end + end +end diff --git a/apps/explorer/test/support/data_case.ex b/apps/explorer/test/support/data_case.ex index e12dd24a82..da18760983 100644 --- a/apps/explorer/test/support/data_case.ex +++ b/apps/explorer/test/support/data_case.ex @@ -34,9 +34,11 @@ defmodule Explorer.DataCase do ExVCR.Config.cassette_library_dir("test/support/fixture/vcr_cassettes") :ok = Ecto.Adapters.SQL.Sandbox.checkout(Explorer.Repo) + :ok = Ecto.Adapters.SQL.Sandbox.checkout(Explorer.Repo.Account) unless tags[:async] do Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo, {:shared, self()}) + Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Account, {:shared, self()}) end Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.BlockNumber.child_id()) diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index 34f4692240..7967d83b8d 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -4,10 +4,20 @@ defmodule Explorer.Factory do require Ecto.Query import Ecto.Query + import Explorer.Chain, only: [hash_to_lower_case_string: 1] import Kernel, except: [+: 2] - alias Bcrypt - alias Explorer.Accounts.{User, UserContact} + alias Explorer.Account.{ + Identity, + Watchlist, + WatchlistAddress + } + + alias Explorer.Accounts.{ + User, + UserContact + } + alias Explorer.Admin.Administrator alias Explorer.Chain.Block.{EmissionReward, Range, Reward} @@ -37,6 +47,116 @@ defmodule Explorer.Factory do alias Explorer.Market.MarketHistory alias Explorer.Repo + alias Ueberauth.Strategy.Auth0 + alias Ueberauth.Auth.Info + alias Ueberauth.Auth + + def account_identity_factory do + %Identity{ + uid: sequence("github|"), + email: sequence(:email, &"me-#{&1}@blockscout.com"), + name: sequence("John") + } + end + + def auth_factory do + %Auth{ + info: %Info{ + birthday: nil, + description: nil, + email: sequence(:email, &"test_user-#{&1}@blockscout.com"), + first_name: nil, + image: sequence("https://example.com/avatar/test_user"), + last_name: nil, + location: nil, + name: sequence("User Test"), + nickname: sequence("test_user"), + phone: nil, + urls: %{profile: nil, website: nil} + }, + provider: :auth0, + strategy: Auth0, + uid: sequence("blockscout|000") + } + end + + def watchlist_address_factory do + %{ + "address_hash" => to_string(build(:address).hash), + "name" => sequence("test"), + "notification_settings" => %{ + "native" => %{ + "incoming" => random_bool(), + "outcoming" => random_bool() + }, + "ERC-20" => %{ + "incoming" => random_bool(), + "outcoming" => random_bool() + }, + "ERC-721" => %{ + "incoming" => random_bool(), + "outcoming" => random_bool() + } + }, + "notification_methods" => %{ + "email" => random_bool() + } + } + end + + def custom_abi_factory do + contract_address_hash = to_string(insert(:contract_address).hash) + + %{"contract_address_hash" => contract_address_hash, "name" => sequence("test"), "abi" => contract_code_info().abi} + end + + def public_tags_request_factory do + %{ + "full_name" => sequence("full name"), + "email" => sequence(:email, &"test_user-#{&1}@blockscout.com"), + "tags" => Enum.join(Enum.map(1..Enum.random(1..2), fn _ -> sequence("Tag") end), ";"), + "website" => sequence("website"), + "additional_comment" => sequence("additional_comment"), + "addresses" => Enum.map(1..Enum.random(1..10), fn _ -> to_string(build(:address).hash) end), + "company" => sequence("company"), + "is_owner" => random_bool() + } + end + + def account_watchlist_factory do + %Watchlist{ + identity: build(:account_identity) + } + end + + def tag_address_factory do + %{"name" => sequence("name"), "address_hash" => to_string(build(:address).hash)} + end + + def tag_transaction_factory do + %{"name" => sequence("name"), "transaction_hash" => to_string(insert(:transaction).hash)} + end + + def account_watchlist_address_factory do + hash = build(:address).hash + + %WatchlistAddress{ + name: "wallet", + watchlist: build(:account_watchlist), + address_hash: hash, + address_hash_hash: hash_to_lower_case_string(hash), + watch_coin_input: true, + watch_coin_output: true, + watch_erc_20_input: true, + watch_erc_20_output: true, + watch_erc_721_input: true, + watch_erc_721_output: true, + watch_erc_1155_input: true, + watch_erc_1155_output: true, + notify_email: true + } + end + def address_factory do %Address{ hash: address_hash() @@ -780,4 +900,6 @@ defmodule Explorer.Factory do user: build(:user) } end + + def random_bool, do: Enum.random([true, false]) end diff --git a/apps/explorer/test/test_helper.exs b/apps/explorer/test/test_helper.exs index 50847130f8..418ea17ea0 100644 --- a/apps/explorer/test/test_helper.exs +++ b/apps/explorer/test/test_helper.exs @@ -12,6 +12,7 @@ ExUnit.start() {:ok, _} = Application.ensure_all_started(:ex_machina) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo, :auto) +Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Account, :auto) Mox.defmock(Explorer.ExchangeRates.Source.TestSource, for: Explorer.ExchangeRates.Source) Mox.defmock(Explorer.KnownTokens.Source.TestSource, for: Explorer.KnownTokens.Source) diff --git a/apps/indexer/config/runtime/test.exs b/apps/indexer/config/runtime/test.exs index b360f5f9b9..476dedd943 100644 --- a/apps/indexer/config/runtime/test.exs +++ b/apps/indexer/config/runtime/test.exs @@ -1,3 +1,5 @@ +import Config + variant = if is_nil(System.get_env("ETHEREUM_JSONRPC_VARIANT")) do "parity" diff --git a/config/config.exs b/config/config.exs index ffdbc80032..8ec1d34312 100644 --- a/config/config.exs +++ b/config/config.exs @@ -35,7 +35,8 @@ config :logger, {LoggerFileBackend, :pending_transactions_to_refetch}, {LoggerFileBackend, :empty_blocks_to_refetch}, {LoggerFileBackend, :api}, - {LoggerFileBackend, :block_import_timings} + {LoggerFileBackend, :block_import_timings}, + {LoggerFileBackend, :account} ] config :logger, :console, diff --git a/config/dev.exs b/config/dev.exs index 21e6c9fd40..323212e107 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -8,3 +8,8 @@ config :logger, :ecto, path: Path.absname("logs/dev/ecto.log") config :logger, :error, path: Path.absname("logs/dev/error.log") + +config :logger, :account, + level: :debug, + path: Path.absname("logs/dev/account.log"), + metadata_filter: [fetcher: :account] diff --git a/config/prod.exs b/config/prod.exs index 2175810d04..1d9be54e3f 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -12,3 +12,9 @@ config :logger, :ecto, config :logger, :error, path: Path.absname("logs/prod/error.log"), rotate: %{max_bytes: 52_428_800, keep: 19} + +config :logger, :account, + level: :info, + path: Path.absname("logs/prod/account.log"), + rotate: %{max_bytes: 52_428_800, keep: 19}, + metadata_filter: [fetcher: :account] diff --git a/config/runtime.exs b/config/runtime.exs index 59c3e150dc..f661efbe40 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -35,6 +35,17 @@ config :block_scout_web, :footer, ### BlockScout Web ### ###################### +# Configures Ueberauth's Auth0 auth provider +config :ueberauth, Ueberauth.Strategy.Auth0.OAuth, + domain: System.get_env("ACCOUNT_AUTH0_DOMAIN"), + client_id: System.get_env("ACCOUNT_AUTH0_CLIENT_ID"), + client_secret: System.get_env("ACCOUNT_AUTH0_CLIENT_SECRET") + +# Configures Ueberauth local settings +config :ueberauth, Ueberauth, + logout_url: System.get_env("ACCOUNT_AUTH0_LOGOUT_URL"), + logout_return_to_url: System.get_env("ACCOUNT_AUTH0_LOGOUT_RETURN_URL") + config :block_scout_web, version: System.get_env("BLOCKSCOUT_VERSION"), release_link: System.get_env("RELEASE_LINK"), @@ -308,6 +319,21 @@ config :explorer, Explorer.SmartContract.RustVerifierInterface, service_url: System.get_env("RUST_VERIFICATION_SERVICE_URL"), enabled: System.get_env("ENABLE_RUST_VERIFICATION_SERVICE") == "true" +config :explorer, Explorer.ThirdPartyIntegrations.AirTable, + table_url: System.get_env("ACCOUNT_PUBLIC_TAGS_AIRTABLE_URL"), + api_key: System.get_env("ACCOUNT_PUBLIC_TAGS_AIRTABLE_API_KEY") + +config :explorer, Explorer.Mailer, + adapter: Bamboo.SendGridAdapter, + api_key: System.get_env("ACCOUNT_SENDGRID_API_KEY") + +config :explorer, Explorer.Account, + enabled: System.get_env("ACCOUNT_ENABLED") == "true", + sendgrid: [ + sender: System.get_env("ACCOUNT_SENDGRID_SENDER"), + template: System.get_env("ACCOUNT_SENDGRID_TEMPLATE") + ] + ############### ### Indexer ### ############### diff --git a/config/runtime/dev.exs b/config/runtime/dev.exs index e7348f1e5d..8de612decb 100644 --- a/config/runtime/dev.exs +++ b/config/runtime/dev.exs @@ -48,8 +48,8 @@ database_api_url = pool_size = if System.get_env("DATABASE_READ_ONLY_API_URL"), - do: String.to_integer(System.get_env("POOL_SIZE", "40")), - else: String.to_integer(System.get_env("POOL_SIZE", "50")) + do: String.to_integer(System.get_env("POOL_SIZE", "30")), + else: String.to_integer(System.get_env("POOL_SIZE", "40")) # Configure your database config :explorer, Explorer.Repo, @@ -61,10 +61,7 @@ config :explorer, Explorer.Repo, database_api = if System.get_env("DATABASE_READ_ONLY_API_URL"), do: nil, else: database hostname_api = if System.get_env("DATABASE_READ_ONLY_API_URL"), do: nil, else: hostname -pool_size_api = - if System.get_env("DATABASE_READ_ONLY_API_URL"), - do: String.to_integer(System.get_env("POOL_SIZE_API", "50")), - else: String.to_integer(System.get_env("POOL_SIZE_API", "10")) +pool_size_api = String.to_integer(System.get_env("POOL_SIZE_API", "10")) # Configure API database config :explorer, Explorer.Repo.Replica1, @@ -73,6 +70,20 @@ config :explorer, Explorer.Repo.Replica1, url: database_api_url, pool_size: pool_size_api +database_account_url = System.get_env("ACCOUNT_DATABASE_URL") || System.get_env("DATABASE_URL") + +pool_size_account = String.to_integer(System.get_env("ACCOUNT_POOL_SIZE", "10")) + +database_account = if System.get_env("ACCOUNT_DATABASE_URL"), do: nil, else: database +hostname_account = if System.get_env("ACCOUNT_DATABASE_URL"), do: nil, else: hostname + +# Configure Account database +config :explorer, Explorer.Repo.Account, + database: database_account, + hostname: hostname_account, + url: database_account_url, + pool_size: pool_size_account + variant = if is_nil(System.get_env("ETHEREUM_JSONRPC_VARIANT")) do "ganache" diff --git a/config/runtime/prod.exs b/config/runtime/prod.exs index 8a5c8f24cd..0e4e58f81a 100644 --- a/config/runtime/prod.exs +++ b/config/runtime/prod.exs @@ -49,6 +49,19 @@ config :explorer, Explorer.Repo.Replica1, pool_size: pool_size_api, ssl: String.equivalent?(System.get_env("ECTO_USE_SSL") || "true", "true") +database_account_url = + if System.get_env("ACCOUNT_DATABASE_URL"), + do: System.get_env("ACCOUNT_DATABASE_URL"), + else: System.get_env("DATABASE_URL") + +pool_size_account = String.to_integer(System.get_env("ACCOUNT_POOL_SIZE", "50")) + +# Configures Account database +config :explorer, Explorer.Repo.Account, + url: database_account_url, + pool_size: pool_size_account, + ssl: String.equivalent?(System.get_env("ECTO_USE_SSL") || "true", "true") + variant = if is_nil(System.get_env("ETHEREUM_JSONRPC_VARIANT")) do "parity" diff --git a/config/runtime/test.exs b/config/runtime/test.exs index 6f604ba05e..38cd15e0df 100644 --- a/config/runtime/test.exs +++ b/config/runtime/test.exs @@ -1,3 +1,5 @@ +import Config + ###################### ### BlockScout Web ### ###################### diff --git a/docker-compose/docker-compose-no-build-ganache.yml b/docker-compose/docker-compose-no-build-ganache.yml index 619e8104b0..62a007a2b0 100644 --- a/docker-compose/docker-compose-no-build-ganache.yml +++ b/docker-compose/docker-compose-no-build-ganache.yml @@ -1,6 +1,10 @@ version: '3.8' services: + redis_db: + image: 'redis:alpine' + command: redis-server + db: image: postgres:14 restart: always @@ -16,6 +20,7 @@ services: depends_on: - db - smart-contract-verifier + - redis_db image: blockscout/blockscout:${DOCKER_TAG:-latest} restart: always container_name: 'blockscout' diff --git a/docker-compose/docker-compose-no-build-geth.yml b/docker-compose/docker-compose-no-build-geth.yml index ec6e64d248..d4f59f9a4d 100644 --- a/docker-compose/docker-compose-no-build-geth.yml +++ b/docker-compose/docker-compose-no-build-geth.yml @@ -1,6 +1,10 @@ version: '3.8' services: + redis_db: + image: 'redis:alpine' + command: redis-server + db: image: postgres:14 restart: always @@ -18,6 +22,7 @@ services: depends_on: - db - smart-contract-verifier + - redis_db image: blockscout/blockscout:${DOCKER_TAG:-latest} restart: always container_name: 'blockscout' diff --git a/docker-compose/docker-compose-no-build-hardhat-network.yml b/docker-compose/docker-compose-no-build-hardhat-network.yml index 9a74d44f10..aaab30fd1e 100644 --- a/docker-compose/docker-compose-no-build-hardhat-network.yml +++ b/docker-compose/docker-compose-no-build-hardhat-network.yml @@ -1,6 +1,10 @@ version: '3.8' services: + redis_db: + image: 'redis:alpine' + command: redis-server + db: image: postgres:14 restart: always @@ -16,6 +20,7 @@ services: depends_on: - db - smart-contract-verifier + - redis_db image: blockscout/blockscout:${DOCKER_TAG:-latest} restart: always container_name: 'blockscout' diff --git a/docker-compose/docker-compose-no-build-open-ethereum-nethermind.yml b/docker-compose/docker-compose-no-build-open-ethereum-nethermind.yml index 9a272acb57..cde891f1ad 100644 --- a/docker-compose/docker-compose-no-build-open-ethereum-nethermind.yml +++ b/docker-compose/docker-compose-no-build-open-ethereum-nethermind.yml @@ -1,6 +1,10 @@ version: '3.8' services: + redis_db: + image: 'redis:alpine' + command: redis-server + db: image: postgres:14 restart: always @@ -18,6 +22,7 @@ services: depends_on: - db - smart-contract-verifier + - redis_db image: blockscout/blockscout:${DOCKER_TAG:-latest} restart: always container_name: 'blockscout' diff --git a/docker-compose/docker-compose-no-rust-verification.yml b/docker-compose/docker-compose-no-rust-verification.yml index 74c3b77642..2f809bcfe2 100644 --- a/docker-compose/docker-compose-no-rust-verification.yml +++ b/docker-compose/docker-compose-no-rust-verification.yml @@ -1,6 +1,10 @@ version: '3.8' services: + redis_db: + image: 'redis:alpine' + command: redis-server + db: image: postgres:14 restart: always @@ -15,6 +19,7 @@ services: blockscout: depends_on: - db + - redis_db image: blockscout/blockscout:${DOCKER_TAG:-latest} build: context: .. diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 0a57ceb4ca..e9043545f3 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -1,6 +1,10 @@ version: '3.8' services: + redis_db: + image: 'redis:alpine' + command: redis-server + db: image: postgres:14 restart: always @@ -16,6 +20,7 @@ services: depends_on: - db - smart-contract-verifier + - redis_db image: blockscout/blockscout:${DOCKER_TAG:-latest} build: context: .. diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index cd2a3ffdb0..6b157a2011 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -19,8 +19,8 @@ BLOCKSCOUT_PROTOCOL= # SECRET_KEY_BASE= # CHECK_ORIGIN= PORT=4000 -# COIN= COIN_NAME= +# COIN= # METADATA_CONTRACT= # VALIDATORS_CONTRACT= # KEYS_MANAGER_CONTRACT= @@ -124,3 +124,19 @@ API_RATE_LIMIT_STATIC_API_KEY= FETCH_REWARDS_WAY=trace_block ENABLE_RUST_VERIFICATION_SERVICE=true RUST_VERIFICATION_SERVICE_URL=http://host.docker.internal:8043/ +# DATABASE_READ_ONLY_API_URL= +# ACCOUNT_DATABASE_URL= +# ACCOUNT_POOL_SIZE= +# ACCOUNT_AUTH0_DOMAIN= +# ACCOUNT_AUTH0_CLIENT_ID= +# ACCOUNT_AUTH0_CLIENT_SECRET= +# ACCOUNT_AUTH0_LOGOUT_URL= +# ACCOUNT_AUTH0_LOGOUT_RETURN_URL= +# ACCOUNT_PUBLIC_TAGS_AIRTABLE_URL= +# ACCOUNT_PUBLIC_TAGS_AIRTABLE_API_KEY= +# ACCOUNT_SENDGRID_API_KEY= +# ACCOUNT_SENDGRID_SENDER= +# ACCOUNT_SENDGRID_TEMPLATE= +ACCOUNT_CLOAK_KEY= +ACCOUNT_ENABLED=false +ACCOUNT_REDIS_URL=redis://redis_db:6379 \ No newline at end of file diff --git a/docker/Makefile b/docker/Makefile index f16843d91e..2ef104427c 100644 --- a/docker/Makefile +++ b/docker/Makefile @@ -367,9 +367,6 @@ endif ifdef API_RATE_LIMIT_WHITELISTED_IPS BLOCKSCOUT_CONTAINER_PARAMS += -e 'API_RATE_LIMIT_WHITELISTED_IPS=$(API_RATE_LIMIT_WHITELISTED_IPS)' endif -ifdef COIN_NAME - BLOCKSCOUT_CONTAINER_PARAMS += -e 'COIN_NAME=$(COIN_NAME)' -endif ifdef INDEXER_DISABLE_PENDING_TRANSACTIONS_FETCHER BLOCKSCOUT_CONTAINER_PARAMS += -e 'INDEXER_DISABLE_PENDING_TRANSACTIONS_FETCHER=$(INDEXER_DISABLE_PENDING_TRANSACTIONS_FETCHER)' endif @@ -466,7 +463,54 @@ endif ifdef RUST_VERIFICATION_SERVICE_URL BLOCKSCOUT_CONTAINER_PARAMS += -e 'RUST_VERIFICATION_SERVICE_URL=$(RUST_VERIFICATION_SERVICE_URL)' endif - +ifdef ACCOUNT_ENABLED + BLOCKSCOUT_CONTAINER_PARAMS += -e 'ACCOUNT_ENABLED=$(ACCOUNT_ENABLED)' +endif +ifdef ACCOUNT_REDIS_URL + BLOCKSCOUT_CONTAINER_PARAMS += -e 'ACCOUNT_REDIS_URL=$(ACCOUNT_REDIS_URL)' +endif +ifdef COIN_NAME + BLOCKSCOUT_CONTAINER_PARAMS += -e 'COIN_NAME=$(COIN_NAME)' +endif +ifdef ACCOUNT_AUTH0_DOMAIN + BLOCKSCOUT_CONTAINER_PARAMS += -e 'ACCOUNT_AUTH0_DOMAIN=$(ACCOUNT_AUTH0_DOMAIN)' +endif +ifdef ACCOUNT_AUTH0_CLIENT_ID + BLOCKSCOUT_CONTAINER_PARAMS += -e 'ACCOUNT_AUTH0_CLIENT_ID=$(ACCOUNT_AUTH0_CLIENT_ID)' +endif +ifdef ACCOUNT_AUTH0_CLIENT_SECRET + BLOCKSCOUT_CONTAINER_PARAMS += -e 'ACCOUNT_AUTH0_CLIENT_SECRET=$(ACCOUNT_AUTH0_CLIENT_SECRET)' +endif +ifdef ACCOUNT_AUTH0_LOGOUT_RETURN_URL + BLOCKSCOUT_CONTAINER_PARAMS += -e 'ACCOUNT_AUTH0_LOGOUT_RETURN_URL=$(ACCOUNT_AUTH0_LOGOUT_RETURN_URL)' +endif +ifdef ACCOUNT_AUTH0_LOGOUT_URL + BLOCKSCOUT_CONTAINER_PARAMS += -e 'ACCOUNT_AUTH0_LOGOUT_URL=$(ACCOUNT_AUTH0_LOGOUT_URL)' +endif +ifdef ACCOUNT_SENDGRID_API_KEY + BLOCKSCOUT_CONTAINER_PARAMS += -e 'ACCOUNT_SENDGRID_API_KEY=$(ACCOUNT_SENDGRID_API_KEY)' +endif +ifdef ACCOUNT_SENDGRID_SENDER + BLOCKSCOUT_CONTAINER_PARAMS += -e 'ACCOUNT_SENDGRID_SENDER=$(ACCOUNT_SENDGRID_SENDER)' +endif +ifdef ACCOUNT_SENDGRID_TEMPLATE + BLOCKSCOUT_CONTAINER_PARAMS += -e 'ACCOUNT_SENDGRID_TEMPLATE=$(ACCOUNT_SENDGRID_TEMPLATE)' +endif +ifdef ACCOUNT_PUBLIC_TAGS_AIRTABLE_URL + BLOCKSCOUT_CONTAINER_PARAMS += -e 'ACCOUNT_PUBLIC_TAGS_AIRTABLE_URL=$(ACCOUNT_PUBLIC_TAGS_AIRTABLE_URL)' +endif +ifdef ACCOUNT_PUBLIC_TAGS_AIRTABLE_API_KEY + BLOCKSCOUT_CONTAINER_PARAMS += -e 'ACCOUNT_PUBLIC_TAGS_AIRTABLE_API_KEY=$(ACCOUNT_PUBLIC_TAGS_AIRTABLE_API_KEY)' +endif +ifdef ACCOUNT_DATABASE_URL + BLOCKSCOUT_CONTAINER_PARAMS += -e 'ACCOUNT_DATABASE_URL=$(ACCOUNT_DATABASE_URL)' +endif +ifdef ACCOUNT_POOL_SIZE + BLOCKSCOUT_CONTAINER_PARAMS += -e 'ACCOUNT_POOL_SIZE=$(ACCOUNT_POOL_SIZE)' +endif +ifdef ACCOUNT_CLOAK_KEY + BLOCKSCOUT_CONTAINER_PARAMS += -e 'ACCOUNT_CLOAK_KEY=$(ACCOUNT_CLOAK_KEY)' +endif HAS_BLOCKSCOUT_IMAGE := $(shell docker images | grep -sw "${BS_CONTAINER_IMAGE} ") build: diff --git a/mix.lock b/mix.lock index ebd7454167..0a53157e37 100644 --- a/mix.lock +++ b/mix.lock @@ -4,16 +4,20 @@ "absinthe_plug": {:git, "https://github.com/blockscout/absinthe_plug.git", "c435d43f316769e1beee1dbe500b623124c96785", [tag: "1.5.3"]}, "absinthe_relay": {:hex, :absinthe_relay, "1.5.2", "cfb8aed70f4e4c7718d3f1c212332d2ea728f17c7fc0f68f1e461f0f5f0c4b9a", [:mix], [{:absinthe, "~> 1.5.0 or ~> 1.6.0 or ~> 1.7.0", [hex: :absinthe, repo: "hexpm", optional: false]}, {:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "0587ee913afa31512e1457a5064ee88427f8fe7bcfbeeecd41c71d9cff0b62b6"}, "accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm", "11b18c220bcc2eab63b5470c038ef10eb6783bcb1fcdb11aa4137defa5ac1bb8"}, + "bamboo": {:hex, :bamboo, "2.2.0", "f10a406d2b7f5123eb1f02edfa043c259db04b47ab956041f279eaac776ef5ce", [:mix], [{:hackney, ">= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.4", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "8c3b14ba7d2f40cb4be04128ed1e2aff06d91d9413d38bafb4afccffa3ade4fc"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.0.1", "9be815469e6bfefec40fa74658ecbbe6897acfb57614df1416eeccd4903f602c", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "486bb95efb645d1efc6794c1ddd776a186a9a713abf06f45708a6ce324fb96cf"}, "benchee": {:hex, :benchee, "1.1.0", "f3a43817209a92a1fade36ef36b86e1052627fd8934a8b937ac9ab3a76c43062", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}], "hexpm", "7da57d545003165a012b587077f6ba90b89210fd88074ce3c60ce239eb5e6d93"}, "benchee_csv": {:hex, :benchee_csv, "1.0.0", "0b3b9223290bfcb8003552705bec9bcf1a89b4a83b70bd686e45295c264f3d16", [:mix], [{:benchee, ">= 0.99.0 and < 2.0.0", [hex: :benchee, repo: "hexpm", optional: false]}, {:csv, "~> 2.0", [hex: :csv, repo: "hexpm", optional: false]}], "hexpm", "cdefb804c021dcf7a99199492026584be9b5a21d6644ac0d01c81c5d97c520d5"}, "binary": {:hex, :binary, "0.0.5", "20d816f7274ea34f1b673b4cff2fdb9ebec9391a7a68c349070d515c66b1b2cf", [:mix], [], "hexpm", "ee1e9ebcab703a4e24db554957fbb540642fe9327eb9e295cb3f07dd7c11ddb2"}, "briefly": {:git, "https://github.com/CargoSense/briefly.git", "1dd66ee19ca84ed60f4eca47fee59227ba960fb7", []}, "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, + "bureaucrat": {:hex, :bureaucrat, "0.2.9", "d98e4d2b9bdbf22e4a45c2113ce8b38b5b63278506c6ff918e3b943a4355d85b", [:mix], [{:inflex, ">= 1.10.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.2.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, ">= 1.0.0", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 1.5 or ~> 2.0 or ~> 3.0 or ~> 4.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "111c8dd84382a62e1026ae011d592ceee918553e5203fe8448d9ba6ccbdfff7d"}, "bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"}, "cbor": {:hex, :cbor, "1.0.0", "35d33a26f6420ce3d2d01c0b1463a748b34c537d5609fc40116daf3666700d36", [:mix], [], "hexpm", "cc5e21e0fa5a0330715a3806c67bc294f8b65d07160f751b5bd6058bed1962ac"}, "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, "cldr_utils": {:hex, :cldr_utils, "2.19.1", "5a7bcd2f2fd432c548e494e850bba8a9e838f1b10202f682ea1d9809d74eff31", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "fbd10f79363e70f3d893ab21e195f444ca87c2c80120b5911761491da4489620"}, + "cloak": {:hex, :cloak, "1.1.2", "7e0006c2b0b98d976d4f559080fabefd81f0e0a50a3c4b621f85ceeb563e80bb", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "940d5ac4fcd51b252930fd112e319ea5ae6ab540b722f3ca60a85666759b9585"}, + "cloak_ecto": {:hex, :cloak_ecto, "1.2.0", "e86a3df3bf0dc8980f70406bcb0af2858bac247d55494d40bc58a152590bd402", [:mix], [{:cloak, "~> 1.1.1", [hex: :cloak, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "8bcc677185c813fe64b786618bd6689b1707b35cd95acaae0834557b15a0c62f"}, "coerce": {:hex, :coerce, "1.0.1", "211c27386315dc2894ac11bc1f413a0e38505d808153367bd5c6e75a4003d096", [:mix], [], "hexpm", "b44a691700f7a1a15b4b7e2ff1fa30bebd669929ac8aa43cffe9e2f8bf051cf1"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, "comeonin": {:hex, :comeonin, "5.3.3", "2c564dac95a35650e9b6acfe6d2952083d8a08e4a89b93a481acb552b325892e", [:mix], [], "hexpm", "3e38c9c2cb080828116597ca8807bb482618a315bfafd98c90bc22a821cc84df"}, @@ -64,7 +68,9 @@ "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, "httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, + "inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"}, "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, + "jose": {:hex, :jose, "1.11.2", "f4c018ccf4fdce22c71e44d471f15f723cb3efab5d909ab2ba202b5bf35557b3", [:mix, :rebar3], [], "hexpm", "98143fbc48d55f3a18daba82d34fe48959d44538e9697c08f34200fa5f0947d2"}, "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm", "fc3499fed7a726995aa659143a248534adc754ebd16ccd437cd93b649a95091f"}, "junit_formatter": {:hex, :junit_formatter, "3.3.1", "c729befb848f1b9571f317d2fefa648e9d4869befc4b2980daca7c1edc468e40", [:mix], [], "hexpm", "761fc5be4b4c15d8ba91a6dafde0b2c2ae6db9da7b8832a55b5a1deb524da72b"}, "libsecp256k1": {:hex, :libsecp256k1, "0.1.10", "d27495e2b9851c7765129b76c53b60f5e275bd6ff68292c50536bf6b8d091a4d", [:make, :mix], [{:mix_erlang_tasks, "0.1.0", [hex: :mix_erlang_tasks, repo: "hexpm", optional: false]}], "hexpm", "09ea06239938571124f7f5a27bc9ac45dfb1cfc2df40d46ee9b59c3d51366652"}, @@ -76,7 +82,7 @@ "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, "memento": {:hex, :memento, "0.3.2", "38cfc8ff9bcb1adff7cbd0f3b78a762636b86dff764729d1c82d0464c539bdd0", [:mix], [], "hexpm", "25cf691a98a0cb70262f4a7543c04bab24648cb2041d937eb64154a8d6f8012b"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, - "mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"}, + "mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, "mimetype_parser": {:hex, :mimetype_parser, "0.1.3", "628ac9fe56aa7edcedb534d68397dd66674ab82493c8ebe39acb9a19b666099d", [:mix], [], "hexpm", "7d8f80c567807ce78cd93c938e7f4b0a20b1aaaaab914bf286f68457d9f7a852"}, "mix_erlang_tasks": {:hex, :mix_erlang_tasks, "0.1.0", "36819fec60b80689eb1380938675af215565a89320a9e29c72c70d97512e4649", [:mix], [], "hexpm", "95d2839c422c482a70c08a8702da8242f86b773f8ab6e8602a4eb72da8da04ed"}, @@ -88,6 +94,7 @@ "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, "number": {:hex, :number, "1.0.3", "932c8a2d478a181c624138958ca88a78070332191b8061717270d939778c9857", [:mix], [{:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "dd397bbc096b2ca965a6a430126cc9cf7b9ef7421130def69bcf572232ca0f18"}, "numbers": {:hex, :numbers, "5.2.4", "f123d5bb7f6acc366f8f445e10a32bd403c8469bdbce8ce049e1f0972b607080", [:mix], [{:coerce, "~> 1.0", [hex: :coerce, repo: "hexpm", optional: false]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "eeccf5c61d5f4922198395bf87a465b6f980b8b862dd22d28198c5e6fab38582"}, + "oauth2": {:hex, :oauth2, "2.0.0", "338382079fe16c514420fa218b0903f8ad2d4bfc0ad0c9f988867dfa246731b0", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "881b8364ac7385f9fddc7949379cbe3f7081da37233a1aa7aab844670a91e7e7"}, "optimal": {:hex, :optimal, "0.3.6", "46bbf52fbbbd238cda81e02560caa84f93a53c75620f1fe19e81e4ae7b07d1dd", [:mix], [], "hexpm", "1a06ea6a653120226b35b283a1cd10039550f2c566edcdec22b29316d73640fd"}, "parallel_stream": {:hex, :parallel_stream, "1.0.6", "b967be2b23f0f6787fab7ed681b4c45a215a81481fb62b01a5b750fa8f30f76c", [:mix], [], "hexpm", "639b2e8749e11b87b9eb42f2ad325d161c170b39b288ac8d04c4f31f8f0823eb"}, "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, @@ -99,7 +106,7 @@ "plug": {:hex, :plug, "1.13.6", "187beb6b67c6cec50503e940f0434ea4692b19384d47e5fdfd701e93cadb4cc2", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02b9c6b9955bce92c829f31d6284bf53c591ca63c4fb9ff81dfd0418667a34ff"}, "plug_cowboy": {:hex, :plug_cowboy, "2.5.2", "62894ccd601cf9597e2c23911ff12798a8a18d237e9739f58a6b04e4988899fe", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ea6e87f774c8608d60c8d34022a7d073bd7680a0a013f049fc62bf35efea1044"}, "plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"}, - "poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"}, + "poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm", "ba8836feea4b394bb718a161fc59a288fe0109b5006d6bdf97b6badfcf6f0f25"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, "postgrex": {:hex, :postgrex, "0.15.13", "7794e697481799aee8982688c261901de493eb64451feee6ea58207d7266d54a", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "3ffb76e1a97cfefe5c6a95632a27ffb67f28871c9741fb585f9d1c3cd2af70f1"}, "prometheus": {:hex, :prometheus, "4.9.1", "ecf9ccf0fdd0fefb13b19f5216aff8b4bdc852171f5c79133bd998ce8210cf65", [:mix, :rebar3], [{:quantile_estimator, "~> 0.2.1", [hex: :quantile_estimator, repo: "hexpm", optional: false]}], "hexpm", "d75e80d7b2c1be6bf296e211e806e939ae3d9e0428f45b4caad1817f028213d3"}, @@ -113,6 +120,7 @@ "que": {:hex, :que, "0.10.1", "788ed0ec92ed69bdf9cfb29bf41a94ca6355b8d44959bd0669cf706e557ac891", [:mix], [{:ex_utils, "~> 0.1.6", [hex: :ex_utils, repo: "hexpm", optional: false]}, {:memento, "~> 0.3.0", [hex: :memento, repo: "hexpm", optional: false]}], "hexpm", "a737b365253e75dbd24b2d51acc1d851049e87baae08cd0c94e2bc5cd65088d5"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "ratio": {:hex, :ratio, "2.4.2", "c8518f3536d49b1b00d88dd20d49f8b11abb7819638093314a6348139f14f9f9", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:numbers, "~> 5.2.0", [hex: :numbers, repo: "hexpm", optional: false]}], "hexpm", "441ef6f73172a3503de65ccf1769030997b0d533b1039422f1e5e0e0b4cbf89e"}, + "redix": {:hex, :redix, "1.1.5", "6fc460d66a5c2287e83e6d73dddc8d527ff59cb4d4f298b41e03a4db8c3b2bd5", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "679afdd4c14502fe9c11387ff1cdcb33065a1cf511097da1eee407f17c7a418b"}, "remote_ip": {:hex, :remote_ip, "1.0.0", "3d7fb45204a5704443f480cee9515e464997f52c35e0a60b6ece1f81484067ae", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9e9fcad4e50c43b5234bb6a9629ed6ab223f3ed07147bd35470e4ee5c8caf907"}, "rustler": {:hex, :rustler, "0.24.0", "b8362a2fee1c9d2c7373b0bfdc98f75bbc02864efcec50df173fe6c4f72d4cc4", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "2773167fca68a6525822ad977b41368ea3c2af876c42ebaa7c9d6bb69b67f1ce"}, "sobelow": {:hex, :sobelow, "0.11.1", "23438964486f8112b41e743bbfd402da3e5b296fdc9eacab29914b79c48916dd", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9897363a7eff96f4809304a90aad819e2ad5e5d24db547af502885146746a53c"}, @@ -127,6 +135,8 @@ "timex": {:hex, :timex, "3.7.9", "790cdfc4acfce434e442f98c02ea6d84d0239073bfd668968f82ac63e9a6788d", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "64691582e5bb87130f721fc709acfb70f24405833998fabf35be968984860ce1"}, "toml": {:hex, :toml, "0.6.2", "38f445df384a17e5d382befe30e3489112a48d3ba4c459e543f748c2f25dd4d1", [:mix], [], "hexpm", "d013e45126d74c0c26a38d31f5e8e9b83ea19fc752470feb9a86071ca5a672fa"}, "tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"}, + "ueberauth": {:hex, :ueberauth, "0.10.1", "6706b410ee6bd9d67eac983ed9dc7fdc1f06b18677d7b8ba71d5725e07cc8826", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bb715b562395c4cc26b2d8e637c6bb0eb8c67d50c0ea543c0f78f06b7e8efdb1"}, + "ueberauth_auth0": {:hex, :ueberauth_auth0, "2.0.0", "f3919834f1f473b39e423f2e90dfc1801929d319634e6649d9e198f4ccd46f3e", [:mix], [{:oauth2, "~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.7", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "7b7b8fd7f2f7314ab910e9327452e5c904296f0dbba227d02b445f25a334a012"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, "wallaby": {:hex, :wallaby, "0.30.1", "81342a34080867ab359aca23de4d1d8c6bbdeb35d8ce2a8c42e42b758d539963", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:httpoison, "~> 0.12 or ~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_ecto, ">= 3.0.0", [hex: :phoenix_ecto, repo: "hexpm", optional: true]}, {:web_driver_client, "~> 0.2.0", [hex: :web_driver_client, repo: "hexpm", optional: false]}], "hexpm", "457251df6a94ff80816524136edbce6400cb1ee979586c90224ff634e9543d78"}, "web_driver_client": {:hex, :web_driver_client, "0.2.0", "63b76cd9eb3b0716ec5467a0f8bead73d3d9612e63f7560d21357f03ad86e31a", [:mix], [{:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:tesla, "~> 1.3", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "83cc6092bc3e74926d1c8455f0ce927d5d1d36707b74d9a65e38c084aab0350f"},