mirror of
https://github.com/vacp2p/linea-monorepo.git
synced 2026-01-08 03:43:56 -05:00
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:
29
.github/workflows/all-tools.yml
vendored
29
.github/workflows/all-tools.yml
vendored
@@ -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
2
.gitignore
vendored
@@ -24,7 +24,7 @@
|
||||
.run/**.run.xml
|
||||
.envrc
|
||||
bin/
|
||||
!operations/src/synctx/bin/
|
||||
!operations/bin/
|
||||
target/
|
||||
tmp/
|
||||
build/
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
dist
|
||||
node_modules
|
||||
src/synctx/
|
||||
coverage
|
||||
@@ -1,3 +1,3 @@
|
||||
dist
|
||||
node_modules
|
||||
src/synctx/
|
||||
coverage
|
||||
@@ -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"]
|
||||
@@ -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
6
operations/bin/dev.js
Executable 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
5
operations/bin/run.js
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import {execute} from '@oclif/core'
|
||||
|
||||
await execute({dir: import.meta.url})
|
||||
18
operations/jest.config.cjs
Normal file
18
operations/jest.config.cjs
Normal 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: [],
|
||||
};
|
||||
@@ -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"],
|
||||
};
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
191
operations/src/commands/eth-transfer.ts
Normal file
191
operations/src/commands/eth-transfer.ts
Normal 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`);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -1,3 +0,0 @@
|
||||
export { Fees, FeeHistory } from "./types";
|
||||
export { get1559Fees } from "./utils";
|
||||
export { getWeb3SignerSignature, estimateTransactionGas, executeTransaction } from "./transactions";
|
||||
@@ -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 };
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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
1
operations/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { run } from "@oclif/core";
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
15787
operations/src/synctx/package-lock.json
generated
15787
operations/src/synctx/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
@@ -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
@@ -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>;
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
3
operations/src/utils/common/index.ts
Normal file
3
operations/src/utils/common/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./fees.js";
|
||||
export * from "./types.js";
|
||||
export * from "./validation.js";
|
||||
31
operations/src/utils/common/validation.ts
Normal file
31
operations/src/utils/common/validation.ts
Normal 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;
|
||||
}
|
||||
@@ -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", () => {
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
70
operations/src/utils/eth-transfer/config.ts
Normal file
70
operations/src/utils/eth-transfer/config.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
3
operations/src/utils/eth-transfer/constants.ts
Normal file
3
operations/src/utils/eth-transfer/constants.ts
Normal 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;
|
||||
5
operations/src/utils/eth-transfer/index.ts
Normal file
5
operations/src/utils/eth-transfer/index.ts
Normal 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";
|
||||
6
operations/src/utils/eth-transfer/validation.ts
Normal file
6
operations/src/utils/eth-transfer/validation.ts
Normal 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;
|
||||
}
|
||||
17
operations/src/utils/synctx/client.ts
Normal file
17
operations/src/utils/synctx/client.ts
Normal 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;
|
||||
};
|
||||
4
operations/src/utils/synctx/index.ts
Normal file
4
operations/src/utils/synctx/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./types.js";
|
||||
export * from "./validation.js";
|
||||
export * from "./transactions.js";
|
||||
export * from "./client.js";
|
||||
13
operations/src/utils/synctx/transactions.ts
Normal file
13
operations/src/utils/synctx/transactions.ts
Normal 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));
|
||||
};
|
||||
25
operations/src/utils/synctx/types.ts
Normal file
25
operations/src/utils/synctx/types.ts
Normal 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;
|
||||
};
|
||||
16
operations/src/utils/synctx/validation.ts
Normal file
16
operations/src/utils/synctx/validation.ts
Normal 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;
|
||||
};
|
||||
@@ -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
5124
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -4,5 +4,4 @@ packages:
|
||||
- 'sdk/**'
|
||||
- 'operations/**'
|
||||
- 'bridge-ui/**'
|
||||
- '!operations/src/synctx/**'
|
||||
- 'ts-libs/**'
|
||||
|
||||
Reference in New Issue
Block a user