feat: implement and document new development flow

This commit is contained in:
Ludo Galabru
2023-02-15 20:37:46 -06:00
parent 5bb2620ac0
commit 66019a06e7
18 changed files with 2173 additions and 1827 deletions

134
Cargo.lock generated
View File

@@ -384,11 +384,11 @@ dependencies = [
"bitcoincore-rpc",
"bitcoincore-rpc-json",
"chainhook-event-observer",
"chainhook-types 1.0.0",
"chainhook-types 1.0.1",
"clap 3.2.23",
"clap_generate",
"clarinet-files",
"clarity-repl 1.4.2",
"clarity-repl 1.4.1",
"criterion",
"crossbeam-channel",
"csv",
@@ -397,7 +397,7 @@ dependencies = [
"flume",
"futures-util",
"hex",
"hiro-system-kit 0.1.0",
"hiro-system-kit 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"num_cpus",
"redis",
"reqwest",
@@ -412,7 +412,7 @@ dependencies = [
[[package]]
name = "chainhook-event-observer"
version = "1.0.1"
version = "1.0.3"
dependencies = [
"base58 0.2.0",
"base64 0.13.1",
@@ -421,8 +421,8 @@ dependencies = [
"chainhook-types 1.0.1",
"clap 3.2.23",
"clap_generate",
"clarinet-utils 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"clarity-repl 1.4.0",
"clarinet-utils",
"clarity-repl 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"crossbeam-channel",
"ctrlc",
"hiro-system-kit 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -440,9 +440,7 @@ dependencies = [
[[package]]
name = "chainhook-types"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e55e9a635deda8267306d6a9eb645440f243fdffedb1df274c2fe42408bdce42"
version = "1.0.1"
dependencies = [
"schemars",
"serde",
@@ -454,6 +452,8 @@ dependencies = [
[[package]]
name = "chainhook-types"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe083f0dd830eb487602a3f1ebc56c1245d9cdb25cc83adb8f35e977bb477dc2"
dependencies = [
"schemars",
"serde",
@@ -558,12 +558,14 @@ dependencies = [
[[package]]
name = "clarinet-files"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07a317e7aa292263aab236ef9c751983b6faca76f73f07d02385a6721c5dd444"
dependencies = [
"bip39",
"bitcoin",
"chainhook-types 1.0.1",
"clarinet-utils 1.0.0",
"clarity-repl 1.4.2",
"chainhook-types 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"clarinet-utils",
"clarity-repl 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"libsecp256k1 0.7.1",
"serde",
"serde_derive",
@@ -572,16 +574,6 @@ dependencies = [
"url",
]
[[package]]
name = "clarinet-utils"
version = "1.0.0"
dependencies = [
"hmac 0.12.1",
"pbkdf2",
"serde",
"sha2 0.10.6",
]
[[package]]
name = "clarinet-utils"
version = "1.0.0"
@@ -596,45 +588,7 @@ dependencies = [
[[package]]
name = "clarity-repl"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec3faf68f2dfe50600f86b7d68cc2329a6bcfb31dd77218b0da9b0507bde8526"
dependencies = [
"ansi_term",
"atty",
"bytes",
"clarity-vm",
"debug_types",
"futures",
"getrandom 0.2.8",
"hiro-system-kit 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"httparse",
"integer-sqrt",
"lazy_static",
"libsecp256k1 0.5.0",
"log",
"memchr",
"pico-args",
"prettytable-rs",
"rand 0.7.3",
"rand_pcg",
"rand_seeder",
"regex",
"reqwest",
"ripemd160",
"rustyline",
"serde",
"serde_derive",
"serde_json",
"sha2 0.10.6",
"sha3 0.9.1",
"tokio",
"tokio-util",
]
[[package]]
name = "clarity-repl"
version = "1.4.2"
version = "1.4.1"
dependencies = [
"ansi_term",
"atty",
@@ -669,10 +623,48 @@ dependencies = [
]
[[package]]
name = "clarity-vm"
version = "2.0.0"
name = "clarity-repl"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d7b6608bc450fe16f915cc6175f4594a817773a7ca3a25e434e4bce6be1fce2"
checksum = "5951d17cedff3fdd6b78791231e13bedd3dd41b82e85ee279f76da576531cdf1"
dependencies = [
"ansi_term",
"atty",
"bytes",
"clarity-vm",
"debug_types",
"futures",
"getrandom 0.2.8",
"hiro-system-kit 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"httparse",
"integer-sqrt",
"lazy_static",
"libsecp256k1 0.5.0",
"log",
"memchr",
"pico-args",
"prettytable-rs",
"rand 0.7.3",
"rand_pcg",
"rand_seeder",
"regex",
"reqwest",
"ripemd160",
"rustyline",
"serde",
"serde_derive",
"serde_json",
"sha2 0.10.6",
"sha3 0.9.1",
"tokio",
"tokio-util",
]
[[package]]
name = "clarity-vm"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59cbb21adf1277368fe098e129de5dc17d8797a1630fe7b598d59c536b744622"
dependencies = [
"integer-sqrt",
"lazy_static",
@@ -687,7 +679,6 @@ dependencies = [
"serde_json",
"serde_stacker",
"sha2-asm",
"slog",
"stacks-common",
"time 0.2.27",
]
@@ -1590,12 +1581,6 @@ dependencies = [
"atty",
"futures",
"lazy_static",
"slog",
"slog-async",
"slog-atomic",
"slog-json",
"slog-scope",
"slog-term",
"tokio",
]
@@ -3677,9 +3662,6 @@ dependencies = [
"serde_json",
"sha2 0.10.6",
"sha3 0.10.6",
"slog",
"slog-json",
"slog-term",
"time 0.2.27",
]
@@ -3689,8 +3671,8 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aca7b97b09d835c932c1ee24335adcb4a85225aaf6eb566aa36fe6f9f18958cd"
dependencies = [
"clarinet-utils 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"clarity-repl 1.4.0",
"clarinet-utils",
"clarity-repl 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"reqwest",
"serde",
"serde_derive",

732
README.md
View File

@@ -1,192 +1,582 @@
# chainhook-cli
## Usage
## Introduction
To get started, [build `clarinet` from source](https://github.com/hirosystems/clarinet#install-from-source-using-cargo), and then `cd components/chainhook-cli` and run `cargo install --path .` to install `chainhook-cli`.
Blockchains are pieces of infrastructure that unblock new use cases and introduce a new generation of decentralized applications, by relying on a public ledger.
`chainhook` is a fork aware transaction indexing engine aiming at helping developers focusing on the informations they need, by helping with the on-chain data extraction. By focusing on the data they care about, developers are working with a much lighter datasets. Benefits are plurals:
- Improved Developer Experience: instead of working with a generic blockchain indexer, taking hours to process every single transactions of every single block, developers can create their own indexes, build, iterate and refine in minutes.
- Cost Optimization: by avoiding full chain indexation, developers avoid massive storage management and unnecessary storage scaling issues. Also `chainhook` helps developers creating elegant event based architectures. Developers write `if_this` / `then_that` **predicates**, being evaluated on transactions and blocks. When the evaluation of these **predicates** appears to be true, the related transactions are packaged as event and forwarded to the configured destination. By using cloud functions as destinations, developers can also cut costs on processing, by only paying processing when a block that contains some data relevant to developer's application is being mined.
- Optimized User Experience: lighter indexes implies faster queries results, which helps minimizing end user responses time.
Before running `chainhook-cli`, you need to [install redis](https://redis.io/docs/getting-started/installation/) and run a redis server locally.
---
## Install chainhook
### Start a Testnet node
### Install from source
```bash
$ git clone https://github.com/hirosystems/chainhook.git
$ cd chainhook
$ cargo chainhook-install
```
---
## Development workflow for Bitcoin chainhooks
### Guide to `if_this` / `then_that` predicate design
To get started with bitcoin predicates, we can use the `chainhook` to generate a template:
```bash
$ chainhook-cli start --testnet
$ chainhook predicates new --bitcoin
```
### Start a Mainnet node
We will focus on the `if_this` and `then_that` parts of the specifications.
The current `bitcoin` predicates supports the following `if_this` constructs:
```jsonc
// Get any transaction matching a given txid
// `txid` mandatory argument admits:
// - 32 bytes hex encoded type. example:
{
"if_this": {
"scope": "txid",
"equals": "0xfaaac1833dc4883e7ec28f61e35b41f896c395f8d288b1a177155de2abd6052f"
}
}
// Get any transaction including an OP_RETURN output starting with a set of characters.
// `starts_with` mandatory argument admits:
// - ASCII string type. example: `X2[`
// - hex encoded bytes. example: `0x589403`
{
"if_this": {
"scope": "outputs",
"op_return": {
"starts_with": "X2["
}
}
}
// Get any transaction including an OP_RETURN output matching the sequence of bytes specified
// `equals` mandatory argument admits:
// - hex encoded bytes. example: `0x589403`
{
"if_this": {
"scope": "outputs",
"op_return": {
"equals": "0x69bd04208265aca9424d0337dac7d9e84371a2c91ece1891d67d3554bd9fdbe60afc6924d4b0773d90000006700010000006600012"
}
}
}
// Get any transaction including an OP_RETURN output ending with a set of characters
// `ends_with` mandatory argument admits:
// - ASCII string type. example: `X2[`
// - hex encoded bytes. example: `0x589403`
{
"if_this": {
"scope": "outputs",
"op_return": {
"ends_with": "0x76a914000000000000000000000000000000000000000088ac"
}
}
}
// Get any transaction including a p2pkh output paying a given recipient
// `p2pkh` construct admits:
// - string type. example: "mr1iPkD9N3RJZZxXRk7xF9d36gffa6exNC"
// - hex encoded bytes type. example: "0x76a914ee9369fb719c0ba43ddf4d94638a970b84775f4788ac"
{
"if_this": {
"scope": "outputs",
"p2pkh": "mr1iPkD9N3RJZZxXRk7xF9d36gffa6exNC"
}
}
// Get any transaction including a p2sh output paying a given recipient
// `p2sh` construct admits:
// - string type. example: "2MxDJ723HBJtEMa2a9vcsns4qztxBuC8Zb2"
// - hex encoded bytes type. example: "0x76a914ee9369fb719c0ba43ddf4d94638a970b84775f4788ac"
{
"if_this": {
"scope": "outputs",
"p2sh": "2MxDJ723HBJtEMa2a9vcsns4qztxBuC8Zb2"
}
}
// Get any transaction including a p2wpkh output paying a given recipient
// `p2wpkh` construct admits:
// - string type. example: "bcrt1qnxknq3wqtphv7sfwy07m7e4sr6ut9yt6ed99jg"
{
"if_this": {
"scope": "outputs",
"p2wpkh": "bcrt1qnxknq3wqtphv7sfwy07m7e4sr6ut9yt6ed99jg"
}
}
// Get any transaction including a p2wsh output paying a given recipient
// `p2wsh` construct admits:
// - string type. example: "bc1qklpmx03a8qkv263gy8te36w0z9yafxplc5kwzc"
{
"if_this": {
"scope": "outputs",
"p2wsh": "bc1qklpmx03a8qkv263gy8te36w0z9yafxplc5kwzc"
}
}
// Get any transaction including a Stacks Proof of Burn commitment
{
"if_this": {
"protocol": "stacks",
"operation": "pob_committed"
}
}
// Get any transaction including a Stacks Proof of Transfer commitment
{
"if_this": {
"protocol": "stacks",
"operation": "pox_committed"
}
}
// Get any transaction including a key registration operation
{
"if_this": {
"protocol": "stacks",
"operation": "leader_key_registered"
}
}
// Get any transaction including a STX transfer operation
{
"if_this": {
"protocol": "stacks",
"operation": "stx_transfered"
}
}
// Get any transaction including a STX lock operation
{
"if_this": {
"protocol": "stacks",
"operation": "stx_locked"
}
}
// Get any transaction including a new Ordinal inscription
{
"if_this": {
"protocol": "ordinals",
"operation": "inscription_revealed"
}
}
```
In terms of actions available, the following `then_that` constructs are supported:
```jsonc
// HTTP Post block / transaction payload to a given endpoint.
// `http_post` construct admits:
// - url (string type). Example: http://localhost:3000/api/v1/wrapBtc
// - authorization_header (string type). Secret to add to the request `authorization` header when posting payloads
{
"then_that": {
"http_post": {
"url": "http://localhost:3000/api/v1/wrapBtc",
"authorization_header": "Bearer cn389ncoiwuencr"
}
}
}
// Append events to a file through filesystem. Convenient for local tests.
// `file_append` construct admits:
// - path (string type). Path to file on disk.
{
"then_that": {
"file_append": {
"path": "/tmp/events.json",
}
}
}
```
Additional configuration knobs available:
```jsonc
// Ignore any block prior to given block:
"start_block": 101
// Ignore any block after given block:
"end_block": 201
// Stop evaluating chainhook after a given number of occurrences found:
"expire_after_occurrence": 1
```
Putting all the pieces together:
```jsonc
// Retrieve and HTTP Post to `http://localhost:3000/api/v1/wrapBtc`
// the 5 first transfers to the p2wpkh `bcrt1qnxk...yt6ed99jg` address,
// of any amount, occurring after block height 10200.
{
"chain": "bitcoin",
"uuid": "1",
"name": "Wrap BTC",
"version": 1,
"networks": {
"testnet": {
"if_this": {
"scope": "outputs",
"p2wpkh": {
"equals": "bcrt1qnxknq3wqtphv7sfwy07m7e4sr6ut9yt6ed99jg"
}
},
"then_that": {
"http_post": {
"url": "http://localhost:3000/api/v1/transfers",
"authorization_header": "Bearer cn389ncoiwuencr"
}
},
"start_block": 10200,
"expire_after_occurrence": 5,
}
}
}
// A specification file can also include different networks.
// In this case, the chainhook will select the predicate
// corresponding to the network it was launched against.
{
"chain": "bitcoin",
"uuid": "1",
"name": "Wrap BTC",
"version": 1,
"networks": {
"testnet": {
"if_this": {
"protocol": "ordinals",
"operation": "inscription_revealed"
},
"then_that": {
"http_post": {
"url": "http://localhost:3000/api/v1/ordinals",
"authorization_header": "Bearer cn389ncoiwuencr"
}
},
"start_block": 10200,
},
"mainnet": {
"if_this": {
"protocol": "ordinals",
"operation": "inscription_revealed"
},
"then_that": {
"http_post": {
"url": "http://my-protocol.xyz/api/v1/ordinals",
"authorization_header": "Bearer cn389ncoiwuencr"
}
},
"start_block": 90232,
}
}
}
```
### Guide to local Bitcoin testnet / mainnet predicate scanning
In order to scan the Bitcoin chain with a given predicate, a `bitcoind` instance with access to the RPC methods `getblockhash` and `getblock` must be accessible. The RPC calls latency will directly impact the speed of the scans.
*Note: the configuration of a `bitcoind` instance is out of scope for this guide.*
Assuming a `bitcoind` node correctly configured, scans can be performed using the following command:
```bash
$ chainhook predicates scan ./path/to/predicate.json --testnet
```
When using the flag `--testnet`, the scan operation will generate a configuration file in memory using the following settings:
```toml
[storage]
driver = "memory"
[chainhooks]
max_stacks_registrations = 500
max_bitcoin_registrations = 500
[network]
mode = "testnet"
bitcoin_node_rpc_url = "http://0.0.0.0:18332"
bitcoin_node_rpc_username = "testnet"
bitcoin_node_rpc_password = "testnet"
stacks_node_rpc_url = "http://0.0.0.0:20443"
```
When using the flag `--mainnet`, the scan operation will generate a configuration file in memory using the following settings:
```toml
[storage]
driver = "memory"
[chainhooks]
max_stacks_registrations = 500
max_bitcoin_registrations = 500
[network]
mode = "mainnet"
bitcoin_node_rpc_url = "http://0.0.0.0:8332"
bitcoin_node_rpc_username = "mainnet"
bitcoin_node_rpc_password = "mainnet"
stacks_node_rpc_url = "http://0.0.0.0:20443"
```
By passing the flag `--config=/path/to/config.toml`, developers can customize the credentials and network address of their bitcoin node.
```bash
$ chainhook config new --testnet
✔ Generated config file Testnet.toml
$ chainhook predicates scan ./path/predicate.json --config=./Testnet.toml
```
**Tips and tricks**
To optimize their experience with scanning, developers have a few knobs they can play with:
- Use of adequate values for `start_block` and `end_block` in predicates will drastically improved the speed.
- Networking: reducing the amount of networks hops between the chainhook process and the bitcoind process can also help a lot.
---
## Development workflow for Stacks chainhooks
### Guide to `if_this` / `then_that` predicate design
To get started with stacks predicates, we can use the `chainhook` to generate a template:
```bash
$ chainhook-cli start --mainnet
$ chainhook predicates new --stacks
```
### Start a Devnet node
We will focus on the `if_this` and `then_that` parts of the specifications.
The current `stacks` predicates supports the following `if_this` constructs:
```jsonc
// Get any transaction matching a given txid
// `txid` mandatory argument admits:
// - 32 bytes hex encoded type. example: "0xfaaac1833dc4883e7ec28f61e35b41f896c395f8d288b1a177155de2abd6052f"
{
"if_this": {
"scope": "txid",
"equals": "0xfaaac1833dc4883e7ec28f61e35b41f896c395f8d288b1a177155de2abd6052f"
}
}
// Get any transaction related to a given fungible token asset identifier
// `asset-identifier` mandatory argument admits:
// - string type, fully qualifying the asset identifier to observe. example: `ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.cbtc-sip10::cbtc`
// `actions` mandatory argument admits:
// - array of string type constrained to `mint`, `transfer` and `burn` values. example: ["mint", "burn"]
{
"if_this": {
"scope": "ft_event",
"asset_identifier": "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.cbtc-token::cbtc",
"actions": ["burn"]
},
}
// Get any transaction related to a given non fungible token asset identifier
// `asset-identifier` mandatory argument admits:
// - string type, fully qualifying the asset identifier to observe. example: `ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.monkey-sip09::monkeys`
// `actions` mandatory argument admits:
// - array of string type constrained to `mint`, `transfer` and `burn` values. example: ["mint", "burn"]
{
"if_this": {
"scope": "nft_event",
"asset_identifier": "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.monkey-sip09::monkeys",
"actions": ["mint", "transfer", "burn"]
},
}
// Get any transaction moving STX tokens
// `actions` mandatory argument admits:
// - array of string type constrained to `mint`, `transfer` and `lock` values. example: ["mint", "lock"]
{
"if_this": {
"scope": "stx_event",
"asset_identifier": "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.monkey-sip09::monkeys",
"actions": ["transfer", "lock"]
},
}
// Get any transaction emitting given print events predicate
// `contract-identifier` mandatory argument admits:
// - string type, fully qualifying the contract to observe. example: `ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.monkey-sip09`
// `contains` mandatory argument admits:
// - string type, used for matching event
{
"if_this": {
"scope": "print_event",
"contract_identifier": "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.monkey-sip09",
"contains": "vault"
},
}
// Get any transaction including a contract deployment
// `deployer` mandatory argument admits:
// - string "*"
// - string encoding a valid STX address. example: "ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG"
{
"if_this": {
"scope": "contract_deployment",
"deployer": "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM"
},
}
// Get any transaction including a contract deployment implementing a given trait (coming soon)
// `implement-trait` mandatory argument admits:
// - string type, fully qualifying the trait's shape to observe. example: `ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.sip09-protocol`
{
"if_this": {
"scope": "contract_deployment",
"implement_trait": "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.sip09-protocol"
},
}
```
In terms of actions available, the following `then_that` constructs are supported:
```jsonc
// HTTP Post block / transaction payload to a given endpoint.
// `http_post` construct admits:
// - url (string type). Example: http://localhost:3000/api/v1/wrapBtc
// - authorization_header (string type). Secret to add to the request `authorization` header when posting payloads
{
"then_that": {
"http_post": {
"url": "http://localhost:3000/api/v1/wrapBtc",
"authorization_header": "Bearer cn389ncoiwuencr"
}
}
}
// Append events to a file through filesystem. Convenient for local tests.
// `file_append` construct admits:
// - path (string type). Path to file on disk.
{
"then_that": {
"file_append": {
"path": "/tmp/events.json",
}
}
}
```
Additional configuration knobs available:
```jsonc
// Ignore any block prior to given block:
"start_block": 101
// Ignore any block after given block:
"end_block": 201
// Stop evaluating chainhook after a given number of occurrences found:
"expire_after_occurrence": 1
// Include decoded clarity values in payload
"decode_clarity_values": true
```
Putting all the pieces together:
```jsonc
// Retrieve and HTTP Post to `http://localhost:3000/api/v1/wrapBtc`
// the 5 first transactions interacting with ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.monkey-sip09,
// emitting print events containing the word 'vault'.
{
"chain": "stacks",
"uuid": "1",
"name": "Lorem ipsum",
"version": 1,
"networks": {
"testnet": {
"if_this": {
"scope": "print_event",
"contract_identifier": "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.monkey-sip09",
"contains": "vault"
},
"then_that": {
"http_post": {
"url": "http://localhost:3000/api/v1/vaults",
"authorization_header": "Bearer cn389ncoiwuencr"
}
},
"start_block": 10200,
"expire_after_occurrence": 5,
}
}
}
// A specification file can also include different networks.
// In this case, the chainhook will select the predicate
// corresponding to the network it was launched against.
{
"chain": "stacks",
"uuid": "1",
"name": "Lorem ipsum",
"version": 1,
"networks": {
"testnet": {
"if_this": {
"scope": "print_event",
"contract_identifier": "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.monkey-sip09",
"contains": "vault"
},
"then_that": {
"http_post": {
"url": "http://localhost:3000/api/v1/vaults",
"authorization_header": "Bearer cn389ncoiwuencr"
}
},
"start_block": 10200,
"expire_after_occurrence": 5,
},
"mainnet": {
"if_this": {
"scope": "print_event",
"contract_identifier": "SP456HQKV0RJXZFY1DGX8MNSNYVE3VGZJSRT459863.monkey-sip09",
"contains": "vault"
},
"then_that": {
"http_post": {
"url": "http://my-protocol.xyz/api/v1/vaults",
"authorization_header": "Bearer cn389ncoiwuencr"
}
},
"start_block": 90232,
"expire_after_occurrence": 5,
}
}
}
```
### Guide to local Stacks testnet / mainnet predicate scanning
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:
```bash
$ chainhook-cli start --devnet
$ chainhook predicates scan ./path/to/predicate.json --testnet
```
## Predicates available
Tbe first time this command run, a chainstate archive will be downloaded, uncompressed and written to disk (aronud 3GB required for testnet, 10GB for mainnet).
### Bitcoin
The subsequent scans will use the cached chainstate if already present, speeding up iterations and the overall feedback loop.
```yaml
# Get any transaction matching a given txid
# `txid` mandatory argument admits:
# - 32 bytes hex encoded type. example: "0xfaaac1833dc4883e7ec28f61e35b41f896c395f8d288b1a177155de2abd6052f"
predicate:
txid: 0xfaaac1833dc4883e7ec28f61e35b41f896c395f8d288b1a177155de2abd6052f
---
## Running `chainhook` in production mode
# Get any transaction including an OP_RETURN output starting with a set of characters.
# `starts-with` mandatory argument admits:
# - ASCII string type. example: `X2[`
# - hex encoded bytes. example: `0x589403`
predicate:
scope: outputs
op-return:
starts-with: X2[
# Get any transaction including an OP_RETURN output matching the sequence of bytes specified
# `equals` mandatory argument admits:
# - hex encoded bytes. example: `0x589403`
predicate:
scope: outputs
op-return:
equals: 0x69bd04208265aca9424d0337dac7d9e84371a2c91ece1891d67d3554bd9fdbe60afc6924d4b0773d90000006700010000006600012
# Get any transaction including an OP_RETURN output ending with a set of characters
# `ends-with` mandatory argument admits:
# - ASCII string type. example: `X2[`
# - hex encoded bytes. example: `0x589403`
predicate:
scope: outputs
op-return:
ends-with: 0x76a914000000000000000000000000000000000000000088ac
# Get any transaction including a p2pkh output paying a given recipient
# `p2pkh` construct admits:
# - string type. example: "mr1iPkD9N3RJZZxXRk7xF9d36gffa6exNC"
# - hex encoded bytes type. example: "0x76a914ee9369fb719c0ba43ddf4d94638a970b84775f4788ac"
predicate:
scope: outputs
p2pkh: mr1iPkD9N3RJZZxXRk7xF9d36gffa6exNC
# Get any transaction including a p2sh output paying a given recipient
# `p2sh` construct admits:
# - string type. example: "2MxDJ723HBJtEMa2a9vcsns4qztxBuC8Zb2"
# - hex encoded bytes type. example: "0x76a914ee9369fb719c0ba43ddf4d94638a970b84775f4788ac"
predicate:
scope: outputs
p2sh: 2MxDJ723HBJtEMa2a9vcsns4qztxBuC8Zb2
# Get any transaction including a p2wpkh output paying a given recipient
# `p2wpkh` construct admits:
# - string type. example: "bcrt1qnxknq3wqtphv7sfwy07m7e4sr6ut9yt6ed99jg"
predicate:
scope: outputs
p2wpkh: bcrt1qnxknq3wqtphv7sfwy07m7e4sr6ut9yt6ed99jg
# Get any transaction including a p2wsh output paying a given recipient
# `p2wsh` construct admits:
# - string type. example: "bc1qklpmx03a8qkv263gy8te36w0z9yafxplc5kwzc"
predicate:
scope: outputs
p2wsh: bc1qklpmx03a8qkv263gy8te36w0z9yafxplc5kwzc
# Get any transaction including a Stacks Proof of Burn commitment
predicate:
protocol: stacks
operation: pob_commit
# Get any transaction including a Stacks Proof of Transfer commitment
predicate:
protocol: stacks
operation: pox_commit
# Get any transaction including a key registration operation
predicate:
protocol: stacks
operation: key_registration
# Get any transaction including a STX transfer operation
predicate:
protocol: stacks
operation: stx_transfer
# Get any transaction including a STX lock operation
predicate:
protocol: stacks
operation: stx_lock
# Get any transaction including a new Ordinal inscription
predicate:
protocol: ordinal
operation: inscription_revealed
```
### Stacks
```yaml
# Get any transaction matching a given txid
# `txid` mandatory argument admits:
# - 32 bytes hex encoded type. example: "0xfaaac1833dc4883e7ec28f61e35b41f896c395f8d288b1a177155de2abd6052f"
predicate:
txid: 0xfaaac1833dc4883e7ec28f61e35b41f896c395f8d288b1a177155de2abd6052f
# Get any transaction related to a given fungible token asset identifier
# `asset-identifier` mandatory argument admits:
# - string type, fully qualifying the asset identifier to observe. example: `ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.cbtc-sip10::cbtc`
# `actions` mandatory argument admits:
# - array of string type constrained to `mint`, `transfer` and `burn` values. example: ["mint", "burn"]
predicate:
ft-event:
asset-identifier: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.cbtc-sip10::cbtc'
actions:
- mint
- burn
# Get any transaction related to a given non fungible token asset identifier
# `asset-identifier` mandatory argument admits:
# - string type, fully qualifying the asset identifier to observe. example: `ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.monkey-sip09::monkeys`
# `actions` mandatory argument admits:
# - array of string type constrained to `mint`, `transfer` and `burn` values. example: ["mint", "burn"]
predicate:
nft-event:
asset-identifier: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.monkey-sip09::monkeys'
actions:
- transfer
- burn
# Get any transaction moving STX tokens
# `actions` mandatory argument admits:
# - array of string type constrained to `mint`, `transfer` and `lock` values. example: ["mint", "lock"]
predicate:
stx-event:
actions:
- mint
- lock
# Get any transaction emitting given print events predicate
# `contract-identifier` mandatory argument admits:
# - string type, fully qualifying the contract to observe. example: `ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.monkey-sip09`
# `contains` mandatory argument admits:
# - string type, used for matching event
predicate:
print-event:
contract-identifier: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.monkey-sip09'
contains: "vault"
# Get any transaction including a contract deployment
# `deployer` mandatory argument admits:
# - string "*"
# - string encoding a valid STX address. example: "ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG"
predicate:
contract-deploy:
deployer: "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM"
# Get any transaction including a contract deployment implementing a given trait (coming soon)
# `impl-trait` mandatory argument admits:
# - string type, fully qualifying the trait's shape to observe. example: `ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.sip09-protocol`
predicate:
contract-deploy:
impl-trait: "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.sip09-protocol"
```
To be documented.

View File

@@ -13,7 +13,7 @@ serde_json = "1"
serde_derive = "1"
redis = "0.21.5"
serde-redis = "0.12.0"
chainhook-event-observer = { version = "=1.0.1", default-features = false, path = "../chainhook-event-observer" }
chainhook-event-observer = { version = "=1.0.3", default-features = false, path = "../chainhook-event-observer" }
chainhook-types = { version = "=1.0.1", path = "../chainhook-types-rs" }
clarinet-files = "1"
hiro-system-kit = "0.1.0"
@@ -43,6 +43,6 @@ hex = "0.4.3"
[features]
default = ["cli"]
cli = ["clap", "clap_generate", "toml", "ctrlc", "hiro_system_kit/log"]
debug = ["hiro_system_kit/debug"]
release = ["hiro_system_kit/release"]
cli = ["clap", "clap_generate", "toml", "ctrlc", "hiro-system-kit/log"]
debug = ["hiro-system-kit/debug"]
release = ["hiro-system-kit/release"]

View File

@@ -1,207 +0,0 @@
# chainhook-cli
## Usage
To get started, [build `clarinet` from source](https://github.com/hirosystems/clarinet#install-from-source-using-cargo), and then `cd components/chainhook-cli` and run `cargo install --path .` to install `chainhook-cli`.
Before running `chainhook-cli`, you need to [install redis](https://redis.io/docs/getting-started/installation/) and run a redis server locally.
### Start a Testnet node
```bash
$ chainhook-cli start --testnet
```
### Start a Mainnet node
```bash
$ chainhook-cli start --mainnet
```
### Start a Devnet node
```bash
$ chainhook-cli start --devnet
```
## Predicates available
### Bitcoin
```yaml
# Get any transaction matching a given txid
# `txid` mandatory argument admits:
# - 32 bytes hex encoded type. example: "0xfaaac1833dc4883e7ec28f61e35b41f896c395f8d288b1a177155de2abd6052f"
predicate:
txid: 0xfaaac1833dc4883e7ec28f61e35b41f896c395f8d288b1a177155de2abd6052f
# Get any transaction including an OP_RETURN output starting with a set of characters.
# `starts-with` mandatory argument admits:
# - ASCII string type. example: `X2[`
# - hex encoded bytes. example: `0x589403`
predicate:
scope: outputs
op-return:
starts-with: X2[
# Get any transaction including an OP_RETURN output matching the sequence of bytes specified
# `equals` mandatory argument admits:
# - hex encoded bytes. example: `0x589403`
predicate:
scope: outputs
op-return:
equals: 0x69bd04208265aca9424d0337dac7d9e84371a2c91ece1891d67d3554bd9fdbe60afc6924d4b0773d90000006700010000006600012
# Get any transaction including an OP_RETURN output ending with a set of characters
# `ends-with` mandatory argument admits:
# - ASCII string type. example: `X2[`
# - hex encoded bytes. example: `0x589403`
predicate:
scope: outputs
op-return:
ends-with: 0x76a914000000000000000000000000000000000000000088ac
# Get any transaction including a Stacks Proof of Burn commitment
predicate:
scope: outputs
stacks-op:
type: pob-commit
# Get any transaction including a Stacks Proof of Transfer commitment
# `recipients` mandatory argument admits:
# - string "*"
# - array of strings type. example: ["mr1iPkD9N3RJZZxXRk7xF9d36gffa6exNC", "muYdXKmX9bByAueDe6KFfHd5Ff1gdN9ErG"]
# - array of hex encoded bytes type. example: ["76a914000000000000000000000000000000000000000088ac", "0x76a914ee9369fb719c0ba43ddf4d94638a970b84775f4788ac"]
predicate:
scope: outputs
stacks-op:
type: pox-commit
recipients: *
# Get any transaction including a key registration operation
predicate:
scope: outputs
stacks-op:
type: key-registration
# Get any transaction including a STX transfer operation
# `recipient` optional argument admits:
# - string encoding a valid STX address. example: "ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG"
# `sender` optional argument admits:
# - string type. example: "mr1iPkD9N3RJZZxXRk7xF9d36gffa6exNC"
# - hex encoded bytes type. example: "0x76a914ee9369fb719c0ba43ddf4d94638a970b84775f4788ac"
predicate:
scope: outputs
stacks-op:
type: stx-transfer
# Get any transaction including a STX lock operation
# `sender` optional argument admits:
# - string type. example: "mr1iPkD9N3RJZZxXRk7xF9d36gffa6exNC"
# - hex encoded bytes type. example: "0x76a914ee9369fb719c0ba43ddf4d94638a970b84775f4788ac"
predicate:
scope: outputs
stacks-op:
type: stx-lock
# Get any transaction including a p2pkh output paying a given recipient
# `p2pkh` construct admits:
# - string type. example: "mr1iPkD9N3RJZZxXRk7xF9d36gffa6exNC"
# - hex encoded bytes type. example: "0x76a914ee9369fb719c0ba43ddf4d94638a970b84775f4788ac"
predicate:
scope: outputs
p2pkh: mr1iPkD9N3RJZZxXRk7xF9d36gffa6exNC
# Get any transaction including a p2sh output paying a given recipient
# `p2sh` construct admits:
# - string type. example: "2MxDJ723HBJtEMa2a9vcsns4qztxBuC8Zb2"
# - hex encoded bytes type. example: "0x76a914ee9369fb719c0ba43ddf4d94638a970b84775f4788ac"
predicate:
scope: outputs
p2sh: 2MxDJ723HBJtEMa2a9vcsns4qztxBuC8Zb2
# Get any transaction including a p2wpkh output paying a given recipient
# `p2wpkh` construct admits:
# - string type. example: "bcrt1qnxknq3wqtphv7sfwy07m7e4sr6ut9yt6ed99jg"
predicate:
scope: outputs
p2wpkh: bcrt1qnxknq3wqtphv7sfwy07m7e4sr6ut9yt6ed99jg
# Get any transaction including a p2wsh output paying a given recipient
# `p2wsh` construct admits:
# - string type. example: "bc1qklpmx03a8qkv263gy8te36w0z9yafxplc5kwzc"
predicate:
scope: outputs
p2wsh: bc1qklpmx03a8qkv263gy8te36w0z9yafxplc5kwzc
# Additional predicates including support for taproot coming soon
```
### Stacks
```yaml
# Get any transaction matching a given txid
# `txid` mandatory argument admits:
# - 32 bytes hex encoded type. example: "0xfaaac1833dc4883e7ec28f61e35b41f896c395f8d288b1a177155de2abd6052f"
predicate:
txid: 0xfaaac1833dc4883e7ec28f61e35b41f896c395f8d288b1a177155de2abd6052f
# Get any transaction related to a given fungible token asset identifier
# `asset-identifier` mandatory argument admits:
# - string type, fully qualifying the asset identifier to observe. example: `ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.cbtc-sip10::cbtc`
# `actions` mandatory argument admits:
# - array of string type constrained to `mint`, `transfer` and `burn` values. example: ["mint", "burn"]
predicate:
ft-event:
asset-identifier: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.cbtc-sip10::cbtc'
actions:
- mint
- burn
# Get any transaction related to a given non fungible token asset identifier
# `asset-identifier` mandatory argument admits:
# - string type, fully qualifying the asset identifier to observe. example: `ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.monkey-sip09::monkeys`
# `actions` mandatory argument admits:
# - array of string type constrained to `mint`, `transfer` and `burn` values. example: ["mint", "burn"]
predicate:
nft-event:
asset-identifier: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.monkey-sip09::monkeys'
actions:
- transfer
- burn
# Get any transaction moving STX tokens
# `actions` mandatory argument admits:
# - array of string type constrained to `mint`, `transfer` and `lock` values. example: ["mint", "lock"]
predicate:
stx-event:
actions:
- mint
- lock
# Get any transaction emitting given print events predicate
# `contract-identifier` mandatory argument admits:
# - string type, fully qualifying the contract to observe. example: `ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.monkey-sip09`
# `contains` mandatory argument admits:
# - string type, used for matching event
predicate:
print-event:
contract-identifier: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.monkey-sip09'
contains: "vault"
# Get any transaction including a contract deployment
# `deployer` mandatory argument admits:
# - string "*"
# - string encoding a valid STX address. example: "ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG"
predicate:
contract-deploy:
deployer: "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM"
# Get any transaction including a contract deployment implementing a given trait (coming soon)
# `impl-trait` mandatory argument admits:
# - string type, fully qualifying the trait's shape to observe. example: `ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.sip09-protocol`
predicate:
contract-deploy:
impl-trait: "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.sip09-protocol"
```

View File

@@ -2,36 +2,30 @@ use super::block;
use crate::archive;
use crate::block::DigestingCommand;
use crate::config::Config;
use crate::scan::bitcoin::scan_bitcoin_chain_with_predicate;
use crate::scan::stacks::scan_stacks_chain_with_predicate;
use chainhook_event_observer::chainhooks::bitcoin::{
handle_bitcoin_hook_action, BitcoinChainhookOccurrence, BitcoinTriggerChainhook,
};
use chainhook_event_observer::chainhooks::types::ChainhookConfig;
use chainhook_event_observer::indexer::bitcoin::standardize_bitcoin_block;
use chainhook_event_observer::chainhooks::types::{ChainhookConfig, ChainhookFullSpecification};
use chainhook_event_observer::observer::{
start_event_observer, EventObserverConfig, ObserverCommand, ObserverEvent,
};
use chainhook_event_observer::utils::Context;
use chainhook_event_observer::{
chainhooks::stacks::{
evaluate_stacks_transaction_predicate_on_transaction, handle_stacks_hook_action,
evaluate_stacks_predicate_on_transaction, handle_stacks_hook_action,
StacksChainhookOccurrence, StacksTriggerChainhook,
},
chainhooks::types::ChainhookSpecification,
};
use bitcoincore_rpc::{Auth, Client, RpcApi};
use chainhook_types::{
BitcoinBlockData, BitcoinBlockMetadata, BitcoinNetwork, BitcoinTransactionData,
BlockIdentifier, StacksBlockData, StacksBlockMetadata, StacksChainEvent, StacksNetwork,
StacksTransactionData,
BlockIdentifier, StacksBlockData, StacksBlockMetadata, StacksChainEvent, StacksTransactionData,
};
use clap::{Parser, Subcommand};
use ctrlc;
use hiro_system_kit;
use redis::{Commands, Connection};
use reqwest::Url;
use std::collections::HashSet;
use std::io::{BufReader, Read};
use std::sync::mpsc::Sender;
use std::{collections::HashMap, process, sync::mpsc::channel, thread};
@@ -53,9 +47,6 @@ enum Command {
/// Start chainhook-cli
#[clap(subcommand)]
Node(NodeCommand),
/// Start chainhook-cli in replay mode
#[clap(name = "replay", bin_name = "replay")]
Replay(ReplayCommand),
}
#[derive(Subcommand, PartialEq, Clone, Debug)]
@@ -86,32 +77,21 @@ struct NewPredicate {
#[derive(Parser, PartialEq, Clone, Debug)]
struct ScanPredicate {
pub devnet: bool,
/// Chainhook spec file to scan (json format)
pub predicate_path: String,
/// Target Testnet network
#[clap(
long = "testnet",
conflicts_with = "devnet",
conflicts_with = "mainnet"
)]
#[clap(long = "testnet", conflicts_with = "mainnet")]
pub testnet: bool,
/// Target Mainnet network
#[clap(
long = "mainnet",
conflicts_with = "testnet",
conflicts_with = "devnet"
)]
#[clap(long = "mainnet", conflicts_with = "testnet")]
pub mainnet: bool,
/// Load config file path
#[clap(
long = "config-path",
conflicts_with = "mainnet",
conflicts_with = "testnet",
conflicts_with = "devnet"
conflicts_with = "testnet"
)]
pub config_path: Option<String>,
/// Load chainhook file path (yaml format)
#[clap(long = "predicate-path", short = 'p')]
pub chainhook_spec_path: String,
}
#[derive(Subcommand, PartialEq, Clone, Debug)]
@@ -218,68 +198,62 @@ pub fn main() {
}
},
Command::Predicates(subcmd) => match subcmd {
PredicatesCommand::New(cmd) => {
// Predicates can either be generated manually by letting developers
// craft their own json payload, or using the interactive approach.
// A list of contracts is displayed, then list of methods, then list of events detected
// 3 files are generated:
// predicates/simnet/name.json
// predicates/devnet/name.json
// predicates/testnet/name.json
// predicates/mainnet/name.json
let manifest = clarinet_files::get_manifest_location(None);
PredicatesCommand::New(_cmd) => {
// let manifest = clarinet_files::get_manifest_location(None);
}
PredicatesCommand::Scan(cmd) => {
let config =
match Config::default(cmd.devnet, cmd.testnet, cmd.mainnet, &cmd.config_path) {
let mut config =
match Config::default(false, cmd.testnet, cmd.mainnet, &cmd.config_path) {
Ok(config) => config,
Err(e) => {
println!("{e}");
process::exit(1);
}
};
start_scan(config, ctx);
let file = std::fs::File::open(cmd.predicate_path).unwrap();
// .map_err(|e| format!("unable to read file {}\n{:?}", cmd.predicate_path, e))?;
let mut file_reader = BufReader::new(file);
let mut file_buffer = vec![];
file_reader.read_to_end(&mut file_buffer).unwrap();
// .map_err(|e| format!("unable to read file {}\n{:?}", file_path, e))?;
let predicate: ChainhookFullSpecification =
serde_json::from_slice(&file_buffer).unwrap();
match predicate {
ChainhookFullSpecification::Bitcoin(predicate) => {
match hiro_system_kit::nestable_block_on(scan_bitcoin_chain_with_predicate(
&predicate, true, &config, &ctx,
)) {
Ok(_) => {}
Err(e) => {
println!("{e}");
error!(ctx.expect_logger(), "{}", e);
process::exit(1);
}
};
}
ChainhookFullSpecification::Stacks(predicate) => {
match hiro_system_kit::nestable_block_on(scan_stacks_chain_with_predicate(
predicate,
true,
&mut config,
&ctx,
)) {
Ok(_) => {}
Err(e) => {
println!("{e}");
error!(ctx.expect_logger(), "{}", e);
process::exit(1);
}
};
}
}
}
},
Command::Replay(cmd) => {
let mut config =
match Config::default(cmd.devnet, cmd.testnet, cmd.mainnet, &cmd.config_path) {
Ok(config) => config,
Err(e) => {
println!("{e}");
process::exit(1);
}
};
if let Some(bitcoind_rpc_url) = cmd.bitcoind_rpc_url {
let url = match Url::parse(&bitcoind_rpc_url) {
Ok(url) => url,
Err(e) => {
println!("{e}");
process::exit(1);
}
};
let host = url
.host()
.expect("unable to retrieve host from bitcoind_rpc_url")
.to_string();
let port = url
.port()
.expect("unable to retrieve port from bitcoind_rpc_url")
.to_string();
let username = url.username().to_string();
let password = url
.password()
.expect("unable to retrieve password from bitcoind_rpc_url")
.to_string();
config.network.bitcoin_node_rpc_url = format!("http://{}:{}", host, port);
config.network.bitcoin_node_rpc_username = username;
config.network.bitcoin_node_rpc_password = password;
}
start_replay(config, cmd.apply_trigger, ctx);
}
}
}
#[allow(dead_code)]
pub fn install_ctrlc_handler(terminate_tx: Sender<DigestingCommand>, ctx: Context) {
ctrlc::set_handler(move || {
warn!(&ctx.expect_logger(), "Manual interruption signal received");
@@ -290,6 +264,7 @@ pub fn install_ctrlc_handler(terminate_tx: Sender<DigestingCommand>, ctx: Contex
.expect("Error setting Ctrl-C handler");
}
#[allow(dead_code)]
pub fn download_dataset_if_required(config: &mut Config, ctx: &Context) -> bool {
if config.is_initial_ingestion_required() {
// Download default tsv.
@@ -323,511 +298,6 @@ pub fn download_dataset_if_required(config: &mut Config, ctx: &Context) -> bool
}
}
pub fn start_scan(mut config: Config, ctx: Context) {
let (digestion_tx, digestion_rx) = channel();
install_ctrlc_handler(digestion_tx.clone(), ctx.clone());
let data_downloaded = download_dataset_if_required(&mut config, &ctx);
if !data_downloaded {
error!(ctx.expect_logger(), "No dataset to scan");
process::exit(1);
}
info!(ctx.expect_logger(), "Scanning...");
}
pub fn start_replay(mut config: Config, apply: bool, ctx: Context) {
let indexer_config = config.network.clone();
let (digestion_tx, digestion_rx) = channel();
let (observer_event_tx, observer_event_rx) = crossbeam_channel::unbounded();
let (observer_command_tx, observer_command_rx) = channel();
let terminate_digestion_tx = digestion_tx.clone();
let context_cloned = ctx.clone();
ctrlc::set_handler(move || {
warn!(
&context_cloned.expect_logger(),
"Manual interruption signal received"
);
terminate_digestion_tx
.send(DigestingCommand::Kill)
.expect("Unable to terminate service");
})
.expect("Error setting Ctrl-C handler");
if config.is_initial_ingestion_required() {
// Download default tsv.
if config.rely_on_remote_tsv() && config.should_download_remote_tsv() {
let url = config.expected_remote_tsv_url();
let mut destination_path = config.expected_cache_path();
destination_path.push("stacks-node-events.tsv");
// Download archive if not already present in cache
if !destination_path.exists() {
info!(ctx.expect_logger(), "Downloading {}", url);
match hiro_system_kit::nestable_block_on(archive::download_tsv_file(&config)) {
Ok(_) => {}
Err(e) => {
error!(ctx.expect_logger(), "{}", e);
process::exit(1);
}
}
let mut destination_path = config.expected_cache_path();
destination_path.push("stacks-node-events.tsv");
}
config.add_local_tsv_source(&destination_path);
let ingestion_config = config.clone();
let seed_digestion_tx = digestion_tx.clone();
let context_cloned = ctx.clone();
thread::spawn(move || {
let res = block::ingestion::start(
seed_digestion_tx.clone(),
&ingestion_config,
context_cloned.clone(),
);
let (_stacks_chain_tip, _bitcoin_chain_tip) = match res {
Ok(chain_tips) => chain_tips,
Err(e) => {
error!(&context_cloned.expect_logger(), "{}", e);
process::exit(1);
}
};
});
}
} else {
info!(
ctx.expect_logger(),
"Streaming blocks from stacks-node {}",
config.expected_stacks_node_event_source()
);
}
let digestion_config = config.clone();
let terminate_observer_command_tx = observer_command_tx.clone();
let context_cloned = ctx.clone();
thread::spawn(move || {
let res = block::digestion::start(digestion_rx, &digestion_config, &context_cloned);
if let Err(e) = res {
crit!(&context_cloned.expect_logger(), "{}", e);
}
let _ = terminate_observer_command_tx.send(ObserverCommand::Terminate);
});
let event_observer_config = EventObserverConfig {
normalization_enabled: true,
grpc_server_enabled: false,
hooks_enabled: true,
bitcoin_rpc_proxy_enabled: true,
event_handlers: vec![],
chainhook_config: None,
ingestion_port: DEFAULT_INGESTION_PORT,
control_port: DEFAULT_CONTROL_PORT,
bitcoin_node_username: config.network.bitcoin_node_rpc_username.clone(),
bitcoin_node_password: config.network.bitcoin_node_rpc_password.clone(),
bitcoin_node_rpc_url: config.network.bitcoin_node_rpc_url.clone(),
stacks_node_rpc_url: config.network.stacks_node_rpc_url.clone(),
operators: HashSet::new(),
display_logs: false,
};
info!(
ctx.expect_logger(),
"Listening for new blockchain events on port {}", DEFAULT_INGESTION_PORT
);
info!(
ctx.expect_logger(),
"Listening for chainhook predicate registrations on port {}", DEFAULT_CONTROL_PORT
);
let context_cloned = ctx.clone();
let _ = std::thread::spawn(move || {
let future = start_event_observer(
event_observer_config,
observer_command_tx,
observer_command_rx,
Some(observer_event_tx),
context_cloned,
);
let _ = hiro_system_kit::nestable_block_on(future);
});
let redis_config = config.expected_redis_config();
let client = redis::Client::open(redis_config.uri.clone()).unwrap();
let mut redis_con = match client.get_connection() {
Ok(con) => con,
Err(message) => {
crit!(ctx.expect_logger(), "Redis: {}", message.to_string());
panic!();
}
};
let auth = Auth::UserPass(
config.network.bitcoin_node_rpc_username.clone(),
config.network.bitcoin_node_rpc_password.clone(),
);
let bitcoin_rpc = match Client::new(&config.network.bitcoin_node_rpc_url, auth) {
Ok(con) => con,
Err(message) => {
crit!(ctx.expect_logger(), "Bitcoin RPC: {}", message.to_string());
panic!();
}
};
loop {
let event = match observer_event_rx.recv() {
Ok(cmd) => cmd,
Err(e) => {
crit!(
ctx.expect_logger(),
"Error: broken channel {}",
e.to_string()
);
break;
}
};
match event {
ObserverEvent::HookRegistered(chainhook) => {
// If start block specified, use it.
// I no start block specified, depending on the nature the hook, we'd like to retrieve:
// - contract-id
match chainhook {
ChainhookSpecification::Stacks(stacks_hook) => {
// Retrieve highest block height stored
let tip_height: u64 = redis_con
.get(&format!("stx:tip"))
.expect("unable to retrieve tip height");
let start_block = stacks_hook.start_block.unwrap_or(2); // TODO(lgalabru): handle STX hooks and genesis block :s
let end_block = stacks_hook.end_block.unwrap_or(tip_height); // TODO(lgalabru): handle STX hooks and genesis block :s
info!(
ctx.expect_logger(), "Processing Stacks chainhook {}, will scan blocks [{}; {}] (apply = {})",
stacks_hook.uuid, start_block, end_block, apply
);
let mut total_hits = vec![];
for cursor in start_block..=end_block {
debug!(
ctx.expect_logger(),
"Evaluating predicate #{} on block #{}", stacks_hook.uuid, cursor
);
let (
block_identifier,
parent_block_identifier,
timestamp,
transactions,
metadata,
) = {
let payload: Vec<String> = redis_con
.hget(
&format!("stx:{}", cursor),
&[
"block_identifier",
"parent_block_identifier",
"timestamp",
"transactions",
"metadata",
],
)
.expect("unable to retrieve tip height");
if payload.len() != 5 {
warn!(
ctx.expect_logger(),
"Unable to retrieve full data for block #{}", cursor
);
continue;
}
(
serde_json::from_str::<BlockIdentifier>(&payload[0]).unwrap(),
serde_json::from_str::<BlockIdentifier>(&payload[1]).unwrap(),
serde_json::from_str::<i64>(&payload[2]).unwrap(),
serde_json::from_str::<Vec<StacksTransactionData>>(&payload[3])
.unwrap(),
serde_json::from_str::<StacksBlockMetadata>(&payload[4])
.unwrap(),
)
};
let mut hits = vec![];
for tx in transactions.iter() {
if evaluate_stacks_transaction_predicate_on_transaction(
&tx,
&stacks_hook,
&ctx,
) {
debug!(
ctx.expect_logger(),
"Action #{} triggered by transaction {} (block #{})",
stacks_hook.uuid,
tx.transaction_identifier.hash,
cursor
);
hits.push(tx);
total_hits.push(tx.transaction_identifier.hash.to_string());
}
}
if hits.len() > 0 {
let block = StacksBlockData {
block_identifier,
parent_block_identifier,
timestamp,
transactions: vec![],
metadata,
};
let trigger = StacksTriggerChainhook {
chainhook: &stacks_hook,
apply: vec![(hits, &block)],
rollback: vec![],
};
let proofs = HashMap::new();
if apply {
if let Some(result) =
handle_stacks_hook_action(trigger, &proofs, &ctx)
{
if let StacksChainhookOccurrence::Http(request) = result {
hiro_system_kit::nestable_block_on(request.send())
.unwrap();
}
}
}
}
}
info!(ctx.expect_logger(), "Stacks chainhook {} scan completed and triggered by {} transactions {}", stacks_hook.uuid, total_hits.len(), total_hits.join(","))
}
ChainhookSpecification::Bitcoin(bitcoin_hook) => {
let start_block = match bitcoin_hook.start_block {
Some(start_block) => start_block,
None => {
warn!(ctx.expect_logger(), "Bitcoin chainhook specification must include a field start_block in replay mode");
continue;
}
};
let tip_height = match bitcoin_rpc.get_blockchain_info() {
Ok(result) => result.blocks,
Err(e) => {
warn!(
ctx.expect_logger(),
"unable to retrieve Bitcoin chain tip ({})",
e.to_string()
);
continue;
}
};
let end_block = bitcoin_hook.end_block.unwrap_or(tip_height);
info!(
ctx.expect_logger(), "Processing Bitcoin chainhook {}, will scan blocks [{}; {}] (apply = {})",
bitcoin_hook.uuid, start_block, end_block, apply
);
let mut total_hits = vec![];
for cursor in start_block..=end_block {
debug!(
ctx.expect_logger(),
"Evaluating predicate #{} on block #{}", bitcoin_hook.uuid, cursor
);
// Try to retrieve block from cache
let cached_block = {
let payload: Vec<String> = redis_con
.hget(
&format!("btc:{}", cursor),
&[
"block_identifier",
"parent_block_identifier",
"timestamp",
"transactions",
"metadata",
],
)
.expect("unable to retrieve tip height");
if payload.len() != 5 {
None
} else {
let block = BitcoinBlockData {
block_identifier: serde_json::from_str::<BlockIdentifier>(
&payload[0],
)
.unwrap(),
parent_block_identifier: serde_json::from_str::<
BlockIdentifier,
>(
&payload[1]
)
.unwrap(),
timestamp: serde_json::from_str::<u32>(&payload[2])
.unwrap(),
transactions: serde_json::from_str::<
Vec<BitcoinTransactionData>,
>(
&payload[3]
)
.unwrap(),
metadata: serde_json::from_str::<BitcoinBlockMetadata>(
&payload[4],
)
.unwrap(),
};
debug!(
ctx.expect_logger(),
"Bitcoin block #{} retrieved from cache",
block.block_identifier.index
);
Some(block)
}
};
let block = match cached_block {
Some(block) => block,
None => {
let block_hash = match bitcoin_rpc.get_block_hash(cursor) {
Ok(block_hash) => block_hash,
Err(e) => {
error!(
ctx.expect_logger(),
"unable to retrieve block hash {}: {}",
cursor,
e.to_string()
);
continue;
}
};
let block = match bitcoin_rpc.get_block(&block_hash) {
Ok(block) => {
standardize_bitcoin_block(
&indexer_config,
cursor,
block,
&ctx,
)
.unwrap() // todo
}
Err(e) => {
error!(
ctx.expect_logger(),
"unable to retrieve block {}: {}",
cursor,
e.to_string()
);
continue;
}
};
let key = format!("btc:{}", block.block_identifier.index);
match redis_con.hset_multiple(
&key,
&[
(
"block_identifier",
json!(block.block_identifier).to_string(),
),
(
"parent_block_identifier",
json!(block.parent_block_identifier).to_string(),
),
("transactions", json!(block.transactions).to_string()),
("metadata", json!(block.metadata).to_string()),
("timestamp", json!(block.timestamp).to_string()),
],
) {
Ok(()) => {
debug!(
ctx.expect_logger(),
"Bitcoin block #{} saved to cache",
block.block_identifier.index
);
}
Err(e) => {
warn!(
ctx.expect_logger(),
"unable to keep block {key} in cache: {}",
e.to_string()
);
}
};
block
}
};
let mut hits = vec![];
for tx in block.transactions.iter() {
if bitcoin_hook.evaluate_transaction_predicate(&tx) {
debug!(
ctx.expect_logger(),
"Action #{} triggered by transaction {} (block #{})",
bitcoin_hook.uuid,
tx.transaction_identifier.hash,
cursor
);
hits.push(tx);
total_hits.push(tx.transaction_identifier.hash.to_string());
}
}
if hits.len() > 0 {
let trigger = BitcoinTriggerChainhook {
chainhook: &bitcoin_hook,
apply: vec![(hits, &block)],
rollback: vec![],
};
let proofs = HashMap::new();
if apply {
if let Some(result) =
handle_bitcoin_hook_action(trigger, &proofs)
{
if let BitcoinChainhookOccurrence::Http(request) = result {
hiro_system_kit::nestable_block_on(request.send())
.unwrap();
}
}
}
}
}
info!(ctx.expect_logger(), "Bitcoin chainhook {} scan completed and triggered by {} transactions {}", bitcoin_hook.uuid, total_hits.len(), total_hits.join(","))
}
}
}
ObserverEvent::BitcoinChainEvent(_chain_update) => {
debug!(ctx.expect_logger(), "Bitcoin update not stored");
}
ObserverEvent::StacksChainEvent(chain_event) => {
match &chain_event {
StacksChainEvent::ChainUpdatedWithBlocks(data) => {
update_storage_with_confirmed_stacks_blocks(
&mut redis_con,
&data.confirmed_blocks,
&ctx,
);
}
StacksChainEvent::ChainUpdatedWithReorg(data) => {
update_storage_with_confirmed_stacks_blocks(
&mut redis_con,
&data.confirmed_blocks,
&ctx,
);
}
StacksChainEvent::ChainUpdatedWithMicroblocks(_)
| StacksChainEvent::ChainUpdatedWithMicroblocksReorg(_) => {}
};
}
ObserverEvent::Terminate => {
break;
}
_ => {}
}
}
}
pub fn seed_storage() {}
pub fn start_node(mut config: Config, ctx: Context) {
let (digestion_tx, digestion_rx) = channel();
let (observer_event_tx, observer_event_rx) = crossbeam_channel::unbounded();
@@ -1009,7 +479,7 @@ pub fn start_node(mut config: Config, ctx: Context) {
// - contract-id
let chainhook_key = chainhook.key();
let mut history: Vec<u64> = vec![];
let history: Vec<u64> = vec![];
let res: Result<(), redis::RedisError> = redis_con.hset_multiple(
&chainhook_key,
&[
@@ -1081,11 +551,8 @@ pub fn start_node(mut config: Config, ctx: Context) {
};
let mut hits = vec![];
for tx in transactions.iter() {
if evaluate_stacks_transaction_predicate_on_transaction(
&tx,
&stacks_hook,
&ctx,
) {
if evaluate_stacks_predicate_on_transaction(&tx, &stacks_hook, &ctx)
{
debug!(
ctx.expect_logger(),
"Action #{} triggered by transaction {} (block #{})",

View File

@@ -13,6 +13,7 @@ pub mod archive;
pub mod block;
mod cli;
pub mod config;
pub mod scan;
fn main() {
cli::main();

View File

@@ -0,0 +1,171 @@
use crate::config::Config;
use bitcoincore_rpc::RpcApi;
use bitcoincore_rpc::{Auth, Client};
use chainhook_event_observer::chainhooks::types::BitcoinChainhookFullSpecification;
use chainhook_event_observer::indexer;
use chainhook_event_observer::utils::Context;
use std::time::Duration;
pub async fn scan_bitcoin_chain_with_predicate(
predicate: &BitcoinChainhookFullSpecification,
apply: bool,
config: &Config,
ctx: &Context,
) -> Result<(), String> {
let auth = Auth::UserPass(
config.network.bitcoin_node_rpc_username.clone(),
config.network.bitcoin_node_rpc_password.clone(),
);
let bitcoin_rpc = match Client::new(&config.network.bitcoin_node_rpc_url, auth) {
Ok(con) => con,
Err(message) => {
return Err(format!("Bitcoin RPC error: {}", message.to_string()));
}
};
let predicate_spec = match predicate.networks.get(&config.network.bitcoin_network) {
Some(predicate) => predicate,
None => {
return Err(format!(
"Specification missing for network {:?}",
config.network.bitcoin_network
));
}
};
let start_block = match predicate_spec.start_block {
Some(start_block) => start_block,
None => {
return Err(
"Bitcoin chainhook specification must include a field start_block in replay mode"
.into(),
);
}
};
let tip_height = match bitcoin_rpc.get_blockchain_info() {
Ok(result) => result.blocks,
Err(e) => {
return Err(format!(
"unable to retrieve Bitcoin chain tip ({})",
e.to_string()
));
}
};
let end_block = predicate_spec.end_block.unwrap_or(tip_height);
info!(
ctx.expect_logger(),
"Processing Bitcoin chainhook {}, will scan blocks [{}; {}] (apply = {})",
predicate.uuid,
start_block,
end_block,
apply
);
use reqwest::Client as HttpClient;
let mut total_hits = vec![];
for cursor in start_block..=end_block {
debug!(
ctx.expect_logger(),
"Evaluating predicate #{} on block #{}", predicate.uuid, cursor
);
let body = json!({
"jsonrpc": "1.0",
"id": "chainhook-cli",
"method": "getblockhash",
"params": [cursor]
});
let http_client = HttpClient::builder()
.timeout(Duration::from_secs(20))
.build()
.expect("Unable to build http client");
let block_hash = http_client
.post(&config.network.bitcoin_node_rpc_url)
.basic_auth(
&config.network.bitcoin_node_rpc_username,
Some(&config.network.bitcoin_node_rpc_password),
)
.header("Content-Type", "application/json")
.header("Host", &config.network.bitcoin_node_rpc_url[7..])
.json(&body)
.send()
.await
.map_err(|e| format!("unable to send request ({})", e))?
.json::<bitcoincore_rpc::jsonrpc::Response>()
.await
.map_err(|e| format!("unable to parse response ({})", e))?
.result::<String>()
.map_err(|e| format!("unable to parse response ({})", e))?;
let body = json!({
"jsonrpc": "1.0",
"id": "chainhook-cli",
"method": "getblock",
"params": [block_hash, 2]
});
let http_client = HttpClient::builder()
.timeout(Duration::from_secs(20))
.build()
.expect("Unable to build http client");
let raw_block = http_client
.post(&config.network.bitcoin_node_rpc_url)
.basic_auth(
&config.network.bitcoin_node_rpc_username,
Some(&config.network.bitcoin_node_rpc_password),
)
.header("Content-Type", "application/json")
.header("Host", &config.network.bitcoin_node_rpc_url[7..])
.json(&body)
.send()
.await
.map_err(|e| format!("unable to send request ({})", e))?
.json::<bitcoincore_rpc::jsonrpc::Response>()
.await
.map_err(|e| format!("unable to parse response ({})", e))?
.result::<indexer::bitcoin::Block>()
.map_err(|e| format!("unable to parse response ({})", e))?;
let block =
indexer::bitcoin::standardize_bitcoin_block(&config.network, cursor, raw_block, ctx)?;
let mut hits = vec![];
for tx in block.transactions.iter() {
if predicate_spec.predicate.evaluate_transaction_predicate(&tx) {
info!(
ctx.expect_logger(),
"Action #{} triggered by transaction {} (block #{})",
predicate.uuid,
tx.transaction_identifier.hash,
cursor
);
hits.push(tx);
total_hits.push(tx.transaction_identifier.hash.to_string());
}
}
if hits.len() > 0 {
if apply {
// let trigger = BitcoinTriggerChainhook {
// chainhook: &predicate,
// apply: vec![(hits, &block)],
// rollback: vec![],
// };
// let proofs = HashMap::new();
// if let Some(result) =
// handle_bitcoin_hook_action(trigger, &proofs)
// {
// if let BitcoinChainhookOccurrence::Http(request) = result {
// hiro_system_kit::nestable_block_on(request.send())
// .unwrap();
// }
// }
}
}
}
// info!(ctx.expect_logger(), "Bitcoin chainhook {} scan completed and triggered by {} transactions {}", predicate.uuid, total_hits.len(), total_hits.join(","))
Ok(())
}

View File

@@ -0,0 +1,2 @@
pub mod bitcoin;
pub mod stacks;

View File

@@ -0,0 +1,188 @@
use std::{
collections::{HashMap, VecDeque},
process,
};
use crate::{
archive,
block::ingestion::{Record, RecordKind},
config::Config,
};
use chainhook_event_observer::utils::AbstractStacksBlock;
use chainhook_event_observer::{
chainhooks::{
stacks::evaluate_stacks_chainhook_on_blocks, types::StacksChainhookFullSpecification,
},
indexer::{self, stacks::standardize_stacks_serialized_block_header, Indexer},
utils::Context,
};
use chainhook_types::BlockIdentifier;
pub async fn scan_stacks_chain_with_predicate(
predicate: StacksChainhookFullSpecification,
_apply: bool,
config: &mut Config,
ctx: &Context,
) -> Result<(), String> {
let selected_predicate =
predicate.into_selected_network_specification(&config.network.stacks_network)?;
let start_block = match selected_predicate.start_block {
Some(start_block) => start_block,
None => {
return Err(
"Chainhook specification must include fields 'start_block' and 'end_block' when using the scan command"
.into(),
);
}
};
let _ = download_dataset_if_required(config, ctx).await;
let seed_tsv_path = config.expected_local_tsv_file().clone();
let (record_tx, record_rx) = std::sync::mpsc::channel();
let _parsing_handle = std::thread::spawn(move || {
let mut reader_builder = csv::ReaderBuilder::default()
.has_headers(false)
.delimiter(b'\t')
.buffer_capacity(8 * (1 << 10))
.from_path(&seed_tsv_path)
.expect("unable to create csv reader");
for result in reader_builder.deserialize() {
// Notice that we need to provide a type hint for automatic
// deserialization.
let record: Record = result.unwrap();
match &record.kind {
RecordKind::StacksBlockReceived => {
match record_tx.send(Some(record)) {
Err(_e) => {
// Abord the traversal once the receiver closed
break;
}
_ => {}
}
}
// RecordKind::BitcoinBlockReceived => {
// let _ = bitcoin_record_tx.send(Some(record));
// }
// RecordKind::StacksMicroblockReceived => {
// let _ = stacks_record_tx.send(Some(record));
// },
_ => {}
};
}
let _ = record_tx.send(None);
});
let mut indexer = Indexer::new(config.network.clone());
let mut canonical_fork = {
let mut cursor = BlockIdentifier::default();
let mut dump = HashMap::new();
while let Ok(Some(record)) = record_rx.recv() {
let (block_identifier, parent_block_identifier) = match &record.kind {
RecordKind::StacksBlockReceived => {
match standardize_stacks_serialized_block_header(&record.raw_log) {
Ok(data) => data,
Err(e) => {
error!(ctx.expect_logger(), "{e}");
continue;
}
}
}
_ => unreachable!(),
};
if start_block > block_identifier.index {
continue;
}
if let Some(end_block) = selected_predicate.end_block {
if block_identifier.index > end_block {
break;
}
}
if block_identifier.index > cursor.index {
cursor = block_identifier.clone(); // todo(lgalabru)
}
dump.insert(block_identifier, (parent_block_identifier, record.raw_log));
}
let mut canonical_fork = VecDeque::new();
while cursor.index > 0 {
let (block_identifer, (parent_block_identifier, blob)) =
match dump.remove_entry(&cursor) {
Some(entry) => entry,
None => break,
};
cursor = parent_block_identifier.clone(); // todo(lgalabru)
canonical_fork.push_front((block_identifer, parent_block_identifier, blob));
}
canonical_fork
};
// let mut hits = vec![];
for (block_identifier, _parent_block_identifier, blob) in canonical_fork.drain(..) {
let block_data = match indexer::stacks::standardize_stacks_serialized_block(
&indexer.config,
&blob,
&mut indexer.stacks_context,
ctx,
) {
Ok(block) => block,
Err(e) => {
error!(&ctx.expect_logger(), "{e}");
continue;
}
};
let blocks: Vec<&dyn AbstractStacksBlock> = vec![&block_data];
let res = evaluate_stacks_chainhook_on_blocks(blocks, &selected_predicate, ctx);
if !res.is_empty() {
println!("Hit at block {}", block_identifier.index);
}
// hits.append(&mut res);
}
Ok(())
}
async fn download_dataset_if_required(config: &mut Config, ctx: &Context) -> bool {
if config.is_initial_ingestion_required() {
// Download default tsv.
if config.rely_on_remote_tsv() && config.should_download_remote_tsv() {
let url = config.expected_remote_tsv_url();
let mut destination_path = config.expected_cache_path();
destination_path.push(archive::default_tsv_file_path(
&config.network.stacks_network,
));
// Download archive if not already present in cache
if !destination_path.exists() {
info!(ctx.expect_logger(), "Downloading {}", url);
match archive::download_tsv_file(&config).await {
Ok(_) => {}
Err(e) => {
error!(ctx.expect_logger(), "{}", e);
process::exit(1);
}
}
}
config.add_local_tsv_source(&destination_path);
}
true
} else {
info!(
ctx.expect_logger(),
"Streaming blocks from stacks-node {}",
config.expected_stacks_node_event_source()
);
false
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "chainhook-event-observer"
version = "1.0.1"
version = "1.0.3"
description = "Stateless Transaction Indexing Engine for Stacks and Bitcoin"
license = "GPL-3.0"
edition = "2021"
@@ -13,7 +13,7 @@ serde_json = { version = "1", features = ["arbitrary_precision"] }
serde_derive = "1"
stacks-rpc-client = "1"
clarinet-utils = "1"
clarity-repl = "1"
clarity-repl = "=1.4.1"
hiro-system-kit = "0.1.0"
# stacks-rpc-client = { version = "1", path = "../../../clarinet/components/stacks-rpc-client" }
# clarinet-utils = { version = "1", path = "../../../clarinet/components/clarinet-utils" }

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
use super::types::{
BitcoinChainhookSpecification, BitcoinPredicateType, ExactMatchingRule, HookAction,
InputPredicate, MatchingRule, OrdinalOperations, OutputPredicate, Protocols, Scopes,
StacksOperations,
BitcoinChainhookFullSpecification, BitcoinChainhookSpecification, BitcoinPredicateType,
ExactMatchingRule, HookAction, InputPredicate, MatchingRule, OrdinalOperations,
OutputPredicate, Protocols, StacksOperations,
};
use base58::FromBase58;
use bitcoincore_rpc::bitcoin::blockdata::opcodes;
@@ -68,7 +68,7 @@ pub fn evaluate_bitcoin_chainhooks_on_chain_event<'a>(
for block in event.new_blocks.iter() {
let mut hits = vec![];
for tx in block.transactions.iter() {
if chainhook.evaluate_transaction_predicate(&tx) {
if chainhook.predicate.evaluate_transaction_predicate(&tx) {
hits.push(tx);
}
}
@@ -94,7 +94,7 @@ pub fn evaluate_bitcoin_chainhooks_on_chain_event<'a>(
for block in event.blocks_to_apply.iter() {
let mut hits = vec![];
for tx in block.transactions.iter() {
if chainhook.evaluate_transaction_predicate(&tx) {
if chainhook.predicate.evaluate_transaction_predicate(&tx) {
hits.push(tx);
}
}
@@ -105,7 +105,7 @@ pub fn evaluate_bitcoin_chainhooks_on_chain_event<'a>(
for block in event.blocks_to_rollback.iter() {
let mut hits = vec![];
for tx in block.transactions.iter() {
if chainhook.evaluate_transaction_predicate(&tx) {
if chainhook.predicate.evaluate_transaction_predicate(&tx) {
hits.push(tx);
}
}
@@ -195,10 +195,10 @@ pub fn handle_bitcoin_hook_action<'a>(
proofs: &HashMap<&'a TransactionIdentifier, String>,
) -> Option<BitcoinChainhookOccurrence> {
match &trigger.chainhook.action {
HookAction::Http(http) => {
HookAction::HttpPost(http) => {
let client = Client::builder().build().unwrap();
let host = format!("{}", http.url);
let method = Method::from_bytes(http.method.as_bytes()).unwrap();
let method = Method::POST;
let body =
serde_json::to_vec(&serialize_bitcoin_payload_to_json(trigger, proofs)).unwrap();
Some(BitcoinChainhookOccurrence::Http(
@@ -209,7 +209,7 @@ pub fn handle_bitcoin_hook_action<'a>(
.body(body),
))
}
HookAction::File(disk) => {
HookAction::FileAppend(disk) => {
let bytes =
serde_json::to_vec(&serialize_bitcoin_payload_to_json(trigger, proofs)).unwrap();
Some(BitcoinChainhookOccurrence::File(
@@ -251,16 +251,16 @@ pub fn handle_bitcoin_hook_action<'a>(
}
}
impl BitcoinChainhookSpecification {
impl BitcoinPredicateType {
pub fn evaluate_transaction_predicate(&self, tx: &BitcoinTransactionData) -> bool {
// TODO(lgalabru): follow-up on this implementation
match &self.predicate {
match &self {
BitcoinPredicateType::Block => true,
BitcoinPredicateType::Txid(ExactMatchingRule::Equals(txid)) => {
tx.transaction_identifier.hash.eq(txid)
}
BitcoinPredicateType::Scope(Scopes::Outputs(OutputPredicate::OpReturn(
MatchingRule::Equals(hex_bytes),
BitcoinPredicateType::Outputs(OutputPredicate::OpReturn(MatchingRule::Equals(
hex_bytes,
))) => {
for output in tx.metadata.outputs.iter() {
if output.script_pubkey.eq(hex_bytes) {
@@ -269,8 +269,8 @@ impl BitcoinChainhookSpecification {
}
false
}
BitcoinPredicateType::Scope(Scopes::Outputs(OutputPredicate::OpReturn(
MatchingRule::StartsWith(hex_bytes),
BitcoinPredicateType::Outputs(OutputPredicate::OpReturn(MatchingRule::StartsWith(
hex_bytes,
))) => {
for output in tx.metadata.outputs.iter() {
if output.script_pubkey.starts_with(hex_bytes) {
@@ -279,8 +279,8 @@ impl BitcoinChainhookSpecification {
}
false
}
BitcoinPredicateType::Scope(Scopes::Outputs(OutputPredicate::OpReturn(
MatchingRule::EndsWith(hex_bytes),
BitcoinPredicateType::Outputs(OutputPredicate::OpReturn(MatchingRule::EndsWith(
hex_bytes,
))) => {
for output in tx.metadata.outputs.iter() {
if output.script_pubkey.ends_with(hex_bytes) {
@@ -289,8 +289,8 @@ impl BitcoinChainhookSpecification {
}
false
}
BitcoinPredicateType::Scope(Scopes::Outputs(OutputPredicate::P2pkh(
ExactMatchingRule::Equals(address),
BitcoinPredicateType::Outputs(OutputPredicate::P2pkh(ExactMatchingRule::Equals(
address,
))) => {
let pubkey_hash = address
.from_base58()
@@ -310,8 +310,8 @@ impl BitcoinChainhookSpecification {
}
false
}
BitcoinPredicateType::Scope(Scopes::Outputs(OutputPredicate::P2sh(
ExactMatchingRule::Equals(address),
BitcoinPredicateType::Outputs(OutputPredicate::P2sh(ExactMatchingRule::Equals(
address,
))) => {
let script_hash = address
.from_base58()
@@ -329,11 +329,11 @@ impl BitcoinChainhookSpecification {
}
false
}
BitcoinPredicateType::Scope(Scopes::Outputs(OutputPredicate::P2wpkh(
ExactMatchingRule::Equals(encoded_address),
BitcoinPredicateType::Outputs(OutputPredicate::P2wpkh(ExactMatchingRule::Equals(
encoded_address,
)))
| BitcoinPredicateType::Scope(Scopes::Outputs(OutputPredicate::P2wsh(
ExactMatchingRule::Equals(encoded_address),
| BitcoinPredicateType::Outputs(OutputPredicate::P2wsh(ExactMatchingRule::Equals(
encoded_address,
))) => {
let address = match Address::from_str(encoded_address) {
Ok(address) => match address.payload {
@@ -353,7 +353,7 @@ impl BitcoinChainhookSpecification {
}
false
}
BitcoinPredicateType::Scope(Scopes::Inputs(InputPredicate::Txid(predicate))) => {
BitcoinPredicateType::Inputs(InputPredicate::Txid(predicate)) => {
// TODO(lgalabru): add support for transaction chainhing, if enabled
for input in tx.metadata.inputs.iter() {
if input.previous_output.txid.eq(&predicate.txid)
@@ -364,7 +364,7 @@ impl BitcoinChainhookSpecification {
}
false
}
BitcoinPredicateType::Scope(Scopes::Inputs(InputPredicate::WitnessScript(_))) => {
BitcoinPredicateType::Inputs(InputPredicate::WitnessScript(_)) => {
// TODO(lgalabru)
unimplemented!()
}

View File

@@ -1,8 +1,8 @@
use crate::utils::{AbstractStacksBlock, Context};
use super::types::{
HookAction, StacksChainhookSpecification, StacksContractDeploymentPredicate,
StacksTransactionFilterPredicate,
BlockIdentifierIndexRule, HookAction, StacksChainhookSpecification,
StacksContractDeploymentPredicate, StacksPredicate,
};
use chainhook_types::{
BlockIdentifier, StacksChainEvent, StacksTransactionData, StacksTransactionEvent,
@@ -204,7 +204,7 @@ pub fn evaluate_stacks_chainhooks_on_chain_event<'a>(
triggered_chainhooks
}
fn evaluate_stacks_chainhook_on_blocks<'a>(
pub fn evaluate_stacks_chainhook_on_blocks<'a>(
blocks: Vec<&'a dyn AbstractStacksBlock>,
chainhook: &'a StacksChainhookSpecification,
ctx: &Context,
@@ -212,10 +212,16 @@ fn evaluate_stacks_chainhook_on_blocks<'a>(
let mut occurrences = vec![];
for block in blocks {
let mut hits = vec![];
for tx in block.get_transactions().iter() {
if evaluate_stacks_transaction_predicate_on_transaction(tx, chainhook, ctx) {
if chainhook.is_predicate_targeting_block_header() {
for tx in block.get_transactions().iter() {
hits.push(tx);
}
} else {
for tx in block.get_transactions().iter() {
if evaluate_stacks_predicate_on_transaction(tx, chainhook, ctx) {
hits.push(tx);
}
}
}
if hits.len() > 0 {
occurrences.push((hits, block));
@@ -224,23 +230,49 @@ fn evaluate_stacks_chainhook_on_blocks<'a>(
occurrences
}
pub fn evaluate_stacks_transaction_predicate_on_transaction<'a>(
pub fn evaluate_stacks_predicate_on_block<'a>(
block: &'a dyn AbstractStacksBlock,
chainhook: &'a StacksChainhookSpecification,
_ctx: &Context,
) -> bool {
match &chainhook.predicate {
StacksPredicate::BlockIdentifierIndex(BlockIdentifierIndexRule::Between(a, b)) => {
block.get_identifier().index.gt(a) && block.get_identifier().index.lt(b)
}
StacksPredicate::BlockIdentifierIndex(BlockIdentifierIndexRule::HigherThan(a)) => {
block.get_identifier().index.gt(a)
}
StacksPredicate::BlockIdentifierIndex(BlockIdentifierIndexRule::LowerThan(a)) => {
block.get_identifier().index.lt(a)
}
StacksPredicate::BlockIdentifierIndex(BlockIdentifierIndexRule::Equals(a)) => {
block.get_identifier().index.eq(a)
}
StacksPredicate::ContractDeployment(_)
| StacksPredicate::ContractCall(_)
| StacksPredicate::FtEvent(_)
| StacksPredicate::NftEvent(_)
| StacksPredicate::StxEvent(_)
| StacksPredicate::PrintEvent(_)
| StacksPredicate::Txid(_) => unreachable!(),
}
}
pub fn evaluate_stacks_predicate_on_transaction<'a>(
transaction: &'a StacksTransactionData,
chainhook: &'a StacksChainhookSpecification,
ctx: &Context,
) -> bool {
match &chainhook.transaction_predicate {
StacksTransactionFilterPredicate::ContractDeployment(
StacksContractDeploymentPredicate::Principal(expected_deployer),
) => match &transaction.metadata.kind {
match &chainhook.predicate {
StacksPredicate::ContractDeployment(StacksContractDeploymentPredicate::Deployer(
expected_deployer,
)) => match &transaction.metadata.kind {
StacksTransactionKind::ContractDeployment(actual_deployment) => actual_deployment
.contract_identifier
.starts_with(expected_deployer),
_ => false,
},
StacksTransactionFilterPredicate::ContractDeployment(
StacksContractDeploymentPredicate::Trait(_expected_trait),
) => match &transaction.metadata.kind {
StacksPredicate::ContractDeployment(StacksContractDeploymentPredicate::ImplementSip09) => match &transaction.metadata.kind {
StacksTransactionKind::ContractDeployment(_actual_deployment) => {
ctx.try_log(|logger| {
slog::warn!(
@@ -252,20 +284,30 @@ pub fn evaluate_stacks_transaction_predicate_on_transaction<'a>(
}
_ => false,
},
StacksTransactionFilterPredicate::ContractCall(expected_contract_call) => {
match &transaction.metadata.kind {
StacksTransactionKind::ContractCall(actual_contract_call) => {
actual_contract_call
.contract_identifier
.eq(&expected_contract_call.contract_identifier)
&& actual_contract_call
.method
.eq(&expected_contract_call.method)
}
_ => false,
StacksPredicate::ContractDeployment(StacksContractDeploymentPredicate::ImplementSip10) => match &transaction.metadata.kind {
StacksTransactionKind::ContractDeployment(_actual_deployment) => {
ctx.try_log(|logger| {
slog::warn!(
logger,
"StacksContractDeploymentPredicate::Trait uninmplemented"
)
});
false
}
}
StacksTransactionFilterPredicate::FtEvent(expected_event) => {
_ => false,
},
StacksPredicate::ContractCall(expected_contract_call) => match &transaction.metadata.kind {
StacksTransactionKind::ContractCall(actual_contract_call) => {
actual_contract_call
.contract_identifier
.eq(&expected_contract_call.contract_identifier)
&& actual_contract_call
.method
.eq(&expected_contract_call.method)
}
_ => false,
},
StacksPredicate::FtEvent(expected_event) => {
let expecting_mint = expected_event.actions.contains(&"mint".to_string());
let expecting_transfer = expected_event.actions.contains(&"transfer".to_string());
let expecting_burn = expected_event.actions.contains(&"burn".to_string());
@@ -280,7 +322,7 @@ pub fn evaluate_stacks_transaction_predicate_on_transaction<'a>(
}
false
}
StacksTransactionFilterPredicate::NftEvent(expected_event) => {
StacksPredicate::NftEvent(expected_event) => {
let expecting_mint = expected_event.actions.contains(&"mint".to_string());
let expecting_transfer = expected_event.actions.contains(&"transfer".to_string());
let expecting_burn = expected_event.actions.contains(&"burn".to_string());
@@ -295,7 +337,7 @@ pub fn evaluate_stacks_transaction_predicate_on_transaction<'a>(
}
false
}
StacksTransactionFilterPredicate::StxEvent(expected_event) => {
StacksPredicate::StxEvent(expected_event) => {
let expecting_mint = expected_event.actions.contains(&"mint".to_string());
let expecting_transfer = expected_event.actions.contains(&"transfer".to_string());
let expecting_lock = expected_event.actions.contains(&"lock".to_string());
@@ -310,7 +352,7 @@ pub fn evaluate_stacks_transaction_predicate_on_transaction<'a>(
}
false
}
StacksTransactionFilterPredicate::PrintEvent(expected_event) => {
StacksPredicate::PrintEvent(expected_event) => {
for event in transaction.metadata.receipt.events.iter() {
match event {
StacksTransactionEvent::SmartContractEvent(actual) => {
@@ -327,9 +369,8 @@ pub fn evaluate_stacks_transaction_predicate_on_transaction<'a>(
}
false
}
StacksTransactionFilterPredicate::TransactionIdentifierHash(txid) => {
txid.eq(&transaction.transaction_identifier.hash)
}
StacksPredicate::Txid(txid) => txid.eq(&transaction.transaction_identifier.hash),
StacksPredicate::BlockIdentifierIndex(_) => unreachable!(),
}
}
@@ -622,8 +663,7 @@ pub fn serialize_stacks_payload_to_json<'a>(
}).collect::<Vec<_>>(),
"chainhook": {
"uuid": trigger.chainhook.uuid,
"transaction_predicate": trigger.chainhook.transaction_predicate,
"block_predicate": trigger.chainhook.transaction_predicate,
"predicate": trigger.chainhook.predicate,
}
})
}
@@ -634,10 +674,10 @@ pub fn handle_stacks_hook_action<'a>(
ctx: &Context,
) -> Option<StacksChainhookOccurrence> {
match &trigger.chainhook.action {
HookAction::Http(http) => {
HookAction::HttpPost(http) => {
let client = Client::builder().build().unwrap();
let host = format!("{}", http.url);
let method = Method::from_bytes(http.method.as_bytes()).unwrap();
let method = Method::POST;
let body = serde_json::to_vec(&serialize_stacks_payload_to_json(trigger, proofs, ctx))
.unwrap();
Some(StacksChainhookOccurrence::Http(
@@ -647,7 +687,7 @@ pub fn handle_stacks_hook_action<'a>(
.body(body),
))
}
HookAction::File(disk) => {
HookAction::FileAppend(disk) => {
let bytes = serde_json::to_vec(&serialize_stacks_payload_to_json(trigger, proofs, ctx))
.unwrap();
Some(StacksChainhookOccurrence::File(

View File

@@ -1,3 +1,5 @@
use std::collections::{BTreeMap, HashMap};
use clarity_repl::clarity::util::hash::hex_bytes;
use reqwest::Url;
use serde::ser::{SerializeSeq, Serializer};
@@ -25,14 +27,10 @@ impl ChainhookConfig {
pub fn get_serialized_stacks_predicates(
&self,
) -> Vec<(&String, &StacksNetwork, &StacksTransactionFilterPredicate)> {
) -> Vec<(&String, &StacksNetwork, &StacksPredicate)> {
let mut stacks = vec![];
for chainhook in self.stacks_chainhooks.iter() {
stacks.push((
&chainhook.uuid,
&chainhook.network,
&chainhook.transaction_predicate,
));
stacks.push((&chainhook.uuid, &chainhook.network, &chainhook.predicate));
}
stacks
}
@@ -193,22 +191,131 @@ pub struct BitcoinChainhookSpecification {
pub action: HookAction,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case", tag = "chain")]
pub enum ChainhookFullSpecification {
Bitcoin(BitcoinChainhookFullSpecification),
Stacks(StacksChainhookFullSpecification),
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
pub struct BitcoinChainhookFullSpecification {
pub uuid: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub owner_uuid: Option<String>,
pub name: String,
pub version: u32,
pub networks: BTreeMap<BitcoinNetwork, BitcoinChainhookNetworkSpecification>,
}
impl BitcoinChainhookFullSpecification {
pub fn into_selected_network_specification(
mut self,
network: &BitcoinNetwork,
) -> Result<BitcoinChainhookSpecification, String> {
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,
expire_after_occurrence: spec.expire_after_occurrence,
predicate: spec.predicate,
action: spec.action,
})
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
pub struct BitcoinChainhookNetworkSpecification {
#[serde(skip_serializing_if = "Option::is_none")]
pub start_block: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub end_block: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expire_after_occurrence: Option<u64>,
#[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<String>,
pub name: String,
pub version: u32,
pub networks: BTreeMap<StacksNetwork, StacksChainhookNetworkSpecification>,
}
impl StacksChainhookFullSpecification {
pub fn into_selected_network_specification(
mut self,
network: &StacksNetwork,
) -> Result<StacksChainhookSpecification, String> {
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,
capture_all_events: spec.capture_all_events,
decode_clarity_values: spec.decode_clarity_values,
expire_after_occurrence: spec.expire_after_occurrence,
predicate: spec.predicate,
action: spec.action,
})
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
pub struct StacksChainhookNetworkSpecification {
#[serde(skip_serializing_if = "Option::is_none")]
pub start_block: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub end_block: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expire_after_occurrence: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub capture_all_events: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub decode_clarity_values: Option<bool>,
#[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 {
Http(HttpHook),
File(FileHook),
HttpPost(HttpHook),
FileAppend(FileHook),
Noop,
}
impl HookAction {
pub fn validate(&self) -> Result<(), String> {
match &self {
HookAction::Http(spec) => {
HookAction::HttpPost(spec) => {
let _ = Url::parse(&spec.url)
.map_err(|e| format!("hook action url invalid ({})", e.to_string()))?;
}
HookAction::File(_) => {}
HookAction::FileAppend(_) => {}
HookAction::Noop => {}
}
Ok(())
@@ -219,7 +326,6 @@ impl HookAction {
#[serde(rename_all = "snake_case")]
pub struct HttpHook {
pub url: String,
pub method: String,
pub authorization_header: String,
}
@@ -286,11 +392,12 @@ impl BitcoinTransactionFilterPredicate {
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
#[serde(rename_all = "snake_case", tag = "scope")]
pub enum BitcoinPredicateType {
Block,
Txid(ExactMatchingRule),
Scope(Scopes),
Inputs(InputPredicate),
Outputs(OutputPredicate),
Protocol(Protocols),
}
@@ -299,7 +406,8 @@ impl BitcoinPredicateType {
match &self {
BitcoinPredicateType::Block => true,
BitcoinPredicateType::Txid(_rules) => true,
BitcoinPredicateType::Scope(_rules) => false,
BitcoinPredicateType::Inputs(_rules) => true,
BitcoinPredicateType::Outputs(_rules) => false,
BitcoinPredicateType::Protocol(_rules) => false,
}
}
@@ -308,7 +416,8 @@ impl BitcoinPredicateType {
match &self {
BitcoinPredicateType::Block => true,
BitcoinPredicateType::Txid(_rules) => true,
BitcoinPredicateType::Scope(_rules) => false,
BitcoinPredicateType::Inputs(_rules) => false,
BitcoinPredicateType::Outputs(_rules) => true,
BitcoinPredicateType::Protocol(_rules) => false,
}
}
@@ -317,19 +426,13 @@ impl BitcoinPredicateType {
match &self {
BitcoinPredicateType::Block => true,
BitcoinPredicateType::Txid(_rules) => true,
BitcoinPredicateType::Scope(_rules) => false,
BitcoinPredicateType::Inputs(_rules) => false,
BitcoinPredicateType::Outputs(_rules) => false,
BitcoinPredicateType::Protocol(_rules) => false,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Scopes {
Inputs(InputPredicate),
Outputs(OutputPredicate),
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum InputPredicate {
@@ -513,32 +616,38 @@ pub struct StacksChainhookSpecification {
#[serde(skip_serializing_if = "Option::is_none")]
pub decode_clarity_values: Option<bool>,
#[serde(rename = "predicate")]
pub transaction_predicate: StacksTransactionFilterPredicate,
pub block_predicate: Option<StacksBlockFilterPredicate>,
pub predicate: StacksPredicate,
pub action: HookAction,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
#[serde(tag = "type", content = "rule")]
pub enum StacksBlockFilterPredicate {
BlockIdentifierHash(BlockIdentifierHashRule),
BlockIdentifierIndex(BlockIdentifierIndexRule),
BitcoinBlockIdentifierHash(BlockIdentifierHashRule),
BitcoinBlockIdentifierIndex(BlockIdentifierHashRule),
impl StacksChainhookSpecification {
pub fn is_predicate_targeting_block_header(&self) -> bool {
match &self.predicate {
StacksPredicate::BlockIdentifierIndex(_)
// | StacksPredicate::BlockIdentifierHash(_)
// | StacksPredicate::BitcoinBlockIdentifierHash(_)
// | StacksPredicate::BitcoinBlockIdentifierIndex(_)
=> true,
_ => false,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
#[serde(tag = "type", content = "rule")]
pub enum StacksTransactionFilterPredicate {
#[serde(tag = "scope")]
pub enum StacksPredicate {
BlockIdentifierIndex(BlockIdentifierIndexRule),
// BlockIdentifierHash(BlockIdentifierHashRule),
// BitcoinBlockIdentifierHash(BlockIdentifierHashRule),
// BitcoinBlockIdentifierIndex(BlockIdentifierHashRule),
ContractDeployment(StacksContractDeploymentPredicate),
ContractCall(StacksContractCallBasedPredicate),
PrintEvent(StacksPrintEventBasedPredicate),
FtEvent(StacksFtEventBasedPredicate),
NftEvent(StacksNftEventBasedPredicate),
StxEvent(StacksStxEventBasedPredicate),
TransactionIdentifierHash(String),
Txid(String),
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
@@ -550,10 +659,11 @@ pub struct StacksContractCallBasedPredicate {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
#[serde(tag = "type", content = "rule")]
// #[serde(tag = "type", content = "rule")]
pub enum StacksContractDeploymentPredicate {
Principal(String),
Trait(String),
Deployer(String),
ImplementSip09,
ImplementSip10,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]

View File

@@ -113,12 +113,12 @@ pub fn standardize_bitcoin_block(
let expected_magic_bytes = get_stacks_canonical_magic_bytes(&indexer_config.bitcoin_network);
let pox_config = get_canonical_pox_config(&indexer_config.bitcoin_network);
ctx.try_log(|logger| slog::info!(logger, "Start processing Bitcoin block {}", block.hash,));
ctx.try_log(|logger| slog::debug!(logger, "Standardizing Bitcoin block {}", block.hash,));
for mut tx in block.tx.into_iter() {
let txid = tx.txid.to_string();
ctx.try_log(|logger| slog::info!(logger, "Start processing Bitcoin transaction {txid}"));
ctx.try_log(|logger| slog::debug!(logger, "Standardizing Bitcoin transaction {txid}"));
let mut stacks_operations = vec![];
if let Some(op) = try_parse_stacks_operation(

View File

@@ -1228,6 +1228,7 @@ pub fn handle_drop_mempool_tx(ctx: &State<Context>) -> Json<JsonValue> {
}))
}
#[openapi(skip)]
#[post("/attachments/new", format = "application/json")]
pub fn handle_new_attachement(ctx: &State<Context>) -> Json<JsonValue> {
ctx.try_log(|logger| slog::info!(logger, "POST /attachments/new"));

View File

@@ -1,8 +1,7 @@
use crate::chainhooks::types::{
BitcoinChainhookSpecification, BitcoinPredicateType, BitcoinTransactionFilterPredicate,
ChainhookConfig, ChainhookSpecification, ExactMatchingRule, HookAction, OutputPredicate, Scope,
Scopes, StacksChainhookSpecification, StacksContractCallBasedPredicate,
StacksTransactionFilterPredicate,
BitcoinChainhookSpecification, BitcoinPredicateType, ChainhookConfig, ChainhookSpecification,
ExactMatchingRule, HookAction, OutputPredicate, StacksChainhookSpecification,
StacksContractCallBasedPredicate, StacksPredicate,
};
use crate::indexer::tests::helpers::transactions::generate_test_tx_bitcoin_p2pkh_transfer;
use crate::indexer::tests::helpers::{
@@ -61,13 +60,10 @@ fn stacks_chainhook_contract_call(
start_block: None,
end_block: None,
expire_after_occurrence: None,
transaction_predicate: StacksTransactionFilterPredicate::ContractCall(
StacksContractCallBasedPredicate {
contract_identifier: contract_identifier.to_string(),
method: method.to_string(),
},
),
block_predicate: None,
predicate: StacksPredicate::ContractCall(StacksContractCallBasedPredicate {
contract_identifier: contract_identifier.to_string(),
method: method.to_string(),
}),
action: HookAction::Noop,
capture_all_events: None,
decode_clarity_values: Some(true),
@@ -89,9 +85,9 @@ fn bitcoin_chainhook_p2pkh(
start_block: None,
end_block: None,
expire_after_occurrence,
predicate: BitcoinPredicateType::Scope(Scopes::Outputs(OutputPredicate::P2pkh(
predicate: BitcoinPredicateType::Outputs(OutputPredicate::P2pkh(
ExactMatchingRule::Equals(address.to_string()),
))),
)),
action: HookAction::Noop,
};
spec

View File

@@ -645,7 +645,9 @@ pub struct StacksChainUpdatedWithMicroblocksReorgData {
pub microblocks_to_apply: Vec<StacksMicroblockData>,
}
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, JsonSchema)]
#[derive(
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum StacksNetwork {
Simnet,
@@ -708,7 +710,9 @@ impl StacksNetwork {
}
#[allow(dead_code)]
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, JsonSchema)]
#[derive(
Debug, PartialEq, Eq, Clone, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum BitcoinNetwork {
Regtest,