Feat/30 refactor operational packages (#116)

* refactor Operational packages

* fix: refactor operations package to use oclif

* adjusting Dockerfile

* adjusting github action

* Update all-tools.yml

Signed-off-by: Andrei A. <andrei.alexandru@consensys.net>

* adjusting github action

---------

Signed-off-by: Andrei A. <andrei.alexandru@consensys.net>
Co-authored-by: VGau <victorien.gauch@consensys.net>
This commit is contained in:
Andrei A.
2024-10-02 15:55:59 +03:00
committed by GitHub
parent 3675b24f45
commit e3d2839fee
52 changed files with 3171 additions and 26037 deletions

View File

@@ -5,22 +5,21 @@ on:
branches:
- main
paths:
- 'operations/**'
- 'operations/'
- '.github/workflows/all-tools.yml'
pull_request:
branches:
- main
paths:
- 'operations/**'
env:
DOCKER_IMAGE_NAME: consensys/linea-alltools
- 'operations/'
- '.github/workflows/all-tools.yml'
jobs:
changes:
runs-on: besu-arm64
name: Filter commit changes
outputs:
all-tools: ${{ steps.filter.outputs.all-tools }}
all-tools: ${{ steps.filter.outputs['all-tools'] }}
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -43,7 +42,7 @@ jobs:
runs-on: besu-arm64
name: Check image tags exist
needs: [ changes, store_image_name_and_tags ]
if: ${{ needs.changes.outputs.all-tools == 'false' }}
if: ${{ needs.changes.outputs['all-tools'] == 'false' }}
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -52,7 +51,7 @@ jobs:
with:
last_commit_tag: ${{ needs.store_image_name_and_tags.outputs.last_commit_tag }}
common_ancestor_tag: ${{ needs.store_image_name_and_tags.outputs.common_ancestor_tag }}
image_name: ${{ env.DOCKER_IMAGE_NAME }}
image_name: consensys/linea-alltools
docker_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -60,7 +59,7 @@ jobs:
runs-on: besu-arm64
name: All tools tag only
needs: [ changes, store_image_name_and_tags, check_image_tags_exist ]
if: ${{ github.event_name != 'pull_request' && needs.changes.outputs.all-tools == 'false' }}
if: ${{ github.event_name != 'pull_request' && needs.changes.outputs['all-tools'] == 'false' }}
outputs:
image_tagged: ${{ steps.image_tag_push.outputs.image_tagged }}
steps:
@@ -75,7 +74,7 @@ jobs:
common_ancestor_tag: ${{ needs.store_image_name_and_tags.outputs.common_ancestor_tag }}
develop_tag: ${{ needs.store_image_name_and_tags.outputs.develop_tag }}
untested_tag_suffix: ${{ needs.store_image_name_and_tags.outputs.untested_tag_suffix }}
image_name: ${{ env.DOCKER_IMAGE_NAME }}
image_name: consensys/linea-alltools
last_commit_tag_exists: ${{ needs.check_image_tags_exist.outputs.last_commit_tag_exists }}
common_ancestor_commit_tag_exists: ${{ needs.check_image_tags_exist.outputs.common_ancestor_commit_tag_exists }}
docker_username: ${{ secrets.DOCKERHUB_USERNAME }}
@@ -83,12 +82,12 @@ jobs:
build-and-publish:
needs: [ changes, store_image_name_and_tags, all-tools-tag-only ]
if: ${{ always() && (needs.changes.outputs.all-tools == 'true' || needs.all-tools-tag-only.result != 'success' || needs.all-tools-tag-only.outputs.image_tagged != 'true') }}
if: ${{ always() && (needs.changes.outputs['all-tools'] == 'true' || needs.all-tools-tag-only.result != 'success' || needs.all-tools-tag-only.outputs.image_tagged != 'true') }}
runs-on: ubuntu-22.04
env:
COMMIT_TAG: ${{ needs.store_image_name_and_tags.outputs.commit_tag }}
DEVELOP_TAG: ${{ needs.store_image_name_and_tags.outputs.develop_tag }}
IMAGE_NAME: ${{ env.DOCKER_IMAGE_NAME }}
IMAGE_NAME: consensys/linea-alltools
name: All tools build and push
steps:
- name: Checkout
@@ -116,15 +115,13 @@ jobs:
- name: Show the "version" build argument
run: |
echo "We inject the commit tag in the docker image ${{ env.COMMIT_TAG }}"
echo COMMIT_TAG=${{ env.COMMIT_TAG }} >> $GITHUB_ENV
echo COMMIT_TAG=${{ env.COMMIT_TAG }} >> GITHUB_ENV
- name: Build and push all tools image
uses: docker/build-push-action@v6
with:
context: .
file: ./operations/Dockerfile
platforms: linux/amd64
# Note: Build amd64 image only
# platforms: linux/amd64,linux/arm64
platforms: linux/amd64 # Note: Build amd64 image only
push: ${{ github.event_name != 'pull_request' }}
tags: |
${{ env.IMAGE_NAME }}:${{ env.COMMIT_TAG }}

2
.gitignore vendored
View File

@@ -24,7 +24,7 @@
.run/**.run.xml
.envrc
bin/
!operations/src/synctx/bin/
!operations/bin/
target/
tmp/
build/

View File

@@ -1,3 +1,3 @@
dist
node_modules
src/synctx/
coverage

View File

@@ -1,3 +1,3 @@
dist
node_modules
src/synctx/
coverage

View File

@@ -1,59 +1,31 @@
# syntax=docker/dockerfile:1.2
FROM node:18-slim AS builder
FROM node:20-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
FROM base AS builder
WORKDIR /usr/src/app
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.json ./
COPY ./operations/package.json ./operations/package.json
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile --prefer-offline && npm install -g typescript
COPY operations/ ./operations/
COPY . .
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile --prefer-offline --ignore-scripts
RUN pnpm run -F operations build
RUN pnpm deploy --filter=./operations --prod ./prod
RUN rm -rf src/synctx && pnpm run -F operations build
FROM node:18-slim AS builder-synctx
ARG TARGETPLATFORM
WORKDIR /opt/synctx
COPY ./operations/src/synctx .
RUN apt-get update \
&& apt-get install --no-install-recommends -y git xz-utils perl \
&& OCLIF_TARGET=$(echo ${TARGETPLATFORM} | sed 's#/#-#;s#amd64#x64#') \
&& yarn global add oclif && yarn && yarn run build && yarn install --production --ignore-scripts --prefer-offline \
&& git init \
&& git config user.email "sre@consensys.net" \
&& git config user.name "cs-sre" \
&& git commit --allow-empty -m "dummy commit" \
&& oclif pack tarballs --targets="${OCLIF_TARGET}" \
&& tar -xvf dist/synctx-*.tar.gz
FROM node:18-slim AS release
FROM node:20-slim AS release
ENV NODE_ENV=production
ENV PATH="${PATH}:/opt/synctx/bin"
WORKDIR /usr/src/app
# Install pnpm
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
USER node
COPY package.json pnpm-lock.yaml ./
COPY --from=builder /usr/src/app/prod ./
RUN pnpm install --prod --frozen-lockfile --prefer-offline
COPY --chown=node:node --from=builder /usr/src/app/operations/dist ./dist
COPY --chown=node:node --from=builder-synctx /opt/synctx/synctx /opt/synctx/
USER node:node
ENTRYPOINT ["node"]
ENTRYPOINT ["./bin/run.js"]

View File

@@ -1,49 +0,0 @@
FROM node:18-alpine AS builder
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install -g typescript && npm ci
COPY . .
RUN rm -rf src/synctx && npm run build
FROM node:18-alpine as builder-synctx
ARG TARGETPLATFORM
WORKDIR /opt/synctx
COPY src/synctx .
RUN apk add --no-cache git perl-utils xz \
&& OCLIF_TARGET=$(echo ${TARGETPLATFORM} | sed 's#/#-#;s#amd64#x64#') \
&& yarn global add oclif && yarn && yarn run build \
&& yarn install --production --ignore-scripts --prefer-offline \
&& git init \
&& git config user.email "sre@consensys.net" \
&& git config user.name "cs-sre" \
&& git commit --allow-empty -m "dummy commit" \
&& oclif pack tarballs --targets="${OCLIF_TARGET}" \
&& tar -xvf dist/synctx-*.tar.gz
FROM node:18-alpine as release
ENV NODE_ENV production
ENV PATH="${PATH}:/opt/synctx/bin"
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci && npm cache clean --force \
&& apk add --no-cache curl jq bash gcompat glibc
COPY --chown=node:node --from=builder /usr/src/app/dist ./dist
COPY --chown=node:node --from=builder-synctx /opt/synctx/synctx /opt/synctx/
USER node:node
ENTRYPOINT ["node"]

6
operations/bin/dev.js Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env -S node --loader ts-node/esm --disable-warning=ExperimentalWarning
// eslint-disable-next-line n/shebang
import {execute} from '@oclif/core'
await execute({development: true, dir: import.meta.url})

5
operations/bin/run.js Executable file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env node
import {execute} from '@oclif/core'
await execute({dir: import.meta.url})

View File

@@ -0,0 +1,18 @@
module.exports = {
preset: "ts-jest/presets/default-esm",
testEnvironment: "node",
testRegex: "test.ts$",
transform: {
"^.+\\.ts$": ["ts-jest", { useESM: true }],
},
verbose: true,
collectCoverage: true,
moduleNameMapper: {
"^(\\.{1,2}/.*)\\.js$": "$1",
},
extensionsToTreatAsEsm: [".ts"],
collectCoverageFrom: ["src/**/*.ts"],
coverageReporters: ["html", "lcov", "text"],
testPathIgnorePatterns: [],
coveragePathIgnorePatterns: [],
};

