Skip to main content

drive/util/batch/drive_op_batch/
mod.rs

1mod address_funds;
2mod contract;
3mod contract_fee_pot;
4mod contract_group;
5mod contract_moderation;
6mod document;
7mod drive_methods;
8pub(crate) mod finalize_task;
9mod group;
10mod identity;
11mod prefunded_specialized_balance;
12mod shielded;
13mod system;
14mod token;
15mod withdrawals;
16
17use crate::util::batch::GroveDbOpBatch;
18
19use crate::drive::contract::moderation::types::ContractTeamActionWrite;
20use crate::drive::document::preallocation_bindings_targeting;
21use crate::drive::Drive;
22use crate::error::Error;
23use crate::fees::op::LowLevelDriveOperation;
24use dpp::block::block_info::BlockInfo;
25use dpp::fee::Credits;
26
27pub use address_funds::AddressFundsOperationType;
28pub use contract::DataContractOperationType;
29pub use contract_fee_pot::ContractFeePotOperationType;
30pub use contract_group::ContractGroupOperationType;
31pub use contract_moderation::ContractModerationOperationType;
32pub use document::DocumentOperation;
33pub use document::DocumentOperationType;
34pub use document::DocumentOperationsForContractDocumentType;
35pub use document::UpdateOperationInfo;
36pub use group::GroupOperationType;
37pub use identity::IdentityOperationType;
38pub use prefunded_specialized_balance::PrefundedSpecializedBalanceOperationType;
39pub use shielded::ShieldedPoolOperationType;
40pub use system::SystemOperationType;
41pub use token::TokenOperationType;
42pub use withdrawals::WithdrawalOperationType;
43
44use grovedb::{EstimatedLayerInformation, TransactionArg};
45
46use crate::fees::op::LowLevelDriveOperation::GroveOperation;
47
48use dpp::version::PlatformVersion;
49use grovedb::batch::{KeyInfoPath, QualifiedGroveDbOp};
50
51use crate::error::drive::DriveError;
52use crate::error::fee::FeeError;
53use crate::util::batch::drive_op_batch::finalize_task::{
54    DriveOperationFinalizationTasks, DriveOperationFinalizeTask,
55};
56use dpp::data_contract::accessors::v0::DataContractV0Getters;
57use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters;
58use dpp::data_contract::document_type::action_fees::ContractFeePot;
59use dpp::data_contract::document_type::DocumentTypeRef;
60use dpp::data_contract::DataContract;
61use dpp::identifier::Identifier;
62
63use std::collections::{BTreeMap, BTreeSet, HashMap};
64
65/// A converter that will get Drive Operations from High Level Operations
66pub trait DriveLowLevelOperationConverter {
67    /// This will get a list of atomic drive operations from a high level operations
68    fn into_low_level_drive_operations(
69        self,
70        drive: &Drive,
71        estimated_costs_only_with_layer_info: &mut Option<
72            HashMap<KeyInfoPath, EstimatedLayerInformation>,
73        >,
74        block_info: &BlockInfo,
75        transaction: TransactionArg,
76        platform_version: &PlatformVersion,
77    ) -> Result<Vec<LowLevelDriveOperation>, Error>;
78}
79
80/// The drive operation context keeps track of changes that might affect other operations
81/// Notably Identity balance changes are kept track of
82pub struct DriveOperationContext {
83    #[allow(dead_code)]
84    #[deprecated(note = "This function is marked as unused.")]
85    #[allow(deprecated)]
86    identity_balance_changes: BTreeMap<[u8; 32], i64>,
87}
88
89/// All types of Drive Operations
90#[allow(clippy::large_enum_variant)]
91#[derive(Clone, Debug)]
92pub enum DriveOperation<'a> {
93    /// A contract operation
94    DataContractOperation(DataContractOperationType<'a>),
95    /// A document operation
96    DocumentOperation(DocumentOperationType<'a>),
97    /// A token operation
98    TokenOperation(TokenOperationType),
99    /// Withdrawal operation
100    WithdrawalOperation(WithdrawalOperationType),
101    /// An identity operation
102    IdentityOperation(IdentityOperationType),
103    /// An operation on prefunded balances
104    PrefundedSpecializedBalanceOperation(PrefundedSpecializedBalanceOperationType),
105    /// A system operation
106    SystemOperation(SystemOperationType),
107    /// A group operation
108    GroupOperation(GroupOperationType),
109    /// A contract group operation
110    ContractGroupOperation(ContractGroupOperationType),
111    /// A contract moderation operation: an entry of a banlist or a suspension list
112    ContractModerationOperation(ContractModerationOperationType),
113    /// A contract fee pot operation: credits of a contract's document action fees
114    ContractFeePotOperation(ContractFeePotOperationType),
115    /// An address funds operation
116    AddressFundsOperation(AddressFundsOperationType),
117    /// A shielded pool operation
118    ShieldedPoolOperation(ShieldedPoolOperationType),
119    /// A single low level groveDB operation
120    GroveDBOperation(QualifiedGroveDbOp),
121    /// Multiple low level groveDB operations
122    GroveDBOpBatch(GroveDbOpBatch),
123    /// An operation that only produces finalization tasks (no low-level ops)
124    FinalizeOperation(DriveOperationFinalizeTask),
125}
126
127impl DriveLowLevelOperationConverter for DriveOperation<'_> {
128    fn into_low_level_drive_operations(
129        self,
130        drive: &Drive,
131        estimated_costs_only_with_layer_info: &mut Option<
132            HashMap<KeyInfoPath, EstimatedLayerInformation>,
133        >,
134        block_info: &BlockInfo,
135        transaction: TransactionArg,
136        platform_version: &PlatformVersion,
137    ) -> Result<Vec<LowLevelDriveOperation>, Error> {
138        match self {
139            DriveOperation::DataContractOperation(contract_operation_type) => {
140                contract_operation_type.into_low_level_drive_operations(
141                    drive,
142                    estimated_costs_only_with_layer_info,
143                    block_info,
144                    transaction,
145                    platform_version,
146                )
147            }
148            DriveOperation::DocumentOperation(document_operation_type) => document_operation_type
149                .into_low_level_drive_operations(
150                    drive,
151                    estimated_costs_only_with_layer_info,
152                    block_info,
153                    transaction,
154                    platform_version,
155                ),
156            DriveOperation::WithdrawalOperation(withdrawal_operation_type) => {
157                withdrawal_operation_type.into_low_level_drive_operations(
158                    drive,
159                    estimated_costs_only_with_layer_info,
160                    block_info,
161                    transaction,
162                    platform_version,
163                )
164            }
165            DriveOperation::IdentityOperation(identity_operation_type) => identity_operation_type
166                .into_low_level_drive_operations(
167                    drive,
168                    estimated_costs_only_with_layer_info,
169                    block_info,
170                    transaction,
171                    platform_version,
172                ),
173            DriveOperation::PrefundedSpecializedBalanceOperation(
174                prefunded_balance_operation_type,
175            ) => prefunded_balance_operation_type.into_low_level_drive_operations(
176                drive,
177                estimated_costs_only_with_layer_info,
178                block_info,
179                transaction,
180                platform_version,
181            ),
182            DriveOperation::SystemOperation(system_operation_type) => system_operation_type
183                .into_low_level_drive_operations(
184                    drive,
185                    estimated_costs_only_with_layer_info,
186                    block_info,
187                    transaction,
188                    platform_version,
189                ),
190            DriveOperation::ShieldedPoolOperation(shielded_pool_operation_type) => {
191                shielded_pool_operation_type.into_low_level_drive_operations(
192                    drive,
193                    estimated_costs_only_with_layer_info,
194                    block_info,
195                    transaction,
196                    platform_version,
197                )
198            }
199            DriveOperation::GroveDBOperation(op) => Ok(vec![GroveOperation(op)]),
200            DriveOperation::GroveDBOpBatch(operations) => Ok(operations
201                .operations
202                .into_iter()
203                .map(GroveOperation)
204                .collect()),
205            DriveOperation::TokenOperation(token_operation_type) => token_operation_type
206                .into_low_level_drive_operations(
207                    drive,
208                    estimated_costs_only_with_layer_info,
209                    block_info,
210                    transaction,
211                    platform_version,
212                ),
213            DriveOperation::GroupOperation(group_operation_type) => group_operation_type
214                .into_low_level_drive_operations(
215                    drive,
216                    estimated_costs_only_with_layer_info,
217                    block_info,
218                    transaction,
219                    platform_version,
220                ),
221            DriveOperation::ContractGroupOperation(contract_group_operation_type) => {
222                contract_group_operation_type.into_low_level_drive_operations(
223                    drive,
224                    estimated_costs_only_with_layer_info,
225                    block_info,
226                    transaction,
227                    platform_version,
228                )
229            }
230            DriveOperation::ContractModerationOperation(contract_moderation_operation_type) => {
231                contract_moderation_operation_type.into_low_level_drive_operations(
232                    drive,
233                    estimated_costs_only_with_layer_info,
234                    block_info,
235                    transaction,
236                    platform_version,
237                )
238            }
239            DriveOperation::ContractFeePotOperation(contract_fee_pot_operation_type) => {
240                contract_fee_pot_operation_type.into_low_level_drive_operations(
241                    drive,
242                    estimated_costs_only_with_layer_info,
243                    block_info,
244                    transaction,
245                    platform_version,
246                )
247            }
248            DriveOperation::AddressFundsOperation(address_funds_operation_type) => {
249                address_funds_operation_type.into_low_level_drive_operations(
250                    drive,
251                    estimated_costs_only_with_layer_info,
252                    block_info,
253                    transaction,
254                    platform_version,
255                )
256            }
257            DriveOperation::FinalizeOperation(_) => Ok(vec![]),
258        }
259    }
260}
261
262impl DriveOperation<'_> {
263    /// Whether the batch this operation is in refunds nobody for the storage its document
264    /// operations remove: see [`ContractModerationOperationType::ForfeitStorageRefunds`].
265    pub fn forfeits_storage_refunds(&self) -> bool {
266        matches!(
267            self,
268            Self::ContractModerationOperation(
269                ContractModerationOperationType::ForfeitStorageRefunds
270            )
271        )
272    }
273
274    /// Whether this operation frees moderation storage its payer is owed a refund of: the
275    /// replacement of a removal record (a restored one, by a fresh deletion), which may leave
276    /// it shorter, the approval that closes a team action, which moves the approvals given
277    /// before it and the action's info to the closed actions, and an approval that deletes the
278    /// approvals of members who left the team. A batch forfeiting its refunds then keeps its
279    /// document operations apart (`Drive::apply_drive_operations` generation 1).
280    pub fn refunds_moderation_storage(&self) -> bool {
281        match self {
282            Self::ContractModerationOperation(
283                ContractModerationOperationType::AddDocumentRemoval {
284                    replaced_record_size,
285                    ..
286                },
287            ) => replaced_record_size.is_some(),
288            Self::ContractModerationOperation(
289                ContractModerationOperationType::AddTeamActionSignature { write, .. },
290            ) => match write {
291                ContractTeamActionWrite::Close { .. } => true,
292                ContractTeamActionWrite::Approve { dropped_signers } => !dropped_signers.is_empty(),
293                ContractTeamActionWrite::Propose { .. } => false,
294            },
295            _ => false,
296        }
297    }
298
299    /// Convert a member of a batch whose document TTL cleanup is complete.
300    pub(crate) fn into_low_level_drive_operations_after_ttl_drain(
301        self,
302        drive: &Drive,
303        estimated_costs_only_with_layer_info: &mut Option<
304            HashMap<KeyInfoPath, EstimatedLayerInformation>,
305        >,
306        block_info: &BlockInfo,
307        transaction: TransactionArg,
308        platform_version: &PlatformVersion,
309    ) -> Result<Vec<LowLevelDriveOperation>, Error> {
310        match self {
311            Self::DocumentOperation(operation) => operation
312                .into_low_level_drive_operations_after_ttl_drain(
313                    drive,
314                    estimated_costs_only_with_layer_info,
315                    block_info,
316                    transaction,
317                    platform_version,
318                ),
319            operation => operation.into_low_level_drive_operations(
320                drive,
321                estimated_costs_only_with_layer_info,
322                block_info,
323                transaction,
324                platform_version,
325            ),
326        }
327    }
328}
329
330impl Drive {
331    /// Prepare every document before conversion starts. Repeated grids may
332    /// spend several per-write budgets, all against the same pre-batch state.
333    pub(crate) fn prepare_drive_operations_time_range_ttl(
334        &self,
335        operations: &[DriveOperation],
336        block_info: &BlockInfo,
337        transaction: TransactionArg,
338        platform_version: &PlatformVersion,
339    ) -> Result<(), Error> {
340        for operation in operations {
341            if let DriveOperation::DocumentOperation(document) = operation {
342                document.prepare_time_range_ttl(self, block_info, transaction, platform_version)?;
343            }
344        }
345        Ok(())
346    }
347
348    /// Refuses a batch that moves one document type's `summableOffCountIndex`
349    /// counters for more than one document. Each document's conversion reads
350    /// its groups' counters and writes them back moved, and no conversion sees
351    /// another's pending writes, so two moves of one counter in one batch
352    /// would apply as one (grovedb keeps one write per key). A document moves
353    /// the counters of its own type when that type keeps them, written or
354    /// deleted, and an inserted document those of every type whose
355    /// preallocated counter index the insert creates (preallocation writes the
356    /// counter at zero). The rule is per type, so it is conservative: two
357    /// documents of different groups are refused too.
358    ///
359    /// A documents batch carries one transition
360    /// (`max_transitions_in_documents_batch`) and only transitions write
361    /// indexOnly documents, so no consensus batch moves a counter twice; this
362    /// keeps it so for every caller of the batch methods, estimation
363    /// included. Only protocol version 14's batch methods call it.
364    pub(crate) fn refuse_repeated_counter_moves(
365        &self,
366        operations: &[DriveOperation],
367        block_info: &BlockInfo,
368        transaction: TransactionArg,
369        platform_version: &PlatformVersion,
370    ) -> Result<(), Error> {
371        let mut moved: BTreeSet<(Identifier, String)> = BTreeSet::new();
372        for operation in operations {
373            let DriveOperation::DocumentOperation(operation) = operation else {
374                continue;
375            };
376            operation.for_each_document_type(
377                self,
378                block_info,
379                transaction,
380                |contract, document_type, preallocates| {
381                    for counter_type in counter_types_moved(contract, document_type, preallocates) {
382                        if !moved.insert((contract.id(), counter_type)) {
383                            return Err(Error::Drive(DriveError::CorruptedCodeExecution(
384                                "a batch moves one document type's summableOffCountIndex \
385                                 counters for more than one document",
386                            )));
387                        }
388                    }
389                    Ok(())
390                },
391                platform_version,
392            )?;
393        }
394        Ok(())
395    }
396}
397
398/// The document types whose `summableOffCountIndex` counters a write of
399/// `document_type` moves: its own when it keeps them, and, when the write
400/// `preallocates` (a plain insert), each type whose preallocated counter index
401/// the insert creates.
402fn counter_types_moved(
403    contract: &DataContract,
404    document_type: DocumentTypeRef,
405    preallocates: bool,
406) -> BTreeSet<String> {
407    let mut moved = BTreeSet::new();
408    if document_type
409        .indexes()
410        .values()
411        .any(|index| index.is_summable_off_count_index())
412    {
413        moved.insert(document_type.name().clone());
414    }
415    if preallocates {
416        moved.extend(
417            preallocation_bindings_targeting(contract, document_type)
418                .filter(|(_, index, _)| index.is_summable_off_count_index())
419                .map(|(referring_type, _, _)| referring_type.name().clone()),
420        );
421    }
422    moved
423}
424
425impl DriveOperationFinalizationTasks for DriveOperation<'_> {
426    fn finalization_tasks(
427        &self,
428        platform_version: &PlatformVersion,
429    ) -> Result<Option<Vec<DriveOperationFinalizeTask>>, Error> {
430        match platform_version
431            .drive
432            .methods
433            .state_transitions
434            .operations
435            .finalization_tasks
436        {
437            0 => self.finalization_tasks_v0(platform_version),
438            version => Err(Error::Drive(DriveError::UnknownVersionMismatch {
439                method: "DriveOperation.finalization_tasks".to_string(),
440                known_versions: vec![0],
441                received: version,
442            })),
443        }
444    }
445}
446
447impl DriveOperation<'_> {
448    fn finalization_tasks_v0(
449        &self,
450        platform_version: &PlatformVersion,
451    ) -> Result<Option<Vec<DriveOperationFinalizeTask>>, Error> {
452        match self {
453            DriveOperation::DataContractOperation(o) => o.finalization_tasks(platform_version),
454            DriveOperation::FinalizeOperation(task) => Ok(Some(vec![task.clone()])),
455            _ => Ok(None),
456        }
457    }
458
459    /// Sums the credits the batch mints into Platform (its `AddToSystemCredits` operations,
460    /// saturating). This is the gross inflow of the batch — the net rule of the daily
461    /// withdrawal limit records it per block, and netting against removals here instead would
462    /// let a same-block deposit and withdrawal hide the inflow.
463    pub fn credit_mints(operations: &[DriveOperation]) -> Credits {
464        operations
465            .iter()
466            .filter_map(|operation| match operation {
467                DriveOperation::SystemOperation(SystemOperationType::AddToSystemCredits {
468                    amount,
469                }) => Some(*amount),
470                _ => None,
471            })
472            .fold(0u64, |total, amount| total.saturating_add(amount))
473    }
474
475    /// Merges every write of one identity balance, of one contract fee pot, and of one
476    /// prefunded specialized balance, into a single net operation.
477    ///
478    /// Each of these operations computes the new value from the one committed before its
479    /// batch, and GroveDB keeps only the last write of a key, so two of them in one batch lose
480    /// the first: a purchase price and a purchase fee leaving the buyer, a contested document's
481    /// voting fund and its creation fee, a sale and the action fee of a contract owner who
482    /// sponsors the gas. Merged, they apply as if in turn. The merged operation takes the place
483    /// of the first one on its key, and a balance or pot the writes leave as it was gets none.
484    /// A key written once keeps its operation untouched.
485    ///
486    /// Two other kinds of write compute from the committed value too and are not merged here,
487    /// because no batch repeats their keys: the total system credits
488    /// ([`SystemOperationType`]) and address balances ([`AddressFundsOperationType`]). Their
489    /// docs say what keeps it so; a change that could repeat one must extend this merge first.
490    pub fn merge_balance_writes(operations: Vec<Self>) -> Result<Vec<Self>, Error> {
491        // Most batches make at most one such write: nothing to merge, and no map to build.
492        if operations
493            .iter()
494            .filter(|operation| balance_write(operation).is_some())
495            .nth(1)
496            .is_none()
497        {
498            return Ok(operations);
499        }
500        let mut writes: BTreeMap<BalanceKey, (usize, i128)> = BTreeMap::new();
501        for (key, change) in operations.iter().filter_map(balance_write) {
502            let (count, net) = writes.entry(key).or_default();
503            *count += 1;
504            *net = net
505                .checked_add(change)
506                .ok_or(Error::Fee(FeeError::Overflow(
507                    "the writes one batch makes to one balance overflow",
508                )))?;
509        }
510        if writes.values().all(|(count, _)| *count == 1) {
511            return Ok(operations);
512        }
513        let mut merged = Vec::with_capacity(operations.len());
514        for operation in operations {
515            let Some((key, _)) = balance_write(&operation) else {
516                merged.push(operation);
517                continue;
518            };
519            match writes.get_mut(&key) {
520                Some((1, _)) => merged.push(operation),
521                // The first write of a repeated key: the merged one goes here, and the entry
522                // is marked done so the later writes are dropped.
523                Some((count, net)) if *count > 1 => {
524                    merged.extend(net_balance_write(key, *net)?);
525                    *count = 0;
526                }
527                _ => {}
528            }
529        }
530        Ok(merged)
531    }
532
533    /// Refuses a batch whose token operations write one identity's balance of a token, one
534    /// token's total supply, or the total one token's shielded pool holds, more than once.
535    ///
536    /// These writes compute the new value from the one committed before the batch too, but a
537    /// transfer writes two balances, a mint or a burn a balance and the supply, and a shield
538    /// or an unshield a balance and the pool total, so they cannot be merged into operations
539    /// of their own kinds. No state transition makes two of them on one key: a batch carries
540    /// one transition, and each writes a key at most once. A batch that did would lose all but
541    /// the last write, so it is refused instead.
542    pub fn refuse_repeated_token_balance_writes(operations: &[Self]) -> Result<(), Error> {
543        let mut written = BTreeSet::new();
544        for key in operations.iter().flat_map(token_balance_writes) {
545            if !written.insert(key) {
546                return Err(Error::Drive(DriveError::CorruptedCodeExecution(
547                    "a batch writes one token balance, token supply or token shielded pool \
548                     total more than once",
549                )));
550            }
551        }
552        Ok(())
553    }
554}
555
556/// A key an identity balance, a contract fee pot or a prefunded specialized balance operation
557/// writes
558#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
559enum BalanceKey {
560    Identity([u8; 32]),
561    FeePot(Identifier, ContractFeePot),
562    PrefundedSpecializedBalance(Identifier),
563}
564
565/// A key a token operation writes: an identity's balance of a token, a token's total supply,
566/// or the total a token's shielded pool holds
567#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
568enum TokenBalanceKey {
569    Holder(Identifier, Identifier),
570    Supply(Identifier),
571    PoolTotal(Identifier),
572}
573
574/// The keys `operation` writes if it is a token operation that moves tokens
575fn token_balance_writes(operation: &DriveOperation) -> Vec<TokenBalanceKey> {
576    let DriveOperation::TokenOperation(operation) = operation else {
577        return vec![];
578    };
579    match operation {
580        TokenOperationType::TokenBurn {
581            token_id,
582            identity_balance_holder_id,
583            ..
584        }
585        | TokenOperationType::TokenMint {
586            token_id,
587            identity_balance_holder_id,
588            ..
589        } => vec![
590            TokenBalanceKey::Holder(*token_id, *identity_balance_holder_id),
591            TokenBalanceKey::Supply(*token_id),
592        ],
593        TokenOperationType::TokenMintMany {
594            token_id,
595            recipients,
596            ..
597        } => recipients
598            .iter()
599            .map(|(recipient_id, _)| TokenBalanceKey::Holder(*token_id, *recipient_id))
600            .chain([TokenBalanceKey::Supply(*token_id)])
601            .collect(),
602        TokenOperationType::TokenTransfer {
603            token_id,
604            sender_id,
605            recipient_id,
606            ..
607        } => vec![
608            TokenBalanceKey::Holder(*token_id, *sender_id),
609            TokenBalanceKey::Holder(*token_id, *recipient_id),
610        ],
611        // A shield moves tokens out of an identity's balance and into the pool, and an
612        // unshield out of the pool and into an identity's balance. Neither moves the total
613        // supply: the pool total is a term of the conservation sum beside the balances.
614        TokenOperationType::TokenShield {
615            token_id,
616            identity_id,
617            ..
618        } => vec![
619            TokenBalanceKey::Holder(*token_id, *identity_id),
620            TokenBalanceKey::PoolTotal(*token_id),
621        ],
622        TokenOperationType::TokenUnshield {
623            token_id,
624            recipient_id,
625            ..
626        } => vec![
627            TokenBalanceKey::Holder(*token_id, *recipient_id),
628            TokenBalanceKey::PoolTotal(*token_id),
629        ],
630        // Minting into the pool raises the total supply and the pool total by the same amount
631        // and burning from it lowers both; neither touches an identity's balance.
632        TokenOperationType::TokenMintToPool { token_id, .. }
633        | TokenOperationType::TokenBurnFromPool { token_id, .. } => vec![
634            TokenBalanceKey::Supply(*token_id),
635            TokenBalanceKey::PoolTotal(*token_id),
636        ],
637        // A transfer inside the pool moves nothing in or out of it, so it leaves the pool
638        // total where it stands and only records nullifiers and appends notes.
639        TokenOperationType::TokenShieldedTransfer { .. }
640        // The rest move no tokens at all: they write freeze records, the token's status, a
641        // direct purchase price, distribution bookkeeping or a history document.
642        | TokenOperationType::TokenFreeze { .. }
643        | TokenOperationType::TokenUnfreeze { .. }
644        | TokenOperationType::TokenSetStatus { .. }
645        | TokenOperationType::TokenSetPriceForDirectPurchase { .. }
646        | TokenOperationType::TokenMarkPerpetualReleaseAsDistributed { .. }
647        | TokenOperationType::TokenMarkPreProgrammedReleaseAsDistributed { .. }
648        | TokenOperationType::TokenMarkOncePerIdentityReleaseAsDistributed { .. }
649        | TokenOperationType::TokenHistory { .. } => vec![],
650    }
651}
652
653/// The key `operation` writes and the signed change it makes there, if it is an identity
654/// balance, contract fee pot or prefunded specialized balance operation
655fn balance_write(operation: &DriveOperation) -> Option<(BalanceKey, i128)> {
656    match operation {
657        DriveOperation::IdentityOperation(IdentityOperationType::AddToIdentityBalance {
658            identity_id,
659            added_balance,
660        }) => Some((BalanceKey::Identity(*identity_id), *added_balance as i128)),
661        DriveOperation::IdentityOperation(IdentityOperationType::RemoveFromIdentityBalance {
662            identity_id,
663            balance_to_remove,
664        }) => Some((
665            BalanceKey::Identity(*identity_id),
666            -(*balance_to_remove as i128),
667        )),
668        DriveOperation::ContractFeePotOperation(ContractFeePotOperationType::AddToPot {
669            contract_id,
670            pot,
671            amount,
672        }) => Some((BalanceKey::FeePot(*contract_id, *pot), *amount as i128)),
673        DriveOperation::ContractFeePotOperation(ContractFeePotOperationType::DeductFromPot {
674            contract_id,
675            pot,
676            amount,
677        }) => Some((BalanceKey::FeePot(*contract_id, *pot), -(*amount as i128))),
678        DriveOperation::PrefundedSpecializedBalanceOperation(
679            PrefundedSpecializedBalanceOperationType::CreateNewPrefundedBalance {
680                prefunded_specialized_balance_id,
681                add_balance,
682            },
683        ) => Some((
684            BalanceKey::PrefundedSpecializedBalance(*prefunded_specialized_balance_id),
685            *add_balance as i128,
686        )),
687        DriveOperation::PrefundedSpecializedBalanceOperation(
688            PrefundedSpecializedBalanceOperationType::DeductFromPrefundedBalance {
689                prefunded_specialized_balance_id,
690                remove_balance,
691            },
692        ) => Some((
693            BalanceKey::PrefundedSpecializedBalance(*prefunded_specialized_balance_id),
694            -(*remove_balance as i128),
695        )),
696        _ => None,
697    }
698}
699
700/// The one operation that makes the signed change `net` at `key`, or none when it is zero
701fn net_balance_write<'a>(key: BalanceKey, net: i128) -> Result<Option<DriveOperation<'a>>, Error> {
702    if net == 0 {
703        return Ok(None);
704    }
705    let amount = Credits::try_from(net.unsigned_abs()).map_err(|_| {
706        Error::Fee(FeeError::Overflow(
707            "the merged writes of one balance overflow credits",
708        ))
709    })?;
710    Ok(Some(match (key, net > 0) {
711        (BalanceKey::Identity(identity_id), true) => {
712            DriveOperation::IdentityOperation(IdentityOperationType::AddToIdentityBalance {
713                identity_id,
714                added_balance: amount,
715            })
716        }
717        (BalanceKey::Identity(identity_id), false) => {
718            DriveOperation::IdentityOperation(IdentityOperationType::RemoveFromIdentityBalance {
719                identity_id,
720                balance_to_remove: amount,
721            })
722        }
723        (BalanceKey::FeePot(contract_id, pot), true) => {
724            DriveOperation::ContractFeePotOperation(ContractFeePotOperationType::AddToPot {
725                contract_id,
726                pot,
727                amount,
728            })
729        }
730        (BalanceKey::FeePot(contract_id, pot), false) => {
731            DriveOperation::ContractFeePotOperation(ContractFeePotOperationType::DeductFromPot {
732                contract_id,
733                pot,
734                amount,
735            })
736        }
737        // Adds to the balance, creating it when it does not exist yet.
738        (BalanceKey::PrefundedSpecializedBalance(prefunded_specialized_balance_id), true) => {
739            DriveOperation::PrefundedSpecializedBalanceOperation(
740                PrefundedSpecializedBalanceOperationType::CreateNewPrefundedBalance {
741                    prefunded_specialized_balance_id,
742                    add_balance: amount,
743                },
744            )
745        }
746        (BalanceKey::PrefundedSpecializedBalance(prefunded_specialized_balance_id), false) => {
747            DriveOperation::PrefundedSpecializedBalanceOperation(
748                PrefundedSpecializedBalanceOperationType::DeductFromPrefundedBalance {
749                    prefunded_specialized_balance_id,
750                    remove_balance: amount,
751                },
752            )
753        }
754    }))
755}
756
757#[cfg(feature = "server")]
758#[cfg(test)]
759mod tests {
760    use grovedb::Element;
761    use std::borrow::Cow;
762    use std::option::Option::None;
763
764    use super::*;
765
766    use crate::util::test_helpers::setup_contract;
767    use dpp::block::block_info::BlockInfo;
768    use dpp::data_contract::accessors::v0::DataContractV0Getters;
769    use dpp::data_contract::DataContract;
770    use dpp::serialization::PlatformSerializableWithPlatformVersion;
771    use dpp::tests::json_document::{json_document_to_contract, json_document_to_document};
772    use dpp::util::cbor_serializer;
773    use rand::Rng;
774    use serde_json::json;
775
776    use crate::util::batch::drive_op_batch::document::DocumentOperation::{
777        AddOperation, UpdateOperation,
778    };
779    use crate::util::batch::drive_op_batch::document::DocumentOperationType::MultipleDocumentOperationsForSameContractDocumentType;
780    use crate::util::batch::drive_op_batch::document::{
781        DocumentOperationsForContractDocumentType, UpdateOperationInfo,
782    };
783    use crate::util::batch::DataContractOperationType::ApplyContract;
784    use crate::util::batch::DocumentOperationType::AddDocument;
785    use crate::util::batch::DriveOperation::{DataContractOperation, DocumentOperation};
786
787    use crate::drive::contract::paths::contract_root_path;
788    use crate::drive::Drive;
789    use crate::util::object_size_info::DocumentInfo::DocumentRefInfo;
790    use crate::util::object_size_info::{DataContractInfo, DocumentTypeInfo, OwnedDocumentInfo};
791    use crate::util::storage_flags::StorageFlags;
792    use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure;
793
794    use crate::state_transition_action::shielded::ShieldedActionNote;
795    use dpp::balances::credits::TokenAmount;
796    use dpp::identity::accessors::IdentityGettersV0;
797    use dpp::identity::Identity;
798
799    #[test]
800    fn test_add_dashpay_documents() {
801        let drive: Drive = setup_drive_with_initial_state_structure(None);
802        let platform_version = PlatformVersion::latest();
803
804        let mut drive_operations = vec![];
805        let db_transaction = drive.grove.start_transaction();
806
807        let contract = json_document_to_contract(
808            "tests/supporting_files/contract/dashpay/dashpay-contract-all-mutable.json",
809            false,
810            platform_version,
811        )
812        .expect("expected to get contract");
813
814        let _document_type = contract
815            .document_type_for_name("contactRequest")
816            .expect("expected to get document type");
817
818        drive_operations.push(DataContractOperation(ApplyContract {
819            contract: Cow::Borrowed(&contract),
820            storage_flags: None,
821        }));
822
823        let random_owner_id = rand::thread_rng().gen::<[u8; 32]>();
824
825        let document_type = contract
826            .document_type_for_name("contactRequest")
827            .expect("expected to get document type");
828
829        let dashpay_cr_document = json_document_to_document(
830            "tests/supporting_files/contract/dashpay/contact-request0.json",
831            Some(random_owner_id.into()),
832            document_type,
833            platform_version,
834        )
835        .expect("expected to get document");
836
837        drive_operations.push(DocumentOperation(AddDocument {
838            owned_document_info: OwnedDocumentInfo {
839                document_info: DocumentRefInfo((
840                    &dashpay_cr_document,
841                    StorageFlags::optional_default_as_cow(),
842                )),
843                owner_id: None,
844            },
845            contract_info: DataContractInfo::BorrowedDataContract(&contract),
846            document_type_info: DocumentTypeInfo::DocumentTypeRef(document_type),
847            override_document: false,
848        }));
849
850        drive
851            .apply_drive_operations(
852                drive_operations,
853                true,
854                &BlockInfo::default(),
855                Some(&db_transaction),
856                platform_version,
857                None,
858            )
859            .expect("expected to insert contract and document");
860
861        let element = drive
862            .grove
863            .get(
864                &contract_root_path(&contract.id().to_buffer()),
865                &[0],
866                Some(&db_transaction),
867                &platform_version.drive.grove_version,
868            )
869            .unwrap()
870            .expect("expected to get contract back");
871
872        assert_eq!(
873            element,
874            Element::Item(
875                contract
876                    .serialize_to_bytes_with_platform_version(platform_version)
877                    .expect("expected to serialize contract"),
878                None
879            )
880        );
881
882        let query_value = json!({
883            "where": [
884            ],
885            "limit": 100,
886            "orderBy": [
887                ["$ownerId", "asc"],
888            ]
889        });
890        let where_cbor = cbor_serializer::serializable_value_to_cbor(&query_value, None)
891            .expect("expected to serialize to cbor");
892
893        let (docs, _, _) = drive
894            .query_documents_cbor_from_contract(
895                &contract,
896                document_type,
897                where_cbor.as_slice(),
898                None,
899                Some(&db_transaction),
900                Some(platform_version.protocol_version),
901            )
902            .expect("expected to query");
903        assert_eq!(docs.len(), 1);
904    }
905
906    #[test]
907    fn test_add_multiple_dashpay_documents_individually_should_succeed() {
908        let drive = setup_drive_with_initial_state_structure(None);
909
910        let platform_version = PlatformVersion::latest();
911
912        let mut drive_operations = vec![];
913        let db_transaction = drive.grove.start_transaction();
914
915        let contract = json_document_to_contract(
916            "tests/supporting_files/contract/dashpay/dashpay-contract-all-mutable.json",
917            false,
918            platform_version,
919        )
920        .expect("expected to get contract");
921
922        let document_type = contract
923            .document_type_for_name("contactRequest")
924            .expect("expected to get document type");
925
926        drive_operations.push(DataContractOperation(ApplyContract {
927            contract: Cow::Borrowed(&contract),
928            storage_flags: None,
929        }));
930        let random_owner_id = rand::thread_rng().gen::<[u8; 32]>();
931
932        let dashpay_cr_document = json_document_to_document(
933            "tests/supporting_files/contract/dashpay/contact-request0.json",
934            Some(random_owner_id.into()),
935            document_type,
936            platform_version,
937        )
938        .expect("expected to get contract");
939
940        drive_operations.push(DocumentOperation(AddDocument {
941            owned_document_info: OwnedDocumentInfo {
942                document_info: DocumentRefInfo((&dashpay_cr_document, None)),
943                owner_id: None,
944            },
945            contract_info: DataContractInfo::BorrowedDataContract(&contract),
946            document_type_info: DocumentTypeInfo::DocumentTypeNameAsStr("contactRequest"),
947            override_document: false,
948        }));
949
950        let random_owner_id = rand::thread_rng().gen::<[u8; 32]>();
951
952        let dashpay_cr_1_document = json_document_to_document(
953            "tests/supporting_files/contract/dashpay/contact-request1.json",
954            Some(random_owner_id.into()),
955            document_type,
956            platform_version,
957        )
958        .expect("expected to get contract");
959
960        drive_operations.push(DocumentOperation(AddDocument {
961            owned_document_info: OwnedDocumentInfo {
962                document_info: DocumentRefInfo((&dashpay_cr_1_document, None)),
963                owner_id: None,
964            },
965            contract_info: DataContractInfo::BorrowedDataContract(&contract),
966            document_type_info: DocumentTypeInfo::DocumentTypeNameAsStr("contactRequest"),
967            override_document: false,
968        }));
969
970        drive
971            .apply_drive_operations(
972                drive_operations,
973                true,
974                &BlockInfo::default(),
975                Some(&db_transaction),
976                platform_version,
977                None,
978            )
979            .expect("expected to be able to insert documents");
980
981        let query_value = json!({
982            "where": [
983            ],
984            "limit": 100,
985            "orderBy": [
986                ["$ownerId", "asc"],
987            ]
988        });
989        let where_cbor = cbor_serializer::serializable_value_to_cbor(&query_value, None)
990            .expect("expected to serialize to cbor");
991
992        let (docs, _, _) = drive
993            .query_documents_cbor_from_contract(
994                &contract,
995                document_type,
996                where_cbor.as_slice(),
997                None,
998                Some(&db_transaction),
999                Some(platform_version.protocol_version),
1000            )
1001            .expect("expected to query");
1002        assert_eq!(docs.len(), 2);
1003    }
1004
1005    #[test]
1006    fn test_add_multiple_dashpay_documents() {
1007        let drive: Drive = setup_drive_with_initial_state_structure(None);
1008
1009        let platform_version = PlatformVersion::latest();
1010
1011        let mut drive_operations = vec![];
1012        let db_transaction = drive.grove.start_transaction();
1013
1014        let contract = json_document_to_contract(
1015            "tests/supporting_files/contract/dashpay/dashpay-contract-all-mutable.json",
1016            false,
1017            platform_version,
1018        )
1019        .expect("expected to get contract");
1020
1021        let document_type = contract
1022            .document_type_for_name("contactRequest")
1023            .expect("expected to get document type");
1024
1025        drive_operations.push(DataContractOperation(ApplyContract {
1026            contract: Cow::Borrowed(&contract),
1027            storage_flags: None,
1028        }));
1029
1030        let random_owner_id = rand::thread_rng().gen::<[u8; 32]>();
1031
1032        let document0 = json_document_to_document(
1033            "tests/supporting_files/contract/dashpay/contact-request0.json",
1034            Some(random_owner_id.into()),
1035            document_type,
1036            platform_version,
1037        )
1038        .expect("expected to get document 0");
1039
1040        let document1 = json_document_to_document(
1041            "tests/supporting_files/contract/dashpay/contact-request1.json",
1042            Some(random_owner_id.into()),
1043            document_type,
1044            platform_version,
1045        )
1046        .expect("expected to get document 1");
1047
1048        let operations = vec![
1049            AddOperation {
1050                owned_document_info: OwnedDocumentInfo {
1051                    document_info: DocumentRefInfo((
1052                        &document0,
1053                        StorageFlags::optional_default_as_cow(),
1054                    )),
1055                    owner_id: Some(random_owner_id),
1056                },
1057                override_document: false,
1058            },
1059            AddOperation {
1060                owned_document_info: OwnedDocumentInfo {
1061                    document_info: DocumentRefInfo((
1062                        &document1,
1063                        StorageFlags::optional_default_as_cow(),
1064                    )),
1065                    owner_id: Some(random_owner_id),
1066                },
1067                override_document: false,
1068            },
1069        ];
1070
1071        drive_operations.push(DocumentOperation(
1072            MultipleDocumentOperationsForSameContractDocumentType {
1073                document_operations: DocumentOperationsForContractDocumentType {
1074                    operations,
1075                    contract: &contract,
1076                    document_type,
1077                },
1078            },
1079        ));
1080
1081        drive
1082            .apply_drive_operations(
1083                drive_operations,
1084                true,
1085                &BlockInfo::default(),
1086                Some(&db_transaction),
1087                platform_version,
1088                None,
1089            )
1090            .expect("expected to be able to insert documents");
1091
1092        let element = drive
1093            .grove
1094            .get(
1095                &contract_root_path(&contract.id().to_buffer()),
1096                &[0],
1097                Some(&db_transaction),
1098                &platform_version.drive.grove_version,
1099            )
1100            .unwrap()
1101            .expect("expected to get contract back");
1102
1103        assert_eq!(
1104            element,
1105            Element::Item(
1106                contract
1107                    .serialize_to_bytes_with_platform_version(platform_version)
1108                    .expect("expected to serialize contract"),
1109                None
1110            )
1111        );
1112
1113        let query_value = json!({
1114            "where": [
1115            ],
1116            "limit": 100,
1117            "orderBy": [
1118                ["$ownerId", "asc"],
1119            ]
1120        });
1121        let where_cbor = cbor_serializer::serializable_value_to_cbor(&query_value, None)
1122            .expect("expected to serialize to cbor");
1123
1124        let (docs, _, _) = drive
1125            .query_documents_cbor_from_contract(
1126                &contract,
1127                document_type,
1128                where_cbor.as_slice(),
1129                None,
1130                Some(&db_transaction),
1131                Some(platform_version.protocol_version),
1132            )
1133            .expect("expected to query");
1134        assert_eq!(docs.len(), 2);
1135    }
1136
1137    #[test]
1138    fn test_add_multiple_family_documents() {
1139        let drive: Drive = setup_drive_with_initial_state_structure(None);
1140
1141        let platform_version = PlatformVersion::latest();
1142
1143        let mut drive_operations = vec![];
1144        let db_transaction = drive.grove.start_transaction();
1145
1146        let contract = setup_contract(
1147            &drive,
1148            "tests/supporting_files/contract/family/family-contract.json",
1149            None,
1150            None,
1151            None::<fn(&mut DataContract)>,
1152            Some(&db_transaction),
1153            None,
1154        );
1155
1156        let document_type = contract
1157            .document_type_for_name("person")
1158            .expect("expected to get document type");
1159
1160        let random_owner_id0 = rand::thread_rng().gen::<[u8; 32]>();
1161
1162        let person_document0 = json_document_to_document(
1163            "tests/supporting_files/contract/family/person0.json",
1164            Some(random_owner_id0.into()),
1165            document_type,
1166            platform_version,
1167        )
1168        .expect("expected to get document");
1169
1170        let random_owner_id1 = rand::thread_rng().gen::<[u8; 32]>();
1171
1172        let person_document1 = json_document_to_document(
1173            "tests/supporting_files/contract/family/person3.json",
1174            Some(random_owner_id1.into()),
1175            document_type,
1176            platform_version,
1177        )
1178        .expect("expected to get document");
1179
1180        let mut operations = vec![];
1181
1182        operations.push(AddOperation {
1183            owned_document_info: OwnedDocumentInfo {
1184                document_info: DocumentRefInfo((
1185                    &person_document0,
1186                    StorageFlags::optional_default_as_cow(),
1187                )),
1188                owner_id: Some(random_owner_id0),
1189            },
1190            override_document: false,
1191        });
1192
1193        let random_owner_id1 = rand::thread_rng().gen::<[u8; 32]>();
1194
1195        operations.push(AddOperation {
1196            owned_document_info: OwnedDocumentInfo {
1197                document_info: DocumentRefInfo((
1198                    &person_document1,
1199                    StorageFlags::optional_default_as_cow(),
1200                )),
1201                owner_id: Some(random_owner_id1),
1202            },
1203            override_document: false,
1204        });
1205
1206        drive_operations.push(DocumentOperation(
1207            MultipleDocumentOperationsForSameContractDocumentType {
1208                document_operations: DocumentOperationsForContractDocumentType {
1209                    operations,
1210                    contract: &contract,
1211                    document_type,
1212                },
1213            },
1214        ));
1215
1216        drive
1217            .apply_drive_operations(
1218                drive_operations,
1219                true,
1220                &BlockInfo::default(),
1221                Some(&db_transaction),
1222                platform_version,
1223                None,
1224            )
1225            .expect("expected to be able to insert documents");
1226
1227        let query_value = json!({
1228            "where": [
1229            ],
1230            "limit": 100,
1231            "orderBy": [
1232                ["$ownerId", "asc"],
1233            ]
1234        });
1235        let where_cbor = cbor_serializer::serializable_value_to_cbor(&query_value, None)
1236            .expect("expected to serialize to cbor");
1237
1238        let (docs, _, _) = drive
1239            .query_documents_cbor_from_contract(
1240                &contract,
1241                document_type,
1242                where_cbor.as_slice(),
1243                None,
1244                Some(&db_transaction),
1245                Some(platform_version.protocol_version),
1246            )
1247            .expect("expected to query");
1248        assert_eq!(docs.len(), 2);
1249    }
1250
1251    #[test]
1252    fn test_update_multiple_family_documents() {
1253        let drive: Drive = setup_drive_with_initial_state_structure(None);
1254
1255        let platform_version = PlatformVersion::latest();
1256
1257        let mut drive_operations = vec![];
1258        let db_transaction = drive.grove.start_transaction();
1259
1260        let contract = setup_contract(
1261            &drive,
1262            "tests/supporting_files/contract/family/family-contract-only-age-index.json",
1263            None,
1264            None,
1265            None::<fn(&mut DataContract)>,
1266            Some(&db_transaction),
1267            None,
1268        );
1269
1270        let document_type = contract
1271            .document_type_for_name("person")
1272            .expect("expected to get document type");
1273
1274        let random_owner_id0 = rand::thread_rng().gen::<[u8; 32]>();
1275
1276        let person_document0 = json_document_to_document(
1277            "tests/supporting_files/contract/family/person0.json",
1278            Some(random_owner_id0.into()),
1279            document_type,
1280            platform_version,
1281        )
1282        .expect("expected to get document");
1283
1284        let random_owner_id1 = rand::thread_rng().gen::<[u8; 32]>();
1285
1286        let person_document1 = json_document_to_document(
1287            "tests/supporting_files/contract/family/person3.json",
1288            Some(random_owner_id1.into()),
1289            document_type,
1290            platform_version,
1291        )
1292        .expect("expected to get document");
1293
1294        let operations = vec![
1295            AddOperation {
1296                owned_document_info: OwnedDocumentInfo {
1297                    document_info: DocumentRefInfo((
1298                        &person_document0,
1299                        StorageFlags::optional_default_as_cow(),
1300                    )),
1301                    owner_id: Some(random_owner_id0),
1302                },
1303                override_document: false,
1304            },
1305            AddOperation {
1306                owned_document_info: OwnedDocumentInfo {
1307                    document_info: DocumentRefInfo((
1308                        &person_document1,
1309                        StorageFlags::optional_default_as_cow(),
1310                    )),
1311                    owner_id: Some(random_owner_id1),
1312                },
1313                override_document: false,
1314            },
1315        ];
1316
1317        drive_operations.push(DocumentOperation(
1318            MultipleDocumentOperationsForSameContractDocumentType {
1319                document_operations: DocumentOperationsForContractDocumentType {
1320                    operations,
1321                    contract: &contract,
1322                    document_type,
1323                },
1324            },
1325        ));
1326
1327        drive
1328            .apply_drive_operations(
1329                drive_operations,
1330                true,
1331                &BlockInfo::default(),
1332                Some(&db_transaction),
1333                platform_version,
1334                None,
1335            )
1336            .expect("expected to be able to insert documents");
1337
1338        // This was the setup now let's do the update
1339
1340        drive_operations = vec![];
1341
1342        let random_owner_id0 = rand::thread_rng().gen::<[u8; 32]>();
1343
1344        let person_document0 = json_document_to_document(
1345            "tests/supporting_files/contract/family/person0-older.json",
1346            Some(random_owner_id0.into()),
1347            document_type,
1348            platform_version,
1349        )
1350        .expect("expected to get document");
1351
1352        let random_owner_id1 = rand::thread_rng().gen::<[u8; 32]>();
1353
1354        let person_document1 = json_document_to_document(
1355            "tests/supporting_files/contract/family/person3-older.json",
1356            Some(random_owner_id1.into()),
1357            document_type,
1358            platform_version,
1359        )
1360        .expect("expected to get document");
1361
1362        let operations = vec![
1363            UpdateOperation(UpdateOperationInfo {
1364                document: &person_document0,
1365                serialized_document: None,
1366                owner_id: Some(random_owner_id0),
1367                storage_flags: None,
1368            }),
1369            UpdateOperation(UpdateOperationInfo {
1370                document: &person_document1,
1371                serialized_document: None,
1372                owner_id: Some(random_owner_id1),
1373                storage_flags: None,
1374            }),
1375        ];
1376
1377        drive_operations.push(DocumentOperation(
1378            MultipleDocumentOperationsForSameContractDocumentType {
1379                document_operations: DocumentOperationsForContractDocumentType {
1380                    operations,
1381                    contract: &contract,
1382                    document_type,
1383                },
1384            },
1385        ));
1386
1387        drive
1388            .apply_drive_operations(
1389                drive_operations,
1390                true,
1391                &BlockInfo::default(),
1392                Some(&db_transaction),
1393                platform_version,
1394                None,
1395            )
1396            .expect("expected to be able to update documents");
1397
1398        let query_value = json!({
1399            "where": [
1400            ],
1401            "limit": 100,
1402            "orderBy": [
1403                ["age", "asc"],
1404            ]
1405        });
1406        let where_cbor = cbor_serializer::serializable_value_to_cbor(&query_value, None)
1407            .expect("expected to serialize to cbor");
1408
1409        let (docs, _, _) = drive
1410            .query_documents_cbor_from_contract(
1411                &contract,
1412                document_type,
1413                where_cbor.as_slice(),
1414                None,
1415                Some(&db_transaction),
1416                Some(platform_version.protocol_version),
1417            )
1418            .expect("expected to query");
1419        assert_eq!(docs.len(), 2);
1420
1421        let query_value = json!({
1422            "where": [
1423                ["age", "==", 35]
1424            ],
1425            "limit": 100,
1426            "orderBy": [
1427                ["age", "asc"],
1428            ]
1429        });
1430        let where_cbor = cbor_serializer::serializable_value_to_cbor(&query_value, None)
1431            .expect("expected to serialize to cbor");
1432
1433        let (docs, _, _) = drive
1434            .query_documents_cbor_from_contract(
1435                &contract,
1436                document_type,
1437                where_cbor.as_slice(),
1438                None,
1439                Some(&db_transaction),
1440                Some(platform_version.protocol_version),
1441            )
1442            .expect("expected to query");
1443        assert_eq!(docs.len(), 0);
1444
1445        let query_value = json!({
1446            "where": [
1447                ["age", "==", 36]
1448            ],
1449            "limit": 100,
1450            "orderBy": [
1451                ["age", "asc"],
1452            ]
1453        });
1454        let where_cbor = cbor_serializer::serializable_value_to_cbor(&query_value, None)
1455            .expect("expected to serialize to cbor");
1456
1457        let (docs, _, _) = drive
1458            .query_documents_cbor_from_contract(
1459                &contract,
1460                document_type,
1461                where_cbor.as_slice(),
1462                None,
1463                Some(&db_transaction),
1464                Some(platform_version.protocol_version),
1465            )
1466            .expect("expected to query");
1467        assert_eq!(docs.len(), 2);
1468    }
1469
1470    #[test]
1471    fn test_update_multiple_family_documents_with_index_being_removed_and_added() {
1472        let drive: Drive = setup_drive_with_initial_state_structure(None);
1473
1474        let platform_version = PlatformVersion::latest();
1475
1476        let db_transaction = drive.grove.start_transaction();
1477
1478        let contract = setup_contract(
1479            &drive,
1480            "tests/supporting_files/contract/family/family-contract-only-age-index.json",
1481            None,
1482            None,
1483            None::<fn(&mut DataContract)>,
1484            Some(&db_transaction),
1485            None,
1486        );
1487
1488        let document_type = contract
1489            .document_type_for_name("person")
1490            .expect("expected to get document type");
1491
1492        let random_owner_id0 = rand::thread_rng().gen::<[u8; 32]>();
1493
1494        let person_document0 = json_document_to_document(
1495            "tests/supporting_files/contract/family/person0.json",
1496            Some(random_owner_id0.into()),
1497            document_type,
1498            platform_version,
1499        )
1500        .expect("expected to get document");
1501
1502        let random_owner_id1 = rand::thread_rng().gen::<[u8; 32]>();
1503
1504        let person_document1 = json_document_to_document(
1505            "tests/supporting_files/contract/family/person3-older.json",
1506            Some(random_owner_id1.into()),
1507            document_type,
1508            platform_version,
1509        )
1510        .expect("expected to get document");
1511
1512        let operations = vec![
1513            AddOperation {
1514                owned_document_info: OwnedDocumentInfo {
1515                    document_info: DocumentRefInfo((
1516                        &person_document0,
1517                        StorageFlags::optional_default_as_cow(),
1518                    )),
1519                    owner_id: Some(random_owner_id0),
1520                },
1521                override_document: false,
1522            },
1523            AddOperation {
1524                owned_document_info: OwnedDocumentInfo {
1525                    document_info: DocumentRefInfo((
1526                        &person_document1,
1527                        StorageFlags::optional_default_as_cow(),
1528                    )),
1529                    owner_id: Some(random_owner_id1),
1530                },
1531                override_document: false,
1532            },
1533        ];
1534        let drive_operations = vec![DocumentOperation(
1535            MultipleDocumentOperationsForSameContractDocumentType {
1536                document_operations: DocumentOperationsForContractDocumentType {
1537                    operations,
1538                    contract: &contract,
1539                    document_type,
1540                },
1541            },
1542        )];
1543
1544        drive
1545            .apply_drive_operations(
1546                drive_operations,
1547                true,
1548                &BlockInfo::default(),
1549                Some(&db_transaction),
1550                platform_version,
1551                None,
1552            )
1553            .expect("expected to be able to insert documents");
1554
1555        // This was the setup now let's do the update
1556
1557        let person_document0 = json_document_to_document(
1558            "tests/supporting_files/contract/family/person0-older.json",
1559            Some(random_owner_id0.into()),
1560            document_type,
1561            platform_version,
1562        )
1563        .expect("expected to get document");
1564
1565        let person_document1 = json_document_to_document(
1566            "tests/supporting_files/contract/family/person3.json",
1567            Some(random_owner_id1.into()),
1568            document_type,
1569            platform_version,
1570        )
1571        .expect("expected to get document");
1572
1573        let operations = vec![
1574            UpdateOperation(UpdateOperationInfo {
1575                document: &person_document0,
1576                serialized_document: None,
1577                owner_id: Some(random_owner_id0),
1578                storage_flags: None,
1579            }),
1580            UpdateOperation(UpdateOperationInfo {
1581                document: &person_document1,
1582                serialized_document: None,
1583                owner_id: Some(random_owner_id1),
1584                storage_flags: None,
1585            }),
1586        ];
1587
1588        let drive_operations = vec![DocumentOperation(
1589            MultipleDocumentOperationsForSameContractDocumentType {
1590                document_operations: DocumentOperationsForContractDocumentType {
1591                    operations,
1592                    contract: &contract,
1593                    document_type,
1594                },
1595            },
1596        )];
1597
1598        drive
1599            .apply_drive_operations(
1600                drive_operations,
1601                true,
1602                &BlockInfo::default(),
1603                Some(&db_transaction),
1604                platform_version,
1605                None,
1606            )
1607            .expect("expected to be able to update documents");
1608
1609        let query_value = json!({
1610            "where": [
1611                ["age", ">=", 5]
1612            ],
1613            "limit": 100,
1614            "orderBy": [
1615                ["age", "asc"],
1616            ]
1617        });
1618        let where_cbor = cbor_serializer::serializable_value_to_cbor(&query_value, None)
1619            .expect("expected to serialize to cbor");
1620
1621        let (docs, _, _) = drive
1622            .query_documents_cbor_from_contract(
1623                &contract,
1624                document_type,
1625                where_cbor.as_slice(),
1626                None,
1627                Some(&db_transaction),
1628                Some(platform_version.protocol_version),
1629            )
1630            .expect("expected to query");
1631        assert_eq!(docs.len(), 2);
1632
1633        let query_value = json!({
1634            "where": [
1635                ["age", "==", 35]
1636            ],
1637            "limit": 100,
1638            "orderBy": [
1639                ["age", "asc"],
1640            ]
1641        });
1642        let where_cbor = cbor_serializer::serializable_value_to_cbor(&query_value, None)
1643            .expect("expected to serialize to cbor");
1644
1645        let (docs, _, _) = drive
1646            .query_documents_cbor_from_contract(
1647                &contract,
1648                document_type,
1649                where_cbor.as_slice(),
1650                None,
1651                Some(&db_transaction),
1652                Some(platform_version.protocol_version),
1653            )
1654            .expect("expected to query");
1655        assert_eq!(docs.len(), 1);
1656
1657        let query_value = json!({
1658            "where": [
1659                ["age", "==", 36]
1660            ],
1661            "limit": 100,
1662            "orderBy": [
1663                ["age", "asc"],
1664            ]
1665        });
1666        let where_cbor = cbor_serializer::serializable_value_to_cbor(&query_value, None)
1667            .expect("expected to serialize to cbor");
1668
1669        let (docs, _, _) = drive
1670            .query_documents_cbor_from_contract(
1671                &contract,
1672                document_type,
1673                where_cbor.as_slice(),
1674                None,
1675                Some(&db_transaction),
1676                Some(platform_version.protocol_version),
1677            )
1678            .expect("expected to query");
1679        assert_eq!(docs.len(), 1);
1680    }
1681
1682    fn add(identity: u8, added_balance: Credits) -> DriveOperation<'static> {
1683        DriveOperation::IdentityOperation(IdentityOperationType::AddToIdentityBalance {
1684            identity_id: [identity; 32],
1685            added_balance,
1686        })
1687    }
1688
1689    fn remove(identity: u8, balance_to_remove: Credits) -> DriveOperation<'static> {
1690        DriveOperation::IdentityOperation(IdentityOperationType::RemoveFromIdentityBalance {
1691            identity_id: [identity; 32],
1692            balance_to_remove,
1693        })
1694    }
1695
1696    fn add_to_pot(pot: ContractFeePot, amount: Credits) -> DriveOperation<'static> {
1697        DriveOperation::ContractFeePotOperation(ContractFeePotOperationType::AddToPot {
1698            contract_id: Identifier::new([9; 32]),
1699            pot,
1700            amount,
1701        })
1702    }
1703
1704    fn deduct_from_pot(pot: ContractFeePot, amount: Credits) -> DriveOperation<'static> {
1705        DriveOperation::ContractFeePotOperation(ContractFeePotOperationType::DeductFromPot {
1706            contract_id: Identifier::new([9; 32]),
1707            pot,
1708            amount,
1709        })
1710    }
1711
1712    fn merged(operations: Vec<DriveOperation<'static>>) -> String {
1713        format!(
1714            "{:?}",
1715            DriveOperation::merge_balance_writes(operations).expect("expected to merge")
1716        )
1717    }
1718
1719    #[test]
1720    fn should_merge_every_write_of_one_identity_balance_into_one_in_place_of_the_first() {
1721        // A purchase price and a purchase fee leaving the buyer, and a sale paying the seller
1722        assert_eq!(
1723            merged(vec![remove(1, 100), add(2, 100), remove(1, 7)]),
1724            format!("{:?}", vec![remove(1, 107), add(2, 100)])
1725        );
1726        // A sale paying a contract owner who also pays the moderators part of the fee
1727        assert_eq!(
1728            merged(vec![add(3, 100), remove(3, 7)]),
1729            format!("{:?}", vec![add(3, 93)])
1730        );
1731    }
1732
1733    #[test]
1734    fn should_merge_the_writes_of_one_fee_pot_and_drop_writes_that_change_nothing() {
1735        assert_eq!(
1736            merged(vec![
1737                add_to_pot(ContractFeePot::Moderators, 5),
1738                add_to_pot(ContractFeePot::Owner, 2),
1739                deduct_from_pot(ContractFeePot::Moderators, 8),
1740                add(4, 10),
1741                remove(4, 10),
1742            ]),
1743            format!(
1744                "{:?}",
1745                vec![
1746                    deduct_from_pot(ContractFeePot::Moderators, 3),
1747                    add_to_pot(ContractFeePot::Owner, 2),
1748                ]
1749            )
1750        );
1751    }
1752
1753    fn fund_vote_poll(add_balance: Credits) -> DriveOperation<'static> {
1754        DriveOperation::PrefundedSpecializedBalanceOperation(
1755            PrefundedSpecializedBalanceOperationType::CreateNewPrefundedBalance {
1756                prefunded_specialized_balance_id: Identifier::new([8; 32]),
1757                add_balance,
1758            },
1759        )
1760    }
1761
1762    fn pay_from_vote_poll(remove_balance: Credits) -> DriveOperation<'static> {
1763        DriveOperation::PrefundedSpecializedBalanceOperation(
1764            PrefundedSpecializedBalanceOperationType::DeductFromPrefundedBalance {
1765                prefunded_specialized_balance_id: Identifier::new([8; 32]),
1766                remove_balance,
1767            },
1768        )
1769    }
1770
1771    #[test]
1772    fn should_merge_the_writes_of_one_prefunded_specialized_balance() {
1773        assert_eq!(
1774            merged(vec![pay_from_vote_poll(3), pay_from_vote_poll(4)]),
1775            format!("{:?}", vec![pay_from_vote_poll(7)])
1776        );
1777        assert_eq!(
1778            merged(vec![fund_vote_poll(10), pay_from_vote_poll(4)]),
1779            format!("{:?}", vec![fund_vote_poll(6)])
1780        );
1781    }
1782
1783    fn token(position: u8) -> Identifier {
1784        Identifier::new([20 + position; 32])
1785    }
1786
1787    fn transfer(from: u8, to: u8) -> DriveOperation<'static> {
1788        DriveOperation::TokenOperation(TokenOperationType::TokenTransfer {
1789            token_id: token(0),
1790            sender_id: Identifier::new([from; 32]),
1791            recipient_id: Identifier::new([to; 32]),
1792            amount: 5,
1793        })
1794    }
1795
1796    fn burn(token_position: u8, holder: u8) -> DriveOperation<'static> {
1797        DriveOperation::TokenOperation(TokenOperationType::TokenBurn {
1798            token_id: token(token_position),
1799            identity_balance_holder_id: Identifier::new([holder; 32]),
1800            burn_amount: 5,
1801        })
1802    }
1803
1804    fn mint(token_position: u8, holder: u8) -> DriveOperation<'static> {
1805        DriveOperation::TokenOperation(TokenOperationType::TokenMint {
1806            token_id: token(token_position),
1807            identity_balance_holder_id: Identifier::new([holder; 32]),
1808            mint_amount: 5,
1809            allow_first_mint: false,
1810            allow_saturation: false,
1811        })
1812    }
1813
1814    #[test]
1815    fn should_refuse_a_batch_that_writes_one_token_balance_or_supply_twice() {
1816        // The sender of a transfer burns too: their balance is written twice.
1817        assert!(DriveOperation::refuse_repeated_token_balance_writes(&[
1818            transfer(1, 2),
1819            burn(0, 1)
1820        ])
1821        .is_err());
1822        // A mint and a burn of one token, by different holders: its supply is written twice.
1823        assert!(
1824            DriveOperation::refuse_repeated_token_balance_writes(&[mint(0, 1), burn(0, 2)])
1825                .is_err()
1826        );
1827    }
1828
1829    #[test]
1830    fn should_admit_token_writes_that_each_touch_their_own_keys() {
1831        assert!(DriveOperation::refuse_repeated_token_balance_writes(&[
1832            transfer(1, 2),
1833            mint(1, 1),
1834            burn(2, 2),
1835            add(1, 10),
1836        ])
1837        .is_ok());
1838    }
1839
1840    #[test]
1841    fn should_keep_a_batch_that_writes_every_key_once_as_it_is() {
1842        let operations = vec![
1843            remove(1, 100),
1844            add(2, 100),
1845            add_to_pot(ContractFeePot::Owner, 2),
1846        ];
1847        assert_eq!(merged(operations.clone()), format!("{operations:?}"));
1848    }
1849
1850    fn note(tag: u8) -> ShieldedActionNote {
1851        ShieldedActionNote {
1852            nullifier: [tag; 32],
1853            cmx: [tag.wrapping_add(1); 32],
1854            cv_net: [tag.wrapping_add(2); 32],
1855            encrypted_note: vec![tag; 216],
1856        }
1857    }
1858
1859    fn shield(token_position: u8, holder: u8, note_tag: u8) -> DriveOperation<'static> {
1860        DriveOperation::TokenOperation(TokenOperationType::TokenShield {
1861            token_id: token(token_position),
1862            identity_id: Identifier::new([holder; 32]),
1863            amount: 5,
1864            notes: vec![note(note_tag)],
1865        })
1866    }
1867
1868    fn unshield(token_position: u8, recipient: u8, note_tag: u8) -> DriveOperation<'static> {
1869        DriveOperation::TokenOperation(TokenOperationType::TokenUnshield {
1870            token_id: token(token_position),
1871            recipient_id: Identifier::new([recipient; 32]),
1872            amount: 5,
1873            nullifiers: vec![[note_tag; 32]],
1874            notes: vec![note(note_tag)],
1875        })
1876    }
1877
1878    fn shielded_transfer(token_position: u8, note_tag: u8) -> DriveOperation<'static> {
1879        DriveOperation::TokenOperation(TokenOperationType::TokenShieldedTransfer {
1880            token_id: token(token_position),
1881            nullifiers: vec![[note_tag; 32]],
1882            notes: vec![note(note_tag)],
1883        })
1884    }
1885
1886    fn mint_to_pool(token_position: u8, note_tag: u8) -> DriveOperation<'static> {
1887        DriveOperation::TokenOperation(TokenOperationType::TokenMintToPool {
1888            token_id: token(token_position),
1889            amount: 5,
1890            allow_first_mint: false,
1891            notes: vec![note(note_tag)],
1892        })
1893    }
1894
1895    fn burn_from_pool(token_position: u8, note_tag: u8) -> DriveOperation<'static> {
1896        DriveOperation::TokenOperation(TokenOperationType::TokenBurnFromPool {
1897            token_id: token(token_position),
1898            amount: 5,
1899            nullifiers: vec![[note_tag; 32]],
1900            notes: vec![note(note_tag)],
1901        })
1902    }
1903
1904    /// Every pool operation that moves the pool's total replaces it with an absolute value, so
1905    /// two of them on one token lose a write however they are paired.
1906    #[test]
1907    fn should_refuse_a_batch_that_writes_one_token_shielded_pool_total_twice() {
1908        for (pairing, first, second) in [
1909            (
1910                "a shield and an unshield",
1911                shield(0, 1, 1),
1912                unshield(0, 2, 2),
1913            ),
1914            (
1915                "a shield and a mint into the pool",
1916                shield(0, 1, 1),
1917                mint_to_pool(0, 2),
1918            ),
1919            (
1920                "an unshield and a burn from the pool",
1921                unshield(0, 1, 1),
1922                burn_from_pool(0, 2),
1923            ),
1924            (
1925                "a mint into and a burn from the pool",
1926                mint_to_pool(0, 1),
1927                burn_from_pool(0, 2),
1928            ),
1929            (
1930                "two mints into the pool",
1931                mint_to_pool(0, 1),
1932                mint_to_pool(0, 2),
1933            ),
1934        ] {
1935            assert!(
1936                DriveOperation::refuse_repeated_token_balance_writes(&[first, second]).is_err(),
1937                "{pairing} both write one pool total"
1938            );
1939        }
1940    }
1941
1942    /// A shield debits the identity it takes tokens from and an unshield credits the one it
1943    /// hands them to, so either paired with a transparent write of that balance loses a write.
1944    #[test]
1945    fn should_refuse_a_batch_that_writes_one_shielding_identity_balance_twice() {
1946        assert!(DriveOperation::refuse_repeated_token_balance_writes(&[
1947            shield(0, 1, 1),
1948            burn(0, 1)
1949        ])
1950        .is_err());
1951        assert!(DriveOperation::refuse_repeated_token_balance_writes(&[
1952            unshield(0, 1, 1),
1953            mint(0, 1)
1954        ])
1955        .is_err());
1956    }
1957
1958    /// Minting into the pool raises the token's total supply and burning from it lowers it, so
1959    /// either paired with a transparent mint or burn of that token loses a write.
1960    #[test]
1961    fn should_refuse_a_batch_that_writes_one_pool_minted_token_supply_twice() {
1962        assert!(DriveOperation::refuse_repeated_token_balance_writes(&[
1963            mint_to_pool(0, 1),
1964            mint(0, 1)
1965        ])
1966        .is_err());
1967        assert!(DriveOperation::refuse_repeated_token_balance_writes(&[
1968            burn_from_pool(0, 1),
1969            burn(0, 1)
1970        ])
1971        .is_err());
1972    }
1973
1974    #[test]
1975    fn should_admit_pool_operations_that_each_touch_their_own_keys() {
1976        // Pools of different tokens are different keys.
1977        assert!(DriveOperation::refuse_repeated_token_balance_writes(&[
1978            shield(0, 1, 1),
1979            mint_to_pool(1, 2)
1980        ])
1981        .is_ok());
1982        // Shielding and unshielding leave the total supply alone, so a mint or a burn of the
1983        // same token by another holder shares no key with them.
1984        assert!(DriveOperation::refuse_repeated_token_balance_writes(&[
1985            shield(0, 1, 1),
1986            mint(0, 2)
1987        ])
1988        .is_ok());
1989        assert!(DriveOperation::refuse_repeated_token_balance_writes(&[
1990            unshield(0, 1, 1),
1991            burn(0, 2)
1992        ])
1993        .is_ok());
1994        // Minting into the pool and burning from it touch no identity balance.
1995        assert!(DriveOperation::refuse_repeated_token_balance_writes(&[
1996            mint_to_pool(0, 1),
1997            transfer(1, 2)
1998        ])
1999        .is_ok());
2000        // A transfer inside the pool moves nothing in or out of it, and no supply or balance,
2001        // so it shares no key with the pool total a shield writes or the supply a mint does.
2002        assert!(DriveOperation::refuse_repeated_token_balance_writes(&[
2003            shielded_transfer(0, 1),
2004            shield(0, 1, 2),
2005            mint(0, 2),
2006        ])
2007        .is_ok());
2008    }
2009
2010    /// A drive holding a token with a shielded pool and two identities each holding `balance`
2011    /// of it: enough state for a composed batch of token operations to be lowered and applied.
2012    fn setup_drive_with_token_pool(balance: TokenAmount) -> (Drive, [u8; 32], [u8; 32], [u8; 32]) {
2013        let drive = setup_drive_with_initial_state_structure(None);
2014        let platform_version = PlatformVersion::latest();
2015        let block_info = BlockInfo::default();
2016        let token_id = [7u8; 32];
2017
2018        let mut holders = [[0u8; 32]; 2];
2019        for (seed, holder) in holders.iter_mut().enumerate() {
2020            let identity = Identity::random_identity(3, Some(seed as u64), platform_version)
2021                .expect("random identity");
2022            *holder = identity.id().to_buffer();
2023            drive
2024                .add_new_identity(identity, false, &block_info, true, None, platform_version)
2025                .expect("insert identity");
2026        }
2027
2028        drive
2029            .create_token_trees(
2030                Identifier::from([5u8; 32]),
2031                0,
2032                token_id,
2033                false,
2034                false,
2035                &block_info,
2036                true,
2037                None,
2038                platform_version,
2039            )
2040            .expect("create token trees");
2041        // Keep the supply consistent with the balances handed out, as a mint would.
2042        drive
2043            .add_to_token_total_supply(
2044                token_id,
2045                balance * 2,
2046                true,
2047                false,
2048                true,
2049                &block_info,
2050                None,
2051                platform_version,
2052            )
2053            .expect("add to total supply");
2054        for holder in holders {
2055            drive
2056                .add_to_identity_token_balance(
2057                    token_id,
2058                    holder,
2059                    balance,
2060                    &block_info,
2061                    true,
2062                    None,
2063                    platform_version,
2064                    None,
2065                )
2066                .expect("add token balance");
2067        }
2068
2069        let operations = drive
2070            .create_token_shielded_pool_trees_operations(
2071                token_id,
2072                false,
2073                &mut None,
2074                None,
2075                platform_version,
2076            )
2077            .expect("pool tree operations");
2078        drive
2079            .apply_batch_low_level_drive_operations(
2080                None,
2081                None,
2082                operations,
2083                &mut vec![],
2084                &platform_version.drive,
2085            )
2086            .expect("create pool trees");
2087
2088        (drive, token_id, holders[0], holders[1])
2089    }
2090
2091    /// Two shields of one token in one batch each read the pool total committed before the
2092    /// batch and write back an absolute value, so the second would discard the first: both
2093    /// identities would lose their tokens while the pool gained only one shield's worth.
2094    #[test]
2095    fn should_refuse_a_batch_that_shields_into_one_token_pool_twice() {
2096        let (drive, token_id, first_holder, second_holder) = setup_drive_with_token_pool(1_000);
2097        let platform_version = PlatformVersion::latest();
2098
2099        let result = drive.apply_drive_operations(
2100            vec![
2101                DriveOperation::TokenOperation(TokenOperationType::TokenShield {
2102                    token_id: Identifier::from(token_id),
2103                    identity_id: Identifier::from(first_holder),
2104                    amount: 400,
2105                    notes: vec![note(1)],
2106                }),
2107                DriveOperation::TokenOperation(TokenOperationType::TokenShield {
2108                    token_id: Identifier::from(token_id),
2109                    identity_id: Identifier::from(second_holder),
2110                    amount: 300,
2111                    notes: vec![note(2)],
2112                }),
2113            ],
2114            true,
2115            &BlockInfo::default(),
2116            None,
2117            platform_version,
2118            None,
2119        );
2120
2121        assert!(
2122            matches!(
2123                &result,
2124                Err(Error::Drive(DriveError::CorruptedCodeExecution(message)))
2125                    if message.contains("more than once")
2126            ),
2127            "a batch shielding into one pool twice must be refused, got {result:?}"
2128        );
2129    }
2130
2131    /// A mint into the pool and a transparent mint of the same token in one batch each read the
2132    /// total supply committed before the batch and write it back absolute, so the second would
2133    /// discard the first and the supply would stop counting both mints.
2134    #[test]
2135    fn should_refuse_a_batch_that_mints_one_token_into_its_pool_and_to_a_holder() {
2136        let (drive, token_id, holder, _) = setup_drive_with_token_pool(1_000);
2137        let platform_version = PlatformVersion::latest();
2138
2139        let result = drive.apply_drive_operations(
2140            vec![
2141                DriveOperation::TokenOperation(TokenOperationType::TokenMintToPool {
2142                    token_id: Identifier::from(token_id),
2143                    amount: 400,
2144                    allow_first_mint: false,
2145                    notes: vec![note(1)],
2146                }),
2147                DriveOperation::TokenOperation(TokenOperationType::TokenMint {
2148                    token_id: Identifier::from(token_id),
2149                    identity_balance_holder_id: Identifier::from(holder),
2150                    mint_amount: 300,
2151                    allow_first_mint: false,
2152                    allow_saturation: false,
2153                }),
2154            ],
2155            true,
2156            &BlockInfo::default(),
2157            None,
2158            platform_version,
2159            None,
2160        );
2161
2162        assert!(
2163            matches!(
2164                &result,
2165                Err(Error::Drive(DriveError::CorruptedCodeExecution(message)))
2166                    if message.contains("more than once")
2167            ),
2168            "a batch minting one token into its pool and to a holder must be refused, got {result:?}"
2169        );
2170    }
2171}