Skip to content

Commit fb19e39

Browse files
author
Ludo Galabru
committed
feat: ability to use file as action
1 parent 6dec536 commit fb19e39

File tree

5 files changed

+148
-62
lines changed

5 files changed

+148
-62
lines changed

components/chainhook-event-observer/src/chainhooks/mod.rs

Lines changed: 82 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use clarity_repl::clarity::util::hash::{hex_bytes, to_hex, Hash160};
1919
use clarity_repl::clarity::vm::types::{CharType, SequenceData, Value as ClarityValue};
2020
use reqwest::{Client, Method};
2121
use serde::Serialize;
22+
use serde_json::Value as JsonValue;
2223
use std::collections::HashMap;
2324
use std::io::Cursor;
2425
use std::iter::Map;
@@ -415,6 +416,7 @@ pub struct BitcoinChainhookOccurrencePayload {
415416

416417
pub enum BitcoinChainhookOccurrence {
417418
Http(RequestBuilder),
419+
File(String, Vec<u8>),
418420
Data(BitcoinChainhookOccurrencePayload),
419421
}
420422

@@ -447,9 +449,37 @@ pub struct StacksChainhookOccurrencePayload {
447449
}
448450
pub enum StacksChainhookOccurrence {
449451
Http(RequestBuilder),
452+
File(String, Vec<u8>),
450453
Data(StacksChainhookOccurrencePayload),
451454
}
452455

456+
pub fn serialize_bitcoin_payload_to_json<'a>(
457+
trigger: BitcoinTriggerChainhook<'a>,
458+
proofs: &HashMap<&'a TransactionIdentifier, String>,
459+
) -> JsonValue {
460+
json!({
461+
"apply": trigger.apply.into_iter().map(|(transaction, block_identifier)| {
462+
json!({
463+
"transaction": transaction,
464+
"block_identifier": block_identifier,
465+
"confirmations": 1, // TODO(lgalabru)
466+
"proof": proofs.get(&transaction.transaction_identifier),
467+
})
468+
}).collect::<Vec<_>>(),
469+
"rollback": trigger.rollback.into_iter().map(|(transaction, block_identifier)| {
470+
json!({
471+
"transaction": transaction,
472+
"block_identifier": block_identifier,
473+
"confirmations": 1, // TODO(lgalabru)
474+
})
475+
}).collect::<Vec<_>>(),
476+
"chainhook": {
477+
"uuid": trigger.chainhook.uuid,
478+
"predicate": trigger.chainhook.predicate,
479+
}
480+
})
481+
}
482+
453483
pub fn handle_bitcoin_hook_action<'a>(
454484
trigger: BitcoinTriggerChainhook<'a>,
455485
proofs: &HashMap<&'a TransactionIdentifier, String>,
@@ -459,28 +489,8 @@ pub fn handle_bitcoin_hook_action<'a>(
459489
let client = Client::builder().build().unwrap();
460490
let host = format!("{}", http.url);
461491
let method = Method::from_bytes(http.method.as_bytes()).unwrap();
462-
let payload = json!({
463-
"apply": trigger.apply.into_iter().map(|(transaction, block_identifier)| {
464-
json!({
465-
"transaction": transaction,
466-
"block_identifier": block_identifier,
467-
"confirmations": 1, // TODO(lgalabru)
468-
"proof": proofs.get(&transaction.transaction_identifier),
469-
})
470-
}).collect::<Vec<_>>(),
471-
"rollback": trigger.rollback.into_iter().map(|(transaction, block_identifier)| {
472-
json!({
473-
"transaction": transaction,
474-
"block_identifier": block_identifier,
475-
"confirmations": 1, // TODO(lgalabru)
476-
})
477-
}).collect::<Vec<_>>(),
478-
"chainhook": {
479-
"uuid": trigger.chainhook.uuid,
480-
"predicate": trigger.chainhook.predicate,
481-
}
482-
});
483-
let body = serde_json::to_vec(&payload).unwrap();
492+
let body =
493+
serde_json::to_vec(&serialize_bitcoin_payload_to_json(trigger, proofs)).unwrap();
484494
Some(BitcoinChainhookOccurrence::Http(
485495
client
486496
.request(method, &host)
@@ -489,6 +499,14 @@ pub fn handle_bitcoin_hook_action<'a>(
489499
.body(body),
490500
))
491501
}
502+
HookAction::File(disk) => {
503+
let bytes =
504+
serde_json::to_vec(&serialize_bitcoin_payload_to_json(trigger, proofs)).unwrap();
505+
Some(BitcoinChainhookOccurrence::File(
506+
disk.path.to_string(),
507+
bytes,
508+
))
509+
}
492510
HookAction::Noop => Some(BitcoinChainhookOccurrence::Data(
493511
BitcoinChainhookOccurrencePayload {
494512
apply: trigger
@@ -757,49 +775,64 @@ pub fn serialize_to_json(value: &ClarityValue) -> serde_json::Value {
757775
}
758776
}
759777

778+
pub fn serialize_stacks_payload_to_json<'a>(
779+
trigger: StacksTriggerChainhook<'a>,
780+
proofs: &HashMap<&'a TransactionIdentifier, String>,
781+
) -> JsonValue {
782+
let decode_clarity_values = trigger.should_decode_clarity_value();
783+
json!({
784+
"apply": trigger.apply.into_iter().map(|(transaction, block_identifier)| {
785+
json!({
786+
"transaction": if decode_clarity_values {
787+
encode_transaction_including_with_clarity_decoding(transaction)
788+
} else {
789+
json!(transaction)
790+
},
791+
"block_identifier": block_identifier,
792+
"confirmations": 1, // TODO(lgalabru)
793+
"proof": proofs.get(&transaction.transaction_identifier),
794+
})
795+
}).collect::<Vec<_>>(),
796+
"rollback": trigger.rollback.into_iter().map(|(transaction, block_identifier)| {
797+
json!({
798+
"transaction": transaction,
799+
"block_identifier": block_identifier,
800+
"confirmations": 1, // TODO(lgalabru)
801+
})
802+
}).collect::<Vec<_>>(),
803+
"chainhook": {
804+
"uuid": trigger.chainhook.uuid,
805+
"predicate": trigger.chainhook.predicate,
806+
}
807+
})
808+
}
809+
760810
pub fn handle_stacks_hook_action<'a>(
761811
trigger: StacksTriggerChainhook<'a>,
762812
proofs: &HashMap<&'a TransactionIdentifier, String>,
763813
) -> Option<StacksChainhookOccurrence> {
764-
let decode_clarity_values = trigger.should_decode_clarity_value();
765814
match &trigger.chainhook.action {
766815
HookAction::Http(http) => {
767816
let client = Client::builder().build().unwrap();
768817
let host = format!("{}", http.url);
769818
let method = Method::from_bytes(http.method.as_bytes()).unwrap();
770-
let payload = json!({
771-
"apply": trigger.apply.into_iter().map(|(transaction, block_identifier)| {
772-
json!({
773-
"transaction": if decode_clarity_values {
774-
encode_transaction_including_with_clarity_decoding(transaction)
775-
} else {
776-
json!(transaction)
777-
},
778-
"block_identifier": block_identifier,
779-
"confirmations": 1, // TODO(lgalabru)
780-
"proof": proofs.get(&transaction.transaction_identifier),
781-
})
782-
}).collect::<Vec<_>>(),
783-
"rollback": trigger.rollback.into_iter().map(|(transaction, block_identifier)| {
784-
json!({
785-
"transaction": transaction,
786-
"block_identifier": block_identifier,
787-
"confirmations": 1, // TODO(lgalabru)
788-
})
789-
}).collect::<Vec<_>>(),
790-
"chainhook": {
791-
"uuid": trigger.chainhook.uuid,
792-
"predicate": trigger.chainhook.predicate,
793-
}
794-
});
795-
let body = serde_json::to_vec(&payload).unwrap();
819+
let body =
820+
serde_json::to_vec(&serialize_stacks_payload_to_json(trigger, proofs)).unwrap();
796821
Some(StacksChainhookOccurrence::Http(
797822
client
798823
.request(method, &host)
799824
.header("Content-Type", "application/json")
800825
.body(body),
801826
))
802827
}
828+
HookAction::File(disk) => {
829+
let bytes =
830+
serde_json::to_vec(&serialize_stacks_payload_to_json(trigger, proofs)).unwrap();
831+
Some(StacksChainhookOccurrence::File(
832+
disk.path.to_string(),
833+
bytes,
834+
))
835+
}
803836
HookAction::Noop => Some(StacksChainhookOccurrence::Data(
804837
StacksChainhookOccurrencePayload {
805838
apply: trigger

components/chainhook-event-observer/src/chainhooks/types.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ pub struct BitcoinChainhookSpecification {
125125
#[serde(rename_all = "snake_case")]
126126
pub enum HookAction {
127127
Http(HttpHook),
128+
File(FileHook),
128129
Noop,
129130
}
130131

@@ -136,6 +137,12 @@ pub struct HttpHook {
136137
pub authorization_header: String,
137138
}
138139

140+
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
141+
#[serde(rename_all = "snake_case")]
142+
pub struct FileHook {
143+
pub path: String,
144+
}
145+
139146
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
140147
pub struct ScriptTemplate {
141148
pub instructions: Vec<ScriptInstruction>,

components/chainhook-event-observer/src/observer/mod.rs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ use rocket::request::{self, FromRequest, Outcome, Request};
2121
use rocket::serde::json::{json, Json, Value as JsonValue};
2222
use rocket::serde::Deserialize;
2323
use rocket::State;
24+
use rocket_okapi::{openapi, openapi_get_routes, request::OpenApiFromRequest};
25+
use schemars::JsonSchema;
2426
use serde_json::error;
2527
use stacks_rpc_client::{PoxInfo, StacksRpc};
2628
use std::collections::{HashMap, HashSet, VecDeque};
@@ -35,9 +37,6 @@ use std::sync::atomic::{AtomicBool, Ordering};
3537
use std::sync::mpsc::{Receiver, Sender};
3638
use std::sync::{Arc, Mutex, RwLock};
3739

38-
use rocket_okapi::{openapi, openapi_get_routes, request::OpenApiFromRequest};
39-
use schemars::JsonSchema;
40-
4140
pub const DEFAULT_INGESTION_PORT: u16 = 20445;
4241
pub const DEFAULT_CONTROL_PORT: u16 = 20446;
4342

@@ -501,6 +500,9 @@ pub async fn start_observer_commands_handler(
501500
BitcoinChainhookOccurrence::Http(request) => {
502501
requests.push(request);
503502
}
503+
BitcoinChainhookOccurrence::File(_path, _bytes) => {
504+
info!("Writing to disk not supported in server mode")
505+
}
504506
BitcoinChainhookOccurrence::Data(payload) => {
505507
if let Some(ref tx) = observer_events_tx {
506508
let _ = tx.send(
@@ -617,6 +619,9 @@ pub async fn start_observer_commands_handler(
617619
StacksChainhookOccurrence::Http(request) => {
618620
requests.push(request);
619621
}
622+
StacksChainhookOccurrence::File(_path, _bytes) => {
623+
info!("Writing to disk not supported in server mode")
624+
}
620625
StacksChainhookOccurrence::Data(payload) => {
621626
if let Some(ref tx) = observer_events_tx {
622627
let _ = tx.send(
@@ -980,11 +985,18 @@ pub async fn handle_bitcoin_rpc_call(
980985
"{}:{}",
981986
bitcoin_config.username, bitcoin_config.password
982987
));
988+
989+
let path = if method == "listunspent" {
990+
"wallet/stacks-mining"
991+
} else {
992+
""
993+
};
994+
983995
let client = Client::new();
984996
let builder = client
985997
.post(format!(
986-
"{}:{}/",
987-
bitcoin_config.rpc_host, bitcoin_config.rpc_port
998+
"{}:{}/{}",
999+
bitcoin_config.rpc_host, bitcoin_config.rpc_port, path
9881000
))
9891001
.header("Content-Type", "application/json")
9901002
.timeout(std::time::Duration::from_secs(5))

components/clarinet-cli/src/chainhooks/types.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ pub struct StxEventPredicateFile {
7474
#[serde(rename_all = "kebab-case")]
7575
pub struct HookActionFile {
7676
http: Option<BTreeMap<String, String>>,
77+
file: Option<BTreeMap<String, String>>,
7778
}
7879

7980
impl ChainhookSpecificationFile {
@@ -200,14 +201,19 @@ impl HookActionFile {
200201
Some(authorization_header) => Ok(authorization_header.to_string()),
201202
None => Err(format!("authorization-header missing for http")),
202203
}?;
203-
204204
Ok(HookAction::Http(HttpHook {
205205
url,
206206
method,
207207
authorization_header,
208208
}))
209+
} else if let Some(ref specs) = self.file {
210+
let path = match specs.get("path") {
211+
Some(path) => Ok(path.to_string()),
212+
None => Err(format!("path missing for file")),
213+
}?;
214+
Ok(HookAction::File(FileHook { path }))
209215
} else {
210-
Err(format!("action not supported (http)"))
216+
Err(format!("action not supported (http, file)"))
211217
}
212218
}
213219
}

components/clarinet-cli/src/runner/api_v1.rs

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ use deno_core::{op, Extension};
4747
use deno_core::{ModuleSpecifier, OpState};
4848
use sha2::{Digest, Sha256};
4949
use std::collections::{BTreeMap, HashMap};
50+
use std::fs::OpenOptions;
51+
use std::io::Write;
5052
use std::sync::mpsc::{self, Sender};
5153
use std::thread::sleep;
5254
use std::time::Duration;
@@ -747,12 +749,38 @@ fn mine_block(state: &mut OpState, args: MineBlockArgs) -> Result<String, AnyErr
747749
},
748750
&HashMap::new(),
749751
);
750-
if let Some(StacksChainhookOccurrence::Http(action)) = result {
751-
let chainhook_tx = match state.try_borrow::<Sender<ChainhookEvent>>() {
752-
Some(chainhook_tx) => chainhook_tx,
753-
None => panic!(),
754-
};
755-
let _ = chainhook_tx.send(ChainhookEvent::PerformRequest(action));
752+
match result {
753+
Some(StacksChainhookOccurrence::Http(action)) => {
754+
let chainhook_tx = match state.try_borrow::<Sender<ChainhookEvent>>() {
755+
Some(chainhook_tx) => chainhook_tx,
756+
None => panic!(),
757+
};
758+
let _ = chainhook_tx.send(ChainhookEvent::PerformRequest(action));
759+
}
760+
Some(StacksChainhookOccurrence::File(path, bytes)) => {
761+
let mut file_path = std::env::current_dir().unwrap();
762+
file_path.push(path);
763+
if !file_path.exists() {
764+
match std::fs::File::open(&file_path) {
765+
Ok(ref mut file) => {
766+
let _ = file.write_all(&bytes);
767+
}
768+
Err(e) => println!("unable to create file {:?}", e),
769+
}
770+
}
771+
let mut file = OpenOptions::new()
772+
.create(false)
773+
.write(true)
774+
.append(true)
775+
.open(file_path)
776+
.unwrap();
777+
778+
if let Err(e) = writeln!(file, "{}", String::from_utf8(bytes).unwrap())
779+
{
780+
eprintln!("Couldn't write to file: {}", e);
781+
}
782+
}
783+
_ => {}
756784
}
757785
}
758786
}

0 commit comments

Comments
 (0)