From adda6ec45061712bd2f16603166d285657cfb8a4 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 9 May 2024 00:54:10 +0000 Subject: [PATCH 01/28] chore(release): 1.6.0 [skip ci] ## [1.6.0](https://github.com/hirosystems/chainhook/compare/v1.5.1...v1.6.0) (2024-05-09) ### Features * add Bitcoin transaction index to typescript client ([#568](https://github.com/hirosystems/chainhook/issues/568)) ([6f7eba4](https://github.com/hirosystems/chainhook/commit/6f7eba4c60d96dc088c08708cc5af2381ee62012)) ### Bug Fixes * add stacks event position to ts client ([#575](https://github.com/hirosystems/chainhook/issues/575)) ([3c48eeb](https://github.com/hirosystems/chainhook/commit/3c48eeb8adf0b6ef7998702f1d082c960f00f950)) * add starting stacks height for prometheus metrics ([#567](https://github.com/hirosystems/chainhook/issues/567)) ([6a8c086](https://github.com/hirosystems/chainhook/commit/6a8c0869278aad54f5100216ceb5b2d8f98ad002)) * make bitcoin payload serialization deserializable ([#569](https://github.com/hirosystems/chainhook/issues/569)) ([5f20a86](https://github.com/hirosystems/chainhook/commit/5f20a869acbd057d855b9601a4fb1072e75ab4c4)) * set `Interrupted` status for streaming predicates that fail ([#574](https://github.com/hirosystems/chainhook/issues/574)) ([11bde53](https://github.com/hirosystems/chainhook/commit/11bde5344b1b42bd7561e632e36265a9698fc095)), closes [#523](https://github.com/hirosystems/chainhook/issues/523) * shut down observer on bitcoin block download failure ([#573](https://github.com/hirosystems/chainhook/issues/573)) ([f3530b7](https://github.com/hirosystems/chainhook/commit/f3530b74d9d25c111338d06085e2dae6fe527932)), closes [#572](https://github.com/hirosystems/chainhook/issues/572) --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35a402868..dc92f40df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +## [1.6.0](https://github.com/hirosystems/chainhook/compare/v1.5.1...v1.6.0) (2024-05-09) + + +### Features + +* add Bitcoin transaction index to typescript client ([#568](https://github.com/hirosystems/chainhook/issues/568)) ([6f7eba4](https://github.com/hirosystems/chainhook/commit/6f7eba4c60d96dc088c08708cc5af2381ee62012)) + + +### Bug Fixes + +* add stacks event position to ts client ([#575](https://github.com/hirosystems/chainhook/issues/575)) ([3c48eeb](https://github.com/hirosystems/chainhook/commit/3c48eeb8adf0b6ef7998702f1d082c960f00f950)) +* add starting stacks height for prometheus metrics ([#567](https://github.com/hirosystems/chainhook/issues/567)) ([6a8c086](https://github.com/hirosystems/chainhook/commit/6a8c0869278aad54f5100216ceb5b2d8f98ad002)) +* make bitcoin payload serialization deserializable ([#569](https://github.com/hirosystems/chainhook/issues/569)) ([5f20a86](https://github.com/hirosystems/chainhook/commit/5f20a869acbd057d855b9601a4fb1072e75ab4c4)) +* set `Interrupted` status for streaming predicates that fail ([#574](https://github.com/hirosystems/chainhook/issues/574)) ([11bde53](https://github.com/hirosystems/chainhook/commit/11bde5344b1b42bd7561e632e36265a9698fc095)), closes [#523](https://github.com/hirosystems/chainhook/issues/523) +* shut down observer on bitcoin block download failure ([#573](https://github.com/hirosystems/chainhook/issues/573)) ([f3530b7](https://github.com/hirosystems/chainhook/commit/f3530b74d9d25c111338d06085e2dae6fe527932)), closes [#572](https://github.com/hirosystems/chainhook/issues/572) + ## [1.5.1](https://github.com/hirosystems/chainhook/compare/v1.5.0...v1.5.1) (2024-04-12) From 9011a0ce49d9d38a6d7a6776d7f37b709a355386 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20C=C3=A1rdenas?= Date: Tue, 14 May 2024 13:36:19 -0600 Subject: [PATCH 02/28] fix: serialize brc-20 data (#585) Co-authored-by: Ludo Galabru Co-authored-by: Micaiah Reid --- Cargo.lock | 2 +- components/chainhook-sdk/Cargo.toml | 2 +- .../src/chainhooks/bitcoin/mod.rs | 4 + .../src/chainhooks/bitcoin/tests.rs | 80 ++++++++++++++++++- .../client/typescript/package-lock.json | 4 +- components/client/typescript/package.json | 2 +- components/client/typescript/src/server.ts | 58 +++++++------- 7 files changed, 115 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7620bfe82..ff62ff446 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -510,7 +510,7 @@ dependencies = [ [[package]] name = "chainhook-sdk" -version = "0.12.7" +version = "0.12.10" dependencies = [ "base58", "base64 0.21.5", diff --git a/components/chainhook-sdk/Cargo.toml b/components/chainhook-sdk/Cargo.toml index c43e3086f..694610b22 100644 --- a/components/chainhook-sdk/Cargo.toml +++ b/components/chainhook-sdk/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "chainhook-sdk" -version = "0.12.7" +version = "0.12.10" description = "Stateless Transaction Indexing Engine for Stacks and Bitcoin" license = "GPL-3.0" edition = "2021" diff --git a/components/chainhook-sdk/src/chainhooks/bitcoin/mod.rs b/components/chainhook-sdk/src/chainhooks/bitcoin/mod.rs index cc1900b63..1f5b306fa 100644 --- a/components/chainhook-sdk/src/chainhooks/bitcoin/mod.rs +++ b/components/chainhook-sdk/src/chainhooks/bitcoin/mod.rs @@ -288,6 +288,10 @@ pub fn serialize_bitcoin_transactions_to_json<'a>( }; metadata.insert("ordinal_operations".into(), json!(ordinals_ops)); + if let Some(ref brc20) = transaction.metadata.brc20_operation { + metadata.insert("brc20_operation".into(), json!(brc20)); + } + metadata.insert( "proof".into(), json!(proofs.get(&transaction.transaction_identifier)), diff --git a/components/chainhook-sdk/src/chainhooks/bitcoin/tests.rs b/components/chainhook-sdk/src/chainhooks/bitcoin/tests.rs index 2f879f1ff..f2024a289 100644 --- a/components/chainhook-sdk/src/chainhooks/bitcoin/tests.rs +++ b/components/chainhook-sdk/src/chainhooks/bitcoin/tests.rs @@ -1,12 +1,15 @@ +use std::collections::HashSet; + use super::super::types::MatchingRule; use super::*; +use crate::chainhooks::types::InscriptionFeedData; use crate::indexer::tests::helpers::accounts; use crate::indexer::tests::helpers::bitcoin_blocks::generate_test_bitcoin_block; use crate::indexer::tests::helpers::transactions::generate_test_tx_bitcoin_p2pkh_transfer; use crate::types::BitcoinTransactionMetadata; use chainhook_types::bitcoin::TxOut; -use chainhook_types::BitcoinNetwork; +use chainhook_types::{BitcoinNetwork, Brc20Operation, Brc20TokenDeployData}; use test_case::test_case; #[test_case( @@ -193,3 +196,78 @@ fn it_serdes_occurrence_payload( let _: BitcoinChainhookOccurrencePayload = serde_json::from_slice(&payload[..]).unwrap(); } + +#[test_case( + "pepe".to_string(); + "including brc20 data" +)] +fn it_serdes_brc20_payload(tick: String) { + let transaction = BitcoinTransactionData { + transaction_identifier: TransactionIdentifier { + hash: "0xc6191000459e4c58611103216e44547e512c01ee04119462644ee09ce9d8e8bb".to_string(), + }, + operations: vec![], + metadata: BitcoinTransactionMetadata { + inputs: vec![], + outputs: vec![], + ordinal_operations: vec![], + stacks_operations: vec![], + brc20_operation: Some(Brc20Operation::Deploy(Brc20TokenDeployData { + tick, + max: "21000000.000000".to_string(), + lim: "1000.000000".to_string(), + dec: "6".to_string(), + address: "3P4WqXDbSLRhzo2H6MT6YFbvBKBDPLbVtQ".to_string(), + inscription_id: + "c6191000459e4c58611103216e44547e512c01ee04119462644ee09ce9d8e8bbi0".to_string(), + self_mint: false, + })), + proof: None, + fee: 0, + index: 0, + }, + }; + let block = generate_test_bitcoin_block(0, 0, vec![transaction.clone()], None); + let mut meta_protocols = HashSet::::new(); + meta_protocols.insert(OrdinalsMetaProtocol::Brc20); + let chainhook = &BitcoinChainhookSpecification { + uuid: "uuid".into(), + owner_uuid: None, + name: "name".into(), + network: BitcoinNetwork::Mainnet, + version: 0, + blocks: None, + start_block: None, + end_block: None, + expire_after_occurrence: None, + predicate: BitcoinPredicateType::OrdinalsProtocol(OrdinalOperations::InscriptionFeed( + InscriptionFeedData { + meta_protocols: Some(meta_protocols), + }, + )), + action: HookAction::Noop, + include_proof: false, + include_inputs: false, + include_outputs: false, + include_witness: false, + enabled: true, + expired_at: None, + }; + let trigger = BitcoinTriggerChainhook { + chainhook, + apply: vec![(vec![&transaction], &block)], + rollback: vec![], + }; + let payload = serde_json::to_vec(&serialize_bitcoin_payload_to_json( + &trigger, + &HashMap::new(), + )) + .unwrap(); + + let deserialized: BitcoinChainhookOccurrencePayload = + serde_json::from_slice(&payload[..]).unwrap(); + assert!(deserialized.apply[0].block.transactions[0] + .metadata + .brc20_operation + .is_some()); +} diff --git a/components/client/typescript/package-lock.json b/components/client/typescript/package-lock.json index cb668eaa5..dd0427e3f 100644 --- a/components/client/typescript/package-lock.json +++ b/components/client/typescript/package-lock.json @@ -1,12 +1,12 @@ { "name": "@hirosystems/chainhook-client", - "version": "1.9.0", + "version": "1.10.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@hirosystems/chainhook-client", - "version": "1.9.0", + "version": "1.10.0", "license": "Apache 2.0", "dependencies": { "@fastify/type-provider-typebox": "^3.2.0", diff --git a/components/client/typescript/package.json b/components/client/typescript/package.json index 6597aea49..9731e83a9 100644 --- a/components/client/typescript/package.json +++ b/components/client/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@hirosystems/chainhook-client", - "version": "1.9.0", + "version": "1.10.0", "description": "Chainhook TypeScript client", "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/components/client/typescript/src/server.ts b/components/client/typescript/src/server.ts index 7d952727a..49ac1707c 100644 --- a/components/client/typescript/src/server.ts +++ b/components/client/typescript/src/server.ts @@ -28,6 +28,8 @@ const ServerOptionsSchema = Type.Object({ wait_for_chainhook_node: Type.Optional(Type.Boolean({ default: true })), /** Validate the JSON schema of received chainhook payloads and report errors when invalid */ validate_chainhook_payloads: Type.Optional(Type.Boolean({ default: false })), + /** Validate the authorization token sent by the server is correct. */ + validate_token_authorization: Type.Optional(Type.Boolean({ default: true })), /** Size limit for received chainhook payloads (default 40MB) */ body_limit: Type.Optional(Type.Number({ default: 41943040 })), /** Node type: `chainhook` or `ordhook` */ @@ -186,6 +188,7 @@ export async function buildServer( } async function isEventAuthorized(request: FastifyRequest, reply: FastifyReply) { + if (!(serverOpts.validate_token_authorization ?? true)) return; const authHeader = request.headers.authorization; if (authHeader && authHeader === `Bearer ${serverOpts.auth_token}`) { return; @@ -200,39 +203,32 @@ export async function buildServer( > = (fastify, options, done) => { const compiledPayloadSchema = TypeCompiler.Compile(PayloadSchema); fastify.addHook('preHandler', isEventAuthorized); - fastify.post( - '/payload', - { - schema: { - body: PayloadSchema, - }, - }, - async (request, reply) => { - if ( - (serverOpts.validate_chainhook_payloads ?? false) && - !compiledPayloadSchema.Check(request.body) - ) { - logger.error( - [...compiledPayloadSchema.Errors(request.body)], - `ChainhookEventObserver received an invalid payload` - ); - await reply.code(422).send(); - return; - } - try { - await callback(request.body.chainhook.uuid, request.body); - await reply.code(200).send(); - } catch (error) { - if (error instanceof BadPayloadRequestError) { - logger.error(error, `ChainhookEventObserver bad payload`); - await reply.code(400).send(); - } else { - logger.error(error, `ChainhookEventObserver error processing payload`); - await reply.code(500).send(); - } + fastify.post('/payload', async (request, reply) => { + if ( + (serverOpts.validate_chainhook_payloads ?? false) && + !compiledPayloadSchema.Check(request.body) + ) { + logger.error( + [...compiledPayloadSchema.Errors(request.body)], + `ChainhookEventObserver received an invalid payload` + ); + await reply.code(422).send(); + return; + } + const body = request.body as Payload; + try { + await callback(body.chainhook.uuid, body); + await reply.code(200).send(); + } catch (error) { + if (error instanceof BadPayloadRequestError) { + logger.error(error, `ChainhookEventObserver bad payload`); + await reply.code(400).send(); + } else { + logger.error(error, `ChainhookEventObserver error processing payload`); + await reply.code(500).send(); } } - ); + }); done(); }; From ddb1e41de42acc16c4d107960c21eccd7c0e0d86 Mon Sep 17 00:00:00 2001 From: Micaiah Reid Date: Wed, 15 May 2024 11:39:44 -0400 Subject: [PATCH 03/28] ci: revamp ci (#594) --- .github/workflows/ci.yaml | 331 ++++++++++++++++-- .github/workflows/pkg-version-bump.yaml | 28 +- CHANGELOG.md | 7 + Cargo.lock | 2 +- components/chainhook-cli/Cargo.toml | 2 +- .../chainhook-cli/src/service/tests/mod.rs | 6 +- .../src/service/tests/observer_tests.rs | 58 ++- .../chainhook-cli/wix}/License.rtf | 0 .../chainhook-cli/wix}/main.wxs | 26 +- docs/chainhook-openapi.json | 2 +- docs/images/chainhook-banner.bmp | Bin 0 -> 114514 bytes docs/images/chainhook-dialog.bmp | Bin 0 -> 615402 bytes rust-toolchain | 2 +- 13 files changed, 358 insertions(+), 106 deletions(-) rename {wix => components/chainhook-cli/wix}/License.rtf (100%) rename {wix => components/chainhook-cli/wix}/main.wxs (86%) create mode 100644 docs/images/chainhook-banner.bmp create mode 100644 docs/images/chainhook-dialog.bmp diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e3ac8d414..5698f939e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -15,13 +15,52 @@ concurrency: cancel-in-progress: true jobs: + + get_release_info: + name: Get Release Info + runs-on: ubuntu-latest + outputs: + tag: ${{ steps.new_release_tag.outputs.TAG }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Get latest release + if: startsWith(github.ref, 'refs/heads/main') + id: release + uses: pozetroninc/github-action-get-latest-release@master + with: + repository: ${{ github.repository }} + excludes: prerelease, draft + + - name: Determine if release build + if: startsWith(github.ref, 'refs/heads/main') + id: new_release_tag + env: + LATEST_RELEASE: ${{ steps.release.outputs.release }} + run: | + CARGO_VERSION=v$(grep "version" components/chainhook-cli/Cargo.toml | head -n 1 | cut -d\" -f2) + if [[ "${CARGO_VERSION}" != "${LATEST_RELEASE}" ]]; then + echo "::set-output name=TAG::${CARGO_VERSION}" + echo "::warning::Will create release for version: ${CARGO_VERSION}" + else + echo "::warning::Will not create a release" + fi + test: + name: Generate test coverage runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 with: persist-credentials: false + - name: Install Rust toolchain + run: | + rustup toolchain install stable --profile minimal + echo "RUST_VERSION_HASH=$(rustc --version | sha256sum | awk '{print $1}')" >> $GITHUB_ENV + - name: Install redis run: sudo apt-get install -y redis-server @@ -33,55 +72,283 @@ jobs: ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ + # Cache crates.toml & crates2.json to allow `cargo install` + ~/.cargo/.crates.toml + ~/.cargo/.crates2.json target/ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - name: Cargo test + - name: Install and run cargo-tarpaulin run: | - rustup update - RUST_BACKTRACE=1 cargo test --all --features redis_tests -- --test-threads=1 + cargo install cargo-tarpaulin + cargo --version + cargo tarpaulin --out lcov --features redis_tests -- --test-threads=1 - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + dist_chainhook: + name: Build Chainhook Distributions + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + platform: linux + target: x86_64-unknown-linux-gnu + architecture: x64 + libc: glibc + - os: windows-latest + platform: windows + target: x86_64-pc-windows-msvc + architecture: x64 + - os: macos-latest + platform: darwin + target: x86_64-apple-darwin + architecture: x64 + - os: macos-latest + platform: darwin + target: aarch64-apple-darwin + architecture: arm64 + + steps: + - name: Configure git to use LF (Windows) + if: matrix.os == 'windows-latest' + run: | + git config --global core.autocrlf false + git config --global core.eol lf + + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust toolchain + run: rustup toolchain install stable --profile minimal --target ${{ matrix.target }} + + - name: Install Rust Target + run: rustup target add ${{ matrix.target }} + + - name: List rust targets + run: rustup target list + + - name: "Get Rust version (unix)" + if: matrix.os != 'windows-latest' + run: echo "RUST_VERSION_HASH=$(rustc --version | shasum -a 256 | awk '{print $1}')" >> $GITHUB_ENV + + - name: "Get Rust version (windows)" + if: matrix.os == 'windows-latest' + shell: bash + run: echo "RUST_VERSION_HASH=$(rustc --version | sha256sum | awk '{print $1}')" >> $GITHUB_ENV + + - name: Cache cargo + uses: actions/cache@v3 + with: + path: | + ~/.cargo/ + ./target/${{ matrix.target }}/release/ + key: ${{ runner.os }}-rust-${{ env.RUST_VERSION_HASH }}-cargo-${{ hashFiles('./Cargo.lock') }} + + - name: Install wix (Windows) + if: matrix.os == 'windows-latest' && steps.cache-cargo.outputs.cache-hit != 'true' + run: cargo install cargo-wix + + # Set environment variables required from cross compiling from macos-x86_64 to macos-arm64 + - name: Configure macos-arm64 cross compile config + 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: Configure artifact names (libc) + if: ${{ matrix.libc }} + shell: bash + run: | + echo "SHORT_TARGET_NAME=${{ matrix.platform }}-${{ matrix.architecture }}-${{ matrix.libc }}" >> $GITHUB_ENV + echo "PRE_GYP_TARGET_NAME=${{ matrix.platform }}-${{ matrix.architecture }}-${{ matrix.libc }}" >> $GITHUB_ENV + + - name: Configure artifact names (not libc) + if: ${{ ! matrix.libc }} + shell: bash + run: | + echo "SHORT_TARGET_NAME=${{ matrix.platform }}-${{ matrix.architecture }}" >> $GITHUB_ENV + echo "PRE_GYP_TARGET_NAME=${{ matrix.platform }}-${{ matrix.architecture }}-unknown" >> $GITHUB_ENV + + - name: Build - Cargo + if: matrix.target != 'x86_64-unknown-linux-musl' + run: cargo build --release --features cli --features debug --no-default-features --target ${{ matrix.target }} + + # Steps for Windows Code Signing with DigiCert + - name: Windows - Setup Certificate + if: startsWith(github.ref, 'refs/heads/main') && matrix.os == 'windows-latest' + run: | + echo "${{ secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12 + cat /d/Certificate_pkcs12.p12 + shell: bash + + - name: Windows - Set variables + if: startsWith(github.ref, 'refs/heads/main') && matrix.os == 'windows-latest' + id: variables + run: | + dir + echo "::set-output name=version::${GITHUB_REF#refs/tags/v}" + echo "::set-output name=KEYPAIR_NAME::gt-standard-keypair" + echo "::set-output name=CERTIFICATE_NAME::gt-certificate" + echo "SM_HOST=${{ secrets.SM_HOST }}" >> "$GITHUB_ENV" + echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> "$GITHUB_ENV" + echo "SM_CLIENT_CERT_FILE=D:\\Certificate_pkcs12.p12" >> "$GITHUB_ENV" + echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> "$GITHUB_ENV" + echo "C:\Program Files (x86)\Windows Kits\10\App Certification Kit" >> $GITHUB_PATH + echo "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools" >> $GITHUB_PATH + echo "C:\Program Files\DigiCert\DigiCert Keylocker Tools" >> $GITHUB_PATH + shell: bash + + - name: Windows - Setup Keylocker KSP + if: startsWith(github.ref, 'refs/heads/main') && matrix.os == 'windows-latest' + run: | + curl -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/Keylockertools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" -o Keylockertools-windows-x64.msi + msiexec /i Keylockertools-windows-x64.msi /quiet /qn + smksp_registrar.exe list + smctl.exe keypair ls + C:\Windows\System32\certutil.exe -csp "DigiCert Signing Manager KSP" -key -user + shell: cmd + + - name: Windows - Certificates Sync + if: startsWith(github.ref, 'refs/heads/main') && matrix.os == 'windows-latest' + run: | + smctl windows certsync + shell: cmd + + - name: Code sign bin (Windows) + if: startsWith(github.ref, 'refs/heads/main') && matrix.os == 'windows-latest' + run: | + $signtool_path = ((Resolve-Path -Path "${env:ProgramFiles(x86)}/Windows Kits/10/bin/10*/x86").Path[-1]) + "/signtool.exe" + $bin_path = (Resolve-Path -Path "target/${{ matrix.target }}/release/chainhook.exe").Path + + & ${signtool_path} sign ` + /d "Chainhook is a reorg-aware indexing engine for the Stacks & Bitcoin blockchains." ` + /du "https://github.com/hirosystems/chainhook" ` + /tr http://timestamp.digicert.com ` + /sha1 "${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }}" ` + /td sha256 ` + /fd sha256 ` + "${bin_path}" + + & ${signtool_path} verify /v /pa "${bin_path}" + + - name: Build Installer (Windows) + if: matrix.os == 'windows-latest' + run: cargo wix -v --no-build --nocapture -p chainhook + + - name: Code sign installed (Windows) + if: startsWith(github.ref, 'refs/heads/main') && matrix.os == 'windows-latest' + run: | + $signtool_path = ((Resolve-Path -Path "${env:ProgramFiles(x86)}/Windows Kits/10/bin/10*/x86").Path[-1]) + "/signtool.exe" + $msi_path = (Resolve-Path -Path "target/wix/*.msi").Path + + & ${signtool_path} sign ` + /d "Chainhook is a reorg-aware indexing engine for the Stacks & Bitcoin blockchains." ` + /du "https://github.com/hirosystems/chainhook" ` + /tr http://timestamp.digicert.com ` + /sha1 "${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }}" ` + /td sha256 ` + /fd sha256 ` + "${msi_path}" + + & ${signtool_path} verify /v /pa "${msi_path}" + + # Don't compress for Windows because winget can't yet unzip files + - name: Compress cargo artifact (Linux) + if: matrix.os != 'windows-latest' + run: tar -C target/${{ matrix.target }}/release -zcvf chainhook-${{ env.SHORT_TARGET_NAME }}.tar.gz chainhook + + - name: Rename cargo artifact (Windows) + if: matrix.os == 'windows-latest' + shell: bash + run: mv target/wix/*.msi chainhook-${{ env.SHORT_TARGET_NAME }}.msi + + # Separate uploads to prevent paths from being preserved + - name: Upload cargo artifacts (Linux) + if: matrix.os != 'windows-latest' + uses: actions/upload-artifact@v2 + with: + name: chainhook-${{ env.SHORT_TARGET_NAME }} + path: chainhook-${{ env.SHORT_TARGET_NAME }}.tar.gz + + - name: Upload cargo artifact (Windows) + if: matrix.os == 'windows-latest' + uses: actions/upload-artifact@v2 + with: + name: chainhook-${{ env.SHORT_TARGET_NAME }} + path: chainhook-${{ env.SHORT_TARGET_NAME }}.msi + + release: + name: Release + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/heads/main') && needs.get_release_info.outputs.tag != '' + needs: + - test + - dist_chainhook + - get_release_info + permissions: + actions: write + contents: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Download pre-built dists + uses: actions/download-artifact@v3 + + - name: Tag and Release + uses: ncipollo/release-action@v1 + with: + artifacts: "**/*.tar.gz,**/*.msi" + tag: ${{ needs.get_release_info.outputs.tag }} + commit: ${{ env.GITHUB_SHA }} + + - name: Trigger pkg-version-bump workflow + uses: peter-evans/repository-dispatch@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + event-type: released + client-payload: '{"tag": "${{ needs.get_release_info.outputs.tag }}"}' + + build-publish: runs-on: ubuntu-latest - needs: test + needs: + - test + - dist_chainhook + - get_release_info outputs: docker_image_digest: ${{ steps.docker_push.outputs.digest }} - new_release_published: ${{ steps.semantic.outputs.new_release_published }} + strategy: + fail-fast: false + matrix: + include: + - name: Chainhook + description: Chainhook is a reorg-aware indexing engine for the Stacks & Bitcoin blockchains. + image: ${{ github.repository }} + artifact: chainhook-linux-x64-glibc + dockerfile: dockerfiles/components/chainhook-node.dockerfile steps: - uses: actions/checkout@v4 with: persist-credentials: false - - name: Semantic Release - uses: cycjimmy/semantic-release-action@v4 - id: semantic - # Only run on non-PR events or only PRs that aren't from forks - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SEMANTIC_RELEASE_PACKAGE: ${{ github.event.repository.name }} - with: - semantic_version: 19 - extra_plugins: | - @semantic-release/changelog@6.0.3 - @semantic-release/git@10.0.1 - conventional-changelog-conventionalcommits@6.1.0 - - name: Checkout tag - if: steps.semantic.outputs.new_release_version != '' + if: needs.get_release_info.outputs.tag != '' uses: actions/checkout@v4 with: persist-credentials: false - ref: v${{ steps.semantic.outputs.new_release_version }} + ref: ${{ needs.get_release_info.outputs.tag }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - + - name: Docker Meta id: meta uses: docker/metadata-action@v5 @@ -91,8 +358,8 @@ jobs: tags: | type=ref,event=branch type=ref,event=pr - type=semver,pattern={{version}},value=${{ steps.semantic.outputs.new_release_version }},enable=${{ steps.semantic.outputs.new_release_version != '' }} - type=semver,pattern={{major}}.{{minor}},value=${{ steps.semantic.outputs.new_release_version }},enable=${{ steps.semantic.outputs.new_release_version != '' }} + type=semver,pattern={{version}},value=${{ needs.get_release_info.outputs.tag }},enable=${{ needs.get_release_info.outputs.tag != '' }} + type=semver,pattern={{major}}.{{minor}},value=${{ needs.get_release_info.outputs.tag }},enable=${{ needs.get_release_info.outputs.tag != '' }} type=raw,value=latest,enable={{is_default_branch}} - name: Log in to DockerHub @@ -101,6 +368,14 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} + - name: Download pre-built dist + uses: actions/download-artifact@v3 + with: + name: ${{ matrix.artifact }} + + - name: Untar pre-built dist + run: tar zxvf *.tar.gz + - name: Build/Push Image uses: docker/build-push-action@v5 id: docker_push @@ -108,11 +383,11 @@ jobs: context: . tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - file: ./dockerfiles/components/chainhook-node.dockerfile + file: ${{ matrix.dockerfile }} cache-from: type=gha cache-to: type=gha,mode=max # Only push if (there's a new release on main branch, or if building a non-main branch) and (Only run on non-PR events or only PRs that aren't from forks) - push: ${{ (github.ref != 'refs/heads/main' || steps.semantic.outputs.new_release_version != '') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} + push: ${{ (github.ref != 'refs/heads/main' || needs.get_release_info.outputs.tag != '') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} deploy-dev: runs-on: ubuntu-latest diff --git a/.github/workflows/pkg-version-bump.yaml b/.github/workflows/pkg-version-bump.yaml index a1199260f..2272fd45f 100644 --- a/.github/workflows/pkg-version-bump.yaml +++ b/.github/workflows/pkg-version-bump.yaml @@ -1,5 +1,5 @@ ## -## Bumps the Clarinet version listed on various package managers. +## Bumps the Chainhook version listed on various package managers. ## name: Package Version Bump @@ -18,24 +18,6 @@ env: GIT_USER_EMAIL: 45208873+blockstack-devops@users.noreply.github.com jobs: - homebrew: - name: Homebrew - runs-on: macos-latest - steps: - - name: Homebrew version bump - env: - HOMEBREW_GITHUB_API_TOKEN: ${{ secrets.GH_TOKEN }} - TAG: ${{ github.event.client_payload.tag || github.event.inputs.tag }} - run: | - git config --global user.name "${GIT_USER_NAME}" - git config --global user.email "${GIT_USER_EMAIL}" - - brew update - brew bump-formula-pr \ - --no-browse \ - --no-audit \ - --tag "${TAG}" \ - ${{ github.event.repository.name }} winget: name: Winget @@ -45,8 +27,8 @@ jobs: env: TAG: ${{ github.event.client_payload.tag || github.event.inputs.tag }} run: | - # Get version infoq - $VERSION=${env:TAG}.substring(1) + # Get version info + VERSION=$(echo "${TAG#v}") # Configure git configs git config --global user.name "${env:GIT_USER_NAME}" @@ -57,8 +39,8 @@ jobs: # Update manifest and submit PR ./wingetcreate.exe update ` - --urls https://github.com/${{ github.repository }}/releases/download/${env:TAG}/clarinet-windows-x64.msi ` + --urls https://github.com/${{ github.repository }}/releases/download/${env:TAG}/chainhook-windows-x64.msi ` --version ${VERSION} ` --token ${{ secrets.GH_TOKEN }} ` --submit ` - HiroSystems.Clarinet + HiroSystems.Chainhook diff --git a/CHANGELOG.md b/CHANGELOG.md index dc92f40df..055db7160 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [1.6.1](https://github.com/hirosystems/chainhook/compare/v1.6.0...v1.6.1) (2024-05-15) + +### Bug Fixes + +* serialize brc-20 data ([#585](https://github.com/hirosystems/chainhook/issues/585)) ([9011a0c](https://github.com/hirosystems/chainhook/commit/9011a0ce49d9d38a6d7a6776d7f37b709a355386)) + + ## [1.6.0](https://github.com/hirosystems/chainhook/compare/v1.5.1...v1.6.0) (2024-05-09) diff --git a/Cargo.lock b/Cargo.lock index ff62ff446..538a2c23d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -472,7 +472,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chainhook" -version = "1.6.0" +version = "1.6.1" dependencies = [ "ansi_term", "atty", diff --git a/components/chainhook-cli/Cargo.toml b/components/chainhook-cli/Cargo.toml index d684056e0..596b76adc 100644 --- a/components/chainhook-cli/Cargo.toml +++ b/components/chainhook-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "chainhook" -version = "1.6.0" +version = "1.6.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/components/chainhook-cli/src/service/tests/mod.rs b/components/chainhook-cli/src/service/tests/mod.rs index 41dd79647..be20a78cc 100644 --- a/components/chainhook-cli/src/service/tests/mod.rs +++ b/components/chainhook-cli/src/service/tests/mod.rs @@ -997,7 +997,7 @@ async fn it_seeds_block_pool_on_startup() -> Result<(), String> { Ok(()) } -fn cleanup_err( +pub fn cleanup_err( error: String, working_dir: &str, redis_port: u16, @@ -1007,8 +1007,8 @@ fn cleanup_err( format!("test failed with error: {error}") } -fn cleanup(working_dir: &str, redis_port: u16, redis_process: &mut Child) { - std::fs::remove_dir_all(&working_dir).unwrap(); +pub fn cleanup(working_dir: &str, redis_port: u16, redis_process: &mut Child) { + let _ = std::fs::remove_dir_all(&working_dir); flush_redis(redis_port); redis_process.kill().unwrap(); } diff --git a/components/chainhook-cli/src/service/tests/observer_tests.rs b/components/chainhook-cli/src/service/tests/observer_tests.rs index ae00f3cc2..a1ca7cc13 100644 --- a/components/chainhook-cli/src/service/tests/observer_tests.rs +++ b/components/chainhook-cli/src/service/tests/observer_tests.rs @@ -10,6 +10,7 @@ use serde_json::Value; use test_case::test_case; use crate::service::tests::{ + cleanup, cleanup_err, helpers::{ build_predicates::build_stacks_payload, mock_service::{ @@ -26,7 +27,7 @@ use super::helpers::{ #[tokio::test] #[cfg_attr(not(feature = "redis_tests"), ignore)] -async fn ping_endpoint_returns_metrics() { +async fn ping_endpoint_returns_metrics() -> Result<(), String> { let TestSetupResult { mut redis_process, working_dir, @@ -43,19 +44,12 @@ async fn ping_endpoint_returns_metrics() { let predicate = build_stacks_payload(Some("devnet"), None, None, None, Some(uuid)); let _ = call_register_predicate(&predicate, chainhook_service_port) .await - .unwrap_or_else(|e| { - std::fs::remove_dir_all(&working_dir).unwrap(); - flush_redis(redis_port); - redis_process.kill().unwrap(); - panic!("test failed with error: {e}"); - }); - - let metrics = call_ping(stacks_ingestion_port).await.unwrap_or_else(|e| { - std::fs::remove_dir_all(&working_dir).unwrap(); - flush_redis(redis_port); - redis_process.kill().unwrap(); - panic!("test failed with error: {e}"); - }); + .map_err(|e| cleanup_err(e, &working_dir, redis_port, &mut redis_process))?; + + sleep(Duration::new(1, 0)); + let metrics = call_ping(stacks_ingestion_port) + .await + .map_err(|e| cleanup_err(e, &working_dir, redis_port, &mut redis_process))?; let result = metrics .get("stacks") .unwrap() @@ -63,14 +57,14 @@ async fn ping_endpoint_returns_metrics() { .unwrap(); assert_eq!(result, 1); - std::fs::remove_dir_all(&working_dir).unwrap(); - flush_redis(redis_port); - redis_process.kill().unwrap(); + sleep(Duration::new(1, 0)); + cleanup(&working_dir, redis_port, &mut redis_process); + Ok(()) } #[tokio::test] #[cfg_attr(not(feature = "redis_tests"), ignore)] -async fn prometheus_endpoint_returns_encoded_metrics() { +async fn prometheus_endpoint_returns_encoded_metrics() -> Result<(), String> { let TestSetupResult { mut redis_process, working_dir, @@ -85,27 +79,21 @@ async fn prometheus_endpoint_returns_encoded_metrics() { let uuid = &get_random_uuid(); let predicate = build_stacks_payload(Some("devnet"), None, None, None, Some(uuid)); - let _ = call_register_predicate(&predicate, chainhook_service_port) + call_register_predicate(&predicate, chainhook_service_port) .await - .unwrap_or_else(|e| { - std::fs::remove_dir_all(&working_dir).unwrap(); - flush_redis(redis_port); - redis_process.kill().unwrap(); - panic!("test failed with error: {e}"); - }); - - let metrics = call_prometheus(prometheus_port).await.unwrap_or_else(|e| { - std::fs::remove_dir_all(&working_dir).unwrap(); - flush_redis(redis_port); - redis_process.kill().unwrap(); - panic!("test failed with error: {e}"); - }); + .map_err(|e| cleanup_err(e, &working_dir, redis_port, &mut redis_process))?; + + sleep(Duration::new(1, 0)); + let metrics = call_prometheus(prometheus_port) + .await + .map_err(|e| cleanup_err(e, &working_dir, redis_port, &mut redis_process))?; + const EXPECTED: &'static str = "# HELP chainhook_stx_registered_predicates The number of Stacks predicates that have been registered by the Chainhook node.\n# TYPE chainhook_stx_registered_predicates gauge\nchainhook_stx_registered_predicates 1\n"; assert!(metrics.contains(EXPECTED)); - std::fs::remove_dir_all(&working_dir).unwrap(); - flush_redis(redis_port); - redis_process.kill().unwrap(); + sleep(Duration::new(1, 0)); + cleanup(&working_dir, redis_port, &mut redis_process); + Ok(()) } async fn await_observer_started(port: u16) { diff --git a/wix/License.rtf b/components/chainhook-cli/wix/License.rtf similarity index 100% rename from wix/License.rtf rename to components/chainhook-cli/wix/License.rtf diff --git a/wix/main.wxs b/components/chainhook-cli/wix/main.wxs similarity index 86% rename from wix/main.wxs rename to components/chainhook-cli/wix/main.wxs index 2fccfeac6..b6ba6baec 100644 --- a/wix/main.wxs +++ b/components/chainhook-cli/wix/main.wxs @@ -33,7 +33,7 @@ - + - + - - + + - + @@ -157,7 +157,7 @@ Disabling the EULA dialog in the installer requires commenting out or removing the following `WixVariable` tag --> - + - + - + diff --git a/docs/chainhook-openapi.json b/docs/chainhook-openapi.json index 1ad29ab88..2db470b28 100644 --- a/docs/chainhook-openapi.json +++ b/docs/chainhook-openapi.json @@ -2,7 +2,7 @@ "openapi": "3.0.0", "info": { "title": "chainhook", - "version": "1.6.0" + "version": "1.6.1" }, "paths": { "/ping": { diff --git a/docs/images/chainhook-banner.bmp b/docs/images/chainhook-banner.bmp new file mode 100644 index 0000000000000000000000000000000000000000..e8305e95dffc8345718cf6836bbb71301db62761 GIT binary patch literal 114514 zcmeI*&ubJ{902eQiP7{Fh(Qxb2nbcdA6Ug;u@^7JLy;`8^*oMzzQR0}L?0 z00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u< z3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(un;;>D0{v$L~d&6+hU{(gY9YuAP+Po8AMhPba<9J3hgGr#}?3^b#G zSYIDMew@_T3l}b|xW0btb3m-&abL}tV`Fx2VSoV!7^szjy?ghDj~_oStK0A2zYjZh z?5McL?%A^^i4|iH4h~ivt5)L^$F;%$0}L?GNCqxly0olj$CZZ==!dQ)6+DV1NMz7^sJV_}qGKZZ4^}LqkJl*W2OY;iUi5r%%JWb?eHG zQID~S>6&4H0R|XoGz0PZb@mzgyLa!xwr$%=*4$mYc7+cgJ}j%*pFVvG`}XZC*|*X1 zXgux>3^2d|1FK=+^5x6R>TXt z-@Xl7w{A^p@Xnn(L!4t?*ka%P`}Zf?uGaC}k86bi1{h$VnGD3|+SxWUGqda&d7M)o z=ay&N)vH(4>zwPQnXVUO<(&Zr>SbVHU?7xQ;@{NIpFhu*<8bcWxv%v&&MD8f*k{wG zO`)f!r{q$v@rmo&VSoV!7-$p&**aUv`kOaz!sgAJKhGtPbIPk}jgn8}aBpCM0R|YT zmw{^PZ0vjg{{7E$$*YZ1uknfN+F^hJ1{i1*1J%~rTJ>*~d>V&)0|N{&z(BnW)T-uI z8>e356W6uF00Rs#zyJdbFu(u<3^2d|0}L?000Rs#zyJdbFu(u<3^2d|0}L?000Rs# zzyJdbFu(u<&2FID`QEka-|XkX_}z;bV1NMzN*SnC&8;?0spIrM%M38U00Yf#pxRnH zHZ~T<$H%J;^Y#BNZ}#(H{O(B%Fu(u<#SBzaY~y!;bar-z_3PJ%moHyd6H3kY-Cqna zzyJdblrXS;`}R<3Ie743^1axxKp#4E=xctD9zB}W?D$>Uv2UsP@%>_-DZK9UCIbvG zzyJf~8@PM-Zc>jYCMJ?Pd-LYaWV<_e?j+mEefhbUE4gBT0R|W-*TBh>CzCq->eZ{y z&%?7ZHf-1s78Vwg?PHAWw$>SBfB^;=V4!veHg4P)UcY{w)Zk;sjuoxJCr+G5wvRDl zEa#?n=SD8&k^u%7V4%>zty{N}`Ws&>+~!-q-@bi&Mg6`N%WF(BzyJdbFz|f?$B!RR z>Tdjdxu>V6WXFMbtZroQn z$7CCJ8DM|`1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz O7+`<_1{nDN8Tb!P!c&j{ literal 0 HcmV?d00001 diff --git a/docs/images/chainhook-dialog.bmp b/docs/images/chainhook-dialog.bmp new file mode 100644 index 0000000000000000000000000000000000000000..615ecd87cc350fd6869683195497543ea5a4a379 GIT binary patch literal 615402 zcmeI*d8{3E`S|hfDkEDFH|zKB9pf*Ul721J1n6p5gq0x?A6LimQ5 zxI|;zg8rdIi7AR8)c`>oQVc;tjK(ztu_B12rPTR#`u$zJ^mE&p`&rJJbNV{z&At81 zc|J4qp67YqGxyFp=YQVxw+m)Ys2`u*@%8Z?Uz;XQm~g_5uZa^DO_;pn|L6VxAK0fi-V~>6Dg%@6U#E!3NU;gr!XTECI z!;_bt`@hGGnqb0|(f`@;^Ri{jw)w)KWDcBk(n(vZs(Q>9268|S$bqyRsNL@igOWM$ z(wDw;N11vcfB*srq!K9EtIh3Ppw0o#POVb$2q1vK{}JF-UM-yioUPj+fB*t{2$b&E z=66oO*?DT!EC?Wg0H<$h{009Izl~YUS0B7qn z2q1t!9s;HNwfUVBaCV+rH46d=Ai$}dS~>?fTc1Gy0R-|8DBZ8k@0@_M^VF(Y5I_I{ zPUY0nIl$Ta3<3xskcU9&er^!w<76cGLfKxfObPjN~K7#-P2;?D9x?h{$IRR(qsa3NefB*uV%BiJufV1@(1Q0+V z4}sGC+WgK5I6F_Rngsy_5a3i!Eu90Lty~0;T)4`JEGRcAi=_ z3jzorz^R;CItMsgpFsct1o99l-LK8>oPe|Q)T&t!KmY+w<oW)-fIuDsrTew{ofB|&o?0~v0tg_$shnCm2RK`wK>z^+ z@(?K9ug&kAfV1<|s#y>~00B9u2q2J$KKWXAnRDfjk6C_iOVzC*bTnwQ3dw5I}%aIkj{SaJD{!00Ic)AyB$so8LJBXXmL^ zvmk%~0-VaJrE`F@^%(>ZKp+o+(*4@}&Ivd>Ppz5-0R#}>R8B3O1DvhTAbg*YUv!{Y<&g+1Q5tWpme`BzjFf4&Qq&qK>z^+IF(aN=KyEx zGYBAnKpq06`?dL<6L5B(S~Uv-2q3_zoLV{uI9s1V009K@5GdWR&F`Fmv-8xdSr9+~ z0Z!%A(mBA{`V0aHAdrVZ>3(f~=LDRcr&i5^00IbbDyNpt0nXNE5I_KdJOoPjYx6rN z;OsoLY8C_#K!8&@wR8?}wmyRZ0tn$h{009Izl~YUS0B7qn2q1t!9s;HNwfUVBaCV+r zH46d=Ai$}dS~>?fTc1Gy0R-|8DBZ8k@0@_M^VF(Y5I_I{PUY0nIl$Ta3<3xskcU9& zer^!w<76cGLfKxfObPjN~ zK7#-P2;?D9x?h{$IRR(qsa3NefB*uV%BiJufV1@(1Q0+V4}sGC+WgK5I6F_Rngsy_ z5a3i!Eu90Ltj^Q%ddCS~v2C9dDhIT*he;OsoLY8C_#Kw!kxqrWd(wyc^tb0%-Q=FXA> zoUP9wfB*t{2#j|1=$}`wUR@n@&_UJ2i4(K*ZP&zuaZOy}IB|BKS~Uv-2p}-p(_{U7 z*kOmcKh5=a(s>(a>oW)-fIuDsV|jYlu~n;9RWEwci<0_wiR(9W4#u@RXXmL^vmk%~ z0=xQoPyb)AV1fJ7Tyu~6+>NvK83YhOAP<2(IeM&R%a<>&X3d&4?!N7scrdPsOB^T8 z&Qq&qK>z^+#`1F0SbcxmOJ4F4_oun$ZguX)+4>9u2q2J$K$D;MwEXbH53i<8o7Spt zm$-g2=U`mBb9SCuH46d=Ah0JVw^>%-pSJ)0`}4MI?%n19XX`TvAb>y~0&Tq9<~fHP za!56K^5ot2ZP&zuaZOy}IB|BKS~Uv-2q4hL$8DdpV#Nygr@7t^&f7R!pFsct1o9AQ z>*Mh}&;4n8?=|&U*Xf*{r&i5^00Ib%$Hn7$?vf=-+@I!}d*?MbXX`TvAb>y~0^{-U zc%NI}pEhUCoZxq*V_l0G8{TVk&dyV-Wy~ z0x=GbiK*{Tn>~BBYwo?!-JG4LR?UI{0tm$THzj7?ym{U~Z7=gSXX`TvAb>y~0xABD zjXmOsBdY1sr@Q9fOWe)bd1}=x2q1t!tanr6jyme7>VN|d;O)J@+nlY>Ab^!w<76cGLAg60{&vodbhgLV= zd~?)6+PHCJHD$__&U$RD_iDZ2h8v>x_3_6aujbC3+u42P?5M*k2mu5T2ngi%Z0`9^ zJn_WpvBw?@w!6*1HP>9zS&!}N>=RBnq1wKEdz;+~o^#GQ=X82MIXgIH1`t31fhYpG z9b00)%P+q?YTveQ-CC_&xw6wftKD6{zs$oAKOD7>x7>0|=l74ZqYkYg1Q0+VAW*`w zCFZ>Cw%dYjZ!@rA!-mek{`=FP{&bsN37)fQ)26Eatnyv2*}L|C&JGTp0R#|0Ac{Z< zzm}Nu* zzm}TwiYu-Nw!h85mMvSV<;$0M&TrGEO{*S!@WD2_5IpDBTW{^${*3nY=$|<|ID7^W zKmdU#0;T*~YTny#zddR{@4N55YSN@ho$}oE*IysCFHb!2M0N1N2X|^;Mmu`fpE*0~ zFbhHe0R#d9rQBL--cwFFrTWJ|{t@g)n}PZ|eOI6K>;G%ktf{tb+ty~YgXf%g-g%wb zk2d@`J2->}5I_KdC<3M2T5{eGfB3^u`})i?&s6pMthMn>$>&U+I<

fd``YVcoiQ zec%4^vf=EgLoEma1P}-alyqy!x$n5+j$mKf4BUP9-F^QZ`L)+x+h$XP=REo3lhyqB z^Eo>pV*mjJ5ZF5il=N!Jx$EDN*MHZId0ckcWqtGBs#UA1XP+Gn*EeHVw5C{mA^lIt3>(_9_JpTRfe^-Ycc398-SHG9){rBG=v&mI`@7-t* z4${w@9d*bBA%Fk^0fEw9Ej|Cwe)h9qpV|zpU%$TRpZodIkAAexrUlP=>ZzxyLk>BF zvjZ{)5I_Kdy^BC;r;MjfS7Q;0D+J|X{YvL{@Cl+fBoxU)uKg zn4>QS0R#{T3G~9Jy;?{8dD$_K`W~+Z3l?PeWBp#K_uO+&%qCSo|M|}c{&(Ud=LMV{ zl>&kgKmdV&K(Bn-tM&Zi7rzMhvCY7bfBfU@&i>#BKiFo~!E@@@q8)zt;n}xm#G$?Z z&e_2sHh=&E2t*O+l}mfIp3_b{t*Wn^V;<+9e||Pk*3R9$d2`HWRF_|VdA9B7wV%gY zA7@9Ufgl7BKp-H{E0^|iJ=a}#U9g{R25Q&mxQ~3|#ECn9x7}u)yFRD>U3%9(e{3ud zk7tatcRkAfj{pJ)#1ZJFOMAJl`hCS?9`)am_uqg2r0d*EFTFHo)jRK1>%wQ4jK{}q zp3B)$=^zLJ1P}-a^wOifT-WPf_qwWn{dLUaV;}q2&i5UwUsv76Nu!@rzc#CWjaJN~ zeoffuxrXIu&W_0jF$f@lKuDmM9_{tIKJkf9gge|~q<+m<{r+n8dza0cHLJBp>)(mT zzL)BK_uW^;oFfc^*EzdI(s>F32q4gcK(9U8>-F7x@4YQ3giop6JF>0w=FMyMx~q#W zx+t8e#Yp|0rpuQvZ+&hs2%a9jjhx*g2|NV>1Q2LJV8lzq`u+6NPmlgx^+!JPk>|a= zWXY0h+O%oA^=kbY9u0N-HWUl(NujkB}Gp1jE@{^y06SNrFuwlc{{!V=49D}o4B!Z_PfB*t52#ok> z*uU3*FOGTCpIf~vn}rJ(zQD5=TyR0m)YaClTdS2TSB`lOFygym_|Dl;=^+RK1P}-a z?CPUo{r?$foKe;9FBbE-;)*K*Z`b#y&7M8Gnm>Pj^k;!yd+oJD@9X-UgR^6@P7DGF zAP^Ghr=xGX?Y5ntDP7-#)r+s~+qZ`RT8wPkw5dAzt4~0D%`0=%=%P`qQ7jaAP|Bzjf=@1R2oSm1a*$_YgfnF0BaZo?L57KpZeOw-Y z{PF5luX@!^Z{Ky-UA;b3$?N0nWP~O_009L0N}wOE9$Dw0IlDekx88bdb=6f@_4Sy= zuAH-D$%#V%0R*~1V8oyO_?~kARzK;*e#T}$x4B+7XZQOESU&;?Adp6&AFdu*=OA8Z zr_t_jG-vmhzqKQP00OB5M*P{2@0^`_^u`y@+2iBo*$5zjK-UQL!__0}W+y!Jl1Q0-=hXnfJYU@1u z`S(++eSP482lmCKz52{B?>FAVg0?CI5EyC#{P|yh_LIfp#fz&cQ>F~1yE%KPTZo-Q z0D&G7=%=Tw`-SHV^=E1CyYIe3=WWjJVL@9J0tgH>0p5I}H@m>2{!H#klO_#~yE%KP zTZo-Q0D&G7=z^ncLwMe>Y}vBDz1L{&zjJeT4-49=5I|t43Gij;%dRk~?@yaMckaMn zZ*%rgw-7sr00KQE&=o(~j@_R-)c2=NojP@3-ObrOENH7j0D++ZO$I*7GmcRK%j>Ny5uF>(tQ4~V8MccyFZPydsxs`g#ZFWO@JqxJ=rxz_5Eox zXU-g$Yi`aS>K0<>5I~@Z1iI!U+tcnGqP{=vpo0#o`t^RK>L6J^pHS5o$YhIdU&9B ztjgzl^_I7_A%Fk^xe0J~Zlc|tkF&dbY-||<2q4gg0B5(MG#t<2?BO_$_5uL}_Fe*< zz4sh@&fa^DwLL`uf#D?3PiNn8#~lL)b@S%URej&rAU-&I5RaN|MgRc>{!5^r&K|^j z&_4Uw&sJ}J>sza5pMCbf5C`NZ&K{5hWit^#0D;{Q;OyOi8Dn_k#*NjQHEXJx*%!X> zg)yGd?V(w-W_hhww-1r6LjVB;_DF!U_lW2Dg$3+TJ8UBY2n+%N&Te;HDW~b_Wi=O+WyFp&&5gpMHEFl)q;`^w2}qF~=MeIJ;){)vtbaVDezZ+4XiW zS+b;>Hf>r@epk-f1Ct^)6afS}PM{yI9$DvQmt9ufc;k&d`C7kzebkYt?=^e-+uuH> zuj~Cd{`li}zV~X(KD9jjKYs^-j@Q{3B}9g`hm5I_KdkidvPhxz*} zU-`|O7ASG9fn_L%9b`n+Xio_>8_xNu=LdGh39?&j>M)DVOK z0tf^I`sImUuJ`-j|9-IFZ3gPsVb}MXwb|Bhe)F4crVO6*x4-?Zs?RCftfS;}R;*Z2 z&7M8Gh_^XAIP3-xKmdU#0wq1rg}E=j_~Ixo&pr2C_0D&`v$acK@rqYefBfSgqt^Aq zAO5iQdP*~`{hIspJvlonEd(Ke00IGl(hlh7{3o7xV)fU*{x#U=HUnS(`q#(p(f7RP zJ=K5y^Pe`;2hX|Wl1s+DzEW(DIO2#Z=jV2Ec5t{2Ab4KmdV&z<7K#NYB0C zf(xRqdF$)s^Ugaj+MOxa_TT&7_oCNZ>ewt=v?%%hG|mpn&Hw@kAP_~MRIl&nyeFJ+ zLiMLV{V9sacfRwTWN*g)4ZYsr_r33Z$?MO}{>USbtoGSwpXh6D&W=h&K?op#KtLdO zfA7M4-}=_Kg8ghWQ2*__zD~}u54YTMOPf^&&-u$={!-QF967cj_Z$Zwd~nqLX`CG# zW&;QyfIt+1+@^S%4s?~Z=0^hFn4l+BeXuL-~T=9{x^NABYhbAK9VN0p`^1Q0+V zAdtJK_jA7bzOmo`{`bK?wi&3ei*s-LrI%jXX4S!S>c6R1OfuP>&JsO{*HINqpIHrCFb#g4}74Q8(;nES66@k``=?W ztNO+_zENzuavk6L{fQaEr+$CY`h7-Y9_!YvEAGV) zedt3mo3``s^5>p=Zt?BQaqM39vX|}l{xr^xI{bnVKmdV&K#qRh?>Xwv@rZeB+O(;9 z&1+uM6DQt&`|UBCxbri^>dy}AH{&+zU%GT@HDkt%;F_DWqYk|w1Q0+VAkd~4clkMI zpM7>!e_m$HqKm2g8Pi+S7yz|bUd+_S3uWqwx!E?54+g7bzySC@sn{sRyFJAoo z{b`&X98v=aAb>y=ft0@7@7T|O{_|1${N$5QR;QkNYF`|J0(AkIp&x)yXQFwAbzRf${d=EwI35h_hp|Rty3NAP^E5-CMi-^RIsOt8n96j10o{=;uE7xfXj8KINHb zo~hpSrZ;u=d}Jh_I6FLcMi4*%ffxcKeYMNqufP8Km|d#|(_9_JnFxPpK-<+o%J?n#~f8L2q1t!NT92( zuD9!)bIz&my6di}uln=8y1Z{=ZCm~RZa?_J52E(B{+sw%b9P~jv%|VGf&c;t#1QC0 zPqz(6chy&fz*C1p0As7fU{#N zQw#zKAP^F0>bI72a%v86c35{t5I_Kd7y_yNSUjgU}XVZ^{AAj;TyB2q1t!NT8|TTF%L-Il$Rr-5Egu0R&IE;yJx32RJ*XGQ}W(00JR_ zrhaQVC#U8BXNPrX1OWsPh#`>LkHvF(Qx0%;Ol68e009I-0!{taa!yXo0nQHV&Ikes zAP_?!wI7S;^rjr(?3l_Fg8%{ugan%Ut>v7Yngg62)}0Xq5I`V?Kx#i0&*@D$z}YdC zDFy)q5C{o0^;^q1IW-43JFGh+2q1t!41v^sES}Swa)7gADpL#s2p|v=XzI6?b8>19 zaCTUCMi4*%ffxd*{a8GwH{}3l$5f^m1Q0+VB+%4vE$8Ia9N_G*?u;OS00J=tQv0!Z zPH)Nq&W@=}F$f@lKuDme-&)SesX4&eVci))009JI2&DF7@toe21DqXGnPLz?0D+J| zQ@^#GlT&kmv%|VGf&c;t#1KgB$KpA?DF-+^rZUAKfB*s^fu??IIVY#)0B47FX9NKR z5Qrg=+Kz^+LIO?w)^bix%>m91>&^%Q2p|wcAhjQh=k%r=;Ov;n z6oUW)2!sTh`mN=hoSFli9oC%@1Q0+VhCpgR7SHKTIl$R5l_>@R1P}-bH1%7{IXN{4 zI6JI6BM2aXKn#J@ek`8Tn{t4&V=7Y&0tg@w5@_nTmUD7y4sdo@cSaCE0D%|+sr^_y zr#IyQXU9~g7z7YNASBS#Z!PEK)EwaKuz^+VhE)6WAU8clmna{Q<-8AKmdV|KvTc9oRd>?fV0E8GlBpD z2*eOb?Z@Idy(tGcJEk(lAb0R#{T2{iRv%Q-nU2RJ*dJ0l1nfItj^)P5|U z)0=XDvtue#3<3xs5E5wWx0Z8qY7TIASa(JcKmdUl0;&C2Jf}D10B6TkrWgbeKp-U0 z)Nd{4zfpsC+l&dI4cz}aEl89@L6 z1Y!uJ_G9s!-joBJ9aEWN5I_KdkU&$vwVabvbAYqMx-)_R0tmzqNbSesIlUerq`=r{(}>hjnKJ0R#|;A&}aS#dCU74sdo%Wr{%n0R%zy&fz*C1p3|FhfU{#NQw#zKAP^F0 z>bI72a%v86c35{t5I_Kd7y_yNSUjgU}XVZ^{AAj;TyB2q1t!NT8|TTF%L-Il$Rr-5Egu0R&IE;yJx32RJ*XGQ}W(00JR_rhaQVC#U8B zXNPrX1OWsPh#`>LkHvF(Qx0%;Ol68e009I-0!{taa!yXo0nQHV&IkesAP_?!wI7S; z^rjr(?3l_Fg8%{ugan%Ut>v7Yngg62)}0Xq5I`V?Kx#i0&*@D$z}YdCDFy)q5C{o0 z^;^q1IW-43JFGh+2q1t!41v^sES}Swa)7gADpL#s2p|v=XzI6?b8>19aCTUCMi4*% zffxd*{a8GwH{}3l$5f^m1Q0+VB+%4vE$8Ia9N_G*?u;OS00J=tQv0!ZPH)Nq&W@=} zF$f@lKuDme-&)SesX4&eVci))009JI2&DF7@toe21DqXGnPLz?0D+J|Q@^#GlT&km zv%|VGf&c;t#1KgB$KpA?DF-+^rZUAKfB*s^fu??IIVY#)0B47FX9NKR5Qrg=+Kz^+LIO?w)^bix%>m91>&^%Q2p|wcAhjQh=k%r=;Ov;n6oUW)2!sTh z`mN=hoSFli9oC%@1Q0+VhCpgR7SHKTIl$R5l_>@R1P}-bH1%7{IXN{4I6JI6BM2aX zKn#J@ek`8Tn{t4&V=7Y&0tg@w5@_nTmUD7y4sdo@cSaCE0D%|+sr^_yr#IyQXU9~g z7z7YNASBS#Z!PEK)EwaKuz^+VhE)6WAU8clmna{Q<-8AKmdV|KvTc9oRd>?fV0E8GlBpD2*eQhe~P-w AqW}N^ literal 0 HcmV?d00001 diff --git a/rust-toolchain b/rust-toolchain index 8142c3012..83025f972 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1,2 +1,2 @@ [toolchain] -channel = "1.73.0" +channel = "1.77.0" From 7537c9a0e4f7b75f9653eff938cffa668aefd7f5 Mon Sep 17 00:00:00 2001 From: Micaiah Reid Date: Wed, 29 May 2024 11:30:36 -0400 Subject: [PATCH 04/28] ci: update codecov threshold (#597) --- .github/codecov.yml | 5 +++++ .github/workflows/ci.yaml | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 .github/codecov.yml diff --git a/.github/codecov.yml b/.github/codecov.yml new file mode 100644 index 000000000..a9df3ec9b --- /dev/null +++ b/.github/codecov.yml @@ -0,0 +1,5 @@ +coverage: + status: + project: + default: + threshold: 3% \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5698f939e..a5eb6a541 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -85,9 +85,10 @@ jobs: cargo tarpaulin --out lcov --features redis_tests -- --test-threads=1 - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + token: ${{ secrets.CODECOV_TOKEN }} + codecov_yml_path: .github/codecov.yml dist_chainhook: name: Build Chainhook Distributions From bfa0362d9efbdfc6661f6bde2284dbceba1ca2d0 Mon Sep 17 00:00:00 2001 From: Micaiah Reid Date: Wed, 29 May 2024 15:27:40 -0400 Subject: [PATCH 05/28] ci: fix logic to auto-approve pushing to dev/stg (#596) --- .github/workflows/ci.yaml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a5eb6a541..bf3faaae7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -419,8 +419,10 @@ jobs: auto-approve-dev: runs-on: ubuntu-latest - if: needs.build-publish.outputs.new_release_published == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) - needs: build-publish + if: startsWith(github.ref, 'refs/heads/main') && needs.get_release_info.outputs.tag != '' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) + needs: + - build-publish + - get_release_info steps: - name: Approve pending deployments run: | @@ -461,10 +463,11 @@ jobs: auto-approve-stg: runs-on: ubuntu-latest - if: needs.build-publish.outputs.new_release_published == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) + if: startsWith(github.ref, 'refs/heads/main') && needs.get_release_info.outputs.tag != '' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) needs: - build-publish - deploy-dev + - get_release_info steps: - name: Approve pending deployments run: | @@ -482,7 +485,8 @@ jobs: needs: - build-publish - deploy-staging - if: needs.build-publish.outputs.new_release_published == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) + - get_release_info + if: startsWith(github.ref, 'refs/heads/main') && needs.get_release_info.outputs.tag != '' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) env: DEPLOY_ENV: prd environment: From f523ba0c79fcfd390e3e525aab4198e5b1cbbb0a Mon Sep 17 00:00:00 2001 From: Micaiah Reid Date: Thu, 30 May 2024 10:29:52 -0400 Subject: [PATCH 06/28] fix: print to console for unknown cli commands (#595) Previously, these were output as `CRIT` errors, but they should just be printed to the console. --- components/chainhook-cli/src/cli/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/chainhook-cli/src/cli/mod.rs b/components/chainhook-cli/src/cli/mod.rs index ce5bce997..005c4662a 100644 --- a/components/chainhook-cli/src/cli/mod.rs +++ b/components/chainhook-cli/src/cli/mod.rs @@ -322,7 +322,7 @@ pub fn main() { let opts: Opts = match Opts::try_parse() { Ok(opts) => opts, Err(e) => { - crit!(ctx.expect_logger(), "{e}"); + println!("{}", e); process::exit(1); } }; From 75a72785e1c2f88e9ff22796f456c4b6fa7e6f3b Mon Sep 17 00:00:00 2001 From: Micaiah Reid Date: Thu, 30 May 2024 12:57:45 -0400 Subject: [PATCH 07/28] fix: use `stacks_node_rpc_url` from config (#579) --- .github/workflows/ci.yaml | 1 - components/chainhook-sdk/src/observer/mod.rs | 16 +++++++++------- components/chainhook-types-rs/src/lib.rs | 2 ++ components/chainhook-types-rs/src/rosetta.rs | 11 +++++++++-- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bf3faaae7..a1066d6b4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -317,7 +317,6 @@ jobs: event-type: released client-payload: '{"tag": "${{ needs.get_release_info.outputs.tag }}"}' - build-publish: runs-on: ubuntu-latest needs: diff --git a/components/chainhook-sdk/src/observer/mod.rs b/components/chainhook-sdk/src/observer/mod.rs index b7d5685e6..4f5197371 100644 --- a/components/chainhook-sdk/src/observer/mod.rs +++ b/components/chainhook-sdk/src/observer/mod.rs @@ -28,6 +28,7 @@ use chainhook_types::{ BitcoinBlockData, BitcoinBlockSignaling, BitcoinChainEvent, BitcoinChainUpdatedWithBlocksData, BitcoinChainUpdatedWithReorgData, BitcoinNetwork, BlockIdentifier, BlockchainEvent, StacksBlockData, StacksChainEvent, StacksNetwork, StacksNodeConfig, TransactionIdentifier, + DEFAULT_STACKS_NODE_RPC, }; use hiro_system_kit; use hiro_system_kit::slog; @@ -176,13 +177,14 @@ impl EventObserverConfig { bitcoin_block_signaling: overrides .and_then(|c| c.bitcoind_zmq_url.as_ref()) .map(|url| BitcoinBlockSignaling::ZeroMQ(url.clone())) - .unwrap_or(BitcoinBlockSignaling::Stacks( - StacksNodeConfig::default_localhost( - overrides - .and_then(|c| c.ingestion_port) - .unwrap_or(DEFAULT_INGESTION_PORT), - ), - )), + .unwrap_or(BitcoinBlockSignaling::Stacks(StacksNodeConfig::new( + overrides + .and_then(|c| c.stacks_node_rpc_url.clone()) + .unwrap_or(DEFAULT_STACKS_NODE_RPC.to_string()), + overrides + .and_then(|c| c.ingestion_port) + .unwrap_or(DEFAULT_INGESTION_PORT), + ))), display_logs: overrides.and_then(|c| c.display_logs).unwrap_or(false), cache_path: overrides .and_then(|c| c.cache_path.clone()) diff --git a/components/chainhook-types-rs/src/lib.rs b/components/chainhook-types-rs/src/lib.rs index 6cf038e06..b5242d163 100644 --- a/components/chainhook-types-rs/src/lib.rs +++ b/components/chainhook-types-rs/src/lib.rs @@ -16,6 +16,8 @@ pub use ordinals::*; pub use processors::*; pub use rosetta::*; +pub const DEFAULT_STACKS_NODE_RPC: &str = "http://localhost:20443"; + pub enum Chain { Bitcoin, Stacks, diff --git a/components/chainhook-types-rs/src/rosetta.rs b/components/chainhook-types-rs/src/rosetta.rs index f00df8cca..d917912cd 100644 --- a/components/chainhook-types-rs/src/rosetta.rs +++ b/components/chainhook-types-rs/src/rosetta.rs @@ -1,7 +1,7 @@ use super::bitcoin::{TxIn, TxOut}; use crate::contract_interface::ContractInterface; use crate::ordinals::OrdinalOperation; -use crate::{events::*, Brc20Operation}; +use crate::{events::*, Brc20Operation, DEFAULT_STACKS_NODE_RPC}; use schemars::JsonSchema; use std::cmp::Ordering; use std::collections::HashSet; @@ -870,9 +870,16 @@ pub struct StacksNodeConfig { } impl StacksNodeConfig { + pub fn new(rpc_url: String, ingestion_port: u16) -> StacksNodeConfig { + StacksNodeConfig { + rpc_url, + ingestion_port, + } + } + pub fn default_localhost(ingestion_port: u16) -> StacksNodeConfig { StacksNodeConfig { - rpc_url: "http://localhost:20443".to_string(), + rpc_url: DEFAULT_STACKS_NODE_RPC.to_string(), ingestion_port, } } From 376a2c39140bc206aeaf7467f8727da5a27c5042 Mon Sep 17 00:00:00 2001 From: Micaiah Reid Date: Thu, 6 Jun 2024 15:54:58 -0400 Subject: [PATCH 08/28] chore(release): publish v1.6.2 (#604) --- CHANGELOG.md | 11 +++++++++++ Cargo.lock | 2 +- components/chainhook-cli/Cargo.toml | 2 +- docs/chainhook-openapi.json | 2 +- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 055db7160..733eac22c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## [1.6.2](https://github.com/hirosystems/chainhook/compare/v1.6.1...v1.6.2) (2024-06-06) + +### Bug Fixes + +* use `stacks_node_rpc_url` from config (#579) +* print to console for unknown cli commands (#595) + +### CI +* revamp ci (#594) - This PR adds builds to our [Releases](https://github.com/hirosystems/chainhook/releases) page for Windows, Linux, and MacOS! + + ## [1.6.1](https://github.com/hirosystems/chainhook/compare/v1.6.0...v1.6.1) (2024-05-15) ### Bug Fixes diff --git a/Cargo.lock b/Cargo.lock index 538a2c23d..ebb0ba5ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -472,7 +472,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chainhook" -version = "1.6.1" +version = "1.6.2" dependencies = [ "ansi_term", "atty", diff --git a/components/chainhook-cli/Cargo.toml b/components/chainhook-cli/Cargo.toml index 596b76adc..182741464 100644 --- a/components/chainhook-cli/Cargo.toml +++ b/components/chainhook-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "chainhook" -version = "1.6.1" +version = "1.6.2" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/docs/chainhook-openapi.json b/docs/chainhook-openapi.json index 2db470b28..c8235911f 100644 --- a/docs/chainhook-openapi.json +++ b/docs/chainhook-openapi.json @@ -2,7 +2,7 @@ "openapi": "3.0.0", "info": { "title": "chainhook", - "version": "1.6.1" + "version": "1.6.2" }, "paths": { "/ping": { From a161034a76b16cd6426cf6a2f1c66e6c6e063912 Mon Sep 17 00:00:00 2001 From: Micaiah Reid Date: Tue, 18 Jun 2024 12:32:21 -0400 Subject: [PATCH 09/28] chore: update predicate scanning status on every trigger (#603) The predicate status is used to report data to users and to pick back up where the service left off on a restart. This change updates the scanning predicate status to be updated on every successful trigger of a scanning predicate, to be sure we don't scan blocks twice on a restart (if the status was set to a previous block, we'd start on that block at startup) --- components/chainhook-cli/src/scan/bitcoin.rs | 9 ++++++++- components/chainhook-cli/src/scan/stacks.rs | 10 +++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/components/chainhook-cli/src/scan/bitcoin.rs b/components/chainhook-cli/src/scan/bitcoin.rs index 886f60998..b3d3185ce 100644 --- a/components/chainhook-cli/src/scan/bitcoin.rs +++ b/components/chainhook-cli/src/scan/bitcoin.rs @@ -98,9 +98,14 @@ pub async fn scan_bitcoin_chainstate_via_rpc_using_predicate( let mut last_scanned_block_confirmations = 0; let http_client = build_http_client(); + let mut loop_did_trigger = false; while let Some(current_block_height) = block_heights_to_scan.pop_front() { if let Some(ref mut predicates_db_conn) = predicates_db_conn { - if number_of_blocks_scanned % 10 == 0 || number_of_blocks_scanned == 0 { + if number_of_blocks_scanned % 100 == 0 + || number_of_blocks_scanned == 0 + // if the last loop did trigger a predicate, update the status + || loop_did_trigger + { set_predicate_scanning_status( &predicate_spec.key(), number_of_blocks_to_scan, @@ -112,6 +117,7 @@ pub async fn scan_bitcoin_chainstate_via_rpc_using_predicate( ); } } + loop_did_trigger = false; if current_block_height > chain_tip { let prev_chain_tip = chain_tip; @@ -177,6 +183,7 @@ pub async fn scan_bitcoin_chainstate_via_rpc_using_predicate( Ok(actions) => { if actions > 0 { number_of_times_triggered += 1; + loop_did_trigger = true } actions_triggered += actions; Ok(()) diff --git a/components/chainhook-cli/src/scan/stacks.rs b/components/chainhook-cli/src/scan/stacks.rs index 009aa16da..b626da29d 100644 --- a/components/chainhook-cli/src/scan/stacks.rs +++ b/components/chainhook-cli/src/scan/stacks.rs @@ -224,9 +224,14 @@ pub async fn scan_stacks_chainstate_via_rocksdb_using_predicate( } }; + let mut loop_did_trigger = false; while let Some(current_block_height) = block_heights_to_scan.pop_front() { if let Some(ref mut predicates_db_conn) = predicates_db_conn { - if number_of_blocks_scanned % 10 == 0 || number_of_blocks_scanned == 0 { + if number_of_blocks_scanned % 1000 == 0 + || number_of_blocks_scanned == 0 + // if the last loop did trigger a predicate, update the status + || loop_did_trigger + { set_predicate_scanning_status( &predicate_spec.key(), number_of_blocks_to_scan, @@ -238,6 +243,8 @@ pub async fn scan_stacks_chainstate_via_rocksdb_using_predicate( ); } } + loop_did_trigger = false; + if current_block_height > chain_tip { let prev_chain_tip = chain_tip; // we've scanned up to the chain tip as of the start of this scan @@ -316,6 +323,7 @@ pub async fn scan_stacks_chainstate_via_rocksdb_using_predicate( } Ok(action) => { number_of_times_triggered += 1; + loop_did_trigger = true; let res = match action { StacksChainhookOccurrence::Http(request, _) => { send_request(request, 3, 1, &ctx).await From 38c66eba30be639398ef318a23d8b0fef951ad07 Mon Sep 17 00:00:00 2001 From: Micaiah Reid Date: Mon, 24 Jun 2024 13:50:20 -0400 Subject: [PATCH 10/28] ci: fix winget pkg version bump (#606) --- .github/workflows/pkg-version-bump.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pkg-version-bump.yaml b/.github/workflows/pkg-version-bump.yaml index 2272fd45f..63d38a6bd 100644 --- a/.github/workflows/pkg-version-bump.yaml +++ b/.github/workflows/pkg-version-bump.yaml @@ -28,7 +28,7 @@ jobs: TAG: ${{ github.event.client_payload.tag || github.event.inputs.tag }} run: | # Get version info - VERSION=$(echo "${TAG#v}") + $VERSION=${env:TAG}.substring(1) # Configure git configs git config --global user.name "${env:GIT_USER_NAME}" @@ -44,3 +44,4 @@ jobs: --token ${{ secrets.GH_TOKEN }} ` --submit ` HiroSystems.Chainhook + From bb2c99d94ec5f4a4143aeba12af51cfb8c56ad7e Mon Sep 17 00:00:00 2001 From: Micaiah Reid Date: Wed, 26 Jun 2024 20:51:56 -0400 Subject: [PATCH 11/28] fix: allow aborting a predicate scan (#601) Previously, when Chainhook was run as a service and the runloop to scan stacks/bitcoin predicates was set, we had no way to abort that scan. If a predicate was set to scan 1m blocks, but the user discovered the predicate was wrong and needed to delete, the user could delete the predicate from the store, but the scan thread had already started and would run until completion. This PR adds an abort signal to an ongoing scan so that when a predicate is deregistered, the scan is canceled. --- components/chainhook-cli/src/cli/mod.rs | 2 + components/chainhook-cli/src/scan/bitcoin.rs | 23 +- components/chainhook-cli/src/scan/common.rs | 6 + components/chainhook-cli/src/scan/stacks.rs | 39 ++- components/chainhook-cli/src/service/mod.rs | 44 ++- .../chainhook-cli/src/service/runloops.rs | 277 ++++++++++-------- .../chainhook-cli/src/service/tests/mod.rs | 1 + .../src/service/tests/runloop_tests.rs | 169 +++++++++++ components/chainhook-sdk/src/observer/mod.rs | 40 ++- .../chainhook-sdk/src/observer/tests/mod.rs | 22 +- components/chainhook-types-rs/src/lib.rs | 1 + 11 files changed, 477 insertions(+), 147 deletions(-) create mode 100644 components/chainhook-cli/src/service/tests/runloop_tests.rs diff --git a/components/chainhook-cli/src/cli/mod.rs b/components/chainhook-cli/src/cli/mod.rs index 005c4662a..387b6dac8 100644 --- a/components/chainhook-cli/src/cli/mod.rs +++ b/components/chainhook-cli/src/cli/mod.rs @@ -517,6 +517,7 @@ async fn handle_command(opts: Opts, ctx: Context) -> Result<(), String> { &predicate_spec, None, &config, + None, &ctx, ) .await?; @@ -545,6 +546,7 @@ async fn handle_command(opts: Opts, ctx: Context) -> Result<(), String> { None, &db_conn, &config, + None, &ctx, ) .await?; diff --git a/components/chainhook-cli/src/scan/bitcoin.rs b/components/chainhook-cli/src/scan/bitcoin.rs index b3d3185ce..0f80e7f54 100644 --- a/components/chainhook-cli/src/scan/bitcoin.rs +++ b/components/chainhook-cli/src/scan/bitcoin.rs @@ -22,13 +22,17 @@ use chainhook_sdk::types::{ }; use chainhook_sdk::utils::{file_append, send_request, Context}; use std::collections::HashMap; +use std::sync::{Arc, RwLock}; + +use super::common::PredicateScanResult; pub async fn scan_bitcoin_chainstate_via_rpc_using_predicate( predicate_spec: &BitcoinChainhookSpecification, unfinished_scan_data: Option, config: &Config, + kill_signal: Option>>, ctx: &Context, -) -> Result { +) -> Result { let predicate_uuid = &predicate_spec.uuid; let auth = Auth::UserPass( config.network.bitcoind_rpc_username.clone(), @@ -62,7 +66,7 @@ pub async fn scan_bitcoin_chainstate_via_rpc_using_predicate( let mut block_heights_to_scan = match block_heights_to_scan { Some(h) => h, // no blocks to scan, go straight to streaming - None => return Ok(false), + None => return Ok(PredicateScanResult::ChainTipReached), }; let mut predicates_db_conn = match config.http_api { @@ -100,6 +104,17 @@ pub async fn scan_bitcoin_chainstate_via_rpc_using_predicate( let mut loop_did_trigger = false; while let Some(current_block_height) = block_heights_to_scan.pop_front() { + if let Some(kill_signal) = kill_signal.clone() { + match kill_signal.read() { + Ok(kill_signal) => { + // if true, we're received the kill signal, so break out of the loop + if *kill_signal { + return Ok(PredicateScanResult::Deregistered); + } + } + Err(_) => {} + } + } if let Some(ref mut predicates_db_conn) = predicates_db_conn { if number_of_blocks_scanned % 100 == 0 || number_of_blocks_scanned == 0 @@ -242,10 +257,10 @@ pub async fn scan_bitcoin_chainstate_via_rpc_using_predicate( set_confirmed_expiration_status(&predicate_spec.key(), predicates_db_conn, ctx); } } - return Ok(true); + return Ok(PredicateScanResult::Expired); } - return Ok(false); + return Ok(PredicateScanResult::ChainTipReached); } pub async fn process_block_with_predicates( diff --git a/components/chainhook-cli/src/scan/common.rs b/components/chainhook-cli/src/scan/common.rs index c6261dff8..ee5ddef61 100644 --- a/components/chainhook-cli/src/scan/common.rs +++ b/components/chainhook-cli/src/scan/common.rs @@ -65,3 +65,9 @@ pub fn get_block_heights_to_scan( }; Ok(block_heights_to_scan) } + +pub enum PredicateScanResult { + ChainTipReached, + Expired, + Deregistered, +} diff --git a/components/chainhook-cli/src/scan/stacks.rs b/components/chainhook-cli/src/scan/stacks.rs index b626da29d..5212af0ec 100644 --- a/components/chainhook-cli/src/scan/stacks.rs +++ b/components/chainhook-cli/src/scan/stacks.rs @@ -1,4 +1,7 @@ -use std::collections::{HashMap, VecDeque}; +use std::{ + collections::{HashMap, VecDeque}, + sync::{Arc, RwLock}, +}; use crate::{ archive::download_stacks_dataset_if_required, @@ -29,6 +32,8 @@ use chainhook_sdk::{ }; use rocksdb::DB; +use super::common::PredicateScanResult; + #[derive(Debug, Clone, Eq, PartialEq)] pub enum DigestingCommand { DigestSeedBlock(BlockIdentifier), @@ -170,16 +175,17 @@ pub async fn scan_stacks_chainstate_via_rocksdb_using_predicate( unfinished_scan_data: Option, stacks_db_conn: &DB, config: &Config, + kill_signal: Option>>, ctx: &Context, -) -> Result<(Option, bool), String> { +) -> Result { let predicate_uuid = &predicate_spec.uuid; let mut chain_tip = match get_last_unconfirmed_block_height_inserted(stacks_db_conn, ctx) { Some(chain_tip) => chain_tip, None => match get_last_block_height_inserted(stacks_db_conn, ctx) { Some(chain_tip) => chain_tip, None => { - info!(ctx.expect_logger(), "No blocks inserted in db; cannot determing Stacks chain tip. Skipping scan of predicate {}", predicate_uuid); - return Ok((None, false)); + info!(ctx.expect_logger(), "No blocks inserted in db; cannot determine Stacks chain tip. Skipping scan of predicate {}", predicate_uuid); + return Ok(PredicateScanResult::ChainTipReached); } }, }; @@ -194,7 +200,13 @@ pub async fn scan_stacks_chainstate_via_rocksdb_using_predicate( let mut block_heights_to_scan = match block_heights_to_scan { Some(h) => h, // no blocks to scan, go straight to streaming - None => return Ok((None, false)), + None => { + debug!( + ctx.expect_logger(), + "Stacks chainstate scan completed. 0 blocks scanned." + ); + return Ok(PredicateScanResult::ChainTipReached); + } }; let mut predicates_db_conn = match config.http_api { @@ -226,6 +238,17 @@ pub async fn scan_stacks_chainstate_via_rocksdb_using_predicate( let mut loop_did_trigger = false; while let Some(current_block_height) = block_heights_to_scan.pop_front() { + if let Some(kill_signal) = kill_signal.clone() { + match kill_signal.read() { + Ok(kill_signal) => { + // if true, we're received the kill signal, so break out of the loop + if *kill_signal { + return Ok(PredicateScanResult::Deregistered); + } + } + Err(_) => {} + } + } if let Some(ref mut predicates_db_conn) = predicates_db_conn { if number_of_blocks_scanned % 1000 == 0 || number_of_blocks_scanned == 0 @@ -255,7 +278,7 @@ pub async fn scan_stacks_chainstate_via_rocksdb_using_predicate( Some(chain_tip) => chain_tip, None => { warn!(ctx.expect_logger(), "No blocks inserted in db; cannot determine Stacks chain tip. Skipping scan of predicate {}", predicate_uuid); - return Ok((None, false)); + return Ok(PredicateScanResult::ChainTipReached); } }, }; @@ -411,10 +434,10 @@ pub async fn scan_stacks_chainstate_via_rocksdb_using_predicate( set_confirmed_expiration_status(&predicate_spec.key(), predicates_db_conn, ctx); } } - return Ok((Some(last_block_scanned), true)); + return Ok(PredicateScanResult::Expired); } - Ok((Some(last_block_scanned), false)) + Ok(PredicateScanResult::ChainTipReached) } pub async fn scan_stacks_chainstate_via_csv_using_predicate( diff --git a/components/chainhook-cli/src/service/mod.rs b/components/chainhook-cli/src/service/mod.rs index 2e74bb5bb..e62f76bb8 100644 --- a/components/chainhook-cli/src/service/mod.rs +++ b/components/chainhook-cli/src/service/mod.rs @@ -16,7 +16,8 @@ use chainhook_sdk::chainhooks::types::{ChainhookConfig, ChainhookFullSpecificati use chainhook_sdk::chainhooks::types::ChainhookSpecification; use chainhook_sdk::observer::{ start_event_observer, HookExpirationData, ObserverCommand, ObserverEvent, - PredicateEvaluationReport, PredicateInterruptedData, StacksObserverStartupContext, + PredicateDeregisteredEvent, PredicateEvaluationReport, PredicateInterruptedData, + StacksObserverStartupContext, }; use chainhook_sdk::types::{Chain, StacksBlockData, StacksChainEvent}; use chainhook_sdk::utils::Context; @@ -26,6 +27,7 @@ use std::sync::mpsc::{channel, Receiver, Sender}; use std::time::{SystemTime, UNIX_EPOCH}; use self::http_api::get_entry_from_predicates_db; +use self::runloops::{BitcoinScanOp, StacksScanOp}; pub struct Service { config: Config, @@ -304,10 +306,16 @@ impl Service { for predicate_with_last_scanned_block in leftover_scans { match predicate_with_last_scanned_block { (ChainhookSpecification::Stacks(spec), last_scanned_block) => { - let _ = stacks_scan_op_tx.send((spec, last_scanned_block)); + let _ = stacks_scan_op_tx.send(StacksScanOp::StartScan { + predicate_spec: spec, + unfinished_scan_data: last_scanned_block, + }); } (ChainhookSpecification::Bitcoin(spec), last_scanned_block) => { - let _ = bitcoin_scan_op_tx.send((spec, last_scanned_block)); + let _ = bitcoin_scan_op_tx.send(BitcoinScanOp::StartScan { + predicate_spec: spec, + unfinished_scan_data: last_scanned_block, + }); } } } @@ -354,10 +362,16 @@ impl Service { } match spec { ChainhookSpecification::Stacks(predicate_spec) => { - let _ = stacks_scan_op_tx.send((predicate_spec, None)); + let _ = stacks_scan_op_tx.send(StacksScanOp::StartScan { + predicate_spec, + unfinished_scan_data: None, + }); } ChainhookSpecification::Bitcoin(predicate_spec) => { - let _ = bitcoin_scan_op_tx.send((predicate_spec, None)); + let _ = bitcoin_scan_op_tx.send(BitcoinScanOp::StartScan { + predicate_spec, + unfinished_scan_data: None, + }); } } } @@ -382,14 +396,30 @@ impl Service { ); } } - ObserverEvent::PredicateDeregistered(uuid) => { + ObserverEvent::PredicateDeregistered(PredicateDeregisteredEvent { + predicate_uuid, + chain, + }) => { if let PredicatesApi::On(ref config) = self.config.http_api { let Ok(mut predicates_db_conn) = open_readwrite_predicates_db_conn_verbose(&config, &ctx) else { continue; }; - let predicate_key = ChainhookSpecification::either_stx_or_btc_key(&uuid); + + match chain { + Chain::Bitcoin => { + let _ = bitcoin_scan_op_tx + .send(BitcoinScanOp::KillScan(predicate_uuid.clone())); + } + Chain::Stacks => { + let _ = stacks_scan_op_tx + .send(StacksScanOp::KillScan(predicate_uuid.clone())); + } + }; + + let predicate_key = + ChainhookSpecification::either_stx_or_btc_key(&predicate_uuid); let res: Result<(), redis::RedisError> = predicates_db_conn.del(predicate_key.clone()); if let Err(e) = res { diff --git a/components/chainhook-cli/src/service/runloops.rs b/components/chainhook-cli/src/service/runloops.rs index a0b85b472..c2f28f394 100644 --- a/components/chainhook-cli/src/service/runloops.rs +++ b/components/chainhook-cli/src/service/runloops.rs @@ -1,4 +1,7 @@ -use std::sync::mpsc::Sender; +use std::{ + collections::HashMap, + sync::{mpsc::Sender, Arc, RwLock}, +}; use chainhook_sdk::{ chainhooks::types::{ @@ -12,7 +15,7 @@ use threadpool::ThreadPool; use crate::{ config::{Config, PredicatesApi}, scan::{ - bitcoin::scan_bitcoin_chainstate_via_rpc_using_predicate, + bitcoin::scan_bitcoin_chainstate_via_rpc_using_predicate, common::PredicateScanResult, stacks::scan_stacks_chainstate_via_rocksdb_using_predicate, }, service::{open_readwrite_predicates_db_conn_or_panic, set_predicate_interrupted_status}, @@ -21,149 +24,191 @@ use crate::{ use super::ScanningData; +pub enum StacksScanOp { + StartScan { + predicate_spec: StacksChainhookSpecification, + unfinished_scan_data: Option, + }, + KillScan(String), +} + pub fn start_stacks_scan_runloop( config: &Config, - stacks_scan_op_rx: crossbeam_channel::Receiver<( - StacksChainhookSpecification, - Option, - )>, + stacks_scan_op_rx: crossbeam_channel::Receiver, observer_command_tx: Sender, ctx: &Context, ) { let stacks_scan_pool = ThreadPool::new(config.limits.max_number_of_concurrent_stacks_scans); - while let Ok((predicate_spec, unfinished_scan_data)) = stacks_scan_op_rx.recv() { - let moved_ctx = ctx.clone(); - let moved_config = config.clone(); - let observer_command_tx = observer_command_tx.clone(); - stacks_scan_pool.execute(move || { - let stacks_db_conn = - match open_readonly_stacks_db_conn(&moved_config.expected_cache_path(), &moved_ctx) - { - Ok(db_conn) => db_conn, - Err(e) => { - // todo: if we repeatedly can't connect to the database, we should restart the - // service to get to a healthy state. I don't know if this has been an issue, though - // so we can monitor and possibly remove this todo - error!( - moved_ctx.expect_logger(), - "unable to open stacks db: {}", - e.to_string() - ); - unimplemented!() - } - }; + let mut kill_signals = HashMap::new(); - let op = scan_stacks_chainstate_via_rocksdb_using_predicate( - &predicate_spec, + while let Ok(op) = stacks_scan_op_rx.recv() { + match op { + StacksScanOp::StartScan { + predicate_spec, unfinished_scan_data, - &stacks_db_conn, - &moved_config, - &moved_ctx, - ); - let res = hiro_system_kit::nestable_block_on(op); - let (last_block_scanned, predicate_is_expired) = match res { - Ok(last_block_scanned) => last_block_scanned, - Err(e) => { - warn!( - moved_ctx.expect_logger(), - "Unable to evaluate predicate on Stacks chainstate: {e}", + } => { + let moved_ctx = ctx.clone(); + let moved_config = config.clone(); + let observer_command_tx = observer_command_tx.clone(); + let kill_signal = Arc::new(RwLock::new(false)); + kill_signals.insert(predicate_spec.uuid.clone(), kill_signal.clone()); + stacks_scan_pool.execute(move || { + let stacks_db_conn = match open_readonly_stacks_db_conn( + &moved_config.expected_cache_path(), + &moved_ctx, + ) { + Ok(db_conn) => db_conn, + Err(e) => { + // todo: if we repeatedly can't connect to the database, we should restart the + // service to get to a healthy state. I don't know if this has been an issue, though + // so we can monitor and possibly remove this todo + error!( + moved_ctx.expect_logger(), + "unable to open stacks db: {}", + e.to_string() + ); + unimplemented!() + } + }; + + let op = scan_stacks_chainstate_via_rocksdb_using_predicate( + &predicate_spec, + unfinished_scan_data, + &stacks_db_conn, + &moved_config, + Some(kill_signal), + &moved_ctx, ); + let res = hiro_system_kit::nestable_block_on(op); + match res { + Ok(PredicateScanResult::Expired) + | Ok(PredicateScanResult::Deregistered) => {} + Ok(PredicateScanResult::ChainTipReached) => { + let _ = observer_command_tx.send(ObserverCommand::EnablePredicate( + ChainhookSpecification::Stacks(predicate_spec), + )); + } + Err(e) => { + warn!( + moved_ctx.expect_logger(), + "Unable to evaluate predicate on Stacks chainstate: {e}", + ); - // Update predicate status in redis - if let PredicatesApi::On(ref api_config) = moved_config.http_api { - let error = - format!("Unable to evaluate predicate on Stacks chainstate: {e}"); - let mut predicates_db_conn = - open_readwrite_predicates_db_conn_or_panic(api_config, &moved_ctx); - set_predicate_interrupted_status( - error, - &predicate_spec.key(), - &mut predicates_db_conn, - &moved_ctx, - ); - } + // Update predicate status in redis + if let PredicatesApi::On(ref api_config) = moved_config.http_api { + let error = format!( + "Unable to evaluate predicate on Stacks chainstate: {e}" + ); + let mut predicates_db_conn = + open_readwrite_predicates_db_conn_or_panic( + api_config, &moved_ctx, + ); + set_predicate_interrupted_status( + error, + &predicate_spec.key(), + &mut predicates_db_conn, + &moved_ctx, + ); + } - return; - } - }; - match last_block_scanned { - Some(last_block_scanned) => { - info!( - moved_ctx.expect_logger(), - "Stacks chainstate scan completed up to block: {}", - last_block_scanned.index - ); - } - None => { - info!( - moved_ctx.expect_logger(), - "Stacks chainstate scan completed. 0 blocks scanned." - ); - } + return; + } + }; + }); } - if !predicate_is_expired { - let _ = observer_command_tx.send(ObserverCommand::EnablePredicate( - ChainhookSpecification::Stacks(predicate_spec), - )); + StacksScanOp::KillScan(predicate_uuid) => { + let Some(kill_signal) = kill_signals.remove(&predicate_uuid) else { + continue; + }; + let mut kill_signal_writer = kill_signal.write().unwrap(); + *kill_signal_writer = true; } - }); + } } let _ = stacks_scan_pool.join(); } +pub enum BitcoinScanOp { + StartScan { + predicate_spec: BitcoinChainhookSpecification, + unfinished_scan_data: Option, + }, + KillScan(String), +} + pub fn start_bitcoin_scan_runloop( config: &Config, - bitcoin_scan_op_rx: crossbeam_channel::Receiver<( - BitcoinChainhookSpecification, - Option, - )>, + bitcoin_scan_op_rx: crossbeam_channel::Receiver, observer_command_tx: Sender, ctx: &Context, ) { let bitcoin_scan_pool = ThreadPool::new(config.limits.max_number_of_concurrent_bitcoin_scans); + let mut kill_signals = HashMap::new(); - while let Ok((predicate_spec, unfinished_scan_data)) = bitcoin_scan_op_rx.recv() { - let moved_ctx = ctx.clone(); - let moved_config = config.clone(); - let observer_command_tx = observer_command_tx.clone(); - bitcoin_scan_pool.execute(move || { - let op = scan_bitcoin_chainstate_via_rpc_using_predicate( - &predicate_spec, + while let Ok(op) = bitcoin_scan_op_rx.recv() { + match op { + BitcoinScanOp::StartScan { + predicate_spec, unfinished_scan_data, - &moved_config, - &moved_ctx, - ); + } => { + let moved_ctx = ctx.clone(); + let moved_config = config.clone(); + let observer_command_tx = observer_command_tx.clone(); + let kill_signal = Arc::new(RwLock::new(false)); + kill_signals.insert(predicate_spec.uuid.clone(), kill_signal.clone()); - let predicate_is_expired = match hiro_system_kit::nestable_block_on(op) { - Ok(predicate_is_expired) => predicate_is_expired, - Err(e) => { - warn!( - moved_ctx.expect_logger(), - "Unable to evaluate predicate on Bitcoin chainstate: {e}", + bitcoin_scan_pool.execute(move || { + let op = scan_bitcoin_chainstate_via_rpc_using_predicate( + &predicate_spec, + unfinished_scan_data, + &moved_config, + Some(kill_signal), + &moved_ctx, ); - // Update predicate status in redis - if let PredicatesApi::On(ref api_config) = moved_config.http_api { - let error = - format!("Unable to evaluate predicate on Bitcoin chainstate: {e}"); - let mut predicates_db_conn = - open_readwrite_predicates_db_conn_or_panic(api_config, &moved_ctx); - set_predicate_interrupted_status( - error, - &predicate_spec.key(), - &mut predicates_db_conn, - &moved_ctx, - ) - } - return; - } - }; - if !predicate_is_expired { - let _ = observer_command_tx.send(ObserverCommand::EnablePredicate( - ChainhookSpecification::Bitcoin(predicate_spec), - )); + match hiro_system_kit::nestable_block_on(op) { + Ok(PredicateScanResult::Expired) + | Ok(PredicateScanResult::Deregistered) => {} + Ok(PredicateScanResult::ChainTipReached) => { + let _ = observer_command_tx.send(ObserverCommand::EnablePredicate( + ChainhookSpecification::Bitcoin(predicate_spec), + )); + } + Err(e) => { + warn!( + moved_ctx.expect_logger(), + "Unable to evaluate predicate on Bitcoin chainstate: {e}", + ); + + // Update predicate status in redis + if let PredicatesApi::On(ref api_config) = moved_config.http_api { + let error = format!( + "Unable to evaluate predicate on Bitcoin chainstate: {e}" + ); + let mut predicates_db_conn = + open_readwrite_predicates_db_conn_or_panic( + api_config, &moved_ctx, + ); + set_predicate_interrupted_status( + error, + &predicate_spec.key(), + &mut predicates_db_conn, + &moved_ctx, + ) + } + return; + } + }; + }); + } + BitcoinScanOp::KillScan(predicate_uuid) => { + let Some(kill_signal) = kill_signals.remove(&predicate_uuid) else { + continue; + }; + let mut kill_signal_writer = kill_signal.write().unwrap(); + *kill_signal_writer = true; } - }); + } } let _ = bitcoin_scan_pool.join(); } diff --git a/components/chainhook-cli/src/service/tests/mod.rs b/components/chainhook-cli/src/service/tests/mod.rs index be20a78cc..71e03ca3f 100644 --- a/components/chainhook-cli/src/service/tests/mod.rs +++ b/components/chainhook-cli/src/service/tests/mod.rs @@ -34,6 +34,7 @@ use super::http_api::document_predicate_api_server; pub mod helpers; mod observer_tests; +mod runloop_tests; async fn test_register_predicate(predicate: JsonValue) -> Result<(), (String, Shutdown)> { // perhaps a little janky, we bind to the port 0 to find an open one, then diff --git a/components/chainhook-cli/src/service/tests/runloop_tests.rs b/components/chainhook-cli/src/service/tests/runloop_tests.rs new file mode 100644 index 000000000..e8d8410fd --- /dev/null +++ b/components/chainhook-cli/src/service/tests/runloop_tests.rs @@ -0,0 +1,169 @@ +use std::{path::PathBuf, sync::mpsc::channel, thread::sleep, time::Duration}; + +use chainhook_sdk::{ + chainhooks::types::{ + BitcoinChainhookSpecification, BitcoinPredicateType, BlockIdentifierIndexRule, HookAction, + StacksChainhookSpecification, StacksPredicate, + }, + types::{BitcoinNetwork, StacksNetwork}, + utils::Context, +}; + +use crate::{ + config::{Config, EventSourceConfig, PathConfig}, + scan::stacks::consolidate_local_stacks_chainstate_using_csv, + service::{ + runloops::{ + start_bitcoin_scan_runloop, start_stacks_scan_runloop, BitcoinScanOp, StacksScanOp, + }, + tests::helpers::{ + mock_bitcoin_rpc::mock_bitcoin_rpc, mock_service::setup_chainhook_service_ports, + }, + }, +}; + +use super::helpers::mock_stacks_node::{create_tmp_working_dir, write_stacks_blocks_to_tsv}; + +#[tokio::test] +async fn test_stacks_runloop_kill_scan() { + let (working_dir, tsv_dir) = create_tmp_working_dir().unwrap_or_else(|e| { + panic!("test failed with error: {e}"); + }); + + write_stacks_blocks_to_tsv(1000, &tsv_dir).unwrap_or_else(|e| { + std::fs::remove_dir_all(&working_dir).unwrap(); + panic!("test failed with error: {e}"); + }); + + let mut config = Config::devnet_default(); + config.storage.working_dir = working_dir.clone(); + config.event_sources = vec![EventSourceConfig::StacksTsvPath(PathConfig { + file_path: PathBuf::from(tsv_dir), + })]; + + let logger = hiro_system_kit::log::setup_logger(); + let _guard = hiro_system_kit::log::setup_global_logger(logger.clone()); + let ctx = Context { + logger: Some(logger), + tracer: false, + }; + + consolidate_local_stacks_chainstate_using_csv(&mut config, &ctx) + .await + .unwrap_or_else(|e| { + std::fs::remove_dir_all(&working_dir).unwrap(); + panic!("test failed with error: {e}"); + }); + + let (scan_op_tx, scan_op_rx) = crossbeam_channel::unbounded(); + let (observer_command_tx, _observer_command_rx) = channel(); + + let _ = hiro_system_kit::thread_named("Stacks scan runloop") + .spawn(move || { + start_stacks_scan_runloop(&config, scan_op_rx, observer_command_tx.clone(), &ctx); + }) + .expect("unable to spawn thread"); + + let uuid = "test".to_string(); + let predicate_spec = StacksChainhookSpecification { + uuid: uuid.clone(), + owner_uuid: None, + name: "idc".to_string(), + network: StacksNetwork::Devnet, + version: 0, + blocks: None, + start_block: Some(1), + end_block: Some(1_000), + expire_after_occurrence: None, + capture_all_events: None, + decode_clarity_values: None, + include_contract_abi: None, + predicate: StacksPredicate::BlockHeight(BlockIdentifierIndexRule::LowerThan(0)), + action: HookAction::Noop, + enabled: false, + expired_at: None, + }; + let op = StacksScanOp::StartScan { + predicate_spec, + unfinished_scan_data: None, + }; + let _ = scan_op_tx.send(op); + sleep(Duration::new(0, 500_000)); + let _ = scan_op_tx.send(StacksScanOp::KillScan(uuid)); + sleep(Duration::new(0, 500_000)); + // todo: currently the scanning runloop is a bit of a black box. we have no insight + // into what or how many predicates are being scanned. so for this test, there's no + // good way to determine if we successfully killed the scan. + // this [issue](https://github.com/hirosystems/chainhook/issues/509) will give us + // more data on these threads. When this is done we should update these tests + // to do some actual verification that the predicate is no longer being scanned + std::fs::remove_dir_all(&working_dir).unwrap(); +} + +#[tokio::test] +async fn test_stacks_bitcoin_kill_scan() { + let (_, _, _, _, bitcoin_rpc_port, _) = + setup_chainhook_service_ports().unwrap_or_else(|e| panic!("test failed with error: {e}")); + + let _ = hiro_system_kit::thread_named("Bitcoin rpc service") + .spawn(move || { + let future = mock_bitcoin_rpc(bitcoin_rpc_port, 1_000); + let _ = hiro_system_kit::nestable_block_on(future); + }) + .expect("unable to spawn thread"); + + sleep(Duration::new(1, 0)); + let mut config = Config::devnet_default(); + config.network.bitcoind_rpc_url = format!("http://0.0.0.0:{bitcoin_rpc_port}"); + + let logger = hiro_system_kit::log::setup_logger(); + let _guard = hiro_system_kit::log::setup_global_logger(logger.clone()); + let ctx = Context { + logger: Some(logger), + tracer: false, + }; + + let (scan_op_tx, scan_op_rx) = crossbeam_channel::unbounded(); + let (observer_command_tx, _observer_command_rx) = channel(); + + let _ = hiro_system_kit::thread_named("Stacks scan runloop") + .spawn(move || { + start_bitcoin_scan_runloop(&config, scan_op_rx, observer_command_tx.clone(), &ctx); + }) + .expect("unable to spawn thread"); + + let uuid = "test".to_string(); + let predicate_spec = BitcoinChainhookSpecification { + uuid: uuid.clone(), + owner_uuid: None, + name: "idc".to_string(), + network: BitcoinNetwork::Regtest, + version: 0, + blocks: None, + start_block: Some(1), + end_block: Some(1_000), + expire_after_occurrence: None, + predicate: BitcoinPredicateType::Block, + action: HookAction::Noop, + enabled: false, + expired_at: None, + include_proof: false, + include_inputs: false, + include_outputs: false, + include_witness: false, + }; + + let op = BitcoinScanOp::StartScan { + predicate_spec, + unfinished_scan_data: None, + }; + let _ = scan_op_tx.send(op); + sleep(Duration::new(0, 50_000_000)); + let _ = scan_op_tx.send(BitcoinScanOp::KillScan(uuid)); + // todo: currently the scanning runloop is a bit of a black box. we have no insight + // into what or how many predicates are being scanned. so for this test, there's no + // good way to determine if we successfully killed the scan. + // this [issue](https://github.com/hirosystems/chainhook/issues/509) will give us + // more data on these threads. When this is done we should update these tests + // to do some actual verification that the predicate is no longer being scanned +} diff --git a/components/chainhook-sdk/src/observer/mod.rs b/components/chainhook-sdk/src/observer/mod.rs index 4f5197371..c4d2dbc40 100644 --- a/components/chainhook-sdk/src/observer/mod.rs +++ b/components/chainhook-sdk/src/observer/mod.rs @@ -26,7 +26,7 @@ use bitcoincore_rpc::bitcoin::{BlockHash, Txid}; use bitcoincore_rpc::{Auth, Client, RpcApi}; use chainhook_types::{ BitcoinBlockData, BitcoinBlockSignaling, BitcoinChainEvent, BitcoinChainUpdatedWithBlocksData, - BitcoinChainUpdatedWithReorgData, BitcoinNetwork, BlockIdentifier, BlockchainEvent, + BitcoinChainUpdatedWithReorgData, BitcoinNetwork, BlockIdentifier, BlockchainEvent, Chain, StacksBlockData, StacksChainEvent, StacksNetwork, StacksNodeConfig, TransactionIdentifier, DEFAULT_STACKS_NODE_RPC, }; @@ -312,7 +312,7 @@ pub enum ObserverEvent { StacksChainEvent((StacksChainEvent, PredicateEvaluationReport)), NotifyBitcoinTransactionProxied, PredicateRegistered(ChainhookSpecification), - PredicateDeregistered(String), + PredicateDeregistered(PredicateDeregisteredEvent), PredicateEnabled(ChainhookSpecification), BitcoinPredicateTriggered(BitcoinChainhookOccurrencePayload), StacksPredicateTriggered(StacksChainhookOccurrencePayload), @@ -322,6 +322,12 @@ pub enum ObserverEvent { StacksChainMempoolEvent(StacksChainMempoolEvent), } +#[derive(Clone, Debug)] +pub struct PredicateDeregisteredEvent { + pub predicate_uuid: String, + pub chain: Chain, +} + #[derive(Debug, Clone, Deserialize, Serialize)] /// JSONRPC Request pub struct BitcoinRPCRequest { @@ -1203,7 +1209,12 @@ pub async fn start_observer_commands_handler( prometheus_monitoring.btc_metrics_deregister_predicate(); } if let Some(ref tx) = observer_events_tx { - let _ = tx.send(ObserverEvent::PredicateDeregistered(hook_uuid.clone())); + let _ = tx.send(ObserverEvent::PredicateDeregistered( + PredicateDeregisteredEvent { + predicate_uuid: hook_uuid.clone(), + chain: Chain::Bitcoin, + }, + )); } } @@ -1384,7 +1395,12 @@ pub async fn start_observer_commands_handler( prometheus_monitoring.stx_metrics_deregister_predicate(); } if let Some(ref tx) = observer_events_tx { - let _ = tx.send(ObserverEvent::PredicateDeregistered(hook_uuid.clone())); + let _ = tx.send(ObserverEvent::PredicateDeregistered( + PredicateDeregisteredEvent { + predicate_uuid: hook_uuid.clone(), + chain: Chain::Stacks, + }, + )); } } @@ -1502,7 +1518,12 @@ pub async fn start_observer_commands_handler( }; // event if the predicate wasn't in the `chainhook_store`, propogate this event to delete from redis if let Some(tx) = &observer_events_tx { - let _ = tx.send(ObserverEvent::PredicateDeregistered(hook_uuid)); + let _ = tx.send(ObserverEvent::PredicateDeregistered( + PredicateDeregisteredEvent { + predicate_uuid: hook_uuid, + chain: Chain::Stacks, + }, + )); }; } ObserverCommand::DeregisterBitcoinPredicate(hook_uuid) => { @@ -1518,9 +1539,14 @@ pub async fn start_observer_commands_handler( // so only those that we find in the store should be removed prometheus_monitoring.btc_metrics_deregister_predicate(); }; - // event if the predicate wasn't in the `chainhook_store`, propogate this event to delete from redis + // even if the predicate wasn't in the `chainhook_store`, propogate this event to delete from redis if let Some(tx) = &observer_events_tx { - let _ = tx.send(ObserverEvent::PredicateDeregistered(hook_uuid)); + let _ = tx.send(ObserverEvent::PredicateDeregistered( + PredicateDeregisteredEvent { + predicate_uuid: hook_uuid.clone(), + chain: Chain::Bitcoin, + }, + )); }; } ObserverCommand::ExpireStacksPredicate(HookExpirationData { diff --git a/components/chainhook-sdk/src/observer/tests/mod.rs b/components/chainhook-sdk/src/observer/tests/mod.rs index 557854726..51525de73 100644 --- a/components/chainhook-sdk/src/observer/tests/mod.rs +++ b/components/chainhook-sdk/src/observer/tests/mod.rs @@ -14,7 +14,7 @@ use crate::indexer::tests::helpers::{ use crate::monitoring::PrometheusMonitoring; use crate::observer::{ start_observer_commands_handler, ChainhookStore, EventObserverConfig, ObserverCommand, - ObserverSidecar, + ObserverSidecar, PredicateDeregisteredEvent, }; use crate::utils::{AbstractBlock, Context}; use chainhook_types::{ @@ -501,7 +501,10 @@ fn test_stacks_chainhook_register_deregister() { chainhook.uuid.clone(), )); assert!(match observer_events_rx.recv() { - Ok(ObserverEvent::PredicateDeregistered(deregistered_chainhook)) => { + Ok(ObserverEvent::PredicateDeregistered(PredicateDeregisteredEvent { + predicate_uuid: deregistered_chainhook, + .. + })) => { assert_eq!(chainhook.uuid, deregistered_chainhook); true } @@ -690,7 +693,10 @@ fn test_stacks_chainhook_auto_deregister() { // Should signal that a hook was deregistered assert!(match observer_events_rx.recv() { - Ok(ObserverEvent::PredicateDeregistered(deregistered_hook)) => { + Ok(ObserverEvent::PredicateDeregistered(PredicateDeregisteredEvent { + predicate_uuid: deregistered_hook, + .. + })) => { assert_eq!(deregistered_hook, chainhook.uuid); true } @@ -856,7 +862,10 @@ fn test_bitcoin_chainhook_register_deregister() { chainhook.uuid.clone(), )); assert!(match observer_events_rx.recv() { - Ok(ObserverEvent::PredicateDeregistered(deregistered_chainhook)) => { + Ok(ObserverEvent::PredicateDeregistered(PredicateDeregisteredEvent { + predicate_uuid: deregistered_chainhook, + .. + })) => { assert_eq!(chainhook.uuid, deregistered_chainhook); true } @@ -1064,7 +1073,10 @@ fn test_bitcoin_chainhook_auto_deregister() { // Should signal that a hook was deregistered assert!(match observer_events_rx.recv() { - Ok(ObserverEvent::PredicateDeregistered(deregistered_hook)) => { + Ok(ObserverEvent::PredicateDeregistered(PredicateDeregisteredEvent { + predicate_uuid: deregistered_hook, + .. + })) => { assert_eq!(deregistered_hook, chainhook.uuid); true } diff --git a/components/chainhook-types-rs/src/lib.rs b/components/chainhook-types-rs/src/lib.rs index b5242d163..b015b6c75 100644 --- a/components/chainhook-types-rs/src/lib.rs +++ b/components/chainhook-types-rs/src/lib.rs @@ -18,6 +18,7 @@ pub use rosetta::*; pub const DEFAULT_STACKS_NODE_RPC: &str = "http://localhost:20443"; +#[derive(Clone, Debug)] pub enum Chain { Bitcoin, Stacks, From 3c347a23b4f0edb5fc8944f7707b8201b8f09418 Mon Sep 17 00:00:00 2001 From: Micaiah Reid Date: Mon, 1 Jul 2024 11:23:58 -0400 Subject: [PATCH 12/28] feat: improve chainhook-sdk interface (#608) ### Description The goal of this PR is to make it much easier to use the Chainhook SDK. Previously, there were many fields that are rarely needed for the average user which had to be set when configuring the SDK. Many of these fields had confusing names that made it difficult to know how they were used in the SDK. Additionally, many of these fields were only needed for observing stacks events, but bitcoin-only users had to specify them anyways. This has a few major changes to the Chainhook SDK: - Removing some unused fields from the event observer config (`cache_path`, `data_handler_tx`, and`ingestion_port`) (694fb4dfd6dd1e066ebaddac41e2324ebd2d3c41) - Renames `display_logs` -> `display_stacks_ingestion_logs` (694fb4dfd6dd1e066ebaddac41e2324ebd2d3c41) - Renames `EventObserverConfigOverrides` -> `StacksEventObserverConfigBuilder` (9da117847fb028e80c722b58678aa6291f68f649) - Renames `ingestion_port` -> `chainhook_stacks_block_ingestion_port` for `StacksEventObserverConfigBuilder` (4e997fc0225e51b1876a4182d2c73b76d2e48dfa) - Adds a `BitcoinEventObserverConfigBuilder` (fc67dff52da16ecb0c3da287afe732aa06f22a21) - Renames some very confusingly named structs (5a4cb39c7a294b3a5768d8ee9378ebe2bfbe578e): - `*ChainhookFullSpecification` => `*ChainhookSpecificationNetworkMap` - `*ChainhookNetworkSpecification` => `*ChainhookSpecification` - `*ChainhookSpecification` => `*ChainhookInstance` - refactor: moves stacks/bitcoin specific types to their respective types folder (83e833626950a8097aa14e54115ba6db6591d268) - adds helpers for registering chainhooks (4debc28103762f94c5eef9cb15cdfedd6d0c32ed) - renames `ChainhookConfig` -> `ChainhookStore` (c54b6e7f171f09a3a058b397b4131fcf8d9dca7e) - add `EventObserverBuilder` to make a clean interface for starting an event observer (fe04dd9aca396eb939c50748b8824849ea05fb8a) - add a bunch of rsdoc comments with examples #### Breaking change? This will break some aspects of the Chainhook SDK. It should be a simple upgrade: - If you're building any of the above structs directly, rename the fields accordingly - If you're using `::new(..)` to build any of the above structs with fields that are removed, you may need to remove some fields - You can probably remove a good bit of code by using the builders ### Example New code example to start a bitcoin event observer: ```rust fn start_observer(ctx: &Context) -> Result<(), String> { let json_predicate = std::fs::read_to_string("./predicate.json").expect("Unable to read file"); let hook_instance: BitcoinChainhookInstance = serde_json::from_str(&json_predicate).expect("unable to parse chainhook spec"); let config = BitcoinEventObserverConfigBuilder::new() .rpc_username("regtest") .rpc_password("test1235") .rpc_url("http://0.0.0.0:8332") .finish()? .register_bitcoin_chainhook_instance(hook_instance)? .to_owned(); let (observer_commands_tx, observer_commands_rx) = channel(); EventObserverBuilder::new(config, &observer_commands_tx, observer_commands_rx, &ctx) .start() .map_err(|e| format!("event observer error: {}", e.to_string())) } ```

Previous usage of starting a bitcoin observer ```rust let json_predicate = std::fs::read_to_string("./predicate.json").expect("Unable to read file"); let hook_spec: BitcoinChainhookFullSpecification = serde_json::from_str(&json_predicate).expect("unable to parse chainhook spec"); let bitcoin_network = BitcoinNetwork::Regtest; let stacks_network = chainhook_sdk::types::StacksNetwork::Mainnet; let mut bitcoin_hook_spec = hook_spec .into_selected_network_specification(&bitcoin_network) .expect("unable to parse bitcoin spec"); bitcoin_hook_spec.enabled = true; let mut chainhook_config = ChainhookConfig::new(); chainhook_config .register_specification(ChainhookSpecification::Bitcoin(bitcoin_hook_spec)) .expect("failed to register chainhook spec"); let config = EventObserverConfig { chainhook_config: Some(chainhook_config), bitcoin_rpc_proxy_enabled: false, ingestion_port: 0, bitcoind_rpc_username: "regtest".to_string(), bitcoind_rpc_password: "test1235".to_string(), bitcoind_rpc_url: "http://0.0.0.0:8332".to_string(), bitcoin_block_signaling: BitcoinBlockSignaling::ZeroMQ("tcp://0.0.0.0:18543".to_string()), display_logs: true, cache_path: String::new(), bitcoin_network: bitcoin_network, stacks_network: stacks_network, data_handler_tx: None, prometheus_monitoring_port: None, }; let (observer_commands_tx, observer_commands_rx) = channel(); // set up context to configure how we display logs from the event observer let logger = hiro_system_kit::log::setup_logger(); let _guard = hiro_system_kit::log::setup_global_logger(logger.clone()); let ctx = chainhook_sdk::utils::Context { logger: Some(logger), tracer: false, }; let moved_ctx = ctx.clone(); let _ = hiro_system_kit::thread_named("Chainhook event observer") .spawn(move || { let future = start_bitcoin_event_observer( config, observer_commands_tx, observer_commands_rx, None, None, moved_ctx, ); match hiro_system_kit::nestable_block_on(future) { Ok(_) => {} Err(e) => { println!("{}", e) } } }) .expect("unable to spawn thread"); ```
Fixes #598 --- components/chainhook-cli/src/cli/mod.rs | 79 +-- components/chainhook-cli/src/config/mod.rs | 8 +- components/chainhook-cli/src/scan/bitcoin.rs | 6 +- components/chainhook-cli/src/scan/stacks.rs | 10 +- .../chainhook-cli/src/service/http_api.rs | 24 +- components/chainhook-cli/src/service/mod.rs | 34 +- .../chainhook-cli/src/service/runloops.rs | 13 +- .../src/service/tests/helpers/mock_service.rs | 15 +- .../chainhook-cli/src/service/tests/mod.rs | 6 +- .../src/service/tests/observer_tests.rs | 10 +- .../src/service/tests/runloop_tests.rs | 11 +- .../src/chainhooks/bitcoin/mod.rs | 348 ++++++++++- .../src/chainhooks/bitcoin/tests.rs | 6 +- .../src/chainhooks/stacks/mod.rs | 234 +++++++- .../chainhook-sdk/src/chainhooks/tests/mod.rs | 28 +- .../chainhook-sdk/src/chainhooks/types.rs | 559 ++---------------- .../chainhook-sdk/src/indexer/bitcoin/mod.rs | 8 +- components/chainhook-sdk/src/observer/mod.rs | 538 +++++++++++++---- .../chainhook-sdk/src/observer/tests/mod.rs | 97 +-- components/chainhook-types-rs/src/bitcoin.rs | 2 +- components/chainhook-types-rs/src/rosetta.rs | 4 +- docs/chainhook-openapi.json | 26 +- 22 files changed, 1213 insertions(+), 853 deletions(-) diff --git a/components/chainhook-cli/src/cli/mod.rs b/components/chainhook-cli/src/cli/mod.rs index 387b6dac8..6859d8fa7 100644 --- a/components/chainhook-cli/src/cli/mod.rs +++ b/components/chainhook-cli/src/cli/mod.rs @@ -14,13 +14,16 @@ use crate::storage::{ is_stacks_block_present, open_readonly_stacks_db_conn, open_readwrite_stacks_db_conn, set_last_confirmed_insert_key, }; - -use chainhook_sdk::chainhooks::types::{ - BitcoinChainhookFullSpecification, BitcoinChainhookNetworkSpecification, BitcoinPredicateType, - ChainhookFullSpecification, FileHook, HookAction, InscriptionFeedData, OrdinalOperations, - StacksChainhookFullSpecification, StacksChainhookNetworkSpecification, StacksPredicate, - StacksPrintEventBasedPredicate, -}; +use chainhook_sdk::chainhooks::bitcoin::BitcoinChainhookSpecification; +use chainhook_sdk::chainhooks::bitcoin::BitcoinChainhookSpecificationNetworkMap; +use chainhook_sdk::chainhooks::bitcoin::BitcoinPredicateType; +use chainhook_sdk::chainhooks::bitcoin::InscriptionFeedData; +use chainhook_sdk::chainhooks::bitcoin::OrdinalOperations; +use chainhook_sdk::chainhooks::stacks::StacksChainhookSpecification; +use chainhook_sdk::chainhooks::stacks::StacksChainhookSpecificationNetworkMap; +use chainhook_sdk::chainhooks::stacks::StacksPredicate; +use chainhook_sdk::chainhooks::stacks::StacksPrintEventBasedPredicate; +use chainhook_sdk::chainhooks::types::{ChainhookSpecificationNetworkMap, FileHook, HookAction}; use chainhook_sdk::types::{BitcoinNetwork, BlockIdentifier, StacksNetwork}; use chainhook_sdk::utils::{BlockHeights, Context}; use clap::{Parser, Subcommand}; @@ -351,7 +354,7 @@ async fn handle_command(opts: Opts, ctx: Context) -> Result<(), String> { .predicates_paths .iter() .map(|p| load_predicate_from_path(p)) - .collect::, _>>()?; + .collect::, _>>()?; info!(ctx.expect_logger(), "Starting service...",); @@ -384,7 +387,7 @@ async fn handle_command(opts: Opts, ctx: Context) -> Result<(), String> { (true, false) => { let mut networks = BTreeMap::new(); - networks.insert(StacksNetwork::Testnet, StacksChainhookNetworkSpecification { + networks.insert(StacksNetwork::Testnet, StacksChainhookSpecification { start_block: Some(34239), end_block: Some(50000), blocks: None, @@ -401,7 +404,7 @@ async fn handle_command(opts: Opts, ctx: Context) -> Result<(), String> { }) }); - networks.insert(StacksNetwork::Mainnet, StacksChainhookNetworkSpecification { + networks.insert(StacksNetwork::Mainnet, StacksChainhookSpecification { start_block: Some(34239), end_block: Some(50000), blocks: None, @@ -418,20 +421,22 @@ async fn handle_command(opts: Opts, ctx: Context) -> Result<(), String> { }) }); - ChainhookFullSpecification::Stacks(StacksChainhookFullSpecification { - uuid: id.to_string(), - owner_uuid: None, - name: "Hello world".into(), - version: 1, - networks, - }) + ChainhookSpecificationNetworkMap::Stacks( + StacksChainhookSpecificationNetworkMap { + uuid: id.to_string(), + owner_uuid: None, + name: "Hello world".into(), + version: 1, + networks, + }, + ) } (false, true) => { let mut networks = BTreeMap::new(); networks.insert( BitcoinNetwork::Mainnet, - BitcoinChainhookNetworkSpecification { + BitcoinChainhookSpecification { start_block: Some(767430), end_block: Some(767430), blocks: None, @@ -451,13 +456,15 @@ async fn handle_command(opts: Opts, ctx: Context) -> Result<(), String> { }, ); - ChainhookFullSpecification::Bitcoin(BitcoinChainhookFullSpecification { - uuid: id.to_string(), - owner_uuid: None, - name: "Hello world".into(), - version: 1, - networks, - }) + ChainhookSpecificationNetworkMap::Bitcoin( + BitcoinChainhookSpecificationNetworkMap { + uuid: id.to_string(), + owner_uuid: None, + name: "Hello world".into(), + version: 1, + networks, + }, + ) } _ => { return Err("command `predicates new` should either provide the flag --stacks or --bitcoin".into()); @@ -500,9 +507,9 @@ async fn handle_command(opts: Opts, ctx: Context) -> Result<(), String> { Config::default(false, cmd.testnet, cmd.mainnet, &cmd.config_path)?; let predicate = load_predicate_from_path(&cmd.predicate_path)?; match predicate { - ChainhookFullSpecification::Bitcoin(predicate) => { + ChainhookSpecificationNetworkMap::Bitcoin(predicate) => { let predicate_spec = match predicate - .into_selected_network_specification(&config.network.bitcoin_network) + .into_specification_for_network(&config.network.bitcoin_network) { Ok(predicate) => predicate, Err(e) => { @@ -522,9 +529,9 @@ async fn handle_command(opts: Opts, ctx: Context) -> Result<(), String> { ) .await?; } - ChainhookFullSpecification::Stacks(predicate) => { + ChainhookSpecificationNetworkMap::Stacks(predicate) => { let predicate_spec = match predicate - .into_selected_network_specification(&config.network.stacks_network) + .into_specification_for_network(&config.network.stacks_network) { Ok(predicate) => predicate, Err(e) => { @@ -569,13 +576,13 @@ async fn handle_command(opts: Opts, ctx: Context) -> Result<(), String> { } PredicatesCommand::Check(cmd) => { let config = Config::default(false, cmd.testnet, cmd.mainnet, &cmd.config_path)?; - let predicate: ChainhookFullSpecification = + let predicate: ChainhookSpecificationNetworkMap = load_predicate_from_path(&cmd.predicate_path)?; match predicate { - ChainhookFullSpecification::Bitcoin(predicate) => { + ChainhookSpecificationNetworkMap::Bitcoin(predicate) => { let _ = match predicate - .into_selected_network_specification(&config.network.bitcoin_network) + .into_specification_for_network(&config.network.bitcoin_network) { Ok(predicate) => predicate, Err(e) => { @@ -586,9 +593,9 @@ async fn handle_command(opts: Opts, ctx: Context) -> Result<(), String> { } }; } - ChainhookFullSpecification::Stacks(predicate) => { + ChainhookSpecificationNetworkMap::Stacks(predicate) => { let _ = match predicate - .into_selected_network_specification(&config.network.stacks_network) + .into_specification_for_network(&config.network.stacks_network) { Ok(predicate) => predicate, Err(e) => { @@ -866,7 +873,7 @@ async fn handle_command(opts: Opts, ctx: Context) -> Result<(), String> { pub fn load_predicate_from_path( predicate_path: &str, -) -> Result { +) -> Result { let file = std::fs::File::open(&predicate_path) .map_err(|e| format!("unable to read file {}\n{:?}", predicate_path, e))?; let mut file_reader = BufReader::new(file); @@ -874,7 +881,7 @@ pub fn load_predicate_from_path( file_reader .read_to_end(&mut file_buffer) .map_err(|e| format!("unable to read file {}\n{:?}", predicate_path, e))?; - let predicate: ChainhookFullSpecification = serde_json::from_slice(&file_buffer) + let predicate: ChainhookSpecificationNetworkMap = serde_json::from_slice(&file_buffer) .map_err(|e| format!("unable to parse json file {}\n{:?}", predicate_path, e))?; Ok(predicate) } diff --git a/components/chainhook-cli/src/config/mod.rs b/components/chainhook-cli/src/config/mod.rs index 812d76021..7c3c1f8eb 100644 --- a/components/chainhook-cli/src/config/mod.rs +++ b/components/chainhook-cli/src/config/mod.rs @@ -1,6 +1,7 @@ pub mod file; pub mod generator; +use chainhook_sdk::chainhooks::types::ChainhookStore; pub use chainhook_sdk::indexer::IndexerConfig; use chainhook_sdk::observer::EventObserverConfig; use chainhook_sdk::types::{ @@ -114,17 +115,14 @@ impl Config { pub fn get_event_observer_config(&self) -> EventObserverConfig { EventObserverConfig { bitcoin_rpc_proxy_enabled: true, - chainhook_config: None, - ingestion_port: DEFAULT_INGESTION_PORT, + registered_chainhooks: ChainhookStore::new(), bitcoind_rpc_username: self.network.bitcoind_rpc_username.clone(), bitcoind_rpc_password: self.network.bitcoind_rpc_password.clone(), bitcoind_rpc_url: self.network.bitcoind_rpc_url.clone(), bitcoin_block_signaling: self.network.bitcoin_block_signaling.clone(), - display_logs: false, - cache_path: self.storage.working_dir.clone(), + display_stacks_ingestion_logs: false, bitcoin_network: self.network.bitcoin_network.clone(), stacks_network: self.network.stacks_network.clone(), - data_handler_tx: None, prometheus_monitoring_port: self.monitoring.prometheus_monitoring_port, } } diff --git a/components/chainhook-cli/src/scan/bitcoin.rs b/components/chainhook-cli/src/scan/bitcoin.rs index 0f80e7f54..3437af311 100644 --- a/components/chainhook-cli/src/scan/bitcoin.rs +++ b/components/chainhook-cli/src/scan/bitcoin.rs @@ -10,7 +10,7 @@ use chainhook_sdk::chainhooks::bitcoin::{ evaluate_bitcoin_chainhooks_on_chain_event, handle_bitcoin_hook_action, BitcoinChainhookOccurrence, BitcoinTriggerChainhook, }; -use chainhook_sdk::chainhooks::types::BitcoinChainhookSpecification; +use chainhook_sdk::chainhooks::bitcoin::BitcoinChainhookInstance; use chainhook_sdk::indexer; use chainhook_sdk::indexer::bitcoin::{ build_http_client, download_and_parse_block_with_retry, retrieve_block_hash_with_retry, @@ -27,7 +27,7 @@ use std::sync::{Arc, RwLock}; use super::common::PredicateScanResult; pub async fn scan_bitcoin_chainstate_via_rpc_using_predicate( - predicate_spec: &BitcoinChainhookSpecification, + predicate_spec: &BitcoinChainhookInstance, unfinished_scan_data: Option, config: &Config, kill_signal: Option>>, @@ -265,7 +265,7 @@ pub async fn scan_bitcoin_chainstate_via_rpc_using_predicate( pub async fn process_block_with_predicates( block: BitcoinBlockData, - predicates: &Vec<&BitcoinChainhookSpecification>, + predicates: &Vec<&BitcoinChainhookInstance>, event_observer_config: &EventObserverConfig, ctx: &Context, ) -> Result { diff --git a/components/chainhook-cli/src/scan/stacks.rs b/components/chainhook-cli/src/scan/stacks.rs index 5212af0ec..55e44df9a 100644 --- a/components/chainhook-cli/src/scan/stacks.rs +++ b/components/chainhook-cli/src/scan/stacks.rs @@ -24,9 +24,9 @@ use chainhook_sdk::{ utils::Context, }; use chainhook_sdk::{ - chainhooks::{ - stacks::{handle_stacks_hook_action, StacksChainhookOccurrence, StacksTriggerChainhook}, - types::StacksChainhookSpecification, + chainhooks::stacks::{ + handle_stacks_hook_action, StacksChainhookInstance, StacksChainhookOccurrence, + StacksTriggerChainhook, }, utils::{file_append, send_request, AbstractStacksBlock}, }; @@ -171,7 +171,7 @@ pub async fn get_canonical_fork_from_tsv( } pub async fn scan_stacks_chainstate_via_rocksdb_using_predicate( - predicate_spec: &StacksChainhookSpecification, + predicate_spec: &StacksChainhookInstance, unfinished_scan_data: Option, stacks_db_conn: &DB, config: &Config, @@ -441,7 +441,7 @@ pub async fn scan_stacks_chainstate_via_rocksdb_using_predicate( } pub async fn scan_stacks_chainstate_via_csv_using_predicate( - predicate_spec: &StacksChainhookSpecification, + predicate_spec: &StacksChainhookInstance, config: &mut Config, ctx: &Context, ) -> Result { diff --git a/components/chainhook-cli/src/service/http_api.rs b/components/chainhook-cli/src/service/http_api.rs index 460b2f9a1..b3519d421 100644 --- a/components/chainhook-cli/src/service/http_api.rs +++ b/components/chainhook-cli/src/service/http_api.rs @@ -5,7 +5,7 @@ use std::{ }; use chainhook_sdk::{ - chainhooks::types::{ChainhookFullSpecification, ChainhookSpecification}, + chainhooks::types::{ChainhookSpecificationNetworkMap, ChainhookInstance}, observer::ObserverCommand, utils::Context, }; @@ -120,7 +120,7 @@ fn handle_get_predicates( #[openapi(tag = "Managing Predicates")] #[post("/v1/chainhooks", format = "application/json", data = "")] fn handle_create_predicate( - predicate: Result, rocket::serde::json::Error>, + predicate: Result, rocket::serde::json::Error>, api_config: &State, background_job_tx: &State>>>, ctx: &State, @@ -149,7 +149,7 @@ fn handle_create_predicate( if let Ok(mut predicates_db_conn) = open_readwrite_predicates_db_conn(api_config) { match get_entry_from_predicates_db( - &ChainhookSpecification::either_stx_or_btc_key(&predicate_uuid), + &ChainhookInstance::either_stx_or_btc_key(&predicate_uuid), &mut predicates_db_conn, &ctx, ) { @@ -195,7 +195,7 @@ fn handle_get_predicate( match open_readwrite_predicates_db_conn(api_config) { Ok(mut predicates_db_conn) => { let (predicate, status) = match get_entry_from_predicates_db( - &ChainhookSpecification::either_stx_or_btc_key(&predicate_uuid), + &ChainhookInstance::either_stx_or_btc_key(&predicate_uuid), &mut predicates_db_conn, &ctx, ) { @@ -281,7 +281,7 @@ pub fn get_entry_from_predicates_db( predicate_key: &str, predicate_db_conn: &mut Connection, _ctx: &Context, -) -> Result, String> { +) -> Result, String> { let entry: HashMap = predicate_db_conn.hgetall(predicate_key).map_err(|e| { format!( "unable to load chainhook associated with key {}: {}", @@ -295,7 +295,7 @@ pub fn get_entry_from_predicates_db( Some(payload) => payload, }; - let spec = ChainhookSpecification::deserialize_specification(&encoded_spec)?; + let spec = ChainhookInstance::deserialize_specification(&encoded_spec)?; let encoded_status = match entry.get("status") { None => Err(format!( @@ -313,9 +313,9 @@ pub fn get_entry_from_predicates_db( pub fn get_entries_from_predicates_db( predicate_db_conn: &mut Connection, ctx: &Context, -) -> Result, String> { +) -> Result, String> { let chainhooks_to_load: Vec = predicate_db_conn - .scan_match(ChainhookSpecification::either_stx_or_btc_key("*")) + .scan_match(ChainhookInstance::either_stx_or_btc_key("*")) .map_err(|e| format!("unable to connect to redis: {}", e.to_string()))? .into_iter() .collect(); @@ -349,7 +349,7 @@ pub fn get_entries_from_predicates_db( pub fn load_predicates_from_redis( config: &crate::config::Config, ctx: &Context, -) -> Result, String> { +) -> Result, String> { let redis_uri: &str = config.expected_api_database_uri(); let client = redis::Client::open(redis_uri) .map_err(|e| format!("unable to connect to redis: {}", e.to_string()))?; @@ -378,11 +378,11 @@ pub fn get_routes_spec() -> (Vec, OpenApi) { } fn serialized_predicate_with_status( - predicate: &ChainhookSpecification, + predicate: &ChainhookInstance, status: &PredicateStatus, ) -> JsonValue { match (predicate, status) { - (ChainhookSpecification::Stacks(spec), status) => json!({ + (ChainhookInstance::Stacks(spec), status) => json!({ "chain": "stacks", "uuid": spec.uuid, "network": spec.network, @@ -390,7 +390,7 @@ fn serialized_predicate_with_status( "status": status, "enabled": spec.enabled, }), - (ChainhookSpecification::Bitcoin(spec), status) => json!({ + (ChainhookInstance::Bitcoin(spec), status) => json!({ "chain": "bitcoin", "uuid": spec.uuid, "network": spec.network, diff --git a/components/chainhook-cli/src/service/mod.rs b/components/chainhook-cli/src/service/mod.rs index e62f76bb8..7019b4e2e 100644 --- a/components/chainhook-cli/src/service/mod.rs +++ b/components/chainhook-cli/src/service/mod.rs @@ -11,9 +11,9 @@ use crate::storage::{ open_readwrite_stacks_db_conn, }; -use chainhook_sdk::chainhooks::types::{ChainhookConfig, ChainhookFullSpecification}; +use chainhook_sdk::chainhooks::types::{ChainhookSpecificationNetworkMap, ChainhookStore}; -use chainhook_sdk::chainhooks::types::ChainhookSpecification; +use chainhook_sdk::chainhooks::types::ChainhookInstance; use chainhook_sdk::observer::{ start_event_observer, HookExpirationData, ObserverCommand, ObserverEvent, PredicateDeregisteredEvent, PredicateEvaluationReport, PredicateInterruptedData, @@ -41,10 +41,10 @@ impl Service { pub async fn run( &mut self, - predicates_from_startup: Vec, + predicates_from_startup: Vec, observer_commands_tx_rx: Option<(Sender, Receiver)>, ) -> Result<(), String> { - let mut chainhook_config = ChainhookConfig::new(); + let mut chainhook_store = ChainhookStore::new(); // store all predicates from Redis that were in the process of scanning when // chainhook was shutdown - we need to resume where we left off @@ -89,7 +89,7 @@ impl Service { continue; } } - match chainhook_config.register_specification(predicate) { + match chainhook_store.register_instance(predicate) { Ok(_) => { debug!( self.ctx.expect_logger(), @@ -115,7 +115,7 @@ impl Service { if let Ok(mut predicates_db_conn) = open_readwrite_predicates_db_conn(api_config) { let uuid = predicate.get_uuid(); match get_entry_from_predicates_db( - &ChainhookSpecification::either_stx_or_btc_key(&uuid), + &ChainhookInstance::either_stx_or_btc_key(&uuid), &mut predicates_db_conn, &self.ctx, ) { @@ -130,7 +130,7 @@ impl Service { } }; } - match chainhook_config.register_full_specification( + match chainhook_store.register_instance_from_network_map( ( &self.config.network.bitcoin_network, &self.config.network.stacks_network, @@ -161,7 +161,7 @@ impl Service { // let (ordinal_indexer_command_tx, ordinal_indexer_command_rx) = channel(); let mut event_observer_config = self.config.get_event_observer_config(); - event_observer_config.chainhook_config = Some(chainhook_config); + event_observer_config.registered_chainhooks = chainhook_store; // Download and ingest a Stacks dump if self.config.rely_on_remote_stacks_tsv() { @@ -305,13 +305,13 @@ impl Service { for predicate_with_last_scanned_block in leftover_scans { match predicate_with_last_scanned_block { - (ChainhookSpecification::Stacks(spec), last_scanned_block) => { + (ChainhookInstance::Stacks(spec), last_scanned_block) => { let _ = stacks_scan_op_tx.send(StacksScanOp::StartScan { predicate_spec: spec, unfinished_scan_data: last_scanned_block, }); } - (ChainhookSpecification::Bitcoin(spec), last_scanned_block) => { + (ChainhookInstance::Bitcoin(spec), last_scanned_block) => { let _ = bitcoin_scan_op_tx.send(BitcoinScanOp::StartScan { predicate_spec: spec, unfinished_scan_data: last_scanned_block, @@ -361,13 +361,13 @@ impl Service { ); } match spec { - ChainhookSpecification::Stacks(predicate_spec) => { + ChainhookInstance::Stacks(predicate_spec) => { let _ = stacks_scan_op_tx.send(StacksScanOp::StartScan { predicate_spec, unfinished_scan_data: None, }); } - ChainhookSpecification::Bitcoin(predicate_spec) => { + ChainhookInstance::Bitcoin(predicate_spec) => { let _ = bitcoin_scan_op_tx.send(BitcoinScanOp::StartScan { predicate_spec, unfinished_scan_data: None, @@ -419,7 +419,7 @@ impl Service { }; let predicate_key = - ChainhookSpecification::either_stx_or_btc_key(&predicate_uuid); + ChainhookInstance::either_stx_or_btc_key(&predicate_uuid); let res: Result<(), redis::RedisError> = predicates_db_conn.del(predicate_key.clone()); if let Err(e) = res { @@ -755,7 +755,7 @@ fn update_status_from_report( last_triggered_height, triggered_count, }, - &(ChainhookSpecification::either_stx_or_btc_key(predicate_uuid)), + &(ChainhookInstance::either_stx_or_btc_key(predicate_uuid)), predicates_db_conn, &ctx, ); @@ -784,7 +784,7 @@ fn update_status_from_report( last_evaluated_height, evaluated_count, }, - &(ChainhookSpecification::either_stx_or_btc_key(predicate_uuid)), + &(ChainhookInstance::either_stx_or_btc_key(predicate_uuid)), predicates_db_conn, &ctx, ); @@ -797,7 +797,7 @@ fn update_status_from_report( &chain, evaluated_count, last_evaluated_height, - &(ChainhookSpecification::either_stx_or_btc_key(predicate_uuid)), + &(ChainhookInstance::either_stx_or_btc_key(predicate_uuid)), predicates_db_conn, &ctx, ); @@ -1236,7 +1236,7 @@ pub fn update_predicate_status( fn update_predicate_spec( predicate_key: &str, - spec: &ChainhookSpecification, + spec: &ChainhookInstance, predicates_db_conn: &mut Connection, ctx: &Context, ) { diff --git a/components/chainhook-cli/src/service/runloops.rs b/components/chainhook-cli/src/service/runloops.rs index c2f28f394..2322c1545 100644 --- a/components/chainhook-cli/src/service/runloops.rs +++ b/components/chainhook-cli/src/service/runloops.rs @@ -4,8 +4,9 @@ use std::{ }; use chainhook_sdk::{ - chainhooks::types::{ - BitcoinChainhookSpecification, ChainhookSpecification, StacksChainhookSpecification, + chainhooks::{ + bitcoin::BitcoinChainhookInstance, stacks::StacksChainhookInstance, + types::ChainhookInstance, }, observer::ObserverCommand, utils::Context, @@ -26,7 +27,7 @@ use super::ScanningData; pub enum StacksScanOp { StartScan { - predicate_spec: StacksChainhookSpecification, + predicate_spec: StacksChainhookInstance, unfinished_scan_data: Option, }, KillScan(String), @@ -85,7 +86,7 @@ pub fn start_stacks_scan_runloop( | Ok(PredicateScanResult::Deregistered) => {} Ok(PredicateScanResult::ChainTipReached) => { let _ = observer_command_tx.send(ObserverCommand::EnablePredicate( - ChainhookSpecification::Stacks(predicate_spec), + ChainhookInstance::Stacks(predicate_spec), )); } Err(e) => { @@ -130,7 +131,7 @@ pub fn start_stacks_scan_runloop( pub enum BitcoinScanOp { StartScan { - predicate_spec: BitcoinChainhookSpecification, + predicate_spec: BitcoinChainhookInstance, unfinished_scan_data: Option, }, KillScan(String), @@ -171,7 +172,7 @@ pub fn start_bitcoin_scan_runloop( | Ok(PredicateScanResult::Deregistered) => {} Ok(PredicateScanResult::ChainTipReached) => { let _ = observer_command_tx.send(ObserverCommand::EnablePredicate( - ChainhookSpecification::Bitcoin(predicate_spec), + ChainhookInstance::Bitcoin(predicate_spec), )); } Err(e) => { diff --git a/components/chainhook-cli/src/service/tests/helpers/mock_service.rs b/components/chainhook-cli/src/service/tests/helpers/mock_service.rs index 55ae3e859..7e50e59b8 100644 --- a/components/chainhook-cli/src/service/tests/helpers/mock_service.rs +++ b/components/chainhook-cli/src/service/tests/helpers/mock_service.rs @@ -8,9 +8,8 @@ use crate::service::{ PredicateStatus, Service, }; use chainhook_sdk::{ - chainhooks::types::{ - ChainhookFullSpecification, ChainhookSpecification, StacksChainhookFullSpecification, - }, + chainhooks::stacks::StacksChainhookSpecificationNetworkMap, + chainhooks::types::{ChainhookInstance, ChainhookSpecificationNetworkMap}, indexer::IndexerConfig, observer::ObserverCommand, types::{BitcoinBlockSignaling, BitcoinNetwork, Chain, StacksNetwork, StacksNodeConfig}, @@ -333,7 +332,7 @@ pub fn get_chainhook_config( pub async fn start_chainhook_service( config: Config, ping_startup_port: u16, - startup_predicates: Option>, + startup_predicates: Option>, ctx: &Context, ) -> Result, String> { let mut service = Service::new(config, ctx.clone()); @@ -389,8 +388,8 @@ pub struct TestSetupResult { pub async fn setup_stacks_chainhook_test( starting_chain_tip: u64, - redis_seed: Option<(StacksChainhookFullSpecification, PredicateStatus)>, - startup_predicates: Option>, + redis_seed: Option<(StacksChainhookSpecificationNetworkMap, PredicateStatus)>, + startup_predicates: Option>, ) -> TestSetupResult { let ( redis_port, @@ -426,14 +425,14 @@ pub async fn setup_stacks_chainhook_test( panic!("test failed with error: {e}"); }); let stacks_spec = predicate - .into_selected_network_specification(&StacksNetwork::Devnet) + .into_specification_for_network(&StacksNetwork::Devnet) .unwrap_or_else(|e| { flush_redis(redis_port); redis_process.kill().unwrap(); panic!("test failed with error: {e}"); }); - let spec = ChainhookSpecification::Stacks(stacks_spec); + let spec = ChainhookInstance::Stacks(stacks_spec); update_predicate_spec(&spec.key(), &spec, &mut connection, &ctx); update_predicate_status(&spec.key(), status, &mut connection, &ctx); } diff --git a/components/chainhook-cli/src/service/tests/mod.rs b/components/chainhook-cli/src/service/tests/mod.rs index 71e03ca3f..3b816b544 100644 --- a/components/chainhook-cli/src/service/tests/mod.rs +++ b/components/chainhook-cli/src/service/tests/mod.rs @@ -1,4 +1,4 @@ -use chainhook_sdk::chainhooks::types::ChainhookFullSpecification; +use chainhook_sdk::chainhooks::types::ChainhookSpecificationNetworkMap; use chainhook_sdk::types::Chain; use chainhook_sdk::utils::Context; use rocket::serde::json::Value as JsonValue; @@ -779,7 +779,7 @@ async fn it_allows_specifying_startup_predicate() -> Result<(), String> { ); let predicate = serde_json::from_value(predicate).expect("failed to set up stacks chanhook spec for test"); - let startup_predicate = ChainhookFullSpecification::Stacks(predicate); + let startup_predicate = ChainhookSpecificationNetworkMap::Stacks(predicate); let TestSetupResult { mut redis_process, working_dir, @@ -819,7 +819,7 @@ async fn register_predicate_responds_409_if_uuid_in_use() -> Result<(), String> ); let stacks_spec = serde_json::from_value(predicate.clone()) .expect("failed to set up stacks chanhook spec for test"); - let startup_predicate = ChainhookFullSpecification::Stacks(stacks_spec); + let startup_predicate = ChainhookSpecificationNetworkMap::Stacks(stacks_spec); let TestSetupResult { mut redis_process, diff --git a/components/chainhook-cli/src/service/tests/observer_tests.rs b/components/chainhook-cli/src/service/tests/observer_tests.rs index a1ca7cc13..cfb7699be 100644 --- a/components/chainhook-cli/src/service/tests/observer_tests.rs +++ b/components/chainhook-cli/src/service/tests/observer_tests.rs @@ -1,6 +1,7 @@ use std::{sync::mpsc::channel, thread::sleep, time::Duration}; use chainhook_sdk::{ + chainhooks::types::ChainhookStore, observer::{start_event_observer, EventObserverConfig}, types::{BitcoinNetwork, StacksNodeConfig}, utils::Context, @@ -184,13 +185,12 @@ async fn it_responds_200_for_unimplemented_endpoints( body: Option<&Value>, ) { let ingestion_port = get_free_port().unwrap(); - let (working_dir, _tsv_dir) = create_tmp_working_dir().unwrap_or_else(|e| { + let (_working_dir, _tsv_dir) = create_tmp_working_dir().unwrap_or_else(|e| { panic!("test failed with error: {e}"); }); let config = EventObserverConfig { - chainhook_config: None, + registered_chainhooks: ChainhookStore::new(), bitcoin_rpc_proxy_enabled: false, - ingestion_port: ingestion_port, bitcoind_rpc_username: format!(""), bitcoind_rpc_password: format!(""), bitcoind_rpc_url: format!(""), @@ -200,11 +200,9 @@ async fn it_responds_200_for_unimplemented_endpoints( ingestion_port: ingestion_port, }, ), - display_logs: false, - cache_path: working_dir, + display_stacks_ingestion_logs: false, bitcoin_network: BitcoinNetwork::Regtest, stacks_network: chainhook_sdk::types::StacksNetwork::Devnet, - data_handler_tx: None, prometheus_monitoring_port: None, }; start_and_ping_event_observer(config, ingestion_port).await; diff --git a/components/chainhook-cli/src/service/tests/runloop_tests.rs b/components/chainhook-cli/src/service/tests/runloop_tests.rs index e8d8410fd..b672499d7 100644 --- a/components/chainhook-cli/src/service/tests/runloop_tests.rs +++ b/components/chainhook-cli/src/service/tests/runloop_tests.rs @@ -1,9 +1,10 @@ use std::{path::PathBuf, sync::mpsc::channel, thread::sleep, time::Duration}; use chainhook_sdk::{ - chainhooks::types::{ - BitcoinChainhookSpecification, BitcoinPredicateType, BlockIdentifierIndexRule, HookAction, - StacksChainhookSpecification, StacksPredicate, + chainhooks::{ + bitcoin::{BitcoinChainhookInstance, BitcoinPredicateType}, + stacks::{StacksChainhookInstance, StacksPredicate}, + types::{BlockIdentifierIndexRule, HookAction}, }, types::{BitcoinNetwork, StacksNetwork}, utils::Context, @@ -65,7 +66,7 @@ async fn test_stacks_runloop_kill_scan() { .expect("unable to spawn thread"); let uuid = "test".to_string(); - let predicate_spec = StacksChainhookSpecification { + let predicate_spec = StacksChainhookInstance { uuid: uuid.clone(), owner_uuid: None, name: "idc".to_string(), @@ -133,7 +134,7 @@ async fn test_stacks_bitcoin_kill_scan() { .expect("unable to spawn thread"); let uuid = "test".to_string(); - let predicate_spec = BitcoinChainhookSpecification { + let predicate_spec = BitcoinChainhookInstance { uuid: uuid.clone(), owner_uuid: None, name: "idc".to_string(), diff --git a/components/chainhook-sdk/src/chainhooks/bitcoin/mod.rs b/components/chainhook-sdk/src/chainhooks/bitcoin/mod.rs index 1f5b306fa..143917a7d 100644 --- a/components/chainhook-sdk/src/chainhooks/bitcoin/mod.rs +++ b/components/chainhook-sdk/src/chainhooks/bitcoin/mod.rs @@ -1,15 +1,12 @@ -use super::types::{ - BitcoinChainhookSpecification, BitcoinPredicateType, DescriptorMatchingRule, ExactMatchingRule, - HookAction, InputPredicate, MatchingRule, OrdinalOperations, OrdinalsMetaProtocol, - OutputPredicate, StacksOperations, -}; +use super::types::{ChainhookInstance, ExactMatchingRule, HookAction, MatchingRule}; use crate::utils::Context; use bitcoincore_rpc_json::bitcoin::{address::Payload, Address}; use chainhook_types::{ - BitcoinBlockData, BitcoinChainEvent, BitcoinTransactionData, BlockIdentifier, + BitcoinBlockData, BitcoinChainEvent, BitcoinNetwork, BitcoinTransactionData, BlockIdentifier, StacksBaseChainOperation, TransactionIdentifier, }; +use schemars::JsonSchema; use hiro_system_kit::slog; @@ -17,9 +14,10 @@ use miniscript::bitcoin::secp256k1::Secp256k1; use miniscript::Descriptor; use reqwest::{Client, Method}; +use serde::{de, Deserialize, Deserializer}; use serde_json::Value as JsonValue; use std::{ - collections::{BTreeMap, HashMap}, + collections::{BTreeMap, HashMap, HashSet}, str::FromStr, }; @@ -27,8 +25,167 @@ use reqwest::RequestBuilder; use hex::FromHex; +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +pub struct BitcoinChainhookSpecification { + #[serde(skip_serializing_if = "Option::is_none")] + pub blocks: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub start_block: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub end_block: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub expire_after_occurrence: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_proof: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_inputs: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_outputs: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_witness: Option, + #[serde(rename = "if_this")] + pub predicate: BitcoinPredicateType, + #[serde(rename = "then_that")] + pub action: HookAction, +} + +/// Maps some [BitcoinChainhookSpecification] to a corresponding [BitcoinNetwork]. This allows maintaining one +/// serialized predicate file for a given predicate on each network. +/// +/// ### Examples +/// Given some file `predicate.json`: +/// ```json +/// { +/// "uuid": "my-id", +/// "name": "My Predicate", +/// "chain": "bitcoin", +/// "version": 1, +/// "networks": { +/// "regtest": { +/// // ... +/// }, +/// "testnet": { +/// // ... +/// }, +/// "mainnet": { +/// // ... +/// } +/// } +/// } +/// ``` +/// You can deserialize the file to this type and create a [BitcoinChainhookInstance] for the desired network: +/// ``` +/// use chainhook_sdk::chainhooks::bitcoin::BitcoinChainhookSpecificationNetworkMap; +/// use chainhook_sdk::chainhooks::bitcoin::BitcoinChainhookInstance; +/// use chainhook_types::BitcoinNetwork; +/// +/// fn get_predicate(network: &BitcoinNetwork) -> Result { +/// let json_predicate = +/// std::fs::read_to_string("./predicate.json").expect("Unable to read file"); +/// let hook_map: BitcoinChainhookSpecificationNetworkMap = +/// serde_json::from_str(&json_predicate).expect("Unable to parse Chainhook map"); +/// hook_map.into_specification_for_network(network) +/// } +/// +/// ``` +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +pub struct BitcoinChainhookSpecificationNetworkMap { + pub uuid: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub owner_uuid: Option, + pub name: String, + pub version: u32, + pub networks: BTreeMap, +} + +impl BitcoinChainhookSpecificationNetworkMap { + pub fn into_specification_for_network( + mut self, + network: &BitcoinNetwork, + ) -> Result { + let spec = self + .networks + .remove(network) + .ok_or("Network unknown".to_string())?; + Ok(BitcoinChainhookInstance { + uuid: self.uuid, + owner_uuid: self.owner_uuid, + name: self.name, + network: network.clone(), + version: self.version, + start_block: spec.start_block, + end_block: spec.end_block, + blocks: spec.blocks, + expire_after_occurrence: spec.expire_after_occurrence, + predicate: spec.predicate, + action: spec.action, + include_proof: spec.include_proof.unwrap_or(false), + include_inputs: spec.include_inputs.unwrap_or(false), + include_outputs: spec.include_outputs.unwrap_or(false), + include_witness: spec.include_witness.unwrap_or(false), + enabled: false, + expired_at: None, + }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct BitcoinChainhookInstance { + pub uuid: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub owner_uuid: Option, + pub name: String, + pub network: BitcoinNetwork, + pub version: u32, + #[serde(skip_serializing_if = "Option::is_none")] + pub blocks: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub start_block: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub end_block: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub expire_after_occurrence: Option, + pub predicate: BitcoinPredicateType, + pub action: HookAction, + pub include_proof: bool, + pub include_inputs: bool, + pub include_outputs: bool, + pub include_witness: bool, + pub enabled: bool, + pub expired_at: Option, +} + +impl BitcoinChainhookInstance { + pub fn key(&self) -> String { + ChainhookInstance::bitcoin_key(&self.uuid) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct BitcoinTransactionFilterPredicate { + pub predicate: BitcoinPredicateType, +} + +impl BitcoinTransactionFilterPredicate { + pub fn new(predicate: BitcoinPredicateType) -> BitcoinTransactionFilterPredicate { + BitcoinTransactionFilterPredicate { predicate } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case", tag = "scope")] +pub enum BitcoinPredicateType { + Block, + Txid(ExactMatchingRule), + Inputs(InputPredicate), + Outputs(OutputPredicate), + StacksProtocol(StacksOperations), + OrdinalsProtocol(OrdinalOperations), +} + pub struct BitcoinTriggerChainhook<'a> { - pub chainhook: &'a BitcoinChainhookSpecification, + pub chainhook: &'a BitcoinChainhookInstance, pub apply: Vec<(Vec<&'a BitcoinTransactionData>, &'a BitcoinBlockData)>, pub rollback: Vec<(Vec<&'a BitcoinTransactionData>, &'a BitcoinBlockData)>, } @@ -44,6 +201,177 @@ pub struct BitcoinChainhookPayload { pub uuid: String, } +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum InputPredicate { + Txid(TxinPredicate), + WitnessScript(MatchingRule), +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum OutputPredicate { + OpReturn(MatchingRule), + P2pkh(ExactMatchingRule), + P2sh(ExactMatchingRule), + P2wpkh(ExactMatchingRule), + P2wsh(ExactMatchingRule), + Descriptor(DescriptorMatchingRule), +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case", tag = "operation")] +pub enum StacksOperations { + StackerRewarded, + BlockCommitted, + LeaderRegistered, + StxTransferred, + StxLocked, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub enum OrdinalsMetaProtocol { + All, + #[serde(rename = "brc-20")] + Brc20, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +pub struct InscriptionFeedData { + #[serde(skip_serializing_if = "Option::is_none")] + pub meta_protocols: Option>, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case", tag = "operation")] +pub enum OrdinalOperations { + InscriptionFeed(InscriptionFeedData), +} + +pub fn get_stacks_canonical_magic_bytes(network: &BitcoinNetwork) -> [u8; 2] { + match network { + BitcoinNetwork::Mainnet => *b"X2", + BitcoinNetwork::Testnet => *b"T2", + BitcoinNetwork::Regtest => *b"id", + BitcoinNetwork::Signet => unreachable!(), + } +} + +pub struct PoxConfig { + pub genesis_block_height: u64, + pub prepare_phase_len: u64, + pub reward_phase_len: u64, + pub rewarded_addresses_per_block: usize, +} + +impl PoxConfig { + pub fn get_pox_cycle_len(&self) -> u64 { + self.prepare_phase_len + self.reward_phase_len + } + + pub fn get_pox_cycle_id(&self, block_height: u64) -> u64 { + (block_height.saturating_sub(self.genesis_block_height)) / self.get_pox_cycle_len() + } + + pub fn get_pos_in_pox_cycle(&self, block_height: u64) -> u64 { + (block_height.saturating_sub(self.genesis_block_height)) % self.get_pox_cycle_len() + } + + pub fn get_burn_address(&self) -> &str { + match self.genesis_block_height { + 666050 => "1111111111111111111114oLvT2", + 2000000 => "burn-address-regtest", + _ => "burn-address", + } + } +} +const POX_CONFIG_MAINNET: PoxConfig = PoxConfig { + genesis_block_height: 666050, + prepare_phase_len: 100, + reward_phase_len: 2100, + rewarded_addresses_per_block: 2, +}; + +const POX_CONFIG_TESTNET: PoxConfig = PoxConfig { + genesis_block_height: 2000000, + prepare_phase_len: 50, + reward_phase_len: 1050, + rewarded_addresses_per_block: 2, +}; + +const POX_CONFIG_DEVNET: PoxConfig = PoxConfig { + genesis_block_height: 100, + prepare_phase_len: 4, + reward_phase_len: 10, + rewarded_addresses_per_block: 2, +}; + +pub fn get_canonical_pox_config(network: &BitcoinNetwork) -> PoxConfig { + match network { + BitcoinNetwork::Mainnet => POX_CONFIG_MAINNET, + BitcoinNetwork::Testnet => POX_CONFIG_TESTNET, + BitcoinNetwork::Regtest => POX_CONFIG_DEVNET, + BitcoinNetwork::Signet => unreachable!(), + } +} + +#[derive(Debug, Clone, PartialEq)] +#[repr(u8)] +pub enum StacksOpcodes { + BlockCommit = b'[', + KeyRegister = b'^', + StackStx = b'x', + PreStx = b'p', + TransferStx = b'$', +} + +impl TryFrom for StacksOpcodes { + type Error = (); + + fn try_from(v: u8) -> Result { + match v { + x if x == StacksOpcodes::BlockCommit as u8 => Ok(StacksOpcodes::BlockCommit), + x if x == StacksOpcodes::KeyRegister as u8 => Ok(StacksOpcodes::KeyRegister), + x if x == StacksOpcodes::StackStx as u8 => Ok(StacksOpcodes::StackStx), + x if x == StacksOpcodes::PreStx as u8 => Ok(StacksOpcodes::PreStx), + x if x == StacksOpcodes::TransferStx as u8 => Ok(StacksOpcodes::TransferStx), + _ => Err(()), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct TxinPredicate { + pub txid: String, + pub vout: u32, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct DescriptorMatchingRule { + // expression defines the bitcoin descriptor. + pub expression: String, + #[serde(default, deserialize_with = "deserialize_descriptor_range")] + pub range: Option<[u32; 2]>, +} + +// deserialize_descriptor_range makes sure that the range value is valid. +fn deserialize_descriptor_range<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let range: [u32; 2] = Deserialize::deserialize(deserializer)?; + if !(range[0] < range[1]) { + Err(de::Error::custom( + "First element of 'range' must be lower than the second element", + )) + } else { + Ok(Some(range)) + } +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct BitcoinChainhookOccurrencePayload { pub apply: Vec, @@ -95,7 +423,7 @@ pub enum BitcoinChainhookOccurrence { pub fn evaluate_bitcoin_chainhooks_on_chain_event<'a>( chain_event: &'a BitcoinChainEvent, - active_chainhooks: &Vec<&'a BitcoinChainhookSpecification>, + active_chainhooks: &Vec<&'a BitcoinChainhookInstance>, ctx: &Context, ) -> ( Vec>, @@ -226,7 +554,7 @@ pub fn serialize_bitcoin_payload_to_json<'a>( } pub fn serialize_bitcoin_transactions_to_json<'a>( - predicate_spec: &BitcoinChainhookSpecification, + predicate_spec: &BitcoinChainhookInstance, transactions: &Vec<&BitcoinTransactionData>, proofs: &HashMap<&'a TransactionIdentifier, String>, ) -> Vec { diff --git a/components/chainhook-sdk/src/chainhooks/bitcoin/tests.rs b/components/chainhook-sdk/src/chainhooks/bitcoin/tests.rs index f2024a289..dd9cb52b2 100644 --- a/components/chainhook-sdk/src/chainhooks/bitcoin/tests.rs +++ b/components/chainhook-sdk/src/chainhooks/bitcoin/tests.rs @@ -2,7 +2,7 @@ use std::collections::HashSet; use super::super::types::MatchingRule; use super::*; -use crate::chainhooks::types::InscriptionFeedData; +use crate::chainhooks::bitcoin::InscriptionFeedData; use crate::indexer::tests::helpers::accounts; use crate::indexer::tests::helpers::bitcoin_blocks::generate_test_bitcoin_block; use crate::indexer::tests::helpers::transactions::generate_test_tx_bitcoin_p2pkh_transfer; @@ -164,7 +164,7 @@ fn it_serdes_occurrence_payload( 3, ); let block = generate_test_bitcoin_block(0, 0, vec![transaction.clone()], None); - let chainhook = &BitcoinChainhookSpecification { + let chainhook = &BitcoinChainhookInstance { uuid: "uuid".into(), owner_uuid: None, name: "name".into(), @@ -230,7 +230,7 @@ fn it_serdes_brc20_payload(tick: String) { let block = generate_test_bitcoin_block(0, 0, vec![transaction.clone()], None); let mut meta_protocols = HashSet::::new(); meta_protocols.insert(OrdinalsMetaProtocol::Brc20); - let chainhook = &BitcoinChainhookSpecification { + let chainhook = &BitcoinChainhookInstance { uuid: "uuid".into(), owner_uuid: None, name: "name".into(), diff --git a/components/chainhook-sdk/src/chainhooks/stacks/mod.rs b/components/chainhook-sdk/src/chainhooks/stacks/mod.rs index d76b7ce5f..de5370422 100644 --- a/components/chainhook-sdk/src/chainhooks/stacks/mod.rs +++ b/components/chainhook-sdk/src/chainhooks/stacks/mod.rs @@ -1,16 +1,15 @@ use crate::utils::{AbstractStacksBlock, Context}; -use super::types::{ - BlockIdentifierIndexRule, ExactMatchingRule, HookAction, StacksChainhookSpecification, - StacksContractDeploymentPredicate, StacksPredicate, StacksPrintEventBasedPredicate, -}; +use super::types::{BlockIdentifierIndexRule, ChainhookInstance, ExactMatchingRule, HookAction}; use chainhook_types::{ - BlockIdentifier, StacksChainEvent, StacksTransactionData, StacksTransactionEvent, - StacksTransactionEventPayload, StacksTransactionKind, TransactionIdentifier, + BlockIdentifier, StacksChainEvent, StacksNetwork, StacksTransactionData, + StacksTransactionEvent, StacksTransactionEventPayload, StacksTransactionKind, + TransactionIdentifier, }; use hiro_system_kit::slog; use regex::Regex; use reqwest::{Client, Method}; +use schemars::JsonSchema; use serde_json::Value as JsonValue; use stacks_codec::clarity::codec::StacksMessageCodec; use stacks_codec::clarity::vm::types::{CharType, SequenceData, Value as ClarityValue}; @@ -19,9 +18,222 @@ use std::io::Cursor; use reqwest::RequestBuilder; +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +pub struct StacksChainhookSpecification { + #[serde(skip_serializing_if = "Option::is_none")] + pub blocks: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub start_block: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub end_block: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub expire_after_occurrence: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub capture_all_events: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub decode_clarity_values: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_contract_abi: Option, + #[serde(rename = "if_this")] + pub predicate: StacksPredicate, + #[serde(rename = "then_that")] + pub action: HookAction, +} + +/// Maps some [StacksChainhookSpecification] to a corresponding [StacksNetwork]. This allows maintaining one +/// serialized predicate file for a given predicate on each network. +/// +/// ### Examples +/// Given some file `predicate.json`: +/// ```json +/// { +/// "uuid": "my-id", +/// "name": "My Predicate", +/// "chain": "stacks", +/// "version": 1, +/// "networks": { +/// "devnet": { +/// // ... +/// }, +/// "testnet": { +/// // ... +/// }, +/// "mainnet": { +/// // ... +/// } +/// } +/// } +/// ``` +/// You can deserialize the file to this type and create a [StacksChainhookInstance] for the desired network: +/// ``` +/// use chainhook_sdk::chainhooks::stacks::StacksChainhookSpecificationNetworkMap; +/// use chainhook_sdk::chainhooks::stacks::StacksChainhookInstance; +/// use chainhook_types::StacksNetwork; +/// +/// fn get_predicate(network: &StacksNetwork) -> Result { +/// let json_predicate = +/// std::fs::read_to_string("./predicate.json").expect("Unable to read file"); +/// let hook_map: StacksChainhookSpecificationNetworkMap = +/// serde_json::from_str(&json_predicate).expect("Unable to parse Chainhook map"); +/// hook_map.into_specification_for_network(network) +/// } +/// +/// ``` +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +pub struct StacksChainhookSpecificationNetworkMap { + pub uuid: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub owner_uuid: Option, + pub name: String, + pub version: u32, + pub networks: BTreeMap, +} + +impl StacksChainhookSpecificationNetworkMap { + pub fn into_specification_for_network( + mut self, + network: &StacksNetwork, + ) -> Result { + let spec = self + .networks + .remove(network) + .ok_or("Network unknown".to_string())?; + Ok(StacksChainhookInstance { + uuid: self.uuid, + owner_uuid: self.owner_uuid, + name: self.name, + network: network.clone(), + version: self.version, + start_block: spec.start_block, + end_block: spec.end_block, + blocks: spec.blocks, + capture_all_events: spec.capture_all_events, + decode_clarity_values: spec.decode_clarity_values, + expire_after_occurrence: spec.expire_after_occurrence, + include_contract_abi: spec.include_contract_abi, + predicate: spec.predicate, + action: spec.action, + enabled: false, + expired_at: None, + }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct StacksChainhookInstance { + pub uuid: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub owner_uuid: Option, + pub name: String, + pub network: StacksNetwork, + pub version: u32, + #[serde(skip_serializing_if = "Option::is_none")] + pub blocks: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub start_block: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub end_block: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub expire_after_occurrence: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub capture_all_events: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub decode_clarity_values: Option, + pub include_contract_abi: Option, + #[serde(rename = "predicate")] + pub predicate: StacksPredicate, + pub action: HookAction, + pub enabled: bool, + pub expired_at: Option, +} + +impl StacksChainhookInstance { + pub fn key(&self) -> String { + ChainhookInstance::stacks_key(&self.uuid) + } + + pub fn is_predicate_targeting_block_header(&self) -> bool { + match &self.predicate { + StacksPredicate::BlockHeight(_) => true, + _ => false, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +#[serde(tag = "scope")] +pub enum StacksPredicate { + BlockHeight(BlockIdentifierIndexRule), + ContractDeployment(StacksContractDeploymentPredicate), + ContractCall(StacksContractCallBasedPredicate), + PrintEvent(StacksPrintEventBasedPredicate), + FtEvent(StacksFtEventBasedPredicate), + NftEvent(StacksNftEventBasedPredicate), + StxEvent(StacksStxEventBasedPredicate), + Txid(ExactMatchingRule), +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct StacksContractCallBasedPredicate { + pub contract_identifier: String, + pub method: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum StacksContractDeploymentPredicate { + Deployer(String), + ImplementTrait(StacksTrait), +} +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum StacksTrait { + Sip09, + Sip10, + #[serde(rename = "*")] + Any, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +#[serde(untagged)] +pub enum StacksPrintEventBasedPredicate { + Contains { + contract_identifier: String, + contains: String, + }, + MatchesRegex { + contract_identifier: String, + #[serde(rename = "matches_regex")] + regex: String, + }, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct StacksFtEventBasedPredicate { + pub asset_identifier: String, + pub actions: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct StacksNftEventBasedPredicate { + pub asset_identifier: String, + pub actions: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct StacksStxEventBasedPredicate { + pub actions: Vec, +} + #[derive(Clone)] pub struct StacksTriggerChainhook<'a> { - pub chainhook: &'a StacksChainhookSpecification, + pub chainhook: &'a StacksChainhookInstance, pub apply: Vec<(Vec<&'a StacksTransactionData>, &'a dyn AbstractStacksBlock)>, pub rollback: Vec<(Vec<&'a StacksTransactionData>, &'a dyn AbstractStacksBlock)>, } @@ -103,7 +315,7 @@ impl<'a> StacksTriggerChainhook<'a> { pub fn evaluate_stacks_chainhooks_on_chain_event<'a>( chain_event: &'a StacksChainEvent, - active_chainhooks: Vec<&'a StacksChainhookSpecification>, + active_chainhooks: Vec<&'a StacksChainhookInstance>, ctx: &Context, ) -> ( Vec>, @@ -301,7 +513,7 @@ pub fn evaluate_stacks_chainhooks_on_chain_event<'a>( pub fn evaluate_stacks_chainhook_on_blocks<'a>( blocks: Vec<&'a dyn AbstractStacksBlock>, - chainhook: &'a StacksChainhookSpecification, + chainhook: &'a StacksChainhookInstance, ctx: &Context, ) -> ( Vec<(Vec<&'a StacksTransactionData>, &'a dyn AbstractStacksBlock)>, @@ -338,7 +550,7 @@ pub fn evaluate_stacks_chainhook_on_blocks<'a>( pub fn evaluate_stacks_predicate_on_block<'a>( block: &'a dyn AbstractStacksBlock, - chainhook: &'a StacksChainhookSpecification, + chainhook: &'a StacksChainhookInstance, _ctx: &Context, ) -> bool { match &chainhook.predicate { @@ -366,7 +578,7 @@ pub fn evaluate_stacks_predicate_on_block<'a>( pub fn evaluate_stacks_predicate_on_transaction<'a>( transaction: &'a StacksTransactionData, - chainhook: &'a StacksChainhookSpecification, + chainhook: &'a StacksChainhookInstance, ctx: &Context, ) -> bool { match &chainhook.predicate { diff --git a/components/chainhook-sdk/src/chainhooks/tests/mod.rs b/components/chainhook-sdk/src/chainhooks/tests/mod.rs index ecedbed5d..a503d9340 100644 --- a/components/chainhook-sdk/src/chainhooks/tests/mod.rs +++ b/components/chainhook-sdk/src/chainhooks/tests/mod.rs @@ -5,20 +5,18 @@ use self::fixtures::get_all_event_payload_types; use super::{ stacks::{ evaluate_stacks_chainhooks_on_chain_event, handle_stacks_hook_action, - StacksChainhookOccurrence, StacksTriggerChainhook, - }, - types::{ - ExactMatchingRule, FileHook, StacksChainhookSpecification, - StacksContractCallBasedPredicate, StacksContractDeploymentPredicate, - StacksFtEventBasedPredicate, StacksNftEventBasedPredicate, StacksPrintEventBasedPredicate, - StacksTrait, + StacksChainhookInstance, StacksChainhookOccurrence, StacksContractCallBasedPredicate, + StacksContractDeploymentPredicate, StacksFtEventBasedPredicate, + StacksNftEventBasedPredicate, StacksPredicate, StacksPrintEventBasedPredicate, + StacksStxEventBasedPredicate, StacksTrait, StacksTriggerChainhook, }, + types::{ExactMatchingRule, FileHook}, }; use crate::{chainhooks::stacks::serialize_stacks_payload_to_json, utils::Context}; use crate::{ chainhooks::{ tests::fixtures::{get_expected_occurrence, get_test_event_payload_by_type}, - types::{HookAction, StacksPredicate, StacksStxEventBasedPredicate}, + types::HookAction, }, utils::AbstractStacksBlock, }; @@ -389,7 +387,7 @@ fn test_stacks_predicates( confirmed_blocks: vec![], }); // Prepare predicate - let chainhook = StacksChainhookSpecification { + let chainhook = StacksChainhookInstance { uuid: "".to_string(), owner_uuid: None, name: "".to_string(), @@ -469,7 +467,7 @@ fn test_stacks_predicate_contract_deploy(predicate: StacksPredicate, expected_ap confirmed_blocks: vec![], }); // Prepare predicate - let chainhook = StacksChainhookSpecification { + let chainhook = StacksChainhookInstance { uuid: "".to_string(), owner_uuid: None, name: "".to_string(), @@ -524,7 +522,7 @@ fn verify_optional_addition_of_contract_abi() { new_blocks, confirmed_blocks: vec![], }); - let mut contract_deploy_chainhook = StacksChainhookSpecification { + let mut contract_deploy_chainhook = StacksChainhookInstance { uuid: "contract-deploy".to_string(), owner_uuid: None, name: "".to_string(), @@ -544,7 +542,7 @@ fn verify_optional_addition_of_contract_abi() { enabled: true, expired_at: None, }; - let contract_call_chainhook = StacksChainhookSpecification { + let contract_call_chainhook = StacksChainhookInstance { uuid: "contract-call".to_string(), owner_uuid: None, name: "".to_string(), @@ -663,7 +661,7 @@ fn test_stacks_predicate_contract_call(predicate: StacksPredicate, expected_appl confirmed_blocks: vec![], }); // Prepare predicate - let chainhook = StacksChainhookSpecification { + let chainhook = StacksChainhookInstance { uuid: "".to_string(), owner_uuid: None, name: "".to_string(), @@ -698,7 +696,7 @@ fn test_stacks_predicate_contract_call(predicate: StacksPredicate, expected_appl #[test] fn test_stacks_hook_action_noop() { - let chainhook = StacksChainhookSpecification { + let chainhook = StacksChainhookInstance { uuid: "".to_string(), owner_uuid: None, name: "".to_string(), @@ -756,7 +754,7 @@ fn test_stacks_hook_action_noop() { #[test] fn test_stacks_hook_action_file_append() { - let chainhook = StacksChainhookSpecification { + let chainhook = StacksChainhookInstance { uuid: "".to_string(), owner_uuid: None, name: "".to_string(), diff --git a/components/chainhook-sdk/src/chainhooks/types.rs b/components/chainhook-sdk/src/chainhooks/types.rs index d67ccd429..dc965d9f4 100644 --- a/components/chainhook-sdk/src/chainhooks/types.rs +++ b/components/chainhook-sdk/src/chainhooks/types.rs @@ -1,51 +1,54 @@ -use std::collections::{BTreeMap, HashSet}; - use chainhook_types::{BitcoinNetwork, StacksNetwork}; use reqwest::Url; use serde::ser::{SerializeSeq, Serializer}; -use serde::{de, Deserialize, Deserializer, Serialize}; +use serde::{Deserialize, Serialize}; use schemars::JsonSchema; use crate::utils::MAX_BLOCK_HEIGHTS_ENTRIES; +use crate::chainhooks::bitcoin::BitcoinChainhookInstance; +use crate::chainhooks::bitcoin::BitcoinChainhookSpecificationNetworkMap; +use crate::chainhooks::stacks::StacksChainhookInstance; +use crate::chainhooks::stacks::StacksChainhookSpecificationNetworkMap; + #[derive(Deserialize, Debug, Clone)] -pub struct ChainhookConfig { - pub stacks_chainhooks: Vec, - pub bitcoin_chainhooks: Vec, +pub struct ChainhookStore { + pub stacks_chainhooks: Vec, + pub bitcoin_chainhooks: Vec, } -impl ChainhookConfig { - pub fn new() -> ChainhookConfig { - ChainhookConfig { +impl ChainhookStore { + pub fn new() -> ChainhookStore { + ChainhookStore { stacks_chainhooks: vec![], bitcoin_chainhooks: vec![], } } - pub fn register_full_specification( + pub fn register_instance_from_network_map( &mut self, networks: (&BitcoinNetwork, &StacksNetwork), - hook: ChainhookFullSpecification, - ) -> Result { + hook: ChainhookSpecificationNetworkMap, + ) -> Result { let spec = match hook { - ChainhookFullSpecification::Stacks(hook) => { - let spec = hook.into_selected_network_specification(networks.1)?; + ChainhookSpecificationNetworkMap::Stacks(hook) => { + let spec = hook.into_specification_for_network(networks.1)?; self.stacks_chainhooks.push(spec.clone()); - ChainhookSpecification::Stacks(spec) + ChainhookInstance::Stacks(spec) } - ChainhookFullSpecification::Bitcoin(hook) => { - let spec = hook.into_selected_network_specification(networks.0)?; + ChainhookSpecificationNetworkMap::Bitcoin(hook) => { + let spec = hook.into_specification_for_network(networks.0)?; self.bitcoin_chainhooks.push(spec.clone()); - ChainhookSpecification::Bitcoin(spec) + ChainhookInstance::Bitcoin(spec) } }; Ok(spec) } - pub fn enable_specification(&mut self, predicate_spec: &mut ChainhookSpecification) { + pub fn enable_instance(&mut self, predicate_spec: &mut ChainhookInstance) { match predicate_spec { - ChainhookSpecification::Stacks(spec_to_enable) => { + ChainhookInstance::Stacks(spec_to_enable) => { for spec in self.stacks_chainhooks.iter_mut() { if spec.uuid.eq(&spec_to_enable.uuid) { spec.enabled = true; @@ -54,7 +57,7 @@ impl ChainhookConfig { } } } - ChainhookSpecification::Bitcoin(spec_to_enable) => { + ChainhookInstance::Bitcoin(spec_to_enable) => { for spec in self.bitcoin_chainhooks.iter_mut() { if spec.uuid.eq(&spec_to_enable.uuid) { spec.enabled = true; @@ -66,13 +69,13 @@ impl ChainhookConfig { }; } - pub fn register_specification(&mut self, spec: ChainhookSpecification) -> Result<(), String> { + pub fn register_instance(&mut self, spec: ChainhookInstance) -> Result<(), String> { match spec { - ChainhookSpecification::Stacks(spec) => { + ChainhookInstance::Stacks(spec) => { let spec = spec.clone(); self.stacks_chainhooks.push(spec); } - ChainhookSpecification::Bitcoin(spec) => { + ChainhookInstance::Bitcoin(spec) => { let spec = spec.clone(); self.bitcoin_chainhooks.push(spec); } @@ -80,10 +83,7 @@ impl ChainhookConfig { Ok(()) } - pub fn deregister_stacks_hook( - &mut self, - hook_uuid: String, - ) -> Option { + pub fn deregister_stacks_hook(&mut self, hook_uuid: String) -> Option { let mut i = 0; while i < self.stacks_chainhooks.len() { if self.stacks_chainhooks[i].uuid == hook_uuid { @@ -99,7 +99,7 @@ impl ChainhookConfig { pub fn deregister_bitcoin_hook( &mut self, hook_uuid: String, - ) -> Option { + ) -> Option { let mut i = 0; while i < self.bitcoin_chainhooks.len() { if self.bitcoin_chainhooks[i].uuid == hook_uuid { @@ -115,7 +115,7 @@ impl ChainhookConfig { pub fn expire_stacks_hook(&mut self, hook_uuid: String, block_height: u64) { let mut i = 0; while i < self.stacks_chainhooks.len() { - if ChainhookSpecification::stacks_key(&self.stacks_chainhooks[i].uuid) == hook_uuid { + if ChainhookInstance::stacks_key(&self.stacks_chainhooks[i].uuid) == hook_uuid { self.stacks_chainhooks[i].expired_at = Some(block_height); break; } else { @@ -127,7 +127,7 @@ impl ChainhookConfig { pub fn expire_bitcoin_hook(&mut self, hook_uuid: String, block_height: u64) { let mut i = 0; while i < self.bitcoin_chainhooks.len() { - if ChainhookSpecification::bitcoin_key(&self.bitcoin_chainhooks[i].uuid) == hook_uuid { + if ChainhookInstance::bitcoin_key(&self.bitcoin_chainhooks[i].uuid) == hook_uuid { self.bitcoin_chainhooks[i].expired_at = Some(block_height); break; } else { @@ -137,7 +137,7 @@ impl ChainhookConfig { } } -impl Serialize for ChainhookConfig { +impl Serialize for ChainhookStore { fn serialize(&self, serializer: S) -> Result where S: Serializer, @@ -157,12 +157,12 @@ impl Serialize for ChainhookConfig { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "snake_case")] -pub enum ChainhookSpecification { - Bitcoin(BitcoinChainhookSpecification), - Stacks(StacksChainhookSpecification), +pub enum ChainhookInstance { + Bitcoin(BitcoinChainhookInstance), + Stacks(StacksChainhookInstance), } -impl ChainhookSpecification { +impl ChainhookInstance { pub fn either_stx_or_btc_key(uuid: &str) -> String { format!("predicate:{}", uuid) } @@ -182,8 +182,8 @@ impl ChainhookSpecification { } } - pub fn deserialize_specification(spec: &str) -> Result { - let spec: ChainhookSpecification = serde_json::from_str(spec) + pub fn deserialize_specification(spec: &str) -> Result { + let spec: ChainhookInstance = serde_json::from_str(spec) .map_err(|e| format!("unable to deserialize predicate {}", e.to_string()))?; Ok(spec) } @@ -196,46 +196,14 @@ impl ChainhookSpecification { } } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct BitcoinChainhookSpecification { - pub uuid: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub owner_uuid: Option, - pub name: String, - pub network: BitcoinNetwork, - pub version: u32, - #[serde(skip_serializing_if = "Option::is_none")] - pub blocks: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub start_block: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub end_block: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub expire_after_occurrence: Option, - pub predicate: BitcoinPredicateType, - pub action: HookAction, - pub include_proof: bool, - pub include_inputs: bool, - pub include_outputs: bool, - pub include_witness: bool, - pub enabled: bool, - pub expired_at: Option, -} - -impl BitcoinChainhookSpecification { - pub fn key(&self) -> String { - ChainhookSpecification::bitcoin_key(&self.uuid) - } -} - #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case", tag = "chain")] -pub enum ChainhookFullSpecification { - Bitcoin(BitcoinChainhookFullSpecification), - Stacks(StacksChainhookFullSpecification), +pub enum ChainhookSpecificationNetworkMap { + Bitcoin(BitcoinChainhookSpecificationNetworkMap), + Stacks(StacksChainhookSpecificationNetworkMap), } -impl ChainhookFullSpecification { +impl ChainhookSpecificationNetworkMap { pub fn validate(&self) -> Result<(), String> { match &self { Self::Bitcoin(data) => { @@ -286,140 +254,13 @@ impl ChainhookFullSpecification { pub fn deserialize_specification( spec: &str, _key: &str, - ) -> Result { - let spec: ChainhookFullSpecification = serde_json::from_str(spec) + ) -> Result { + let spec: ChainhookSpecificationNetworkMap = serde_json::from_str(spec) .map_err(|e| format!("unable to deserialize predicate {}", e.to_string()))?; Ok(spec) } } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -pub struct BitcoinChainhookFullSpecification { - pub uuid: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub owner_uuid: Option, - pub name: String, - pub version: u32, - pub networks: BTreeMap, -} - -impl BitcoinChainhookFullSpecification { - pub fn into_selected_network_specification( - mut self, - network: &BitcoinNetwork, - ) -> Result { - let spec = self - .networks - .remove(network) - .ok_or("Network unknown".to_string())?; - Ok(BitcoinChainhookSpecification { - uuid: self.uuid, - owner_uuid: self.owner_uuid, - name: self.name, - network: network.clone(), - version: self.version, - start_block: spec.start_block, - end_block: spec.end_block, - blocks: spec.blocks, - expire_after_occurrence: spec.expire_after_occurrence, - predicate: spec.predicate, - action: spec.action, - include_proof: spec.include_proof.unwrap_or(false), - include_inputs: spec.include_inputs.unwrap_or(false), - include_outputs: spec.include_outputs.unwrap_or(false), - include_witness: spec.include_witness.unwrap_or(false), - enabled: false, - expired_at: None, - }) - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -pub struct BitcoinChainhookNetworkSpecification { - #[serde(skip_serializing_if = "Option::is_none")] - pub blocks: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub start_block: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub end_block: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub expire_after_occurrence: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub include_proof: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub include_inputs: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub include_outputs: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub include_witness: Option, - #[serde(rename = "if_this")] - pub predicate: BitcoinPredicateType, - #[serde(rename = "then_that")] - pub action: HookAction, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -pub struct StacksChainhookFullSpecification { - pub uuid: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub owner_uuid: Option, - pub name: String, - pub version: u32, - pub networks: BTreeMap, -} - -impl StacksChainhookFullSpecification { - pub fn into_selected_network_specification( - mut self, - network: &StacksNetwork, - ) -> Result { - let spec = self - .networks - .remove(network) - .ok_or("Network unknown".to_string())?; - Ok(StacksChainhookSpecification { - uuid: self.uuid, - owner_uuid: self.owner_uuid, - name: self.name, - network: network.clone(), - version: self.version, - start_block: spec.start_block, - end_block: spec.end_block, - blocks: spec.blocks, - capture_all_events: spec.capture_all_events, - decode_clarity_values: spec.decode_clarity_values, - expire_after_occurrence: spec.expire_after_occurrence, - include_contract_abi: spec.include_contract_abi, - predicate: spec.predicate, - action: spec.action, - enabled: false, - expired_at: None, - }) - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -pub struct StacksChainhookNetworkSpecification { - #[serde(skip_serializing_if = "Option::is_none")] - pub blocks: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub start_block: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub end_block: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub expire_after_occurrence: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub capture_all_events: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub decode_clarity_values: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub include_contract_abi: Option, - #[serde(rename = "if_this")] - pub predicate: StacksPredicate, - #[serde(rename = "then_that")] - pub action: HookAction, -} - #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum HookAction { @@ -454,7 +295,7 @@ pub struct HttpHook { pub struct FileHook { pub path: String, } - +// todo: can we remove this struct? #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] pub struct ScriptTemplate { pub instructions: Vec, @@ -499,177 +340,6 @@ impl ScriptTemplate { } } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct BitcoinTransactionFilterPredicate { - pub predicate: BitcoinPredicateType, -} - -impl BitcoinTransactionFilterPredicate { - pub fn new(predicate: BitcoinPredicateType) -> BitcoinTransactionFilterPredicate { - BitcoinTransactionFilterPredicate { predicate } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case", tag = "scope")] -pub enum BitcoinPredicateType { - Block, - Txid(ExactMatchingRule), - Inputs(InputPredicate), - Outputs(OutputPredicate), - StacksProtocol(StacksOperations), - OrdinalsProtocol(OrdinalOperations), -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum InputPredicate { - Txid(TxinPredicate), - WitnessScript(MatchingRule), -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum OutputPredicate { - OpReturn(MatchingRule), - P2pkh(ExactMatchingRule), - P2sh(ExactMatchingRule), - P2wpkh(ExactMatchingRule), - P2wsh(ExactMatchingRule), - Descriptor(DescriptorMatchingRule), -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case", tag = "operation")] -pub enum StacksOperations { - StackerRewarded, - BlockCommitted, - LeaderRegistered, - StxTransferred, - StxLocked, -} - -#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum OrdinalsMetaProtocol { - All, - #[serde(rename = "brc-20")] - Brc20, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -pub struct InscriptionFeedData { - #[serde(skip_serializing_if = "Option::is_none")] - pub meta_protocols: Option>, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case", tag = "operation")] -pub enum OrdinalOperations { - InscriptionFeed(InscriptionFeedData), -} - -pub fn get_stacks_canonical_magic_bytes(network: &BitcoinNetwork) -> [u8; 2] { - match network { - BitcoinNetwork::Mainnet => *b"X2", - BitcoinNetwork::Testnet => *b"T2", - BitcoinNetwork::Regtest => *b"id", - BitcoinNetwork::Signet => unreachable!(), - } -} - -pub struct PoxConfig { - pub genesis_block_height: u64, - pub prepare_phase_len: u64, - pub reward_phase_len: u64, - pub rewarded_addresses_per_block: usize, -} - -impl PoxConfig { - pub fn get_pox_cycle_len(&self) -> u64 { - self.prepare_phase_len + self.reward_phase_len - } - - pub fn get_pox_cycle_id(&self, block_height: u64) -> u64 { - (block_height.saturating_sub(self.genesis_block_height)) / self.get_pox_cycle_len() - } - - pub fn get_pos_in_pox_cycle(&self, block_height: u64) -> u64 { - (block_height.saturating_sub(self.genesis_block_height)) % self.get_pox_cycle_len() - } - - pub fn get_burn_address(&self) -> &str { - match self.genesis_block_height { - 666050 => "1111111111111111111114oLvT2", - 2000000 => "burn-address-regtest", - _ => "burn-address", - } - } -} - -const POX_CONFIG_MAINNET: PoxConfig = PoxConfig { - genesis_block_height: 666050, - prepare_phase_len: 100, - reward_phase_len: 2100, - rewarded_addresses_per_block: 2, -}; - -const POX_CONFIG_TESTNET: PoxConfig = PoxConfig { - genesis_block_height: 2000000, - prepare_phase_len: 50, - reward_phase_len: 1050, - rewarded_addresses_per_block: 2, -}; - -const POX_CONFIG_DEVNET: PoxConfig = PoxConfig { - genesis_block_height: 100, - prepare_phase_len: 4, - reward_phase_len: 10, - rewarded_addresses_per_block: 2, -}; - -pub fn get_canonical_pox_config(network: &BitcoinNetwork) -> PoxConfig { - match network { - BitcoinNetwork::Mainnet => POX_CONFIG_MAINNET, - BitcoinNetwork::Testnet => POX_CONFIG_TESTNET, - BitcoinNetwork::Regtest => POX_CONFIG_DEVNET, - BitcoinNetwork::Signet => unreachable!(), - } -} - -#[derive(Debug, Clone, PartialEq)] -#[repr(u8)] -pub enum StacksOpcodes { - BlockCommit = '[' as u8, - KeyRegister = '^' as u8, - StackStx = 'x' as u8, - PreStx = 'p' as u8, - TransferStx = '$' as u8, -} - -impl TryFrom for StacksOpcodes { - type Error = (); - - fn try_from(v: u8) -> Result { - match v { - x if x == StacksOpcodes::BlockCommit as u8 => Ok(StacksOpcodes::BlockCommit), - x if x == StacksOpcodes::KeyRegister as u8 => Ok(StacksOpcodes::KeyRegister), - x if x == StacksOpcodes::StackStx as u8 => Ok(StacksOpcodes::StackStx), - x if x == StacksOpcodes::PreStx as u8 => Ok(StacksOpcodes::PreStx), - x if x == StacksOpcodes::TransferStx as u8 => Ok(StacksOpcodes::TransferStx), - _ => Err(()), - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct TxinPredicate { - pub txid: String, - pub vout: u32, -} - #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum BlockIdentifierIndexRule { @@ -700,30 +370,6 @@ pub enum ExactMatchingRule { Equals(String), } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct DescriptorMatchingRule { - // expression defines the bitcoin descriptor. - pub expression: String, - #[serde(default, deserialize_with = "deserialize_descriptor_range")] - pub range: Option<[u32; 2]>, -} - -// deserialize_descriptor_range makes sure that the range value is valid. -fn deserialize_descriptor_range<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - let range: [u32; 2] = Deserialize::deserialize(deserializer)?; - if !(range[0] < range[1]) { - Err(de::Error::custom( - "First element of 'range' must be lower than the second element", - )) - } else { - Ok(Some(range)) - } -} - #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum BlockIdentifierHashRule { @@ -731,121 +377,6 @@ pub enum BlockIdentifierHashRule { BuildsOff(String), } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct StacksChainhookSpecification { - pub uuid: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub owner_uuid: Option, - pub name: String, - pub network: StacksNetwork, - pub version: u32, - #[serde(skip_serializing_if = "Option::is_none")] - pub blocks: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub start_block: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub end_block: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub expire_after_occurrence: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub capture_all_events: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub decode_clarity_values: Option, - pub include_contract_abi: Option, - #[serde(rename = "predicate")] - pub predicate: StacksPredicate, - pub action: HookAction, - pub enabled: bool, - pub expired_at: Option, -} - -impl StacksChainhookSpecification { - pub fn key(&self) -> String { - ChainhookSpecification::stacks_key(&self.uuid) - } - - pub fn is_predicate_targeting_block_header(&self) -> bool { - match &self.predicate { - StacksPredicate::BlockHeight(_) - // | &StacksPredicate::BitcoinBlockHeight(_) - => true, - _ => false, - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -#[serde(tag = "scope")] -pub enum StacksPredicate { - BlockHeight(BlockIdentifierIndexRule), - ContractDeployment(StacksContractDeploymentPredicate), - ContractCall(StacksContractCallBasedPredicate), - PrintEvent(StacksPrintEventBasedPredicate), - FtEvent(StacksFtEventBasedPredicate), - NftEvent(StacksNftEventBasedPredicate), - StxEvent(StacksStxEventBasedPredicate), - Txid(ExactMatchingRule), -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct StacksContractCallBasedPredicate { - pub contract_identifier: String, - pub method: String, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -// #[serde(tag = "type", content = "rule")] -pub enum StacksContractDeploymentPredicate { - Deployer(String), - ImplementTrait(StacksTrait), -} -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum StacksTrait { - Sip09, - Sip10, - #[serde(rename = "*")] - Any, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -#[serde(untagged)] -pub enum StacksPrintEventBasedPredicate { - Contains { - contract_identifier: String, - contains: String, - }, - MatchesRegex { - contract_identifier: String, - #[serde(rename = "matches_regex")] - regex: String, - }, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct StacksFtEventBasedPredicate { - pub asset_identifier: String, - pub actions: Vec, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct StacksNftEventBasedPredicate { - pub asset_identifier: String, - pub actions: Vec, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct StacksStxEventBasedPredicate { - pub actions: Vec, -} - pub fn opcode_to_hex(asm: &str) -> Option { match asm { "OP_PUSHBYTES_0" => Some(0x00), diff --git a/components/chainhook-sdk/src/indexer/bitcoin/mod.rs b/components/chainhook-sdk/src/indexer/bitcoin/mod.rs index 65889dbbf..c28826020 100644 --- a/components/chainhook-sdk/src/indexer/bitcoin/mod.rs +++ b/components/chainhook-sdk/src/indexer/bitcoin/mod.rs @@ -1,9 +1,8 @@ use std::time::Duration; -use crate::chainhooks::types::{ +use crate::chainhooks::bitcoin::{ get_canonical_pox_config, get_stacks_canonical_magic_bytes, PoxConfig, StacksOpcodes, }; - use crate::observer::BitcoinConfig; use crate::utils::Context; use bitcoincore_rpc::bitcoin::hashes::Hash; @@ -92,9 +91,8 @@ pub struct GetRawTransactionResultVinScriptSig { } impl BitcoinTransactionInputFullBreakdown { - /// Whether this input is from a coinbase tx. - /// The [txid], [vout] and [script_sig] fields are not provided - /// for coinbase transactions. + /// Whether this input is from a coinbase tx. If there is not a [BitcoinTransactionInputFullBreakdown::txid] field, the transaction is a coinbase transaction. + // Note: vout and script_sig fields are also not provided for coinbase transactions. pub fn is_coinbase(&self) -> bool { self.txid.is_none() } diff --git a/components/chainhook-sdk/src/observer/mod.rs b/components/chainhook-sdk/src/observer/mod.rs index c4d2dbc40..0b887acba 100644 --- a/components/chainhook-sdk/src/observer/mod.rs +++ b/components/chainhook-sdk/src/observer/mod.rs @@ -4,14 +4,15 @@ mod zmq; use crate::chainhooks::bitcoin::{ evaluate_bitcoin_chainhooks_on_chain_event, handle_bitcoin_hook_action, - BitcoinChainhookOccurrence, BitcoinChainhookOccurrencePayload, BitcoinTriggerChainhook, + BitcoinChainhookInstance, BitcoinChainhookOccurrence, BitcoinChainhookOccurrencePayload, + BitcoinTriggerChainhook, }; use crate::chainhooks::stacks::{ - evaluate_stacks_chainhooks_on_chain_event, handle_stacks_hook_action, + evaluate_stacks_chainhooks_on_chain_event, handle_stacks_hook_action, StacksChainhookInstance, StacksChainhookOccurrence, StacksChainhookOccurrencePayload, }; use crate::chainhooks::types::{ - ChainhookConfig, ChainhookFullSpecification, ChainhookSpecification, + ChainhookInstance, ChainhookSpecificationNetworkMap, ChainhookStore, }; use crate::indexer::bitcoin::{ @@ -39,7 +40,6 @@ use rocket::Shutdown; use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::error::Error; use std::net::{IpAddr, Ipv4Addr}; -use std::path::PathBuf; use std::str; use std::str::FromStr; use std::sync::mpsc::{Receiver, Sender}; @@ -68,40 +68,286 @@ pub enum DataHandlerEvent { #[derive(Debug, Clone)] pub struct EventObserverConfig { - pub chainhook_config: Option, + pub registered_chainhooks: ChainhookStore, pub bitcoin_rpc_proxy_enabled: bool, - pub ingestion_port: u16, pub bitcoind_rpc_username: String, pub bitcoind_rpc_password: String, pub bitcoind_rpc_url: String, pub bitcoin_block_signaling: BitcoinBlockSignaling, - pub display_logs: bool, - pub cache_path: String, + pub display_stacks_ingestion_logs: bool, pub bitcoin_network: BitcoinNetwork, pub stacks_network: StacksNetwork, - pub data_handler_tx: Option>, pub prometheus_monitoring_port: Option, } +/// A builder that is used to create a general purpose [EventObserverConfig]. +/// +/// ## Examples +/// ``` +/// use chainhook_sdk::observer::EventObserverConfig; +/// use chainhook_sdk::observer::EventObserverConfigBuilder; +/// +/// fn get_config() -> Result { +/// EventObserverConfigBuilder::new() +/// .bitcoind_rpc_password("my_password") +/// .bitcoin_network("mainnet") +/// .stacks_network("mainnet") +/// .finish() +/// } +/// ``` #[derive(Deserialize, Debug, Clone)] -pub struct EventObserverConfigOverrides { - pub ingestion_port: Option, +pub struct EventObserverConfigBuilder { pub bitcoind_rpc_username: Option, pub bitcoind_rpc_password: Option, pub bitcoind_rpc_url: Option, pub bitcoind_zmq_url: Option, + pub chainhook_stacks_block_ingestion_port: Option, pub stacks_node_rpc_url: Option, - pub display_logs: Option, - pub cache_path: Option, + pub display_stacks_ingestion_logs: Option, pub bitcoin_network: Option, pub stacks_network: Option, + pub prometheus_monitoring_port: Option, +} + +impl EventObserverConfigBuilder { + pub fn new() -> Self { + EventObserverConfigBuilder { + bitcoind_rpc_username: None, + bitcoind_rpc_password: None, + bitcoind_rpc_url: None, + bitcoind_zmq_url: None, + chainhook_stacks_block_ingestion_port: None, + stacks_node_rpc_url: None, + display_stacks_ingestion_logs: None, + bitcoin_network: None, + stacks_network: None, + prometheus_monitoring_port: None, + } + } + + /// Sets the bitcoind node's RPC username. + pub fn bitcoind_rpc_username(&mut self, username: &str) -> &mut Self { + self.bitcoind_rpc_username = Some(username.to_string()); + self + } + + /// Sets the bitcoind node's RPC password. + pub fn bitcoind_rpc_password(&mut self, password: &str) -> &mut Self { + self.bitcoind_rpc_password = Some(password.to_string()); + self + } + + /// Sets the bitcoind node's RPC url. + pub fn bitcoind_rpc_url(&mut self, url: &str) -> &mut Self { + self.bitcoind_rpc_url = Some(url.to_string()); + self + } + + /// Sets the bitcoind node's ZMQ url, used by the observer to receive new block events from bitcoind. + pub fn bitcoind_zmq_url(&mut self, url: &str) -> &mut Self { + self.bitcoind_zmq_url = Some(url.to_string()); + self + } + + /// Sets the Bitcoin network. Must be a valid bitcoin network string according to [BitcoinNetwork::from_str]. + pub fn bitcoin_network(&mut self, network: &str) -> &mut Self { + self.bitcoin_network = Some(network.to_string()); + self + } + + /// Sets the Stacks network. Must be a valid bitcoin network string according to [StacksNetwork::from_str]. + pub fn stacks_network(&mut self, network: &str) -> &mut Self { + self.stacks_network = Some(network.to_string()); + self + } + + /// Sets the Stacks node's RPC url. + pub fn stacks_node_rpc_url(&mut self, url: &str) -> &mut Self { + self.stacks_node_rpc_url = Some(url.to_string()); + self + } + + /// Sets the port at which Chainhook will observer Stacks blockchain events. The Stacks node's config should have an events_observer + /// entry matching this port in order to send block events the Chainhook. + pub fn chainhook_stacks_block_ingestion_port(&mut self, port: u16) -> &mut Self { + self.chainhook_stacks_block_ingestion_port = Some(port); + self + } + + /// Sets whether Chainhook should display Stacks ingestion logs. + pub fn display_stacks_ingestion_logs(&mut self, display_logs: bool) -> &mut Self { + self.display_stacks_ingestion_logs = Some(display_logs); + self + } + + /// Sets the Prometheus monitoring port. + pub fn prometheus_monitoring_port(&mut self, port: u16) -> &mut Self { + self.prometheus_monitoring_port = Some(port); + self + } + + /// Attempts to convert a [EventObserverConfigBuilder] instance into an [EventObserverConfig], filling in + /// defaults as necessary according to [EventObserverConfig::default]. + /// + /// This function will return an error if the `bitcoin_network` or `stacks_network` strings are set and are not a valid [BitcoinNetwork] or [StacksNetwork]. + /// + pub fn finish(&self) -> Result { + EventObserverConfig::new_using_overrides(Some(self)) + } +} + +/// A builder that is used to create an [EventObserverConfig] that is tailored for use with a bitcoind node emitting events via the ZMQ interface. +/// +/// ## Examples +/// ``` +/// use chainhook_sdk::observer::EventObserverConfig; +/// use chainhook_sdk::observer::BitcoinEventObserverConfigBuilder; +/// +/// fn get_config() -> Result { +/// BitcoinEventObserverConfigBuilder::new() +/// .rpc_password("my_password") +/// .network("mainnet") +/// .finish() +/// } +/// ``` +pub struct BitcoinEventObserverConfigBuilder { + pub bitcoind_rpc_username: Option, + pub bitcoind_rpc_password: Option, + pub bitcoind_rpc_url: Option, + pub bitcoin_network: Option, + pub bitcoind_zmq_url: Option, + pub prometheus_monitoring_port: Option, +} +impl BitcoinEventObserverConfigBuilder { + pub fn new() -> Self { + BitcoinEventObserverConfigBuilder { + bitcoind_rpc_username: None, + bitcoind_rpc_password: None, + bitcoind_rpc_url: None, + bitcoin_network: None, + bitcoind_zmq_url: None, + prometheus_monitoring_port: None, + } + } + + /// Sets the bitcoind node's RPC username. + pub fn rpc_username(&mut self, username: &str) -> &mut Self { + self.bitcoind_rpc_username = Some(username.to_string()); + self + } + + /// Sets the bitcoind node's RPC password. + pub fn rpc_password(&mut self, password: &str) -> &mut Self { + self.bitcoind_rpc_password = Some(password.to_string()); + self + } + + /// Sets the bitcoind node's RPC url. + pub fn rpc_url(&mut self, url: &str) -> &mut Self { + self.bitcoind_rpc_url = Some(url.to_string()); + self + } + + /// Sets the bitcoind node's ZMQ url, used by the observer to receive new block events from bitcoind. + pub fn zmq_url(&mut self, url: &str) -> &mut Self { + self.bitcoind_zmq_url = Some(url.to_string()); + self + } + + /// Sets the Bitcoin network. Must be a valid bitcoin network string according to [BitcoinNetwork::from_str]. + pub fn network(&mut self, network: &str) -> &mut Self { + self.bitcoin_network = Some(network.to_string()); + self + } + + /// Sets the Prometheus monitoring port. + pub fn prometheus_monitoring_port(&mut self, port: u16) -> &mut Self { + self.prometheus_monitoring_port = Some(port); + self + } + + /// Attempts to convert a [BitcoinEventObserverConfigBuilder] instance into an [EventObserverConfig], filling in + /// defaults as necessary according to [EventObserverConfig::default]. + /// + /// This function will return an error if the `bitcoin_network` string is set and is not a valid [BitcoinNetwork]. + pub fn finish(&self) -> Result { + let bitcoin_network = if let Some(network) = self.bitcoin_network.as_ref() { + BitcoinNetwork::from_str(&network)? + } else { + BitcoinNetwork::Regtest + }; + Ok(EventObserverConfig { + registered_chainhooks: ChainhookStore::new(), + bitcoin_rpc_proxy_enabled: false, + bitcoind_rpc_username: self + .bitcoind_rpc_username + .clone() + .unwrap_or_else(|| "devnet".into()), + bitcoind_rpc_password: self + .bitcoind_rpc_password + .clone() + .unwrap_or_else(|| "devnet".into()), + bitcoind_rpc_url: self + .bitcoind_rpc_url + .clone() + .unwrap_or_else(|| "http://localhost:18443".into()), + bitcoin_block_signaling: BitcoinBlockSignaling::ZeroMQ( + self.bitcoind_zmq_url + .clone() + .unwrap_or_else(|| "tcp://0.0.0.0:18543".into()), + ), + display_stacks_ingestion_logs: false, + bitcoin_network: bitcoin_network, + stacks_network: StacksNetwork::Devnet, + prometheus_monitoring_port: self.prometheus_monitoring_port, + }) + } } impl EventObserverConfig { - pub fn get_cache_path_buf(&self) -> PathBuf { - let mut path_buf = PathBuf::new(); - path_buf.push(&self.cache_path); - path_buf + pub fn default() -> Self { + EventObserverConfig { + registered_chainhooks: ChainhookStore::new(), + bitcoin_rpc_proxy_enabled: false, + bitcoind_rpc_username: "devnet".into(), + bitcoind_rpc_password: "devnet".into(), + bitcoind_rpc_url: "http://localhost:18443".into(), + bitcoin_block_signaling: BitcoinBlockSignaling::Stacks(StacksNodeConfig::new( + DEFAULT_STACKS_NODE_RPC.to_string(), + DEFAULT_INGESTION_PORT, + )), + display_stacks_ingestion_logs: false, + bitcoin_network: BitcoinNetwork::Regtest, + stacks_network: StacksNetwork::Devnet, + prometheus_monitoring_port: None, + } + } + + /// Adds a [ChainhookInstance] to config's the registered chainhook store, returning the updated config. + pub fn register_chainhook_instance( + &mut self, + spec: ChainhookInstance, + ) -> Result<&mut Self, String> { + let mut chainhook_config = ChainhookStore::new(); + chainhook_config.register_instance(spec)?; + self.registered_chainhooks = chainhook_config; + + Ok(self) + } + + /// Adds a [BitcoinChainhookInstance] to the config's registered chainhook store, returning the updated config. + pub fn register_bitcoin_chainhook_instance( + &mut self, + spec: BitcoinChainhookInstance, + ) -> Result<&mut Self, String> { + self.register_chainhook_instance(ChainhookInstance::Bitcoin(spec)) + } + /// Adds a [StacksChainhookInstance] to the config's registered chainhook store, returning the updated config. + pub fn register_stacks_chainhook_instance( + &mut self, + spec: StacksChainhookInstance, + ) -> Result<&mut Self, String> { + self.register_chainhook_instance(ChainhookInstance::Stacks(spec)) } pub fn get_bitcoin_config(&self) -> BitcoinConfig { @@ -115,23 +361,6 @@ impl EventObserverConfig { bitcoin_config } - pub fn get_chainhook_store(&self) -> ChainhookStore { - let mut chainhook_store = ChainhookStore::new(); - // If authorization not required, we create a default ChainhookConfig - if let Some(ref chainhook_config) = self.chainhook_config { - let mut chainhook_config = chainhook_config.clone(); - chainhook_store - .predicates - .stacks_chainhooks - .append(&mut chainhook_config.stacks_chainhooks); - chainhook_store - .predicates - .bitcoin_chainhooks - .append(&mut chainhook_config.bitcoin_chainhooks); - } - chainhook_store - } - pub fn get_stacks_node_config(&self) -> &StacksNodeConfig { match self.bitcoin_block_signaling { BitcoinBlockSignaling::Stacks(ref config) => config, @@ -143,7 +372,7 @@ impl EventObserverConfig { /// /// *Note: This is used by external crates, so it should not be removed, even if not used internally by Chainhook.* pub fn new_using_overrides( - overrides: Option<&EventObserverConfigOverrides>, + overrides: Option<&EventObserverConfigBuilder>, ) -> Result { let bitcoin_network = if let Some(network) = overrides.and_then(|c| c.bitcoin_network.as_ref()) { @@ -161,38 +390,35 @@ impl EventObserverConfig { let config = EventObserverConfig { bitcoin_rpc_proxy_enabled: false, - chainhook_config: None, - ingestion_port: overrides - .and_then(|c| c.ingestion_port) - .unwrap_or(DEFAULT_INGESTION_PORT), + registered_chainhooks: ChainhookStore::new(), bitcoind_rpc_username: overrides .and_then(|c| c.bitcoind_rpc_username.clone()) - .unwrap_or("devnet".to_string()), + .unwrap_or_else(|| "devnet".to_string()), bitcoind_rpc_password: overrides .and_then(|c| c.bitcoind_rpc_password.clone()) - .unwrap_or("devnet".to_string()), + .unwrap_or_else(|| "devnet".to_string()), bitcoind_rpc_url: overrides .and_then(|c| c.bitcoind_rpc_url.clone()) - .unwrap_or("http://localhost:18443".to_string()), + .unwrap_or_else(|| "http://localhost:18443".to_string()), bitcoin_block_signaling: overrides .and_then(|c| c.bitcoind_zmq_url.as_ref()) .map(|url| BitcoinBlockSignaling::ZeroMQ(url.clone())) - .unwrap_or(BitcoinBlockSignaling::Stacks(StacksNodeConfig::new( - overrides - .and_then(|c| c.stacks_node_rpc_url.clone()) - .unwrap_or(DEFAULT_STACKS_NODE_RPC.to_string()), - overrides - .and_then(|c| c.ingestion_port) - .unwrap_or(DEFAULT_INGESTION_PORT), - ))), - display_logs: overrides.and_then(|c| c.display_logs).unwrap_or(false), - cache_path: overrides - .and_then(|c| c.cache_path.clone()) - .unwrap_or("cache".to_string()), + .unwrap_or_else(|| { + BitcoinBlockSignaling::Stacks(StacksNodeConfig::new( + overrides + .and_then(|c| c.stacks_node_rpc_url.clone()) + .unwrap_or_else(|| DEFAULT_STACKS_NODE_RPC.to_string()), + overrides + .and_then(|c| c.chainhook_stacks_block_ingestion_port) + .unwrap_or_else(|| DEFAULT_INGESTION_PORT), + )) + }), + display_stacks_ingestion_logs: overrides + .and_then(|c| c.display_stacks_ingestion_logs) + .unwrap_or_else(|| false), bitcoin_network, stacks_network, - data_handler_tx: None, - prometheus_monitoring_port: None, + prometheus_monitoring_port: overrides.and_then(|c| c.prometheus_monitoring_port), }; Ok(config) } @@ -211,8 +437,8 @@ pub enum ObserverCommand { PropagateBitcoinChainEvent(BlockchainEvent), PropagateStacksChainEvent(StacksChainEvent), PropagateStacksMempoolEvent(StacksChainMempoolEvent), - RegisterPredicate(ChainhookFullSpecification), - EnablePredicate(ChainhookSpecification), + RegisterPredicate(ChainhookSpecificationNetworkMap), + EnablePredicate(ChainhookInstance), DeregisterBitcoinPredicate(String), DeregisterStacksPredicate(String), ExpireBitcoinPredicate(HookExpirationData), @@ -311,9 +537,9 @@ pub enum ObserverEvent { BitcoinChainEvent((BitcoinChainEvent, PredicateEvaluationReport)), StacksChainEvent((StacksChainEvent, PredicateEvaluationReport)), NotifyBitcoinTransactionProxied, - PredicateRegistered(ChainhookSpecification), + PredicateRegistered(ChainhookInstance), PredicateDeregistered(PredicateDeregisteredEvent), - PredicateEnabled(ChainhookSpecification), + PredicateEnabled(ChainhookInstance), BitcoinPredicateTriggered(BitcoinChainhookOccurrencePayload), StacksPredicateTriggered(StacksChainhookOccurrencePayload), PredicatesTriggered(usize), @@ -350,22 +576,6 @@ pub struct BitcoinConfig { pub bitcoin_block_signaling: BitcoinBlockSignaling, } -#[derive(Debug, Clone)] -pub struct ChainhookStore { - pub predicates: ChainhookConfig, -} - -impl ChainhookStore { - pub fn new() -> Self { - Self { - predicates: ChainhookConfig { - stacks_chainhooks: vec![], - bitcoin_chainhooks: vec![], - }, - } - } -} - #[derive(Debug, Clone)] pub struct BitcoinBlockDataCached { pub block: BitcoinBlockData, @@ -441,6 +651,98 @@ impl ObserverSidecar { } } +/// A helper struct used to configure and call [start_event_observer], which spawns a thread to observer chain events. +/// +/// ### Examples +/// ``` +/// use chainhook_sdk::observer::EventObserverBuilder; +/// use chainhook_sdk::observer::EventObserverConfig; +/// use chainhook_sdk::observer::ObserverCommand; +/// use chainhook_sdk::utils::Context; +/// use std::error::Error; +/// use std::sync::mpsc::{Receiver, Sender}; +/// +/// fn start_event_observer( +/// config: EventObserverConfig, +/// observer_commands_tx: &Sender, +/// observer_commands_rx: Receiver, +/// ctx: &Context, +/// )-> Result<(), Box> { +/// EventObserverBuilder::new( +/// config, +/// &observer_commands_tx, +/// observer_commands_rx, +/// &ctx +/// ) +/// .start() +/// } +/// ``` +pub struct EventObserverBuilder { + config: EventObserverConfig, + observer_commands_tx: Sender, + observer_commands_rx: Receiver, + ctx: Context, + observer_events_tx: Option>, + observer_sidecar: Option, + stacks_startup_context: Option, +} + +impl EventObserverBuilder { + pub fn new( + config: EventObserverConfig, + observer_commands_tx: &Sender, + observer_commands_rx: Receiver, + ctx: &Context, + ) -> Self { + EventObserverBuilder { + config: config, + observer_commands_tx: observer_commands_tx.clone(), + observer_commands_rx: observer_commands_rx, + ctx: ctx.clone(), + observer_events_tx: None, + observer_sidecar: None, + stacks_startup_context: None, + } + } + + /// Sets the `observer_events_tx` Sender. Set this and listen on the corresponding + /// Receiver to be notified of every [ObserverEvent]. + pub fn events_tx( + &mut self, + observer_events_tx: crossbeam_channel::Sender, + ) -> &mut Self { + self.observer_events_tx = Some(observer_events_tx); + self + } + + /// Sets a sidecar for the observer. See [ObserverSidecar]. + pub fn sidecar(&mut self, sidecar: ObserverSidecar) -> &mut Self { + self.observer_sidecar = Some(sidecar); + self + } + + /// Sets the Stacks startup context. See [StacksObserverStartupContext]. + pub fn stacks_startup_context(&mut self, context: StacksObserverStartupContext) -> &mut Self { + self.stacks_startup_context = Some(context); + self + } + + /// Starts the event observer, calling [start_event_observer]. This function consumes the + /// [EventObserverBuilder] and spawns a new thread to run the observer. + pub fn start(self) -> Result<(), Box> { + start_event_observer( + self.config, + self.observer_commands_tx, + self.observer_commands_rx, + self.observer_events_tx, + self.observer_sidecar, + self.stacks_startup_context, + self.ctx, + ) + } +} + +/// Spawns a thread to observe blockchain events. Use [EventObserverBuilder] to configure easily. pub fn start_event_observer( config: EventObserverConfig, observer_commands_tx: Sender, @@ -543,7 +845,7 @@ pub async fn start_bitcoin_event_observer( observer_sidecar: Option, ctx: Context, ) -> Result<(), Box> { - let chainhook_store = config.get_chainhook_store(); + let chainhook_store = config.registered_chainhooks.clone(); #[cfg(feature = "zeromq")] { let ctx_moved = ctx.clone(); @@ -556,8 +858,8 @@ pub async fn start_bitcoin_event_observer( let prometheus_monitoring = PrometheusMonitoring::new(); prometheus_monitoring.initialize( - chainhook_store.predicates.stacks_chainhooks.len() as u64, - chainhook_store.predicates.bitcoin_chainhooks.len() as u64, + chainhook_store.stacks_chainhooks.len() as u64, + chainhook_store.bitcoin_chainhooks.len() as u64, None, ); @@ -609,7 +911,7 @@ pub async fn start_stacks_event_observer( indexer.seed_stacks_block_pool(stacks_startup_context.block_pool_seed, &ctx); - let log_level = if config.display_logs { + let log_level = if config.display_stacks_ingestion_logs { if cfg!(feature = "cli") { LogLevel::Critical } else { @@ -623,7 +925,7 @@ pub async fn start_stacks_event_observer( let bitcoin_rpc_proxy_enabled = config.bitcoin_rpc_proxy_enabled; let bitcoin_config = config.get_bitcoin_config(); - let chainhook_store = config.get_chainhook_store(); + let chainhook_store = config.registered_chainhooks.clone(); let indexer_rw_lock = Arc::new(RwLock::new(indexer)); @@ -631,8 +933,8 @@ pub async fn start_stacks_event_observer( let prometheus_monitoring = PrometheusMonitoring::new(); prometheus_monitoring.initialize( - chainhook_store.predicates.stacks_chainhooks.len() as u64, - chainhook_store.predicates.bitcoin_chainhooks.len() as u64, + chainhook_store.stacks_chainhooks.len() as u64, + chainhook_store.bitcoin_chainhooks.len() as u64, Some(stacks_startup_context.last_block_height_appended), ); @@ -1075,7 +1377,6 @@ pub async fn start_observer_commands_handler( let mut report = PredicateEvaluationReport::new(); let bitcoin_chainhooks = chainhook_store - .predicates .bitcoin_chainhooks .iter() .filter(|p| p.enabled) @@ -1202,7 +1503,6 @@ pub async fn start_observer_commands_handler( for hook_uuid in hooks_ids_to_deregister.iter() { if chainhook_store - .predicates .deregister_bitcoin_hook(hook_uuid.clone()) .is_some() { @@ -1226,12 +1526,10 @@ pub async fn start_observer_commands_handler( } } Err(e) => { - chainhook_store - .predicates - .deregister_bitcoin_hook(data.chainhook.uuid.clone()); + chainhook_store.deregister_bitcoin_hook(data.chainhook.uuid.clone()); if let Some(ref tx) = observer_events_tx { let _ = tx.send(ObserverEvent::PredicateInterrupted(PredicateInterruptedData { - predicate_key: ChainhookSpecification::bitcoin_key(&data.chainhook.uuid), + predicate_key: ChainhookInstance::bitcoin_key(&data.chainhook.uuid), error: format!("Unable to evaluate predicate on Bitcoin chainstate: {}", e) })); } @@ -1254,7 +1552,6 @@ pub async fn start_observer_commands_handler( let mut report = PredicateEvaluationReport::new(); let stacks_chainhooks = chainhook_store - .predicates .stacks_chainhooks .iter() .filter(|p| p.enabled) @@ -1388,7 +1685,6 @@ pub async fn start_observer_commands_handler( for hook_uuid in hooks_ids_to_deregister.iter() { if chainhook_store - .predicates .deregister_stacks_hook(hook_uuid.clone()) .is_some() { @@ -1420,12 +1716,10 @@ pub async fn start_observer_commands_handler( } } Err(e) => { - chainhook_store - .predicates - .deregister_stacks_hook(data.chainhook.uuid.clone()); + chainhook_store.deregister_stacks_hook(data.chainhook.uuid.clone()); if let Some(ref tx) = observer_events_tx { let _ = tx.send(ObserverEvent::PredicateInterrupted(PredicateInterruptedData { - predicate_key: ChainhookSpecification::stacks_key(&data.chainhook.uuid), + predicate_key: ChainhookInstance::stacks_key(&data.chainhook.uuid), error: format!("Unable to evaluate predicate on Bitcoin chainstate: {}", e) })); } @@ -1458,28 +1752,26 @@ pub async fn start_observer_commands_handler( ObserverCommand::RegisterPredicate(spec) => { ctx.try_log(|logger| slog::info!(logger, "Handling RegisterPredicate command")); - let mut spec = match chainhook_store - .predicates - .register_full_specification(networks, spec) - { - Ok(spec) => spec, - Err(e) => { - ctx.try_log(|logger| { - slog::warn!( - logger, - "Unable to register new chainhook spec: {}", - e.to_string() - ) - }); - continue; - } - }; + let mut spec = + match chainhook_store.register_instance_from_network_map(networks, spec) { + Ok(spec) => spec, + Err(e) => { + ctx.try_log(|logger| { + slog::warn!( + logger, + "Unable to register new chainhook spec: {}", + e.to_string() + ) + }); + continue; + } + }; match spec { - ChainhookSpecification::Bitcoin(_) => { + ChainhookInstance::Bitcoin(_) => { prometheus_monitoring.btc_metrics_register_predicate() } - ChainhookSpecification::Stacks(_) => { + ChainhookInstance::Stacks(_) => { prometheus_monitoring.stx_metrics_register_predicate() } }; @@ -1493,12 +1785,12 @@ pub async fn start_observer_commands_handler( ctx.try_log(|logger| { slog::debug!(logger, "Enabling Predicate {}", spec.uuid()) }); - chainhook_store.predicates.enable_specification(&mut spec); + chainhook_store.enable_instance(&mut spec); } } ObserverCommand::EnablePredicate(mut spec) => { ctx.try_log(|logger| slog::info!(logger, "Enabling Predicate {}", spec.uuid())); - chainhook_store.predicates.enable_specification(&mut spec); + chainhook_store.enable_instance(&mut spec); if let Some(ref tx) = observer_events_tx { let _ = tx.send(ObserverEvent::PredicateEnabled(spec)); } @@ -1507,9 +1799,7 @@ pub async fn start_observer_commands_handler( ctx.try_log(|logger| { slog::info!(logger, "Handling DeregisterStacksPredicate command") }); - let hook = chainhook_store - .predicates - .deregister_stacks_hook(hook_uuid.clone()); + let hook = chainhook_store.deregister_stacks_hook(hook_uuid.clone()); if hook.is_some() { // on startup, only the predicates in the `chainhook_store` are added to the monitoring count, @@ -1530,9 +1820,7 @@ pub async fn start_observer_commands_handler( ctx.try_log(|logger| { slog::info!(logger, "Handling DeregisterBitcoinPredicate command") }); - let hook = chainhook_store - .predicates - .deregister_bitcoin_hook(hook_uuid.clone()); + let hook = chainhook_store.deregister_bitcoin_hook(hook_uuid.clone()); if hook.is_some() { // on startup, only the predicates in the `chainhook_store` are added to the monitoring count, @@ -1554,9 +1842,7 @@ pub async fn start_observer_commands_handler( block_height, }) => { ctx.try_log(|logger| slog::info!(logger, "Handling ExpireStacksPredicate command")); - chainhook_store - .predicates - .expire_stacks_hook(hook_uuid, block_height); + chainhook_store.expire_stacks_hook(hook_uuid, block_height); } ObserverCommand::ExpireBitcoinPredicate(HookExpirationData { hook_uuid, @@ -1565,9 +1851,7 @@ pub async fn start_observer_commands_handler( ctx.try_log(|logger| { slog::info!(logger, "Handling ExpireBitcoinPredicate command") }); - chainhook_store - .predicates - .expire_bitcoin_hook(hook_uuid, block_height); + chainhook_store.expire_bitcoin_hook(hook_uuid, block_height); } } } diff --git a/components/chainhook-sdk/src/observer/tests/mod.rs b/components/chainhook-sdk/src/observer/tests/mod.rs index 51525de73..aee263086 100644 --- a/components/chainhook-sdk/src/observer/tests/mod.rs +++ b/components/chainhook-sdk/src/observer/tests/mod.rs @@ -1,10 +1,18 @@ +use crate::chainhooks::bitcoin::BitcoinChainhookInstance; +use crate::chainhooks::bitcoin::BitcoinChainhookSpecification; +use crate::chainhooks::bitcoin::BitcoinChainhookSpecificationNetworkMap; +use crate::chainhooks::bitcoin::BitcoinPredicateType; +use crate::chainhooks::bitcoin::InscriptionFeedData; +use crate::chainhooks::bitcoin::OrdinalOperations; +use crate::chainhooks::bitcoin::OutputPredicate; +use crate::chainhooks::stacks::StacksChainhookInstance; +use crate::chainhooks::stacks::StacksChainhookSpecification; +use crate::chainhooks::stacks::StacksChainhookSpecificationNetworkMap; +use crate::chainhooks::stacks::StacksContractCallBasedPredicate; +use crate::chainhooks::stacks::StacksPredicate; use crate::chainhooks::types::{ - BitcoinChainhookFullSpecification, BitcoinChainhookNetworkSpecification, - BitcoinChainhookSpecification, BitcoinPredicateType, ChainhookConfig, - ChainhookFullSpecification, ChainhookSpecification, ExactMatchingRule, HookAction, - InscriptionFeedData, OrdinalOperations, OutputPredicate, StacksChainhookFullSpecification, - StacksChainhookNetworkSpecification, StacksChainhookSpecification, - StacksContractCallBasedPredicate, StacksPredicate, + ChainhookInstance, ChainhookSpecificationNetworkMap, ChainhookStore, ExactMatchingRule, + HookAction, }; use crate::indexer::fork_scratch_pad::ForkScratchPad; use crate::indexer::tests::helpers::transactions::generate_test_tx_bitcoin_p2pkh_transfer; @@ -12,9 +20,9 @@ use crate::indexer::tests::helpers::{ accounts, bitcoin_blocks, stacks_blocks, transactions::generate_test_tx_stacks_contract_call, }; use crate::monitoring::PrometheusMonitoring; +use crate::observer::PredicateDeregisteredEvent; use crate::observer::{ - start_observer_commands_handler, ChainhookStore, EventObserverConfig, ObserverCommand, - ObserverSidecar, PredicateDeregisteredEvent, + start_observer_commands_handler, EventObserverConfig, ObserverCommand, ObserverSidecar, }; use crate::utils::{AbstractBlock, Context}; use chainhook_types::{ @@ -31,25 +39,20 @@ use super::{ObserverEvent, DEFAULT_INGESTION_PORT}; fn generate_test_config() -> (EventObserverConfig, ChainhookStore) { let config: EventObserverConfig = EventObserverConfig { - chainhook_config: Some(ChainhookConfig::new()), + registered_chainhooks: ChainhookStore::new(), bitcoin_rpc_proxy_enabled: false, - ingestion_port: 0, bitcoind_rpc_username: "user".into(), bitcoind_rpc_password: "user".into(), bitcoind_rpc_url: "http://localhost:18443".into(), - display_logs: false, + display_stacks_ingestion_logs: false, bitcoin_block_signaling: BitcoinBlockSignaling::Stacks( StacksNodeConfig::default_localhost(DEFAULT_INGESTION_PORT), ), - cache_path: "cache".into(), bitcoin_network: BitcoinNetwork::Regtest, stacks_network: StacksNetwork::Devnet, - data_handler_tx: None, prometheus_monitoring_port: None, }; - let predicates = ChainhookConfig::new(); - let chainhook_store = ChainhookStore { predicates }; - (config, chainhook_store) + (config, ChainhookStore::new()) } fn stacks_chainhook_contract_call( @@ -57,11 +60,11 @@ fn stacks_chainhook_contract_call( contract_identifier: &str, expire_after_occurrence: Option, method: &str, -) -> StacksChainhookFullSpecification { +) -> StacksChainhookSpecificationNetworkMap { let mut networks = BTreeMap::new(); networks.insert( StacksNetwork::Devnet, - StacksChainhookNetworkSpecification { + StacksChainhookSpecification { start_block: None, end_block: None, blocks: None, @@ -77,7 +80,7 @@ fn stacks_chainhook_contract_call( }, ); - let spec = StacksChainhookFullSpecification { + let spec = StacksChainhookSpecificationNetworkMap { uuid: format!("{}", id), name: format!("Chainhook {}", id), owner_uuid: None, @@ -91,11 +94,11 @@ fn bitcoin_chainhook_p2pkh( id: u8, address: &str, expire_after_occurrence: Option, -) -> BitcoinChainhookFullSpecification { +) -> BitcoinChainhookSpecificationNetworkMap { let mut networks = BTreeMap::new(); networks.insert( BitcoinNetwork::Regtest, - BitcoinChainhookNetworkSpecification { + BitcoinChainhookSpecification { start_block: None, end_block: None, blocks: None, @@ -111,7 +114,7 @@ fn bitcoin_chainhook_p2pkh( }, ); - let spec = BitcoinChainhookFullSpecification { + let spec = BitcoinChainhookSpecificationNetworkMap { uuid: format!("{}", id), name: format!("Chainhook {}", id), owner_uuid: None, @@ -121,11 +124,11 @@ fn bitcoin_chainhook_p2pkh( spec } -fn bitcoin_chainhook_ordinals(id: u8) -> BitcoinChainhookFullSpecification { +fn bitcoin_chainhook_ordinals(id: u8) -> BitcoinChainhookSpecificationNetworkMap { let mut networks = BTreeMap::new(); networks.insert( BitcoinNetwork::Regtest, - BitcoinChainhookNetworkSpecification { + BitcoinChainhookSpecification { start_block: None, end_block: None, blocks: None, @@ -143,7 +146,7 @@ fn bitcoin_chainhook_ordinals(id: u8) -> BitcoinChainhookFullSpecification { }, ); - let spec = BitcoinChainhookFullSpecification { + let spec = BitcoinChainhookSpecificationNetworkMap { uuid: format!("{}", id), name: format!("Chainhook {}", id), owner_uuid: None, @@ -159,19 +162,19 @@ fn generate_and_register_new_stacks_chainhook( id: u8, contract_name: &str, method: &str, -) -> StacksChainhookSpecification { +) -> StacksChainhookInstance { let contract_identifier = format!("{}.{}", accounts::deployer_stx_address(), contract_name); let chainhook = stacks_chainhook_contract_call(id, &contract_identifier, None, method); let _ = observer_commands_tx.send(ObserverCommand::RegisterPredicate( - ChainhookFullSpecification::Stacks(chainhook.clone()), + ChainhookSpecificationNetworkMap::Stacks(chainhook.clone()), )); let mut chainhook = chainhook - .into_selected_network_specification(&StacksNetwork::Devnet) + .into_specification_for_network(&StacksNetwork::Devnet) .unwrap(); chainhook.enabled = true; - let _ = observer_commands_tx.send(ObserverCommand::EnablePredicate( - ChainhookSpecification::Stacks(chainhook.clone()), - )); + let _ = observer_commands_tx.send(ObserverCommand::EnablePredicate(ChainhookInstance::Stacks( + chainhook.clone(), + ))); assert!(match observer_events_rx.recv() { Ok(ObserverEvent::PredicateRegistered(_)) => { // assert_eq!( @@ -182,9 +185,9 @@ fn generate_and_register_new_stacks_chainhook( } _ => false, }); - let _ = observer_commands_tx.send(ObserverCommand::EnablePredicate( - ChainhookSpecification::Stacks(chainhook.clone()), - )); + let _ = observer_commands_tx.send(ObserverCommand::EnablePredicate(ChainhookInstance::Stacks( + chainhook.clone(), + ))); assert!(match observer_events_rx.recv() { Ok(ObserverEvent::PredicateEnabled(_)) => { // assert_eq!( @@ -204,17 +207,17 @@ fn generate_and_register_new_bitcoin_chainhook( id: u8, p2pkh_address: &str, expire_after_occurrence: Option, -) -> BitcoinChainhookSpecification { +) -> BitcoinChainhookInstance { let chainhook = bitcoin_chainhook_p2pkh(id, &p2pkh_address, expire_after_occurrence); let _ = observer_commands_tx.send(ObserverCommand::RegisterPredicate( - ChainhookFullSpecification::Bitcoin(chainhook.clone()), + ChainhookSpecificationNetworkMap::Bitcoin(chainhook.clone()), )); let mut chainhook = chainhook - .into_selected_network_specification(&BitcoinNetwork::Regtest) + .into_specification_for_network(&BitcoinNetwork::Regtest) .unwrap(); chainhook.enabled = true; let _ = observer_commands_tx.send(ObserverCommand::EnablePredicate( - ChainhookSpecification::Bitcoin(chainhook.clone()), + ChainhookInstance::Bitcoin(chainhook.clone()), )); assert!(match observer_events_rx.recv() { Ok(ObserverEvent::PredicateRegistered(_)) => { @@ -323,17 +326,17 @@ fn generate_and_register_new_ordinals_chainhook( observer_commands_tx: &Sender, observer_events_rx: &crossbeam_channel::Receiver, id: u8, -) -> BitcoinChainhookSpecification { +) -> BitcoinChainhookInstance { let chainhook = bitcoin_chainhook_ordinals(id); let _ = observer_commands_tx.send(ObserverCommand::RegisterPredicate( - ChainhookFullSpecification::Bitcoin(chainhook.clone()), + ChainhookSpecificationNetworkMap::Bitcoin(chainhook.clone()), )); let mut chainhook = chainhook - .into_selected_network_specification(&BitcoinNetwork::Regtest) + .into_specification_for_network(&BitcoinNetwork::Regtest) .unwrap(); chainhook.enabled = true; let _ = observer_commands_tx.send(ObserverCommand::EnablePredicate( - ChainhookSpecification::Bitcoin(chainhook.clone()), + ChainhookInstance::Bitcoin(chainhook.clone()), )); assert!(match observer_events_rx.recv() { Ok(ObserverEvent::PredicateRegistered(_)) => { @@ -585,15 +588,15 @@ fn test_stacks_chainhook_auto_deregister() { let contract_identifier = format!("{}.{}", accounts::deployer_stx_address(), "counter"); let chainhook = stacks_chainhook_contract_call(0, &contract_identifier, Some(1), "increment"); let _ = observer_commands_tx.send(ObserverCommand::RegisterPredicate( - ChainhookFullSpecification::Stacks(chainhook.clone()), + ChainhookSpecificationNetworkMap::Stacks(chainhook.clone()), )); let mut chainhook = chainhook - .into_selected_network_specification(&StacksNetwork::Devnet) + .into_specification_for_network(&StacksNetwork::Devnet) .unwrap(); chainhook.enabled = true; - let _ = observer_commands_tx.send(ObserverCommand::EnablePredicate( - ChainhookSpecification::Stacks(chainhook.clone()), - )); + let _ = observer_commands_tx.send(ObserverCommand::EnablePredicate(ChainhookInstance::Stacks( + chainhook.clone(), + ))); assert!(match observer_events_rx.recv() { Ok(ObserverEvent::PredicateRegistered(_)) => { // assert_eq!( diff --git a/components/chainhook-types-rs/src/bitcoin.rs b/components/chainhook-types-rs/src/bitcoin.rs index 79743bb14..7718b65b7 100644 --- a/components/chainhook-types-rs/src/bitcoin.rs +++ b/components/chainhook-types-rs/src/bitcoin.rs @@ -63,7 +63,7 @@ impl TxOut { /// #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash, Serialize, Deserialize)] pub struct Witness { - /// contains the witness Vec> serialization without the initial varint indicating the + /// contains the witness `Vec>` serialization without the initial varint indicating the /// number of elements (which is stored in `witness_elements`) content: Vec, diff --git a/components/chainhook-types-rs/src/rosetta.rs b/components/chainhook-types-rs/src/rosetta.rs index d917912cd..a794a27b0 100644 --- a/components/chainhook-types-rs/src/rosetta.rs +++ b/components/chainhook-types-rs/src/rosetta.rs @@ -456,9 +456,9 @@ pub struct PublicKey { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum CurveType { - /// `y (255-bits) || x-sign-bit (1-bit)` - `32 bytes` (https://ed25519.cr.yp.to/ed25519-20110926.pdf) + /// `y (255-bits) || x-sign-bit (1-bit)` - `32 bytes` () Edwards25519, - /// SEC compressed - `33 bytes` (https://secg.org/sec1-v2.pdf#subsubsection.2.3.3) + /// SEC compressed - `33 bytes` () Secp256k1, } diff --git a/docs/chainhook-openapi.json b/docs/chainhook-openapi.json index c8235911f..1375554f7 100644 --- a/docs/chainhook-openapi.json +++ b/docs/chainhook-openapi.json @@ -49,7 +49,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ChainhookFullSpecification" + "$ref": "#/components/schemas/ChainhookSpecificationNetworkMap" } } }, @@ -154,9 +154,10 @@ }, "components": { "schemas": { - "ChainhookFullSpecification": { + "ChainhookSpecificationNetworkMap": { "oneOf": [ { + "description": "Maps some [BitcoinChainhookSpecification] to a corresponding [BitcoinNetwork]. This allows maintaining one serialized predicate file for a given predicate on each network.\n\n### Examples Given some file `predicate.json`: ```json { \"uuid\": \"my-id\", \"name\": \"My Predicate\", \"chain\": \"bitcoin\", \"version\": 1, \"networks\": { \"regtest\": { // ... }, \"testnet\": { // ... }, \"mainnet\": { // ... } } } ``` You can deserialize the file to this type and create a [BitcoinChainhookInstance] for the desired network: ``` use chainhook_sdk::chainhooks::bitcoin::BitcoinChainhookSpecificationNetworkMap; use chainhook_sdk::chainhooks::bitcoin::BitcoinChainhookInstance; use chainhook_types::BitcoinNetwork;\n\nfn get_predicate(network: &BitcoinNetwork) -> Result { let json_predicate = std::fs::read_to_string(\"./predicate.json\").expect(\"Unable to read file\"); let hook_map: BitcoinChainhookSpecificationNetworkMap = serde_json::from_str(&json_predicate).expect(\"Unable to parse Chainhook map\"); hook_map.into_specification_for_network(network) }\n\n```", "type": "object", "required": [ "chain", @@ -196,7 +197,7 @@ ], "properties": { "regtest": { - "$ref": "#/components/schemas/BitcoinChainhookNetworkSpecification" + "$ref": "#/components/schemas/BitcoinChainhookSpecification" } } }, @@ -207,7 +208,7 @@ ], "properties": { "testnet": { - "$ref": "#/components/schemas/BitcoinChainhookNetworkSpecification" + "$ref": "#/components/schemas/BitcoinChainhookSpecification" } } }, @@ -218,7 +219,7 @@ ], "properties": { "signet": { - "$ref": "#/components/schemas/BitcoinChainhookNetworkSpecification" + "$ref": "#/components/schemas/BitcoinChainhookSpecification" } } }, @@ -229,7 +230,7 @@ ], "properties": { "mainnet": { - "$ref": "#/components/schemas/BitcoinChainhookNetworkSpecification" + "$ref": "#/components/schemas/BitcoinChainhookSpecification" } } } @@ -238,6 +239,7 @@ } }, { + "description": "Maps some [StacksChainhookSpecification] to a corresponding [StacksNetwork]. This allows maintaining one serialized predicate file for a given predicate on each network.\n\n### Examples Given some file `predicate.json`: ```json { \"uuid\": \"my-id\", \"name\": \"My Predicate\", \"chain\": \"stacks\", \"version\": 1, \"networks\": { \"devnet\": { // ... }, \"testnet\": { // ... }, \"mainnet\": { // ... } } } ``` You can deserialize the file to this type and create a [StacksChainhookInstance] for the desired network: ``` use chainhook_sdk::chainhooks::stacks::StacksChainhookSpecificationNetworkMap; use chainhook_sdk::chainhooks::stacks::StacksChainhookInstance; use chainhook_types::StacksNetwork;\n\nfn get_predicate(network: &StacksNetwork) -> Result { let json_predicate = std::fs::read_to_string(\"./predicate.json\").expect(\"Unable to read file\"); let hook_map: StacksChainhookSpecificationNetworkMap = serde_json::from_str(&json_predicate).expect(\"Unable to parse Chainhook map\"); hook_map.into_specification_for_network(network) }\n\n```", "type": "object", "required": [ "chain", @@ -277,7 +279,7 @@ ], "properties": { "simnet": { - "$ref": "#/components/schemas/StacksChainhookNetworkSpecification" + "$ref": "#/components/schemas/StacksChainhookSpecification" } } }, @@ -288,7 +290,7 @@ ], "properties": { "devnet": { - "$ref": "#/components/schemas/StacksChainhookNetworkSpecification" + "$ref": "#/components/schemas/StacksChainhookSpecification" } } }, @@ -299,7 +301,7 @@ ], "properties": { "testnet": { - "$ref": "#/components/schemas/StacksChainhookNetworkSpecification" + "$ref": "#/components/schemas/StacksChainhookSpecification" } } }, @@ -310,7 +312,7 @@ ], "properties": { "mainnet": { - "$ref": "#/components/schemas/StacksChainhookNetworkSpecification" + "$ref": "#/components/schemas/StacksChainhookSpecification" } } } @@ -329,7 +331,7 @@ "mainnet" ] }, - "BitcoinChainhookNetworkSpecification": { + "BitcoinChainhookSpecification": { "type": "object", "required": [ "if_this", @@ -857,7 +859,7 @@ "mainnet" ] }, - "StacksChainhookNetworkSpecification": { + "StacksChainhookSpecification": { "type": "object", "required": [ "if_this", From 1779def7b2b58c8399171ed28765c629fed0dff1 Mon Sep 17 00:00:00 2001 From: Micaiah Reid Date: Mon, 1 Jul 2024 13:57:51 -0400 Subject: [PATCH 13/28] ci: add snapcraft.yaml (#607) --- snapcraft.yaml | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 snapcraft.yaml diff --git a/snapcraft.yaml b/snapcraft.yaml new file mode 100644 index 000000000..d0c8be750 --- /dev/null +++ b/snapcraft.yaml @@ -0,0 +1,77 @@ +name: chainhook +summary: A reorg-aware indexing engine for the Stacks and Bitcoin blockchains. +description: | + Blockchains serve as foundational infrastructure that unblocks new use + cases and introduces a new generation of decentralized applications by + relying on a public ledger. + `chainhook` is a reorg-aware transaction indexing engine that helps + developers get reliable blockchain data, regardless of forks and + reorgs. By focusing only on the data devs care about, Chainhook helps + developers work with much lighter datasets and build IFTTT logic into + their applications. + Chainhook can be used as a tool in your local development environment + and as a service in the cloud environment. + + Key Features: + - **Faster, More Efficient Indexing:** Instead of working with a + generic blockchain indexer, taking hours to process every single + transaction of every single block, developers can create their own + indexes, build, iterate, and refine them in minutes. Chainhook can + help developers avoid massive storage management and storage scaling + issues by avoiding full chain indexation. Lighter indexes imply faster + query results, which helps minimize end-user response time. This leads + to an enhanced Developer Experience and an improved End-User + Experience. + - **Re-org and Fork Aware:** Chainhook keeps a store of possible chain + forks and checks each new chain event against the forks to maintain + the current valid fork. All triggers, also known as **predicates**, + are evaluated against the current valid fork. In the event of a reorg, + Chainhook computes a list of new blocks to apply and old blocks to + rollback and evaluates the registered predicates against those blocks. + - **IFTTT Logic, powering your applications:** Chainhook helps + developers create elegant event-based architectures using triggers, + also known as **predicates**. Developers can write “if_this / + then_that” **predicates**that when triggered, are packaged as events + and forwarded to the configured destination. By using cloud functions + as destinations, developers can also cut costs on processing by only + paying for processing when a block that contains some data relevant to + the developer's application is being mined. +adopt-info: chainhook-version + +base: core22 +confinement: strict + + +parts: + chainhook-version: + plugin: nil + source: . + override-pull: | + craftctl default + craftctl set version=$(git describe --tags --abbrev=0) + snapcraft-preload: + source: https://github.com/sergiusens/snapcraft-preload.git + plugin: cmake + cmake-parameters: + - -DCMAKE_INSTALL_PREFIX=/ + build-packages: + - on amd64: + - gcc-multilib + - g++-multilib + chainhook: + plugin: rust + source: ./ + build-packages: + - libssl-dev + - pkg-config + - libclang-11-dev + +apps: + chainhook: + command: bin/snapcraft-preload $SNAP/chainhook + plugs: + - network + - network-bind + - mount-observe + - home + - desktop From 67e28b1612bee197f03f9a3de498d890e8e0b105 Mon Sep 17 00:00:00 2001 From: Micaiah Reid Date: Fri, 5 Jul 2024 09:45:13 -0400 Subject: [PATCH 14/28] feat: predicate validation (#611) ### Description This PR adds validation for predicates. The `ChainhookSpecificationNetworkMap::validate(&self)` method will now check each of the fields in a predicate that could have invalid data and return a string with _all_ of the errors separated by a `"\n"`. I'm open to other ways of formatting the returned errors, but I think it will be nice for users to see _everything_ that is wrong with their spec in the first use rather than being given just the first error. ### Example Here is an example result: ``` invalid Stacks predicate 'predicate_name' for network simnet: invalid 'then_that' value: invalid 'http_post' data: url string must be a valid Url: relative URL without a base invalid Stacks predicate 'predicate_name' for network simnet: invalid 'then_that' value: invalid 'http_post' data: auth header must be a valid header value: failed to parse header value invalid Stacks predicate 'predicate_name' for network simnet: invalid 'if_this' value: invalid predicate for scope 'print_event': invalid contract identifier: ParseError("Invalid principal literal: base58ck checksum 0x147e6835 does not match expected 0x9b3dfe6a") ``` --- ### Checklist - [x] All tests pass - [x] Tests added in this PR (if applicable) --- components/chainhook-cli/src/cli/mod.rs | 2 + .../chainhook-cli/src/service/http_api.rs | 2 +- .../chainhook-cli/src/service/tests/mod.rs | 2 +- .../src/chainhooks/bitcoin/mod.rs | 223 +++++++++++++++-- .../bitcoin/tests/hook_spec_validation.rs | 198 +++++++++++++++ .../bitcoin/{tests.rs => tests/mod.rs} | 1 + .../src/chainhooks/stacks/mod.rs | 226 +++++++++++++++++- .../stacks/tests/hook_spec_validation.rs | 211 ++++++++++++++++ .../src/chainhooks/stacks/tests/mod.rs | 1 + .../chainhook-sdk/src/chainhooks/types.rs | 125 +++++++--- components/chainhook-types-rs/src/rosetta.rs | 29 +++ 11 files changed, 958 insertions(+), 62 deletions(-) create mode 100644 components/chainhook-sdk/src/chainhooks/bitcoin/tests/hook_spec_validation.rs rename components/chainhook-sdk/src/chainhooks/bitcoin/{tests.rs => tests/mod.rs} (99%) create mode 100644 components/chainhook-sdk/src/chainhooks/stacks/tests/hook_spec_validation.rs create mode 100644 components/chainhook-sdk/src/chainhooks/stacks/tests/mod.rs diff --git a/components/chainhook-cli/src/cli/mod.rs b/components/chainhook-cli/src/cli/mod.rs index 6859d8fa7..879ee8438 100644 --- a/components/chainhook-cli/src/cli/mod.rs +++ b/components/chainhook-cli/src/cli/mod.rs @@ -506,6 +506,7 @@ async fn handle_command(opts: Opts, ctx: Context) -> Result<(), String> { let mut config = Config::default(false, cmd.testnet, cmd.mainnet, &cmd.config_path)?; let predicate = load_predicate_from_path(&cmd.predicate_path)?; + predicate.validate()?; match predicate { ChainhookSpecificationNetworkMap::Bitcoin(predicate) => { let predicate_spec = match predicate @@ -578,6 +579,7 @@ async fn handle_command(opts: Opts, ctx: Context) -> Result<(), String> { let config = Config::default(false, cmd.testnet, cmd.mainnet, &cmd.config_path)?; let predicate: ChainhookSpecificationNetworkMap = load_predicate_from_path(&cmd.predicate_path)?; + predicate.validate()?; match predicate { ChainhookSpecificationNetworkMap::Bitcoin(predicate) => { diff --git a/components/chainhook-cli/src/service/http_api.rs b/components/chainhook-cli/src/service/http_api.rs index b3519d421..9a1e87d08 100644 --- a/components/chainhook-cli/src/service/http_api.rs +++ b/components/chainhook-cli/src/service/http_api.rs @@ -5,7 +5,7 @@ use std::{ }; use chainhook_sdk::{ - chainhooks::types::{ChainhookSpecificationNetworkMap, ChainhookInstance}, + chainhooks::types::{ChainhookInstance, ChainhookSpecificationNetworkMap}, observer::ObserverCommand, utils::Context, }; diff --git a/components/chainhook-cli/src/service/tests/mod.rs b/components/chainhook-cli/src/service/tests/mod.rs index 3b816b544..0ad6d6891 100644 --- a/components/chainhook-cli/src/service/tests/mod.rs +++ b/components/chainhook-cli/src/service/tests/mod.rs @@ -122,7 +122,7 @@ async fn it_handles_bitcoin_predicates_with_network(network: &str) { #[test_case(json!({ "scope": "outputs","p2sh": {"equals": "2MxDJ723HBJtEMa2a9vcsns4qztxBuC8Zb2"}}) ; "with scope outputs type p2sh")] #[test_case(json!({"scope": "outputs","p2wpkh": {"equals": "bcrt1qnxknq3wqtphv7sfwy07m7e4sr6ut9yt6ed99jg"}}) ; "with scope outputs type p2wpkh")] #[test_case(json!({"scope": "outputs","p2wsh": {"equals": "bc1qklpmx03a8qkv263gy8te36w0z9yafxplc5kwzc"}}) ; "with scope outputs type p2wsh")] -#[test_case(json!({"scope": "outputs","descriptor": {"expression": "a descriptor", "range": [0,3]}}) ; "with scope outputs type descriptor")] +#[test_case(json!({"scope": "outputs","descriptor": {"expression": "wpkh(02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9)", "range": [0,3]}}) ; "with scope outputs type descriptor")] #[test_case(json!({"scope": "stacks_protocol","operation": "stacker_rewarded"}) ; "with scope stacks_protocol operation stacker_rewarded")] #[test_case(json!({"scope": "stacks_protocol","operation": "block_committed"}) ; "with scope stacks_protocol operation block_committed")] #[test_case(json!({"scope": "stacks_protocol","operation": "leader_registered"}) ; "with scope stacks_protocol operation leader_registered")] diff --git a/components/chainhook-sdk/src/chainhooks/bitcoin/mod.rs b/components/chainhook-sdk/src/chainhooks/bitcoin/mod.rs index 143917a7d..a88a78e48 100644 --- a/components/chainhook-sdk/src/chainhooks/bitcoin/mod.rs +++ b/components/chainhook-sdk/src/chainhooks/bitcoin/mod.rs @@ -1,5 +1,8 @@ -use super::types::{ChainhookInstance, ExactMatchingRule, HookAction, MatchingRule}; -use crate::utils::Context; +use super::types::{ + append_error_context, validate_txid, ChainhookInstance, ExactMatchingRule, HookAction, + MatchingRule, +}; +use crate::utils::{Context, MAX_BLOCK_HEIGHTS_ENTRIES}; use bitcoincore_rpc_json::bitcoin::{address::Payload, Address}; use chainhook_types::{ @@ -49,6 +52,90 @@ pub struct BitcoinChainhookSpecification { pub action: HookAction, } +impl BitcoinChainhookSpecification { + pub fn new(predicate: BitcoinPredicateType, action: HookAction) -> Self { + BitcoinChainhookSpecification { + blocks: None, + start_block: None, + end_block: None, + expire_after_occurrence: None, + include_proof: None, + include_inputs: None, + include_outputs: None, + include_witness: None, + predicate, + action, + } + } + + pub fn blocks(&mut self, blocks: Vec) -> &mut Self { + self.blocks = Some(blocks); + self + } + + pub fn start_block(&mut self, start_block: u64) -> &mut Self { + self.start_block = Some(start_block); + self + } + + pub fn end_block(&mut self, end_block: u64) -> &mut Self { + self.end_block = Some(end_block); + self + } + + pub fn expire_after_occurrence(&mut self, occurrence: u64) -> &mut Self { + self.expire_after_occurrence = Some(occurrence); + self + } + + pub fn include_proof(&mut self, do_include: bool) -> &mut Self { + self.include_proof = Some(do_include); + self + } + + pub fn include_inputs(&mut self, do_include: bool) -> &mut Self { + self.include_inputs = Some(do_include); + self + } + + pub fn include_outputs(&mut self, do_include: bool) -> &mut Self { + self.include_outputs = Some(do_include); + self + } + + pub fn include_witness(&mut self, do_include: bool) -> &mut Self { + self.include_witness = Some(do_include); + self + } + + pub fn validate(&self) -> Result<(), Vec> { + let mut errors = vec![]; + if let Err(e) = self.action.validate() { + errors.append(&mut append_error_context("invalid 'then_that' value", e)); + } + if let Err(e) = self.predicate.validate() { + errors.append(&mut append_error_context("invalid 'if_this' value", e)); + } + + if let Some(end_block) = self.end_block { + let start_block = self.start_block.unwrap_or(0); + if start_block > end_block { + errors.push( + "Chainhook specification field `end_block` should be greater than `start_block`.".into() + ); + } + if (end_block - start_block) > MAX_BLOCK_HEIGHTS_ENTRIES { + errors.push(format!("Chainhook specification exceeds max number of blocks to scan. Maximum: {}, Attempted: {}", MAX_BLOCK_HEIGHTS_ENTRIES, (end_block - start_block))); + } + } + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } + } +} + /// Maps some [BitcoinChainhookSpecification] to a corresponding [BitcoinNetwork]. This allows maintaining one /// serialized predicate file for a given predicate on each network. /// @@ -184,6 +271,41 @@ pub enum BitcoinPredicateType { OrdinalsProtocol(OrdinalOperations), } +impl BitcoinPredicateType { + pub fn validate(&self) -> Result<(), Vec> { + match self { + BitcoinPredicateType::Block => {} + BitcoinPredicateType::Txid(ExactMatchingRule::Equals(txid)) => { + if let Err(e) = validate_txid(txid) { + return Err(append_error_context( + "invalid predicate for scope 'txid'", + vec![e], + )); + } + } + BitcoinPredicateType::Inputs(input) => { + if let Err(e) = input.validate() { + return Err(append_error_context( + "invalid predicate for scope 'inputs'", + e, + )); + } + } + BitcoinPredicateType::Outputs(outputs) => { + if let Err(e) = outputs.validate() { + return Err(append_error_context( + "invalid predicate for scope 'outputs'", + vec![e], + )); + } + } + BitcoinPredicateType::StacksProtocol(_) => {} + BitcoinPredicateType::OrdinalsProtocol(_) => {} + } + Ok(()) + } +} + pub struct BitcoinTriggerChainhook<'a> { pub chainhook: &'a BitcoinChainhookInstance, pub apply: Vec<(Vec<&'a BitcoinTransactionData>, &'a BitcoinBlockData)>, @@ -208,6 +330,15 @@ pub enum InputPredicate { WitnessScript(MatchingRule), } +impl InputPredicate { + pub fn validate(&self) -> Result<(), Vec> { + match self { + InputPredicate::Txid(txin) => txin.validate(), + InputPredicate::WitnessScript(_) => Ok(()), + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum OutputPredicate { @@ -219,6 +350,20 @@ pub enum OutputPredicate { Descriptor(DescriptorMatchingRule), } +impl OutputPredicate { + pub fn validate(&self) -> Result<(), String> { + match self { + OutputPredicate::OpReturn(_) => {} + OutputPredicate::P2pkh(ExactMatchingRule::Equals(_p2pkh)) => {} + OutputPredicate::P2sh(ExactMatchingRule::Equals(_p2sh)) => {} + OutputPredicate::P2wpkh(ExactMatchingRule::Equals(_p2wpkh)) => {} + OutputPredicate::P2wsh(ExactMatchingRule::Equals(_p2wsh)) => {} + OutputPredicate::Descriptor(descriptor) => descriptor.validate()?, + } + Ok(()) + } +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case", tag = "operation")] pub enum StacksOperations { @@ -348,6 +493,15 @@ pub struct TxinPredicate { pub vout: u32, } +impl TxinPredicate { + pub fn validate(&self) -> Result<(), Vec> { + if let Err(e) = validate_txid(&self.txid) { + return Err(vec![e]); + } + Ok(()) + } +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct DescriptorMatchingRule { @@ -357,6 +511,43 @@ pub struct DescriptorMatchingRule { pub range: Option<[u32; 2]>, } +impl DescriptorMatchingRule { + pub fn validate(&self) -> Result<(), String> { + let _ = self.derive_script_pubkeys()?; + Ok(()) + } + + pub fn derive_script_pubkeys(&self) -> Result, String> { + let DescriptorMatchingRule { expression, range } = self; + // To derive from descriptors, we need to provide a secp context. + let (sig, ver) = (&Secp256k1::signing_only(), &Secp256k1::verification_only()); + let (desc, _) = Descriptor::parse_descriptor(&sig, expression) + .map_err(|e| format!("invalid descriptor: {}", e.to_string()))?; + + // If the descriptor is derivable (`has_wildcard()`), we rely on the `range` field + // defined by the predicate OR fallback to a default range of [0,5] when not set. + // When the descriptor is not derivable we force to create a unique iteration by + // ranging over [0,1]. + let range = if desc.has_wildcard() { + range.unwrap_or([0, 5]) + } else { + [0, 1] + }; + + let mut script_pubkeys = vec![]; + // Derive the addresses and try to match them against the outputs. + for i in range[0]..range[1] { + let derived = desc + .derived_descriptor(&ver, i) + .map_err(|e| format!("error deriving descriptor: {}", e))?; + + // Extract and encode the derived pubkey. + script_pubkeys.push(hex::encode(derived.script_pubkey().as_bytes())); + } + Ok(script_pubkeys) + } +} + // deserialize_descriptor_range makes sure that the range value is valid. fn deserialize_descriptor_range<'de, D>(deserializer: D) -> Result, D::Error> where @@ -788,31 +979,11 @@ impl BitcoinPredicateType { } false } - BitcoinPredicateType::Outputs(OutputPredicate::Descriptor( - DescriptorMatchingRule { expression, range }, - )) => { - // To derive from descriptors, we need to provide a secp context. - let (sig, ver) = (&Secp256k1::signing_only(), &Secp256k1::verification_only()); - let (desc, _) = Descriptor::parse_descriptor(&sig, expression).unwrap(); - - // If the descriptor is derivable (`has_wildcard()`), we rely on the `range` field - // defined by the predicate OR fallback to a default range of [0,5] when not set. - // When the descriptor is not derivable we force to create a unique iteration by - // ranging over [0,1]. - let range = if desc.has_wildcard() { - range.unwrap_or([0, 5]) - } else { - [0, 1] - }; - - // Derive the addresses and try to match them against the outputs. - for i in range[0]..range[1] { - let derived = desc.derived_descriptor(&ver, i).unwrap(); - - // Extract and encode the derived pubkey. - let script_pubkey = hex::encode(derived.script_pubkey().as_bytes()); + BitcoinPredicateType::Outputs(OutputPredicate::Descriptor(descriptor)) => { + let script_pubkeys = descriptor.derive_script_pubkeys().unwrap(); - // Match that script against the tx outputs. + for script_pubkey in script_pubkeys { + // Match the script against the tx outputs. for (index, output) in tx.metadata.outputs.iter().enumerate() { if output.script_pubkey[2..] == script_pubkey { ctx.try_log(|logger| { diff --git a/components/chainhook-sdk/src/chainhooks/bitcoin/tests/hook_spec_validation.rs b/components/chainhook-sdk/src/chainhooks/bitcoin/tests/hook_spec_validation.rs new file mode 100644 index 000000000..33d7861aa --- /dev/null +++ b/components/chainhook-sdk/src/chainhooks/bitcoin/tests/hook_spec_validation.rs @@ -0,0 +1,198 @@ +use super::*; +use crate::chainhooks::{bitcoin::InscriptionFeedData, types::ChainhookSpecificationNetworkMap}; +use chainhook_types::BitcoinNetwork; +use test_case::test_case; +use crate::chainhooks::types::HttpHook; + +lazy_static! { + static ref TXID_NO_PREFIX: String = "1234567890123456789012345678901234567890123456789012345678901234".into(); + static ref TXID_NOT_HEX: String = "0xw234567890123456789012345678901234567890123456789012345678901234".into(); + static ref TXID_SHORT: String = "0x234567890123456789012345678901234567890123456789012345678901234".into(); + static ref TXID_LONG: String = "0x11234567890123456789012345678901234567890123456789012345678901234".into(); + static ref TXID_VALID: String = "0x1234567890123456789012345678901234567890123456789012345678901234".into(); + + static ref TXID_PREDICATE_ERR: String = "invalid predicate for scope 'txid': txid must be a 32 byte (64 character) hexadecimal string prefixed with '0x'".into(); + static ref INPUT_TXID_ERR: String = "invalid predicate for scope 'inputs': txid must be a 32 byte (64 character) hexadecimal string prefixed with '0x'".into(); + static ref DESCRIPTOR_KEY_SHORT_ERR: String = "invalid predicate for scope 'outputs': invalid descriptor: unexpected «unexpected «Key too short (<66 char), doesn't match any format»»".into(); + static ref INVALID_DESCRIPTOR_ERR: String = "invalid predicate for scope 'outputs': invalid descriptor: Anything but c:pk(key) (P2PK), c:pk_h(key) (P2PKH), and thresh_m(k,...) up to n=3 is invalid by standardness (bare).\n ".into(); + static ref INVALID_URL_ERR: String = "invalid 'http_post' data: url string must be a valid Url: relative URL without a base".into(); + static ref INVALID_HTTP_HEADER_ERR: String = "invalid 'http_post' data: auth header must be a valid header value: failed to parse header value".into(); + static ref INVALID_SPEC_NETWORK_MAP_ERR: String = "invalid Bitcoin predicate 'test' for network regtest: invalid 'then_that' value: invalid 'http_post' data: url string must be a valid Url: relative URL without a base\ninvalid Bitcoin predicate 'test' for network regtest: invalid 'then_that' value: invalid 'http_post' data: auth header must be a valid header value: failed to parse header value\ninvalid Bitcoin predicate 'test' for network regtest: invalid 'if_this' value: invalid predicate for scope 'txid': txid must be a 32 byte (64 character) hexadecimal string prefixed with '0x'\ninvalid Bitcoin predicate 'test' for network testnet: invalid 'then_that' value: invalid 'http_post' data: url string must be a valid Url: relative URL without a base\ninvalid Bitcoin predicate 'test' for network testnet: invalid 'then_that' value: invalid 'http_post' data: auth header must be a valid header value: failed to parse header value\ninvalid Bitcoin predicate 'test' for network testnet: invalid 'if_this' value: invalid predicate for scope 'txid': txid must be a 32 byte (64 character) hexadecimal string prefixed with '0x'\ninvalid Bitcoin predicate 'test' for network signet: invalid 'then_that' value: invalid 'http_post' data: url string must be a valid Url: relative URL without a base\ninvalid Bitcoin predicate 'test' for network signet: invalid 'then_that' value: invalid 'http_post' data: auth header must be a valid header value: failed to parse header value\ninvalid Bitcoin predicate 'test' for network signet: invalid 'if_this' value: invalid predicate for scope 'txid': txid must be a 32 byte (64 character) hexadecimal string prefixed with '0x'\ninvalid Bitcoin predicate 'test' for network mainnet: invalid 'then_that' value: invalid 'http_post' data: url string must be a valid Url: relative URL without a base\ninvalid Bitcoin predicate 'test' for network mainnet: invalid 'then_that' value: invalid 'http_post' data: auth header must be a valid header value: failed to parse header value\ninvalid Bitcoin predicate 'test' for network mainnet: invalid 'if_this' value: invalid predicate for scope 'txid': txid must be a 32 byte (64 character) hexadecimal string prefixed with '0x'".into(); + + static ref INVALID_TXID_PREDICATE: BitcoinPredicateType = + BitcoinPredicateType::Txid(ExactMatchingRule::Equals("test".into())); + static ref INVALID_HOOK_ACTION: HookAction = + HookAction::HttpPost(HttpHook { url: "".into(), authorization_header: "\n".into() }); + static ref ALL_INVALID_SPEC: BitcoinChainhookSpecification = BitcoinChainhookSpecification::new(INVALID_TXID_PREDICATE.clone(), INVALID_HOOK_ACTION.clone()); + static ref ALL_INVALID_SPEC_NETWORK_MAP: ChainhookSpecificationNetworkMap = + ChainhookSpecificationNetworkMap::Bitcoin( + BitcoinChainhookSpecificationNetworkMap { + uuid: "test".into(), + owner_uuid: None, + name: "test".into(), + version: 1, + networks: BTreeMap::from([ + (BitcoinNetwork::Regtest, ALL_INVALID_SPEC.clone()), + (BitcoinNetwork::Signet, ALL_INVALID_SPEC.clone()), + (BitcoinNetwork::Mainnet, ALL_INVALID_SPEC.clone()), + (BitcoinNetwork::Testnet, ALL_INVALID_SPEC.clone()), + ]) + } + ); +} + +// BitcoinPredicateType::Block +#[test_case(&BitcoinPredicateType::Block, None; "block")] +// BitcoinPredicateType::Txid +#[test_case( + &BitcoinPredicateType::Txid(ExactMatchingRule::Equals(TXID_NO_PREFIX.clone())), + Some(vec![TXID_PREDICATE_ERR.clone()]); "txid without 0x" +)] +#[test_case( + &BitcoinPredicateType::Txid(ExactMatchingRule::Equals(TXID_NOT_HEX.clone())), + Some(vec![TXID_PREDICATE_ERR.clone()]); "txid not hex" +)] +#[test_case( + &BitcoinPredicateType::Txid(ExactMatchingRule::Equals(TXID_SHORT.clone())), + Some(vec![TXID_PREDICATE_ERR.clone()]); "txid too short" +)] +#[test_case( + &BitcoinPredicateType::Txid(ExactMatchingRule::Equals(TXID_LONG.clone())), + Some(vec![TXID_PREDICATE_ERR.clone()]); "txid too long" +)] +#[test_case( + &BitcoinPredicateType::Txid(ExactMatchingRule::Equals(TXID_VALID.clone())), + None; "txid just right" +)] +// BitcoinPredicateType::Inputs +#[test_case( + &BitcoinPredicateType::Inputs(InputPredicate::Txid(TxinPredicate { txid: TXID_NO_PREFIX.clone(), vout: 0})), + Some(vec![INPUT_TXID_ERR.clone()]); "inputs txid without 0x" +)] +#[test_case( + &BitcoinPredicateType::Inputs(InputPredicate::Txid(TxinPredicate { txid: TXID_NOT_HEX.clone(), vout: 0})), + Some(vec![INPUT_TXID_ERR.clone()]); "inputs txid not hex" +)] +#[test_case( + &BitcoinPredicateType::Inputs(InputPredicate::Txid(TxinPredicate { txid: TXID_SHORT.clone(), vout: 0})), + Some(vec![INPUT_TXID_ERR.clone()]); "inputs txid too short" +)] +#[test_case( + &BitcoinPredicateType::Inputs(InputPredicate::Txid(TxinPredicate { txid: TXID_LONG.clone(), vout: 0})), + Some(vec![INPUT_TXID_ERR.clone()]); "inputs txid too long" +)] +#[test_case( + &BitcoinPredicateType::Inputs(InputPredicate::Txid(TxinPredicate { txid: TXID_VALID.clone(), vout: 0})), + None; "inputs txid just right" +)] +// BitcoinPredicateType::Outputs +#[test_case( + &BitcoinPredicateType::Outputs(OutputPredicate::OpReturn(MatchingRule::Equals("".into()))), + None; "outputs opreturn" +)] +#[test_case( + &BitcoinPredicateType::Outputs(OutputPredicate::P2pkh(ExactMatchingRule::Equals("".into()))), + None; "outputs p2pkh" +)] +#[test_case( + &BitcoinPredicateType::Outputs(OutputPredicate::P2sh(ExactMatchingRule::Equals("".into()))), + None; "outputs p2sh" +)] +#[test_case( + &BitcoinPredicateType::Outputs(OutputPredicate::P2wpkh(ExactMatchingRule::Equals("".into()))), + None; "outputs p2wpkh" +)] +#[test_case( + &BitcoinPredicateType::Outputs(OutputPredicate::P2wsh(ExactMatchingRule::Equals("".into()))), + None; "outputs p2wsh" +)] +#[test_case( + &BitcoinPredicateType::Outputs(OutputPredicate::Descriptor( + DescriptorMatchingRule { + expression: "wpkh(02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9)".into(), + range: None + } + )), + None; "outputs descriptor ok" +)] +#[test_case( + &BitcoinPredicateType::Outputs(OutputPredicate::Descriptor( + DescriptorMatchingRule { + expression: "wpkh(0)".into(), + range: None + } + )), + Some(vec![DESCRIPTOR_KEY_SHORT_ERR.clone()]); "outputs descriptor too short" +)] +#[test_case( + &BitcoinPredicateType::Outputs(OutputPredicate::Descriptor( + DescriptorMatchingRule { + expression: "0".into(), + range: None + } + )), + Some(vec![INVALID_DESCRIPTOR_ERR.clone()]); "outputs invalid descriptor" +)] +// BitcoinPredicateType::StacksProtocol +#[test_case(&BitcoinPredicateType::StacksProtocol(StacksOperations::StackerRewarded), None; "stacks protocol")] +// BitcoinPredicateType::OrdinalsProtocol +#[test_case(&BitcoinPredicateType::OrdinalsProtocol(OrdinalOperations::InscriptionFeed(InscriptionFeedData { meta_protocols: None})), None; "ordinals protocol")] +fn it_validates_bitcoin_predicates(predicate: &BitcoinPredicateType, expected_err: Option>) { + if let Err(e) = predicate.validate() { + if let Some(expected) = expected_err { + assert_eq!(e, expected); + } + else { + panic!( + "Unexpected error in predicate validation: {:?}", + predicate + ); + } + } else { + if let Some(_) = expected_err { + panic!( + "Missing expected error for predicate validation: {:?}", + predicate + ); + } + } +} + + +#[test_case(&INVALID_HOOK_ACTION, Some(vec![INVALID_URL_ERR.clone(), INVALID_HTTP_HEADER_ERR.clone()]); "invalid http_post action" +)] +fn it_validates_hook_actions(action: &HookAction, expected_err: Option>) { + if let Err(e) = action.validate() { + if let Some(expected) = expected_err { + assert_eq!(e, expected); + } + else { + panic!( + "Unexpected error in predicate validation: {:?}", + action + ); + } + } else { + if let Some(_) = expected_err { + panic!( + "Missing expected error for predicate validation: {:?}", + action + ); + } + } +} + +#[test_case(&ALL_INVALID_SPEC_NETWORK_MAP, INVALID_SPEC_NETWORK_MAP_ERR.clone())] +fn it_validates_bitcoin_chainhook_specs( + predicate: &ChainhookSpecificationNetworkMap, + expected_err: String, +) { + if let Err(e) = predicate.validate() { + assert_eq!(e, expected_err); + } else { + panic!( + "Missing expected error for predicate validation: {:?}", + predicate + ); + } +} diff --git a/components/chainhook-sdk/src/chainhooks/bitcoin/tests.rs b/components/chainhook-sdk/src/chainhooks/bitcoin/tests/mod.rs similarity index 99% rename from components/chainhook-sdk/src/chainhooks/bitcoin/tests.rs rename to components/chainhook-sdk/src/chainhooks/bitcoin/tests/mod.rs index dd9cb52b2..4f3f80a36 100644 --- a/components/chainhook-sdk/src/chainhooks/bitcoin/tests.rs +++ b/components/chainhook-sdk/src/chainhooks/bitcoin/tests/mod.rs @@ -11,6 +11,7 @@ use chainhook_types::bitcoin::TxOut; use chainhook_types::{BitcoinNetwork, Brc20Operation, Brc20TokenDeployData}; use test_case::test_case; +mod hook_spec_validation; #[test_case( "0x6affAAAA", diff --git a/components/chainhook-sdk/src/chainhooks/stacks/mod.rs b/components/chainhook-sdk/src/chainhooks/stacks/mod.rs index de5370422..417c0ca99 100644 --- a/components/chainhook-sdk/src/chainhooks/stacks/mod.rs +++ b/components/chainhook-sdk/src/chainhooks/stacks/mod.rs @@ -1,6 +1,10 @@ -use crate::utils::{AbstractStacksBlock, Context}; +use crate::utils::{AbstractStacksBlock, Context, MAX_BLOCK_HEIGHTS_ENTRIES}; -use super::types::{BlockIdentifierIndexRule, ChainhookInstance, ExactMatchingRule, HookAction}; +use super::types::validate_txid; +use super::types::{ + append_error_context, BlockIdentifierIndexRule, ChainhookInstance, ExactMatchingRule, + HookAction, +}; use chainhook_types::{ BlockIdentifier, StacksChainEvent, StacksNetwork, StacksTransactionData, StacksTransactionEvent, StacksTransactionEventPayload, StacksTransactionKind, @@ -12,7 +16,10 @@ use reqwest::{Client, Method}; use schemars::JsonSchema; use serde_json::Value as JsonValue; use stacks_codec::clarity::codec::StacksMessageCodec; +use stacks_codec::clarity::vm::types::PrincipalData; +use stacks_codec::clarity::vm::types::QualifiedContractIdentifier; use stacks_codec::clarity::vm::types::{CharType, SequenceData, Value as ClarityValue}; +use stacks_codec::clarity::ClarityName; use std::collections::{BTreeMap, HashMap}; use std::io::Cursor; @@ -40,6 +47,84 @@ pub struct StacksChainhookSpecification { pub action: HookAction, } +impl StacksChainhookSpecification { + pub fn new(predicate: StacksPredicate, action: HookAction) -> Self { + StacksChainhookSpecification { + blocks: None, + start_block: None, + end_block: None, + expire_after_occurrence: None, + capture_all_events: None, + include_contract_abi: None, + decode_clarity_values: None, + predicate, + action, + } + } + + pub fn blocks(&mut self, blocks: Vec) -> &mut Self { + self.blocks = Some(blocks); + self + } + + pub fn start_block(&mut self, start_block: u64) -> &mut Self { + self.start_block = Some(start_block); + self + } + + pub fn end_block(&mut self, end_block: u64) -> &mut Self { + self.end_block = Some(end_block); + self + } + + pub fn expire_after_occurrence(&mut self, occurrence: u64) -> &mut Self { + self.expire_after_occurrence = Some(occurrence); + self + } + + pub fn capture_all_events(&mut self, do_capture: bool) -> &mut Self { + self.capture_all_events = Some(do_capture); + self + } + + pub fn include_contract_abi(&mut self, do_include: bool) -> &mut Self { + self.include_contract_abi = Some(do_include); + self + } + + pub fn decode_clarity_values(&mut self, do_decode: bool) -> &mut Self { + self.decode_clarity_values = Some(do_decode); + self + } + + pub fn validate(&self) -> Result<(), Vec> { + let mut errors = vec![]; + if let Err(e) = self.action.validate() { + errors.append(&mut append_error_context("invalid 'then_that' value", e)); + } + if let Err(e) = self.predicate.validate() { + errors.append(&mut append_error_context("invalid 'if_this' value", e)); + } + + if let Some(end_block) = self.end_block { + let start_block = self.start_block.unwrap_or(0); + if start_block > end_block { + errors.push( + "Chainhook specification field `end_block` should be greater than `start_block`.".into() + ); + } + if (end_block - start_block) > MAX_BLOCK_HEIGHTS_ENTRIES { + errors.push(format!("Chainhook specification exceeds max number of blocks to scan. Maximum: {}, Attempted: {}", MAX_BLOCK_HEIGHTS_ENTRIES, (end_block - start_block))); + } + } + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } + } +} + /// Maps some [StacksChainhookSpecification] to a corresponding [StacksNetwork]. This allows maintaining one /// serialized predicate file for a given predicate on each network. /// @@ -174,6 +259,57 @@ pub enum StacksPredicate { Txid(ExactMatchingRule), } +impl StacksPredicate { + pub fn validate(&self) -> Result<(), Vec> { + match self { + StacksPredicate::BlockHeight(height) => { + if let Err(e) = height.validate() { + return Err(append_error_context( + "invalid predicate for scope 'block_height'", + vec![e], + )); + } + } + StacksPredicate::ContractDeployment(predicate) => { + if let Err(e) = predicate.validate() { + return Err(append_error_context( + "invalid predicate for scope 'contract_deployment'", + vec![e], + )); + } + } + StacksPredicate::ContractCall(predicate) => { + if let Err(e) = predicate.validate() { + return Err(append_error_context( + "invalid predicate for scope 'contract_call'", + e, + )); + } + } + StacksPredicate::PrintEvent(predicate) => { + if let Err(e) = predicate.validate() { + return Err(append_error_context( + "invalid predicate for scope 'print_event'", + e, + )); + } + } + StacksPredicate::FtEvent(_) => {} + StacksPredicate::NftEvent(_) => {} + StacksPredicate::StxEvent(_) => {} + StacksPredicate::Txid(ExactMatchingRule::Equals(txid)) => { + if let Err(e) = validate_txid(&txid) { + return Err(append_error_context( + "invalid predicate for scope 'txid'", + vec![e], + )); + } + } + } + Ok(()) + } +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct StacksContractCallBasedPredicate { @@ -181,12 +317,58 @@ pub struct StacksContractCallBasedPredicate { pub method: String, } +fn validate_contract_identifier(id: &String) -> Result<(), String> { + if let Err(e) = QualifiedContractIdentifier::parse(&id) { + return Err(format!("invalid contract identifier: {}", e.to_string())); + } + Ok(()) +} + +impl StacksContractCallBasedPredicate { + pub fn validate(&self) -> Result<(), Vec> { + let mut errors = vec![]; + + if let Err(e) = validate_contract_identifier(&self.contract_identifier) { + errors.push(e); + } + if let Err(e) = ClarityName::try_from(self.method.clone()) { + errors.push(format!("invalid contract method: {:?}", e)); + } + + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum StacksContractDeploymentPredicate { Deployer(String), ImplementTrait(StacksTrait), } + +impl StacksContractDeploymentPredicate { + pub fn validate(&self) -> Result<(), String> { + match self { + StacksContractDeploymentPredicate::Deployer(deployer) => { + if !deployer.eq("*") { + if let Err(e) = PrincipalData::parse_standard_principal(&deployer) { + return Err(format!( + "contract deployer must be a valid Stacks address: {}", + e + )); + } + } + } + StacksContractDeploymentPredicate::ImplementTrait(_) => {} + } + Ok(()) + } +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum StacksTrait { @@ -211,6 +393,43 @@ pub enum StacksPrintEventBasedPredicate { }, } +impl StacksPrintEventBasedPredicate { + pub fn validate(&self) -> Result<(), Vec> { + let mut errors = vec![]; + match self { + StacksPrintEventBasedPredicate::Contains { + contract_identifier, + .. + } => { + if !contract_identifier.eq("*") { + if let Err(e) = validate_contract_identifier(&contract_identifier) { + errors.push(e); + } + } + } + StacksPrintEventBasedPredicate::MatchesRegex { + contract_identifier, + regex, + } => { + if !contract_identifier.eq("*") { + if let Err(e) = validate_contract_identifier(&contract_identifier) { + errors.push(e); + } + } + if let Err(e) = Regex::new(regex) { + errors.push(format!("invalid regex: {}", e.to_string())) + } + } + } + + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct StacksFtEventBasedPredicate { @@ -1150,3 +1369,6 @@ pub fn handle_stacks_hook_action<'a>( )), } } + +#[cfg(test)] +pub mod tests; diff --git a/components/chainhook-sdk/src/chainhooks/stacks/tests/hook_spec_validation.rs b/components/chainhook-sdk/src/chainhooks/stacks/tests/hook_spec_validation.rs new file mode 100644 index 000000000..b541e0e7a --- /dev/null +++ b/components/chainhook-sdk/src/chainhooks/stacks/tests/hook_spec_validation.rs @@ -0,0 +1,211 @@ +use std::collections::BTreeMap; +use crate::chainhooks::stacks::{StacksChainhookSpecification, StacksChainhookSpecificationNetworkMap, StacksContractCallBasedPredicate, StacksContractDeploymentPredicate, StacksPredicate, StacksPrintEventBasedPredicate}; +use crate::chainhooks::types::*; +use crate::chainhooks::types::HttpHook; +use chainhook_types::StacksNetwork; +use test_case::test_case; + +lazy_static! { + static ref TXID_NO_PREFIX: String = "1234567890123456789012345678901234567890123456789012345678901234".into(); + static ref TXID_NOT_HEX: String = "0xw234567890123456789012345678901234567890123456789012345678901234".into(); + static ref TXID_SHORT: String = "0x234567890123456789012345678901234567890123456789012345678901234".into(); + static ref TXID_LONG: String = "0x11234567890123456789012345678901234567890123456789012345678901234".into(); + static ref TXID_VALID: String = "0x1234567890123456789012345678901234567890123456789012345678901234".into(); + static ref STACKS_ADDRESS_INVALID: String = "SQ1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM".into(); + static ref STACKS_ADDRESS_VALID_MAINNET: String = "SP000000000000000000002Q6VF78".into(); + static ref STACKS_ADDRESS_VALID_TESTNET: String = "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM".into(); + static ref STACKS_ADDRESS_VALID_MULTISIG: String = "SN2QE43MMXFDMAT3TPRGQ38BQ50VSRMBRQ6B16W5J".into(); + static ref CONTRACT_ID_INVALID_ADDRESS: String = "SQ1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.contract-name".into(); + static ref CONTRACT_ID_NO_PERIOD: String = "SQ1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGMcontract-name".into(); + static ref CONTRACT_ID_INVALID_NAME: String = "SQ1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.!&*!".into(); + static ref CONTRACT_ID_VALID: String = "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.contract-name".into(); + static ref INVALID_METHOD: String = "!@*&!*".into(); + static ref INVALID_REGEX: String = "[\\]".into(); + static ref VALID_REGEX: String = "anything".into(); + + static ref TXID_PREDICATE_ERR: String = "invalid predicate for scope 'txid': txid must be a 32 byte (64 character) hexadecimal string prefixed with '0x'".into(); + static ref INPUT_TXID_ERR: String = "invalid predicate for scope 'inputs': txid must be a 32 byte (64 character) hexadecimal string prefixed with '0x'".into(); + static ref INVALID_SPEC_NETWORK_MAP_ERR: String = "invalid Stacks predicate 'test' for network simnet: invalid 'then_that' value: invalid 'http_post' data: url string must be a valid Url: relative URL without a base\ninvalid Stacks predicate 'test' for network simnet: invalid 'then_that' value: invalid 'http_post' data: auth header must be a valid header value: failed to parse header value\ninvalid Stacks predicate 'test' for network simnet: invalid 'if_this' value: invalid predicate for scope 'print_event': invalid contract identifier: ParseError(\"Invalid principal literal: base58ck checksum 0x147e6835 does not match expected 0x9b3dfe6a\")\ninvalid Stacks predicate 'test' for network simnet: invalid 'if_this' value: invalid predicate for scope 'print_event': invalid regex: regex parse error:\n [\\]\n ^\nerror: unclosed character class\ninvalid Stacks predicate 'test' for network devnet: invalid 'then_that' value: invalid 'http_post' data: url string must be a valid Url: relative URL without a base\ninvalid Stacks predicate 'test' for network devnet: invalid 'then_that' value: invalid 'http_post' data: auth header must be a valid header value: failed to parse header value\ninvalid Stacks predicate 'test' for network devnet: invalid 'if_this' value: invalid predicate for scope 'print_event': invalid contract identifier: ParseError(\"Invalid principal literal: base58ck checksum 0x147e6835 does not match expected 0x9b3dfe6a\")\ninvalid Stacks predicate 'test' for network devnet: invalid 'if_this' value: invalid predicate for scope 'print_event': invalid regex: regex parse error:\n [\\]\n ^\nerror: unclosed character class\ninvalid Stacks predicate 'test' for network testnet: invalid 'then_that' value: invalid 'http_post' data: url string must be a valid Url: relative URL without a base\ninvalid Stacks predicate 'test' for network testnet: invalid 'then_that' value: invalid 'http_post' data: auth header must be a valid header value: failed to parse header value\ninvalid Stacks predicate 'test' for network testnet: invalid 'if_this' value: invalid predicate for scope 'print_event': invalid contract identifier: ParseError(\"Invalid principal literal: base58ck checksum 0x147e6835 does not match expected 0x9b3dfe6a\")\ninvalid Stacks predicate 'test' for network testnet: invalid 'if_this' value: invalid predicate for scope 'print_event': invalid regex: regex parse error:\n [\\]\n ^\nerror: unclosed character class\ninvalid Stacks predicate 'test' for network mainnet: invalid 'then_that' value: invalid 'http_post' data: url string must be a valid Url: relative URL without a base\ninvalid Stacks predicate 'test' for network mainnet: invalid 'then_that' value: invalid 'http_post' data: auth header must be a valid header value: failed to parse header value\ninvalid Stacks predicate 'test' for network mainnet: invalid 'if_this' value: invalid predicate for scope 'print_event': invalid contract identifier: ParseError(\"Invalid principal literal: base58ck checksum 0x147e6835 does not match expected 0x9b3dfe6a\")\ninvalid Stacks predicate 'test' for network mainnet: invalid 'if_this' value: invalid predicate for scope 'print_event': invalid regex: regex parse error:\n [\\]\n ^\nerror: unclosed character class".into(); + static ref CONTRACT_DEPLOYER_ERR: String = "invalid predicate for scope 'contract_deployment': contract deployer must be a valid Stacks address: ParseError(\"Invalid principal literal: base58ck checksum 0x147e6835 does not match expected 0x9b3dfe6a\")".into(); + static ref CONTRACT_ID_ERR: String = "invalid predicate for scope 'contract_call': invalid contract identifier: ParseError(\"Invalid principal literal: base58ck checksum 0x147e6835 does not match expected 0x9b3dfe6a\")".into(); + static ref CONTRACT_ID_NO_PERIOD_ERR: String = "invalid predicate for scope 'contract_call': invalid contract identifier: ParseError(\"Invalid principal literal: expected a `.` in a qualified contract name\")".into(); + static ref CONTRACT_METHOD_ERR: String = "invalid predicate for scope 'contract_call': invalid contract method: BadNameValue(\"ClarityName\", \"!@*&!*\")".into(); + static ref PRINT_EVENT_ID_ERR: String = "invalid predicate for scope 'print_event': invalid contract identifier: ParseError(\"Invalid principal literal: base58ck checksum 0x147e6835 does not match expected 0x9b3dfe6a\")".into(); + static ref INVALID_REGEX_ERR: String = "invalid predicate for scope 'print_event': invalid regex: regex parse error:\n [\\]\n ^\nerror: unclosed character class".into(); + + static ref INVALID_PREDICATE: StacksPredicate = StacksPredicate::PrintEvent(StacksPrintEventBasedPredicate::MatchesRegex { contract_identifier: CONTRACT_ID_INVALID_ADDRESS.clone(), regex: INVALID_REGEX.clone() }); + static ref INVALID_HOOK_ACTION: HookAction = + HookAction::HttpPost(HttpHook { url: "".into(), authorization_header: "\n".into() }); + static ref ALL_INVALID_SPEC: StacksChainhookSpecification = StacksChainhookSpecification::new(INVALID_PREDICATE.clone(), INVALID_HOOK_ACTION.clone()); + static ref ALL_INVALID_SPEC_NETWORK_MAP: ChainhookSpecificationNetworkMap = + ChainhookSpecificationNetworkMap::Stacks( + StacksChainhookSpecificationNetworkMap { + uuid: "test".into(), + owner_uuid: None, + name: "test".into(), + version: 1, + networks: BTreeMap::from([ + (StacksNetwork::Simnet, ALL_INVALID_SPEC.clone()), + (StacksNetwork::Devnet, ALL_INVALID_SPEC.clone()), + (StacksNetwork::Testnet, ALL_INVALID_SPEC.clone()), + (StacksNetwork::Mainnet, ALL_INVALID_SPEC.clone()), + ]) + } + ); + +} + +// StacksPredicate::BlockHeight +#[test_case( + &StacksPredicate::BlockHeight(BlockIdentifierIndexRule::LowerThan(0)), + Some(vec!["invalid predicate for scope 'block_height': 'lower_than' filter must be greater than 0".to_string()]); + "invalid lower than" +)] +#[test_case(&StacksPredicate::BlockHeight(BlockIdentifierIndexRule::LowerThan(1)), None; "valid lower than")] +#[test_case( + &StacksPredicate::BlockHeight(BlockIdentifierIndexRule::Between(10, 5)), + Some(vec!["invalid predicate for scope 'block_height': 'between' filter must have left-hand-side valud greater than right-hand-side value".to_string()]); + "invalid between" +)] +#[test_case(&StacksPredicate::BlockHeight(BlockIdentifierIndexRule::Between(5, 10)), None; "valid between")] +// StacksPredicate::ContractDeployment +#[test_case( + &StacksPredicate::ContractDeployment(StacksContractDeploymentPredicate::Deployer(STACKS_ADDRESS_INVALID.clone())), + Some(vec![CONTRACT_DEPLOYER_ERR.clone()]); + "deployer bad prefix" +)] +#[test_case( + &StacksPredicate::ContractDeployment(StacksContractDeploymentPredicate::Deployer(STACKS_ADDRESS_VALID_MAINNET.clone())), + None; + "deployer valid mainnet" +)] +#[test_case( + &StacksPredicate::ContractDeployment(StacksContractDeploymentPredicate::Deployer(STACKS_ADDRESS_VALID_TESTNET.clone())), + None; + "deployer valid testnet" +)] +#[test_case( + &StacksPredicate::ContractDeployment(StacksContractDeploymentPredicate::Deployer(STACKS_ADDRESS_VALID_MULTISIG.clone())), + None; + "deployer valid multisig" +)] +#[test_case( + &StacksPredicate::ContractDeployment(StacksContractDeploymentPredicate::Deployer("*".to_string())), + None; + "deployer valid wildcard" +)] +// StacksPredicate::ContractCall +#[test_case( + &StacksPredicate::ContractCall(StacksContractCallBasedPredicate { contract_identifier: CONTRACT_ID_INVALID_ADDRESS.clone(), method: INVALID_METHOD.clone()}), + Some(vec![CONTRACT_ID_ERR.clone(), CONTRACT_METHOD_ERR.clone()]); + "invalid id with invalid method" +)] +#[test_case( + &StacksPredicate::ContractCall(StacksContractCallBasedPredicate { contract_identifier: CONTRACT_ID_VALID.clone(), method: INVALID_METHOD.clone()}), + Some(vec![CONTRACT_METHOD_ERR.clone()]); + "valid id with invalid method" +)] +#[test_case( + &StacksPredicate::ContractCall(StacksContractCallBasedPredicate { contract_identifier: CONTRACT_ID_NO_PERIOD.clone(), method: "contract-name".to_string()}), + Some(vec![CONTRACT_ID_NO_PERIOD_ERR.clone()]); + "id no period" +)] +#[test_case( + &StacksPredicate::ContractCall(StacksContractCallBasedPredicate { contract_identifier: CONTRACT_ID_INVALID_NAME.clone(), method: "contract-name".to_string()}), + Some(vec![CONTRACT_ID_ERR.clone()]); + "id invalid contract name" +)] +#[test_case( + &StacksPredicate::ContractCall(StacksContractCallBasedPredicate { contract_identifier: CONTRACT_ID_VALID.clone(), method: "contract-name".to_string()}), + None; + "id valid" +)] +// StacksPredicate::PrintEvent +#[test_case( + &StacksPredicate::PrintEvent(StacksPrintEventBasedPredicate::Contains { contract_identifier: CONTRACT_ID_INVALID_ADDRESS.clone(), contains: "string".to_string() }), + Some(vec![PRINT_EVENT_ID_ERR.clone()]); + "contains invalid id" +)] +#[test_case( + &StacksPredicate::PrintEvent(StacksPrintEventBasedPredicate::Contains { contract_identifier: CONTRACT_ID_VALID.clone(), contains: "string".to_string() }), + None; + "contains valid" +)] +#[test_case( + &StacksPredicate::PrintEvent(StacksPrintEventBasedPredicate::Contains { contract_identifier: "*".to_string(), contains: "string".to_string() }), + None; + "allows wildcard contract id" +)] +#[test_case( + &StacksPredicate::PrintEvent(StacksPrintEventBasedPredicate::MatchesRegex { contract_identifier: CONTRACT_ID_INVALID_ADDRESS.clone(), regex: VALID_REGEX.clone() }), + Some(vec![PRINT_EVENT_ID_ERR.clone()]); + "regex invalid id" +)] +#[test_case( + &StacksPredicate::PrintEvent(StacksPrintEventBasedPredicate::MatchesRegex { contract_identifier: CONTRACT_ID_VALID.clone(), regex: INVALID_REGEX.clone() }), + Some(vec![INVALID_REGEX_ERR.clone()]); + "regex invalid regex" +)] +#[test_case( + &StacksPredicate::PrintEvent(StacksPrintEventBasedPredicate::MatchesRegex { contract_identifier: CONTRACT_ID_INVALID_ADDRESS.clone(), regex: INVALID_REGEX.clone() }), + Some(vec![PRINT_EVENT_ID_ERR.clone(), INVALID_REGEX_ERR.clone()]); + "regex invalid both" +)] +#[test_case( + &StacksPredicate::PrintEvent(StacksPrintEventBasedPredicate::MatchesRegex { contract_identifier: CONTRACT_ID_VALID.clone(), regex: VALID_REGEX.clone() }), + None; + "regex valid" +)] +// StacksPredicate::Txid +#[test_case( + &StacksPredicate::Txid(ExactMatchingRule::Equals(TXID_NO_PREFIX.clone())), + Some(vec![TXID_PREDICATE_ERR.clone()]); "txid without 0x" +)] +#[test_case( + &StacksPredicate::Txid(ExactMatchingRule::Equals(TXID_NOT_HEX.clone())), + Some(vec![TXID_PREDICATE_ERR.clone()]); "txid not hex" +)] +#[test_case( + &StacksPredicate::Txid(ExactMatchingRule::Equals(TXID_SHORT.clone())), + Some(vec![TXID_PREDICATE_ERR.clone()]); "txid too short" +)] +#[test_case( + &StacksPredicate::Txid(ExactMatchingRule::Equals(TXID_LONG.clone())), + Some(vec![TXID_PREDICATE_ERR.clone()]); "txid too long" +)] +#[test_case( + &StacksPredicate::Txid(ExactMatchingRule::Equals(TXID_VALID.clone())), + None; "txid just right" +)] +fn it_validates_stacks_predicates(predicate: &StacksPredicate, expected_err: Option>) { + if let Err(e) = predicate.validate() { + if let Some(expected) = expected_err { + assert_eq!(e, expected); + } else { + panic!("Unexpected error in predicate validation: {:?}", predicate); + } + } else { + if let Some(_) = expected_err { + panic!( + "Missing expected error for predicate validation: {:?}", + predicate + ); + } + } +} + + +#[test_case(&ALL_INVALID_SPEC_NETWORK_MAP, INVALID_SPEC_NETWORK_MAP_ERR.clone())] +fn it_validates_stacks_chainhook_specs( + predicate: &ChainhookSpecificationNetworkMap, + expected_err: String, +) { + if let Err(e) = predicate.validate() { + assert_eq!(e, expected_err); + } else { + panic!( + "Missing expected error for predicate validation: {:?}", + predicate + ); + } +} diff --git a/components/chainhook-sdk/src/chainhooks/stacks/tests/mod.rs b/components/chainhook-sdk/src/chainhooks/stacks/tests/mod.rs new file mode 100644 index 000000000..1694a9e5e --- /dev/null +++ b/components/chainhook-sdk/src/chainhooks/stacks/tests/mod.rs @@ -0,0 +1 @@ +mod hook_spec_validation; diff --git a/components/chainhook-sdk/src/chainhooks/types.rs b/components/chainhook-sdk/src/chainhooks/types.rs index dc965d9f4..69eaf6f14 100644 --- a/components/chainhook-sdk/src/chainhooks/types.rs +++ b/components/chainhook-sdk/src/chainhooks/types.rs @@ -1,12 +1,11 @@ +use std::str::FromStr; + use chainhook_types::{BitcoinNetwork, StacksNetwork}; -use reqwest::Url; use serde::ser::{SerializeSeq, Serializer}; use serde::{Deserialize, Serialize}; use schemars::JsonSchema; -use crate::utils::MAX_BLOCK_HEIGHTS_ENTRIES; - use crate::chainhooks::bitcoin::BitcoinChainhookInstance; use crate::chainhooks::bitcoin::BitcoinChainhookSpecificationNetworkMap; use crate::chainhooks::stacks::StacksChainhookInstance; @@ -207,38 +206,38 @@ impl ChainhookSpecificationNetworkMap { pub fn validate(&self) -> Result<(), String> { match &self { Self::Bitcoin(data) => { - for (_, spec) in data.networks.iter() { - let _ = spec.action.validate()?; - if let Some(end_block) = spec.end_block { - let start_block = spec.start_block.unwrap_or(0); - if start_block > end_block { - return Err( - "Chainhook specification field `end_block` should be greater than `start_block`." - .into(), - ); - } - if (end_block - start_block) > MAX_BLOCK_HEIGHTS_ENTRIES { - return Err(format!("Chainhook specification exceeds max number of blocks to scan. Maximum: {}, Attempted: {}", MAX_BLOCK_HEIGHTS_ENTRIES, (end_block - start_block))); - } + let mut errors = vec![]; + for (network, spec) in data.networks.iter() { + if let Err(e) = spec.validate() { + errors.append(&mut append_error_context( + &format!( + "invalid Bitcoin predicate '{}' for network {}", + data.name, network + ), + e, + )); } } + if !errors.is_empty() { + return Err(errors.join("\n")); + } } Self::Stacks(data) => { - for (_, spec) in data.networks.iter() { - let _ = spec.action.validate()?; - if let Some(end_block) = spec.end_block { - let start_block = spec.start_block.unwrap_or(0); - if start_block > end_block { - return Err( - "Chainhook specification field `end_block` should be greater than `start_block`." - .into(), - ); - } - if (end_block - start_block) > MAX_BLOCK_HEIGHTS_ENTRIES { - return Err(format!("Chainhook specification exceeds max number of blocks to scan. Maximum: {}, Attempted: {}", MAX_BLOCK_HEIGHTS_ENTRIES, (end_block - start_block))); - } + let mut errors = vec![]; + for (network, spec) in data.networks.iter() { + if let Err(e) = spec.validate() { + errors.append(&mut append_error_context( + &format!( + "invalid Stacks predicate '{}' for network {}", + data.name, network + ), + e, + )); } } + if !errors.is_empty() { + return Err(errors.join("\n")); + } } } Ok(()) @@ -270,11 +269,12 @@ pub enum HookAction { } impl HookAction { - pub fn validate(&self) -> Result<(), String> { + pub fn validate(&self) -> Result<(), Vec> { match &self { HookAction::HttpPost(spec) => { - let _ = Url::parse(&spec.url) - .map_err(|e| format!("hook action url invalid ({})", e.to_string()))?; + if let Err(e) = spec.validate() { + return Err(append_error_context("invalid 'http_post' data", e)); + } } HookAction::FileAppend(_) => {} HookAction::Noop => {} @@ -290,6 +290,27 @@ pub struct HttpHook { pub authorization_header: String, } +impl HttpHook { + pub fn validate(&self) -> Result<(), Vec> { + let mut errors = vec![]; + if let Err(e) = reqwest::Url::from_str(&self.url) { + errors.push(format!("url string must be a valid Url: {}", e.to_string())); + } + if let Err(e) = reqwest::header::HeaderValue::from_str(&self.authorization_header) { + errors.push(format!( + "auth header must be a valid header value: {}", + e.to_string() + )); + }; + + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct FileHook { @@ -349,6 +370,26 @@ pub enum BlockIdentifierIndexRule { Between(u64, u64), } +impl BlockIdentifierIndexRule { + pub fn validate(&self) -> Result<(), String> { + match self { + BlockIdentifierIndexRule::Equals(_) => {} + BlockIdentifierIndexRule::HigherThan(_) => {} + BlockIdentifierIndexRule::LowerThan(val) => { + if val.eq(&0) { + return Err("'lower_than' filter must be greater than 0".into()); + } + } + BlockIdentifierIndexRule::Between(lhs, rhs) => { + if lhs >= rhs { + return Err("'between' filter must have left-hand-side valud greater than right-hand-side value".into()); + } + } + } + Ok(()) + } +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum Scope { @@ -898,3 +939,23 @@ pub fn opcode_to_hex(asm: &str) -> Option { _ => None, } } + +pub fn append_error_context(context: &str, errors: Vec) -> Vec { + errors + .iter() + .map(|e| format!("{}: {}", context, e)) + .collect() +} + +pub fn is_hex(s: &str) -> bool { + s.chars().all(|c| c.is_digit(16)) +} + +pub fn validate_txid(txid: &String) -> Result<(), String> { + if !txid.starts_with("0x") || txid.len() != 66 || !is_hex(&txid[2..66]) { + return Err( + "txid must be a 32 byte (64 character) hexadecimal string prefixed with '0x'".into(), + ); + } + Ok(()) +} diff --git a/components/chainhook-types-rs/src/rosetta.rs b/components/chainhook-types-rs/src/rosetta.rs index a794a27b0..9ebbb18e6 100644 --- a/components/chainhook-types-rs/src/rosetta.rs +++ b/components/chainhook-types-rs/src/rosetta.rs @@ -759,12 +759,18 @@ pub enum StacksNetwork { Mainnet, } +impl std::fmt::Display for StacksNetwork { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } +} impl StacksNetwork { pub fn from_str(network: &str) -> Result { let value = match network { "devnet" => StacksNetwork::Devnet, "testnet" => StacksNetwork::Testnet, "mainnet" => StacksNetwork::Mainnet, + "simnet" => StacksNetwork::Simnet, _ => { return Err(format!( "network '{}' unsupported (mainnet, testnet, devnet, simnet)", @@ -775,6 +781,15 @@ impl StacksNetwork { Ok(value) } + pub fn as_str(&self) -> &str { + match self { + StacksNetwork::Devnet => "devnet", + StacksNetwork::Testnet => "testnet", + StacksNetwork::Mainnet => "mainnet", + StacksNetwork::Simnet => "simnet", + } + } + pub fn is_simnet(&self) -> bool { match self { StacksNetwork::Simnet => true, @@ -839,6 +854,11 @@ pub enum BitcoinNetwork { Mainnet, } +impl std::fmt::Display for BitcoinNetwork { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } +} impl BitcoinNetwork { pub fn from_str(network: &str) -> Result { let value = match network { @@ -855,6 +875,15 @@ impl BitcoinNetwork { }; Ok(value) } + + pub fn as_str(&self) -> &str { + match self { + BitcoinNetwork::Regtest => "regtest", + BitcoinNetwork::Testnet => "testnet", + BitcoinNetwork::Mainnet => "mainnet", + BitcoinNetwork::Signet => "signet", + } + } } #[derive(Deserialize, Debug, Clone, PartialEq)] From 007511f99af07ba89c9a47dddeafe0ed227783a1 Mon Sep 17 00:00:00 2001 From: Micaiah Reid Date: Fri, 5 Jul 2024 11:25:02 -0400 Subject: [PATCH 15/28] docs: update installation instructions (#614) --- README.md | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 49da6af87..8dc8cfa68 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,35 @@ Benefits are plurals: - Cost Optimization for data storage management and scaling. --- -## Install chainhook +## Install Chainhook -### Install from the source +There are a few options when installing Chainhook. + +### macOS +Chainhook can be installed on macOS using Homebrew with the following command: +```terminal +brew install chainhook +``` + +### Linux +Chainhook is also available on the [snap store](https://snapcraft.io/chainhook) for Linux users. +To install, run: +```terminal +sudo snap install chainhook +``` + +### Windows +Windows users can install via winget: +``` +winget install HiroSystems.Chainhook +``` + +### Download Builds +If you don't like using package managers and want to download our builds directly, they are published to our [release page](https://github.com/hirosystems/chainhook/releases). + +### Install from Source + +Finally, you can also build our source code directly: ```bash $ git clone https://github.com/hirosystems/chainhook.git From 7c2daeb232b7f6656395d98130ad81237d72a1d0 Mon Sep 17 00:00:00 2001 From: MicaiahReid Date: Fri, 5 Jul 2024 11:34:50 -0400 Subject: [PATCH 16/28] chore(release): publish v1.7.0 --- CHANGELOG.md | 14 ++++++++++++++ Cargo.lock | 2 +- components/chainhook-cli/Cargo.toml | 2 +- docs/chainhook-openapi.json | 2 +- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 733eac22c..3f0b52171 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +## [1.7.0](https://github.com/hirosystems/chainhook/compare/v1.6.2...v1.7.0) (2024-07-05) + +### New Features +* predicate validation (#611) +* improve chainhook-sdk interface (#608) + +### Bug Fixes + +* allow aborting a predicate scan (#601) + +### CI +* add snapcraft.yaml (#607) - releases are now available on the [snap store](https://snapcraft.io/chainhook)! +* winget pkg version bump (#606) - releases are now available on winget! + ## [1.6.2](https://github.com/hirosystems/chainhook/compare/v1.6.1...v1.6.2) (2024-06-06) ### Bug Fixes diff --git a/Cargo.lock b/Cargo.lock index ebb0ba5ac..b78b13b42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -472,7 +472,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chainhook" -version = "1.6.2" +version = "1.7.0" dependencies = [ "ansi_term", "atty", diff --git a/components/chainhook-cli/Cargo.toml b/components/chainhook-cli/Cargo.toml index 182741464..35bddde57 100644 --- a/components/chainhook-cli/Cargo.toml +++ b/components/chainhook-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "chainhook" -version = "1.6.2" +version = "1.7.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/docs/chainhook-openapi.json b/docs/chainhook-openapi.json index 1375554f7..5ca702928 100644 --- a/docs/chainhook-openapi.json +++ b/docs/chainhook-openapi.json @@ -2,7 +2,7 @@ "openapi": "3.0.0", "info": { "title": "chainhook", - "version": "1.6.2" + "version": "1.7.0" }, "paths": { "/ping": { From fb22fafa6880cbd5d810a226f35b6737b31f0080 Mon Sep 17 00:00:00 2001 From: Hugo Caillard <911307+hugocaillard@users.noreply.github.com> Date: Wed, 10 Jul 2024 18:44:01 +0200 Subject: [PATCH 17/28] refactor: patch stacks-codec dependency --- Cargo.lock | 5 +++++ Cargo.toml | 4 ++-- components/chainhook-sdk/Cargo.toml | 1 - 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b78b13b42..5da6ac31b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5204,3 +5204,8 @@ dependencies = [ "cc", "pkg-config", ] + +[[patch.unused]] +name = "stacks-codec" +version = "2.7.0" +source = "git+https://github.com/hirosystems/clarinet.git#b60ee86cb214865701071254dd5786e3aee3bec6" diff --git a/Cargo.toml b/Cargo.toml index 36f216cd7..04d88f5a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,5 +7,5 @@ members = [ default-members = ["components/chainhook-cli", "components/chainhook-sdk"] resolver = "2" -# [patch.'https://github.com/hirosystems/clarinet.git'] -# stacks-codec = { path = "../clarinet/components/stacks-codec" } +[patch.crates-io] +stacks-codec = { git = "https://github.com/hirosystems/clarinet.git" } diff --git a/components/chainhook-sdk/Cargo.toml b/components/chainhook-sdk/Cargo.toml index 694610b22..0a36f6179 100644 --- a/components/chainhook-sdk/Cargo.toml +++ b/components/chainhook-sdk/Cargo.toml @@ -13,7 +13,6 @@ serde_json = { version = "1", features = ["arbitrary_precision"] } serde-hex = "0.1.0" serde_derive = "1" stacks-codec = "2.4.1" -# hiro-system-kit = { version = "0.1.0", path = "../../../clarinet/components/hiro-system-kit" } hiro-system-kit = { version = "0.3.4", optional = true } chainhook-types = { version = "1.3.6", path = "../chainhook-types-rs" } rocket = { version = "=0.5.0", features = ["json"] } From f8ad46ec3688fa37f3c4a2b9357f0e911ec4cf4f Mon Sep 17 00:00:00 2001 From: Hugo Caillard <911307+hugocaillard@users.noreply.github.com> Date: Mon, 15 Jul 2024 18:11:34 +0200 Subject: [PATCH 18/28] chore: update cargo lock --- Cargo.lock | 84 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 51 insertions(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5da6ac31b..566a15056 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -660,10 +660,9 @@ dependencies = [ ] [[package]] -name = "clarity-vm" +name = "clarity" version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0af203c4eea9bb753ff2798c3bcf9f3fd8ea7a24bde7222d96f82fd9beb6d165" +source = "git+https://github.com/stacks-network/stacks-core.git?branch=feat/clarity-wasm-develop#455653214ea64d062c3b6fb6e72ac137d1a86343" dependencies = [ "hashbrown 0.14.3", "integer-sqrt", @@ -676,7 +675,7 @@ dependencies = [ "serde_derive", "serde_json", "serde_stacker", - "sha2-asm", + "sha2-asm 0.5.5", "slog", "stacks-common", "wasmtime", @@ -1294,12 +1293,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "fallible-iterator" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" - [[package]] name = "fallible-iterator" version = "0.3.0" @@ -1589,7 +1582,7 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" dependencies = [ - "fallible-iterator 0.3.0", + "fallible-iterator", "indexmap 2.1.0", "stable_deref_trait", ] @@ -1653,9 +1646,9 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.8.4" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" dependencies = [ "hashbrown 0.14.3", ] @@ -2081,9 +2074,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.25.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29f835d03d717946d28b1d1ed632eb6f0e24a299388ee623d0c23118d3e8a7fa" +checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" dependencies = [ "cc", "pkg-config", @@ -3109,12 +3102,12 @@ dependencies = [ [[package]] name = "rusqlite" -version = "0.28.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01e213bc3ecb39ac32e81e51ebe31fd888a940515173e3a18a35f8c6e896422a" +checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" dependencies = [ - "bitflags 1.3.2", - "fallible-iterator 0.2.0", + "bitflags 2.4.1", + "fallible-iterator", "fallible-streaming-iterator", "hashlink", "libsqlite3-sys", @@ -3472,6 +3465,7 @@ dependencies = [ "cfg-if", "cpufeatures", "digest 0.10.7", + "sha2-asm 0.6.4", ] [[package]] @@ -3483,6 +3477,15 @@ dependencies = [ "cc", ] +[[package]] +name = "sha2-asm" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b845214d6175804686b2bd482bcffe96651bb2d1200742b712003504a2dac1ab" +dependencies = [ + "cc", +] + [[package]] name = "sha3" version = "0.10.8" @@ -3695,20 +3698,18 @@ dependencies = [ [[package]] name = "stacks-codec" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5791855bf9de767f55042f1a034a2575373c6433b0965f1efac14e7215f71b" +version = "2.7.0" +source = "git+https://github.com/hirosystems/clarinet.git#b37d9f829167565848a65467de5d5cfad8a16602" dependencies = [ - "clarity-vm", + "clarity", "serde", - "wsts", + "wsts 8.1.0", ] [[package]] name = "stacks-common" -version = "0.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c78cb37406e4b6a269f0b6aa0ee222922c9d30c263a43af7e005169e20edf12b" +version = "0.0.2" +source = "git+https://github.com/stacks-network/stacks-core.git?branch=feat/clarity-wasm-develop#455653214ea64d062c3b6fb6e72ac137d1a86343" dependencies = [ "chrono", "curve25519-dalek 2.0.0", @@ -3732,7 +3733,7 @@ dependencies = [ "slog-term", "time", "winapi", - "wsts", + "wsts 9.1.0", ] [[package]] @@ -5089,6 +5090,28 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "wsts" +version = "9.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f38f50568ce9a30f2d041c26b1286fd453a166aec0059eb63aa51a15c46d776" +dependencies = [ + "aes-gcm", + "bs58 0.5.1", + "hashbrown 0.14.3", + "hex", + "num-traits", + "p256k1", + "polynomial", + "primitive-types", + "rand_core 0.6.4", + "serde", + "sha2", + "thiserror", + "tracing", + "tracing-subscriber", +] + [[package]] name = "wyz" version = "0.5.1" @@ -5204,8 +5227,3 @@ dependencies = [ "cc", "pkg-config", ] - -[[patch.unused]] -name = "stacks-codec" -version = "2.7.0" -source = "git+https://github.com/hirosystems/clarinet.git#b60ee86cb214865701071254dd5786e3aee3bec6" From 5ea25827d9809aecfce9e52a75e237cc133d8c10 Mon Sep 17 00:00:00 2001 From: Chris Guimaraes Date: Tue, 16 Jul 2024 10:33:52 +0100 Subject: [PATCH 19/28] chore: fix typo on monitoring --- components/chainhook-sdk/src/monitoring.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/chainhook-sdk/src/monitoring.rs b/components/chainhook-sdk/src/monitoring.rs index 70dd6c404..05d78d061 100644 --- a/components/chainhook-sdk/src/monitoring.rs +++ b/components/chainhook-sdk/src/monitoring.rs @@ -405,7 +405,7 @@ async fn serve_req( ctx.try_log(|logger| { slog::debug!( logger, - "Prometheus monitoring: respsonding to metrics request" + "Prometheus monitoring: responding to metrics request" ) }); From c208f07a8b5c865fb1c803b3c441faf1c5a8e612 Mon Sep 17 00:00:00 2001 From: omahs <73983677+omahs@users.noreply.github.com> Date: Tue, 16 Jul 2024 14:19:02 +0200 Subject: [PATCH 20/28] fix typos --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8dc8cfa68..cc09e89e0 100644 --- a/README.md +++ b/README.md @@ -654,7 +654,7 @@ Putting all the pieces together: Developers can test their Stacks predicates without spinning up a Stacks node. To date, the Stacks blockchain has just over 2 years of activity, and the `chainhook` utility is able to work with both `testnet` and `mainnet` chainstates, in memory. -To test a Stacks `if_this` / `then_that` predicate, the following command can by used: +To test a Stacks `if_this` / `then_that` predicate, the following command can be used: ```bash $ chainhook predicates scan ./path/to/predicate.json --testnet @@ -693,7 +693,7 @@ If you encounter a bug or have a feature request, we encourage you to follow the 1. **Search for existing issues:** Before submitting a new issue, please search [existing and closed issues](../../issues) to check if a similar problem or feature request has already been reported. 1. **Open a new issue:** If it hasn't been addressed, please [open a new issue](../../issues/new/choose). Choose the appropriate issue template and provide as much detail as possible, including steps to reproduce the bug or a clear description of the requested feature. - 1. **Evaluation SLA:** Our team reads and evaluates all the issues and pull requests. We are avaliable Monday to Friday and we make a best effort to respond within 7 business days. + 1. **Evaluation SLA:** Our team reads and evaluates all the issues and pull requests. We are available Monday to Friday and we make a best effort to respond within 7 business days. Please **do not** use the issue tracker for personal support requests or to ask for the status of a transaction. You'll find help at the [#support Discord channel](https://discord.gg/SK3DxdsP). From b8de15d884642373090849b8f05f7d5e4b7d6186 Mon Sep 17 00:00:00 2001 From: omahs <73983677+omahs@users.noreply.github.com> Date: Tue, 16 Jul 2024 14:22:35 +0200 Subject: [PATCH 21/28] fix typo --- docs/how-to-guides/how-to-use-chainhooks-with-bitcoin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/how-to-guides/how-to-use-chainhooks-with-bitcoin.md b/docs/how-to-guides/how-to-use-chainhooks-with-bitcoin.md index 8eae61b76..4d46ed39c 100644 --- a/docs/how-to-guides/how-to-use-chainhooks-with-bitcoin.md +++ b/docs/how-to-guides/how-to-use-chainhooks-with-bitcoin.md @@ -153,7 +153,7 @@ Get any transaction, including a `p2wsh` output paying a given recipient: `p2wsh` (Pay-to-Witness-Script-Hash) is a Bitcoin transaction output script type used in Segregated Witness (SegWit) that enables users to send funds to a hashed script, allowing for more complex transaction conditions and greater scalability by separating the script from the transaction data. **Wallet Descriptors** provide a compact and semi-standardized method for describing how scripts and addresses within a wallet are generated. Chainhooks users that want to track addresses derived from an extended pubkey or a multisig-wallet for example, can now rely on this feature instead of defining one predicate per address. -For example if we wanted to track the first 3 addressed generated by the following descriptor: +For example if we wanted to track the first 3 addresses generated by the following descriptor: ``` wpkh(tprv8ZgxMBicQKsPePxn6j3TjvB2MBzQkuhGgc6oRh2WZancZQgxktcnjZJ44XdsRiw3jNkbVTK9JW6KFHvnRKgAMtSyuBevMJprSkZ4PTfmTgV/84'/1'/0'/0/*) ``` From aaac941dd174e8ff76b0ad935d82c11a55dea4d5 Mon Sep 17 00:00:00 2001 From: Chris Guimaraes Date: Tue, 16 Jul 2024 14:52:31 +0100 Subject: [PATCH 22/28] chore: fix typo --- components/chainhook-cli/src/scan/stacks.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/chainhook-cli/src/scan/stacks.rs b/components/chainhook-cli/src/scan/stacks.rs index 55e44df9a..57dd2d04b 100644 --- a/components/chainhook-cli/src/scan/stacks.rs +++ b/components/chainhook-cli/src/scan/stacks.rs @@ -78,7 +78,7 @@ pub async fn get_canonical_fork_from_tsv( let mut start_block = start_block.unwrap_or(0); info!( ctx.expect_logger(), - "Parsing tsv file to determine canoncial fork" + "Parsing tsv file to determine canonical fork" ); let parsing_handle = hiro_system_kit::thread_named("Stacks chainstate CSV parsing") .spawn(move || { From cbfd97e07eb3d3ba793bedb7f7425e6a8f791924 Mon Sep 17 00:00:00 2001 From: Hugo Caillard <911307+hugocaillard@users.noreply.github.com> Date: Wed, 17 Jul 2024 17:22:45 +0200 Subject: [PATCH 23/28] chore: update cargo lock --- Cargo.lock | 673 +---------------------------------------------------- 1 file changed, 4 insertions(+), 669 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 566a15056..ddb12285b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,18 +103,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "anyhow" -version = "1.0.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" - -[[package]] -name = "arbitrary" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" - [[package]] name = "arc-swap" version = "1.6.0" @@ -252,15 +240,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72" -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - [[package]] name = "bindgen" version = "0.64.0" @@ -662,7 +641,7 @@ dependencies = [ [[package]] name = "clarity" version = "2.3.0" -source = "git+https://github.com/stacks-network/stacks-core.git?branch=feat/clarity-wasm-develop#455653214ea64d062c3b6fb6e72ac137d1a86343" +source = "git+https://github.com/stacks-network/stacks-core.git?branch=feat/clarity-wasm-develop#61e4e4500d6efe5658f8db5ba7daa21d0d368fa5" dependencies = [ "hashbrown 0.14.3", "integer-sqrt", @@ -678,7 +657,6 @@ dependencies = [ "sha2-asm 0.5.5", "slog", "stacks-common", - "wasmtime", ] [[package]] @@ -724,15 +702,6 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" -[[package]] -name = "cpp_demangle" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeaa953eaad386a53111e47172c2fedba671e5684c8dd601a5f474f4f118710f" -dependencies = [ - "cfg-if", -] - [[package]] name = "cpufeatures" version = "0.2.11" @@ -742,115 +711,6 @@ dependencies = [ "libc", ] -[[package]] -name = "cranelift-bforest" -version = "0.102.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e7e56668d2263f92b691cb9e4a2fcb186ca0384941fe420484322fa559c3329" -dependencies = [ - "cranelift-entity", -] - -[[package]] -name = "cranelift-codegen" -version = "0.102.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a9ff61938bf11615f55b80361288c68865318025632ea73c65c0b44fa16283c" -dependencies = [ - "bumpalo", - "cranelift-bforest", - "cranelift-codegen-meta", - "cranelift-codegen-shared", - "cranelift-control", - "cranelift-entity", - "cranelift-isle", - "gimli", - "hashbrown 0.14.3", - "log", - "regalloc2", - "smallvec 1.11.2", - "target-lexicon", -] - -[[package]] -name = "cranelift-codegen-meta" -version = "0.102.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50656bf19e3d4a153b404ff835b8b59e924cfa3682ebe0d3df408994f37983f6" -dependencies = [ - "cranelift-codegen-shared", -] - -[[package]] -name = "cranelift-codegen-shared" -version = "0.102.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388041deeb26109f1ea73c1812ea26bfd406c94cbce0bb5230aa44277e43b209" - -[[package]] -name = "cranelift-control" -version = "0.102.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39b7c512ffac527e5b5df9beae3d67ab85d07dca6d88942c16195439fedd1d3" -dependencies = [ - "arbitrary", -] - -[[package]] -name = "cranelift-entity" -version = "0.102.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb25f573701284fe2bcf88209d405342125df00764b396c923e11eafc94d892" -dependencies = [ - "serde", - "serde_derive", -] - -[[package]] -name = "cranelift-frontend" -version = "0.102.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57374fd11d72cf9ffb85ff64506ed831440818318f58d09f45b4185e5e9c376" -dependencies = [ - "cranelift-codegen", - "log", - "smallvec 1.11.2", - "target-lexicon", -] - -[[package]] -name = "cranelift-isle" -version = "0.102.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae769b235f6ea2f86623a3ff157cc04a4ff131dc9fe782c2ebd35f272043581e" - -[[package]] -name = "cranelift-native" -version = "0.102.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dc7bfb8f13a0526fe20db338711d9354729b861c336978380bb10f7f17dd207" -dependencies = [ - "cranelift-codegen", - "libc", - "target-lexicon", -] - -[[package]] -name = "cranelift-wasm" -version = "0.102.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c5f41a4af931b756be05af0dd374ce200aae2d52cea16b0beb07e8b52732c35" -dependencies = [ - "cranelift-codegen", - "cranelift-entity", - "cranelift-frontend", - "itertools", - "log", - "smallvec 1.11.2", - "wasmparser", - "wasmtime-types", -] - [[package]] name = "crc32fast" version = "1.3.2" @@ -1108,15 +968,6 @@ dependencies = [ "parking_lot_core", ] -[[package]] -name = "debugid" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" -dependencies = [ - "uuid", -] - [[package]] name = "der" version = "0.7.8" @@ -1199,16 +1050,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "directories-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" -dependencies = [ - "cfg-if", - "dirs-sys-next", -] - [[package]] name = "dirs-next" version = "2.0.0" @@ -1497,19 +1338,6 @@ dependencies = [ "byteorder", ] -[[package]] -name = "fxprof-processed-profile" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27d12c0aed7f1e24276a241aadc4cb8ea9f83000f34bc062b7cc2d51e3b0fabd" -dependencies = [ - "bitflags 2.4.1", - "debugid", - "fxhash", - "serde", - "serde_json", -] - [[package]] name = "generator" version = "0.7.5" @@ -1581,11 +1409,6 @@ name = "gimli" version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" -dependencies = [ - "fallible-iterator", - "indexmap 2.1.0", - "stable_deref_trait", -] [[package]] name = "glob" @@ -1624,15 +1447,6 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -[[package]] -name = "hashbrown" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" -dependencies = [ - "ahash", -] - [[package]] name = "hashbrown" version = "0.14.3" @@ -1824,12 +1638,6 @@ dependencies = [ "cc", ] -[[package]] -name = "id-arena" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" - [[package]] name = "ident_case" version = "1.0.1" @@ -1944,26 +1752,6 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" -[[package]] -name = "ittapi" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b996fe614c41395cdaedf3cf408a9534851090959d90d54a535f675550b64b1" -dependencies = [ - "anyhow", - "ittapi-sys", - "log", -] - -[[package]] -name = "ittapi-sys" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5385394064fa2c886205dba02598013ce83d3e92d33dbdc0c52fe0e7bf4fc" -dependencies = [ - "cc", -] - [[package]] name = "jobserver" version = "0.1.27" @@ -2024,12 +1812,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" -[[package]] -name = "leb128" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" - [[package]] name = "libc" version = "0.2.151" @@ -2141,15 +1923,6 @@ dependencies = [ "libc", ] -[[package]] -name = "mach" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" -dependencies = [ - "libc", -] - [[package]] name = "matchers" version = "0.1.0" @@ -2171,15 +1944,6 @@ version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" -[[package]] -name = "memfd" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2cffa4ad52c6f791f4f8b15f0c05f9824b2ced1160e88cc393d64fff9a8ac64" -dependencies = [ - "rustix", -] - [[package]] name = "memoffset" version = "0.6.5" @@ -2189,15 +1953,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - [[package]] name = "mime" version = "0.3.17" @@ -2280,7 +2035,7 @@ dependencies = [ "cc", "cfg-if", "libc", - "memoffset 0.6.5", + "memoffset", ] [[package]] @@ -2360,9 +2115,6 @@ version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ - "crc32fast", - "hashbrown 0.14.3", - "indexmap 2.1.0", "memchr", ] @@ -2478,12 +2230,6 @@ dependencies = [ "windows-targets 0.48.5", ] -[[package]] -name = "paste" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" - [[package]] name = "pear" version = "0.2.8" @@ -2860,19 +2606,6 @@ dependencies = [ "syn 2.0.43", ] -[[package]] -name = "regalloc2" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad156d539c879b7a24a363a2016d77961786e71f48f2e2fc8302a92abd2429a6" -dependencies = [ - "hashbrown 0.13.2", - "log", - "rustc-hash", - "slice-group-by", - "smallvec 1.11.2", -] - [[package]] name = "regex" version = "1.10.2" @@ -3538,12 +3271,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "slice-group-by" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "826167069c09b99d56f31e9ae5c99049e932a98c9dc2dac47645b08dbbf76ba7" - [[package]] name = "slog" version = "2.7.0" @@ -3662,12 +3389,6 @@ dependencies = [ "der", ] -[[package]] -name = "sptr" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a" - [[package]] name = "stable-pattern" version = "0.1.0" @@ -3677,12 +3398,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - [[package]] name = "stacker" version = "0.1.15" @@ -3699,7 +3414,7 @@ dependencies = [ [[package]] name = "stacks-codec" version = "2.7.0" -source = "git+https://github.com/hirosystems/clarinet.git#b37d9f829167565848a65467de5d5cfad8a16602" +source = "git+https://github.com/hirosystems/clarinet.git#ce1ffeaaf645de2d9a581d1d727e660cacb1e288" dependencies = [ "clarity", "serde", @@ -3709,7 +3424,7 @@ dependencies = [ [[package]] name = "stacks-common" version = "0.0.2" -source = "git+https://github.com/stacks-network/stacks-core.git?branch=feat/clarity-wasm-develop#455653214ea64d062c3b6fb6e72ac137d1a86343" +source = "git+https://github.com/stacks-network/stacks-core.git?branch=feat/clarity-wasm-develop#61e4e4500d6efe5658f8db5ba7daa21d0d368fa5" dependencies = [ "chrono", "curve25519-dalek 2.0.0", @@ -4488,24 +4203,6 @@ version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" -[[package]] -name = "wasm-encoder" -version = "0.36.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "822b645bf4f2446b949776ffca47e2af60b167209ffb70814ef8779d299cd421" -dependencies = [ - "leb128", -] - -[[package]] -name = "wasm-encoder" -version = "0.202.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfd106365a7f5f7aa3c1916a98cbb3ad477f5ff96ddb130285a91c6e7429e67a" -dependencies = [ - "leb128", -] - [[package]] name = "wasm-streams" version = "0.3.0" @@ -4519,322 +4216,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "wasmparser" -version = "0.116.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a58e28b80dd8340cb07b8242ae654756161f6fc8d0038123d679b7b99964fa50" -dependencies = [ - "indexmap 2.1.0", - "semver", -] - -[[package]] -name = "wasmtime" -version = "15.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "642e12d108e800215263e3b95972977f473957923103029d7d617db701d67ba4" -dependencies = [ - "anyhow", - "async-trait", - "bincode", - "bumpalo", - "cfg-if", - "fxprof-processed-profile", - "indexmap 2.1.0", - "libc", - "log", - "object", - "once_cell", - "paste", - "psm", - "rayon", - "serde", - "serde_derive", - "serde_json", - "target-lexicon", - "wasm-encoder 0.36.2", - "wasmparser", - "wasmtime-cache", - "wasmtime-component-macro", - "wasmtime-cranelift", - "wasmtime-environ", - "wasmtime-fiber", - "wasmtime-jit", - "wasmtime-runtime", - "wat", - "windows-sys 0.48.0", -] - -[[package]] -name = "wasmtime-asm-macros" -version = "15.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beada8bb15df52503de0a4c58de4357bfd2f96d9a44a6e547bad11efdd988b47" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "wasmtime-cache" -version = "15.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aba5bf44d044d25892c03fb3534373936ee204141ff92bac8297787ac7f22318" -dependencies = [ - "anyhow", - "base64 0.21.5", - "bincode", - "directories-next", - "log", - "rustix", - "serde", - "serde_derive", - "sha2", - "toml 0.5.11", - "windows-sys 0.48.0", - "zstd", -] - -[[package]] -name = "wasmtime-component-macro" -version = "15.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ccba556991465cca68d5a54769684bcf489fb532059da55105f851642d52c1" -dependencies = [ - "anyhow", - "proc-macro2", - "quote", - "syn 2.0.43", - "wasmtime-component-util", - "wasmtime-wit-bindgen", - "wit-parser", -] - -[[package]] -name = "wasmtime-component-util" -version = "15.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05492a177a6006cb73f034d6e9a6fad6da55b23c4398835cb0012b5fa51ecf67" - -[[package]] -name = "wasmtime-cranelift" -version = "15.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe2e7532f1d6adbcc57e69bb6a7c503f0859076d07a9b4b6aabe8021ff8a05fd" -dependencies = [ - "anyhow", - "cfg-if", - "cranelift-codegen", - "cranelift-control", - "cranelift-entity", - "cranelift-frontend", - "cranelift-native", - "cranelift-wasm", - "gimli", - "log", - "object", - "target-lexicon", - "thiserror", - "wasmparser", - "wasmtime-cranelift-shared", - "wasmtime-environ", - "wasmtime-versioned-export-macros", -] - -[[package]] -name = "wasmtime-cranelift-shared" -version = "15.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c98d5378a856cbf058d36278627dfabf0ed68a888142958c7ae8e6af507dafa" -dependencies = [ - "anyhow", - "cranelift-codegen", - "cranelift-control", - "cranelift-native", - "gimli", - "object", - "target-lexicon", - "wasmtime-environ", -] - -[[package]] -name = "wasmtime-environ" -version = "15.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6d33a9f421da810a070cd56add9bc51f852bd66afbb8b920489d6242f15b70e" -dependencies = [ - "anyhow", - "cranelift-entity", - "gimli", - "indexmap 2.1.0", - "log", - "object", - "serde", - "serde_derive", - "target-lexicon", - "thiserror", - "wasmparser", - "wasmtime-types", -] - -[[package]] -name = "wasmtime-fiber" -version = "15.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "404741f4c6d7f4e043be2e8b466406a2aee289ccdba22bf9eba6399921121b97" -dependencies = [ - "anyhow", - "cc", - "cfg-if", - "rustix", - "wasmtime-asm-macros", - "wasmtime-versioned-export-macros", - "windows-sys 0.48.0", -] - -[[package]] -name = "wasmtime-jit" -version = "15.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d0994a86d6dca5f7d9740d7f2bd0568be06d2014a550361dc1c397d289d81ef" -dependencies = [ - "addr2line", - "anyhow", - "bincode", - "cfg-if", - "cpp_demangle", - "gimli", - "ittapi", - "log", - "object", - "rustc-demangle", - "rustix", - "serde", - "serde_derive", - "target-lexicon", - "wasmtime-environ", - "wasmtime-jit-debug", - "wasmtime-jit-icache-coherence", - "wasmtime-runtime", - "windows-sys 0.48.0", -] - -[[package]] -name = "wasmtime-jit-debug" -version = "15.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0c4b74e606d1462d648631d5bc328e3d5b14e7f9d3ff93bc6db062fb8c5cd8" -dependencies = [ - "object", - "once_cell", - "rustix", - "wasmtime-versioned-export-macros", -] - -[[package]] -name = "wasmtime-jit-icache-coherence" -version = "15.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3090a69ba1476979e090aa7ed4bc759178bafdb65b22f98b9ba24fc6e7e578d5" -dependencies = [ - "cfg-if", - "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "wasmtime-runtime" -version = "15.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b993ac8380385ed67bf71b51b9553edcf1ab0801b78a805a067de581b9a3e88a" -dependencies = [ - "anyhow", - "cc", - "cfg-if", - "indexmap 2.1.0", - "libc", - "log", - "mach", - "memfd", - "memoffset 0.9.1", - "paste", - "rand", - "rustix", - "sptr", - "wasm-encoder 0.36.2", - "wasmtime-asm-macros", - "wasmtime-environ", - "wasmtime-fiber", - "wasmtime-jit-debug", - "wasmtime-versioned-export-macros", - "wasmtime-wmemcheck", - "windows-sys 0.48.0", -] - -[[package]] -name = "wasmtime-types" -version = "15.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b5778112fcab2dc3d4371f4203ab8facf0c453dd94312b0a88dd662955e64e0" -dependencies = [ - "cranelift-entity", - "serde", - "serde_derive", - "thiserror", - "wasmparser", -] - -[[package]] -name = "wasmtime-versioned-export-macros" -version = "15.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f50f51f8d79bfd2aa8e9d9a0ae7c2d02b45fe412e62ff1b87c0c81b07c738231" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.43", -] - -[[package]] -name = "wasmtime-wit-bindgen" -version = "15.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b804dfd3d0c0d6d37aa21026fe7772ba1a769c89ee4f5c4f13b82d91d75216f" -dependencies = [ - "anyhow", - "heck 0.4.1", - "indexmap 2.1.0", - "wit-parser", -] - -[[package]] -name = "wasmtime-wmemcheck" -version = "15.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6060bc082cc32d9a45587c7640e29e3c7b89ada82677ac25d87850aaccb368" - -[[package]] -name = "wast" -version = "202.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbcb11204515c953c9b42ede0a46a1c5e17f82af05c4fae201a8efff1b0f4fe" -dependencies = [ - "bumpalo", - "leb128", - "memchr", - "unicode-width", - "wasm-encoder 0.202.0", -] - -[[package]] -name = "wat" -version = "1.202.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4de4b15a47135c56a3573406e9977b9518787a6154459b4842a9b9d3d1684848" -dependencies = [ - "wast", -] - [[package]] name = "web-sys" version = "0.3.66" @@ -5051,23 +4432,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "wit-parser" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "316b36a9f0005f5aa4b03c39bc3728d045df136f8c13a73b7db4510dec725e08" -dependencies = [ - "anyhow", - "id-arena", - "indexmap 2.1.0", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", -] - [[package]] name = "wsts" version = "8.1.0" @@ -5198,32 +4562,3 @@ dependencies = [ "system-deps", "zeromq-src", ] - -[[package]] -name = "zstd" -version = "0.11.2+zstd.1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "5.0.2+zstd.1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" -dependencies = [ - "libc", - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.10+zstd.1.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa" -dependencies = [ - "cc", - "pkg-config", -] From 8b3ca8b6cc9c81101743546c38ab64cc4bf513b9 Mon Sep 17 00:00:00 2001 From: Hugo Caillard <911307+hugocaillard@users.noreply.github.com> Date: Thu, 18 Jul 2024 11:06:34 +0200 Subject: [PATCH 24/28] chore: upgrade tokio and reqwest dependencies --- Cargo.lock | 320 ++++++++++++++++++++-------- components/chainhook-cli/Cargo.toml | 6 +- components/chainhook-sdk/Cargo.toml | 4 +- 3 files changed, 231 insertions(+), 99 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ddb12285b..0f0bc2625 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -222,6 +222,12 @@ version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.6.0" @@ -439,7 +445,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03915af431787e6ffdcc74c645077518c6b6e01f80b761e0fbbfa288536311b3" dependencies = [ - "smallvec 1.11.2", + "smallvec 1.13.2", "target-lexicon", ] @@ -502,7 +508,7 @@ dependencies = [ "fxhash", "hex", "hiro-system-kit", - "hyper", + "hyper 0.14.27", "lazy_static", "miniscript", "prometheus", @@ -641,7 +647,7 @@ dependencies = [ [[package]] name = "clarity" version = "2.3.0" -source = "git+https://github.com/stacks-network/stacks-core.git?branch=feat/clarity-wasm-develop#61e4e4500d6efe5658f8db5ba7daa21d0d368fa5" +source = "git+https://github.com/stacks-network/stacks-core.git?branch=feat/clarity-wasm-develop#6eb4a2377cb8d18275280530abb6044dee9081ee" dependencies = [ "hashbrown 0.14.3", "integer-sqrt", @@ -686,16 +692,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -1427,7 +1423,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.11", "indexmap 2.1.0", "slab", "tokio", @@ -1554,6 +1550,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http-body" version = "0.4.6" @@ -1561,7 +1568,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http", + "http 0.2.11", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", "pin-project-lite", ] @@ -1588,8 +1618,8 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", - "http-body", + "http 0.2.11", + "http-body 0.4.6", "httparse", "httpdate", "itoa", @@ -1601,18 +1631,61 @@ dependencies = [ "want", ] +[[package]] +name = "hyper" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "httparse", + "itoa", + "pin-project-lite", + "smallvec 1.13.2", + "tokio", + "want", +] + [[package]] name = "hyper-rustls" -version = "0.24.2" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" dependencies = [ "futures-util", - "http", - "hyper", + "http 1.1.0", + "hyper 1.4.1", + "hyper-util", "rustls", + "rustls-pki-types", "tokio", "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "hyper 1.4.1", + "pin-project-lite", + "socket2 0.5.6", + "tokio", + "tower", + "tower-service", + "tracing", ] [[package]] @@ -2005,7 +2078,7 @@ dependencies = [ "bytes", "encoding_rs", "futures-util", - "http", + "http 0.2.11", "httparse", "log", "memchr", @@ -2226,7 +2299,7 @@ dependencies = [ "cfg-if", "libc", "redox_syscall", - "smallvec 1.11.2", + "smallvec 1.13.2", "windows-targets 0.48.5", ] @@ -2477,6 +2550,53 @@ dependencies = [ "cc", ] +[[package]] +name = "quinn" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ceeeeabace7857413798eb1ffa1e9c905a9946a57d81fb69b4b71c4d8eb3ad" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf517c03a109db8100448a4be38d498df8a210a99fe0e1b9eaf39e78c640efe" +dependencies = [ + "bytes", + "rand", + "ring", + "rustc-hash", + "rustls", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + +[[package]] +name = "quinn-udp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9096629c45860fc7fb143e125eb826b5e721e10be3263160c7d60ca832cf8c46" +dependencies = [ + "libc", + "once_cell", + "socket2 0.5.6", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "quote" version = "1.0.33" @@ -2652,20 +2772,21 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "reqwest" -version = "0.11.23" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" +checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" dependencies = [ - "base64 0.21.5", + "base64 0.22.1", "bytes", - "encoding_rs", + "futures-channel", "futures-core", "futures-util", - "h2", - "http", - "http-body", - "hyper", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.4.1", "hyper-rustls", + "hyper-util", "ipnet", "js-sys", "log", @@ -2673,12 +2794,14 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "quinn", "rustls", "rustls-pemfile", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", - "system-configuration", + "sync_wrapper", "tokio", "tokio-rustls", "tokio-util", @@ -2779,8 +2902,8 @@ dependencies = [ "cookie", "either", "futures", - "http", - "hyper", + "http 0.2.11", + "hyper 0.14.27", "indexmap 2.1.0", "log", "memchr", @@ -2789,7 +2912,7 @@ dependencies = [ "pin-project-lite", "ref-cast", "serde", - "smallvec 1.11.2", + "smallvec 1.13.2", "stable-pattern", "state", "time", @@ -2845,7 +2968,7 @@ dependencies = [ "hashlink", "libsqlite3-sys", "serde_json", - "smallvec 1.11.2", + "smallvec 1.13.2", ] [[package]] @@ -2903,32 +3026,42 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.10" +version = "0.23.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" +checksum = "4828ea528154ae444e5a642dbb7d5623354030dc9822b83fd9bb79683c7399d0" dependencies = [ - "log", + "once_cell", "ring", + "rustls-pki-types", "rustls-webpki", - "sct", + "subtle", + "zeroize", ] [[package]] name = "rustls-pemfile" -version = "1.0.4" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" dependencies = [ - "base64 0.21.5", + "base64 0.22.1", + "rustls-pki-types", ] +[[package]] +name = "rustls-pki-types" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" + [[package]] name = "rustls-webpki" -version = "0.101.7" +version = "0.102.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +checksum = "f9a6fccd794a42c2c105b513a2f62bc3fd8f3ba57a4593677ceb0bd035164d78" dependencies = [ "ring", + "rustls-pki-types", "untrusted", ] @@ -2989,16 +3122,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "secp256k1" version = "0.24.3" @@ -3346,9 +3469,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.2" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" @@ -3424,7 +3547,7 @@ dependencies = [ [[package]] name = "stacks-common" version = "0.0.2" -source = "git+https://github.com/stacks-network/stacks-core.git?branch=feat/clarity-wasm-develop#61e4e4500d6efe5658f8db5ba7daa21d0d368fa5" +source = "git+https://github.com/stacks-network/stacks-core.git?branch=feat/clarity-wasm-develop#6eb4a2377cb8d18275280530abb6044dee9081ee" dependencies = [ "chrono", "curve25519-dalek 2.0.0", @@ -3523,25 +3646,10 @@ dependencies = [ ] [[package]] -name = "system-configuration" -version = "0.5.1" +name = "sync_wrapper" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" -dependencies = [ - "core-foundation-sys", - "libc", -] +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" [[package]] name = "system-deps" @@ -3765,9 +3873,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.36.0" +version = "1.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +checksum = "eb2caba9f80616f438e09748d5acda951967e1ea58508ef53d9c6402485a46df" dependencies = [ "backtrace", "bytes", @@ -3784,9 +3892,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", @@ -3795,19 +3903,20 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.24.1" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ "rustls", + "rustls-pki-types", "tokio", ] [[package]] name = "tokio-stream" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" dependencies = [ "futures-core", "pin-project-lite", @@ -3816,16 +3925,15 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", - "tracing", ] [[package]] @@ -3896,6 +4004,27 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + [[package]] name = "tower-service" version = "0.3.2" @@ -3956,7 +4085,7 @@ dependencies = [ "once_cell", "regex", "sharded-slab", - "smallvec 1.11.2", + "smallvec 1.13.2", "thread_local", "tracing", "tracing-core", @@ -4205,9 +4334,9 @@ checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" [[package]] name = "wasm-streams" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4609d447824375f43e1ffbc051b50ad8f4b3ae8219680c94452ea05eb240ac7" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" dependencies = [ "futures-util", "js-sys", @@ -4228,9 +4357,12 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.25.3" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" +checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" +dependencies = [ + "rustls-pki-types", +] [[package]] name = "winapi" @@ -4424,9 +4556,9 @@ dependencies = [ [[package]] name = "winreg" -version = "0.50.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" dependencies = [ "cfg-if", "windows-sys 0.48.0", diff --git a/components/chainhook-cli/Cargo.toml b/components/chainhook-cli/Cargo.toml index 35bddde57..6ee9a5a43 100644 --- a/components/chainhook-cli/Cargo.toml +++ b/components/chainhook-cli/Cargo.toml @@ -18,19 +18,19 @@ rand = "0.8.5" chainhook-sdk = { version = "0.12.6", default-features = false, features = [ "zeromq", ], path = "../chainhook-sdk" } -hiro-system-kit = "0.3.4" +hiro-system-kit = "0.3.4" # hiro-system-kit = { path = "../../../clarinet/components/hiro-system-kit" } clap = { version = "3.2.23", features = ["derive"], optional = true } clap_generate = { version = "3.0.3", optional = true } toml = { version = "0.5.6", features = ["preserve_order"], optional = true } ctrlc = { version = "3.2.2", optional = true } -reqwest = { version = "0.11", default-features = false, features = [ +reqwest = { version = "0.12", default-features = false, features = [ "blocking", "stream", "json", "rustls-tls", ] } -tokio = { version = "1.35.1", features = ["full"] } +tokio = { version = "1.38.1", features = ["full"] } futures-util = "0.3.24" flate2 = "1.0.24" tar = "0.4.38" diff --git a/components/chainhook-sdk/Cargo.toml b/components/chainhook-sdk/Cargo.toml index 0a36f6179..fd852ed7e 100644 --- a/components/chainhook-sdk/Cargo.toml +++ b/components/chainhook-sdk/Cargo.toml @@ -19,12 +19,12 @@ rocket = { version = "=0.5.0", features = ["json"] } bitcoincore-rpc = "0.18.0" bitcoincore-rpc-json = "0.18.0" base64 = "0.21.5" -reqwest = { version = "0.11", default-features = false, features = [ +reqwest = { version = "0.12", default-features = false, features = [ "blocking", "json", "rustls-tls", ] } -tokio = { version = "1.35.1", features = ["full"] } +tokio = { version = "1.38.1", features = ["full"] } base58 = "0.2.0" schemars = { version = "0.8.16", git = "https://github.com/hirosystems/schemars.git", branch = "feat-chainhook-fixes" } crossbeam-channel = "0.5.6" From f177a77572e5492dbee67980875b77aadfab6a09 Mon Sep 17 00:00:00 2001 From: Hugo Caillard <911307+hugocaillard@users.noreply.github.com> Date: Thu, 18 Jul 2024 15:36:06 +0200 Subject: [PATCH 25/28] refactor: use clarity crate --- Cargo.lock | 1 + components/chainhook-sdk/Cargo.toml | 4 ++++ components/chainhook-sdk/src/chainhooks/stacks/mod.rs | 10 +++++----- components/chainhook-sdk/src/indexer/stacks/mod.rs | 6 +++--- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ddb12285b..463c6b0a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -496,6 +496,7 @@ dependencies = [ "bitcoincore-rpc", "bitcoincore-rpc-json", "chainhook-types", + "clarity", "crossbeam-channel", "dashmap", "futures", diff --git a/components/chainhook-sdk/Cargo.toml b/components/chainhook-sdk/Cargo.toml index 0a36f6179..577a1b0da 100644 --- a/components/chainhook-sdk/Cargo.toml +++ b/components/chainhook-sdk/Cargo.toml @@ -13,6 +13,10 @@ serde_json = { version = "1", features = ["arbitrary_precision"] } serde-hex = "0.1.0" serde_derive = "1" stacks-codec = "2.4.1" +clarity = { git = "https://github.com/stacks-network/stacks-core.git", branch = "feat/clarity-wasm-develop", package = "clarity", default-features = false, features = [ + "canonical", + "log", +] } hiro-system-kit = { version = "0.3.4", optional = true } chainhook-types = { version = "1.3.6", path = "../chainhook-types-rs" } rocket = { version = "=0.5.0", features = ["json"] } diff --git a/components/chainhook-sdk/src/chainhooks/stacks/mod.rs b/components/chainhook-sdk/src/chainhooks/stacks/mod.rs index 417c0ca99..27fd8a0ce 100644 --- a/components/chainhook-sdk/src/chainhooks/stacks/mod.rs +++ b/components/chainhook-sdk/src/chainhooks/stacks/mod.rs @@ -10,16 +10,16 @@ use chainhook_types::{ StacksTransactionEvent, StacksTransactionEventPayload, StacksTransactionKind, TransactionIdentifier, }; +use clarity::codec::StacksMessageCodec; +use clarity::vm::types::{ + CharType, PrincipalData, QualifiedContractIdentifier, SequenceData, Value as ClarityValue, +}; +use clarity::vm::ClarityName; use hiro_system_kit::slog; use regex::Regex; use reqwest::{Client, Method}; use schemars::JsonSchema; use serde_json::Value as JsonValue; -use stacks_codec::clarity::codec::StacksMessageCodec; -use stacks_codec::clarity::vm::types::PrincipalData; -use stacks_codec::clarity::vm::types::QualifiedContractIdentifier; -use stacks_codec::clarity::vm::types::{CharType, SequenceData, Value as ClarityValue}; -use stacks_codec::clarity::ClarityName; use std::collections::{BTreeMap, HashMap}; use std::io::Cursor; diff --git a/components/chainhook-sdk/src/indexer/stacks/mod.rs b/components/chainhook-sdk/src/indexer/stacks/mod.rs index d3ff0a963..1ba7a5d27 100644 --- a/components/chainhook-sdk/src/indexer/stacks/mod.rs +++ b/components/chainhook-sdk/src/indexer/stacks/mod.rs @@ -1,18 +1,18 @@ mod blocks_pool; pub use blocks_pool::StacksBlockPool; -use stacks_codec::codec::{StacksTransaction, TransactionAuth, TransactionPayload}; use crate::chainhooks::stacks::try_decode_clarity_value; use crate::indexer::AssetClassCache; use crate::indexer::{IndexerConfig, StacksChainContext}; use crate::utils::Context; use chainhook_types::*; +use clarity::codec::StacksMessageCodec; +use clarity::vm::types::{SequenceData, Value as ClarityValue}; use hiro_system_kit::slog; use rocket::serde::json::Value as JsonValue; use rocket::serde::Deserialize; -use stacks_codec::clarity::codec::StacksMessageCodec; -use stacks_codec::clarity::vm::types::{SequenceData, Value as ClarityValue}; +use stacks_codec::codec::{StacksTransaction, TransactionAuth, TransactionPayload}; use std::collections::{BTreeMap, HashMap, HashSet}; use std::convert::TryInto; use std::io::Cursor; From 2924ebc8e511e2517e8aee53a2c940ac26884aa5 Mon Sep 17 00:00:00 2001 From: Brady Ouren Date: Thu, 18 Jul 2024 07:27:54 -0700 Subject: [PATCH 26/28] feat: PoxConfig improvements (#616) closes #547 remove redundant PoxConfig and update uses to the chainhooks::types version --------- Co-authored-by: brady.ouren --- Cargo.lock | 4 +- components/chainhook-cli/src/cli/mod.rs | 4 +- components/chainhook-cli/src/config/file.rs | 9 ++ components/chainhook-cli/src/config/mod.rs | 32 ++++- .../src/service/tests/helpers/mock_service.rs | 2 + components/chainhook-sdk/Cargo.toml | 2 +- .../src/chainhooks/bitcoin/mod.rs | 73 +---------- .../src/chainhooks/stacks/mod.rs | 2 +- .../chainhook-sdk/src/chainhooks/types.rs | 113 ++++++++++++++++++ .../chainhook-sdk/src/indexer/bitcoin/mod.rs | 3 +- components/chainhook-sdk/src/indexer/mod.rs | 44 +------ .../chainhook-sdk/src/indexer/stacks/mod.rs | 4 +- components/chainhook-types-rs/Cargo.toml | 2 +- 13 files changed, 173 insertions(+), 121 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ddb12285b..41ffb771a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -489,7 +489,7 @@ dependencies = [ [[package]] name = "chainhook-sdk" -version = "0.12.10" +version = "0.12.11" dependencies = [ "base58", "base64 0.21.5", @@ -524,7 +524,7 @@ dependencies = [ [[package]] name = "chainhook-types" -version = "1.3.6" +version = "1.3.7" dependencies = [ "hex", "schemars", diff --git a/components/chainhook-cli/src/cli/mod.rs b/components/chainhook-cli/src/cli/mod.rs index 879ee8438..958fded1e 100644 --- a/components/chainhook-cli/src/cli/mod.rs +++ b/components/chainhook-cli/src/cli/mod.rs @@ -51,10 +51,10 @@ enum Command { /// Run a service streaming blocks and evaluating registered predicates #[clap(subcommand)] Service(ServiceCommand), - /// Stacks related subcommands + /// Stacks related subcommands #[clap(subcommand)] Stacks(StacksCommand), - /// Generate documentation + /// Generate documentation #[clap(subcommand)] Docs(DocsCommand), } diff --git a/components/chainhook-cli/src/config/file.rs b/components/chainhook-cli/src/config/file.rs index fcf8b6e18..6ef2fa2fa 100644 --- a/components/chainhook-cli/src/config/file.rs +++ b/components/chainhook-cli/src/config/file.rs @@ -3,6 +3,7 @@ use chainhook_sdk::types::BitcoinNetwork; #[derive(Deserialize, Debug, Clone)] pub struct ConfigFile { pub storage: StorageConfigFile, + pub pox_config: Option, pub http_api: Option, pub event_source: Option>, pub limits: LimitsConfigFile, @@ -10,6 +11,14 @@ pub struct ConfigFile { pub monitoring: Option, } +#[derive(Deserialize, Debug, Clone)] +pub struct PoxConfigFile { + pub first_burnchain_block_height: Option, + pub prepare_phase_len: Option, + pub reward_phase_len: Option, + pub rewarded_addresses_per_block: Option, +} + #[derive(Deserialize, Debug, Clone)] pub struct StorageConfigFile { pub working_dir: Option, diff --git a/components/chainhook-cli/src/config/mod.rs b/components/chainhook-cli/src/config/mod.rs index 7c3c1f8eb..c6667cd12 100644 --- a/components/chainhook-cli/src/config/mod.rs +++ b/components/chainhook-cli/src/config/mod.rs @@ -1,7 +1,7 @@ pub mod file; pub mod generator; -use chainhook_sdk::chainhooks::types::ChainhookStore; +use chainhook_sdk::chainhooks::types::{ChainhookStore, PoxConfig}; pub use chainhook_sdk::indexer::IndexerConfig; use chainhook_sdk::observer::EventObserverConfig; use chainhook_sdk::types::{ @@ -28,6 +28,7 @@ pub const BITCOIN_MAX_PREDICATE_REGISTRATION: usize = 50; #[derive(Clone, Debug, PartialEq)] pub struct Config { pub storage: StorageConfig, + pub pox_config: PoxConfig, pub http_api: PredicatesApi, pub event_sources: Vec, pub limits: LimitsConfig, @@ -153,10 +154,36 @@ impl Config { } else { None }; + let default_pox_config = match stacks_network { + StacksNetwork::Mainnet => PoxConfig::mainnet_default(), + StacksNetwork::Devnet => PoxConfig::testnet_default(), + _ => PoxConfig::default(), + }; let config = Config { storage: StorageConfig { working_dir: config_file.storage.working_dir.unwrap_or("cache".into()), }, + pox_config: match config_file.pox_config { + None => default_pox_config, + Some(pox_config) => PoxConfig { + first_burnchain_block_height: pox_config + .first_burnchain_block_height + .unwrap_or(default_pox_config.first_burnchain_block_height) + .into(), + prepare_phase_len: pox_config + .prepare_phase_len + .unwrap_or(default_pox_config.prepare_phase_len) + .into(), + reward_phase_len: pox_config + .reward_phase_len + .unwrap_or(default_pox_config.reward_phase_len) + .into(), + rewarded_addresses_per_block: pox_config + .rewarded_addresses_per_block + .unwrap_or(default_pox_config.rewarded_addresses_per_block) + .into(), + }, + }, http_api: match config_file.http_api { None => PredicatesApi::Off, Some(http_api) => match http_api.disabled { @@ -332,6 +359,7 @@ impl Config { storage: StorageConfig { working_dir: default_cache_path(), }, + pox_config: PoxConfig::devnet_default(), http_api: PredicatesApi::Off, event_sources: vec![], limits: LimitsConfig { @@ -364,6 +392,7 @@ impl Config { storage: StorageConfig { working_dir: default_cache_path(), }, + pox_config: PoxConfig::testnet_default(), http_api: PredicatesApi::Off, event_sources: vec![EventSourceConfig::StacksTsvUrl(UrlConfig { file_url: DEFAULT_TESTNET_STACKS_TSV_ARCHIVE.into(), @@ -398,6 +427,7 @@ impl Config { storage: StorageConfig { working_dir: default_cache_path(), }, + pox_config: PoxConfig::mainnet_default(), http_api: PredicatesApi::Off, event_sources: vec![EventSourceConfig::StacksTsvUrl(UrlConfig { file_url: DEFAULT_MAINNET_STACKS_TSV_ARCHIVE.into(), diff --git a/components/chainhook-cli/src/service/tests/helpers/mock_service.rs b/components/chainhook-cli/src/service/tests/helpers/mock_service.rs index 7e50e59b8..2f6293f56 100644 --- a/components/chainhook-cli/src/service/tests/helpers/mock_service.rs +++ b/components/chainhook-cli/src/service/tests/helpers/mock_service.rs @@ -7,6 +7,7 @@ use crate::service::{ http_api::start_predicate_api_server, update_predicate_spec, update_predicate_status, PredicateStatus, Service, }; +use chainhook_sdk::chainhooks::types::PoxConfig; use chainhook_sdk::{ chainhooks::stacks::StacksChainhookSpecificationNetworkMap, chainhooks::types::{ChainhookInstance, ChainhookSpecificationNetworkMap}, @@ -297,6 +298,7 @@ pub fn get_chainhook_config( }; Config { http_api: PredicatesApi::On(api_config), + pox_config: PoxConfig::devnet_default(), storage: StorageConfig { working_dir: working_dir.into(), }, diff --git a/components/chainhook-sdk/Cargo.toml b/components/chainhook-sdk/Cargo.toml index 0a36f6179..a2e2493c5 100644 --- a/components/chainhook-sdk/Cargo.toml +++ b/components/chainhook-sdk/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "chainhook-sdk" -version = "0.12.10" +version = "0.12.11" description = "Stateless Transaction Indexing Engine for Stacks and Bitcoin" license = "GPL-3.0" edition = "2021" diff --git a/components/chainhook-sdk/src/chainhooks/bitcoin/mod.rs b/components/chainhook-sdk/src/chainhooks/bitcoin/mod.rs index a88a78e48..d8e8d2948 100644 --- a/components/chainhook-sdk/src/chainhooks/bitcoin/mod.rs +++ b/components/chainhook-sdk/src/chainhooks/bitcoin/mod.rs @@ -1,6 +1,6 @@ use super::types::{ append_error_context, validate_txid, ChainhookInstance, ExactMatchingRule, HookAction, - MatchingRule, + MatchingRule, PoxConfig, TxinPredicate, }; use crate::utils::{Context, MAX_BLOCK_HEIGHTS_ENTRIES}; @@ -403,60 +403,11 @@ pub fn get_stacks_canonical_magic_bytes(network: &BitcoinNetwork) -> [u8; 2] { } } -pub struct PoxConfig { - pub genesis_block_height: u64, - pub prepare_phase_len: u64, - pub reward_phase_len: u64, - pub rewarded_addresses_per_block: usize, -} - -impl PoxConfig { - pub fn get_pox_cycle_len(&self) -> u64 { - self.prepare_phase_len + self.reward_phase_len - } - - pub fn get_pox_cycle_id(&self, block_height: u64) -> u64 { - (block_height.saturating_sub(self.genesis_block_height)) / self.get_pox_cycle_len() - } - - pub fn get_pos_in_pox_cycle(&self, block_height: u64) -> u64 { - (block_height.saturating_sub(self.genesis_block_height)) % self.get_pox_cycle_len() - } - - pub fn get_burn_address(&self) -> &str { - match self.genesis_block_height { - 666050 => "1111111111111111111114oLvT2", - 2000000 => "burn-address-regtest", - _ => "burn-address", - } - } -} -const POX_CONFIG_MAINNET: PoxConfig = PoxConfig { - genesis_block_height: 666050, - prepare_phase_len: 100, - reward_phase_len: 2100, - rewarded_addresses_per_block: 2, -}; - -const POX_CONFIG_TESTNET: PoxConfig = PoxConfig { - genesis_block_height: 2000000, - prepare_phase_len: 50, - reward_phase_len: 1050, - rewarded_addresses_per_block: 2, -}; - -const POX_CONFIG_DEVNET: PoxConfig = PoxConfig { - genesis_block_height: 100, - prepare_phase_len: 4, - reward_phase_len: 10, - rewarded_addresses_per_block: 2, -}; - pub fn get_canonical_pox_config(network: &BitcoinNetwork) -> PoxConfig { match network { - BitcoinNetwork::Mainnet => POX_CONFIG_MAINNET, - BitcoinNetwork::Testnet => POX_CONFIG_TESTNET, - BitcoinNetwork::Regtest => POX_CONFIG_DEVNET, + BitcoinNetwork::Mainnet => PoxConfig::mainnet_default(), + BitcoinNetwork::Testnet => PoxConfig::testnet_default(), + BitcoinNetwork::Regtest => PoxConfig::devnet_default(), BitcoinNetwork::Signet => unreachable!(), } } @@ -486,22 +437,6 @@ impl TryFrom for StacksOpcodes { } } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct TxinPredicate { - pub txid: String, - pub vout: u32, -} - -impl TxinPredicate { - pub fn validate(&self) -> Result<(), Vec> { - if let Err(e) = validate_txid(&self.txid) { - return Err(vec![e]); - } - Ok(()) - } -} - #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct DescriptorMatchingRule { diff --git a/components/chainhook-sdk/src/chainhooks/stacks/mod.rs b/components/chainhook-sdk/src/chainhooks/stacks/mod.rs index 417c0ca99..b5ae3fe3b 100644 --- a/components/chainhook-sdk/src/chainhooks/stacks/mod.rs +++ b/components/chainhook-sdk/src/chainhooks/stacks/mod.rs @@ -1,10 +1,10 @@ use crate::utils::{AbstractStacksBlock, Context, MAX_BLOCK_HEIGHTS_ENTRIES}; -use super::types::validate_txid; use super::types::{ append_error_context, BlockIdentifierIndexRule, ChainhookInstance, ExactMatchingRule, HookAction, }; +use super::types::{validate_txid, PoxConfig}; use chainhook_types::{ BlockIdentifier, StacksChainEvent, StacksNetwork, StacksTransactionData, StacksTransactionEvent, StacksTransactionEventPayload, StacksTransactionKind, diff --git a/components/chainhook-sdk/src/chainhooks/types.rs b/components/chainhook-sdk/src/chainhooks/types.rs index 69eaf6f14..ea3a0bdb2 100644 --- a/components/chainhook-sdk/src/chainhooks/types.rs +++ b/components/chainhook-sdk/src/chainhooks/types.rs @@ -361,6 +361,119 @@ impl ScriptTemplate { } } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] +pub struct PoxConfig { + pub first_burnchain_block_height: u64, + pub prepare_phase_len: u64, + pub reward_phase_len: u64, + // TODO currently unused + pub rewarded_addresses_per_block: usize, +} + +impl PoxConfig { + pub fn mainnet_default() -> PoxConfig { + PoxConfig { + first_burnchain_block_height: 666050, + prepare_phase_len: 100, + reward_phase_len: 2000, + rewarded_addresses_per_block: 2, + } + } + + pub fn testnet_default() -> PoxConfig { + PoxConfig { + first_burnchain_block_height: 2000000, + prepare_phase_len: 50, + reward_phase_len: 1000, + rewarded_addresses_per_block: 2, + } + } + + pub fn devnet_default() -> PoxConfig { + Self::default() + } + pub fn get_pox_cycle_len(&self) -> u64 { + self.prepare_phase_len + self.reward_phase_len + } + + pub fn get_pox_cycle_id(&self, block_height: u64) -> u64 { + (block_height.saturating_sub(self.first_burnchain_block_height)) / self.get_pox_cycle_len() + } + + pub fn get_pos_in_pox_cycle(&self, block_height: u64) -> u64 { + (block_height.saturating_sub(self.first_burnchain_block_height)) % self.get_pox_cycle_len() + } + + pub fn get_burn_address(&self) -> &str { + match self.first_burnchain_block_height { + 666050 => "1111111111111111111114oLvT2", + 2000000 => "burn-address-regtest", + _ => "burn-address", + } + } +} + +impl Default for PoxConfig { + fn default() -> PoxConfig { + PoxConfig { + first_burnchain_block_height: 100, + prepare_phase_len: 5, + reward_phase_len: 15, + rewarded_addresses_per_block: 2, + } + } +} + +pub fn get_canonical_pox_config(network: &BitcoinNetwork) -> PoxConfig { + match network { + BitcoinNetwork::Mainnet => PoxConfig::mainnet_default(), + BitcoinNetwork::Testnet => PoxConfig::testnet_default(), + BitcoinNetwork::Regtest => PoxConfig::default(), + BitcoinNetwork::Signet => unreachable!(), + } +} + +#[derive(Debug, Clone, PartialEq)] +#[repr(u8)] +pub enum StacksOpcodes { + BlockCommit = '[' as u8, + KeyRegister = '^' as u8, + StackStx = 'x' as u8, + PreStx = 'p' as u8, + TransferStx = '$' as u8, +} + +impl TryFrom for StacksOpcodes { + type Error = (); + + fn try_from(v: u8) -> Result { + match v { + x if x == StacksOpcodes::BlockCommit as u8 => Ok(StacksOpcodes::BlockCommit), + x if x == StacksOpcodes::KeyRegister as u8 => Ok(StacksOpcodes::KeyRegister), + x if x == StacksOpcodes::StackStx as u8 => Ok(StacksOpcodes::StackStx), + x if x == StacksOpcodes::PreStx as u8 => Ok(StacksOpcodes::PreStx), + x if x == StacksOpcodes::TransferStx as u8 => Ok(StacksOpcodes::TransferStx), + _ => Err(()), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct TxinPredicate { + pub txid: String, + pub vout: u32, +} + +impl TxinPredicate { + pub fn validate(&self) -> Result<(), Vec> { + if let Err(e) = validate_txid(&self.txid) { + return Err(vec![e]); + } + Ok(()) + } +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum BlockIdentifierIndexRule { diff --git a/components/chainhook-sdk/src/indexer/bitcoin/mod.rs b/components/chainhook-sdk/src/indexer/bitcoin/mod.rs index c28826020..ae6077eeb 100644 --- a/components/chainhook-sdk/src/indexer/bitcoin/mod.rs +++ b/components/chainhook-sdk/src/indexer/bitcoin/mod.rs @@ -1,8 +1,9 @@ use std::time::Duration; use crate::chainhooks::bitcoin::{ - get_canonical_pox_config, get_stacks_canonical_magic_bytes, PoxConfig, StacksOpcodes, + get_canonical_pox_config, get_stacks_canonical_magic_bytes, StacksOpcodes, }; +use crate::chainhooks::types::PoxConfig; use crate::observer::BitcoinConfig; use crate::utils::Context; use bitcoincore_rpc::bitcoin::hashes::Hash; diff --git a/components/chainhook-sdk/src/indexer/mod.rs b/components/chainhook-sdk/src/indexer/mod.rs index b9b1aa544..2a3195115 100644 --- a/components/chainhook-sdk/src/indexer/mod.rs +++ b/components/chainhook-sdk/src/indexer/mod.rs @@ -2,7 +2,10 @@ pub mod bitcoin; pub mod fork_scratch_pad; pub mod stacks; -use crate::utils::{AbstractBlock, Context}; +use crate::{ + chainhooks::types::PoxConfig, + utils::{AbstractBlock, Context}, +}; use chainhook_types::{ BitcoinBlockSignaling, BitcoinNetwork, BlockHeader, BlockIdentifier, BlockchainEvent, @@ -22,45 +25,6 @@ pub struct AssetClassCache { pub decimals: u8, } -#[derive(Deserialize, Debug, Clone)] -pub struct PoxConfig { - pub first_burnchain_block_height: u32, - pub prepare_phase_block_length: u32, - pub reward_phase_block_length: u32, -} - -impl PoxConfig { - pub fn mainnet_default() -> PoxConfig { - PoxConfig { - first_burnchain_block_height: 666050, - prepare_phase_block_length: 100, - reward_phase_block_length: 2000, - } - } - - pub fn testnet_default() -> PoxConfig { - PoxConfig { - first_burnchain_block_height: 2000000, - prepare_phase_block_length: 50, - reward_phase_block_length: 1000, - } - } - - pub fn devnet_default() -> PoxConfig { - Self::default() - } -} - -impl Default for PoxConfig { - fn default() -> PoxConfig { - PoxConfig { - first_burnchain_block_height: 100, - prepare_phase_block_length: 4, - reward_phase_block_length: 6, - } - } -} - pub struct StacksChainContext { asset_class_map: HashMap, pox_config: PoxConfig, diff --git a/components/chainhook-sdk/src/indexer/stacks/mod.rs b/components/chainhook-sdk/src/indexer/stacks/mod.rs index d3ff0a963..b10295490 100644 --- a/components/chainhook-sdk/src/indexer/stacks/mod.rs +++ b/components/chainhook-sdk/src/indexer/stacks/mod.rs @@ -327,9 +327,7 @@ pub fn standardize_stacks_block( chain_ctx: &mut StacksChainContext, ctx: &Context, ) -> Result { - let pox_cycle_length: u64 = (chain_ctx.pox_config.prepare_phase_block_length - + chain_ctx.pox_config.reward_phase_block_length) - .into(); + let pox_cycle_length: u64 = (chain_ctx.pox_config.get_pox_cycle_len()).into(); let current_len = u64::saturating_sub( block.burn_block_height, 1 + (chain_ctx.pox_config.first_burnchain_block_height as u64), diff --git a/components/chainhook-types-rs/Cargo.toml b/components/chainhook-types-rs/Cargo.toml index eed3d610c..493ecf1df 100644 --- a/components/chainhook-types-rs/Cargo.toml +++ b/components/chainhook-types-rs/Cargo.toml @@ -2,7 +2,7 @@ name = "chainhook-types" description = "Bitcoin and Stacks data schemas, based on the Rosetta specification" license = "MIT" -version = "1.3.6" +version = "1.3.7" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From c9675894655293167be51d29d3c935eb10c3e195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20C=C3=A1rdenas?= Date: Mon, 22 Jul 2024 14:44:08 -0600 Subject: [PATCH 27/28] chore: add abi support to client predicate (#626) --- components/client/typescript/package-lock.json | 4 ++-- components/client/typescript/package.json | 2 +- components/client/typescript/src/schemas/stacks/if_this.ts | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/components/client/typescript/package-lock.json b/components/client/typescript/package-lock.json index dd0427e3f..467e93860 100644 --- a/components/client/typescript/package-lock.json +++ b/components/client/typescript/package-lock.json @@ -1,12 +1,12 @@ { "name": "@hirosystems/chainhook-client", - "version": "1.10.0", + "version": "1.11.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@hirosystems/chainhook-client", - "version": "1.10.0", + "version": "1.11.0", "license": "Apache 2.0", "dependencies": { "@fastify/type-provider-typebox": "^3.2.0", diff --git a/components/client/typescript/package.json b/components/client/typescript/package.json index 9731e83a9..259c18aa6 100644 --- a/components/client/typescript/package.json +++ b/components/client/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@hirosystems/chainhook-client", - "version": "1.10.0", + "version": "1.11.0", "description": "Chainhook TypeScript client", "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/components/client/typescript/src/schemas/stacks/if_this.ts b/components/client/typescript/src/schemas/stacks/if_this.ts index c87047c0c..9f3f709eb 100644 --- a/components/client/typescript/src/schemas/stacks/if_this.ts +++ b/components/client/typescript/src/schemas/stacks/if_this.ts @@ -79,6 +79,7 @@ export const StacksIfThisOptionsSchema = Type.Object({ end_block: Type.Optional(Type.Integer()), expire_after_occurrence: Type.Optional(Type.Integer()), decode_clarity_values: Type.Optional(Type.Boolean()), + include_contract_abi: Type.Optional(Type.Boolean()), }); export type StacksIfThisOptions = Static; From 0d3c33b99a7f1892f596051d0726f10c2150533b Mon Sep 17 00:00:00 2001 From: Elias Rad <146735585+nnsW3@users.noreply.github.com> Date: Mon, 5 Aug 2024 18:20:52 +0300 Subject: [PATCH 28/28] docs: fix spelling issue (#629) --- README.md | 4 ++-- .../how-to-run-chainhook-as-a-service-using-stacks.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cc09e89e0..c96298a89 100644 --- a/README.md +++ b/README.md @@ -476,8 +476,8 @@ The current `stacks` predicates support the following `if_this` constructs: // `contract-identifier` mandatory argument admits: // - string type, fully qualifying the contract to observe. example: `ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.monkey-sip09` // `contains` or `matches_regex` argument admits: -// - `contains` string type, used for matching event containing the specific string. -// - `matches_regex` string type that should be a valid regex, used for matching event that regex matches with specific string. +// - `contains` string type, used for matching events containing the specific string. +// - `matches_regex` string type that should be a valid regex, used for matching an event that regex matches with the specified string. { "if_this": { "scope": "print_event", diff --git a/docs/how-to-guides/how-to-run-chainhook-as-a-service-using-stacks.md b/docs/how-to-guides/how-to-run-chainhook-as-a-service-using-stacks.md index 2f1849060..9cab40811 100644 --- a/docs/how-to-guides/how-to-run-chainhook-as-a-service-using-stacks.md +++ b/docs/how-to-guides/how-to-run-chainhook-as-a-service-using-stacks.md @@ -11,7 +11,7 @@ Start with the prerequisite section and configure your files to start the chainh ### Configure Your Stacks Node - Configure your Stacks node using the [Stacks node configuration](https://docs.stacks.co/docs/nodes-and-miners/stacks-node-configuration) documentation. -- We Recommend using the latest version of Stacks. You can check the latest version by following [this](https://github.com/stacks-network/stacks-blockchain/releases) link. +- We recommend using the latest version of Stacks. You can check the latest version by following [this](https://github.com/stacks-network/stacks-blockchain/releases) link. - Set up your Bitcoin node by following [this](how-to-run-chainhook-as-a-service-using-bitcoind.md#setting-up-a-bitcoin-node) article, then get the `rpcuser`, `rpcpassword`, and `rpc_port` values defined in the `bitcoin.conf` file. A `Stacks.toml` file is generated when configuring your Stacks node. Below is the sample `Stacks.toml` file.