Skip to content

Commit

Permalink
feat: include contract_abi in stacks transaction (#378)
Browse files Browse the repository at this point in the history
### Description

<!-- Describe the bug this PR fixes or the feature it adds. Link to any
related issues and PRs -->

#### Breaking change?

<!-- If applicable, list the APIs/functionality which this PR breaks -->

### Example

<!-- If applicable, add an example on how this improves the application
-->

---

### Checklist

- [x] All tests pass
- [x] Tests added in this PR (if applicable)
  • Loading branch information
MicaiahReid authored Sep 18, 2023
1 parent c6ca1ba commit 5503c3d
Show file tree
Hide file tree
Showing 15 changed files with 427 additions and 74 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,9 @@ Additional configuration knobs available:

// Include decoded clarity values in payload
"decode_clarity_values": true

// Include the contract ABI for transactions that deploy contracts:
"include_contract_abi": true
```

Putting all the pieces together:
Expand Down
2 changes: 2 additions & 0 deletions components/chainhook-cli/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ async fn handle_command(opts: Opts, ctx: Context) -> Result<(), String> {
expire_after_occurrence: None,
capture_all_events: None,
decode_clarity_values: None,
include_contract_abi: None,
action: HookAction::FileAppend(FileHook {
path: "arkadiko.txt".into()
})
Expand All @@ -358,6 +359,7 @@ async fn handle_command(opts: Opts, ctx: Context) -> Result<(), String> {
expire_after_occurrence: None,
capture_all_events: None,
decode_clarity_values: None,
include_contract_abi: None,
action: HookAction::FileAppend(FileHook {
path: "arkadiko.txt".into()
})
Expand Down
2 changes: 1 addition & 1 deletion components/chainhook-cli/src/scan/bitcoin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ pub async fn scan_bitcoin_chainstate_via_rpc_using_predicate(
Err((e, _)) => {
warn!(
ctx.expect_logger(),
"Unable to standardize block#{} {}: {}", current_block_height, block_hash, e
"Unable to standardize block #{} {}: {}", current_block_height, block_hash, e
);
continue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ fn create_stacks_new_transaction(index: u64) -> NewTransaction {
raw_result: format!("0x0703"),
raw_tx: format!("0x00000000010400e2cd0871da5bdd38c4d5569493dc3b14aac4e0a10000000000000019000000000000000000008373b16e4a6f9d87864c314dd77bbd8b27a2b1805e96ec5a6509e7e4f833cd6a7bdb2462c95f6968a867ab6b0e8f0a6498e600dbc46cfe9f84c79709da7b9637010200000000040000000000000000000000000000000000000000000000000000000000000000"),
execution_cost: None,
contract_abi: None
}
}

Expand Down
72 changes: 39 additions & 33 deletions components/chainhook-sdk/src/chainhooks/stacks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -508,17 +508,41 @@ pub fn evaluate_stacks_predicate_on_transaction<'a>(
}
}

fn encode_transaction_including_with_clarity_decoding(
transaction: &StacksTransactionData,
fn serialize_stacks_block(
block: &dyn AbstractStacksBlock,
transactions: Vec<&StacksTransactionData>,
decode_clarity_values: bool,
include_contract_abi: bool,
ctx: &Context,
) -> serde_json::Value {
json!({
"block_identifier": block.get_identifier(),
"parent_block_identifier": block.get_parent_identifier(),
"timestamp": block.get_timestamp(),
"transactions": transactions.into_iter().map(|transaction| {
serialize_stacks_transaction(&transaction, decode_clarity_values, include_contract_abi, ctx)
}).collect::<Vec<_>>(),
"metadata": block.get_serialized_metadata(),
})
}

fn serialize_stacks_transaction(
transaction: &StacksTransactionData,
decode_clarity_values: bool,
include_contract_abi: bool,
ctx: &Context,
) -> serde_json::Value {
let mut json = json!({
"transaction_identifier": transaction.transaction_identifier,
"operations": transaction.operations,
"metadata": {
"success": transaction.metadata.success,
"raw_tx": transaction.metadata.raw_tx,
"result": serialized_decoded_clarity_value(&transaction.metadata.result, ctx),
"result": if decode_clarity_values {
serialized_decoded_clarity_value(&transaction.metadata.result, ctx)
} else {
json!(transaction.metadata.result)
},
"sender": transaction.metadata.sender,
"fee": transaction.metadata.fee,
"kind": transaction.metadata.kind,
Expand All @@ -527,15 +551,21 @@ fn encode_transaction_including_with_clarity_decoding(
"mutated_assets_radius": transaction.metadata.receipt.mutated_assets_radius,
"contract_calls_stack": transaction.metadata.receipt.contract_calls_stack,
"events": transaction.metadata.receipt.events.iter().map(|event| {
serialized_event_with_decoded_clarity_value(event, ctx)
if decode_clarity_values { serialized_event_with_decoded_clarity_value(event, ctx) } else { json!(event) }
}).collect::<Vec<serde_json::Value>>(),
},
"description": transaction.metadata.description,
"sponsor": transaction.metadata.sponsor,
"execution_cost": transaction.metadata.execution_cost,
"position": transaction.metadata.position,
"position": transaction.metadata.position
},
})
});
if include_contract_abi {
if let Some(abi) = &transaction.metadata.contract_abi {
json["metadata"]["contract_abi"] = json!(abi);
}
}
json
}

pub fn serialized_event_with_decoded_clarity_value(
Expand Down Expand Up @@ -764,37 +794,13 @@ pub fn serialize_stacks_payload_to_json<'a>(
ctx: &Context,
) -> JsonValue {
let decode_clarity_values = trigger.should_decode_clarity_value();
let include_contract_abi = trigger.chainhook.include_contract_abi;
json!({
"apply": trigger.apply.into_iter().map(|(transactions, block)| {
json!({
"block_identifier": block.get_identifier(),
"parent_block_identifier": block.get_parent_identifier(),
"timestamp": block.get_timestamp(),
"transactions": transactions.iter().map(|transaction| {
if decode_clarity_values {
encode_transaction_including_with_clarity_decoding(transaction, ctx)
} else {
json!(transaction)
}
}).collect::<Vec<_>>(),
"metadata": block.get_serialized_metadata(),
})
serialize_stacks_block(block, transactions, decode_clarity_values, include_contract_abi, ctx)
}).collect::<Vec<_>>(),
"rollback": trigger.rollback.into_iter().map(|(transactions, block)| {
json!({
"block_identifier": block.get_identifier(),
"parent_block_identifier": block.get_parent_identifier(),
"timestamp": block.get_timestamp(),
"transactions": transactions.iter().map(|transaction| {
if decode_clarity_values {
encode_transaction_including_with_clarity_decoding(transaction, ctx)
} else {
json!(transaction)
}
}).collect::<Vec<_>>(),
"metadata": block.get_serialized_metadata(),
// "proof": proofs.get(&transaction.transaction_identifier),
})
serialize_stacks_block(block, transactions, decode_clarity_values, include_contract_abi, ctx)
}).collect::<Vec<_>>(),
"chainhook": {
"uuid": trigger.chainhook.uuid,
Expand Down

Large diffs are not rendered by default.

115 changes: 114 additions & 1 deletion components/chainhook-sdk/src/chainhooks/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use super::{
StacksTrait,
},
};
use crate::utils::Context;
use crate::{chainhooks::stacks::serialize_stacks_payload_to_json, utils::Context};
use crate::{
chainhooks::{
tests::fixtures::{get_expected_occurrence, get_test_event_by_type},
Expand Down Expand Up @@ -348,6 +348,7 @@ fn test_stacks_predicates(
expire_after_occurrence: None,
capture_all_events: None,
decode_clarity_values: None,
include_contract_abi: false,
predicate: predicate,
action: HookAction::Noop,
enabled: true,
Expand Down Expand Up @@ -427,6 +428,7 @@ fn test_stacks_predicate_contract_deploy(predicate: StacksPredicate, expected_ap
expire_after_occurrence: None,
capture_all_events: None,
decode_clarity_values: None,
include_contract_abi: false,
predicate: predicate,
action: HookAction::Noop,
enabled: true,
Expand All @@ -447,6 +449,114 @@ fn test_stacks_predicate_contract_deploy(predicate: StacksPredicate, expected_ap
}
}

#[test]
fn verify_optional_addition_of_contract_abi() {
// "mine" two blocks
// - one contract deploy (which should have a contract abi) and
// - one contract call (which should not)
let new_blocks = vec![
StacksBlockUpdate {
block: fixtures::build_stacks_testnet_block_with_contract_deployment(),
parent_microblocks_to_apply: vec![],
parent_microblocks_to_rollback: vec![],
},
StacksBlockUpdate {
block: fixtures::build_stacks_testnet_block_with_contract_call(),
parent_microblocks_to_apply: vec![],
parent_microblocks_to_rollback: vec![],
},
];
let event: StacksChainEvent =
StacksChainEvent::ChainUpdatedWithBlocks(StacksChainUpdatedWithBlocksData {
new_blocks,
confirmed_blocks: vec![],
});
let mut contract_deploy_chainhook = StacksChainhookSpecification {
uuid: "contract-deploy".to_string(),
owner_uuid: None,
name: "".to_string(),
network: StacksNetwork::Testnet,
version: 1,
blocks: None,
start_block: None,
end_block: None,
expire_after_occurrence: None,
capture_all_events: None,
decode_clarity_values: None,
include_contract_abi: true,
predicate: StacksPredicate::ContractDeployment(
StacksContractDeploymentPredicate::Deployer("*".to_string()),
),
action: HookAction::Noop,
enabled: true,
expired_at: None,
};
let contract_call_chainhook = StacksChainhookSpecification {
uuid: "contract-call".to_string(),
owner_uuid: None,
name: "".to_string(),
network: StacksNetwork::Testnet,
version: 1,
blocks: None,
start_block: None,
end_block: None,
expire_after_occurrence: None,
capture_all_events: None,
decode_clarity_values: None,
include_contract_abi: true,
predicate: StacksPredicate::ContractCall(StacksContractCallBasedPredicate {
contract_identifier: "ST13F481SBR0R7Z6NMMH8YV2FJJYXA5JPA0AD3HP9.subnet-v1".to_string(),
method: "commit-block".to_string(),
}),
action: HookAction::Noop,
enabled: true,
expired_at: None,
};

let predicates = vec![&contract_deploy_chainhook, &contract_call_chainhook];
let (triggered, _blocks, _) =
evaluate_stacks_chainhooks_on_chain_event(&event, predicates, &Context::empty());
assert_eq!(triggered.len(), 2);

for t in triggered.into_iter() {
let result = serialize_stacks_payload_to_json(t, &HashMap::new(), &Context::empty());
let result = result.as_object().unwrap();
let uuid = result.get("chainhook").unwrap().get("uuid").unwrap();
let apply_blocks = result.get("apply").unwrap();
for block in apply_blocks.as_array().unwrap() {
let transactions = block.get("transactions").unwrap();
for transaction in transactions.as_array().unwrap() {
let contract_abi = transaction.get("metadata").unwrap().get("contract_abi");
if uuid == "contract-call" {
assert_eq!(contract_abi, None);
} else if uuid == "contract-deploy" {
assert!(contract_abi.is_some())
} else {
unreachable!()
}
}
}
}
contract_deploy_chainhook.include_contract_abi = false;
let predicates = vec![&contract_deploy_chainhook, &contract_call_chainhook];
let (triggered, _blocks, _) =
evaluate_stacks_chainhooks_on_chain_event(&event, predicates, &Context::empty());
assert_eq!(triggered.len(), 2);

for t in triggered.into_iter() {
let result = serialize_stacks_payload_to_json(t, &HashMap::new(), &Context::empty());
let result = result.as_object().unwrap();
let apply_blocks = result.get("apply").unwrap();
for block in apply_blocks.as_array().unwrap() {
let transactions = block.get("transactions").unwrap();
for transaction in transactions.as_array().unwrap() {
let contract_abi = transaction.get("metadata").unwrap().get("contract_abi");
assert_eq!(contract_abi, None);
}
}
}
}

#[test_case(
StacksPredicate::ContractCall(StacksContractCallBasedPredicate {
contract_identifier: "ST13F481SBR0R7Z6NMMH8YV2FJJYXA5JPA0AD3HP9.subnet-v1".to_string(),
Expand Down Expand Up @@ -512,6 +622,7 @@ fn test_stacks_predicate_contract_call(predicate: StacksPredicate, expected_appl
expire_after_occurrence: None,
capture_all_events: None,
decode_clarity_values: None,
include_contract_abi: false,
predicate: predicate,
action: HookAction::Noop,
enabled: true,
Expand Down Expand Up @@ -546,6 +657,7 @@ fn test_stacks_hook_action_noop() {
expire_after_occurrence: None,
capture_all_events: None,
decode_clarity_values: None,
include_contract_abi: false,
predicate: StacksPredicate::Txid(ExactMatchingRule::Equals(
"0xb92c2ade84a8b85f4c72170680ae42e65438aea4db72ba4b2d6a6960f4141ce8".to_string(),
)),
Expand Down Expand Up @@ -603,6 +715,7 @@ fn test_stacks_hook_action_file_append() {
expire_after_occurrence: None,
capture_all_events: None,
decode_clarity_values: Some(true),
include_contract_abi: false,
predicate: StacksPredicate::Txid(ExactMatchingRule::Equals(
"0xb92c2ade84a8b85f4c72170680ae42e65438aea4db72ba4b2d6a6960f4141ce8".to_string(),
)),
Expand Down
4 changes: 4 additions & 0 deletions components/chainhook-sdk/src/chainhooks/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,7 @@ impl StacksChainhookFullSpecification {
capture_all_events: spec.capture_all_events,
decode_clarity_values: spec.decode_clarity_values,
expire_after_occurrence: spec.expire_after_occurrence,
include_contract_abi: spec.include_contract_abi.unwrap_or(false),
predicate: spec.predicate,
action: spec.action,
enabled: false,
Expand All @@ -432,6 +433,8 @@ pub struct StacksChainhookNetworkSpecification {
pub capture_all_events: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub decode_clarity_values: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_contract_abi: Option<bool>,
#[serde(rename = "if_this")]
pub predicate: StacksPredicate,
#[serde(rename = "then_that")]
Expand Down Expand Up @@ -732,6 +735,7 @@ pub struct StacksChainhookSpecification {
pub capture_all_events: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub decode_clarity_values: Option<bool>,
pub include_contract_abi: bool,
#[serde(rename = "predicate")]
pub predicate: StacksPredicate,
pub action: HookAction,
Expand Down
4 changes: 4 additions & 0 deletions components/chainhook-sdk/src/indexer/stacks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ pub struct NewTransaction {
pub raw_result: String,
pub raw_tx: String,
pub execution_cost: Option<StacksTransactionExecutionCost>,
pub contract_abi: Option<ContractInterface>,
}

#[derive(Deserialize, Debug)]
Expand All @@ -89,6 +90,7 @@ pub struct NewMicroblockTransaction {
pub microblock_sequence: usize,
pub microblock_hash: String,
pub microblock_parent_hash: String,
pub contract_abi: Option<ContractInterface>,
}

#[derive(Debug, Deserialize, Serialize)]
Expand Down Expand Up @@ -311,6 +313,7 @@ pub fn standardize_stacks_block(
description,
position: StacksTransactionPosition::anchor_block(tx.tx_index),
proof: None,
contract_abi: tx.contract_abi.clone(),
},
});
}
Expand Down Expand Up @@ -452,6 +455,7 @@ pub fn standardize_stacks_microblock_trail(
tx.tx_index,
),
proof: None,
contract_abi: tx.contract_abi.clone(),
},
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ pub fn generate_test_tx_stacks_contract_call(
sponsor: None,
position: chainhook_types::StacksTransactionPosition::anchor_block(0),
proof: None,
contract_abi: None,
},
}
}
Expand Down
1 change: 1 addition & 0 deletions components/chainhook-sdk/src/observer/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ fn stacks_chainhook_contract_call(
expire_after_occurrence,
capture_all_events: None,
decode_clarity_values: Some(true),
include_contract_abi: None,
predicate: StacksPredicate::ContractCall(StacksContractCallBasedPredicate {
contract_identifier: contract_identifier.to_string(),
method: method.to_string(),
Expand Down
Loading

0 comments on commit 5503c3d

Please sign in to comment.