bsv-sdk 0.2.82

Pure Rust implementation of the BSV Blockchain SDK
Documentation
//! KVStore interpreter for Historian.
//!
//! Translates the TS SDK kvStoreInterpreter.ts. Provides the interpreter
//! callback used by Historian to extract key-value data from PushDrop
//! transaction outputs.

use crate::transaction::Transaction;

use super::types::{KvContext, KvProtocol};

/// KVStore interpreter function for use with Historian.
///
/// Decodes PushDrop outputs from a transaction and extracts the value
/// for a matching key. Returns None if the output is not a valid KVStore
/// token or does not match the target key in the context.
///
/// This function matches the `InterpreterFn<String, KvContext>` signature
/// expected by Historian.
pub fn kv_store_interpreter(
    tx: &Transaction,
    output_index: usize,
    ctx: Option<&KvContext>,
) -> Option<String> {
    let ctx = ctx?;

    if output_index >= tx.outputs.len() {
        return None;
    }

    let output = &tx.outputs[output_index];
    let chunks = output.locking_script.chunks();

    // Extract PushDrop data fields: all data-push chunks before the first
    // opcode chunk (OP_DROP, OP_2DROP, etc.). In KVStore format, data fields
    // include the controller (33-byte pubkey), so we cannot use the pubkey
    // heuristic here.
    let mut fields: Vec<Vec<u8>> = Vec::new();
    for chunk in chunks {
        if let Some(data) = &chunk.data {
            fields.push(data.clone());
        } else {
            // Hit an opcode (OP_DROP, OP_2DROP, OP_CHECKSIG), stop collecting.
            break;
        }
    }

    // Support both old format (5 fields) and new format with tags (6 fields).
    let has_tags = fields.len() == KvProtocol::FIELD_COUNT;
    let is_old_format = fields.len() == KvProtocol::OLD_FIELD_COUNT;

    if !has_tags && !is_old_format {
        return None;
    }

    // Extract key from the PushDrop field.
    let key = String::from_utf8(fields.get(KvProtocol::KEY)?.clone()).ok()?;
    if key != ctx.key {
        return None;
    }

    // Extract and verify protocolID.
    let protocol_str = String::from_utf8(fields.get(KvProtocol::PROTOCOL_ID)?.clone()).ok()?;
    let expected_protocol = format!("[{},\"{}\"]", ctx.protocol_id.0, ctx.protocol_id.1);
    if protocol_str != expected_protocol {
        return None;
    }

    // Extract value.
    String::from_utf8(fields.get(KvProtocol::VALUE)?.clone()).ok()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::primitives::private_key::PrivateKey;
    use crate::script::templates::push_drop::PushDrop;
    use crate::script::templates::ScriptTemplateLock;
    use crate::transaction::{Transaction, TransactionOutput};

    fn make_kv_tx(key: &str, value: &str, protocol_id: &str) -> Transaction {
        let pk = PrivateKey::from_hex("1").unwrap();
        let fields = vec![
            protocol_id.as_bytes().to_vec(),
            key.as_bytes().to_vec(),
            value.as_bytes().to_vec(),
            pk.to_public_key().to_der(), // controller
            b"[]".to_vec(),              // tags (empty)
        ];
        let pd = PushDrop::new(fields, pk);
        let lock_script = pd.lock().unwrap();

        let mut tx = Transaction::new();
        tx.outputs.push(TransactionOutput {
            satoshis: Some(1),
            locking_script: lock_script,
            change: false,
        });
        tx
    }

    #[test]
    fn test_interpreter_extracts_matching_key() {
        let tx = make_kv_tx("mykey", "myvalue", "[1,\"kvstore\"]");
        let ctx = KvContext {
            key: "mykey".to_string(),
            protocol_id: (1, "kvstore".to_string()),
        };

        let result = kv_store_interpreter(&tx, 0, Some(&ctx));
        assert_eq!(result, Some("myvalue".to_string()));
    }

    #[test]
    fn test_interpreter_returns_none_for_wrong_key() {
        let tx = make_kv_tx("mykey", "myvalue", "[1,\"kvstore\"]");
        let ctx = KvContext {
            key: "otherkey".to_string(),
            protocol_id: (1, "kvstore".to_string()),
        };

        let result = kv_store_interpreter(&tx, 0, Some(&ctx));
        assert_eq!(result, None);
    }

    #[test]
    fn test_interpreter_returns_none_for_wrong_protocol() {
        let tx = make_kv_tx("mykey", "myvalue", "[2,\"other\"]");
        let ctx = KvContext {
            key: "mykey".to_string(),
            protocol_id: (1, "kvstore".to_string()),
        };

        let result = kv_store_interpreter(&tx, 0, Some(&ctx));
        assert_eq!(result, None);
    }

    #[test]
    fn test_interpreter_returns_none_no_context() {
        let tx = make_kv_tx("mykey", "myvalue", "[1,\"kvstore\"]");
        let result = kv_store_interpreter(&tx, 0, None);
        assert_eq!(result, None);
    }

    #[test]
    fn test_interpreter_returns_none_empty_tx() {
        let tx = Transaction::new();
        let ctx = KvContext {
            key: "mykey".to_string(),
            protocol_id: (1, "kvstore".to_string()),
        };
        let result = kv_store_interpreter(&tx, 0, Some(&ctx));
        assert_eq!(result, None);
    }
}