From c1acec5ef7513a7d5fa84e35a7fa7ed8c23b5954 Mon Sep 17 00:00:00 2001 From: Roman Petriv Date: Thu, 23 Nov 2023 16:30:29 +0200 Subject: [PATCH 01/52] fix: add validation to check bridge init log producer (#98) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # What ❔ - validate bridge init log to be produced by bridge contract ## Why ❔ - if token is bridged outside of bridge it can't rely on provided token l1 address --- .../worker/src/token/token.service.spec.ts | 110 +++++++++++++++++- packages/worker/src/token/token.service.ts | 12 +- 2 files changed, 116 insertions(+), 6 deletions(-) diff --git a/packages/worker/src/token/token.service.spec.ts b/packages/worker/src/token/token.service.spec.ts index 309c41a460..b52b51de38 100644 --- a/packages/worker/src/token/token.service.spec.ts +++ b/packages/worker/src/token/token.service.spec.ts @@ -15,7 +15,11 @@ describe("TokenService", () => { let stopGetTokenInfoDurationMetricMock: jest.Mock; beforeEach(async () => { - blockchainServiceMock = mock(); + blockchainServiceMock = mock({ + bridgeAddresses: { + l2Erc20DefaultBridge: "0x0000000000000000000000000000000000001111", + }, + }); tokenRepositoryMock = mock(); stopGetTokenInfoDurationMetricMock = jest.fn(); @@ -60,6 +64,7 @@ describe("TokenService", () => { transactionReceipt = mock({ logs: [], + to: "0x0000000000000000000000000000000000001111", }); deployedContractAddress = mock({ @@ -255,6 +260,54 @@ describe("TokenService", () => { }); }); + describe("when there is a bridge initialization log in transaction receipt which is not produced by the bridge contract", () => { + beforeEach(() => { + transactionReceipt.to = "0x0000000000000000000000000000000000001112"; + transactionReceipt.logs = [ + mock({ + topics: [ + "0x290afdae231a3fc0bbae8b1af63698b0a1d79b21ad17df0342dfb952fe74f8e5", + "0x000000000000000000000000c7e0220d02d549c4846a6ec31d89c3b670ebe35c", + "0x0100014340e955cbf39159da998b3374bee8f3c0b3c75a7a9e3df6b85052379d", + "0x000000000000000000000000dc187378edd8ed1585fb47549cc5fe633295d571", + ], + }), + mock({ + address: "0xdc187378edD8Ed1585fb47549Cc5fe633295d571", + topics: [ + "0xe6b2ac4004ee4493db8844da5db69722d2128345671818c3c41928655a83fb2c", + "0x0000000000000000000000000db321efaa9e380d0b37b55b530cdaa62728b9a3", + ], + data: "0x000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000441444c3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000441444c3100000000000000000000000000000000000000000000000000000000", + }), + ]; + }); + + it("starts the get token info duration metric", async () => { + await tokenService.saveERC20Token(deployedContractAddress, transactionReceipt); + expect(startGetTokenInfoDurationMetricMock).toHaveBeenCalledTimes(1); + }); + + it("gets token data by the contract address", async () => { + await tokenService.saveERC20Token(deployedContractAddress, transactionReceipt); + expect(blockchainServiceMock.getERC20TokenData).toHaveBeenCalledTimes(1); + expect(blockchainServiceMock.getERC20TokenData).toHaveBeenCalledWith(deployedContractAddress.address); + }); + + it("upserts the token without l1Address", async () => { + await tokenService.saveERC20Token(deployedContractAddress, transactionReceipt); + expect(tokenRepositoryMock.upsert).toHaveBeenCalledTimes(1); + expect(tokenRepositoryMock.upsert).toHaveBeenCalledWith({ + ...tokenData, + blockNumber: deployedContractAddress.blockNumber, + transactionHash: deployedContractAddress.transactionHash, + l2Address: deployedContractAddress.address, + l1Address: undefined, + logIndex: deployedContractAddress.logIndex, + }); + }); + }); + describe("when there is a bridge initialize log in transaction receipt for the current token address", () => { let bridgedToken; @@ -311,6 +364,61 @@ describe("TokenService", () => { }); }); + describe("when there is a bridge initialize log in transaction receipt which is not produced by the bridge contract", () => { + beforeEach(() => { + transactionReceipt.to = "0x0000000000000000000000000000000000001112"; + transactionReceipt.logs = [ + mock({ + topics: [ + "0x290afdae231a3fc0bbae8b1af63698b0a1d79b21ad17df0342dfb952fe74f8e5", + "0x000000000000000000000000913389f49358cb49a8e9e984a5871df43f80eb96", + "0x01000125c745537b5254be2ca086aee7fbd5d91789ed15790a942f9422d36447", + "0x0000000000000000000000005a393c95e7bddd0281650023d8c746fb1f596b7b", + ], + }), + mock({ + address: "0x5a393c95e7Bddd0281650023D8C746fB1F596B7b", + topics: [ + "0x81e8e92e5873539605a102eddae7ed06d19bea042099a437cbc3644415eb7404", + "0x000000000000000000000000c8f8ce6491227a6a2ab92e67a64011a4eba1c6cf", + ], + data: "0x000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000134c313131206465706c6f79656420746f204c310000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044c31313100000000000000000000000000000000000000000000000000000000", + }), + ]; + + deployedContractAddress = mock({ + address: "0x5a393c95e7bddd0281650023d8c746fb1f596b7b", + blockNumber: 10, + transactionHash: "transactionHash", + logIndex: 20, + }); + }); + + it("starts the get token info duration metric", async () => { + await tokenService.saveERC20Token(deployedContractAddress, transactionReceipt); + expect(startGetTokenInfoDurationMetricMock).toHaveBeenCalledTimes(1); + }); + + it("gets token data by the contract address", async () => { + await tokenService.saveERC20Token(deployedContractAddress, transactionReceipt); + expect(blockchainServiceMock.getERC20TokenData).toHaveBeenCalledTimes(1); + expect(blockchainServiceMock.getERC20TokenData).toHaveBeenCalledWith(deployedContractAddress.address); + }); + + it("upserts the token without l1Address", async () => { + await tokenService.saveERC20Token(deployedContractAddress, transactionReceipt); + expect(tokenRepositoryMock.upsert).toHaveBeenCalledTimes(1); + expect(tokenRepositoryMock.upsert).toHaveBeenCalledWith({ + ...tokenData, + blockNumber: deployedContractAddress.blockNumber, + transactionHash: deployedContractAddress.transactionHash, + l2Address: deployedContractAddress.address, + l1Address: undefined, + logIndex: deployedContractAddress.logIndex, + }); + }); + }); + describe("if the token symbol is empty", () => { beforeEach(() => { jest.spyOn(blockchainServiceMock, "getERC20TokenData").mockResolvedValueOnce({ diff --git a/packages/worker/src/token/token.service.ts b/packages/worker/src/token/token.service.ts index dbb1963723..cd11f77b16 100644 --- a/packages/worker/src/token/token.service.ts +++ b/packages/worker/src/token/token.service.ts @@ -61,11 +61,13 @@ export class TokenService { l1Address?: string; }; - const bridgeLog = transactionReceipt.logs?.find( - (log) => - isLogOfType(log, [LogType.BridgeInitialization, LogType.BridgeInitialize]) && - log.address.toLowerCase() === contractAddress.address.toLowerCase() - ); + const bridgeLog = + transactionReceipt.to.toLowerCase() === this.blockchainService.bridgeAddresses.l2Erc20DefaultBridge && + transactionReceipt.logs?.find( + (log) => + isLogOfType(log, [LogType.BridgeInitialization, LogType.BridgeInitialize]) && + log.address.toLowerCase() === contractAddress.address.toLowerCase() + ); if (bridgeLog) { const parsedLog = parseLog(CONTRACT_INTERFACES.L2_STANDARD_ERC20, bridgeLog); From c508169a066489668ca82f42d37f1977446f23af Mon Sep 17 00:00:00 2001 From: Vasyl Ivanchuk Date: Tue, 28 Nov 2023 17:25:01 +0200 Subject: [PATCH 02/52] feat: add sepolia production config (#100) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # What ❔ 1. Remove new prover notification since it's not needed anymore. 2. Add Sepolia Testnet production configuration. ## Why ❔ We need a Sepolia block explorer production environment to eventually use it as a main Testnet environment. ## Checklist - [X] PR title corresponds to the body of PR (we generate changelog entries from PRs). - [X] Tests for the changes have been added / updated. --- .../app/src/components/NewProverInfoBox.vue | 44 ------------------- .../app/src/components/batches/InfoTable.vue | 6 --- .../components/batches/InfoTableSection.vue | 3 -- .../app/src/components/blocks/InfoTable.vue | 6 --- .../src/components/blocks/InfoTableBlock.vue | 3 -- packages/app/src/composables/useBatch.ts | 20 +-------- packages/app/src/composables/useBlock.ts | 15 +------ .../app/src/composables/useRuntimeConfig.ts | 3 +- packages/app/src/configs/dev.config.json | 5 --- packages/app/src/configs/index.ts | 1 - packages/app/src/configs/local.config.json | 1 - .../app/src/configs/production.config.json | 21 +++++++-- packages/app/src/configs/staging.config.json | 4 -- packages/app/src/locales/en.json | 10 ++--- packages/app/src/locales/uk.json | 7 +-- packages/app/src/utils/constants.ts | 2 - .../tests/composables/useTransaction.spec.ts | 5 +-- .../features/artifacts/artifactsSet1.feature | 4 +- .../features/artifacts/artifactsSet3.feature | 4 +- .../redirection/redirectionSet3.feature | 6 +-- packages/app/tests/mocks.ts | 2 - 21 files changed, 35 insertions(+), 137 deletions(-) delete mode 100644 packages/app/src/components/NewProverInfoBox.vue diff --git a/packages/app/src/components/NewProverInfoBox.vue b/packages/app/src/components/NewProverInfoBox.vue deleted file mode 100644 index 07636ef04b..0000000000 --- a/packages/app/src/components/NewProverInfoBox.vue +++ /dev/null @@ -1,44 +0,0 @@ - - - - - diff --git a/packages/app/src/components/batches/InfoTable.vue b/packages/app/src/components/batches/InfoTable.vue index 934f06ac9d..e0a0874d46 100644 --- a/packages/app/src/components/batches/InfoTable.vue +++ b/packages/app/src/components/batches/InfoTable.vue @@ -11,7 +11,6 @@ import { useI18n } from "vue-i18n"; import { useWindowSize } from "@vueuse/core"; -import NewProverInfoBox from "@/components/NewProverInfoBox.vue"; import InfoTableSection from "@/components/batches/InfoTableSection.vue"; import CopyContent from "@/components/common/table/fields/CopyContent.vue"; import TimeField from "@/components/common/table/fields/TimeField.vue"; @@ -88,11 +87,6 @@ const tableInfoItems = computed(() => { url: currentNetwork.value.l1ExplorerUrl ? `${currentNetwork.value.l1ExplorerUrl}/tx/${props.batch[key]}` : undefined, - ...(key === "proveTxHash" && - props.batch.isProvenByNewProver && { - additionalContentComponent: NewProverInfoBox, - additionalContentProps: { context: "batch" }, - }), }, { label: t(`batches.${timeKey}`), diff --git a/packages/app/src/components/batches/InfoTableSection.vue b/packages/app/src/components/batches/InfoTableSection.vue index 49ea0ee25c..86e332fcab 100644 --- a/packages/app/src/components/batches/InfoTableSection.vue +++ b/packages/app/src/components/batches/InfoTableSection.vue @@ -17,9 +17,6 @@ -
- -
diff --git a/packages/app/src/components/blocks/InfoTable.vue b/packages/app/src/components/blocks/InfoTable.vue index 1b3eb3bdaf..9c2ddc43c4 100644 --- a/packages/app/src/components/blocks/InfoTable.vue +++ b/packages/app/src/components/blocks/InfoTable.vue @@ -11,7 +11,6 @@ import { useI18n } from "vue-i18n"; import { useWindowSize } from "@vueuse/core"; -import NewProverInfoBox from "@/components/NewProverInfoBox.vue"; import InfoTableBlock from "@/components/blocks/InfoTableBlock.vue"; import CopyContent from "@/components/common/table/fields/CopyContent.vue"; import TimeField from "@/components/common/table/fields/TimeField.vue"; @@ -113,11 +112,6 @@ const tableInfoItems = computed(() => { url: currentNetwork.value.l1ExplorerUrl ? `${currentNetwork.value.l1ExplorerUrl}/tx/${props.block[key]}` : undefined, - ...(key === "proveTxHash" && - props.block.isProvenByNewProver && { - additionalContentComponent: NewProverInfoBox, - additionalContentProps: { context: "block" }, - }), }, { label: t(`blocks.table.${timeKey}`), diff --git a/packages/app/src/components/blocks/InfoTableBlock.vue b/packages/app/src/components/blocks/InfoTableBlock.vue index 36c6b0e782..cade76c928 100644 --- a/packages/app/src/components/blocks/InfoTableBlock.vue +++ b/packages/app/src/components/blocks/InfoTableBlock.vue @@ -26,9 +26,6 @@ -
- -
diff --git a/packages/app/src/composables/useBatch.ts b/packages/app/src/composables/useBatch.ts index 7eeb2916ff..57a65b2599 100644 --- a/packages/app/src/composables/useBatch.ts +++ b/packages/app/src/composables/useBatch.ts @@ -4,34 +4,19 @@ import { $fetch, FetchError } from "ohmyfetch"; import useContext from "@/composables/useContext"; -export type BatchDetails = Api.Response.BatchDetails & { - isProvenByNewProver?: boolean; -}; +export type BatchDetails = Api.Response.BatchDetails; export default (context = useContext()) => { const isRequestPending = ref(false); const isRequestFailed = ref(false); const batchItem = ref(null); - const getBatchNewProof = async (id: string) => { - try { - return await $fetch(`${context.currentNetwork.value.newProverUrl}/proof_${id}.bin`, { method: "HEAD" }); - } catch (error: unknown) { - return null; - } - }; - const getById = async (id: string) => { isRequestPending.value = true; isRequestFailed.value = false; try { - const batch = await $fetch(`${context.currentNetwork.value.apiUrl}/batches/${id}`); - if (batch.proveTxHash) { - const proof = await getBatchNewProof(id); - batch.isProvenByNewProver = !!proof; - } - batchItem.value = batch; + batchItem.value = await $fetch(`${context.currentNetwork.value.apiUrl}/batches/${id}`); } catch (error: unknown) { batchItem.value = null; if (!(error instanceof FetchError) || error.response?.status !== 404) { @@ -43,7 +28,6 @@ export default (context = useContext()) => { }; return { - getBatchNewProof, getById, batchItem, isRequestPending, diff --git a/packages/app/src/composables/useBlock.ts b/packages/app/src/composables/useBlock.ts index c2002feeb5..c605f43de8 100644 --- a/packages/app/src/composables/useBlock.ts +++ b/packages/app/src/composables/useBlock.ts @@ -43,25 +43,12 @@ export default (context = useContext()) => { const isRequestFailed = ref(false); const blockItem = ref(null); - const getBatchNewProof = async (id: number) => { - try { - return await $fetch(`${context.currentNetwork.value.newProverUrl}/proof_${id}.bin`, { method: "HEAD" }); - } catch (error: unknown) { - return null; - } - }; - const getById = async (id: string) => { isRequestPending.value = true; isRequestFailed.value = false; try { - const data = await $fetch(`${context.currentNetwork.value.apiUrl}/blocks/${id}`); - if (data.l1BatchNumber && data.proveTxHash) { - const proof = await getBatchNewProof(data.l1BatchNumber); - data.isProvenByNewProver = !!proof; - } - blockItem.value = data; + blockItem.value = await $fetch(`${context.currentNetwork.value.apiUrl}/blocks/${id}`); } catch (error: unknown) { blockItem.value = null; if (!(error instanceof FetchError) || error.response?.status !== 404) { diff --git a/packages/app/src/composables/useRuntimeConfig.ts b/packages/app/src/composables/useRuntimeConfig.ts index eca6a21706..b0a053ee8e 100644 --- a/packages/app/src/composables/useRuntimeConfig.ts +++ b/packages/app/src/composables/useRuntimeConfig.ts @@ -8,11 +8,10 @@ export const DEFAULT_NETWORK: NetworkConfig = { icon: "/images/icons/zksync-arrows.svg", l1ExplorerUrl: "https://goerli.etherscan.io", l2ChainId: 280, - l2NetworkName: "zkSync Era Testnet", + l2NetworkName: "zkSync Era Goerli Testnet", l2WalletUrl: "https://goerli.portal.zksync.io/", maintenance: false, name: "goerli", - newProverUrl: "https://storage.googleapis.com/zksync-era-testnet-proofs/proofs_fri", published: true, rpcUrl: "https://testnet.era.zksync.dev", }; diff --git a/packages/app/src/configs/dev.config.json b/packages/app/src/configs/dev.config.json index 052fed4a04..7f69a0d55e 100644 --- a/packages/app/src/configs/dev.config.json +++ b/packages/app/src/configs/dev.config.json @@ -12,7 +12,6 @@ "l2WalletUrl": "http://localhost:3000", "maintenance": false, "name": "local", - "newProverUrl": "https://storage.googleapis.com/zksync-era-testnet-proofs/proofs_fri", "published": true, "rpcUrl": "http://localhost:3050" }, @@ -30,7 +29,6 @@ "l2WalletUrl": "https://goerli.staging-portal.zksync.dev/", "maintenance": false, "name": "goerli", - "newProverUrl": "https://storage.googleapis.com/zksync-era-testnet-proofs/proofs_fri", "published": true, "rpcUrl": "https://testnet.era.zksync.dev" }, @@ -46,7 +44,6 @@ "l2WalletUrl": "https://staging-portal.zksync.dev/?network=era-boojnet", "maintenance": false, "name": "sepolia", - "newProverUrl": "https://storage.googleapis.com/zksync-era-testnet-proofs/proofs_fri", "published": true, "rpcUrl": "https://sepolia.era.zksync.dev" }, @@ -63,7 +60,6 @@ "l2WalletUrl": "https://goerli-beta.staging-portal.zksync.dev/", "maintenance": false, "name": "goerli-beta", - "newProverUrl": "https://storage.googleapis.com/zksync-era-stage-proofs/proofs_fri", "published": true, "rpcUrl": "https://z2-dev-api.zksync.dev" }, @@ -81,7 +77,6 @@ "l2WalletUrl": "https://staging-portal.zksync.dev/", "maintenance": false, "name": "mainnet", - "newProverUrl": "https://storage.googleapis.com/zksync-era-mainnet-proofs/proofs_fri", "published": true, "rpcUrl": "https://mainnet.era.zksync.io" } diff --git a/packages/app/src/configs/index.ts b/packages/app/src/configs/index.ts index 3856403daf..5660e1ccdd 100644 --- a/packages/app/src/configs/index.ts +++ b/packages/app/src/configs/index.ts @@ -3,7 +3,6 @@ export type NetworkConfig = { icon: string; verificationApiUrl?: string; apiUrl: string; - newProverUrl: string; rpcUrl: string; bridgeUrl?: string; l2NetworkName: string; diff --git a/packages/app/src/configs/local.config.json b/packages/app/src/configs/local.config.json index d88a9f9ca1..afd0e82298 100644 --- a/packages/app/src/configs/local.config.json +++ b/packages/app/src/configs/local.config.json @@ -12,7 +12,6 @@ "l2WalletUrl": "http://localhost:3000", "maintenance": false, "name": "local", - "newProverUrl": "https://storage.googleapis.com/zksync-era-testnet-proofs/proofs_fri", "published": true, "rpcUrl": "http://localhost:3050" } diff --git a/packages/app/src/configs/production.config.json b/packages/app/src/configs/production.config.json index 3df42c773d..bc25acca32 100644 --- a/packages/app/src/configs/production.config.json +++ b/packages/app/src/configs/production.config.json @@ -10,14 +10,30 @@ "icon": "/images/icons/zksync-arrows.svg", "l1ExplorerUrl": "https://goerli.etherscan.io", "l2ChainId": 280, - "l2NetworkName": "zkSync Era Testnet", + "l2NetworkName": "zkSync Era Goerli Testnet", "l2WalletUrl": "https://goerli.portal.zksync.io/", "maintenance": false, "name": "goerli", - "newProverUrl": "https://storage.googleapis.com/zksync-era-testnet-proofs/proofs_fri", "published": true, "rpcUrl": "https://testnet.era.zksync.dev" }, + { + "apiUrl": "https://block-explorer-api.sepolia.zksync.dev", + "verificationApiUrl": "https://explorer.sepolia.era.zksync.dev", + "bridgeUrl": "https://bridge.zksync.io", + "hostnames": [ + "https://sepolia.explorer.zksync.io" + ], + "icon": "/images/icons/zksync-arrows.svg", + "l1ExplorerUrl": "https://sepolia.etherscan.io", + "l2ChainId": 300, + "l2NetworkName": "zkSync Era Sepolia Testnet", + "l2WalletUrl": "https://portal.zksync.io/", + "maintenance": false, + "name": "sepolia", + "published": true, + "rpcUrl": "https://sepolia.era.zksync.dev" + }, { "apiUrl": "https://block-explorer-api.mainnet.zksync.io", "verificationApiUrl": "https://zksync2-mainnet-explorer.zksync.io", @@ -32,7 +48,6 @@ "l2WalletUrl": "https://portal.zksync.io/", "maintenance": false, "name": "mainnet", - "newProverUrl": "https://storage.googleapis.com/zksync-era-mainnet-proofs/proofs_fri", "published": true, "rpcUrl": "https://mainnet.era.zksync.io" } diff --git a/packages/app/src/configs/staging.config.json b/packages/app/src/configs/staging.config.json index 17d966e752..7be5e57441 100644 --- a/packages/app/src/configs/staging.config.json +++ b/packages/app/src/configs/staging.config.json @@ -14,7 +14,6 @@ "l2WalletUrl": "https://goerli.staging-portal.zksync.dev/", "maintenance": false, "name": "goerli", - "newProverUrl": "https://storage.googleapis.com/zksync-era-testnet-proofs/proofs_fri", "published": true, "rpcUrl": "https://testnet.era.zksync.dev" }, @@ -30,7 +29,6 @@ "l2WalletUrl": "https://staging-portal.zksync.dev/?network=era-boojnet", "maintenance": false, "name": "sepolia", - "newProverUrl": "https://storage.googleapis.com/zksync-era-testnet-proofs/proofs_fri", "published": true, "rpcUrl": "https://sepolia.era.zksync.dev" }, @@ -47,7 +45,6 @@ "l2WalletUrl": "https://goerli-beta.staging-portal.zksync.dev/", "maintenance": false, "name": "goerli-beta", - "newProverUrl": "https://storage.googleapis.com/zksync-era-stage-proofs/proofs_fri", "published": true, "rpcUrl": "https://z2-dev-api.zksync.dev" }, @@ -65,7 +62,6 @@ "l2WalletUrl": "https://staging-portal.zksync.dev/", "maintenance": false, "name": "mainnet", - "newProverUrl": "https://storage.googleapis.com/zksync-era-mainnet-proofs/proofs_fri", "published": true, "rpcUrl": "https://mainnet.era.zksync.io" } diff --git a/packages/app/src/locales/en.json b/packages/app/src/locales/en.json index 26d044011f..f11d13509f 100644 --- a/packages/app/src/locales/en.json +++ b/packages/app/src/locales/en.json @@ -1,6 +1,5 @@ { "general": { - "checkHere": "Check here", "l2NetworkName": "zkSync Era", "l1NetworkName": "Ethereum" }, @@ -60,8 +59,7 @@ "executedAtTooltip": "Time when block was executed", "notYetExecuted": "Not yet executed", "notFound": "Not Found", - "notFoundHomePage": "We haven't had any blocks yet. Please, check again later.", - "newProverInfo": "This block was also proved with our upcoming new prover, Boojum. Want to learn more and verify yourself?" + "notFoundHomePage": "We haven't had any blocks yet. Please, check again later." }, "status": { "verified": "Verified", @@ -164,8 +162,7 @@ "unableToLoadMore": "Unable to load more", "tryAgain": "Try again ->" }, - "unknown": "Unknown", - "newProverInfo": "This transaction was also proved with our upcoming new prover, Boojum. Want to learn more and verify yourself?" + "unknown": "Unknown" }, "logs": { "name": "Name", @@ -273,8 +270,7 @@ "age": "Age", "notFound": "Not Found", "notFoundHomePage": "We haven't had any batches yet. Please, check again later.", - "transactionsShort": "txns", - "newProverInfo": "This batch was also proved with our upcoming new prover, Boojum. Want to learn more and verify yourself?" + "transactionsShort": "txns" }, "tooltipInfo": "Latest batches submitted to Ethereum Network", "status": { diff --git a/packages/app/src/locales/uk.json b/packages/app/src/locales/uk.json index 742d34ac0a..579c69d370 100644 --- a/packages/app/src/locales/uk.json +++ b/packages/app/src/locales/uk.json @@ -1,6 +1,5 @@ { "general": { - "checkHere": "Перевірити тут", "l2NetworkName": "zkSync Era", "l1NetworkName": "Ethereum" }, @@ -37,8 +36,7 @@ "notYetProven": "Ще не підтверджений", "executeTxHash": "Виконання хешу", "executedAt": "Виконаний", - "notYetExecuted": "Ще не виконано", - "newProverInfo": "Цей блок був також підтверджений нашим новим майбутнім прувером, Boojum. Бажаєте дізнатися більше та перевірити самостійно?" + "notYetExecuted": "Ще не виконано" }, "status": { "verified": "Перевірено", @@ -99,8 +97,7 @@ "showMore": "Показати більше транзакцій ->", "unableToLoadMore": "Неможливо завантажити більше", "tryAgain": "Спробуйте знову ->" - }, - "newProverInfo": "Ця транзакція була також підтверджена нашим новим майбутнім прувером, Boojum. Бажаєте дізнатися більше та перевірити самостійно?" + } }, "logs": { "address": "Адреса", diff --git a/packages/app/src/utils/constants.ts b/packages/app/src/utils/constants.ts index 66ad54e518..834827ee7b 100644 --- a/packages/app/src/utils/constants.ts +++ b/packages/app/src/utils/constants.ts @@ -2,8 +2,6 @@ import { checksumAddress } from "./formatters"; export const ETH_TOKEN_L2_ADDRESS = checksumAddress("0x000000000000000000000000000000000000800A"); -export const NEW_PROVER_CLI_URL = "https://github.com/matter-labs/era-boojum-validator-cli"; - export const PROXY_CONTRACT_IMPLEMENTATION_ABI = [ { inputs: [], diff --git a/packages/app/tests/composables/useTransaction.spec.ts b/packages/app/tests/composables/useTransaction.spec.ts index 089e865f93..6ca5c1927e 100644 --- a/packages/app/tests/composables/useTransaction.spec.ts +++ b/packages/app/tests/composables/useTransaction.spec.ts @@ -234,9 +234,7 @@ vi.mock("ohmyfetch", async () => { }, }); } - if (url.endsWith(".bin")) { - return Promise.resolve({}); - } + if (url.includes("/0x00000d03dd8c01f1049143cf9c4c817e4b167f1d1b83e5c6f0f10d89ba1e7bcf")) { const error = new mod.FetchError("Not found"); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -602,7 +600,6 @@ describe("useTransaction:", () => { const { transaction, isRequestFailed, getByHash } = useTransaction({ currentNetwork: { value: { - newProverUrl: "http://prover.url", apiUrl: "http://api.url", }, }, diff --git a/packages/app/tests/e2e/features/artifacts/artifactsSet1.feature b/packages/app/tests/e2e/features/artifacts/artifactsSet1.feature index cf5f9e2ccc..c001055d46 100644 --- a/packages/app/tests/e2e/features/artifacts/artifactsSet1.feature +++ b/packages/app/tests/e2e/features/artifacts/artifactsSet1.feature @@ -92,8 +92,8 @@ Feature: Main Page Then Check the "" value is actual for "" switcher Examples: - | Value | Dropdown | - | zkSync Era Testnet | network | + | Value | Dropdown | + | zkSync Era Goerli Testnet | network | Scenario: Network stats is displayed Then Element with "text" "Network Stats" should be "visible" diff --git a/packages/app/tests/e2e/features/artifacts/artifactsSet3.feature b/packages/app/tests/e2e/features/artifacts/artifactsSet3.feature index 2520f2ec52..652968f457 100644 --- a/packages/app/tests/e2e/features/artifacts/artifactsSet3.feature +++ b/packages/app/tests/e2e/features/artifacts/artifactsSet3.feature @@ -65,7 +65,7 @@ Feature: Main Page @id249 @testnet @testnetSmokeSuite Scenario Outline: Verify table contains "" column name on Tokens page Given I go to page "/tokenlist" - And Table "Tokens" should have "5" rows + And Table "Tokens" should have "1" rows Then Column with "" name is visible Examples: @@ -77,7 +77,7 @@ Feature: Main Page @id249 @mainnet Scenario Outline: Verify table contains "" column name on Tokens page Given I go to page "/tokenlist" - # And Table "Tokens" should have "5" rows + # And Table "Tokens" should have "56" rows Then Column with "" name is visible Examples: diff --git a/packages/app/tests/e2e/features/redirection/redirectionSet3.feature b/packages/app/tests/e2e/features/redirection/redirectionSet3.feature index 9e8f9ed129..3d6c238a45 100644 --- a/packages/app/tests/e2e/features/redirection/redirectionSet3.feature +++ b/packages/app/tests/e2e/features/redirection/redirectionSet3.feature @@ -74,6 +74,6 @@ Feature: Redirection Then Current page have "" address Examples: - | Initial page | Network | url | - | /address/0x000000000000000000000000000000000000800A | zkSync Era Mainnet | /address/0x000000000000000000000000000000000000800A/?network=mainnet | - | /address/0x000000000000000000000000000000000000800A | zkSync Era Testnet | /address/0x000000000000000000000000000000000000800A/?network=goerli | + | Initial page | Network | url | + | /address/0x000000000000000000000000000000000000800A | zkSync Era Mainnet | /address/0x000000000000000000000000000000000000800A/?network=mainnet | + | /address/0x000000000000000000000000000000000000800A | zkSync Era Goerli Testnet | /address/0x000000000000000000000000000000000000800A/?network=goerli | diff --git a/packages/app/tests/mocks.ts b/packages/app/tests/mocks.ts index 109d5f2c51..33dd3858dd 100644 --- a/packages/app/tests/mocks.ts +++ b/packages/app/tests/mocks.ts @@ -47,7 +47,6 @@ export const GOERLI_NETWORK: NetworkConfig = { maintenance: false, published: true, hostnames: [], - newProverUrl: "", }; export const GOERLI_BETA_NETWORK: NetworkConfig = { name: "goerli-beta", @@ -62,7 +61,6 @@ export const GOERLI_BETA_NETWORK: NetworkConfig = { maintenance: false, published: true, hostnames: ["https://goerli-beta.staging-scan-v2.zksync.dev/"], - newProverUrl: "", }; export const useContractEventsMock = (params: any = {}) => { From 16298174d7e84c55848c4083f78f305bd937f7c5 Mon Sep 17 00:00:00 2001 From: Roman Petriv Date: Wed, 29 Nov 2023 15:06:47 +0200 Subject: [PATCH 03/52] feat: add coingecko token offchain data provider (#102) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # What ❔ - add coingecko tokens off-chain data provider ## Why ❔ - to support both L1 and L2 zksync tokens ## Checklist - [+ ] PR title corresponds to the body of PR (we generate changelog entries from PRs). - [ +] Tests for the changes have been added / updated. --- packages/api/src/common/types.ts | 1 + .../api/src/token/token.controller.spec.ts | 5 +- packages/api/src/token/token.controller.ts | 1 + packages/api/src/token/token.service.ts | 3 +- .../app/src/composables/useTokenLibrary.ts | 18 +- packages/app/src/locales/en.json | 1 + packages/app/src/locales/uk.json | 1 + packages/app/src/views/TokensView.vue | 20 +- .../tests/composables/useTokenLibrary.spec.ts | 48 ++- packages/worker/.env.example | 5 +- packages/worker/src/app.module.ts | 18 +- packages/worker/src/config.spec.ts | 6 +- packages/worker/src/config.ts | 11 +- .../src/repositories/token.repository.spec.ts | 55 +++ .../src/repositories/token.repository.ts | 15 +- ...coingeckoTokenOffChainDataProvider.spec.ts | 315 ++++++++++++++++++ .../coingeckoTokenOffChainDataProvider.ts | 204 ++++++++++++ ...portalsFiTokenOffChainDataProvider.spec.ts | 40 +-- .../portalsFiTokenOffChainDataProvider.ts | 35 +- .../tokenOffChainDataProvider.abstract.ts | 9 +- .../tokenOffChainDataSaver.service.spec.ts | 47 +-- .../tokenOffChainDataSaver.service.ts | 52 ++- 22 files changed, 775 insertions(+), 135 deletions(-) create mode 100644 packages/worker/src/token/tokenOffChainData/providers/coingecko/coingeckoTokenOffChainDataProvider.spec.ts create mode 100644 packages/worker/src/token/tokenOffChainData/providers/coingecko/coingeckoTokenOffChainDataProvider.ts rename packages/worker/src/token/tokenOffChainData/providers/{ => portalsFi}/portalsFiTokenOffChainDataProvider.spec.ts (84%) rename packages/worker/src/token/tokenOffChainData/providers/{ => portalsFi}/portalsFiTokenOffChainDataProvider.ts (83%) diff --git a/packages/api/src/common/types.ts b/packages/api/src/common/types.ts index eb37a7b7ae..743cd580ca 100644 --- a/packages/api/src/common/types.ts +++ b/packages/api/src/common/types.ts @@ -6,6 +6,7 @@ interface IPaginationFilterOptions { blockNumber?: number; address?: string; l1BatchNumber?: number; + minLiquidity?: number; } export interface IPaginationOptions extends NestIPaginationOptions { diff --git a/packages/api/src/token/token.controller.spec.ts b/packages/api/src/token/token.controller.spec.ts index aca8783e78..2b6cfe890f 100644 --- a/packages/api/src/token/token.controller.spec.ts +++ b/packages/api/src/token/token.controller.spec.ts @@ -52,7 +52,10 @@ describe("TokenController", () => { it("queries tokens with the specified options", async () => { await controller.getTokens(pagingOptions, 1000); expect(serviceMock.findAll).toHaveBeenCalledTimes(1); - expect(serviceMock.findAll).toHaveBeenCalledWith({ minLiquidity: 1000 }, { ...pagingOptions, route: "tokens" }); + expect(serviceMock.findAll).toHaveBeenCalledWith( + { minLiquidity: 1000 }, + { ...pagingOptions, filterOptions: { minLiquidity: 1000 }, route: "tokens" } + ); }); it("returns the tokens", async () => { diff --git a/packages/api/src/token/token.controller.ts b/packages/api/src/token/token.controller.ts index 44ea613065..660f18ae3f 100644 --- a/packages/api/src/token/token.controller.ts +++ b/packages/api/src/token/token.controller.ts @@ -47,6 +47,7 @@ export class TokenController { minLiquidity, }, { + filterOptions: { minLiquidity }, ...pagingOptions, route: entityName, } diff --git a/packages/api/src/token/token.service.ts b/packages/api/src/token/token.service.ts index c4dd045655..4adafbc315 100644 --- a/packages/api/src/token/token.service.ts +++ b/packages/api/src/token/token.service.ts @@ -1,7 +1,8 @@ import { Injectable } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { Repository, FindOptionsSelect, MoreThanOrEqual } from "typeorm"; -import { Pagination, IPaginationOptions } from "nestjs-typeorm-paginate"; +import { Pagination } from "nestjs-typeorm-paginate"; +import { IPaginationOptions } from "../common/types"; import { paginate } from "../common/utils"; import { Token, ETH_TOKEN } from "./token.entity"; diff --git a/packages/app/src/composables/useTokenLibrary.ts b/packages/app/src/composables/useTokenLibrary.ts index 9c92820b45..f6e71044cd 100644 --- a/packages/app/src/composables/useTokenLibrary.ts +++ b/packages/app/src/composables/useTokenLibrary.ts @@ -7,10 +7,20 @@ import useContext, { type Context } from "@/composables/useContext"; const retrieveTokens = useMemoize( async (context: Context): Promise => { - const tokensResponse = await $fetch>( - `${context.currentNetwork.value.apiUrl}/tokens?minLiquidity=0&limit=100` - ); - return tokensResponse.items; + const tokens = []; + let page = 1; + let hasMore = true; + + while (hasMore) { + const tokensResponse = await $fetch>( + `${context.currentNetwork.value.apiUrl}/tokens?minLiquidity=0&limit=100&page=${page}` + ); + tokens.push(...tokensResponse.items); + page++; + hasMore = tokensResponse.meta.totalPages > tokensResponse.meta.currentPage; + } + + return tokens; }, { getKey(context: Context) { diff --git a/packages/app/src/locales/en.json b/packages/app/src/locales/en.json index f11d13509f..3f6a3c4810 100644 --- a/packages/app/src/locales/en.json +++ b/packages/app/src/locales/en.json @@ -583,6 +583,7 @@ "tokenListView": { "title": "Token List", "heading": "Tokens", + "offChainDataPoweredBy": "Off-chain data powered by", "table": { "tokenName": "Token Name", "price": "Price", diff --git a/packages/app/src/locales/uk.json b/packages/app/src/locales/uk.json index 579c69d370..47ad5b8c56 100644 --- a/packages/app/src/locales/uk.json +++ b/packages/app/src/locales/uk.json @@ -325,6 +325,7 @@ "tokenListView": { "title": "Список Токенів", "heading": "Токени", + "offChainDataPoweredBy": "Off-chain дані взяті з", "table": { "tokenName": "Назва Токена", "price": "Ціна", diff --git a/packages/app/src/views/TokensView.vue b/packages/app/src/views/TokensView.vue index 8d4cb8f0ab..7cedbb96b6 100644 --- a/packages/app/src/views/TokensView.vue +++ b/packages/app/src/views/TokensView.vue @@ -4,7 +4,13 @@ -

{{ t("tokenListView.heading") }}

+
+

{{ t("tokenListView.heading") }}

+
+ {{ t("tokenListView.offChainDataPoweredBy") }}{{ " " }} + CoinGecko API +
+
{{ t("failedRequest") }} @@ -51,4 +57,16 @@ getTokens(); .tokens-container { @apply mt-8; } + +.tokens-header { + @apply flex justify-between items-end gap-4; + + .coingecko-attribution { + @apply mr-1 text-gray-300; + + a { + @apply text-blue-100; + } + } +} diff --git a/packages/app/tests/composables/useTokenLibrary.spec.ts b/packages/app/tests/composables/useTokenLibrary.spec.ts index c6eca5bb79..1e7c734e16 100644 --- a/packages/app/tests/composables/useTokenLibrary.spec.ts +++ b/packages/app/tests/composables/useTokenLibrary.spec.ts @@ -13,16 +13,40 @@ vi.mock("ohmyfetch", async () => { const mod = await vi.importActual("ohmyfetch"); return { ...mod, - $fetch: vi.fn().mockResolvedValue([ - { - decimals: 18, - iconURL: "https://icon.url", - l1Address: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", - l2Address: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", - name: "Ether", - symbol: "ETH", - } as Api.Response.Token, - ]), + $fetch: vi + .fn() + .mockResolvedValueOnce({ + items: [ + { + decimals: 18, + iconURL: "https://icon.url", + l1Address: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + l2Address: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + name: "Ether", + symbol: "ETH", + } as Api.Response.Token, + ], + meta: { + totalPages: 2, + currentPage: 1, + }, + }) + .mockResolvedValueOnce({ + items: [ + { + decimals: 18, + iconURL: "https://icon2.url", + l1Address: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeef", + l2Address: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeef", + name: "Ether2", + symbol: "ETH2", + } as Api.Response.Token, + ], + meta: { + totalPages: 2, + currentPage: 2, + }, + }), }; }); @@ -31,7 +55,9 @@ describe("useTokenLibrary:", () => { const { getTokens } = useTokenLibrary(); await getTokens(); await getTokens(); - expect($fetch).toHaveBeenCalledTimes(1); + await getTokens(); + await getTokens(); + expect($fetch).toHaveBeenCalledTimes(2); }); it("sets isRequestPending to true when request is pending", async () => { const { isRequestPending, getTokens } = useTokenLibrary(); diff --git a/packages/worker/.env.example b/packages/worker/.env.example index eb575ece89..7402762a37 100644 --- a/packages/worker/.env.example +++ b/packages/worker/.env.example @@ -32,7 +32,10 @@ DISABLE_BLOCKS_REVERT=false ENABLE_TOKEN_OFFCHAIN_DATA_SAVER=false UPDATE_TOKEN_OFFCHAIN_DATA_INTERVAL=86400000 -TOKEN_OFFCHAIN_DATA_MIN_LIQUIDITY_FILTER=0 +SELECTED_TOKEN_OFFCHAIN_DATA_PROVIDER=coingecko FROM_BLOCK=0 TO_BLOCK= + +COINGECKO_IS_PRO_PLAN=false +COINGECKO_API_KEY= diff --git a/packages/worker/src/app.module.ts b/packages/worker/src/app.module.ts index 43ce3900fb..7fc4504e81 100644 --- a/packages/worker/src/app.module.ts +++ b/packages/worker/src/app.module.ts @@ -1,8 +1,8 @@ import { Module, Logger } from "@nestjs/common"; import { TypeOrmModule } from "@nestjs/typeorm"; -import { ConfigModule } from "@nestjs/config"; +import { ConfigModule, ConfigService } from "@nestjs/config"; import { EventEmitterModule } from "@nestjs/event-emitter"; -import { HttpModule } from "@nestjs/axios"; +import { HttpModule, HttpService } from "@nestjs/axios"; import { PrometheusModule } from "@willsoto/nestjs-prometheus"; import config from "./config"; import { HealthModule } from "./health/health.module"; @@ -18,7 +18,8 @@ import { BalanceService, BalancesCleanerService } from "./balance"; import { TransferService } from "./transfer/transfer.service"; import { TokenService } from "./token/token.service"; import { TokenOffChainDataProvider } from "./token/tokenOffChainData/tokenOffChainDataProvider.abstract"; -import { PortalsFiTokenOffChainDataProvider } from "./token/tokenOffChainData/providers/portalsFiTokenOffChainDataProvider"; +import { CoingeckoTokenOffChainDataProvider } from "./token/tokenOffChainData/providers/coingecko/coingeckoTokenOffChainDataProvider"; +import { PortalsFiTokenOffChainDataProvider } from "./token/tokenOffChainData/providers/portalsFi/portalsFiTokenOffChainDataProvider"; import { TokenOffChainDataSaverService } from "./token/tokenOffChainData/tokenOffChainDataSaver.service"; import { CounterModule } from "./counter/counter.module"; import { @@ -100,7 +101,16 @@ import { UnitOfWorkModule } from "./unitOfWork"; TokenService, { provide: TokenOffChainDataProvider, - useClass: PortalsFiTokenOffChainDataProvider, + useFactory: (configService: ConfigService, httpService: HttpService) => { + const selectedProvider = configService.get("tokens.selectedTokenOffChainDataProvider"); + switch (selectedProvider) { + case "portalsFi": + return new PortalsFiTokenOffChainDataProvider(httpService); + default: + return new CoingeckoTokenOffChainDataProvider(configService, httpService); + } + }, + inject: [ConfigService, HttpService], }, TokenOffChainDataSaverService, BatchRepository, diff --git a/packages/worker/src/config.spec.ts b/packages/worker/src/config.spec.ts index 2b5ae3909a..89c3d438f4 100644 --- a/packages/worker/src/config.spec.ts +++ b/packages/worker/src/config.spec.ts @@ -45,7 +45,11 @@ describe("config", () => { tokens: { enableTokenOffChainDataSaver: false, updateTokenOffChainDataInterval: 86_400_000, - tokenOffChainDataMinLiquidityFilter: 0, + tokenOffChainDataProviders: ["coingecko", "portalsFi"], + selectedTokenOffChainDataProvider: "coingecko", + coingecko: { + isProPlan: false, + }, }, metrics: { collectDbConnectionPoolMetricsInterval: 10000, diff --git a/packages/worker/src/config.ts b/packages/worker/src/config.ts index bf14f74486..53e0ed8258 100644 --- a/packages/worker/src/config.ts +++ b/packages/worker/src/config.ts @@ -21,9 +21,11 @@ export default () => { DISABLE_BLOCKS_REVERT, ENABLE_TOKEN_OFFCHAIN_DATA_SAVER, UPDATE_TOKEN_OFFCHAIN_DATA_INTERVAL, - TOKEN_OFFCHAIN_DATA_MIN_LIQUIDITY_FILTER, + SELECTED_TOKEN_OFFCHAIN_DATA_PROVIDER, FROM_BLOCK, TO_BLOCK, + COINGECKO_IS_PRO_PLAN, + COINGECKO_API_KEY, } = process.env; return { @@ -59,7 +61,12 @@ export default () => { tokens: { enableTokenOffChainDataSaver: ENABLE_TOKEN_OFFCHAIN_DATA_SAVER === "true", updateTokenOffChainDataInterval: parseInt(UPDATE_TOKEN_OFFCHAIN_DATA_INTERVAL, 10) || 86_400_000, - tokenOffChainDataMinLiquidityFilter: parseInt(TOKEN_OFFCHAIN_DATA_MIN_LIQUIDITY_FILTER, 10) || 0, + tokenOffChainDataProviders: ["coingecko", "portalsFi"], + selectedTokenOffChainDataProvider: SELECTED_TOKEN_OFFCHAIN_DATA_PROVIDER || "coingecko", + coingecko: { + isProPlan: COINGECKO_IS_PRO_PLAN === "true", + apiKey: COINGECKO_API_KEY, + }, }, metrics: { collectDbConnectionPoolMetricsInterval: parseInt(COLLECT_DB_CONNECTION_POOL_METRICS_INTERVAL, 10) || 10000, diff --git a/packages/worker/src/repositories/token.repository.spec.ts b/packages/worker/src/repositories/token.repository.spec.ts index 8d5955961c..f0be5707eb 100644 --- a/packages/worker/src/repositories/token.repository.spec.ts +++ b/packages/worker/src/repositories/token.repository.spec.ts @@ -174,6 +174,61 @@ describe("TokenRepository", () => { }); describe("updateTokenOffChainData", () => { + it("throws error when no l1Address or l2Address provided", async () => { + const updatedAt = new Date(); + await expect( + repository.updateTokenOffChainData({ + liquidity: 1000000, + usdPrice: 55.89037747, + updatedAt, + }) + ).rejects.toThrowError("l1Address or l2Address must be provided"); + }); + + it("updates token offchain data using l1Address when provided", async () => { + const updatedAt = new Date(); + await repository.updateTokenOffChainData({ + l1Address: "0xD754fF5e8A6f257E162F72578A4Bb0493C0681d1", + liquidity: 1000000, + usdPrice: 55.89037747, + updatedAt, + }); + + expect(entityManagerMock.update).toBeCalledWith( + Token, + { + l1Address: "0xD754fF5e8A6f257E162F72578A4Bb0493C0681d1", + }, + { + liquidity: 1000000, + usdPrice: 55.89037747, + offChainDataUpdatedAt: updatedAt, + } + ); + }); + + it("updates token offchain data using l2Address when provided", async () => { + const updatedAt = new Date(); + await repository.updateTokenOffChainData({ + l2Address: "0xD754fF5e8A6f257E162F72578A4Bb0493C0681d1", + liquidity: 1000000, + usdPrice: 55.89037747, + updatedAt, + }); + + expect(entityManagerMock.update).toBeCalledWith( + Token, + { + l2Address: "0xD754fF5e8A6f257E162F72578A4Bb0493C0681d1", + }, + { + liquidity: 1000000, + usdPrice: 55.89037747, + offChainDataUpdatedAt: updatedAt, + } + ); + }); + it("updates token offchain data when iconURL is not provided", async () => { const updatedAt = new Date(); await repository.updateTokenOffChainData({ diff --git a/packages/worker/src/repositories/token.repository.ts b/packages/worker/src/repositories/token.repository.ts index 5b9e701096..dd5de197ed 100644 --- a/packages/worker/src/repositories/token.repository.ts +++ b/packages/worker/src/repositories/token.repository.ts @@ -63,22 +63,27 @@ export class TokenRepository extends BaseRepository { public async updateTokenOffChainData({ l1Address, + l2Address, liquidity, usdPrice, updatedAt, iconURL, }: { - l1Address: string; - liquidity: number; - usdPrice: number; - updatedAt: Date; + l1Address?: string; + l2Address?: string; + liquidity?: number; + usdPrice?: number; + updatedAt?: Date; iconURL?: string; }): Promise { + if (!l1Address && !l2Address) { + throw new Error("l1Address or l2Address must be provided"); + } const transactionManager = this.unitOfWork.getTransactionManager(); await transactionManager.update( this.entityTarget, { - l1Address, + ...(l1Address ? { l1Address } : { l2Address }), }, { liquidity, diff --git a/packages/worker/src/token/tokenOffChainData/providers/coingecko/coingeckoTokenOffChainDataProvider.spec.ts b/packages/worker/src/token/tokenOffChainData/providers/coingecko/coingeckoTokenOffChainDataProvider.spec.ts new file mode 100644 index 0000000000..eb64860589 --- /dev/null +++ b/packages/worker/src/token/tokenOffChainData/providers/coingecko/coingeckoTokenOffChainDataProvider.spec.ts @@ -0,0 +1,315 @@ +import { Test } from "@nestjs/testing"; +import { mock } from "jest-mock-extended"; +import { Logger } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { HttpService } from "@nestjs/axios"; +import { AxiosResponse, AxiosError } from "axios"; +import { setTimeout } from "timers/promises"; +import * as rxjs from "rxjs"; +import { CoingeckoTokenOffChainDataProvider } from "./coingeckoTokenOffChainDataProvider"; + +const bridgedTokens = ["someAddress", "address1"]; +const providerTokensListResponse = [ + { + id: "ethereum", + platforms: {}, + }, + { + id: "token1", + platforms: { + ethereum: "address1", + }, + }, + { + id: "token2", + platforms: { + somePlatform: "address22", + zksync: "address2", + }, + }, + { + id: "token3", + platforms: { + somePlatform: "address3", + }, + }, +]; + +jest.useFakeTimers().setSystemTime(new Date("2023-01-01T02:00:00.000Z")); + +jest.mock("timers/promises", () => ({ + setTimeout: jest.fn().mockResolvedValue(null), +})); + +describe("CoingeckoTokenOffChainDataProvider", () => { + let provider: CoingeckoTokenOffChainDataProvider; + let configServiceMock: ConfigService; + let httpServiceMock: HttpService; + + beforeEach(async () => { + configServiceMock = mock({ + get: jest.fn().mockReturnValueOnce(true).mockReturnValueOnce("apiKey"), + }); + httpServiceMock = mock(); + const module = await Test.createTestingModule({ + providers: [ + CoingeckoTokenOffChainDataProvider, + { + provide: ConfigService, + useValue: configServiceMock, + }, + { + provide: HttpService, + useValue: httpServiceMock, + }, + ], + }).compile(); + module.useLogger(mock()); + + provider = module.get(CoingeckoTokenOffChainDataProvider); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("getTokensOffChainData", () => { + let pipeMock = jest.fn(); + + beforeEach(() => { + pipeMock = jest.fn(); + jest.spyOn(httpServiceMock, "get").mockReturnValue({ + pipe: pipeMock, + } as unknown as rxjs.Observable); + jest.spyOn(rxjs, "catchError").mockImplementation((callback) => callback as any); + }); + + it("uses correct API url and API key for pro plan", async () => { + pipeMock.mockImplementationOnce((callback) => { + callback({ + stack: "error stack", + response: { + status: 404, + }, + } as AxiosError); + }); + + await provider.getTokensOffChainData({ bridgedTokensToInclude: bridgedTokens }); + expect(httpServiceMock.get).toBeCalledWith( + "https://pro-api.coingecko.com/api/v3/coins/list?include_platform=true&x_cg_pro_api_key=apiKey" + ); + }); + + it("uses correct API url and API key for demo plan", async () => { + const module = await Test.createTestingModule({ + providers: [ + CoingeckoTokenOffChainDataProvider, + { + provide: ConfigService, + useValue: mock({ + get: jest.fn().mockReturnValueOnce(false).mockReturnValueOnce("apiKey"), + }), + }, + { + provide: HttpService, + useValue: httpServiceMock, + }, + ], + }).compile(); + module.useLogger(mock()); + const providerWithDemoPlan = module.get(CoingeckoTokenOffChainDataProvider); + + pipeMock.mockImplementationOnce((callback) => { + callback({ + stack: "error stack", + response: { + status: 404, + }, + } as AxiosError); + }); + + await providerWithDemoPlan.getTokensOffChainData({ bridgedTokensToInclude: bridgedTokens }); + expect(httpServiceMock.get).toBeCalledWith( + "https://api.coingecko.com/api/v3/coins/list?include_platform=true&x_cg_demo_api_key=apiKey" + ); + }); + + it("returns empty array when fetching tokens list constantly fails", async () => { + pipeMock.mockImplementation((callback) => { + callback({ + stack: "error stack", + } as AxiosError); + }); + + const tokens = await provider.getTokensOffChainData({ bridgedTokensToInclude: bridgedTokens }); + expect(tokens).toEqual([]); + }); + + it("retries for 5 times each time doubling timeout when fetching tokens list constantly fails", async () => { + pipeMock.mockImplementation((callback) => { + callback({ + stack: "error stack", + response: { + data: "response data", + status: 500, + }, + } as AxiosError); + }); + + await provider.getTokensOffChainData({ bridgedTokensToInclude: bridgedTokens }); + expect(httpServiceMock.get).toBeCalledTimes(6); + expect(setTimeout).toBeCalledTimes(5); + }); + + describe("when provider rate limit is reached", () => { + describe("when server provides rate limit reset date", () => { + it("retries API call after waiting for rate limit to reset if reset Date is in the future", async () => { + pipeMock.mockImplementationOnce((callback) => { + callback({ + stack: "error stack", + response: { + headers: { + "x-ratelimit-reset": "2023-01-01 02:02:00 +0000", + } as any, + status: 429, + }, + } as AxiosError); + }); + pipeMock.mockImplementationOnce((callback) => { + callback({ + stack: "error stack", + response: { + status: 404, + }, + } as AxiosError); + }); + + await provider.getTokensOffChainData({ bridgedTokensToInclude: bridgedTokens }); + expect(httpServiceMock.get).toBeCalledTimes(2); + expect(setTimeout).toBeCalledTimes(1); + expect(setTimeout).toBeCalledWith(121000); + }); + + it("retries API call after immediately if reset Date is not in the future", async () => { + pipeMock.mockImplementationOnce((callback) => { + callback({ + stack: "error stack", + response: { + headers: { + "x-ratelimit-reset": "2023-01-01 01:59:00 +0000", + } as any, + status: 429, + }, + } as AxiosError); + }); + pipeMock.mockImplementationOnce((callback) => { + callback({ + stack: "error stack", + response: { + status: 404, + }, + } as AxiosError); + }); + + await provider.getTokensOffChainData({ bridgedTokensToInclude: bridgedTokens }); + expect(httpServiceMock.get).toBeCalledTimes(2); + expect(setTimeout).toBeCalledTimes(1); + expect(setTimeout).toBeCalledWith(0); + }); + }); + + describe("when server does not provide rate limit reset date", () => { + it("retries API call after waiting for 61 seconds", async () => { + pipeMock.mockImplementationOnce((callback) => { + callback({ + stack: "error stack", + response: { + headers: {}, + status: 429, + }, + } as AxiosError); + }); + pipeMock.mockImplementationOnce((callback) => { + callback({ + stack: "error stack", + response: { + status: 404, + }, + } as AxiosError); + }); + + await provider.getTokensOffChainData({ bridgedTokensToInclude: bridgedTokens }); + expect(httpServiceMock.get).toBeCalledTimes(2); + expect(setTimeout).toBeCalledTimes(1); + expect(setTimeout).toBeCalledWith(61000); + }); + }); + }); + + it("fetches offchain tokens data and returns filtered tokens list", async () => { + pipeMock + .mockReturnValueOnce( + new rxjs.Observable((subscriber) => { + subscriber.next({ + data: providerTokensListResponse, + }); + }) + ) + .mockReturnValueOnce( + new rxjs.Observable((subscriber) => { + subscriber.next({ + data: [ + { + id: "ethereum", + market_cap: 100, + current_price: 10, + image: "http://ethereum.img", + }, + { + id: "token1", + market_cap: 101, + current_price: 11, + image: "http://token1.img", + }, + { + id: "token2", + market_cap: 102, + current_price: 12, + image: "http://token2.img", + }, + ], + }); + }) + ); + + const tokens = await provider.getTokensOffChainData({ bridgedTokensToInclude: bridgedTokens }); + expect(httpServiceMock.get).toBeCalledTimes(2); + expect(httpServiceMock.get).toBeCalledWith( + "https://pro-api.coingecko.com/api/v3/coins/list?include_platform=true&x_cg_pro_api_key=apiKey" + ); + expect(httpServiceMock.get).toBeCalledWith( + "https://pro-api.coingecko.com/api/v3/coins/markets?vs_currency=usd&ids=ethereum%2Ctoken1%2Ctoken2&per_page=3&page=1&locale=en&x_cg_pro_api_key=apiKey" + ); + expect(tokens).toEqual([ + { + l1Address: "0x0000000000000000000000000000000000000000", + liquidity: 100, + usdPrice: 10, + iconURL: "http://ethereum.img", + }, + { + l1Address: "address1", + liquidity: 101, + usdPrice: 11, + iconURL: "http://token1.img", + }, + { + l2Address: "address2", + liquidity: 102, + usdPrice: 12, + iconURL: "http://token2.img", + }, + ]); + }); + }); +}); diff --git a/packages/worker/src/token/tokenOffChainData/providers/coingecko/coingeckoTokenOffChainDataProvider.ts b/packages/worker/src/token/tokenOffChainData/providers/coingecko/coingeckoTokenOffChainDataProvider.ts new file mode 100644 index 0000000000..5acdc9f8bd --- /dev/null +++ b/packages/worker/src/token/tokenOffChainData/providers/coingecko/coingeckoTokenOffChainDataProvider.ts @@ -0,0 +1,204 @@ +import { Injectable, Logger } from "@nestjs/common"; +import { HttpService } from "@nestjs/axios"; +import { ConfigService } from "@nestjs/config"; +import { AxiosError } from "axios"; +import { setTimeout } from "timers/promises"; +import { catchError, firstValueFrom } from "rxjs"; +import { utils } from "zksync-web3"; +import { TokenOffChainDataProvider, ITokenOffChainData } from "../../tokenOffChainDataProvider.abstract"; + +const API_NUMBER_OF_TOKENS_PER_REQUEST = 250; +const API_INITIAL_RETRY_TIMEOUT = 5000; +const API_RETRY_ATTEMPTS = 5; + +interface ITokenListItemProviderResponse { + id: string; + platforms: Record; +} + +interface ITokenMarketDataProviderResponse { + id: string; + image?: string; + current_price?: number; + market_cap?: number; +} + +class ProviderResponseError extends Error { + constructor(message: string, public readonly status: number, public readonly rateLimitResetDate?: Date) { + super(message); + } +} + +@Injectable() +export class CoingeckoTokenOffChainDataProvider implements TokenOffChainDataProvider { + private readonly logger: Logger; + private readonly isProPlan: boolean; + private readonly apiKey: string; + private readonly apiUrl: string; + + constructor(configService: ConfigService, private readonly httpService: HttpService) { + this.logger = new Logger(CoingeckoTokenOffChainDataProvider.name); + this.isProPlan = configService.get("tokens.coingecko.isProPlan"); + this.apiKey = configService.get("tokens.coingecko.apiKey"); + this.apiUrl = this.isProPlan ? "https://pro-api.coingecko.com/api/v3" : "https://api.coingecko.com/api/v3"; + } + + public async getTokensOffChainData({ + bridgedTokensToInclude, + }: { + bridgedTokensToInclude: string[]; + }): Promise { + const tokensList = await this.getTokensList(); + // Include ETH, all zksync L2 tokens and bridged tokens + const supportedTokens = tokensList.filter( + (token) => + token.id === "ethereum" || + token.platforms.zksync || + bridgedTokensToInclude.find((bridgetTokenAddress) => bridgetTokenAddress === token.platforms.ethereum) + ); + + const tokensOffChainData: ITokenOffChainData[] = []; + let tokenIdsPerRequest = []; + for (let i = 0; i < supportedTokens.length; i++) { + tokenIdsPerRequest.push(supportedTokens[i].id); + if (tokenIdsPerRequest.length === API_NUMBER_OF_TOKENS_PER_REQUEST || i === supportedTokens.length - 1) { + const tokensMarkedData = await this.getTokensMarketData(tokenIdsPerRequest); + tokensOffChainData.push( + ...tokensMarkedData.map((tokenMarketData) => { + const token = supportedTokens.find((t) => t.id === tokenMarketData.id); + return { + l1Address: token.id === "ethereum" ? utils.ETH_ADDRESS : token.platforms.ethereum, + l2Address: token.platforms.zksync, + liquidity: tokenMarketData.market_cap, + usdPrice: tokenMarketData.current_price, + iconURL: tokenMarketData.image, + }; + }) + ); + tokenIdsPerRequest = []; + } + } + return tokensOffChainData; + } + + private getTokensMarketData(tokenIds: string[]) { + return this.makeApiRequestRetryable({ + path: "/coins/markets", + query: { + vs_currency: "usd", + ids: tokenIds.join(","), + per_page: tokenIds.length.toString(), + page: "1", + locale: "en", + }, + }); + } + + private async getTokensList() { + const list = await this.makeApiRequestRetryable({ + path: "/coins/list", + query: { + include_platform: "true", + }, + }); + if (!list) { + return []; + } + return list + .filter((item) => item.id === "ethereum" || item.platforms.zksync || item.platforms.ethereum) + .map((item) => ({ + ...item, + platforms: { + // use substring(0, 42) to fix some instances when after address there is some additional text + zksync: item.platforms.zksync?.substring(0, 42), + ethereum: item.platforms.ethereum?.substring(0, 42), + }, + })); + } + + private async makeApiRequestRetryable({ + path, + query, + retryAttempt = 0, + retryTimeout = API_INITIAL_RETRY_TIMEOUT, + }: { + path: string; + query?: Record; + retryAttempt?: number; + retryTimeout?: number; + }): Promise { + try { + return await this.makeApiRequest(path, query); + } catch (err) { + if (err.status === 404) { + return null; + } + if (err.status === 429) { + const rateLimitResetIn = err.rateLimitResetDate.getTime() - new Date().getTime(); + await setTimeout(rateLimitResetIn >= 0 ? rateLimitResetIn + 1000 : 0); + return this.makeApiRequestRetryable({ + path, + query, + }); + } + if (retryAttempt >= API_RETRY_ATTEMPTS) { + this.logger.error({ + message: `Failed to fetch data at ${path} from coingecko after ${retryAttempt} retries`, + provider: CoingeckoTokenOffChainDataProvider.name, + }); + return null; + } + await setTimeout(retryTimeout); + return this.makeApiRequestRetryable({ + path, + query, + retryAttempt: retryAttempt + 1, + retryTimeout: retryTimeout * 2, + }); + } + } + + private async makeApiRequest(path: string, query?: Record): Promise { + const queryString = new URLSearchParams({ + ...query, + ...(this.isProPlan + ? { + x_cg_pro_api_key: this.apiKey, + } + : { + x_cg_demo_api_key: this.apiKey, + }), + }).toString(); + + const { data } = await firstValueFrom<{ data: T }>( + this.httpService.get(`${this.apiUrl}${path}?${queryString}`).pipe( + catchError((error: AxiosError) => { + if (error.response?.status === 429) { + const rateLimitReset = error.response.headers["x-ratelimit-reset"]; + // use specified reset date or 60 seconds by default + const rateLimitResetDate = rateLimitReset + ? new Date(rateLimitReset) + : new Date(new Date().getTime() + 60000); + this.logger.debug({ + message: `Reached coingecko rate limit, reset at ${rateLimitResetDate}`, + stack: error.stack, + status: error.response.status, + response: error.response.data, + provider: CoingeckoTokenOffChainDataProvider.name, + }); + throw new ProviderResponseError(error.message, error.response.status, rateLimitResetDate); + } + this.logger.error({ + message: `Failed to fetch data at ${path} from coingecko`, + stack: error.stack, + status: error.response?.status, + response: error.response?.data, + provider: CoingeckoTokenOffChainDataProvider.name, + }); + throw new ProviderResponseError(error.message, error.response?.status); + }) + ) + ); + return data; + } +} diff --git a/packages/worker/src/token/tokenOffChainData/providers/portalsFiTokenOffChainDataProvider.spec.ts b/packages/worker/src/token/tokenOffChainData/providers/portalsFi/portalsFiTokenOffChainDataProvider.spec.ts similarity index 84% rename from packages/worker/src/token/tokenOffChainData/providers/portalsFiTokenOffChainDataProvider.spec.ts rename to packages/worker/src/token/tokenOffChainData/providers/portalsFi/portalsFiTokenOffChainDataProvider.spec.ts index b79df56888..1b93928d4d 100644 --- a/packages/worker/src/token/tokenOffChainData/providers/portalsFiTokenOffChainDataProvider.spec.ts +++ b/packages/worker/src/token/tokenOffChainData/providers/portalsFi/portalsFiTokenOffChainDataProvider.spec.ts @@ -7,10 +7,10 @@ import { setTimeout } from "timers/promises"; import * as rxjs from "rxjs"; import { PortalsFiTokenOffChainDataProvider } from "./portalsFiTokenOffChainDataProvider"; -const MIN_TOKENS_LIQUIDITY_FILTER = 0; const TOKENS_INFO_API_URL = "https://api.portals.fi/v2/tokens"; const TOKENS_INFO_API_QUERY = `networks=ethereum&limit=250&sortBy=liquidity&sortDirection=desc`; +const bridgedTokens = ["address1", "address2", "address3"]; const providerTokensResponse = [ { address: "address1", @@ -29,6 +29,11 @@ const providerTokensResponse = [ liquidity: 3000000, price: 10.7678787, }, + { + address: "unknown-token-address", + liquidity: 0, + price: 0, + }, ]; jest.mock("timers/promises", () => ({ @@ -77,7 +82,13 @@ describe("PortalsFiTokenOffChainDataProvider", () => { } as AxiosError); }); - const tokens = await provider.getTokensOffChainData(MIN_TOKENS_LIQUIDITY_FILTER); + const tokens = await provider.getTokensOffChainData({ bridgedTokensToInclude: bridgedTokens }); + expect(tokens).toEqual([]); + }); + + it("returns empty array when no bridgedTokensToInclude provided", async () => { + const tokens = await provider.getTokensOffChainData({ bridgedTokensToInclude: [] }); + expect(httpServiceMock.get).not.toBeCalled(); expect(tokens).toEqual([]); }); @@ -92,7 +103,7 @@ describe("PortalsFiTokenOffChainDataProvider", () => { } as AxiosError); }); - await provider.getTokensOffChainData(MIN_TOKENS_LIQUIDITY_FILTER); + await provider.getTokensOffChainData({ bridgedTokensToInclude: bridgedTokens }); expect(httpServiceMock.get).toBeCalledTimes(6); expect(setTimeout).toBeCalledTimes(5); }); @@ -120,7 +131,7 @@ describe("PortalsFiTokenOffChainDataProvider", () => { }) ); - const tokens = await provider.getTokensOffChainData(MIN_TOKENS_LIQUIDITY_FILTER); + const tokens = await provider.getTokensOffChainData({ bridgedTokensToInclude: bridgedTokens }); expect(httpServiceMock.get).toBeCalledTimes(2); expect(httpServiceMock.get).toBeCalledWith(`${TOKENS_INFO_API_URL}?${TOKENS_INFO_API_QUERY}&page=0`); expect(httpServiceMock.get).toBeCalledWith(`${TOKENS_INFO_API_URL}?${TOKENS_INFO_API_QUERY}&page=1`); @@ -140,25 +151,6 @@ describe("PortalsFiTokenOffChainDataProvider", () => { ]); }); - it("includes minLiquidity filter when provided minLiquidity filter > 0", async () => { - pipeMock.mockReturnValueOnce( - new rxjs.Observable((subscriber) => { - subscriber.next({ - data: { - more: false, - tokens: [providerTokensResponse[0]], - }, - }); - }) - ); - - await provider.getTokensOffChainData(1000000); - expect(httpServiceMock.get).toBeCalledTimes(1); - expect(httpServiceMock.get).toBeCalledWith( - `${TOKENS_INFO_API_URL}?${TOKENS_INFO_API_QUERY}&page=0&minLiquidity=1000000` - ); - }); - it("retries when provider API call fails", async () => { pipeMock .mockImplementationOnce((callback) => { @@ -181,7 +173,7 @@ describe("PortalsFiTokenOffChainDataProvider", () => { }) ); - const tokens = await provider.getTokensOffChainData(MIN_TOKENS_LIQUIDITY_FILTER); + const tokens = await provider.getTokensOffChainData({ bridgedTokensToInclude: bridgedTokens }); expect(httpServiceMock.get).toBeCalledTimes(2); expect(httpServiceMock.get).toBeCalledWith(`${TOKENS_INFO_API_URL}?${TOKENS_INFO_API_QUERY}&page=0`); expect(httpServiceMock.get).toBeCalledWith(`${TOKENS_INFO_API_URL}?${TOKENS_INFO_API_QUERY}&page=0`); diff --git a/packages/worker/src/token/tokenOffChainData/providers/portalsFiTokenOffChainDataProvider.ts b/packages/worker/src/token/tokenOffChainData/providers/portalsFi/portalsFiTokenOffChainDataProvider.ts similarity index 83% rename from packages/worker/src/token/tokenOffChainData/providers/portalsFiTokenOffChainDataProvider.ts rename to packages/worker/src/token/tokenOffChainData/providers/portalsFi/portalsFiTokenOffChainDataProvider.ts index 98ff62281a..ed61d85357 100644 --- a/packages/worker/src/token/tokenOffChainData/providers/portalsFiTokenOffChainDataProvider.ts +++ b/packages/worker/src/token/tokenOffChainData/providers/portalsFi/portalsFiTokenOffChainDataProvider.ts @@ -3,7 +3,7 @@ import { HttpService } from "@nestjs/axios"; import { AxiosError } from "axios"; import { setTimeout } from "timers/promises"; import { catchError, firstValueFrom } from "rxjs"; -import { TokenOffChainDataProvider, ITokenOffChainData } from "../tokenOffChainDataProvider.abstract"; +import { TokenOffChainDataProvider, ITokenOffChainData } from "../../tokenOffChainDataProvider.abstract"; const TOKENS_INFO_API_URL = "https://api.portals.fi/v2/tokens"; const API_INITIAL_RETRY_TIMEOUT = 5000; @@ -35,34 +35,43 @@ export class PortalsFiTokenOffChainDataProvider implements TokenOffChainDataProv this.logger = new Logger(PortalsFiTokenOffChainDataProvider.name); } - public async getTokensOffChainData(minLiquidity: number): Promise { + public async getTokensOffChainData({ + bridgedTokensToInclude, + }: { + bridgedTokensToInclude: string[]; + }): Promise { let page = 0; let hasMore = true; const tokens = []; + // This provider only supports bridged tokens + if (!bridgedTokensToInclude.length) { + return []; + } + while (hasMore) { - const tokensInfoPage = await this.getTokensOffChainDataPageRetryable({ page, minLiquidity }); + const tokensInfoPage = await this.getTokensOffChainDataPageRetryable({ page }); tokens.push(...tokensInfoPage.tokens); page++; hasMore = tokensInfoPage.hasMore; } - return tokens; + return tokens.filter((token) => + bridgedTokensToInclude.find((bridgetTokenAddress) => bridgetTokenAddress === token.l1Address) + ); } private async getTokensOffChainDataPageRetryable({ page, - minLiquidity, retryAttempt = 0, retryTimeout = API_INITIAL_RETRY_TIMEOUT, }: { page: number; - minLiquidity: number; retryAttempt?: number; retryTimeout?: number; }): Promise { try { - return await this.getTokensOffChainDataPage({ page, minLiquidity }); + return await this.getTokensOffChainDataPage({ page }); } catch { if (retryAttempt >= API_RETRY_ATTEMPTS) { this.logger.error({ @@ -77,29 +86,19 @@ export class PortalsFiTokenOffChainDataProvider implements TokenOffChainDataProv await setTimeout(retryTimeout); return this.getTokensOffChainDataPageRetryable({ page, - minLiquidity, retryAttempt: retryAttempt + 1, retryTimeout: retryTimeout * 2, }); } } - private async getTokensOffChainDataPage({ - page, - minLiquidity, - }: { - page: number; - minLiquidity: number; - }): Promise { + private async getTokensOffChainDataPage({ page }: { page: number }): Promise { const query = { networks: "ethereum", limit: "250", sortBy: "liquidity", sortDirection: "desc", page: page.toString(), - ...(minLiquidity && { - minLiquidity: minLiquidity.toString(), - }), }; const queryString = new URLSearchParams(query).toString(); diff --git a/packages/worker/src/token/tokenOffChainData/tokenOffChainDataProvider.abstract.ts b/packages/worker/src/token/tokenOffChainData/tokenOffChainDataProvider.abstract.ts index ff99d4cec7..6d169c848e 100644 --- a/packages/worker/src/token/tokenOffChainData/tokenOffChainDataProvider.abstract.ts +++ b/packages/worker/src/token/tokenOffChainData/tokenOffChainDataProvider.abstract.ts @@ -1,10 +1,11 @@ export interface ITokenOffChainData { - l1Address: string; - liquidity: number; - usdPrice: number; + l1Address?: string; + l2Address?: string; + liquidity?: number; + usdPrice?: number; iconURL?: string; } export abstract class TokenOffChainDataProvider { - abstract getTokensOffChainData: (minLiquidity: number) => Promise; + abstract getTokensOffChainData: (settings: { bridgedTokensToInclude: string[] }) => Promise; } diff --git a/packages/worker/src/token/tokenOffChainData/tokenOffChainDataSaver.service.spec.ts b/packages/worker/src/token/tokenOffChainData/tokenOffChainDataSaver.service.spec.ts index 2e810e3a86..f2c97456bf 100644 --- a/packages/worker/src/token/tokenOffChainData/tokenOffChainDataSaver.service.spec.ts +++ b/packages/worker/src/token/tokenOffChainData/tokenOffChainDataSaver.service.spec.ts @@ -20,9 +20,9 @@ jest.mock("../../utils/waitFor"); describe("TokenOffChainDataSaverService", () => { const OFFCHAIN_DATA_UPDATE_INTERVAL = 86_400_000; - const MIN_LIQUIDITY_FILTER = 1000000; const tokenOffChainDataMock = { - l1Address: "address", + l1Address: "l1Address", + l2Address: "l2Address", liquidity: 100000, usdPrice: 12.6789, iconURL: "http://icon.com", @@ -46,11 +46,7 @@ describe("TokenOffChainDataSaverService", () => { tokenRepositoryMock, tokenOffChainDataProviderMock, mock({ - get: jest - .fn() - .mockImplementation((key) => - key === "tokens.updateTokenOffChainDataInterval" ? OFFCHAIN_DATA_UPDATE_INTERVAL : MIN_LIQUIDITY_FILTER - ), + get: jest.fn().mockReturnValue(OFFCHAIN_DATA_UPDATE_INTERVAL), }) ); }); @@ -91,31 +87,21 @@ describe("TokenOffChainDataSaverService", () => { expect(tokenOffChainDataProviderMock.getTokensOffChainData).not.toBeCalled(); }); - it("does not update offchain data when there are no bridged token atm and waits for the next update", async () => { - tokenOffChainDataSaverService.start(); - await tokenOffChainDataSaverService.stop(); - - const [conditionPredicate, waitTime] = (waitFor as jest.Mock).mock.calls[0]; - expect(tokenRepositoryMock.getOffChainDataLastUpdatedAt).toBeCalledTimes(1); - expect(waitFor).toBeCalledTimes(1); - expect(conditionPredicate()).toBeTruthy(); - expect(waitTime).toBe(OFFCHAIN_DATA_UPDATE_INTERVAL); - expect(tokenRepositoryMock.getBridgedTokens).toBeCalledTimes(1); - expect(tokenOffChainDataProviderMock.getTokensOffChainData).not.toBeCalled(); - }); - - it("updates offchain data when data is too old and there are bridged tokens to update", async () => { + it("updates offchain data when data is too old", async () => { const lastUpdatedAt = new Date("2022-01-01T01:00:00.000Z"); jest.spyOn(tokenRepositoryMock, "getOffChainDataLastUpdatedAt").mockResolvedValueOnce(lastUpdatedAt); - jest.spyOn(tokenRepositoryMock, "getBridgedTokens").mockResolvedValueOnce([{ l1Address: "address" } as Token]); + jest.spyOn(tokenRepositoryMock, "getBridgedTokens").mockResolvedValueOnce([{ l1Address: "l1Address" } as Token]); tokenOffChainDataSaverService.start(); await tokenOffChainDataSaverService.stop(); - expect(tokenOffChainDataProviderMock.getTokensOffChainData).toBeCalledWith(MIN_LIQUIDITY_FILTER); + expect(tokenOffChainDataProviderMock.getTokensOffChainData).toBeCalledWith({ + bridgedTokensToInclude: ["l1Address"], + }); expect(tokenRepositoryMock.updateTokenOffChainData).toHaveBeenCalledTimes(1); expect(tokenRepositoryMock.updateTokenOffChainData).toHaveBeenCalledWith({ - l1Address: "address", + l1Address: "l1Address", + l2Address: "l2Address", liquidity: 100000, usdPrice: 12.6789, updatedAt: new Date(), @@ -123,16 +109,19 @@ describe("TokenOffChainDataSaverService", () => { }); }); - it("updates offchain data when data was never updated and there are bridged tokens to update", async () => { - jest.spyOn(tokenRepositoryMock, "getBridgedTokens").mockResolvedValueOnce([{ l1Address: "address" } as Token]); + it("updates offchain data when data was never updated", async () => { + jest.spyOn(tokenRepositoryMock, "getBridgedTokens").mockResolvedValueOnce([{ l1Address: "l1Address" } as Token]); tokenOffChainDataSaverService.start(); await tokenOffChainDataSaverService.stop(); - expect(tokenOffChainDataProviderMock.getTokensOffChainData).toBeCalledWith(MIN_LIQUIDITY_FILTER); + expect(tokenOffChainDataProviderMock.getTokensOffChainData).toBeCalledWith({ + bridgedTokensToInclude: ["l1Address"], + }); expect(tokenRepositoryMock.updateTokenOffChainData).toHaveBeenCalledTimes(1); expect(tokenRepositoryMock.updateTokenOffChainData).toHaveBeenCalledWith({ - l1Address: "address", + l1Address: "l1Address", + l2Address: "l2Address", liquidity: 100000, usdPrice: 12.6789, updatedAt: new Date(), @@ -141,7 +130,7 @@ describe("TokenOffChainDataSaverService", () => { }); it("waits for specified timeout or worker stoppage after offchain data update", async () => { - jest.spyOn(tokenRepositoryMock, "getBridgedTokens").mockResolvedValueOnce([{ l1Address: "address" } as Token]); + jest.spyOn(tokenRepositoryMock, "getBridgedTokens").mockResolvedValueOnce([{ l1Address: "l1Address" } as Token]); tokenOffChainDataSaverService.start(); await tokenOffChainDataSaverService.stop(); diff --git a/packages/worker/src/token/tokenOffChainData/tokenOffChainDataSaver.service.ts b/packages/worker/src/token/tokenOffChainData/tokenOffChainDataSaver.service.ts index 5084c27a6f..6e600b2293 100644 --- a/packages/worker/src/token/tokenOffChainData/tokenOffChainDataSaver.service.ts +++ b/packages/worker/src/token/tokenOffChainData/tokenOffChainDataSaver.service.ts @@ -10,7 +10,6 @@ const UPDATE_TOKENS_BATCH_SIZE = 100; @Injectable() export class TokenOffChainDataSaverService extends Worker { private readonly updateTokenOffChainDataInterval: number; - private readonly tokenOffChainDataMinLiquidityFilter: number; private readonly logger: Logger; public constructor( @@ -20,7 +19,6 @@ export class TokenOffChainDataSaverService extends Worker { ) { super(); this.updateTokenOffChainDataInterval = configService.get("tokens.updateTokenOffChainDataInterval"); - this.tokenOffChainDataMinLiquidityFilter = configService.get("tokens.tokenOffChainDataMinLiquidityFilter"); this.logger = new Logger(TokenOffChainDataSaverService.name); } @@ -37,37 +35,33 @@ export class TokenOffChainDataSaverService extends Worker { if (!nextUpdateTimeout) { const bridgedTokens = await this.tokenRepository.getBridgedTokens(); - if (bridgedTokens.length) { - const tokensInfo = await this.tokenOffChainDataProvider.getTokensOffChainData( - this.tokenOffChainDataMinLiquidityFilter - ); - const tokensToUpdate = tokensInfo.filter((token) => - bridgedTokens.find((t) => t.l1Address === token.l1Address) - ); - const updatedAt = new Date(); + const tokensToUpdate = await this.tokenOffChainDataProvider.getTokensOffChainData({ + bridgedTokensToInclude: bridgedTokens.map((t) => t.l1Address), + }); + const updatedAt = new Date(); - let updateTokensTasks = []; - for (let i = 0; i < tokensToUpdate.length; i++) { - updateTokensTasks.push( - this.tokenRepository.updateTokenOffChainData({ - l1Address: tokensToUpdate[i].l1Address, - liquidity: tokensToUpdate[i].liquidity, - usdPrice: tokensToUpdate[i].usdPrice, - updatedAt, - iconURL: tokensToUpdate[i].iconURL, - }) - ); - if (updateTokensTasks.length === UPDATE_TOKENS_BATCH_SIZE || i === tokensToUpdate.length - 1) { - await Promise.all(updateTokensTasks); - updateTokensTasks = []; - } + let updateTokensTasks = []; + for (let i = 0; i < tokensToUpdate.length; i++) { + updateTokensTasks.push( + this.tokenRepository.updateTokenOffChainData({ + l1Address: tokensToUpdate[i].l1Address, + l2Address: tokensToUpdate[i].l2Address, + liquidity: tokensToUpdate[i].liquidity, + usdPrice: tokensToUpdate[i].usdPrice, + updatedAt, + iconURL: tokensToUpdate[i].iconURL, + }) + ); + if (updateTokensTasks.length === UPDATE_TOKENS_BATCH_SIZE || i === tokensToUpdate.length - 1) { + await Promise.all(updateTokensTasks); + updateTokensTasks = []; } - - this.logger.log("Updated tokens offchain data", { - totalTokensUpdated: tokensToUpdate.length, - }); } + this.logger.log("Updated tokens offchain data", { + totalTokensUpdated: tokensToUpdate.length, + }); + nextUpdateTimeout = this.updateTokenOffChainDataInterval; } } catch (err) { From de0ded3827799b71d689a384b3d933f2cdd5eae6 Mon Sep 17 00:00:00 2001 From: Vasyl Ivanchuk Date: Thu, 30 Nov 2023 13:46:42 +0200 Subject: [PATCH 04/52] fix: hide sepolia network on prod (#104) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # What ❔ Hide Sepolia network on prod. ## Why ❔ We need to hold off on announcing Sepolia support in Block Explorer until all other teams are ready to do this. ## Checklist - [X] PR title corresponds to the body of PR (we generate changelog entries from PRs). --- packages/app/src/configs/production.config.json | 2 +- packages/app/tests/e2e/features/artifacts/artifactsSet3.feature | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/configs/production.config.json b/packages/app/src/configs/production.config.json index bc25acca32..0214a28683 100644 --- a/packages/app/src/configs/production.config.json +++ b/packages/app/src/configs/production.config.json @@ -31,7 +31,7 @@ "l2WalletUrl": "https://portal.zksync.io/", "maintenance": false, "name": "sepolia", - "published": true, + "published": false, "rpcUrl": "https://sepolia.era.zksync.dev" }, { diff --git a/packages/app/tests/e2e/features/artifacts/artifactsSet3.feature b/packages/app/tests/e2e/features/artifacts/artifactsSet3.feature index 652968f457..57f7fc174c 100644 --- a/packages/app/tests/e2e/features/artifacts/artifactsSet3.feature +++ b/packages/app/tests/e2e/features/artifacts/artifactsSet3.feature @@ -65,7 +65,7 @@ Feature: Main Page @id249 @testnet @testnetSmokeSuite Scenario Outline: Verify table contains "" column name on Tokens page Given I go to page "/tokenlist" - And Table "Tokens" should have "1" rows + # And Table "Tokens" should have "1" rows Then Column with "" name is visible Examples: From 9b7f885988064660d1c28e8da540b187fc06b77f Mon Sep 17 00:00:00 2001 From: Roman Petriv Date: Thu, 30 Nov 2023 16:17:34 +0200 Subject: [PATCH 05/52] fix: rename tokens page title to top tokens (#103) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # What ❔ Renamed tokens page title to "To tokens" ## Why ❔ We show only top tokens there, not all of them --- packages/app/src/locales/en.json | 4 ++-- packages/app/src/locales/uk.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/app/src/locales/en.json b/packages/app/src/locales/en.json index 3f6a3c4810..f9480fd9e3 100644 --- a/packages/app/src/locales/en.json +++ b/packages/app/src/locales/en.json @@ -581,8 +581,8 @@ "title": "Transactions" }, "tokenListView": { - "title": "Token List", - "heading": "Tokens", + "title": "Top tokens", + "heading": "Top tokens", "offChainDataPoweredBy": "Off-chain data powered by", "table": { "tokenName": "Token Name", diff --git a/packages/app/src/locales/uk.json b/packages/app/src/locales/uk.json index 47ad5b8c56..898260232f 100644 --- a/packages/app/src/locales/uk.json +++ b/packages/app/src/locales/uk.json @@ -323,8 +323,8 @@ "title": "Транзакції" }, "tokenListView": { - "title": "Список Токенів", - "heading": "Токени", + "title": "Top токени", + "heading": "Top токени", "offChainDataPoweredBy": "Off-chain дані взяті з", "table": { "tokenName": "Назва Токена", From 20e9f9def0f7887c64f9e795acd3e25122f7abe2 Mon Sep 17 00:00:00 2001 From: Vasyl Ivanchuk Date: Mon, 4 Dec 2023 13:10:00 +0200 Subject: [PATCH 06/52] fix: different no transaction messages for non-existing and empty batches/blocks (#107) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # What ❔ Different _no transaction_ messages for non-existing and empty batches/blocks: Empty batch: image Non-existing batch: image ## Why ❔ For better UX we want to show different _no transaction_ messages in different cases: `This Batch doesn’t have any transactions` - for empty batch, `This Batch has not been created or sealed yet` - for non-existing batch, same for blocks. ## Checklist - [X] PR title corresponds to the body of PR (we generate changelog entries from PRs). - [X] Tests for the changes have been added / updated. --- .../app/src/components/batches/InfoTable.vue | 21 +++++++--- .../batches/TransactionEmptyState.vue | 27 ++++++------ .../app/src/components/blocks/InfoTable.vue | 17 +++++--- .../blocks/TransactionEmptyState.vue | 30 ++++++-------- packages/app/src/locales/en.json | 16 +++----- packages/app/src/locales/uk.json | 2 +- packages/app/src/views/BatchView.vue | 12 +++--- packages/app/src/views/BlockView.vue | 12 +++--- .../components/batches/InfoTable.spec.ts | 31 ++++++++++++++ .../batches/TransactionEmptyState.spec.ts | 17 +++++++- .../tests/components/blocks/InfoTable.spec.ts | 34 +++++++++++++++ .../blocks/TransactionEmptyState.spec.ts | 41 +++++++++++++++++++ packages/app/tests/views/BatchView.spec.ts | 29 ++----------- packages/app/tests/views/BlockView.spec.ts | 27 +----------- 14 files changed, 194 insertions(+), 122 deletions(-) create mode 100644 packages/app/tests/components/blocks/TransactionEmptyState.spec.ts diff --git a/packages/app/src/components/batches/InfoTable.vue b/packages/app/src/components/batches/InfoTable.vue index e0a0874d46..1784549814 100644 --- a/packages/app/src/components/batches/InfoTable.vue +++ b/packages/app/src/components/batches/InfoTable.vue @@ -31,6 +31,10 @@ const props = defineProps({ type: Object as PropType, default: null, }, + batchNumber: { + type: String, + required: true, + }, loading: { type: Boolean, default: true, @@ -45,15 +49,20 @@ const tableInfoItems = computed(() => { component?: Component; url?: string; }; - if (!props.batch) { - return []; - } + let tableItems: InfoTableItem[] = [ { label: t("batches.index"), tooltip: t("batches.indexTooltip"), - value: props.batch.number, + value: props.batchNumber, }, + ]; + + if (!props.batch) { + return [tableItems]; + } + + tableItems.push( { label: t("batches.size"), tooltip: t("batches.sizeTooltip"), @@ -70,8 +79,8 @@ const tableInfoItems = computed(() => { tooltip: t("batches.rootHashTooltip"), value: props.batch.rootHash ? { value: props.batch.rootHash } : t("batches.noRootHashYet"), component: props.batch.rootHash ? CopyContent : undefined, - }, - ]; + } + ); for (const [key, timeKey] of [ ["commitTxHash", "committedAt", "notYetCommitted"], ["proveTxHash", "provenAt", "notYetProven"], diff --git a/packages/app/src/components/batches/TransactionEmptyState.vue b/packages/app/src/components/batches/TransactionEmptyState.vue index 73f7dc0b42..f7271c2287 100644 --- a/packages/app/src/components/batches/TransactionEmptyState.vue +++ b/packages/app/src/components/batches/TransactionEmptyState.vue @@ -1,14 +1,10 @@ @@ -19,16 +15,17 @@ import { useI18n } from "vue-i18n"; import EmptyState from "@/components/common/EmptyState.vue"; const { t } = useI18n(); + +defineProps({ + batchExists: { + type: Boolean, + required: true, + }, +}); diff --git a/packages/app/src/components/blocks/InfoTable.vue b/packages/app/src/components/blocks/InfoTable.vue index 9c2ddc43c4..63ddc25491 100644 --- a/packages/app/src/components/blocks/InfoTable.vue +++ b/packages/app/src/components/blocks/InfoTable.vue @@ -32,6 +32,10 @@ const props = defineProps({ type: Object as PropType, default: null, }, + blockNumber: { + type: String, + required: true, + }, loading: { type: Boolean, default: true, @@ -52,11 +56,13 @@ const tableInfoItems = computed(() => { disabledTooltip?: string; }; }; + let tableItems: InfoTableItem[] = [ + { label: t("blocks.table.blockNumber"), tooltip: t("blocks.table.blockNumberTooltip"), value: props.blockNumber }, + ]; if (!props.block) { - return []; + return [tableItems]; } - let tableItems: InfoTableItem[] = [ - { label: t("blocks.table.blockNumber"), tooltip: t("blocks.table.blockNumberTooltip"), value: props.block.number }, + tableItems.push( { label: t("blocks.table.blockSize"), tooltip: t("blocks.table.blockSizeTooltip"), @@ -95,8 +101,8 @@ const tableInfoItems = computed(() => { tooltip: t("blocks.table.timestampTooltip"), value: { value: props.block.timestamp }, component: TimeField, - }, - ]; + } + ); for (const [key, timeKey] of [ ["commitTxHash", "committedAt", "notYetCommitted"], ["proveTxHash", "provenAt", "notYetProven"], @@ -135,6 +141,7 @@ const tableInfoItems = computed(() => { .two-section-view { @apply grid gap-4 pb-1.5 lg:grid-cols-2; } + .hide-mobile { @apply hidden lg:block; } diff --git a/packages/app/src/components/blocks/TransactionEmptyState.vue b/packages/app/src/components/blocks/TransactionEmptyState.vue index 77699cfb06..e5837e96e8 100644 --- a/packages/app/src/components/blocks/TransactionEmptyState.vue +++ b/packages/app/src/components/blocks/TransactionEmptyState.vue @@ -1,17 +1,10 @@ @@ -22,16 +15,17 @@ import { useI18n } from "vue-i18n"; import EmptyState from "@/components/common/EmptyState.vue"; const { t } = useI18n(); + +defineProps({ + blockExists: { + type: Boolean, + required: true, + }, +}); diff --git a/packages/app/src/locales/en.json b/packages/app/src/locales/en.json index f9480fd9e3..b62ec696f3 100644 --- a/packages/app/src/locales/en.json +++ b/packages/app/src/locales/en.json @@ -73,12 +73,8 @@ "transactionTable": { "title": "Block Transactions", "showMore": "Show more transactions ->", - "notFound": { - "title": "This Block doesn't have any transactions", - "subtitle": "We always have a zero block at the end of the batch. Want to know why?", - "urlTitle": "Visit our docs page", - "url": "https://docs-v2-zksync.web.app/dev/developer-guides/transactions/blocks.html#blocks-in-zksync-2-0" - } + "noTransactions": "This Block doesn't have any transactions", + "blockNotFound": "This Block has not been created or sealed yet" } }, "transfers": { @@ -258,10 +254,8 @@ "transactionTable": { "title": "Batch Transactions", "error": "Something went wrong", - "notFound": { - "title": "This Batch doesn't have any transactions", - "subtitle": "We can't find transactions for this batch \n We'll fix it in a moment; please refresh the page" - } + "noTransactions": "This Batch doesn't have any transactions", + "batchNotFound": "This Batch has not been created or sealed yet" }, "table": { "status": "Status", @@ -409,7 +403,7 @@ }, "contractVerification": { "title": "Smart Contract Verification", - "subtitle": "Source code verification provides transparency for users interacting with smart contracts. By uploading the source code, zkScan will match the compiled code with that on the blockchain.", + "subtitle": "Source code verification provides transparency for users interacting with smart contracts. By uploading the source code, Era Explorer will match the compiled code with that on the blockchain.", "resources": { "title": "You can also verify your smart-contract using {hardhat}", "links": { diff --git a/packages/app/src/locales/uk.json b/packages/app/src/locales/uk.json index 898260232f..57b0dd91d0 100644 --- a/packages/app/src/locales/uk.json +++ b/packages/app/src/locales/uk.json @@ -212,7 +212,7 @@ }, "contractVerification": { "title": "Верифікація Смарт контракту", - "subtitle": "Перевірка вихідного коду забезпечує прозорість для користувачів, які взаємодіють зі смарт-контрактами. Завантаживши вихідний код, zkScan зіставить скомпільований код із кодом у блокчейні.", + "subtitle": "Перевірка вихідного коду забезпечує прозорість для користувачів, які взаємодіють зі смарт-контрактами. Завантаживши вихідний код, Era Explorer зіставить скомпільований код із кодом у блокчейні.", "form": { "title": "Деталі Контракту", "compilationInfo": "Деталі компіляції", diff --git a/packages/app/src/views/BatchView.vue b/packages/app/src/views/BatchView.vue index 50b415d158..1472dc4664 100644 --- a/packages/app/src/views/BatchView.vue +++ b/packages/app/src/views/BatchView.vue @@ -7,19 +7,19 @@
- + <Title v-if="!batchPending" :title="t('batches.batchNumber')" :value="id"> {{ parseInt(id) }}
- + -
+

{{ t("batches.transactionTable.title") }}

@@ -49,7 +49,7 @@ import { isBlockNumber } from "@/utils/validators"; const { t } = useI18n(); -const { useNotFoundView, setNotFoundView } = useNotFound(); +const { setNotFoundView } = useNotFound(); const { getById, batchItem, isRequestPending: batchPending, isRequestFailed: batchFailed } = useBatch(); const props = defineProps({ @@ -89,8 +89,6 @@ const transactionsSearchParams = computed(() => ({ l1BatchNumber: parseInt(props.id), })); -useNotFoundView(batchPending, batchFailed, batchItem); - watchEffect(() => { if (!props.id || !isBlockNumber(props.id)) { return setNotFoundView(); diff --git a/packages/app/src/views/BlockView.vue b/packages/app/src/views/BlockView.vue index 548ed82edc..ce8fe4d117 100644 --- a/packages/app/src/views/BlockView.vue +++ b/packages/app/src/views/BlockView.vue @@ -7,15 +7,15 @@
- + <Title v-if="!blockPending" :title="t('blocks.blockNumber')" :value="id"> {{ parseInt(id) }}
- +
-
+

{{ t("blocks.transactionTable.title") }}

@@ -53,7 +53,7 @@ import { isBlockNumber } from "@/utils/validators"; const { t } = useI18n(); -const { useNotFoundView, setNotFoundView } = useNotFound(); +const { setNotFoundView } = useNotFound(); const { getById, blockItem, isRequestPending: blockPending, isRequestFailed: blockFailed } = useBlock(); const props = defineProps({ @@ -81,8 +81,6 @@ const transactionsSearchParams = computed(() => ({ blockNumber: parseInt(props.id), })); -useNotFoundView(blockPending, blockFailed, blockItem); - watchEffect(() => { if (!props.id || !isBlockNumber(props.id)) { return setNotFoundView(); diff --git a/packages/app/tests/components/batches/InfoTable.spec.ts b/packages/app/tests/components/batches/InfoTable.spec.ts index 63fc2948a8..db21332a81 100644 --- a/packages/app/tests/components/batches/InfoTable.spec.ts +++ b/packages/app/tests/components/batches/InfoTable.spec.ts @@ -60,6 +60,7 @@ describe("InfoTable:", () => { }, props: { batch: batchItem, + batchNumber: batchItem.number.toString(), loading: false, }, }); @@ -118,6 +119,33 @@ describe("InfoTable:", () => { wrapper.unmount(); }); + + describe("when batch is not set", () => { + it("renders only batch number", () => { + const wrapper = mount(InfoTable, { + global: { + stubs: { + InfoTooltip: { template: "
" }, + }, + plugins: [i18n], + }, + props: { + batchNumber: batchItem.number.toString(), + loading: false, + }, + }); + + const rowArray = wrapper.findAll("tr"); + expect(rowArray.length).toBe(1); + + const batchIndex = rowArray[0].findAll("td"); + expect(batchIndex[0].find(".batch-info-field-label").text()).toBe(i18n.global.t("batches.index")); + expect(batchIndex[0].findComponent(InfoTooltip).text()).toBe(i18n.global.t("batches.indexTooltip")); + expect(batchIndex[1].text()).toBe("42"); + wrapper.unmount(); + }); + }); + it("renders loading state", () => { const wrapper = mount(InfoTable, { global: { @@ -125,6 +153,7 @@ describe("InfoTable:", () => { }, props: { loading: true, + batchNumber: batchItem.number.toString(), }, }); expect(wrapper.findAll(".content-loader").length).toBe(20); @@ -141,6 +170,7 @@ describe("InfoTable:", () => { props: { batch: batchItem, loading: false, + batchNumber: batchItem.number.toString(), }, }); @@ -175,6 +205,7 @@ describe("InfoTable:", () => { }, props: { batch: batchItem, + batchNumber: batchItem.number.toString(), loading: false, }, }); diff --git a/packages/app/tests/components/batches/TransactionEmptyState.spec.ts b/packages/app/tests/components/batches/TransactionEmptyState.spec.ts index bda6806b78..f683d2923f 100644 --- a/packages/app/tests/components/batches/TransactionEmptyState.spec.ts +++ b/packages/app/tests/components/batches/TransactionEmptyState.spec.ts @@ -16,13 +16,26 @@ describe("TransactionEmptyState", () => { en: enUS, }, }); - it("renders component properly", async () => { + it("renders component properly for existing batch", async () => { const { getByText } = render(TransactionEmptyState, { global: { plugins: [i18n], }, + props: { + batchExists: true, + }, }); getByText("This Batch doesn't have any transactions"); - getByText("We can't find transactions for this batch We'll fix it in a moment; please refresh the page"); + }); + it("renders component properly for nonexisting batch", async () => { + const { getByText } = render(TransactionEmptyState, { + global: { + plugins: [i18n], + }, + props: { + batchExists: false, + }, + }); + getByText("This Batch has not been created or sealed yet"); }); }); diff --git a/packages/app/tests/components/blocks/InfoTable.spec.ts b/packages/app/tests/components/blocks/InfoTable.spec.ts index 17ca8b8227..c2422369e7 100644 --- a/packages/app/tests/components/blocks/InfoTable.spec.ts +++ b/packages/app/tests/components/blocks/InfoTable.spec.ts @@ -59,6 +59,7 @@ describe("InfoTable:", () => { executeTxHash: "0x8d1a78d1da5aba1d0755ec9dbcba938f3920681d2a3d4d374ef265a50858f364", executedAt: "2022-04-13T16:54:37.784185Z", }, + blockNumber: "1", loading: false, }, }); @@ -113,6 +114,33 @@ describe("InfoTable:", () => { expect(executed[0].findComponent(InfoTooltip).text()).toBe(i18n.global.t("blocks.table.executedAtTooltip")); expect(executed[1].text()).includes(localDateFromISOString("2022-04-13T16:54:37.784185Z")); }); + describe("when block is not set", () => { + it("renders only block number", () => { + const wrapper = mount(InfoTable, { + global: { + stubs: { + RouterLink: RouterLinkStub, + InfoTooltip: { template: "
" }, + }, + plugins: [i18n], + }, + props: { + blockNumber: "1", + loading: false, + }, + }); + + const rowArray = wrapper.findAll("tr"); + expect(rowArray.length).toBe(1); + + const blockNumber = rowArray[0].findAll("td"); + expect(blockNumber[0].find(".block-info-field-label").text()).toBe(i18n.global.t("blocks.table.blockNumber")); + expect(blockNumber[0].findComponent(InfoTooltip).text()).toBe(i18n.global.t("blocks.table.blockNumberTooltip")); + expect(blockNumber[1].text()).toBe("1"); + + wrapper.unmount(); + }); + }); it("renders loading state", () => { const wrapper = mount(InfoTable, { global: { @@ -123,6 +151,7 @@ describe("InfoTable:", () => { }, props: { loading: true, + blockNumber: "1", }, }); expect(wrapper.findAll(".content-loader").length).toBe(24); @@ -150,6 +179,7 @@ describe("InfoTable:", () => { executeTxHash: "0x8d1a78d1da5aba1d0755ec9dbcba938f3920681d2a3d4d374ef265a50858f364", executedAt: "2022-04-13T16:54:37.784185Z", }, + blockNumber: "1", loading: false, }, }); @@ -180,6 +210,7 @@ describe("InfoTable:", () => { executeTxHash: "0x8d1a78d1da5aba1d0755ec9dbcba938f3920681d2a3d4d374ef265a50858f364", executedAt: "2022-04-13T16:54:37.784185Z", }, + blockNumber: "1", loading: false, }, }); @@ -210,6 +241,7 @@ describe("InfoTable:", () => { executeTxHash: "0x8d1a78d1da5aba1d0755ec9dbcba938f3920681d2a3d4d374ef265a50858f364", executedAt: "2022-04-13T16:54:37.784185Z", }, + blockNumber: "1", loading: false, }, }); @@ -243,6 +275,7 @@ describe("InfoTable:", () => { executeTxHash: "0x8d1a78d1da5aba1d0755ec9dbcba938f3920681d2a3d4d374ef265a50858f364", executedAt: "2022-04-13T16:54:37.784185Z", }, + blockNumber: "1", loading: false, }, }); @@ -284,6 +317,7 @@ describe("InfoTable:", () => { executeTxHash: "0x8d1a78d1da5aba1d0755ec9dbcba938f3920681d2a3d4d374ef265a50858f364", executedAt: "2022-04-13T16:54:37.784185Z", }, + blockNumber: "1", loading: false, }, }); diff --git a/packages/app/tests/components/blocks/TransactionEmptyState.spec.ts b/packages/app/tests/components/blocks/TransactionEmptyState.spec.ts new file mode 100644 index 0000000000..d13a086010 --- /dev/null +++ b/packages/app/tests/components/blocks/TransactionEmptyState.spec.ts @@ -0,0 +1,41 @@ +import { createI18n } from "vue-i18n"; + +import { describe, it } from "vitest"; + +import { render } from "@testing-library/vue"; + +import TransactionEmptyState from "@/components/blocks/TransactionEmptyState.vue"; + +import enUS from "@/locales/en.json"; + +describe("TransactionEmptyState", () => { + const i18n = createI18n({ + locale: "en", + allowComposition: true, + messages: { + en: enUS, + }, + }); + it("renders component properly for existing block", async () => { + const { getByText } = render(TransactionEmptyState, { + global: { + plugins: [i18n], + }, + props: { + blockExists: true, + }, + }); + getByText("This Block doesn't have any transactions"); + }); + it("renders component properly for nonexisting block", async () => { + const { getByText } = render(TransactionEmptyState, { + global: { + plugins: [i18n], + }, + props: { + blockExists: false, + }, + }); + getByText("This Block has not been created or sealed yet"); + }); +}); diff --git a/packages/app/tests/views/BatchView.spec.ts b/packages/app/tests/views/BatchView.spec.ts index d94fff9c21..3f86549bd5 100644 --- a/packages/app/tests/views/BatchView.spec.ts +++ b/packages/app/tests/views/BatchView.spec.ts @@ -3,16 +3,15 @@ import { createI18n } from "vue-i18n"; import { describe, expect, it, vi } from "vitest"; import { mount } from "@vue/test-utils"; -import { $fetch, FetchError } from "ohmyfetch"; import enUS from "@/locales/en.json"; +import $testId from "@/plugins/testId"; import routes from "@/router/routes"; import BatchView from "@/views/BatchView.vue"; -const notFoundRoute = { name: "not-found", meta: { title: "404 Not Found" } }; const router = { - resolve: vi.fn(() => notFoundRoute), + resolve: vi.fn(), replace: vi.fn(), currentRoute: { value: {}, @@ -54,27 +53,6 @@ describe("BatchView:", () => { expect(i18n.global.t(routes.find((e) => e.name === "batch")?.meta?.title as string)).toBe("Batch"); }); - it("route is replaced with not found view on request 404 error", async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const error: any = new FetchError("404"); - error.response = { - status: 404, - }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const mock = ($fetch as any).mockRejectedValue(error); - mount(BatchView, { - props: { - id: "42", - }, - global: { - stubs: ["router-link"], - plugins: [i18n], - }, - }); - await new Promise((resolve) => setImmediate(resolve)); - expect(router.replace).toHaveBeenCalledWith(notFoundRoute); - mock.mockRestore(); - }); it("shows correct trimmed title", () => { const wrapper = mount(BatchView, { props: { @@ -82,9 +60,10 @@ describe("BatchView:", () => { }, global: { stubs: ["router-link"], - plugins: [i18n], + plugins: [i18n, $testId], }, }); + expect(wrapper.find(".breadcrumb-item span").text()).toBe("Batch #42"); }); }); diff --git a/packages/app/tests/views/BlockView.spec.ts b/packages/app/tests/views/BlockView.spec.ts index 6e8d60ca7a..4e80c08a8a 100644 --- a/packages/app/tests/views/BlockView.spec.ts +++ b/packages/app/tests/views/BlockView.spec.ts @@ -3,7 +3,6 @@ import { createI18n } from "vue-i18n"; import { describe, expect, it, vi } from "vitest"; import { mount } from "@vue/test-utils"; -import { $fetch, FetchError } from "ohmyfetch"; import enUS from "@/locales/en.json"; @@ -11,9 +10,8 @@ import $testId from "@/plugins/testId"; import routes from "@/router/routes"; import BlockView from "@/views/BlockView.vue"; -const notFoundRoute = { name: "not-found", meta: { title: "404 Not Found" } }; const router = { - resolve: vi.fn(() => notFoundRoute), + resolve: vi.fn(), replace: vi.fn(), currentRoute: { value: {}, @@ -55,27 +53,6 @@ describe("BlockView:", () => { expect(i18n.global.t(routes.find((e) => e.name === "block")?.meta?.title as string)).toBe("Block"); }); - it("route is replaced with not found view on request 404 error", async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const error: any = new FetchError("404"); - error.response = { - status: 404, - }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const mock = ($fetch as any).mockRejectedValue(error); - mount(BlockView, { - props: { - id: "12", - }, - global: { - stubs: ["router-link"], - plugins: [i18n, $testId], - }, - }); - await new Promise((resolve) => setImmediate(resolve)); - expect(router.replace).toHaveBeenCalledWith(notFoundRoute); - mock.mockRestore(); - }); it("shows correct trimmed title", () => { const wrapper = mount(BlockView, { props: { @@ -83,7 +60,7 @@ describe("BlockView:", () => { }, global: { stubs: ["router-link"], - plugins: [i18n], + plugins: [i18n, $testId], }, }); expect(wrapper.find(".breadcrumb-item span").text()).toBe("Block #42"); From 7a9f82627791a660eb6883d95e87c2f4504d0779 Mon Sep 17 00:00:00 2001 From: Vasyl Ivanchuk Date: Tue, 5 Dec 2023 12:02:08 +0200 Subject: [PATCH 07/52] feat: enable sepolia network on prod (#108) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # What ❔ Enable sepolia network on prod. ## Checklist - [X] PR title corresponds to the body of PR (we generate changelog entries from PRs). --- packages/app/src/configs/production.config.json | 2 +- packages/app/tests/e2e/features/copying.feature | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/configs/production.config.json b/packages/app/src/configs/production.config.json index 0214a28683..bc25acca32 100644 --- a/packages/app/src/configs/production.config.json +++ b/packages/app/src/configs/production.config.json @@ -31,7 +31,7 @@ "l2WalletUrl": "https://portal.zksync.io/", "maintenance": false, "name": "sepolia", - "published": false, + "published": true, "rpcUrl": "https://sepolia.era.zksync.dev" }, { diff --git a/packages/app/tests/e2e/features/copying.feature b/packages/app/tests/e2e/features/copying.feature index f0089581f5..6aa9b48c39 100644 --- a/packages/app/tests/e2e/features/copying.feature +++ b/packages/app/tests/e2e/features/copying.feature @@ -109,7 +109,7 @@ Feature: Copying Examples: | Text | - | 0x00030000000000020005000000000002000200000001035500000060011002700000010f0010019d000100000000001f0000008001000039000000400010043f00000001012001900000003c0000c13d0000000002000031000000040120008c0000029c0000413d0000000201000367000000000301043b000000e003300270000001110430009c000000440000a13d000001120430009c000000650000a13d000001130430009c000000e80000613d000001140130009c0000028b0000613d000001150130009c0000029c0000c13d0000000001000416000000000110004c0000029c0000c13d000000040100008a00000000011000310000011d02000041000000200310008c000000000300001900000000030240190000011d01100197000000000410004c000000000200a0190000011d0110009c00000000010300190000000001026019000000000110004c0000029c0000c13d00000004010000390000000201100367000000000101043b0000011e011001970000000000100435000000200000043f0000000001000019043803fe0000040f000000000101041a000000400200043d00000000001204350000010f010000410000010f0320009c000000000102401900000040011002100000011f011001c7000004390001042e0000000001000416000000000110004c0000029c0000c13d0000002001000039000001000010044300000120000004430000011001000041000004390001042e000001180130009c000000a20000213d0000011b0130009c0000020b0000613d0000011c0130009c0000029c0000c13d0000000001000416000000000110004c0000029c0000c13d000000040100008a00000000011000310000011d02000041000000000310004c000000000300001900000000030240190000011d01100197000000000410004c000000000200a0190000011d0110009c00000000010300190000000001026019000000000110004c0000029c0000c13d0000000101000039000000000101041a000000400200043d00000000001204350000010f010000410000010f0320009c000000000102401900000040011002100000011f011001c7000004390001042e000001160430009c000002330000613d000001170130009c0000029c0000c13d0000000001000416000000000110004c0000029c0000c13d000000040100008a00000000011000310000011d02000041000000600310008c000000000300001900000000030240190000011d01100197000000000410004c000000000200a0190000011d0110009c00000000010300190000000001026019000000000110004c0000029c0000c13d00000002010003670000000402100370000000000402043b0000011e0240009c0000029c0000213d0000002402100370000000000202043b0000011e052001970000011e0220009c0000029c0000213d00000000020004110000004401100370000000000301043b000080060120008c0000033e0000613d000000090100008a000000000112016f000080010110008c0000033e0000613d000000400100043d00000064021000390000012703000041000000000032043500000044021000390000012803000041000000000032043500000024021000390000003e030000390000000000320435000001290200004100000000002104350000000402100039000000200300003900000000003204350000010f020000410000010f0310009c000000000102801900000040011002100000012a011001c70000043a00010430000001190130009c000002710000613d0000011a0130009c0000029c0000c13d0000000001000416000000000110004c0000029c0000c13d000000040100008a00000000011000310000011d02000041000000400310008c000000000300001900000000030240190000011d01100197000000000410004c000000000200a0190000011d0110009c00000000010300190000000001026019000000000110004c0000029c0000c13d00000002010003670000000402100370000000000402043b0000011e0240009c0000029c0000213d0000002401100370000000000501043b0000000001000411000080010110008c000002b80000c13d0000000101000039000000000301041a0000000002530019000000000332004b000000000300001900000001030040390000000103300190000000e20000c13d000400000005001d000000000021041b0000000000400435000000200000043f0000010f0100004100000000020004140000010f0320009c0000000001024019000000c00110021000000122011001c70000801002000039000500000004001d043804330000040f000000050500002900000001022001900000029c0000613d000000000101043b000000000301041a00000004040000290000000002430019000000000332004b0000000003000019000000010300403900000001033001900000038b0000613d000001310100004100000000001004350000001101000039000000040010043f00000132010000410000043a00010430000000040320008a0000011d04000041000000400530008c000000000500001900000000050440190000011d03300197000000000630004c000000000400a0190000011d0330009c00000000030500190000000003046019000000000330004c0000029c0000c13d0000000403100370000000000803043b0000011e0380009c0000029c0000213d0000002403100370000000000303043b000001210430009c0000029c0000213d00000023043000390000011d05000041000000000624004b000000000600001900000000060580190000011d022001970000011d04400197000000000724004b0000000005008019000000000224013f0000011d0220009c00000000020600190000000002056019000000000220004c0000029c0000c13d0000000402300039000000000121034f000000000101043b000001210210009c0000026b0000213d000000bf02100039000000200500008a000000000252016f000001210420009c0000026b0000213d000000400020043f000000800010043f000000240330003900000000023100190000000004000031000000000242004b0000029c0000213d000400000005001d0000001f0210018f000000020330036700000005041002720000012b0000613d00000000050000190000000506500210000000000763034f000000000707043b000000a00660003900000000007604350000000105500039000000000645004b000001230000413d000500000008001d000000000520004c0000013b0000613d0000000504400210000000000343034f0000000302200210000000a004400039000000000504043300000000052501cf000000000525022f000000000303043b0000010002200089000000000323022f00000000022301cf000000000252019f0000000000240435000000a00110003900000000000104350000000001000416000300000001001d00000000010004100000000000100435000000200000043f0000010f0100004100000000020004140000010f0320009c0000000001024019000000c00110021000000122011001c70000801002000039043804330000040f0000000102200190000000050400002900000004070000290000029c0000613d000000000101043b000000000201041a00000003090000290000000002920049000000000021041b0000000101000039000000000201041a0000000002920049000000000021041b0000012302000041000000400100043d000000200310003900000000002304350000006002400210000000240310003900000000002304350000000008000411000000600280021000000058031000390000000000230435000000380210003900000000009204350000006c03100039000000800200043d000000000420004c000001700000613d00000000040000190000000005340019000000a006400039000000000606043300000000006504350000002004400039000000000524004b000001690000413d000000000332001900000000000304350000004c0320003900000000003104350000008b02200039000000000272016f000000000a12001900000000022a004b000000000200001900000001020040390000012103a0009c0000026b0000213d00000001022001900000026b0000c13d000100000008001d0000004000a0043f000001240200004100000000002a04350000000402a000390000002003000039000000000032043500000000020104330000002403a0003900000000002304350000004403a00039000000000420004c000001930000613d000000000400001900000000053400190000002004400039000000000614001900000000060604330000000000650435000000000524004b0000018c0000413d000000000132001900000000000104350000001f01200039000000000171016f0000010f020000410000010f03a0009c000000000302001900000000030a4019000000400330021000000044011000390000010f0410009c00000000010280190000006001100210000000000131019f00000000030004140000010f0430009c0000000002034019000000c002200210000000000112019f000080080200003900020000000a001d0438042e0000040f000000020a000029000000000301001900000060033002700000010f03300197000000200430008c000000200400003900000000040340190000001f0540018f0000000506400272000001bc0000613d0000000007000019000000050870021000000000098a0019000000000881034f000000000808043b00000000008904350000000107700039000000000867004b000001b40000413d000000000750004c000001cb0000613d0000000506600210000000000761034f00000000066a00190000000305500210000000000806043300000000085801cf000000000858022f000000000707043b0000010005500089000000000757022f00000000055701cf000000000585019f0000000000560435000100000003001f0000000102200190000003d80000613d0000001f01400039000000600210018f0000000001a20019000000000221004b00000000020000190000000102004039000001210410009c000000050400002900000003050000290000026b0000213d00000001022001900000026b0000c13d000000400010043f000000200230008c0000029c0000413d00000020021000390000004003000039000000000032043500000000005104350000004003100039000000800200043d000000000023043500000060031000390000011e06400197000000000420004c000001f00000613d00000000040000190000000005340019000000a007400039000000000707043300000000007504350000002004400039000000000524004b000001e90000413d000000000332001900000000000304350000007f022000390000000403000029000000000232016f0000010f030000410000010f0410009c000000000103801900000040011002100000010f0420009c00000000020380190000006002200210000000000112019f00000000020004140000010f0420009c0000000002038019000000c002200210000000000112019f00000125011001c70000800d020000390000000303000039000001260400004100000001050000290438042e0000040f0000000101200190000003d60000c13d0000029c0000013d0000000001000416000000000110004c0000029c0000c13d000000040100008a00000000011000310000011d02000041000000000310004c000000000300001900000000030240190000011d01100197000000000410004c000000000200a0190000011d0110009c00000000010300190000000001026019000000000110004c0000029c0000c13d000000c001000039000000400010043f0000000501000039000000800010043f0000013501000041000000a00010043f0000002001000039000000400200043d000500000002001d00000000021204360000008001000039043804100000040f000000050400002900000000014100490000010f020000410000010f0310009c00000000010280190000010f0340009c000000000204401900000040022002100000006001100210000000000121019f000004390001042e000000040220008a0000011d03000041000000200420008c000000000400001900000000040340190000011d02200197000000000520004c000000000300a0190000011d0220009c00000000020400190000000002036019000000000220004c0000029c0000c13d0000000401100370000000000401043b0000011e0140009c0000029c0000213d0000000001000416000400000001001d00000000010004100000000000100435000000200000043f0000010f0100004100000000020004140000010f0320009c0000000001024019000000c00110021000000122011001c70000801002000039000500000004001d043804330000040f000000050400002900000001022001900000029c0000613d000000000101043b000000000201041a00000004050000290000000002520049000000000021041b0000000101000039000000000201041a0000000002520049000000000021041b0000012302000041000000400100043d0000002003100039000000000023043500000060024002100000002403100039000000000023043500000038021000390000000000520435000000380200003900000000002104350000012f0210009c000002ca0000413d000001310100004100000000001004350000004101000039000000040010043f00000132010000410000043a000104300000000001000416000000000110004c0000029c0000c13d000000040100008a00000000011000310000011d02000041000000000310004c000000000300001900000000030240190000011d01100197000000000410004c000000000200a0190000011d0110009c00000000010300190000000001026019000000000110004c0000029c0000c13d000000400100043d000000120200003900000000002104350000010f020000410000010f0310009c000000000102801900000040011002100000011f011001c7000004390001042e0000000001000416000000000110004c0000029c0000c13d000000040100008a00000000011000310000011d02000041000000000310004c000000000300001900000000030240190000011d01100197000000000410004c000000000200a0190000011d0110009c00000000010300190000000001026019000000000110004c0000029e0000613d00000000010000190000043a00010430000000400100043d000400000001001d043804230000040f00000004030000290000002001300039000001200200004100000000002104350000000301000039000000000013043500000000010300190000002002000039000000400300043d000500000003001d0000000002230436043804100000040f000000050400002900000000014100490000010f020000410000010f0310009c00000000010280190000010f0340009c000000000204401900000040022002100000006001100210000000000121019f000004390001042e000000400100043d00000044021000390000013303000041000000000032043500000024021000390000001f030000390000000000320435000001290200004100000000002104350000000402100039000000200300003900000000003204350000010f020000410000010f0310009c000000000102801900000040011002100000012e011001c70000043a000104300000006007100039000000400070043f00000124020000410000000000270435000000640210003900000020030000390000000000320435000000840310003900000000020104330000000000230435000000a403100039000000000420004c000002df0000613d000000000400001900000000053400190000002004400039000000000614001900000000060604330000000000650435000000000524004b000002d80000413d000000000132001900000000000104350000001f01200039000000200200008a000000000121016f0000010f020000410000010f0370009c00000000030200190000000003074019000000400330021000000044011000390000010f0410009c00000000010280190000006001100210000000000131019f00000000030004140000010f0430009c0000000002034019000000c002200210000000000112019f0000800802000039000300000007001d0438042e0000040f000000030a000029000000000301001900000060033002700000010f03300197000000200430008c000000200400003900000000040340190000001f0540018f0000000506400272000003090000613d0000000007000019000000050870021000000000098a0019000000000881034f000000000808043b00000000008904350000000107700039000000000867004b000003010000413d000000000750004c000003180000613d0000000506600210000000000761034f00000000066a00190000000305500210000000000806043300000000085801cf000000000858022f000000000707043b0000010005500089000000000757022f00000000055701cf000000000585019f0000000000560435000100000003001f0000000102200190000003650000613d0000001f01400039000000600210018f0000000001a20019000000000221004b00000000020000190000000102004039000001210410009c000000050500002900000004040000290000026b0000213d00000001022001900000026b0000c13d000000400010043f000000200230008c0000029c0000413d00000000004104350000010f0200004100000000030004140000010f0430009c00000000030280190000010f0410009c00000000010280190000004001100210000000c002300210000000000112019f0000012b011001c70000011e065001970000800d020000390000000303000039000001300400004100000000050004110438042e0000040f0000000101200190000003d60000c13d0000029c0000013d000400000003001d0000000000400435000000200000043f0000010f0100004100000000020004140000010f0320009c0000000001024019000000c00110021000000122011001c70000801002000039000500000004001d000300000005001d043804330000040f000000050300002900000001022001900000029c0000613d000000000101043b000000000201041a0000000401000029000000000112004b0000039f0000813d000000400100043d00000044021000390000012d03000041000000000032043500000024021000390000001f030000390000000000320435000001290200004100000000002104350000000402100039000000200300003900000000003204350000010f020000410000010f0310009c000000000102801900000040011002100000012e011001c70000043a00010430000000400200043d0000001f0430018f0000000503300272000003720000613d000000000500001900000005065002100000000007620019000000000661034f000000000606043b00000000006704350000000105500039000000000635004b0000036a0000413d000000000540004c000003810000613d0000000503300210000000000131034f00000000033200190000000304400210000000000503043300000000054501cf000000000545022f000000000101043b0000010004400089000000000141022f00000000014101cf000000000151019f00000000001304350000010f0100004100000001030000310000010f0430009c00000000030180190000010f0420009c000000000102401900000040011002100000006002300210000000000112019f0000043a00010430000000000021041b000000400100043d00000000004104350000010f0200004100000000030004140000010f0430009c00000000030280190000010f0410009c00000000010280190000004001100210000000c002300210000000000112019f0000012b011001c70000800d02000039000000020300003900000134040000410438042e0000040f00000001012001900000029c0000613d000003d60000013d000200000002001d0000000000300435000000200000043f0000010f0100004100000000020004140000010f0320009c0000000001024019000000c00110021000000122011001c70000801002000039043804330000040f000000030300002900000001022001900000029c0000613d000000040200002900000002040000290000000002240049000000000101043b000000000021041b00000000003004350000010f0100004100000000020004140000010f0320009c0000000001024019000000c00110021000000122011001c70000801002000039043804330000040f0000000306000029000000050500002900000001022001900000029c0000613d000000000101043b000000000201041a00000004030000290000000002320019000000000021041b000000400100043d00000000003104350000010f0200004100000000030004140000010f0430009c00000000030280190000010f0410009c00000000010280190000004001100210000000c002300210000000000112019f0000012b011001c70000800d0200003900000003030000390000012c040000410438042e0000040f00000001012001900000029c0000613d0000000001000019000004390001042e000000400200043d0000001f0430018f0000000503300272000003e50000613d000000000500001900000005065002100000000007620019000000000661034f000000000606043b00000000006704350000000105500039000000000635004b000003dd0000413d000000000540004c000003f40000613d0000000503300210000000000131034f00000000033200190000000304400210000000000503043300000000054501cf000000000545022f000000000101043b0000010004400089000000000141022f00000000014101cf000000000151019f00000000001304350000010f0100004100000001030000310000010f0430009c00000000030180190000010f0420009c000000000102401900000040011002100000006002300210000000000112019f0000043a000104300000010f0200004100000000030004140000010f0430009c00000000030280190000010f0410009c00000000010280190000004001100210000000c002300210000000000112019f00000122011001c70000801002000039043804330000040f00000001022001900000040e0000613d000000000101043b000000000001042d00000000010000190000043a0001043000000000030104330000000002320436000000000430004c0000041c0000613d000000000400001900000000052400190000002004400039000000000614001900000000060604330000000000650435000000000534004b000004150000413d000000000123001900000000000104350000001f01300039000000200300008a000000000131016f0000000001120019000000000001042d000001360210009c000004280000813d0000004001100039000000400010043f000000000001042d000001310100004100000000001004350000004101000039000000040010043f00000132010000410000043a0001043000000431002104210000000102000039000000000001042d0000000002000019000000000001042d00000436002104230000000102000039000000000001042d0000000002000019000000000001042d0000043800000432000004390001042e0000043a00010430000000000000000000000000000000000000000000000000000000000000000000000000ffffffff00000002000000000000000000000000000000400000010000000000000000000000000000000000000000000000000000000000000000000000000051cff8d80000000000000000000000000000000000000000000000000000000084bc3eaf0000000000000000000000000000000000000000000000000000000084bc3eb00000000000000000000000000000000000000000000000000000000095d89b41000000000000000000000000000000000000000000000000000000009cc7f7080000000000000000000000000000000000000000000000000000000051cff8d900000000000000000000000000000000000000000000000000000000579952fc00000000000000000000000000000000000000000000000000000000313ce56600000000000000000000000000000000000000000000000000000000313ce5670000000000000000000000000000000000000000000000000000000040c10f190000000000000000000000000000000000000000000000000000000006fdde030000000000000000000000000000000000000000000000000000000018160ddd8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000200000000000000000000000004554480000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffff02000000000000000000000000000000000000400000000000000000000000006c0960f90000000000000000000000000000000000000000000000000000000062f84b24000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000c405fe8958410bbaf0c73b7a0c3e20859e86ca168a4c9b0def9c54d2555a306b616c206163636573732063616e2063616c6c2074686973206d6574686f6400004f6e6c792073797374656d20636f6e747261637473207769746820737065636908c379a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000840000000000000000000000000200000000000000000000000000000000000020000000000000000000000000ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef5472616e7366657220616d6f756e7420657863656564732062616c616e6365000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffa02717ead6b9200dd235aad468c9809ea400fe33ac69b5bfaa6d3e90fc922b63984e487b7100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002400000000000000000000000043616c6c61626c65206f6e6c792062792074686520626f6f746c6f61646572000f6798a560793a54c3bcfe86a93cde1e73087d944c0ea20544137d41213968854574686572000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffc000000000000000000000000000000000000000000000000000000000000000006487144452ecab754e690468f49e4e530cd29f0c84820730e6499b5510870110 | + | 0x00030000000000020005000000000002000200000001035500000060011002700000010f0010019d000100000000001f0000008001000039000000400010043f00000001012001900000003c0000c13d0000000002000031000000040120008c000002990000413d0000000201000367000000000301043b000000e003300270000001110430009c000000440000a13d000001120430009c000000650000a13d000001130430009c000000e50000613d000001140130009c000002880000613d000001150130009c000002990000c13d0000000001000416000000000101004b000002990000c13d000000040100008a00000000011000310000011d02000041000000200310008c000000000300001900000000030240190000011d01100197000000000401004b000000000200a0190000011d0110009c00000000010300190000000001026019000000000101004b000002990000c13d00000004010000390000000201100367000000000101043b0000011e011001970000000000100435000000200000043f0000000001000019043503fb0000040f000000000101041a000000400200043d00000000001204350000010f010000410000010f0320009c000000000102401900000040011002100000011f011001c7000004360001042e0000000001000416000000000101004b000002990000c13d0000002001000039000001000010044300000120000004430000011001000041000004360001042e000001180130009c000000a20000213d0000011b0130009c000002080000613d0000011c0130009c000002990000c13d0000000001000416000000000101004b000002990000c13d000000040100008a00000000011000310000011d02000041000000000301004b000000000300001900000000030240190000011d01100197000000000401004b000000000200a0190000011d0110009c00000000010300190000000001026019000000000101004b000002990000c13d0000000101000039000000000101041a000000400200043d00000000001204350000010f010000410000010f0320009c000000000102401900000040011002100000011f011001c7000004360001042e000001160430009c000002300000613d000001170130009c000002990000c13d0000000001000416000000000101004b000002990000c13d000000040100008a00000000011000310000011d02000041000000600310008c000000000300001900000000030240190000011d01100197000000000401004b000000000200a0190000011d0110009c00000000010300190000000001026019000000000101004b000002990000c13d00000002010003670000000402100370000000000402043b0000011e0240009c000002990000213d0000002402100370000000000202043b0000011e052001970000011e0220009c000002990000213d00000000020004110000004401100370000000000301043b000080060120008c0000033b0000613d000000090100008a000000000112016f000080010110008c0000033b0000613d000000400100043d00000064021000390000012703000041000000000032043500000044021000390000012803000041000000000032043500000024021000390000003e030000390000000000320435000001290200004100000000002104350000000402100039000000200300003900000000003204350000010f020000410000010f0310009c000000000102801900000040011002100000012a011001c70000043700010430000001190130009c0000026e0000613d0000011a0130009c000002990000c13d0000000001000416000000000101004b000002990000c13d000000040100008a00000000011000310000011d02000041000000400310008c000000000300001900000000030240190000011d01100197000000000401004b000000000200a0190000011d0110009c00000000010300190000000001026019000000000101004b000002990000c13d00000002010003670000000402100370000000000402043b0000011e0240009c000002990000213d0000002401100370000000000501043b0000000001000411000080010110008c000002b50000c13d0000000101000039000000000301041a0000000002530019000000000332004b000000000300001900000001030040390000000103300190000000df0000c13d000400000005001d000000000021041b0000000000400435000000200000043f0000010f0100004100000000020004140000010f0320009c0000000001024019000000c00110021000000122011001c70000801002000039000500000004001d043504300000040f00000005050000290000000102200190000002990000613d000000000101043b000000000201041a0000000403000029000000000232001a000000df0000413d000003880000013d000001310100004100000000001004350000001101000039000000040010043f00000132010000410000043700010430000000040320008a0000011d04000041000000400530008c000000000500001900000000050440190000011d03300197000000000603004b000000000400a0190000011d0330009c00000000030500190000000003046019000000000303004b000002990000c13d0000000403100370000000000803043b0000011e0380009c000002990000213d0000002403100370000000000303043b000001210430009c000002990000213d00000023043000390000011d05000041000000000624004b000000000600001900000000060580190000011d022001970000011d04400197000000000724004b0000000005008019000000000224013f0000011d0220009c00000000020600190000000002056019000000000202004b000002990000c13d0000000402300039000000000121034f000000000101043b000001210210009c000002680000213d000000bf02100039000000200500008a000000000252016f000001210420009c000002680000213d000000400020043f000000800010043f000000240330003900000000023100190000000004000031000000000242004b000002990000213d000400000005001d0000001f0210018f00000002033003670000000504100272000001280000613d00000000050000190000000506500210000000000763034f000000000707043b000000a00660003900000000007604350000000105500039000000000645004b000001200000413d000500000008001d000000000502004b000001380000613d0000000504400210000000000343034f0000000302200210000000a004400039000000000504043300000000052501cf000000000525022f000000000303043b0000010002200089000000000323022f00000000022301cf000000000252019f0000000000240435000000a00110003900000000000104350000000001000416000300000001001d00000000010004100000000000100435000000200000043f0000010f0100004100000000020004140000010f0320009c0000000001024019000000c00110021000000122011001c70000801002000039043504300000040f000000010220019000000005040000290000000407000029000002990000613d000000000101043b000000000201041a00000003090000290000000002920049000000000021041b0000000101000039000000000201041a0000000002920049000000000021041b0000012302000041000000400100043d000000200310003900000000002304350000006002400210000000240310003900000000002304350000000008000411000000600280021000000058031000390000000000230435000000380210003900000000009204350000006c03100039000000800200043d000000000402004b0000016d0000613d00000000040000190000000005340019000000a006400039000000000606043300000000006504350000002004400039000000000524004b000001660000413d000000000332001900000000000304350000004c0320003900000000003104350000008b02200039000000000272016f000000000a12001900000000022a004b000000000200001900000001020040390000012103a0009c000002680000213d0000000102200190000002680000c13d000100000008001d0000004000a0043f000001240200004100000000002a04350000000402a000390000002003000039000000000032043500000000020104330000002403a0003900000000002304350000004403a00039000000000402004b000001900000613d000000000400001900000000053400190000002004400039000000000614001900000000060604330000000000650435000000000524004b000001890000413d000000000132001900000000000104350000001f01200039000000000171016f0000010f020000410000010f03a0009c000000000302001900000000030a4019000000400330021000000044011000390000010f0410009c00000000010280190000006001100210000000000131019f00000000030004140000010f0430009c0000000002034019000000c002200210000000000112019f000080080200003900020000000a001d0435042b0000040f000000020a000029000000000301001900000060033002700000010f03300197000000200430008c000000200400003900000000040340190000001f0540018f0000000506400272000001b90000613d0000000007000019000000050870021000000000098a0019000000000881034f000000000808043b00000000008904350000000107700039000000000867004b000001b10000413d000000000705004b000001c80000613d0000000506600210000000000761034f00000000066a00190000000305500210000000000806043300000000085801cf000000000858022f000000000707043b0000010005500089000000000757022f00000000055701cf000000000585019f0000000000560435000100000003001f0000000102200190000003d50000613d0000001f01400039000000600210018f0000000001a20019000000000221004b00000000020000190000000102004039000001210410009c00000005040000290000000305000029000002680000213d0000000102200190000002680000c13d000000400010043f000000200230008c000002990000413d00000020021000390000004003000039000000000032043500000000005104350000004003100039000000800200043d000000000023043500000060031000390000011e06400197000000000402004b000001ed0000613d00000000040000190000000005340019000000a007400039000000000707043300000000007504350000002004400039000000000524004b000001e60000413d000000000332001900000000000304350000007f022000390000000403000029000000000232016f0000010f030000410000010f0410009c000000000103801900000040011002100000010f0420009c00000000020380190000006002200210000000000112019f00000000020004140000010f0420009c0000000002038019000000c002200210000000000112019f00000125011001c70000800d020000390000000303000039000001260400004100000001050000290435042b0000040f0000000101200190000003d30000c13d000002990000013d0000000001000416000000000101004b000002990000c13d000000040100008a00000000011000310000011d02000041000000000301004b000000000300001900000000030240190000011d01100197000000000401004b000000000200a0190000011d0110009c00000000010300190000000001026019000000000101004b000002990000c13d000000c001000039000000400010043f0000000501000039000000800010043f0000013501000041000000a00010043f0000002001000039000000400200043d000500000002001d000000000212043600000080010000390435040d0000040f000000050400002900000000014100490000010f020000410000010f0310009c00000000010280190000010f0340009c000000000204401900000040022002100000006001100210000000000121019f000004360001042e000000040220008a0000011d03000041000000200420008c000000000400001900000000040340190000011d02200197000000000502004b000000000300a0190000011d0220009c00000000020400190000000002036019000000000202004b000002990000c13d0000000401100370000000000401043b0000011e0140009c000002990000213d0000000001000416000400000001001d00000000010004100000000000100435000000200000043f0000010f0100004100000000020004140000010f0320009c0000000001024019000000c00110021000000122011001c70000801002000039000500000004001d043504300000040f00000005040000290000000102200190000002990000613d000000000101043b000000000201041a00000004050000290000000002520049000000000021041b0000000101000039000000000201041a0000000002520049000000000021041b0000012302000041000000400100043d0000002003100039000000000023043500000060024002100000002403100039000000000023043500000038021000390000000000520435000000380200003900000000002104350000012f0210009c000002c70000413d000001310100004100000000001004350000004101000039000000040010043f000001320100004100000437000104300000000001000416000000000101004b000002990000c13d000000040100008a00000000011000310000011d02000041000000000301004b000000000300001900000000030240190000011d01100197000000000401004b000000000200a0190000011d0110009c00000000010300190000000001026019000000000101004b000002990000c13d000000400100043d000000120200003900000000002104350000010f020000410000010f0310009c000000000102801900000040011002100000011f011001c7000004360001042e0000000001000416000000000101004b000002990000c13d000000040100008a00000000011000310000011d02000041000000000301004b000000000300001900000000030240190000011d01100197000000000401004b000000000200a0190000011d0110009c00000000010300190000000001026019000000000101004b0000029b0000613d00000000010000190000043700010430000000400100043d000400000001001d043504200000040f00000004030000290000002001300039000001200200004100000000002104350000000301000039000000000013043500000000010300190000002002000039000000400300043d000500000003001d00000000022304360435040d0000040f000000050400002900000000014100490000010f020000410000010f0310009c00000000010280190000010f0340009c000000000204401900000040022002100000006001100210000000000121019f000004360001042e000000400100043d00000044021000390000013303000041000000000032043500000024021000390000001f030000390000000000320435000001290200004100000000002104350000000402100039000000200300003900000000003204350000010f020000410000010f0310009c000000000102801900000040011002100000012e011001c700000437000104300000006007100039000000400070043f00000124020000410000000000270435000000640210003900000020030000390000000000320435000000840310003900000000020104330000000000230435000000a403100039000000000402004b000002dc0000613d000000000400001900000000053400190000002004400039000000000614001900000000060604330000000000650435000000000524004b000002d50000413d000000000132001900000000000104350000001f01200039000000200200008a000000000121016f0000010f020000410000010f0370009c00000000030200190000000003074019000000400330021000000044011000390000010f0410009c00000000010280190000006001100210000000000131019f00000000030004140000010f0430009c0000000002034019000000c002200210000000000112019f0000800802000039000300000007001d0435042b0000040f000000030a000029000000000301001900000060033002700000010f03300197000000200430008c000000200400003900000000040340190000001f0540018f0000000506400272000003060000613d0000000007000019000000050870021000000000098a0019000000000881034f000000000808043b00000000008904350000000107700039000000000867004b000002fe0000413d000000000705004b000003150000613d0000000506600210000000000761034f00000000066a00190000000305500210000000000806043300000000085801cf000000000858022f000000000707043b0000010005500089000000000757022f00000000055701cf000000000585019f0000000000560435000100000003001f0000000102200190000003620000613d0000001f01400039000000600210018f0000000001a20019000000000221004b00000000020000190000000102004039000001210410009c00000005050000290000000404000029000002680000213d0000000102200190000002680000c13d000000400010043f000000200230008c000002990000413d00000000004104350000010f0200004100000000030004140000010f0430009c00000000030280190000010f0410009c00000000010280190000004001100210000000c002300210000000000112019f0000012b011001c70000011e065001970000800d020000390000000303000039000001300400004100000000050004110435042b0000040f0000000101200190000003d30000c13d000002990000013d000400000003001d0000000000400435000000200000043f0000010f0100004100000000020004140000010f0320009c0000000001024019000000c00110021000000122011001c70000801002000039000500000004001d000300000005001d043504300000040f00000005030000290000000102200190000002990000613d000000000101043b000000000201041a0000000401000029000000000112004b0000039c0000813d000000400100043d00000044021000390000012d03000041000000000032043500000024021000390000001f030000390000000000320435000001290200004100000000002104350000000402100039000000200300003900000000003204350000010f020000410000010f0310009c000000000102801900000040011002100000012e011001c70000043700010430000000400200043d0000001f0430018f00000005033002720000036f0000613d000000000500001900000005065002100000000007620019000000000661034f000000000606043b00000000006704350000000105500039000000000635004b000003670000413d000000000504004b0000037e0000613d0000000503300210000000000131034f00000000033200190000000304400210000000000503043300000000054501cf000000000545022f000000000101043b0000010004400089000000000141022f00000000014101cf000000000151019f00000000001304350000010f0100004100000001030000310000010f0430009c00000000030180190000010f0420009c000000000102401900000040011002100000006002300210000000000112019f0000043700010430000000000021041b000000400100043d00000000003104350000010f0200004100000000030004140000010f0430009c00000000030280190000010f0410009c00000000010280190000004001100210000000c002300210000000000112019f0000012b011001c70000800d02000039000000020300003900000134040000410435042b0000040f0000000101200190000002990000613d000003d30000013d000200000002001d0000000000300435000000200000043f0000010f0100004100000000020004140000010f0320009c0000000001024019000000c00110021000000122011001c70000801002000039043504300000040f00000003030000290000000102200190000002990000613d000000040200002900000002040000290000000002240049000000000101043b000000000021041b00000000003004350000010f0100004100000000020004140000010f0320009c0000000001024019000000c00110021000000122011001c70000801002000039043504300000040f000000030600002900000005050000290000000102200190000002990000613d000000000101043b000000000201041a00000004030000290000000002320019000000000021041b000000400100043d00000000003104350000010f0200004100000000030004140000010f0430009c00000000030280190000010f0410009c00000000010280190000004001100210000000c002300210000000000112019f0000012b011001c70000800d0200003900000003030000390000012c040000410435042b0000040f0000000101200190000002990000613d0000000001000019000004360001042e000000400200043d0000001f0430018f0000000503300272000003e20000613d000000000500001900000005065002100000000007620019000000000661034f000000000606043b00000000006704350000000105500039000000000635004b000003da0000413d000000000504004b000003f10000613d0000000503300210000000000131034f00000000033200190000000304400210000000000503043300000000054501cf000000000545022f000000000101043b0000010004400089000000000141022f00000000014101cf000000000151019f00000000001304350000010f0100004100000001030000310000010f0430009c00000000030180190000010f0420009c000000000102401900000040011002100000006002300210000000000112019f00000437000104300000010f0200004100000000030004140000010f0430009c00000000030280190000010f0410009c00000000010280190000004001100210000000c002300210000000000112019f00000122011001c70000801002000039043504300000040f00000001022001900000040b0000613d000000000101043b000000000001042d0000000001000019000004370001043000000000030104330000000002320436000000000403004b000004190000613d000000000400001900000000052400190000002004400039000000000614001900000000060604330000000000650435000000000534004b000004120000413d000000000123001900000000000104350000001f01300039000000200300008a000000000131016f0000000001120019000000000001042d000001360210009c000004250000813d0000004001100039000000400010043f000000000001042d000001310100004100000000001004350000004101000039000000040010043f000001320100004100000437000104300000042e002104210000000102000039000000000001042d0000000002000019000000000001042d00000433002104230000000102000039000000000001042d0000000002000019000000000001042d0000043500000432000004360001042e0000043700010430000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffff00000002000000000000000000000000000000400000010000000000000000000000000000000000000000000000000000000000000000000000000051cff8d80000000000000000000000000000000000000000000000000000000084bc3eaf0000000000000000000000000000000000000000000000000000000084bc3eb00000000000000000000000000000000000000000000000000000000095d89b41000000000000000000000000000000000000000000000000000000009cc7f7080000000000000000000000000000000000000000000000000000000051cff8d900000000000000000000000000000000000000000000000000000000579952fc00000000000000000000000000000000000000000000000000000000313ce56600000000000000000000000000000000000000000000000000000000313ce5670000000000000000000000000000000000000000000000000000000040c10f190000000000000000000000000000000000000000000000000000000006fdde030000000000000000000000000000000000000000000000000000000018160ddd8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000200000000000000000000000004554480000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffff02000000000000000000000000000000000000400000000000000000000000006c0960f90000000000000000000000000000000000000000000000000000000062f84b24000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000c405fe8958410bbaf0c73b7a0c3e20859e86ca168a4c9b0def9c54d2555a306b616c206163636573732063616e2063616c6c2074686973206d6574686f6400004f6e6c792073797374656d20636f6e747261637473207769746820737065636908c379a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000840000000000000000000000000200000000000000000000000000000000000020000000000000000000000000ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef5472616e7366657220616d6f756e7420657863656564732062616c616e6365000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffa02717ead6b9200dd235aad468c9809ea400fe33ac69b5bfaa6d3e90fc922b63984e487b7100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002400000000000000000000000043616c6c61626c65206f6e6c792062792074686520626f6f746c6f61646572000f6798a560793a54c3bcfe86a93cde1e73087d944c0ea20544137d41213968854574686572000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffc000000000000000000000000000000000000000000000000000000000000000007b3ba959bf7eac2739d6ef137ed2b810585c27a9dbd1782f8efe2a761831b7e4 | @id266:I @testnet Scenario Outline: Check "" hashes copying on Block page From bb89707c5336753fe820525b25a603ab70d5162b Mon Sep 17 00:00:00 2001 From: Vasyl Ivanchuk Date: Tue, 5 Dec 2023 14:29:41 +0200 Subject: [PATCH 08/52] feat: return gasUsed, gasPerPubdata, maxFeePerGas and maxPriorityFeePerGas fields for transaction (#109) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # What ❔ Return `gasUsed`, `gasPerPubdata`, `maxFeePerGas` and `maxPriorityFeePerGas` fields for transaction. ## Why ❔ For better UX we want to show gas and fee related fields on UI so we need to return these fields from the API first. ## Checklist - [X] PR title corresponds to the body of PR (we generate changelog entries from PRs). - [X] Tests for the changes have been added / updated. --- .../api/account/account.controller.spec.ts | 4 +- .../transaction.controller.spec.ts | 7 +- packages/api/src/block/block.controller.ts | 6 +- packages/api/src/block/block.module.ts | 4 +- packages/api/src/block/block.service.spec.ts | 10 +- packages/api/src/block/block.service.ts | 16 +- ...blockDetail.dto.ts => blockDetails.dto.ts} | 2 +- ...etail.entity.ts => blockDetails.entity.ts} | 2 +- .../hexToDecimalNumber.transformer.spec.ts | 27 +++ .../hexToDecimalNumber.transformer.ts | 17 ++ .../src/transaction/dtos/transaction.dto.ts | 43 +++- .../dtos/transactionDetails.dto.ts | 11 + .../entities/transaction.entity.ts | 10 + .../entities/transactionDetails.entity.ts | 18 ++ .../src/transaction/transaction.controller.ts | 9 +- .../api/src/transaction/transaction.module.ts | 3 +- .../transaction/transaction.service.spec.ts | 51 ++++- .../src/transaction/transaction.service.ts | 12 +- packages/api/test/account-api.e2e-spec.ts | 6 +- packages/api/test/address.e2e-spec.ts | 6 +- packages/api/test/block-api.e2e-spec.ts | 6 +- packages/api/test/block.e2e-spec.ts | 6 +- packages/api/test/log-api.e2e-spec.ts | 6 +- packages/api/test/stats-api.e2e-spec.ts | 6 +- packages/api/test/stats.e2e-spec.ts | 6 +- packages/api/test/token-api.e2e-spec.ts | 6 +- packages/api/test/token.e2e-spec.ts | 6 +- packages/api/test/transaction-api.e2e-spec.ts | 6 +- packages/api/test/transaction.e2e-spec.ts | 191 +++++++++++++----- 29 files changed, 381 insertions(+), 122 deletions(-) rename packages/api/src/block/{blockDetail.dto.ts => blockDetails.dto.ts} (98%) rename packages/api/src/block/{blockDetail.entity.ts => blockDetails.entity.ts} (97%) create mode 100644 packages/api/src/common/transformers/hexToDecimalNumber.transformer.spec.ts create mode 100644 packages/api/src/common/transformers/hexToDecimalNumber.transformer.ts create mode 100644 packages/api/src/transaction/dtos/transactionDetails.dto.ts create mode 100644 packages/api/src/transaction/entities/transactionDetails.entity.ts diff --git a/packages/api/src/api/account/account.controller.spec.ts b/packages/api/src/api/account/account.controller.spec.ts index 912a81c166..f5b2f14c6e 100644 --- a/packages/api/src/api/account/account.controller.spec.ts +++ b/packages/api/src/api/account/account.controller.spec.ts @@ -3,7 +3,7 @@ import { mock } from "jest-mock-extended"; import { BadRequestException, Logger } from "@nestjs/common"; import { L2_ETH_TOKEN_ADDRESS } from "../../common/constants"; import { BlockService } from "../../block/block.service"; -import { BlockDetail } from "../../block/blockDetail.entity"; +import { BlockDetails } from "../../block/blockDetails.entity"; import { TransactionService } from "../../transaction/transaction.service"; import { BalanceService } from "../../balance/balance.service"; import { TransactionStatus } from "../../transaction/entities/transaction.entity"; @@ -629,7 +629,7 @@ describe("AccountController", () => { it("returns blocks list response when block by miner are found", async () => { jest .spyOn(blockServiceMock, "findMany") - .mockResolvedValue([{ number: 1, timestamp: new Date("2023-03-03") } as BlockDetail]); + .mockResolvedValue([{ number: 1, timestamp: new Date("2023-03-03") } as BlockDetails]); const response = await controller.getAccountMinedBlocks(address, { page: 1, offset: 10, diff --git a/packages/api/src/api/transaction/transaction.controller.spec.ts b/packages/api/src/api/transaction/transaction.controller.spec.ts index 5e85dddc84..a74a252aa0 100644 --- a/packages/api/src/api/transaction/transaction.controller.spec.ts +++ b/packages/api/src/api/transaction/transaction.controller.spec.ts @@ -3,7 +3,8 @@ import { mock } from "jest-mock-extended"; import { Logger } from "@nestjs/common"; import { TransactionService } from "../../transaction/transaction.service"; import { TransactionReceiptService } from "../../transaction/transactionReceipt.service"; -import { TransactionStatus, Transaction } from "../../transaction/entities/transaction.entity"; +import { TransactionStatus } from "../../transaction/entities/transaction.entity"; +import { TransactionDetails } from "../../transaction/entities/transactionDetails.entity"; import { TransactionReceipt } from "../../transaction/entities/transactionReceipt.entity"; import { ResponseStatus, ResponseMessage } from "../dtos/common/responseBase.dto"; import { TransactionController } from "./transaction.controller"; @@ -56,7 +57,7 @@ describe("TransactionController", () => { it("returns isError as 0 when transaction is successful", async () => { jest .spyOn(transactionServiceMock, "findOne") - .mockResolvedValue({ status: TransactionStatus.Included } as Transaction); + .mockResolvedValue({ status: TransactionStatus.Included } as TransactionDetails); const response = await controller.getTransactionStatus(transactionHash); expect(response).toEqual({ @@ -72,7 +73,7 @@ describe("TransactionController", () => { it("returns isError as 1 when transaction is failed", async () => { jest .spyOn(transactionServiceMock, "findOne") - .mockResolvedValue({ status: TransactionStatus.Failed } as Transaction); + .mockResolvedValue({ status: TransactionStatus.Failed } as TransactionDetails); const response = await controller.getTransactionStatus(transactionHash); expect(response).toEqual({ diff --git a/packages/api/src/block/block.controller.ts b/packages/api/src/block/block.controller.ts index 25488e4506..f9a3a4febf 100644 --- a/packages/api/src/block/block.controller.ts +++ b/packages/api/src/block/block.controller.ts @@ -14,7 +14,7 @@ import { PagingOptionsDto, ListFiltersDto } from "../common/dtos"; import { ApiListPageOkResponse } from "../common/decorators/apiListPageOkResponse"; import { BlockService } from "./block.service"; import { BlockDto } from "./block.dto"; -import { BlockDetailDto } from "./blockDetail.dto"; +import { BlockDetailsDto } from "./blockDetails.dto"; import { swagger } from "../config/featureFlags"; const entityName = "blocks"; @@ -48,12 +48,12 @@ export class BlockController { example: "1", description: "Block number", }) - @ApiOkResponse({ description: "Block was returned successfully", type: BlockDetailDto }) + @ApiOkResponse({ description: "Block was returned successfully", type: BlockDetailsDto }) @ApiBadRequestResponse({ description: "Block number is invalid" }) @ApiNotFoundResponse({ description: "Block with the specified number does not exist" }) public async getBlock( @Param("blockNumber", new ParseLimitedIntPipe({ min: 0 })) blockNumber: number - ): Promise { + ): Promise { const block = await this.blockService.findOne(blockNumber); if (!block) { throw new NotFoundException(); diff --git a/packages/api/src/block/block.module.ts b/packages/api/src/block/block.module.ts index cbca1ea4d3..05687a8292 100644 --- a/packages/api/src/block/block.module.ts +++ b/packages/api/src/block/block.module.ts @@ -3,10 +3,10 @@ import { TypeOrmModule } from "@nestjs/typeorm"; import { BlockService } from "../block/block.service"; import { BlockController } from "./block.controller"; import { Block } from "./block.entity"; -import { BlockDetail } from "./blockDetail.entity"; +import { BlockDetails } from "./blockDetails.entity"; @Module({ - imports: [TypeOrmModule.forFeature([Block, BlockDetail])], + imports: [TypeOrmModule.forFeature([Block, BlockDetails])], controllers: [BlockController], providers: [BlockService], exports: [BlockService], diff --git a/packages/api/src/block/block.service.spec.ts b/packages/api/src/block/block.service.spec.ts index 90884e8de7..b1d2c853da 100644 --- a/packages/api/src/block/block.service.spec.ts +++ b/packages/api/src/block/block.service.spec.ts @@ -6,7 +6,7 @@ import { Pagination, IPaginationMeta } from "nestjs-typeorm-paginate"; import * as utils from "../common/utils"; import { BlockService, FindManyOptions } from "./block.service"; import { Block } from "./block.entity"; -import { BlockDetail } from "./blockDetail.entity"; +import { BlockDetails } from "./blockDetails.entity"; jest.mock("../common/utils"); @@ -14,11 +14,11 @@ describe("BlockService", () => { let blockRecord; let service: BlockService; let repositoryMock: Repository; - let blockDetailRepositoryMock: Repository; + let blockDetailRepositoryMock: Repository; beforeEach(async () => { repositoryMock = mock>(); - blockDetailRepositoryMock = mock>(); + blockDetailRepositoryMock = mock>(); blockRecord = { number: 123, @@ -32,7 +32,7 @@ describe("BlockService", () => { useValue: repositoryMock, }, { - provide: getRepositoryToken(BlockDetail), + provide: getRepositoryToken(BlockDetails), useValue: blockDetailRepositoryMock, }, ], @@ -305,7 +305,7 @@ describe("BlockService", () => { let filterOptions: FindManyOptions; beforeEach(() => { - queryBuilderMock = mock>({ + queryBuilderMock = mock>({ getMany: jest.fn().mockResolvedValue([ { number: 1, diff --git a/packages/api/src/block/block.service.ts b/packages/api/src/block/block.service.ts index bc289d9886..ef5f37735c 100644 --- a/packages/api/src/block/block.service.ts +++ b/packages/api/src/block/block.service.ts @@ -5,13 +5,13 @@ import { Pagination } from "nestjs-typeorm-paginate"; import { paginate } from "../common/utils"; import { IPaginationOptions } from "../common/types"; import { Block } from "./block.entity"; -import { BlockDetail } from "./blockDetail.entity"; +import { BlockDetails } from "./blockDetails.entity"; export interface FindManyOptions { miner?: string; page?: number; offset?: number; - selectFields?: (keyof BlockDetail)[]; + selectFields?: (keyof BlockDetails)[]; } @Injectable() @@ -19,8 +19,8 @@ export class BlockService { public constructor( @InjectRepository(Block) private readonly blocksRepository: Repository, - @InjectRepository(BlockDetail) - private readonly blockDetailsRepository: Repository + @InjectRepository(BlockDetails) + private readonly blockDetailsRepository: Repository ) {} private getBlock(filterOptions: FindOptionsWhere, orderOptions: FindOptionsOrder): Promise { @@ -50,9 +50,9 @@ export class BlockService { public async findOne( number: number, - selectFields?: (keyof BlockDetail)[], - relations: FindOptionsRelations = { batch: true } - ): Promise { + selectFields?: (keyof BlockDetails)[], + relations: FindOptionsRelations = { batch: true } + ): Promise { return await this.blockDetailsRepository.findOne({ where: { number }, relations: relations, @@ -90,7 +90,7 @@ export class BlockService { return await paginate(queryBuilder, paginationOptions, () => this.count(filterOptions)); } - public async findMany({ miner, page = 1, offset = 10, selectFields }: FindManyOptions): Promise { + public async findMany({ miner, page = 1, offset = 10, selectFields }: FindManyOptions): Promise { const queryBuilder = this.blockDetailsRepository.createQueryBuilder("block"); queryBuilder.addSelect(selectFields); if (miner) { diff --git a/packages/api/src/block/blockDetail.dto.ts b/packages/api/src/block/blockDetails.dto.ts similarity index 98% rename from packages/api/src/block/blockDetail.dto.ts rename to packages/api/src/block/blockDetails.dto.ts index 6034d3c86b..d80673960e 100644 --- a/packages/api/src/block/blockDetail.dto.ts +++ b/packages/api/src/block/blockDetails.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty } from "@nestjs/swagger"; import { BlockDto } from "./block.dto"; -export class BlockDetailDto extends BlockDto { +export class BlockDetailsDto extends BlockDto { @ApiProperty({ type: String, description: "The hash of the previous block", diff --git a/packages/api/src/block/blockDetail.entity.ts b/packages/api/src/block/blockDetails.entity.ts similarity index 97% rename from packages/api/src/block/blockDetail.entity.ts rename to packages/api/src/block/blockDetails.entity.ts index b5d81236eb..151f812a98 100644 --- a/packages/api/src/block/blockDetail.entity.ts +++ b/packages/api/src/block/blockDetails.entity.ts @@ -3,7 +3,7 @@ import { Block } from "./block.entity"; import { hexTransformer } from "../common/transformers/hex.transformer"; @Entity({ name: "blocks" }) -export class BlockDetail extends Block { +export class BlockDetails extends Block { @Column({ type: "bytea", transformer: hexTransformer, nullable: true }) public readonly parentHash?: string; diff --git a/packages/api/src/common/transformers/hexToDecimalNumber.transformer.spec.ts b/packages/api/src/common/transformers/hexToDecimalNumber.transformer.spec.ts new file mode 100644 index 0000000000..b53f7c9d9a --- /dev/null +++ b/packages/api/src/common/transformers/hexToDecimalNumber.transformer.spec.ts @@ -0,0 +1,27 @@ +import { hexToDecimalNumberTransformer } from "./hexToDecimalNumber.transformer"; + +describe("hexToDecimalNumberTransformer", () => { + describe("to", () => { + it("returns null for null input", () => { + const result = hexToDecimalNumberTransformer.to(null); + expect(result).toBeNull(); + }); + + it("returns hex representation of the decimal number string", () => { + const result = hexToDecimalNumberTransformer.to("800"); + expect(result).toBe("0x0320"); + }); + }); + + describe("from", () => { + it("returns null for null input", () => { + const result = hexToDecimalNumberTransformer.from(null); + expect(result).toBeNull(); + }); + + it("returns decimal representation of the hex number string", () => { + const result = hexToDecimalNumberTransformer.from("0x320"); + expect(result).toBe("800"); + }); + }); +}); diff --git a/packages/api/src/common/transformers/hexToDecimalNumber.transformer.ts b/packages/api/src/common/transformers/hexToDecimalNumber.transformer.ts new file mode 100644 index 0000000000..edb849a2a8 --- /dev/null +++ b/packages/api/src/common/transformers/hexToDecimalNumber.transformer.ts @@ -0,0 +1,17 @@ +import { BigNumber } from "ethers"; +import { ValueTransformer } from "typeorm"; + +export const hexToDecimalNumberTransformer: ValueTransformer = { + to(decimalNumberStr: string | null): string | null { + if (!decimalNumberStr) { + return null; + } + return BigNumber.from(decimalNumberStr).toHexString(); + }, + from(hexNumberStr: string | null): string | null { + if (!hexNumberStr) { + return null; + } + return BigNumber.from(hexNumberStr).toString(); + }, +}; diff --git a/packages/api/src/transaction/dtos/transaction.dto.ts b/packages/api/src/transaction/dtos/transaction.dto.ts index 94ee8676ad..8b77f54a1d 100644 --- a/packages/api/src/transaction/dtos/transaction.dto.ts +++ b/packages/api/src/transaction/dtos/transaction.dto.ts @@ -40,7 +40,7 @@ export class TransactionDto { @ApiProperty({ type: String, description: "The amount this transaction sent", - example: "0x2386f26fc10000", + example: "100000000", }) public readonly value: string; @@ -58,6 +58,47 @@ export class TransactionDto { }) public readonly nonce: number; + @ApiProperty({ + type: String, + description: "Gas price", + example: "100000000", + }) + public readonly gasPrice: string; + + @ApiProperty({ + type: String, + description: "Gas limit", + example: "100000000", + }) + public readonly gasLimit: string; + + @ApiProperty({ + type: String, + description: "Gas per pubdata limit", + example: "100000000", + examples: ["100000000", null], + required: false, + }) + public readonly gasPerPubdata?: string; + + @ApiProperty({ + type: String, + description: "Max fee per gas", + example: "100000000", + examples: ["100000000", null], + required: false, + }) + public readonly maxFeePerGas?: string; + + @ApiProperty({ + type: String, + description: "Max priority fee per gas", + example: "100000000", + examples: ["100000000", null], + required: false, + }) + public readonly maxPriorityFeePerGas?: string; + @ApiProperty({ type: Number, description: "The number (height) of the block this transaction was mined in", diff --git a/packages/api/src/transaction/dtos/transactionDetails.dto.ts b/packages/api/src/transaction/dtos/transactionDetails.dto.ts new file mode 100644 index 0000000000..0f18c8b72a --- /dev/null +++ b/packages/api/src/transaction/dtos/transactionDetails.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { TransactionDto } from "./transaction.dto"; + +export class TransactionDetailsDto extends TransactionDto { + @ApiProperty({ + type: String, + description: "Gas used by the transaction", + example: "50000000", + }) + public readonly gasUsed: string; +} diff --git a/packages/api/src/transaction/entities/transaction.entity.ts b/packages/api/src/transaction/entities/transaction.entity.ts index 60a0ac0566..e4a1b357e8 100644 --- a/packages/api/src/transaction/entities/transaction.entity.ts +++ b/packages/api/src/transaction/entities/transaction.entity.ts @@ -3,6 +3,7 @@ import { BaseEntity } from "../../common/entities/base.entity"; import { normalizeAddressTransformer } from "../../common/transformers/normalizeAddress.transformer"; import { bigIntNumberTransformer } from "../../common/transformers/bigIntNumber.transformer"; import { hexTransformer } from "../../common/transformers/hex.transformer"; +import { hexToDecimalNumberTransformer } from "../../common/transformers/hexToDecimalNumber.transformer"; import { TransactionReceipt } from "./transactionReceipt.entity"; import { Transfer } from "../../transfer/transfer.entity"; import { Block } from "../../block/block.entity"; @@ -61,6 +62,15 @@ export class Transaction extends BaseEntity { @Column({ type: "varchar", length: 128 }) public readonly gasPrice: string; + @Column({ type: "varchar", length: 128, nullable: true, transformer: hexToDecimalNumberTransformer }) + public readonly gasPerPubdata?: string; + + @Column({ type: "varchar", length: 128, nullable: true }) + public readonly maxFeePerGas?: string; + + @Column({ type: "varchar", length: 128, nullable: true }) + public readonly maxPriorityFeePerGas?: string; + @ManyToOne(() => Block) @JoinColumn({ name: "blockNumber" }) public readonly block: Block; diff --git a/packages/api/src/transaction/entities/transactionDetails.entity.ts b/packages/api/src/transaction/entities/transactionDetails.entity.ts new file mode 100644 index 0000000000..1b8fdb9f4f --- /dev/null +++ b/packages/api/src/transaction/entities/transactionDetails.entity.ts @@ -0,0 +1,18 @@ +import { Entity } from "typeorm"; +import { Transaction } from "./transaction.entity"; + +@Entity({ name: "transactions" }) +export class TransactionDetails extends Transaction { + public get gasUsed(): string { + return this.transactionReceipt ? this.transactionReceipt.gasUsed : null; + } + + toJSON(): any { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { transactionReceipt, ...restFields } = super.toJSON(); + return { + ...restFields, + gasUsed: this.gasUsed, + }; + } +} diff --git a/packages/api/src/transaction/transaction.controller.ts b/packages/api/src/transaction/transaction.controller.ts index 087e73eecd..f8465723d9 100644 --- a/packages/api/src/transaction/transaction.controller.ts +++ b/packages/api/src/transaction/transaction.controller.ts @@ -14,6 +14,7 @@ import { buildDateFilter } from "../common/utils"; import { FilterTransactionsOptionsDto } from "./dtos/filterTransactionsOptions.dto"; import { TransferDto } from "../transfer/transfer.dto"; import { TransactionDto } from "./dtos/transaction.dto"; +import { TransactionDetailsDto } from "./dtos/transactionDetails.dto"; import { TransferService } from "../transfer/transfer.service"; import { LogDto } from "../log/log.dto"; import { LogService } from "../log/log.service"; @@ -72,12 +73,12 @@ export class TransactionController { @ApiNotFoundResponse({ description: "Transaction with the specified hash does not exist" }) public async getTransaction( @Param("transactionHash", new ParseTransactionHashPipe()) transactionHash: string - ): Promise { - const transaction = await this.transactionService.findOne(transactionHash); - if (!transaction) { + ): Promise { + const transactionDetail = await this.transactionService.findOne(transactionHash); + if (!transactionDetail) { throw new NotFoundException(); } - return transaction; + return transactionDetail; } @Get(":transactionHash/transfers") diff --git a/packages/api/src/transaction/transaction.module.ts b/packages/api/src/transaction/transaction.module.ts index 299313a968..b121a319bd 100644 --- a/packages/api/src/transaction/transaction.module.ts +++ b/packages/api/src/transaction/transaction.module.ts @@ -4,6 +4,7 @@ import { TransactionController } from "./transaction.controller"; import { TransactionService } from "./transaction.service"; import { TransactionReceiptService } from "./transactionReceipt.service"; import { Transaction } from "./entities/transaction.entity"; +import { TransactionDetails } from "./entities/transactionDetails.entity"; import { AddressTransaction } from "./entities/addressTransaction.entity"; import { TransactionReceipt } from "./entities/transactionReceipt.entity"; import { Batch } from "../batch/batch.entity"; @@ -13,7 +14,7 @@ import { LogModule } from "../log/log.module"; @Module({ imports: [ - TypeOrmModule.forFeature([Transaction, AddressTransaction, TransactionReceipt, Batch]), + TypeOrmModule.forFeature([Transaction, TransactionDetails, AddressTransaction, TransactionReceipt, Batch]), TransferModule, LogModule, CounterModule, diff --git a/packages/api/src/transaction/transaction.service.spec.ts b/packages/api/src/transaction/transaction.service.spec.ts index 788582b82f..b8c5e7e3cb 100644 --- a/packages/api/src/transaction/transaction.service.spec.ts +++ b/packages/api/src/transaction/transaction.service.spec.ts @@ -8,6 +8,7 @@ import { SortingOrder } from "../common/types"; import { CounterService } from "../counter/counter.service"; import { TransactionService, FilterTransactionsOptions } from "./transaction.service"; import { Transaction } from "./entities/transaction.entity"; +import { TransactionDetails } from "./entities/transactionDetails.entity"; import { AddressTransaction } from "./entities/addressTransaction.entity"; import { Batch } from "../batch/batch.entity"; @@ -17,6 +18,7 @@ describe("TransactionService", () => { let transaction; let service: TransactionService; let repositoryMock: typeorm.Repository; + let repositoryDetailMock: typeorm.Repository; let addressTransactionRepositoryMock: typeorm.Repository; let batchRepositoryMock: typeorm.Repository; let counterServiceMock: CounterService; @@ -25,6 +27,7 @@ describe("TransactionService", () => { beforeEach(async () => { counterServiceMock = mock(); repositoryMock = mock>(); + repositoryDetailMock = mock>(); addressTransactionRepositoryMock = mock>(); batchRepositoryMock = mock>(); transaction = { @@ -38,6 +41,10 @@ describe("TransactionService", () => { provide: getRepositoryToken(Transaction), useValue: repositoryMock, }, + { + provide: getRepositoryToken(TransactionDetails), + useValue: repositoryDetailMock, + }, { provide: getRepositoryToken(AddressTransaction), useValue: addressTransactionRepositoryMock, @@ -61,21 +68,45 @@ describe("TransactionService", () => { }); describe("findOne", () => { + let queryBuilderMock; + const hash = "txHash"; + beforeEach(() => { - (repositoryMock.findOne as jest.Mock).mockResolvedValue(transaction); + queryBuilderMock = mock>(); + (repositoryDetailMock.createQueryBuilder as jest.Mock).mockReturnValue(queryBuilderMock); + (queryBuilderMock.getOne as jest.Mock).mockResolvedValue(null); }); - it("queries transactions by specified transaction hash", async () => { - await service.findOne(transactionHash); - expect(repositoryMock.findOne).toHaveBeenCalledTimes(1); - expect(repositoryMock.findOne).toHaveBeenCalledWith({ - where: { hash: transactionHash }, - relations: { batch: true }, - }); + it("creates query builder with proper params", async () => { + await service.findOne(hash); + expect(repositoryDetailMock.createQueryBuilder).toHaveBeenCalledWith("transaction"); }); - it("returns transaction by hash", async () => { - const result = await service.findOne(transactionHash); + it("filters transactions by the specified hash", async () => { + await service.findOne(hash); + expect(queryBuilderMock.where).toHaveBeenCalledWith({ hash }); + }); + + it("joins batch record to get batch specific fields", async () => { + await service.findOne(hash); + expect(queryBuilderMock.leftJoinAndSelect).toHaveBeenCalledWith("transaction.batch", "batch"); + }); + + it("joins transactionReceipt record to get transactionReceipt specific fields", async () => { + await service.findOne(hash); + expect(queryBuilderMock.leftJoin).toHaveBeenCalledWith("transaction.transactionReceipt", "transactionReceipt"); + }); + + it("selects only needed transactionReceipt fields", async () => { + await service.findOne(hash); + expect(queryBuilderMock.addSelect).toHaveBeenCalledWith(["transactionReceipt.gasUsed"]); + }); + + it("returns paginated result", async () => { + const transaction = mock(); + (queryBuilderMock.getOne as jest.Mock).mockResolvedValue(transaction); + + const result = await service.findOne(hash); expect(result).toBe(transaction); }); }); diff --git a/packages/api/src/transaction/transaction.service.ts b/packages/api/src/transaction/transaction.service.ts index 32661d830d..a79376148f 100644 --- a/packages/api/src/transaction/transaction.service.ts +++ b/packages/api/src/transaction/transaction.service.ts @@ -5,6 +5,7 @@ import { Pagination } from "nestjs-typeorm-paginate"; import { paginate } from "../common/utils"; import { IPaginationOptions, CounterCriteria, SortingOrder } from "../common/types"; import { Transaction } from "./entities/transaction.entity"; +import { TransactionDetails } from "./entities/transactionDetails.entity"; import { AddressTransaction } from "./entities/addressTransaction.entity"; import { Batch } from "../batch/batch.entity"; import { CounterService } from "../counter/counter.service"; @@ -29,6 +30,8 @@ export class TransactionService { constructor( @InjectRepository(Transaction) private readonly transactionRepository: Repository, + @InjectRepository(TransactionDetails) + private readonly transactionDetailsRepository: Repository, @InjectRepository(AddressTransaction) private readonly addressTransactionRepository: Repository, @InjectRepository(Batch) @@ -36,8 +39,13 @@ export class TransactionService { private readonly counterService: CounterService ) {} - public async findOne(hash: string): Promise { - return await this.transactionRepository.findOne({ where: { hash }, relations: { batch: true } }); + public async findOne(hash: string): Promise { + const queryBuilder = this.transactionDetailsRepository.createQueryBuilder("transaction"); + queryBuilder.leftJoinAndSelect("transaction.batch", "batch"); + queryBuilder.leftJoin("transaction.transactionReceipt", "transactionReceipt"); + queryBuilder.addSelect(["transactionReceipt.gasUsed"]); + queryBuilder.where({ hash }); + return await queryBuilder.getOne(); } public async exists(hash: string): Promise { diff --git a/packages/api/test/account-api.e2e-spec.ts b/packages/api/test/account-api.e2e-spec.ts index 340d95cd58..f529cc56aa 100644 --- a/packages/api/test/account-api.e2e-spec.ts +++ b/packages/api/test/account-api.e2e-spec.ts @@ -4,7 +4,7 @@ import { getRepositoryToken } from "@nestjs/typeorm"; import * as request from "supertest"; import { Repository } from "typeorm"; import { BatchDetails } from "../src/batch/batchDetails.entity"; -import { BlockDetail } from "../src/block/blockDetail.entity"; +import { BlockDetails } from "../src/block/blockDetails.entity"; import { AddressTransaction } from "../src/transaction/entities/addressTransaction.entity"; import { Transaction } from "../src/transaction/entities/transaction.entity"; import { TransactionReceipt } from "../src/transaction/entities/transactionReceipt.entity"; @@ -23,7 +23,7 @@ describe("Account API (e2e)", () => { let addressTransferRepository: Repository; let transferRepository: Repository; let transactionReceiptRepository: Repository; - let blockRepository: Repository; + let blockRepository: Repository; let batchRepository: Repository; let tokenRepository: Repository; let balanceRepository: Repository; @@ -42,7 +42,7 @@ describe("Account API (e2e)", () => { addressTransferRepository = app.get>(getRepositoryToken(AddressTransfer)); transferRepository = app.get>(getRepositoryToken(Transfer)); transactionReceiptRepository = app.get>(getRepositoryToken(TransactionReceipt)); - blockRepository = app.get>(getRepositoryToken(BlockDetail)); + blockRepository = app.get>(getRepositoryToken(BlockDetails)); batchRepository = app.get>(getRepositoryToken(BatchDetails)); tokenRepository = app.get>(getRepositoryToken(Token)); balanceRepository = app.get>(getRepositoryToken(Balance)); diff --git a/packages/api/test/address.e2e-spec.ts b/packages/api/test/address.e2e-spec.ts index 68fc5ff46c..772878a9ee 100644 --- a/packages/api/test/address.e2e-spec.ts +++ b/packages/api/test/address.e2e-spec.ts @@ -7,7 +7,7 @@ import { AppModule } from "../src/app.module"; import { configureApp } from "../src/configureApp"; import { Address } from "../src/address/address.entity"; import { Balance } from "../src/balance/balance.entity"; -import { BlockDetail } from "../src/block/blockDetail.entity"; +import { BlockDetails } from "../src/block/blockDetails.entity"; import { Transaction } from "../src/transaction/entities/transaction.entity"; import { AddressTransaction } from "../src/transaction/entities/addressTransaction.entity"; import { TransactionReceipt } from "../src/transaction/entities/transactionReceipt.entity"; @@ -21,7 +21,7 @@ import { AddressTransfer } from "../src/transfer/addressTransfer.entity"; describe("AddressController (e2e)", () => { let app: INestApplication; let addressRepository: Repository
; - let blockRepository: Repository; + let blockRepository: Repository; let transactionRepository: Repository; let addressTransactionRepository: Repository; let transactionReceiptRepository: Repository; @@ -45,7 +45,7 @@ describe("AddressController (e2e)", () => { await app.init(); addressRepository = app.get>(getRepositoryToken(Address)); - blockRepository = app.get>(getRepositoryToken(BlockDetail)); + blockRepository = app.get>(getRepositoryToken(BlockDetails)); transactionRepository = app.get>(getRepositoryToken(Transaction)); addressTransactionRepository = app.get>(getRepositoryToken(AddressTransaction)); transactionReceiptRepository = app.get>(getRepositoryToken(TransactionReceipt)); diff --git a/packages/api/test/block-api.e2e-spec.ts b/packages/api/test/block-api.e2e-spec.ts index dab94a051e..c04f833be8 100644 --- a/packages/api/test/block-api.e2e-spec.ts +++ b/packages/api/test/block-api.e2e-spec.ts @@ -5,12 +5,12 @@ import { Repository } from "typeorm"; import { getRepositoryToken } from "@nestjs/typeorm"; import { AppModule } from "../src/app.module"; import { configureApp } from "../src/configureApp"; -import { BlockDetail } from "../src/block/blockDetail.entity"; +import { BlockDetails } from "../src/block/blockDetails.entity"; import { BatchDetails } from "../src/batch/batchDetails.entity"; describe("Block API (e2e)", () => { let app: INestApplication; - let blockRepository: Repository; + let blockRepository: Repository; let batchRepository: Repository; beforeAll(async () => { @@ -24,7 +24,7 @@ describe("Block API (e2e)", () => { await app.init(); - blockRepository = app.get>(getRepositoryToken(BlockDetail)); + blockRepository = app.get>(getRepositoryToken(BlockDetails)); batchRepository = app.get>(getRepositoryToken(BatchDetails)); for (let i = 0; i < 9; i++) { diff --git a/packages/api/test/block.e2e-spec.ts b/packages/api/test/block.e2e-spec.ts index c2bcf91129..3cf78d81e8 100644 --- a/packages/api/test/block.e2e-spec.ts +++ b/packages/api/test/block.e2e-spec.ts @@ -5,12 +5,12 @@ import { Repository } from "typeorm"; import { getRepositoryToken } from "@nestjs/typeorm"; import { AppModule } from "../src/app.module"; import { configureApp } from "../src/configureApp"; -import { BlockDetail } from "../src/block/blockDetail.entity"; +import { BlockDetails } from "../src/block/blockDetails.entity"; import { BatchDetails } from "../src/batch/batchDetails.entity"; describe("BlockController (e2e)", () => { let app: INestApplication; - let blockRepository: Repository; + let blockRepository: Repository; let batchRepository: Repository; beforeAll(async () => { @@ -24,7 +24,7 @@ describe("BlockController (e2e)", () => { await app.init(); - blockRepository = app.get>(getRepositoryToken(BlockDetail)); + blockRepository = app.get>(getRepositoryToken(BlockDetails)); batchRepository = app.get>(getRepositoryToken(BatchDetails)); for (let i = 0; i < 9; i++) { diff --git a/packages/api/test/log-api.e2e-spec.ts b/packages/api/test/log-api.e2e-spec.ts index f81f801a1e..ad04223e9e 100644 --- a/packages/api/test/log-api.e2e-spec.ts +++ b/packages/api/test/log-api.e2e-spec.ts @@ -4,7 +4,7 @@ import { getRepositoryToken } from "@nestjs/typeorm"; import * as request from "supertest"; import { Repository } from "typeorm"; import { BatchDetails } from "../src/batch/batchDetails.entity"; -import { BlockDetail } from "../src/block/blockDetail.entity"; +import { BlockDetails } from "../src/block/blockDetails.entity"; import { Log } from "../src/log/log.entity"; import { Transaction } from "../src/transaction/entities/transaction.entity"; import { TransactionReceipt } from "../src/transaction/entities/transactionReceipt.entity"; @@ -15,7 +15,7 @@ describe("Logs API (e2e)", () => { let app: INestApplication; let transactionRepository: Repository; let transactionReceiptRepository: Repository; - let blockRepository: Repository; + let blockRepository: Repository; let batchRepository: Repository; let logRepository: Repository; @@ -30,7 +30,7 @@ describe("Logs API (e2e)", () => { transactionRepository = app.get>(getRepositoryToken(Transaction)); transactionReceiptRepository = app.get>(getRepositoryToken(TransactionReceipt)); - blockRepository = app.get>(getRepositoryToken(BlockDetail)); + blockRepository = app.get>(getRepositoryToken(BlockDetails)); batchRepository = app.get>(getRepositoryToken(BatchDetails)); logRepository = app.get>(getRepositoryToken(Log)); diff --git a/packages/api/test/stats-api.e2e-spec.ts b/packages/api/test/stats-api.e2e-spec.ts index d9fec2f445..c805e8648c 100644 --- a/packages/api/test/stats-api.e2e-spec.ts +++ b/packages/api/test/stats-api.e2e-spec.ts @@ -4,14 +4,14 @@ import { getRepositoryToken } from "@nestjs/typeorm"; import * as request from "supertest"; import { Repository } from "typeorm"; import { BatchDetails } from "../src/batch/batchDetails.entity"; -import { BlockDetail } from "../src/block/blockDetail.entity"; +import { BlockDetails } from "../src/block/blockDetails.entity"; import { Token, ETH_TOKEN } from "../src/token/token.entity"; import { AppModule } from "../src/app.module"; import { configureApp } from "../src/configureApp"; describe("Stats API (e2e)", () => { let app: INestApplication; - let blockRepository: Repository; + let blockRepository: Repository; let batchRepository: Repository; let tokenRepository: Repository; @@ -24,7 +24,7 @@ describe("Stats API (e2e)", () => { configureApp(app); await app.init(); - blockRepository = app.get>(getRepositoryToken(BlockDetail)); + blockRepository = app.get>(getRepositoryToken(BlockDetails)); batchRepository = app.get>(getRepositoryToken(BatchDetails)); tokenRepository = app.get>(getRepositoryToken(Token)); diff --git a/packages/api/test/stats.e2e-spec.ts b/packages/api/test/stats.e2e-spec.ts index 291ce881dc..4666a6e379 100644 --- a/packages/api/test/stats.e2e-spec.ts +++ b/packages/api/test/stats.e2e-spec.ts @@ -6,14 +6,14 @@ import { getRepositoryToken } from "@nestjs/typeorm"; import { AppModule } from "../src/app.module"; import { configureApp } from "../src/configureApp"; import { BatchDetails } from "../src/batch/batchDetails.entity"; -import { BlockDetail } from "../src/block/blockDetail.entity"; +import { BlockDetails } from "../src/block/blockDetails.entity"; import { Transaction } from "../src/transaction/entities/transaction.entity"; import { Counter } from "../src/counter/counter.entity"; describe("StatsController (e2e)", () => { let app: INestApplication; let batchRepository: Repository; - let blockRepository: Repository; + let blockRepository: Repository; let transactionRepository: Repository; let counterRepository: Repository; @@ -29,7 +29,7 @@ describe("StatsController (e2e)", () => { await app.init(); batchRepository = app.get>(getRepositoryToken(BatchDetails)); - blockRepository = app.get>(getRepositoryToken(BlockDetail)); + blockRepository = app.get>(getRepositoryToken(BlockDetails)); transactionRepository = app.get>(getRepositoryToken(Transaction)); counterRepository = app.get>(getRepositoryToken(Counter)); diff --git a/packages/api/test/token-api.e2e-spec.ts b/packages/api/test/token-api.e2e-spec.ts index ce825b8535..f3735bc8e2 100644 --- a/packages/api/test/token-api.e2e-spec.ts +++ b/packages/api/test/token-api.e2e-spec.ts @@ -4,14 +4,14 @@ import { getRepositoryToken } from "@nestjs/typeorm"; import * as request from "supertest"; import { Repository } from "typeorm"; import { BatchDetails } from "../src/batch/batchDetails.entity"; -import { BlockDetail } from "../src/block/blockDetail.entity"; +import { BlockDetails } from "../src/block/blockDetails.entity"; import { Token, ETH_TOKEN } from "../src/token/token.entity"; import { AppModule } from "../src/app.module"; import { configureApp } from "../src/configureApp"; describe("Token API (e2e)", () => { let app: INestApplication; - let blockRepository: Repository; + let blockRepository: Repository; let batchRepository: Repository; let tokenRepository: Repository; @@ -24,7 +24,7 @@ describe("Token API (e2e)", () => { configureApp(app); await app.init(); - blockRepository = app.get>(getRepositoryToken(BlockDetail)); + blockRepository = app.get>(getRepositoryToken(BlockDetails)); batchRepository = app.get>(getRepositoryToken(BatchDetails)); tokenRepository = app.get>(getRepositoryToken(Token)); diff --git a/packages/api/test/token.e2e-spec.ts b/packages/api/test/token.e2e-spec.ts index 3ca44fae97..b307db3298 100644 --- a/packages/api/test/token.e2e-spec.ts +++ b/packages/api/test/token.e2e-spec.ts @@ -6,7 +6,7 @@ import { getRepositoryToken } from "@nestjs/typeorm"; import { AppModule } from "../src/app.module"; import { configureApp } from "../src/configureApp"; import { Token, TokenType, ETH_TOKEN } from "../src/token/token.entity"; -import { BlockDetail } from "../src/block/blockDetail.entity"; +import { BlockDetails } from "../src/block/blockDetails.entity"; import { Transaction } from "../src/transaction/entities/transaction.entity"; import { Transfer, TransferType } from "../src/transfer/transfer.entity"; import { BatchDetails } from "../src/batch/batchDetails.entity"; @@ -14,7 +14,7 @@ import { BatchDetails } from "../src/batch/batchDetails.entity"; describe("TokenController (e2e)", () => { let app: INestApplication; let tokenRepository: Repository; - let blockRepository: Repository; + let blockRepository: Repository; let transactionRepository: Repository; let transferRepository: Repository; let batchRepository: Repository; @@ -31,7 +31,7 @@ describe("TokenController (e2e)", () => { await app.init(); tokenRepository = app.get>(getRepositoryToken(Token)); - blockRepository = app.get>(getRepositoryToken(BlockDetail)); + blockRepository = app.get>(getRepositoryToken(BlockDetails)); transactionRepository = app.get>(getRepositoryToken(Transaction)); transferRepository = app.get>(getRepositoryToken(Transfer)); batchRepository = app.get>(getRepositoryToken(BatchDetails)); diff --git a/packages/api/test/transaction-api.e2e-spec.ts b/packages/api/test/transaction-api.e2e-spec.ts index 387de4330b..4fb83e5454 100644 --- a/packages/api/test/transaction-api.e2e-spec.ts +++ b/packages/api/test/transaction-api.e2e-spec.ts @@ -4,7 +4,7 @@ import { getRepositoryToken } from "@nestjs/typeorm"; import * as request from "supertest"; import { Repository } from "typeorm"; import { BatchDetails } from "../src/batch/batchDetails.entity"; -import { BlockDetail } from "../src/block/blockDetail.entity"; +import { BlockDetails } from "../src/block/blockDetails.entity"; import { Transaction } from "../src/transaction/entities/transaction.entity"; import { TransactionReceipt } from "../src/transaction/entities/transactionReceipt.entity"; import { AppModule } from "../src/app.module"; @@ -14,7 +14,7 @@ describe("Transaction API (e2e)", () => { let app: INestApplication; let transactionRepository: Repository; let transactionReceiptRepository: Repository; - let blockRepository: Repository; + let blockRepository: Repository; let batchRepository: Repository; beforeAll(async () => { @@ -28,7 +28,7 @@ describe("Transaction API (e2e)", () => { transactionRepository = app.get>(getRepositoryToken(Transaction)); transactionReceiptRepository = app.get>(getRepositoryToken(TransactionReceipt)); - blockRepository = app.get>(getRepositoryToken(BlockDetail)); + blockRepository = app.get>(getRepositoryToken(BlockDetails)); batchRepository = app.get>(getRepositoryToken(BatchDetails)); await batchRepository.insert({ diff --git a/packages/api/test/transaction.e2e-spec.ts b/packages/api/test/transaction.e2e-spec.ts index 37003bac45..e38111a34d 100644 --- a/packages/api/test/transaction.e2e-spec.ts +++ b/packages/api/test/transaction.e2e-spec.ts @@ -2,12 +2,14 @@ import { Test, TestingModule } from "@nestjs/testing"; import { INestApplication } from "@nestjs/common"; import * as request from "supertest"; import { Repository } from "typeorm"; +import { BigNumber } from "ethers"; import { getRepositoryToken } from "@nestjs/typeorm"; import { AppModule } from "../src/app.module"; import { configureApp } from "../src/configureApp"; import { Token, TokenType } from "../src/token/token.entity"; -import { BlockDetail } from "../src/block/blockDetail.entity"; +import { BlockDetails } from "../src/block/blockDetails.entity"; import { Transaction } from "../src/transaction/entities/transaction.entity"; +import { TransactionReceipt } from "../src/transaction/entities/transactionReceipt.entity"; import { ETH_TOKEN } from "../src/token/token.entity"; import { AddressTransaction } from "../src/transaction/entities/addressTransaction.entity"; import { Transfer, TransferType } from "../src/transfer/transfer.entity"; @@ -17,8 +19,9 @@ import { BatchDetails } from "../src/batch/batchDetails.entity"; describe("TransactionController (e2e)", () => { let app: INestApplication; let tokenRepository: Repository; - let blockRepository: Repository; + let blockRepository: Repository; let transactionRepository: Repository; + let transactionReceiptRepository: Repository; let addressTransactionRepository: Repository; let transferRepository: Repository; let logRepository: Repository; @@ -36,8 +39,9 @@ describe("TransactionController (e2e)", () => { await app.init(); tokenRepository = app.get>(getRepositoryToken(Token)); - blockRepository = app.get>(getRepositoryToken(BlockDetail)); + blockRepository = app.get>(getRepositoryToken(BlockDetails)); transactionRepository = app.get>(getRepositoryToken(Transaction)); + transactionReceiptRepository = app.get>(getRepositoryToken(TransactionReceipt)); addressTransactionRepository = app.get>(getRepositoryToken(AddressTransaction)); transferRepository = app.get>(getRepositoryToken(Transfer)); logRepository = app.get>(getRepositoryToken(Log)); @@ -125,6 +129,11 @@ describe("TransactionController (e2e)", () => { receivedAt: `2022-11-21T18:16:0${i}.000Z`, l1BatchNumber: i < 3 ? 1 : i, receiptStatus: i < 9 ? 1 : 0, + gasPrice: BigNumber.from(1000 + i).toString(), + gasLimit: BigNumber.from(2000 + i).toString(), + maxFeePerGas: BigNumber.from(3000 + i).toString(), + maxPriorityFeePerGas: BigNumber.from(4000 + i).toString(), + gasPerPubdata: BigNumber.from(5000 + i).toHexString(), }; await transactionRepository.insert(transactionSpec); @@ -137,6 +146,14 @@ describe("TransactionController (e2e)", () => { transactionIndex: transactionSpec.transactionIndex, }); } + + await transactionReceiptRepository.insert({ + transactionHash: transactionSpec.hash, + from: transactionSpec.from, + status: 1, + gasUsed: (7000 + i).toString(), + cumulativeGasUsed: (10000 + i).toString(), + }); } for (let i = 0; i < 20; i++) { @@ -208,6 +225,7 @@ describe("TransactionController (e2e)", () => { await tokenRepository.delete({}); await addressTransactionRepository.delete({}); await transactionRepository.delete({}); + await transactionReceiptRepository.delete({}); await blockRepository.delete({}); await batchRepository.delete({}); @@ -241,8 +259,11 @@ describe("TransactionController (e2e)", () => { executeTxHash: null, fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", - gasLimit: "1000000", - gasPrice: "100", + gasLimit: "2009", + gasPrice: "1009", + gasPerPubdata: "5009", + maxFeePerGas: "3009", + maxPriorityFeePerGas: "4009", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e19", isL1BatchSealed: false, isL1Originated: true, @@ -264,8 +285,11 @@ describe("TransactionController (e2e)", () => { executeTxHash: "0xeb5ead20476b91008c3b6e44005017e697de78e4fd868d99d2c58566655c5ab8", fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", - gasLimit: "1000000", - gasPrice: "100", + gasLimit: "2008", + gasPrice: "1008", + gasPerPubdata: "5008", + maxFeePerGas: "3008", + maxPriorityFeePerGas: "4008", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e18", isL1BatchSealed: true, isL1Originated: true, @@ -287,8 +311,11 @@ describe("TransactionController (e2e)", () => { executeTxHash: "0xeb5ead20476b91008c3b6e44005017e697de78e4fd868d99d2c58566655c5ab7", fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", - gasLimit: "1000000", - gasPrice: "100", + gasLimit: "2007", + gasPrice: "1007", + gasPerPubdata: "5007", + maxFeePerGas: "3007", + maxPriorityFeePerGas: "4007", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e17", isL1BatchSealed: true, isL1Originated: true, @@ -310,8 +337,11 @@ describe("TransactionController (e2e)", () => { executeTxHash: null, fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", - gasLimit: "1000000", - gasPrice: "100", + gasLimit: "2006", + gasPrice: "1006", + gasPerPubdata: "5006", + maxFeePerGas: "3006", + maxPriorityFeePerGas: "4006", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e16", isL1BatchSealed: true, isL1Originated: true, @@ -333,8 +363,11 @@ describe("TransactionController (e2e)", () => { executeTxHash: null, fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", - gasLimit: "1000000", - gasPrice: "100", + gasLimit: "2005", + gasPrice: "1005", + gasPerPubdata: "5005", + maxFeePerGas: "3005", + maxPriorityFeePerGas: "4005", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e15", isL1BatchSealed: true, isL1Originated: true, @@ -356,8 +389,11 @@ describe("TransactionController (e2e)", () => { executeTxHash: null, fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", - gasLimit: "1000000", - gasPrice: "100", + gasPrice: "1004", + gasLimit: "2004", + gasPerPubdata: "5004", + maxFeePerGas: "3004", + maxPriorityFeePerGas: "4004", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e14", isL1BatchSealed: true, isL1Originated: true, @@ -379,8 +415,11 @@ describe("TransactionController (e2e)", () => { executeTxHash: null, fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", - gasLimit: "1000000", - gasPrice: "100", + gasLimit: "2003", + gasPrice: "1003", + gasPerPubdata: "5003", + maxFeePerGas: "3003", + maxPriorityFeePerGas: "4003", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e13", isL1BatchSealed: true, isL1Originated: true, @@ -402,8 +441,11 @@ describe("TransactionController (e2e)", () => { executeTxHash: null, fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", - gasLimit: "1000000", - gasPrice: "100", + gasPrice: "1002", + gasLimit: "2002", + gasPerPubdata: "5002", + maxFeePerGas: "3002", + maxPriorityFeePerGas: "4002", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e12", isL1BatchSealed: false, isL1Originated: true, @@ -425,8 +467,11 @@ describe("TransactionController (e2e)", () => { executeTxHash: null, fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", - gasLimit: "1000000", - gasPrice: "100", + gasPrice: "1001", + gasLimit: "2001", + gasPerPubdata: "5001", + maxFeePerGas: "3001", + maxPriorityFeePerGas: "4001", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e11", isL1BatchSealed: false, isL1Originated: true, @@ -448,8 +493,11 @@ describe("TransactionController (e2e)", () => { executeTxHash: null, fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", - gasLimit: "1000000", - gasPrice: "100", + gasLimit: "2000", + gasPrice: "1000", + gasPerPubdata: "5000", + maxFeePerGas: "3000", + maxPriorityFeePerGas: "4000", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e10", isL1BatchSealed: false, isL1Originated: true, @@ -481,8 +529,11 @@ describe("TransactionController (e2e)", () => { executeTxHash: "0xeb5ead20476b91008c3b6e44005017e697de78e4fd868d99d2c58566655c5ab8", fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", - gasLimit: "1000000", - gasPrice: "100", + gasLimit: "2008", + gasPrice: "1008", + gasPerPubdata: "5008", + maxFeePerGas: "3008", + maxPriorityFeePerGas: "4008", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e18", isL1BatchSealed: true, isL1Originated: true, @@ -504,8 +555,11 @@ describe("TransactionController (e2e)", () => { executeTxHash: "0xeb5ead20476b91008c3b6e44005017e697de78e4fd868d99d2c58566655c5ab7", fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", - gasLimit: "1000000", - gasPrice: "100", + gasLimit: "2007", + gasPrice: "1007", + gasPerPubdata: "5007", + maxFeePerGas: "3007", + maxPriorityFeePerGas: "4007", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e17", isL1BatchSealed: true, isL1Originated: true, @@ -527,8 +581,11 @@ describe("TransactionController (e2e)", () => { executeTxHash: null, fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", - gasLimit: "1000000", - gasPrice: "100", + gasLimit: "2006", + gasPrice: "1006", + gasPerPubdata: "5006", + maxFeePerGas: "3006", + maxPriorityFeePerGas: "4006", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e16", isL1BatchSealed: true, isL1Originated: true, @@ -606,8 +663,11 @@ describe("TransactionController (e2e)", () => { executeTxHash: null, fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", - gasLimit: "1000000", - gasPrice: "100", + gasLimit: "2001", + gasPrice: "1001", + gasPerPubdata: "5001", + maxFeePerGas: "3001", + maxPriorityFeePerGas: "4001", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e11", isL1BatchSealed: false, isL1Originated: true, @@ -654,8 +714,11 @@ describe("TransactionController (e2e)", () => { executeTxHash: null, fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", - gasLimit: "1000000", - gasPrice: "100", + gasLimit: "2001", + gasPrice: "1001", + gasPerPubdata: "5001", + maxFeePerGas: "3001", + maxPriorityFeePerGas: "4001", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e11", isL1BatchSealed: false, isL1Originated: true, @@ -702,8 +765,11 @@ describe("TransactionController (e2e)", () => { executeTxHash: "0xeb5ead20476b91008c3b6e44005017e697de78e4fd868d99d2c58566655c5ab7", fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", - gasLimit: "1000000", - gasPrice: "100", + gasLimit: "2007", + gasPrice: "1007", + gasPerPubdata: "5007", + maxFeePerGas: "3007", + maxPriorityFeePerGas: "4007", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e17", isL1BatchSealed: true, isL1Originated: true, @@ -725,8 +791,11 @@ describe("TransactionController (e2e)", () => { executeTxHash: null, fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", - gasLimit: "1000000", - gasPrice: "100", + gasLimit: "2006", + gasPrice: "1006", + gasPerPubdata: "5006", + maxFeePerGas: "3006", + maxPriorityFeePerGas: "4006", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e16", isL1BatchSealed: true, isL1Originated: true, @@ -809,8 +878,12 @@ describe("TransactionController (e2e)", () => { executeTxHash: "0xeb5ead20476b91008c3b6e44005017e697de78e4fd868d99d2c58566655c5ab8", fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", - gasLimit: "1000000", - gasPrice: "100", + gasLimit: "2008", + gasPrice: "1008", + gasUsed: "7008", + gasPerPubdata: "5008", + maxFeePerGas: "3008", + maxPriorityFeePerGas: "4008", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e18", isL1BatchSealed: true, isL1Originated: true, @@ -840,8 +913,12 @@ describe("TransactionController (e2e)", () => { executeTxHash: null, fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", - gasLimit: "1000000", - gasPrice: "100", + gasLimit: "2005", + gasPrice: "1005", + gasUsed: "7005", + gasPerPubdata: "5005", + maxFeePerGas: "3005", + maxPriorityFeePerGas: "4005", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e15", isL1BatchSealed: true, isL1Originated: true, @@ -871,8 +948,12 @@ describe("TransactionController (e2e)", () => { executeTxHash: null, fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", - gasLimit: "1000000", - gasPrice: "100", + gasLimit: "2003", + gasPrice: "1003", + gasUsed: "7003", + gasPerPubdata: "5003", + maxFeePerGas: "3003", + maxPriorityFeePerGas: "4003", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e13", isL1BatchSealed: true, isL1Originated: true, @@ -902,8 +983,12 @@ describe("TransactionController (e2e)", () => { executeTxHash: null, fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", - gasLimit: "1000000", - gasPrice: "100", + gasLimit: "2000", + gasPrice: "1000", + gasUsed: "7000", + gasPerPubdata: "5000", + maxFeePerGas: "3000", + maxPriorityFeePerGas: "4000", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e10", isL1BatchSealed: true, isL1Originated: true, @@ -933,8 +1018,12 @@ describe("TransactionController (e2e)", () => { executeTxHash: null, fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", - gasLimit: "1000000", - gasPrice: "100", + gasLimit: "2009", + gasPrice: "1009", + gasUsed: "7009", + gasPerPubdata: "5009", + maxFeePerGas: "3009", + maxPriorityFeePerGas: "4009", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e19", isL1BatchSealed: true, isL1Originated: true, @@ -964,8 +1053,12 @@ describe("TransactionController (e2e)", () => { executeTxHash: null, fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", - gasLimit: "1000000", - gasPrice: "100", + gasLimit: "2000", + gasPrice: "1000", + gasUsed: "7000", + gasPerPubdata: "5000", + maxFeePerGas: "3000", + maxPriorityFeePerGas: "4000", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e10", isL1BatchSealed: true, isL1Originated: true, From 845500d2a1d28294ebd6ea1f2e6bbce3e956baad Mon Sep 17 00:00:00 2001 From: Roman Petriv Date: Tue, 5 Dec 2023 15:47:07 +0200 Subject: [PATCH 09/52] feat: add transaction error and revert reason (#110) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # What ❔ - fetch and store tx error and revert reason; - return tx error and revert reason via API; - display tx error on UI; ## Why ❔ - fast way to see what's the reason of transaction failure; ## Checklist - [+] PR title corresponds to the body of PR (we generate changelog entries from PRs). - [+] Tests for the changes have been added / updated. - [+] Documentation comments have been added / updated. --- .../transaction.controller.spec.ts | 50 +++++++ .../api/transaction/transaction.controller.ts | 2 +- .../src/transaction/dtos/transaction.dto.ts | 18 +++ .../entities/transaction.entity.ts | 6 + packages/api/test/transaction.e2e-spec.ts | 46 ++++++ .../transactions/infoTable/GeneralInfo.vue | 19 ++- packages/app/src/composables/common/Api.d.ts | 2 + .../app/src/composables/useTransaction.ts | 4 + packages/app/src/locales/en.json | 2 + packages/app/src/locales/uk.json | 3 + .../transactions/GeneralInfo.spec.ts | 4 +- .../components/transactions/Table.spec.ts | 2 + .../tests/composables/useTransaction.spec.ts | 4 + .../tests/composables/useTransactions.spec.ts | 2 + .../src/blockchain/blockchain.service.spec.ts | 140 ++++++++++++++++++ .../src/blockchain/blockchain.service.ts | 20 +++ .../worker/src/entities/transaction.entity.ts | 6 + .../1700684231991-AddTransactionError.ts | 15 ++ .../repositories/transaction.repository.ts | 2 + .../transaction/transaction.processor.spec.ts | 52 ++++++- .../src/transaction/transaction.processor.ts | 24 ++- 21 files changed, 411 insertions(+), 12 deletions(-) create mode 100644 packages/worker/src/migrations/1700684231991-AddTransactionError.ts diff --git a/packages/api/src/api/transaction/transaction.controller.spec.ts b/packages/api/src/api/transaction/transaction.controller.spec.ts index a74a252aa0..049fec6b5e 100644 --- a/packages/api/src/api/transaction/transaction.controller.spec.ts +++ b/packages/api/src/api/transaction/transaction.controller.spec.ts @@ -85,6 +85,56 @@ describe("TransactionController", () => { }, }); }); + + it("returns transaction error in errDescription when transaction is failed and transaction error is present", async () => { + jest.spyOn(transactionServiceMock, "findOne").mockResolvedValue({ + status: TransactionStatus.Failed, + error: "Error", + revertReason: "Reverted", + } as TransactionDetails); + + const response = await controller.getTransactionStatus(transactionHash); + expect(response).toEqual({ + status: ResponseStatus.OK, + message: ResponseMessage.OK, + result: { + isError: "1", + errDescription: "Error", + }, + }); + }); + + it("returns transaction revert reason in errDescription when transaction is failed and transaction revert reason is present", async () => { + jest + .spyOn(transactionServiceMock, "findOne") + .mockResolvedValue({ status: TransactionStatus.Failed, revertReason: "Reverted" } as TransactionDetails); + + const response = await controller.getTransactionStatus(transactionHash); + expect(response).toEqual({ + status: ResponseStatus.OK, + message: ResponseMessage.OK, + result: { + isError: "1", + errDescription: "Reverted", + }, + }); + }); + + it("returns empty errDescription when transaction is failed and transaction error and revert reason are not present", async () => { + jest + .spyOn(transactionServiceMock, "findOne") + .mockResolvedValue({ status: TransactionStatus.Failed } as TransactionDetails); + + const response = await controller.getTransactionStatus(transactionHash); + expect(response).toEqual({ + status: ResponseStatus.OK, + message: ResponseMessage.OK, + result: { + isError: "1", + errDescription: "", + }, + }); + }); }); describe("getTransactionReceiptStatus", () => { diff --git a/packages/api/src/api/transaction/transaction.controller.ts b/packages/api/src/api/transaction/transaction.controller.ts index 29d993b78d..cede0e0c59 100644 --- a/packages/api/src/api/transaction/transaction.controller.ts +++ b/packages/api/src/api/transaction/transaction.controller.ts @@ -36,7 +36,7 @@ export class TransactionController { message: ResponseMessage.OK, result: { isError: hasError ? ResponseStatus.OK : ResponseStatus.NOTOK, - errDescription: "", + errDescription: transaction?.error || transaction?.revertReason || "", }, }; } diff --git a/packages/api/src/transaction/dtos/transaction.dto.ts b/packages/api/src/transaction/dtos/transaction.dto.ts index 8b77f54a1d..597ec07753 100644 --- a/packages/api/src/transaction/dtos/transaction.dto.ts +++ b/packages/api/src/transaction/dtos/transaction.dto.ts @@ -186,4 +186,22 @@ export class TransactionDto { examples: ["included", "committed", "proved", "verified", "failed"], }) public readonly status: TransactionStatus; + + @ApiProperty({ + type: String, + description: "Transaction error", + example: "Some test error", + examples: ["Some test error", null], + nullable: true, + }) + public readonly error?: string; + + @ApiProperty({ + type: String, + description: "Transaction revert reason", + example: "Some test revert reason", + examples: ["Some test revert reason", null], + nullable: true, + }) + public readonly revertReason?: string; } diff --git a/packages/api/src/transaction/entities/transaction.entity.ts b/packages/api/src/transaction/entities/transaction.entity.ts index e4a1b357e8..b907d539c5 100644 --- a/packages/api/src/transaction/entities/transaction.entity.ts +++ b/packages/api/src/transaction/entities/transaction.entity.ts @@ -101,6 +101,12 @@ export class Transaction extends BaseEntity { @OneToMany(() => Transfer, (transfer) => transfer.transaction) public readonly transfers: Transfer[]; + @Column({ nullable: true }) + public readonly error?: string; + + @Column({ nullable: true }) + public readonly revertReason?: string; + public get status(): TransactionStatus { if (this.receiptStatus === 0) { return TransactionStatus.Failed; diff --git a/packages/api/test/transaction.e2e-spec.ts b/packages/api/test/transaction.e2e-spec.ts index e38111a34d..28f55e29f1 100644 --- a/packages/api/test/transaction.e2e-spec.ts +++ b/packages/api/test/transaction.e2e-spec.ts @@ -256,6 +256,8 @@ describe("TransactionController (e2e)", () => { blockNumber: 9, commitTxHash: null, data: "0x000000000000000000000000000000000000000000000000016345785d8a0000", + error: null, + revertReason: null, executeTxHash: null, fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", @@ -282,6 +284,8 @@ describe("TransactionController (e2e)", () => { blockNumber: 8, commitTxHash: "0xeb5ead20476b91008c3b6e44005017e697de78e4fd868d99d2c58566655c5aa8", data: "0x000000000000000000000000000000000000000000000000016345785d8a0000", + error: null, + revertReason: null, executeTxHash: "0xeb5ead20476b91008c3b6e44005017e697de78e4fd868d99d2c58566655c5ab8", fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", @@ -308,6 +312,8 @@ describe("TransactionController (e2e)", () => { blockNumber: 7, commitTxHash: "0xeb5ead20476b91008c3b6e44005017e697de78e4fd868d99d2c58566655c5aa7", data: "0x000000000000000000000000000000000000000000000000016345785d8a0000", + error: null, + revertReason: null, executeTxHash: "0xeb5ead20476b91008c3b6e44005017e697de78e4fd868d99d2c58566655c5ab7", fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", @@ -334,6 +340,8 @@ describe("TransactionController (e2e)", () => { blockNumber: 6, commitTxHash: "0xeb5ead20476b91008c3b6e44005017e697de78e4fd868d99d2c58566655c5aa6", data: "0x000000000000000000000000000000000000000000000000016345785d8a0000", + error: null, + revertReason: null, executeTxHash: null, fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", @@ -360,6 +368,8 @@ describe("TransactionController (e2e)", () => { blockNumber: 5, commitTxHash: "0xeb5ead20476b91008c3b6e44005017e697de78e4fd868d99d2c58566655c5aa5", data: "0x000000000000000000000000000000000000000000000000016345785d8a0000", + error: null, + revertReason: null, executeTxHash: null, fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", @@ -386,6 +396,8 @@ describe("TransactionController (e2e)", () => { blockNumber: 4, commitTxHash: "0xeb5ead20476b91008c3b6e44005017e697de78e4fd868d99d2c58566655c5aa4", data: "0x000000000000000000000000000000000000000000000000016345785d8a0000", + error: null, + revertReason: null, executeTxHash: null, fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", @@ -412,6 +424,8 @@ describe("TransactionController (e2e)", () => { blockNumber: 3, commitTxHash: "0xeb5ead20476b91008c3b6e44005017e697de78e4fd868d99d2c58566655c5aa3", data: "0x000000000000000000000000000000000000000000000000016345785d8a0000", + error: null, + revertReason: null, executeTxHash: null, fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", @@ -438,6 +452,8 @@ describe("TransactionController (e2e)", () => { blockNumber: 1, commitTxHash: null, data: "0x000000000000000000000000000000000000000000000000016345785d8a0000", + error: null, + revertReason: null, executeTxHash: null, fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", @@ -464,6 +480,8 @@ describe("TransactionController (e2e)", () => { blockNumber: 1, commitTxHash: null, data: "0x000000000000000000000000000000000000000000000000016345785d8a0000", + error: null, + revertReason: null, executeTxHash: null, fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", @@ -490,6 +508,8 @@ describe("TransactionController (e2e)", () => { blockNumber: 1, commitTxHash: null, data: "0x000000000000000000000000000000000000000000000000016345785d8a0000", + error: null, + revertReason: null, executeTxHash: null, fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", @@ -526,6 +546,8 @@ describe("TransactionController (e2e)", () => { blockNumber: 8, commitTxHash: "0xeb5ead20476b91008c3b6e44005017e697de78e4fd868d99d2c58566655c5aa8", data: "0x000000000000000000000000000000000000000000000000016345785d8a0000", + error: null, + revertReason: null, executeTxHash: "0xeb5ead20476b91008c3b6e44005017e697de78e4fd868d99d2c58566655c5ab8", fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", @@ -552,6 +574,8 @@ describe("TransactionController (e2e)", () => { blockNumber: 7, commitTxHash: "0xeb5ead20476b91008c3b6e44005017e697de78e4fd868d99d2c58566655c5aa7", data: "0x000000000000000000000000000000000000000000000000016345785d8a0000", + error: null, + revertReason: null, executeTxHash: "0xeb5ead20476b91008c3b6e44005017e697de78e4fd868d99d2c58566655c5ab7", fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", @@ -578,6 +602,8 @@ describe("TransactionController (e2e)", () => { blockNumber: 6, commitTxHash: "0xeb5ead20476b91008c3b6e44005017e697de78e4fd868d99d2c58566655c5aa6", data: "0x000000000000000000000000000000000000000000000000016345785d8a0000", + error: null, + revertReason: null, executeTxHash: null, fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", @@ -660,6 +686,8 @@ describe("TransactionController (e2e)", () => { blockNumber: 1, commitTxHash: null, data: "0x000000000000000000000000000000000000000000000000016345785d8a0000", + error: null, + revertReason: null, executeTxHash: null, fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", @@ -711,6 +739,8 @@ describe("TransactionController (e2e)", () => { blockNumber: 1, commitTxHash: null, data: "0x000000000000000000000000000000000000000000000000016345785d8a0000", + error: null, + revertReason: null, executeTxHash: null, fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", @@ -762,6 +792,8 @@ describe("TransactionController (e2e)", () => { blockNumber: 7, commitTxHash: "0xeb5ead20476b91008c3b6e44005017e697de78e4fd868d99d2c58566655c5aa7", data: "0x000000000000000000000000000000000000000000000000016345785d8a0000", + error: null, + revertReason: null, executeTxHash: "0xeb5ead20476b91008c3b6e44005017e697de78e4fd868d99d2c58566655c5ab7", fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", @@ -788,6 +820,8 @@ describe("TransactionController (e2e)", () => { blockNumber: 6, commitTxHash: "0xeb5ead20476b91008c3b6e44005017e697de78e4fd868d99d2c58566655c5aa6", data: "0x000000000000000000000000000000000000000000000000016345785d8a0000", + error: null, + revertReason: null, executeTxHash: null, fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", @@ -875,6 +909,8 @@ describe("TransactionController (e2e)", () => { blockNumber: 8, commitTxHash: "0xeb5ead20476b91008c3b6e44005017e697de78e4fd868d99d2c58566655c5aa8", data: "0x000000000000000000000000000000000000000000000000016345785d8a0000", + error: null, + revertReason: null, executeTxHash: "0xeb5ead20476b91008c3b6e44005017e697de78e4fd868d99d2c58566655c5ab8", fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", @@ -910,6 +946,8 @@ describe("TransactionController (e2e)", () => { blockNumber: 5, commitTxHash: "0xeb5ead20476b91008c3b6e44005017e697de78e4fd868d99d2c58566655c5aa5", data: "0x000000000000000000000000000000000000000000000000016345785d8a0000", + error: null, + revertReason: null, executeTxHash: null, fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", @@ -945,6 +983,8 @@ describe("TransactionController (e2e)", () => { blockNumber: 3, commitTxHash: "0xeb5ead20476b91008c3b6e44005017e697de78e4fd868d99d2c58566655c5aa3", data: "0x000000000000000000000000000000000000000000000000016345785d8a0000", + error: null, + revertReason: null, executeTxHash: null, fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", @@ -980,6 +1020,8 @@ describe("TransactionController (e2e)", () => { blockNumber: 1, commitTxHash: null, data: "0x000000000000000000000000000000000000000000000000016345785d8a0000", + error: null, + revertReason: null, executeTxHash: null, fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", @@ -1015,6 +1057,8 @@ describe("TransactionController (e2e)", () => { blockNumber: 9, commitTxHash: null, data: "0x000000000000000000000000000000000000000000000000016345785d8a0000", + error: null, + revertReason: null, executeTxHash: null, fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", @@ -1050,6 +1094,8 @@ describe("TransactionController (e2e)", () => { blockNumber: 1, commitTxHash: null, data: "0x000000000000000000000000000000000000000000000000016345785d8a0000", + error: null, + revertReason: null, executeTxHash: null, fee: "0x2386f26fc10000", from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", diff --git a/packages/app/src/components/transactions/infoTable/GeneralInfo.vue b/packages/app/src/components/transactions/infoTable/GeneralInfo.vue index ef84402e10..9141e4769f 100644 --- a/packages/app/src/components/transactions/infoTable/GeneralInfo.vue +++ b/packages/app/src/components/transactions/infoTable/GeneralInfo.vue @@ -30,6 +30,19 @@ /> + + + + {{ t("transactions.table.reason") }} + + + {{ t("transactions.table.reasonTooltip") }} + + + + {{ transaction.error || transaction.revertReason || "" }} + + {{ t("transactions.table.block") }} @@ -233,9 +246,6 @@ const tokenTransfers = computed(() => { diff --git a/packages/app/src/composables/common/Api.d.ts b/packages/app/src/composables/common/Api.d.ts index 371236776a..736992256b 100644 --- a/packages/app/src/composables/common/Api.d.ts +++ b/packages/app/src/composables/common/Api.d.ts @@ -86,6 +86,8 @@ declare namespace Api { l1BatchNumber: number | null; isL1BatchSealed: boolean; status: "included" | "committed" | "proved" | "verified" | "failed"; + error: string | null; + revertReason: string | null; }; type Transfer = { diff --git a/packages/app/src/composables/useTransaction.ts b/packages/app/src/composables/useTransaction.ts index fe48c1da0f..f920b7689a 100644 --- a/packages/app/src/composables/useTransaction.ts +++ b/packages/app/src/composables/useTransaction.ts @@ -64,6 +64,8 @@ export type TransactionItem = { status: TransactionStatus; l1BatchNumber: number | null; isL1BatchSealed: boolean; + error?: string | null; + revertReason?: string | null; logs: TransactionLogEntry[]; transfers: TokenTransfer[]; }; @@ -222,6 +224,8 @@ export function mapTransaction( status: transaction.status, l1BatchNumber: transaction.l1BatchNumber, isL1BatchSealed: transaction.isL1BatchSealed, + error: transaction.error, + revertReason: transaction.revertReason, logs: logs.map((item) => ({ address: item.address, diff --git a/packages/app/src/locales/en.json b/packages/app/src/locales/en.json index b62ec696f3..c1dc5faa01 100644 --- a/packages/app/src/locales/en.json +++ b/packages/app/src/locales/en.json @@ -98,6 +98,8 @@ "table": { "status": "Status", "statusTooltip": "The status of the transaction", + "reason": "Reason", + "reasonTooltip": "The failure reason of the transaction", "txnHash": "Txn hash", "transactionHash": "Transaction Hash", "transactionHashTooltip": "Transaction hash is a unique 66-character identifier that is generated whenever a transaction is executed", diff --git a/packages/app/src/locales/uk.json b/packages/app/src/locales/uk.json index 57b0dd91d0..10df6b08ad 100644 --- a/packages/app/src/locales/uk.json +++ b/packages/app/src/locales/uk.json @@ -72,6 +72,9 @@ }, "table": { "status": "Статус", + "statusTooltip": "Статус транзакції", + "reason": "Причина", + "reasonTooltip": "Причина невиконання транзакції", "transactionHash": "Хеш Транзакції", "nonce": "Нонс", "created": "Створено", diff --git a/packages/app/tests/components/transactions/GeneralInfo.spec.ts b/packages/app/tests/components/transactions/GeneralInfo.spec.ts index 9b101af347..d24bf80f66 100644 --- a/packages/app/tests/components/transactions/GeneralInfo.spec.ts +++ b/packages/app/tests/components/transactions/GeneralInfo.spec.ts @@ -310,15 +310,17 @@ describe("Transaction info table", () => { plugins: [i18n, $testId], }, props: { - transaction: { ...transaction, status: "failed" }, + transaction: { ...transaction, status: "failed", revertReason: "Revert reason" }, loading: false, }, }); await nextTick(); const status = wrapper.findAll("tbody tr td:nth-child(2)")[1]; const badges = status.findAllComponents(Badge); + const reason = wrapper.find(".transaction-reason-value"); expect(badges.length).toBe(1); expect(badges[0].text()).toBe(i18n.global.t("transactions.statusComponent.failed")); + expect(reason.text()).toBe("Revert reason"); }); it("renders included transaction status", async () => { const wrapper = mount(Table, { diff --git a/packages/app/tests/components/transactions/Table.spec.ts b/packages/app/tests/components/transactions/Table.spec.ts index 50cc9d4000..afe9966596 100644 --- a/packages/app/tests/components/transactions/Table.spec.ts +++ b/packages/app/tests/components/transactions/Table.spec.ts @@ -57,6 +57,8 @@ const transaction: TransactionListItem = { executeTxHash: null, proveTxHash: null, isL1BatchSealed: false, + error: null, + revertReason: null, }; const contractAbi: AbiFragment[] = [ diff --git a/packages/app/tests/composables/useTransaction.spec.ts b/packages/app/tests/composables/useTransaction.spec.ts index 6ca5c1927e..a93a8f37fe 100644 --- a/packages/app/tests/composables/useTransaction.spec.ts +++ b/packages/app/tests/composables/useTransaction.spec.ts @@ -88,6 +88,8 @@ vi.mock("ohmyfetch", async () => { commitTxHash: "0xe6a7ed0b6bf1c49f27feae3a71e5ba2aa4abaa6e372524369529946eb61a6936", executeTxHash: "0xdd70c8c2f59d88b9970c3b48a1230320f051d4502d0277124db481a42ada5c33", proveTxHash: "0x688c20e2106984bb0ccdadecf01e7bf12088b0ba671d888eca8e577ceac0d790", + error: null, + revertReason: null, }; return { ...mod, @@ -444,6 +446,8 @@ describe("useTransaction:", () => { nonce: 24, receivedAt: "2023-02-28T08:42:08.198Z", status: "verified", + error: null, + revertReason: null, l1BatchNumber: 11014, isL1BatchSealed: true, logs: [ diff --git a/packages/app/tests/composables/useTransactions.spec.ts b/packages/app/tests/composables/useTransactions.spec.ts index bbc526e5fb..a5e3263953 100644 --- a/packages/app/tests/composables/useTransactions.spec.ts +++ b/packages/app/tests/composables/useTransactions.spec.ts @@ -27,6 +27,8 @@ const transaction: TransactionListItem = { executeTxHash: null, proveTxHash: null, isL1BatchSealed: false, + error: null, + revertReason: null, }; vi.mock("ohmyfetch", () => { diff --git a/packages/worker/src/blockchain/blockchain.service.spec.ts b/packages/worker/src/blockchain/blockchain.service.spec.ts index 5593bd3042..bacfa748b1 100644 --- a/packages/worker/src/blockchain/blockchain.service.spec.ts +++ b/packages/worker/src/blockchain/blockchain.service.spec.ts @@ -1471,6 +1471,146 @@ describe("BlockchainService", () => { }); }); + describe("debugTraceTransaction", () => { + const traceTransactionResult = { + type: "Call", + from: "0x0000000000000000000000000000000000000000", + to: "0x0000000000000000000000000000000000008001", + error: null, + revertReason: "Exceed daily limit", + }; + let timeoutSpy; + + beforeEach(() => { + jest.spyOn(provider, "send").mockResolvedValue(traceTransactionResult); + timeoutSpy = jest.spyOn(timersPromises, "setTimeout"); + }); + + it("starts the rpc call duration metric", async () => { + await blockchainService.debugTraceTransaction( + "0xc0ae49e96910fa9df22eb59c0977905864664d495bc95906120695aa26e1710b" + ); + expect(startRpcCallDurationMetricMock).toHaveBeenCalledTimes(1); + }); + + it("gets transaction trace", async () => { + await blockchainService.debugTraceTransaction( + "0xc0ae49e96910fa9df22eb59c0977905864664d495bc95906120695aa26e1710b" + ); + expect(provider.send).toHaveBeenCalledTimes(1); + expect(provider.send).toHaveBeenCalledWith("debug_traceTransaction", [ + "0xc0ae49e96910fa9df22eb59c0977905864664d495bc95906120695aa26e1710b", + { + tracer: "callTracer", + tracerConfig: { onlyTopCall: false }, + }, + ]); + }); + + it("gets transaction trace with only top call", async () => { + await blockchainService.debugTraceTransaction( + "0xc0ae49e96910fa9df22eb59c0977905864664d495bc95906120695aa26e1710b", + true + ); + expect(provider.send).toHaveBeenCalledTimes(1); + expect(provider.send).toHaveBeenCalledWith("debug_traceTransaction", [ + "0xc0ae49e96910fa9df22eb59c0977905864664d495bc95906120695aa26e1710b", + { + tracer: "callTracer", + tracerConfig: { onlyTopCall: true }, + }, + ]); + }); + + it("stops the rpc call duration metric", async () => { + await blockchainService.debugTraceTransaction( + "0xc0ae49e96910fa9df22eb59c0977905864664d495bc95906120695aa26e1710b" + ); + expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1); + expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "debugTraceTransaction" }); + }); + + it("returns transaction trace", async () => { + const result = await blockchainService.debugTraceTransaction( + "0xc0ae49e96910fa9df22eb59c0977905864664d495bc95906120695aa26e1710b" + ); + expect(result).toEqual(traceTransactionResult); + }); + + describe("if the call throws an error", () => { + beforeEach(() => { + jest + .spyOn(provider, "send") + .mockRejectedValueOnce(new Error("RPC call error")) + .mockRejectedValueOnce(new Error("RPC call error")) + .mockResolvedValueOnce(traceTransactionResult); + }); + + it("retries RPC call with a default timeout", async () => { + await blockchainService.debugTraceTransaction( + "0xc0ae49e96910fa9df22eb59c0977905864664d495bc95906120695aa26e1710b" + ); + expect(provider.send).toHaveBeenCalledTimes(3); + expect(timeoutSpy).toHaveBeenCalledTimes(2); + expect(timeoutSpy).toHaveBeenNthCalledWith(1, defaultRetryTimeout); + expect(timeoutSpy).toHaveBeenNthCalledWith(2, defaultRetryTimeout); + }); + + it("stops the rpc call duration metric only for the successful retry", async () => { + await blockchainService.debugTraceTransaction( + "0xc0ae49e96910fa9df22eb59c0977905864664d495bc95906120695aa26e1710b" + ); + expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1); + expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "debugTraceTransaction" }); + }); + + it("returns result of the successful RPC call", async () => { + const result = await blockchainService.debugTraceTransaction( + "0xc0ae49e96910fa9df22eb59c0977905864664d495bc95906120695aa26e1710b" + ); + expect(result).toEqual(traceTransactionResult); + }); + }); + + describe("if the call throws a timeout error", () => { + beforeEach(() => { + jest + .spyOn(provider, "send") + .mockRejectedValueOnce({ code: "TIMEOUT" }) + .mockRejectedValueOnce({ code: "TIMEOUT" }) + .mockResolvedValueOnce(traceTransactionResult); + }); + + it("retries RPC call with a quick timeout", async () => { + await blockchainService.debugTraceTransaction( + "0xc0ae49e96910fa9df22eb59c0977905864664d495bc95906120695aa26e1710b" + ); + expect(timeoutSpy).toHaveBeenCalledTimes(2); + expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout); + expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout); + }); + }); + + describe("if the call throws a connection refused error", () => { + beforeEach(() => { + jest + .spyOn(provider, "send") + .mockRejectedValueOnce({ code: "TIMEOUT" }) + .mockRejectedValueOnce({ code: "TIMEOUT" }) + .mockResolvedValueOnce(traceTransactionResult); + }); + + it("retries RPC call with a quick timeout", async () => { + await blockchainService.debugTraceTransaction( + "0xc0ae49e96910fa9df22eb59c0977905864664d495bc95906120695aa26e1710b" + ); + expect(timeoutSpy).toHaveBeenCalledTimes(2); + expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout); + expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout); + }); + }); + }); + describe("onModuleInit", () => { let bridgeAddresses; beforeEach(() => { diff --git a/packages/worker/src/blockchain/blockchain.service.ts b/packages/worker/src/blockchain/blockchain.service.ts index cf6dbf64b8..741631c8d5 100644 --- a/packages/worker/src/blockchain/blockchain.service.ts +++ b/packages/worker/src/blockchain/blockchain.service.ts @@ -15,6 +15,14 @@ export interface BridgeAddresses { l2Erc20DefaultBridge: string; } +export interface TraceTransactionResult { + type: string; + from: string; + to: string; + error: string | null; + revertReason: string | null; +} + @Injectable() export class BlockchainService implements OnModuleInit { private readonly logger: Logger; @@ -121,6 +129,18 @@ export class BlockchainService implements OnModuleInit { }, "getDefaultBridgeAddresses"); } + public async debugTraceTransaction(txHash: string, onlyTopCall = false): Promise { + return await this.rpcCall(async () => { + return await this.provider.send("debug_traceTransaction", [ + txHash, + { + tracer: "callTracer", + tracerConfig: { onlyTopCall }, + }, + ]); + }, "debugTraceTransaction"); + } + public async on(eventName: EventType, listener: Listener): Promise { this.provider.on(eventName, listener); } diff --git a/packages/worker/src/entities/transaction.entity.ts b/packages/worker/src/entities/transaction.entity.ts index 0ec6d9b067..ca1f3511ec 100644 --- a/packages/worker/src/entities/transaction.entity.ts +++ b/packages/worker/src/entities/transaction.entity.ts @@ -90,4 +90,10 @@ export class Transaction extends CountableEntity { @Column({ type: "int", default: 1 }) public readonly receiptStatus: number; + + @Column({ nullable: true }) + public readonly error?: string; + + @Column({ nullable: true }) + public readonly revertReason?: string; } diff --git a/packages/worker/src/migrations/1700684231991-AddTransactionError.ts b/packages/worker/src/migrations/1700684231991-AddTransactionError.ts new file mode 100644 index 0000000000..c0ccce4efd --- /dev/null +++ b/packages/worker/src/migrations/1700684231991-AddTransactionError.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddTransactionError1700684231991 implements MigrationInterface { + name = "AddTransactionError1700684231991"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "transactions" ADD "error" character varying`); + await queryRunner.query(`ALTER TABLE "transactions" ADD "revertReason" character varying`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "transactions" DROP COLUMN "revertReason"`); + await queryRunner.query(`ALTER TABLE "transactions" DROP COLUMN "error"`); + } +} diff --git a/packages/worker/src/repositories/transaction.repository.ts b/packages/worker/src/repositories/transaction.repository.ts index b055c40769..610d1469be 100644 --- a/packages/worker/src/repositories/transaction.repository.ts +++ b/packages/worker/src/repositories/transaction.repository.ts @@ -10,6 +10,8 @@ export interface TransactionDto extends types.TransactionResponse { receiptStatus: number; isL1Originated: boolean; receivedAt: Date; + error?: string; + revertReason?: string; } @Injectable() diff --git a/packages/worker/src/transaction/transaction.processor.spec.ts b/packages/worker/src/transaction/transaction.processor.spec.ts index e152da109e..e6b28d30a2 100644 --- a/packages/worker/src/transaction/transaction.processor.spec.ts +++ b/packages/worker/src/transaction/transaction.processor.spec.ts @@ -3,7 +3,7 @@ import { Logger } from "@nestjs/common"; import { mock } from "jest-mock-extended"; import { types } from "zksync-web3"; import { TransactionRepository, TransactionReceiptRepository } from "../repositories"; -import { BlockchainService } from "../blockchain"; +import { BlockchainService, TraceTransactionResult } from "../blockchain"; import { TransactionProcessor } from "./transaction.processor"; import { LogProcessor } from "../log"; @@ -83,11 +83,16 @@ describe("TransactionProcessor", () => { status: 1, }); const transactionDetails = mock(); + const traceTransactionResult = mock({ + error: "Some error", + revertReason: "Some revert reason", + }); beforeEach(() => { jest.spyOn(blockchainServiceMock, "getTransaction").mockResolvedValue(transaction); jest.spyOn(blockchainServiceMock, "getTransactionReceipt").mockResolvedValue(transactionReceipt); jest.spyOn(blockchainServiceMock, "getTransactionDetails").mockResolvedValue(transactionDetails); + jest.spyOn(blockchainServiceMock, "debugTraceTransaction").mockResolvedValue(traceTransactionResult); }); it("starts the transaction duration metric", async () => { @@ -176,5 +181,50 @@ describe("TransactionProcessor", () => { await transactionProcessor.add(transaction.hash, blockDetails); expect(stopTxProcessingDurationMetricMock).toHaveBeenCalledTimes(1); }); + + describe("when transaction has failed status", () => { + beforeEach(() => { + (blockchainServiceMock.getTransactionReceipt as jest.Mock).mockResolvedValueOnce({ + transactionIndex: 0, + logs: [], + status: 0, + }); + }); + + it("reads transaction trace", async () => { + await transactionProcessor.add(transaction.hash, blockDetails); + expect(blockchainServiceMock.debugTraceTransaction).toHaveBeenCalledTimes(1); + expect(blockchainServiceMock.debugTraceTransaction).toHaveBeenCalledWith(transaction.hash, true); + }); + + describe("when transaction trace contains error and revert reason", () => { + it("adds the transaction info with error and revert reason", async () => { + await transactionProcessor.add(transaction.hash, blockDetails); + expect(transactionRepositoryMock.add).toHaveBeenCalledTimes(1); + expect(transactionRepositoryMock.add).toHaveBeenCalledWith({ + ...transaction, + ...transactionDetails, + l1BatchNumber: blockDetails.l1BatchNumber, + receiptStatus: 0, + error: traceTransactionResult.error, + revertReason: traceTransactionResult.revertReason, + }); + }); + }); + + describe("when transaction trace doe not contain error and revert reason", () => { + it("adds the transaction info without error and revert reason", async () => { + (blockchainServiceMock.debugTraceTransaction as jest.Mock).mockResolvedValueOnce(null); + await transactionProcessor.add(transaction.hash, blockDetails); + expect(transactionRepositoryMock.add).toHaveBeenCalledTimes(1); + expect(transactionRepositoryMock.add).toHaveBeenCalledWith({ + ...transaction, + ...transactionDetails, + l1BatchNumber: blockDetails.l1BatchNumber, + receiptStatus: 0, + }); + }); + }); + }); }); }); diff --git a/packages/worker/src/transaction/transaction.processor.ts b/packages/worker/src/transaction/transaction.processor.ts index 6a9f352efb..0f45a00f54 100644 --- a/packages/worker/src/transaction/transaction.processor.ts +++ b/packages/worker/src/transaction/transaction.processor.ts @@ -44,17 +44,29 @@ export class TransactionProcessor { throw new Error(`Some of the blockchain transaction APIs returned null for a transaction ${transactionHash}`); } + const transactionToAdd = { + ...transaction, + ...transactionDetails, + l1BatchNumber: blockDetails.l1BatchNumber, + receiptStatus: transactionReceipt.status, + } as TransactionDto; + + if (transactionReceipt.status === 0) { + const debugTraceTransactionResult = await this.blockchainService.debugTraceTransaction(transactionHash, true); + if (debugTraceTransactionResult?.error) { + transactionToAdd.error = debugTraceTransactionResult.error; + } + if (debugTraceTransactionResult?.revertReason) { + transactionToAdd.revertReason = debugTraceTransactionResult.revertReason; + } + } + this.logger.debug({ message: "Adding transaction data to the DB", blockNumber: blockDetails.number, transactionHash, }); - await this.transactionRepository.add({ - ...transaction, - ...transactionDetails, - l1BatchNumber: blockDetails.l1BatchNumber, - receiptStatus: transactionReceipt.status, - } as TransactionDto); + await this.transactionRepository.add(transactionToAdd); this.logger.debug({ message: "Adding transaction receipt data to the DB", From aacfa469451e8c6431f9cd4fc80f693f9dddef2d Mon Sep 17 00:00:00 2001 From: Vasyl Ivanchuk Date: Tue, 5 Dec 2023 20:30:13 +0200 Subject: [PATCH 10/52] fix: add sepolia staging hostname (#111) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # What ❔ Sepolia staging hostname. - [X] PR title corresponds to the body of PR (we generate changelog entries from PRs). --- packages/app/src/configs/staging.config.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/app/src/configs/staging.config.json b/packages/app/src/configs/staging.config.json index 7be5e57441..254c3f0859 100644 --- a/packages/app/src/configs/staging.config.json +++ b/packages/app/src/configs/staging.config.json @@ -21,7 +21,9 @@ "apiUrl": "https://block-explorer-api.sepolia.zksync.dev", "verificationApiUrl": "https://explorer.sepolia.era.zksync.dev", "bridgeUrl": "https://staging.goerli.bridge.zksync.dev", - "hostnames": [], + "hostnames": [ + "https://sepolia.staging-scan-v2.zksync.dev" + ], "icon": "/images/icons/zksync-arrows.svg", "l1ExplorerUrl": "https://sepolia.etherscan.io", "l2ChainId": 300, From 65d0a8d3d91643d32cdc680f91df95628e0fb376 Mon Sep 17 00:00:00 2001 From: Roman Petriv Date: Wed, 6 Dec 2023 15:23:02 +0200 Subject: [PATCH 11/52] chore: readme fixes (#113) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # What ❔ - remove $ from copy-pasteable commands - add sepolia links --- README.md | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index f8b7ec1d91..e9276bde56 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ flowchart ## 🛠 Installation ```bash -$ npm install +npm install ``` ## ⚙️ Setting up env variables @@ -63,7 +63,7 @@ Make sure you have [zksync-era](https://github.com/matter-labs/zksync-era) repo The following script sets `.env` files for [Worker](./packages/worker) and [API](./packages/api) packages as well as environment configuration file for [App](./packages/app) package based on your local [zksync-era](https://github.com/matter-labs/zksync-era) repo setup. ```bash -$ npm run hyperchain:configure +npm run hyperchain:configure ``` You can review and edit generated files if you need to change any settings. @@ -72,18 +72,18 @@ You can review and edit generated files if you need to change any settings. Before running the solution, make sure you have a database server up and running, you have created a database and set up all the required environment variables. To create a database run the following command: ```bash -$ npm run db:create +npm run db:create ``` To run all the packages (`Worker`, `API` and front-end `App`) in `development` mode run the following command from the root directory. ```bash -$ npm run dev +npm run dev ``` For `production` mode run: ```bash -$ npm run build -$ npm run start +npm run build +npm run start ``` Each component can also be started individually. Follow individual packages `README` for details. @@ -105,15 +105,15 @@ To verify front-end `App` is running open http://localhost:3010 in your browser. ## 🕵️‍♂️ Testing Run unit tests for all packages: ```bash -$ npm run test +npm run test ``` Run e2e tests for all packages: ```bash -$ npm run test:e2e +npm run test:e2e ``` Run tests for a specific package: ```bash -$ npm run test -w {package} +npm run test -w {package} ``` For more details on testing please check individual packages `README`. @@ -129,7 +129,9 @@ zkSync Era Block Explorer is distributed under the terms of either at your option. ## 🔗 Production links -- Testnet API: https://block-explorer-api.testnets.zksync.dev +- Testnet Goerli API: https://block-explorer-api.testnets.zksync.dev +- Testnet Sepolia API: https://block-explorer-api.sepolia.zksync.dev - Mainnet API: https://block-explorer-api.mainnet.zksync.io -- Testnet App: https://goerli.explorer.zksync.io +- Testnet Goerli App: https://goerli.explorer.zksync.io +- Testnet Sepolia App: https://sepolia.explorer.zksync.io - Mainnet App: https://explorer.zksync.io From b897a78555feae476cb1176a1101df40c321fdac Mon Sep 17 00:00:00 2001 From: Vasyl Ivanchuk Date: Thu, 7 Dec 2023 12:47:02 +0200 Subject: [PATCH 12/52] feat: show gas fields on UI (#114) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # What ❔ image ## Why ❔ For better user experience it was requested to show gas fields and limits on UI. ## Checklist - [X] PR title corresponds to the body of PR (we generate changelog entries from PRs). - [X] Tests for the changes have been added / updated. --- packages/app/mock/transactions/Execute.json | 8 ++- .../transactions/infoTable/GeneralInfo.vue | 52 ++++++++++++++++++- packages/app/src/composables/common/Api.d.ts | 6 +++ .../app/src/composables/useTransaction.ts | 27 +++++++++- packages/app/src/locales/en.json | 10 +++- packages/app/src/locales/uk.json | 10 +++- .../transactions/GeneralInfo.spec.ts | 43 ++++++++++++++- .../components/transactions/Table.spec.ts | 6 +++ .../tests/composables/useTransaction.spec.ts | 24 +++++++++ .../tests/composables/useTransactions.spec.ts | 6 +++ 10 files changed, 185 insertions(+), 7 deletions(-) diff --git a/packages/app/mock/transactions/Execute.json b/packages/app/mock/transactions/Execute.json index ade6fbb50d..32b78b14b5 100644 --- a/packages/app/mock/transactions/Execute.json +++ b/packages/app/mock/transactions/Execute.json @@ -165,5 +165,11 @@ ], "isL1BatchSealed": false, "to": "0x4732C03B2CF6eDe46500e799DE79a15Df44929eB", - "value": "0x00" + "value": "0x00", + "gasPrice": "4000", + "gasLimit": "5000", + "gasUsed": "3000", + "gasPerPubdata": "800", + "maxFeePerGas": "7000", + "maxPriorityFeePerGas": "8000" } diff --git a/packages/app/src/components/transactions/infoTable/GeneralInfo.vue b/packages/app/src/components/transactions/infoTable/GeneralInfo.vue index 9141e4769f..eafef5b7ba 100644 --- a/packages/app/src/components/transactions/infoTable/GeneralInfo.vue +++ b/packages/app/src/components/transactions/infoTable/GeneralInfo.vue @@ -166,7 +166,48 @@ - + + + {{ t("transactions.table.gasLimitAndUsed") }} + {{ + t("transactions.table.gasLimitAndUsedTooltip") + }} + + {{ transaction?.gasLimit }} | {{ transaction?.gasUsed }} ({{ gasUsedPercent }}%) + + + + {{ t("transactions.table.gasPerPubdata") }} + {{ + t("transactions.table.gasPerPubdataTooltip") + }} + + {{ transaction.gasPerPubdata }} + + + + {{ t("transactions.table.maxFeePerGas") }} + + {{ t("transactions.table.maxFeePerGasTooltip") }} + + + + + + + + + {{ t("transactions.table.maxPriorityFeePerGas") }} + + {{ t("transactions.table.maxPriorityFeePerGasTooltip") }} + + + + + + {{ t("transactions.table.nonce") }} @@ -242,6 +283,15 @@ const tokenTransfers = computed(() => { // exclude transfers with no amount, such as NFT until we fully support them return props.transaction?.transfers.filter((transfer) => transfer.amount) || []; }); + +const gasUsedPercent = computed(() => { + if (props.transaction) { + const gasLimit = parseInt(props.transaction.gasLimit, 10); + const gasUsed = parseInt(props.transaction.gasUsed, 10); + return parseFloat(((gasUsed / gasLimit) * 100).toFixed(2)); + } + return null; +});