1use crate::Error;
2use ark_core::contract::BoardingContract;
3use ark_core::contract::ContractContext;
4use ark_core::contract::ContractSpec;
5use ark_core::contract::ContractState;
6use ark_core::contract::ContractType;
7use ark_core::contract::DefaultVtxoContract;
8use ark_core::contract::DelegateVtxoContract;
9use ark_core::contract::SpendSelection;
10use ark_core::contract::StoredContract;
11use ark_core::contract::VhtlcContract;
12use ark_core::server;
13use ark_core::server::VirtualTxOutPoint;
14use ark_core::ArkAddress;
15use ark_core::BoardingOutput;
16use ark_core::Vtxo;
17use bitcoin::Address;
18use bitcoin::Amount;
19use bitcoin::Network;
20use bitcoin::Script;
21use bitcoin::ScriptBuf;
22use bitcoin::Sequence;
23use bitcoin::XOnlyPublicKey;
24use std::collections::HashMap;
25use std::marker::PhantomData;
26#[cfg(feature = "sqlite")]
27use std::path::Path;
28#[cfg(feature = "sqlite")]
29use std::sync::Mutex;
30use std::time::SystemTime;
31use std::time::UNIX_EPOCH;
32
33trait DynContractHandler: Send + Sync {
34 fn contract_type(&self) -> ContractType;
35 fn validate(&self, stored: &StoredContract, ctx: &ContractContext) -> Result<(), Error>;
36 fn spendable_selections(
37 &self,
38 stored: &StoredContract,
39 ctx: &ContractContext,
40 ) -> Result<Vec<SpendSelection>, Error>;
41}
42
43struct ContractHandler<T> {
44 _marker: PhantomData<T>,
45}
46
47impl<T> Default for ContractHandler<T> {
48 fn default() -> Self {
49 Self {
50 _marker: PhantomData,
51 }
52 }
53}
54
55impl<T: ContractSpec> DynContractHandler for ContractHandler<T> {
56 fn contract_type(&self) -> ContractType {
57 T::contract_type()
58 }
59
60 fn validate(&self, stored: &StoredContract, ctx: &ContractContext) -> Result<(), Error> {
61 if stored.contract_type != T::contract_type() {
62 return Err(Error::ad_hoc("unexpected contract type"));
63 }
64 if stored.contract_version != T::VERSION {
65 return Err(Error::ad_hoc(format!(
66 "unsupported contract version: {}",
67 stored.contract_version
68 )));
69 }
70
71 let data: T = serde_json::from_value(stored.data.clone())
72 .map_err(|e| Error::ad_hoc(format!("failed to decode contract data: {e}")))?;
73 let derived_script = data.script_pubkey(ctx)?;
74 if derived_script != stored.script_pubkey {
75 return Err(Error::ad_hoc("contract script mismatch"));
76 }
77
78 Ok(())
79 }
80
81 fn spendable_selections(
82 &self,
83 stored: &StoredContract,
84 ctx: &ContractContext,
85 ) -> Result<Vec<SpendSelection>, Error> {
86 self.validate(stored, ctx)?;
87 let data: T = serde_json::from_value(stored.data.clone())
88 .map_err(|e| Error::ad_hoc(format!("failed to decode contract data: {e}")))?;
89 data.spendable_selections(ctx).map_err(Into::into)
90 }
91}
92
93#[derive(Default)]
94pub struct ContractRegistry {
95 handlers: HashMap<ContractType, Box<dyn DynContractHandler>>,
96}
97
98impl ContractRegistry {
99 pub fn new() -> Self {
100 Self::default()
101 }
102
103 pub fn register<T: ContractSpec>(&mut self) -> Result<(), Error> {
104 let contract_type = T::contract_type();
105 if self.handlers.contains_key(&contract_type) {
106 return Err(Error::ad_hoc(format!(
107 "contract handler already registered: {contract_type}"
108 )));
109 }
110 let handler = Box::new(ContractHandler::<T>::default());
111 debug_assert_eq!(handler.contract_type(), contract_type);
112 self.handlers.insert(contract_type, handler);
113 Ok(())
114 }
115
116 fn handler_for(&self, contract_type: &ContractType) -> Result<&dyn DynContractHandler, Error> {
117 self.handlers
118 .get(contract_type)
119 .map(|handler| handler.as_ref())
120 .ok_or_else(|| Error::ad_hoc(format!("unknown contract type: {contract_type}")))
121 }
122}
123
124pub trait ContractStore: Send + Sync {
129 fn insert(&mut self, contract: StoredContract) -> Result<(), Error>;
130 fn get_by_script(&self, script_pubkey: &Script) -> Result<Option<StoredContract>, Error>;
131 fn list(&self) -> Result<Vec<StoredContract>, Error>;
132 fn update_state(&mut self, script_pubkey: &Script, state: ContractState) -> Result<(), Error>;
133}
134
135#[derive(Default)]
136pub struct MemoryContractStore {
137 contracts: HashMap<ScriptBuf, StoredContract>,
138}
139
140impl MemoryContractStore {
141 pub fn new() -> Self {
142 Self::default()
143 }
144}
145
146impl ContractStore for MemoryContractStore {
147 fn insert(&mut self, contract: StoredContract) -> Result<(), Error> {
148 if self.contracts.contains_key(&contract.script_pubkey) {
149 return Err(Error::ad_hoc("contract script already exists"));
150 }
151 self.contracts
152 .insert(contract.script_pubkey.clone(), contract);
153 Ok(())
154 }
155
156 fn get_by_script(&self, script_pubkey: &Script) -> Result<Option<StoredContract>, Error> {
157 Ok(self.contracts.get(script_pubkey).cloned())
158 }
159
160 fn list(&self) -> Result<Vec<StoredContract>, Error> {
161 Ok(self.contracts.values().cloned().collect())
162 }
163
164 fn update_state(&mut self, script_pubkey: &Script, state: ContractState) -> Result<(), Error> {
165 let contract = self
166 .contracts
167 .get_mut(script_pubkey)
168 .ok_or_else(|| Error::ad_hoc("unknown contract script"))?;
169 contract.state = state;
170 Ok(())
171 }
172}
173
174#[cfg(feature = "sqlite")]
175pub struct SqliteContractStore {
176 connection: Mutex<rusqlite::Connection>,
177}
178
179#[cfg(feature = "sqlite")]
180impl SqliteContractStore {
181 pub fn new<P: AsRef<Path>>(db_path: P) -> Result<Self, Error> {
182 let db_path = db_path.as_ref();
183 if let Some(parent) = db_path.parent() {
184 std::fs::create_dir_all(parent).map_err(|e| {
185 Error::consumer(format!("failed to create contract store directory: {e}"))
186 })?;
187 }
188
189 let connection = rusqlite::Connection::open(db_path)
190 .map_err(|e| Error::consumer(format!("failed to open contract store: {e}")))?;
191 let store = Self {
192 connection: Mutex::new(connection),
193 };
194 store.initialize()?;
195 Ok(store)
196 }
197
198 pub fn new_default() -> Result<Self, Error> {
199 Self::new("contracts.db")
200 }
201
202 fn initialize(&self) -> Result<(), Error> {
203 let connection = self.connection()?;
204 connection
205 .execute_batch(
206 "CREATE TABLE IF NOT EXISTS contracts (
207 script_pubkey BLOB PRIMARY KEY NOT NULL,
208 contract_type TEXT NOT NULL,
209 contract_version INTEGER NOT NULL,
210 state TEXT NOT NULL,
211 created_at INTEGER NOT NULL,
212 key_index INTEGER,
213 data TEXT NOT NULL
214 );",
215 )
216 .map_err(|e| Error::consumer(format!("failed to initialize contract store: {e}")))?;
217 Ok(())
218 }
219
220 fn connection(&self) -> Result<std::sync::MutexGuard<'_, rusqlite::Connection>, Error> {
221 self.connection
222 .lock()
223 .map_err(|_| Error::ad_hoc("contract store connection lock poisoned"))
224 }
225
226 fn state_to_str(state: ContractState) -> &'static str {
227 match state {
228 ContractState::Active => "active",
229 ContractState::Inactive => "inactive",
230 }
231 }
232
233 fn state_from_str(value: &str) -> Result<ContractState, Error> {
234 match value {
235 "active" => Ok(ContractState::Active),
236 "inactive" => Ok(ContractState::Inactive),
237 _ => Err(Error::ad_hoc(format!("unknown contract state: {value}"))),
238 }
239 }
240
241 fn row_to_contract(row: &rusqlite::Row<'_>) -> rusqlite::Result<StoredContract> {
242 let script_pubkey: Vec<u8> = row.get("script_pubkey")?;
243 let contract_type: String = row.get("contract_type")?;
244 let contract_version: i64 = row.get("contract_version")?;
245 let state: String = row.get("state")?;
246 let created_at: i64 = row.get("created_at")?;
247 let key_index: Option<i64> = row.get("key_index")?;
248 let data: String = row.get("data")?;
249
250 let contract_type = ContractType::new(contract_type).map_err(|e| {
251 rusqlite::Error::FromSqlConversionFailure(1, rusqlite::types::Type::Text, Box::new(e))
252 })?;
253 let state = Self::state_from_str(&state).map_err(|e| {
254 rusqlite::Error::FromSqlConversionFailure(3, rusqlite::types::Type::Text, Box::new(e))
255 })?;
256 let data = serde_json::from_str(&data).map_err(|e| {
257 rusqlite::Error::FromSqlConversionFailure(6, rusqlite::types::Type::Text, Box::new(e))
258 })?;
259
260 Ok(StoredContract {
261 contract_type,
262 contract_version: u32::try_from(contract_version).map_err(|e| {
263 rusqlite::Error::FromSqlConversionFailure(
264 2,
265 rusqlite::types::Type::Integer,
266 Box::new(e),
267 )
268 })?,
269 script_pubkey: ScriptBuf::from_bytes(script_pubkey),
270 state,
271 created_at: u64::try_from(created_at).map_err(|e| {
272 rusqlite::Error::FromSqlConversionFailure(
273 4,
274 rusqlite::types::Type::Integer,
275 Box::new(e),
276 )
277 })?,
278 key_index: key_index
279 .map(|value| {
280 u32::try_from(value).map_err(|e| {
281 rusqlite::Error::FromSqlConversionFailure(
282 5,
283 rusqlite::types::Type::Integer,
284 Box::new(e),
285 )
286 })
287 })
288 .transpose()?,
289 data,
290 })
291 }
292}
293
294#[cfg(feature = "sqlite")]
295impl ContractStore for SqliteContractStore {
296 fn insert(&mut self, contract: StoredContract) -> Result<(), Error> {
297 let data = serde_json::to_string(&contract.data)
298 .map_err(|e| Error::ad_hoc(format!("failed to encode contract data: {e}")))?;
299 let connection = self.connection()?;
300 connection
301 .execute(
302 "INSERT INTO contracts (
303 script_pubkey,
304 contract_type,
305 contract_version,
306 state,
307 created_at,
308 key_index,
309 data
310 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
311 rusqlite::params![
312 contract.script_pubkey.as_bytes(),
313 contract.contract_type.as_str(),
314 i64::from(contract.contract_version),
315 Self::state_to_str(contract.state),
316 i64::try_from(contract.created_at).map_err(|e| Error::ad_hoc(format!(
317 "contract created_at does not fit sqlite integer: {e}"
318 )))?,
319 contract.key_index.map(i64::from),
320 data,
321 ],
322 )
323 .map_err(|e| {
324 if matches!(e, rusqlite::Error::SqliteFailure(ref err, _) if err.extended_code == rusqlite::ffi::SQLITE_CONSTRAINT_PRIMARYKEY)
325 {
326 Error::ad_hoc("contract script already exists")
327 } else {
328 Error::consumer(format!("failed to insert contract: {e}"))
329 }
330 })?;
331 Ok(())
332 }
333
334 fn get_by_script(&self, script_pubkey: &Script) -> Result<Option<StoredContract>, Error> {
335 let connection = self.connection()?;
336 let mut statement = connection
337 .prepare(
338 "SELECT script_pubkey, contract_type, contract_version, state, created_at, key_index, data
339 FROM contracts
340 WHERE script_pubkey = ?1",
341 )
342 .map_err(|e| Error::consumer(format!("failed to prepare contract lookup: {e}")))?;
343 let mut rows = statement
344 .query(rusqlite::params![script_pubkey.as_bytes()])
345 .map_err(|e| Error::consumer(format!("failed to lookup contract: {e}")))?;
346 let Some(row) = rows
347 .next()
348 .map_err(|e| Error::consumer(format!("failed to read contract: {e}")))?
349 else {
350 return Ok(None);
351 };
352 Self::row_to_contract(row)
353 .map(Some)
354 .map_err(|e| Error::consumer(format!("failed to decode contract: {e}")))
355 }
356
357 fn list(&self) -> Result<Vec<StoredContract>, Error> {
358 let connection = self.connection()?;
359 let mut statement = connection
360 .prepare(
361 "SELECT script_pubkey, contract_type, contract_version, state, created_at, key_index, data
362 FROM contracts
363 ORDER BY created_at, rowid",
364 )
365 .map_err(|e| Error::consumer(format!("failed to prepare contract list: {e}")))?;
366 let rows = statement
367 .query_map([], Self::row_to_contract)
368 .map_err(|e| Error::consumer(format!("failed to list contracts: {e}")))?;
369 rows.collect::<rusqlite::Result<Vec<_>>>()
370 .map_err(|e| Error::consumer(format!("failed to decode contracts: {e}")))
371 }
372
373 fn update_state(&mut self, script_pubkey: &Script, state: ContractState) -> Result<(), Error> {
374 let connection = self.connection()?;
375 let updated = connection
376 .execute(
377 "UPDATE contracts SET state = ?1 WHERE script_pubkey = ?2",
378 rusqlite::params![Self::state_to_str(state), script_pubkey.as_bytes()],
379 )
380 .map_err(|e| Error::consumer(format!("failed to update contract state: {e}")))?;
381 if updated == 0 {
382 return Err(Error::ad_hoc("unknown contract script"));
383 }
384 Ok(())
385 }
386}
387
388#[derive(Clone, Debug, PartialEq)]
393pub struct AnnotatedVtxo {
394 contract: StoredContract,
395 vtxo: VirtualTxOutPoint,
396 spend_selections: Vec<SpendSelection>,
397}
398
399impl AnnotatedVtxo {
400 pub(crate) fn new(
401 contract: StoredContract,
402 vtxo: VirtualTxOutPoint,
403 spend_selections: Vec<SpendSelection>,
404 ) -> Self {
405 Self {
406 contract,
407 vtxo,
408 spend_selections,
409 }
410 }
411
412 pub fn contract(&self) -> &StoredContract {
413 &self.contract
414 }
415
416 pub fn vtxo(&self) -> &VirtualTxOutPoint {
417 &self.vtxo
418 }
419
420 pub fn spend_selections(&self) -> &[SpendSelection] {
421 &self.spend_selections
422 }
423
424 pub fn spend_selection(
425 &self,
426 kind: ark_core::contract::SpendPathKind,
427 ) -> Result<SpendSelection, Error> {
428 self.spend_selections
429 .iter()
430 .find(|selection| selection.path.kind == kind)
431 .cloned()
432 .ok_or_else(|| Error::ad_hoc(format!("missing {kind:?} spend path")))
433 }
434
435 pub fn tapscripts(&self) -> Vec<ScriptBuf> {
436 self.spend_selections
437 .iter()
438 .map(|selection| selection.path.script.clone())
439 .collect()
440 }
441
442 pub fn script_pubkey(&self) -> ScriptBuf {
443 self.contract.script_pubkey.clone()
444 }
445
446 pub fn server_pk(&self) -> Result<XOnlyPublicKey, Error> {
447 Ok(self.vtxo_contract_data()?.server)
448 }
449
450 pub fn owner_pk(&self) -> Result<XOnlyPublicKey, Error> {
451 Ok(self.vtxo_contract_data()?.owner)
452 }
453
454 pub fn exit_delay(&self) -> Result<Sequence, Error> {
455 Ok(self.vtxo_contract_data()?.exit_delay)
456 }
457
458 fn vtxo_contract_data(&self) -> Result<VtxoContractData, Error> {
459 offchain_vtxo_data(&self.contract)
460 }
461}
462
463#[derive(Clone, Copy, Debug, PartialEq, Eq)]
464struct VtxoContractData {
465 server: XOnlyPublicKey,
466 owner: XOnlyPublicKey,
467 exit_delay: Sequence,
468}
469
470#[derive(Clone, Debug, PartialEq)]
475pub struct AnnotatedBoardingOutput {
476 contract: StoredContract,
477 output: BoardingOutput,
478 spend_selections: Vec<SpendSelection>,
479}
480
481impl AnnotatedBoardingOutput {
482 pub(crate) fn new(
483 contract: StoredContract,
484 output: BoardingOutput,
485 spend_selections: Vec<SpendSelection>,
486 ) -> Self {
487 Self {
488 contract,
489 output,
490 spend_selections,
491 }
492 }
493
494 pub fn contract(&self) -> &StoredContract {
495 &self.contract
496 }
497
498 pub fn output(&self) -> &BoardingOutput {
499 &self.output
500 }
501
502 pub fn spend_selections(&self) -> &[SpendSelection] {
503 &self.spend_selections
504 }
505
506 pub fn spend_selection(
507 &self,
508 kind: ark_core::contract::SpendPathKind,
509 ) -> Result<SpendSelection, Error> {
510 self.spend_selections
511 .iter()
512 .find(|selection| selection.path.kind == kind)
513 .cloned()
514 .ok_or_else(|| Error::ad_hoc(format!("missing {kind:?} spend path")))
515 }
516
517 pub fn tapscripts(&self) -> Vec<ScriptBuf> {
518 self.spend_selections
519 .iter()
520 .map(|selection| selection.path.script.clone())
521 .collect()
522 }
523
524 pub fn address(&self) -> &Address {
525 self.output.address()
526 }
527
528 pub fn script_pubkey(&self) -> ScriptBuf {
529 self.contract.script_pubkey.clone()
530 }
531
532 pub fn server_pk(&self) -> XOnlyPublicKey {
533 self.output.server_pk()
534 }
535
536 pub fn owner_pk(&self) -> XOnlyPublicKey {
537 self.output.owner_pk()
538 }
539
540 pub fn exit_delay(&self) -> Sequence {
541 self.output.exit_delay()
542 }
543
544 pub fn can_be_claimed_unilaterally_by_owner(
545 &self,
546 now: std::time::Duration,
547 confirmation_blocktime: std::time::Duration,
548 confirmations: u64,
549 ) -> bool {
550 self.output
551 .can_be_claimed_unilaterally_by_owner(now, confirmation_blocktime, confirmations)
552 }
553}
554
555#[derive(Clone, Debug, PartialEq)]
556pub(crate) struct ActiveOffchainContract {
557 pub address: ArkAddress,
558 pub vtxo: Vtxo,
559 pub spend_selections: Vec<SpendSelection>,
560}
561
562impl ActiveOffchainContract {
563 pub fn spend_selection(
564 &self,
565 kind: ark_core::contract::SpendPathKind,
566 ) -> Result<SpendSelection, Error> {
567 self.spend_selections
568 .iter()
569 .find(|selection| selection.path.kind == kind)
570 .cloned()
571 .ok_or_else(|| Error::ad_hoc(format!("missing {kind:?} spend path")))
572 }
573}
574
575#[derive(Clone, Debug)]
576pub struct AnnotatedVtxoList {
577 dust: Amount,
578 vtxos: Vec<AnnotatedVtxo>,
579}
580
581impl AnnotatedVtxoList {
582 pub fn new(dust: Amount, vtxos: Vec<AnnotatedVtxo>) -> Self {
583 Self { dust, vtxos }
584 }
585
586 pub fn into_inner(self) -> Vec<AnnotatedVtxo> {
587 self.vtxos
588 }
589
590 pub fn all(&self) -> impl Iterator<Item = &AnnotatedVtxo> {
591 self.vtxos.iter()
592 }
593
594 pub fn all_unspent(&self) -> impl Iterator<Item = &AnnotatedVtxo> {
595 let dust = self.dust;
596 self.vtxos
597 .iter()
598 .filter(move |entry| entry.vtxo.is_unspent(dust))
599 }
600
601 pub fn spendable_offchain(&self) -> impl Iterator<Item = &AnnotatedVtxo> {
602 let dust = self.dust;
603 self.vtxos
604 .iter()
605 .filter(move |entry| entry.vtxo.is_spendable_offchain(dust))
606 }
607
608 pub fn spendable_offchain_at<'a>(
609 &'a self,
610 server_info: &'a server::Info,
611 now_unix_secs: i64,
612 ) -> impl Iterator<Item = &'a AnnotatedVtxo> + 'a {
613 self.spendable_offchain().filter(move |entry| {
614 !entry
615 .server_pk()
616 .map(|server_pk| server_info.signer_requires_recovery_at(server_pk, now_unix_secs))
617 .unwrap_or(false)
618 })
619 }
620
621 pub fn pending_recovery_due_to_signer_at<'a>(
622 &'a self,
623 server_info: &'a server::Info,
624 now_unix_secs: i64,
625 ) -> impl Iterator<Item = &'a AnnotatedVtxo> + 'a {
626 self.spendable_offchain().filter(move |entry| {
627 entry
628 .server_pk()
629 .map(|server_pk| server_info.signer_requires_recovery_at(server_pk, now_unix_secs))
630 .unwrap_or(false)
631 })
632 }
633
634 pub fn batch_settleable_at<'a>(
635 &'a self,
636 server_info: &'a server::Info,
637 now_unix_secs: i64,
638 ) -> impl Iterator<Item = &'a AnnotatedVtxo> + 'a {
639 self.all_unspent().filter(move |entry| {
640 entry.vtxo.is_recoverable(server_info.dust)
641 || !entry
642 .server_pk()
643 .map(|server_pk| {
644 server_info.signer_requires_recovery_at(server_pk, now_unix_secs)
645 })
646 .unwrap_or(false)
647 })
648 }
649
650 pub fn pre_confirmed(&self) -> impl Iterator<Item = &AnnotatedVtxo> {
651 let dust = self.dust;
652 self.vtxos
653 .iter()
654 .filter(move |entry| entry.vtxo.is_pre_confirmed_spendable(dust))
655 }
656
657 pub fn confirmed(&self) -> impl Iterator<Item = &AnnotatedVtxo> {
658 let dust = self.dust;
659 self.vtxos
660 .iter()
661 .filter(move |entry| entry.vtxo.is_confirmed_spendable(dust))
662 }
663
664 pub fn recoverable(&self) -> impl Iterator<Item = &AnnotatedVtxo> {
665 self.vtxos
666 .iter()
667 .filter(move |entry| entry.vtxo.is_recoverable(self.dust))
668 }
669
670 pub fn could_exit_unilaterally(&self) -> impl Iterator<Item = &AnnotatedVtxo> {
671 self.pre_confirmed().chain(self.confirmed())
672 }
673
674 pub fn spent(&self) -> impl Iterator<Item = &AnnotatedVtxo> {
675 let dust = self.dust;
676 self.vtxos
677 .iter()
678 .filter(move |entry| entry.vtxo.is_spent_status(dust))
679 }
680}
681
682pub struct ContractManager {
690 network: Network,
691 registry: ContractRegistry,
692 store: Box<dyn ContractStore>,
693}
694
695impl ContractManager {
696 pub fn new(network: Network, store: Box<dyn ContractStore>) -> Self {
697 Self {
698 network,
699 registry: ContractRegistry::new(),
700 store,
701 }
702 }
703
704 pub fn new_with_builtins(
705 network: Network,
706 store: Box<dyn ContractStore>,
707 ) -> Result<Self, Error> {
708 let mut manager = Self::new(network, store);
709 manager.register_builtins()?;
710 Ok(manager)
711 }
712
713 pub fn in_memory(network: Network) -> Self {
714 Self::new(network, Box::new(MemoryContractStore::new()))
715 }
716
717 pub fn in_memory_with_builtins(network: Network) -> Result<Self, Error> {
718 Self::new_with_builtins(network, Box::new(MemoryContractStore::new()))
719 }
720
721 pub fn network(&self) -> Network {
722 self.network
723 }
724
725 pub fn register<T: ContractSpec>(&mut self) -> Result<(), Error> {
726 self.registry.register::<T>()
727 }
728
729 pub fn register_builtins(&mut self) -> Result<(), Error> {
730 self.register::<DefaultVtxoContract>()?;
731 self.register::<DelegateVtxoContract>()?;
732 self.register::<BoardingContract>()?;
733 self.register::<VhtlcContract>()
734 }
735
736 pub fn insert<T: ContractSpec>(
737 &mut self,
738 contract: T,
739 state: ContractState,
740 key_index: Option<u32>,
741 ) -> Result<StoredContract, Error> {
742 let stored = self.stored_contract(contract, state, key_index)?;
743 self.store.insert(stored.clone())?;
744 Ok(stored)
745 }
746
747 pub fn insert_or_get<T: ContractSpec>(
748 &mut self,
749 contract: T,
750 state: ContractState,
751 key_index: Option<u32>,
752 ) -> Result<StoredContract, Error> {
753 let stored = self.stored_contract(contract, state, key_index)?;
754
755 match self.store.get_by_script(&stored.script_pubkey)? {
756 None => {
757 self.store.insert(stored.clone())?;
758 Ok(stored)
759 }
760 Some(existing) if same_stored_contract(&existing, &stored) => Ok(existing),
761 Some(existing) if can_share_script_row(&existing, &stored)? => Ok(existing),
762 Some(_) => Err(Error::ad_hoc(
763 "contract script already exists with different data",
764 )),
765 }
766 }
767
768 fn stored_contract<T: ContractSpec>(
769 &self,
770 contract: T,
771 state: ContractState,
772 key_index: Option<u32>,
773 ) -> Result<StoredContract, Error> {
774 let ctx = ContractContext::new(self.network);
775 let stored = StoredContract {
776 contract_type: T::contract_type(),
777 contract_version: T::VERSION,
778 script_pubkey: contract.script_pubkey(&ctx)?,
779 state,
780 created_at: now_secs()?,
781 key_index,
782 data: serde_json::to_value(contract)
783 .map_err(|e| Error::ad_hoc(format!("failed to encode contract data: {e}")))?,
784 };
785
786 let handler = self.registry.handler_for(&stored.contract_type)?;
787 handler.validate(&stored, &ctx)?;
788 Ok(stored)
789 }
790
791 pub fn insert_stored(&mut self, stored: StoredContract) -> Result<(), Error> {
792 let ctx = ContractContext::new(self.network);
793 let handler = self.registry.handler_for(&stored.contract_type)?;
794 handler.validate(&stored, &ctx)?;
795 self.store.insert(stored)
796 }
797
798 pub fn get(&self, script_pubkey: &Script) -> Result<Option<StoredContract>, Error> {
799 self.store.get_by_script(script_pubkey)
800 }
801
802 pub fn get_typed<T: ContractSpec>(&self, script_pubkey: &Script) -> Result<Option<T>, Error> {
803 let Some(stored) = self.store.get_by_script(script_pubkey)? else {
804 return Ok(None);
805 };
806
807 if stored.contract_type != T::contract_type() {
808 return Err(Error::ad_hoc("unexpected contract type"));
809 }
810 if stored.contract_version != T::VERSION {
811 return Err(Error::ad_hoc(format!(
812 "unsupported contract version: {}",
813 stored.contract_version
814 )));
815 }
816
817 serde_json::from_value(stored.data)
818 .map(Some)
819 .map_err(|e| Error::ad_hoc(format!("failed to decode contract data: {e}")))
820 }
821
822 pub fn list(&self) -> Result<Vec<StoredContract>, Error> {
823 self.store.list()
824 }
825
826 pub fn list_by_type(&self, contract_type: ContractType) -> Result<Vec<StoredContract>, Error> {
827 Ok(self
828 .store
829 .list()?
830 .into_iter()
831 .filter(|contract| contract.contract_type == contract_type)
832 .collect())
833 }
834
835 pub fn list_active_by_type(
836 &self,
837 contract_type: ContractType,
838 ) -> Result<Vec<StoredContract>, Error> {
839 Ok(self
840 .list_by_type(contract_type)?
841 .into_iter()
842 .filter(|contract| contract.state == ContractState::Active)
843 .collect())
844 }
845
846 pub fn update_state(
847 &mut self,
848 script_pubkey: &Script,
849 state: ContractState,
850 ) -> Result<(), Error> {
851 self.store.update_state(script_pubkey, state)
852 }
853
854 pub fn spendable_selections(
855 &self,
856 stored: &StoredContract,
857 ) -> Result<Vec<SpendSelection>, Error> {
858 let ctx = ContractContext::new(self.network);
859 let handler = self.registry.handler_for(&stored.contract_type)?;
860 handler.spendable_selections(stored, &ctx)
861 }
862
863 pub(crate) fn active_offchain_contracts(
864 &self,
865 unilateral_exit_delay_candidates: &[Sequence],
866 ) -> Result<Vec<ActiveOffchainContract>, Error> {
867 let ctx = ContractContext::new(self.network);
868 self.store
869 .list()?
870 .into_iter()
871 .filter(|stored| stored.state == ContractState::Active)
872 .filter_map(|stored| {
873 match active_offchain_contract_from_stored(
874 self,
875 &ctx,
876 stored,
877 unilateral_exit_delay_candidates,
878 ) {
879 Ok(Some(contract)) => Some(Ok(contract)),
880 Ok(None) => None,
881 Err(e) => Some(Err(e)),
882 }
883 })
884 .collect()
885 }
886
887 pub fn annotate_vtxos(
888 &self,
889 vtxos: Vec<VirtualTxOutPoint>,
890 ) -> Result<Vec<AnnotatedVtxo>, Error> {
891 vtxos
892 .into_iter()
893 .map(|vtxo| {
894 let contract = self
895 .store
896 .get_by_script(&vtxo.script)?
897 .ok_or_else(|| Error::ad_hoc("unknown contract script"))?;
898 let spend_selections = self.spendable_selections(&contract)?;
899 Ok(AnnotatedVtxo::new(contract, vtxo, spend_selections))
900 })
901 .collect()
902 }
903
904 pub(crate) fn annotated_boarding_outputs_for_exit_delays(
911 &self,
912 compatible_default_exit_delays: &[Sequence],
913 ) -> Result<Vec<AnnotatedBoardingOutput>, Error> {
914 let ctx = ContractContext::new(self.network);
915 self.store
916 .list()?
917 .into_iter()
918 .filter(|stored| stored.state == ContractState::Active)
919 .filter_map(|stored| {
920 boarding_contract_from_stored(&stored, compatible_default_exit_delays)
921 .map(|contract| (stored, contract))
922 })
923 .map(|(stored, contract)| {
924 let output = contract.boarding_output(&ctx)?;
925 let spend_selections = self.spendable_selections(&stored)?;
926 Ok(AnnotatedBoardingOutput::new(
927 stored,
928 output,
929 spend_selections,
930 ))
931 })
932 .collect()
933 }
934
935 pub fn annotated_boarding_outputs(&self) -> Result<Vec<AnnotatedBoardingOutput>, Error> {
936 self.annotated_boarding_outputs_for_exit_delays(&[])
937 }
938}
939
940fn same_stored_contract(a: &StoredContract, b: &StoredContract) -> bool {
941 a.contract_type == b.contract_type
942 && a.contract_version == b.contract_version
943 && a.data == b.data
944}
945
946fn can_share_script_row(a: &StoredContract, b: &StoredContract) -> Result<bool, Error> {
952 let default_vtxo_boarding = a.contract_type == ContractType::default_vtxo()
953 && b.contract_type == ContractType::boarding();
954 let boarding_default_vtxo = a.contract_type == ContractType::boarding()
955 && b.contract_type == ContractType::default_vtxo();
956 if !default_vtxo_boarding && !boarding_default_vtxo {
957 return Ok(false);
958 }
959
960 Ok(two_leaf_vtxo_data(a)? == two_leaf_vtxo_data(b)?)
963}
964
965fn active_offchain_contract_from_stored(
966 manager: &ContractManager,
967 ctx: &ContractContext,
968 stored: StoredContract,
969 unilateral_exit_delay_candidates: &[Sequence],
970) -> Result<Option<ActiveOffchainContract>, Error> {
971 if stored.contract_type == ContractType::delegate_vtxo() {
972 let contract: DelegateVtxoContract = serde_json::from_value(stored.data.clone())
973 .map_err(|e| Error::ad_hoc(format!("failed to decode delegate vtxo contract: {e}")))?;
974 return Ok(Some(active_offchain_contract(
975 manager,
976 &stored,
977 contract.vtxo(ctx)?,
978 )?));
979 }
980
981 if stored.contract_type != ContractType::default_vtxo()
982 && stored.contract_type != ContractType::boarding()
983 {
984 return Ok(None);
985 }
986
987 let data = two_leaf_vtxo_data(&stored)?;
988
989 if stored.contract_type == ContractType::boarding()
993 && !unilateral_exit_delay_candidates.contains(&data.exit_delay)
994 {
995 return Ok(None);
996 }
997
998 let contract = DefaultVtxoContract {
999 server: data.server,
1000 owner: data.owner,
1001 exit_delay: data.exit_delay,
1002 };
1003 Ok(Some(active_offchain_contract(
1004 manager,
1005 &stored,
1006 contract.vtxo(ctx)?,
1007 )?))
1008}
1009
1010fn active_offchain_contract(
1011 manager: &ContractManager,
1012 stored: &StoredContract,
1013 vtxo: Vtxo,
1014) -> Result<ActiveOffchainContract, Error> {
1015 Ok(ActiveOffchainContract {
1016 address: vtxo.to_ark_address(),
1017 vtxo,
1018 spend_selections: manager.spendable_selections(stored)?,
1019 })
1020}
1021
1022fn offchain_vtxo_data(stored: &StoredContract) -> Result<VtxoContractData, Error> {
1023 if stored.contract_type == ContractType::delegate_vtxo() {
1024 return delegate_vtxo_data(stored);
1025 }
1026 two_leaf_vtxo_data(stored)
1027}
1028
1029fn delegate_vtxo_data(stored: &StoredContract) -> Result<VtxoContractData, Error> {
1030 if stored.contract_type != ContractType::delegate_vtxo() {
1031 return Err(Error::ad_hoc(format!(
1032 "contract type {} is not a delegate vtxo contract",
1033 stored.contract_type
1034 )));
1035 }
1036 let contract: DelegateVtxoContract = serde_json::from_value(stored.data.clone())
1037 .map_err(|e| Error::ad_hoc(format!("failed to decode delegate vtxo contract: {e}")))?;
1038 Ok(VtxoContractData {
1039 server: contract.server,
1040 owner: contract.owner,
1041 exit_delay: contract.exit_delay,
1042 })
1043}
1044
1045fn two_leaf_vtxo_data(stored: &StoredContract) -> Result<VtxoContractData, Error> {
1051 if stored.contract_type == ContractType::default_vtxo() {
1052 let contract: DefaultVtxoContract = serde_json::from_value(stored.data.clone())
1053 .map_err(|e| Error::ad_hoc(format!("failed to decode default vtxo contract: {e}")))?;
1054 return Ok(VtxoContractData {
1055 server: contract.server,
1056 owner: contract.owner,
1057 exit_delay: contract.exit_delay,
1058 });
1059 }
1060 if stored.contract_type == ContractType::boarding() {
1061 let contract: BoardingContract = serde_json::from_value(stored.data.clone())
1062 .map_err(|e| Error::ad_hoc(format!("failed to decode boarding contract: {e}")))?;
1063 return Ok(VtxoContractData {
1064 server: contract.server,
1065 owner: contract.owner,
1066 exit_delay: contract.exit_delay,
1067 });
1068 }
1069 Err(Error::ad_hoc(format!(
1070 "contract type {} is not a two-leaf vtxo contract",
1071 stored.contract_type
1072 )))
1073}
1074
1075fn boarding_contract_from_stored(
1081 stored: &StoredContract,
1082 compatible_default_vtxo_exit_delays: &[Sequence],
1083) -> Option<BoardingContract> {
1084 let data = two_leaf_vtxo_data(stored).ok()?;
1085
1086 if stored.contract_type == ContractType::boarding()
1089 || compatible_default_vtxo_exit_delays.contains(&data.exit_delay)
1090 {
1091 return Some(BoardingContract {
1092 server: data.server,
1093 owner: data.owner,
1094 exit_delay: data.exit_delay,
1095 });
1096 }
1097
1098 None
1099}
1100
1101fn now_secs() -> Result<u64, Error> {
1102 SystemTime::now()
1103 .duration_since(UNIX_EPOCH)
1104 .map(|duration| duration.as_secs())
1105 .map_err(|e| Error::ad_hoc(format!("system clock before unix epoch: {e}")))
1106}
1107
1108#[cfg(test)]
1109mod tests {
1110 use super::*;
1111 use ark_core::contract::SpendPathKind;
1112 use bitcoin::Amount;
1113 use bitcoin::OutPoint;
1114 use bitcoin::Sequence;
1115 use bitcoin::XOnlyPublicKey;
1116 use std::str::FromStr;
1117
1118 fn test_keys() -> (XOnlyPublicKey, XOnlyPublicKey, XOnlyPublicKey) {
1119 let server = XOnlyPublicKey::from_str(
1120 "18845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166",
1121 )
1122 .unwrap();
1123 let owner = XOnlyPublicKey::from_str(
1124 "28845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166",
1125 )
1126 .unwrap();
1127 let delegator = XOnlyPublicKey::from_str(
1128 "38845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166",
1129 )
1130 .unwrap();
1131 (server, owner, delegator)
1132 }
1133
1134 #[test]
1135 fn stores_and_dispatches_default_contract() {
1136 let (server, owner, _) = test_keys();
1137 let mut manager = ContractManager::in_memory(Network::Regtest);
1138 manager.register_builtins().unwrap();
1139
1140 let contract = DefaultVtxoContract {
1141 server,
1142 owner,
1143 exit_delay: Sequence::from_seconds_ceil(86400).unwrap(),
1144 };
1145 let stored = manager
1146 .insert(contract.clone(), ContractState::Active, Some(7))
1147 .unwrap();
1148
1149 assert_eq!(stored.contract_type, ContractType::default_vtxo());
1150 assert_eq!(stored.key_index, Some(7));
1151 assert_eq!(
1152 manager.get(&stored.script_pubkey).unwrap(),
1153 Some(stored.clone())
1154 );
1155 assert_eq!(
1156 manager
1157 .get_typed::<DefaultVtxoContract>(&stored.script_pubkey)
1158 .unwrap(),
1159 Some(contract)
1160 );
1161
1162 let selections = manager.spendable_selections(&stored).unwrap();
1163 assert_eq!(selections.len(), 2);
1164 assert!(selections
1165 .iter()
1166 .all(|selection| !selection.path.script.is_empty()));
1167 }
1168
1169 #[test]
1170 fn annotates_vtxos_with_contract_spend_paths() {
1171 let (server, owner, _) = test_keys();
1172 let mut manager = ContractManager::in_memory(Network::Regtest);
1173 manager.register_builtins().unwrap();
1174
1175 let contract = DefaultVtxoContract {
1176 server,
1177 owner,
1178 exit_delay: Sequence::from_seconds_ceil(86400).unwrap(),
1179 };
1180 let stored = manager
1181 .insert(contract, ContractState::Active, Some(7))
1182 .unwrap();
1183 let vtxo = VirtualTxOutPoint {
1184 outpoint: OutPoint::null(),
1185 created_at: 0,
1186 expires_at: 0,
1187 amount: Amount::from_sat(42_000),
1188 script: stored.script_pubkey.clone(),
1189 is_preconfirmed: false,
1190 is_swept: false,
1191 is_unrolled: false,
1192 is_spent: false,
1193 spent_by: None,
1194 commitment_txids: Vec::new(),
1195 settled_by: None,
1196 ark_txid: None,
1197 assets: Vec::new(),
1198 };
1199
1200 let annotated = manager.annotate_vtxos(vec![vtxo.clone()]).unwrap();
1201
1202 assert_eq!(annotated.len(), 1);
1203 assert_eq!(annotated[0].contract, stored);
1204 assert_eq!(annotated[0].vtxo, vtxo);
1205 assert_eq!(annotated[0].spend_selections.len(), 2);
1206 }
1207
1208 #[test]
1209 fn annotates_boarding_outputs_with_contract_spend_paths() {
1210 let (server, owner, _) = test_keys();
1211 let mut manager = ContractManager::in_memory(Network::Regtest);
1212 manager.register_builtins().unwrap();
1213
1214 let contract = BoardingContract {
1215 server,
1216 owner,
1217 exit_delay: Sequence::from_seconds_ceil(86400).unwrap(),
1218 };
1219 let stored = manager
1220 .insert(contract, ContractState::Active, Some(7))
1221 .unwrap();
1222
1223 let annotated = manager.annotated_boarding_outputs().unwrap();
1224
1225 assert_eq!(annotated.len(), 1);
1226 assert_eq!(annotated[0].contract, stored);
1227 assert_eq!(annotated[0].script_pubkey(), stored.script_pubkey);
1228 assert_eq!(annotated[0].server_pk(), server);
1229 assert_eq!(annotated[0].owner_pk(), owner);
1230 assert_eq!(annotated[0].spend_selections.len(), 2);
1231 assert!(annotated[0].spend_selection(SpendPathKind::Forfeit).is_ok());
1232 assert!(annotated[0].spend_selection(SpendPathKind::Exit).is_ok());
1233 }
1234
1235 #[test]
1236 fn default_vtxo_and_boarding_can_share_script_row() {
1237 let (server, owner, _) = test_keys();
1238 let mut manager = ContractManager::in_memory(Network::Regtest);
1239 manager.register_builtins().unwrap();
1240 let exit_delay = Sequence::from_seconds_ceil(86400).unwrap();
1241
1242 let default = DefaultVtxoContract {
1243 server,
1244 owner,
1245 exit_delay,
1246 };
1247 let boarding = BoardingContract {
1248 server,
1249 owner,
1250 exit_delay,
1251 };
1252
1253 let stored_default = manager
1254 .insert_or_get(default, ContractState::Active, Some(7))
1255 .unwrap();
1256 let stored_boarding = manager
1257 .insert_or_get(boarding, ContractState::Active, Some(7))
1258 .unwrap();
1259
1260 assert_eq!(stored_boarding, stored_default);
1261 assert_eq!(stored_default.contract_type, ContractType::default_vtxo());
1262 assert_eq!(manager.list().unwrap().len(), 1);
1263
1264 let boarding_outputs = manager
1265 .annotated_boarding_outputs_for_exit_delays(&[exit_delay])
1266 .unwrap();
1267 assert_eq!(boarding_outputs.len(), 1);
1268 assert_eq!(boarding_outputs[0].contract, stored_default);
1269 assert_eq!(boarding_outputs[0].server_pk(), server);
1270 assert_eq!(boarding_outputs[0].owner_pk(), owner);
1271 }
1272
1273 #[test]
1274 fn malformed_builtin_contract_fails_vtxo_annotation() {
1275 let (server, owner, _) = test_keys();
1276 let ctx = ContractContext::new(Network::Regtest);
1277 let script_pubkey = DefaultVtxoContract {
1278 server,
1279 owner,
1280 exit_delay: Sequence::from_height(10),
1281 }
1282 .script_pubkey(&ctx)
1283 .unwrap();
1284 let mut store = MemoryContractStore::default();
1285 let stored = StoredContract {
1286 contract_type: ContractType::default_vtxo(),
1287 contract_version: DefaultVtxoContract::VERSION,
1288 script_pubkey,
1289 state: ContractState::Active,
1290 created_at: 0,
1291 key_index: None,
1292 data: serde_json::json!({"bad": "shape"}),
1293 };
1294 store.insert(stored.clone()).unwrap();
1295 let mut manager = ContractManager::new(Network::Regtest, Box::new(store));
1296 manager.register_builtins().unwrap();
1297 let vtxo = VirtualTxOutPoint {
1298 outpoint: OutPoint::null(),
1299 created_at: 0,
1300 expires_at: 0,
1301 amount: Amount::from_sat(42_000),
1302 script: stored.script_pubkey,
1303 is_preconfirmed: false,
1304 is_swept: false,
1305 is_unrolled: false,
1306 is_spent: false,
1307 spent_by: None,
1308 commitment_txids: Vec::new(),
1309 settled_by: None,
1310 ark_txid: None,
1311 assets: Vec::new(),
1312 };
1313
1314 let err = manager.annotate_vtxos(vec![vtxo]).unwrap_err();
1315
1316 assert!(
1317 format!("{err:?}").contains("failed to decode contract data"),
1318 "{err:?}"
1319 );
1320 }
1321
1322 #[test]
1323 fn boarding_contract_can_annotate_offchain_vtxo() {
1324 let (server, owner, _) = test_keys();
1325 let mut manager = ContractManager::in_memory(Network::Regtest);
1326 manager.register_builtins().unwrap();
1327 let exit_delay = Sequence::from_seconds_ceil(86400).unwrap();
1328
1329 let stored = manager
1330 .insert_or_get(
1331 BoardingContract {
1332 server,
1333 owner,
1334 exit_delay,
1335 },
1336 ContractState::Active,
1337 Some(7),
1338 )
1339 .unwrap();
1340 let vtxo = VirtualTxOutPoint {
1341 outpoint: OutPoint::null(),
1342 created_at: 0,
1343 expires_at: 0,
1344 amount: Amount::from_sat(42_000),
1345 script: stored.script_pubkey.clone(),
1346 is_preconfirmed: false,
1347 is_swept: false,
1348 is_unrolled: false,
1349 is_spent: false,
1350 spent_by: None,
1351 commitment_txids: Vec::new(),
1352 settled_by: None,
1353 ark_txid: None,
1354 assets: Vec::new(),
1355 };
1356
1357 let annotated = manager.annotate_vtxos(vec![vtxo]).unwrap();
1358
1359 assert_eq!(annotated.len(), 1);
1360 assert_eq!(annotated[0].contract, stored);
1361 assert_eq!(annotated[0].server_pk().unwrap(), server);
1362 assert_eq!(annotated[0].owner_pk().unwrap(), owner);
1363 assert_eq!(annotated[0].exit_delay().unwrap(), exit_delay);
1364 }
1365
1366 #[cfg(feature = "sqlite")]
1367 #[test]
1368 fn sqlite_store_persists_contracts() {
1369 let (server, owner, _) = test_keys();
1370 let tempdir = tempfile::tempdir().unwrap();
1371 let db_path = tempdir.path().join("contracts.db");
1372 let mut manager = ContractManager::new(
1373 Network::Regtest,
1374 Box::new(SqliteContractStore::new(&db_path).unwrap()),
1375 );
1376 manager.register_builtins().unwrap();
1377
1378 let contract = DefaultVtxoContract {
1379 server,
1380 owner,
1381 exit_delay: Sequence::from_seconds_ceil(86400).unwrap(),
1382 };
1383 let stored = manager
1384 .insert(contract, ContractState::Active, Some(7))
1385 .unwrap();
1386 manager
1387 .update_state(&stored.script_pubkey, ContractState::Inactive)
1388 .unwrap();
1389
1390 let mut reopened = ContractManager::new(
1391 Network::Regtest,
1392 Box::new(SqliteContractStore::new(&db_path).unwrap()),
1393 );
1394 reopened.register_builtins().unwrap();
1395
1396 let persisted = reopened.get(&stored.script_pubkey).unwrap().unwrap();
1397 assert_eq!(persisted.state, ContractState::Inactive);
1398 assert_eq!(persisted.contract_type, ContractType::default_vtxo());
1399 assert_eq!(persisted.key_index, Some(7));
1400 assert_eq!(persisted.data, stored.data);
1401 assert_eq!(reopened.list().unwrap().len(), 1);
1402 }
1403
1404 #[test]
1405 fn store_enforces_script_uniqueness() {
1406 let (server, owner, _) = test_keys();
1407 let mut manager = ContractManager::in_memory(Network::Regtest);
1408 manager.register_builtins().unwrap();
1409 let contract = DefaultVtxoContract {
1410 server,
1411 owner,
1412 exit_delay: Sequence::from_seconds_ceil(86400).unwrap(),
1413 };
1414
1415 manager
1416 .insert(contract.clone(), ContractState::Active, None)
1417 .unwrap();
1418 assert!(manager
1419 .insert(contract, ContractState::Active, None)
1420 .is_err());
1421 }
1422
1423 #[test]
1424 fn validates_script_mismatch() {
1425 let (server, owner, delegator) = test_keys();
1426 let mut manager = ContractManager::in_memory(Network::Regtest);
1427 manager.register_builtins().unwrap();
1428 let default = DefaultVtxoContract {
1429 server,
1430 owner,
1431 exit_delay: Sequence::from_seconds_ceil(86400).unwrap(),
1432 };
1433 let delegate = DelegateVtxoContract {
1434 server,
1435 owner,
1436 delegator,
1437 exit_delay: Sequence::from_seconds_ceil(86400).unwrap(),
1438 };
1439 let ctx = ContractContext::new(Network::Regtest);
1440 let stored = StoredContract {
1441 contract_type: ContractType::default_vtxo(),
1442 contract_version: DefaultVtxoContract::VERSION,
1443 script_pubkey: delegate.script_pubkey(&ctx).unwrap(),
1444 state: ContractState::Active,
1445 created_at: 0,
1446 key_index: None,
1447 data: serde_json::to_value(default).unwrap(),
1448 };
1449
1450 assert!(manager.insert_stored(stored).is_err());
1451 }
1452}