diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dcea29ec..d96e138b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,10 +7,6 @@ on: - 'release/**' - 'master' pull_request: - branches: - - 'master' - - '**' - types: [opened, synchronize, reopened] jobs: fmt: @@ -20,10 +16,8 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - # Toolchain file bug workaround: https://github.com/dtolnay/rust-toolchain/issues/153 - run: rustup component add rustfmt - - name: Run Fmt Check - run: cargo fmt --all -- --check + - run: cargo fmt --all -- --check clippy: name: Clippy @@ -33,226 +27,30 @@ jobs: - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - run: rustup component add clippy - - name: Run clippy - run: cargo clippy -- -D warnings -A clippy::uninlined_format_args - - build: - if: | - (github.event_name == 'push' || github.event_name == 'pull_request') && - github.actor != 'github-actions[bot]' && - ( - !(github.event_name == 'pull_request' && - startsWith(github.event.pull_request.head.ref, 'release/') && - github.event.pull_request.base.ref == 'master') || - (github.event_name == 'pull_request' && - (contains(github.event.pull_request.head.ref, '/merge') || - startsWith(github.event.pull_request.head.ref, 'merge'))) - ) && - !(github.ref == 'refs/heads/master' && - github.event_name == 'push' && - (contains(github.event.head_commit.message, 'Release v') || - contains(github.event.head_commit.message, 'release/'))) - name: ${{ matrix.target }} (${{ matrix.runner }}) - runs-on: ${{ matrix.runner }} - timeout-minutes: 240 - strategy: - fail-fast: false - matrix: - include: - - runner: ubuntu-22.04 - target: x86_64-unknown-linux-gnu - platform: linux - arch: amd64 - - runner: macos-15-intel - target: x86_64-apple-darwin - platform: darwin - arch: amd64 - - runner: macos-latest - target: aarch64-apple-darwin - platform: darwin - arch: arm64 - - runner: windows-latest - target: x86_64-pc-windows-msvc - platform: win32 - arch: amd64 - - env: - BUILD_TYPE: release + - run: cargo clippy -- -D warnings -A clippy::uninlined_format_args + test: + name: Test + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - with: - ref: ${{ github.event_name == 'pull_request' && format('refs/pull/{0}/merge', github.event.pull_request.number) || github.ref_name }} - - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.target }} - uses: Swatinem/rust-cache@v2 with: - key: ${{ matrix.target }} cache-on-failure: true - - - name: Apple M1 setup - if: matrix.target == 'aarch64-apple-darwin' - run: | - echo "SDKROOT=$(xcrun -sdk macosx --show-sdk-path)" >> $GITHUB_ENV - echo "MACOSX_DEPLOYMENT_TARGET=$(xcrun -sdk macosx --show-sdk-platform-version)" >> $GITHUB_ENV - - - name: Linux ARM setup - if: matrix.target == 'aarch64-unknown-linux-gnu' - run: | - sudo apt-get update -y - sudo apt-get install -y gcc-aarch64-linux-gnu - echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV - - - name: Install MSVC target - if: matrix.target == 'x86_64-pc-windows-msvc' - run: rustup target add x86_64-pc-windows-msvc - - - name: Install OpenSSL development libraries - if: matrix.target == 'aarch64-unknown-linux-gnu' || matrix.target == 'x86_64-unknown-linux-gnu' + - name: Install dependencies run: | sudo apt-get update -y sudo apt-get install -y libssl-dev pkg-config - - - name: Setup cross-compilation for pkg-config - if: matrix.target == 'aarch64-unknown-linux-gnu' - run: | - echo "PKG_CONFIG_ALLOW_CROSS=1" >> $GITHUB_ENV - echo "PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu" >> $GITHUB_ENV - echo "PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig" >> $GITHUB_ENV - echo "PKG_CONFIG_LIBDIR=/usr/lib/aarch64-linux-gnu/pkgconfig" >> $GITHUB_ENV - - - name: Extract version name - id: extract_version - shell: bash - run: echo "VERSION_NAME=${GITHUB_REF#refs/heads/release/}" >> $GITHUB_ENV - - - name: Install vcpkg on Windows - if: matrix.target == 'x86_64-pc-windows-msvc' - shell: pwsh - run: | - git clone https://github.com/Microsoft/vcpkg.git C:\vcpkg - C:\vcpkg\bootstrap-vcpkg.bat - C:\vcpkg\vcpkg integrate install - echo "VCPKG_ROOT=C:\vcpkg" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - echo "C:\vcpkg" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - - - name: Install CMake on Windows - if: matrix.target == 'x86_64-pc-windows-msvc' - shell: pwsh - run: | - choco install cmake --version=3.20.0 --installargs 'ADD_CMAKE_TO_PATH=System' - refreshenv - cmake --version - echo "CMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - - - name: Install dependencies with vcpkg on Windows - if: matrix.target == 'x86_64-pc-windows-msvc' - shell: pwsh - run: | - C:\vcpkg\vcpkg install openssl:x64-windows-static-md zlib:x64-windows-static-md - - - name: Build binaries - working-directory: crates/cli - env: - CMAKE_TOOLCHAIN_FILE: C:/vcpkg/scripts/buildsystems/vcpkg.cmake - shell: bash - run: | - set -eo pipefail - target="${{ matrix.target }}" - flags=() - - if [[ "$target" != *msvc* && "$target" != "aarch64-unknown-linux-gnu" ]]; then - flags+=(--features jemalloc) - fi - - [[ "$target" == *windows* ]] && exe=".exe" - - if [[ "$target" == *windows* ]]; then - export PATH="$PATH:/c/vcpkg" - echo "CMAKE_TOOLCHAIN_FILE: $CMAKE_TOOLCHAIN_FILE" - ls -l "$CMAKE_TOOLCHAIN_FILE" || echo "Toolchain file not found!" - fi - - if [[ "${{ env.BUILD_TYPE }}" == "release" ]]; then - RUST_BACKTRACE=1 CMAKE_TOOLCHAIN_FILE="$CMAKE_TOOLCHAIN_FILE" cargo build --release --target "$target" "${flags[@]}" -vv - else - RUST_BACKTRACE=1 CMAKE_TOOLCHAIN_FILE="$CMAKE_TOOLCHAIN_FILE" cargo build --target "$target" "${flags[@]}" -vv - fi - - - name: Smoke Test - shell: bash - run: | - set -eo pipefail - target="${{ matrix.target }}" - build_type="${{ env.BUILD_TYPE }}" - binary_path="${{ github.workspace }}/target/$target/$build_type/rrelayer_cli" - - if [[ "$target" == *windows* ]]; then - binary_path+=".exe" - fi - - echo "Running smoke test for $binary_path" - "$binary_path" --version - "$binary_path" help - - - name: Archive binaries - id: artifacts - if: startsWith(github.ref, 'refs/heads/release/') - env: - PLATFORM_NAME: ${{ matrix.platform }} - TARGET: ${{ matrix.target }} - ARCH: ${{ matrix.arch }} - VERSION_NAME: ${{ env.VERSION_NAME }} - shell: bash - run: | - set -eo pipefail - BUILD_DIR="${{ github.workspace }}/target/${TARGET}/${{ env.BUILD_TYPE }}" - CLI_BINARY_NAME="rrelayer_cli" - [[ "$PLATFORM_NAME" == "win32" ]] && CLI_BINARY_NAME="rrelayer_cli.exe" - - # Create a temporary staging directory for creating the archive - STAGING_DIR="staging" - mkdir -p "$STAGING_DIR" - - # Copy binaries to the staging directory - echo "Copying $BUILD_DIR/$CLI_BINARY_NAME to $STAGING_DIR/" - cp "$BUILD_DIR/$CLI_BINARY_NAME" "$STAGING_DIR/" - - # Create the final archive - if [ "$PLATFORM_NAME" == "linux" ] || [ "$PLATFORM_NAME" == "darwin" ]; then - FILE_NAME="rrelayer_${PLATFORM_NAME}-${ARCH}.tar.gz" - tar -czvf "$FILE_NAME" -C "$STAGING_DIR" . - else - FILE_NAME="rrelayer_${PLATFORM_NAME}-${ARCH}.zip" - (cd "$STAGING_DIR" && 7z a -tzip "${{ github.workspace }}/$FILE_NAME" .) - fi - - echo "Created archive: $FILE_NAME" - echo "file_name=$FILE_NAME" >> $GITHUB_OUTPUT - - name: Run tests - shell: bash - run: | - set -eo pipefail - target="${{ matrix.target }}" - - cargo test --exclude rust-sdk-playground --workspace --release --target "$target" - - - name: Upload artifact - if: startsWith(github.ref, 'refs/heads/release/') - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.platform }}-${{ matrix.arch }} - path: ${{ steps.artifacts.outputs.file_name }} + run: cargo test --exclude rust-sdk-playground --workspace create_pr: name: Create Release PR runs-on: ubuntu-22.04 - needs: build + needs: test if: | - github.actor != 'github-actions[bot]' && + github.actor != 'github-actions[bot]' && startsWith(github.ref, 'refs/heads/release/') steps: - uses: actions/checkout@v4 @@ -261,172 +59,17 @@ jobs: fetch-depth: 0 - name: Extract version from branch name - shell: bash run: | VERSION=${GITHUB_REF#refs/heads/release/} echo "VERSION_NAME=$VERSION" >> $GITHUB_ENV - name: Update Cargo.toml versions - shell: bash run: | sed -i 's/^version = ".*"/version = "${{ env.VERSION_NAME }}"/' crates/cli/Cargo.toml sed -i 's/^version = ".*"/version = "${{ env.VERSION_NAME }}"/' crates/core/Cargo.toml sed -i 's/^version = ".*"/version = "${{ env.VERSION_NAME }}"/' crates/sdk/Cargo.toml sed -i 's/^version = ".*"/version = "${{ env.VERSION_NAME }}"/' Cargo.toml - # Temporarily commented out changelog update for release process - # - name: Update Changelog - # shell: bash - # run: | - # CHANGELOG_FILE="documentation/rrelayer/docs/pages/changelog.mdx" - # DAY=$(date '+%d' | sed 's/^0*//') - # MONTH=$(date '+%B') - # YEAR=$(date '+%Y') - # - # # Add ordinal suffix - # case $DAY in - # 1|21|31) SUFFIX="st";; - # 2|22) SUFFIX="nd";; - # 3|23) SUFFIX="rd";; - # *) SUFFIX="th";; - # esac - # - # DATE="$DAY$SUFFIX $MONTH $YEAR" - # - # echo "Updating changelog for version ${{ env.VERSION_NAME }}" - # - # # Create changelog if it doesn't exist - # if [[ ! -f "$CHANGELOG_FILE" ]]; then - # cat > "$CHANGELOG_FILE" << EOF - # # Changelog - # - # ## Changes Not Deployed - # ------------------------------------------------- - # - # ### Features - # ------------------------------------------------- - # - # ### Bug fixes - # ------------------------------------------------- - # - # ### Breaking changes - # ------------------------------------------------- - # - # ## Releases - # ------------------------------------------------- - # - # all release branches are deployed through \`release/VERSION_NUMBER\` branches - # - # EOF - # fi - # - # # Create a temporary file to work with - # cp "$CHANGELOG_FILE" changelog_temp.md - # - # # Extract just the bug fixes line directly from Changes Not Deployed section - # BUG_FIXES=$(sed -n '/^## Changes Not Deployed/,/^## Releases/p' changelog_temp.md | grep "^- fix:" | head -5) - # - # # Use the simple extraction for now - # FEATURES="" - # BUG_FIXES="$BUG_FIXES" - # BREAKING_CHANGES="" - # - # # Save the extracted content for PR creation - # { - # echo "### Changes in this release:" - # echo "-------------------------------------------------" - # echo "### Features" - # echo "-------------------------------------------------" - # if [[ -n "$FEATURES" ]]; then - # echo "$FEATURES" - # fi - # echo "" - # echo "### Bug fixes" - # echo "-------------------------------------------------" - # if [[ -n "$BUG_FIXES" ]]; then - # echo "$BUG_FIXES" - # fi - # echo "" - # echo "### Breaking changes" - # echo "-------------------------------------------------" - # if [[ -n "$BREAKING_CHANGES" ]]; then - # echo "$BREAKING_CHANGES" - # fi - # } > pr_body_content.txt - # - # # Save to environment variable - # echo "PR_BODY_CONTENT<> $GITHUB_ENV - # cat pr_body_content.txt >> $GITHUB_ENV - # echo "EOF" >> $GITHUB_ENV - # - # # Create new release entry - # cat > new_release.txt << EOF - # # ${{ env.VERSION_NAME }}-beta - $DATE - # - # github branch - https://github.com/joshstevens19/rrelayer/tree/release/${{ env.VERSION_NAME }} - # - # - linux binary - https://github.com/joshstevens19/rrelayer/releases/download/v${{ env.VERSION_NAME }}/rrelayer_linux-amd64.tar.gz - # - mac apple silicon binary - https://github.com/joshstevens19/rrelayer/releases/download/v${{ env.VERSION_NAME }}/rrelayer_darwin-arm64.tar.gz - # - mac apple intel binary - https://github.com/joshstevens19/rrelayer/releases/download/v${{ env.VERSION_NAME }}/rrelayer_darwin-amd64.tar.gz - # - windows binary - https://github.com/joshstevens19/rrelayer/releases/download/v${{ env.VERSION_NAME }}/rrelayer_win32-amd64.zip - # EOF - # - # # Add sections if they have content - # if [[ -n "$FEATURES" && "$FEATURES" =~ [^[:space:]] ]]; then - # echo "" >> new_release.txt - # echo "### Features" >> new_release.txt - # echo "-------------------------------------------------" >> new_release.txt - # echo "$FEATURES" >> new_release.txt - # fi - # - # if [[ -n "$BUG_FIXES" && "$BUG_FIXES" =~ [^[:space:]] ]]; then - # echo "" >> new_release.txt - # echo "### Bug fixes" >> new_release.txt - # echo "-------------------------------------------------" >> new_release.txt - # echo "$BUG_FIXES" >> new_release.txt - # fi - # - # if [[ -n "$BREAKING_CHANGES" && "$BREAKING_CHANGES" =~ [^[:space:]] ]]; then - # echo "" >> new_release.txt - # echo "### Breaking changes" >> new_release.txt - # echo "-------------------------------------------------" >> new_release.txt - # echo "$BREAKING_CHANGES" >> new_release.txt - # fi - # - # # Get existing releases (everything after "## Releases") - # EXISTING_RELEASES=$(awk '/^## Releases/,0' changelog_temp.md | tail -n +5) - # - # # Create new changelog - # cat > "$CHANGELOG_FILE" << EOF - # # Changelog - # - # ## Changes Not Deployed - # ------------------------------------------------- - # - # ### Features - # ------------------------------------------------- - # - # ### Bug fixes - # ------------------------------------------------- - # - # ### Breaking changes - # ------------------------------------------------- - # - # ## Releases - # ------------------------------------------------- - # - # all release branches are deployed through \`release/VERSION_NUMBER\` branches - # - # $(cat new_release.txt) - # - # $EXISTING_RELEASES - # EOF - # - # # Clean up temporary files - # rm -f changelog_temp.md new_release.txt pr_body_content.txt - # - # echo "Changelog updated successfully" - - name: Commit changes run: | git config --local user.email "action@github.com" @@ -443,10 +86,8 @@ jobs: PR_EXISTS=$(gh pr list --base master --head release/${{ env.VERSION_NAME }} --json number --jq length) if [ "$PR_EXISTS" -gt 0 ]; then echo "pr_exists=true" >> $GITHUB_OUTPUT - echo "PR already exists, skipping creation" else echo "pr_exists=false" >> $GITHUB_OUTPUT - echo "No PR exists, will create one" fi - name: Create Pull Request @@ -459,334 +100,43 @@ jobs: --body "## Release v${{ env.VERSION_NAME }} This PR contains: - - ✅ Version bump to ${{ env.VERSION_NAME }} - - ✅ Changelog updated with release notes - - ✅ Ready for release + - Version bump to ${{ env.VERSION_NAME }} - **Merging this PR will automatically create a GitHub Release with binaries.** - - ${{ env.PR_BODY_CONTENT }}" \ + **Merging this PR will automatically create a GitHub Release and build Docker images.**" \ --base master \ --head release/${{ env.VERSION_NAME }} - release_build: - name: Build Release Binaries - runs-on: ${{ matrix.runner }} - if: | - github.actor != 'github-actions[bot]' && - github.ref == 'refs/heads/master' && - github.event_name == 'push' - timeout-minutes: 240 - strategy: - fail-fast: false - matrix: - include: - - runner: ubuntu-22.04 - target: x86_64-unknown-linux-gnu - platform: linux - arch: amd64 - - runner: macos-15-intel - target: x86_64-apple-darwin - platform: darwin - arch: amd64 - - runner: macos-latest - target: aarch64-apple-darwin - platform: darwin - arch: arm64 - - runner: windows-latest - target: x86_64-pc-windows-msvc - platform: win32 - arch: amd64 - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Check if this is a release commit and extract version - id: check_release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - shell: bash - run: | - echo "=== DEBUG: Checking recent commits ===" - git log --oneline -10 --pretty=format:"%H %s" - echo "" - echo "=== DEBUG: Checking for release pattern ===" - - # Check the most recent commit (HEAD) for release pattern - RECENT_COMMIT_MSG=$(git log --oneline -1 --pretty=format:"%s") - echo "Most recent commit: '$RECENT_COMMIT_MSG'" - - # Try to extract version from Release v pattern (including PR number) - VERSION_FROM_RELEASE=$(echo "$RECENT_COMMIT_MSG" | grep -o 'Release v[0-9]*\.[0-9]*\.[0-9]*' | sed 's/Release v//' || echo "") - echo "VERSION_FROM_RELEASE: '$VERSION_FROM_RELEASE'" - - if [[ -n "$VERSION_FROM_RELEASE" ]]; then - echo "VERSION_NAME=$VERSION_FROM_RELEASE" >> $GITHUB_ENV - echo "is_release=true" >> $GITHUB_OUTPUT - echo "Found release from commit title: Release v$VERSION_FROM_RELEASE" - else - echo "Not a release commit" - echo "is_release=false" >> $GITHUB_OUTPUT - fi - - - name: Skip if not a release - if: steps.check_release.outputs.is_release != 'true' - run: | - echo "Skipping release build - not a release commit" - exit 0 - - - name: Checkout release branch - if: steps.check_release.outputs.is_release == 'true' - uses: actions/checkout@v4 - with: - ref: release/${{ env.VERSION_NAME }} - fetch-depth: 0 - - - uses: dtolnay/rust-toolchain@stable - if: steps.check_release.outputs.is_release == 'true' - with: - targets: ${{ matrix.target }} - - - uses: Swatinem/rust-cache@v2 - if: steps.check_release.outputs.is_release == 'true' - with: - key: ${{ matrix.target }} - cache-on-failure: true - - - name: Apple M1 setup - if: steps.check_release.outputs.is_release == 'true' && matrix.target == 'aarch64-apple-darwin' - run: | - echo "SDKROOT=$(xcrun -sdk macosx --show-sdk-path)" >> $GITHUB_ENV - echo "MACOSX_DEPLOYMENT_TARGET=$(xcrun -sdk macosx --show-sdk-platform-version)" >> $GITHUB_ENV - - - name: Install MSVC target - if: steps.check_release.outputs.is_release == 'true' && matrix.target == 'x86_64-pc-windows-msvc' - run: rustup target add x86_64-pc-windows-msvc - - - name: Install OpenSSL development libraries - if: steps.check_release.outputs.is_release == 'true' && matrix.target == 'x86_64-unknown-linux-gnu' - run: | - sudo apt-get update -y - sudo apt-get install -y libssl-dev pkg-config - - - name: Install vcpkg on Windows - if: steps.check_release.outputs.is_release == 'true' && matrix.target == 'x86_64-pc-windows-msvc' - shell: pwsh - run: | - git clone https://github.com/Microsoft/vcpkg.git C:\vcpkg - C:\vcpkg\bootstrap-vcpkg.bat - C:\vcpkg\vcpkg integrate install - echo "VCPKG_ROOT=C:\vcpkg" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - echo "C:\vcpkg" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - - - name: Install CMake on Windows - if: steps.check_release.outputs.is_release == 'true' && matrix.target == 'x86_64-pc-windows-msvc' - shell: pwsh - run: | - choco install cmake --version=3.20.0 --installargs 'ADD_CMAKE_TO_PATH=System' - refreshenv - cmake --version - echo "CMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - - - name: Install dependencies with vcpkg on Windows - if: steps.check_release.outputs.is_release == 'true' && matrix.target == 'x86_64-pc-windows-msvc' - shell: pwsh - run: | - C:\vcpkg\vcpkg install openssl:x64-windows-static-md zlib:x64-windows-static-md - - - name: Build binaries - if: steps.check_release.outputs.is_release == 'true' - working-directory: crates/cli - env: - CMAKE_TOOLCHAIN_FILE: C:/vcpkg/scripts/buildsystems/vcpkg.cmake - shell: bash - run: | - set -eo pipefail - target="${{ matrix.target }}" - - if [[ "$target" != *msvc* && "$target" != "aarch64-unknown-linux-gnu" ]]; then - flags+=(--features jemalloc) - fi - - if [[ "$target" == *windows* ]]; then - export PATH="$PATH:/c/vcpkg" - echo "CMAKE_TOOLCHAIN_FILE: $CMAKE_TOOLCHAIN_FILE" - ls -l "$CMAKE_TOOLCHAIN_FILE" || echo "Toolchain file not found!" - fi - - RUST_BACKTRACE=1 CMAKE_TOOLCHAIN_FILE="$CMAKE_TOOLCHAIN_FILE" cargo build --release --target "$target" -vv - - - name: Smoke Test - if: steps.check_release.outputs.is_release == 'true' - shell: bash - run: | - set -eo pipefail - target="${{ matrix.target }}" - binary_path="${{ github.workspace }}/target/$target/release/rrelayer_cli" - - if [[ "$target" == *windows* ]]; then - binary_path+=".exe" - fi - - echo "Running smoke test for $binary_path" - "$binary_path" --version - "$binary_path" help - - - name: Archive binaries - if: steps.check_release.outputs.is_release == 'true' - id: artifacts - env: - PLATFORM_NAME: ${{ matrix.platform }} - TARGET: ${{ matrix.target }} - ARCH: ${{ matrix.arch }} - VERSION_NAME: ${{ env.VERSION_NAME }} - shell: bash - run: | - set -eo pipefail - BUILD_DIR="${{ github.workspace }}/target/${TARGET}/release" - CLI_BINARY_NAME="rrelayer_cli" - [[ "$PLATFORM_NAME" == "win32" ]] && CLI_BINARY_NAME="rrelayer_cli.exe" - - # Create a temporary staging directory for creating the archive - STAGING_DIR="staging" - mkdir -p "$STAGING_DIR" - - # Copy binaries to the staging directory - echo "Copying $BUILD_DIR/$CLI_BINARY_NAME to $STAGING_DIR/" - cp "$BUILD_DIR/$CLI_BINARY_NAME" "$STAGING_DIR/" - - # Create the final archive - if [ "$PLATFORM_NAME" == "linux" ] || [ "$PLATFORM_NAME" == "darwin" ]; then - FILE_NAME="rrelayer_${PLATFORM_NAME}-${ARCH}.tar.gz" - tar -czvf "$FILE_NAME" -C "$STAGING_DIR" . - else - FILE_NAME="rrelayer_${PLATFORM_NAME}-${ARCH}.zip" - (cd "$STAGING_DIR" && 7z a -tzip "${{ github.workspace }}/$FILE_NAME" .) - fi - - echo "Created archive: $FILE_NAME" - echo "file_name=$FILE_NAME" >> $GITHUB_OUTPUT - - - name: Upload artifact - if: steps.check_release.outputs.is_release == 'true' - uses: actions/upload-artifact@v4 - with: - name: release-${{ matrix.platform }}-${{ matrix.arch }} - path: ${{ steps.artifacts.outputs.file_name }} - release: name: Create GitHub Release runs-on: ubuntu-22.04 - needs: release_build + needs: [fmt, clippy, test] if: | - github.actor != 'github-actions[bot]' && - github.ref == 'refs/heads/master' && + github.actor != 'github-actions[bot]' && + github.ref == 'refs/heads/master' && github.event_name == 'push' - outputs: - version: ${{ steps.check_release.outputs.version }} - is_release: ${{ steps.check_release.outputs.is_release }} steps: - - name: Checkout repository - uses: actions/checkout@v4 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Check if this is a release commit and extract version + - name: Check if this is a release commit id: check_release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - shell: bash run: | - echo "=== DEBUG: Checking recent commits ===" - git log --oneline -10 --pretty=format:"%H %s" - echo "" - echo "=== DEBUG: Checking for release pattern ===" - - # Check the most recent commit (HEAD) for release pattern - RECENT_COMMIT_MSG=$(git log --oneline -1 --pretty=format:"%s") - echo "Most recent commit: '$RECENT_COMMIT_MSG'" - - # Try to extract version from Release v pattern (including PR number) - VERSION_FROM_RELEASE=$(echo "$RECENT_COMMIT_MSG" | grep -o 'Release v[0-9]*\.[0-9]*\.[0-9]*' | sed 's/Release v//' || echo "") - echo "VERSION_FROM_RELEASE: '$VERSION_FROM_RELEASE'" - - if [[ -n "$VERSION_FROM_RELEASE" ]]; then - echo "VERSION_NAME=$VERSION_FROM_RELEASE" >> $GITHUB_ENV + COMMIT_MSG=$(git log --oneline -1 --pretty=format:"%s") + VERSION=$(echo "$COMMIT_MSG" | grep -o 'Release v[0-9]*\.[0-9]*\.[0-9]*' | sed 's/Release v//' || echo "") + if [[ -n "$VERSION" ]]; then + echo "version=$VERSION" >> $GITHUB_OUTPUT echo "is_release=true" >> $GITHUB_OUTPUT - echo "version=$VERSION_FROM_RELEASE" >> $GITHUB_OUTPUT - echo "Found release from commit title: Release v$VERSION_FROM_RELEASE" else - echo "Not a release commit" echo "is_release=false" >> $GITHUB_OUTPUT - echo "version=" >> $GITHUB_OUTPUT fi - - name: Skip if not a release - if: steps.check_release.outputs.is_release != 'true' - run: | - echo "Skipping release creation - not a release commit" - exit 0 - - - name: Download release artifacts - if: steps.check_release.outputs.is_release == 'true' - uses: actions/download-artifact@v4 - with: - path: ./release-artifacts - - - name: Display structure of downloaded files - if: steps.check_release.outputs.is_release == 'true' - run: ls -la ./release-artifacts/ - - name: Create GitHub Release - if: steps.check_release.outputs.is_release == 'true' - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: v${{ env.VERSION_NAME }} - release_name: Release v${{ env.VERSION_NAME }} - draft: false - prerelease: false - body: | - Release v${{ env.VERSION_NAME }} - - ## Installation - ```bash - # Latest version - curl -sSfL https://docs.rrelayer.com/install.sh | bash - - # Specific version - curl -sSfL https://docs.rrelayer.com/install.sh | bash -s -- --version ${{ env.VERSION_NAME }} - ``` - continue-on-error: true - - - name: Upload Release Assets if: steps.check_release.outputs.is_release == 'true' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - for platform_dir in ./release-artifacts/*/; do - for file in "$platform_dir"*; do - if [[ -f "$file" ]]; then - filename=$(basename "$file") - echo "Uploading $filename" - gh release upload v${{ env.VERSION_NAME }} "$file" --clobber - fi - done - done - - docker: - name: Build and Publish Docker Image - needs: release - if: | - github.actor != 'github-actions[bot]' && - github.ref == 'refs/heads/master' && - github.event_name == 'push' - uses: ./.github/workflows/docker.yml - with: - version: ${{ needs.release.outputs.version }} - secrets: inherit + VERSION="${{ steps.check_release.outputs.version }}" + gh release create "v${VERSION}" \ + --title "Release v${VERSION}" \ + --generate-notes diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 9ee58659..ba379f5e 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -4,13 +4,7 @@ on: workflow_dispatch: inputs: version: - description: 'Release version (e.g., 1.2.3) - leave empty for master build' - required: false - type: string - workflow_call: - inputs: - version: - description: 'Release version (e.g., 1.2.3)' + description: 'Release version (e.g., 1.2.3) - leave empty for edge build' required: false type: string release: @@ -28,19 +22,13 @@ jobs: contents: read packages: write steps: - - name: Checkout repository - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Create docker-binary directory - run: mkdir -p docker-binary - - - name: Set up Rust toolchain - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@stable with: targets: x86_64-unknown-linux-gnu - - name: Cache Rust dependencies - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@v2 with: key: x86_64-unknown-linux-gnu-docker @@ -53,8 +41,7 @@ jobs: working-directory: crates/cli env: RUSTFLAGS: '-C target-cpu=x86-64-v2' - run: | - cargo build --release --target x86_64-unknown-linux-gnu --features jemalloc + run: cargo build --release --target x86_64-unknown-linux-gnu --features jemalloc - name: Prepare binary for Docker run: | @@ -62,23 +49,22 @@ jobs: cp target/x86_64-unknown-linux-gnu/release/rrelayer_cli docker-binary/rrelayer_cli chmod +x docker-binary/rrelayer_cli - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + - uses: docker/setup-buildx-action@v3 - - name: Log into registry ${{ env.REGISTRY }} + - name: Log into registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Generate SHA-based internal tag + - name: Generate tag id: tags run: | SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7) echo "tag=${{ env.IMAGE }}:sha-${SHORT_SHA}-amd64" >> $GITHUB_OUTPUT - - name: Build and push AMD64 Docker image + - name: Build and push AMD64 image uses: docker/build-push-action@v6 with: context: . @@ -88,8 +74,8 @@ jobs: platforms: linux/amd64 provenance: mode=max sbom: true - cache-from: type=gha - cache-to: type=gha,mode=max + cache-from: type=gha,scope=docker-amd64 + cache-to: type=gha,mode=max,scope=docker-amd64 build-arm64: name: Build ARM64 Docker Image @@ -98,19 +84,13 @@ jobs: contents: read packages: write steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Create docker-binary directory - run: mkdir -p docker-binary + - uses: actions/checkout@v4 - - name: Set up Rust toolchain - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@stable with: targets: aarch64-unknown-linux-gnu - - name: Cache Rust dependencies - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@v2 with: key: aarch64-unknown-linux-gnu-docker @@ -123,8 +103,7 @@ jobs: working-directory: crates/cli env: RUSTFLAGS: '-C target-cpu=neoverse-n1' - run: | - cargo build --release --target aarch64-unknown-linux-gnu + run: cargo build --release --target aarch64-unknown-linux-gnu - name: Prepare binary for Docker run: | @@ -132,23 +111,22 @@ jobs: cp target/aarch64-unknown-linux-gnu/release/rrelayer_cli docker-binary/rrelayer_cli chmod +x docker-binary/rrelayer_cli - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + - uses: docker/setup-buildx-action@v3 - - name: Log into registry ${{ env.REGISTRY }} + - name: Log into registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Generate SHA-based internal tag + - name: Generate tag id: tags run: | SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7) echo "tag=${{ env.IMAGE }}:sha-${SHORT_SHA}-arm64" >> $GITHUB_OUTPUT - - name: Build and push ARM64 Docker image + - name: Build and push ARM64 image uses: docker/build-push-action@v6 with: context: . @@ -158,8 +136,8 @@ jobs: platforms: linux/arm64 provenance: mode=max sbom: true - cache-from: type=gha - cache-to: type=gha,mode=max + cache-from: type=gha,scope=docker-arm64 + cache-to: type=gha,mode=max,scope=docker-arm64 create-manifest: name: Create Multi-Arch Manifest @@ -169,49 +147,37 @@ jobs: contents: read packages: write steps: - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + - uses: docker/setup-buildx-action@v3 - - name: Log into registry ${{ env.REGISTRY }} + - name: Log into registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Determine version + id: version + run: | + if [ "${{ github.event_name }}" = "release" ]; then + VERSION="${{ github.event.release.tag_name }}" + echo "version=${VERSION#v}" >> $GITHUB_OUTPUT + elif [ -n "${{ inputs.version }}" ]; then + echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT + else + echo "version=" >> $GITHUB_OUTPUT + fi + - name: Create and push multi-arch manifest run: | SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7) - AMD64_IMAGE="${{ env.IMAGE }}:sha-${SHORT_SHA}-amd64" - ARM64_IMAGE="${{ env.IMAGE }}:sha-${SHORT_SHA}-arm64" - - echo "Source images:" - echo " AMD64: $AMD64_IMAGE" - echo " ARM64: $ARM64_IMAGE" - - VERSION="${{ inputs.version }}" - + AMD64="${{ env.IMAGE }}:sha-${SHORT_SHA}-amd64" + ARM64="${{ env.IMAGE }}:sha-${SHORT_SHA}-arm64" + VERSION="${{ steps.version.outputs.version }}" + if [ -n "$VERSION" ]; then - # Release build: create version tag and latest tag - echo "Creating release manifests for version: $VERSION" - - # Version tag (e.g., 1.2.3) - echo "Creating manifest for tag: $VERSION" - docker buildx imagetools create -t "${{ env.IMAGE }}:$VERSION" \ - "$AMD64_IMAGE" \ - "$ARM64_IMAGE" - - # Latest tag - echo "Creating manifest for tag: latest" - docker buildx imagetools create -t "${{ env.IMAGE }}:latest" \ - "$AMD64_IMAGE" \ - "$ARM64_IMAGE" + docker buildx imagetools create -t "${{ env.IMAGE }}:${VERSION}" "$AMD64" "$ARM64" + docker buildx imagetools create -t "${{ env.IMAGE }}:latest" "$AMD64" "$ARM64" else - # Non-release build (master): create master tag only - echo "Creating master manifest" - docker buildx imagetools create -t "${{ env.IMAGE }}:master" \ - "$AMD64_IMAGE" \ - "$ARM64_IMAGE" + docker buildx imagetools create -t "${{ env.IMAGE }}:edge" "$AMD64" "$ARM64" fi - - echo "Manifests created successfully" diff --git a/Cargo.toml b/Cargo.toml index bdc8b63e..ef855207 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,11 +10,25 @@ members = [ [workspace.dependencies] serde = { version = "1", features = ["derive"] } -serde_json = "1.0" -anyhow = "1.0.98" -alloy = { version = "1.1.3", features = ["full", "signer-mnemonic", "signer-keystore", "eips", "eip712"] } -alloy-rlp = "0.3.12" -alloy-eips = "1.1.3" +serde_json = "1" +serde_yaml = "0.9" +anyhow = "1" +thiserror = "2.0" +alloy = { version = "1.7", features = ["full", "signer-mnemonic", "signer-keystore", "eips", "eip712"] } +reqwest = { version = "0.13.2", features = ["json", "query"] } +dotenvy = "0.15.7" +tokio = { version = "1", features = ["full"] } +tokio-postgres = { version = "0.7", features = ["with-uuid-1", "with-chrono-0_4", "with-serde_json-1"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +async-trait = "0.1" +chrono = "0.4" +hex = "0.4" +base64 = "0.22" +rand = "0.10" +hmac = "0.12" +sha2 = "0.10" +regex = "1" [profile.release] lto = "fat" diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index da537f45..604c2525 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -11,26 +11,24 @@ rrelayer = { path = "../sdk" } # external dependencies clap = { version = "4.4.11", features = ["derive"] } dialoguer = "0.11" -regex = "1.5.4" +regex = { workspace = true } colored = "2.0" -tokio = "1.35.1" -serde = { version = "1.0.204", features = ["derive"] } -serde_json = "1.0.120" -serde_yaml = "0.9.34" -thiserror = "1.0" +tokio = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +serde_yaml = { workspace = true } +thiserror = { workspace = true } prettytable = "0.10.0" -chrono = "0.4.41" -hex = "0.4.3" +chrono = { workspace = true } +hex = { workspace = true } rpassword = "7.3" alloy = { workspace = true, features = ["full", "signer-mnemonic", "eips", "eip712", "signer-keystore"] } -alloy-rlp = { workspace = true } -alloy-eips = { workspace = true } # build tikv-jemalloc-ctl = { version = "0.6.0", optional = true } tikv-jemallocator = { version = "0.6.0", optional = true } -rand = "0.8.5" +rand = { workspace = true } [features] -jemalloc = ["dep:tikv-jemallocator", "dep:tikv-jemalloc-ctl"] +jemalloc = ["dep:tikv-jemallocator", "dep:tikv-jemalloc-ctl"] \ No newline at end of file diff --git a/crates/cli/src/commands/auth.rs b/crates/cli/src/commands/auth.rs index cfed4623..4e076a92 100644 --- a/crates/cli/src/commands/auth.rs +++ b/crates/cli/src/commands/auth.rs @@ -3,8 +3,7 @@ use crate::credentials::{self}; use crate::error::CliError; use clap::Subcommand; // use dialoguer::{Input, Password}; -use rand::Rng; -use rand::distributions::Alphanumeric; +use rand::distr::{Alphanumeric, SampleString}; #[derive(Subcommand)] pub enum AuthCommand { @@ -46,11 +45,7 @@ pub async fn handle_auth_command(cmd: &AuthCommand) -> Result<(), CliError> { list_profiles().await?; } AuthCommand::GenApiKey => { - let key = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(8) - .map(char::from) - .collect::(); + let key = Alphanumeric.sample_string(&mut rand::rng(), 8); println!( "API key generated - note you have to config it in the networks yaml - {}", key diff --git a/crates/cli/src/commands/new.rs b/crates/cli/src/commands/new.rs index 668f1d6c..89b5ea9d 100644 --- a/crates/cli/src/commands/new.rs +++ b/crates/cli/src/commands/new.rs @@ -3,8 +3,7 @@ use std::{fs, path::Path}; use crate::project_location::ProjectLocation; use crate::{commands::error::InitError, print_error_message, print_success_message}; use dialoguer::{Confirm, Input}; -use rand::Rng; -use rand::distributions::Alphanumeric; +use rand::distr::{Alphanumeric, SampleString}; use rrelayer_core::network::ChainId; use rrelayer_core::{ ApiConfig, NetworkSetupConfig, RawSigningProviderConfig, SetupConfig, SigningProvider, @@ -25,11 +24,9 @@ fn write_gitignore(path: &Path) -> Result<(), WriteFileError> { } fn generate_random_credentials() -> (String, String) { - let username: String = - rand::thread_rng().sample_iter(&Alphanumeric).take(8).map(char::from).collect(); - - let password: String = - rand::thread_rng().sample_iter(&Alphanumeric).take(16).map(char::from).collect(); + let mut rng = rand::rng(); + let username = Alphanumeric.sample_string(&mut rng, 8); + let password = Alphanumeric.sample_string(&mut rng, 16); (format!("development_username_{}", username), format!("development_password_{}", password)) } diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 094b2b25..15164dee 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -18,50 +18,56 @@ include = [ ] [dependencies] -tokio-postgres = { version = "0.7", features = ["with-uuid-1", "with-chrono-0_4", "with-serde_json-1"] } -tokio = { version = "1", features = ["full"] } -axum = "0.7" -tower-http = { version = "0.5", features = ["cors"] } -dotenv = "0.15.0" +tokio-postgres = { workspace = true } +tokio = { workspace = true } +axum = "0.8" +tower-http = { version = "0.6", features = ["cors"] } +dotenvy = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -serde_yaml = "0.9.30" -async-trait = "0.1" -reqwest = { version = "0.12", features = ["json"] } +serde_yaml = { workspace = true } +async-trait = { workspace = true } +reqwest = { workspace = true } alloy = { workspace = true, features = ["full", "signer-mnemonic", "eips", "eip712", "signer-keystore", "signer-aws", "json-rpc"] } -alloy-rlp = { workspace = true } -alloy-eips = { workspace = true } uuid = { version = "1.0", features = ["v4", "serde"] } -rand = "0.8" -aws-sdk-secretsmanager = "1.14.0" -aws-sdk-kms = "1.14.0" -aws-sdk-sts = "1.14.0" -aws-config = "1.1.4" -base64 = "0.22.0" -chrono = "0.4.19" +rand = { workspace = true } +rand_core = "0.6" +aws-sdk-secretsmanager = { version = "1.14.0", optional = true } +aws-sdk-kms = { version = "1.14.0", optional = true } +aws-sdk-sts = { version = "1.14.0", optional = true } +aws-config = { version = "1.1.4", optional = true } +base64 = { workspace = true } +chrono = { workspace = true } bytes = "1.5.0" -regex = "1.5.4" -thiserror = "1.0" -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "time"] } +regex = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["fmt", "time"] } once_cell = "1.20.2" native-tls = "0.2.12" -bb8 = "0.8.3" -bb8-postgres = "0.8.1" +bb8 = "0.9" +bb8-postgres = "0.9" postgres-native-tls = "0.5" -hex = "0.4.3" -anyhow = "1.0.98" +hex = { workspace = true } +anyhow = { workspace = true } rust_decimal = { version = "1.35.0", features = ["db-tokio-postgres"] } -google-secretmanager1 = "6.0" -hyper = "0.14" -hyper-rustls = "0.24" +google-secretmanager1 = { version = "7.0", features = ["yup-oauth2-service-account"], optional = true } +yup-oauth2 = { version = "12", default-features = false, features = ["service-account", "hyper-rustls", "ring"], optional = true } +hyper-rustls = "0.27" rustls = { version = "0.23.35", features = ["ring"]} -hmac = "0.12" -sha2 = "0.10" -p256 = { version = "0.13", features = ["ecdsa"] } +sha2 = { workspace = true, optional = true } +p256 = { version = "0.13", features = ["ecdsa"], optional = true } tower = "0.5.2" subtle = "2.6.1" -cryptoki = "0.10" -secrecy = "0.8" -jsonwebtoken = "9.3" -rsa = "0.9" +cryptoki = { version = "0.12", optional = true } +secrecy = { version = "0.10", optional = true } +jsonwebtoken = { version = "10", optional = true } + +[features] +default = ["gcp", "aws", "privy", "turnkey", "pkcs11", "fireblocks"] +gcp = ["dep:google-secretmanager1", "dep:yup-oauth2"] +aws = ["dep:aws-sdk-kms", "dep:aws-sdk-sts", "dep:aws-sdk-secretsmanager", "dep:aws-config", "alloy/signer-aws"] +privy = [] +turnkey = ["dep:p256"] +pkcs11 = ["dep:cryptoki", "dep:secrecy"] +fireblocks = ["dep:jsonwebtoken", "dep:sha2"] diff --git a/crates/core/src/authentication/basic_auth.rs b/crates/core/src/authentication/basic_auth.rs index c9811ae8..5b5ab7ca 100644 --- a/crates/core/src/authentication/basic_auth.rs +++ b/crates/core/src/authentication/basic_auth.rs @@ -5,7 +5,6 @@ use axum::extract::Request; use axum::middleware::Next; use axum::response::Response; use axum::{ - async_trait, extract::FromRequestParts, http::{request::Parts, HeaderMap, StatusCode}, }; @@ -89,7 +88,6 @@ impl BasicAuthCredentials { pub struct Authenticated; /// Basic auth extractor that validates server-wide credentials -#[async_trait] impl FromRequestParts for Authenticated where S: Send + Sync, diff --git a/crates/core/src/environment.rs b/crates/core/src/environment.rs index 74504b45..f169b966 100644 --- a/crates/core/src/environment.rs +++ b/crates/core/src/environment.rs @@ -1,6 +1,6 @@ use std::path::Path; -use dotenv::{dotenv, from_path}; +use dotenvy::{dotenv, from_path}; pub fn load_env_from_project_path(project_path: &Path) { if from_path(project_path.join(".env")).is_err() { diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 4389fb49..88bc06b9 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -14,10 +14,11 @@ pub use provider::create_retry_client; pub mod relayer; pub mod safe_proxy; pub use safe_proxy::{SafeProxyError, SafeProxyManager, SafeTransaction}; +#[cfg(feature = "aws")] +pub use yaml::AwsKmsSigningProviderConfig; pub use yaml::{ - read, ApiConfig, AwsKmsSigningProviderConfig, GasProviders, NetworkSetupConfig, - RateLimitConfig, RateLimitWithInterval, RawSigningProviderConfig, SafeProxyConfig, SetupConfig, - SigningProvider, UserRateLimitConfig, + read, ApiConfig, GasProviders, NetworkSetupConfig, RateLimitConfig, RateLimitWithInterval, + RawSigningProviderConfig, SafeProxyConfig, SetupConfig, SigningProvider, UserRateLimitConfig, }; mod shared; pub use shared::{common_types, utils::get_chain_id}; @@ -30,11 +31,13 @@ mod schema; pub mod signing; pub mod transaction; mod wallet; -pub use wallet::{generate_seed_phrase, AwsKmsWalletManager, WalletError}; +#[cfg(feature = "aws")] +pub use wallet::AwsKmsWalletManager; +pub use wallet::{generate_seed_phrase, WalletError}; mod background_tasks; mod rate_limiting; pub use rate_limiting::RATE_LIMIT_HEADER_NAME; -mod webhooks; +pub mod webhooks; mod yaml; pub use docker::generate_docker_file; diff --git a/crates/core/src/middleware/authenticated.rs b/crates/core/src/middleware/authenticated.rs index e8558114..7aaed35d 100644 --- a/crates/core/src/middleware/authenticated.rs +++ b/crates/core/src/middleware/authenticated.rs @@ -1,5 +1,4 @@ use axum::{ - async_trait, extract::FromRequestParts, http::{request::Parts, StatusCode}, }; @@ -20,7 +19,6 @@ use tracing::error; #[allow(dead_code)] pub struct Authenticated; -#[async_trait] impl FromRequestParts for Authenticated where S: Send + Sync, diff --git a/crates/core/src/network/api/mod.rs b/crates/core/src/network/api/mod.rs index a7038dfe..bbd4b3b2 100644 --- a/crates/core/src/network/api/mod.rs +++ b/crates/core/src/network/api/mod.rs @@ -11,6 +11,6 @@ mod networks; pub fn create_network_routes() -> Router> { Router::new() .route("/", get(networks::networks)) - .route("/:chain_id", get(network::network)) - .route("/gas/price/:chain_id", get(get_gas_price::get_gas_price)) + .route("/{chain_id}", get(network::network)) + .route("/gas/price/{chain_id}", get(get_gas_price::get_gas_price)) } diff --git a/crates/core/src/postgres.rs b/crates/core/src/postgres.rs index 22c4bb1c..e5a639f1 100644 --- a/crates/core/src/postgres.rs +++ b/crates/core/src/postgres.rs @@ -4,7 +4,7 @@ use crate::rrelayer_error; use bb8::{Pool, RunError}; use bb8_postgres::PostgresConnectionManager; use bytes::Buf; -use dotenv::dotenv; +use dotenvy::dotenv; use native_tls::TlsConnector; use postgres_native_tls::MakeTlsConnector; use tokio::{task, time::timeout}; diff --git a/crates/core/src/provider/evm_provider.rs b/crates/core/src/provider/evm_provider.rs index 7036f38c..d3ec6917 100644 --- a/crates/core/src/provider/evm_provider.rs +++ b/crates/core/src/provider/evm_provider.rs @@ -1,15 +1,28 @@ use crate::gas::BLOB_GAS_PER_BLOB; use crate::provider::layer_extensions::RpcLoggingLayer; use crate::relayer::Relayer; +#[cfg(feature = "aws")] +use crate::wallet::AwsKmsWalletManager; +#[cfg(feature = "fireblocks")] +use crate::wallet::FireblocksWalletManager; +#[cfg(feature = "pkcs11")] +use crate::wallet::Pkcs11WalletManager; +#[cfg(feature = "privy")] +use crate::wallet::PrivyWalletManager; +#[cfg(feature = "turnkey")] +use crate::wallet::TurnkeyWalletManager; use crate::wallet::{ - AwsKmsWalletManager, CompositeWalletManager, FireblocksWalletManager, ImportKeyResult, - MnemonicWalletManager, Pkcs11WalletManager, PrivateKeyWalletManager, PrivyWalletManager, - TurnkeyWalletManager, WalletError, WalletManagerTrait, -}; -use crate::yaml::{ - AwsKmsSigningProviderConfig, FireblocksSigningProviderConfig, Pkcs11SigningProviderConfig, - TurnkeySigningProviderConfig, + CompositeWalletManager, ImportKeyResult, MnemonicWalletManager, PrivateKeyWalletManager, + WalletError, WalletManagerTrait, }; +#[cfg(feature = "aws")] +use crate::yaml::AwsKmsSigningProviderConfig; +#[cfg(feature = "fireblocks")] +use crate::yaml::FireblocksSigningProviderConfig; +#[cfg(feature = "pkcs11")] +use crate::yaml::Pkcs11SigningProviderConfig; +#[cfg(feature = "turnkey")] +use crate::yaml::TurnkeySigningProviderConfig; use crate::{ gas::{ BaseGasFeeEstimator, BlobGasEstimatorResult, BlobGasPriceResult, GasEstimatorError, @@ -21,6 +34,7 @@ use crate::{ NetworkSetupConfig, }; use alloy::consensus::{SignableTransaction, TxEnvelope}; +use alloy::eips::eip2718::Encodable2718; use alloy::network::{AnyNetwork, AnyTransactionReceipt}; use alloy::rpc::client::RpcClient; use alloy::rpc::types::serde_helpers::WithOtherFields; @@ -40,8 +54,7 @@ use alloy::{ RpcError, TransportErrorKind, }, }; -use alloy_eips::eip2718::Encodable2718; -use rand::{thread_rng, Rng}; +use rand::RngExt; use reqwest::Url; use std::sync::Arc; use std::time::Duration; @@ -168,6 +181,7 @@ impl EvmProvider { Self::new_internal(network_setup_config, wallet_manager, gas_estimator, true).await } + #[cfg(feature = "privy")] pub async fn new_with_privy( network_setup_config: &NetworkSetupConfig, app_id: String, @@ -179,6 +193,7 @@ impl EvmProvider { Self::new_internal(network_setup_config, wallet_manager, gas_estimator, true).await } + #[cfg(feature = "aws")] pub async fn new_with_aws_kms( network_setup_config: &NetworkSetupConfig, aws_kms_config: AwsKmsSigningProviderConfig, @@ -188,6 +203,7 @@ impl EvmProvider { Self::new_internal(network_setup_config, wallet_manager, gas_estimator, true).await } + #[cfg(feature = "turnkey")] pub async fn new_with_turnkey( network_setup_config: &NetworkSetupConfig, turnkey_config: TurnkeySigningProviderConfig, @@ -207,6 +223,7 @@ impl EvmProvider { Self::new_internal(network_setup_config, wallet_manager, gas_estimator, false).await } + #[cfg(feature = "pkcs11")] pub async fn new_with_pkcs11( network_setup_config: &NetworkSetupConfig, pkcs11_config: Pkcs11SigningProviderConfig, @@ -216,6 +233,7 @@ impl EvmProvider { Self::new_internal(network_setup_config, wallet_manager, gas_estimator, true).await } + #[cfg(feature = "fireblocks")] pub async fn new_with_fireblocks( network_setup_config: &NetworkSetupConfig, fireblocks_config: FireblocksSigningProviderConfig, @@ -282,8 +300,8 @@ impl EvmProvider { } pub fn rpc_client(&self) -> Arc { - let mut rng = thread_rng(); - let index = rng.gen_range(0..self.rpc_clients.len()); + let mut rng = rand::rng(); + let index = rng.random_range(0..self.rpc_clients.len()); self.rpc_clients[index].clone() } diff --git a/crates/core/src/provider/mod.rs b/crates/core/src/provider/mod.rs index 3560b222..b7312c09 100644 --- a/crates/core/src/provider/mod.rs +++ b/crates/core/src/provider/mod.rs @@ -1,7 +1,20 @@ use std::path::Path; +use std::sync::Arc; use thiserror::Error; +use crate::wallet::get_mnemonic_from_signing_key; +#[cfg(feature = "aws")] +use crate::wallet::AwsKmsWalletManager; +#[cfg(feature = "fireblocks")] +use crate::wallet::FireblocksWalletManager; +use crate::wallet::MnemonicWalletManager; +#[cfg(feature = "pkcs11")] +use crate::wallet::Pkcs11WalletManager; +#[cfg(feature = "privy")] +use crate::wallet::PrivyWalletManager; +#[cfg(feature = "turnkey")] +use crate::wallet::TurnkeyWalletManager; use crate::{gas::get_gas_estimator, network::ChainId, SetupConfig, SigningProvider, WalletError}; mod evm_provider; @@ -9,7 +22,6 @@ mod layer_extensions; use self::evm_provider::EvmProviderNewError; use crate::gas::GasEstimatorError; -use crate::wallet::get_mnemonic_from_signing_key; pub use evm_provider::{ create_retry_client, EvmProvider, RelayerProvider, RetryClientError, SendTransactionError, }; @@ -62,158 +74,151 @@ pub async fn load_providers( .as_ref() .map(|private_keys| private_keys.iter().map(|pk| pk.raw.clone()).collect()); - // Check if we have a main signing provider (non-private-key) - let has_main_signing_provider = signing_key.privy.is_some() - || signing_key.aws_kms.is_some() - || signing_key.turnkey.is_some() - || signing_key.pkcs11.is_some() - || signing_key.fireblocks.is_some() - || signing_key.raw.is_some() - || signing_key.aws_secret_manager.is_some() - || signing_key.gcp_secret_manager.is_some(); + let has_main_signing_provider = signing_key.has_main_signing_provider(); + + let gas_estimator = get_gas_estimator(&config.provider_urls, setup_config, config).await?; // If we only have private keys and no main signing provider, use private key manager only if let Some(private_keys) = &private_key_strings { if !has_main_signing_provider { - let provider = EvmProvider::new_with_private_keys( - config, - private_keys.clone(), - get_gas_estimator(&config.provider_urls, setup_config, config).await?, - ) - .await?; + let provider = + EvmProvider::new_with_private_keys(config, private_keys.clone(), gas_estimator) + .await?; providers.push(provider); continue; } } - let provider = if let Some(privy) = &signing_key.privy { - if private_key_strings.is_some() { - // Use composite manager with privy + private keys - let privy_manager = std::sync::Arc::new( - crate::wallet::PrivyWalletManager::new( + #[allow(unused_mut)] + let mut provider: Option = None; + + #[cfg(feature = "privy")] + if provider.is_none() { + if let Some(privy) = &signing_key.privy { + provider = Some(if private_key_strings.is_some() { + let privy_manager = Arc::new( + PrivyWalletManager::new(privy.app_id.clone(), privy.app_secret.clone()) + .await?, + ); + EvmProvider::new_with_composite( + config, + privy_manager, + private_key_strings.clone(), + gas_estimator.clone(), + ) + .await? + } else { + EvmProvider::new_with_privy( + config, privy.app_id.clone(), privy.app_secret.clone(), + gas_estimator.clone(), ) - .await?, - ); - EvmProvider::new_with_composite( - config, - privy_manager, - private_key_strings, - get_gas_estimator(&config.provider_urls, setup_config, config).await?, - ) - .await? - } else { - EvmProvider::new_with_privy( - config, - privy.app_id.clone(), - privy.app_secret.clone(), - get_gas_estimator(&config.provider_urls, setup_config, config).await?, - ) - .await? + .await? + }); } - } else if let Some(aws_kms) = &signing_key.aws_kms { - if private_key_strings.is_some() { - // Use composite manager with aws_kms + private keys - let aws_manager = - std::sync::Arc::new(crate::wallet::AwsKmsWalletManager::new(aws_kms.clone())); - EvmProvider::new_with_composite( - config, - aws_manager, - private_key_strings, - get_gas_estimator(&config.provider_urls, setup_config, config).await?, - ) - .await? - } else { - EvmProvider::new_with_aws_kms( - config, - aws_kms.clone(), - get_gas_estimator(&config.provider_urls, setup_config, config).await?, - ) - .await? + } + + #[cfg(feature = "aws")] + if provider.is_none() { + if let Some(aws_kms) = &signing_key.aws_kms { + provider = Some(if private_key_strings.is_some() { + let aws_manager = Arc::new(AwsKmsWalletManager::new(aws_kms.clone())); + EvmProvider::new_with_composite( + config, + aws_manager, + private_key_strings.clone(), + gas_estimator.clone(), + ) + .await? + } else { + EvmProvider::new_with_aws_kms(config, aws_kms.clone(), gas_estimator.clone()) + .await? + }); } - } else if let Some(turnkey) = &signing_key.turnkey { - if private_key_strings.is_some() { - // Use composite manager with turnkey + private keys - let turnkey_manager = std::sync::Arc::new( - crate::wallet::TurnkeyWalletManager::new(turnkey.clone()).await?, - ); - EvmProvider::new_with_composite( - config, - turnkey_manager, - private_key_strings, - get_gas_estimator(&config.provider_urls, setup_config, config).await?, - ) - .await? - } else { - EvmProvider::new_with_turnkey( - config, - turnkey.clone(), - get_gas_estimator(&config.provider_urls, setup_config, config).await?, - ) - .await? + } + + #[cfg(feature = "turnkey")] + if provider.is_none() { + if let Some(turnkey) = &signing_key.turnkey { + provider = Some(if private_key_strings.is_some() { + let turnkey_manager = + Arc::new(TurnkeyWalletManager::new(turnkey.clone()).await?); + EvmProvider::new_with_composite( + config, + turnkey_manager, + private_key_strings.clone(), + gas_estimator.clone(), + ) + .await? + } else { + EvmProvider::new_with_turnkey(config, turnkey.clone(), gas_estimator.clone()) + .await? + }); } - } else if let Some(pkcs11) = &signing_key.pkcs11 { - if private_key_strings.is_some() { - let pkcs11_manager = - std::sync::Arc::new(crate::wallet::Pkcs11WalletManager::new(pkcs11.clone())?); - EvmProvider::new_with_composite( - config, - pkcs11_manager, - private_key_strings, - get_gas_estimator(&config.provider_urls, setup_config, config).await?, - ) - .await? - } else { - EvmProvider::new_with_pkcs11( - config, - pkcs11.clone(), - get_gas_estimator(&config.provider_urls, setup_config, config).await?, - ) - .await? + } + + #[cfg(feature = "pkcs11")] + if provider.is_none() { + if let Some(pkcs11) = &signing_key.pkcs11 { + provider = Some(if private_key_strings.is_some() { + let pkcs11_manager = Arc::new(Pkcs11WalletManager::new(pkcs11.clone())?); + EvmProvider::new_with_composite( + config, + pkcs11_manager, + private_key_strings.clone(), + gas_estimator.clone(), + ) + .await? + } else { + EvmProvider::new_with_pkcs11(config, pkcs11.clone(), gas_estimator.clone()) + .await? + }); } - } else if let Some(fireblocks) = &signing_key.fireblocks { - if private_key_strings.is_some() { - let fireblocks_manager = std::sync::Arc::new( - crate::wallet::FireblocksWalletManager::new(fireblocks.clone()).await?, - ); - EvmProvider::new_with_composite( - config, - fireblocks_manager, - private_key_strings, - get_gas_estimator(&config.provider_urls, setup_config, config).await?, - ) - .await? - } else { - EvmProvider::new_with_fireblocks( - config, - fireblocks.clone(), - get_gas_estimator(&config.provider_urls, setup_config, config).await?, - ) - .await? + } + + #[cfg(feature = "fireblocks")] + if provider.is_none() { + if let Some(fireblocks) = &signing_key.fireblocks { + provider = Some(if private_key_strings.is_some() { + let fireblocks_manager = + Arc::new(FireblocksWalletManager::new(fireblocks.clone()).await?); + EvmProvider::new_with_composite( + config, + fireblocks_manager, + private_key_strings.clone(), + gas_estimator.clone(), + ) + .await? + } else { + EvmProvider::new_with_fireblocks( + config, + fireblocks.clone(), + gas_estimator.clone(), + ) + .await? + }); } + } + + // Fallback to mnemonic-based signing (raw, aws_secret_manager, gcp_secret_manager) + let provider = if let Some(p) = provider { + p } else { let mnemonic = get_mnemonic_from_signing_key(project_path, signing_key).await?; if private_key_strings.is_some() { - // Use composite manager with mnemonic + private keys - let mnemonic_manager = - std::sync::Arc::new(crate::wallet::MnemonicWalletManager::new(&mnemonic)); + let mnemonic_manager = Arc::new(MnemonicWalletManager::new(&mnemonic)); EvmProvider::new_with_composite( config, mnemonic_manager, private_key_strings, - get_gas_estimator(&config.provider_urls, setup_config, config).await?, + gas_estimator, ) .await? } else { - EvmProvider::new_with_mnemonic( - config, - &mnemonic, - get_gas_estimator(&config.provider_urls, setup_config, config).await?, - ) - .await? + EvmProvider::new_with_mnemonic(config, &mnemonic, gas_estimator).await? } }; diff --git a/crates/core/src/relayer/api/mod.rs b/crates/core/src/relayer/api/mod.rs index b1dec1ee..9749f366 100644 --- a/crates/core/src/relayer/api/mod.rs +++ b/crates/core/src/relayer/api/mod.rs @@ -40,15 +40,15 @@ use update_relay_max_gas_price::update_relay_max_gas_price; pub fn create_relayer_routes() -> Router> { // All routes handle authentication internally via validate_allowed_passed_basic_auth + validate_auth_basic_or_api_key Router::new() - .route("/:chain_id/new", post(create_relayer)) - .route("/:chain_id/import", post(import_relayer)) + .route("/{chain_id}/new", post(create_relayer)) + .route("/{chain_id}/import", post(import_relayer)) .route("/", get(get_relayers)) - .route("/:relayer_id", get(get_relayer_api)) - .route("/:relayer_id", delete(delete_relayer)) - .route("/:relayer_id/pause", put(pause_relayer)) - .route("/:relayer_id/unpause", put(unpause_relayer)) - .route("/:relayer_id/gas/max/:cap", put(update_relay_max_gas_price)) - .route("/:relayer_id/clone", post(clone_relayer)) - .route("/:relayer_id/allowlists", get(get_allowlist_addresses)) - .route("/:relayer_id/gas/eip1559/:enabled", put(update_relay_eip1559_status)) + .route("/{relayer_id}", get(get_relayer_api)) + .route("/{relayer_id}", delete(delete_relayer)) + .route("/{relayer_id}/pause", put(pause_relayer)) + .route("/{relayer_id}/unpause", put(unpause_relayer)) + .route("/{relayer_id}/gas/max/{cap}", put(update_relay_max_gas_price)) + .route("/{relayer_id}/clone", post(clone_relayer)) + .route("/{relayer_id}/allowlists", get(get_allowlist_addresses)) + .route("/{relayer_id}/gas/eip1559/{enabled}", put(update_relay_eip1559_status)) } diff --git a/crates/core/src/shared/utils.rs b/crates/core/src/shared/utils.rs index 861b2a88..d4f81302 100644 --- a/crates/core/src/shared/utils.rs +++ b/crates/core/src/shared/utils.rs @@ -2,8 +2,8 @@ use crate::network::ChainId; use crate::shared::{bad_request, HttpError}; use crate::transaction::types::TransactionBlob; use crate::{create_retry_client, rrelayer_error}; +use alloy::eips::eip4844::Blob; use alloy::primitives::U256; -use alloy_eips::eip4844::Blob; use std::time::Duration; use tokio::time::sleep; diff --git a/crates/core/src/signing/api/mod.rs b/crates/core/src/signing/api/mod.rs index 2436aa83..64175e4c 100644 --- a/crates/core/src/signing/api/mod.rs +++ b/crates/core/src/signing/api/mod.rs @@ -18,22 +18,22 @@ pub use sign_typed_data::SignTypedDataResult; pub fn create_signing_routes() -> Router> { // All signing routes handle authentication internally via validate_allowed_passed_basic_auth + validate_auth_basic_or_api_key Router::new() - .route("/relayers/:relayer_id/message", post(sign_text::sign_text)) - .route("/relayers/:relayer_id/typed-data", post(sign_typed_data::sign_typed_data)) + .route("/relayers/{relayer_id}/message", post(sign_text::sign_text)) + .route("/relayers/{relayer_id}/typed-data", post(sign_typed_data::sign_typed_data)) .route( - "/relayers/:relayer_id/text-history", + "/relayers/{relayer_id}/text-history", get(get_signed_text_history::get_signed_text_history), ) .route( - "/relayers/:relayer_id/typed-data-history", + "/relayers/{relayer_id}/typed-data-history", get(get_signed_typed_data_history::get_signed_typed_data_history), ) // @deprecated TODO: remove in a few months - .route("/:relayer_id/message", post(sign_text::sign_text)) - .route("/:relayer_id/typed-data", post(sign_typed_data::sign_typed_data)) - .route("/:relayer_id/text-history", get(get_signed_text_history::get_signed_text_history)) + .route("/{relayer_id}/message", post(sign_text::sign_text)) + .route("/{relayer_id}/typed-data", post(sign_typed_data::sign_typed_data)) + .route("/{relayer_id}/text-history", get(get_signed_text_history::get_signed_text_history)) .route( - "/:relayer_id/typed-data-history", + "/{relayer_id}/typed-data-history", get(get_signed_typed_data_history::get_signed_typed_data_history), ) } diff --git a/crates/core/src/startup.rs b/crates/core/src/startup.rs index 1efcc8ab..0f5a6c19 100644 --- a/crates/core/src/startup.rs +++ b/crates/core/src/startup.rs @@ -37,7 +37,7 @@ use axum::{ routing::get, Json, Router, }; -use dotenv::dotenv; +use dotenvy::dotenv; use rustls::crypto::ring::default_provider; use rustls::crypto::CryptoProvider; use std::collections::HashMap; @@ -191,16 +191,10 @@ async fn start_api( }; // Check if only private keys are configured - if signing_provider.private_keys.is_some() - && signing_provider.raw.is_none() - && signing_provider.aws_secret_manager.is_none() - && signing_provider.gcp_secret_manager.is_none() - && signing_provider.privy.is_none() - && signing_provider.aws_kms.is_none() - && signing_provider.turnkey.is_none() - && signing_provider.pkcs11.is_none() - && signing_provider.fireblocks.is_none() - { + let is_private_key_only = signing_provider.private_keys.is_some() + && !signing_provider.has_main_signing_provider(); + + if is_private_key_only { Some(network_config.chain_id) } else { None diff --git a/crates/core/src/transaction/api/mod.rs b/crates/core/src/transaction/api/mod.rs index 1c9c213f..05059cbd 100644 --- a/crates/core/src/transaction/api/mod.rs +++ b/crates/core/src/transaction/api/mod.rs @@ -27,27 +27,27 @@ pub use types::TransactionSpeed; pub fn create_transactions_routes() -> Router> { // All transaction routes handle authentication internally via validate_allowed_passed_basic_auth + validate_auth_basic_or_api_key Router::new() - .route("/hash/:tx_hash", get(get_transaction_by_tx_hash::get_transaction_by_tx_hash_api)) + .route("/hash/{tx_hash}", get(get_transaction_by_tx_hash::get_transaction_by_tx_hash_api)) .route( - "/external/:external_id", + "/external/{external_id}", get(get_transaction_by_external_id::get_transaction_by_external_id_api), ) - .route("/:id", get(get_transaction_by_id::get_transaction_by_id_api)) - .route("/status/:id", get(get_transaction_status::get_transaction_status)) - .route("/relayers/:relayer_id/send", post(send_transaction::handle_send_transaction)) + .route("/{id}", get(get_transaction_by_id::get_transaction_by_id_api)) + .route("/status/{id}", get(get_transaction_status::get_transaction_status)) + .route("/relayers/{relayer_id}/send", post(send_transaction::handle_send_transaction)) .route( - "/relayers/:chain_id/send-random", + "/relayers/{chain_id}/send-random", post(send_random_transaction::send_transaction_random), ) - .route("/replace/:transaction_id", put(replace_transaction::replace_transaction)) - .route("/cancel/:transaction_id", put(cancel_transaction::cancel_transaction)) - .route("/relayers/:relayer_id", get(get_relayer_transactions::get_relayer_transactions)) + .route("/replace/{transaction_id}", put(replace_transaction::replace_transaction)) + .route("/cancel/{transaction_id}", put(cancel_transaction::cancel_transaction)) + .route("/relayers/{relayer_id}", get(get_relayer_transactions::get_relayer_transactions)) .route( - "/relayers/:relayer_id/pending/count", + "/relayers/{relayer_id}/pending/count", get(get_transactions_pending_count::get_transactions_pending_count), ) .route( - "/relayers/:relayer_id/inmempool/count", + "/relayers/{relayer_id}/inmempool/count", get(get_transactions_inmempool_count::get_transactions_inmempool_count), ) } diff --git a/crates/core/src/transaction/api/send_random_transaction.rs b/crates/core/src/transaction/api/send_random_transaction.rs index 55fc8b54..cccfcff4 100644 --- a/crates/core/src/transaction/api/send_random_transaction.rs +++ b/crates/core/src/transaction/api/send_random_transaction.rs @@ -9,7 +9,7 @@ use axum::{ http::HeaderMap, Json, }; -use rand::seq::SliceRandom; +use rand::seq::IndexedRandom; use std::sync::Arc; /// Handles random relayer selection for transaction requests @@ -44,7 +44,7 @@ async fn select_random_relayer( return Err(not_found(format!("No relayers found for chain {}", chain_id))); } - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); // TODO: it should be smart enough to also only pick the one with enough native funds to send the tx let available_relayers: Vec<_> = relayers .into_iter() diff --git a/crates/core/src/transaction/types/transaction.rs b/crates/core/src/transaction/types/transaction.rs index 96d43b45..7346f1c8 100644 --- a/crates/core/src/transaction/types/transaction.rs +++ b/crates/core/src/transaction/types/transaction.rs @@ -1,5 +1,10 @@ use std::fmt::Display; +use alloy::eips::eip4844::{ + builder::{SidecarBuilder, SimpleCoder}, + BlobTransactionSidecar, +}; +use alloy::eips::eip7594::BlobTransactionSidecarVariant; use alloy::{ consensus::{ TxEip1559, TxEip4844, TxEip4844Variant, TxEip4844WithSidecar, TxLegacy, TypedTransaction, @@ -7,11 +12,6 @@ use alloy::{ eips::eip2930::AccessList, primitives::TxKind, }; -use alloy_eips::eip4844::{ - builder::{SidecarBuilder, SimpleCoder}, - BlobTransactionSidecar, -}; -use alloy_eips::eip7594::BlobTransactionSidecarVariant; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use thiserror::Error; diff --git a/crates/core/src/transaction/types/transaction_blob.rs b/crates/core/src/transaction/types/transaction_blob.rs index 5a04aa85..c0a031a8 100644 --- a/crates/core/src/transaction/types/transaction_blob.rs +++ b/crates/core/src/transaction/types/transaction_blob.rs @@ -1,5 +1,5 @@ +use alloy::eips::eip4844::{Blob, BYTES_PER_BLOB}; use alloy::primitives::FixedBytes; -use alloy_eips::eip4844::{Blob, BYTES_PER_BLOB}; use anyhow::{anyhow, Result}; use serde::{Deserialize, Serialize}; use std::fmt::Display; diff --git a/crates/core/src/wallet/mnemonic_signing_key_providers/mod.rs b/crates/core/src/wallet/mnemonic_signing_key_providers/mod.rs index 537f83dd..29cca8fe 100644 --- a/crates/core/src/wallet/mnemonic_signing_key_providers/mod.rs +++ b/crates/core/src/wallet/mnemonic_signing_key_providers/mod.rs @@ -2,11 +2,16 @@ use crate::wallet::WalletError; use crate::SigningProvider; use std::path::Path; +#[cfg(feature = "aws")] mod aws_secret_manager; +#[cfg(feature = "aws")] use aws_secret_manager::get_aws_secret; +#[cfg(feature = "gcp")] mod gcp_secret_manager; +#[cfg(feature = "gcp")] use gcp_secret_manager::get_gcp_secret; +#[allow(unused_variables)] pub async fn get_mnemonic_from_signing_key( project_path: &Path, signing_key: &SigningProvider, @@ -15,11 +20,13 @@ pub async fn get_mnemonic_from_signing_key( return Ok(raw.mnemonic.clone()); } + #[cfg(feature = "aws")] if let Some(aws_secret_manager) = &signing_key.aws_secret_manager { let result = get_aws_secret(aws_secret_manager).await?; return Ok(result); } + #[cfg(feature = "gcp")] if let Some(gcp_secret_manager) = &signing_key.gcp_secret_manager { let result = get_gcp_secret(project_path, gcp_secret_manager).await?; return Ok(result); diff --git a/crates/core/src/wallet/mnemonic_wallet_manager.rs b/crates/core/src/wallet/mnemonic_wallet_manager.rs index 44ac1163..ab2876cd 100644 --- a/crates/core/src/wallet/mnemonic_wallet_manager.rs +++ b/crates/core/src/wallet/mnemonic_wallet_manager.rs @@ -13,7 +13,6 @@ use alloy::signers::{ Signer, }; use async_trait::async_trait; -use rand::thread_rng; use std::collections::HashMap; use tokio::sync::Mutex; @@ -139,7 +138,7 @@ impl WalletManagerTrait for MnemonicWalletManager { /// Generates a new 24-word BIP39 mnemonic seed phrase. pub fn generate_seed_phrase() -> Result { - let mut rng = thread_rng(); + let mut rng = rand_core::OsRng; let mnemonic = Mnemonic::::new_with_count(&mut rng, 24) .map_err(|e| WalletError::MnemonicError(format!("Failed to generate mnemonic: {}", e)))?; let phrase = mnemonic.to_phrase(); diff --git a/crates/core/src/wallet/mod.rs b/crates/core/src/wallet/mod.rs index 43b8162d..1f15ebf9 100644 --- a/crates/core/src/wallet/mod.rs +++ b/crates/core/src/wallet/mod.rs @@ -12,22 +12,32 @@ pub use mnemonic_wallet_manager::{generate_seed_phrase, MnemonicWalletManager}; mod mnemonic_signing_key_providers; pub use mnemonic_signing_key_providers::get_mnemonic_from_signing_key; +#[cfg(feature = "aws")] mod aws_kms_wallet_manager; +#[cfg(feature = "aws")] pub use aws_kms_wallet_manager::AwsKmsWalletManager; +#[cfg(feature = "privy")] mod privy_wallet_manager; +#[cfg(feature = "privy")] pub use privy_wallet_manager::PrivyWalletManager; +#[cfg(feature = "turnkey")] mod turnkey_wallet_manager; +#[cfg(feature = "turnkey")] pub use turnkey_wallet_manager::TurnkeyWalletManager; mod private_key_wallet_manager; pub use private_key_wallet_manager::PrivateKeyWalletManager; +#[cfg(feature = "pkcs11")] mod pkcs11_wallet_manager; +#[cfg(feature = "pkcs11")] pub use pkcs11_wallet_manager::Pkcs11WalletManager; +#[cfg(feature = "fireblocks")] mod fireblocks_wallet_manager; +#[cfg(feature = "fireblocks")] pub use fireblocks_wallet_manager::FireblocksWalletManager; mod composite_wallet_manager; @@ -56,7 +66,7 @@ pub enum WalletError { StringEncodingError(#[from] std::string::FromUtf8Error), #[error("RLP decoding error: {0}")] - RlpError(#[from] alloy_rlp::Error), + RlpError(#[from] alloy::rlp::Error), #[error("Signature parsing error: {0}")] SignatureError(#[from] alloy::primitives::SignatureError), diff --git a/crates/core/src/wallet/pkcs11_wallet_manager.rs b/crates/core/src/wallet/pkcs11_wallet_manager.rs index 65d5a164..08525c21 100644 --- a/crates/core/src/wallet/pkcs11_wallet_manager.rs +++ b/crates/core/src/wallet/pkcs11_wallet_manager.rs @@ -6,12 +6,12 @@ use alloy::consensus::{SignableTransaction, TypedTransaction}; use alloy::dyn_abi::TypedData; use alloy::primitives::{keccak256, Address, Signature, B256, U256}; use async_trait::async_trait; -use cryptoki::context::{CInitializeArgs, Pkcs11}; +use cryptoki::context::{CInitializeArgs, CInitializeFlags, Pkcs11}; use cryptoki::mechanism::Mechanism; use cryptoki::object::{Attribute, AttributeType, ObjectClass, ObjectHandle}; use cryptoki::session::{Session, UserType}; use cryptoki::slot::Slot; -use secrecy::Secret; +use cryptoki::types::AuthPin; use std::collections::HashMap; use std::path::Path; use tokio::sync::Mutex; @@ -40,7 +40,7 @@ impl Pkcs11WalletManager { WalletError::GenericSignerError(format!("Failed to load PKCS#11 library: {}", e)) })?; - match ctx.initialize(CInitializeArgs::OsThreads) { + match ctx.initialize(CInitializeArgs::new(CInitializeFlags::OS_LOCKING_OK)) { Ok(_) => debug!("PKCS#11 library initialized successfully"), Err(e) => { let error_str = e.to_string().to_lowercase(); @@ -87,7 +87,7 @@ impl Pkcs11WalletManager { })?; if let Some(pin) = &self.config.pin { - let secret_pin = Secret::new(pin.clone()); + let secret_pin = AuthPin::from(pin.clone()); match session.login(UserType::User, Some(&secret_pin)) { Ok(_) => debug!("Successfully authenticated with PKCS#11 token"), Err(e) => { diff --git a/crates/core/src/wallet/privy_wallet_manager.rs b/crates/core/src/wallet/privy_wallet_manager.rs index dd968500..e62677e9 100644 --- a/crates/core/src/wallet/privy_wallet_manager.rs +++ b/crates/core/src/wallet/privy_wallet_manager.rs @@ -3,7 +3,7 @@ use crate::wallet::{WalletError, WalletManagerChainId, WalletManagerTrait}; use alloy::consensus::{TxEnvelope, TypedTransaction}; use alloy::dyn_abi::TypedData; use alloy::primitives::Signature; -use alloy_rlp::Decodable; +use alloy::rlp::Decodable; use async_trait::async_trait; use serde::{Deserialize, Serialize}; use std::collections::HashMap; diff --git a/crates/core/src/wallet/turnkey_wallet_manager.rs b/crates/core/src/wallet/turnkey_wallet_manager.rs index a01c324c..be6f43bf 100644 --- a/crates/core/src/wallet/turnkey_wallet_manager.rs +++ b/crates/core/src/wallet/turnkey_wallet_manager.rs @@ -4,13 +4,10 @@ use crate::yaml::TurnkeySigningProviderConfig; use alloy::consensus::{TxEnvelope, TypedTransaction}; use alloy::dyn_abi::TypedData; use alloy::primitives::{keccak256, Signature}; -use alloy_rlp::Decodable; +use alloy::rlp::Decodable; use async_trait::async_trait; use base64::{engine::general_purpose, Engine as _}; -use p256::{ - ecdsa::{signature::Signer, Signature as EcdsaSignature, SigningKey}, - SecretKey, -}; +use p256::ecdsa::{signature::Signer, Signature as EcdsaSignature, SigningKey}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::str::FromStr; @@ -134,13 +131,11 @@ impl TurnkeyWalletManager { WalletError::ApiError { message: format!("Failed to decode private key: {}", e) } })?; - let secret_key = SecretKey::from_slice(&private_key_bytes).map_err(|e| { + let signing_key = SigningKey::from_slice(&private_key_bytes).map_err(|e| { error!("Failed to create secret key from bytes: {}", e); WalletError::ApiError { message: format!("Failed to create secret key: {}", e) } })?; - let signing_key = SigningKey::from(&secret_key); - let signature: EcdsaSignature = signing_key.sign(body.as_bytes()); let signature_bytes = signature.to_der(); let signature_hex = hex::encode(&signature_bytes); @@ -392,7 +387,7 @@ impl WalletManagerTrait for TurnkeyWalletManager { access_list: tx.access_list.clone(), }; - let encoded = alloy_rlp::encode(&unsigned_tx); + let encoded = alloy::rlp::encode(&unsigned_tx); format!("0x02{}", hex::encode(&encoded)) } TypedTransaction::Legacy(tx) => { @@ -406,7 +401,7 @@ impl WalletManagerTrait for TurnkeyWalletManager { input: tx.input.clone(), }; - let encoded = alloy_rlp::encode(&unsigned_tx); + let encoded = alloy::rlp::encode(&unsigned_tx); format!("0x{}", hex::encode(&encoded)) } TypedTransaction::Eip2930(tx) => { @@ -421,7 +416,7 @@ impl WalletManagerTrait for TurnkeyWalletManager { access_list: tx.access_list.clone(), }; - let encoded = alloy_rlp::encode(&unsigned_tx); + let encoded = alloy::rlp::encode(&unsigned_tx); format!("0x01{}", hex::encode(&encoded)) } TypedTransaction::Eip4844(tx_variant) => { @@ -448,7 +443,7 @@ impl WalletManagerTrait for TurnkeyWalletManager { max_fee_per_blob_gas: tx.max_fee_per_blob_gas, }; - let encoded = alloy_rlp::encode(&unsigned_tx); + let encoded = alloy::rlp::encode(&unsigned_tx); format!("0x03{}", hex::encode(&encoded)) } _ => { diff --git a/crates/core/src/webhooks/mod.rs b/crates/core/src/webhooks/mod.rs index c3330ccb..af39ded1 100644 --- a/crates/core/src/webhooks/mod.rs +++ b/crates/core/src/webhooks/mod.rs @@ -5,6 +5,10 @@ pub use manager::WebhookManager; mod low_balance_payload; mod payload; -pub use low_balance_payload::WebhookLowBalancePayload; +pub use low_balance_payload::{WebhookBalanceAlertData, WebhookLowBalancePayload}; +pub use payload::{ + WebhookPayload, WebhookSigningData, WebhookSigningPayload, WebhookTransactionData, +}; mod sender; mod types; +pub use types::WebhookEventType; diff --git a/crates/core/src/yaml.rs b/crates/core/src/yaml.rs index 5613a514..93195473 100644 --- a/crates/core/src/yaml.rs +++ b/crates/core/src/yaml.rs @@ -16,6 +16,7 @@ use crate::shared::utils::format_token_amount; use crate::transaction::types::TransactionSpeed; use crate::{rrelayer_error, shared::common_types::EvmAddress}; +#[cfg(feature = "gcp")] #[derive(Debug, Serialize, Deserialize, Clone)] pub struct GcpSecretManagerProviderConfig { pub id: String, @@ -25,6 +26,7 @@ pub struct GcpSecretManagerProviderConfig { pub service_account_key_path: String, } +#[cfg(feature = "aws")] #[derive(Debug, Serialize, Deserialize, Clone)] pub struct AwsSecretManagerProviderConfig { pub id: String, @@ -32,6 +34,7 @@ pub struct AwsSecretManagerProviderConfig { pub region: String, } +#[cfg(feature = "aws")] #[derive(Debug, Serialize, Deserialize, Clone)] pub struct AwsKmsSigningProviderConfig { pub region: String, @@ -47,12 +50,14 @@ pub struct RawSigningProviderConfig { pub mnemonic: String, } +#[cfg(feature = "privy")] #[derive(Debug, Serialize, Deserialize, Clone)] pub struct PrivySigningProviderConfig { pub app_id: String, pub app_secret: String, } +#[cfg(feature = "turnkey")] #[derive(Debug, Serialize, Deserialize, Clone)] pub struct TurnkeySigningProviderConfig { pub api_public_key: String, @@ -61,6 +66,7 @@ pub struct TurnkeySigningProviderConfig { pub wallet_id: String, } +#[cfg(feature = "pkcs11")] #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Pkcs11SigningProviderConfig { pub library_path: String, @@ -75,6 +81,7 @@ pub struct Pkcs11SigningProviderConfig { pub test_mode: Option, } +#[cfg(feature = "fireblocks")] #[derive(Debug, Serialize, Deserialize, Clone)] pub struct FireblocksSigningProviderConfig { pub api_key: String, @@ -92,29 +99,36 @@ pub struct PrivateKeyConfig { pub raw: String, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct SigningProvider { #[serde(skip_serializing_if = "Option::is_none", default)] pub raw: Option, + #[cfg(feature = "aws")] #[serde(skip_serializing_if = "Option::is_none", default)] pub aws_secret_manager: Option, + #[cfg(feature = "gcp")] #[serde(skip_serializing_if = "Option::is_none", default)] pub gcp_secret_manager: Option, + #[cfg(feature = "privy")] #[serde(skip_serializing_if = "Option::is_none", default)] pub privy: Option, + #[cfg(feature = "aws")] #[serde(skip_serializing_if = "Option::is_none", default)] pub aws_kms: Option, + #[cfg(feature = "turnkey")] #[serde(skip_serializing_if = "Option::is_none", default)] pub turnkey: Option, + #[cfg(feature = "pkcs11")] #[serde(skip_serializing_if = "Option::is_none", default)] pub pkcs11: Option, + #[cfg(feature = "fireblocks")] #[serde(skip_serializing_if = "Option::is_none", default)] pub fireblocks: Option, @@ -147,6 +161,7 @@ pub struct RateLimitConfig { pub fallback_to_relayer: bool, } +#[cfg(feature = "aws")] impl AwsKmsSigningProviderConfig { pub fn validate(&self) -> Result<(), String> { if self.region.is_empty() { @@ -156,6 +171,7 @@ impl AwsKmsSigningProviderConfig { } } +#[cfg(feature = "turnkey")] impl TurnkeySigningProviderConfig { pub fn validate(&self) -> Result<(), String> { if self.api_public_key.is_empty() { @@ -174,6 +190,7 @@ impl TurnkeySigningProviderConfig { } } +#[cfg(feature = "fireblocks")] impl FireblocksSigningProviderConfig { pub fn validate(&self) -> Result<(), String> { if self.api_key.is_empty() { @@ -226,8 +243,23 @@ impl FireblocksSigningProviderConfig { Ok(()) } + + /// Returns the appropriate base URL based on sandbox setting + pub fn get_base_url(&self) -> String { + if self.sandbox.unwrap_or(false) { + "https://sandbox-api.fireblocks.io".to_string() + } else { + "https://api.fireblocks.io".to_string() + } + } + + /// Returns true if running in sandbox mode + pub fn is_sandbox(&self) -> bool { + self.sandbox.unwrap_or(false) + } } +#[cfg(feature = "pkcs11")] impl Pkcs11SigningProviderConfig { pub fn validate(&self) -> Result<(), String> { if self.library_path.is_empty() { @@ -257,112 +289,113 @@ impl Pkcs11SigningProviderConfig { } } -impl FireblocksSigningProviderConfig { - /// Returns the appropriate base URL based on sandbox setting - pub fn get_base_url(&self) -> String { - if self.sandbox.unwrap_or(false) { - // Use sandbox environment - "https://sandbox-api.fireblocks.io".to_string() - } else { - // Use production environment (default) - "https://api.fireblocks.io".to_string() - } - } - - /// Returns true if running in sandbox mode - pub fn is_sandbox(&self) -> bool { - self.sandbox.unwrap_or(false) - } -} - impl SigningProvider { pub fn from_raw(raw: RawSigningProviderConfig) -> Self { - Self { - raw: Some(raw), - aws_secret_manager: None, - gcp_secret_manager: None, - privy: None, - aws_kms: None, - turnkey: None, - pkcs11: None, - fireblocks: None, - private_keys: None, - } + Self { raw: Some(raw), ..Default::default() } } + #[cfg(feature = "aws")] pub fn from_aws_kms(aws_kms: AwsKmsSigningProviderConfig) -> Self { - Self { - raw: None, - aws_secret_manager: None, - gcp_secret_manager: None, - privy: None, - aws_kms: Some(aws_kms), - turnkey: None, - pkcs11: None, - fireblocks: None, - private_keys: None, - } + Self { aws_kms: Some(aws_kms), ..Default::default() } } + #[cfg(feature = "turnkey")] pub fn from_turnkey(turnkey: TurnkeySigningProviderConfig) -> Self { - Self { - raw: None, - aws_secret_manager: None, - gcp_secret_manager: None, - privy: None, - aws_kms: None, - turnkey: Some(turnkey), - pkcs11: None, - fireblocks: None, - private_keys: None, - } + Self { turnkey: Some(turnkey), ..Default::default() } } + #[cfg(feature = "pkcs11")] pub fn from_pkcs11(pkcs11: Pkcs11SigningProviderConfig) -> Self { - Self { - raw: None, - aws_secret_manager: None, - gcp_secret_manager: None, - privy: None, - aws_kms: None, - turnkey: None, - pkcs11: Some(pkcs11), - fireblocks: None, - private_keys: None, - } + Self { pkcs11: Some(pkcs11), ..Default::default() } } + #[cfg(feature = "fireblocks")] pub fn from_fireblocks(fireblocks: FireblocksSigningProviderConfig) -> Self { - Self { - raw: None, - aws_secret_manager: None, - gcp_secret_manager: None, - privy: None, - aws_kms: None, - turnkey: None, - pkcs11: None, - fireblocks: Some(fireblocks), - private_keys: None, + Self { fireblocks: Some(fireblocks), ..Default::default() } + } + + /// Returns true if any main signing provider (non-private-key) is configured. + pub fn has_main_signing_provider(&self) -> bool { + #[allow(unused_mut)] + let mut result = self.raw.is_some(); + + #[cfg(feature = "aws")] + { + result = result || self.aws_kms.is_some() || self.aws_secret_manager.is_some(); + } + + #[cfg(feature = "gcp")] + if self.gcp_secret_manager.is_some() { + result = true; + } + + #[cfg(feature = "privy")] + if self.privy.is_some() { + result = true; + } + + #[cfg(feature = "turnkey")] + if self.turnkey.is_some() { + result = true; + } + + #[cfg(feature = "pkcs11")] + if self.pkcs11.is_some() { + result = true; } + + #[cfg(feature = "fireblocks")] + if self.fireblocks.is_some() { + result = true; + } + + result } -} -impl SigningProvider { pub fn validate(&self) -> Result<(), String> { - let configured_methods = [ - self.raw.is_some(), - self.aws_secret_manager.is_some(), - self.gcp_secret_manager.is_some(), - self.privy.is_some(), - self.aws_kms.is_some(), - self.turnkey.is_some(), - self.pkcs11.is_some(), - self.fireblocks.is_some(), - self.private_keys.is_some(), - ] - .iter() - .filter(|&&x| x) - .count(); + let mut configured_methods = 0usize; + + if self.raw.is_some() { + configured_methods += 1; + } + if self.private_keys.is_some() { + configured_methods += 1; + } + + #[cfg(feature = "aws")] + { + if self.aws_secret_manager.is_some() { + configured_methods += 1; + } + if self.aws_kms.is_some() { + configured_methods += 1; + } + } + + #[cfg(feature = "gcp")] + if self.gcp_secret_manager.is_some() { + configured_methods += 1; + } + + #[cfg(feature = "privy")] + if self.privy.is_some() { + configured_methods += 1; + } + + #[cfg(feature = "turnkey")] + if self.turnkey.is_some() { + configured_methods += 1; + } + + #[cfg(feature = "pkcs11")] + if self.pkcs11.is_some() { + configured_methods += 1; + } + + #[cfg(feature = "fireblocks")] + if self.fireblocks.is_some() { + configured_methods += 1; + } match configured_methods { 0 => Err("Signing key is not set".to_string()), @@ -999,18 +1032,22 @@ pub fn read(file_path: &PathBuf, raw_yaml: bool) -> Result | null; +} +``` + +## Signing Webhook Payload + +```ts +import { WebhookSigningPayload, WebhookSigningData } from 'rrelayer'; + +export interface WebhookSigningData { + relayerId: string; + chainId: number; + signature: string; + signedAt: string; + message?: string | null; + domainData?: Record | null; + messageData?: Record | null; + primaryType?: string | null; +} + +export interface WebhookSigningPayload { + eventType: WebhookEventType; + signing: WebhookSigningData; + timestamp: string; + apiVersion: string; +} +``` + +## Low Balance Webhook Payload + +```ts +import { WebhookLowBalancePayload, WebhookBalanceAlertData } from 'rrelayer'; + +export interface WebhookBalanceAlertData { + relayerId: string; + address: `0x${string}`; + chainId: number; + currentBalance: string; + minimumBalance: string; + currentBalanceFormatted: string; + minimumBalanceFormatted: string; + detectedAt: string; +} + +export interface WebhookLowBalancePayload { + eventType: WebhookEventType; + balanceAlert: WebhookBalanceAlertData; + timestamp: string; + apiVersion: string; +} +``` + +## Usage Examples + +### Handling Transaction Webhooks + +```ts +import express from 'express'; +import { + WebhookPayload, + WebhookEventType, +} from 'rrelayer'; + +const app = express(); +app.use(express.json()); + +app.post('/webhooks/transactions', (req, res) => { + const payload = req.body as WebhookPayload; + + switch (payload.eventType) { + case WebhookEventType.TransactionMined: + console.log(`Transaction ${payload.transaction.id} mined at block ${payload.transaction.minedAtBlockNumber}`); + break; + case WebhookEventType.TransactionFailed: + console.log(`Transaction ${payload.transaction.id} failed`); + break; + case WebhookEventType.TransactionReplaced: + console.log(`Transaction replaced. New: ${payload.transaction.id}, Original: ${payload.originalTransaction?.id}`); + break; + } + + res.status(200).json({ received: true }); +}); +``` + +### Handling Signing Webhooks + +```ts +import { + WebhookSigningPayload, + WebhookEventType, +} from 'rrelayer'; + +app.post('/webhooks/signing', (req, res) => { + const payload = req.body as WebhookSigningPayload; + + if (payload.eventType === WebhookEventType.TextSigned) { + console.log(`Message signed by relayer ${payload.signing.relayerId}: "${payload.signing.message}"`); + } else if (payload.eventType === WebhookEventType.TypedDataSigned) { + console.log(`Typed data signed, primary type: ${payload.signing.primaryType}`); + } + + res.status(200).json({ received: true }); +}); +``` + +### Handling Low Balance Alerts + +```ts +import { + WebhookLowBalancePayload, + WebhookEventType, +} from 'rrelayer'; + +app.post('/webhooks/alerts', (req, res) => { + const payload = req.body as WebhookLowBalancePayload; + + if (payload.eventType === WebhookEventType.LowBalance) { + const { relayerId, chainId, currentBalanceFormatted, minimumBalanceFormatted } = payload.balanceAlert; + console.log( + `Low balance alert: Relayer ${relayerId} on chain ${chainId} has ${currentBalanceFormatted} ETH (minimum: ${minimumBalanceFormatted} ETH)` + ); + } + + res.status(200).json({ received: true }); +}); +``` \ No newline at end of file diff --git a/documentation/rrelayer/docs/pages/integration/sdk/webhooks/rust.mdx b/documentation/rrelayer/docs/pages/integration/sdk/webhooks/rust.mdx new file mode 100644 index 00000000..9bbe2a1d --- /dev/null +++ b/documentation/rrelayer/docs/pages/integration/sdk/webhooks/rust.mdx @@ -0,0 +1,227 @@ +# Webhook Types + +The Rust SDK re-exports all webhook payload types from `rrelayer_core` so you can deserialize and type-check incoming webhook payloads. + +## Event Types + +```rust +use rrelayer::{WebhookEventType}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum WebhookEventType { + TransactionQueued, + TransactionSent, + TransactionMined, + TransactionConfirmed, + TransactionFailed, + TransactionExpired, + TransactionCancelled, + TransactionReplaced, + TextSigned, + TypedDataSigned, + LowBalance, +} +``` + +## Transaction Webhook Payload + +```rust +use rrelayer::{WebhookPayload, WebhookTransactionData}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WebhookPayload { + pub event_type: WebhookEventType, + pub transaction: WebhookTransactionData, + pub timestamp: DateTime, + pub api_version: String, + pub original_transaction: Option, + pub receipt: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WebhookTransactionData { + pub id: TransactionId, + pub relayer_id: RelayerId, + pub to: EvmAddress, + pub from: EvmAddress, + pub value: TransactionValue, + pub data: TransactionData, + pub chain_id: ChainId, + pub status: TransactionStatus, + pub transaction_hash: Option, + pub queued_at: DateTime, + pub sent_at: Option>, + pub confirmed_at: Option>, + pub expires_at: DateTime, + pub external_id: Option, + pub blobs: Option>, + pub nonce: TransactionNonce, + pub mined_at: Option>, + pub mined_at_block_number: Option, + pub sent_with_max_priority_fee_per_gas: Option, + pub sent_with_max_fee_per_gas: Option, + pub is_noop: bool, +} +``` + +## Signing Webhook Payload + +```rust +use rrelayer::{WebhookSigningPayload, WebhookSigningData}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WebhookSigningPayload { + pub event_type: WebhookEventType, + pub signing: WebhookSigningData, + pub timestamp: DateTime, + pub api_version: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WebhookSigningData { + pub relayer_id: RelayerId, + pub chain_id: ChainId, + pub signature: Signature, + pub signed_at: DateTime, + pub message: Option, + pub domain_data: Option, + pub message_data: Option, + pub primary_type: Option, +} +``` + +## Low Balance Webhook Payload + +```rust +use rrelayer::{WebhookLowBalancePayload, WebhookBalanceAlertData}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WebhookLowBalancePayload { + pub event_type: WebhookEventType, + pub balance_alert: WebhookBalanceAlertData, + pub timestamp: DateTime, + pub api_version: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WebhookBalanceAlertData { + pub relayer_id: String, + pub address: EvmAddress, + pub chain_id: ChainId, + pub current_balance: String, + pub minimum_balance: String, + pub current_balance_formatted: String, + pub minimum_balance_formatted: String, + pub detected_at: DateTime, +} +``` + +## Usage Examples + +### Deserializing Transaction Webhooks + +```rust +use rrelayer::{WebhookPayload, WebhookEventType}; + +async fn handle_webhook(body: String) -> Result<(), Box> { + let payload: WebhookPayload = serde_json::from_str(&body)?; + + match payload.event_type { + WebhookEventType::TransactionMined => { + println!( + "Transaction {} mined at block {:?}", + payload.transaction.id, + payload.transaction.mined_at_block_number + ); + } + WebhookEventType::TransactionFailed => { + println!("Transaction {} failed", payload.transaction.id); + } + WebhookEventType::TransactionReplaced => { + if let Some(original) = &payload.original_transaction { + println!( + "Transaction replaced. New: {}, Original: {}", + payload.transaction.id, original.id + ); + } + } + _ => {} + } + + Ok(()) +} +``` + +### Deserializing Signing Webhooks + +```rust +use rrelayer::{WebhookSigningPayload, WebhookEventType}; + +async fn handle_signing_webhook(body: String) -> Result<(), Box> { + let payload: WebhookSigningPayload = serde_json::from_str(&body)?; + + match payload.event_type { + WebhookEventType::TextSigned => { + println!( + "Message signed by relayer {}: {:?}", + payload.signing.relayer_id, + payload.signing.message + ); + } + WebhookEventType::TypedDataSigned => { + println!( + "Typed data signed, primary type: {:?}", + payload.signing.primary_type + ); + } + _ => {} + } + + Ok(()) +} +``` + +### Deserializing Low Balance Alerts + +```rust +use rrelayer::{WebhookLowBalancePayload, WebhookEventType}; + +async fn handle_balance_alert(body: String) -> Result<(), Box> { + let payload: WebhookLowBalancePayload = serde_json::from_str(&body)?; + + if payload.event_type == WebhookEventType::LowBalance { + let alert = &payload.balance_alert; + println!( + "Low balance: Relayer {} on chain {} has {} ETH (minimum: {} ETH)", + alert.relayer_id, + alert.chain_id, + alert.current_balance_formatted, + alert.minimum_balance_formatted + ); + } + + Ok(()) +} +``` + +### Using with Axum + +```rust +use axum::{Json, http::StatusCode}; +use rrelayer::{WebhookPayload, WebhookEventType}; + +async fn webhook_handler( + Json(payload): Json, +) -> StatusCode { + match payload.event_type { + WebhookEventType::TransactionConfirmed => { + // Process confirmed transaction + println!("Confirmed: {}", payload.transaction.id); + } + _ => {} + } + + StatusCode::OK +} +``` \ No newline at end of file diff --git a/documentation/rrelayer/vocs.config.tsx b/documentation/rrelayer/vocs.config.tsx index eb3f37a0..a1f516ed 100644 --- a/documentation/rrelayer/vocs.config.tsx +++ b/documentation/rrelayer/vocs.config.tsx @@ -502,6 +502,66 @@ export default defineConfig({ }, ], }, + { + text: 'Webhooks', + collapsed: true, + items: [ + { + text: 'Node', + collapsed: true, + link: '/integration/sdk/webhooks/node', + items: [ + { + text: 'Event Types', + link: '/integration/sdk/webhooks/node#event-types', + }, + { + text: 'Transaction Payload', + link: '/integration/sdk/webhooks/node#transaction-webhook-payload', + }, + { + text: 'Signing Payload', + link: '/integration/sdk/webhooks/node#signing-webhook-payload', + }, + { + text: 'Low Balance Payload', + link: '/integration/sdk/webhooks/node#low-balance-webhook-payload', + }, + { + text: 'Usage Examples', + link: '/integration/sdk/webhooks/node#usage-examples', + }, + ], + }, + { + text: 'Rust', + collapsed: true, + link: '/integration/sdk/webhooks/rust', + items: [ + { + text: 'Event Types', + link: '/integration/sdk/webhooks/rust#event-types', + }, + { + text: 'Transaction Payload', + link: '/integration/sdk/webhooks/rust#transaction-webhook-payload', + }, + { + text: 'Signing Payload', + link: '/integration/sdk/webhooks/rust#signing-webhook-payload', + }, + { + text: 'Low Balance Payload', + link: '/integration/sdk/webhooks/rust#low-balance-webhook-payload', + }, + { + text: 'Usage Examples', + link: '/integration/sdk/webhooks/rust#usage-examples', + }, + ], + }, + ], + }, { text: 'Sign', collapsed: true, diff --git a/playground/rust-sdk-playground/Cargo.toml b/playground/rust-sdk-playground/Cargo.toml index 5384033e..c04e573d 100644 --- a/playground/rust-sdk-playground/Cargo.toml +++ b/playground/rust-sdk-playground/Cargo.toml @@ -14,12 +14,12 @@ path = "src/lib.rs" [dependencies] rrelayer = { path = "../../crates/sdk" } eyre = "0.6" -anyhow = "1.0" -tokio = "1.47.1" -serde_json = "1.0.142" -serde = { version = "1.0", features = ["derive"] } +anyhow = { workspace = true } +tokio = { workspace = true } +serde_json = { workspace = true } +serde = { workspace = true } alloy = { workspace = true, features = ["full", "signer-mnemonic", "eips", "eip712"] } -hex = "0.4" -async-trait = "0.1" -thiserror = "1.0" +hex = { workspace = true } +async-trait = { workspace = true } +thiserror = { workspace = true } futures-util = "0.3" \ No newline at end of file diff --git a/sdk/typescript/package-lock.json b/sdk/typescript/package-lock.json index d5ce4dda..f95efc8c 100644 --- a/sdk/typescript/package-lock.json +++ b/sdk/typescript/package-lock.json @@ -1,12 +1,12 @@ { "name": "rrelayer", - "version": "1.0.7", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rrelayer", - "version": "1.0.7", + "version": "1.2.0", "license": "MIT", "dependencies": { "axios": "^1.6.7", diff --git a/sdk/typescript/src/api/index.ts b/sdk/typescript/src/api/index.ts index a7e63173..60326f8c 100644 --- a/sdk/typescript/src/api/index.ts +++ b/sdk/typescript/src/api/index.ts @@ -3,5 +3,6 @@ export * from './network'; export * from './relayer'; export * from './transaction'; export * from './signing'; +export * from './webhook'; export const RATE_LIMIT_HEADER_NAME = 'x-rrelayer-rate-limit-key'; diff --git a/sdk/typescript/src/api/webhook/index.ts b/sdk/typescript/src/api/webhook/index.ts new file mode 100644 index 00000000..fcb073fe --- /dev/null +++ b/sdk/typescript/src/api/webhook/index.ts @@ -0,0 +1 @@ +export * from './types'; diff --git a/sdk/typescript/src/api/webhook/types.ts b/sdk/typescript/src/api/webhook/types.ts new file mode 100644 index 00000000..f31e3e4c --- /dev/null +++ b/sdk/typescript/src/api/webhook/types.ts @@ -0,0 +1,82 @@ +export enum WebhookEventType { + TransactionQueued = 'transaction_queued', + TransactionSent = 'transaction_sent', + TransactionMined = 'transaction_mined', + TransactionConfirmed = 'transaction_confirmed', + TransactionFailed = 'transaction_failed', + TransactionExpired = 'transaction_expired', + TransactionCancelled = 'transaction_cancelled', + TransactionReplaced = 'transaction_replaced', + TextSigned = 'text_signed', + TypedDataSigned = 'typed_data_signed', + LowBalance = 'low_balance', +} + +export interface WebhookTransactionData { + id: string; + relayerId: string; + to: `0x${string}`; + from: `0x${string}`; + value: string; + data: string; + chainId: number; + status: string; + txHash?: string | null; + queuedAt: string; + sentAt?: string | null; + confirmedAt?: string | null; + expiresAt: string; + externalId?: string | null; + blobs?: string[] | null; + nonce: string; + minedAt?: string | null; + minedAtBlockNumber?: string | null; + maxPriorityFee?: string | null; + maxFee?: string | null; + isNoop: boolean; +} + +export interface WebhookPayload { + eventType: WebhookEventType; + transaction: WebhookTransactionData; + timestamp: string; + apiVersion: string; + originalTransaction?: WebhookTransactionData | null; + receipt?: Record | null; +} + +export interface WebhookSigningData { + relayerId: string; + chainId: number; + signature: string; + signedAt: string; + message?: string | null; + domainData?: Record | null; + messageData?: Record | null; + primaryType?: string | null; +} + +export interface WebhookSigningPayload { + eventType: WebhookEventType; + signing: WebhookSigningData; + timestamp: string; + apiVersion: string; +} + +export interface WebhookBalanceAlertData { + relayerId: string; + address: `0x${string}`; + chainId: number; + currentBalance: string; + minimumBalance: string; + currentBalanceFormatted: string; + minimumBalanceFormatted: string; + detectedAt: string; +} + +export interface WebhookLowBalancePayload { + eventType: WebhookEventType; + balanceAlert: WebhookBalanceAlertData; + timestamp: string; + apiVersion: string; +}