View File

@@ -1,12 +0,0 @@
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
rootDir: ".",
testRegex: "test.ts$",
verbose: true,
collectCoverage: true,
collectCoverageFrom: ["src/**/*.ts"],
coverageReporters: ["html", "lcov", "text"],
testPathIgnorePatterns: ["src/common/index.ts", "src/ethTransfer/index.ts", "src/synctx/src/index.ts"],
coveragePathIgnorePatterns: ["src/common/index.ts", "src/ethTransfer/index.ts", "src/synctx/src/index.ts"],
};

View File

@@ -5,27 +5,56 @@
"author": "Consensys Software Inc.",
"license": "Apache-2.0",
"scripts": {
"build": "tsc -p tsconfig.build.json",
"build": "shx rm -rf dist && tsc -p tsconfig.build.json",
"prettier": "prettier -c '**/*.{js,ts}'",
"prettier:fix": "prettier -w '**/*.{js,ts}'",
"lint:ts": "npx eslint '**/*.{js,ts}'",
"lint:ts:fix": "npx eslint --fix '**/*.{js,ts}'",
"test": "npx jest --bail --detectOpenHandles --forceExit",
"lint:fix": "npm run lint:ts:fix && npm run prettier:fix",
"clean": "rimraf node_modules"
"lint": "eslint . --ext .ts",
"lint:fix": "eslint . --ext .ts --fix",
"test": "node --experimental-vm-modules node_modules/jest/bin/jest --bail --detectOpenHandles --forceExit",
"clean": "rimraf node_modules dist coverage",
"postpack": "shx rm -f oclif.manifest.json",
"posttest": "pnpm run lint",
"prepack": "oclif manifest && oclif readme",
"version": "oclif readme && git add README.md"
},
"dependencies": {
"axios": "^1.6.0",
"ethers": "^6.8.1",
"yargs": "^17.7.2"
"@oclif/core": "4.0.23",
"@oclif/plugin-help": "6.2.13",
"@oclif/plugin-plugins": "5.4.10",
"axios": "1.7.7",
"ethers": "6.13.2"
},
"devDependencies": {
"@jest/globals": "^29.7.0",
"@types/jest": "^29.5.7",
"@types/yargs": "^17.0.29",
"jest": "^29.7.0",
"jest-mock-extended": "^3.0.5",
"ts-jest": "^29.1.1",
"rimraf": "^3.0.2"
}
}
"@jest/globals": "29.7.0",
"@oclif/test": "4.0.9",
"@types/jest": "29.5.13",
"jest": "29.7.0",
"jest-mock-extended": "3.0.5",
"shx": "0.3.4",
"ts-jest": "29.2.5"
},
"engines": {
"node": ">=20"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"type": "module",
"bin": {
"operations": "./bin/run.js"
},
"oclif": {
"bin": "operations",
"dirname": "operations",
"commands": "./dist/commands",
"plugins": [
"@oclif/plugin-help",
"@oclif/plugin-plugins"
],
"topicSeparator": " "
},
"files": [
"/bin",
"/dist",
"/oclif.manifest.json"
]
}

View File

