• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tari-project / tari / 24462322093

15 Apr 2026 03:10PM UTC coverage: 61.006% (-0.06%) from 61.068%
24462322093

push

github

SWvheerden
chore: new version 5.3.0-pre.8

70645 of 115800 relevant lines covered (61.01%)

224419.43 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

7.63
/base_layer/wallet/src/output_manager_service/storage/sqlite_db/output_sql.rs
1
//  Copyright 2021. The Tari Project
2
//
3
//  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
4
//  following conditions are met:
5
//
6
//  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
7
//  disclaimer.
8
//
9
//  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
10
//  following disclaimer in the documentation and/or other materials provided with the distribution.
11
//
12
//  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
13
//  products derived from this software without specific prior written permission.
14
//
15
//  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
16
//  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17
//  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
18
//  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
19
//  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
20
//  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
21
//  USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
22

23
use std::{
24
    convert::{TryFrom, TryInto},
25
    ops::Range,
26
    str::FromStr,
27
};
28

29
use borsh::BorshDeserialize;
30
use chrono::NaiveDateTime;
31
use derivative::Derivative;
32
use diesel::{
33
    dsl::{count_star, not, sql},
34
    prelude::*,
35
    sql_query,
36
    sql_types::{BigInt, Nullable},
37
};
38
use log::*;
39
use tari_common_sqlite::util::diesel_ext::ExpectedRowsExtension;
40
use tari_common_types::{
41
    transaction::TxId,
42
    types::{
43
        ComAndPubSignature,
44
        CompressedCommitment,
45
        CompressedPublicKey,
46
        FixedHash,
47
        HashOutput,
48
        PrivateKey,
49
        RangeProof,
50
    },
51
};
52
use tari_crypto::tari_utilities::ByteArray;
53
use tari_script::{ExecutionStack, TariScript};
54
use tari_transaction_components::{
55
    MicroMinotari,
56
    key_manager::TariKeyId,
57
    transaction_components::{
58
        EncryptedData,
59
        MemoField,
60
        OutputFeatures,
61
        OutputType,
62
        TransactionOutputVersion,
63
        WalletOutput,
64
    },
65
};
66
use tari_transaction_key_manager::legacy_key_manager::{LegacyTariKeyId, LegacyTransactionKeyManagerInterface};
67
use tari_utilities::hex::Hex;
68

69
use crate::{
70
    output_manager_service::{
71
        TRANSACTION_INPUTS_LIMIT,
72
        UtxoSelectionFilter,
73
        UtxoSelectionOrdering,
74
        error::OutputManagerStorageError,
75
        input_selection::{UtxoSelectionCriteria, UtxoSelectionMode},
76
        service::Balance,
77
        storage::{
78
            OutputSource,
79
            OutputStatus,
80
            database::{OutputBackendQuery, SortDirection},
81
            models::{DbWalletOutput, SpendingPriority},
82
            sqlite_db::{CoinBucket, UpdateOutput, UpdateOutputSql},
83
        },
84
    },
85
    schema::outputs,
86
};
87

88
const LOG_TARGET: &str = "wallet::output_manager_service::database::wallet";
89

90
#[derive(Clone, Derivative, Debug, Queryable, Identifiable, PartialEq, QueryableByName)]
×
91
#[diesel(table_name = outputs)]
92
pub struct OutputSql {
93
    pub id: i32, // Auto inc primary key
94
    pub commitment: Vec<u8>,
95
    pub rangeproof: Option<Vec<u8>>,
96
    pub spending_key: String,
97
    pub value: i64,
98
    pub output_type: i32,
99
    pub maturity: i64,
100
    pub status: i32,
101
    pub hash: Vec<u8>,
102
    pub script: Vec<u8>,
103
    pub input_data: Vec<u8>,
104
    pub script_private_key: String,
105
    pub script_lock_height: i64,
106
    pub sender_offset_public_key: Vec<u8>,
107
    pub metadata_signature_ephemeral_commitment: Vec<u8>,
108
    pub metadata_signature_ephemeral_pubkey: Vec<u8>,
109
    pub metadata_signature_u_a: Vec<u8>,
110
    pub metadata_signature_u_x: Vec<u8>,
111
    pub metadata_signature_u_y: Vec<u8>,
112
    pub mined_height: Option<i64>,
113
    pub mined_in_block: Option<Vec<u8>>,
114
    pub marked_deleted_at_height: Option<i64>,
115
    pub marked_deleted_in_block: Option<Vec<u8>>,
116
    pub received_in_tx_id: Option<i64>,
117
    pub spent_in_tx_id: Option<i64>,
118
    pub coinbase_extra: Option<Vec<u8>>,
119
    pub features_json: String,
120
    pub spending_priority: i32,
121
    pub covenant: Vec<u8>,
122
    pub mined_timestamp: Option<NaiveDateTime>,
123
    pub encrypted_data: Vec<u8>,
124
    pub minimum_value_promise: i64,
125
    pub source: i32,
126
    pub last_validation_timestamp: Option<NaiveDateTime>,
127
    pub payment_id: Option<Vec<u8>>,
128
    pub user_payment_id: Option<Vec<u8>>,
129
}
130

