use crate::primitives::{from_hex, to_hex, PublicKey};
use crate::script::templates::PushDrop;
use crate::script::LockingScript;
use crate::wallet::{
Counterparty, CreateActionArgs, CreateActionInput, CreateActionOutput, DecryptArgs,
EncryptArgs, GetPublicKeyArgs, ListOutputsArgs, Protocol, QueryMode, RelinquishOutputArgs,
SecurityLevel, SignActionArgs, SignActionSpend, WalletInterface,
};
use crate::{Error, Result};
use super::interpreter::KVStoreInterpreter;
use super::types::{
decode_value_with_ttl, encode_value_with_ttl, KVStoreConfig, KVStoreEntry, KVStoreGetOptions,
KVStoreQuery, KVStoreRemoveOptions, KVStoreSetOptions, KVStoreToken, LookupValueResult,
WalletOutput,
};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;
pub struct LocalKVStore<W: WalletInterface + std::fmt::Debug> {
wallet: W,
config: KVStoreConfig,
state: Arc<Mutex<LocalKVStoreState>>,
}
impl<W: WalletInterface + std::fmt::Debug> std::fmt::Debug for LocalKVStore<W> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LocalKVStore")
.field("wallet", &self.wallet)
.field("config", &self.config)
.finish()
}
}
#[derive(Default)]
struct LocalKVStoreState {
key_locks: HashMap<String, Vec<tokio::sync::oneshot::Sender<()>>>,
}
impl<W: WalletInterface + std::fmt::Debug> LocalKVStore<W> {
pub fn new(wallet: W, config: KVStoreConfig) -> Result<Self> {
if config.protocol_id.is_empty() {
return Err(Error::KvStoreEmptyContext);
}
Ok(Self {
wallet,
config,
state: Arc::new(Mutex::new(LocalKVStoreState::default())),
})
}
pub async fn get(&self, key: &str, default_value: &str) -> Result<String> {
if key.is_empty() {
return Err(Error::KvStoreInvalidKey);
}
let result = self.lookup_value(key, 5).await?;
if !result.value_exists {
return Ok(default_value.to_string());
}
let (value, is_expired) = decode_value_with_ttl(&result.value);
if is_expired {
return Ok(default_value.to_string());
}
Ok(value)
}
pub async fn get_entry(
&self,
key: &str,
options: Option<KVStoreGetOptions>,
) -> Result<Option<KVStoreEntry>> {
if key.is_empty() {
return Err(Error::KvStoreInvalidKey);
}
let result = self.lookup_value(key, 5).await?;
if !result.value_exists {
return Ok(None);
}
let (value, is_expired) = decode_value_with_ttl(&result.value);
if is_expired {
return Ok(None);
}
let options = options.unwrap_or_default();
let protocol_id = &self.config.protocol_id;
let pubkey_result = self
.wallet
.get_public_key(
GetPublicKeyArgs {
identity_key: true,
protocol_id: None,
key_id: None,
counterparty: None,
for_self: None,
},
self.originator(),
)
.await?;
let controller = pubkey_result.public_key.clone();
let mut entry = KVStoreEntry::new(&value, &value, &controller, protocol_id);
if options.include_token && !result.outpoints.is_empty() {
if let Some(output) = result.outputs.first() {
let parts: Vec<&str> = output.outpoint.split('.').collect();
if parts.len() == 2 {
let token =
KVStoreToken::new(parts[0], parts[1].parse().unwrap_or(0), output.satoshis)
.with_beef(result.input_beef.clone().unwrap_or_default());
entry = entry.with_token(token);
}
}
}
Ok(Some(entry))
}
pub async fn set(
&self,
key: &str,
value: &str,
options: Option<KVStoreSetOptions>,
) -> Result<String> {
if key.is_empty() {
return Err(Error::KvStoreInvalidKey);
}
if value.is_empty() {
return Err(Error::KvStoreInvalidValue);
}
let options = options.unwrap_or_default();
let protocol_id = options
.protocol_id
.as_ref()
.unwrap_or(&self.config.protocol_id);
self.acquire_key_lock(key).await;
let result = self.set_internal(key, value, protocol_id, &options).await;
self.release_key_lock(key).await;
result
}
async fn set_internal(
&self,
key: &str,
value: &str,
_protocol_id: &str,
options: &KVStoreSetOptions,
) -> Result<String> {
let stored_value = if let Some(ttl) = options.ttl {
encode_value_with_ttl(value, ttl)
} else {
value.to_string()
};
let lookup_result = self.lookup_value(key, 10).await?;
if lookup_result.value_exists
&& lookup_result.value == stored_value
&& !lookup_result.outpoints.is_empty()
{
return Ok(lookup_result
.outpoints
.last()
.expect("outpoints non-empty")
.clone());
}
let value_bytes = if self.config.encrypt {
self.encrypt_value(key, stored_value.as_bytes()).await?
} else {
stored_value.as_bytes().to_vec()
};
let locking_script = self.create_locking_script(key, &value_bytes).await?;
let inputs = self.build_inputs(&lookup_result)?;
let input_beef = lookup_result.input_beef.clone();
let token_amount = options.token_amount.unwrap_or(self.config.token_amount);
let description = options
.description
.clone()
.unwrap_or_else(|| format!("Update {} in {}", key, self.config.protocol_id));
let tags = options
.tags
.clone()
.unwrap_or_else(|| vec![key.to_string()]);
let outputs = vec![CreateActionOutput {
locking_script: locking_script.to_binary(),
satoshis: token_amount,
output_description: format!("KV entry: {}", key),
basket: Some(self.config.protocol_id.clone()),
custom_instructions: None,
tags: Some(tags),
}];
let create_result = self
.wallet
.create_action(
CreateActionArgs {
description,
inputs: if inputs.is_empty() {
None
} else {
Some(inputs.clone())
},
outputs: Some(outputs),
input_beef: input_beef.clone(),
lock_time: None,
version: None,
labels: None,
options: None,
},
self.originator(),
)
.await?;
if let Some(signable) = create_result.signable_transaction.as_ref() {
if !inputs.is_empty() {
let reference_str = to_hex(&signable.reference);
let spends = self
.prepare_spends(key, &lookup_result.outputs, &signable.tx, input_beef)
.await?;
let sign_result = self
.wallet
.sign_action(
SignActionArgs {
reference: reference_str,
spends,
options: None,
},
self.originator(),
)
.await;
if let Err(e) = sign_result {
for input in &inputs {
let _ = self
.wallet
.relinquish_output(
RelinquishOutputArgs {
basket: self.config.protocol_id.clone(),
output: input.outpoint.clone(),
},
self.originator(),
)
.await;
}
return Err(e);
}
}
}
match create_result.txid {
Some(txid) => Ok(format!("{}.0", to_hex(&txid))),
None => Err(Error::KvStoreError("No txid in result".to_string())),
}
}
pub async fn remove(
&self,
key: &str,
options: Option<KVStoreRemoveOptions>,
) -> Result<Vec<String>> {
if key.is_empty() {
return Err(Error::KvStoreInvalidKey);
}
let options = options.unwrap_or_default();
let mut txids = Vec::new();
loop {
let lookup_result = self.lookup_value(key, 100).await?;
if !lookup_result.value_exists || lookup_result.outputs.is_empty() {
break;
}
let inputs = self.build_inputs(&lookup_result)?;
let input_beef = lookup_result.input_beef.clone();
if inputs.is_empty() {
break;
}
let description = options
.description
.clone()
.unwrap_or_else(|| format!("Remove {} from {}", key, self.config.protocol_id));
let create_result = self
.wallet
.create_action(
CreateActionArgs {
description,
inputs: Some(inputs.clone()),
outputs: None, input_beef: input_beef.clone(),
lock_time: None,
version: None,
labels: None,
options: None,
},
self.originator(),
)
.await?;
if let Some(signable) = &create_result.signable_transaction {
let reference_str = to_hex(&signable.reference);
let spends = self
.prepare_spends(key, &lookup_result.outputs, &signable.tx, input_beef)
.await?;
let sign_result = self
.wallet
.sign_action(
SignActionArgs {
reference: reference_str,
spends,
options: None,
},
self.originator(),
)
.await;
if let Err(e) = sign_result {
for input in &inputs {
let _ = self
.wallet
.relinquish_output(
RelinquishOutputArgs {
basket: self.config.protocol_id.clone(),
output: input.outpoint.clone(),
},
self.originator(),
)
.await;
}
return Err(e);
}
}
if let Some(txid) = create_result.txid {
txids.push(to_hex(&txid));
}
if lookup_result.outputs.len() < 100 {
break;
}
}
Ok(txids)
}
pub async fn keys(&self) -> Result<Vec<String>> {
let entries = self.list(None).await?;
let keys: Vec<String> = entries.into_iter().map(|e| e.key).collect();
Ok(keys)
}
pub async fn list(&self, query: Option<KVStoreQuery>) -> Result<Vec<KVStoreEntry>> {
let query = query.unwrap_or_default();
let tags = query.key.as_ref().map(|k| vec![k.clone()]);
let list_result = self
.wallet
.list_outputs(
ListOutputsArgs {
basket: self.config.protocol_id.clone(),
tags,
tag_query_mode: Some(QueryMode::All),
include: Some(crate::wallet::OutputInclude::EntireTransactions),
include_custom_instructions: None,
include_tags: Some(true),
include_labels: None,
limit: query.limit,
offset: query.skip.map(|s| s as i32),
seek_permission: None,
},
self.originator(),
)
.await?;
let mut entries = Vec::new();
let pubkey_result = self
.wallet
.get_public_key(
GetPublicKeyArgs {
identity_key: true,
protocol_id: None,
key_id: None,
counterparty: None,
for_self: None,
},
self.originator(),
)
.await?;
let controller = pubkey_result.public_key.clone();
for output in &list_result.outputs {
let locking_script_bytes = match &output.locking_script {
Some(bytes) => bytes,
None => continue,
};
let script = match LockingScript::from_binary(locking_script_bytes) {
Ok(s) => s,
Err(_) => continue,
};
if let Some(fields) = KVStoreInterpreter::extract_fields(&script) {
let key = match fields.key_string() {
Some(k) => k,
None => continue,
};
let value_bytes = if self.config.encrypt {
match self.decrypt_value(&key, fields.value_bytes()).await {
Ok(v) => v,
Err(_) => continue,
}
} else {
fields.value.clone()
};
let value = String::from_utf8(value_bytes).unwrap_or_default();
let tags = fields.tags_vec();
entries.push(
KVStoreEntry::new(&key, &value, &controller, &self.config.protocol_id)
.with_tags(tags),
);
}
}
if let Some(filter_tags) = &query.tags {
let mode = query.tag_query_mode.as_deref().unwrap_or("all");
entries.retain(|e| {
if mode == "any" {
filter_tags.iter().any(|t| e.tags.contains(t))
} else {
filter_tags.iter().all(|t| e.tags.contains(t))
}
});
}
Ok(entries)
}
pub async fn has(&self, key: &str) -> Result<bool> {
if key.is_empty() {
return Err(Error::KvStoreInvalidKey);
}
let result = self.lookup_value(key, 1).await?;
Ok(result.value_exists)
}
pub async fn count(&self) -> Result<usize> {
let list_result = self
.wallet
.list_outputs(
ListOutputsArgs {
basket: self.config.protocol_id.clone(),
tags: None,
tag_query_mode: None,
include: None,
include_custom_instructions: None,
include_tags: None,
include_labels: None,
limit: None,
offset: None,
seek_permission: None,
},
self.originator(),
)
.await?;
Ok(list_result.total_outputs as usize)
}
pub async fn batch_get(&self, keys: &[&str]) -> Result<Vec<Option<String>>> {
let mut results = Vec::with_capacity(keys.len());
for key in keys {
if key.is_empty() {
return Err(Error::KvStoreInvalidKey);
}
let lookup = self.lookup_value(key, 5).await?;
if lookup.value_exists {
results.push(Some(lookup.value));
} else {
results.push(None);
}
}
Ok(results)
}
pub async fn batch_set(&self, entries: &[(&str, &str)]) -> Result<()> {
for (key, value) in entries {
self.set(key, value, None).await?;
}
Ok(())
}
pub async fn batch_remove(&self, keys: &[&str]) -> Result<()> {
for key in keys {
self.remove(key, None).await?;
}
Ok(())
}
pub async fn clear(&self) -> Result<()> {
let keys = self.keys().await?;
for key in keys {
self.remove(&key, None).await?;
}
Ok(())
}
fn originator(&self) -> &str {
self.config.originator.as_deref().unwrap_or("kvstore")
}
fn get_protocol(&self, _key: &str) -> Protocol {
Protocol::new(SecurityLevel::Counterparty, &self.config.protocol_id)
}
async fn encrypt_value(&self, key: &str, plaintext: &[u8]) -> Result<Vec<u8>> {
let protocol = self.get_protocol(key);
let result = self
.wallet
.encrypt(
EncryptArgs {
plaintext: plaintext.to_vec(),
protocol_id: protocol,
key_id: key.to_string(),
counterparty: Some(Counterparty::Self_),
},
self.originator(),
)
.await?;
Ok(result.ciphertext)
}
async fn decrypt_value(&self, key: &str, ciphertext: &[u8]) -> Result<Vec<u8>> {
let protocol = self.get_protocol(key);
let result = self
.wallet
.decrypt(
DecryptArgs {
ciphertext: ciphertext.to_vec(),
protocol_id: protocol,
key_id: key.to_string(),
counterparty: Some(Counterparty::Self_),
},
self.originator(),
)
.await?;
Ok(result.plaintext)
}
async fn create_locking_script(&self, key: &str, value: &[u8]) -> Result<LockingScript> {
let pubkey_result = self
.wallet
.get_public_key(
GetPublicKeyArgs {
identity_key: false,
protocol_id: Some(self.get_protocol(key)),
key_id: Some(key.to_string()),
counterparty: Some(Counterparty::Self_),
for_self: Some(true),
},
self.originator(),
)
.await?;
let pubkey = PublicKey::from_hex(&pubkey_result.public_key)?;
let fields = vec![value.to_vec()];
let pushdrop = PushDrop::new(pubkey, fields);
Ok(pushdrop.lock())
}
async fn lookup_value(&self, key: &str, limit: u32) -> Result<LookupValueResult> {
let list_result = self
.wallet
.list_outputs(
ListOutputsArgs {
basket: self.config.protocol_id.clone(),
tags: Some(vec![key.to_string()]),
tag_query_mode: Some(QueryMode::All),
include: Some(crate::wallet::OutputInclude::EntireTransactions),
include_custom_instructions: None,
include_tags: Some(true),
include_labels: None,
limit: Some(limit),
offset: None,
seek_permission: None,
},
self.originator(),
)
.await?;
if list_result.outputs.is_empty() {
return Ok(LookupValueResult::not_found(String::new()));
}
let last_output = list_result.outputs.last().expect("outputs non-empty");
let locking_script_bytes = last_output
.locking_script
.as_ref()
.ok_or_else(|| Error::KvStoreError("No locking script in output".to_string()))?;
let script = LockingScript::from_binary(locking_script_bytes)?;
let pushdrop = PushDrop::decode(&script)?;
if pushdrop.fields.is_empty() {
return Err(Error::KvStoreError(
"Invalid KVStore token: no fields".to_string(),
));
}
let value_bytes = &pushdrop.fields[0];
let value = if self.config.encrypt {
let decrypted = self.decrypt_value(key, value_bytes).await?;
String::from_utf8(decrypted).map_err(|e| Error::KvStoreError(e.to_string()))?
} else {
String::from_utf8(value_bytes.clone())
.map_err(|e| Error::KvStoreError(e.to_string()))?
};
let outpoints: Vec<String> = list_result
.outputs
.iter()
.map(|o| format!("{}.{}", to_hex(&o.outpoint.txid), o.outpoint.vout))
.collect();
let outputs: Vec<WalletOutput> = list_result
.outputs
.iter()
.filter_map(|o| {
o.locking_script.as_ref().map(|script| WalletOutput {
outpoint: format!("{}.{}", to_hex(&o.outpoint.txid), o.outpoint.vout),
satoshis: o.satoshis,
locking_script: script.clone(),
tags: o.tags.clone().unwrap_or_default(),
})
})
.collect();
Ok(LookupValueResult::found(
value,
outpoints,
list_result.beef,
outputs,
))
}
fn build_inputs(&self, lookup_result: &LookupValueResult) -> Result<Vec<CreateActionInput>> {
lookup_result
.outputs
.iter()
.map(|output| {
let parts: Vec<&str> = output.outpoint.split('.').collect();
let txid_bytes = from_hex(parts[0]).map_err(|_| {
Error::KvStoreCorruptedState(format!(
"invalid txid hex in outpoint: {}",
output.outpoint
))
})?;
if txid_bytes.len() != 32 {
return Err(Error::KvStoreCorruptedState(format!(
"txid must be 32 bytes, got {} in outpoint: {}",
txid_bytes.len(),
output.outpoint
)));
}
let mut txid = [0u8; 32];
txid.copy_from_slice(&txid_bytes);
let vout: u32 = parts
.get(1)
.ok_or_else(|| {
Error::KvStoreCorruptedState(format!(
"missing vout in outpoint: {}",
output.outpoint
))
})?
.parse()
.map_err(|_| {
Error::KvStoreCorruptedState(format!(
"invalid vout in outpoint: {}",
output.outpoint
))
})?;
Ok(CreateActionInput {
outpoint: crate::wallet::Outpoint::new(txid, vout),
input_description: "KV entry input".to_string(),
unlocking_script: None,
unlocking_script_length: Some(107), sequence_number: None,
})
})
.collect()
}
async fn prepare_spends(
&self,
_key: &str,
outputs: &[WalletOutput],
_tx_bytes: &[u8],
_input_beef: Option<Vec<u8>>,
) -> Result<HashMap<u32, SignActionSpend>> {
let mut spends = HashMap::new();
for (i, _output) in outputs.iter().enumerate() {
spends.insert(
i as u32,
SignActionSpend {
unlocking_script: Vec::new(),
sequence_number: None,
},
);
}
Ok(spends)
}
async fn acquire_key_lock(&self, key: &str) {
let mut state = self.state.lock().await;
if state.key_locks.contains_key(key) {
let (tx, rx) = tokio::sync::oneshot::channel();
state
.key_locks
.get_mut(key)
.expect("key exists in key_locks")
.push(tx);
drop(state);
let _ = rx.await;
} else {
state.key_locks.insert(key.to_string(), Vec::new());
}
}
async fn release_key_lock(&self, key: &str) {
let mut state = self.state.lock().await;
if let Some(queue) = state.key_locks.get_mut(key) {
if let Some(tx) = queue.pop() {
let _ = tx.send(());
} else {
state.key_locks.remove(key);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicBool, Ordering};
use crate::wallet::{
AbortActionArgs, AbortActionResult, AcquireCertificateArgs, AuthenticatedResult,
CreateActionResult, CreateHmacArgs, CreateHmacResult, CreateSignatureResult, DecryptResult,
DiscoverByAttributesArgs, DiscoverByIdentityKeyArgs, DiscoverCertificatesResult,
EncryptResult, GetHeaderArgs, GetHeaderResult, GetHeightResult, GetNetworkResult,
GetPublicKeyResult, GetVersionResult, InternalizeActionArgs, InternalizeActionResult,
ListActionsArgs, ListActionsResult, ListCertificatesArgs, ListCertificatesResult,
ListOutputsResult, ProveCertificateArgs, ProveCertificateResult, RelinquishCertificateArgs,
RelinquishCertificateResult, RelinquishOutputResult, RevealCounterpartyKeyLinkageResult,
RevealSpecificKeyLinkageResult, SignActionResult, VerifyHmacArgs, VerifyHmacResult,
WalletCertificate, WalletRevealCounterpartyArgs, WalletRevealSpecificArgs,
};
#[derive(Debug)]
struct MockWallet {
list_outputs_error: AtomicBool,
create_action_error: AtomicBool,
public_key_hex: String,
}
impl MockWallet {
fn new() -> Self {
let privkey = crate::primitives::PrivateKey::random();
let pubkey = privkey.public_key();
let pubkey_hex = crate::primitives::to_hex(&pubkey.to_compressed());
Self {
list_outputs_error: AtomicBool::new(false),
create_action_error: AtomicBool::new(false),
public_key_hex: pubkey_hex,
}
}
fn with_list_outputs_error(self) -> Self {
self.list_outputs_error.store(true, Ordering::SeqCst);
self
}
fn with_create_action_error(self) -> Self {
self.create_action_error.store(true, Ordering::SeqCst);
self
}
}
#[async_trait::async_trait]
impl WalletInterface for MockWallet {
async fn get_public_key(
&self,
_args: GetPublicKeyArgs,
_originator: &str,
) -> Result<GetPublicKeyResult> {
Ok(GetPublicKeyResult {
public_key: self.public_key_hex.clone(),
})
}
async fn encrypt(&self, args: EncryptArgs, _originator: &str) -> Result<EncryptResult> {
Ok(EncryptResult {
ciphertext: args.plaintext,
})
}
async fn decrypt(&self, args: DecryptArgs, _originator: &str) -> Result<DecryptResult> {
Ok(DecryptResult {
plaintext: args.ciphertext,
})
}
async fn create_hmac(
&self,
_args: CreateHmacArgs,
_originator: &str,
) -> Result<CreateHmacResult> {
Ok(CreateHmacResult { hmac: [0u8; 32] })
}
async fn verify_hmac(
&self,
_args: VerifyHmacArgs,
_originator: &str,
) -> Result<VerifyHmacResult> {
Ok(VerifyHmacResult { valid: true })
}
async fn create_signature(
&self,
_args: crate::wallet::CreateSignatureArgs,
_originator: &str,
) -> Result<CreateSignatureResult> {
Ok(CreateSignatureResult {
signature: vec![0u8; 64],
})
}
async fn verify_signature(
&self,
_args: crate::wallet::VerifySignatureArgs,
_originator: &str,
) -> Result<crate::wallet::VerifySignatureResult> {
Ok(crate::wallet::VerifySignatureResult { valid: true })
}
async fn reveal_counterparty_key_linkage(
&self,
_args: WalletRevealCounterpartyArgs,
_originator: &str,
) -> Result<RevealCounterpartyKeyLinkageResult> {
Err(Error::WalletError("not implemented".to_string()))
}
async fn reveal_specific_key_linkage(
&self,
_args: WalletRevealSpecificArgs,
_originator: &str,
) -> Result<RevealSpecificKeyLinkageResult> {
Err(Error::WalletError("not implemented".to_string()))
}
async fn create_action(
&self,
_args: CreateActionArgs,
_originator: &str,
) -> Result<CreateActionResult> {
if self.create_action_error.load(Ordering::SeqCst) {
return Err(Error::WalletError("wallet error".to_string()));
}
Ok(CreateActionResult {
txid: Some([0u8; 32]),
tx: None,
no_send_change: None,
send_with_results: None,
signable_transaction: None,
input_type: None,
inputs: None,
reference_number: None,
beef: None,
})
}
async fn sign_action(
&self,
_args: SignActionArgs,
_originator: &str,
) -> Result<SignActionResult> {
Ok(SignActionResult {
txid: Some([0u8; 32]),
tx: None,
send_with_results: None,
})
}
async fn abort_action(
&self,
_args: AbortActionArgs,
_originator: &str,
) -> Result<AbortActionResult> {
Ok(AbortActionResult { aborted: true })
}
async fn list_actions(
&self,
_args: ListActionsArgs,
_originator: &str,
) -> Result<ListActionsResult> {
Ok(ListActionsResult {
actions: vec![],
total_actions: 0,
})
}
async fn internalize_action(
&self,
_args: InternalizeActionArgs,
_originator: &str,
) -> Result<InternalizeActionResult> {
Ok(InternalizeActionResult { accepted: true })
}
async fn list_outputs(
&self,
_args: ListOutputsArgs,
_originator: &str,
) -> Result<ListOutputsResult> {
if self.list_outputs_error.load(Ordering::SeqCst) {
return Err(Error::WalletError("wallet error".to_string()));
}
Ok(ListOutputsResult {
outputs: vec![],
total_outputs: 0,
beef: None,
})
}
async fn relinquish_output(
&self,
_args: RelinquishOutputArgs,
_originator: &str,
) -> Result<RelinquishOutputResult> {
Ok(RelinquishOutputResult { relinquished: true })
}
async fn acquire_certificate(
&self,
_args: AcquireCertificateArgs,
_originator: &str,
) -> Result<WalletCertificate> {
Err(Error::WalletError("not implemented".to_string()))
}
async fn list_certificates(
&self,
_args: ListCertificatesArgs,
_originator: &str,
) -> Result<ListCertificatesResult> {
Ok(ListCertificatesResult {
certificates: vec![],
total_certificates: 0,
})
}
async fn prove_certificate(
&self,
_args: ProveCertificateArgs,
_originator: &str,
) -> Result<ProveCertificateResult> {
Err(Error::WalletError("not implemented".to_string()))
}
async fn relinquish_certificate(
&self,
_args: RelinquishCertificateArgs,
_originator: &str,
) -> Result<RelinquishCertificateResult> {
Ok(RelinquishCertificateResult { relinquished: true })
}
async fn discover_by_identity_key(
&self,
_args: DiscoverByIdentityKeyArgs,
_originator: &str,
) -> Result<DiscoverCertificatesResult> {
Ok(DiscoverCertificatesResult {
certificates: vec![],
total_certificates: 0,
})
}
async fn discover_by_attributes(
&self,
_args: DiscoverByAttributesArgs,
_originator: &str,
) -> Result<DiscoverCertificatesResult> {
Ok(DiscoverCertificatesResult {
certificates: vec![],
total_certificates: 0,
})
}
async fn is_authenticated(&self, _originator: &str) -> Result<AuthenticatedResult> {
Ok(AuthenticatedResult {
authenticated: true,
})
}
async fn wait_for_authentication(&self, _originator: &str) -> Result<AuthenticatedResult> {
Ok(AuthenticatedResult {
authenticated: true,
})
}
async fn get_height(&self, _originator: &str) -> Result<GetHeightResult> {
Ok(GetHeightResult { height: 0 })
}
async fn get_header_for_height(
&self,
_args: GetHeaderArgs,
_originator: &str,
) -> Result<GetHeaderResult> {
Err(Error::WalletError("not implemented".to_string()))
}
async fn get_network(&self, _originator: &str) -> Result<GetNetworkResult> {
Ok(GetNetworkResult {
network: crate::wallet::Network::Mainnet,
})
}
async fn get_version(&self, _originator: &str) -> Result<GetVersionResult> {
Ok(GetVersionResult {
version: "mock-1.0".to_string(),
})
}
}
#[test]
fn test_local_kvstore_config() {
let config = KVStoreConfig::default();
assert_eq!(config.protocol_id, "kvstore");
assert!(config.encrypt);
}
#[test]
fn test_new_local_kvstore_empty_context() {
let wallet = MockWallet::new();
let config = KVStoreConfig::new().with_protocol_id("");
let result = LocalKVStore::new(wallet, config);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::KvStoreEmptyContext));
}
#[test]
fn test_new_local_kvstore_success() {
let wallet = MockWallet::new();
let config = KVStoreConfig::default();
let result = LocalKVStore::new(wallet, config);
assert!(result.is_ok());
}
#[tokio::test]
async fn test_get_empty_key() {
let wallet = MockWallet::new();
let store = LocalKVStore::new(wallet, KVStoreConfig::default()).unwrap();
let result = store.get("", "default").await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::KvStoreInvalidKey));
}
#[tokio::test]
async fn test_set_empty_key() {
let wallet = MockWallet::new();
let store = LocalKVStore::new(wallet, KVStoreConfig::default()).unwrap();
let result = store.set("", "value", None).await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::KvStoreInvalidKey));
}
#[tokio::test]
async fn test_remove_empty_key() {
let wallet = MockWallet::new();
let store = LocalKVStore::new(wallet, KVStoreConfig::default()).unwrap();
let result = store.remove("", None).await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::KvStoreInvalidKey));
}
#[tokio::test]
async fn test_set_empty_value() {
let wallet = MockWallet::new();
let store = LocalKVStore::new(wallet, KVStoreConfig::default()).unwrap();
let result = store.set("key", "", None).await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::KvStoreInvalidValue));
}
#[tokio::test]
async fn test_get_wallet_error() {
let wallet = MockWallet::new().with_list_outputs_error();
let store = LocalKVStore::new(wallet, KVStoreConfig::default()).unwrap();
let result = store.get("key", "default").await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, Error::WalletError(_)));
}
#[tokio::test]
async fn test_get_returns_default_when_not_found() {
let wallet = MockWallet::new(); let store = LocalKVStore::new(wallet, KVStoreConfig::default()).unwrap();
let result = store.get("nonexistent_key", "my_default").await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), "my_default");
}
#[tokio::test]
async fn test_set_success() {
let wallet = MockWallet::new();
let store = LocalKVStore::new(wallet, KVStoreConfig::default()).unwrap();
let result = store.set("key1", "value1", None).await;
if let Err(ref e) = result {
eprintln!("Set failed with error: {:?}", e);
}
assert!(result.is_ok(), "Set should succeed but got: {:?}", result);
}
#[tokio::test]
async fn test_set_wallet_error() {
let wallet = MockWallet::new().with_create_action_error();
let store = LocalKVStore::new(wallet, KVStoreConfig::default()).unwrap();
let result = store.set("key1", "value1", None).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, Error::WalletError(_)));
}
#[tokio::test]
async fn test_remove_list_outputs_error() {
let wallet = MockWallet::new().with_list_outputs_error();
let store = LocalKVStore::new(wallet, KVStoreConfig::default()).unwrap();
let result = store.remove("key1", None).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, Error::WalletError(_)));
}
#[tokio::test]
async fn test_remove_not_found_returns_empty() {
let wallet = MockWallet::new(); let store = LocalKVStore::new(wallet, KVStoreConfig::default()).unwrap();
let result = store.remove("nonexistent_key", None).await;
assert!(result.is_ok());
assert!(result.unwrap().is_empty());
}
#[test]
fn test_lookup_value_result_not_found() {
let result = LookupValueResult::not_found("default".to_string());
assert!(!result.value_exists);
assert_eq!(result.value, "default");
}
#[test]
fn test_lookup_value_result_found() {
let result = LookupValueResult::found(
"my_value".to_string(),
vec!["txid.0".to_string()],
None,
vec![],
);
assert!(result.value_exists);
assert_eq!(result.value, "my_value");
}
#[tokio::test]
async fn test_local_batch_set_and_get() {
let wallet = MockWallet::new();
let store = LocalKVStore::new(wallet, KVStoreConfig::default()).unwrap();
let entries = vec![("key1", "value1"), ("key2", "value2"), ("key3", "value3")];
let result = store.batch_set(&entries).await;
assert!(result.is_ok(), "batch_set should succeed: {:?}", result);
let keys = vec!["key1", "key2", "key3"];
let result = store.batch_get(&keys).await;
assert!(result.is_ok(), "batch_get should succeed: {:?}", result);
let values = result.unwrap();
assert_eq!(values.len(), 3);
for v in &values {
assert!(v.is_none());
}
}
#[tokio::test]
async fn test_local_batch_remove() {
let wallet = MockWallet::new();
let store = LocalKVStore::new(wallet, KVStoreConfig::default()).unwrap();
let entries = vec![("key1", "value1"), ("key2", "value2")];
store.batch_set(&entries).await.unwrap();
let keys = vec!["key1", "key2"];
let result = store.batch_remove(&keys).await;
assert!(result.is_ok(), "batch_remove should succeed: {:?}", result);
}
#[tokio::test]
async fn test_local_batch_get_missing_keys() {
let wallet = MockWallet::new();
let store = LocalKVStore::new(wallet, KVStoreConfig::default()).unwrap();
store.set("key1", "value1", None).await.unwrap();
let keys = vec!["key1", "key2", "key3"];
let result = store.batch_get(&keys).await;
assert!(result.is_ok());
let values = result.unwrap();
assert_eq!(values.len(), 3);
assert!(values[0].is_none());
assert!(values[1].is_none());
assert!(values[2].is_none());
}
#[tokio::test]
async fn test_local_batch_empty() {
let wallet = MockWallet::new();
let store = LocalKVStore::new(wallet, KVStoreConfig::default()).unwrap();
let empty_get: Vec<&str> = vec![];
let result = store.batch_get(&empty_get).await;
assert!(result.is_ok());
assert!(result.unwrap().is_empty());
let empty_set: Vec<(&str, &str)> = vec![];
let result = store.batch_set(&empty_set).await;
assert!(result.is_ok());
let empty_remove: Vec<&str> = vec![];
let result = store.batch_remove(&empty_remove).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_local_batch_get_invalid_key() {
let wallet = MockWallet::new();
let store = LocalKVStore::new(wallet, KVStoreConfig::default()).unwrap();
let keys = vec!["valid_key", ""];
let result = store.batch_get(&keys).await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::KvStoreInvalidKey));
}
#[tokio::test]
async fn test_local_batch_get_wallet_error() {
let wallet = MockWallet::new().with_list_outputs_error();
let store = LocalKVStore::new(wallet, KVStoreConfig::default()).unwrap();
let keys = vec!["key1", "key2"];
let result = store.batch_get(&keys).await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::WalletError(_)));
}
#[tokio::test]
async fn test_local_batch_set_wallet_error() {
let wallet = MockWallet::new().with_create_action_error();
let store = LocalKVStore::new(wallet, KVStoreConfig::default()).unwrap();
let entries = vec![("key1", "value1"), ("key2", "value2")];
let result = store.batch_set(&entries).await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::WalletError(_)));
}
#[tokio::test]
async fn test_local_batch_remove_wallet_error() {
let wallet = MockWallet::new().with_list_outputs_error();
let store = LocalKVStore::new(wallet, KVStoreConfig::default()).unwrap();
let keys = vec!["key1", "key2"];
let result = store.batch_remove(&keys).await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::WalletError(_)));
}
#[test]
fn test_ttl_encode_decode_roundtrip() {
use super::super::types::{decode_value_with_ttl, encode_value_with_ttl};
let encoded = encode_value_with_ttl("hello", std::time::Duration::from_secs(3600));
let (decoded, is_expired) = decode_value_with_ttl(&encoded);
assert_eq!(decoded, "hello");
assert!(!is_expired, "Entry with 1 hour TTL should not be expired");
}
#[test]
fn test_ttl_expired_value() {
use super::super::types::decode_value_with_ttl;
let envelope = r#"{"v":"old_value","e":1000000}"#;
let (decoded, is_expired) = decode_value_with_ttl(envelope);
assert_eq!(decoded, "old_value");
assert!(is_expired, "Entry with past expiration should be expired");
}
#[test]
fn test_ttl_plain_value_not_expired() {
use super::super::types::decode_value_with_ttl;
let (decoded, is_expired) = decode_value_with_ttl("plain_value");
assert_eq!(decoded, "plain_value");
assert!(!is_expired, "Plain values should never be expired");
}
#[test]
fn test_ttl_json_that_is_not_envelope() {
use super::super::types::decode_value_with_ttl;
let (decoded, is_expired) = decode_value_with_ttl(r#"{"name":"Alice"}"#);
assert_eq!(decoded, r#"{"name":"Alice"}"#);
assert!(!is_expired, "Non-envelope JSON should not be expired");
}
#[test]
fn test_ttl_zero_duration() {
use super::super::types::{decode_value_with_ttl, encode_value_with_ttl};
let encoded = encode_value_with_ttl("ephemeral", std::time::Duration::from_secs(0));
let (decoded, is_expired) = decode_value_with_ttl(&encoded);
assert_eq!(decoded, "ephemeral");
assert!(is_expired, "Zero-TTL entry should be expired immediately");
}
#[test]
fn test_ttl_set_options_builder() {
let opts = KVStoreSetOptions::new().with_ttl(std::time::Duration::from_secs(300));
assert_eq!(opts.ttl, Some(std::time::Duration::from_secs(300)));
}
#[test]
fn test_ttl_set_options_default_no_ttl() {
let opts = KVStoreSetOptions::default();
assert!(opts.ttl.is_none());
}
#[tokio::test]
async fn test_set_with_ttl_success() {
let wallet = MockWallet::new();
let store = LocalKVStore::new(wallet, KVStoreConfig::default()).unwrap();
let opts = KVStoreSetOptions::new().with_ttl(std::time::Duration::from_secs(3600));
let result = store.set("ttl_key", "ttl_value", Some(opts)).await;
assert!(result.is_ok(), "Set with TTL should succeed: {:?}", result);
}
#[test]
fn test_ttl_value_with_special_characters() {
use super::super::types::{decode_value_with_ttl, encode_value_with_ttl};
let value = r#"{"key": "val\"ue", "num": 42}"#;
let encoded = encode_value_with_ttl(value, std::time::Duration::from_secs(3600));
let (decoded, is_expired) = decode_value_with_ttl(&encoded);
assert_eq!(decoded, value);
assert!(!is_expired);
}
#[test]
fn test_ttl_future_expiration() {
use super::super::types::decode_value_with_ttl;
let future_ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
+ 999_999;
let envelope = format!(r#"{{"v":"future","e":{}}}"#, future_ts);
let (decoded, is_expired) = decode_value_with_ttl(&envelope);
assert_eq!(decoded, "future");
assert!(!is_expired, "Future expiration should not be expired");
}
}