@@ -0,0 +1,191 @@
import { ethers, TransactionLike } from "ethers";
import { Command, Flags } from "@oclif/core";
import { validateEthereumAddress, validateHexString, validateUrl, get1559Fees } from "../utils/common/index.js";
import {
validateETHThreshold,
calculateRewards,
validateConfig,
estimateTransactionGas,
executeTransaction,
getWeb3SignerSignature,
DEFAULT_GAS_ESTIMATION_PERCENTILE,
DEFAULT_MAX_FEE_PER_GAS,
WEB3_SIGNER_PUBLIC_KEY_LENGTH,
} from "../utils/eth-transfer/index.js";
export default class EthTransfer extends Command {
static examples = [
// Example 1: Basic usage with required flags
`<%= config.bin %> <%= command.id %>
--sender-address=0xYourSenderAddress
--destination-address=0xYourDestinationAddress
--threshold=10
--blockchain-rpc-url=https://mainnet.infura.io/v3/YOUR-PROJECT-ID
--web3-signer-url=http://localhost:8545
--web3-signer-public-key=0xYourWeb3SignerPublicKey`,
// Example 2: Including optional flags with custom values
`<%= config.bin %> <%= command.id %>
--sender-address=0xYourSenderAddress
--destination-address=0xYourDestinationAddress
--threshold=5
--blockchain-rpc-url=https://mainnet.infura.io/v3/YOUR-PROJECT-ID
--web3-signer-url=http://localhost:8545
--web3-signer-public-key=0xYourWeb3SignerPublicKey
--max-fee-per-gas=150000000000
--gas-estimation-percentile=20`,
// Example 3: Using the dry-run flag to simulate the transaction
`<%= config.bin %> <%= command.id %>
--sender-address=0xYourSenderAddress
--destination-address=0xYourDestinationAddress
--threshold=1.5
--blockchain-rpc-url=http://127.0.0.1:8545
--web3-signer-url=http://127.0.0.1:8546
--web3-signer-public-key=0xYourWeb3SignerPublicKey
--dry-run`,
];
static flags = {
"sender-address": Flags.string({
char: "s",
description: "Sender address",
required: true,
parse: async (input) => validateEthereumAddress("sender-address", input),
env: "ETH_TRANSFER_SENDER_ADDRESS",
}),
"destination-address": Flags.string({
char: "d",
description: "Destination address",
required: true,
parse: async (input) => validateEthereumAddress("destination-address", input),
env: "ETH_TRANSFER_DESTINATION_ADDRESS",
}),
threshold: Flags.string({
char: "t",
description: "Balance threshold of Validator address",
required: true,
parse: async (input) => validateETHThreshold(input),
env: "ETH_TRANSFER_THRESHOLD",
}),
"blockchain-rpc-url": Flags.string({
description: "Blockchain RPC URL",
required: true,
parse: async (input) => validateUrl("blockchain-rpc-url", input, ["http:", "https:"]),
env: "ETH_TRANSFER_BLOCKCHAIN_RPC_URL",
}),
"web3-signer-url": Flags.string({
description: "Web3 Signer URL",
required: true,
parse: async (input) => validateUrl("web3-signer-url", input, ["http:", "https:"]),
env: "ETH_TRANSFER_WEB3_SIGNER_URL",
}),
"web3-signer-public-key": Flags.string({
description: "Web3 Signer Public Key",
required: true,
parse: async (input) => validateHexString("web3-signer-public-key", input, WEB3_SIGNER_PUBLIC_KEY_LENGTH),
env: "ETH_TRANSFER_WEB3_SIGNER_PUBLIC_KEY",
}),
"dry-run": Flags.boolean({
description: "Dry run flag",
required: false,
default: false,
env: "ETH_TRANSFER_DRY_RUN",
}),
"max-fee-per-gas": Flags.string({
description: "MaxFeePerGas in wei",
required: false,
default: DEFAULT_MAX_FEE_PER_GAS,
env: "ETH_TRANSFER_MAX_FEE_PER_GAS",
}),
"gas-estimation-percentile": Flags.integer({
description: "Gas estimation percentile (0-100)",
required: false,
default: DEFAULT_GAS_ESTIMATION_PERCENTILE,
env: "ETH_TRANSFER_GAS_ESTIMATION_PERCENTILE",
}),
};
public async run(): Promise<void> {
const { flags } = await this.parse(EthTransfer);
const {
senderAddress,
destinationAddress,
threshold,
blockchainRpcUrl,
web3SignerUrl,
web3SignerPublicKey,
maxFeePerGas,
gasEstimationPercentile,
dryRun,
} = validateConfig(flags);
const provider = new ethers.JsonRpcProvider(blockchainRpcUrl);
const [{ chainId }, senderBalance, fees, nonce] = await Promise.all([
provider.getNetwork(),
provider.getBalance(senderAddress),
get1559Fees(provider, maxFeePerGas, gasEstimationPercentile),
provider.getTransactionCount(senderAddress),
]);
if (senderBalance <= ethers.parseEther(threshold)) {
this.log(`Sender balance (${ethers.formatEther(senderBalance)} ETH) is less than threshold. No action needed.`);
return;
}
const rewards = calculateRewards(senderBalance);
if (rewards === 0n) {
this.log(`No rewards to send.`);
return;
}
const transactionRequest: TransactionLike = {
to: destinationAddress,
value: rewards,
type: 2,
chainId,
maxFeePerGas: fees.maxFeePerGas,
maxPriorityFeePerGas: fees.maxPriorityFeePerGas,
nonce: nonce,
};
const transactionGasLimit = await estimateTransactionGas(provider, {
...transactionRequest,
from: senderAddress,
} as ethers.TransactionRequest);
const transaction: TransactionLike = {
...transactionRequest,
gasLimit: transactionGasLimit,
};
const signature = await getWeb3SignerSignature(web3SignerUrl, web3SignerPublicKey, transaction);
if (dryRun) {
this.log("Dry run enabled: Skipping transaction submission to blockchain.");
this.log(`Here is the expected rewards: ${ethers.formatEther(rewards)} ETH`);
return;
}
const receipt = await executeTransaction(provider, {
...transaction,
signature,
});
if (!receipt) {
throw new Error(`Transaction receipt not found for this transaction ${JSON.stringify(transaction)}`);
}
if (receipt.status === 0) {
throw new Error(`Transaction reverted. Receipt: ${JSON.stringify(receipt)}`);
}
this.log(
`Transaction succeeded. Rewards sent: ${ethers.formatEther(rewards)} ETH. Receipt: ${JSON.stringify(receipt)}`,
);
this.log(`Rewards sent: ${ethers.formatEther(rewards)} ETH`);
}
}

View File