131
impl OutputSql {
132
    /// Return all outputs
133
    pub fn index(conn: &mut SqliteConnection) -> Result<Vec<OutputSql>, OutputManagerStorageError> {
2✔
134
        Ok(outputs::table.load::<OutputSql>(conn)?)
2✔
135
    }
2✔
136

137
    /// Return all outputs with a given status
138
    pub fn index_status(
2✔
139
        statuses: Vec<OutputStatus>,
2✔
140
        conn: &mut SqliteConnection,
2✔
141
    ) -> Result<Vec<OutputSql>, OutputManagerStorageError> {
2✔
142
        Ok(outputs::table
2✔
143
            .filter(outputs::status.eq_any::<Vec<i32>>(statuses.into_iter().map(|s| s as i32).collect()))
2✔
144
            .load(conn)?)
2✔
145
    }
2✔
146

147
    /// Retrieves UTXOs by a set of given rules
148
    #[allow(clippy::cast_sign_loss)]
149
    pub fn fetch_outputs_by_query(
×
150
        q: OutputBackendQuery,
×
151
        conn: &mut SqliteConnection,
×
152
    ) -> Result<Vec<OutputSql>, OutputManagerStorageError> {
×
153
        let mut query = outputs::table
×
154
            .into_boxed()
×
155
            .filter(outputs::script_lock_height.le(q.tip_height))
×
156
            .filter(outputs::maturity.le(q.tip_height))
×
157
            // make sure we account for i64 wrap around to neg
158
            .filter(outputs::script_lock_height.ge(0))
×
159
            .filter(outputs::maturity.ge(0));
×
160

161
        if let Some((offset, limit)) = q.pagination {
×
162
            query = query.offset(offset).limit(limit);
×
163
        }
×
164

165
        // filtering by OutputStatus
166
        query = match q.status.len() {
×
167
            0 => query,
×
168
            1 => query.filter(outputs::status.eq(*q.status.first().expect("Already checked") as i32)),
×
169
            _ => query.filter(outputs::status.eq_any::<Vec<i32>>(q.status.into_iter().map(|s| s as i32).collect())),
×
170
        };
171

172
        // filtering by Commitment
173
        if !q.commitments.is_empty() {
×
174
            query = match q.commitments.len() {
×
175
                0 => query,
×
176
                1 => query.filter(outputs::commitment.eq(q.commitments.first().expect("Already checked").to_vec())),
×
177
                _ => query.filter(
×
178
                    outputs::commitment.eq_any::<Vec<Vec<u8>>>(q.commitments.into_iter().map(|c| c.to_vec()).collect()),
×
179
                ),
180
            };
181
        }
×
182

183
        // if set, filtering by minimum value
184
        if let Some((min, is_inclusive)) = q.value_min {
×
185
            query = if is_inclusive {
×
186
                query.filter(outputs::value.ge(min))
×
187
            } else {
188
                query.filter(outputs::value.gt(min))
×
189
            };
190
        }
×
191

192
        // if set, filtering by max value
193
        if let Some((max, is_inclusive)) = q.value_max {
×
194
            query = if is_inclusive {
×
195
                query.filter(outputs::value.le(max))
×
196
            } else {
197
                query.filter(outputs::value.lt(max))
×
198
            };
199
        }
×
200

201
        use SortDirection::{Asc, Desc};
202
        Ok(q.sorting
×
203
            .into_iter()
×
204
            .fold(query, |query, s| match s {
×
205
                ("value", d) => match d {
×
206
                    Asc => query.then_order_by(outputs::value.asc()),
×
207
                    Desc => query.then_order_by(outputs::value.desc()),
×
208
                },
209
                ("mined_height", d) => match d {
×
210
                    Asc => query.then_order_by(outputs::mined_height.asc()),
×
211
                    Desc => query.then_order_by(outputs::mined_height.desc()),
×
212
                },
213
                _ => query,
×
214
            })
×
215
            .load(conn)?)
×
216
    }
×
217

218
    /// Retrieves UTXOs than can be spent, sorted by priority, then value from smallest to largest.
219
    #[allow(clippy::cast_sign_loss, clippy::too_many_lines)]
220
    pub fn fetch_unspent_outputs_for_spending(
×
221
        selection_criteria: &UtxoSelectionCriteria,
×
222
        amount: u64,
×
223
        tip_height: Option<u64>,
×
224
        conn: &mut SqliteConnection,
×
225
    ) -> Result<Vec<OutputSql>, OutputManagerStorageError> {
×
226
        let i64_tip_height = tip_height.and_then(|h| i64::try_from(h).ok()).unwrap_or(i64::MAX);
×
227
        let i64_value = i64::try_from(selection_criteria.min_dust).unwrap_or(i64::MAX);
×
228

229
        let mut query = outputs::table
×
230
            .into_boxed()
×
231
            .filter(outputs::status.eq(OutputStatus::Unspent as i32))
×
232
            .filter(outputs::value.gt(i64_value))
×
233
            .order_by(outputs::spending_priority.desc());
×
234

235
        // NOTE: Safe mode presets `script_lock_height` and `maturity` filters for all queries
236
        if selection_criteria.mode == UtxoSelectionMode::Safe {
×
237
            query = query
×
238
                .filter(outputs::script_lock_height.le(i64_tip_height))
×
239
                .filter(outputs::maturity.le(i64_tip_height))
×
240
                // make sure we account for i64 wrap around to neg
×
241
                .filter(outputs::script_lock_height.ge(0))
×
242
                .filter(outputs::maturity.ge(0));
×
243
        };
×
244

245
        match &selection_criteria.filter {
×
246
            UtxoSelectionFilter::Standard => {
247
                query = query.filter(
×
248
                    outputs::output_type
×
249
                        .eq(i32::from(OutputType::Standard.as_byte()))
×
250
                        .or(outputs::output_type.eq(i32::from(OutputType::Coinbase.as_byte()))),
×
251
                );
252

253
                if selection_criteria.excluding_onesided {
×
254
                    query = query.filter(outputs::source.ne(OutputSource::OneSided as i32));
×
255
                }
×
256

257
                if selection_criteria.excluding_multisig {
×
258
                    query = query.filter(outputs::source.ne(OutputSource::Multisig as i32));
×
259
                }
×
260
            },
261

262
            UtxoSelectionFilter::SpecificOutputs { commitments } => {
×
263
                query = match commitments.len() {
×
264
                    0 => query,
×
265
                    1 => query.filter(outputs::commitment.eq(commitments.first().expect("Already checked").to_vec())),
×
266
                    _ => query.filter(
×
267
                        outputs::commitment.eq_any::<Vec<Vec<u8>>>(commitments.iter().map(|c| c.to_vec()).collect()),
×
268
                    ),
269
                };
270
            },
271

272
            UtxoSelectionFilter::MustInclude { commitments } => {
×
273
                return Self::handle_must_include_filter(selection_criteria, commitments, amount, tip_height, conn);
×
274
            },
275
        }
276

277
        for exclude in &selection_criteria.excluding {
×
278
            query = query.filter(outputs::commitment.ne(exclude.as_bytes()));
×
279
        }
×
280

281
        query = match selection_criteria.ordering {
×
282
            UtxoSelectionOrdering::SmallestFirst => query.then_order_by(outputs::value.asc()),
×
283
            UtxoSelectionOrdering::LargestFirst => query.then_order_by(outputs::value.desc()),
×
284
            UtxoSelectionOrdering::Default => {
285
                // NOTE: keeping filtering by `script_lock_height` and `maturity` for all modes
286
                // lets get the max value for all utxos
287
                let max: Option<i64> = outputs::table
×
288
                    .filter(outputs::status.eq(OutputStatus::Unspent as i32))
×
289
                    .filter(outputs::script_lock_height.le(i64_tip_height))
×
290
                    .filter(outputs::maturity.le(i64_tip_height))
×
291
                    // make sure we account for i64 wrap around to neg
292
                    .filter(outputs::script_lock_height.ge(0))
×
293
                    .filter(outputs::maturity.ge(0))
×
294
                    .order(outputs::value.desc())
×
295
                    .select(outputs::value)
×
296
                    .first(conn)
×
297
                    .optional()?;
×
298

299
                match max {
×
300
                    // Want to reduce the number of inputs to reduce fees
301
                    Some(max) if amount > max as u64 => query.then_order_by(outputs::value.desc()),
×
302

303
                    // Use the smaller utxos to make up this transaction.
304
                    _ => query.then_order_by(outputs::value.asc()),
×
305
                }
306
            },
307
        };
308

309
        Ok(query.limit(i64::from(TRANSACTION_INPUTS_LIMIT)).load(conn)?)
×
310
    }
×
311

312
    /// Retrieves UTXOs within a specified limited range with minimum target amount for spending. If not enough UTXOs
313
    /// can be found, an empty vector is returned.
314
    pub fn get_range_limited_outputs_for_spending(
×
315
        selection_criteria: &UtxoSelectionCriteria,
×
316
        tip_height: Option<u64>,
×
317
        conn: &mut SqliteConnection,
×
318
    ) -> Result<(Vec<OutputSql>, MicroMinotari), OutputManagerStorageError> {
×
319
        let range_limit =
×
320
            selection_criteria
×
321
                .range_limit
×
322
                .as_ref()
×
323
                .ok_or_else(|| OutputManagerStorageError::RangeLimitError {
×
324
                    reason: "Range limit must be specified".to_string(),
×
325
                })?;
×
326
        let amounts_from = i64::try_from(range_limit.range.start).unwrap_or(i64::MAX);
×
327
        let amounts_to = i64::try_from(range_limit.range.end).unwrap_or(i64::MAX);
×
328

329
        let mut query = outputs::table
×
330
            .into_boxed()
×
331
            .filter(outputs::status.eq(OutputStatus::Unspent as i32))
×
332
            .filter(outputs::value.ge(amounts_from))
×
333
            .filter(outputs::value.lt(amounts_to));
×
334

335
        // NOTE: Safe mode presets `script_lock_height` and `maturity` filters for all queries
336
        let i64_tip_height = tip_height.and_then(|h| i64::try_from(h).ok()).unwrap_or(i64::MAX);
×
337
        if selection_criteria.mode == UtxoSelectionMode::Safe {
×
338
            query = query
×
339
                .filter(outputs::script_lock_height.le(i64_tip_height))
×
340
                .filter(outputs::maturity.le(i64_tip_height))
×
341
                // make sure we account for i64 wrap around to neg
×
342
                .filter(outputs::script_lock_height.ge(0))
×
343
                .filter(outputs::maturity.ge(0));
×
344
        };
×
345

346
        for exclude in &selection_criteria.excluding {
×
347
            query = query.filter(outputs::commitment.ne(exclude.as_bytes()));
×
348
        }
×
349

350
        query = query.then_order_by(outputs::value.asc());
×
351

352
        let transaction_input_limit = u32::try_from(range_limit.transaction_input_limit)
×
353
            .unwrap_or(u32::MAX)
×
354
            .min(TRANSACTION_INPUTS_LIMIT);
×
355
        let outputs: Vec<OutputSql> = query.limit(i64::from(transaction_input_limit)).load(conn)?;
×
356

357
        // If all the outputs together don't reach target, we cannot continue
358
        let total_sum: u64 = outputs.iter().fold(0u64, |acc, o| acc.saturating_add(o.value as u64));
×
359
        if total_sum < range_limit.target_minimum_amount {
×
360
            debug!(
×
361
                target: LOG_TARGET,
×
362
                "Total unspent outputs' value in the specified range was less than the target_minimum_amount: {} < {}",
363
                total_sum, range_limit.target_minimum_amount
364
            );
365
            return Ok((Vec::new(), MicroMinotari::zero()));
×
366
        }
×
367

368
        Ok((outputs, MicroMinotari::from(total_sum)))
×
369
    }
×
370

371
    /// Retrieves UTXO counts grouped by the provided ranges
372
    pub fn count_outputs_in_ranges(
×
373
        selection_criteria: &UtxoSelectionCriteria,
×
374
        ranges: &[Range<u64>],
×
375
        tip_height: Option<u64>,
×
376
        conn: &mut SqliteConnection,
×
377
    ) -> Result<Vec<CoinBucket>, OutputManagerStorageError> {
×
378
        let mut result = Vec::with_capacity(ranges.len());
×
379
        let i64_tip_height = tip_height.and_then(|h| i64::try_from(h).ok()).unwrap_or(i64::MAX);
×
380

381
        for range in ranges {
×
382
            let amounts_from = i64::try_from(range.start).unwrap_or(i64::MAX);
×
383
            let amounts_to = i64::try_from(range.end).unwrap_or(i64::MAX);
×
384

385
            let mut query = outputs::table
×
386
                .into_boxed()
×
387
                .filter(outputs::status.eq(OutputStatus::Unspent as i32))
×
388
                .filter(outputs::value.ge(amounts_from))
×
389
                .filter(outputs::value.lt(amounts_to));
×
390

391
            if selection_criteria.mode == UtxoSelectionMode::Safe {
×
392
                query = query
×
393
                    .filter(outputs::script_lock_height.le(i64_tip_height))
×
394
                    .filter(outputs::maturity.le(i64_tip_height))
×
395
                    // make sure we account for i64 wrap around to neg
×
396
                    .filter(outputs::script_lock_height.ge(0))
×
397
                    .filter(outputs::maturity.ge(0));
×
398
            }
×
399

400
            // Rust
401
            let (count_res, sum_res) = query
×
402
                .select((count_star(), sql::<Nullable<BigInt>>("SUM(value)")))
×
403
                .first::<(i64, Option<i64>)>(conn)
×
404
                .optional()?
×
405
                .unwrap_or_default();
×
406

407
            result.push(CoinBucket {
×
408
                number_of_outputs: count_res as u64,
×
409
                total_value: sum_res.unwrap_or(0) as u64,
×
410
                range: range.clone(),
×
411
            });
×
412
        }
413

414
        Ok(result)
×
415
    }
×
416

417
    fn handle_must_include_filter(
×
418
        selection_criteria: &UtxoSelectionCriteria,
×
419
        commitments: &[CompressedCommitment],
×
420
        amount: u64,
×
421
        tip_height: Option<u64>,
×
422
        conn: &mut SqliteConnection,
×
423
    ) -> Result<Vec<OutputSql>, OutputManagerStorageError> {
×
424
        if commitments.is_empty() {
×
425
            // If no commitments specified, fall back to standard behavior
426
            let mut selection_criteria = selection_criteria.clone();
×
427
            selection_criteria.filter = UtxoSelectionFilter::Standard;
×
428
            return OutputSql::fetch_unspent_outputs_for_spending(&selection_criteria, amount, tip_height, conn);
×
429
        }
×
430

431
        let i64_tip_height = tip_height.and_then(|h| i64::try_from(h).ok()).unwrap_or(i64::MAX);
×
432
        let i64_value = i64::try_from(selection_criteria.min_dust).unwrap_or(i64::MAX);
×
433

434
        let mut query = outputs::table
×
435
            .into_boxed()
×
436
            .filter(outputs::status.eq(OutputStatus::Unspent as i32))
×
437
            .filter(outputs::value.gt(i64_value))
×
438
            .order_by(outputs::spending_priority.desc());
×
439

440
        // NOTE: Safe mode presets `script_lock_height` and `maturity` filters for all queries
441
        if selection_criteria.mode == UtxoSelectionMode::Safe {
×
442
            query = query
×
443
                .filter(outputs::script_lock_height.le(i64_tip_height))
×
444
                .filter(outputs::maturity.le(i64_tip_height))
×
445
                // make sure we account for i64 wrap around to neg
×
446
                .filter(outputs::script_lock_height.ge(0))
×
447
                .filter(outputs::maturity.ge(0));
×
448
        }
×
449

450
        query = query.filter(
×
451
            outputs::output_type
×
452
                .eq(i32::from(OutputType::Standard.as_byte()))
×
453
                .or(outputs::output_type.eq(i32::from(OutputType::Coinbase.as_byte()))),
×
454
        );
455

456
        if selection_criteria.excluding_onesided {
×
457
            query = query.filter(outputs::source.ne(OutputSource::OneSided as i32));
×
458
        }
×
459

460
        if selection_criteria.excluding_multisig {
×
461
            query = query.filter(outputs::source.ne(OutputSource::Multisig as i32));
×
462
        }
×
463

464
        // Exclude the must-include outputs from the main query
465
        for commitment in commitments {
×
466
            query = query.filter(outputs::commitment.ne(commitment.to_vec()));
×
467
        }
×
468

469
        for exclude in &selection_criteria.excluding {
×
470
            query = query.filter(outputs::commitment.ne(exclude.as_bytes()));
×
471
        }
×
472

473
        query = match selection_criteria.ordering {
×
474
            UtxoSelectionOrdering::SmallestFirst => query.then_order_by(outputs::value.asc()),
×
475
            UtxoSelectionOrdering::LargestFirst => query.then_order_by(outputs::value.desc()),
×
476
            UtxoSelectionOrdering::Default => {
477
                let max: Option<i64> = outputs::table
×
478
                    .filter(outputs::status.eq(OutputStatus::Unspent as i32))
×
479
                    .filter(outputs::script_lock_height.le(i64_tip_height))
×
480
                    .filter(outputs::maturity.le(i64_tip_height))
×
481
                    // make sure we account for i64 wrap around to neg
482
                    .filter(outputs::script_lock_height.ge(0))
×
483
                    .filter(outputs::maturity.ge(0))
×
484
                    .order(outputs::value.desc())
×
485
                    .select(outputs::value)
×
486
                    .first(conn)
×
487
                    .optional()?;
×
488

489
                match max {
×
490
                    Some(max) if amount > max as u64 => query.then_order_by(outputs::value.desc()),
×
491
                    _ => query.then_order_by(outputs::value.asc()),
×
492
                }
493
            },
494
        };
495

496
        // First, get the must-include outputs
497
        let mut must_include_query = outputs::table
×
498
            .into_boxed()
×
499
            .filter(outputs::value.gt(i64_value))
×
500
            .order_by(outputs::spending_priority.desc());
×
501

502
        // Apply safe mode filters if needed
503
        if selection_criteria.mode == UtxoSelectionMode::Safe {
×
504
            must_include_query = must_include_query
×
505
                .filter(outputs::script_lock_height.le(i64_tip_height))
×
506
                .filter(outputs::maturity.le(i64_tip_height))
×
507
                // make sure we account for i64 wrap around to neg
×
508
                .filter(outputs::script_lock_height.ge(0))
×
509
                .filter(outputs::maturity.ge(0));
×
510
        }
×
511

512
        // Filter for the specific commitments
513
        must_include_query = must_include_query
×
514
            .filter(outputs::commitment.eq_any::<Vec<Vec<u8>>>(commitments.iter().map(|c| c.to_vec()).collect()));
×
515

516
        // Apply excluding filters
517
        for exclude in &selection_criteria.excluding {
×
518
            must_include_query = must_include_query.filter(outputs::commitment.ne(exclude.as_bytes()));
×
519
        }
×
520

521
        let must_include_outputs: Vec<OutputSql> = must_include_query.load(conn)?;
×
522

523
        // Calculate total value of must-include outputs
524
        let must_include_total: i64 = must_include_outputs.iter().map(|o| o.value).sum();
×
525
        let i64_amount = i64::try_from(amount).unwrap_or(i64::MAX);
×
526

527
        // We cannot do an exact amount, we need more than required because if we do an exact amount, we won't have
528
        // enough left for fees.
529
        if must_include_total > i64_amount {
×
530
            return Ok(must_include_outputs);
×
531
        }
×
532

533
        // Otherwise, we need additional outputs
534
        let remaining_limit = i64::from(TRANSACTION_INPUTS_LIMIT) - must_include_outputs.len() as i64;
×
535
        let mut final_outputs = must_include_outputs;
×
536

537
        if remaining_limit > 0 {
×
538
            let additional_outputs: Vec<OutputSql> = query.limit(remaining_limit).load(conn)?;
×
539
            final_outputs.extend(additional_outputs);
×
540
        }
×
541

542
        Ok(final_outputs)
×
543
    }
×
544

545
    /// Return all unspent outputs that have a maturity above the provided chain tip
546
    #[allow(clippy::cast_possible_wrap)]
547
    pub fn index_time_locked(
×
548
        tip: u64,
×
549
        conn: &mut SqliteConnection,
×
550
    ) -> Result<Vec<OutputSql>, OutputManagerStorageError> {
×
551
        Ok(outputs::table
×
552
            .filter(outputs::status.eq(OutputStatus::Unspent as i32))
×
553
            .filter(
×
554
                outputs::script_lock_height.gt(tip as i64)
×
555
            .or(outputs::maturity.gt(tip as i64))
×
556
            // make sure we account for i64 wrap around to neg
557
            .or(outputs::script_lock_height.lt(0))
×
558
            .or(outputs::maturity.lt(0)),
×
559
            )
560
            .load(conn)?)
×
561
    }
×
562

563
    pub fn index_unconfirmed(conn: &mut SqliteConnection) -> Result<Vec<OutputSql>, OutputManagerStorageError> {
×
564
        Ok(outputs::table
×
565
            .filter(
×
566
                outputs::status
×
567
                    .eq(OutputStatus::UnspentMinedUnconfirmed as i32)
×
568
                    .or(outputs::mined_in_block.is_null())
×
569
                    .or(outputs::mined_height.is_null()),
×
570
            )
571
            .order(outputs::id.asc())
×
572
            .load(conn)?)
×
573
    }
×
574

575
    pub fn index_by_output_type(
×
576
        output_type: OutputType,
×
577
        conn: &mut SqliteConnection,
×
578
    ) -> Result<Vec<OutputSql>, OutputManagerStorageError> {
×
579
        let res = diesel::sql_query("SELECT * FROM outputs where output_type & $1 = $1 ORDER BY id;")
×
580
            .bind::<diesel::sql_types::Integer, _>(i32::from(output_type.as_byte()))
×
581
            .load(conn)?;
×
582
        Ok(res)
×
583
    }
×
584

585
    pub fn index_unspent(conn: &mut SqliteConnection) -> Result<Vec<OutputSql>, OutputManagerStorageError> {
×
586
        Ok(outputs::table
×
587
            .filter(outputs::status.eq(OutputStatus::Unspent as i32))
×
588
            .order(outputs::id.asc())
×
589
            .load(conn)?)
×
590
    }
×
591

592
    pub fn index_marked_deleted_in_block_is_null(
×
593
        conn: &mut SqliteConnection,
×
594
    ) -> Result<Vec<OutputSql>, OutputManagerStorageError> {
×
595
        Ok(outputs::table
×
596
            // Return outputs not marked as deleted or confirmed
597
            .filter(outputs::marked_deleted_in_block.is_null().or(outputs::status.eq(OutputStatus::SpentMinedUnconfirmed as i32)))
×
598
            // Only return mined
599
            .filter(outputs::mined_in_block.is_not_null().and(outputs::mined_height.is_not_null()))
×
600
            .order(outputs::id.asc())
×
601
            .load(conn)?)
×
602
    }
×
603

604
    pub fn index_invalid(
×
605
        timestamp: &NaiveDateTime,
×
606
        conn: &mut SqliteConnection,
×
607
    ) -> Result<Vec<OutputSql>, OutputManagerStorageError> {
×
608
        Ok(outputs::table
×
609
            .filter(
×
610
                outputs::status
×
611
                    .eq(OutputStatus::Invalid as i32)
×
612
                    .or(outputs::status.eq(OutputStatus::CancelledInbound as i32)),
×
613
            )
614
            .filter(
×
615
                outputs::last_validation_timestamp
×
616
                    .le(timestamp)
×
617
                    .or(outputs::last_validation_timestamp.is_null()),
×
618
            )
619
            .order(outputs::id.asc())
×
620
            .load(conn)?)
×
621
    }
×
622

623
    pub fn index_by_output_hashes(
×
624
        conn: &mut SqliteConnection,
×
625
        hashes: &[HashOutput],
×
626
    ) -> Result<Vec<OutputSql>, OutputManagerStorageError> {
×
627
        let outputs = outputs::table
×
628
            .filter(outputs::hash.eq_any(hashes.iter().map(|h| h.as_slice())))
×
629
            .load(conn)?;
×
630

631
        Ok(outputs)
×
632
    }
×
633

634
    pub fn first_by_mined_height_desc(
×
635
        conn: &mut SqliteConnection,
×
636
    ) -> Result<Option<OutputSql>, OutputManagerStorageError> {
×
637
        Ok(outputs::table
×
638
            .filter(outputs::mined_height.is_not_null())
×
639
            .order(outputs::mined_height.desc())
×
640
            .first(conn)
×
641
            .optional()?)
×
642
    }
×
643

644
    pub fn first_by_marked_deleted_height_desc(
×
645
        conn: &mut SqliteConnection,
×
646
    ) -> Result<Option<OutputSql>, OutputManagerStorageError> {
×
647
        Ok(outputs::table
×
648
            .filter(outputs::marked_deleted_at_height.is_not_null())
×
649
            .order(outputs::marked_deleted_at_height.desc())
×
650
            .first(conn)
×
651
            .optional()?)
×
652
    }
×
653

654
    /// Find a particular Output, if it exists
655
    pub fn find(spending_key: &str, conn: &mut SqliteConnection) -> Result<OutputSql, OutputManagerStorageError> {
6✔
656
        Ok(outputs::table
6✔
657
            .filter(outputs::spending_key.eq(spending_key.to_string()))
6✔
658
            .first::<OutputSql>(conn)?)
6✔
659
    }
6✔
660

661
    pub fn find_by_tx_id(
×
662
        tx_id: TxId,
×
663
        conn: &mut SqliteConnection,
×
664
    ) -> Result<Vec<OutputSql>, OutputManagerStorageError> {
×
665
        Ok(outputs::table
×
666
            .filter(
×
667
                outputs::received_in_tx_id
×
668
                    .eq(tx_id.as_i64_wrapped())
×
669
                    .or(outputs::spent_in_tx_id.eq(tx_id.as_i64_wrapped())),
×
670
            )
671
            .load(conn)?)
×
672
    }
×
673

674
    /// Verify that outputs with specified commitments exist in the database
675
    pub fn verify_outputs_exist(
×
676
        commitments: &[CompressedCommitment],
×
677
        conn: &mut SqliteConnection,
×
678
    ) -> Result<bool, OutputManagerStorageError> {
×
679
        #[derive(QueryableByName, Clone)]
×
680
        struct CountQueryResult {
681
            #[diesel(sql_type = diesel::sql_types::BigInt)]
682
            count: i64,
683
        }
684
        let placeholders = commitments
×
685
            .iter()
×
686
            .map(|v| format!("x'{}'", v.to_hex()))
×
687
            .collect::<Vec<_>>()
×
688
            .join(", ");
×
689
        let query = sql_query(format!(
×
690
            "SELECT COUNT(*) as count FROM outputs WHERE commitment IN ({placeholders})"
691
        ));
692
        let query_result = query.load::<CountQueryResult>(conn)?;
×
693
        let commitments_len = i64::try_from(commitments.len())
×
694
            .map_err(|e| OutputManagerStorageError::ConversionError { reason: e.to_string() })?;
×
695
        Ok(query_result.first().expect("Already checked").count == commitments_len)
×
696
    }
×
697

698
    /// Return the available, time locked, pending incoming and pending outgoing balance
699
    #[allow(clippy::cast_possible_wrap)]
700
    pub fn get_balance(
×
701
        current_tip_for_time_lock_calculation: Option<u64>,
×
702
        conn: &mut SqliteConnection,
×
703
    ) -> Result<Balance, OutputManagerStorageError> {
×
704
        #[derive(QueryableByName, Clone)]
×
705
        struct BalanceQueryResult {
706
            #[diesel(sql_type = diesel::sql_types::BigInt)]
707
            amount: i64,
708
            #[diesel(sql_type = diesel::sql_types::Text)]
709
            category: String,
710
        }
711
        let balance_query_result = if let Some(current_tip) = current_tip_for_time_lock_calculation {
×
712
            sql_query(
×
713
                "SELECT coalesce(sum(value), 0) as amount, 'available_balance' as category \
714
                 FROM outputs WHERE status = ? AND maturity <= ? AND maturity >= 0 AND script_lock_height <= ? AND script_lock_height >= 0 AND output_type != ? \
715
                 UNION ALL \
716
                 SELECT coalesce(sum(value), 0) as amount, 'time_locked_balance' as category \
717
                 FROM outputs WHERE status = ? AND (maturity > ? OR maturity < 0 OR script_lock_height > ? OR script_lock_height < 0) AND output_type != ? \
718
                 UNION ALL \
719
                 SELECT coalesce(sum(value), 0) as amount, 'pending_incoming_balance' as category \
720
                 FROM outputs WHERE (source != ? AND status = ? OR status = ? OR status = ?) AND output_type != ? \
721
                 UNION ALL \
722
                 SELECT coalesce(sum(value), 0) as amount, 'pending_outgoing_balance' as category \
723
                 FROM outputs WHERE status = ? OR status = ? OR status = ?",
724
            )
725
                // available_balance
726
                .bind::<diesel::sql_types::Integer, _>(OutputStatus::Unspent as i32)
×
727
                .bind::<diesel::sql_types::BigInt, _>(current_tip as i64)
×
728
                .bind::<diesel::sql_types::BigInt, _>(current_tip as i64)
×
729
                .bind::<diesel::sql_types::Integer, _>(OutputType::Burn as i32)
×
730
                // time_locked_balance
731
                .bind::<diesel::sql_types::Integer, _>(OutputStatus::Unspent as i32)
×
732
                .bind::<diesel::sql_types::BigInt, _>(current_tip as i64)
×
733
                .bind::<diesel::sql_types::BigInt, _>(current_tip as i64)
×
734
                .bind::<diesel::sql_types::Integer, _>(OutputType::Burn as i32)
×
735
                // pending_incoming_balance
736
                .bind::<diesel::sql_types::Integer, _>(OutputSource::Coinbase as i32)
×
737
                .bind::<diesel::sql_types::Integer, _>(OutputStatus::EncumberedToBeReceived as i32)
×
738
                .bind::<diesel::sql_types::Integer, _>(OutputStatus::ShortTermEncumberedToBeReceived as i32)
×
739
                .bind::<diesel::sql_types::Integer, _>(OutputStatus::UnspentMinedUnconfirmed as i32)
×
740
                .bind::<diesel::sql_types::Integer, _>(OutputType::Burn as i32)
×
741
                // pending_outgoing_balance
742
                .bind::<diesel::sql_types::Integer, _>(OutputStatus::EncumberedToBeSpent as i32)
×
743
                .bind::<diesel::sql_types::Integer, _>(OutputStatus::ShortTermEncumberedToBeSpent as i32)
×
744
                .bind::<diesel::sql_types::Integer, _>(OutputStatus::SpentMinedUnconfirmed as i32)
×
745
                .load::<BalanceQueryResult>(conn)?
×
746
        } else {
747
            sql_query(
×
748
                "SELECT coalesce(sum(value), 0) as amount, 'available_balance' as category \
749
                 FROM outputs WHERE status = ? AND output_type != ?\
750
                 UNION ALL \
751
                 SELECT coalesce(sum(value), 0) as amount, 'pending_incoming_balance' as category \
752
                 FROM outputs WHERE (source != ? AND status = ? OR status = ? OR status = ?) AND output_type != ? \
753
                 UNION ALL \
754
                 SELECT coalesce(sum(value), 0) as amount, 'pending_outgoing_balance' as category \
755
                 FROM outputs WHERE status = ? OR status = ? OR status = ?",
756
            )
757
                // available_balance
758
                .bind::<diesel::sql_types::Integer, _>(OutputStatus::Unspent as i32)
×
759
                .bind::<diesel::sql_types::Integer, _>(OutputType::Burn as i32)
×
760
                // pending_incoming_balance
761
                .bind::<diesel::sql_types::Integer, _>(OutputSource::Coinbase as i32)
×
762
                .bind::<diesel::sql_types::Integer, _>(OutputStatus::EncumberedToBeReceived as i32)
×
763
                .bind::<diesel::sql_types::Integer, _>(OutputStatus::ShortTermEncumberedToBeReceived as i32)
×
764
                .bind::<diesel::sql_types::Integer, _>(OutputStatus::UnspentMinedUnconfirmed as i32)
×
765
                .bind::<diesel::sql_types::Integer, _>(OutputType::Burn as i32)
×
766
                // pending_outgoing_balance
767
                .bind::<diesel::sql_types::Integer, _>(OutputStatus::EncumberedToBeSpent as i32)
×
768
                .bind::<diesel::sql_types::Integer, _>(OutputStatus::ShortTermEncumberedToBeSpent as i32)
×
769
                .bind::<diesel::sql_types::Integer, _>(OutputStatus::SpentMinedUnconfirmed as i32)
×
770
                .load::<BalanceQueryResult>(conn)?
×
771
        };
772
        let mut available_balance = None;
×
773
        let mut time_locked_balance = Some(None);
×
774
        let mut pending_incoming_balance = None;
×
775
        let mut pending_outgoing_balance = None;
×
776
        for balance in balance_query_result {
×
777
            match balance.category.as_str() {
×
778
                "available_balance" => available_balance = Some(MicroMinotari::from(balance.amount as u64)),
×
779
                "time_locked_balance" => time_locked_balance = Some(Some(MicroMinotari::from(balance.amount as u64))),
×
780
                "pending_incoming_balance" => {
×
781
                    pending_incoming_balance = Some(MicroMinotari::from(balance.amount as u64))
×
782
                },
783
                "pending_outgoing_balance" => {
×
784
                    pending_outgoing_balance = Some(MicroMinotari::from(balance.amount as u64))
×
785
                },
786
                _ => {
787
                    return Err(OutputManagerStorageError::UnexpectedResult(
×
788
                        "Unexpected category in balance query".to_string(),
×
789
                    ));
×
790
                },
791
            }
792
        }
793

794
        Ok(Balance {
795
            available_balance: available_balance.ok_or_else(|| {
×
796
                OutputManagerStorageError::UnexpectedResult("Available balance could not be calculated".to_string())
×
797
            })?,
×
798
            time_locked_balance: time_locked_balance.ok_or_else(|| {
×
799
                OutputManagerStorageError::UnexpectedResult("Time locked balance could not be calculated".to_string())
×
800
            })?,
×
801
            pending_incoming_balance: pending_incoming_balance.ok_or_else(|| {
×
802
                OutputManagerStorageError::UnexpectedResult(
×
803
                    "Pending incoming balance could not be calculated".to_string(),
×
804
                )
×
805
            })?,
×
806
            pending_outgoing_balance: pending_outgoing_balance.ok_or_else(|| {
×
807
                OutputManagerStorageError::UnexpectedResult(
×
808
                    "Pending outgoing balance could not be calculated".to_string(),
×
809
                )
×
810
            })?,
×
811
        })
812
    }
×
813

814
    /// Return the available, time locked, pending incoming and pending outgoing balance
815
    #[allow(clippy::cast_possible_wrap)]
816
    #[allow(clippy::too_many_lines)]
817
    pub fn get_balance_payment_id(
×
818
        current_tip_for_time_lock_calculation: Option<u64>,
×
819
        payment_id: Vec<u8>,
×
820
        conn: &mut SqliteConnection,
×
821
    ) -> Result<Balance, OutputManagerStorageError> {
×
822
        #[derive(QueryableByName, Clone)]
×
823
        struct BalanceQueryResult {
824
            #[diesel(sql_type = diesel::sql_types::BigInt)]
825
            amount: i64,
826
            #[diesel(sql_type = diesel::sql_types::Text)]
827
            category: String,
828
        }
829
        let balance_query_result = if let Some(current_tip) = current_tip_for_time_lock_calculation {
×
830
            let balance_query = sql_query(
×
831
                "SELECT coalesce(sum(value), 0) as amount, 'available_balance' as category \
832
                 FROM outputs WHERE status = ? AND maturity <= ? AND maturity >= 0 AND script_lock_height <= ? AND script_lock_height >= 0 AND user_payment_id = ? \
833
                 UNION ALL \
834
                 SELECT coalesce(sum(value), 0) as amount, 'time_locked_balance' as category \
835
                 FROM outputs WHERE status = ? AND ((maturity > ? OR maturity < 0 OR script_lock_height > ? OR script_lock_height < 0) AND user_payment_id = ?) \
836
                 UNION ALL \
837
                 SELECT coalesce(sum(value), 0) as amount, 'pending_incoming_balance' as category \
838
                 FROM outputs WHERE source != ? AND (status = ? OR status = ? OR status = ?) AND user_payment_id = ? \
839
                 UNION ALL \
840
                 SELECT coalesce(sum(value), 0) as amount, 'pending_outgoing_balance' as category \
841
                 FROM outputs WHERE (status = ? OR status = ? OR status = ?) AND user_payment_id = ?",
842
            )
843
                // available_balance
844
                .bind::<diesel::sql_types::Integer, _>(OutputStatus::Unspent as i32)
×
845
                .bind::<diesel::sql_types::BigInt, _>(current_tip as i64)
×
846
                .bind::<diesel::sql_types::BigInt, _>(current_tip as i64)
×
847
                .bind::<diesel::sql_types::Binary, _>(payment_id.clone())
×
848
                // time_locked_balance
849
                .bind::<diesel::sql_types::Integer, _>(OutputStatus::Unspent as i32)
×
850
                .bind::<diesel::sql_types::BigInt, _>(current_tip as i64)
×
851
                .bind::<diesel::sql_types::BigInt, _>(current_tip as i64)
×
852
                .bind::<diesel::sql_types::Binary, _>(payment_id.clone())
×
853
                // pending_incoming_balance
854
                .bind::<diesel::sql_types::Integer, _>(OutputSource::Coinbase as i32)
×
855
                .bind::<diesel::sql_types::Integer, _>(OutputStatus::EncumberedToBeReceived as i32)
×
856
                .bind::<diesel::sql_types::Integer, _>(OutputStatus::ShortTermEncumberedToBeReceived as i32)
×
857
                .bind::<diesel::sql_types::Integer, _>(OutputStatus::UnspentMinedUnconfirmed as i32)
×
858
                .bind::<diesel::sql_types::Binary, _>(payment_id.clone())
×
859
                // pending_outgoing_balance
860
                .bind::<diesel::sql_types::Integer, _>(OutputStatus::EncumberedToBeSpent as i32)
×
861
                .bind::<diesel::sql_types::Integer, _>(OutputStatus::ShortTermEncumberedToBeSpent as i32)
×
862
                .bind::<diesel::sql_types::Integer, _>(OutputStatus::SpentMinedUnconfirmed as i32)
×
863
                .bind::<diesel::sql_types::Binary, _>(payment_id);
×
864
            balance_query.load::<BalanceQueryResult>(conn)?
×
865
        } else {
866
            let balance_query = sql_query(
×
867
             "SELECT coalesce(sum(value), 0) as amount, 'available_balance' as category \
868
             FROM outputs WHERE status = ? AND user_payment_id = ?\
869
             UNION ALL \
870
             SELECT coalesce(sum(value), 0) as amount, 'pending_incoming_balance' as category \
871
             FROM outputs WHERE source != ? AND (status = ? OR status = ? OR status = ?) AND user_payment_id = ? \
872
             UNION ALL \
873
             SELECT coalesce(sum(value), 0) as amount, 'pending_outgoing_balance' as category \
874
             FROM outputs WHERE (status = ? OR status = ? OR status = ?) AND user_payment_id = ?",
875
            )
876
                // available_balance
877
                .bind::<diesel::sql_types::Integer, _>(OutputStatus::Unspent as i32)
×
878
                .bind::<diesel::sql_types::Binary, _>(payment_id.clone())
×
879
                // pending_incoming_balance
880
                .bind::<diesel::sql_types::Integer, _>(OutputSource::Coinbase as i32)
×
881
                .bind::<diesel::sql_types::Integer, _>(OutputStatus::EncumberedToBeReceived as i32)
×
882
                .bind::<diesel::sql_types::Integer, _>(OutputStatus::ShortTermEncumberedToBeReceived as i32)
×
883
                .bind::<diesel::sql_types::Integer, _>(OutputStatus::UnspentMinedUnconfirmed as i32)
×
884
                .bind::<diesel::sql_types::Binary, _>(payment_id.clone())
×
885
                // pending_outgoing_balance
886
                .bind::<diesel::sql_types::Integer, _>(OutputStatus::EncumberedToBeSpent as i32)
×
887
                .bind::<diesel::sql_types::Integer, _>(OutputStatus::ShortTermEncumberedToBeSpent as i32)
×
888
                .bind::<diesel::sql_types::Integer, _>(OutputStatus::SpentMinedUnconfirmed as i32)
×
889
                .bind::<diesel::sql_types::Binary, _>(payment_id);
×
890
            balance_query.load::<BalanceQueryResult>(conn)?
×
891
        };
892
        let mut available_balance = None;
×
893
        let mut time_locked_balance = Some(None);
×
894
        let mut pending_incoming_balance = None;
×
895
        let mut pending_outgoing_balance = None;
×
896
        for balance in balance_query_result {
×
897
            match balance.category.as_str() {
×
898
                "available_balance" => available_balance = Some(MicroMinotari::from(balance.amount as u64)),
×
899
                "time_locked_balance" => time_locked_balance = Some(Some(MicroMinotari::from(balance.amount as u64))),
×
900
                "pending_incoming_balance" => {
×
901
                    pending_incoming_balance = Some(MicroMinotari::from(balance.amount as u64))
×
902
                },
903
                "pending_outgoing_balance" => {
×
904
                    pending_outgoing_balance = Some(MicroMinotari::from(balance.amount as u64))
×
905
                },
906
                _ => {
907
                    return Err(OutputManagerStorageError::UnexpectedResult(
×
908
                        "Unexpected category in balance query".to_string(),
×
909
                    ));
×
910
                },
911
            }
912
        }
913

914
        Ok(Balance {
915
            available_balance: available_balance.ok_or_else(|| {
×
916
                OutputManagerStorageError::UnexpectedResult("Available balance could not be calculated".to_string())
×
917
            })?,
×
918
            time_locked_balance: time_locked_balance.ok_or_else(|| {
×
919
                OutputManagerStorageError::UnexpectedResult("Time locked balance could not be calculated".to_string())
×
920
            })?,
×
921
            pending_incoming_balance: pending_incoming_balance.ok_or_else(|| {
×
922
                OutputManagerStorageError::UnexpectedResult(
×
923
                    "Pending incoming balance could not be calculated".to_string(),
×
924
                )
×
925
            })?,
×
926
            pending_outgoing_balance: pending_outgoing_balance.ok_or_else(|| {
×
927
                OutputManagerStorageError::UnexpectedResult(
×
928
                    "Pending outgoing balance could not be calculated".to_string(),
×
929
                )
×
930
            })?,
×
931
        })
932
    }
×
933

934
    pub fn find_by_commitment(
×
935
        commitment: &[u8],
×
936
        conn: &mut SqliteConnection,
×
937
    ) -> Result<OutputSql, OutputManagerStorageError> {
×
938
        Ok(outputs::table
×
939
            .filter(outputs::commitment.eq(commitment))
×
940
            .first::<OutputSql>(conn)?)
×
941
    }
×
942

943
    pub fn find_by_commitments_excluding_statuses(
×
944
        commitments: Vec<&[u8]>,
×
945
        statuses: &[OutputStatus],
×
946
        conn: &mut SqliteConnection,
×
947
    ) -> Result<Vec<OutputSql>, OutputManagerStorageError> {
×
948
        let status_values: Vec<i32> = statuses.iter().map(|s| *s as i32).collect();
×
949
        Ok(outputs::table
×
950
            .filter(outputs::commitment.eq_any(commitments))
×
951
            .filter(not(outputs::status.eq_any(status_values)))
×
952
            .load(conn)?)
×
953
    }
×
954

955
    pub fn update_by_commitments(
×
956
        commitments: Vec<&[u8]>,
×
957
        updated_output: UpdateOutput,
×
958
        conn: &mut SqliteConnection,
×
959
    ) -> Result<usize, OutputManagerStorageError> {
×
960
        Ok(
961
            diesel::update(outputs::table.filter(outputs::commitment.eq_any(commitments)))
×
962
                .set(UpdateOutputSql::from(updated_output))
×
963
                .execute(conn)?,
×
964
        )
965
    }
×
966

967
    pub fn find_by_commitment_and_cancelled(
×
968
        commitment: &[u8],
×
969
        cancelled: bool,
×
970
        conn: &mut SqliteConnection,
×
971
    ) -> Result<OutputSql, OutputManagerStorageError> {
×
972
        let cancelled_flag = OutputStatus::CancelledInbound as i32;
×
973

974
        let mut request = outputs::table.filter(outputs::commitment.eq(commitment)).into_boxed();
×
975
        if cancelled {
×
976
            request = request.filter(outputs::status.eq(cancelled_flag))
×
977
        } else {
978
            request = request.filter(outputs::status.ne(cancelled_flag))
×
979
        };
980

981
        Ok(request.first::<OutputSql>(conn)?)
×
982
    }
×
983

984
    pub fn find_by_tx_id_and_status(
×
985
        tx_id: TxId,
×
986
        status: OutputStatus,
×
987
        conn: &mut SqliteConnection,
×
988
    ) -> Result<Vec<OutputSql>, OutputManagerStorageError> {
×
989
        Ok(outputs::table
×
990
            .filter(
×
991
                outputs::received_in_tx_id
×
992
                    .eq(Some(tx_id.as_u64() as i64))
×
993
                    .or(outputs::spent_in_tx_id.eq(Some(tx_id.as_u64() as i64))),
×
994
            )
995
            .filter(outputs::status.eq(status as i32))
×
996
            .load(conn)?)
×
997
    }
×
998

999
    /// Find outputs via tx_id that are encumbered. Any outputs that are encumbered cannot be marked as spent.
1000
    pub fn find_by_tx_id_and_encumbered(
1✔
1001
        tx_id: TxId,
1✔
1002
        conn: &mut SqliteConnection,
1✔
1003
    ) -> Result<Vec<OutputSql>, OutputManagerStorageError> {
1✔
1004
        Ok(outputs::table
1✔
1005
            .filter(
1✔
1006
                outputs::received_in_tx_id
1✔
1007
                    .eq(Some(tx_id.as_u64() as i64))
1✔
1008
                    .or(outputs::spent_in_tx_id.eq(Some(tx_id.as_u64() as i64))),
1✔
1009
            )
1010
            .filter(
1✔
1011
                outputs::status
1✔
1012
                    .eq(OutputStatus::EncumberedToBeReceived as i32)
1✔
1013
                    .or(outputs::status.eq(OutputStatus::EncumberedToBeSpent as i32))
1✔
1014
                    .or(outputs::status.eq(OutputStatus::ShortTermEncumberedToBeReceived as i32))
1✔
1015
                    .or(outputs::status.eq(OutputStatus::ShortTermEncumberedToBeSpent as i32)),
1✔
1016
            )
1017
            .load(conn)?)
1✔
1018
    }
1✔
1019

1020
    /// Find a particular Output, if it exists and is in the specified Spent state
1021
    pub fn find_status(
2✔
1022
        spending_key: &str,
2✔
1023
        status: OutputStatus,
2✔
1024
        conn: &mut SqliteConnection,
2✔
1025
    ) -> Result<OutputSql, OutputManagerStorageError> {
2✔
1026
        Ok(outputs::table
2✔
1027
            .filter(outputs::status.eq(status as i32))
2✔
1028
            .filter(outputs::spending_key.eq(spending_key.to_string()))
2✔
1029
            .first::<OutputSql>(conn)?)
2✔
1030
    }
2✔
1031

1032
    /// Find a particular Output, if it exists and is in the specified Spent state
1033
    pub fn find_by_hash(
×
1034
        hash: &[u8],
×
1035
        status: OutputStatus,
×
1036
        conn: &mut SqliteConnection,
×
1037
    ) -> Result<OutputSql, OutputManagerStorageError> {
×
1038
        Ok(outputs::table
×
1039
            .filter(outputs::status.eq(status as i32))
×
1040
            .filter(outputs::hash.eq(hash))
×
1041
            .first::<OutputSql>(conn)?)
×
1042
    }
×
1043

1044
    pub fn delete(&self, conn: &mut SqliteConnection) -> Result<(), OutputManagerStorageError> {
1✔
1045
        let num_deleted =
1✔
1046
            diesel::delete(outputs::table.filter(outputs::spending_key.eq(&self.spending_key))).execute(conn)?;
1✔
1047

1048
        if num_deleted == 0 {
1✔
1049
            return Err(OutputManagerStorageError::ValuesNotFound);
×
1050
        }
1✔
1051

1052
        Ok(())
1✔
1053
    }
1✔
1054

1055
    pub fn update(
2✔
1056
        &self,
2✔
1057
        updated_output: UpdateOutput,
2✔
1058
        conn: &mut SqliteConnection,
2✔
1059
    ) -> Result<OutputSql, OutputManagerStorageError> {
2✔
1060
        diesel::update(outputs::table.filter(outputs::id.eq(&self.id)))
2✔
1061
            .set(UpdateOutputSql::from(updated_output))
2✔
1062
            .execute(conn)
2✔
1063
            .num_rows_affected_or_not_found(1)?;
2✔
1064

1065
        OutputSql::find(&self.spending_key, conn)
2✔
1066
    }
2✔
1067

1068
    #[allow(clippy::too_many_lines)]
1069
    pub fn to_db_wallet_output<KM: LegacyTransactionKeyManagerInterface>(
×
1070
        self,
×
1071
        key_manager: &KM,
×
1072
    ) -> Result<DbWalletOutput, OutputManagerStorageError> {
×
1073
        let features: OutputFeatures =
×
1074
            serde_json::from_str(&self.features_json).map_err(|s| OutputManagerStorageError::ConversionError {
×
1075
                reason: format!("Could not convert json into OutputFeatures:{s}"),
×
1076
            })?;
×
1077

1078
        let covenant = BorshDeserialize::deserialize(&mut self.covenant.as_bytes()).map_err(|e| {
×
1079
            error!(
×
1080
                target: LOG_TARGET,
×
1081
                "Could not create Covenant from stored bytes ({e}), They might be encrypted"
1082
            );
1083
            OutputManagerStorageError::ConversionError {
×
1084
                reason: "Covenant could not be converted from bytes".to_string(),
×
1085
            }
×
1086
        })?;
×
1087

1088
        let encrypted_data = EncryptedData::from_bytes(&self.encrypted_data)?;
×
1089
        let payment_id = match self.payment_id {
×
1090
            Some(bytes) => MemoField::from_bytes(&bytes),
×
1091
            None => MemoField::new_empty(),
×
1092
        };
1093
        let commitment = CompressedCommitment::from_vec(&self.commitment)?;
×
1094
        let hash = match <Vec<u8> as TryInto<FixedHash>>::try_into(self.hash) {
×
1095
            Ok(v) => v,
×
1096
            Err(e) => {
×
1097
                error!(target: LOG_TARGET, "Malformed output hash: {e}");
×
1098
                return Err(OutputManagerStorageError::ConversionError {
×
1099
                    reason: "Malformed output hash".to_string(),
×
1100
                });
×
1101
            },
1102
        };
1103
        let commitment_mask_key_id = match TariKeyId::from_str(&self.spending_key) {
×
1104
            Ok(kid) => kid,
×
1105
            Err(_) => {
1106
                let legacy = LegacyTariKeyId::from_str(&self.spending_key).map_err(|e| {
×
1107
                    error!(
×
1108
                        target: LOG_TARGET,
×
1109
                        "Could not create spending key id({}) from stored string ({e})",self.spending_key
1110
                    );
1111
                    OutputManagerStorageError::ConversionError {
×
1112
                        reason: format!(
×
1113
                            "Spending key id({}) could not be converted from string ({e})",
×
1114
                            self.spending_key
×
1115
                        ),
×
1116
                    }
×
1117
                })?;
×
1118
                key_manager.convert_legacy_tari_key_id_to_current(&legacy)?
×
1119
            },
1120
        };
1121

1122
        let script_key_id = match TariKeyId::from_str(&self.script_private_key) {
×
1123
            Ok(kid) => kid,
×
1124
            Err(_) => {
1125
                let legacy = LegacyTariKeyId::from_str(&self.script_private_key).map_err(|e| {
×
1126
                    error!(
×
1127
                        target: LOG_TARGET,
×
1128
                        "Could not create script private key id({}) from stored string ({e})",self.script_private_key
1129
                    );
1130
                    OutputManagerStorageError::ConversionError {
×
1131
                        reason: format!(
×
1132
                            "Could not create script private key id({}) from stored string ({e})",
×
1133
                            self.script_private_key
×
1134
                        ),
×
1135
                    }
×
1136
                })?;
×
1137
                key_manager.convert_legacy_tari_key_id_to_current(&legacy)?
×
1138
            },
1139
        };
1140
        let wallet_output = WalletOutput::new_from_parts(
×
1141
            TransactionOutputVersion::get_current_version(),
×
1142
            MicroMinotari::from(self.value as u64),
×
1143
            commitment_mask_key_id,
×
1144
            features,
×
1145
            TariScript::from_bytes(self.script.as_slice())?,
×
1146
            ExecutionStack::from_bytes(self.input_data.as_slice())?,
×
1147
            script_key_id,
×
1148
            CompressedPublicKey::from_vec(&self.sender_offset_public_key).map_err(|_| {
×
1149
                error!(
×
1150
                    target: LOG_TARGET,
×
1151
                    "Could not create PublicKey from stored bytes, They might be encrypted"
1152
                );
1153
                OutputManagerStorageError::ConversionError {
×
1154
                    reason: "PrivateKey could not be converted from bytes".to_string(),
×
1155
                }
×
1156
            })?,
×
1157
            ComAndPubSignature::new(
×
1158
                CompressedCommitment::from_vec(&self.metadata_signature_ephemeral_commitment).map_err(|_| {
×
1159
                    error!(
×
1160
                        target: LOG_TARGET,
×
1161
                        "Could not create Commitment from stored bytes, They might be encrypted"
1162
                    );
1163
                    OutputManagerStorageError::ConversionError {
×
1164
                        reason: "Commitment could not be converted from bytes".to_string(),
×
1165
                    }
×
1166
                })?,
×
1167
                CompressedPublicKey::from_vec(&self.metadata_signature_ephemeral_pubkey).map_err(|_| {
×
1168
                    error!(
×
1169
                        target: LOG_TARGET,
×
1170
                        "Could not create PublicKey from stored bytes, They might be encrypted"
1171
                    );
1172
                    OutputManagerStorageError::ConversionError {
×
1173
                        reason: "PublicKey could not be converted from bytes".to_string(),
×
1174
                    }
×
1175
                })?,
×
1176
                PrivateKey::from_vec(&self.metadata_signature_u_a).map_err(|_| {
×
1177
                    error!(
×
1178
                        target: LOG_TARGET,
×
1179
                        "Could not create PrivateKey from stored bytes, They might be encrypted"
1180
                    );
1181
                    OutputManagerStorageError::ConversionError {
×
1182
                        reason: "PrivateKey could not be converted from bytes".to_string(),
×
1183
                    }
×
1184
                })?,
×
1185
                PrivateKey::from_vec(&self.metadata_signature_u_x).map_err(|_| {
×
1186
                    error!(
×
1187
                        target: LOG_TARGET,
×
1188
                        "Could not create PrivateKey from stored bytes, They might be encrypted"
1189
                    );
1190
                    OutputManagerStorageError::ConversionError {
×
1191
                        reason: "PrivateKey could not be converted from bytes".to_string(),
×
1192
                    }
×
1193
                })?,
×
1194
                PrivateKey::from_vec(&self.metadata_signature_u_y).map_err(|_| {
×
1195
                    error!(
×
1196
                        target: LOG_TARGET,
×
1197
                        "Could not create PrivateKey from stored bytes, They might be encrypted"
1198
                    );
1199
                    OutputManagerStorageError::ConversionError {
×
1200
                        reason: "PrivateKey could not be converted from bytes".to_string(),
×
1201
                    }
×
1202
                })?,
×
1203
            ),
1204
            self.script_lock_height as u64,
×
1205
            covenant,
×
1206
            encrypted_data,
×
1207
            MicroMinotari::from(self.minimum_value_promise as u64),
×
1208
            match self.rangeproof {
×
1209
                Some(bytes) => Some(RangeProof::from_canonical_bytes(&bytes)?),
×
1210
                None => None,
×
1211
            },
1212
            payment_id.clone(),
×
1213
            hash,
×
1214
            commitment.clone(),
×
1215
        );
1216

1217
        let spending_priority = SpendingPriority::try_from(self.spending_priority as u32).map_err(|e| {
×
1218
            OutputManagerStorageError::ConversionError {
×
1219
                reason: format!("Could not convert spending priority from i32: {e}"),
×
1220
            }
×
1221
        })?;
×
1222
        let mined_in_block = match self.mined_in_block {
×
1223
            Some(v) => v.try_into().ok(),
×
1224
            None => None,
×
1225
        };
1226
        let marked_deleted_in_block = match self.marked_deleted_in_block {
×
1227
            Some(v) => v.try_into().ok(),
×
1228
            None => None,
×
1229
        };
1230
        Ok(DbWalletOutput {
1231
            commitment,
×
1232
            wallet_output,
×
1233
            hash,
×
1234
            status: self.status.try_into()?,
×
1235
            mined_height: self.mined_height.map(|mh| mh as u64),
×
1236
            mined_in_block,
×
1237
            mined_timestamp: self.mined_timestamp.map(|mt| mt.and_utc()),
×
1238
            marked_deleted_at_height: self.marked_deleted_at_height.map(|d| d as u64),
×
1239
            marked_deleted_in_block,
×
1240
            spending_priority,
×
1241
            source: self.source.try_into()?,
×
1242
            received_in_tx_id: self.received_in_tx_id.map(|d| (d as u64).into()),
×
1243
            spent_in_tx_id: self.spent_in_tx_id.map(|d| (d as u64).into()),
×
1244
            payment_id,
×
1245
        })
1246
    }
×
1247
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc