@@ -15,7 +14,6 @@
import { useTitle } from "@vueuse/core";
import IndexerDelayAlert from "@/components/IndexerDelayAlert.vue";
-import NetworkDeprecated from "@/components/NetworkDeprecated.vue";
import TheFooter from "@/components/TheFooter.vue";
import TheHeader from "@/components/header/TheHeader.vue";
diff --git a/packages/app/src/components/FeeData.vue b/packages/app/src/components/FeeData.vue
index fadf95e40d..2e07f44784 100644
--- a/packages/app/src/components/FeeData.vue
+++ b/packages/app/src/components/FeeData.vue
@@ -27,7 +27,7 @@
{{
t(
@@ -40,7 +40,7 @@
{{ t("transactions.table.feeDetails.whatIsPaymaster") }}
@@ -57,12 +57,13 @@ import { BigNumber } from "ethers";
import TokenAmountPrice from "@/components/TokenAmountPrice.vue";
import TransferTableCell from "@/components/transactions/infoTable/TransferTableCell.vue";
+import useContext from "@/composables/useContext";
import useToken from "@/composables/useToken";
import type { Token } from "@/composables/useToken";
import type { FeeData } from "@/composables/useTransaction";
-import { ETH_TOKEN_L2_ADDRESS } from "@/utils/constants";
+const { currentNetwork } = useContext();
const props = defineProps({
showDetails: {
@@ -81,7 +82,7 @@ const collapsed = ref(false);
const buttonTitle = computed(() =>
collapsed.value ? t("transactions.table.feeDetails.closeDetails") : t("transactions.table.feeDetails.moreDetails")
);
-getTokenInfo(ETH_TOKEN_L2_ADDRESS);
+getTokenInfo(currentNetwork.value.baseTokenAddress);
const initialFee = computed(() => {
if (props.feeData) {
diff --git a/packages/app/src/components/NetworkDeprecated.vue b/packages/app/src/components/NetworkDeprecated.vue
deleted file mode 100644
index a963d8bfa8..0000000000
--- a/packages/app/src/components/NetworkDeprecated.vue
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
- We are ending our support of Goerli testnet. Please use Sepolia. For more info see
- this announcement.
-
-
-
-
-
-
diff --git a/packages/app/src/components/TheFooter.vue b/packages/app/src/components/TheFooter.vue
index 86b0c44590..8a3ad69b09 100644
--- a/packages/app/src/components/TheFooter.vue
+++ b/packages/app/src/components/TheFooter.vue
@@ -24,7 +24,7 @@ const config = useRuntimeConfig();
const navigation = reactive([
{
label: computed(() => t("footer.nav.docs")),
- url: "https://era.zksync.io/docs/dev/",
+ url: "https://docs.zksync.io/build/tooling/block-explorer/getting-started.html",
},
{
label: computed(() => t("footer.nav.terms")),
diff --git a/packages/app/src/components/balances/Table.vue b/packages/app/src/components/balances/Table.vue
index 4cc870264d..f69d1b8e62 100644
--- a/packages/app/src/components/balances/Table.vue
+++ b/packages/app/src/components/balances/Table.vue
@@ -58,12 +58,12 @@ import TableBodyColumn from "@/components/common/table/TableBodyColumn.vue";
import TableHeadColumn from "@/components/common/table/TableHeadColumn.vue";
import CopyContent from "@/components/common/table/fields/CopyContent.vue";
+import useContext from "@/composables/useContext";
import useTokenLibrary from "@/composables/useTokenLibrary";
import type { Balances } from "@/composables/useAddress";
-import { ETH_TOKEN_L2_ADDRESS } from "@/utils/constants";
-
+const { currentNetwork } = useContext();
const { t } = useI18n();
const props = defineProps({
@@ -98,9 +98,9 @@ const displayedBalances = computed(() => {
.sort((a, b) => {
if (!a.token || !b.token) return 0;
- if (a.token.l2Address === ETH_TOKEN_L2_ADDRESS) {
+ if (a.token.l2Address === currentNetwork.value.baseTokenAddress) {
return -1;
- } else if (b.token.l2Address === ETH_TOKEN_L2_ADDRESS) {
+ } else if (b.token.l2Address === currentNetwork.value.baseTokenAddress) {
return 1;
}
diff --git a/packages/app/src/components/blocks/InfoTable.vue b/packages/app/src/components/blocks/InfoTable.vue
index 63ddc25491..0ce0fbebee 100644
--- a/packages/app/src/components/blocks/InfoTable.vue
+++ b/packages/app/src/components/blocks/InfoTable.vue
@@ -91,9 +91,9 @@ const tableInfoItems = computed(() => {
: {}),
},
{
- label: t("blocks.table.rootHash"),
- tooltip: t("blocks.table.rootHashTooltip"),
- value: props.block.hash ? { value: props.block.hash } : t("blocks.table.noRootHashYet"),
+ label: t("blocks.table.blockHash"),
+ tooltip: t("blocks.table.blockHashTooltip"),
+ value: props.block.hash ? { value: props.block.hash } : t("blocks.table.noBlockHashYet"),
component: props.block.hash ? CopyContent : undefined,
},
{
diff --git a/packages/app/src/components/common/CheckBoxInput.stories.ts b/packages/app/src/components/common/CheckBoxInput.stories.ts
new file mode 100644
index 0000000000..6f1d6a20fc
--- /dev/null
+++ b/packages/app/src/components/common/CheckBoxInput.stories.ts
@@ -0,0 +1,29 @@
+import CheckBoxInput from "./CheckBoxInput.vue";
+
+export default {
+ title: "Common/CheckBoxInput",
+ component: CheckBoxInput,
+};
+
+type Args = {
+ modelValue: boolean;
+};
+
+const Template = (args: Args) => ({
+ components: { CheckBoxInput },
+ setup() {
+ return { args };
+ },
+ template: `
+
CheckBox Input`,
+});
+
+export const Checked = Template.bind({}) as unknown as { args: Args };
+Checked.args = {
+ modelValue: true,
+};
+
+export const Unchecked = Template.bind({}) as unknown as { args: Args };
+Unchecked.args = {
+ modelValue: false,
+};
diff --git a/packages/app/src/components/common/CheckBoxInput.vue b/packages/app/src/components/common/CheckBoxInput.vue
new file mode 100644
index 0000000000..04f1de75b6
--- /dev/null
+++ b/packages/app/src/components/common/CheckBoxInput.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/app/src/components/form/FormItem.vue b/packages/app/src/components/form/FormItem.vue
index 8e179ae1be..f5a72f24e3 100644
--- a/packages/app/src/components/form/FormItem.vue
+++ b/packages/app/src/components/form/FormItem.vue
@@ -42,7 +42,7 @@ defineProps({
}
.label-inline-block {
.form-item-label {
- @apply inline-block;
+ @apply float-left;
}
}
diff --git a/packages/app/src/components/header/TheHeader.vue b/packages/app/src/components/header/TheHeader.vue
index 68948b4abc..823ca0990c 100644
--- a/packages/app/src/components/header/TheHeader.vue
+++ b/packages/app/src/components/header/TheHeader.vue
@@ -159,7 +159,7 @@ const { currentNetwork } = useContext();
const navigation = reactive([
{
label: computed(() => t("header.nav.documentation")),
- url: "https://era.zksync.io/docs/dev/",
+ url: "https://docs.zksync.io/build/tooling/block-explorer/getting-started.html",
},
]);
@@ -191,10 +191,6 @@ const links = [
label: computed(() => t("header.nav.contractVerification")),
to: { name: "contract-verification" },
},
- {
- label: computed(() => t("header.nav.portal")),
- url: computed(() => currentNetwork.value.l2WalletUrl),
- },
];
if (currentNetwork.value.bridgeUrl) {
@@ -305,20 +301,12 @@ const hasContent = computed(() => {
.hero-banner-container {
@apply absolute left-0 top-full flex h-64 w-full items-end justify-end overflow-hidden bg-primary-900;
- &.goerli {
- @apply h-[25rem] md:h-[23rem] lg:h-[19rem];
- }
-
.hero-image {
@apply h-5/6 w-auto;
}
}
.home-banner {
@apply h-80;
-
- &.goerli {
- @apply h-[30rem] md:h-[27rem] lg:h-[24rem];
- }
}
}
.header-mobile-popover {
diff --git a/packages/app/src/components/transactions/EthAmountPrice.vue b/packages/app/src/components/transactions/EthAmountPrice.vue
index 60469e4281..c6e0a39317 100644
--- a/packages/app/src/components/transactions/EthAmountPrice.vue
+++ b/packages/app/src/components/transactions/EthAmountPrice.vue
@@ -6,11 +6,12 @@ import { computed, type PropType } from "vue";
import TokenAmountPrice from "@/components/TokenAmountPrice.vue";
+import useContext from "@/composables/useContext";
import useToken, { type Token } from "@/composables/useToken";
import type { BigNumberish } from "ethers";
-import { ETH_TOKEN_L2_ADDRESS } from "@/utils/constants";
+const { currentNetwork } = useContext();
defineProps({
amount: {
@@ -21,7 +22,7 @@ defineProps({
});
const { getTokenInfo, tokenInfo } = useToken();
-getTokenInfo(ETH_TOKEN_L2_ADDRESS);
+getTokenInfo(currentNetwork.value.baseTokenAddress);
const token = computed
(() => {
return tokenInfo.value;
diff --git a/packages/app/src/components/transactions/Table.vue b/packages/app/src/components/transactions/Table.vue
index 447c5ab7bd..46597e9910 100644
--- a/packages/app/src/components/transactions/Table.vue
+++ b/packages/app/src/components/transactions/Table.vue
@@ -212,6 +212,7 @@ import TokenAmountPriceTableCell from "@/components/transactions/TokenAmountPric
import TransactionDirectionTableCell from "@/components/transactions/TransactionDirectionTableCell.vue";
import TransactionNetworkSquareBlock from "@/components/transactions/TransactionNetworkSquareBlock.vue";
+import useContext from "@/composables/useContext";
import useToken, { type Token } from "@/composables/useToken";
import { decodeDataWithABI } from "@/composables/useTransactionData";
import useTransactions, { type TransactionListItem, type TransactionSearchParams } from "@/composables/useTransactions";
@@ -220,9 +221,10 @@ import type { Direction } from "@/components/transactions/TransactionDirectionTa
import type { AbiFragment } from "@/composables/useAddress";
import type { NetworkOrigin } from "@/types";
-import { ETH_TOKEN_L2_ADDRESS } from "@/utils/constants";
import { utcStringFromISOString } from "@/utils/helpers";
+const { currentNetwork } = useContext();
+
const { t, te } = useI18n();
const props = defineProps({
@@ -251,7 +253,7 @@ const searchParams = computed(() => props.searchParams ?? {});
const { data, load, total, pending, pageSize } = useTransactions(searchParams);
const { getTokenInfo, tokenInfo, isRequestPending: isLoadingEthTokenInfo } = useToken();
-getTokenInfo(ETH_TOKEN_L2_ADDRESS);
+getTokenInfo(currentNetwork.value.baseTokenAddress);
const ethToken = computed(() => {
return tokenInfo.value;
diff --git a/packages/app/src/components/transactions/infoTable/GeneralInfo.vue b/packages/app/src/components/transactions/infoTable/GeneralInfo.vue
index f1b554ed4a..40fbb238d6 100644
--- a/packages/app/src/components/transactions/infoTable/GeneralInfo.vue
+++ b/packages/app/src/components/transactions/infoTable/GeneralInfo.vue
@@ -30,7 +30,7 @@
/>
-
+
{{ t("transactions.table.reason") }}
diff --git a/packages/app/src/components/transactions/infoTable/TransferTableCell.stories.ts b/packages/app/src/components/transactions/infoTable/TransferTableCell.stories.ts
index ab3bf55a8d..4b26f215f0 100644
--- a/packages/app/src/components/transactions/infoTable/TransferTableCell.stories.ts
+++ b/packages/app/src/components/transactions/infoTable/TransferTableCell.stories.ts
@@ -34,7 +34,7 @@ Default.args = {
decimals: 6,
l1Address: "0xd35cceead182dcee0f148ebac9447da2c4d449c4",
l2Address: "0x54a14d7559baf2c8e8fa504e019d32479739018c",
- name: "USD Coin (goerli)",
+ name: "USD Coin (testnet)",
symbol: "USDC",
},
},
diff --git a/packages/app/src/composables/useContext.ts b/packages/app/src/composables/useContext.ts
index a03133aaa0..632611bcd5 100644
--- a/packages/app/src/composables/useContext.ts
+++ b/packages/app/src/composables/useContext.ts
@@ -8,6 +8,7 @@ import { DEFAULT_NETWORK } from "./useRuntimeConfig";
import type { NetworkConfig } from "@/configs";
+import { checksumAddress } from "@/utils/formatters";
import { getWindowLocation } from "@/utils/helpers";
const network = useStorage("selectedNetwork_v2", DEFAULT_NETWORK.name);
@@ -26,9 +27,12 @@ export default (): Context => {
const environmentConfig = useEnvironmentConfig();
const networks = computed(() => {
- return Array.isArray(environmentConfig.networks.value) && environmentConfig.networks.value.length
- ? environmentConfig.networks.value
- : [DEFAULT_NETWORK];
+ const configuredNetworks =
+ Array.isArray(environmentConfig.networks.value) && environmentConfig.networks.value.length
+ ? environmentConfig.networks.value
+ : [DEFAULT_NETWORK];
+ configuredNetworks.forEach((network) => (network.baseTokenAddress = checksumAddress(network.baseTokenAddress)));
+ return configuredNetworks;
});
const currentNetwork = computed(() => {
return (
diff --git a/packages/app/src/composables/useEnvironmentConfig.ts b/packages/app/src/composables/useEnvironmentConfig.ts
index 0bf3c76df6..30c4dbd8ae 100644
--- a/packages/app/src/composables/useEnvironmentConfig.ts
+++ b/packages/app/src/composables/useEnvironmentConfig.ts
@@ -2,6 +2,8 @@ import { computed, ref } from "vue";
import type { EnvironmentConfig, NetworkConfig, RuntimeConfig } from "@/configs";
+import { checksumAddress } from "@/utils/formatters";
+
const config = ref(null);
const HYPERCHAIN_CONFIG_NAME = "hyperchain";
@@ -24,6 +26,11 @@ export async function loadEnvironmentConfig(runtimeConfig: RuntimeConfig): Promi
} else {
envConfig = (await import(`../configs/${runtimeConfig.appEnvironment}.config.json`)).default;
}
+
+ envConfig.networks?.forEach((networkConfig) => {
+ networkConfig.baseTokenAddress = checksumAddress(networkConfig.baseTokenAddress);
+ });
+
config.value = envConfig;
}
diff --git a/packages/app/src/composables/useRuntimeConfig.ts b/packages/app/src/composables/useRuntimeConfig.ts
index b0a053ee8e..27c5d1efce 100644
--- a/packages/app/src/composables/useRuntimeConfig.ts
+++ b/packages/app/src/composables/useRuntimeConfig.ts
@@ -1,19 +1,20 @@
import type { NetworkConfig, RuntimeConfig } from "@/configs";
+import { checksumAddress } from "@/utils/formatters";
export const DEFAULT_NETWORK: NetworkConfig = {
- apiUrl: "https://block-explorer-api.testnets.zksync.dev",
- verificationApiUrl: "https://zksync2-testnet-explorer.zksync.dev",
- bridgeUrl: "https://goerli.bridge.zksync.io",
- hostnames: ["https://goerli.explorer.zksync.io"],
+ apiUrl: "https://block-explorer-api.sepolia.zksync.dev",
+ verificationApiUrl: "https://explorer.sepolia.era.zksync.dev",
+ bridgeUrl: "https://portal.zksync.io/bridge/?network=sepolia",
+ hostnames: ["https://sepolia.explorer.zksync.io"],
icon: "/images/icons/zksync-arrows.svg",
- l1ExplorerUrl: "https://goerli.etherscan.io",
- l2ChainId: 280,
- l2NetworkName: "zkSync Era Goerli Testnet",
- l2WalletUrl: "https://goerli.portal.zksync.io/",
+ l1ExplorerUrl: "https://sepolia.etherscan.io",
+ l2ChainId: 300,
+ l2NetworkName: "zkSync Era Sepolia Testnet",
maintenance: false,
- name: "goerli",
+ name: "sepolia",
published: true,
- rpcUrl: "https://testnet.era.zksync.dev",
+ baseTokenAddress: checksumAddress("0x000000000000000000000000000000000000800A"),
+ rpcUrl: "https://sepolia.era.zksync.dev",
};
export default (): RuntimeConfig => {
diff --git a/packages/app/src/composables/useToken.ts b/packages/app/src/composables/useToken.ts
index 3f1a7c308f..7bb0196a06 100644
--- a/packages/app/src/composables/useToken.ts
+++ b/packages/app/src/composables/useToken.ts
@@ -9,7 +9,6 @@ import useTokenLibrary from "@/composables/useTokenLibrary";
import type { Hash } from "@/types";
export type Token = Api.Response.Token;
-
export const retrieveToken = useMemoize(
(tokenAddress: Hash, context: Context = useContext()): Promise => {
return $fetch(`${context.currentNetwork.value.apiUrl}/tokens/${tokenAddress}`);
@@ -20,10 +19,8 @@ export const retrieveToken = useMemoize(
},
}
);
-
export default () => {
const { getToken, getTokens } = useTokenLibrary();
-
const isRequestPending = ref(false);
const isRequestFailed = ref(false);
const tokenInfo = ref(null as Token | null);
diff --git a/packages/app/src/composables/useTokenLibrary.ts b/packages/app/src/composables/useTokenLibrary.ts
index d7e3dc12d0..e6728405c9 100644
--- a/packages/app/src/composables/useTokenLibrary.ts
+++ b/packages/app/src/composables/useTokenLibrary.ts
@@ -39,7 +39,6 @@ export default (context = useContext()) => {
const isRequestPending = ref(false);
const isRequestFailed = ref(false);
const tokens = ref([]);
-
const getToken = (tokenAddress: string) => {
return tokens.value.find((token) => token.l2Address === tokenAddress);
};
diff --git a/packages/app/src/configs/dev.config.json b/packages/app/src/configs/dev.config.json
index 07de06fbd1..5db1f5c485 100644
--- a/packages/app/src/configs/dev.config.json
+++ b/packages/app/src/configs/dev.config.json
@@ -9,43 +9,26 @@
"icon": "/images/icons/zksync-arrows.svg",
"l2ChainId": 270,
"l2NetworkName": "Local",
- "l2WalletUrl": "http://localhost:3000",
"maintenance": false,
"name": "local",
"published": true,
- "rpcUrl": "http://localhost:3050"
- },
- {
- "apiUrl": "https://block-explorer-api.testnets.zksync.dev",
- "verificationApiUrl": "https://zksync2-testnet-explorer.zksync.dev",
- "bridgeUrl": "https://staging.goerli.bridge.zksync.dev",
- "hostnames": [
- "https://goerli.staging-scan-v2.zksync.dev"
- ],
- "icon": "/images/icons/zksync-arrows.svg",
- "l1ExplorerUrl": "https://goerli.etherscan.io",
- "l2ChainId": 280,
- "l2NetworkName": "zkSync Era Goerli Testnet",
- "l2WalletUrl": "https://goerli.staging-portal.zksync.dev/",
- "maintenance": false,
- "name": "goerli",
- "published": true,
- "rpcUrl": "https://testnet.era.zksync.dev"
+ "rpcUrl": "http://localhost:3050",
+ "baseTokenAddress": "0x000000000000000000000000000000000000800A"
},
{
"apiUrl": "https://block-explorer-api.sepolia.zksync.dev",
"verificationApiUrl": "https://explorer.sepolia.era.zksync.dev",
- "bridgeUrl": "https://staging.goerli.bridge.zksync.dev",
+ "bridgeUrl": "https://portal.zksync.io/bridge/?network=sepolia",
"hostnames": [],
"icon": "/images/icons/zksync-arrows.svg",
"l1ExplorerUrl": "https://sepolia.etherscan.io",
"l2ChainId": 300,
"l2NetworkName": "zkSync Era Sepolia Testnet",
- "l2WalletUrl": "https://staging-portal.zksync.dev/?network=era-boojnet",
"maintenance": false,
"name": "sepolia",
"published": true,
- "rpcUrl": "https://sepolia.era.zksync.dev"
+ "rpcUrl": "https://sepolia.era.zksync.dev",
+ "baseTokenAddress": "0x000000000000000000000000000000000000800a"
},
{
"apiUrl": "https://block-explorer-api.stage.zksync.dev",
@@ -57,16 +40,16 @@
"l1ExplorerUrl": "https://goerli.etherscan.io",
"l2ChainId": 270,
"l2NetworkName": "Goerli (Stage2)",
- "l2WalletUrl": "https://goerli-beta.staging-portal.zksync.dev/",
"maintenance": false,
"name": "goerli-beta",
"published": true,
- "rpcUrl": "https://z2-dev-api.zksync.dev"
+ "rpcUrl": "https://z2-dev-api.zksync.dev",
+ "baseTokenAddress": "0x000000000000000000000000000000000000800a"
},
{
"apiUrl": "https://block-explorer-api.mainnet.zksync.io",
"verificationApiUrl": "https://zksync2-mainnet-explorer.zksync.io",
- "bridgeUrl": "https://staging.bridge.zksync.dev",
+ "bridgeUrl": "https://portal.zksync.io/bridge/?network=mainnet",
"hostnames": [
"https://staging-scan-v2.zksync.dev"
],
@@ -74,12 +57,12 @@
"l1ExplorerUrl": "https://etherscan.io",
"l2ChainId": 324,
"l2NetworkName": "zkSync Era Mainnet",
- "l2WalletUrl": "https://staging-portal.zksync.dev/",
"maintenance": false,
"name": "mainnet",
"published": true,
"rpcUrl": "https://mainnet.era.zksync.io",
- "tokensMinLiquidity": 0
+ "tokensMinLiquidity": 0,
+ "baseTokenAddress": "0x000000000000000000000000000000000000800a"
}
]
}
diff --git a/packages/app/src/configs/index.ts b/packages/app/src/configs/index.ts
index edee2ceeae..8e9d98fd8f 100644
--- a/packages/app/src/configs/index.ts
+++ b/packages/app/src/configs/index.ts
@@ -6,13 +6,13 @@ export type NetworkConfig = {
rpcUrl: string;
bridgeUrl?: string;
l2NetworkName: string;
- l2WalletUrl: string;
- l2ChainId: 270 | 280 | 324;
+ l2ChainId: 270 | 300 | 324;
l1ExplorerUrl?: string;
maintenance: boolean;
published: boolean;
hostnames: string[];
tokensMinLiquidity?: number;
+ baseTokenAddress: string;
};
export type EnvironmentConfig = {
diff --git a/packages/app/src/configs/local.config.json b/packages/app/src/configs/local.config.json
index afd0e82298..e0e91d9a19 100644
--- a/packages/app/src/configs/local.config.json
+++ b/packages/app/src/configs/local.config.json
@@ -9,11 +9,11 @@
"icon": "/images/icons/zksync-arrows.svg",
"l2ChainId": 270,
"l2NetworkName": "Local",
- "l2WalletUrl": "http://localhost:3000",
"maintenance": false,
"name": "local",
"published": true,
- "rpcUrl": "http://localhost:3050"
+ "rpcUrl": "http://localhost:3050",
+ "baseTokenAddress": "0x000000000000000000000000000000000000800A"
}
]
}
diff --git a/packages/app/src/configs/production.config.json b/packages/app/src/configs/production.config.json
index d2a5f9eab7..b4b2f7a9ee 100644
--- a/packages/app/src/configs/production.config.json
+++ b/packages/app/src/configs/production.config.json
@@ -1,26 +1,9 @@
{
"networks": [
- {
- "apiUrl": "https://block-explorer-api.testnets.zksync.dev",
- "verificationApiUrl": "https://zksync2-testnet-explorer.zksync.dev",
- "bridgeUrl": "https://goerli.bridge.zksync.io",
- "hostnames": [
- "https://goerli.explorer.zksync.io"
- ],
- "icon": "/images/icons/zksync-arrows.svg",
- "l1ExplorerUrl": "https://goerli.etherscan.io",
- "l2ChainId": 280,
- "l2NetworkName": "zkSync Era Goerli Testnet",
- "l2WalletUrl": "https://goerli.portal.zksync.io/",
- "maintenance": false,
- "name": "goerli",
- "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",
+ "bridgeUrl": "https://portal.zksync.io/bridge/?network=sepolia",
"hostnames": [
"https://sepolia.explorer.zksync.io"
],
@@ -28,16 +11,16 @@
"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"
+ "rpcUrl": "https://sepolia.era.zksync.dev",
+ "baseTokenAddress": "0x000000000000000000000000000000000000800a"
},
{
"apiUrl": "https://block-explorer-api.mainnet.zksync.io",
"verificationApiUrl": "https://zksync2-mainnet-explorer.zksync.io",
- "bridgeUrl": "https://bridge.zksync.io",
+ "bridgeUrl": "https://portal.zksync.io/bridge/?network=mainnet",
"hostnames": [
"https://explorer.zksync.io"
],
@@ -45,12 +28,12 @@
"l1ExplorerUrl": "https://etherscan.io",
"l2ChainId": 324,
"l2NetworkName": "zkSync Era Mainnet",
- "l2WalletUrl": "https://portal.zksync.io/",
"maintenance": false,
"name": "mainnet",
"published": true,
"rpcUrl": "https://mainnet.era.zksync.io",
- "tokensMinLiquidity": 0
+ "tokensMinLiquidity": 0,
+ "baseTokenAddress": "0x000000000000000000000000000000000000800a"
}
]
}
diff --git a/packages/app/src/configs/staging.config.json b/packages/app/src/configs/staging.config.json
index fe9e8d4c27..18bf193ac8 100644
--- a/packages/app/src/configs/staging.config.json
+++ b/packages/app/src/configs/staging.config.json
@@ -1,26 +1,9 @@
{
"networks": [
- {
- "apiUrl": "https://block-explorer-api.testnets.zksync.dev",
- "verificationApiUrl": "https://zksync2-testnet-explorer.zksync.dev",
- "bridgeUrl": "https://staging.goerli.bridge.zksync.dev",
- "hostnames": [
- "https://goerli.staging-scan-v2.zksync.dev"
- ],
- "icon": "/images/icons/zksync-arrows.svg",
- "l1ExplorerUrl": "https://goerli.etherscan.io",
- "l2ChainId": 280,
- "l2NetworkName": "zkSync Era Goerli Testnet",
- "l2WalletUrl": "https://goerli.staging-portal.zksync.dev/",
- "maintenance": false,
- "name": "goerli",
- "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://staging.goerli.bridge.zksync.dev",
+ "bridgeUrl": "https://portal.zksync.io/bridge/?network=sepolia",
"hostnames": [
"https://sepolia.staging-scan-v2.zksync.dev"
],
@@ -28,11 +11,11 @@
"l1ExplorerUrl": "https://sepolia.etherscan.io",
"l2ChainId": 300,
"l2NetworkName": "zkSync Era Sepolia Testnet",
- "l2WalletUrl": "https://staging-portal.zksync.dev/?network=era-boojnet",
"maintenance": false,
"name": "sepolia",
"published": true,
- "rpcUrl": "https://sepolia.era.zksync.dev"
+ "rpcUrl": "https://sepolia.era.zksync.dev",
+ "baseTokenAddress": "0x000000000000000000000000000000000000800a"
},
{
"apiUrl": "https://block-explorer-api.stage.zksync.dev",
@@ -44,16 +27,16 @@
"l1ExplorerUrl": "https://goerli.etherscan.io",
"l2ChainId": 270,
"l2NetworkName": "Goerli (Stage2)",
- "l2WalletUrl": "https://goerli-beta.staging-portal.zksync.dev/",
"maintenance": false,
"name": "goerli-beta",
"published": true,
- "rpcUrl": "https://z2-dev-api.zksync.dev"
+ "rpcUrl": "https://z2-dev-api.zksync.dev",
+ "baseTokenAddress": "0x000000000000000000000000000000000000800a"
},
{
"apiUrl": "https://block-explorer-api.mainnet.zksync.io",
"verificationApiUrl": "https://zksync2-mainnet-explorer.zksync.io",
- "bridgeUrl": "https://staging.bridge.zksync.dev",
+ "bridgeUrl": "https://portal.zksync.io/bridge/?network=mainnet",
"hostnames": [
"https://staging-scan-v2.zksync.dev"
],
@@ -61,12 +44,12 @@
"l1ExplorerUrl": "https://etherscan.io",
"l2ChainId": 324,
"l2NetworkName": "zkSync Era Mainnet",
- "l2WalletUrl": "https://staging-portal.zksync.dev/",
"maintenance": false,
"name": "mainnet",
"published": true,
"rpcUrl": "https://mainnet.era.zksync.io",
- "tokensMinLiquidity": 0
+ "tokensMinLiquidity": 0,
+ "baseTokenAddress": "0x000000000000000000000000000000000000800a"
}
]
}
diff --git a/packages/app/src/locales/en.json b/packages/app/src/locales/en.json
index 662856dcc6..2e92691d33 100644
--- a/packages/app/src/locales/en.json
+++ b/packages/app/src/locales/en.json
@@ -40,9 +40,9 @@
"blockNumberTooltip": "Block height, indicates the length of the blockchain, increases after the addition of the new block",
"blockSize": "Block Size",
"blockSizeTooltip": "Number of transactions inside the block",
- "rootHash": "Root hash",
- "rootHashTooltip": "State root hash obtained after this block execution",
- "noRootHashYet": "No root hash yet",
+ "blockHash": "Block hash",
+ "blockHashTooltip": "The hash of the current block",
+ "noBlockHashYet": "No block hash yet",
"commitTxHash": "Commit tx hash",
"commitTxHashTooltip": "Hash of the L1 transaction sent to the smart contract to commit the block",
"committedAt": "Committed",
@@ -281,7 +281,6 @@
"header": {
"nav": {
"blockExplorer": "Block Explorer",
- "portal": "Portal",
"documentation": "Documentation",
"tools": "Tools",
"apiDocs": "API Documentation",
@@ -458,7 +457,8 @@
"validation": {
"required": "Solc version is required"
},
- "error": "Unable to get list of supported Solc versions"
+ "error": "Unable to get list of supported Solc versions",
+ "zkVM": "zkVM"
},
"zksolcVersion": {
"label": "Zksolc Version",
diff --git a/packages/app/src/locales/uk.json b/packages/app/src/locales/uk.json
index b37af6f27c..ab2f0a8ad6 100644
--- a/packages/app/src/locales/uk.json
+++ b/packages/app/src/locales/uk.json
@@ -26,8 +26,8 @@
"timestamp": "Створено",
"blockNumber": "Номер",
"blockSize": "Розмір",
- "rootHash": "Кореневий хеш",
- "noRootHashYet": "Ще немає кореневого хешу",
+ "blockHash": "Хеш блоку",
+ "noBlockHashYet": "Ще немає хешу блоку",
"committedAt": "Затверджений",
"commitTxHash": "Завірений хеш",
"notYetCommitted": "Ще не затверджено",
@@ -152,7 +152,6 @@
"contractVerification": "Верифікація Смарт контракту",
"debugger": "zkEVM Налагоджувач",
"blockExplorer": "Провідник",
- "portal": "Портал",
"documentation": "Документація"
}
},
diff --git a/packages/app/src/utils/constants.ts b/packages/app/src/utils/constants.ts
index 834827ee7b..7048c3c0ef 100644
--- a/packages/app/src/utils/constants.ts
+++ b/packages/app/src/utils/constants.ts
@@ -1,7 +1,3 @@
-import { checksumAddress } from "./formatters";
-
-export const ETH_TOKEN_L2_ADDRESS = checksumAddress("0x000000000000000000000000000000000000800A");
-
export const PROXY_CONTRACT_IMPLEMENTATION_ABI = [
{
inputs: [],
diff --git a/packages/app/src/views/ContractVerificationView.vue b/packages/app/src/views/ContractVerificationView.vue
index f6581a0b5a..2bc9272bfa 100644
--- a/packages/app/src/views/ContractVerificationView.vue
+++ b/packages/app/src/views/ContractVerificationView.vue
@@ -16,7 +16,7 @@
-
+
{{ t("contractVerification.resources.links.hardhat") }}
@@ -84,12 +84,13 @@
class="label-inline-block"
>
{{ t(`contractVerification.form.${selectedZkCompiler.name}Version.details`) }}
-
+
+ {{ t("contractVerification.form.solcVersion.zkVM") }}
[
},
]);
+const isZkVMSolcCompiler = ref(false);
const selectedCompilationType = ref(CompilationTypeOptionsEnum.soliditySingleFile);
const isSingleFile = computed(() =>
[CompilationTypeOptionsEnum.soliditySingleFile, CompilationTypeOptionsEnum.vyperSingleFile].includes(
@@ -350,7 +365,16 @@ const selectedZkCompiler = computed(() => {
});
const selectedCompiler = computed(() => {
const compiler = compilerTypeMap[selectedCompilationType.value].compiler;
- return compilerVersions.value[compiler];
+ const compilerInfo = compilerVersions.value[compiler];
+ if (compiler === CompilerEnum.solc) {
+ return {
+ ...compilerInfo,
+ versions: compilerInfo.versions?.filter((version) =>
+ isZkVMSolcCompiler.value ? version.startsWith(zkVMVersionPrefix) : !version.startsWith(zkVMVersionPrefix)
+ ),
+ };
+ }
+ return compilerInfo;
});
const selectedZkCompilerVersion = ref(
selectedZkCompiler.value.versions[selectedZkCompiler.value.versions.length - 1] || ""
@@ -496,6 +520,10 @@ const v$ = useVuelidate(
form
);
+function onZkVMSelectionChanged() {
+ selectedCompilerVersion.value = selectedCompiler.value.versions[0] || "";
+}
+
function onCompilationTypeChange() {
selectedZkCompilerVersion.value = selectedZkCompiler.value.versions[0] || "";
selectedCompilerVersion.value = selectedCompiler.value.versions[0] || "";
diff --git a/packages/app/tests/components/AddressLink.spec.ts b/packages/app/tests/components/AddressLink.spec.ts
index cde2801f78..f35af187c4 100644
--- a/packages/app/tests/components/AddressLink.spec.ts
+++ b/packages/app/tests/components/AddressLink.spec.ts
@@ -6,7 +6,7 @@ import { mount, RouterLinkStub } from "@vue/test-utils";
import AddressLink from "@/components/AddressLink.vue";
-const l1ExplorerUrlMock = vi.fn((): string | null => "https://goerli.etherscan.io");
+const l1ExplorerUrlMock = vi.fn((): string | null => "https://sepolia.etherscan.io/");
vi.mock("@/composables/useContext", () => {
return {
default: () => ({
@@ -66,7 +66,7 @@ describe("Address Link", () => {
},
});
expect(wrapper.find("a").attributes().href).toBe(
- "https://goerli.etherscan.io/address/0x0000000000000000000000000000000000000001"
+ "https://sepolia.etherscan.io//address/0x0000000000000000000000000000000000000001"
);
});
describe("when L1 explorer url is not set", () => {
diff --git a/packages/app/tests/components/FeeData.spec.ts b/packages/app/tests/components/FeeData.spec.ts
index 94e9e713e8..d89e1cb2d7 100644
--- a/packages/app/tests/components/FeeData.spec.ts
+++ b/packages/app/tests/components/FeeData.spec.ts
@@ -46,7 +46,7 @@ const feeData = {
{
tokenInfo: {
decimals: 18,
- l1Address: "0x0000000000000000000000000000000000000000",
+ l1Address: "0x0000000000000000000000000000000000000001",
l2Address: "0x0000000000000000000000000000000000000000",
symbol: "ETH",
name: "Ether",
@@ -189,7 +189,7 @@ describe("FeeToken", () => {
await fireEvent.click(container.querySelector(".toggle-button")!);
const link = container.querySelector(".refunded-link");
expect(link?.getAttribute("href")).toBe(
- "https://era.zksync.io/docs/dev/developer-guides/transactions/fee-model.html#refunds"
+ "https://docs.zksync.io/build/developer-reference/fee-model.html#refunds"
);
expect(link?.getAttribute("target")).toBe("_blank");
expect(link?.textContent).toBe("Why am I being refunded?");
@@ -240,14 +240,14 @@ describe("FeeToken", () => {
const refundedLink = container.querySelector(".refunded-link");
expect(refundedLink?.getAttribute("href")).toBe(
- "https://era.zksync.io/docs/dev/developer-guides/transactions/fee-model.html#refunds"
+ "https://docs.zksync.io/build/developer-reference/fee-model.html#refunds"
);
expect(refundedLink?.getAttribute("target")).toBe("_blank");
expect(refundedLink?.textContent).toBe("Why is Paymaster being refunded?");
const paymasterLink = container.querySelector(".paymaster-link");
expect(paymasterLink?.getAttribute("href")).toBe(
- "https://era.zksync.io/docs/reference/concepts/account-abstraction.html#paymasters"
+ "https://docs.zksync.io/build/developer-reference/account-abstraction.html#paymasters"
);
expect(paymasterLink?.getAttribute("target")).toBe("_blank");
expect(paymasterLink?.textContent).toBe("What is Paymaster?");
diff --git a/packages/app/tests/components/NetworkStats.spec.ts b/packages/app/tests/components/NetworkStats.spec.ts
index cac0a5393c..12d01fee54 100644
--- a/packages/app/tests/components/NetworkStats.spec.ts
+++ b/packages/app/tests/components/NetworkStats.spec.ts
@@ -10,7 +10,7 @@ import NetworkStats from "@/components/NetworkStats.vue";
import enUS from "@/locales/en.json";
-const currentNetworkMock = vi.fn(() => "goerli");
+const currentNetworkMock = vi.fn(() => "testnet");
vi.mock("@/composables/useContext", () => {
return {
diff --git a/packages/app/tests/components/NetworkSwitch.spec.ts b/packages/app/tests/components/NetworkSwitch.spec.ts
index f9de37c50b..fd3b89c496 100644
--- a/packages/app/tests/components/NetworkSwitch.spec.ts
+++ b/packages/app/tests/components/NetworkSwitch.spec.ts
@@ -5,7 +5,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import { fireEvent, render } from "@testing-library/vue";
-import { GOERLI_BETA_NETWORK, GOERLI_NETWORK } from "./../mocks";
+import { TESTNET_BETA_NETWORK, TESTNET_NETWORK } from "./../mocks";
import NetworkSwitch from "@/components/NetworkSwitch.vue";
@@ -29,13 +29,13 @@ vi.mock("@/utils/helpers", () => ({
describe("NetworkSwitch:", () => {
beforeEach(() => {
vi.spyOn(useEnvironmentConfig, "default").mockReturnValue({
- networks: computed(() => [GOERLI_NETWORK, GOERLI_BETA_NETWORK]),
+ networks: computed(() => [TESTNET_NETWORK, TESTNET_BETA_NETWORK]),
});
});
it("renders listbox button with selected option", async () => {
const { container } = render(NetworkSwitch);
- expect(container.querySelector(".network-item-label")?.textContent).toBe("Goerli");
+ expect(container.querySelector(".network-item-label")?.textContent).toBe("Testnet");
});
it("renders list of networks when button is clicked", async () => {
@@ -45,14 +45,14 @@ describe("NetworkSwitch:", () => {
await fireEvent.click(container.querySelector(".toggle-button")!);
const options = container.querySelectorAll(".network-list-item-container > *");
- expect(options[0].getAttribute("href")).toBe("?network=goerli");
- expect(options[0].textContent).toBe("Goerli");
+ expect(options[0].getAttribute("href")).toBe("?network=testnet");
+ expect(options[0].textContent).toBe("Testnet");
expect(options[0].tagName).toBe("LABEL");
- expect(options[0].querySelector("img")?.getAttribute("alt")).toBe("Goerli logo");
- expect(options[1].getAttribute("href")).toBe("https://goerli-beta.staging-scan-v2.zksync.dev/");
- expect(options[1].textContent).toBe("Goerli Beta");
+ expect(options[0].querySelector("img")?.getAttribute("alt")).toBe("Testnet logo");
+ expect(options[1].getAttribute("href")).toBe("https://testnet-beta.staging-scan-v2.zksync.dev/");
+ expect(options[1].textContent).toBe("Testnet Beta");
expect(options[1].tagName).toBe("A");
- expect(options[1].querySelector("img")?.getAttribute("alt")).toBe("Goerli Beta logo");
+ expect(options[1].querySelector("img")?.getAttribute("alt")).toBe("Testnet Beta logo");
});
it("uses relative url schema for networks when on localhost", async () => {
@@ -64,11 +64,11 @@ describe("NetworkSwitch:", () => {
await fireEvent.click(container.querySelector(".toggle-button")!);
const options = container.querySelectorAll(".network-list-item-container > *");
- expect(options[0].getAttribute("href")).toBe("?network=goerli");
- expect(options[0].textContent).toBe("Goerli");
+ expect(options[0].getAttribute("href")).toBe("?network=testnet");
+ expect(options[0].textContent).toBe("Testnet");
expect(options[0].tagName).toBe("LABEL");
- expect(options[1].getAttribute("href")).toBe("?network=goerli-beta");
- expect(options[1].textContent).toBe("Goerli Beta");
+ expect(options[1].getAttribute("href")).toBe("?network=testnet-beta");
+ expect(options[1].textContent).toBe("Testnet Beta");
expect(options[1].tagName).toBe("A");
});
@@ -80,11 +80,11 @@ describe("NetworkSwitch:", () => {
await fireEvent.click(container.querySelector(".toggle-button")!);
const options = container.querySelectorAll(".network-list-item-container > *");
- expect(options[0].getAttribute("href")).toBe("?network=goerli");
- expect(options[0].textContent).toBe("Goerli");
+ expect(options[0].getAttribute("href")).toBe("?network=testnet");
+ expect(options[0].textContent).toBe("Testnet");
expect(options[0].tagName).toBe("LABEL");
- expect(options[1].getAttribute("href")).toBe("?network=goerli-beta");
- expect(options[1].textContent).toBe("Goerli Beta");
+ expect(options[1].getAttribute("href")).toBe("?network=testnet-beta");
+ expect(options[1].textContent).toBe("Testnet Beta");
expect(options[1].tagName).toBe("A");
});
});
diff --git a/packages/app/tests/components/TheFooter.spec.ts b/packages/app/tests/components/TheFooter.spec.ts
index b799dc76a7..8ecf1a640d 100644
--- a/packages/app/tests/components/TheFooter.spec.ts
+++ b/packages/app/tests/components/TheFooter.spec.ts
@@ -24,7 +24,9 @@ describe("TheFooter:", () => {
},
});
const links = wrapper.findAll("a");
- expect(links[0].attributes("href")).toBe("https://era.zksync.io/docs/dev/");
+ expect(links[0].attributes("href")).toBe(
+ "https://docs.zksync.io/build/tooling/block-explorer/getting-started.html"
+ );
expect(links[1].attributes("href")).toBe("https://zksync.io/terms");
expect(links[2].attributes("href")).toBe("https://zksync.io/contact");
});
diff --git a/packages/app/tests/components/TheHeader.spec.ts b/packages/app/tests/components/TheHeader.spec.ts
index 693806228c..a5367a5832 100644
--- a/packages/app/tests/components/TheHeader.spec.ts
+++ b/packages/app/tests/components/TheHeader.spec.ts
@@ -23,7 +23,6 @@ vi.mock("@/composables/useContext", () => {
default: () => ({
currentNetwork: computed(() => ({
maintenance: maintenanceMock(),
- l2WalletUrl: "https://portal.zksync.io/",
bridgeUrl: "https://bridge.zksync.io/",
apiUrl: "https://api-url",
})),
@@ -60,10 +59,10 @@ describe("TheHeader:", () => {
const toolsLinks = dropdown[1].findAll("a");
expect(toolsLinks[0].attributes("href")).toBe("https://api-url/docs");
expect(toolsLinksRouter[0].props().to.name).toBe("contract-verification");
- expect(toolsLinks[2].attributes("href")).toBe("https://portal.zksync.io/");
+ expect(toolsLinks[2].attributes("href")).toBe("https://bridge.zksync.io/");
expect(wrapper.findAll(".navigation-container > .navigation-link")[0].attributes("href")).toBe(
- "https://era.zksync.io/docs/dev/"
+ "https://docs.zksync.io/build/tooling/block-explorer/getting-started.html"
);
});
it("renders social links", () => {
diff --git a/packages/app/tests/components/balances/Table.spec.ts b/packages/app/tests/components/balances/Table.spec.ts
index b28cdc473a..2d4cc33f7c 100644
--- a/packages/app/tests/components/balances/Table.spec.ts
+++ b/packages/app/tests/components/balances/Table.spec.ts
@@ -20,7 +20,7 @@ const tokenLINK: Api.Response.Token = {
l1Address: "0x514910771AF9Ca656af840dff83E8264EcF986CA",
l2Address: "0x514910771AF9Ca656af840dff83E8264EcF986CA",
symbol: "LINK",
- name: "ChainLink Token (goerli)",
+ name: "ChainLink Token (testnet)",
decimals: 18,
usdPrice: 100,
liquidity: 100000000,
diff --git a/packages/app/tests/components/batches/InfoTable.spec.ts b/packages/app/tests/components/batches/InfoTable.spec.ts
index db21332a81..37404f28b2 100644
--- a/packages/app/tests/components/batches/InfoTable.spec.ts
+++ b/packages/app/tests/components/batches/InfoTable.spec.ts
@@ -14,7 +14,7 @@ import type { BatchDetails } from "@/composables/useBatch";
import { localDateFromISOString } from "@/utils/helpers";
-const l1ExplorerUrlMock = vi.fn((): string | null => "https://goerli.etherscan.io");
+const l1ExplorerUrlMock = vi.fn((): string | null => "https://sepolia.etherscan.io/");
vi.mock("@/composables/useContext", () => {
return {
default: () => ({
@@ -175,13 +175,13 @@ describe("InfoTable:", () => {
});
expect(wrapper.findAll("a")[0].attributes("href")).toEqual(
- "https://goerli.etherscan.io/tx/0x0ab34d8523b67f80783305760a2989ffe6ab205621813db5420a3012845f5ac7"
+ "https://sepolia.etherscan.io//tx/0x0ab34d8523b67f80783305760a2989ffe6ab205621813db5420a3012845f5ac7"
);
expect(wrapper.findAll("a")[1].attributes("href")).toEqual(
- "https://goerli.etherscan.io/tx/0x87c5c5bf78100d88766101f13ec78d3b3356929556ee971cfacb6fe2a53b210a"
+ "https://sepolia.etherscan.io//tx/0x87c5c5bf78100d88766101f13ec78d3b3356929556ee971cfacb6fe2a53b210a"
);
expect(wrapper.findAll("a")[2].attributes("href")).toEqual(
- "https://goerli.etherscan.io/tx/0x57c44d7c183633f81bfa155bd30e68a94e3ff12c1e6265a4b5e06b6d4a7a1fa8"
+ "https://sepolia.etherscan.io//tx/0x57c44d7c183633f81bfa155bd30e68a94e3ff12c1e6265a4b5e06b6d4a7a1fa8"
);
wrapper.unmount();
});
diff --git a/packages/app/tests/components/blocks/InfoTable.spec.ts b/packages/app/tests/components/blocks/InfoTable.spec.ts
index c2422369e7..9bbc286390 100644
--- a/packages/app/tests/components/blocks/InfoTable.spec.ts
+++ b/packages/app/tests/components/blocks/InfoTable.spec.ts
@@ -15,7 +15,7 @@ import type { Block } from "@/composables/useBlock";
import { localDateFromISOString } from "@/utils/helpers";
-const l1ExplorerUrlMock = vi.fn((): string | null => "https://goerli.etherscan.io");
+const l1ExplorerUrlMock = vi.fn((): string | null => "https://sepolia.etherscan.io/");
vi.mock("@/composables/useContext", () => {
return {
default: () => ({
@@ -81,10 +81,10 @@ describe("InfoTable:", () => {
expect(batch[0].find(".block-info-field-label").text()).toBe(i18n.global.t("blocks.table.batch"));
expect(batch[0].findComponent(InfoTooltip).text()).toBe(i18n.global.t("blocks.table.batchTooltip"));
expect(batch[1].findComponent(RouterLinkStub).text()).toBe("1");
- const rootHash = rowArray[4].findAll("td");
- expect(rootHash[0].find(".block-info-field-label").text()).toBe(i18n.global.t("blocks.table.rootHash"));
- expect(rootHash[0].findComponent(InfoTooltip).text()).toBe(i18n.global.t("blocks.table.rootHashTooltip"));
- expect(rootHash[1].text()).toBe("0xcd7533748f8f0c8f406f366e83d5e92d174845405418745d0f7228b85025cd6e");
+ const blockHash = rowArray[4].findAll("td");
+ expect(blockHash[0].find(".block-info-field-label").text()).toBe(i18n.global.t("blocks.table.blockHash"));
+ expect(blockHash[0].findComponent(InfoTooltip).text()).toBe(i18n.global.t("blocks.table.blockHashTooltip"));
+ expect(blockHash[1].text()).toBe("0xcd7533748f8f0c8f406f366e83d5e92d174845405418745d0f7228b85025cd6e");
const timestamp = rowArray[5].findAll("td");
expect(timestamp[0].find(".block-info-field-label").text()).toBe(i18n.global.t("blocks.table.timestamp"));
expect(timestamp[0].findComponent(InfoTooltip).text()).toBe(i18n.global.t("blocks.table.timestampTooltip"));
@@ -184,7 +184,7 @@ describe("InfoTable:", () => {
},
});
expect(wrapper.findAll("a")[0].attributes("href")).toEqual(
- "https://goerli.etherscan.io/tx/0x5b5a05691d974803f5f095c1b918d2dd19152ed0a9de506d545c96df6cb9cac2"
+ "https://sepolia.etherscan.io//tx/0x5b5a05691d974803f5f095c1b918d2dd19152ed0a9de506d545c96df6cb9cac2"
);
});
it("renders proveTxHash url properly", () => {
@@ -215,7 +215,7 @@ describe("InfoTable:", () => {
},
});
expect(wrapper.findAll("a")[1].attributes("href")).toEqual(
- "https://goerli.etherscan.io/tx/0xfb3532f4c38c2eaf78248da64cf80a354429d58204761d6ea6439391043f6fa9"
+ "https://sepolia.etherscan.io//tx/0xfb3532f4c38c2eaf78248da64cf80a354429d58204761d6ea6439391043f6fa9"
);
});
it("renders executeTxHash url properly", () => {
@@ -246,7 +246,7 @@ describe("InfoTable:", () => {
},
});
expect(wrapper.findAll("a")[2].attributes("href")).toEqual(
- "https://goerli.etherscan.io/tx/0x8d1a78d1da5aba1d0755ec9dbcba938f3920681d2a3d4d374ef265a50858f364"
+ "https://sepolia.etherscan.io//tx/0x8d1a78d1da5aba1d0755ec9dbcba938f3920681d2a3d4d374ef265a50858f364"
);
});
it("renders batch number as text with tooltip when batch is not sealed yet", () => {
diff --git a/packages/app/tests/components/common/CheckBoxInput.spec.ts b/packages/app/tests/components/common/CheckBoxInput.spec.ts
new file mode 100644
index 0000000000..f51e052269
--- /dev/null
+++ b/packages/app/tests/components/common/CheckBoxInput.spec.ts
@@ -0,0 +1,39 @@
+import { describe, expect, it } from "vitest";
+
+import { render } from "@testing-library/vue";
+
+import CheckBoxInput from "@/components/common/CheckBoxInput.vue";
+
+describe("CheckBoxInput", () => {
+ it("renders default slot", () => {
+ const { container } = render(CheckBoxInput, {
+ slots: {
+ default: {
+ template: "CheckBox Input",
+ },
+ },
+ props: {
+ modelValue: true,
+ },
+ });
+ expect(container.textContent).toBe("CheckBox Input");
+ });
+ it("renders checked state correctly", async () => {
+ const { container } = render(CheckBoxInput, {
+ props: {
+ modelValue: true,
+ },
+ });
+ expect(container.querySelector(".checkbox-input-container")!.classList.contains("checked")).toBe(true);
+ expect(container.querySelector(".checkbox-input-container input")?.checked).toBe(true);
+ });
+ it("renders unchecked state correctly", async () => {
+ const { container } = render(CheckBoxInput, {
+ props: {
+ modelValue: false,
+ },
+ });
+ expect(container.querySelector(".checkbox-input-container")!.classList.contains("checked")).toBe(false);
+ expect(container.querySelector(".checkbox-input-container input")?.checked).toBe(false);
+ });
+});
diff --git a/packages/app/tests/components/transactions/GeneralInfo.spec.ts b/packages/app/tests/components/transactions/GeneralInfo.spec.ts
index 63450cbd36..a5554ce777 100644
--- a/packages/app/tests/components/transactions/GeneralInfo.spec.ts
+++ b/packages/app/tests/components/transactions/GeneralInfo.spec.ts
@@ -49,7 +49,7 @@ const transaction: TransactionItem = {
type: "refund",
tokenInfo: {
address: "0x000000000000000000000000000000000000800A",
- l1Address: "0x0000000000000000000000000000000000000000",
+ l1Address: "0x0000000000000000000000000000000000000001",
l2Address: "0x000000000000000000000000000000000000800A",
symbol: "ETH",
name: "Ether",
@@ -65,7 +65,7 @@ const transaction: TransactionItem = {
type: "refund",
tokenInfo: {
address: "0x000000000000000000000000000000000000800A",
- l1Address: "0x0000000000000000000000000000000000000000",
+ l1Address: "0x0000000000000000000000000000000000000001",
l2Address: "0x000000000000000000000000000000000000800A",
symbol: "ETH",
name: "Ether",
diff --git a/packages/app/tests/components/transactions/Status.spec.ts b/packages/app/tests/components/transactions/Status.spec.ts
index 9fe7d16e27..16a564809e 100644
--- a/packages/app/tests/components/transactions/Status.spec.ts
+++ b/packages/app/tests/components/transactions/Status.spec.ts
@@ -17,7 +17,7 @@ import $testId from "@/plugins/testId";
const { currentNetwork } = useContext();
-const l1ExplorerUrlMock = vi.fn((): string | null => "https://goerli.etherscan.io");
+const l1ExplorerUrlMock = vi.fn((): string | null => "https://sepolia.etherscan.io/");
vi.mock("@/composables/useContext", () => {
return {
default: () => ({
diff --git a/packages/app/tests/components/transactions/TransferInfo.spec.ts b/packages/app/tests/components/transactions/TransferInfo.spec.ts
index 006443a079..02fb9ead41 100644
--- a/packages/app/tests/components/transactions/TransferInfo.spec.ts
+++ b/packages/app/tests/components/transactions/TransferInfo.spec.ts
@@ -16,7 +16,7 @@ vi.mock("ohmyfetch", () => {
};
});
-const l1ExplorerUrlMock = vi.fn((): string | null => "https://goerli.etherscan.io");
+const l1ExplorerUrlMock = vi.fn((): string | null => "https://sepolia.etherscan.io/");
vi.mock("@/composables/useContext", () => {
return {
default: () => ({
@@ -74,7 +74,7 @@ describe("TransferInfo:", () => {
});
expect(wrapper.find("span")?.text()).toBe("From");
expect(wrapper.findAll("a")[0].attributes("href")).toEqual(
- "https://goerli.etherscan.io/address/0x6c10d9c1744f149d4b17660e14faa247964749c7"
+ "https://sepolia.etherscan.io//address/0x6c10d9c1744f149d4b17660e14faa247964749c7"
);
expect(wrapper.findAll("a")[0].text()).toEqual("0x6c10d9c1744...49c7");
expect(wrapper.find(".copy-btn")).toBeTruthy();
diff --git a/packages/app/tests/components/transactions/TransferTableCell.spec.ts b/packages/app/tests/components/transactions/TransferTableCell.spec.ts
index 0f7b703484..41c453ed17 100644
--- a/packages/app/tests/components/transactions/TransferTableCell.spec.ts
+++ b/packages/app/tests/components/transactions/TransferTableCell.spec.ts
@@ -38,7 +38,7 @@ describe("TransferTableCell:", () => {
decimals: 18,
l1Address: "0x63bfb2118771bd0da7a6936667a7bb705a06c1ba",
l2Address: "0x4732c03b2cf6ede46500e799de79a15df44929eb",
- name: "ChainLink Token (goerli)",
+ name: "ChainLink Token (testnet)",
symbol: "LINK",
usdPrice: 1,
},
diff --git a/packages/app/tests/components/transfers/Table.spec.ts b/packages/app/tests/components/transfers/Table.spec.ts
index 0ae976e6b1..37a1ea76b0 100644
--- a/packages/app/tests/components/transfers/Table.spec.ts
+++ b/packages/app/tests/components/transfers/Table.spec.ts
@@ -50,7 +50,7 @@ const transfer = {
l2Address: "0x0faF6df7054946141266420b43783387A78d82A9",
l1Address: "0xd35CCeEAD182dcee0F148EbaC9447DA2c4D449c4",
symbol: "USDC",
- name: "USD Coin (goerli)",
+ name: "USD Coin (testnet)",
decimals: 6,
usdPrice: 1,
},
diff --git a/packages/app/tests/composables/useContext.spec.ts b/packages/app/tests/composables/useContext.spec.ts
index 5e106c3146..8e32ca0054 100644
--- a/packages/app/tests/composables/useContext.spec.ts
+++ b/packages/app/tests/composables/useContext.spec.ts
@@ -11,7 +11,7 @@ vi.mock("@/utils/helpers", () => ({
getWindowLocation: () => location,
}));
-import { GOERLI_BETA_NETWORK, GOERLI_NETWORK } from "../mocks";
+import { TESTNET_BETA_NETWORK, TESTNET_NETWORK } from "../mocks";
import * as useContext from "@/composables/useContext";
import * as useEnvironmentConfig from "@/composables/useEnvironmentConfig";
@@ -29,10 +29,10 @@ describe("useContext:", () => {
describe("networks:", () => {
it("returns environment networks", () => {
const mockEnvironmentConfig = vi.spyOn(useEnvironmentConfig, "default").mockReturnValue({
- networks: computed(() => [GOERLI_NETWORK, GOERLI_BETA_NETWORK]),
+ networks: computed(() => [TESTNET_NETWORK, TESTNET_BETA_NETWORK]),
});
const context = useContext.default();
- expect(context.networks.value).toEqual([GOERLI_NETWORK, GOERLI_BETA_NETWORK]);
+ expect(context.networks.value).toEqual([TESTNET_NETWORK, TESTNET_BETA_NETWORK]);
mockEnvironmentConfig.mockRestore();
});
@@ -49,37 +49,37 @@ describe("useContext:", () => {
expect(context.currentNetwork.value).toEqual(DEFAULT_NETWORK);
});
it("sets network by query param", () => {
- location.search = "?network=" + GOERLI_BETA_NETWORK.name;
+ location.search = "?network=" + TESTNET_BETA_NETWORK.name;
const mockEnvironmentConfig = vi.spyOn(useEnvironmentConfig, "default").mockReturnValue({
- networks: computed(() => [GOERLI_NETWORK, GOERLI_BETA_NETWORK]),
+ networks: computed(() => [TESTNET_NETWORK, TESTNET_BETA_NETWORK]),
});
const context = useContext.default();
context.identifyNetwork();
- expect(context.currentNetwork.value).toEqual(GOERLI_BETA_NETWORK);
+ expect(context.currentNetwork.value).toEqual(TESTNET_BETA_NETWORK);
mockEnvironmentConfig.mockRestore();
});
it("sets network by hostname", () => {
- location.origin = GOERLI_BETA_NETWORK.hostnames[0];
+ location.origin = TESTNET_BETA_NETWORK.hostnames[0];
const mockEnvironmentConfig = vi.spyOn(useEnvironmentConfig, "default").mockReturnValue({
- networks: computed(() => [GOERLI_NETWORK, GOERLI_BETA_NETWORK]),
+ networks: computed(() => [TESTNET_NETWORK, TESTNET_BETA_NETWORK]),
});
const context = useContext.default();
context.identifyNetwork();
- expect(context.currentNetwork.value).toEqual(GOERLI_BETA_NETWORK);
+ expect(context.currentNetwork.value).toEqual(TESTNET_BETA_NETWORK);
mockEnvironmentConfig.mockRestore();
});
it("sets network by sessionStorage", () => {
const mockStorage = vi.spyOn(Storage.prototype, "getItem");
- Storage.prototype.getItem = vi.fn(() => GOERLI_BETA_NETWORK.name);
+ Storage.prototype.getItem = vi.fn(() => TESTNET_BETA_NETWORK.name);
const mockEnvironmentConfig = vi.spyOn(useEnvironmentConfig, "default").mockReturnValue({
- networks: computed(() => [GOERLI_NETWORK, GOERLI_BETA_NETWORK]),
+ networks: computed(() => [TESTNET_NETWORK, TESTNET_BETA_NETWORK]),
});
const context = useContext.default();
context.identifyNetwork();
- expect(context.currentNetwork.value).toEqual(GOERLI_BETA_NETWORK);
+ expect(context.currentNetwork.value).toEqual(TESTNET_BETA_NETWORK);
mockStorage.mockRestore();
mockEnvironmentConfig.mockRestore();
});
diff --git a/packages/app/tests/composables/useContractABI.spec.ts b/packages/app/tests/composables/useContractABI.spec.ts
index f4e1040e52..40fe4d4d92 100644
--- a/packages/app/tests/composables/useContractABI.spec.ts
+++ b/packages/app/tests/composables/useContractABI.spec.ts
@@ -4,7 +4,7 @@ import { describe, expect, it, type SpyInstance, vi } from "vitest";
import { $fetch } from "ohmyfetch";
-import { GOERLI_BETA_NETWORK, GOERLI_NETWORK } from "../mocks";
+import { TESTNET_BETA_NETWORK, TESTNET_NETWORK } from "../mocks";
import useContractABI from "@/composables/useContractABI";
@@ -84,11 +84,11 @@ describe("useContractABI:", () => {
});
it("does not reuse cache if network changed", async () => {
const mock = ($fetch as unknown as SpyInstance).mockClear();
- const currentNetwork = ref(GOERLI_NETWORK);
+ const currentNetwork = ref(TESTNET_NETWORK);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { getCollection } = useContractABI({ currentNetwork } as any);
await getCollection(["0x1230000000000000000000000000000000000000"]);
- currentNetwork.value = GOERLI_BETA_NETWORK;
+ currentNetwork.value = TESTNET_BETA_NETWORK;
await getCollection(["0x1230000000000000000000000000000000000000"]);
expect(mock).toBeCalledTimes(2);
mock.mockRestore();
diff --git a/packages/app/tests/composables/useEnvironmentConfig.spec.ts b/packages/app/tests/composables/useEnvironmentConfig.spec.ts
index 4b5e6d2191..f26bcb6f53 100644
--- a/packages/app/tests/composables/useEnvironmentConfig.spec.ts
+++ b/packages/app/tests/composables/useEnvironmentConfig.spec.ts
@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from "vitest";
-import { GOERLI_BETA_NETWORK, GOERLI_NETWORK } from "../mocks";
+import { TESTNET_BETA_NETWORK, TESTNET_NETWORK } from "../mocks";
import useEnvironmentConfig, { loadEnvironmentConfig } from "@/composables/useEnvironmentConfig";
@@ -9,7 +9,7 @@ import type { RuntimeConfig } from "@/configs";
vi.mock("../../src/configs/local.config", () => {
return {
default: {
- networks: [GOERLI_BETA_NETWORK, GOERLI_NETWORK],
+ networks: [TESTNET_BETA_NETWORK, TESTNET_NETWORK],
},
};
});
@@ -17,7 +17,7 @@ vi.mock("../../src/configs/local.config", () => {
vi.mock("../../src/configs/staging.config", () => {
return {
default: {
- networks: [GOERLI_BETA_NETWORK, { ...GOERLI_NETWORK, published: false }],
+ networks: [TESTNET_BETA_NETWORK, { ...TESTNET_NETWORK, published: false }],
},
};
});
@@ -42,7 +42,7 @@ describe("useEnvironmentConfig:", () => {
it("sets networks data to config", async () => {
const { networks } = useEnvironmentConfig();
await loadEnvironmentConfig({ appEnvironment: "local" } as RuntimeConfig);
- expect(networks.value).toEqual([GOERLI_BETA_NETWORK, GOERLI_NETWORK]);
+ expect(networks.value).toEqual([TESTNET_BETA_NETWORK, TESTNET_NETWORK]);
});
it("sets networks to values from runtime config if specified", async () => {
@@ -77,7 +77,7 @@ describe("useEnvironmentConfig:", () => {
it("returns only published network configs", async () => {
const { networks } = useEnvironmentConfig();
await loadEnvironmentConfig({ appEnvironment: "staging" } as RuntimeConfig);
- expect(networks.value).toEqual([GOERLI_BETA_NETWORK]);
+ expect(networks.value).toEqual([TESTNET_BETA_NETWORK]);
});
});
});
diff --git a/packages/app/tests/composables/useTokenLibrary.spec.ts b/packages/app/tests/composables/useTokenLibrary.spec.ts
index 39a8c24de9..e4b69256e1 100644
--- a/packages/app/tests/composables/useTokenLibrary.spec.ts
+++ b/packages/app/tests/composables/useTokenLibrary.spec.ts
@@ -4,7 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, type SpyInstance, vi } fro
import { $fetch, FetchError } from "ohmyfetch";
-import { GOERLI_BETA_NETWORK } from "../mocks";
+import { TESTNET_BETA_NETWORK } from "../mocks";
import useTokenLibrary from "@/composables/useTokenLibrary";
@@ -73,9 +73,9 @@ describe("useTokenLibrary:", () => {
it("requests all tokens from using if min liquidity is defined", async () => {
const { getTokens, tokens } = useTokenLibrary({
currentNetwork: computed(() => ({
- ...GOERLI_BETA_NETWORK,
+ ...TESTNET_BETA_NETWORK,
tokensMinLiquidity: 0,
- name: "goerli_with_liquidity",
+ name: "testnet_with_liquidity",
})),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any);
@@ -104,7 +104,7 @@ describe("useTokenLibrary:", () => {
fetchSpy.mockReset();
fetchSpy.mockRejectedValue(new FetchError("An error occurred"));
const { isRequestFailed, getTokens } = useTokenLibrary({
- currentNetwork: computed(() => GOERLI_BETA_NETWORK),
+ currentNetwork: computed(() => TESTNET_BETA_NETWORK),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any);
await getTokens();
diff --git a/packages/app/tests/composables/useTransaction.spec.ts b/packages/app/tests/composables/useTransaction.spec.ts
index 02781945e5..3f637336db 100644
--- a/packages/app/tests/composables/useTransaction.spec.ts
+++ b/packages/app/tests/composables/useTransaction.spec.ts
@@ -280,7 +280,7 @@ describe("useTransaction:", () => {
l2Address: "0x4732c03b2cf6ede46500e799de79a15df44929eb",
address: "0x4732c03b2cf6ede46500e799de79a15df44929eb",
symbol: "LINK",
- name: "ChainLink Token (goerli)",
+ name: "ChainLink Token (testnet)",
decimals: 18,
usdPrice: 1,
},
@@ -303,7 +303,7 @@ describe("useTransaction:", () => {
l2Address: "0x4732c03b2cf6ede46500e799de79a15df44929eb",
address: "0x4732c03b2cf6ede46500e799de79a15df44929eb",
symbol: "LINK",
- name: "ChainLink Token (goerli)",
+ name: "ChainLink Token (testnet)",
decimals: 18,
usdPrice: 1,
},
@@ -326,7 +326,7 @@ describe("useTransaction:", () => {
l2Address: "0x4732c03b2cf6ede46500e799de79a15df44929eb",
address: "0x4732c03b2cf6ede46500e799de79a15df44929eb",
symbol: "LINK",
- name: "ChainLink Token (goerli)",
+ name: "ChainLink Token (testnet)",
decimals: 18,
usdPrice: 1,
},
@@ -404,6 +404,7 @@ describe("useTransaction:", () => {
fee: "0x521f303519100",
feeData: {
amountPaid: "0x521f303519100",
+ paymasterAddress: undefined,
isPaidByPaymaster: false,
refunds: [
{
@@ -519,6 +520,9 @@ describe("useTransaction:", () => {
toNetwork: "L2",
type: "transfer",
tokenInfo: {
+ iconURL: undefined,
+ liquidity: undefined,
+ usdPrice: undefined,
address: "0x1bAbcaeA2e4BE1f1e1A149c454806F2D21d7f47D",
l1Address: undefined,
l2Address: "0x1bAbcaeA2e4BE1f1e1A149c454806F2D21d7f47D",
@@ -535,6 +539,9 @@ describe("useTransaction:", () => {
toNetwork: "L2",
type: "transfer",
tokenInfo: {
+ iconURL: undefined,
+ liquidity: undefined,
+ usdPrice: undefined,
address: "0x1bAbcaeA2e4BE1f1e1A149c454806F2D21d7f47C",
l1Address: null,
l2Address: "0x1bAbcaeA2e4BE1f1e1A149c454806F2D21d7f47C",
diff --git a/packages/app/tests/e2e/features/artifacts/artifactsSet1.feature b/packages/app/tests/e2e/features/artifacts/artifactsSet1.feature
index 4b1cb560f2..26bba2cc7c 100644
--- a/packages/app/tests/e2e/features/artifacts/artifactsSet1.feature
+++ b/packages/app/tests/e2e/features/artifacts/artifactsSet1.feature
@@ -4,53 +4,30 @@ Feature: Main Page
Background:
Given I am on main page
- @id253 @featureEnv @testnet
- Scenario Outline: Check the element "" in Tools section is available, clickable and have correct href
- Given I click by text "Tools"
- Given Element with "text" "" should be "visible"
- When Element with "text" "" should be "clickable"
- Then Element with "text" "" should have "" value
-
- Examples:
- | Sub-Section | url |
- | Smart Contract Verification | /contracts/verify |
- | Portal | https://goerli.staging-portal.zksync.dev/ |
-
- @id253 @featureEnv @mainnet
- Scenario Outline: Check the element "" in Tools section is available, clickable and have correct href
- Given I click by text "Tools"
- Given Element with "text" "" should be "visible"
- When Element with "text" "" should be "clickable"
- Then Element with "text" "" should have "" value
- Examples:
- | Sub-Section | url |
- | Smart Contract Verification | /contracts/verify |
- | Portal | https://staging-portal.zksync.dev/ |
-
- @id253:I @productionEnv @testnet
- Scenario Outline: Check the element "" in Tools section is available, clickable and have correct href
+ @id253:I @featureEnv @testnetSmokeSuite @testnet
+ Scenario Outline: Check the element "" in Tools section is available, clickable and have correct href (Sepolia)
Given I click by text "Tools"
Given Element with "text" "" should be "visible"
When Element with "text" "" should be "clickable"
Then Element with "text" "" should have "" value
Examples:
- | Sub-Section | url |
- | Smart Contract Verification | /contracts/verify |
- | Portal | https://goerli.portal.zksync.io/ |
+ | Sub-Section | url |
+ | Smart Contract Verification | /contracts/verify |
+ | Bridge | https://portal.zksync.io/bridge/?network=sepolia |
- @id253:I @productionEnv @mainnet
- Scenario Outline: Check the element "" in Tools section is available, clickable and have correct href
+ @id253:II @mainnet
+ Scenario Outline: Check the element "" in Tools section is available, clickable and have correct href (Mainnet)
Given I click by text "Tools"
Given Element with "text" "" should be "visible"
When Element with "text" "" should be "clickable"
Then Element with "text" "" should have "" value
Examples:
- | Sub-Section | url |
- | Smart Contract Verification | /contracts/verify |
- | Portal | https://portal.zksync.io/ |
+ | Sub-Section | url |
+ | Smart Contract Verification | /contracts/verify |
+ | Bridge | https://portal.zksync.io/bridge/?network=mainnet |
@id231
Scenario Outline: Check social networks icon "" is available, clickable and have correct href
@@ -83,9 +60,8 @@ Feature: Main Page
Examples:
| Value | Dropdown |
| zkSync Era Sepolia Testnet | network |
- | zkSync Era Goerli Testnet | network |
| Goerli (Stage2) | network |
-
+
@id254:II @productionEnv
Scenario Outline: Check dropdown "" for "" and verify
Given Set the "" value for "" switcher
@@ -93,7 +69,6 @@ Feature: Main Page
Examples:
| Value | Dropdown |
- | zkSync Era Goerli Testnet | network |
| zkSync Era Sepolia Testnet | network |
Scenario: Network stats is displayed
@@ -139,7 +114,7 @@ Feature: Main Page
Given I go to page "/address/0x8f0F33583a56908F7F933cd6F0AaE382aC3fd8f6"
Then Element with "id" "search" should be "visible"
- @id209:I @testnet
+ @id209:I @testnet
Scenario Outline: Verify Transaction table contains "" row
Given I go to page "/tx/0xe7a91cc9b270d062328ef995e0ef67195a3703d43ce4e1d375f87d5c64e51981"
When Table contains row with ""
@@ -201,7 +176,7 @@ Feature: Main Page
| Created | 2023-05-14 |
- @id211 @testnet
+ @id211 @testnet
Scenario Outline: Verify Contract info table contains "" row
Given I go to page "/address/0x3e7676937A7E96CFB7616f255b9AD9FF47363D4b"
Then Element with "text" "" should be "visible"
diff --git a/packages/app/tests/e2e/features/artifacts/artifactsSet2.feature b/packages/app/tests/e2e/features/artifacts/artifactsSet2.feature
index 05043dc146..27e205d119 100644
--- a/packages/app/tests/e2e/features/artifacts/artifactsSet2.feature
+++ b/packages/app/tests/e2e/features/artifacts/artifactsSet2.feature
@@ -36,7 +36,7 @@ Feature: Main Page
Examples:
| Row | Value |
- | Root hash | 0xfa8fadc06c46dc8a3c52 |
+ | Block hash | 0xfa8fadc06c46dc8a3c52 |
| Timestamp | 2023-02-09 |
| Commit tx hash | 0xc3211d8bc51163f923ff |
@@ -48,7 +48,7 @@ Feature: Main Page
Examples:
| Row | Value |
- | Root hash | 0xfa8fadc06c46dc8a3c52 |
+ | Block hash | 0xfa8fadc06c46dc8a3c52 |
| Timestamp | 2023-03-24 |
| Commit tx hash | 0xeb94693555bd2ef92c82 |
diff --git a/packages/app/tests/e2e/features/copying.feature b/packages/app/tests/e2e/features/copying.feature
index 5a2e8a46ba..6ce0fab371 100644
--- a/packages/app/tests/e2e/features/copying.feature
+++ b/packages/app/tests/e2e/features/copying.feature
@@ -120,7 +120,7 @@ Feature: Copying
Examples:
| Row | Text |
- | Root hash | 0x51f81bcdfc324a0dff2b5bec9d92e21cbebc4d5e29d3a3d30de3e03fbeab8d7f |
+ | Block hash | 0x51f81bcdfc324a0dff2b5bec9d92e21cbebc4d5e29d3a3d30de3e03fbeab8d7f |
| Commit tx hash | 0x6ad6a118e09a27e39ee57c63e812953788de4974987c76bc954c14a8c32688e8 |
| Prove tx hash | 0xfbd3a89cee83e4f28999bc8fd5e96d133b7ebc367d5c7026f173d21687998379 |
| Execute tx hash | 0x5131c1bb47dca3d42ccdfd12d1ab7224cbb88fb9ad91b94e2da26631602f6fab |
@@ -134,7 +134,7 @@ Feature: Copying
Examples:
| Row | Text |
- | Root hash | 0x51f81bcdfc324a0dff2b5bec9d92e21cbebc4d5e29d3a3d30de3e03fbeab8d7f |
+ | Block hash | 0x51f81bcdfc324a0dff2b5bec9d92e21cbebc4d5e29d3a3d30de3e03fbeab8d7f |
| Commit tx hash | 0x33143afba6c91f77d18b0d7a50248e6255461ec0e0cd80a06d3bd86f2686768c |
| Prove tx hash | 0x424cdbfb877178a909fbbe6dca6ef131a752e6c91c8b24470d919e30c06e3692 |
| Execute tx hash | 0x51425089db3b2ce38b1893ec2f1dc23e3f5db8e9f48f06bb624e99d77fe76aca |
@@ -178,7 +178,7 @@ Feature: Copying
Examples:
| Row | Text |
| ETH | 0x000000000000000000000000000000000000800A |
- | USDC | 0x3355df6D4c9C3035724Fd0e3914dE96A5a83aaf4 |
+ | USDC.e | 0x3355df6D4c9C3035724Fd0e3914dE96A5a83aaf4 |
| MUTE | 0x0e97C7a0F8B2C9885C8ac9fC6136e829CbC21d42 |
| COMBO | 0xc2B13Bb90E33F1E191b8aA8F44Ce11534D5698E3 |
| PERP | 0x42c1c56be243c250AB24D2ecdcC77F9cCAa59601 |
diff --git a/packages/app/tests/e2e/features/redirection/redirectionSet1.feature b/packages/app/tests/e2e/features/redirection/redirectionSet1.feature
index ab2e4c44a9..4a9871b9c2 100644
--- a/packages/app/tests/e2e/features/redirection/redirectionSet1.feature
+++ b/packages/app/tests/e2e/features/redirection/redirectionSet1.feature
@@ -13,7 +13,7 @@ Feature: Redirection
Examples:
| Extra button name | url |
- | Docs | https://era.zksync.io/docs/dev/ |
+ | Docs | https://docs.zksync.io/build/tooling/block-explorer/getting-started.html |
| Terms | https://zksync.io/terms |
| Contact | https://zksync.io/contact |
@@ -27,12 +27,12 @@ Feature: Redirection
| Icon | url |
# discord renamed to "join"
| join | https://join.zksync.dev/ |
- | twitter | https://twitter.com/zksync |
+ | twitter | https://x.com/zksync |
@id251
Scenario: Verify redirection for Documentation link
Given I click by text "Documentation"
- Then New page have "https://era.zksync.io/docs/dev/" address
+ Then New page have "https://docs.zksync.io/build/tooling/block-explorer/getting-started.html" address
@id252
Scenario Outline: Verify redirection for "" in BE menu
@@ -45,7 +45,7 @@ Feature: Redirection
| Blocks | /blocks/ |
| Transactions | /transactions/ |
- @id253:II
+ @id253:I
Scenario Outline: Verify redirection for "" in Tools menu
Given I click by text "Tools "
When I click by element with partial href "" and text ""
@@ -56,26 +56,15 @@ Feature: Redirection
| Smart Contract Verification | /contracts/verify |
# | zkEVM Debugger | /tools/debugger |
- @id253:III @featureEnv @testnet
- Scenario Outline: Verify redirection for "" in Tools menu
- Given I click by text "Tools "
- When I click by element with partial href "" and text ""
- Then New page have "" address
-
- Examples:
- | Sub-Section | url |
- | Portal | https://goerli.staging-portal.zksync.dev/ |
-
-
- @id253:IIII @productionEnv @testnet
- Scenario Outline: Verify redirection for "" in Tools menu
+ @id253:III @featureEnv @testnetSmokeSuite @testnet
+ Scenario Outline: Verify redirection for "" in Tools menu (Sepolia)
Given I click by text "Tools "
When I click by element with partial href "" and text ""
Then New page have "" address
Examples:
- | Sub-Section | url | redirect_url |
- | Portal | https://zksync.io/explore#bridges | https://goerli.portal.zksync.io |
+ | Sub-Section | url | redirect_url |
+ | Bridge | https://portal.zksync.io/bridge/?network=sepolia | https://portal.zksync.io/bridge/?network=sepolia |
@id253:IV @featureEnv @mainnet
Scenario Outline: Verify redirection for "" in Tools menu
@@ -84,19 +73,8 @@ Feature: Redirection
Then New page have "" address
Examples:
- | Sub-Section | url |
- | Portal | https://staging-portal.zksync.dev/ |
-
-
- @id253:IV @productionEnv @mainnet
- Scenario Outline: Verify redirection for "" in Tools menu
- Given I click by text "Tools "
- When I click by element with partial href "" and text ""
- Then New page have "" address
-
- Examples:
- | Sub-Section | url | redirect_url |
- | Portal | https://zksync.io/explore#bridges | https://portal.zksync.io |
+ | Sub-Section | url |
+ | Bridge | https://portal.zksync.io/bridge/?network=mainnet |
#Account page
@id259 @testnet
diff --git a/packages/app/tests/e2e/features/redirection/redirectionSet3.feature b/packages/app/tests/e2e/features/redirection/redirectionSet3.feature
index 2aecd735ee..9fc6aade9d 100644
--- a/packages/app/tests/e2e/features/redirection/redirectionSet3.feature
+++ b/packages/app/tests/e2e/features/redirection/redirectionSet3.feature
@@ -62,7 +62,6 @@ Feature: Redirection
Examples:
| Initial page | Network | url |
- | /address/0x000000000000000000000000000000000000800A | zkSync Era Goerli Testnet | /address/0x000000000000000000000000000000000000800A/?network=goerli |
| /address/0x000000000000000000000000000000000000800A | Goerli (Stage2) | /address/0x000000000000000000000000000000000000800A/?network=goerli-beta |
| /address/0x000000000000000000000000000000000000800A | zkSync Era Mainnet | /address/0x000000000000000000000000000000000000800A/?network=mainnet |
| /address/0x000000000000000000000000000000000000800A | zkSync Era Sepolia Testnet | /address/0x000000000000000000000000000000000000800A/?network=sepolia |
@@ -75,6 +74,5 @@ Feature: Redirection
Examples:
| Initial page | Network | url |
- | /address/0x000000000000000000000000000000000000800A | zkSync Era Goerli Testnet | /address/0x000000000000000000000000000000000000800A/?network=goerli |
| /address/0x000000000000000000000000000000000000800A | zkSync Era Sepolia Testnet | /address/0x000000000000000000000000000000000000800A/?network=sepolia |
| /address/0x000000000000000000000000000000000000800A | zkSync Era Mainnet | /address/0x000000000000000000000000000000000000800A/?network=mainnet |
diff --git a/packages/app/tests/e2e/src/data/data.ts b/packages/app/tests/e2e/src/data/data.ts
index 64b8362960..4d2bd51969 100644
--- a/packages/app/tests/e2e/src/data/data.ts
+++ b/packages/app/tests/e2e/src/data/data.ts
@@ -1,5 +1,4 @@
export enum NetworkSwitcher {
- zkSyncEraGoerli = "/?network=goerli",
goerliStage2 = "/?network=goerli-beta",
zkSyncEraMainnet = "/?network=mainnet",
zkSyncEraSepolia = "/?network=sepolia",
diff --git a/packages/app/tests/e2e/src/pages/base.page.ts b/packages/app/tests/e2e/src/pages/base.page.ts
index 2111d0ac2b..d3efdf341a 100644
--- a/packages/app/tests/e2e/src/pages/base.page.ts
+++ b/packages/app/tests/e2e/src/pages/base.page.ts
@@ -169,7 +169,7 @@ export class BasePage {
async getEtherscanDomain(networkType: string) {
if (networkType.includes("goerli")) {
- return "https://goerli.etherscan.io"; // Testnet
+ return "https://goerli.etherscan.io/"; // Testnet
} else {
return "https://etherscan.io"; // Mainnet
}
diff --git a/packages/app/tests/mocks.ts b/packages/app/tests/mocks.ts
index 33dd3858dd..4a0146c0e8 100644
--- a/packages/app/tests/mocks.ts
+++ b/packages/app/tests/mocks.ts
@@ -34,33 +34,33 @@ export const ETH_TOKEN_MOCK = {
iconURL: null,
};
-export const GOERLI_NETWORK: NetworkConfig = {
- name: "goerli",
+export const TESTNET_NETWORK: NetworkConfig = {
+ name: "testnet",
verificationApiUrl: "https://zksync2-testnet-explorer.zksync.dev",
apiUrl: "https://block-explorer-api.testnets.zksync.dev",
icon: "",
- l2ChainId: 280,
+ l2ChainId: 300,
rpcUrl: "",
- l2NetworkName: "Goerli",
- l2WalletUrl: "",
- l1ExplorerUrl: "http://goerli-block-explorer",
+ l2NetworkName: "Testnet",
+ l1ExplorerUrl: "http://testnet-block-explorer",
maintenance: false,
published: true,
hostnames: [],
+ baseTokenAddress: checksumAddress("0x000000000000000000000000000000000000800A"),
};
-export const GOERLI_BETA_NETWORK: NetworkConfig = {
- name: "goerli-beta",
+export const TESTNET_BETA_NETWORK: NetworkConfig = {
+ name: "testnet-beta",
verificationApiUrl: "https://zksync2-testnet-explorer.zksync.dev",
apiUrl: "https://block-explorer-api.mock.zksync.dev",
icon: "",
l2ChainId: 270,
rpcUrl: "",
- l2NetworkName: "Goerli Beta",
- l2WalletUrl: "",
- l1ExplorerUrl: "http://goerli-beta-block-explorer",
+ l2NetworkName: "Testnet Beta",
+ l1ExplorerUrl: "http://testnet-beta-block-explorer",
maintenance: false,
published: true,
- hostnames: ["https://goerli-beta.staging-scan-v2.zksync.dev/"],
+ hostnames: ["https://testnet-beta.staging-scan-v2.zksync.dev/"],
+ baseTokenAddress: checksumAddress("0x000000000000000000000000000000000000800A"),
};
export const useContractEventsMock = (params: any = {}) => {
@@ -79,11 +79,11 @@ export const useWalletMock = (params: any = {}) => {
const mockWallet = vi.spyOn(composablesFactory, "useWallet").mockReturnValue({
...composablesFactory.useWallet({
currentNetwork: computed(() => ({
- chainName: GOERLI_NETWORK.name,
- explorerUrl: GOERLI_NETWORK.l1ExplorerUrl!,
+ chainName: TESTNET_NETWORK.name,
+ explorerUrl: TESTNET_NETWORK.l1ExplorerUrl!,
l1ChainId: 5,
- l2ChainId: GOERLI_NETWORK.l2ChainId,
- rpcUrl: GOERLI_NETWORK.rpcUrl,
+ l2ChainId: TESTNET_NETWORK.l2ChainId,
+ rpcUrl: TESTNET_NETWORK.rpcUrl,
})),
getL2Provider: () => undefined as unknown as Provider,
}),
@@ -186,10 +186,10 @@ export const useTransactionMock = (params: any = {}) => {
export const useContextMock = (params: any = {}) => {
const mockContextConfig = vi.spyOn(useContext, "default").mockReturnValue({
getL2Provider: () => vi.fn(() => null),
- currentNetwork: computed(() => GOERLI_NETWORK),
+ currentNetwork: computed(() => TESTNET_NETWORK),
identifyNetwork: () => undefined,
isReady: computed(() => true),
- networks: computed(() => [GOERLI_NETWORK]),
+ networks: computed(() => [TESTNET_NETWORK]),
...params,
});
diff --git a/packages/app/tests/views/ContractVerificationView.spec.ts b/packages/app/tests/views/ContractVerificationView.spec.ts
index 8f96244a2b..064dcb4a89 100644
--- a/packages/app/tests/views/ContractVerificationView.spec.ts
+++ b/packages/app/tests/views/ContractVerificationView.spec.ts
@@ -44,13 +44,11 @@ describe("ContractVerificationView:", () => {
en: enUS,
},
});
-
it("has correct title", async () => {
expect(i18n.global.t(routes.find((e) => e.name === "contract-verification")?.meta?.title as string)).toBe(
"Smart Contract Verification"
);
});
-
it("uses contract address from query", async () => {
const wrapper = mount(ContractVerificationView, {
global: {
@@ -113,6 +111,52 @@ describe("ContractVerificationView:", () => {
expect(wrapper.find("#sourceCode").exists()).toBe(false);
expect(wrapper.find(".multi-file-verification").exists()).toBe(true);
});
+ it("shows zkVM checkbox by default", async () => {
+ const wrapper = mount(ContractVerificationView, {
+ global: {
+ stubs: ["router-link"],
+ plugins: [i18n, $testId],
+ },
+ });
+
+ expect(wrapper.find(".checkbox-input-container").exists()).toBe(true);
+ });
+ it("shows zkVM checkbox when solidity MFV was selected", async () => {
+ const wrapper = mount(ContractVerificationView, {
+ global: {
+ stubs: ["router-link"],
+ plugins: [i18n, $testId],
+ },
+ });
+
+ await wrapper.find("#compilerType").trigger("click");
+ await wrapper.find(`[aria-labelledby="compilerType"] > li:nth-child(2)`).trigger("click");
+ expect(wrapper.find(".checkbox-input-container").exists()).toBe(true);
+ });
+ it("doesn't show zkVM checkbox when vyper single file verification was select", async () => {
+ const wrapper = mount(ContractVerificationView, {
+ global: {
+ stubs: ["router-link"],
+ plugins: [i18n, $testId],
+ },
+ });
+
+ await wrapper.find("#compilerType").trigger("click");
+ await wrapper.find(`[aria-labelledby="compilerType"] > li:nth-child(3)`).trigger("click");
+ expect(wrapper.find(".checkbox-input-container").exists()).toBe(false);
+ });
+ it("doesn't show zkVM checkbox when vyper MFV was select", async () => {
+ const wrapper = mount(ContractVerificationView, {
+ global: {
+ stubs: ["router-link"],
+ plugins: [i18n, $testId],
+ },
+ });
+
+ await wrapper.find("#compilerType").trigger("click");
+ await wrapper.find(`[aria-labelledby="compilerType"] > li:nth-child(4)`).trigger("click");
+ expect(wrapper.find(".checkbox-input-container").exists()).toBe(false);
+ });
it("shows custom error text", async () => {
const mock = vi.spyOn(useContractVerification, "default").mockReturnValue({
...useContractVerification.default(),
@@ -309,7 +353,7 @@ describe("ContractVerificationView:", () => {
expect(wrapper.find(".docs-link").text()).toEqual("Details");
expect(wrapper.find(".docs-link").attributes("href")).toEqual(
- "https://era.zksync.io/docs/tools/block-explorer/contract-verification.html#enter-contract-details"
+ "https://docs.zksync.io/build/tooling/block-explorer/contract-verification.html#user-interface"
);
});
it("resets uploaded files block when clicking on clear button", async () => {
diff --git a/packages/data-fetcher/.env.example b/packages/data-fetcher/.env.example
new file mode 100644
index 0000000000..786b5051f5
--- /dev/null
+++ b/packages/data-fetcher/.env.example
@@ -0,0 +1,15 @@
+LOG_LEVEL=debug
+PORT=3040
+
+GRACEFUL_SHUTDOWN_TIMEOUT_MS=0
+
+BLOCKCHAIN_RPC_URL=http://localhost:3050
+
+RPC_CALLS_DEFAULT_RETRY_TIMEOUT=30000
+RPC_CALLS_QUICK_RETRY_TIMEOUT=5000
+RPC_CALLS_RETRIES_MAX_TOTAL_TIMEOUT=120000
+
+RPC_CALLS_CONNECTION_TIMEOUT=60000
+RPC_CALLS_CONNECTION_QUICK_TIMEOUT=10000
+
+MAX_BLOCKS_BATCH_SIZE=20
\ No newline at end of file
diff --git a/packages/data-fetcher/.eslintignore b/packages/data-fetcher/.eslintignore
new file mode 100644
index 0000000000..8f4ceb1f53
--- /dev/null
+++ b/packages/data-fetcher/.eslintignore
@@ -0,0 +1 @@
+/test/scripts/*
\ No newline at end of file
diff --git a/packages/data-fetcher/.eslintrc.js b/packages/data-fetcher/.eslintrc.js
new file mode 100644
index 0000000000..8f5aedb718
--- /dev/null
+++ b/packages/data-fetcher/.eslintrc.js
@@ -0,0 +1,25 @@
+module.exports = {
+ parser: '@typescript-eslint/parser',
+ parserOptions: {
+ project: 'tsconfig.json',
+ tsconfigRootDir : __dirname,
+ sourceType: 'module',
+ },
+ plugins: ['@typescript-eslint/eslint-plugin'],
+ extends: [
+ 'plugin:@typescript-eslint/recommended',
+ 'plugin:prettier/recommended',
+ ],
+ root: true,
+ env: {
+ node: true,
+ jest: true,
+ },
+ ignorePatterns: ['.eslintrc.js'],
+ rules: {
+ '@typescript-eslint/interface-name-prefix': 'off',
+ '@typescript-eslint/explicit-function-return-type': 'off',
+ '@typescript-eslint/explicit-module-boundary-types': 'off',
+ '@typescript-eslint/no-explicit-any': 'off',
+ },
+};
diff --git a/packages/data-fetcher/Dockerfile b/packages/data-fetcher/Dockerfile
new file mode 100644
index 0000000000..135a84c9c2
--- /dev/null
+++ b/packages/data-fetcher/Dockerfile
@@ -0,0 +1,43 @@
+FROM node:18.17.1-alpine AS base-stage
+ENV NODE_ENV=production
+
+WORKDIR /usr/src/app
+
+RUN apk add --update python3 make g++ && rm -rf /var/cache/apk/*
+
+COPY --chown=node:node .npmrc .npmrc
+COPY --chown=node:node lerna.json ./
+COPY --chown=node:node package*.json ./
+COPY --chown=node:node ./packages/data-fetcher/package*.json ./packages/data-fetcher/
+RUN npm ci --ignore-scripts --only=production && npm cache clean --force
+COPY --chown=node:node ./packages/data-fetcher/. ./packages/data-fetcher
+RUN rm -f .npmrc
+
+FROM base-stage AS development-stage
+ENV NODE_ENV=development
+COPY --chown=node:node .npmrc .npmrc
+RUN npm ci
+RUN rm -f .npmrc
+
+FROM development-stage AS build-stage
+RUN npm run build
+
+FROM base-stage AS production-stage
+
+# HEALTHCHECK --interval=30s --timeout=3s --retries=5 \
+# CMD curl -f http://localhost:${PORT}/health || exit 1
+
+COPY --chown=node:node --from=build-stage /usr/src/app/packages/data-fetcher/dist ./packages/data-fetcher/dist
+
+ARG NODE_ENV=production
+ENV NODE_ENV $NODE_ENV
+
+ARG PORT=3040
+ENV PORT $PORT
+
+EXPOSE $PORT 9229 9230
+
+USER node
+WORKDIR /usr/src/app/packages/data-fetcher
+
+CMD [ "node", "dist/main.js" ]
diff --git a/packages/data-fetcher/README.md b/packages/data-fetcher/README.md
new file mode 100644
index 0000000000..e26f2b4993
--- /dev/null
+++ b/packages/data-fetcher/README.md
@@ -0,0 +1,52 @@
+# zkSync Era Block Explorer Data Fetcher
+## Overview
+
+`zkSync Era Block Explorer Data Fetcher` service exposes and implements an HTTP endpoint to retrieve aggregated data for a certain block / range of blocks from the blockchain. This endpoint is called by the [Block Explorer Worker](/packages/worker) service.
+
+## Installation
+
+```bash
+$ npm install
+```
+
+## Setting up env variables
+
+- Create `.env` file in the `data-fetcher` package folder and copy paste `.env.example` content in there.
+```
+cp .env.example .env
+```
+- In order to tell the service where to get the blockchain data from set the value of the `BLOCKCHAIN_RPC_URL` env var to your blockchain RPC API URL. For zkSync Era testnet it can be set to `https://zksync2-testnet.zksync.dev`. For zkSync Era mainnet - `https://zksync2-mainnet.zksync.io`.
+
+## Running the app
+
+```bash
+# development
+$ npm run dev
+
+# watch mode
+$ npm run dev:watch
+
+# debug mode
+$ npm run dev:debug
+
+# production mode
+$ npm run start
+```
+
+## Test
+
+```bash
+# unit tests
+$ npm run test
+
+# unit tests debug mode
+$ npm run test:debug
+
+# test coverage
+$ npm run test:cov
+```
+
+## Development
+
+### Linter
+Run `npm run lint` to make sure the code base follows configured linter rules.
diff --git a/packages/data-fetcher/nest-cli.json b/packages/data-fetcher/nest-cli.json
new file mode 100644
index 0000000000..256648114a
--- /dev/null
+++ b/packages/data-fetcher/nest-cli.json
@@ -0,0 +1,5 @@
+{
+ "$schema": "https://json.schemastore.org/nest-cli",
+ "collection": "@nestjs/schematics",
+ "sourceRoot": "src"
+}
diff --git a/packages/data-fetcher/package.json b/packages/data-fetcher/package.json
new file mode 100644
index 0000000000..d044a3e55a
--- /dev/null
+++ b/packages/data-fetcher/package.json
@@ -0,0 +1,113 @@
+{
+ "name": "data-fetcher",
+ "version": "0.0.0",
+ "title": "zkSync Era Block Explorer Data Fetcher",
+ "description": "zkSync Era Block Explorer Data Fetcher",
+ "author": "Matter Labs",
+ "private": true,
+ "license": "MIT",
+ "repository": "https://github.com/matter-labs/block-explorer",
+ "scripts": {
+ "prebuild": "rimraf dist",
+ "build": "nest build",
+ "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
+ "dev": "nest start",
+ "dev:watch": "nest start --watch",
+ "dev:debug": "nest start --debug 0.0.0.0:9229 --watch",
+ "start": "node dist/main",
+ "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
+ "test": "jest",
+ "test:watch": "jest --watch",
+ "test:cov": "jest --coverage",
+ "test:ci": "jest --coverage",
+ "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
+ "test:e2e": "jest --config ./test/jest-e2e.json"
+ },
+ "dependencies": {
+ "@nestjs/common": "^9.0.0",
+ "@nestjs/config": "^2.2.0",
+ "@nestjs/core": "^9.0.0",
+ "@nestjs/platform-express": "^9.0.0",
+ "@nestjs/terminus": "^9.1.2",
+ "@willsoto/nestjs-prometheus": "^4.7.0",
+ "ethers": "^5.7.1",
+ "nest-winston": "^1.7.0",
+ "prom-client": "^14.1.0",
+ "reflect-metadata": "^0.1.13",
+ "rimraf": "^3.0.2",
+ "rxjs": "^7.2.0",
+ "winston": "^3.8.2",
+ "zksync-web3": "0.15.4"
+ },
+ "devDependencies": {
+ "@nestjs/cli": "^9.0.0",
+ "@nestjs/schematics": "^9.0.0",
+ "@nestjs/testing": "^9.0.0",
+ "@types/express": "^4.17.13",
+ "@types/jest": "28.1.8",
+ "@types/supertest": "^2.0.11",
+ "@typescript-eslint/eslint-plugin": "^5.0.0",
+ "@typescript-eslint/parser": "^5.0.0",
+ "eslint-config-prettier": "^8.3.0",
+ "eslint-plugin-prettier": "^4.0.0",
+ "jest": "29.2.1",
+ "jest-junit": "^14.0.1",
+ "jest-mock-extended": "^3.0.1",
+ "lint-staged": "^13.0.3",
+ "source-map-support": "^0.5.20",
+ "supertest": "^6.1.3",
+ "ts-jest": "29.0.3",
+ "ts-loader": "^9.2.3",
+ "ts-node": "^10.0.0",
+ "tsconfig-paths": "4.1.0"
+ },
+ "jest": {
+ "moduleFileExtensions": [
+ "js",
+ "json",
+ "ts"
+ ],
+ "rootDir": "src",
+ "testRegex": ".*\\.spec\\.ts$",
+ "transform": {
+ "^.+\\.(t|j)s$": "ts-jest"
+ },
+ "collectCoverageFrom": [
+ "**/*.(t|j)s"
+ ],
+ "coverageDirectory": "../coverage",
+ "coverageThreshold": {
+ "global": {
+ "branches": 95,
+ "functions": 84,
+ "lines": 90,
+ "statements": 90
+ }
+ },
+ "testEnvironment": "node",
+ "coveragePathIgnorePatterns": [
+ "src/main.ts",
+ ".module.ts",
+ "src/logger.ts"
+ ],
+ "reporters": [
+ "default",
+ [
+ "jest-junit",
+ {
+ "suiteName": "Data Fetcher Unit tests"
+ }
+ ]
+ ]
+ },
+ "prettier": "@matterlabs/prettier-config",
+ "lint-staged": {
+ "*.{js,ts}": [
+ "npm run lint"
+ ]
+ },
+ "engines": {
+ "npm": ">=9.0.0",
+ "node": ">=18.0.0"
+ }
+}
diff --git a/packages/data-fetcher/src/abis/erc721.json b/packages/data-fetcher/src/abis/erc721.json
new file mode 100644
index 0000000000..b234eedfb6
--- /dev/null
+++ b/packages/data-fetcher/src/abis/erc721.json
@@ -0,0 +1,333 @@
+[
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address"
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "approve",
+ "outputs": [],
+ "payable": false,
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address"
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "mint",
+ "outputs": [],
+ "payable": false,
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "from",
+ "type": "address"
+ },
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address"
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "safeTransferFrom",
+ "outputs": [],
+ "payable": false,
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "from",
+ "type": "address"
+ },
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address"
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ },
+ {
+ "internalType": "bytes",
+ "name": "_data",
+ "type": "bytes"
+ }
+ ],
+ "name": "safeTransferFrom",
+ "outputs": [],
+ "payable": false,
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address"
+ },
+ {
+ "internalType": "bool",
+ "name": "approved",
+ "type": "bool"
+ }
+ ],
+ "name": "setApprovalForAll",
+ "outputs": [],
+ "payable": false,
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "from",
+ "type": "address"
+ },
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address"
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "transferFrom",
+ "outputs": [],
+ "payable": false,
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [],
+ "payable": false,
+ "stateMutability": "nonpayable",
+ "type": "constructor"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "from",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "to",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "Transfer",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "owner",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "approved",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "Approval",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "owner",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "operator",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "internalType": "bool",
+ "name": "approved",
+ "type": "bool"
+ }
+ ],
+ "name": "ApprovalForAll",
+ "type": "event"
+ },
+ {
+ "constant": true,
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "owner",
+ "type": "address"
+ }
+ ],
+ "name": "balanceOf",
+ "outputs": [
+ {
+ "internalType": "uint256",
+ "name": "",
+ "type": "uint256"
+ }
+ ],
+ "payable": false,
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "getApproved",
+ "outputs": [
+ {
+ "internalType": "address",
+ "name": "",
+ "type": "address"
+ }
+ ],
+ "payable": false,
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "owner",
+ "type": "address"
+ },
+ {
+ "internalType": "address",
+ "name": "operator",
+ "type": "address"
+ }
+ ],
+ "name": "isApprovedForAll",
+ "outputs": [
+ {
+ "internalType": "bool",
+ "name": "",
+ "type": "bool"
+ }
+ ],
+ "payable": false,
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "ownerOf",
+ "outputs": [
+ {
+ "internalType": "address",
+ "name": "",
+ "type": "address"
+ }
+ ],
+ "payable": false,
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [
+ {
+ "internalType": "bytes4",
+ "name": "interfaceId",
+ "type": "bytes4"
+ }
+ ],
+ "name": "supportsInterface",
+ "outputs": [
+ {
+ "internalType": "bool",
+ "name": "",
+ "type": "bool"
+ }
+ ],
+ "payable": false,
+ "stateMutability": "view",
+ "type": "function"
+ }
+]
\ No newline at end of file
diff --git a/packages/data-fetcher/src/abis/l2StandardERC20.json b/packages/data-fetcher/src/abis/l2StandardERC20.json
new file mode 100644
index 0000000000..901717080d
--- /dev/null
+++ b/packages/data-fetcher/src/abis/l2StandardERC20.json
@@ -0,0 +1,64 @@
+[
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "l1Token",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "internalType": "string",
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "internalType": "string",
+ "name": "symbol",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "internalType": "uint8",
+ "name": "decimals",
+ "type": "uint8"
+ }
+ ],
+ "name": "BridgeInitialize",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "l1Token",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "internalType": "string",
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "internalType": "string",
+ "name": "symbol",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "internalType": "uint8",
+ "name": "decimals",
+ "type": "uint8"
+ }
+ ],
+ "name": "BridgeInitialization",
+ "type": "event"
+ }
+]
\ No newline at end of file
diff --git a/packages/worker/src/abis/transferEventWithNoIndexes.json b/packages/data-fetcher/src/abis/transferEventWithNoIndexes.json
similarity index 100%
rename from packages/worker/src/abis/transferEventWithNoIndexes.json
rename to packages/data-fetcher/src/abis/transferEventWithNoIndexes.json
diff --git a/packages/worker/src/address/address.service.spec.ts b/packages/data-fetcher/src/address/address.service.spec.ts
similarity index 60%
rename from packages/worker/src/address/address.service.spec.ts
rename to packages/data-fetcher/src/address/address.service.spec.ts
index 9352c9957f..d71ba0e796 100644
--- a/packages/worker/src/address/address.service.spec.ts
+++ b/packages/data-fetcher/src/address/address.service.spec.ts
@@ -2,18 +2,15 @@ import { Test } from "@nestjs/testing";
import { Logger } from "@nestjs/common";
import { mock } from "jest-mock-extended";
import { types } from "zksync-web3";
-import { AddressRepository } from "../repositories";
import { AddressService } from "./address.service";
import { BlockchainService } from "../blockchain/blockchain.service";
describe("AddressService", () => {
let blockchainServiceMock: BlockchainService;
- let addressRepositoryMock: AddressRepository;
let addressService: AddressService;
beforeEach(async () => {
blockchainServiceMock = mock();
- addressRepositoryMock = mock();
const app = await Test.createTestingModule({
providers: [
@@ -22,10 +19,6 @@ describe("AddressService", () => {
provide: BlockchainService,
useValue: blockchainServiceMock,
},
- {
- provide: AddressRepository,
- useValue: addressRepositoryMock,
- },
],
}).compile();
@@ -34,7 +27,7 @@ describe("AddressService", () => {
addressService = app.get(AddressService);
});
- describe("saveContractAddresses", () => {
+ describe("getContractAddresses", () => {
const logs = [
mock({
topics: [
@@ -75,55 +68,36 @@ describe("AddressService", () => {
});
it("gets byte code for deployed contracts", async () => {
- await addressService.saveContractAddresses(logs, transactionReceipt);
+ await addressService.getContractAddresses(logs, transactionReceipt);
expect(blockchainServiceMock.getCode).toHaveBeenCalledTimes(2);
expect(blockchainServiceMock.getCode).toHaveBeenCalledWith("0xdc187378edD8Ed1585fb47549Cc5fe633295d571");
expect(blockchainServiceMock.getCode).toHaveBeenCalledWith("0xD144ca8Aa2E7DFECD56a3CCcBa1cd873c8e5db58");
});
- it("upserts contract addresses", async () => {
- await addressService.saveContractAddresses(logs, transactionReceipt);
- expect(addressRepositoryMock.upsert).toHaveBeenCalledTimes(2);
- expect(addressRepositoryMock.upsert).toHaveBeenCalledWith({
- address: "0xdc187378edD8Ed1585fb47549Cc5fe633295d571",
- bytecode: "bytecode1",
- createdInBlockNumber: transactionReceipt.blockNumber,
- creatorTxHash: transactionReceipt.transactionHash,
- creatorAddress: transactionReceipt.from,
- createdInLogIndex: logs[0].logIndex,
- });
- expect(addressRepositoryMock.upsert).toHaveBeenCalledWith({
- address: "0xD144ca8Aa2E7DFECD56a3CCcBa1cd873c8e5db58",
- bytecode: "bytecode2",
- createdInBlockNumber: transactionReceipt.blockNumber,
- creatorTxHash: transactionReceipt.transactionHash,
- creatorAddress: transactionReceipt.from,
- createdInLogIndex: logs[2].logIndex,
- });
- });
-
- it("returns created contract addresses", async () => {
- const result = await addressService.saveContractAddresses(logs, transactionReceipt);
- expect(result).toStrictEqual([
+ it("returns contract addresses", async () => {
+ const contractAddresses = await addressService.getContractAddresses(logs, transactionReceipt);
+ expect(contractAddresses).toStrictEqual([
{
address: "0xdc187378edD8Ed1585fb47549Cc5fe633295d571",
- blockNumber: 10,
- creatorAddress: "from",
- transactionHash: "transactionHash",
- logIndex: 1,
+ bytecode: "bytecode1",
+ blockNumber: transactionReceipt.blockNumber,
+ transactionHash: transactionReceipt.transactionHash,
+ creatorAddress: transactionReceipt.from,
+ logIndex: logs[0].logIndex,
},
{
address: "0xD144ca8Aa2E7DFECD56a3CCcBa1cd873c8e5db58",
- blockNumber: 10,
- creatorAddress: "from",
- transactionHash: "transactionHash",
- logIndex: 3,
+ bytecode: "bytecode2",
+ blockNumber: transactionReceipt.blockNumber,
+ transactionHash: transactionReceipt.transactionHash,
+ creatorAddress: transactionReceipt.from,
+ logIndex: logs[2].logIndex,
},
]);
});
it("returns an empty array if no logs specified", async () => {
- const result = await addressService.saveContractAddresses(null, transactionReceipt);
+ const result = await addressService.getContractAddresses(null, transactionReceipt);
expect(result).toStrictEqual([]);
});
});
diff --git a/packages/worker/src/address/address.service.ts b/packages/data-fetcher/src/address/address.service.ts
similarity index 59%
rename from packages/worker/src/address/address.service.ts
rename to packages/data-fetcher/src/address/address.service.ts
index a9d8d56b83..87b7705966 100644
--- a/packages/worker/src/address/address.service.ts
+++ b/packages/data-fetcher/src/address/address.service.ts
@@ -2,7 +2,6 @@ import { Injectable, Logger } from "@nestjs/common";
import { BigNumber } from "ethers";
import { types } from "zksync-web3";
import { BlockchainService } from "../blockchain/blockchain.service";
-import { AddressRepository } from "../repositories";
import { LogType } from "../log/logType";
import { ExtractContractAddressHandler } from "./interface/extractContractAddressHandler.interface";
import { defaultContractDeployedHandler } from "./extractContractDeployedHandlers";
@@ -17,29 +16,11 @@ export class AddressService {
private readonly logger: Logger;
public changedBalances: Map>;
- constructor(
- private readonly blockchainService: BlockchainService,
- private readonly addressRepository: AddressRepository
- ) {
+ constructor(private readonly blockchainService: BlockchainService) {
this.logger = new Logger(AddressService.name);
}
- private async saveContractAddress(contractAddress: ContractAddress): Promise {
- const bytecode = await this.blockchainService.getCode(contractAddress.address);
-
- const addressDto = {
- address: contractAddress.address,
- bytecode,
- createdInBlockNumber: contractAddress.blockNumber,
- creatorTxHash: contractAddress.transactionHash,
- creatorAddress: contractAddress.creatorAddress,
- createdInLogIndex: contractAddress.logIndex,
- };
-
- await this.addressRepository.upsert(addressDto);
- }
-
- public async saveContractAddresses(
+ public async getContractAddresses(
logs: types.Log[],
transactionReceipt: types.TransactionReceipt
): Promise {
@@ -61,8 +42,15 @@ export class AddressService {
}
}
- this.logger.debug({ message: "Saving contract addresses.", transactionReceipt: transactionReceipt.blockNumber });
- await Promise.all(contractAddresses.map((contractAddress) => this.saveContractAddress(contractAddress)));
+ this.logger.debug({
+ message: "Requesting contracts' bytecode",
+ transactionReceipt: transactionReceipt.blockNumber,
+ });
+ await Promise.all(
+ contractAddresses.map(async (contractAddress) => {
+ contractAddress.bytecode = await this.blockchainService.getCode(contractAddress.address);
+ })
+ );
return contractAddresses;
}
diff --git a/packages/worker/src/address/extractContractDeployedHandlers/default.handler.spec.ts b/packages/data-fetcher/src/address/extractContractDeployedHandlers/default.handler.spec.ts
similarity index 100%
rename from packages/worker/src/address/extractContractDeployedHandlers/default.handler.spec.ts
rename to packages/data-fetcher/src/address/extractContractDeployedHandlers/default.handler.spec.ts
diff --git a/packages/worker/src/address/extractContractDeployedHandlers/default.handler.ts b/packages/data-fetcher/src/address/extractContractDeployedHandlers/default.handler.ts
similarity index 100%
rename from packages/worker/src/address/extractContractDeployedHandlers/default.handler.ts
rename to packages/data-fetcher/src/address/extractContractDeployedHandlers/default.handler.ts
diff --git a/packages/worker/src/address/extractContractDeployedHandlers/index.ts b/packages/data-fetcher/src/address/extractContractDeployedHandlers/index.ts
similarity index 100%
rename from packages/worker/src/address/extractContractDeployedHandlers/index.ts
rename to packages/data-fetcher/src/address/extractContractDeployedHandlers/index.ts
diff --git a/packages/worker/src/address/interface/contractAddress.interface.ts b/packages/data-fetcher/src/address/interface/contractAddress.interface.ts
similarity index 87%
rename from packages/worker/src/address/interface/contractAddress.interface.ts
rename to packages/data-fetcher/src/address/interface/contractAddress.interface.ts
index 9e89041b22..2de3d8deb6 100644
--- a/packages/worker/src/address/interface/contractAddress.interface.ts
+++ b/packages/data-fetcher/src/address/interface/contractAddress.interface.ts
@@ -4,4 +4,5 @@ export interface ContractAddress {
transactionHash: string;
creatorAddress: string;
logIndex: number;
+ bytecode?: string;
}
diff --git a/packages/worker/src/address/interface/extractContractAddressHandler.interface.ts b/packages/data-fetcher/src/address/interface/extractContractAddressHandler.interface.ts
similarity index 100%
rename from packages/worker/src/address/interface/extractContractAddressHandler.interface.ts
rename to packages/data-fetcher/src/address/interface/extractContractAddressHandler.interface.ts
diff --git a/packages/data-fetcher/src/app.module.ts b/packages/data-fetcher/src/app.module.ts
new file mode 100644
index 0000000000..0b4f1f0eff
--- /dev/null
+++ b/packages/data-fetcher/src/app.module.ts
@@ -0,0 +1,38 @@
+import { Module, Logger } from "@nestjs/common";
+import { ConfigModule } from "@nestjs/config";
+import { PrometheusModule } from "@willsoto/nestjs-prometheus";
+import config from "./config";
+import { HealthModule } from "./health/health.module";
+import { BlockchainService } from "./blockchain";
+import { BlockService, BlockController } from "./block";
+import { TransactionService } from "./transaction";
+import { LogService } from "./log";
+import { AddressService } from "./address/address.service";
+import { BalanceService } from "./balance";
+import { TransferService } from "./transfer/transfer.service";
+import { TokenService } from "./token/token.service";
+import { JsonRpcProviderModule } from "./rpcProvider/jsonRpcProvider.module";
+import { MetricsModule } from "./metrics";
+
+@Module({
+ imports: [
+ ConfigModule.forRoot({ isGlobal: true, load: [config] }),
+ PrometheusModule.register(),
+ JsonRpcProviderModule.forRoot(),
+ MetricsModule,
+ HealthModule,
+ ],
+ controllers: [BlockController],
+ providers: [
+ BlockchainService,
+ AddressService,
+ BalanceService,
+ TransferService,
+ TokenService,
+ TransactionService,
+ LogService,
+ BlockService,
+ Logger,
+ ],
+})
+export class AppModule {}
diff --git a/packages/data-fetcher/src/balance/balance.service.spec.ts b/packages/data-fetcher/src/balance/balance.service.spec.ts
new file mode 100644
index 0000000000..56c37ec14b
--- /dev/null
+++ b/packages/data-fetcher/src/balance/balance.service.spec.ts
@@ -0,0 +1,534 @@
+import { Test, TestingModuleBuilder } from "@nestjs/testing";
+import { Logger } from "@nestjs/common";
+import { mock } from "jest-mock-extended";
+import { BigNumber } from "ethers";
+import { utils } from "zksync-web3";
+import { Transfer } from "../transfer/interfaces/transfer.interface";
+import { BlockchainService } from "../blockchain/blockchain.service";
+import { TokenType } from "../token/token.service";
+import { BalanceService } from "./";
+
+describe("BalanceService", () => {
+ let testingModuleBuilder: TestingModuleBuilder;
+ let blockchainServiceMock: BlockchainService;
+ let balanceService: BalanceService;
+
+ beforeEach(async () => {
+ blockchainServiceMock = mock();
+
+ testingModuleBuilder = Test.createTestingModule({
+ providers: [
+ BalanceService,
+ {
+ provide: BlockchainService,
+ useValue: blockchainServiceMock,
+ },
+ ],
+ });
+ const app = await testingModuleBuilder.compile();
+
+ app.useLogger(mock());
+
+ balanceService = app.get(BalanceService);
+ });
+
+ describe("clearTrackedState", () => {
+ const blockNumber = 10;
+ const blockNumber2 = 15;
+
+ beforeEach(() => {
+ balanceService.changedBalances.set(
+ blockNumber,
+ new Map>()
+ );
+ balanceService.changedBalances.set(
+ blockNumber2,
+ new Map>()
+ );
+ });
+
+ it("clears tracked balances for the specified block number", () => {
+ balanceService.clearTrackedState(blockNumber);
+ expect(balanceService.changedBalances.size).toBe(1);
+ expect(balanceService.changedBalances.has(blockNumber2)).toBe(true);
+ });
+ });
+
+ describe("trackChangedBalances", () => {
+ const transfers = [
+ mock({
+ tokenAddress: "0x000000000000000000000000000000000000800a",
+ from: "0x36615cf349d7f6344891b1e7ca7c72883f5dc049",
+ to: "0x0000000000000000000000000000000000008001",
+ blockNumber: 10,
+ tokenType: TokenType.BaseToken,
+ }),
+ mock({
+ tokenAddress: "0x000000000000000000000000000000000000800a",
+ from: "0xd206eaf6819007535e893410cfa01885ce40e99a",
+ to: "0x0000000000000000000000000000000000008001",
+ blockNumber: 10,
+ tokenType: TokenType.BaseToken,
+ }),
+ mock({
+ tokenAddress: "0x2392e98fb47cf05773144db3ce8002fac4f39c84",
+ from: "0x0000000000000000000000000000000000000000",
+ to: "0x36615cf349d7f6344891b1e7ca7c72883f5dc049",
+ blockNumber: 10,
+ tokenType: TokenType.ERC20,
+ }),
+ ];
+
+ it("processes null as a transfers array", () => {
+ balanceService.trackChangedBalances(null);
+ expect(balanceService.changedBalances.size).toBe(0);
+ });
+
+ it("processes empty transfers array", () => {
+ balanceService.trackChangedBalances([]);
+ expect(balanceService.changedBalances.size).toBe(0);
+ });
+
+ it("does not track changed balance for 0x000 address", () => {
+ const transfers = [
+ mock({
+ tokenAddress: "0x2392e98fb47cf05773144db3ce8002fac4f39c84",
+ from: "0x000000000000000000000000000000000000800a",
+ to: "0x0000000000000000000000000000000000008001",
+ blockNumber: 10,
+ tokenType: TokenType.ERC20,
+ }),
+ mock({
+ tokenAddress: "0x000000000000000000000000000000000000800a",
+ from: "0x000000000000000000000000000000000000800a",
+ to: "0x0000000000000000000000000000000000008001",
+ blockNumber: 10,
+ tokenType: TokenType.BaseToken,
+ }),
+ mock({
+ tokenAddress: "0x000000000000000000000000000000000000800a",
+ from: "0xd206eaf6819007535e893410cfa01885ce40e99a",
+ to: "0x0000000000000000000000000000000000000000",
+ blockNumber: 10,
+ tokenType: TokenType.BaseToken,
+ }),
+ mock({
+ tokenAddress: "0x2392e98fb47cf05773144db3ce8002fac4f39c84",
+ from: "0xd206eaf6819007535e893410cfa01885ce40e99a",
+ to: "0x0000000000000000000000000000000000000000",
+ blockNumber: 10,
+ tokenType: TokenType.ERC20,
+ }),
+ ];
+
+ balanceService.trackChangedBalances(transfers);
+ expect(balanceService.changedBalances.has(transfers[0].blockNumber)).toBe(true);
+ expect(balanceService.changedBalances.get(transfers[0].blockNumber).size).toBe(3);
+ const blockChangedBalances = balanceService.changedBalances.get(transfers[0].blockNumber);
+ expect(blockChangedBalances.has("0x0000000000000000000000000000000000008001")).toBe(true);
+ expect(blockChangedBalances.has("0x000000000000000000000000000000000000800a")).toBe(true);
+ expect(blockChangedBalances.has("0xd206eaf6819007535e893410cfa01885ce40e99a")).toBe(true);
+ expect(blockChangedBalances.has("0x0000000000000000000000000000000000000000")).toBe(false);
+ expect(
+ blockChangedBalances
+ .get("0x0000000000000000000000000000000000008001")
+ .has("0x2392e98fb47cf05773144db3ce8002fac4f39c84")
+ ).toBe(true);
+
+ expect(
+ blockChangedBalances
+ .get("0x000000000000000000000000000000000000800a")
+ .has("0x2392e98fb47cf05773144db3ce8002fac4f39c84")
+ ).toBe(true);
+
+ expect(
+ blockChangedBalances
+ .get("0x0000000000000000000000000000000000008001")
+ .has("0x000000000000000000000000000000000000800a")
+ ).toBe(true);
+
+ expect(
+ blockChangedBalances
+ .get("0x000000000000000000000000000000000000800a")
+ .has("0x000000000000000000000000000000000000800a")
+ ).toBe(true);
+
+ expect(
+ blockChangedBalances
+ .get("0xd206eaf6819007535e893410cfa01885ce40e99a")
+ .has("0x000000000000000000000000000000000000800a")
+ ).toBe(true);
+
+ expect(
+ blockChangedBalances
+ .get("0xd206eaf6819007535e893410cfa01885ce40e99a")
+ .has("0x2392e98fb47cf05773144db3ce8002fac4f39c84")
+ ).toBe(true);
+
+ expect(
+ blockChangedBalances
+ .get("0x0000000000000000000000000000000000008001")
+ .get("0x2392e98fb47cf05773144db3ce8002fac4f39c84")
+ ).toEqual({
+ balance: undefined,
+ tokenType: TokenType.ERC20,
+ });
+
+ expect(
+ blockChangedBalances
+ .get("0x000000000000000000000000000000000000800a")
+ .get("0x2392e98fb47cf05773144db3ce8002fac4f39c84")
+ ).toEqual({
+ balance: undefined,
+ tokenType: TokenType.ERC20,
+ });
+
+ expect(
+ blockChangedBalances
+ .get("0x0000000000000000000000000000000000008001")
+ .get("0x000000000000000000000000000000000000800a")
+ ).toEqual({
+ balance: undefined,
+ tokenType: TokenType.BaseToken,
+ });
+
+ expect(
+ blockChangedBalances
+ .get("0x000000000000000000000000000000000000800a")
+ .get("0x000000000000000000000000000000000000800a")
+ ).toEqual({
+ balance: undefined,
+ tokenType: TokenType.BaseToken,
+ });
+
+ expect(
+ blockChangedBalances
+ .get("0xd206eaf6819007535e893410cfa01885ce40e99a")
+ .get("0x000000000000000000000000000000000000800a")
+ ).toEqual({
+ balance: undefined,
+ tokenType: TokenType.BaseToken,
+ });
+
+ expect(
+ blockChangedBalances
+ .get("0xd206eaf6819007535e893410cfa01885ce40e99a")
+ .get("0x2392e98fb47cf05773144db3ce8002fac4f39c84")
+ ).toEqual({
+ balance: undefined,
+ tokenType: TokenType.ERC20,
+ });
+ });
+
+ it("tracks changed balance addresses for transfers", () => {
+ balanceService.trackChangedBalances(transfers);
+ expect(balanceService.changedBalances.has(transfers[0].blockNumber)).toBe(true);
+ const blockChangedBalances = balanceService.changedBalances.get(transfers[0].blockNumber);
+ expect(blockChangedBalances.size).toBe(3);
+ expect(blockChangedBalances.has("0x0000000000000000000000000000000000008001")).toBe(true);
+ expect(blockChangedBalances.has("0x36615cf349d7f6344891b1e7ca7c72883f5dc049")).toBe(true);
+ expect(blockChangedBalances.has("0xd206eaf6819007535e893410cfa01885ce40e99a")).toBe(true);
+ expect(
+ blockChangedBalances
+ .get("0x0000000000000000000000000000000000008001")
+ .has("0x000000000000000000000000000000000000800a")
+ ).toBe(true);
+ expect(
+ blockChangedBalances
+ .get("0x0000000000000000000000000000000000008001")
+ .get("0x000000000000000000000000000000000000800a")
+ ).toEqual({
+ balance: undefined,
+ tokenType: TokenType.BaseToken,
+ });
+ expect(
+ blockChangedBalances
+ .get("0x36615cf349d7f6344891b1e7ca7c72883f5dc049")
+ .has("0x000000000000000000000000000000000000800a")
+ ).toBe(true);
+ expect(
+ blockChangedBalances
+ .get("0x36615cf349d7f6344891b1e7ca7c72883f5dc049")
+ .get("0x000000000000000000000000000000000000800a")
+ ).toEqual({
+ balance: undefined,
+ tokenType: TokenType.BaseToken,
+ });
+ expect(
+ blockChangedBalances
+ .get("0x36615cf349d7f6344891b1e7ca7c72883f5dc049")
+ .has("0x2392e98fb47cf05773144db3ce8002fac4f39c84")
+ ).toBe(true);
+ expect(
+ blockChangedBalances
+ .get("0x36615cf349d7f6344891b1e7ca7c72883f5dc049")
+ .get("0x2392e98fb47cf05773144db3ce8002fac4f39c84")
+ ).toEqual({
+ balance: undefined,
+ tokenType: TokenType.ERC20,
+ });
+ expect(
+ blockChangedBalances
+ .get("0xd206eaf6819007535e893410cfa01885ce40e99a")
+ .has("0x000000000000000000000000000000000000800a")
+ ).toBe(true);
+ expect(
+ blockChangedBalances
+ .get("0xd206eaf6819007535e893410cfa01885ce40e99a")
+ .get("0x000000000000000000000000000000000000800a")
+ ).toEqual({
+ balance: undefined,
+ tokenType: TokenType.BaseToken,
+ });
+ });
+
+ it("merge changed balances with existing changed balances for the block", () => {
+ const existingBlockBalances = new Map>();
+ existingBlockBalances.set(
+ "0x0000000000000000000000000000000000008007",
+ new Map([
+ ["0x000000000000000000000000000000000000800a", { balance: undefined, tokenType: TokenType.BaseToken }],
+ ["0x0000000000000000000000000000000000008123", { balance: undefined, tokenType: TokenType.ERC20 }],
+ ])
+ );
+
+ existingBlockBalances.set(
+ "0x36615cf349d7f6344891b1e7ca7c72883f5dc049",
+ new Map([
+ ["0x000000000000000000000000000000000000800a", { balance: undefined, tokenType: TokenType.BaseToken }],
+ ])
+ );
+
+ balanceService.changedBalances.set(transfers[0].blockNumber, existingBlockBalances);
+
+ balanceService.trackChangedBalances(transfers);
+ expect(balanceService.changedBalances.has(transfers[0].blockNumber)).toBe(true);
+ const blockChangedBalances = balanceService.changedBalances.get(transfers[0].blockNumber);
+ expect(blockChangedBalances.size).toBe(4);
+ expect(blockChangedBalances.has("0x0000000000000000000000000000000000008007")).toBe(true);
+ expect(
+ blockChangedBalances
+ .get("0x0000000000000000000000000000000000008007")
+ .has("0x000000000000000000000000000000000000800a")
+ ).toBe(true);
+ expect(
+ blockChangedBalances
+ .get("0x0000000000000000000000000000000000008007")
+ .get("0x000000000000000000000000000000000000800a")
+ ).toEqual({
+ balance: undefined,
+ tokenType: TokenType.BaseToken,
+ });
+ expect(
+ blockChangedBalances
+ .get("0x0000000000000000000000000000000000008007")
+ .has("0x0000000000000000000000000000000000008123")
+ ).toBe(true);
+ expect(
+ blockChangedBalances
+ .get("0x0000000000000000000000000000000000008007")
+ .get("0x0000000000000000000000000000000000008123")
+ ).toEqual({
+ balance: undefined,
+ tokenType: TokenType.ERC20,
+ });
+ expect(blockChangedBalances.has("0x0000000000000000000000000000000000008001")).toBe(true);
+ expect(blockChangedBalances.has("0x36615cf349d7f6344891b1e7ca7c72883f5dc049")).toBe(true);
+ expect(blockChangedBalances.has("0xd206eaf6819007535e893410cfa01885ce40e99a")).toBe(true);
+ expect(
+ blockChangedBalances
+ .get("0x0000000000000000000000000000000000008001")
+ .has("0x000000000000000000000000000000000000800a")
+ ).toBe(true);
+ expect(
+ blockChangedBalances
+ .get("0x0000000000000000000000000000000000008001")
+ .get("0x000000000000000000000000000000000000800a")
+ ).toEqual({
+ balance: undefined,
+ tokenType: TokenType.BaseToken,
+ });
+ expect(
+ blockChangedBalances
+ .get("0x36615cf349d7f6344891b1e7ca7c72883f5dc049")
+ .has("0x000000000000000000000000000000000000800a")
+ ).toBe(true);
+ expect(
+ blockChangedBalances
+ .get("0x36615cf349d7f6344891b1e7ca7c72883f5dc049")
+ .get("0x000000000000000000000000000000000000800a")
+ ).toEqual({
+ balance: undefined,
+ tokenType: TokenType.BaseToken,
+ });
+ expect(
+ blockChangedBalances
+ .get("0x36615cf349d7f6344891b1e7ca7c72883f5dc049")
+ .has("0x2392e98fb47cf05773144db3ce8002fac4f39c84")
+ ).toBe(true);
+ expect(
+ blockChangedBalances
+ .get("0x36615cf349d7f6344891b1e7ca7c72883f5dc049")
+ .get("0x2392e98fb47cf05773144db3ce8002fac4f39c84")
+ ).toEqual({
+ balance: undefined,
+ tokenType: TokenType.ERC20,
+ });
+ expect(
+ blockChangedBalances
+ .get("0xd206eaf6819007535e893410cfa01885ce40e99a")
+ .has("0x000000000000000000000000000000000000800a")
+ ).toBe(true);
+ expect(
+ blockChangedBalances
+ .get("0xd206eaf6819007535e893410cfa01885ce40e99a")
+ .get("0x000000000000000000000000000000000000800a")
+ ).toEqual({
+ balance: undefined,
+ tokenType: TokenType.BaseToken,
+ });
+ });
+ });
+
+ describe("getChangedBalances", () => {
+ const blockNumber = 5;
+ const addresses = ["36615cf349d7f6344891b1e7ca7c72883f5dc049", "0000000000000000000000000000000000008001"];
+
+ const tokenAddresses = [
+ ["0x0000000000000000000000000000000000008001", "0x000000000000000000000000000000000000800a"],
+ ["0x36615cf349d7f6344891b1e7ca7c72883f5dc049", "0x000000000000000000000000000000000000800a"],
+ ];
+
+ beforeEach(() => {
+ const blockBalances = new Map>();
+ blockBalances.set(
+ utils.ETH_ADDRESS,
+ new Map([
+ [utils.ETH_ADDRESS, { balance: undefined, tokenType: TokenType.BaseToken }],
+ ])
+ );
+ blockBalances.set(
+ addresses[0],
+ new Map([
+ [tokenAddresses[0][0], { balance: undefined, tokenType: TokenType.ERC20 }],
+ [tokenAddresses[0][1], { balance: undefined, tokenType: TokenType.BaseToken }],
+ ])
+ );
+ blockBalances.set(
+ addresses[1],
+ new Map([
+ [tokenAddresses[1][0], { balance: undefined, tokenType: TokenType.ERC20 }],
+ [tokenAddresses[1][1], { balance: undefined, tokenType: TokenType.BaseToken }],
+ ])
+ );
+ balanceService.changedBalances.set(blockNumber, blockBalances);
+
+ jest.spyOn(blockchainServiceMock, "getBalance").mockResolvedValueOnce(BigNumber.from(1));
+ jest.spyOn(blockchainServiceMock, "getBalance").mockResolvedValueOnce(BigNumber.from(2));
+ jest.spyOn(blockchainServiceMock, "getBalance").mockResolvedValueOnce(BigNumber.from(3));
+ jest.spyOn(blockchainServiceMock, "getBalance").mockResolvedValueOnce(BigNumber.from(4));
+ jest.spyOn(blockchainServiceMock, "getBalance").mockResolvedValueOnce(BigNumber.from(5));
+ });
+
+ it("processes block number with no tracked balances", async () => {
+ await balanceService.getChangedBalances(blockNumber + 10);
+ expect(blockchainServiceMock.getBalance).toHaveBeenCalledTimes(0);
+ });
+
+ it("requests balances from the blockchain service", async () => {
+ await balanceService.getChangedBalances(blockNumber);
+ expect(blockchainServiceMock.getBalance).toHaveBeenCalledTimes(5);
+ expect(blockchainServiceMock.getBalance).toHaveBeenCalledWith(utils.ETH_ADDRESS, blockNumber, utils.ETH_ADDRESS);
+ expect(blockchainServiceMock.getBalance).toHaveBeenCalledWith(addresses[0], blockNumber, tokenAddresses[0][0]);
+ expect(blockchainServiceMock.getBalance).toHaveBeenCalledWith(addresses[0], blockNumber, tokenAddresses[0][1]);
+ expect(blockchainServiceMock.getBalance).toHaveBeenCalledWith(addresses[1], blockNumber, tokenAddresses[1][0]);
+ expect(blockchainServiceMock.getBalance).toHaveBeenCalledWith(addresses[1], blockNumber, tokenAddresses[1][1]);
+ });
+
+ it("returns changed balances", async () => {
+ const changedBalances = await balanceService.getChangedBalances(blockNumber);
+ expect(changedBalances).toEqual([
+ {
+ address: "0x0000000000000000000000000000000000000000",
+ blockNumber: 5,
+ tokenAddress: "0x0000000000000000000000000000000000000000",
+ balance: BigNumber.from(1),
+ tokenType: TokenType.BaseToken,
+ },
+ {
+ address: addresses[0],
+ blockNumber: 5,
+ tokenAddress: tokenAddresses[0][0],
+ balance: BigNumber.from(2),
+ tokenType: TokenType.ERC20,
+ },
+ {
+ address: addresses[0],
+ blockNumber: 5,
+ tokenAddress: tokenAddresses[0][1],
+ balance: BigNumber.from(3),
+ tokenType: TokenType.BaseToken,
+ },
+ {
+ address: addresses[1],
+ blockNumber: 5,
+ tokenAddress: tokenAddresses[1][0],
+ balance: BigNumber.from(4),
+ tokenType: TokenType.ERC20,
+ },
+ {
+ address: addresses[1],
+ blockNumber: 5,
+ tokenAddress: tokenAddresses[1][1],
+ balance: BigNumber.from(5),
+ tokenType: TokenType.BaseToken,
+ },
+ ]);
+ });
+
+ describe("when some getBalance throw errors", () => {
+ beforeEach(() => {
+ jest.spyOn(blockchainServiceMock, "getBalance").mockReset();
+ jest.spyOn(blockchainServiceMock, "getBalance").mockResolvedValueOnce(BigNumber.from(1));
+ jest.spyOn(blockchainServiceMock, "getBalance").mockResolvedValueOnce(BigNumber.from(2));
+ jest.spyOn(blockchainServiceMock, "getBalance").mockRejectedValueOnce("balanceOf function is not defined");
+ jest.spyOn(blockchainServiceMock, "getBalance").mockResolvedValueOnce(BigNumber.from(4));
+ jest.spyOn(blockchainServiceMock, "getBalance").mockResolvedValueOnce(BigNumber.from(5));
+ });
+
+ it("returns only successfully fetched balances", async () => {
+ const changedBalances = await balanceService.getChangedBalances(blockNumber);
+ expect(changedBalances).toEqual([
+ {
+ address: "0x0000000000000000000000000000000000000000",
+ blockNumber: 5,
+ tokenAddress: "0x0000000000000000000000000000000000000000",
+ balance: BigNumber.from(1),
+ tokenType: TokenType.BaseToken,
+ },
+ {
+ address: addresses[0],
+ blockNumber: 5,
+ tokenAddress: tokenAddresses[0][0],
+ balance: BigNumber.from(2),
+ tokenType: TokenType.ERC20,
+ },
+ {
+ address: addresses[1],
+ blockNumber: 5,
+ tokenAddress: tokenAddresses[1][0],
+ balance: BigNumber.from(4),
+ tokenType: TokenType.ERC20,
+ },
+ {
+ address: addresses[1],
+ blockNumber: 5,
+ tokenAddress: tokenAddresses[1][1],
+ balance: BigNumber.from(5),
+ tokenType: TokenType.BaseToken,
+ },
+ ]);
+ });
+ });
+ });
+});
diff --git a/packages/data-fetcher/src/balance/balance.service.ts b/packages/data-fetcher/src/balance/balance.service.ts
new file mode 100644
index 0000000000..8876adb6bb
--- /dev/null
+++ b/packages/data-fetcher/src/balance/balance.service.ts
@@ -0,0 +1,122 @@
+import { Injectable, Logger } from "@nestjs/common";
+import { BigNumber } from "ethers";
+import { utils } from "zksync-web3";
+import { BlockchainService } from "../blockchain/blockchain.service";
+import { TokenType } from "../token/token.service";
+import { Transfer } from "../transfer/interfaces/transfer.interface";
+
+export type BlockChangedBalances = Map>;
+
+export interface Balance {
+ address: string;
+ tokenAddress: string;
+ blockNumber: number;
+ balance: BigNumber;
+ tokenType: TokenType;
+}
+
+@Injectable()
+export class BalanceService {
+ private readonly logger: Logger;
+ public changedBalances: Map;
+
+ constructor(private readonly blockchainService: BlockchainService) {
+ this.logger = new Logger(BalanceService.name);
+ this.changedBalances = new Map();
+ }
+
+ public clearTrackedState(blockNumber: number): void {
+ this.changedBalances.delete(blockNumber);
+ }
+
+ public trackChangedBalances(transfers: Transfer[]): void {
+ if (!transfers?.length) {
+ return;
+ }
+
+ const blockChangedBalances =
+ this.changedBalances.get(transfers[0].blockNumber) ||
+ new Map>();
+
+ for (const transfer of transfers) {
+ const changedBalancesAddresses = new Set([transfer.from, transfer.to]);
+ for (const changedBalanceAddress of changedBalancesAddresses) {
+ if (changedBalanceAddress === utils.ETH_ADDRESS) {
+ continue;
+ }
+
+ if (!blockChangedBalances.has(changedBalanceAddress)) {
+ blockChangedBalances.set(
+ changedBalanceAddress,
+ new Map()
+ );
+ }
+
+ blockChangedBalances
+ .get(changedBalanceAddress)
+ .set(transfer.tokenAddress, { balance: undefined, tokenType: transfer.tokenType });
+ }
+ }
+
+ this.changedBalances.set(transfers[0].blockNumber, blockChangedBalances);
+ }
+
+ public async getChangedBalances(blockNumber: number): Promise {
+ if (!this.changedBalances.has(blockNumber)) {
+ return null;
+ }
+
+ const blockChangedBalances = this.changedBalances.get(blockNumber);
+ const balanceAddresses: string[][] = [];
+ const getBalancePromises: Promise[] = [];
+
+ for (const [address, tokenAddresses] of blockChangedBalances) {
+ for (const [tokenAddress] of tokenAddresses) {
+ balanceAddresses.push([address, tokenAddress]);
+ getBalancePromises.push(this.blockchainService.getBalance(address, blockNumber, tokenAddress));
+ }
+ }
+
+ this.logger.debug({ message: "Getting balances from the blockchain.", blockNumber });
+ const balances = await Promise.allSettled(getBalancePromises);
+
+ for (let i = 0; i < balances.length; i++) {
+ const [address, tokenAddress] = balanceAddresses[i];
+ if (balances[i].status === "fulfilled") {
+ const blockChangedBalancesForAddress = blockChangedBalances.get(address);
+ blockChangedBalancesForAddress.set(tokenAddress, {
+ balance: (balances[i] as PromiseFulfilledResult).value,
+ tokenType: blockChangedBalancesForAddress.get(tokenAddress).tokenType,
+ });
+ } else {
+ this.logger.warn({
+ message: "Get balance for token failed",
+ tokenAddress,
+ address,
+ blockNumber,
+ reason: (balances[i] as PromiseRejectedResult).reason,
+ });
+ // since we have internal retries for contract and RPC calls if an error gets through to here
+ // it means it is not retryable and happens because balanceOf function is not defined or fails with a permanent error in the contract
+ // in such case we're not saving the balance at all
+ blockChangedBalances.get(address).delete(tokenAddress);
+ }
+ }
+
+ const balanceRecords: Balance[] = [];
+
+ for (const [address, addressTokenBalances] of blockChangedBalances) {
+ for (const [tokenAddress, addressTokenBalance] of addressTokenBalances) {
+ balanceRecords.push({
+ address,
+ tokenAddress,
+ blockNumber,
+ balance: addressTokenBalance.balance,
+ tokenType: addressTokenBalance.tokenType,
+ });
+ }
+ }
+
+ return balanceRecords;
+ }
+}
diff --git a/packages/data-fetcher/src/balance/index.ts b/packages/data-fetcher/src/balance/index.ts
new file mode 100644
index 0000000000..ea2baee9f5
--- /dev/null
+++ b/packages/data-fetcher/src/balance/index.ts
@@ -0,0 +1 @@
+export * from "./balance.service";
diff --git a/packages/data-fetcher/src/block/block.controller.spec.ts b/packages/data-fetcher/src/block/block.controller.spec.ts
new file mode 100644
index 0000000000..3abe2b5662
--- /dev/null
+++ b/packages/data-fetcher/src/block/block.controller.spec.ts
@@ -0,0 +1,89 @@
+import { Test } from "@nestjs/testing";
+import { mock } from "jest-mock-extended";
+import { ConfigService } from "@nestjs/config";
+import { BadRequestException } from "@nestjs/common";
+import { BlockController } from "./block.controller";
+import { BlockService } from "./block.service";
+
+describe("BlockController", () => {
+ let controller: BlockController;
+ let configServiceMock: ConfigService;
+ let blockServiceMock: BlockService;
+
+ beforeEach(async () => {
+ configServiceMock = mock({
+ get: jest.fn().mockReturnValue(10),
+ });
+ blockServiceMock = mock();
+ const module = await Test.createTestingModule({
+ controllers: [BlockController],
+ providers: [
+ {
+ provide: ConfigService,
+ useValue: configServiceMock,
+ },
+ {
+ provide: BlockService,
+ useValue: blockServiceMock,
+ },
+ ],
+ }).compile();
+ controller = module.get(BlockController);
+ });
+
+ describe("getBlocks", () => {
+ it("throws an error if requested blocks range is larger than allowed", async () => {
+ await expect(controller.getBlocks(0, 11)).rejects.toThrowError(
+ new BadRequestException(`Max supported batch is 10.`)
+ );
+ });
+
+ it("throws an error if to block number is less than from block number", async () => {
+ await expect(controller.getBlocks(1, 0)).rejects.toThrowError(
+ new BadRequestException(`To block is less than from block.`)
+ );
+ });
+
+ it("returns blocks information for requested blocks", async () => {
+ const blockDetails = [
+ {
+ block: { number: 3 },
+ },
+ {
+ block: { number: 4 },
+ },
+ {
+ block: { number: 5 },
+ },
+ ];
+
+ (blockServiceMock.getData as jest.Mock).mockResolvedValueOnce(blockDetails[0]);
+ (blockServiceMock.getData as jest.Mock).mockResolvedValueOnce(blockDetails[1]);
+ (blockServiceMock.getData as jest.Mock).mockResolvedValueOnce(blockDetails[2]);
+
+ const blocksData = await controller.getBlocks(3, 5);
+ expect(blockServiceMock.getData).toHaveBeenCalledTimes(3);
+ expect(blockServiceMock.getData).toHaveBeenCalledWith(3);
+ expect(blockServiceMock.getData).toHaveBeenCalledWith(4);
+ expect(blockServiceMock.getData).toHaveBeenCalledWith(5);
+
+ expect(blocksData).toEqual(blockDetails);
+ });
+
+ it("returns block information if only from block is specified", async () => {
+ const blockDetails = [
+ {
+ block: { number: 3 },
+ },
+ ];
+
+ (blockServiceMock.getData as jest.Mock).mockResolvedValueOnce(blockDetails[0]);
+
+ const blocksData = await controller.getBlocks(3);
+ expect(blockServiceMock.getData).toHaveBeenCalledTimes(1);
+ expect(blockServiceMock.getData).toHaveBeenCalledWith(3);
+
+ expect(blocksData).toEqual(blockDetails);
+ });
+ });
+});
diff --git a/packages/data-fetcher/src/block/block.controller.ts b/packages/data-fetcher/src/block/block.controller.ts
new file mode 100644
index 0000000000..333aa84a32
--- /dev/null
+++ b/packages/data-fetcher/src/block/block.controller.ts
@@ -0,0 +1,37 @@
+import { Controller, Get, Query, BadRequestException } from "@nestjs/common";
+import { ConfigService } from "@nestjs/config";
+import { ParseLimitedIntPipe } from "../common/pipes/parseLimitedInt.pipe";
+import { BlockService, BlockData } from "./";
+
+@Controller("blocks")
+export class BlockController {
+ private readonly maxBlocksBatchSize: number;
+
+ constructor(configService: ConfigService, private readonly blockService: BlockService) {
+ this.maxBlocksBatchSize = configService.get("maxBlocksBatchSize");
+ }
+
+ @Get("")
+ public async getBlocks(
+ @Query("from", new ParseLimitedIntPipe({ min: 0 })) from: number,
+ @Query("to", new ParseLimitedIntPipe({ min: 0, isOptional: true })) to?: number | null
+ ): Promise {
+ to = to != null ? to : from;
+
+ if (to < from) {
+ throw new BadRequestException("To block is less than from block.");
+ }
+
+ // +1 since from and to are inclusive
+ if (to - from + 1 > this.maxBlocksBatchSize) {
+ throw new BadRequestException(`Max supported batch is ${this.maxBlocksBatchSize}.`);
+ }
+
+ const getBlockDataPromises = [];
+ for (let blockNumber = from; blockNumber <= to; blockNumber++) {
+ getBlockDataPromises.push(this.blockService.getData(blockNumber));
+ }
+
+ return await Promise.all(getBlockDataPromises);
+ }
+}
diff --git a/packages/data-fetcher/src/block/block.service.spec.ts b/packages/data-fetcher/src/block/block.service.spec.ts
new file mode 100644
index 0000000000..a4c4291d8b
--- /dev/null
+++ b/packages/data-fetcher/src/block/block.service.spec.ts
@@ -0,0 +1,315 @@
+import { Test } from "@nestjs/testing";
+import { Logger } from "@nestjs/common";
+import { types } from "zksync-web3";
+import { mock } from "jest-mock-extended";
+import { BigNumber } from "ethers";
+import { TransactionService } from "../transaction";
+import { LogService } from "../log";
+import { BlockchainService } from "../blockchain";
+import { BalanceService } from "../balance";
+import { BlockService } from "./";
+import { TokenType } from "../token/token.service";
+
+describe("BlockService", () => {
+ let blockService: BlockService;
+ let blockchainServiceMock: BlockchainService;
+ let transactionServiceMock: TransactionService;
+ let logServiceMock: LogService;
+ let balanceServiceMock: BalanceService;
+
+ let startGetBlockInfoDurationMetricMock: jest.Mock;
+ let stopGetBlockInfoDurationMetricMock: jest.Mock;
+
+ let startBlockDurationMetricMock: jest.Mock;
+ let stopBlockDurationMetricMock: jest.Mock;
+
+ let startBalancesDurationMetricMock: jest.Mock;
+ let stopBalancesDurationMetricMock: jest.Mock;
+
+ const getBlockService = async () => {
+ const app = await Test.createTestingModule({
+ providers: [
+ BlockService,
+ {
+ provide: BlockchainService,
+ useValue: blockchainServiceMock,
+ },
+ {
+ provide: TransactionService,
+ useValue: transactionServiceMock,
+ },
+ {
+ provide: LogService,
+ useValue: logServiceMock,
+ },
+ {
+ provide: BalanceService,
+ useValue: balanceServiceMock,
+ },
+ {
+ provide: "PROM_METRIC_BLOCK_PROCESSING_DURATION_SECONDS",
+ useValue: {
+ startTimer: startBlockDurationMetricMock,
+ },
+ },
+ {
+ provide: "PROM_METRIC_BALANCES_PROCESSING_DURATION_SECONDS",
+ useValue: {
+ startTimer: startBalancesDurationMetricMock,
+ },
+ },
+ {
+ provide: "PROM_METRIC_GET_BLOCK_INFO_DURATION_SECONDS",
+ useValue: {
+ startTimer: startGetBlockInfoDurationMetricMock,
+ },
+ },
+ ],
+ }).compile();
+
+ app.useLogger(mock());
+
+ return app.get(BlockService);
+ };
+
+ const transactionData = [
+ {
+ transaction: {
+ hash: "transactionHash1",
+ },
+ },
+ {
+ transaction: {
+ hash: "transactionHash2",
+ },
+ },
+ ];
+
+ const blockLogData = {
+ logs: [{ logIndex: 0 }, { logIndex: 1 }],
+ transfers: [{ logIndex: 0 }, { logIndex: 1 }],
+ };
+
+ const blockInfoData = {
+ hash: "hash",
+ transactions: ["transactionHash1", "transactionHash2"],
+ };
+
+ const blockDetailsData = {
+ blockHash: "blockHash",
+ };
+
+ const blockChangedBalances = [
+ {
+ address: "0x0000000000000000000000000000000000000000",
+ blockNumber: 5,
+ tokenAddress: "0x0000000000000000000000000000000000000000",
+ balance: BigNumber.from(1),
+ tokenType: TokenType.BaseToken,
+ },
+ ];
+
+ beforeEach(async () => {
+ blockchainServiceMock = mock({
+ getBlock: jest.fn().mockResolvedValue(blockInfoData),
+ getBlockDetails: jest.fn().mockResolvedValue(blockDetailsData),
+ getLogs: jest.fn().mockResolvedValue([]),
+ });
+ transactionServiceMock = mock({
+ getData: jest.fn().mockResolvedValueOnce(transactionData[0]).mockResolvedValueOnce(transactionData[1]),
+ });
+ logServiceMock = mock({
+ getData: jest.fn().mockResolvedValue(blockLogData),
+ });
+ balanceServiceMock = mock({
+ getChangedBalances: jest.fn().mockResolvedValueOnce(blockChangedBalances),
+ clearTrackedState: jest.fn(),
+ });
+
+ stopGetBlockInfoDurationMetricMock = jest.fn();
+ startGetBlockInfoDurationMetricMock = jest.fn().mockReturnValue(stopGetBlockInfoDurationMetricMock);
+
+ stopBlockDurationMetricMock = jest.fn();
+ startBlockDurationMetricMock = jest.fn().mockReturnValue(stopBlockDurationMetricMock);
+
+ stopBalancesDurationMetricMock = jest.fn();
+ startBalancesDurationMetricMock = jest.fn().mockReturnValue(stopBalancesDurationMetricMock);
+
+ blockService = await getBlockService();
+ });
+
+ const blockNumber = 1;
+
+ describe("getData", () => {
+ it("starts the get block info metric", async () => {
+ await blockService.getData(blockNumber);
+ expect(startGetBlockInfoDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("returns data with block and block details info", async () => {
+ const blockData = await blockService.getData(blockNumber);
+ expect(blockchainServiceMock.getBlock).toHaveBeenCalledTimes(1);
+ expect(blockchainServiceMock.getBlock).toHaveBeenCalledWith(blockNumber);
+ expect(blockchainServiceMock.getBlockDetails).toHaveBeenCalledTimes(1);
+ expect(blockchainServiceMock.getBlockDetails).toHaveBeenCalledWith(blockNumber);
+ expect(blockData.block).toEqual(blockInfoData);
+ expect(blockData.blockDetails).toEqual(blockDetailsData);
+ });
+
+ it("stops the get block info metric", async () => {
+ await blockService.getData(blockNumber);
+ expect(stopGetBlockInfoDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("starts the block processing duration metric", async () => {
+ await blockService.getData(blockNumber);
+ expect(startBlockDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("returns data with block info", async () => {
+ const blockData = await blockService.getData(blockNumber);
+ expect(blockData.block).toEqual(blockInfoData);
+ expect(blockData.blockDetails).toEqual(blockDetailsData);
+ });
+
+ it("returns null if block does not exist in blockchain", async () => {
+ (blockchainServiceMock.getBlock as jest.Mock).mockResolvedValue(null);
+ const blockData = await blockService.getData(blockNumber);
+ expect(blockData).toBeNull();
+ });
+
+ it("returns null if block details does not exist in blockchain", async () => {
+ (blockchainServiceMock.getBlockDetails as jest.Mock).mockResolvedValue(null);
+ const blockData = await blockService.getData(blockNumber);
+ expect(blockData).toBeNull();
+ });
+
+ it("returns block transactions data", async () => {
+ const blockData = await blockService.getData(blockNumber);
+ expect(transactionServiceMock.getData).toHaveBeenCalledTimes(2);
+ expect(transactionServiceMock.getData).toHaveBeenCalledWith(blockInfoData.transactions[0], blockDetailsData);
+ expect(transactionServiceMock.getData).toHaveBeenCalledWith(blockInfoData.transactions[1], blockDetailsData);
+ expect(blockData.transactions).toEqual(transactionData);
+ });
+
+ describe("when processing fails with an error", () => {
+ beforeEach(() => {
+ jest.spyOn(transactionServiceMock, "getData").mockReset();
+ jest.spyOn(transactionServiceMock, "getData").mockRejectedValue(new Error("log service error"));
+ });
+
+ it("throws the generated error", async () => {
+ await expect(blockService.getData(blockNumber)).rejects.toThrowError(new Error("log service error"));
+ });
+
+ it("stops block processing duration metric and sets label to error", async () => {
+ expect.assertions(2);
+ try {
+ await blockService.getData(blockNumber);
+ } catch {
+ expect(stopBlockDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopBlockDurationMetricMock).toHaveBeenCalledWith({
+ status: "error",
+ action: "get",
+ });
+ }
+ });
+
+ it("clears tracked address changes state", async () => {
+ expect.assertions(2);
+ try {
+ await blockService.getData(blockNumber);
+ } catch {
+ expect(balanceServiceMock.clearTrackedState).toHaveBeenCalledTimes(1);
+ expect(balanceServiceMock.clearTrackedState).toHaveBeenCalledWith(blockNumber);
+ }
+ });
+ });
+
+ describe("when block does not contain transactions", () => {
+ let logs: types.Log[];
+ beforeEach(() => {
+ const blockData = {
+ ...blockInfoData,
+ transactions: [],
+ } as types.Block;
+ jest.spyOn(blockchainServiceMock, "getBlock").mockReset();
+ jest.spyOn(blockchainServiceMock, "getBlock").mockResolvedValueOnce(blockData);
+ logs = [{ logIndex: 0 } as types.Log, { logIndex: 1 } as types.Log];
+ jest.spyOn(blockchainServiceMock, "getLogs").mockResolvedValueOnce(logs);
+ });
+
+ it("reads logs for block from the blockchain", async () => {
+ await blockService.getData(blockNumber);
+ expect(blockchainServiceMock.getLogs).toHaveBeenCalledTimes(1);
+ expect(blockchainServiceMock.getLogs).toHaveBeenCalledWith({
+ fromBlock: blockNumber,
+ toBlock: blockNumber,
+ });
+ });
+
+ it("gets and returns block data", async () => {
+ const blockData = await blockService.getData(blockNumber);
+ expect(logServiceMock.getData).toHaveBeenCalledTimes(1);
+ expect(logServiceMock.getData).toHaveBeenCalledWith(logs, blockDetailsData);
+ expect(blockData.blockLogs).toEqual(blockLogData.logs);
+ expect(blockData.blockTransfers).toEqual(blockLogData.transfers);
+ });
+ });
+
+ it("starts the balances duration metric", async () => {
+ await blockService.getData(blockNumber);
+ expect(startBalancesDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("returns changed balances", async () => {
+ const blockData = await blockService.getData(blockNumber);
+ expect(balanceServiceMock.getChangedBalances).toHaveBeenCalledTimes(1);
+ expect(balanceServiceMock.getChangedBalances).toHaveBeenCalledWith(blockNumber);
+ expect(blockData.changedBalances).toEqual(blockChangedBalances);
+ });
+
+ it("returns empty array as changed balances if there are no any", async () => {
+ (balanceServiceMock.getChangedBalances as jest.Mock).mockReset();
+ (balanceServiceMock.getChangedBalances as jest.Mock).mockResolvedValue(null);
+ const blockData = await blockService.getData(blockNumber);
+ expect(balanceServiceMock.getChangedBalances).toHaveBeenCalledTimes(1);
+ expect(balanceServiceMock.getChangedBalances).toHaveBeenCalledWith(blockNumber);
+ expect(blockData.changedBalances).toEqual([]);
+ });
+
+ it("stops the balances duration metric", async () => {
+ await blockService.getData(blockNumber);
+ expect(stopBalancesDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("stops the duration metric", async () => {
+ await blockService.getData(blockNumber);
+ expect(stopBlockDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("sets duration metric label to success", async () => {
+ await blockService.getData(blockNumber);
+ expect(stopBlockDurationMetricMock).toHaveBeenCalledWith({
+ status: "success",
+ action: "get",
+ });
+ });
+
+ it("clears tracked address changes state", async () => {
+ await blockService.getData(blockNumber);
+ expect(balanceServiceMock.clearTrackedState).toHaveBeenCalledTimes(1);
+ expect(balanceServiceMock.clearTrackedState).toHaveBeenCalledWith(blockNumber);
+ });
+
+ it("returns empty block logs array", async () => {
+ const blockData = await blockService.getData(blockNumber);
+ expect(blockData.blockLogs).toEqual([]);
+ });
+
+ it("returns empty block transfers array", async () => {
+ const blockData = await blockService.getData(blockNumber);
+ expect(blockData.blockTransfers).toEqual([]);
+ });
+ });
+});
diff --git a/packages/data-fetcher/src/block/block.service.ts b/packages/data-fetcher/src/block/block.service.ts
new file mode 100644
index 0000000000..5c1bf7eae7
--- /dev/null
+++ b/packages/data-fetcher/src/block/block.service.ts
@@ -0,0 +1,103 @@
+import { Injectable, Logger } from "@nestjs/common";
+import { InjectMetric } from "@willsoto/nestjs-prometheus";
+import { Histogram } from "prom-client";
+import { types } from "zksync-web3";
+import { BlockchainService } from "../blockchain/blockchain.service";
+import { BalanceService, Balance } from "../balance/balance.service";
+import { TransactionService, TransactionData } from "../transaction";
+import { Transfer } from "../transfer/interfaces/transfer.interface";
+import { LogService, LogsData } from "../log";
+import {
+ BLOCK_PROCESSING_DURATION_METRIC_NAME,
+ BALANCES_PROCESSING_DURATION_METRIC_NAME,
+ GET_BLOCK_INFO_DURATION_METRIC_NAME,
+ BlockProcessingMetricLabels,
+ ProcessingActionMetricLabel,
+} from "../metrics";
+
+export interface BlockData {
+ block: types.Block;
+ blockDetails: types.BlockDetails;
+ transactions: TransactionData[];
+ blockLogs: types.Log[];
+ blockTransfers: Transfer[];
+ changedBalances: Balance[];
+}
+
+@Injectable()
+export class BlockService {
+ private readonly logger: Logger;
+
+ public constructor(
+ private readonly blockchainService: BlockchainService,
+ private readonly transactionService: TransactionService,
+ private readonly logService: LogService,
+ private readonly balanceService: BalanceService,
+ @InjectMetric(BLOCK_PROCESSING_DURATION_METRIC_NAME)
+ private readonly processingDurationMetric: Histogram,
+ @InjectMetric(BALANCES_PROCESSING_DURATION_METRIC_NAME)
+ private readonly balancesProcessingDurationMetric: Histogram,
+ @InjectMetric(GET_BLOCK_INFO_DURATION_METRIC_NAME)
+ private readonly getBlockInfoDurationMetric: Histogram
+ ) {
+ this.logger = new Logger(BlockService.name);
+ }
+
+ public async getData(blockNumber: number): Promise {
+ const stopDurationMeasuring = this.processingDurationMetric.startTimer();
+
+ this.logger.debug({ message: "Getting block data from the blockchain", blockNumber });
+ const stopGetBlockInfoDurationMetric = this.getBlockInfoDurationMetric.startTimer();
+ const [block, blockDetails] = await Promise.all([
+ this.blockchainService.getBlock(blockNumber),
+ this.blockchainService.getBlockDetails(blockNumber),
+ ]);
+ stopGetBlockInfoDurationMetric();
+
+ if (!block || !blockDetails) {
+ return null;
+ }
+
+ let blockProcessingStatus = "success";
+ let transactions: TransactionData[] = [];
+ let blockLogData: LogsData;
+ let changedBalances: Balance[];
+ let blockLogs: types.Log[];
+
+ try {
+ transactions = await Promise.all(
+ block.transactions.map((transactionHash) => this.transactionService.getData(transactionHash, blockDetails))
+ );
+
+ if (block.transactions.length === 0) {
+ blockLogs = await this.blockchainService.getLogs({
+ fromBlock: blockNumber,
+ toBlock: blockNumber,
+ });
+
+ blockLogData = await this.logService.getData(blockLogs, blockDetails);
+ }
+
+ const stopBalancesDurationMeasuring = this.balancesProcessingDurationMetric.startTimer();
+ this.logger.debug({ message: "Getting balances", blockNumber });
+ changedBalances = await this.balanceService.getChangedBalances(blockNumber);
+ stopBalancesDurationMeasuring();
+ } catch (error) {
+ blockProcessingStatus = "error";
+ throw error;
+ } finally {
+ this.balanceService.clearTrackedState(blockNumber);
+ stopDurationMeasuring({ status: blockProcessingStatus, action: "get" });
+ }
+
+ this.logger.debug({ message: "Successfully generated block data", blockNumber });
+ return {
+ block,
+ blockDetails,
+ blockLogs: blockLogs || [],
+ blockTransfers: blockLogData?.transfers || [],
+ transactions,
+ changedBalances: changedBalances || [],
+ };
+ }
+}
diff --git a/packages/data-fetcher/src/block/index.ts b/packages/data-fetcher/src/block/index.ts
new file mode 100644
index 0000000000..7230f09c4f
--- /dev/null
+++ b/packages/data-fetcher/src/block/index.ts
@@ -0,0 +1,2 @@
+export * from "./block.service";
+export * from "./block.controller";
diff --git a/packages/data-fetcher/src/blockchain/blockchain.service.spec.ts b/packages/data-fetcher/src/blockchain/blockchain.service.spec.ts
new file mode 100644
index 0000000000..a496fd4dcd
--- /dev/null
+++ b/packages/data-fetcher/src/blockchain/blockchain.service.spec.ts
@@ -0,0 +1,2396 @@
+import * as ethers from "ethers";
+import { mock } from "jest-mock-extended";
+import { utils, types } from "zksync-web3";
+import { Test, TestingModule } from "@nestjs/testing";
+import { Logger } from "@nestjs/common";
+import { ConfigService } from "@nestjs/config";
+import * as timersPromises from "timers/promises";
+import { BlockchainService, BridgeAddresses } from "./blockchain.service";
+import { JsonRpcProviderBase } from "../rpcProvider";
+import { RetryableContract } from "./retryableContract";
+
+jest.mock("./retryableContract");
+const metricProviderKey = "PROM_METRIC_BLOCKCHAIN_RPC_CALL_DURATION_SECONDS";
+
+describe("BlockchainService", () => {
+ let app: TestingModule;
+ const l2Erc20Bridge = "l2Erc20Bridge";
+ let blockchainService: BlockchainService;
+ let provider: JsonRpcProviderBase;
+ let providerFormatterMock;
+ let configServiceMock: ConfigService;
+ let startRpcCallDurationMetricMock: jest.Mock;
+ let stopRpcCallDurationMetricMock: jest.Mock;
+ const defaultRetryTimeout = 2;
+ const quickRetryTimeout = 1;
+ const retriesMaxTotalTimeout = 100;
+
+ beforeEach(async () => {
+ providerFormatterMock = {
+ blockTag: jest.fn(),
+ };
+
+ provider = mock({
+ formatter: providerFormatterMock,
+ });
+
+ configServiceMock = mock({
+ get: jest.fn().mockImplementation((configName) => {
+ switch (configName) {
+ case "blockchain.rpcCallDefaultRetryTimeout":
+ return defaultRetryTimeout;
+ case "blockchain.rpcCallQuickRetryTimeout":
+ return quickRetryTimeout;
+ case "blockchain.rpcCallRetriesMaxTotalTimeout":
+ return retriesMaxTotalTimeout;
+ }
+ }),
+ });
+
+ stopRpcCallDurationMetricMock = jest.fn();
+ startRpcCallDurationMetricMock = jest.fn().mockReturnValue(stopRpcCallDurationMetricMock);
+
+ app = await Test.createTestingModule({
+ providers: [
+ BlockchainService,
+ {
+ provide: ConfigService,
+ useValue: configServiceMock,
+ },
+ {
+ provide: JsonRpcProviderBase,
+ useValue: provider,
+ },
+ {
+ provide: metricProviderKey,
+ useValue: {
+ startTimer: startRpcCallDurationMetricMock,
+ },
+ },
+ ],
+ }).compile();
+
+ app.useLogger(mock());
+
+ blockchainService = app.get(BlockchainService);
+
+ blockchainService.bridgeAddresses = mock({
+ l2Erc20DefaultBridge: l2Erc20Bridge.toLowerCase(),
+ });
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe("getL1BatchNumber", () => {
+ const batchNumber = 10;
+ let timeoutSpy;
+
+ beforeEach(() => {
+ jest.spyOn(provider, "getL1BatchNumber").mockResolvedValue(batchNumber);
+ timeoutSpy = jest.spyOn(timersPromises, "setTimeout");
+ });
+
+ it("starts the rpc call duration metric", async () => {
+ await blockchainService.getL1BatchNumber();
+ expect(startRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets batch number", async () => {
+ await blockchainService.getL1BatchNumber();
+ expect(provider.getL1BatchNumber).toHaveBeenCalledTimes(1);
+ });
+
+ it("stops the rpc call duration metric", async () => {
+ await blockchainService.getL1BatchNumber();
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getL1BatchNumber" });
+ });
+
+ it("returns the batch number", async () => {
+ const result = await blockchainService.getL1BatchNumber();
+ expect(result).toEqual(batchNumber);
+ });
+
+ describe("if the call throws an error", () => {
+ const error = new Error("RPC call error");
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getL1BatchNumber")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(batchNumber);
+ });
+
+ it("retries RPC call with a default timeout", async () => {
+ await blockchainService.getL1BatchNumber();
+ expect(provider.getL1BatchNumber).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.getL1BatchNumber();
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getL1BatchNumber" });
+ });
+
+ it("returns result of the successful RPC call", async () => {
+ const result = await blockchainService.getL1BatchNumber();
+ expect(result).toEqual(batchNumber);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getL1BatchNumber()).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a timeout error", () => {
+ const error = new Error();
+ (error as any).code = "TIMEOUT";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getL1BatchNumber")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(batchNumber);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getL1BatchNumber();
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getL1BatchNumber()).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a connection refused error", () => {
+ const error = new Error();
+ (error as any).code = "ECONNREFUSED";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getL1BatchNumber")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(batchNumber);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getL1BatchNumber();
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getL1BatchNumber()).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a connection reset error", () => {
+ const error = new Error();
+ (error as any).code = "ECONNRESET";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getL1BatchNumber")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(batchNumber);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getL1BatchNumber();
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getL1BatchNumber()).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a network error", () => {
+ const error = new Error();
+ (error as any).code = "NETWORK_ERROR";
+
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getL1BatchNumber")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(batchNumber);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getL1BatchNumber();
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getL1BatchNumber()).rejects.toThrowError(error);
+ });
+ });
+ });
+ });
+
+ describe("getBatchDetails", () => {
+ const batchNumber = 10;
+ const batchDetails: types.BatchDetails = mock({ number: 10 });
+ let timeoutSpy;
+
+ beforeEach(() => {
+ jest.spyOn(provider, "getL1BatchDetails").mockResolvedValue(batchDetails);
+ timeoutSpy = jest.spyOn(timersPromises, "setTimeout");
+ });
+
+ it("starts the rpc call duration metric", async () => {
+ await blockchainService.getL1BatchDetails(batchNumber);
+ expect(startRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets batch details by the specified batch number", async () => {
+ await blockchainService.getL1BatchDetails(batchNumber);
+ expect(provider.getL1BatchDetails).toHaveBeenCalledTimes(1);
+ expect(provider.getL1BatchDetails).toHaveBeenCalledWith(batchNumber);
+ });
+
+ it("stops the rpc call duration metric", async () => {
+ await blockchainService.getL1BatchDetails(batchNumber);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getL1BatchDetails" });
+ });
+
+ it("returns the batch details", async () => {
+ const result = await blockchainService.getL1BatchDetails(batchNumber);
+ expect(result).toEqual(batchDetails);
+ });
+
+ it("sets default committedAt, provenAt and executedAt for the very first batch", async () => {
+ jest.spyOn(provider, "getL1BatchDetails").mockResolvedValueOnce({ number: 0 } as types.BatchDetails);
+ const result = await blockchainService.getL1BatchDetails(0);
+ expect(result).toEqual({
+ number: 0,
+ committedAt: new Date(0),
+ provenAt: new Date(0),
+ executedAt: new Date(0),
+ });
+ });
+
+ describe("if the call throws an error", () => {
+ const error = new Error("RPC call error");
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getL1BatchDetails")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(batchDetails);
+ });
+
+ it("retries RPC call with a default timeout", async () => {
+ await blockchainService.getL1BatchDetails(batchNumber);
+ expect(provider.getL1BatchDetails).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.getL1BatchDetails(batchNumber);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getL1BatchDetails" });
+ });
+
+ it("returns result of the successful RPC call", async () => {
+ const result = await blockchainService.getL1BatchDetails(batchNumber);
+ expect(result).toEqual(batchDetails);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getL1BatchDetails(batchNumber)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a timeout error", () => {
+ const error = new Error();
+ (error as any).code = "TIMEOUT";
+
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getL1BatchDetails")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(batchDetails);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getL1BatchDetails(batchNumber);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getL1BatchDetails(batchNumber)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a connection refused error", () => {
+ const error = new Error();
+ (error as any).code = "ECONNREFUSED";
+
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getL1BatchDetails")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(batchDetails);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getL1BatchDetails(batchNumber);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getL1BatchDetails(batchNumber)).rejects.toThrowError(error);
+ });
+ });
+ });
+ });
+
+ describe("getBlock", () => {
+ const blockNumber = 10;
+ const block: types.Block = mock({ number: 10 });
+ let timeoutSpy;
+
+ beforeEach(() => {
+ jest.spyOn(provider, "getBlock").mockResolvedValue(block);
+ timeoutSpy = jest.spyOn(timersPromises, "setTimeout");
+ });
+
+ it("starts the rpc call duration metric", async () => {
+ await blockchainService.getBlock(blockNumber);
+ expect(startRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets block by the specified block number", async () => {
+ await blockchainService.getBlock(blockNumber);
+ expect(provider.getBlock).toHaveBeenCalledTimes(1);
+ expect(provider.getBlock).toHaveBeenCalledWith(blockNumber);
+ });
+
+ it("stops the rpc call duration metric", async () => {
+ await blockchainService.getBlock(blockNumber);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getBlock" });
+ });
+
+ it("returns the block", async () => {
+ const result = await blockchainService.getBlock(blockNumber);
+ expect(result).toEqual(block);
+ });
+
+ describe("if the call throws an error", () => {
+ const error = new Error("RPC call error");
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getBlock")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(block);
+ });
+
+ it("retries RPC call with a default timeout", async () => {
+ await blockchainService.getBlock(blockNumber);
+ expect(provider.getBlock).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.getBlock(blockNumber);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getBlock" });
+ });
+
+ it("returns result of the successful RPC call", async () => {
+ const result = await blockchainService.getBlock(blockNumber);
+ expect(result).toEqual(block);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getBlock(blockNumber)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a timeout error", () => {
+ const error = new Error();
+ (error as any).code = "TIMEOUT";
+
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getBlock")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(block);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getBlock(blockNumber);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getBlock(blockNumber)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a connection refused error", () => {
+ const error = new Error();
+ (error as any).code = "ECONNREFUSED";
+
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getBlock")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(block);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getBlock(blockNumber);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getBlock(blockNumber)).rejects.toThrowError(error);
+ });
+ });
+ });
+ });
+
+ describe("getBlockNumber", () => {
+ const blockNumber = 10;
+ let timeoutSpy;
+
+ beforeEach(() => {
+ jest.spyOn(provider, "getBlockNumber").mockResolvedValue(blockNumber);
+ timeoutSpy = jest.spyOn(timersPromises, "setTimeout");
+ });
+
+ it("starts the rpc call duration metric", async () => {
+ await blockchainService.getBlockNumber();
+ expect(startRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets block number", async () => {
+ await blockchainService.getBlockNumber();
+ expect(provider.getBlockNumber).toHaveBeenCalledTimes(1);
+ });
+
+ it("stops the rpc call duration metric", async () => {
+ await blockchainService.getBlockNumber();
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getBlockNumber" });
+ });
+
+ it("returns the block number", async () => {
+ const result = await blockchainService.getBlockNumber();
+ expect(result).toEqual(blockNumber);
+ });
+
+ describe("if the call throws an error", () => {
+ const error = new Error("RPC call error");
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getBlockNumber")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(blockNumber);
+ });
+
+ it("retries RPC call with a default timeout", async () => {
+ await blockchainService.getBlockNumber();
+ expect(provider.getBlockNumber).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.getBlockNumber();
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getBlockNumber" });
+ });
+
+ it("returns result of the successful RPC call", async () => {
+ const result = await blockchainService.getBlockNumber();
+ expect(result).toEqual(blockNumber);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getBlockNumber()).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a timeout error", () => {
+ const error = new Error();
+ (error as any).code = "TIMEOUT";
+
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getBlockNumber")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(blockNumber);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getBlockNumber();
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getBlockNumber()).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a connection refused error", () => {
+ const error = new Error();
+ (error as any).code = "ECONNREFUSED";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getBlockNumber")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(blockNumber);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getBlockNumber();
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getBlockNumber()).rejects.toThrowError(error);
+ });
+ });
+ });
+ });
+
+ describe("getBlockDetails", () => {
+ const blockNumber = 10;
+ const blockDetails: types.BlockDetails = mock({ number: 10 });
+ let timeoutSpy;
+
+ beforeEach(() => {
+ jest.spyOn(provider, "getBlockDetails").mockResolvedValue(blockDetails);
+ timeoutSpy = jest.spyOn(timersPromises, "setTimeout");
+ });
+
+ it("starts the rpc call duration metric", async () => {
+ await blockchainService.getBlockDetails(blockNumber);
+ expect(startRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets block details by the specified block number", async () => {
+ await blockchainService.getBlockDetails(blockNumber);
+ expect(provider.getBlockDetails).toHaveBeenCalledTimes(1);
+ expect(provider.getBlockDetails).toHaveBeenCalledWith(blockNumber);
+ });
+
+ it("stops the rpc call duration metric", async () => {
+ await blockchainService.getBlockDetails(blockNumber);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getBlockDetails" });
+ });
+
+ it("returns the block details", async () => {
+ const result = await blockchainService.getBlockDetails(blockNumber);
+ expect(result).toEqual(blockDetails);
+ });
+
+ describe("if the call throws an error", () => {
+ const error = new Error("RPC call error");
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getBlockDetails")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(blockDetails);
+ });
+
+ it("retries RPC call with a default timeout", async () => {
+ await blockchainService.getBlockDetails(blockNumber);
+ expect(provider.getBlockDetails).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.getBlockDetails(blockNumber);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getBlockDetails" });
+ });
+
+ it("returns result of the successful RPC call", async () => {
+ const result = await blockchainService.getBlockDetails(blockNumber);
+ expect(result).toEqual(blockDetails);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getBlockDetails(blockNumber)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a timeout error", () => {
+ const error = new Error();
+ (error as any).code = "TIMEOUT";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getBlockDetails")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(blockDetails);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getBlockDetails(blockNumber);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getBlockDetails(blockNumber)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a connection refused error", () => {
+ const error = new Error();
+ (error as any).code = "ECONNREFUSED";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getBlockDetails")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(blockDetails);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getBlockDetails(blockNumber);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getBlockDetails(blockNumber)).rejects.toThrowError(error);
+ });
+ });
+ });
+ });
+
+ describe("getTransaction", () => {
+ const transactionHash = "transactionHash";
+ const transaction: types.TransactionResponse = mock({ hash: "transactionHash" });
+ let timeoutSpy;
+
+ beforeEach(() => {
+ jest.spyOn(provider, "getTransaction").mockResolvedValue(transaction);
+ timeoutSpy = jest.spyOn(timersPromises, "setTimeout");
+ });
+
+ it("starts the rpc call duration metric", async () => {
+ await blockchainService.getTransaction(transactionHash);
+ expect(startRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets transaction by the specified hash", async () => {
+ await blockchainService.getTransaction(transactionHash);
+ expect(provider.getTransaction).toHaveBeenCalledTimes(1);
+ expect(provider.getTransaction).toHaveBeenCalledWith(transactionHash);
+ });
+
+ it("stops the rpc call duration metric", async () => {
+ await blockchainService.getTransaction(transactionHash);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getTransaction" });
+ });
+
+ it("returns the transaction", async () => {
+ const result = await blockchainService.getTransaction(transactionHash);
+ expect(result).toEqual(transaction);
+ });
+
+ describe("if the call throws an error", () => {
+ const error = new Error("RPC call error");
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getTransaction")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(transaction);
+ });
+
+ it("retries RPC call with a default timeout", async () => {
+ await blockchainService.getTransaction(transactionHash);
+ expect(provider.getTransaction).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.getTransaction(transactionHash);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getTransaction" });
+ });
+
+ it("returns result of the successful RPC call", async () => {
+ const result = await blockchainService.getTransaction(transactionHash);
+ expect(result).toEqual(transaction);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getTransaction(transactionHash)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a timeout error", () => {
+ const error = new Error();
+ (error as any).code = "TIMEOUT";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getTransaction")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(transaction);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getTransaction(transactionHash);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getTransaction(transactionHash)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a connection refused error", () => {
+ const error = new Error();
+ (error as any).code = "ECONNREFUSED";
+
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getTransaction")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(transaction);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getTransaction(transactionHash);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getTransaction(transactionHash)).rejects.toThrowError(error);
+ });
+ });
+ });
+ });
+
+ describe("getTransactionDetails", () => {
+ const transactionHash = "transactionHash";
+ const transactionDetails: types.TransactionDetails = mock({
+ initiatorAddress: "initiatorAddress",
+ });
+ let timeoutSpy;
+
+ beforeEach(() => {
+ jest.spyOn(provider, "getTransactionDetails").mockResolvedValue(transactionDetails);
+ timeoutSpy = jest.spyOn(timersPromises, "setTimeout");
+ });
+
+ it("starts the rpc call duration metric", async () => {
+ await blockchainService.getTransactionDetails(transactionHash);
+ expect(startRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets transaction details by the specified hash", async () => {
+ await blockchainService.getTransactionDetails(transactionHash);
+ expect(provider.getTransactionDetails).toHaveBeenCalledTimes(1);
+ expect(provider.getTransactionDetails).toHaveBeenCalledWith(transactionHash);
+ });
+
+ it("stops the rpc call duration metric", async () => {
+ await blockchainService.getTransactionDetails(transactionHash);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getTransactionDetails" });
+ });
+
+ it("returns the transaction details", async () => {
+ const result = await blockchainService.getTransactionDetails(transactionHash);
+ expect(result).toEqual(transactionDetails);
+ });
+
+ describe("if the call throws an error", () => {
+ const error = new Error("RPC call error");
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getTransactionDetails")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(transactionDetails);
+ });
+
+ it("retries RPC call with a default timeout", async () => {
+ await blockchainService.getTransactionDetails(transactionHash);
+ expect(provider.getTransactionDetails).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.getTransactionDetails(transactionHash);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getTransactionDetails" });
+ });
+
+ it("returns result of the successful RPC call", async () => {
+ const result = await blockchainService.getTransactionDetails(transactionHash);
+ expect(result).toEqual(transactionDetails);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getTransactionDetails(transactionHash)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a timeout error", () => {
+ const error = new Error();
+ (error as any).code = "TIMEOUT";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getTransactionDetails")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(transactionDetails);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getTransactionDetails(transactionHash);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getTransactionDetails(transactionHash)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a connection refused error", () => {
+ const error = new Error();
+ (error as any).code = "ECONNREFUSED";
+
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getTransactionDetails")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(transactionDetails);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getTransactionDetails(transactionHash);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getTransactionDetails(transactionHash)).rejects.toThrowError(error);
+ });
+ });
+ });
+ });
+
+ describe("getTransactionReceipt", () => {
+ const transactionHash = "transactionHash";
+ const transactionReceipt: types.TransactionReceipt = mock({
+ transactionHash: "initiatorAddress",
+ });
+ let timeoutSpy;
+
+ beforeEach(() => {
+ jest.spyOn(provider, "getTransactionReceipt").mockResolvedValue(transactionReceipt);
+ timeoutSpy = jest.spyOn(timersPromises, "setTimeout");
+ });
+
+ it("starts the rpc call duration metric", async () => {
+ await blockchainService.getTransactionReceipt(transactionHash);
+ expect(startRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets transaction receipt by the specified hash", async () => {
+ await blockchainService.getTransactionReceipt(transactionHash);
+ expect(provider.getTransactionReceipt).toHaveBeenCalledTimes(1);
+ expect(provider.getTransactionReceipt).toHaveBeenCalledWith(transactionHash);
+ });
+
+ it("stops the rpc call duration metric", async () => {
+ await blockchainService.getTransactionReceipt(transactionHash);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getTransactionReceipt" });
+ });
+
+ it("returns the transaction receipt", async () => {
+ const result = await blockchainService.getTransactionReceipt(transactionHash);
+ expect(result).toEqual(transactionReceipt);
+ });
+
+ describe("if the call throws an error", () => {
+ const error = new Error("RPC call error");
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getTransactionReceipt")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(transactionReceipt);
+ });
+
+ it("retries RPC call with a default timeout", async () => {
+ await blockchainService.getTransactionReceipt(transactionHash);
+ expect(provider.getTransactionReceipt).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.getTransactionReceipt(transactionHash);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getTransactionReceipt" });
+ });
+
+ it("returns result of the successful RPC call", async () => {
+ const result = await blockchainService.getTransactionReceipt(transactionHash);
+ expect(result).toEqual(transactionReceipt);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getTransactionReceipt(transactionHash)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a timeout error", () => {
+ const error = new Error();
+ (error as any).code = "TIMEOUT";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getTransactionReceipt")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(transactionReceipt);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getTransactionReceipt(transactionHash);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getTransactionReceipt(transactionHash)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a connection refused error", () => {
+ const error = new Error();
+ (error as any).code = "ECONNREFUSED";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getTransactionReceipt")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(transactionReceipt);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getTransactionReceipt(transactionHash);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getTransactionReceipt(transactionHash)).rejects.toThrowError(error);
+ });
+ });
+ });
+ });
+
+ describe("getLogs", () => {
+ const fromBlock = 10;
+ const toBlock = 20;
+ const logs: types.Log[] = [mock({ logIndex: 1 }), mock({ logIndex: 2 })];
+ let timeoutSpy;
+
+ beforeEach(() => {
+ jest.spyOn(provider, "getLogs").mockResolvedValue(logs);
+ timeoutSpy = jest.spyOn(timersPromises, "setTimeout");
+ });
+
+ it("starts the rpc call duration metric", async () => {
+ await blockchainService.getLogs({ fromBlock, toBlock });
+ expect(startRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets logs by the specified from and to block numbers", async () => {
+ await blockchainService.getLogs({ fromBlock, toBlock });
+ expect(provider.getLogs).toHaveBeenCalledTimes(1);
+ expect(provider.getLogs).toHaveBeenCalledWith({ fromBlock, toBlock });
+ });
+
+ it("stops the rpc call duration metric", async () => {
+ await blockchainService.getLogs({ fromBlock, toBlock });
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getLogs" });
+ });
+
+ it("returns the logs", async () => {
+ const result = await blockchainService.getLogs({ fromBlock, toBlock });
+ expect(result).toEqual(logs);
+ });
+
+ describe("if the call throws an error", () => {
+ const error = new Error("RPC call error");
+
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getLogs")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(logs);
+ });
+
+ it("retries RPC call with a default timeout", async () => {
+ await blockchainService.getLogs({ fromBlock, toBlock });
+ expect(provider.getLogs).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.getLogs({ fromBlock, toBlock });
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getLogs" });
+ });
+
+ it("returns result of the successful RPC call", async () => {
+ const result = await blockchainService.getLogs({ fromBlock, toBlock });
+ expect(result).toEqual(logs);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getLogs({ fromBlock, toBlock })).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a timeout error", () => {
+ const error = new Error();
+ (error as any).code = "TIMEOUT";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getLogs")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(logs);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getLogs({ fromBlock, toBlock });
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getLogs({ fromBlock, toBlock })).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a connection refused error", () => {
+ const error = new Error();
+ (error as any).code = "ECONNREFUSED";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getLogs")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(logs);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getLogs({ fromBlock, toBlock });
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getLogs({ fromBlock, toBlock })).rejects.toThrowError(error);
+ });
+ });
+ });
+ });
+
+ describe("getCode", () => {
+ const address = "address";
+ const bytecode = "0x0123345";
+ let timeoutSpy;
+
+ beforeEach(() => {
+ jest.spyOn(provider, "getCode").mockResolvedValue(bytecode);
+ timeoutSpy = jest.spyOn(timersPromises, "setTimeout");
+ });
+
+ it("starts the rpc call duration metric", async () => {
+ await blockchainService.getCode(address);
+ expect(startRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets bytecode for the specified address", async () => {
+ await blockchainService.getCode(address);
+ expect(provider.getCode).toHaveBeenCalledTimes(1);
+ expect(provider.getCode).toHaveBeenCalledWith(address);
+ });
+
+ it("stops the rpc call duration metric", async () => {
+ await blockchainService.getCode(address);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getCode" });
+ });
+
+ it("returns the bytecode", async () => {
+ const result = await blockchainService.getCode(address);
+ expect(result).toEqual(bytecode);
+ });
+
+ describe("if the call throws an error", () => {
+ const error = new Error("RPC call error");
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getCode")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(bytecode);
+ });
+
+ it("retries RPC call with a default timeout", async () => {
+ await blockchainService.getCode(address);
+ expect(provider.getCode).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.getCode(address);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getCode" });
+ });
+
+ it("returns result of the successful RPC call", async () => {
+ const result = await blockchainService.getCode(address);
+ expect(result).toEqual(bytecode);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getCode(address)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a timeout error", () => {
+ const error = new Error();
+ (error as any).code = "TIMEOUT";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getCode")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(bytecode);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getCode(address);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getCode(address)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a connection refused error", () => {
+ const error = new Error();
+ (error as any).code = "ECONNREFUSED";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getCode")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(bytecode);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getCode(address);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getCode(address)).rejects.toThrowError(error);
+ });
+ });
+ });
+ });
+
+ describe("getDefaultBridgeAddresses", () => {
+ const bridgeAddress = {
+ erc20L1: "erc20L1",
+ erc20L2: "erc20L2",
+ wethL1: "wethL1",
+ wethL2: "wethL2",
+ };
+ let timeoutSpy;
+
+ beforeEach(() => {
+ jest.spyOn(provider, "getDefaultBridgeAddresses").mockResolvedValue(bridgeAddress);
+ timeoutSpy = jest.spyOn(timersPromises, "setTimeout");
+ });
+
+ it("starts the rpc call duration metric", async () => {
+ await blockchainService.getDefaultBridgeAddresses();
+ expect(startRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets bridge addresses", async () => {
+ await blockchainService.getDefaultBridgeAddresses();
+ expect(provider.getDefaultBridgeAddresses).toHaveBeenCalledTimes(1);
+ });
+
+ it("stops the rpc call duration metric", async () => {
+ await blockchainService.getDefaultBridgeAddresses();
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getDefaultBridgeAddresses" });
+ });
+
+ it("returns bridge addresses", async () => {
+ const result = await blockchainService.getDefaultBridgeAddresses();
+ expect(result).toEqual(bridgeAddress);
+ });
+
+ describe("if the call throws an error", () => {
+ const error = new Error("RPC call error");
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getDefaultBridgeAddresses")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(bridgeAddress);
+ });
+
+ it("retries RPC call with a default timeout", async () => {
+ await blockchainService.getDefaultBridgeAddresses();
+ expect(provider.getDefaultBridgeAddresses).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.getDefaultBridgeAddresses();
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getDefaultBridgeAddresses" });
+ });
+
+ it("returns result of the successful RPC call", async () => {
+ const result = await blockchainService.getDefaultBridgeAddresses();
+ expect(result).toEqual(bridgeAddress);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getDefaultBridgeAddresses()).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a timeout error", () => {
+ const error = new Error();
+ (error as any).code = "TIMEOUT";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getDefaultBridgeAddresses")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(bridgeAddress);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getDefaultBridgeAddresses();
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getDefaultBridgeAddresses()).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a connection refused error", () => {
+ const error = new Error();
+ (error as any).code = "ECONNREFUSED";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getDefaultBridgeAddresses")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(bridgeAddress);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getDefaultBridgeAddresses();
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getDefaultBridgeAddresses()).rejects.toThrowError(error);
+ });
+ });
+ });
+ });
+
+ describe("on", () => {
+ beforeEach(() => {
+ provider.on = jest.fn();
+ });
+
+ it("subscribes to the new events", () => {
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
+ const handler = () => {};
+ blockchainService.on("block", handler);
+ expect(provider.on).toHaveBeenCalledTimes(1);
+ expect(provider.on).toHaveBeenCalledWith("block", handler);
+ });
+ });
+
+ describe("getERC20TokenData", () => {
+ const contractAddress = "contractAddress";
+ const symbol = "symbol";
+ const decimals = 18;
+ const name = "name";
+ let symbolMock: jest.Mock;
+ let decimalMock: jest.Mock;
+ let nameMock: jest.Mock;
+
+ beforeEach(() => {
+ symbolMock = jest.fn().mockResolvedValue(symbol);
+ decimalMock = jest.fn().mockResolvedValue(decimals);
+ nameMock = jest.fn().mockResolvedValue(name);
+
+ (RetryableContract as any as jest.Mock).mockReturnValue(
+ mock({
+ symbol: symbolMock,
+ decimals: decimalMock,
+ name: nameMock,
+ })
+ );
+ });
+
+ it("uses ERC20 token contract interface", async () => {
+ await blockchainService.getERC20TokenData(contractAddress);
+ expect(RetryableContract).toHaveBeenCalledTimes(1);
+ expect(RetryableContract).toBeCalledWith(contractAddress, utils.IERC20, provider);
+ });
+
+ it("gets contact symbol", async () => {
+ await blockchainService.getERC20TokenData(contractAddress);
+ expect(symbolMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets contact decimals", async () => {
+ await blockchainService.getERC20TokenData(contractAddress);
+ expect(decimalMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets contact name", async () => {
+ await blockchainService.getERC20TokenData(contractAddress);
+ expect(nameMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("returns token data", async () => {
+ const tokenData = await blockchainService.getERC20TokenData(contractAddress);
+ expect(tokenData).toEqual({ symbol, decimals, name });
+ });
+
+ describe("when contract function throws an error", () => {
+ const error = new Error("contract error");
+
+ beforeEach(() => {
+ symbolMock = jest.fn().mockImplementation(() => {
+ throw error;
+ });
+ decimalMock = jest.fn().mockResolvedValue(decimals);
+ nameMock = jest.fn().mockResolvedValue(name);
+
+ (RetryableContract as any as jest.Mock).mockReturnValue(
+ mock({
+ symbol: symbolMock,
+ decimals: decimalMock,
+ name: nameMock,
+ })
+ );
+ });
+
+ it("throws an error", async () => {
+ await expect(blockchainService.getERC20TokenData(contractAddress)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("getBalance", () => {
+ const blockNumber = 5;
+ let blockTag: string;
+ let tokenAddress: string;
+ const address = "address";
+
+ beforeEach(() => {
+ blockTag = "latest";
+ tokenAddress = "tokenAddress";
+ jest.spyOn(providerFormatterMock, "blockTag").mockReturnValueOnce(blockTag);
+ });
+
+ it("gets block tag for the specified blockNumber", async () => {
+ await blockchainService.getBalance(address, blockNumber, tokenAddress);
+ expect(providerFormatterMock.blockTag).toHaveBeenCalledTimes(1);
+ expect(providerFormatterMock.blockTag).toHaveBeenCalledWith(blockNumber);
+ });
+
+ describe("if token address is ETH", () => {
+ let timeoutSpy;
+ const balance = ethers.BigNumber.from(10);
+
+ beforeEach(() => {
+ tokenAddress = utils.ETH_ADDRESS;
+ jest.spyOn(provider, "getBalance").mockResolvedValue(ethers.BigNumber.from(10));
+ timeoutSpy = jest.spyOn(timersPromises, "setTimeout");
+ });
+
+ it("starts the rpc call duration metric", async () => {
+ await blockchainService.getBalance(address, blockNumber, tokenAddress);
+ expect(startRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets the balance for ETH", async () => {
+ await blockchainService.getBalance(address, blockNumber, tokenAddress);
+ expect(provider.getBalance).toHaveBeenCalledTimes(1);
+ expect(provider.getBalance).toHaveBeenCalledWith(address, blockTag);
+ });
+
+ it("stops the rpc call duration metric", async () => {
+ await blockchainService.getBalance(address, blockNumber, tokenAddress);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getBalance" });
+ });
+
+ it("returns the address balance for ETH", async () => {
+ jest.spyOn(provider, "getBalance").mockResolvedValueOnce(ethers.BigNumber.from(15));
+
+ const balance = await blockchainService.getBalance(address, blockNumber, tokenAddress);
+ expect(balance).toStrictEqual(balance);
+ });
+
+ describe("if the call throws an error", () => {
+ const error = new Error("RPC call error");
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getBalance")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(balance);
+ });
+
+ it("retries RPC call with a default timeout", async () => {
+ await blockchainService.getBalance(address, blockNumber, tokenAddress);
+ expect(provider.getBalance).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.getBalance(address, blockNumber, tokenAddress);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getBalance" });
+ });
+
+ it("returns result of the successful RPC call", async () => {
+ const result = await blockchainService.getBalance(address, blockNumber, tokenAddress);
+ expect(result).toEqual(balance);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getBalance(address, blockNumber, tokenAddress)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a timeout error", () => {
+ const error = new Error();
+ (error as any).code = "TIMEOUT";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getBalance")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(balance);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getBalance(address, blockNumber, tokenAddress);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getBalance(address, blockNumber, tokenAddress)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a connection refused error", () => {
+ const error = new Error();
+ (error as any).code = "ECONNREFUSED";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getBalance")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(balance);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getBalance(address, blockNumber, tokenAddress);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getBalance(address, blockNumber, tokenAddress)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a connection reset error", () => {
+ const error = new Error();
+ (error as any).code = "ECONNRESET";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getBalance")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(balance);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getBalance(address, blockNumber, tokenAddress);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getBalance(address, blockNumber, tokenAddress)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a network error", () => {
+ const error = new Error();
+ (error as any).code = "NETWORK_ERROR";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getBalance")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(balance);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getBalance(address, blockNumber, tokenAddress);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getBalance(address, blockNumber, tokenAddress)).rejects.toThrowError(error);
+ });
+ });
+ });
+ });
+
+ describe("if token address is not ETH", () => {
+ beforeEach(() => {
+ tokenAddress = "0x22b44df5aa1ee4542b6318ff971f183135f5e4ce";
+ });
+
+ describe("if ERC20 Contract function throws an exception", () => {
+ const error = new Error("Ethers Contract error");
+
+ beforeEach(() => {
+ (RetryableContract as any as jest.Mock).mockReturnValueOnce(
+ mock({
+ balanceOf: jest.fn().mockImplementationOnce(() => {
+ throw error;
+ }),
+ })
+ );
+ });
+
+ it("throws an error", async () => {
+ await expect(blockchainService.getBalance(address, blockNumber, tokenAddress)).rejects.toThrowError(error);
+ });
+ });
+
+ describe("when there is a token with the specified address", () => {
+ let balanceOfMock: jest.Mock;
+
+ beforeEach(() => {
+ balanceOfMock = jest.fn().mockResolvedValueOnce(ethers.BigNumber.from(20));
+ (RetryableContract as any as jest.Mock).mockReturnValueOnce(
+ mock({
+ balanceOf: balanceOfMock,
+ })
+ );
+ });
+
+ it("uses the proper token contract", async () => {
+ await blockchainService.getBalance(address, blockNumber, tokenAddress);
+ expect(RetryableContract).toHaveBeenCalledTimes(1);
+ expect(RetryableContract).toBeCalledWith(tokenAddress, utils.IERC20, provider);
+ });
+
+ it("gets the balance for the specified address and block", async () => {
+ await blockchainService.getBalance(address, blockNumber, tokenAddress);
+ expect(balanceOfMock).toHaveBeenCalledTimes(1);
+ expect(balanceOfMock).toHaveBeenCalledWith(address, { blockTag });
+ });
+
+ it("returns the balance of the token", async () => {
+ const balance = await blockchainService.getBalance(address, blockNumber, tokenAddress);
+ expect(balance).toStrictEqual(ethers.BigNumber.from(20));
+ });
+ });
+ });
+ });
+
+ 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", () => {
+ const error = new Error("RPC call error");
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "send")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(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("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(
+ blockchainService.debugTraceTransaction(
+ "0xc0ae49e96910fa9df22eb59c0977905864664d495bc95906120695aa26e1710b"
+ )
+ ).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a timeout error", () => {
+ const error = new Error();
+ (error as any).code = "TIMEOUT";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "send")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .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("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(
+ blockchainService.debugTraceTransaction(
+ "0xc0ae49e96910fa9df22eb59c0977905864664d495bc95906120695aa26e1710b"
+ )
+ ).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a connection refused error", () => {
+ const error = new Error();
+ (error as any).code = "ECONNREFUSED";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "send")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .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("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(
+ blockchainService.debugTraceTransaction(
+ "0xc0ae49e96910fa9df22eb59c0977905864664d495bc95906120695aa26e1710b"
+ )
+ ).rejects.toThrowError(error);
+ });
+ });
+ });
+ });
+
+ describe("onModuleInit", () => {
+ let bridgeAddresses;
+
+ describe("when l2 ERC20 default bridge is defined", () => {
+ beforeEach(() => {
+ bridgeAddresses = {
+ erc20L2: "l2Erc20DefaultBridge",
+ };
+
+ jest.spyOn(provider, "getDefaultBridgeAddresses").mockResolvedValueOnce(bridgeAddresses);
+ });
+
+ it("inits L2 ERC20 bridge address", async () => {
+ await blockchainService.onModuleInit();
+ expect(blockchainService.bridgeAddresses.l2Erc20DefaultBridge).toBe(bridgeAddresses.erc20L2.toLowerCase());
+ });
+ });
+
+ describe("when l2 ERC20 default bridge is not defined", () => {
+ beforeEach(() => {
+ bridgeAddresses = {
+ erc20L2: null,
+ };
+
+ jest.spyOn(provider, "getDefaultBridgeAddresses").mockResolvedValueOnce(bridgeAddresses);
+ });
+
+ it("sets L2 ERC20 bridge address to null", async () => {
+ await blockchainService.onModuleInit();
+ expect(blockchainService.bridgeAddresses.l2Erc20DefaultBridge).toBe(undefined);
+ });
+ });
+ });
+});
diff --git a/packages/data-fetcher/src/blockchain/blockchain.service.ts b/packages/data-fetcher/src/blockchain/blockchain.service.ts
new file mode 100644
index 0000000000..665b5ffc1d
--- /dev/null
+++ b/packages/data-fetcher/src/blockchain/blockchain.service.ts
@@ -0,0 +1,203 @@
+import { Injectable, OnModuleInit, Logger } from "@nestjs/common";
+import { BigNumber } from "ethers";
+import { utils, types } from "zksync-web3";
+import { Histogram } from "prom-client";
+import { InjectMetric } from "@willsoto/nestjs-prometheus";
+import { EventType, Listener } from "@ethersproject/abstract-provider";
+import { ConfigService } from "@nestjs/config";
+import { setTimeout } from "timers/promises";
+import { JsonRpcProviderBase } from "../rpcProvider";
+import { BLOCKCHAIN_RPC_CALL_DURATION_METRIC_NAME, BlockchainRpcCallMetricLabel } from "../metrics";
+import { RetryableContract } from "./retryableContract";
+
+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;
+ private readonly rpcCallsDefaultRetryTimeout: number;
+ private readonly rpcCallsQuickRetryTimeout: number;
+ private readonly rpcCallRetriesMaxTotalTimeout: number;
+ private readonly errorCodesForQuickRetry: string[] = ["NETWORK_ERROR", "ECONNRESET", "ECONNREFUSED", "TIMEOUT"];
+ public bridgeAddresses: BridgeAddresses;
+
+ public constructor(
+ configService: ConfigService,
+ private readonly provider: JsonRpcProviderBase,
+ @InjectMetric(BLOCKCHAIN_RPC_CALL_DURATION_METRIC_NAME)
+ private readonly rpcCallDurationMetric: Histogram
+ ) {
+ this.logger = new Logger(BlockchainService.name);
+ this.rpcCallsDefaultRetryTimeout = configService.get("blockchain.rpcCallDefaultRetryTimeout");
+ this.rpcCallsQuickRetryTimeout = configService.get("blockchain.rpcCallQuickRetryTimeout");
+ this.rpcCallRetriesMaxTotalTimeout = configService.get("blockchain.rpcCallRetriesMaxTotalTimeout");
+ }
+
+ private async rpcCall(action: () => Promise, functionName: string, retriesTotalTimeAwaited = 0): Promise {
+ const stopDurationMeasuring = this.rpcCallDurationMetric.startTimer();
+ try {
+ const result = await action();
+ stopDurationMeasuring({ function: functionName });
+ return result;
+ } catch (error) {
+ this.logger.error({ message: error.message, code: error.code }, error.stack);
+ const retryTimeout = this.errorCodesForQuickRetry.includes(error.code)
+ ? this.rpcCallsQuickRetryTimeout
+ : this.rpcCallsDefaultRetryTimeout;
+
+ const totalTimeAwaited = retriesTotalTimeAwaited + retryTimeout;
+ if (totalTimeAwaited > this.rpcCallRetriesMaxTotalTimeout) {
+ this.logger.error({ message: "Exceeded retries total timeout, failing the request", functionName });
+ throw error;
+ }
+
+ await setTimeout(retryTimeout);
+ return this.rpcCall(action, functionName, totalTimeAwaited);
+ }
+ }
+
+ public async getL1BatchNumber(): Promise {
+ return await this.rpcCall(async () => {
+ return await this.provider.getL1BatchNumber();
+ }, "getL1BatchNumber");
+ }
+
+ public async getL1BatchDetails(batchNumber: number): Promise {
+ return await this.rpcCall(async () => {
+ const batchDetails = await this.provider.getL1BatchDetails(batchNumber);
+ if (batchDetails && batchNumber === 0) {
+ batchDetails.committedAt = batchDetails.provenAt = batchDetails.executedAt = new Date(0);
+ }
+ return batchDetails;
+ }, "getL1BatchDetails");
+ }
+
+ public async getBlock(blockHashOrBlockTag: types.BlockTag): Promise {
+ return await this.rpcCall(async () => {
+ return await this.provider.getBlock(blockHashOrBlockTag);
+ }, "getBlock");
+ }
+
+ public async getBlockNumber(): Promise {
+ return await this.rpcCall(async () => {
+ return await this.provider.getBlockNumber();
+ }, "getBlockNumber");
+ }
+
+ public async getBlockDetails(blockNumber: number): Promise {
+ return await this.rpcCall(async () => {
+ return await this.provider.getBlockDetails(blockNumber);
+ }, "getBlockDetails");
+ }
+
+ public async getTransaction(transactionHash: string): Promise {
+ return await this.rpcCall(async () => {
+ return await this.provider.getTransaction(transactionHash);
+ }, "getTransaction");
+ }
+
+ public async getTransactionDetails(transactionHash: string): Promise {
+ return await this.rpcCall(async () => {
+ return await this.provider.getTransactionDetails(transactionHash);
+ }, "getTransactionDetails");
+ }
+
+ public async getTransactionReceipt(transactionHash: string): Promise {
+ return await this.rpcCall(async () => {
+ return await this.provider.getTransactionReceipt(transactionHash);
+ }, "getTransactionReceipt");
+ }
+
+ public async getLogs(eventFilter: { fromBlock: number; toBlock: number }): Promise {
+ return await this.rpcCall(async () => {
+ return await this.provider.getLogs(eventFilter);
+ }, "getLogs");
+ }
+
+ public async getCode(address: string): Promise {
+ return await this.rpcCall(async () => {
+ return await this.provider.getCode(address);
+ }, "getCode");
+ }
+
+ public async getDefaultBridgeAddresses(): Promise<{ erc20L1: string; erc20L2: string }> {
+ return await this.rpcCall(async () => {
+ return await this.provider.getDefaultBridgeAddresses();
+ }, "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);
+ }
+
+ public async getERC20TokenData(contractAddress: string): Promise<{ symbol: string; decimals: number; name: string }> {
+ const erc20Contract = new RetryableContract(contractAddress, utils.IERC20, this.provider);
+ const [symbol, decimals, name] = await Promise.all([
+ erc20Contract.symbol(),
+ erc20Contract.decimals(),
+ erc20Contract.name(),
+ ]);
+ return {
+ symbol,
+ decimals,
+ name,
+ };
+ }
+
+ public async getBalance(address: string, blockNumber: number, tokenAddress: string): Promise {
+ const blockTag = this.provider.formatter.blockTag(blockNumber);
+
+ if (utils.isETH(tokenAddress)) {
+ return await this.rpcCall(async () => {
+ return await this.provider.getBalance(address, blockTag);
+ }, "getBalance");
+ }
+
+ const erc20Contract = new RetryableContract(tokenAddress, utils.IERC20, this.provider);
+ return await erc20Contract.balanceOf(address, { blockTag });
+ }
+
+ public async getBridgeContracts(): Promise<{
+ l1Erc20Bridge: string;
+ l1SharedDefaultBridge: string;
+ l2SharedDefaultBridge;
+ l1Erc20DefaultBridge: string;
+ l1WethBridge: string;
+ l2Erc20DefaultBridge: string;
+ l2WethBridge: string;
+ }> {
+ return await this.rpcCall(async () => {
+ return await this.provider.send("zks_getBridgeContracts", []);
+ }, "getBridgeContracts");
+ }
+
+ public async onModuleInit(): Promise {
+ const bridgeAddresses = await this.getDefaultBridgeAddresses();
+ this.bridgeAddresses = {
+ l2Erc20DefaultBridge: bridgeAddresses.erc20L2?.toLowerCase(),
+ };
+ this.logger.debug(`L2 ERC20 Bridge is set to: ${this.bridgeAddresses.l2Erc20DefaultBridge}`);
+ }
+}
diff --git a/packages/data-fetcher/src/blockchain/index.ts b/packages/data-fetcher/src/blockchain/index.ts
new file mode 100644
index 0000000000..ef903b3fc2
--- /dev/null
+++ b/packages/data-fetcher/src/blockchain/index.ts
@@ -0,0 +1 @@
+export * from "./blockchain.service";
diff --git a/packages/data-fetcher/src/blockchain/retryableContract.spec.ts b/packages/data-fetcher/src/blockchain/retryableContract.spec.ts
new file mode 100644
index 0000000000..cfbce3611b
--- /dev/null
+++ b/packages/data-fetcher/src/blockchain/retryableContract.spec.ts
@@ -0,0 +1,267 @@
+import * as ethers from "ethers";
+import { mock } from "jest-mock-extended";
+import { utils } from "zksync-web3";
+import { setTimeout } from "timers/promises";
+import { RetryableContract } from "./retryableContract";
+
+jest.mock("../config", () => ({
+ default: () => ({
+ blockchain: {
+ rpcCallRetriesMaxTotalTimeout: 200000,
+ },
+ }),
+}));
+
+jest.mock("ethers", () => ({
+ ...jest.requireActual("ethers"),
+ Contract: jest.fn(),
+}));
+
+jest.mock("@nestjs/common", () => ({
+ Logger: jest.fn().mockReturnValue({
+ error: jest.fn(),
+ debug: jest.fn(),
+ warn: jest.fn(),
+ }),
+}));
+
+jest.mock("timers/promises", () => ({
+ setTimeout: jest.fn().mockResolvedValue(null),
+}));
+
+describe("RetryableContract", () => {
+ const tokenAddress = "tokenAddress";
+ const providerMock = mock({});
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe("constructor", () => {
+ it("inits Contract instance with specified ctor params", async () => {
+ new RetryableContract(tokenAddress, utils.IERC20, providerMock);
+ expect(ethers.Contract).toHaveBeenCalledTimes(1);
+ expect(ethers.Contract).toBeCalledWith(tokenAddress, utils.IERC20, providerMock);
+ });
+ });
+
+ describe("contract field access", () => {
+ const fieldValue = "fieldValue";
+ let contract: RetryableContract;
+
+ beforeEach(() => {
+ (ethers.Contract as any as jest.Mock).mockReturnValue({
+ contractField: fieldValue,
+ });
+
+ contract = new RetryableContract(tokenAddress, utils.IERC20, providerMock);
+ });
+
+ it("returns field value", () => {
+ const result = contract.contractField;
+ expect(result).toBe(fieldValue);
+ });
+ });
+
+ describe("contract sync function call", () => {
+ const functionResult = "functionResult";
+ let contract: RetryableContract;
+
+ beforeEach(() => {
+ (ethers.Contract as any as jest.Mock).mockReturnValue({
+ contractFn: () => functionResult,
+ });
+
+ contract = new RetryableContract(tokenAddress, utils.IERC20, providerMock);
+ });
+
+ it("returns function call result", () => {
+ const result = contract.contractFn();
+ expect(result).toBe(functionResult);
+ });
+ });
+
+ describe("contract async function call", () => {
+ const functionResult = "functionResult";
+ let contract: RetryableContract;
+
+ beforeEach(() => {
+ (ethers.Contract as any as jest.Mock).mockReturnValue({
+ contractFn: async () => functionResult,
+ });
+
+ contract = new RetryableContract(tokenAddress, utils.IERC20, providerMock);
+ });
+
+ it("returns function call async result", async () => {
+ const result = await contract.contractFn();
+ expect(result).toBe(functionResult);
+ });
+
+ describe("when throws a permanent call exception function error", () => {
+ const callExceptionError = {
+ code: "CALL_EXCEPTION",
+ method: "contractFn(address)",
+ transaction: {
+ data: "0x00",
+ to: "to",
+ },
+ message: "call revert exception ....",
+ };
+
+ beforeEach(() => {
+ (ethers.Contract as any as jest.Mock).mockReturnValue({
+ contractFn: async () => {
+ throw callExceptionError;
+ },
+ });
+
+ contract = new RetryableContract(tokenAddress, utils.IERC20, providerMock);
+ });
+
+ it("throws an error", async () => {
+ expect.assertions(1);
+
+ try {
+ await contract.contractFn();
+ } catch (e) {
+ expect(e).toBe(callExceptionError);
+ }
+ });
+ });
+
+ describe("when throws an invalid argument function error", () => {
+ const invalidArgumentError = {
+ code: "INVALID_ARGUMENT",
+ };
+
+ beforeEach(() => {
+ (ethers.Contract as any as jest.Mock).mockReturnValue({
+ contractFn: async () => {
+ throw invalidArgumentError;
+ },
+ });
+
+ contract = new RetryableContract(tokenAddress, utils.IERC20, providerMock);
+ });
+
+ it("throws an error", async () => {
+ expect.assertions(1);
+
+ try {
+ await contract.contractFn();
+ } catch (e) {
+ expect(e).toBe(invalidArgumentError);
+ }
+ });
+ });
+
+ describe("when throws a few network errors before returning a result", () => {
+ const functionResult = "functionResult";
+ const error = new Error();
+ (error as any).code = "NETWORK_ERROR";
+
+ beforeEach(() => {
+ let countOfFailedRequests = 0;
+ (ethers.Contract as any as jest.Mock).mockReturnValue({
+ contractFn: async () => {
+ if (countOfFailedRequests++ < 4) {
+ throw error;
+ }
+ return functionResult;
+ },
+ });
+
+ contract = new RetryableContract(tokenAddress, utils.IERC20, providerMock, 20000);
+ });
+
+ it("retries and returns the result when it's available", async () => {
+ const result = await contract.contractFn();
+ expect(result).toBe(functionResult);
+ expect(setTimeout).toBeCalledTimes(4);
+ expect(setTimeout).toBeCalledWith(20000);
+ expect(setTimeout).toBeCalledWith(40000);
+ expect(setTimeout).toBeCalledWith(60000);
+ expect(setTimeout).toBeCalledWith(60000);
+ });
+
+ describe("and retries total time exceeds the retries total max timeout", () => {
+ beforeEach(() => {
+ let countOfFailedRequests = 0;
+ (ethers.Contract as any as jest.Mock).mockReturnValue({
+ contractFn: async () => {
+ if (countOfFailedRequests++ < 5) {
+ throw error;
+ }
+ return functionResult;
+ },
+ });
+
+ contract = new RetryableContract(tokenAddress, utils.IERC20, providerMock, 20000);
+ });
+
+ it("throws an error", async () => {
+ await expect(contract.contractFn()).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("when throws a few errors with no method or message before returning a result", () => {
+ const functionResult = "functionResult";
+ const error = new Error();
+ (error as any).code = "CALL_EXCEPTION";
+
+ beforeEach(() => {
+ let countOfFailedRequests = 0;
+ (ethers.Contract as any as jest.Mock).mockReturnValue({
+ contractFn: async () => {
+ countOfFailedRequests++;
+ if (countOfFailedRequests <= 2) {
+ throw {
+ ...error,
+ transaction: {},
+ method: "contractFn()",
+ };
+ }
+ if (countOfFailedRequests <= 4) {
+ throw error;
+ }
+ return functionResult;
+ },
+ });
+
+ contract = new RetryableContract(tokenAddress, utils.IERC20, providerMock, 20000);
+ });
+
+ it("retries and returns the result when it's available", async () => {
+ const result = await contract.contractFn();
+ expect(result).toBe(functionResult);
+ expect(setTimeout).toBeCalledTimes(4);
+ expect(setTimeout).toBeCalledWith(20000);
+ expect(setTimeout).toBeCalledWith(40000);
+ expect(setTimeout).toBeCalledWith(60000);
+ expect(setTimeout).toBeCalledWith(60000);
+ });
+
+ describe("and retries total time exceeds the retries total max timeout", () => {
+ beforeEach(() => {
+ let countOfFailedRequests = 0;
+ (ethers.Contract as any as jest.Mock).mockReturnValue({
+ contractFn: async () => {
+ if (countOfFailedRequests++ < 5) {
+ throw error;
+ }
+ return functionResult;
+ },
+ });
+
+ contract = new RetryableContract(tokenAddress, utils.IERC20, providerMock, 20000);
+ });
+
+ it("throws an error", async () => {
+ await expect(contract.contractFn()).rejects.toThrowError(error);
+ });
+ });
+ });
+ });
+});
diff --git a/packages/data-fetcher/src/blockchain/retryableContract.ts b/packages/data-fetcher/src/blockchain/retryableContract.ts
new file mode 100644
index 0000000000..846b2ca5f2
--- /dev/null
+++ b/packages/data-fetcher/src/blockchain/retryableContract.ts
@@ -0,0 +1,124 @@
+import { Logger } from "@nestjs/common";
+import { Provider } from "@ethersproject/abstract-provider";
+import { setTimeout } from "timers/promises";
+import { Contract, ContractInterface, Signer, errors } from "ethers";
+import config from "../config";
+
+const { blockchain } = config();
+
+interface EthersError {
+ code: string;
+ method: string;
+ transaction: {
+ data: string;
+ to: string;
+ };
+ message: string;
+}
+
+const MAX_RETRY_INTERVAL = 60000;
+
+const PERMANENT_ERRORS: string[] = [
+ errors.INVALID_ARGUMENT,
+ errors.MISSING_ARGUMENT,
+ errors.UNEXPECTED_ARGUMENT,
+ errors.NOT_IMPLEMENTED,
+];
+
+const shouldRetry = (calledFunctionName: string, error: EthersError): boolean => {
+ return (
+ !PERMANENT_ERRORS.includes(error.code) &&
+ !(
+ error.code === errors.CALL_EXCEPTION &&
+ error.method?.startsWith(`${calledFunctionName}(`) &&
+ !!error.transaction &&
+ error.message?.startsWith("call revert exception")
+ )
+ );
+};
+
+const retryableFunctionCall = async (
+ result: Promise,
+ functionCall: () => any,
+ logger: Logger,
+ functionName: string,
+ addressOrName: string,
+ retryTimeout: number,
+ retriesTotalTimeAwaited = 0
+): Promise => {
+ try {
+ return await result;
+ } catch (error) {
+ const isRetryable = shouldRetry(functionName, error);
+ if (!isRetryable) {
+ logger.warn({
+ message: `Requested contract function ${functionName} failed to execute, not retryable`,
+ contractAddress: addressOrName,
+ error,
+ });
+ throw error;
+ }
+
+ const exceededRetriesTotalTimeout =
+ retriesTotalTimeAwaited + retryTimeout > blockchain.rpcCallRetriesMaxTotalTimeout;
+ const failedStatus = exceededRetriesTotalTimeout ? "exceeded total retries timeout" : "retrying...";
+ logger.warn({
+ message: `Requested contract function ${functionName} failed to execute, ${failedStatus}`,
+ contractAddress: addressOrName,
+ error,
+ });
+
+ if (exceededRetriesTotalTimeout) {
+ throw error;
+ }
+ }
+ await setTimeout(retryTimeout);
+
+ const nextRetryTimeout = Math.min(retryTimeout * 2, MAX_RETRY_INTERVAL);
+ return retryableFunctionCall(
+ functionCall(),
+ functionCall,
+ logger,
+ functionName,
+ addressOrName,
+ nextRetryTimeout,
+ retriesTotalTimeAwaited + retryTimeout
+ );
+};
+
+const getProxyHandler = (addressOrName: string, logger: Logger, retryTimeout: number) => {
+ return {
+ get: function (target, propertyKey, receiver) {
+ if (target.contract[propertyKey] instanceof Function) {
+ return function (...args) {
+ const result = target.contract[propertyKey].apply(this, args);
+ if (result instanceof Promise) {
+ return retryableFunctionCall(
+ result,
+ () => target.contract[propertyKey].apply(this, args),
+ logger,
+ propertyKey,
+ addressOrName,
+ retryTimeout
+ );
+ }
+ return result;
+ };
+ }
+ return Reflect.get(target.contract, propertyKey, receiver);
+ },
+ };
+};
+
+export class RetryableContract extends Contract {
+ constructor(
+ addressOrName: string,
+ contractInterface: ContractInterface,
+ signerOrProvider: Signer | Provider,
+ retryTimeout = 1000
+ ) {
+ const logger = new Logger("Contract");
+ super(addressOrName, contractInterface, signerOrProvider);
+ return new Proxy({ contract: this }, getProxyHandler(addressOrName, logger, retryTimeout));
+ }
+}
diff --git a/packages/data-fetcher/src/common/pipes/parseLimitedInt.pipe.spec.ts b/packages/data-fetcher/src/common/pipes/parseLimitedInt.pipe.spec.ts
new file mode 100644
index 0000000000..82da2e28ea
--- /dev/null
+++ b/packages/data-fetcher/src/common/pipes/parseLimitedInt.pipe.spec.ts
@@ -0,0 +1,84 @@
+import { BadRequestException } from "@nestjs/common";
+import { ParseLimitedIntPipe } from "./parseLimitedInt.pipe";
+
+describe("ParseLimitedIntPipe", () => {
+ describe("transform", () => {
+ describe("throws a BadRequestException", () => {
+ it("if specified input is not valid", async () => {
+ const pipe = new ParseLimitedIntPipe();
+ expect.assertions(2);
+
+ try {
+ await pipe.transform("invalidAddressParam");
+ } catch (error) {
+ expect(error).toBeInstanceOf(BadRequestException);
+ expect(error.message).toBe("Validation failed (numeric string is expected)");
+ }
+ });
+
+ it("if specified value is less than specified min value", async () => {
+ const pipe = new ParseLimitedIntPipe({ min: 1, max: 10 });
+ expect.assertions(2);
+
+ try {
+ await pipe.transform("0");
+ } catch (error) {
+ expect(error).toBeInstanceOf(BadRequestException);
+ expect(error.message).toBe("Validation failed: specified int is out of defined boundaries: [1;10].");
+ }
+ });
+
+ it("if specified value is higher than specified max value", async () => {
+ const pipe = new ParseLimitedIntPipe({ min: 1, max: 10 });
+ expect.assertions(2);
+
+ try {
+ await pipe.transform("11");
+ } catch (error) {
+ expect(error).toBeInstanceOf(BadRequestException);
+ expect(error.message).toBe("Validation failed: specified int is out of defined boundaries: [1;10].");
+ }
+ });
+
+ it("if no min option specified uses 0 as a min value", async () => {
+ const pipe = new ParseLimitedIntPipe({ max: 10 });
+ expect.assertions(2);
+
+ try {
+ await pipe.transform("-10");
+ } catch (error) {
+ expect(error).toBeInstanceOf(BadRequestException);
+ expect(error.message).toBe("Validation failed: specified int is out of defined boundaries: [0;10].");
+ }
+ });
+
+ it("if no max option specified uses max int number as a max value", async () => {
+ const pipe = new ParseLimitedIntPipe({ min: 1 });
+ expect.assertions(2);
+
+ try {
+ await pipe.transform("9007199254740992");
+ } catch (error) {
+ expect(error).toBeInstanceOf(BadRequestException);
+ expect(error.message).toBe(
+ "Validation failed: specified int is out of defined boundaries: [1;9007199254740991]."
+ );
+ }
+ });
+ });
+
+ it("returns undefined when value is not defined and isOptional set to true", async () => {
+ const pipe = new ParseLimitedIntPipe({ isOptional: true });
+
+ const parsedInt = await pipe.transform(null);
+ expect(parsedInt).toBe(undefined);
+ });
+
+ it("returns parsed value", async () => {
+ const pipe = new ParseLimitedIntPipe();
+
+ const parsedInt = await pipe.transform("10");
+ expect(parsedInt).toBe(10);
+ });
+ });
+});
diff --git a/packages/data-fetcher/src/common/pipes/parseLimitedInt.pipe.ts b/packages/data-fetcher/src/common/pipes/parseLimitedInt.pipe.ts
new file mode 100644
index 0000000000..4a804417ec
--- /dev/null
+++ b/packages/data-fetcher/src/common/pipes/parseLimitedInt.pipe.ts
@@ -0,0 +1,34 @@
+import { ArgumentMetadata, ParseIntPipe, ParseIntPipeOptions } from "@nestjs/common";
+
+declare interface ParseLimitedIntPipeOptions extends ParseIntPipeOptions {
+ min?: number;
+ max?: number;
+ isOptional?: boolean;
+}
+
+export class ParseLimitedIntPipe extends ParseIntPipe {
+ constructor(private readonly options?: ParseLimitedIntPipeOptions) {
+ super(options);
+ }
+
+ public override async transform(value: string | number, metadata?: ArgumentMetadata): Promise {
+ if (!value && value !== 0 && this.options.isOptional) {
+ return undefined;
+ }
+ const parsedInt = await super.transform(value as string, metadata);
+ let { min, max } = this.options || {};
+
+ if (isNaN(min)) {
+ min = 0;
+ }
+ if (isNaN(max)) {
+ max = Number.MAX_SAFE_INTEGER;
+ }
+
+ if (parsedInt < min || parsedInt > max) {
+ throw this.exceptionFactory(`Validation failed: specified int is out of defined boundaries: [${min};${max}].`);
+ }
+
+ return parsedInt;
+ }
+}
diff --git a/packages/data-fetcher/src/config.spec.ts b/packages/data-fetcher/src/config.spec.ts
new file mode 100644
index 0000000000..bbeb4995b0
--- /dev/null
+++ b/packages/data-fetcher/src/config.spec.ts
@@ -0,0 +1,31 @@
+import config from "./config";
+
+describe("config", () => {
+ const env = process.env;
+
+ beforeAll(() => {
+ process.env = {};
+ });
+
+ afterAll(() => {
+ process.env = env;
+ });
+
+ it("sets default values", () => {
+ expect(config()).toEqual({
+ port: 3040,
+ blockchain: {
+ rpcUrl: "http://localhost:3050",
+ rpcCallDefaultRetryTimeout: 30000,
+ rpcCallQuickRetryTimeout: 5000,
+ rpcCallRetriesMaxTotalTimeout: 120000,
+ rpcCallConnectionTimeout: 60000,
+ rpcCallConnectionQuickTimeout: 10000,
+ wsMaxConnections: 5,
+ useWebSocketsForTransactions: false,
+ },
+ maxBlocksBatchSize: 20,
+ gracefulShutdownTimeoutMs: 0,
+ });
+ });
+});
diff --git a/packages/data-fetcher/src/config.ts b/packages/data-fetcher/src/config.ts
new file mode 100644
index 0000000000..7c4819da0c
--- /dev/null
+++ b/packages/data-fetcher/src/config.ts
@@ -0,0 +1,37 @@
+import { config } from "dotenv";
+config();
+
+export default () => {
+ const {
+ PORT,
+ BLOCKCHAIN_RPC_URL,
+ RPC_CALLS_DEFAULT_RETRY_TIMEOUT,
+ RPC_CALLS_QUICK_RETRY_TIMEOUT,
+ RPC_CALLS_RETRIES_MAX_TOTAL_TIMEOUT,
+ RPC_CALLS_CONNECTION_TIMEOUT,
+ RPC_CALLS_CONNECTION_QUICK_TIMEOUT,
+ WS_MAX_CONNECTIONS,
+ USE_WEBSOCKETS_FOR_TRANSACTIONS,
+ MAX_BLOCKS_BATCH_SIZE,
+ GRACEFUL_SHUTDOWN_TIMEOUT_MS,
+ } = process.env;
+
+ return {
+ port: parseInt(PORT, 10) || 3040,
+ blockchain: {
+ rpcUrl: BLOCKCHAIN_RPC_URL || "http://localhost:3050",
+
+ rpcCallDefaultRetryTimeout: parseInt(RPC_CALLS_DEFAULT_RETRY_TIMEOUT, 10) || 30000,
+ rpcCallQuickRetryTimeout: parseInt(RPC_CALLS_QUICK_RETRY_TIMEOUT, 10) || 5000,
+ rpcCallRetriesMaxTotalTimeout: parseInt(RPC_CALLS_RETRIES_MAX_TOTAL_TIMEOUT, 10) || 120000,
+
+ rpcCallConnectionTimeout: parseInt(RPC_CALLS_CONNECTION_TIMEOUT, 10) || 60000,
+ rpcCallConnectionQuickTimeout: parseInt(RPC_CALLS_CONNECTION_QUICK_TIMEOUT, 10) || 10000,
+
+ wsMaxConnections: parseInt(WS_MAX_CONNECTIONS, 10) || 5,
+ useWebSocketsForTransactions: USE_WEBSOCKETS_FOR_TRANSACTIONS === "true",
+ },
+ maxBlocksBatchSize: parseInt(MAX_BLOCKS_BATCH_SIZE, 10) || 20,
+ gracefulShutdownTimeoutMs: parseInt(GRACEFUL_SHUTDOWN_TIMEOUT_MS, 10) || 0,
+ };
+};
diff --git a/packages/data-fetcher/src/constants.ts b/packages/data-fetcher/src/constants.ts
new file mode 100644
index 0000000000..8acfc2bcb0
--- /dev/null
+++ b/packages/data-fetcher/src/constants.ts
@@ -0,0 +1,38 @@
+import { utils } from "ethers";
+import { abi as ethTokenAbi } from "zksync-web3/abi/IEthToken.json";
+import { abi as erc20Abi } from "zksync-web3/abi/IERC20.json";
+import { abi as l2BridgeAbi } from "zksync-web3/abi/IL2Bridge.json";
+import * as erc721Abi from "./abis/erc721.json";
+import * as transferEventWithNoIndexesAbi from "./abis/transferEventWithNoIndexes.json";
+import * as l2StandardERC20Abi from "./abis/l2StandardERC20.json";
+
+export const ZERO_HASH_64 = "0x0000000000000000000000000000000000000000000000000000000000000000";
+export const BASE_TOKEN_ADDRESS = "0x000000000000000000000000000000000000800a";
+export const ETH_L1_ADDRESS = "0x0000000000000000000000000000000000000001";
+
+export const CONTRACT_INTERFACES = {
+ ERC20: {
+ interface: new utils.Interface(erc20Abi),
+ abi: erc20Abi,
+ },
+ ERC721: {
+ interface: new utils.Interface(erc721Abi),
+ abi: erc721Abi,
+ },
+ L2_STANDARD_ERC20: {
+ interface: new utils.Interface(l2StandardERC20Abi),
+ abi: l2StandardERC20Abi,
+ },
+ TRANSFER_WITH_NO_INDEXES: {
+ interface: new utils.Interface(transferEventWithNoIndexesAbi),
+ abi: transferEventWithNoIndexesAbi,
+ },
+ ETH_TOKEN: {
+ interface: new utils.Interface(ethTokenAbi),
+ abi: ethTokenAbi,
+ },
+ L2_BRIDGE: {
+ interface: new utils.Interface(l2BridgeAbi),
+ abi: l2BridgeAbi,
+ },
+};
diff --git a/packages/data-fetcher/src/health/health.controller.spec.ts b/packages/data-fetcher/src/health/health.controller.spec.ts
new file mode 100644
index 0000000000..a4efa52496
--- /dev/null
+++ b/packages/data-fetcher/src/health/health.controller.spec.ts
@@ -0,0 +1,119 @@
+import { ServiceUnavailableException, Logger } from "@nestjs/common";
+import { Test, TestingModule } from "@nestjs/testing";
+import { HealthCheckService, HealthCheckResult } from "@nestjs/terminus";
+import { mock } from "jest-mock-extended";
+import { ConfigService } from "@nestjs/config";
+import { setTimeout } from "node:timers/promises";
+import { JsonRpcHealthIndicator } from "./jsonRpcProvider.health";
+import { HealthController } from "./health.controller";
+
+jest.mock("node:timers/promises", () => ({
+ setTimeout: jest.fn().mockResolvedValue(null),
+}));
+
+describe("HealthController", () => {
+ let healthCheckServiceMock: HealthCheckService;
+ let jsonRpcHealthIndicatorMock: JsonRpcHealthIndicator;
+ let configServiceMock: ConfigService;
+ let healthController: HealthController;
+
+ beforeEach(async () => {
+ configServiceMock = mock({
+ get: jest.fn().mockReturnValue(1),
+ });
+ healthCheckServiceMock = mock({
+ check: jest.fn().mockImplementation((healthChecks) => {
+ for (const healthCheck of healthChecks) {
+ healthCheck();
+ }
+ }),
+ });
+ jsonRpcHealthIndicatorMock = mock();
+
+ const app: TestingModule = await Test.createTestingModule({
+ controllers: [HealthController],
+ providers: [
+ {
+ provide: HealthCheckService,
+ useValue: healthCheckServiceMock,
+ },
+ {
+ provide: JsonRpcHealthIndicator,
+ useValue: jsonRpcHealthIndicatorMock,
+ },
+ {
+ provide: ConfigService,
+ useValue: configServiceMock,
+ },
+ ],
+ }).compile();
+
+ app.useLogger(mock());
+
+ healthController = app.get(HealthController);
+ });
+
+ describe("check", () => {
+ it("checks health of the JSON RPC provider", async () => {
+ await healthController.check();
+ expect(jsonRpcHealthIndicatorMock.isHealthy).toHaveBeenCalledTimes(1);
+ expect(jsonRpcHealthIndicatorMock.isHealthy).toHaveBeenCalledWith("jsonRpcProvider");
+ });
+
+ it("returns the overall check status", async () => {
+ const healthCheckResult = mock({ status: "ok" });
+ jest.spyOn(healthCheckServiceMock, "check").mockResolvedValueOnce(healthCheckResult);
+ const result = await healthController.check();
+ expect(result).toBe(healthCheckResult);
+ });
+
+ describe("when health checks fail with an error", () => {
+ const error: ServiceUnavailableException = new ServiceUnavailableException({
+ status: "error",
+ rpc: {
+ status: "down",
+ },
+ });
+
+ beforeEach(() => {
+ jest.spyOn(healthCheckServiceMock, "check").mockImplementation(() => {
+ throw error;
+ });
+ });
+
+ it("throws generated error", async () => {
+ expect.assertions(4);
+ try {
+ await healthController.check();
+ } catch (e) {
+ expect(e).toBeInstanceOf(ServiceUnavailableException);
+ expect(e.message).toBe("Service Unavailable Exception");
+ expect(e.response).toEqual(error.getResponse());
+ expect(e.stack).toEqual(error.stack);
+ }
+ });
+ });
+ });
+
+ describe("beforeApplicationShutdown", () => {
+ beforeEach(() => {
+ (setTimeout as jest.Mock).mockReset();
+ });
+
+ it("defined and returns void", async () => {
+ const result = await healthController.beforeApplicationShutdown();
+ expect(result).toBeUndefined();
+ });
+
+ it("awaits configured shutdown timeout", async () => {
+ await healthController.beforeApplicationShutdown("SIGTERM");
+ expect(setTimeout).toBeCalledTimes(1);
+ expect(setTimeout).toBeCalledWith(1);
+ });
+
+ it("does not await shutdown timeout if signal is not SIGTERM", async () => {
+ await healthController.beforeApplicationShutdown("SIGINT");
+ expect(setTimeout).toBeCalledTimes(0);
+ });
+ });
+});
diff --git a/packages/data-fetcher/src/health/health.controller.ts b/packages/data-fetcher/src/health/health.controller.ts
new file mode 100644
index 0000000000..bede1a9da1
--- /dev/null
+++ b/packages/data-fetcher/src/health/health.controller.ts
@@ -0,0 +1,39 @@
+import { Logger, Controller, Get, BeforeApplicationShutdown } from "@nestjs/common";
+import { HealthCheckService, HealthCheck, HealthCheckResult } from "@nestjs/terminus";
+import { ConfigService } from "@nestjs/config";
+import { setTimeout } from "node:timers/promises";
+import { JsonRpcHealthIndicator } from "./jsonRpcProvider.health";
+
+@Controller(["health", "ready"])
+export class HealthController implements BeforeApplicationShutdown {
+ private readonly logger: Logger;
+ private readonly gracefulShutdownTimeoutMs: number;
+
+ constructor(
+ private readonly healthCheckService: HealthCheckService,
+ private readonly jsonRpcHealthIndicator: JsonRpcHealthIndicator,
+ configService: ConfigService
+ ) {
+ this.logger = new Logger(HealthController.name);
+ this.gracefulShutdownTimeoutMs = configService.get("gracefulShutdownTimeoutMs");
+ }
+
+ @Get()
+ @HealthCheck()
+ public async check(): Promise {
+ try {
+ return await this.healthCheckService.check([() => this.jsonRpcHealthIndicator.isHealthy("jsonRpcProvider")]);
+ } catch (error) {
+ this.logger.error({ message: error.message, response: error.getResponse() }, error.stack);
+ throw error;
+ }
+ }
+
+ public async beforeApplicationShutdown(signal?: string): Promise {
+ if (this.gracefulShutdownTimeoutMs && signal === "SIGTERM") {
+ this.logger.debug(`Awaiting ${this.gracefulShutdownTimeoutMs}ms before shutdown`);
+ await setTimeout(this.gracefulShutdownTimeoutMs);
+ this.logger.debug(`Timeout reached, shutting down now`);
+ }
+ }
+}
diff --git a/packages/data-fetcher/src/health/health.module.ts b/packages/data-fetcher/src/health/health.module.ts
new file mode 100644
index 0000000000..dae128825d
--- /dev/null
+++ b/packages/data-fetcher/src/health/health.module.ts
@@ -0,0 +1,11 @@
+import { Module } from "@nestjs/common";
+import { TerminusModule } from "@nestjs/terminus";
+import { HealthController } from "./health.controller";
+import { JsonRpcHealthIndicator } from "./jsonRpcProvider.health";
+
+@Module({
+ controllers: [HealthController],
+ imports: [TerminusModule],
+ providers: [JsonRpcHealthIndicator],
+})
+export class HealthModule {}
diff --git a/packages/data-fetcher/src/health/jsonRpcProvider.health.spec.ts b/packages/data-fetcher/src/health/jsonRpcProvider.health.spec.ts
new file mode 100644
index 0000000000..2cfaa28919
--- /dev/null
+++ b/packages/data-fetcher/src/health/jsonRpcProvider.health.spec.ts
@@ -0,0 +1,56 @@
+import { Test, TestingModule } from "@nestjs/testing";
+import { mock } from "jest-mock-extended";
+import { HealthCheckError } from "@nestjs/terminus";
+import { JsonRpcProviderBase } from "../rpcProvider";
+import { JsonRpcHealthIndicator } from "./jsonRpcProvider.health";
+
+describe("JsonRpcHealthIndicator", () => {
+ const healthIndicatorKey = "rpcProvider";
+ let jsonRpcProviderMock: JsonRpcProviderBase;
+ let jsonRpcHealthIndicator: JsonRpcHealthIndicator;
+
+ beforeEach(async () => {
+ jsonRpcProviderMock = mock();
+
+ const app: TestingModule = await Test.createTestingModule({
+ providers: [
+ JsonRpcHealthIndicator,
+ {
+ provide: JsonRpcProviderBase,
+ useValue: jsonRpcProviderMock,
+ },
+ ],
+ }).compile();
+
+ jsonRpcHealthIndicator = app.get(JsonRpcHealthIndicator);
+ });
+
+ describe("isHealthy", () => {
+ describe("when rpcProvider is open", () => {
+ beforeEach(() => {
+ jest.spyOn(jsonRpcProviderMock, "getState").mockReturnValueOnce("open");
+ });
+
+ it("returns OK health indicator result", async () => {
+ const result = await jsonRpcHealthIndicator.isHealthy(healthIndicatorKey);
+ expect(result).toEqual({ [healthIndicatorKey]: { rpcProviderState: "open", status: "up" } });
+ });
+ });
+
+ describe("when rpcProvider is closed", () => {
+ beforeEach(() => {
+ jest.spyOn(jsonRpcProviderMock, "getState").mockReturnValueOnce("closed");
+ });
+
+ it("throws HealthCheckError error", async () => {
+ expect.assertions(2);
+ try {
+ await jsonRpcHealthIndicator.isHealthy(healthIndicatorKey);
+ } catch (error) {
+ expect(error).toBeInstanceOf(HealthCheckError);
+ expect(error.message).toBe("JSON RPC provider is not in open state");
+ }
+ });
+ });
+ });
+});
diff --git a/packages/data-fetcher/src/health/jsonRpcProvider.health.ts b/packages/data-fetcher/src/health/jsonRpcProvider.health.ts
new file mode 100644
index 0000000000..60406bfb4c
--- /dev/null
+++ b/packages/data-fetcher/src/health/jsonRpcProvider.health.ts
@@ -0,0 +1,22 @@
+import { Injectable } from "@nestjs/common";
+import { HealthIndicator, HealthIndicatorResult, HealthCheckError } from "@nestjs/terminus";
+import { JsonRpcProviderBase } from "../rpcProvider";
+
+@Injectable()
+export class JsonRpcHealthIndicator extends HealthIndicator {
+ constructor(private readonly provider: JsonRpcProviderBase) {
+ super();
+ }
+
+ async isHealthy(key: string): Promise {
+ const rpcProviderState = this.provider.getState();
+ const isHealthy = rpcProviderState === "open";
+ const result = this.getStatus(key, isHealthy, { rpcProviderState });
+
+ if (isHealthy) {
+ return result;
+ }
+
+ throw new HealthCheckError("JSON RPC provider is not in open state", result);
+ }
+}
diff --git a/packages/data-fetcher/src/log/index.ts b/packages/data-fetcher/src/log/index.ts
new file mode 100644
index 0000000000..690d9e9229
--- /dev/null
+++ b/packages/data-fetcher/src/log/index.ts
@@ -0,0 +1,2 @@
+export * from "./log.service";
+export * from "./logType";
diff --git a/packages/data-fetcher/src/log/log.service.spec.ts b/packages/data-fetcher/src/log/log.service.spec.ts
new file mode 100644
index 0000000000..90f7d3e3e9
--- /dev/null
+++ b/packages/data-fetcher/src/log/log.service.spec.ts
@@ -0,0 +1,150 @@
+import { Test } from "@nestjs/testing";
+import { Logger } from "@nestjs/common";
+import { mock } from "jest-mock-extended";
+import { types } from "zksync-web3";
+import { LogService } from "./log.service";
+import { TransferService } from "../transfer/transfer.service";
+import { Transfer } from "../transfer/interfaces/transfer.interface";
+import { TokenService, Token } from "../token/token.service";
+import { AddressService } from "../address/address.service";
+import { BalanceService } from "../balance/balance.service";
+import { ContractAddress } from "../address/interface/contractAddress.interface";
+
+describe("LogService", () => {
+ let logService: LogService;
+ let addressServiceMock: AddressService;
+ let balanceServiceMock: BalanceService;
+ let transferServiceMock: TransferService;
+ let tokenServiceMock: TokenService;
+
+ beforeEach(async () => {
+ addressServiceMock = mock();
+ balanceServiceMock = mock();
+ transferServiceMock = mock();
+ tokenServiceMock = mock();
+
+ const app = await Test.createTestingModule({
+ providers: [
+ LogService,
+ {
+ provide: AddressService,
+ useValue: addressServiceMock,
+ },
+ {
+ provide: BalanceService,
+ useValue: balanceServiceMock,
+ },
+ {
+ provide: TransferService,
+ useValue: transferServiceMock,
+ },
+ {
+ provide: TokenService,
+ useValue: tokenServiceMock,
+ },
+ ],
+ }).compile();
+
+ app.useLogger(mock());
+
+ logService = app.get(LogService);
+ });
+
+ describe("getData", () => {
+ const blockDetails = {
+ number: 1,
+ timestamp: new Date().getTime() / 1000,
+ } as types.BlockDetails;
+
+ const deployedContractAddresses = [
+ mock({ address: "0xdc187378edD8Ed1585fb47549Cc5fe633295d571" }),
+ mock({ address: "0xD144ca8Aa2E7DFECD56a3CCcBa1cd873c8e5db58" }),
+ ];
+
+ const transfers = [
+ { from: "from1", to: "to1", logIndex: 0 } as Transfer,
+ { from: "from2", to: "to2", logIndex: 1 } as Transfer,
+ ];
+ const logs: types.Log[] = [{ logIndex: 0 } as types.Log, { logIndex: 1 } as types.Log];
+ const tokens: Token[] = [
+ {
+ l1Address: "l1Address1",
+ } as Token,
+ {
+ l1Address: "l1Address2",
+ } as Token,
+ ];
+
+ let transactionReceipt: types.TransactionReceipt;
+ let transactionDetails: types.TransactionDetails;
+
+ beforeEach(() => {
+ jest.spyOn(addressServiceMock, "getContractAddresses").mockResolvedValueOnce(deployedContractAddresses);
+ jest.spyOn(transferServiceMock, "getTransfers").mockReturnValueOnce(transfers);
+ jest.spyOn(tokenServiceMock, "getERC20Token").mockResolvedValueOnce(tokens[0]);
+ jest.spyOn(tokenServiceMock, "getERC20Token").mockResolvedValueOnce(tokens[1]);
+
+ transactionReceipt = mock();
+ transactionDetails = mock({
+ receivedAt: new Date(),
+ });
+ });
+
+ describe("when transaction details and receipt are defined", () => {
+ beforeEach(() => {
+ transactionReceipt = mock({
+ transactionIndex: 0,
+ logs: logs,
+ });
+ });
+
+ it("returns data with transaction transfers", async () => {
+ const logsData = await logService.getData(logs, blockDetails, transactionDetails, transactionReceipt);
+ expect(transferServiceMock.getTransfers).toHaveBeenCalledTimes(1);
+ expect(transferServiceMock.getTransfers).toHaveBeenCalledWith(
+ logs,
+ blockDetails,
+ transactionDetails,
+ transactionReceipt
+ );
+ expect(logsData.transfers).toEqual(transfers);
+ });
+
+ it("tracks changed balances", async () => {
+ await logService.getData(logs, blockDetails, transactionDetails, transactionReceipt);
+ expect(balanceServiceMock.trackChangedBalances).toHaveBeenCalledTimes(1);
+ expect(balanceServiceMock.trackChangedBalances).toHaveBeenCalledWith(transfers);
+ });
+
+ it("returns data with deployed contracts' addresses", async () => {
+ const logsData = await logService.getData(logs, blockDetails, transactionDetails, transactionReceipt);
+ expect(addressServiceMock.getContractAddresses).toHaveBeenCalledTimes(1);
+ expect(addressServiceMock.getContractAddresses).toHaveBeenCalledWith(logs, transactionReceipt);
+ expect(logsData.contractAddresses).toEqual(deployedContractAddresses);
+ });
+
+ it("returns data with ERC20 tokens", async () => {
+ const logsData = await logService.getData(logs, blockDetails, transactionDetails, transactionReceipt);
+ expect(tokenServiceMock.getERC20Token).toHaveBeenCalledTimes(2);
+ expect(tokenServiceMock.getERC20Token).toHaveBeenCalledWith(deployedContractAddresses[0], transactionReceipt);
+ expect(tokenServiceMock.getERC20Token).toHaveBeenCalledWith(deployedContractAddresses[1], transactionReceipt);
+ expect(logsData.tokens).toEqual(tokens);
+ });
+ });
+
+ describe("when transaction details and receipt are not defined", () => {
+ it("tracks changed balances", async () => {
+ await logService.getData(logs, blockDetails);
+ expect(balanceServiceMock.trackChangedBalances).toHaveBeenCalledTimes(1);
+ expect(balanceServiceMock.trackChangedBalances).toHaveBeenCalledWith(transfers);
+ });
+
+ it("returns data with transaction transfers", async () => {
+ const logsData = await logService.getData(logs, blockDetails);
+ expect(transferServiceMock.getTransfers).toHaveBeenCalledTimes(1);
+ expect(transferServiceMock.getTransfers).toHaveBeenCalledWith(logs, blockDetails, undefined, undefined);
+ expect(logsData.transfers).toEqual(transfers);
+ });
+ });
+ });
+});
diff --git a/packages/data-fetcher/src/log/log.service.ts b/packages/data-fetcher/src/log/log.service.ts
new file mode 100644
index 0000000000..18ce191b22
--- /dev/null
+++ b/packages/data-fetcher/src/log/log.service.ts
@@ -0,0 +1,69 @@
+import { Injectable, Logger } from "@nestjs/common";
+import { types } from "zksync-web3";
+import { AddressService } from "../address/address.service";
+import { BalanceService } from "../balance/balance.service";
+import { TransferService } from "../transfer/transfer.service";
+import { TokenService } from "../token/token.service";
+import { Transfer } from "../transfer/interfaces/transfer.interface";
+import { ContractAddress } from "../address/interface/contractAddress.interface";
+import { Token } from "../token/token.service";
+
+export interface LogsData {
+ transfers: Transfer[];
+ contractAddresses?: ContractAddress[];
+ tokens?: Token[];
+}
+
+@Injectable()
+export class LogService {
+ private readonly logger: Logger;
+
+ public constructor(
+ private readonly addressService: AddressService,
+ private readonly balanceService: BalanceService,
+ private readonly transferService: TransferService,
+ private readonly tokenService: TokenService
+ ) {
+ this.logger = new Logger(LogService.name);
+ }
+
+ public async getData(
+ logs: types.Log[],
+ blockDetails: types.BlockDetails,
+ transactionDetails?: types.TransactionDetails,
+ transactionReceipt?: types.TransactionReceipt
+ ): Promise {
+ const transfers = this.transferService.getTransfers(logs, blockDetails, transactionDetails, transactionReceipt);
+
+ const logsData: LogsData = {
+ transfers,
+ };
+
+ this.balanceService.trackChangedBalances(transfers);
+
+ if (transactionReceipt) {
+ const transactionHash = transactionReceipt.transactionHash;
+
+ this.logger.debug({ message: "Extracting contracts", blockNumber: blockDetails.number, transactionHash });
+ const contractAddresses = await this.addressService.getContractAddresses(logs, transactionReceipt);
+
+ this.logger.debug({
+ message: "Extracting ERC20 tokens",
+ blockNumber: blockDetails.number,
+ transactionHash,
+ });
+ const tokens = (
+ await Promise.all(
+ contractAddresses.map((contractAddress) =>
+ this.tokenService.getERC20Token(contractAddress, transactionReceipt)
+ )
+ )
+ ).filter((token) => !!token);
+
+ logsData.contractAddresses = contractAddresses;
+ logsData.tokens = tokens;
+ }
+
+ return logsData;
+ }
+}
diff --git a/packages/data-fetcher/src/log/logType.spec.ts b/packages/data-fetcher/src/log/logType.spec.ts
new file mode 100644
index 0000000000..237834f4a2
--- /dev/null
+++ b/packages/data-fetcher/src/log/logType.spec.ts
@@ -0,0 +1,22 @@
+import { types } from "zksync-web3";
+import { mock } from "jest-mock-extended";
+import { isLogOfType, LogType } from "./logType";
+
+describe("isLogOfType", () => {
+ let log: types.Log;
+ beforeEach(() => {
+ log = mock({
+ topics: ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "arg1"],
+ });
+ });
+
+ it("returns true if the first log topic is equal to any of the specified event hashes", () => {
+ const result = isLogOfType(log, [LogType.Approval, LogType.Transfer, LogType.BridgeBurn]);
+ expect(result).toBe(true);
+ });
+
+ it("returns false if the first log topic isn't equal to any of the specified event hashes", () => {
+ const result = isLogOfType(log, [LogType.BridgeBurn, LogType.Approval, LogType.BridgeInitialization]);
+ expect(result).toBe(false);
+ });
+});
diff --git a/packages/data-fetcher/src/log/logType.ts b/packages/data-fetcher/src/log/logType.ts
new file mode 100644
index 0000000000..7a6dcb9a7d
--- /dev/null
+++ b/packages/data-fetcher/src/log/logType.ts
@@ -0,0 +1,54 @@
+import { types } from "zksync-web3";
+
+export enum LogType {
+ // ERC20
+ // event Transfer(address indexed from, address indexed to, uint256 value);
+ // ERC721
+ // event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
+ Transfer = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+
+ // event Approval(address indexed owner, address indexed spender, uint256 value);
+ Approval = "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925",
+
+ //
+ // BridgeInitialization was used first, then it was renamed to BridgeInitialize so now we should support both
+ // event BridgeInitialization(address indexed l1Token, string name, string symbol, uint8 decimals);
+ BridgeInitialization = "0xe6b2ac4004ee4493db8844da5db69722d2128345671818c3c41928655a83fb2c",
+ // event BridgeInitialize(address indexed l1Token, string name, string symbol, uint8 decimals);
+ BridgeInitialize = "0x81e8e92e5873539605a102eddae7ed06d19bea042099a437cbc3644415eb7404",
+ //
+
+ // event BridgeMint(address indexed _account, uint256 _amount);
+ BridgeMint = "0x397b33b307fc137878ebfc75b295289ec0ee25a31bb5bf034f33256fe8ea2aa6",
+
+ // event BridgeBurn(address indexed _account, uint256 _amount);
+ BridgeBurn = "0x9b5b9a05e4726d8bb959f1440e05c6b8109443f2083bc4e386237d7654526553",
+
+ // event DepositInitiated(address indexed from, address indexed to, address indexed l1Token, uint256 amount);
+ DepositInitiated = "0x7abe8fd2d210cf1e5d2cb3e277afd776d77269c8869b02c39f0bb542de0fdba1",
+
+ // event FinalizeDeposit(address indexed l1Sender, address indexed l2Receiver, address indexed l2Token, uint256 amount);
+ FinalizeDeposit = "0xb84fba9af218da60d299dc177abd5805e7ac541d2673cbee7808c10017874f63",
+
+ // event ClaimedFailedDeposit(address indexed to, address indexed l1Token, uint256 amount);
+ ClaimedFailedDeposit = "0xbe066dc591f4a444f75176d387c3e6c775e5706d9ea9a91d11eb49030c66cf60",
+
+ // event WithdrawalInitiated(address indexed l2Sender, address indexed l1Receiver, address indexed l2Token, uint256 amount);
+ WithdrawalInitiated = "0x2fc3848834aac8e883a2d2a17a7514dc4f2d3dd268089df9b9f5d918259ef3b0",
+
+ // event WithdrawalFinalized(address indexed to, address indexed l1Token, uint256 amount);
+ WithdrawalFinalized = "0xac1b18083978656d557d6e91c88203585cfda1031bdb14538327121ef140d383",
+
+ // event ContractDeployed(address indexed deployerAddress, bytes32 indexed bytecodeHash, address indexed contractAddress);
+ ContractDeployed = "0x290afdae231a3fc0bbae8b1af63698b0a1d79b21ad17df0342dfb952fe74f8e5",
+
+ // event Mint(address indexed account, uint256 amount)
+ Mint = "0x0f6798a560793a54c3bcfe86a93cde1e73087d944c0ea20544137d4121396885",
+
+ // event Withdrawal(address indexed _l2Sender, address indexed _l1Receiver, uint256 _amount)
+ Withdrawal = "0x2717ead6b9200dd235aad468c9809ea400fe33ac69b5bfaa6d3e90fc922b6398",
+}
+
+export const isLogOfType = (log: types.Log, types: LogType[]): boolean => {
+ return types.some((type) => log.topics[0] === type);
+};
diff --git a/packages/data-fetcher/src/logger.ts b/packages/data-fetcher/src/logger.ts
new file mode 100644
index 0000000000..29d66a8d32
--- /dev/null
+++ b/packages/data-fetcher/src/logger.ts
@@ -0,0 +1,29 @@
+import { utilities, WinstonModule } from "nest-winston";
+import { format, transports, Logform } from "winston";
+
+const { NODE_ENV, LOG_LEVEL } = process.env;
+
+let defaultLogLevel = "debug";
+const loggerFormatters: Logform.Format[] = [
+ NODE_ENV === "production"
+ ? format.timestamp()
+ : format.timestamp({
+ format: "DD/MM/YYYY HH:mm:ss.SSS",
+ }),
+ format.ms(),
+ utilities.format.nestLike("DataFetcher", {}),
+];
+
+if (NODE_ENV === "production") {
+ defaultLogLevel = "info";
+ loggerFormatters.push(format.json());
+}
+
+export default WinstonModule.createLogger({
+ level: LOG_LEVEL || defaultLogLevel,
+ transports: [
+ new transports.Console({
+ format: format.combine(...loggerFormatters),
+ }),
+ ],
+});
diff --git a/packages/data-fetcher/src/main.ts b/packages/data-fetcher/src/main.ts
new file mode 100644
index 0000000000..5f0eb36543
--- /dev/null
+++ b/packages/data-fetcher/src/main.ts
@@ -0,0 +1,24 @@
+import { NestFactory } from "@nestjs/core";
+import { ConfigService } from "@nestjs/config";
+import logger from "./logger";
+import { AppModule } from "./app.module";
+import overrideBigNumberToJson from "./utils/overrideBigNumberToJson";
+
+overrideBigNumberToJson();
+
+async function bootstrap() {
+ process.on("uncaughtException", function (error) {
+ logger.error(error.message, error.stack, "UnhandledExceptions");
+ process.exit(1);
+ });
+
+ const app = await NestFactory.create(AppModule, {
+ logger,
+ });
+
+ const configService = app.get(ConfigService);
+ app.enableShutdownHooks();
+ await app.listen(configService.get("port"));
+}
+
+bootstrap();
diff --git a/packages/data-fetcher/src/metrics/index.ts b/packages/data-fetcher/src/metrics/index.ts
new file mode 100644
index 0000000000..99e2d7f1e6
--- /dev/null
+++ b/packages/data-fetcher/src/metrics/index.ts
@@ -0,0 +1,2 @@
+export * from "./metrics.provider";
+export * from "./metrics.module";
diff --git a/packages/data-fetcher/src/metrics/metrics.module.ts b/packages/data-fetcher/src/metrics/metrics.module.ts
new file mode 100644
index 0000000000..bb70e8f94d
--- /dev/null
+++ b/packages/data-fetcher/src/metrics/metrics.module.ts
@@ -0,0 +1,8 @@
+import { Module } from "@nestjs/common";
+import { metricProviders } from "./metrics.provider";
+
+@Module({
+ providers: metricProviders,
+ exports: metricProviders,
+})
+export class MetricsModule {}
diff --git a/packages/data-fetcher/src/metrics/metrics.provider.ts b/packages/data-fetcher/src/metrics/metrics.provider.ts
new file mode 100644
index 0000000000..1f7b801d7a
--- /dev/null
+++ b/packages/data-fetcher/src/metrics/metrics.provider.ts
@@ -0,0 +1,63 @@
+import { Provider } from "@nestjs/common";
+import { makeHistogramProvider } from "@willsoto/nestjs-prometheus";
+
+export const BLOCK_PROCESSING_DURATION_METRIC_NAME = "block_processing_duration_seconds";
+export type BlockProcessingMetricLabels = "status" | "action";
+
+export const TRANSACTION_PROCESSING_DURATION_METRIC_NAME = "transaction_processing_duration_seconds";
+export type ProcessingActionMetricLabel = "action";
+
+export const BALANCES_PROCESSING_DURATION_METRIC_NAME = "balances_processing_duration_seconds";
+
+export const GET_BLOCK_INFO_DURATION_METRIC_NAME = "get_block_info_duration_seconds";
+export const GET_TRANSACTION_INFO_DURATION_METRIC_NAME = "get_transaction_info_duration_seconds";
+export const GET_TOKEN_INFO_DURATION_METRIC_NAME = "get_token_info_duration_seconds";
+
+export const BLOCKCHAIN_RPC_CALL_DURATION_METRIC_NAME = "blockchain_rpc_call_duration_seconds";
+export type BlockchainRpcCallMetricLabel = "function";
+
+const metricsBuckets = [
+ 0.01, 0.025, 0.05, 0.075, 0.1, 0.125, 0.15, 0.175, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1, 1.25, 1.5, 2, 2.5, 3, 4,
+ 5, 7, 10, 20, 30,
+];
+
+export const metricProviders: Provider[] = [
+ makeHistogramProvider({
+ name: BLOCK_PROCESSING_DURATION_METRIC_NAME,
+ help: "block processing duration in seconds.",
+ labelNames: ["status", "action"],
+ buckets: metricsBuckets,
+ }),
+ makeHistogramProvider({
+ name: TRANSACTION_PROCESSING_DURATION_METRIC_NAME,
+ help: "transaction processing duration in seconds.",
+ buckets: metricsBuckets,
+ }),
+ makeHistogramProvider({
+ name: BALANCES_PROCESSING_DURATION_METRIC_NAME,
+ help: "balances processing duration in seconds.",
+ buckets: metricsBuckets,
+ }),
+ makeHistogramProvider({
+ name: BLOCKCHAIN_RPC_CALL_DURATION_METRIC_NAME,
+ help: "blockchain rpc call duration in seconds.",
+ labelNames: ["function"],
+ buckets: metricsBuckets,
+ }),
+ makeHistogramProvider({
+ name: GET_BLOCK_INFO_DURATION_METRIC_NAME,
+ help: "get block info duration in seconds.",
+ labelNames: ["action"],
+ buckets: metricsBuckets,
+ }),
+ makeHistogramProvider({
+ name: GET_TRANSACTION_INFO_DURATION_METRIC_NAME,
+ help: "get transaction info duration in seconds.",
+ buckets: metricsBuckets,
+ }),
+ makeHistogramProvider({
+ name: GET_TOKEN_INFO_DURATION_METRIC_NAME,
+ help: "get token info duration in seconds.",
+ buckets: metricsBuckets,
+ }),
+];
diff --git a/packages/data-fetcher/src/rpcProvider/index.ts b/packages/data-fetcher/src/rpcProvider/index.ts
new file mode 100644
index 0000000000..f00c9c8b85
--- /dev/null
+++ b/packages/data-fetcher/src/rpcProvider/index.ts
@@ -0,0 +1,4 @@
+export * from "./jsonRpcProviderBase";
+export * from "./jsonRpcProviderExtended";
+export * from "./webSocketProviderExtended";
+export * from "./wrappedWebSocketProvider";
diff --git a/packages/data-fetcher/src/rpcProvider/jsonRpcProvider.module.ts b/packages/data-fetcher/src/rpcProvider/jsonRpcProvider.module.ts
new file mode 100644
index 0000000000..e3db352c58
--- /dev/null
+++ b/packages/data-fetcher/src/rpcProvider/jsonRpcProvider.module.ts
@@ -0,0 +1,57 @@
+import { Module, DynamicModule, Logger } from "@nestjs/common";
+import { ConfigService } from "@nestjs/config";
+import { JsonRpcProviderBase, JsonRpcProviderExtended, WrappedWebSocketProvider } from "./index";
+
+@Module({
+ providers: [
+ Logger,
+ {
+ provide: JsonRpcProviderBase,
+ useFactory: (configService: ConfigService, logger: Logger) => {
+ const providerUrl = configService.get("blockchain.rpcUrl");
+ const connectionTimeout = configService.get("blockchain.rpcCallConnectionTimeout");
+ const connectionQuickTimeout = configService.get("blockchain.rpcCallConnectionQuickTimeout");
+ const providerUrlProtocol = new URL(providerUrl).protocol;
+
+ logger.debug(`Initializing RPC provider with the following URL: ${providerUrl}.`, "RpcProviderModule");
+
+ if (providerUrlProtocol === "http:" || providerUrlProtocol === "https:") {
+ return new JsonRpcProviderExtended(providerUrl, connectionTimeout, connectionQuickTimeout);
+ }
+
+ throw new Error(
+ `RPC URL protocol is not supported. HTTP(s) URL is expected. Actual protocol: ${providerUrlProtocol}.`
+ );
+ },
+ inject: [ConfigService, Logger],
+ },
+ {
+ provide: WrappedWebSocketProvider,
+ useFactory: (configService: ConfigService, logger: Logger) => {
+ const providerUrl = configService.get("blockchain.wsRpcUrl");
+ const connectionTimeout = configService.get("blockchain.rpcCallConnectionTimeout");
+ const connectionQuickTimeout = configService.get("blockchain.rpcCallConnectionQuickTimeout");
+ const maxConnections = configService.get("blockchain.wsMaxConnections");
+ const useWebSocketsForTransactions = configService.get("blockchain.useWebSocketsForTransactions");
+
+ if (!useWebSocketsForTransactions) {
+ return null;
+ }
+
+ logger.debug(`Initializing WS RPC provider with the following URL: ${providerUrl}.`, "RpcProviderModule");
+
+ return new WrappedWebSocketProvider(providerUrl, connectionTimeout, connectionQuickTimeout, maxConnections);
+ },
+ inject: [ConfigService, Logger],
+ },
+ ],
+ exports: [JsonRpcProviderBase, WrappedWebSocketProvider],
+})
+export class JsonRpcProviderModule {
+ static forRoot(): DynamicModule {
+ return {
+ module: JsonRpcProviderModule,
+ global: true,
+ };
+ }
+}
diff --git a/packages/data-fetcher/src/rpcProvider/jsonRpcProviderBase.ts b/packages/data-fetcher/src/rpcProvider/jsonRpcProviderBase.ts
new file mode 100644
index 0000000000..d88b7c11fd
--- /dev/null
+++ b/packages/data-fetcher/src/rpcProvider/jsonRpcProviderBase.ts
@@ -0,0 +1,7 @@
+import { Provider } from "zksync-web3";
+
+export type ProviderState = "connecting" | "open" | "closed";
+
+export abstract class JsonRpcProviderBase extends Provider {
+ public abstract getState(): ProviderState;
+}
diff --git a/packages/data-fetcher/src/rpcProvider/jsonRpcProviderExtended.spec.ts b/packages/data-fetcher/src/rpcProvider/jsonRpcProviderExtended.spec.ts
new file mode 100644
index 0000000000..5d66e4526e
--- /dev/null
+++ b/packages/data-fetcher/src/rpcProvider/jsonRpcProviderExtended.spec.ts
@@ -0,0 +1,119 @@
+import { mock } from "jest-mock-extended";
+const baseSendPromise = jest.fn();
+class JsonRpcProviderBaseMock {
+ public send() {
+ return baseSendPromise();
+ }
+}
+jest.mock("../logger");
+jest.useFakeTimers();
+jest.mock("zksync-web3", () => ({
+ Provider: JsonRpcProviderBaseMock,
+}));
+import { JsonRpcProviderExtended } from "./jsonRpcProviderExtended";
+
+describe("JsonRpcProviderExtended", () => {
+ let jsonRpcProvider: JsonRpcProviderExtended;
+ const timer = mock();
+ let lastCallback: () => void;
+
+ beforeEach(async () => {
+ jsonRpcProvider = new JsonRpcProviderExtended("url", 120_000, 10_000);
+
+ jest.spyOn(global, "setTimeout").mockImplementation((callback: () => void) => {
+ lastCallback = callback;
+ return timer;
+ });
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe("send", () => {
+ it("calls base implementation and returns its value", async () => {
+ baseSendPromise.mockResolvedValueOnce({
+ method: "method",
+ params: [1, 2],
+ });
+ const result = await jsonRpcProvider.send("method", [1, 2]);
+ expect(result).toStrictEqual({
+ method: "method",
+ params: [1, 2],
+ });
+ });
+
+ it("starts quick timeout", async () => {
+ await jsonRpcProvider.send("method", [1, 2]);
+ expect(global.setTimeout).toBeCalledTimes(1);
+ expect(global.setTimeout).toBeCalledWith(expect.any(Function), 10_000);
+ });
+
+ it("clears quick timeout", async () => {
+ jest.spyOn(global, "clearTimeout");
+ await jsonRpcProvider.send("method", [1, 2]);
+ expect(global.clearTimeout).toBeCalledTimes(1);
+ expect(global.clearTimeout).toBeCalledWith(timer);
+ });
+
+ describe("when base send throws an error", () => {
+ const error = new Error("test error");
+ beforeEach(() => {
+ baseSendPromise.mockRejectedValue(error);
+ });
+
+ it("throws the same error", async () => {
+ await expect(jsonRpcProvider.send("method", [1, 2])).rejects.toThrowError(error);
+ });
+ });
+
+ describe("when timeout occurs faster than send returns value", () => {
+ beforeEach(() => {
+ baseSendPromise.mockImplementationOnce(() => {
+ lastCallback();
+ return {
+ method: "method1",
+ params: [1, 2],
+ };
+ });
+ baseSendPromise.mockResolvedValueOnce({
+ method: "method2",
+ params: [2, 3],
+ });
+ });
+
+ it("waits for internal timeout and returns the result of the second call", async () => {
+ const result = await jsonRpcProvider.send("method", [1, 2]);
+ expect(global.setTimeout).toBeCalledTimes(1);
+ expect(global.setTimeout).toBeCalledWith(expect.any(Function), 10_000);
+ expect(result).toStrictEqual({
+ method: "method2",
+ params: [2, 3],
+ });
+ });
+
+ it("when timeout is already cleared does not throw an error", async () => {
+ baseSendPromise.mockImplementationOnce(() => {
+ lastCallback();
+ return {
+ method: "method1",
+ params: [1, 2],
+ };
+ });
+ jest.spyOn(global, "setTimeout").mockImplementation((callback: () => void) => {
+ lastCallback = callback;
+ return null;
+ });
+ const result = await jsonRpcProvider.send("method", [1, 2]);
+ expect(result).toBeUndefined();
+ });
+ });
+ });
+
+ describe("getState", () => {
+ it("returns open", () => {
+ const state = jsonRpcProvider.getState();
+ expect(state).toBe("open");
+ });
+ });
+});
diff --git a/packages/data-fetcher/src/rpcProvider/jsonRpcProviderExtended.ts b/packages/data-fetcher/src/rpcProvider/jsonRpcProviderExtended.ts
new file mode 100644
index 0000000000..eb2caaaf30
--- /dev/null
+++ b/packages/data-fetcher/src/rpcProvider/jsonRpcProviderExtended.ts
@@ -0,0 +1,64 @@
+import { Provider } from "zksync-web3";
+import { ProviderState, JsonRpcProviderBase } from "./jsonRpcProviderBase";
+import logger from "../logger";
+
+export class QuickTimeoutError extends Error {
+ constructor() {
+ super();
+ }
+}
+
+export class JsonRpcProviderExtended extends Provider implements JsonRpcProviderBase {
+ private readonly connectionQuickTimeout;
+ constructor(providerUrl: string, connectionTimeout: number, connectionQuickTimeout: number) {
+ super({
+ url: providerUrl,
+ timeout: connectionTimeout,
+ });
+ this.connectionQuickTimeout = connectionQuickTimeout;
+ }
+
+ private startQuickTimeout(timeout) {
+ let timer: NodeJS.Timer = null;
+ const promise = new Promise((resolve, reject) => {
+ timer = setTimeout(() => {
+ timer ? reject(new QuickTimeoutError()) : resolve(undefined);
+ }, timeout);
+ });
+
+ const cancel = () => {
+ if (timer) {
+ clearTimeout(timer);
+ timer = null;
+ }
+ };
+
+ return { promise, cancel };
+ }
+
+ public getState(): ProviderState {
+ return "open";
+ }
+
+ public override async send(method: string, params: Array): Promise {
+ const quickTimeout = this.startQuickTimeout(this.connectionQuickTimeout);
+ try {
+ return await Promise.race([quickTimeout.promise, super.send(method, params)]);
+ } catch (e) {
+ if (e instanceof QuickTimeoutError) {
+ logger.error({
+ message: "RPC provider: quick timeout",
+ stack: e.stack,
+ method,
+ params,
+ timeout: this.connectionQuickTimeout,
+ context: JsonRpcProviderExtended.name,
+ });
+ return super.send(method, params);
+ }
+ throw e;
+ } finally {
+ quickTimeout.cancel();
+ }
+ }
+}
diff --git a/packages/data-fetcher/src/rpcProvider/webSocketProviderExtended.ts b/packages/data-fetcher/src/rpcProvider/webSocketProviderExtended.ts
new file mode 100644
index 0000000000..0d777bc4b7
--- /dev/null
+++ b/packages/data-fetcher/src/rpcProvider/webSocketProviderExtended.ts
@@ -0,0 +1,120 @@
+import { providers } from "ethers";
+import logger from "../logger";
+import { ProviderState } from "./jsonRpcProviderBase";
+
+const expectedPongBack = 10000;
+const checkInterval = 12000;
+const pendingRequestsLimit = 100000;
+
+export class TimeoutError extends Error {
+ constructor(message: string) {
+ super(message);
+ }
+}
+
+export class WebSocketProviderExtended extends providers.WebSocketProvider {
+ private state: ProviderState = "connecting";
+ private readonly connectionQuickTimeout: number;
+ private readonly connectionTimeout: number;
+
+ constructor(providerUrl, connectionTimeout: number, connectionQuickTimeout: number) {
+ super(providerUrl);
+ this.connectionTimeout = connectionTimeout;
+ this.connectionQuickTimeout = connectionQuickTimeout;
+ this.attachStateCheck();
+ }
+
+ public override async send(method: string, params: Array): Promise {
+ const quickTimeout = this.startTimeout(this.connectionQuickTimeout, "WS RPC provider: quick timeout");
+ try {
+ return await Promise.race([quickTimeout.promise, super.send(method, params)]);
+ } catch (e) {
+ if (e instanceof TimeoutError) {
+ logger.error({
+ message: e.message,
+ stack: e.stack,
+ method,
+ params,
+ timeout: this.connectionQuickTimeout,
+ context: WebSocketProviderExtended.name,
+ });
+
+ const timeout = this.startTimeout(this.connectionTimeout, "WS RPC provider: timeout");
+ try {
+ return await Promise.race([timeout.promise, super.send(method, params)]);
+ } finally {
+ timeout.cancel();
+ }
+ }
+ throw e;
+ } finally {
+ quickTimeout.cancel();
+ }
+ }
+
+ private startTimeout(timeout: number, errorMessage = "WS RPC provider: timeout") {
+ let timer: NodeJS.Timer = null;
+ const promise = new Promise((resolve, reject) => {
+ timer = setTimeout(() => {
+ timer ? reject(new TimeoutError(errorMessage)) : resolve(undefined);
+ }, timeout);
+ });
+
+ const cancel = () => {
+ if (timer) {
+ clearTimeout(timer);
+ timer = null;
+ }
+ };
+
+ return { promise, cancel };
+ }
+
+ private attachStateCheck(): void {
+ let pingTimeout: NodeJS.Timeout;
+ let keepAliveInterval: NodeJS.Timeout;
+
+ this._websocket.on("open", () => {
+ this.state = "open";
+
+ logger.debug("Web socket has been opened");
+
+ keepAliveInterval = setInterval(() => {
+ this._websocket.ping();
+ pingTimeout = setTimeout(() => {
+ logger.error({
+ message: "No response for the ping request. Web socket connection will be terminated",
+ context: WebSocketProviderExtended.name,
+ });
+ this._websocket.terminate();
+ }, expectedPongBack);
+
+ if (Object.keys(this._requests).length > pendingRequestsLimit) {
+ logger.error({
+ message: "Too many pending requests. Web socket connection will be terminated",
+ context: WebSocketProviderExtended.name,
+ });
+ this._websocket.terminate();
+ return;
+ }
+ }, checkInterval);
+ });
+
+ this._websocket.on("close", () => {
+ this.state = "closed";
+
+ logger.debug("Web socket has been closed");
+
+ if (keepAliveInterval) clearInterval(keepAliveInterval);
+ if (pingTimeout) clearTimeout(pingTimeout);
+ });
+
+ this._websocket.on("pong", () => {
+ if (pingTimeout) clearTimeout(pingTimeout);
+ });
+ }
+
+ public getState(): ProviderState {
+ return this.state;
+ }
+}
diff --git a/packages/data-fetcher/src/rpcProvider/wrappedWebSocketProvider.ts b/packages/data-fetcher/src/rpcProvider/wrappedWebSocketProvider.ts
new file mode 100644
index 0000000000..d9d34e6db4
--- /dev/null
+++ b/packages/data-fetcher/src/rpcProvider/wrappedWebSocketProvider.ts
@@ -0,0 +1,56 @@
+import { ProviderState } from "./jsonRpcProviderBase";
+import { WebSocketProviderExtended } from "./webSocketProviderExtended";
+
+const monitorInterval = 10000;
+
+export class WrappedWebSocketProvider {
+ private readonly providerUrl: string;
+ private readonly connectionTimeout: number;
+ private readonly connectionQuickTimeout: number;
+ private instances: WebSocketProviderExtended[] = [];
+
+ constructor(providerUrl: string, connectionTimeout: number, connectionQuickTimeout: number, maxConnections = 5) {
+ this.providerUrl = providerUrl;
+ this.connectionTimeout = connectionTimeout;
+ this.connectionQuickTimeout = connectionQuickTimeout;
+
+ for (let i = 0; i < maxConnections; i++) {
+ this.instances[i] = new WebSocketProviderExtended(
+ this.providerUrl,
+ this.connectionTimeout,
+ this.connectionQuickTimeout
+ );
+ }
+ this.monitorInstances();
+ }
+
+ public getProvider(): WebSocketProviderExtended {
+ const totalActiveInstances = this.instances.filter((instance) => instance.getState() !== "closed");
+ const randomInstanceNumber = Math.floor(Math.random() * totalActiveInstances.length);
+ return this.instances[randomInstanceNumber];
+ }
+
+ private monitorInstances(): void {
+ setInterval(() => {
+ for (let i = 0; i < this.instances.length; i++) {
+ if (this.instances[i].getState() === "closed") {
+ this.instances[i] = new WebSocketProviderExtended(
+ this.providerUrl,
+ this.connectionTimeout,
+ this.connectionQuickTimeout
+ );
+ }
+ }
+ }, monitorInterval);
+ }
+
+ public getState(): ProviderState {
+ if (this.instances.find((instance) => instance.getState() === "open")) {
+ return "open";
+ }
+ if (this.instances.find((instance) => instance.getState() === "connecting")) {
+ return "connecting";
+ }
+ return "closed";
+ }
+}
diff --git a/packages/data-fetcher/src/token/token.service.spec.ts b/packages/data-fetcher/src/token/token.service.spec.ts
new file mode 100644
index 0000000000..1108c8f46b
--- /dev/null
+++ b/packages/data-fetcher/src/token/token.service.spec.ts
@@ -0,0 +1,523 @@
+import { mock } from "jest-mock-extended";
+import { types } from "zksync-web3";
+import { BASE_TOKEN_ADDRESS, ETH_L1_ADDRESS } from "../constants";
+import { Test, TestingModule } from "@nestjs/testing";
+import { Logger } from "@nestjs/common";
+import { BlockchainService } from "../blockchain/blockchain.service";
+import { TokenService } from "./token.service";
+import { ContractAddress } from "../address/interface/contractAddress.interface";
+
+describe("TokenService", () => {
+ let tokenService: TokenService;
+ let blockchainServiceMock: BlockchainService;
+ let startGetTokenInfoDurationMetricMock: jest.Mock;
+ let stopGetTokenInfoDurationMetricMock: jest.Mock;
+
+ beforeEach(async () => {
+ blockchainServiceMock = mock({
+ bridgeAddresses: {
+ l2Erc20DefaultBridge: "0x0000000000000000000000000000000000001111",
+ },
+ });
+
+ stopGetTokenInfoDurationMetricMock = jest.fn();
+ startGetTokenInfoDurationMetricMock = jest.fn().mockReturnValue(stopGetTokenInfoDurationMetricMock);
+
+ const app: TestingModule = await Test.createTestingModule({
+ providers: [
+ TokenService,
+ {
+ provide: BlockchainService,
+ useValue: blockchainServiceMock,
+ },
+ {
+ provide: "PROM_METRIC_GET_TOKEN_INFO_DURATION_SECONDS",
+ useValue: {
+ startTimer: startGetTokenInfoDurationMetricMock,
+ },
+ },
+ ],
+ }).compile();
+
+ app.useLogger(mock());
+
+ tokenService = app.get(TokenService);
+ });
+
+ describe("getERC20Token", () => {
+ let deployedContractAddress: ContractAddress;
+ let transactionReceipt: types.TransactionReceipt;
+ let tokenData;
+
+ beforeEach(() => {
+ tokenData = {
+ symbol: "symbol",
+ decimals: 18,
+ name: "name",
+ };
+
+ transactionReceipt = mock({
+ logs: [],
+ to: "0x0000000000000000000000000000000000001111",
+ });
+
+ deployedContractAddress = mock({
+ address: "0xdc187378edd8ed1585fb47549cc5fe633295d571",
+ blockNumber: 10,
+ transactionHash: "transactionHash",
+ logIndex: 20,
+ });
+
+ jest.spyOn(blockchainServiceMock, "getERC20TokenData").mockResolvedValue(tokenData);
+ });
+
+ describe("when there is neither bridge initialization nor bridge initialize log for the current token address", () => {
+ beforeEach(() => {
+ transactionReceipt.logs = [];
+ });
+
+ it("starts the get token info duration metric", async () => {
+ await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(startGetTokenInfoDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets token data by the contract address", async () => {
+ await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(blockchainServiceMock.getERC20TokenData).toHaveBeenCalledTimes(1);
+ expect(blockchainServiceMock.getERC20TokenData).toHaveBeenCalledWith(deployedContractAddress.address);
+ });
+
+ it("returns the token without l1Address", async () => {
+ const token = await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(token).toStrictEqual({
+ ...tokenData,
+ blockNumber: deployedContractAddress.blockNumber,
+ transactionHash: deployedContractAddress.transactionHash,
+ l2Address: deployedContractAddress.address,
+ logIndex: deployedContractAddress.logIndex,
+ });
+ });
+
+ describe("when contract is ETH L2 contract", () => {
+ it("returns ETH token with ETH l1Address", async () => {
+ const ethTokenData = {
+ symbol: "ETH",
+ decimals: 18,
+ name: "Ethers",
+ l1Address: ETH_L1_ADDRESS,
+ };
+ const deployedETHContractAddress = mock({
+ address: BASE_TOKEN_ADDRESS,
+ blockNumber: 0,
+ transactionHash: "transactionHash",
+ logIndex: 0,
+ });
+ (blockchainServiceMock.getERC20TokenData as jest.Mock).mockResolvedValueOnce(ethTokenData);
+ const token = await tokenService.getERC20Token(deployedETHContractAddress, transactionReceipt);
+ expect(token).toStrictEqual({
+ ...ethTokenData,
+ blockNumber: deployedETHContractAddress.blockNumber,
+ transactionHash: deployedETHContractAddress.transactionHash,
+ l2Address: BASE_TOKEN_ADDRESS,
+ l1Address: ETH_L1_ADDRESS,
+ logIndex: deployedETHContractAddress.logIndex,
+ });
+ });
+ });
+
+ it("tracks the get token info duration metric", async () => {
+ await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(stopGetTokenInfoDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ describe("if ERC20 Contract function throws an exception", () => {
+ beforeEach(() => {
+ jest.spyOn(blockchainServiceMock, "getERC20TokenData").mockImplementation(() => {
+ throw new Error("Ethers Contract error");
+ });
+ });
+
+ it("returns null", async () => {
+ const token = await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(token).toBeNull();
+ });
+
+ it("does not track the get token info duration metric", async () => {
+ await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(stopGetTokenInfoDurationMetricMock).toHaveBeenCalledTimes(0);
+ });
+ });
+ });
+
+ describe("when transaction receipt does not contain logs", () => {
+ beforeEach(() => {
+ transactionReceipt.logs = null;
+ });
+
+ it("starts the get token info duration metric", async () => {
+ await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(startGetTokenInfoDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets token data by the contract address", async () => {
+ await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(blockchainServiceMock.getERC20TokenData).toHaveBeenCalledTimes(1);
+ expect(blockchainServiceMock.getERC20TokenData).toHaveBeenCalledWith(deployedContractAddress.address);
+ });
+
+ it("returns the token without l1Address", async () => {
+ const token = await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(token).toStrictEqual({
+ ...tokenData,
+ blockNumber: deployedContractAddress.blockNumber,
+ transactionHash: deployedContractAddress.transactionHash,
+ l2Address: deployedContractAddress.address,
+ logIndex: deployedContractAddress.logIndex,
+ });
+ });
+
+ it("tracks the get token info duration metric", async () => {
+ await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(stopGetTokenInfoDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ describe("if ERC20 Contract function throws an exception", () => {
+ beforeEach(() => {
+ jest.spyOn(blockchainServiceMock, "getERC20TokenData").mockImplementation(() => {
+ throw new Error("Ethers Contract error");
+ });
+ });
+
+ it("returns null", async () => {
+ const token = await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(token).toBeNull();
+ });
+
+ it("does not track the get token info duration metric", async () => {
+ await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(stopGetTokenInfoDurationMetricMock).toHaveBeenCalledTimes(0);
+ });
+ });
+ });
+
+ describe("when there is a bridge initialization log in transaction receipt for the current token address", () => {
+ let bridgedToken;
+
+ beforeEach(() => {
+ transactionReceipt.logs = [
+ mock({
+ topics: [
+ "0x290afdae231a3fc0bbae8b1af63698b0a1d79b21ad17df0342dfb952fe74f8e5",
+ "0x000000000000000000000000c7e0220d02d549c4846a6ec31d89c3b670ebe35c",
+ "0x0100014340e955cbf39159da998b3374bee8f3c0b3c75a7a9e3df6b85052379d",
+ "0x000000000000000000000000dc187378edd8ed1585fb47549cc5fe633295d571",
+ ],
+ }),
+ mock({
+ address: "0xdc187378edD8Ed1585fb47549Cc5fe633295d571",
+ topics: [
+ "0xe6b2ac4004ee4493db8844da5db69722d2128345671818c3c41928655a83fb2c",
+ "0x0000000000000000000000000db321efaa9e380d0b37b55b530cdaa62728b9a3",
+ ],
+ data: "0x000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000441444c3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000441444c3100000000000000000000000000000000000000000000000000000000",
+ }),
+ ];
+
+ bridgedToken = {
+ name: "ADL1",
+ symbol: "ADL1",
+ decimals: 18,
+ };
+ });
+
+ it("extract token info from log and does not call web3 API to get token data", async () => {
+ await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(blockchainServiceMock.getERC20TokenData).toHaveBeenCalledTimes(0);
+ });
+
+ it("returns the token with l1Address", async () => {
+ const token = await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(token).toStrictEqual({
+ ...bridgedToken,
+ blockNumber: deployedContractAddress.blockNumber,
+ transactionHash: deployedContractAddress.transactionHash,
+ l2Address: deployedContractAddress.address,
+ l1Address: "0x0Db321EFaa9E380d0B37B55B530CDaA62728B9a3",
+ logIndex: deployedContractAddress.logIndex,
+ });
+ });
+ });
+
+ 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.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(startGetTokenInfoDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets token data by the contract address", async () => {
+ await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(blockchainServiceMock.getERC20TokenData).toHaveBeenCalledTimes(1);
+ expect(blockchainServiceMock.getERC20TokenData).toHaveBeenCalledWith(deployedContractAddress.address);
+ });
+
+ it("returns the token without l1Address", async () => {
+ const token = await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(token).toStrictEqual({
+ ...tokenData,
+ blockNumber: deployedContractAddress.blockNumber,
+ transactionHash: deployedContractAddress.transactionHash,
+ l2Address: deployedContractAddress.address,
+ logIndex: deployedContractAddress.logIndex,
+ });
+ });
+ });
+
+ describe("when there is a bridge initialize log in transaction receipt for the current token address", () => {
+ let bridgedToken;
+
+ beforeEach(() => {
+ 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,
+ });
+
+ bridgedToken = {
+ name: "L111 deployed to L1",
+ symbol: "L111",
+ decimals: 18,
+ };
+ });
+
+ it("extract token info from log and does not call web3 API to get token data", async () => {
+ await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(blockchainServiceMock.getERC20TokenData).toHaveBeenCalledTimes(0);
+ });
+
+ it("returns the token with l1Address", async () => {
+ const token = await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(token).toStrictEqual({
+ ...bridgedToken,
+ blockNumber: deployedContractAddress.blockNumber,
+ transactionHash: deployedContractAddress.transactionHash,
+ l2Address: deployedContractAddress.address,
+ l1Address: "0xc8F8cE6491227a6a2Ab92e67a64011a4Eba1C6CF",
+ logIndex: deployedContractAddress.logIndex,
+ });
+ });
+ });
+
+ 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.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(startGetTokenInfoDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets token data by the contract address", async () => {
+ await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(blockchainServiceMock.getERC20TokenData).toHaveBeenCalledTimes(1);
+ expect(blockchainServiceMock.getERC20TokenData).toHaveBeenCalledWith(deployedContractAddress.address);
+ });
+
+ it("returns the token without l1Address", async () => {
+ const token = await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(token).toStrictEqual({
+ ...tokenData,
+ blockNumber: deployedContractAddress.blockNumber,
+ transactionHash: deployedContractAddress.transactionHash,
+ l2Address: deployedContractAddress.address,
+ logIndex: deployedContractAddress.logIndex,
+ });
+ });
+ });
+
+ describe("when there is a bridge initialize log in transaction receipt but the default bridge contract is not defined", () => {
+ beforeEach(() => {
+ blockchainServiceMock.bridgeAddresses.l2Erc20DefaultBridge = undefined;
+ 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.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(startGetTokenInfoDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets token data by the contract address", async () => {
+ await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(blockchainServiceMock.getERC20TokenData).toHaveBeenCalledTimes(1);
+ expect(blockchainServiceMock.getERC20TokenData).toHaveBeenCalledWith(deployedContractAddress.address);
+ });
+
+ it("returns the token without l1Address", async () => {
+ const token = await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(token).toStrictEqual({
+ ...tokenData,
+ blockNumber: deployedContractAddress.blockNumber,
+ transactionHash: deployedContractAddress.transactionHash,
+ l2Address: deployedContractAddress.address,
+ logIndex: deployedContractAddress.logIndex,
+ });
+ });
+ });
+
+ describe("if the token symbol or name has special symbols", () => {
+ beforeEach(() => {
+ jest.spyOn(blockchainServiceMock, "getERC20TokenData").mockResolvedValueOnce({
+ ...tokenData,
+ symbol: "\0\0\0\0\0\0test symbol",
+ name: "\0\0\0\0\0\0test name",
+ });
+ });
+
+ it("returns token with special chars replaced", async () => {
+ const token = await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(token).toEqual({
+ blockNumber: 10,
+ decimals: 18,
+ l2Address: "0xdc187378edd8ed1585fb47549cc5fe633295d571",
+ logIndex: 20,
+ name: "test name",
+ symbol: "test symbol",
+ transactionHash: "transactionHash",
+ });
+ });
+ });
+
+ describe("if the token symbol is empty", () => {
+ beforeEach(() => {
+ jest.spyOn(blockchainServiceMock, "getERC20TokenData").mockResolvedValueOnce({
+ ...tokenData,
+ symbol: "",
+ });
+ });
+
+ it("returns null", async () => {
+ const token = await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(token).toBeNull();
+ });
+ });
+
+ describe("if the token symbol has special symbols only", () => {
+ beforeEach(() => {
+ jest.spyOn(blockchainServiceMock, "getERC20TokenData").mockResolvedValueOnce({
+ ...tokenData,
+ symbol: "\0\0\0\0\0\0",
+ });
+ });
+
+ it("returns null", async () => {
+ const token = await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(token).toBeNull();
+ });
+ });
+
+ describe("when transactionReceipt param is not provided", () => {
+ it("returns the token without l1Address when token is valid", async () => {
+ const token = await tokenService.getERC20Token(deployedContractAddress);
+ expect(token).toStrictEqual({
+ ...tokenData,
+ blockNumber: deployedContractAddress.blockNumber,
+ transactionHash: deployedContractAddress.transactionHash,
+ l2Address: deployedContractAddress.address,
+ logIndex: deployedContractAddress.logIndex,
+ });
+ });
+ });
+ });
+});
diff --git a/packages/data-fetcher/src/token/token.service.ts b/packages/data-fetcher/src/token/token.service.ts
new file mode 100644
index 0000000000..a9ccf88a63
--- /dev/null
+++ b/packages/data-fetcher/src/token/token.service.ts
@@ -0,0 +1,121 @@
+import { types } from "zksync-web3";
+import { Injectable, Logger } from "@nestjs/common";
+import { InjectMetric } from "@willsoto/nestjs-prometheus";
+import { Histogram } from "prom-client";
+import { LogType, isLogOfType } from "../log/logType";
+import { BlockchainService } from "../blockchain/blockchain.service";
+import { GET_TOKEN_INFO_DURATION_METRIC_NAME } from "../metrics";
+import { ContractAddress } from "../address/interface/contractAddress.interface";
+import parseLog from "../utils/parseLog";
+import { CONTRACT_INTERFACES, BASE_TOKEN_ADDRESS, ETH_L1_ADDRESS } from "../constants";
+
+export interface Token {
+ l2Address: string;
+ l1Address: string;
+ symbol: string;
+ decimals: number;
+ name: string;
+ blockNumber: number;
+ transactionHash: string;
+ logIndex: number;
+}
+
+export enum TokenType {
+ BaseToken = "BASETOKEN",
+ ERC20 = "ERC20",
+ ERC721 = "ERC721",
+}
+
+@Injectable()
+export class TokenService {
+ private readonly logger: Logger;
+
+ constructor(
+ private readonly blockchainService: BlockchainService,
+ @InjectMetric(GET_TOKEN_INFO_DURATION_METRIC_NAME)
+ private readonly getTokenInfoDurationMetric: Histogram
+ ) {
+ this.logger = new Logger(TokenService.name);
+ }
+
+ private async getERC20TokenData(contractAddress: string): Promise<{
+ symbol: string;
+ decimals: number;
+ name: string;
+ }> {
+ try {
+ return await this.blockchainService.getERC20TokenData(contractAddress);
+ } catch {
+ this.logger.log({
+ message: "Cannot parse ERC20 contract. Might be a token of a different type.",
+ contractAddress,
+ });
+ return null;
+ }
+ }
+
+ private removeSpecialChars(str: string | null): string {
+ if (!str) {
+ return str;
+ }
+ return str.replace(/\0/g, "");
+ }
+
+ public async getERC20Token(
+ contractAddress: ContractAddress,
+ transactionReceipt?: types.TransactionReceipt
+ ): Promise {
+ let erc20Token: {
+ symbol: string;
+ decimals: number;
+ name: string;
+ l1Address?: string;
+ };
+
+ const bridgeLog =
+ transactionReceipt &&
+ 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);
+ erc20Token = {
+ name: parsedLog.args.name,
+ symbol: parsedLog.args.symbol,
+ decimals: parsedLog.args.decimals,
+ l1Address: parsedLog.args.l1Token,
+ };
+ } else {
+ const stopGetTokenInfoDurationMetric = this.getTokenInfoDurationMetric.startTimer();
+ erc20Token = await this.getERC20TokenData(contractAddress.address);
+ if (erc20Token) {
+ stopGetTokenInfoDurationMetric();
+ }
+ }
+
+ if (erc20Token) {
+ erc20Token.symbol = this.removeSpecialChars(erc20Token.symbol);
+ erc20Token.name = this.removeSpecialChars(erc20Token.name);
+
+ if (erc20Token.symbol) {
+ return {
+ ...erc20Token,
+ blockNumber: contractAddress.blockNumber,
+ transactionHash: contractAddress.transactionHash,
+ l2Address: contractAddress.address,
+ logIndex: contractAddress.logIndex,
+ // add L1 address for ETH token
+ ...(contractAddress.address.toLowerCase() === BASE_TOKEN_ADDRESS && {
+ l1Address: ETH_L1_ADDRESS,
+ }),
+ };
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/packages/data-fetcher/src/transaction/index.ts b/packages/data-fetcher/src/transaction/index.ts
new file mode 100644
index 0000000000..6ac0ba14b0
--- /dev/null
+++ b/packages/data-fetcher/src/transaction/index.ts
@@ -0,0 +1 @@
+export * from "./transaction.service";
diff --git a/packages/data-fetcher/src/transaction/transaction.service.spec.ts b/packages/data-fetcher/src/transaction/transaction.service.spec.ts
new file mode 100644
index 0000000000..c8582d46e5
--- /dev/null
+++ b/packages/data-fetcher/src/transaction/transaction.service.spec.ts
@@ -0,0 +1,202 @@
+import { Test } from "@nestjs/testing";
+import { Logger } from "@nestjs/common";
+import { mock } from "jest-mock-extended";
+import { types } from "zksync-web3";
+import { BlockchainService, TraceTransactionResult } from "../blockchain";
+import { TransactionService } from "./transaction.service";
+import { LogService } from "../log";
+
+describe("TransactionService", () => {
+ let transactionProcessor: TransactionService;
+ let blockchainServiceMock: BlockchainService;
+ let logServiceMock: LogService;
+
+ let startTxProcessingDurationMetricMock: jest.Mock;
+ let stopTxProcessingDurationMetricMock: jest.Mock;
+
+ let startGetTransactionInfoDurationMetricMock: jest.Mock;
+ let stopGetTransactionInfoDurationMetricMock: jest.Mock;
+
+ beforeEach(async () => {
+ blockchainServiceMock = mock();
+ logServiceMock = mock();
+
+ stopTxProcessingDurationMetricMock = jest.fn();
+ startTxProcessingDurationMetricMock = jest.fn().mockReturnValue(stopTxProcessingDurationMetricMock);
+
+ stopGetTransactionInfoDurationMetricMock = jest.fn();
+ startGetTransactionInfoDurationMetricMock = jest.fn().mockReturnValue(stopGetTransactionInfoDurationMetricMock);
+
+ const app = await Test.createTestingModule({
+ providers: [
+ TransactionService,
+ {
+ provide: BlockchainService,
+ useValue: blockchainServiceMock,
+ },
+ {
+ provide: LogService,
+ useValue: logServiceMock,
+ },
+ {
+ provide: "PROM_METRIC_TRANSACTION_PROCESSING_DURATION_SECONDS",
+ useValue: {
+ startTimer: startTxProcessingDurationMetricMock,
+ },
+ },
+ {
+ provide: "PROM_METRIC_GET_TRANSACTION_INFO_DURATION_SECONDS",
+ useValue: {
+ startTimer: startGetTransactionInfoDurationMetricMock,
+ },
+ },
+ ],
+ }).compile();
+
+ app.useLogger(mock());
+
+ transactionProcessor = app.get(TransactionService);
+ });
+
+ describe("getData", () => {
+ const blockDetails = mock({
+ number: 1,
+ l1BatchNumber: 3,
+ });
+ const transaction = mock({ hash: "0" });
+ const transactionReceipt = mock({
+ transactionIndex: 0,
+ logs: [mock(), mock()],
+ 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 () => {
+ await transactionProcessor.getData(transaction.hash, blockDetails);
+ expect(startTxProcessingDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("starts the get info transaction duration metric", async () => {
+ await transactionProcessor.getData(transaction.hash, blockDetails);
+ expect(stopGetTransactionInfoDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("reads transaction data by hash", async () => {
+ await transactionProcessor.getData(transaction.hash, blockDetails);
+ expect(blockchainServiceMock.getTransaction).toHaveBeenCalledTimes(1);
+ expect(blockchainServiceMock.getTransaction).toHaveBeenCalledWith(transaction.hash);
+ });
+
+ it("reads transaction details by hash", async () => {
+ await transactionProcessor.getData(transaction.hash, blockDetails);
+ expect(blockchainServiceMock.getTransactionDetails).toHaveBeenCalledTimes(1);
+ expect(blockchainServiceMock.getTransactionDetails).toHaveBeenCalledWith(transaction.hash);
+ });
+
+ it("reads transaction receipt by hash", async () => {
+ await transactionProcessor.getData(transaction.hash, blockDetails);
+ expect(blockchainServiceMock.getTransactionReceipt).toHaveBeenCalledTimes(1);
+ expect(blockchainServiceMock.getTransactionReceipt).toHaveBeenCalledWith(transaction.hash);
+ });
+
+ it("stops the get info transaction duration metric", async () => {
+ await transactionProcessor.getData(transaction.hash, blockDetails);
+ expect(stopGetTransactionInfoDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("throws error if transaction data by hash API returns null", async () => {
+ jest.spyOn(blockchainServiceMock, "getTransaction").mockResolvedValue(null);
+ await expect(transactionProcessor.getData(transaction.hash, blockDetails)).rejects.toThrowError(
+ new Error(`Some of the blockchain transaction APIs returned null for a transaction ${transaction.hash}`)
+ );
+ });
+
+ it("throws error if transaction details by hash API returns null", async () => {
+ jest.spyOn(blockchainServiceMock, "getTransactionDetails").mockResolvedValue(null);
+ await expect(transactionProcessor.getData(transaction.hash, blockDetails)).rejects.toThrowError(
+ new Error(`Some of the blockchain transaction APIs returned null for a transaction ${transaction.hash}`)
+ );
+ });
+
+ it("throws error if transaction receipt by hash API returns null", async () => {
+ jest.spyOn(blockchainServiceMock, "getTransactionReceipt").mockResolvedValue(null);
+ await expect(transactionProcessor.getData(transaction.hash, blockDetails)).rejects.toThrowError(
+ new Error(`Some of the blockchain transaction APIs returned null for a transaction ${transaction.hash}`)
+ );
+ });
+
+ it("returns data with transaction info", async () => {
+ const txData = await transactionProcessor.getData(transaction.hash, blockDetails);
+ expect(txData.transaction).toEqual({
+ ...transaction,
+ ...transactionDetails,
+ l1BatchNumber: blockDetails.l1BatchNumber,
+ receiptStatus: transactionReceipt.status,
+ });
+ });
+
+ it("returns data with transaction receipt", async () => {
+ const txData = await transactionProcessor.getData(transaction.hash, blockDetails);
+ expect(txData.transactionReceipt).toEqual(transactionReceipt);
+ });
+
+ it("stops the transaction duration metric", async () => {
+ await transactionProcessor.getData(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.getData(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("returns data with transaction info with error and revert reason", async () => {
+ const txData = await transactionProcessor.getData(transaction.hash, blockDetails);
+ expect(txData.transaction).toEqual({
+ ...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("returns data with transaction info without error and revert reason", async () => {
+ (blockchainServiceMock.debugTraceTransaction as jest.Mock).mockResolvedValueOnce(null);
+ const txData = await transactionProcessor.getData(transaction.hash, blockDetails);
+ expect(txData.transaction).toEqual({
+ ...transaction,
+ ...transactionDetails,
+ l1BatchNumber: blockDetails.l1BatchNumber,
+ receiptStatus: 0,
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/packages/data-fetcher/src/transaction/transaction.service.ts b/packages/data-fetcher/src/transaction/transaction.service.ts
new file mode 100644
index 0000000000..3b0e31dc4d
--- /dev/null
+++ b/packages/data-fetcher/src/transaction/transaction.service.ts
@@ -0,0 +1,90 @@
+import { Injectable, Logger } from "@nestjs/common";
+import { InjectMetric } from "@willsoto/nestjs-prometheus";
+import { Histogram } from "prom-client";
+import { types } from "zksync-web3";
+import { BlockchainService } from "../blockchain/blockchain.service";
+import { TRANSACTION_PROCESSING_DURATION_METRIC_NAME, GET_TRANSACTION_INFO_DURATION_METRIC_NAME } from "../metrics";
+import { LogService, LogsData } from "../log/log.service";
+
+export interface TransactionInfo extends types.TransactionResponse {
+ fee: string;
+ receiptStatus: number;
+ isL1Originated: boolean;
+ receivedAt: Date;
+ error?: string;
+ revertReason?: string;
+}
+
+export interface TransactionData extends LogsData {
+ transaction: TransactionInfo;
+ transactionReceipt: types.TransactionReceipt;
+}
+
+@Injectable()
+export class TransactionService {
+ private readonly logger: Logger;
+
+ public constructor(
+ private readonly blockchainService: BlockchainService,
+ private readonly logService: LogService,
+ @InjectMetric(TRANSACTION_PROCESSING_DURATION_METRIC_NAME)
+ private readonly transactionProcessingDurationMetric: Histogram,
+ @InjectMetric(GET_TRANSACTION_INFO_DURATION_METRIC_NAME)
+ private readonly getTransactionInfoDurationMetric: Histogram
+ ) {
+ this.logger = new Logger(TransactionService.name);
+ }
+
+ public async getData(transactionHash: string, blockDetails: types.BlockDetails): Promise {
+ const stopTransactionProcessingMeasuring = this.transactionProcessingDurationMetric.startTimer();
+
+ this.logger.debug({
+ message: "Getting transaction data from the blockchain",
+ blockNumber: blockDetails.number,
+ transactionHash,
+ });
+ const stopGetTransactionInfoDurationMetric = this.getTransactionInfoDurationMetric.startTimer();
+ const [transaction, transactionDetails, transactionReceipt] = await Promise.all([
+ this.blockchainService.getTransaction(transactionHash),
+ this.blockchainService.getTransactionDetails(transactionHash),
+ this.blockchainService.getTransactionReceipt(transactionHash),
+ ]);
+ stopGetTransactionInfoDurationMetric();
+
+ if (!transaction || !transactionDetails || !transactionReceipt) {
+ throw new Error(`Some of the blockchain transaction APIs returned null for a transaction ${transactionHash}`);
+ }
+
+ const transactionInfo = {
+ ...transaction,
+ ...transactionDetails,
+ l1BatchNumber: blockDetails.l1BatchNumber,
+ receiptStatus: transactionReceipt.status,
+ } as TransactionInfo;
+
+ if (transactionReceipt.status === 0) {
+ const debugTraceTransactionResult = await this.blockchainService.debugTraceTransaction(transactionHash, true);
+ if (debugTraceTransactionResult?.error) {
+ transactionInfo.error = debugTraceTransactionResult.error;
+ }
+ if (debugTraceTransactionResult?.revertReason) {
+ transactionInfo.revertReason = debugTraceTransactionResult.revertReason;
+ }
+ }
+
+ const logsData = await this.logService.getData(
+ transactionReceipt.logs,
+ blockDetails,
+ transactionDetails,
+ transactionReceipt
+ );
+
+ stopTransactionProcessingMeasuring();
+
+ return {
+ ...logsData,
+ transaction: transactionInfo,
+ transactionReceipt,
+ };
+ }
+}
diff --git a/packages/worker/src/transfer/extractHandlers/finalizeDeposit/default.handler.spec.ts b/packages/data-fetcher/src/transfer/extractHandlers/finalizeDeposit/default.handler.spec.ts
similarity index 93%
rename from packages/worker/src/transfer/extractHandlers/finalizeDeposit/default.handler.spec.ts
rename to packages/data-fetcher/src/transfer/extractHandlers/finalizeDeposit/default.handler.spec.ts
index 1c8f6eaa23..6f89d0cc43 100644
--- a/packages/worker/src/transfer/extractHandlers/finalizeDeposit/default.handler.spec.ts
+++ b/packages/data-fetcher/src/transfer/extractHandlers/finalizeDeposit/default.handler.spec.ts
@@ -1,10 +1,11 @@
import { BigNumber } from "ethers";
-import { types, utils } from "zksync-web3";
+import { types } from "zksync-web3";
import { mock } from "jest-mock-extended";
import { ZERO_HASH_64 } from "../../../constants";
-import { TransferType } from "../../../entities/transfer.entity";
-import { TokenType } from "../../../entities/token.entity";
+import { TransferType } from "../../transfer.service";
+import { TokenType } from "../../../token/token.service";
import { defaultFinalizeDepositHandler } from "./default.handler";
+import { BASE_TOKEN_ADDRESS } from "../../../../src/constants";
describe("defaultFinalizeDepositHandler", () => {
let log: types.Log;
@@ -66,8 +67,8 @@ describe("defaultFinalizeDepositHandler", () => {
it("extracts transfer with L2_ETH_TOKEN_ADDRESS as a tokenAddress if l2Token is 0x0000000000000000000000000000000000000000", () => {
log.topics[3] = ZERO_HASH_64;
const result = defaultFinalizeDepositHandler.extract(log, blockDetails);
- expect(result.tokenAddress).toBe(utils.L2_ETH_TOKEN_ADDRESS);
- expect(result.tokenType).toBe(TokenType.ETH);
+ expect(result.tokenAddress).toBe(BASE_TOKEN_ADDRESS);
+ expect(result.tokenType).toBe(TokenType.BaseToken);
});
it("extracts transfer with tokenAddress field populated with lower cased l2Token", () => {
diff --git a/packages/worker/src/transfer/extractHandlers/finalizeDeposit/default.handler.ts b/packages/data-fetcher/src/transfer/extractHandlers/finalizeDeposit/default.handler.ts
similarity index 72%
rename from packages/worker/src/transfer/extractHandlers/finalizeDeposit/default.handler.ts
rename to packages/data-fetcher/src/transfer/extractHandlers/finalizeDeposit/default.handler.ts
index db5f0f2c04..17d6d8d5dc 100644
--- a/packages/worker/src/transfer/extractHandlers/finalizeDeposit/default.handler.ts
+++ b/packages/data-fetcher/src/transfer/extractHandlers/finalizeDeposit/default.handler.ts
@@ -1,12 +1,12 @@
import { utils, types } from "zksync-web3";
import { Transfer } from "../../interfaces/transfer.interface";
import { ExtractTransferHandler } from "../../interfaces/extractTransferHandler.interface";
-import { TransferType } from "../../../entities/transfer.entity";
-import { TokenType } from "../../../entities/token.entity";
+import { TransferType } from "../../transfer.service";
+import { TokenType } from "../../../token/token.service";
import { unixTimeToDate } from "../../../utils/date";
import parseLog from "../../../utils/parseLog";
-import { CONTRACT_INTERFACES } from "../../../constants";
-
+import { BASE_TOKEN_ADDRESS, CONTRACT_INTERFACES } from "../../../constants";
+import { isBaseToken } from "../../../utils/token";
export const defaultFinalizeDepositHandler: ExtractTransferHandler = {
matches: (): boolean => true,
extract: (
@@ -16,7 +16,7 @@ export const defaultFinalizeDepositHandler: ExtractTransferHandler = {
): Transfer => {
const parsedLog = parseLog(CONTRACT_INTERFACES.L2_BRIDGE, log);
const tokenAddress =
- parsedLog.args.l2Token === utils.ETH_ADDRESS ? utils.L2_ETH_TOKEN_ADDRESS : parsedLog.args.l2Token.toLowerCase();
+ parsedLog.args.l2Token === utils.ETH_ADDRESS ? BASE_TOKEN_ADDRESS : parsedLog.args.l2Token.toLowerCase();
return {
from: parsedLog.args.l1Sender.toLowerCase(),
@@ -26,7 +26,7 @@ export const defaultFinalizeDepositHandler: ExtractTransferHandler = {
amount: parsedLog.args.amount,
tokenAddress,
type: TransferType.Deposit,
- tokenType: tokenAddress === utils.L2_ETH_TOKEN_ADDRESS ? TokenType.ETH : TokenType.ERC20,
+ tokenType: isBaseToken(tokenAddress) ? TokenType.BaseToken : TokenType.ERC20,
isFeeOrRefund: false,
logIndex: log.logIndex,
transactionIndex: log.transactionIndex,
diff --git a/packages/worker/src/transfer/extractHandlers/index.ts b/packages/data-fetcher/src/transfer/extractHandlers/index.ts
similarity index 100%
rename from packages/worker/src/transfer/extractHandlers/index.ts
rename to packages/data-fetcher/src/transfer/extractHandlers/index.ts
diff --git a/packages/worker/src/transfer/extractHandlers/mint/ethMintFromL1.handler.spec.ts b/packages/data-fetcher/src/transfer/extractHandlers/mint/ethMintFromL1.handler.spec.ts
similarity index 93%
rename from packages/worker/src/transfer/extractHandlers/mint/ethMintFromL1.handler.spec.ts
rename to packages/data-fetcher/src/transfer/extractHandlers/mint/ethMintFromL1.handler.spec.ts
index ff6441d3cd..315d7f22dc 100644
--- a/packages/worker/src/transfer/extractHandlers/mint/ethMintFromL1.handler.spec.ts
+++ b/packages/data-fetcher/src/transfer/extractHandlers/mint/ethMintFromL1.handler.spec.ts
@@ -1,9 +1,10 @@
import { BigNumber } from "ethers";
-import { types, utils } from "zksync-web3";
+import { types } from "zksync-web3";
import { mock } from "jest-mock-extended";
-import { TransferType } from "../../../entities/transfer.entity";
-import { TokenType } from "../../../entities/token.entity";
+import { TransferType } from "../../transfer.service";
+import { TokenType } from "../../../token/token.service";
import { ethMintFromL1Handler } from "./ethMintFromL1.handler";
+import { BASE_TOKEN_ADDRESS } from "../../../constants";
describe("ethMintFromL1Handler", () => {
let log: types.Log;
@@ -63,7 +64,7 @@ describe("ethMintFromL1Handler", () => {
it("extracts transfer with tokenType as ETH", () => {
const result = ethMintFromL1Handler.extract(log, blockDetails);
- expect(result.tokenType).toBe(TokenType.ETH);
+ expect(result.tokenType).toBe(TokenType.BaseToken);
});
it("extracts transfer with populated amount", () => {
@@ -73,7 +74,7 @@ describe("ethMintFromL1Handler", () => {
it("extracts transfer with L2_ETH_TOKEN_ADDRESS as tokenAddress", () => {
const result = ethMintFromL1Handler.extract(log, blockDetails);
- expect(result.tokenAddress).toBe(utils.L2_ETH_TOKEN_ADDRESS);
+ expect(result.tokenAddress).toBe(BASE_TOKEN_ADDRESS);
});
it("extracts transfer of deposit type", () => {
diff --git a/packages/worker/src/transfer/extractHandlers/mint/ethMintFromL1.handler.ts b/packages/data-fetcher/src/transfer/extractHandlers/mint/ethMintFromL1.handler.ts
similarity index 75%
rename from packages/worker/src/transfer/extractHandlers/mint/ethMintFromL1.handler.ts
rename to packages/data-fetcher/src/transfer/extractHandlers/mint/ethMintFromL1.handler.ts
index 7b0a0220d9..b8a004da93 100644
--- a/packages/worker/src/transfer/extractHandlers/mint/ethMintFromL1.handler.ts
+++ b/packages/data-fetcher/src/transfer/extractHandlers/mint/ethMintFromL1.handler.ts
@@ -1,14 +1,13 @@
-import { utils, types } from "zksync-web3";
+import { types } from "zksync-web3";
import { Transfer } from "../../interfaces/transfer.interface";
import { ExtractTransferHandler } from "../../interfaces/extractTransferHandler.interface";
-import { TransferType } from "../../../entities/transfer.entity";
-import { TokenType } from "../../../entities/token.entity";
+import { TransferType } from "../../transfer.service";
+import { TokenType } from "../../../token/token.service";
import { unixTimeToDate } from "../../../utils/date";
import parseLog from "../../../utils/parseLog";
-import { CONTRACT_INTERFACES } from "../../../constants";
-
+import { BASE_TOKEN_ADDRESS, CONTRACT_INTERFACES } from "../../../constants";
export const ethMintFromL1Handler: ExtractTransferHandler = {
- matches: (log: types.Log): boolean => log.address.toLowerCase() === utils.L2_ETH_TOKEN_ADDRESS,
+ matches: (log: types.Log): boolean => log.address.toLowerCase() === BASE_TOKEN_ADDRESS,
extract: (
log: types.Log,
blockDetails: types.BlockDetails,
@@ -22,9 +21,9 @@ export const ethMintFromL1Handler: ExtractTransferHandler = {
transactionHash: log.transactionHash,
blockNumber: log.blockNumber,
amount: parsedLog.args.amount,
- tokenAddress: utils.L2_ETH_TOKEN_ADDRESS,
+ tokenAddress: BASE_TOKEN_ADDRESS,
type: TransferType.Deposit,
- tokenType: TokenType.ETH,
+ tokenType: TokenType.BaseToken,
isFeeOrRefund: false,
logIndex: log.logIndex,
transactionIndex: log.transactionIndex,
diff --git a/packages/worker/src/transfer/extractHandlers/transfer/contractDeployerTransfer.handler.spec.ts b/packages/data-fetcher/src/transfer/extractHandlers/transfer/contractDeployerTransfer.handler.spec.ts
similarity index 98%
rename from packages/worker/src/transfer/extractHandlers/transfer/contractDeployerTransfer.handler.spec.ts
rename to packages/data-fetcher/src/transfer/extractHandlers/transfer/contractDeployerTransfer.handler.spec.ts
index 57e64e8949..7ed0b4aea7 100644
--- a/packages/worker/src/transfer/extractHandlers/transfer/contractDeployerTransfer.handler.spec.ts
+++ b/packages/data-fetcher/src/transfer/extractHandlers/transfer/contractDeployerTransfer.handler.spec.ts
@@ -1,8 +1,8 @@
import { BigNumber } from "ethers";
import { types } from "zksync-web3";
import { mock } from "jest-mock-extended";
-import { TransferType } from "../../../entities/transfer.entity";
-import { TokenType } from "../../../entities/token.entity";
+import { TransferType } from "../../transfer.service";
+import { TokenType } from "../../../token/token.service";
import { contractDeployerTransferHandler } from "./contractDeployerTransfer.handler";
describe("contractDeployerTransferHandler", () => {
diff --git a/packages/worker/src/transfer/extractHandlers/transfer/contractDeployerTransfer.handler.ts b/packages/data-fetcher/src/transfer/extractHandlers/transfer/contractDeployerTransfer.handler.ts
similarity index 92%
rename from packages/worker/src/transfer/extractHandlers/transfer/contractDeployerTransfer.handler.ts
rename to packages/data-fetcher/src/transfer/extractHandlers/transfer/contractDeployerTransfer.handler.ts
index 76e0cbf12e..92c73dfb0d 100644
--- a/packages/worker/src/transfer/extractHandlers/transfer/contractDeployerTransfer.handler.ts
+++ b/packages/data-fetcher/src/transfer/extractHandlers/transfer/contractDeployerTransfer.handler.ts
@@ -2,8 +2,8 @@ import { utils, types } from "zksync-web3";
import { ExtractTransferHandler } from "../../interfaces/extractTransferHandler.interface";
import { Transfer } from "../../interfaces/transfer.interface";
import { ZERO_HASH_64 } from "../../../constants";
-import { TransferType } from "../../../entities/transfer.entity";
-import { TokenType } from "../../../entities/token.entity";
+import { TransferType } from "../../transfer.service";
+import { TokenType } from "../../../token/token.service";
import { unixTimeToDate } from "../../../utils/date";
import parseLog from "../../../utils/parseLog";
import { CONTRACT_INTERFACES } from "../../../constants";
diff --git a/packages/worker/src/transfer/extractHandlers/transfer/default.handler.spec.ts b/packages/data-fetcher/src/transfer/extractHandlers/transfer/default.handler.spec.ts
similarity index 95%
rename from packages/worker/src/transfer/extractHandlers/transfer/default.handler.spec.ts
rename to packages/data-fetcher/src/transfer/extractHandlers/transfer/default.handler.spec.ts
index 579e25ef74..536e89187b 100644
--- a/packages/worker/src/transfer/extractHandlers/transfer/default.handler.spec.ts
+++ b/packages/data-fetcher/src/transfer/extractHandlers/transfer/default.handler.spec.ts
@@ -1,10 +1,10 @@
import { BigNumber } from "ethers";
-import { types, utils } from "zksync-web3";
+import { types } from "zksync-web3";
import { mock } from "jest-mock-extended";
-import { TransferType } from "../../../entities/transfer.entity";
-import { TokenType } from "../../../entities/token.entity";
+import { TransferType } from "../../transfer.service";
+import { TokenType } from "../../../token/token.service";
import { defaultTransferHandler } from "./default.handler";
-
+import { BASE_TOKEN_ADDRESS } from "../../../../src/constants";
describe("defaultTransferHandler", () => {
let log: types.Log;
let blockDetails: types.BlockDetails;
@@ -99,10 +99,10 @@ describe("defaultTransferHandler", () => {
});
it("extracts transfer with 0x000000000000000000000000000000000000800a as a tokenAddress if log address is 0x000000000000000000000000000000000000800a", () => {
- log.address = utils.L2_ETH_TOKEN_ADDRESS;
+ log.address = BASE_TOKEN_ADDRESS;
const result = defaultTransferHandler.extract(log, blockDetails);
- expect(result.tokenAddress).toBe(utils.L2_ETH_TOKEN_ADDRESS);
- expect(result.tokenType).toBe(TokenType.ETH);
+ expect(result.tokenAddress).toBe(BASE_TOKEN_ADDRESS);
+ expect(result.tokenType).toBe(TokenType.BaseToken);
});
it("extracts transfer with tokenAddress field populated with lower cased log address", () => {
diff --git a/packages/worker/src/transfer/extractHandlers/transfer/default.handler.ts b/packages/data-fetcher/src/transfer/extractHandlers/transfer/default.handler.ts
similarity index 87%
rename from packages/worker/src/transfer/extractHandlers/transfer/default.handler.ts
rename to packages/data-fetcher/src/transfer/extractHandlers/transfer/default.handler.ts
index 1762bc2df8..2fe7911330 100644
--- a/packages/worker/src/transfer/extractHandlers/transfer/default.handler.ts
+++ b/packages/data-fetcher/src/transfer/extractHandlers/transfer/default.handler.ts
@@ -1,10 +1,11 @@
import { utils, types } from "zksync-web3";
import { Transfer } from "../../interfaces/transfer.interface";
import { ExtractTransferHandler } from "../../interfaces/extractTransferHandler.interface";
-import { TransferType } from "../../../entities/transfer.entity";
-import { TokenType } from "../../../entities/token.entity";
+import { TransferType } from "../../transfer.service";
+import { TokenType } from "../../../token/token.service";
import { unixTimeToDate } from "../../../utils/date";
import parseLog from "../../../utils/parseLog";
+import { isBaseToken } from "../../../utils/token";
import { CONTRACT_INTERFACES } from "../../../constants";
export const defaultTransferHandler: ExtractTransferHandler = {
@@ -36,7 +37,7 @@ export const defaultTransferHandler: ExtractTransferHandler = {
amount: parsedLog.args.value,
tokenAddress,
type: transferType,
- tokenType: tokenAddress === utils.L2_ETH_TOKEN_ADDRESS ? TokenType.ETH : TokenType.ERC20,
+ tokenType: isBaseToken(tokenAddress) ? TokenType.BaseToken : TokenType.ERC20,
isFeeOrRefund: [TransferType.Fee, TransferType.Refund].includes(transferType),
logIndex: log.logIndex,
transactionIndex: log.transactionIndex,
diff --git a/packages/worker/src/transfer/extractHandlers/transfer/erc721Transfer.handle.spec.ts b/packages/data-fetcher/src/transfer/extractHandlers/transfer/erc721Transfer.handle.spec.ts
similarity index 97%
rename from packages/worker/src/transfer/extractHandlers/transfer/erc721Transfer.handle.spec.ts
rename to packages/data-fetcher/src/transfer/extractHandlers/transfer/erc721Transfer.handle.spec.ts
index 38b74f0a14..7c9e1b4c25 100644
--- a/packages/worker/src/transfer/extractHandlers/transfer/erc721Transfer.handle.spec.ts
+++ b/packages/data-fetcher/src/transfer/extractHandlers/transfer/erc721Transfer.handle.spec.ts
@@ -2,8 +2,8 @@ import { BigNumber } from "ethers";
import { types } from "zksync-web3";
import { mock } from "jest-mock-extended";
import { ZERO_HASH_64 } from "../../../constants";
-import { TransferType } from "../../../entities/transfer.entity";
-import { TokenType } from "../../../entities/token.entity";
+import { TransferType } from "../../transfer.service";
+import { TokenType } from "../../../token/token.service";
import { erc721TransferHandler } from "./erc721Transfer.handler";
describe("erc721TransferHandler", () => {
diff --git a/packages/worker/src/transfer/extractHandlers/transfer/erc721Transfer.handler.ts b/packages/data-fetcher/src/transfer/extractHandlers/transfer/erc721Transfer.handler.ts
similarity index 92%
rename from packages/worker/src/transfer/extractHandlers/transfer/erc721Transfer.handler.ts
rename to packages/data-fetcher/src/transfer/extractHandlers/transfer/erc721Transfer.handler.ts
index e7245281c2..d8b824e2dd 100644
--- a/packages/worker/src/transfer/extractHandlers/transfer/erc721Transfer.handler.ts
+++ b/packages/data-fetcher/src/transfer/extractHandlers/transfer/erc721Transfer.handler.ts
@@ -1,8 +1,8 @@
import { utils, types } from "zksync-web3";
import { Transfer } from "../../interfaces/transfer.interface";
import { ExtractTransferHandler } from "../../interfaces/extractTransferHandler.interface";
-import { TransferType } from "../../../entities/transfer.entity";
-import { TokenType } from "../../../entities/token.entity";
+import { TransferType } from "../../transfer.service";
+import { TokenType } from "../../../token/token.service";
import { unixTimeToDate } from "../../../utils/date";
import parseLog from "../../../utils/parseLog";
import { CONTRACT_INTERFACES } from "../../../constants";
diff --git a/packages/worker/src/transfer/extractHandlers/withdrawal/ethWithdrawalToL1.handler.spec.ts b/packages/data-fetcher/src/transfer/extractHandlers/withdrawal/ethWithdrawalToL1.handler.spec.ts
similarity index 93%
rename from packages/worker/src/transfer/extractHandlers/withdrawal/ethWithdrawalToL1.handler.spec.ts
rename to packages/data-fetcher/src/transfer/extractHandlers/withdrawal/ethWithdrawalToL1.handler.spec.ts
index a279e45014..3cd7d65150 100644
--- a/packages/worker/src/transfer/extractHandlers/withdrawal/ethWithdrawalToL1.handler.spec.ts
+++ b/packages/data-fetcher/src/transfer/extractHandlers/withdrawal/ethWithdrawalToL1.handler.spec.ts
@@ -1,9 +1,10 @@
import { BigNumber } from "ethers";
-import { types, utils } from "zksync-web3";
+import { types } from "zksync-web3";
import { mock } from "jest-mock-extended";
-import { TransferType } from "../../../entities/transfer.entity";
-import { TokenType } from "../../../entities/token.entity";
+import { TransferType } from "../../transfer.service";
+import { TokenType } from "../../../token/token.service";
import { ethWithdrawalToL1Handler } from "./ethWithdrawalToL1.handler";
+import { BASE_TOKEN_ADDRESS } from "../../../constants";
describe("ethWithdrawalToL1Handler", () => {
let log: types.Log;
@@ -69,7 +70,7 @@ describe("ethWithdrawalToL1Handler", () => {
it("extracts transfer with L2_ETH_TOKEN_ADDRESS as tokenAddress", () => {
const result = ethWithdrawalToL1Handler.extract(log, blockDetails);
- expect(result.tokenAddress).toBe(utils.L2_ETH_TOKEN_ADDRESS);
+ expect(result.tokenAddress).toBe(BASE_TOKEN_ADDRESS);
});
it("extracts transfer of deposit type", () => {
@@ -79,7 +80,7 @@ describe("ethWithdrawalToL1Handler", () => {
it("extracts transfer of ETH token type", () => {
const result = ethWithdrawalToL1Handler.extract(log, blockDetails);
- expect(result.tokenType).toBe(TokenType.ETH);
+ expect(result.tokenType).toBe(TokenType.BaseToken);
});
it("adds isFeeOrRefund as false", () => {
diff --git a/packages/worker/src/transfer/extractHandlers/withdrawal/ethWithdrawalToL1.handler.ts b/packages/data-fetcher/src/transfer/extractHandlers/withdrawal/ethWithdrawalToL1.handler.ts
similarity index 75%
rename from packages/worker/src/transfer/extractHandlers/withdrawal/ethWithdrawalToL1.handler.ts
rename to packages/data-fetcher/src/transfer/extractHandlers/withdrawal/ethWithdrawalToL1.handler.ts
index c561eb88db..d7d97fa968 100644
--- a/packages/worker/src/transfer/extractHandlers/withdrawal/ethWithdrawalToL1.handler.ts
+++ b/packages/data-fetcher/src/transfer/extractHandlers/withdrawal/ethWithdrawalToL1.handler.ts
@@ -1,14 +1,14 @@
-import { utils, types } from "zksync-web3";
+import { types } from "zksync-web3";
import { Transfer } from "../../interfaces/transfer.interface";
import { ExtractTransferHandler } from "../../interfaces/extractTransferHandler.interface";
-import { TransferType } from "../../../entities/transfer.entity";
-import { TokenType } from "../../../entities/token.entity";
+import { TransferType } from "../../transfer.service";
+import { TokenType } from "../../../token/token.service";
import { unixTimeToDate } from "../../../utils/date";
import parseLog from "../../../utils/parseLog";
-import { CONTRACT_INTERFACES } from "../../../constants";
+import { BASE_TOKEN_ADDRESS, CONTRACT_INTERFACES } from "../../../constants";
export const ethWithdrawalToL1Handler: ExtractTransferHandler = {
- matches: (log: types.Log): boolean => log.address.toLowerCase() === utils.L2_ETH_TOKEN_ADDRESS,
+ matches: (log: types.Log): boolean => log.address.toLowerCase() === BASE_TOKEN_ADDRESS,
extract: (
log: types.Log,
blockDetails: types.BlockDetails,
@@ -21,9 +21,9 @@ export const ethWithdrawalToL1Handler: ExtractTransferHandler = {
transactionHash: log.transactionHash,
blockNumber: log.blockNumber,
amount: parsedLog.args._amount,
- tokenAddress: utils.L2_ETH_TOKEN_ADDRESS,
+ tokenAddress: BASE_TOKEN_ADDRESS,
type: TransferType.Withdrawal,
- tokenType: TokenType.ETH,
+ tokenType: TokenType.BaseToken,
isFeeOrRefund: false,
logIndex: log.logIndex,
transactionIndex: log.transactionIndex,
diff --git a/packages/worker/src/transfer/extractHandlers/withdrawalInitiated/default.handler.spec.ts b/packages/data-fetcher/src/transfer/extractHandlers/withdrawalInitiated/default.handler.spec.ts
similarity index 93%
rename from packages/worker/src/transfer/extractHandlers/withdrawalInitiated/default.handler.spec.ts
rename to packages/data-fetcher/src/transfer/extractHandlers/withdrawalInitiated/default.handler.spec.ts
index 76e6cec0e1..f350a9bc20 100644
--- a/packages/worker/src/transfer/extractHandlers/withdrawalInitiated/default.handler.spec.ts
+++ b/packages/data-fetcher/src/transfer/extractHandlers/withdrawalInitiated/default.handler.spec.ts
@@ -1,10 +1,11 @@
import { BigNumber } from "ethers";
-import { types, utils } from "zksync-web3";
+import { types } from "zksync-web3";
import { mock } from "jest-mock-extended";
import { ZERO_HASH_64 } from "../../../constants";
-import { TransferType } from "../../../entities/transfer.entity";
-import { TokenType } from "../../../entities/token.entity";
+import { TransferType } from "../../transfer.service";
+import { TokenType } from "../../../token/token.service";
import { defaultWithdrawalInitiatedHandler } from "./default.handler";
+import { BASE_TOKEN_ADDRESS } from "../../../../src/constants";
describe("defaultWithdrawalInitiatedHandler", () => {
let log: types.Log;
@@ -66,8 +67,8 @@ describe("defaultWithdrawalInitiatedHandler", () => {
it("extracts transfer with L2_ETH_TOKEN_ADDRESS as a tokenAddress if l2Token is 0x0000000000000000000000000000000000000000", () => {
log.topics[3] = ZERO_HASH_64;
const result = defaultWithdrawalInitiatedHandler.extract(log, blockDetails);
- expect(result.tokenAddress).toBe(utils.L2_ETH_TOKEN_ADDRESS);
- expect(result.tokenType).toBe(TokenType.ETH);
+ expect(result.tokenAddress).toBe(BASE_TOKEN_ADDRESS);
+ expect(result.tokenType).toBe(TokenType.BaseToken);
});
it("extracts transfer with tokenAddress field populated with lower cased l2Token", () => {
diff --git a/packages/worker/src/transfer/extractHandlers/withdrawalInitiated/default.handler.ts b/packages/data-fetcher/src/transfer/extractHandlers/withdrawalInitiated/default.handler.ts
similarity index 72%
rename from packages/worker/src/transfer/extractHandlers/withdrawalInitiated/default.handler.ts
rename to packages/data-fetcher/src/transfer/extractHandlers/withdrawalInitiated/default.handler.ts
index 287ccf10d4..1bfd806ee2 100644
--- a/packages/worker/src/transfer/extractHandlers/withdrawalInitiated/default.handler.ts
+++ b/packages/data-fetcher/src/transfer/extractHandlers/withdrawalInitiated/default.handler.ts
@@ -1,11 +1,12 @@
import { utils, types } from "zksync-web3";
import { Transfer } from "../../interfaces/transfer.interface";
import { ExtractTransferHandler } from "../../interfaces/extractTransferHandler.interface";
-import { TransferType } from "../../../entities/transfer.entity";
-import { TokenType } from "../../../entities/token.entity";
+import { TransferType } from "../../transfer.service";
+import { TokenType } from "../../../token/token.service";
import { unixTimeToDate } from "../../../utils/date";
import parseLog from "../../../utils/parseLog";
-import { CONTRACT_INTERFACES } from "../../../constants";
+import { isBaseToken } from "../../../utils/token";
+import { BASE_TOKEN_ADDRESS, CONTRACT_INTERFACES } from "../../../constants";
export const defaultWithdrawalInitiatedHandler: ExtractTransferHandler = {
matches: (): boolean => true,
@@ -17,7 +18,7 @@ export const defaultWithdrawalInitiatedHandler: ExtractTransferHandler = {
const parsedLog = parseLog(CONTRACT_INTERFACES.L2_BRIDGE, log);
const tokenAddress =
- parsedLog.args.l2Token === utils.ETH_ADDRESS ? utils.L2_ETH_TOKEN_ADDRESS : parsedLog.args.l2Token.toLowerCase();
+ parsedLog.args.l2Token === utils.ETH_ADDRESS ? BASE_TOKEN_ADDRESS : parsedLog.args.l2Token.toLowerCase();
return {
from: parsedLog.args.l2Sender.toLowerCase(),
@@ -27,7 +28,7 @@ export const defaultWithdrawalInitiatedHandler: ExtractTransferHandler = {
amount: parsedLog.args.amount,
tokenAddress,
type: TransferType.Withdrawal,
- tokenType: tokenAddress === utils.L2_ETH_TOKEN_ADDRESS ? TokenType.ETH : TokenType.ERC20,
+ tokenType: isBaseToken(tokenAddress) ? TokenType.BaseToken : TokenType.ERC20,
isFeeOrRefund: false,
logIndex: log.logIndex,
transactionIndex: log.transactionIndex,
diff --git a/packages/worker/src/transfer/interfaces/extractTransferHandler.interface.ts b/packages/data-fetcher/src/transfer/interfaces/extractTransferHandler.interface.ts
similarity index 77%
rename from packages/worker/src/transfer/interfaces/extractTransferHandler.interface.ts
rename to packages/data-fetcher/src/transfer/interfaces/extractTransferHandler.interface.ts
index c61793b85c..9450bab230 100644
--- a/packages/worker/src/transfer/interfaces/extractTransferHandler.interface.ts
+++ b/packages/data-fetcher/src/transfer/interfaces/extractTransferHandler.interface.ts
@@ -1,9 +1,8 @@
import { types } from "zksync-web3";
-import { BridgeAddresses } from "../../blockchain";
import { Transfer } from "./transfer.interface";
export interface ExtractTransferHandler {
- matches: (log: types.Log, txReceipt?: types.TransactionReceipt, bridgeAddresses?: BridgeAddresses) => boolean;
+ matches: (log: types.Log, txReceipt?: types.TransactionReceipt) => boolean;
extract: (
log: types.Log,
blockDetails: types.BlockDetails,
diff --git a/packages/worker/src/transfer/interfaces/transfer.interface.ts b/packages/data-fetcher/src/transfer/interfaces/transfer.interface.ts
similarity index 78%
rename from packages/worker/src/transfer/interfaces/transfer.interface.ts
rename to packages/data-fetcher/src/transfer/interfaces/transfer.interface.ts
index b8197f9f15..944166210f 100644
--- a/packages/worker/src/transfer/interfaces/transfer.interface.ts
+++ b/packages/data-fetcher/src/transfer/interfaces/transfer.interface.ts
@@ -1,6 +1,6 @@
import { BigNumber } from "ethers";
-import { TransferType } from "../../entities/transfer.entity";
-import { TokenType } from "../../entities/token.entity";
+import { TransferType } from "../transfer.service";
+import { TokenType } from "../../token/token.service";
export interface TransferFields {
tokenId?: BigNumber;
diff --git a/packages/worker/src/transfer/transfer.service.spec.ts b/packages/data-fetcher/src/transfer/transfer.service.spec.ts
similarity index 82%
rename from packages/worker/src/transfer/transfer.service.spec.ts
rename to packages/data-fetcher/src/transfer/transfer.service.spec.ts
index fe5bc10411..bc9f726b26 100644
--- a/packages/worker/src/transfer/transfer.service.spec.ts
+++ b/packages/data-fetcher/src/transfer/transfer.service.spec.ts
@@ -3,10 +3,8 @@ import { Logger } from "@nestjs/common";
import { mock } from "jest-mock-extended";
import { BigNumber } from "ethers";
import { types } from "zksync-web3";
-import { TransferRepository } from "../repositories";
import { TransferService } from "./transfer.service";
-import { BlockchainService } from "../blockchain/blockchain.service";
-import { TokenType } from "../entities/token.entity";
+import { TokenType } from "../token/token.service";
import * as ethDepositNoFee from "../../test/transactionReceipts/eth/deposit-no-fee.json";
import * as ethDepositZeroValue from "../../test/transactionReceipts/eth/deposit-zero-value.json";
@@ -65,29 +63,11 @@ jest.mock("../logger", () => ({
const toTxReceipt = (receipt: any): types.TransactionReceipt => receipt as types.TransactionReceipt;
describe("TransferService", () => {
- let transferRepositoryMock: TransferRepository;
let transferService: TransferService;
beforeEach(async () => {
- transferRepositoryMock = mock();
-
const app = await Test.createTestingModule({
- providers: [
- TransferService,
- {
- provide: BlockchainService,
- useValue: {
- bridgeAddresses: {
- l1Erc20DefaultBridge: "0xc0543dab6ac5d3e3ff2e5a5e39e15186d0306808",
- l2Erc20DefaultBridge: "0xc7e0220d02d549c4846a6ec31d89c3b670ebe35c",
- },
- },
- },
- {
- provide: TransferRepository,
- useValue: transferRepositoryMock,
- },
- ],
+ providers: [TransferService],
}).compile();
app.useLogger(mock());
@@ -95,7 +75,7 @@ describe("TransferService", () => {
transferService = app.get(TransferService);
});
- describe("saveTransfers", () => {
+ describe("getTransfers", () => {
const receivedAt = new Date();
const blockDetails = mock();
blockDetails.timestamp = new Date().getTime() / 1000;
@@ -104,16 +84,15 @@ describe("TransferService", () => {
});
transactionDetails.receivedAt = receivedAt;
- it("does not save transfers if no logs specified", async () => {
- await transferService.saveTransfers(null, null);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(0);
+ it("returns an empty array if no logs are specified", async () => {
+ const transfers = await transferService.getTransfers(null, null);
+ expect(transfers).toStrictEqual([]);
});
describe("eth", () => {
describe("deposit with no fee", () => {
const txReceipt = toTxReceipt(ethDepositNoFee);
-
- it("properly saves and returns transfers", async () => {
+ it("returns proper transfers", async () => {
const expectedTransfers = [
{
amount: BigNumber.from("0x6f05b59d3b20000"),
@@ -121,7 +100,7 @@ describe("TransferService", () => {
from: "0xd206eaf6819007535e893410cfa01885ce40e99a",
to: "0xd206eaf6819007535e893410cfa01885ce40e99a",
tokenAddress: "0x000000000000000000000000000000000000800a",
- tokenType: TokenType.ETH,
+ tokenType: TokenType.BaseToken,
transactionHash: "0x7cc7cc0326af164b15de04de3b153a7a55afb14a7897298a0a84f9507d483d1d",
type: "deposit",
isFeeOrRefund: false,
@@ -136,7 +115,7 @@ describe("TransferService", () => {
from: "0xd206eaf6819007535e893410cfa01885ce40e99a",
to: "0xd754ff5e8a6f257e162f72578a4bb0493c0681d8",
tokenAddress: "0x000000000000000000000000000000000000800a",
- tokenType: TokenType.ETH,
+ tokenType: TokenType.BaseToken,
transactionHash: "0x7cc7cc0326af164b15de04de3b153a7a55afb14a7897298a0a84f9507d483d1d",
type: "transfer",
isFeeOrRefund: false,
@@ -151,7 +130,7 @@ describe("TransferService", () => {
from: "0xd206eaf6819007535e893410cfa01885ce40e99a",
to: "0xd206eaf6819007535e893410cfa01885ce40e99a",
tokenAddress: "0x000000000000000000000000000000000000800a",
- tokenType: TokenType.ETH,
+ tokenType: TokenType.BaseToken,
transactionHash: "0x7cc7cc0326af164b15de04de3b153a7a55afb14a7897298a0a84f9507d483d1d",
type: "deposit",
isFeeOrRefund: false,
@@ -162,22 +141,20 @@ describe("TransferService", () => {
},
];
- const result = await transferService.saveTransfers(
+ const transfers = await transferService.getTransfers(
txReceipt.logs,
blockDetails,
transactionDetails,
txReceipt
);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
describe("deposit", () => {
const txReceipt = toTxReceipt(ethDeposit);
- it("saves deposit, transfer, fee and refund transfers", async () => {
+ it("returns deposit, transfer, fee and refund transfers", async () => {
const expectedTransfers = [
{
from: "0xfb7e0856e44eff812a44a9f47733d7d55c39aa28",
@@ -186,7 +163,7 @@ describe("TransferService", () => {
blockNumber: 7485644,
amount: BigNumber.from("0x2386f26fc10000"),
tokenAddress: "0x000000000000000000000000000000000000800a",
- tokenType: TokenType.ETH,
+ tokenType: TokenType.BaseToken,
type: "deposit",
isFeeOrRefund: false,
isInternal: false,
@@ -201,7 +178,7 @@ describe("TransferService", () => {
blockNumber: 7485644,
amount: BigNumber.from("0x2386f26fc10000"),
tokenAddress: "0x000000000000000000000000000000000000800a",
- tokenType: TokenType.ETH,
+ tokenType: TokenType.BaseToken,
type: "transfer",
isFeeOrRefund: false,
isInternal: false,
@@ -216,7 +193,7 @@ describe("TransferService", () => {
blockNumber: 7485644,
amount: BigNumber.from("0x0141b56ff62900"),
tokenAddress: "0x000000000000000000000000000000000000800a",
- tokenType: TokenType.ETH,
+ tokenType: TokenType.BaseToken,
type: "fee",
isFeeOrRefund: true,
isInternal: false,
@@ -231,7 +208,7 @@ describe("TransferService", () => {
blockNumber: 7485644,
amount: BigNumber.from("0x29eb1faec300"),
tokenAddress: "0x000000000000000000000000000000000000800a",
- tokenType: TokenType.ETH,
+ tokenType: TokenType.BaseToken,
type: "refund",
isFeeOrRefund: true,
isInternal: false,
@@ -241,22 +218,20 @@ describe("TransferService", () => {
},
];
- const result = await transferService.saveTransfers(
+ const transfers = await transferService.getTransfers(
txReceipt.logs,
blockDetails,
transactionDetails,
txReceipt
);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
describe("deposit to different address", () => {
const txReceipt = toTxReceipt(ethDepositToDifferentAddress);
- it("saves deposit, transfer, fee and refund transfers", async () => {
+ it("returns deposit, transfer, fee and refund transfers", async () => {
const expectedTransfers = [
{
from: "0xfb7e0856e44eff812a44a9f47733d7d55c39aa28",
@@ -265,7 +240,7 @@ describe("TransferService", () => {
blockNumber: 7483775,
amount: BigNumber.from("0x11c37937e08000"),
tokenAddress: "0x000000000000000000000000000000000000800a",
- tokenType: TokenType.ETH,
+ tokenType: TokenType.BaseToken,
type: "deposit",
isFeeOrRefund: false,
isInternal: false,
@@ -280,7 +255,7 @@ describe("TransferService", () => {
blockNumber: 7483775,
amount: BigNumber.from("0x11c37937e08000"),
tokenAddress: "0x000000000000000000000000000000000000800a",
- tokenType: TokenType.ETH,
+ tokenType: TokenType.BaseToken,
type: "transfer",
isFeeOrRefund: false,
isInternal: false,
@@ -295,7 +270,7 @@ describe("TransferService", () => {
blockNumber: 7483775,
amount: BigNumber.from("0x0150b5fa93bf00"),
tokenAddress: "0x000000000000000000000000000000000000800a",
- tokenType: TokenType.ETH,
+ tokenType: TokenType.BaseToken,
type: "fee",
isFeeOrRefund: true,
isInternal: false,
@@ -310,7 +285,7 @@ describe("TransferService", () => {
blockNumber: 7483775,
amount: BigNumber.from("0xdb01bc43a500"),
tokenAddress: "0x000000000000000000000000000000000000800a",
- tokenType: TokenType.ETH,
+ tokenType: TokenType.BaseToken,
type: "refund",
isFeeOrRefund: true,
isInternal: false,
@@ -320,22 +295,20 @@ describe("TransferService", () => {
},
];
- const result = await transferService.saveTransfers(
+ const transfers = await transferService.getTransfers(
txReceipt.logs,
blockDetails,
transactionDetails,
txReceipt
);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
describe("zero value deposit", () => {
const txReceipt = toTxReceipt(ethDepositZeroValue);
- it("saves fee and refund transfers", async () => {
+ it("returns fee and refund transfers", async () => {
const expectedTransfers = [
{
from: "0xfb7e0856e44eff812a44a9f47733d7d55c39aa28",
@@ -344,7 +317,7 @@ describe("TransferService", () => {
blockNumber: 7485219,
amount: BigNumber.from("0x010425b6917e00"),
tokenAddress: "0x000000000000000000000000000000000000800a",
- tokenType: TokenType.ETH,
+ tokenType: TokenType.BaseToken,
type: "fee",
isFeeOrRefund: true,
isInternal: false,
@@ -359,7 +332,7 @@ describe("TransferService", () => {
blockNumber: 7485219,
amount: BigNumber.from("0x7c948f3acf00"),
tokenAddress: "0x000000000000000000000000000000000000800a",
- tokenType: TokenType.ETH,
+ tokenType: TokenType.BaseToken,
type: "refund",
isFeeOrRefund: true,
isInternal: false,
@@ -369,22 +342,20 @@ describe("TransferService", () => {
},
];
- const result = await transferService.saveTransfers(
+ const transfers = await transferService.getTransfers(
txReceipt.logs,
blockDetails,
transactionDetails,
txReceipt
);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
describe("transfer", () => {
const txReceipt = toTxReceipt(ethTransfer);
- it("properly saves transfers", async () => {
+ it("returns proper transfers", async () => {
const expectedTransfers = [
{
amount: BigNumber.from("0x018034d06a6900"),
@@ -392,7 +363,7 @@ describe("TransferService", () => {
from: "0x481e48ce19781c3ca573967216dee75fdcf70f54",
to: "0x0000000000000000000000000000000000008001",
tokenAddress: "0x000000000000000000000000000000000000800a",
- tokenType: TokenType.ETH,
+ tokenType: TokenType.BaseToken,
transactionHash: "0xc697e19d80645ec37df566e1227edad4652d010e43c508bbd04efbaeb47e2c48",
type: "fee",
isFeeOrRefund: true,
@@ -407,7 +378,7 @@ describe("TransferService", () => {
from: "0x481e48ce19781c3ca573967216dee75fdcf70f54",
to: "0xc9593dc3dcad5f3804aaa5af12a9d74d0c00e4b0",
tokenAddress: "0x000000000000000000000000000000000000800a",
- tokenType: TokenType.ETH,
+ tokenType: TokenType.BaseToken,
transactionHash: "0xc697e19d80645ec37df566e1227edad4652d010e43c508bbd04efbaeb47e2c48",
type: "transfer",
isFeeOrRefund: false,
@@ -421,17 +392,15 @@ describe("TransferService", () => {
const txDetails = mock