@@ -1,68 +1,23 @@
import { Command } from "@oclif/core";
import { BigNumber, UnsignedTransaction, ethers } from "ethers";
import { Flags } from "@oclif/core";
import { Command, Flags } from "@oclif/core";
import { ethers } from "ethers";
import { readFileSync } from "fs";
import {
getPendingTransactions,
isLocalPort,
isValidNodeTarget,
parsePendingTransactions,
Transaction,
Txpool,
ClientApi,
getClientType,
} from "../utils/synctx/index.js";
type Txpool = {
pending: object;
queued: object;
};
//TODO: add pagination for besu?
const clientApi: { [key: string]: { api: string; params: Array<any> } } = {
const clientApi: ClientApi = {
geth: { api: "txpool_content", params: [] },
besu: { api: "txpool_besuPendingTransactions", params: [2000] },
};
const isValidNodeTarget = (sourceNode: string, targetNode: string) => {
try {
/* eslint-disable no-new */
if (sourceNode) {
new URL(sourceNode);
}
new URL(targetNode);
return true;
} catch {}
return false;
};
const isLocalPort = (value: string) => {
try {
const port = Number(value);
return port >= 1 && port <= 65535;
} catch {}
return false;
};
const parsePendingTransactions = (pool: Txpool) => {
return (Object.values(pool.pending).map((el: any) => Object.values(el)) as unknown as any[]).flat();
};
const getPendingTransactions = (sourcePool: any, targetPool: any) => {
// calculate diff between source and target
const targetPendingTransactions = new Set();
targetPool.forEach((tx: any) => {
targetPendingTransactions.add(tx.hash);
});
return sourcePool.filter((tx: any) => !targetPendingTransactions.has(tx.hash));
};
const getClientType = async (nodeProvider: ethers.providers.JsonRpcProvider) => {
// Fetch client type of Besu or Geth
const res: string = await nodeProvider.send("web3_clientVersion", []);
const clientType = res.slice(0, 4).toLowerCase();
if (!["geth", "besu"].includes(clientType)) {
throw new Error(`Invalid node client type, must be either geth or besu`);
}
return clientType;
};
export default class Sync extends Command {
export default class Synctx extends Command {
static examples = [
"<%= config.bin %> <%= command.id %> --source=8500 --target=8501 --local",
"<%= config.bin %> <%= command.id %> --source=http://geth-archive-1:8545 --target=http://geth-validator-1:8545 --concurrency=10",
@@ -74,7 +29,7 @@ export default class Sync extends Command {
description: "source node to sync from, mutually exclusive with file flag",
helpGroup: "Node",
multiple: false,
required: true,
required: false,
default: "",
env: "SYNCTX_SOURCE",
}),
@@ -114,23 +69,24 @@ export default class Sync extends Command {
description: "local txs file to read from, mutually exclusive with source flag",
helpGroup: "Config",
multiple: false,
requiredOrDefaulted: true,
required: false,
default: "",
env: "SYNCTX_FILE",
}),
};
public async run(): Promise<void> {
const { flags } = await this.parse(Sync);
let sourceNode: string = flags.source;
public async run(): Promise<void> {
const { flags } = await this.parse(Synctx);
let sourceNode: string = flags.source ?? "";
let targetNode: string = flags.target;
const filePath: string = flags.file;
let pendingTransactionsToSync: any[] = [];
const filePath: string = flags.file ?? "";
let pendingTransactionsToSync: Transaction[] = [];
const concurrentCount = flags.concurrency as number;
if ((filePath === "" && sourceNode === "") || (filePath !== "" && sourceNode !== "")) {
this.error(
"Invalid flag values are supplied, source and file are mutually exclusive and at least one needs to be specified",
"Invalid flag values are supplied; source and file are mutually exclusive, and at least one needs to be specified",
);
}
@@ -140,11 +96,11 @@ export default class Sync extends Command {
}
if (!isValidNodeTarget(sourceNode, targetNode)) {
this.error("Invalid nodes supplied to source and/or target, must be valid URL");
this.error("Invalid nodes supplied to source and/or target; must be valid URLs");
}
const sourceProvider = sourceNode ? new ethers.providers.JsonRpcProvider(sourceNode) : undefined;
const targetProvider = new ethers.providers.JsonRpcProvider(targetNode);
const sourceProvider = sourceNode ? new ethers.JsonRpcProvider(sourceNode) : undefined;
const targetProvider = new ethers.JsonRpcProvider(targetNode);
const sourceClientType = sourceProvider ? await getClientType(sourceProvider) : undefined;
const targetClientType = await getClientType(targetProvider);
@@ -173,7 +129,7 @@ export default class Sync extends Command {
this.log(`Skip fetching txs from source node as txs file is supplied`);
}
} catch (err) {
this.error(`Invalid rpc provider(s) - ${err}`);
this.error(`Invalid RPC provider(s) - ${err}`);
}
if (
@@ -184,10 +140,14 @@ export default class Sync extends Command {
return;
}
const sourcePendingTransactions: any =
sourceClientType === "geth" ? parsePendingTransactions(sourceTransactionPool) : sourceTransactionPool;
const targetPendingTransactions: any =
targetClientType === "geth" ? parsePendingTransactions(targetTransactionPool) : targetTransactionPool;
const sourcePendingTransactions: Transaction[] =
sourceClientType === "geth"
? parsePendingTransactions(sourceTransactionPool)
: (sourceTransactionPool as unknown as Transaction[]);
const targetPendingTransactions: Transaction[] =
targetClientType === "geth"
? parsePendingTransactions(targetTransactionPool)
: (targetTransactionPool as unknown as Transaction[]);
if (sourceNode) {
this.log(`Source pending transactions: ${sourcePendingTransactions.length}`);
@@ -202,51 +162,52 @@ export default class Sync extends Command {
if (sourceNode) {
this.log(`Delta between source and target pending transactions is 0.`);
} else {
this.log(`No txs found from file ${filePath}`);
this.log(`No txs found in file ${filePath}`);
}
return;
}
this.log(`Pending transactions to process: ${pendingTransactionsToSync.length}`);
// track errors serializing transactions
// Track errors during serialization
let errorSerialization = 0;
const transactions: Array<string> = [];
// Asynchronous loop to handle address resolution
for (const tx of pendingTransactionsToSync) {
const transaction: UnsignedTransaction = {
to: tx.to,
nonce: Number.parseInt(tx.nonce.toString()),
gasLimit: BigNumber.from(tx.gas),
// Resolve the 'to' address if necessary
const toAddress = tx.to ? await ethers.resolveAddress(tx.to) : undefined;
const transaction: ethers.TransactionLike<string> = {
to: toAddress,
nonce: Number(tx.nonce),
gasLimit: BigInt(tx.gas),
...(Number(tx.type) === 2
? {
gasPrice: BigNumber.from(tx.maxFeePerGas),
maxFeePerGas: BigNumber.from(tx.maxFeePerGas),
maxPriorityFeePerGas: BigNumber.from(tx.maxPriorityFeePerGas),
maxFeePerGas: BigInt(tx.maxFeePerGas!),
maxPriorityFeePerGas: BigInt(tx.maxPriorityFeePerGas!),
}
: { gasPrice: BigNumber.from(tx.gasPrice) }),
: { gasPrice: BigInt(tx.gasPrice!) }),
data: tx.input || "0x",
value: BigNumber.from(tx.value),
...(tx.chainId && Number(tx.type) !== 0 ? { chainId: Number.parseInt(tx.chainId.toString()) } : {}),
...(Number(tx.type) === 1 || Number(tx.type) === 2 ? { accessList: tx.accessList, type: Number(tx.type) } : {}),
value: BigInt(tx.value),
...(tx.chainId && Number(tx.type) !== 0 ? { chainId: Number(tx.chainId) } : {}),
...(Number(tx.type) === 1 || Number(tx.type) === 2
? { accessList: tx.accessList as ethers.AccessListish, type: Number(tx.type) }
: {}),
};
const rawTx = ethers.utils.serializeTransaction(
transaction,
ethers.utils.splitSignature({
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
v: Number.parseInt(tx.v!.toString()),
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
r: tx.r!,
s: tx.s,
}),
);
try {
const rawTx = ethers.Transaction.from(transaction).serialized;
if (ethers.utils.keccak256(rawTx) !== tx.hash) {
if (ethers.keccak256(rawTx) !== tx.hash) {
errorSerialization++;
this.warn(`Failed to serialize transaction: ${tx.hash}`);
}
transactions.push(rawTx);
} catch (error) {
errorSerialization++;
this.warn(`Failed to serialize transaction: ${tx.hash}`);
this.warn(`Error serializing transaction ${tx.hash}: ${(error as Error).message}`);
}
transactions.push(rawTx);
}
const totalBatchesToProcess = Math.ceil(transactions.length / concurrentCount);
@@ -273,20 +234,18 @@ export default class Sync extends Command {
this.log(`Processing batch: ${i + 1} of ${totalBatchesToProcess}, size ${transactionBatch.length}`);
const transactionPromises = transactionBatch.map((transactionReq) => {
return targetProvider.sendTransaction(transactionReq);
const transactionPromises = transactionBatch.map((rawTransaction) => {
return targetProvider.broadcastTransaction(rawTransaction);
});
const results = await Promise.allSettled(transactionPromises);
success += results.filter((result: PromiseSettledResult<unknown>) => result.status === "fulfilled").length;
success = results.filter((result) => result.status === "fulfilled").length;
const resultErrors = results.filter(
(result: PromiseSettledResult<unknown>): result is PromiseRejectedResult => result.status === "rejected",
);
errors += resultErrors.length;
const resultErrors = results.filter((result): result is PromiseRejectedResult => result.status === "rejected");
errors = resultErrors.length;
resultErrors.forEach((result) => {
this.log(`${result.reason.message.toString()}`);
this.log(`Error broadcasting transaction: ${(result.reason as Error).message}`);
});
totalSuccess += success;

View File

@@ -1,3 +0,0 @@
export { Fees, FeeHistory } from "./types";
export { get1559Fees } from "./utils";
export { getWeb3SignerSignature, estimateTransactionGas, executeTransaction } from "./transactions";

View File

@@ -1,48 +0,0 @@
import { ethers } from "ethers";
function sanitizeAddress(argName: string) {
return (input: string) => {
if (!ethers.isAddress(input)) {
throw new Error(`${argName} is not a valid Ethereum address.`);
}
return input;
};
}
function isValidUrl(input: string, allowedProtocols: string[]): boolean {
try {
const url = new URL(input);
return allowedProtocols.includes(url.protocol);
} catch (e) {
return false;
}
}
function sanitizeUrl(argName: string, allowedProtocols: string[]) {
return (input: string) => {
if (!isValidUrl(input, allowedProtocols)) {
throw new Error(`${argName}, with value: ${input} is not a valid URL`);
}
return input;
};
}
function sanitizeHexString(argName: string, expectedLength: number) {
return (input: string) => {
if (!ethers.isHexString(input, expectedLength)) {
throw new Error(`${argName} must be hexadecimal string of length ${expectedLength}.`);
}
return input;
};
}
function sanitizeETHThreshold() {
return (input: string) => {
if (parseInt(input) <= 1) {
throw new Error("Threshold must be higher than 1 ETH");
}
return input;
};
}
export { sanitizeAddress, sanitizeHexString, sanitizeETHThreshold, sanitizeUrl, isValidUrl };

View File

@@ -1,177 +0,0 @@
import { ethers, TransactionLike } from "ethers";
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import { sanitizeAddress, sanitizeHexString, sanitizeUrl, sanitizeETHThreshold } from "./cli";
import { estimateTransactionGas, executeTransaction, get1559Fees, getWeb3SignerSignature } from "../common";
import { Config } from "./types";
import { calculateRewards } from "./utils";
const WEB3_SIGNER_PUBLIC_KEY_LENGTH = 64;
const DEFAULT_MAX_FEE_PER_GAS = "100000000000";
const DEFAULT_GAS_ESTIMATION_PERCENTILE = "10";
const argv = yargs(hideBin(process.argv))
.option("sender-address", {
describe: "Sender address",
type: "string",
demandOption: true,
coerce: sanitizeAddress("sender-address"),
})
.option("destination-address", {
describe: "Destination address",
type: "string",
demandOption: true,
coerce: sanitizeAddress("destination-address"),
})
.option("threshold", {
describe: "Balance threshold of Validator address",
type: "string",
demandOption: true,
coerce: sanitizeETHThreshold(),
})
.option("blockchain-rpc-url", {
describe: "Blockchain rpc url",
type: "string",
demandOption: true,
coerce: sanitizeUrl("blockchain-rpc-url", ["http:", "https:"]),
})
.option("web3-signer-url", {
describe: "Web3 Signer URL",
type: "string",
demandOption: true,
coerce: sanitizeUrl("web3-signer-url", ["http:", "https:"]),
})
.option("web3-signer-public-key", {
describe: "Web3 Signer Public Key",
type: "string",
demandOption: true,
coerce: sanitizeHexString("web3-signer-public-key", WEB3_SIGNER_PUBLIC_KEY_LENGTH),
})
.option("dry-run", {
describe: "Dry run flag",
type: "string",
demandOption: false,
})
.option("max-fee-per-gas", {
describe: "MaxFeePerGas in wei",
type: "string",
default: DEFAULT_MAX_FEE_PER_GAS,
demandOption: false,
})
.option("gas-estimation-percentile", {
describe: "Gas estimation percentile",
type: "string",
default: DEFAULT_GAS_ESTIMATION_PERCENTILE,
demandOption: false,
})
.parseSync();
function getConfig(args: typeof argv): Config {
const destinationAddress: string = args.destinationAddress;
const senderAddress = args.senderAddress;
const threshold = args.threshold;
const blockchainRpcUrl = args.blockchainRpcUrl;
const web3SignerUrl = args.web3SignerUrl;
const web3SignerPublicKey = args.web3SignerPublicKey;
const dryRunCheck = args.dryRun !== "false";
const maxFeePerGasArg = args.maxFeePerGas;
const gasEstimationPercentileArg = args.gasEstimationPercentile;
return {
destinationAddress,
senderAddress,
threshold,
blockchainRpcUrl,
web3SignerUrl,
web3SignerPublicKey,
maxFeePerGas: BigInt(maxFeePerGasArg),
gasEstimationPercentile: parseInt(gasEstimationPercentileArg),
dryRun: dryRunCheck,
};
}
const main = async (args: typeof argv) => {
const {
destinationAddress,
senderAddress,
threshold,
blockchainRpcUrl,
web3SignerUrl,
web3SignerPublicKey,
maxFeePerGas,
gasEstimationPercentile,
dryRun,
} = getConfig(args);
const provider = new ethers.JsonRpcProvider(blockchainRpcUrl);
const [{ chainId }, senderBalance, fees, nonce] = await Promise.all([
provider.getNetwork(),
provider.getBalance(senderAddress),
get1559Fees(provider, maxFeePerGas, gasEstimationPercentile),
provider.getTransactionCount(senderAddress),
]);
if (senderBalance <= ethers.parseEther(threshold)) {
console.log(`Sender balance (${ethers.formatEther(senderBalance)} ETH) is less than threshold. No action needed.`);
return;
}
const rewards = calculateRewards(senderBalance);
if (rewards == 0n) {
console.log(`No rewards to send.`);
return;
}
const transactionRequest: TransactionLike = {
to: destinationAddress,
value: rewards,
type: 2,
chainId,
maxFeePerGas: fees.maxFeePerGas,
maxPriorityFeePerGas: fees.maxPriorityFeePerGas,
nonce: nonce,
};
const transactionGasLimit = await estimateTransactionGas(provider, {
...transactionRequest,
from: senderAddress,
} as ethers.TransactionRequest);
const transaction: TransactionLike = {
...transactionRequest,
gasLimit: transactionGasLimit,
};
const signature = await getWeb3SignerSignature(web3SignerUrl, web3SignerPublicKey, transaction);
if (dryRun) {
console.log("Dryrun enabled: Skipping transaction submission to blockchain.");
console.log(`Here is the expected rewards: ${ethers.formatEther(rewards)} ETH`);
return;
}
const receipt = await executeTransaction(provider, { ...transaction, signature });
if (!receipt) {
throw new Error(`Transaction receipt not found for this transaction ${JSON.stringify(transaction)}`);
}
if (receipt.status == 0) {
throw new Error(`Transaction reverted. Receipt: ${JSON.stringify(receipt)}`);
}
console.log(
`Transaction succeed. Rewards sent: ${ethers.formatEther(rewards)} ETH. Receipt: ${JSON.stringify(receipt)}`,
);
console.log(`Rewards sent: ${ethers.formatEther(rewards)} ETH`);
};
main(argv)
.then(() => process.exit(0))
.catch((error) => {
console.error("The ETH transfer script failed with the following error:", error);
process.exit(1);
});

View File

@@ -1,11 +0,0 @@
export type Config = {
senderAddress: string;
destinationAddress: string;
threshold: string;
blockchainRpcUrl: string;
web3SignerUrl: string;
web3SignerPublicKey: string;
maxFeePerGas: bigint;
gasEstimationPercentile: number;
dryRun: boolean;
};

1
operations/src/index.ts Normal file
View File

@@ -0,0 +1 @@
export { run } from "@oclif/core";

View File

@@ -1,9 +0,0 @@
#!/usr/bin/env -S node --loader ts-node/esm --no-warnings=ExperimentalWarning
// eslint-disable-next-line node/shebang
async function main() {
const { execute } = await import("@oclif/core");
await execute({ development: true, dir: import.meta.url });
}
await main();

View File

@@ -1,8 +0,0 @@
#!/usr/bin/env node
async function main() {
const { execute } = await import("@oclif/core");
await execute({ dir: import.meta.url });
}
await main();

File diff suppressed because it is too large Load Diff

View File

@@ -1,81 +0,0 @@
{
"author": "cs-sre",
"bin": {
"synctx": "./bin/run.js"
},
"dependencies": {
"@oclif/core": "^3",
"@oclif/plugin-help": "^5",
"@oclif/plugin-plugins": "^3.7.0",
"ethers": "^5.7.2"
},
"description": "Sync pending transactions from source to target node",
"devDependencies": {
"@oclif/prettier-config": "^0.2.1",
"@oclif/test": "^3",
"@types/chai": "^4",
"@types/mocha": "^10",
"@types/node": "^18",
"chai": "^4",
"eslint": "^8",
"eslint-config-oclif": "^5",
"eslint-config-oclif-typescript": "^3",
"eslint-config-prettier": "^9.0.0",
"mocha": "^10",
"nodemon": "^3.0.1",
"oclif": "^4.0.3",
"shx": "^0.3.4",
"ts-node": "^10.9.1",
"typescript": "^5"
},
"peerDependencies": {
"ethers": "^5.7.2"
},
"engines": {
"node": ">=20"
},
"files": [
"/bin",
"/dist",
"/npm-shrinkwrap.json",
"/oclif.manifest.json"
],
"homepage": "https://github.com/scripts/synctx",
"license": "MIT",
"main": "",
"name": "synctx",
"oclif": {
"bin": "synctx",
"dirname": "synctx",
"commands": "./dist",
"default": ".",
"plugins": [
"@oclif/plugin-help",
"@oclif/plugin-plugins"
],
"topicSeparator": " ",
"topics": {
"hello": {
"description": "Say hello to the world and others"
}
}
},
"repository": "scripts/synctx",
"scripts": {
"build": "rm -rf dist && tsc -b",
"lint": "eslint . --ext .ts",
"postpack": "rm -f oclif.manifest.json",
"posttest": "npm run lint",
"prepack": "npm run build && oclif manifest && oclif readme",
"test": "mocha --forbid-only --full-trace \"test/**/*.test.ts\"",
"version": "oclif readme && git add README.md"
},
"version": "0.0.0",
"bugs": "https://github.com/scripts/synctx/issues",
"keywords": [
"oclif"
],
"types": "dist/index.d.ts",
"exports": "./lib/index.js",
"type": "module"
}

View File

@@ -1,15 +0,0 @@
{
"compilerOptions": {
"declaration": true,
"module": "Node16",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"target": "es2022",
"moduleResolution": "node16"
},
"include": ["./src/*"],
"ts-node": {
"esm": true
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
import { describe, afterEach, jest, it, expect, beforeEach } from "@jest/globals";
import { JsonRpcProvider } from "ethers";
import { MockProxy, mock, mockClear } from "jest-mock-extended";
import { get1559Fees } from "../utils";
import { get1559Fees } from "../fees.js";
describe("Utils", () => {
let providerMock: MockProxy<JsonRpcProvider>;

View File

@@ -1,56 +1,58 @@
import { describe, it, expect } from "@jest/globals";
import { ethers } from "ethers";
import { isValidUrl, sanitizeAddress, sanitizeETHThreshold, sanitizeHexString, sanitizeUrl } from "../cli";
import { isValidProtocolUrl, validateEthereumAddress, validateHexString, validateUrl } from "../validation.js";
describe("CLI", () => {
describe("sanitizeAddress", () => {
describe("Validation utils", () => {
describe("validateEthereumAddress", () => {
it("should throw an error when the input is not a valid Ethereum address", () => {
const invalidAddress = "0x0a0e";
expect(() => sanitizeAddress("Address")(invalidAddress)).toThrow(`Address is not a valid Ethereum address.`);
expect(() => validateEthereumAddress("Address", invalidAddress)).toThrow(
`Address is not a valid Ethereum address.`,
);
});
it("should return the input when it is a valid Ethereum address", () => {
const address = ethers.hexlify(ethers.randomBytes(20));
expect(sanitizeAddress("Address")(address)).toStrictEqual(address);
expect(validateEthereumAddress("Address", address)).toStrictEqual(address);
});
});
describe("isValidUrl", () => {
describe("isValidProtocolUrl", () => {
it("should return false when the url is not valid", () => {
const invalidUrl = "www.test.com";
expect(isValidUrl(invalidUrl, ["http:", "https:"])).toBeFalsy();
expect(isValidProtocolUrl(invalidUrl, ["http:", "https:"])).toBeFalsy();
});
it("should return false when the url protocol is not allowed", () => {
const invalidUrl = "tcp://test.com";
expect(isValidUrl(invalidUrl, ["http:", "https:"])).toBeFalsy();
expect(isValidProtocolUrl(invalidUrl, ["http:", "https:"])).toBeFalsy();
});
it("should return true when the url valid", () => {
const url = "http://test.com";
expect(isValidUrl(url, ["http:", "https:"])).toBeTruthy();
expect(isValidProtocolUrl(url, ["http:", "https:"])).toBeTruthy();
});
});
describe("sanitizeUrl", () => {
describe("validateUrl", () => {
it("should throw an error when the input is not a valid url", () => {
const invalidUrl = "www.test.com";
expect(() => sanitizeUrl("Url", ["http:", "https:"])(invalidUrl)).toThrow(
expect(() => validateUrl("Url", invalidUrl, ["http:", "https:"])).toThrow(
`Url, with value: ${invalidUrl} is not a valid URL`,
);
});
it("should return the input when it is a valid url", () => {
const url = "http://test.com";
expect(sanitizeUrl("Url", ["http:", "https:"])(url)).toStrictEqual(url);
expect(validateUrl("Url", url, ["http:", "https:"])).toStrictEqual(url);
});
});
describe("sanitizeHexString", () => {
describe("validateHexString", () => {
it("should throw an error when the input is not a hex string", () => {
const invalidHexString = "0a1f";
const expectedLength = 2;
expect(() => sanitizeHexString("HexString", expectedLength)(invalidHexString)).toThrow(
expect(() => validateHexString("HexString", invalidHexString, expectedLength)).toThrow(
`HexString must be hexadecimal string of length ${expectedLength}.`,
);
});
@@ -58,7 +60,7 @@ describe("CLI", () => {
it("should throw an error when the input length is not equal to the expected length", () => {
const hexString = "0x0a1f";
const expectedLength = 4;
expect(() => sanitizeHexString("HexString", expectedLength)(hexString)).toThrow(
expect(() => validateHexString("HexString", hexString, expectedLength)).toThrow(
`HexString must be hexadecimal string of length ${expectedLength}.`,
);
});
@@ -66,19 +68,7 @@ describe("CLI", () => {
it("should return the input when it is a hex string", () => {
const hexString = "0x0a1f";
const expectedLength = 2;
expect(sanitizeHexString("HexString", expectedLength)(hexString)).toStrictEqual(hexString);
});
});
describe("sanitizeETHThreshold", () => {
it("should throw an error when the input threshold is less than 1 ETH", () => {
const invalidThreshold = "0.5";
expect(() => sanitizeETHThreshold()(invalidThreshold)).toThrow("Threshold must be higher than 1 ETH");
});
it("should return the input when it is higher than 1 ETH", () => {
const threshold = "2";
expect(sanitizeETHThreshold()(threshold)).toStrictEqual(threshold);
expect(validateHexString("HexString", hexString, expectedLength)).toStrictEqual(hexString);
});
});
});

View File

@@ -1,5 +1,5 @@
import { ethers } from "ethers";
import { FeeHistory, Fees } from "./types";
import { FeeHistory, Fees } from "./types.js";
export async function get1559Fees(
provider: ethers.JsonRpcProvider,

View File

@@ -0,0 +1,3 @@
export * from "./fees.js";
export * from "./types.js";
export * from "./validation.js";

View File

@@ -0,0 +1,31 @@
import { ethers } from "ethers";
export function validateEthereumAddress(argName: string, input: string) {
if (!ethers.isAddress(input)) {
throw new Error(`${argName} is not a valid Ethereum address.`);
}
return input;
}
export function isValidProtocolUrl(input: string, allowedProtocols: string[]): boolean {
try {
const url = new URL(input);
return allowedProtocols.includes(url.protocol);
} catch (e) {
return false;
}
}
export function validateUrl(argName: string, input: string, allowedProtocols: string[]) {
if (!isValidProtocolUrl(input, allowedProtocols)) {
throw new Error(`${argName}, with value: ${input} is not a valid URL`);
}
return input;
}
export function validateHexString(argName: string, input: string, expectedLength: number) {
if (!ethers.isHexString(input, expectedLength)) {
throw new Error(`${argName} must be hexadecimal string of length ${expectedLength}.`);
}
return input;
}

View File

@@ -1,6 +1,6 @@
import { describe, it, expect } from "@jest/globals";
import { ethers } from "ethers";
import { calculateRewards } from "../utils";
import { calculateRewards } from "../rewards";
describe("Utils", () => {
describe("calculateRewards", () => {

View File

@@ -2,7 +2,7 @@ import { describe, jest, it, expect, afterAll } from "@jest/globals";
import axios from "axios";
import { JsonRpcProvider, Signature, TransactionLike, TransactionResponse, ethers } from "ethers";
import { MockProxy, mock, mockClear } from "jest-mock-extended";
import { estimateTransactionGas, executeTransaction, getWeb3SignerSignature } from "../transactions";
import { estimateTransactionGas, executeTransaction, getWeb3SignerSignature } from "../transactions.js";
jest.mock("axios");

View File

@@ -0,0 +1,16 @@
import { describe, it, expect } from "@jest/globals";
import { validateETHThreshold } from "../validation.js";
describe("Validation utils", () => {
describe("validateETHThreshold", () => {
it("should throw an error when the input threshold is less than 1 ETH", () => {
const invalidThreshold = "0.5";
expect(() => validateETHThreshold(invalidThreshold)).toThrow("Threshold must be higher than 1 ETH");
});
it("should return the input when it is higher than 1 ETH", () => {
const threshold = "2";
expect(validateETHThreshold(threshold)).toStrictEqual(threshold);
});
});
});

View File

@@ -0,0 +1,70 @@
import { FlagOutput } from "@oclif/core/lib/interfaces/parser";
export type Config = {
senderAddress: string;
destinationAddress: string;
threshold: string;
blockchainRpcUrl: string;
web3SignerUrl: string;
web3SignerPublicKey: string;
maxFeePerGas: bigint;
gasEstimationPercentile: number;
dryRun: boolean;
};
export function validateConfig(flags: FlagOutput): Config {
const {
senderAddress,
destinationAddress,
threshold,
blockchainRpcUrl,
web3SignerUrl,
web3SignerPublicKey,
dryRun,
maxFeePerGas: maxFeePerGasArg,
gasEstimationPercentile,
} = flags;
const requiredFlags = [
"senderAddress",
"destinationAddress",
"threshold",
"blockchainRpcUrl",
"web3SignerUrl",
"web3SignerPublicKey",
];
for (const flagName of requiredFlags) {
if (!flags[flagName]) {
throw new Error(`Missing required flag: ${flagName}`);
}
}
let maxFeePerGas: bigint;
try {
maxFeePerGas = BigInt(maxFeePerGasArg);
if (maxFeePerGas <= 0n) {
throw new Error();
}
} catch {
throw new Error(`Invalid value for --max-fee-per-gas: ${maxFeePerGasArg}. Must be a positive integer in wei.`);
}
if (gasEstimationPercentile < 0 || gasEstimationPercentile > 100) {
throw new Error(
`Invalid value for --gas-estimation-percentile: ${gasEstimationPercentile}. Must be an integer between 0 and 100.`,
);
}
return {
senderAddress,
destinationAddress,
threshold,
blockchainRpcUrl,
web3SignerUrl,
web3SignerPublicKey,
maxFeePerGas,
gasEstimationPercentile,
dryRun,
};
}

View File

@@ -0,0 +1,3 @@
export const WEB3_SIGNER_PUBLIC_KEY_LENGTH = 64;
export const DEFAULT_MAX_FEE_PER_GAS = "100000000000";
export const DEFAULT_GAS_ESTIMATION_PERCENTILE = 10;

View File

@@ -0,0 +1,5 @@
export * from "./config.js";
export * from "./constants.js";
export * from "./rewards.js";
export * from "./transactions.js";
export * from "./validation.js";

View File

@@ -0,0 +1,6 @@
export function validateETHThreshold(input: string) {
if (parseInt(input) <= 1) {
throw new Error("Threshold must be higher than 1 ETH");
}
return input;
}

View File

@@ -0,0 +1,17 @@
import { ethers } from "ethers";
export type ClientApi = {
[key: string]: {
api: string;
params: Array<unknown>;
};
};
export const getClientType = async (nodeProvider: ethers.JsonRpcProvider): Promise<string> => {
const res: string = await nodeProvider.send("web3_clientVersion", []);
const clientType = res.slice(0, 4).toLowerCase();
if (!["geth", "besu"].includes(clientType)) {
throw new Error(`Invalid node client type, must be either geth or besu`);
}
return clientType;
};

View File

@@ -0,0 +1,4 @@
export * from "./types.js";
export * from "./validation.js";
export * from "./transactions.js";
export * from "./client.js";

View File

@@ -0,0 +1,13 @@
import { Transaction, Txpool } from "./types.js";
export const parsePendingTransactions = (pool: Txpool): Transaction[] => {
const pendingAddresses = Object.values(pool.pending);
const transactionsByNonce = pendingAddresses.map((txsByNonce) => Object.values(txsByNonce));
const transactions = transactionsByNonce.flat();
return transactions;
};
export const getPendingTransactions = (sourcePool: Transaction[], targetPool: Transaction[]): Transaction[] => {
const targetPendingTransactions = new Set(targetPool.map((tx) => tx.hash));
return sourcePool.filter((tx) => !targetPendingTransactions.has(tx.hash));
};

View File

@@ -0,0 +1,25 @@
import { ethers } from "ethers";
export type Transaction = {
hash: string;
nonce: number;
gas: string;
maxFeePerGas?: string;
maxPriorityFeePerGas?: string;
gasPrice?: string;
input?: string;
value: string;
chainId?: string;
accessList?: ethers.AccessListish | null;
type: string | number;
to?: string;
};
export type Txpool = {
pending: {
[address: string]: {
[nonce: string]: Transaction;
};
};
queued: object;
};

View File

@@ -0,0 +1,16 @@
export const isValidNodeTarget = (sourceNode: string, targetNode: string): boolean => {
try {
if (sourceNode) {
new URL(sourceNode);
}
new URL(targetNode);
return true;
} catch (e) {
return false;
}
};
export const isLocalPort = (value: string): boolean => {
const port = Number(value);
return !isNaN(port) && port >= 1 && port <= 65535;
};

View File

@@ -1,29 +1,16 @@
{
"compilerOptions": {
"target": "es2021",
"module": "commonjs",
"declaration": true,
"rootDir": "./src",
"outDir": "dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"strictNullChecks": true,
"resolveJsonModule": true,
"noImplicitThis": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"strictPropertyInitialization": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
},
"include": [
"src/common/**/*.ts",
"src/common/**/*.d.ts",
"src/common/**/*.js",
"src/ethTransfer/**/*.ts",
"src/ethTransfer/**/*.d.ts",
"src/ethTransfer/**/*.js"
]
}
"extends": "../tsconfig.json",
"compilerOptions": {
"declaration": true,
"module": "ES2022",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"target": "es2022",
"moduleResolution": "Node",
"skipLibCheck": true,
"esModuleInterop": true,
},
"include": ["./src/**/*"],
"exclude": ["node_modules", "dist"]
}

5124
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,5 +4,4 @@ packages:
- 'sdk/**'
- 'operations/**'
- 'bridge-ui/**'
- '!operations/src/synctx/**'
- 'ts-libs/**'