use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
pub struct KVStoreConfig {
pub protocol_id: String,
pub service_name: String,
pub token_amount: u64,
pub topics: Vec<String>,
pub originator: Option<String>,
pub encrypt: bool,
}
impl Default for KVStoreConfig {
fn default() -> Self {
Self {
protocol_id: "kvstore".to_string(),
service_name: "ls_kvstore".to_string(),
token_amount: 1,
topics: vec!["tm_kvstore".to_string()],
originator: None,
encrypt: true,
}
}
}
impl KVStoreConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_protocol_id(mut self, protocol_id: impl Into<String>) -> Self {
self.protocol_id = protocol_id.into();
self
}
pub fn with_service_name(mut self, service_name: impl Into<String>) -> Self {
self.service_name = service_name.into();
self
}
pub fn with_token_amount(mut self, amount: u64) -> Self {
self.token_amount = amount;
self
}
pub fn with_topics(mut self, topics: Vec<String>) -> Self {
self.topics = topics;
self
}
pub fn with_originator(mut self, originator: impl Into<String>) -> Self {
self.originator = Some(originator.into());
self
}
pub fn with_encrypt(mut self, encrypt: bool) -> Self {
self.encrypt = encrypt;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KVStoreToken {
pub txid: String,
pub output_index: u32,
pub satoshis: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub beef: Option<Vec<u8>>,
}
impl KVStoreToken {
pub fn new(txid: impl Into<String>, output_index: u32, satoshis: u64) -> Self {
Self {
txid: txid.into(),
output_index,
satoshis,
beef: None,
}
}
pub fn with_beef(mut self, beef: Vec<u8>) -> Self {
self.beef = Some(beef);
self
}
pub fn outpoint_string(&self) -> String {
format!("{}.{}", self.txid, self.output_index)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KVStoreEntry {
pub key: String,
pub value: String,
pub controller: String,
pub protocol_id: String,
#[serde(default)]
pub tags: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub token: Option<KVStoreToken>,
#[serde(skip_serializing_if = "Option::is_none")]
pub history: Option<Vec<String>>,
}
impl KVStoreEntry {
pub fn new(
key: impl Into<String>,
value: impl Into<String>,
controller: impl Into<String>,
protocol_id: impl Into<String>,
) -> Self {
Self {
key: key.into(),
value: value.into(),
controller: controller.into(),
protocol_id: protocol_id.into(),
tags: Vec::new(),
token: None,
history: None,
}
}
pub fn with_tags(mut self, tags: Vec<String>) -> Self {
self.tags = tags;
self
}
pub fn with_token(mut self, token: KVStoreToken) -> Self {
self.token = Some(token);
self
}
pub fn with_history(mut self, history: Vec<String>) -> Self {
self.history = Some(history);
self
}
}
#[derive(Debug, Clone)]
pub struct KVStoreLookupResult {
pub txid: String,
pub output_index: u32,
pub output_script: String,
pub satoshis: u64,
pub beef: Option<Vec<u8>>,
}
impl KVStoreLookupResult {
pub fn new(
txid: impl Into<String>,
output_index: u32,
output_script: impl Into<String>,
satoshis: u64,
) -> Self {
Self {
txid: txid.into(),
output_index,
output_script: output_script.into(),
satoshis,
beef: None,
}
}
pub fn with_beef(mut self, beef: Vec<u8>) -> Self {
self.beef = Some(beef);
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KVStoreQuery {
#[serde(skip_serializing_if = "Option::is_none")]
pub key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub controller: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub protocol_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tag_query_mode: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub skip: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sort_order: Option<String>,
}
impl KVStoreQuery {
pub fn new() -> Self {
Self::default()
}
pub fn with_key(mut self, key: impl Into<String>) -> Self {
self.key = Some(key.into());
self
}
pub fn with_controller(mut self, controller: impl Into<String>) -> Self {
self.controller = Some(controller.into());
self
}
pub fn with_protocol_id(mut self, protocol_id: impl Into<String>) -> Self {
self.protocol_id = Some(protocol_id.into());
self
}
pub fn with_tags(mut self, tags: Vec<String>) -> Self {
self.tags = Some(tags);
self
}
pub fn with_tag_query_mode(mut self, mode: impl Into<String>) -> Self {
self.tag_query_mode = Some(mode.into());
self
}
pub fn with_limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
pub fn with_skip(mut self, skip: u32) -> Self {
self.skip = Some(skip);
self
}
pub fn with_sort_order(mut self, order: impl Into<String>) -> Self {
self.sort_order = Some(order.into());
self
}
pub fn to_json(&self) -> serde_json::Value {
serde_json::to_value(self).unwrap_or(serde_json::Value::Object(Default::default()))
}
}
#[derive(Debug, Clone, Default)]
pub struct KVStoreGetOptions {
pub history: bool,
pub include_token: bool,
pub service_name: Option<String>,
}
impl KVStoreGetOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_history(mut self, history: bool) -> Self {
self.history = history;
self
}
pub fn with_include_token(mut self, include: bool) -> Self {
self.include_token = include;
self
}
pub fn with_service_name(mut self, name: impl Into<String>) -> Self {
self.service_name = Some(name.into());
self
}
}
#[derive(Debug, Clone, Default)]
pub struct KVStoreSetOptions {
pub protocol_id: Option<String>,
pub description: Option<String>,
pub token_amount: Option<u64>,
pub tags: Option<Vec<String>>,
pub ttl: Option<std::time::Duration>,
}
impl KVStoreSetOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_protocol_id(mut self, protocol_id: impl Into<String>) -> Self {
self.protocol_id = Some(protocol_id.into());
self
}
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub fn with_token_amount(mut self, amount: u64) -> Self {
self.token_amount = Some(amount);
self
}
pub fn with_tags(mut self, tags: Vec<String>) -> Self {
self.tags = Some(tags);
self
}
pub fn with_ttl(mut self, ttl: std::time::Duration) -> Self {
self.ttl = Some(ttl);
self
}
}
#[derive(Debug, Clone, Default)]
pub struct KVStoreRemoveOptions {
pub protocol_id: Option<String>,
pub description: Option<String>,
}
impl KVStoreRemoveOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_protocol_id(mut self, protocol_id: impl Into<String>) -> Self {
self.protocol_id = Some(protocol_id.into());
self
}
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct TtlEnvelope {
v: String,
e: u64,
}
pub(crate) fn encode_value_with_ttl(value: &str, ttl: std::time::Duration) -> String {
let expiration = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
+ ttl.as_secs();
let envelope = TtlEnvelope {
v: value.to_string(),
e: expiration,
};
serde_json::to_string(&envelope).unwrap_or_else(|_| value.to_string())
}
pub(crate) fn decode_value_with_ttl(stored: &str) -> (String, bool) {
if let Ok(envelope) = serde_json::from_str::<TtlEnvelope>(stored) {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let is_expired = now >= envelope.e;
(envelope.v, is_expired)
} else {
(stored.to_string(), false)
}
}
#[derive(Debug, Clone)]
pub(crate) struct LookupValueResult {
pub value: String,
pub outpoints: Vec<String>,
pub input_beef: Option<Vec<u8>>,
pub outputs: Vec<WalletOutput>,
pub value_exists: bool,
}
impl LookupValueResult {
pub fn not_found(default_value: String) -> Self {
Self {
value: default_value,
outpoints: Vec::new(),
input_beef: None,
outputs: Vec::new(),
value_exists: false,
}
}
pub fn found(
value: String,
outpoints: Vec<String>,
input_beef: Option<Vec<u8>>,
outputs: Vec<WalletOutput>,
) -> Self {
Self {
value,
outpoints,
input_beef,
outputs,
value_exists: true,
}
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub(crate) struct WalletOutput {
pub outpoint: String,
pub satoshis: u64,
pub locking_script: Vec<u8>,
pub tags: Vec<String>,
}
pub struct KvProtocolFields;
impl KvProtocolFields {
pub const PROTOCOL_ID: usize = 0;
pub const KEY: usize = 1;
pub const VALUE: usize = 2;
pub const CONTROLLER: usize = 3;
pub const TAGS: usize = 4;
pub const SIGNATURE: usize = 5;
pub const MIN_FIELDS_OLD: usize = 5;
pub const MIN_FIELDS_NEW: usize = 6;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_kvstore_config_default() {
let config = KVStoreConfig::default();
assert_eq!(config.protocol_id, "kvstore");
assert_eq!(config.service_name, "ls_kvstore");
assert_eq!(config.token_amount, 1);
assert_eq!(config.topics, vec!["tm_kvstore"]);
assert!(config.originator.is_none());
assert!(config.encrypt);
}
#[test]
fn test_kvstore_config_builder() {
let config = KVStoreConfig::new()
.with_protocol_id("my_protocol")
.with_service_name("ls_custom")
.with_token_amount(100)
.with_topics(vec!["tm_custom".to_string()])
.with_originator("myapp")
.with_encrypt(false);
assert_eq!(config.protocol_id, "my_protocol");
assert_eq!(config.service_name, "ls_custom");
assert_eq!(config.token_amount, 100);
assert_eq!(config.topics, vec!["tm_custom"]);
assert_eq!(config.originator, Some("myapp".to_string()));
assert!(!config.encrypt);
}
#[test]
fn test_kvstore_token_serialization() {
let token = KVStoreToken::new("abc123def456", 0, 1);
let json = serde_json::to_string(&token).unwrap();
assert!(json.contains("abc123def456"));
assert!(json.contains("outputIndex"));
let decoded: KVStoreToken = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.txid, "abc123def456");
assert_eq!(decoded.output_index, 0);
assert_eq!(decoded.satoshis, 1);
}
#[test]
fn test_kvstore_token_with_beef() {
let token = KVStoreToken::new("abc123", 0, 1).with_beef(vec![1, 2, 3, 4]);
assert_eq!(token.beef, Some(vec![1, 2, 3, 4]));
}
#[test]
fn test_kvstore_token_outpoint_string() {
let token = KVStoreToken::new("abc123", 5, 1);
assert_eq!(token.outpoint_string(), "abc123.5");
}
#[test]
fn test_kvstore_entry_serialization() {
let entry = KVStoreEntry::new("test_key", "test_value", "02abc...", "kvstore")
.with_tags(vec!["tag1".to_string()]);
let json = serde_json::to_string(&entry).unwrap();
let decoded: KVStoreEntry = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.key, "test_key");
assert_eq!(decoded.value, "test_value");
assert_eq!(decoded.controller, "02abc...");
assert_eq!(decoded.protocol_id, "kvstore");
assert_eq!(decoded.tags, vec!["tag1"]);
}
#[test]
fn test_kvstore_entry_with_history() {
let history = vec![
"old_value".to_string(),
"middle_value".to_string(),
"current_value".to_string(),
];
let entry = KVStoreEntry::new("test_key", "current_value", "02abc...", "kvstore")
.with_history(history.clone());
assert_eq!(entry.history, Some(history.clone()));
let json = serde_json::to_string(&entry).unwrap();
assert!(json.contains("history"));
assert!(json.contains("old_value"));
let decoded: KVStoreEntry = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.history, Some(history));
}
#[test]
fn test_kvstore_entry_without_history_omits_field() {
let entry = KVStoreEntry::new("test_key", "test_value", "02abc...", "kvstore");
assert!(entry.history.is_none());
let json = serde_json::to_string(&entry).unwrap();
assert!(!json.contains("history"));
}
#[test]
fn test_kvstore_query_default() {
let query = KVStoreQuery::default();
assert!(query.key.is_none());
assert!(query.controller.is_none());
assert!(query.protocol_id.is_none());
assert!(query.tags.is_none());
assert!(query.limit.is_none());
}
#[test]
fn test_kvstore_query_builder() {
let query = KVStoreQuery::new()
.with_key("my_key")
.with_controller("02abc...")
.with_tags(vec!["important".to_string()])
.with_tag_query_mode("all")
.with_limit(10)
.with_skip(5)
.with_sort_order("desc");
assert_eq!(query.key, Some("my_key".to_string()));
assert_eq!(query.controller, Some("02abc...".to_string()));
assert_eq!(query.tags, Some(vec!["important".to_string()]));
assert_eq!(query.tag_query_mode, Some("all".to_string()));
assert_eq!(query.limit, Some(10));
assert_eq!(query.skip, Some(5));
assert_eq!(query.sort_order, Some("desc".to_string()));
}
#[test]
fn test_kvstore_query_serialization() {
let query = KVStoreQuery::new()
.with_key("my_key")
.with_tags(vec!["important".to_string()])
.with_tag_query_mode("all")
.with_limit(10);
let json = serde_json::to_string(&query).unwrap();
assert!(json.contains("my_key"));
assert!(json.contains("important"));
assert!(json.contains("tagQueryMode"));
let minimal = KVStoreQuery::new();
let json = serde_json::to_string(&minimal).unwrap();
assert_eq!(json, "{}");
}
#[test]
fn test_kvstore_query_to_json() {
let query = KVStoreQuery::new().with_key("test").with_limit(5);
let json = query.to_json();
assert_eq!(json["key"], "test");
assert_eq!(json["limit"], 5);
}
#[test]
fn test_kvstore_get_options() {
let opts = KVStoreGetOptions::new()
.with_history(true)
.with_include_token(true)
.with_service_name("ls_custom");
assert!(opts.history);
assert!(opts.include_token);
assert_eq!(opts.service_name, Some("ls_custom".to_string()));
}
#[test]
fn test_kvstore_set_options() {
let opts = KVStoreSetOptions::new()
.with_protocol_id("custom")
.with_description("Test operation")
.with_token_amount(100)
.with_tags(vec!["tag1".to_string()]);
assert_eq!(opts.protocol_id, Some("custom".to_string()));
assert_eq!(opts.description, Some("Test operation".to_string()));
assert_eq!(opts.token_amount, Some(100));
assert_eq!(opts.tags, Some(vec!["tag1".to_string()]));
}
#[test]
fn test_kvstore_remove_options() {
let opts = KVStoreRemoveOptions::new()
.with_protocol_id("custom")
.with_description("Remove operation");
assert_eq!(opts.protocol_id, Some("custom".to_string()));
assert_eq!(opts.description, Some("Remove operation".to_string()));
}
#[test]
fn test_lookup_value_result_not_found() {
let result = LookupValueResult::not_found("default".to_string());
assert_eq!(result.value, "default");
assert!(!result.value_exists);
assert!(result.outpoints.is_empty());
}
#[test]
fn test_lookup_value_result_found() {
let result = LookupValueResult::found(
"my_value".to_string(),
vec!["txid.0".to_string()],
Some(vec![1, 2, 3]),
vec![],
);
assert_eq!(result.value, "my_value");
assert!(result.value_exists);
assert_eq!(result.outpoints, vec!["txid.0"]);
assert!(result.input_beef.is_some());
}
#[test]
fn test_kv_protocol_fields() {
assert_eq!(KvProtocolFields::PROTOCOL_ID, 0);
assert_eq!(KvProtocolFields::KEY, 1);
assert_eq!(KvProtocolFields::VALUE, 2);
assert_eq!(KvProtocolFields::CONTROLLER, 3);
assert_eq!(KvProtocolFields::TAGS, 4);
assert_eq!(KvProtocolFields::SIGNATURE, 5);
assert_eq!(KvProtocolFields::MIN_FIELDS_OLD, 5);
assert_eq!(KvProtocolFields::MIN_FIELDS_NEW, 6);
}
}