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

stacks-network / stacks-core / 26250451051-1

21 May 2026 08:11PM UTC coverage: 85.585% (-0.1%) from 85.712%
26250451051-1

Pull #7215

github

ec9d4c
web-flow
Merge 9487bf852 into af1280aac
Pull Request #7215: Chore: fix flake in non_blocking_minority_configured_to_favour_...

188844 of 220651 relevant lines covered (85.58%)

18975267.44 hits per line

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

80.97
/clarity/src/vm/database/structures.rs
1
// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
2
// Copyright (C) 2020 Stacks Open Internet Foundation
3
//
4
// This program is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// This program is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

17
use std::io::Write;
18

19
use serde::Deserialize;
20
use stacks_common::util::hash::{hex_bytes, to_hex};
21

22
use crate::vm::analysis::ContractAnalysis;
23
use crate::vm::contexts::ContractContext;
24
use crate::vm::database::ClarityDatabase;
25
use crate::vm::errors::{RuntimeError, VmExecutionError, VmInternalError};
26
use crate::vm::types::{PrincipalData, TypeSignature};
27

28
pub trait ClaritySerializable {
29
    fn serialize(&self) -> String;
30
}
31

32
pub trait ClarityDeserializable<T> {
33
    fn deserialize(json: &str) -> Result<T, VmExecutionError>;
34
}
35

36
impl ClaritySerializable for String {
37
    fn serialize(&self) -> String {
1,743,846✔
38
        self.into()
1,743,846✔
39
    }
1,743,846✔
40
}
41

42
impl ClarityDeserializable<String> for String {
43
    fn deserialize(serialized: &str) -> Result<String, VmExecutionError> {
121,648✔
44
        Ok(serialized.into())
121,648✔
45
    }
121,648✔
46
}
47

48
/// JSON deserialization helper for non-WASM targets.
49
#[cfg(not(target_family = "wasm"))]
50
fn deserialize_json<T: serde::de::DeserializeOwned>(json: &str) -> Result<T, VmExecutionError> {
120,332,141✔
51
    let mut deserializer = serde_json::Deserializer::from_str(json);
120,332,141✔
52

53
    // serde's default 128 depth limit can be exhausted by a 64-stack-depth AST, so
54
    // disable the recursion limit and let stacker spill the deserializer to the heap
55
    // instead of overflowing.
56
    deserializer.disable_recursion_limit();
120,332,141✔
57
    let deserializer = serde_stacker::Deserializer::new(&mut deserializer);
120,332,141✔
58

59
    T::deserialize(deserializer)
120,332,141✔
60
        .map_err(|_| VmInternalError::Expect("Failed to deserialize vm.Value".into()).into())
120,332,141✔
61
}
120,332,141✔
62

63
/// JSON deserialization helper for WASM targets, which don't have access to
64
/// `serde_stacker` and thus can't disable the recursion limit.
65
#[cfg(target_family = "wasm")]
66
fn deserialize_json<T: serde::de::DeserializeOwned>(json: &str) -> Result<T, VmExecutionError> {
67
    serde_json::from_str(json)
68
        .map_err(|_| VmInternalError::Expect("Failed to deserialize vm.Value".into()).into())
69
}
70

71
macro_rules! clarity_serializable {
72
    ($Name:ident) => {
73
        impl ClaritySerializable for $Name {
74
            fn serialize(&self) -> String {
11,063,788✔
75
                serde_json::to_string(self).expect("Failed to serialize vm.Value")
11,063,788✔
76
            }
11,063,788✔
77
        }
78
        impl ClarityDeserializable<$Name> for $Name {
79
            fn deserialize(json: &str) -> Result<Self, VmExecutionError> {
95,944,071✔
80
                deserialize_json(json)
95,944,071✔
81
            }
95,944,071✔
82
        }
83
    };
84
}
85

86
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87
pub struct FungibleTokenMetadata {
88
    pub total_supply: Option<u128>,
89
}
90

91
clarity_serializable!(FungibleTokenMetadata);
92

93
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
94
pub struct NonFungibleTokenMetadata {
95
    pub key_type: TypeSignature,
96
}
97

98
clarity_serializable!(NonFungibleTokenMetadata);
99

100
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
101
pub struct DataMapMetadata {
102
    pub key_type: TypeSignature,
103
    pub value_type: TypeSignature,
104
}
105

106
clarity_serializable!(DataMapMetadata);
107

108
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
109
pub struct DataVariableMetadata {
110
    pub value_type: TypeSignature,
111
}
112

113
clarity_serializable!(DataVariableMetadata);
114

115
#[derive(Serialize, Deserialize)]
116
pub struct SimmedBlock {
117
    pub block_height: u64,
118
    pub block_time: u64,
119
    pub block_header_hash: [u8; 32],
120
    pub burn_chain_header_hash: [u8; 32],
121
    pub vrf_seed: [u8; 32],
122
}
123

124
clarity_serializable!(SimmedBlock);
125

126
clarity_serializable!(PrincipalData);
127
clarity_serializable!(i128);
128
clarity_serializable!(u128);
129
clarity_serializable!(u64);
130

131
/// Handle serialization of [`ContractContext`] behind a wrapper struct with a single
132
/// `contract_context` field.
133
///
134
/// This is for compatibility with the current on-disk format, where the `ContractContext` was
135
/// previously serialized via the `Contract` type. This removes/isolates that dependency and
136
/// allows us to work directly with `ContractContext`s.
137
mod contract_context {
138
    use super::*;
139

140
    #[derive(Serialize, Deserialize)]
141
    pub struct Wrapper<T> {
142
        pub contract_context: T,
143
    }
144

145
    impl ClaritySerializable for ContractContext {
146
        fn serialize(&self) -> String {
938,809✔
147
            serde_json::to_string(&Wrapper {
938,809✔
148
                contract_context: self,
938,809✔
149
            })
938,809✔
150
            .expect("Failed to serialize vm.Value")
938,809✔
151
        }
938,809✔
152
    }
153
    impl ClarityDeserializable<ContractContext> for ContractContext {
154
        fn deserialize(json: &str) -> Result<Self, VmExecutionError> {
24,388,070✔
155
            deserialize_json::<Wrapper<ContractContext>>(json).map(|w| w.contract_context)
24,388,070✔
156
        }
24,388,070✔
157
    }
158
}
159

160
clarity_serializable!(ContractAnalysis);
161

162
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
163
pub enum STXBalance {
164
    Unlocked {
165
        amount: u128,
166
    },
167
    LockedPoxOne {
168
        amount_unlocked: u128,
169
        amount_locked: u128,
170
        unlock_height: u64,
171
    },
172
    LockedPoxTwo {
173
        amount_unlocked: u128,
174
        amount_locked: u128,
175
        unlock_height: u64,
176
    },
177
    LockedPoxThree {
178
        amount_unlocked: u128,
179
        amount_locked: u128,
180
        unlock_height: u64,
181
    },
182
    LockedPoxFour {
183
        amount_unlocked: u128,
184
        amount_locked: u128,
185
        unlock_height: u64,
186
    },
187
}
188

189
/// Lifetime-limited handle to an uncommitted balance structure.
190
/// All balance mutations (debits, credits, locks, unlocks) must go through this structure.
191
pub struct STXBalanceSnapshot<'db, 'conn> {
192
    principal: PrincipalData,
193
    balance: STXBalance,
194
    burn_block_height: u64,
195
    db_ref: &'conn mut ClarityDatabase<'db>,
196
}
197

198
impl ClaritySerializable for STXBalance {
199
    #[allow(clippy::expect_used)]
200
    fn serialize(&self) -> String {
16,077,606✔
201
        let mut buffer = Vec::new();
16,077,606✔
202
        match self {
16,077,606✔
203
            STXBalance::Unlocked { amount } => {
15,122,118✔
204
                buffer
15,122,118✔
205
                    .write_all(&amount.to_be_bytes())
15,122,118✔
206
                    .expect("STXBalance serialization: failed writing amount_unlocked.");
15,122,118✔
207
                buffer
15,122,118✔
208
                    .write_all(&0u128.to_be_bytes())
15,122,118✔
209
                    .expect("STXBalance serialization: failed writing amount_locked.");
15,122,118✔
210
                buffer
15,122,118✔
211
                    .write_all(&0u64.to_be_bytes())
15,122,118✔
212
                    .expect("STXBalance serialization: failed writing unlock_height.");
15,122,118✔
213
            }
15,122,118✔
214
            STXBalance::LockedPoxOne {
215
                amount_unlocked,
2,016✔
216
                amount_locked,
2,016✔
217
                unlock_height,
2,016✔
218
            } => {
2,016✔
219
                buffer
2,016✔
220
                    .write_all(&amount_unlocked.to_be_bytes())
2,016✔
221
                    .expect("STXBalance serialization: failed writing amount_unlocked.");
2,016✔
222
                buffer
2,016✔
223
                    .write_all(&amount_locked.to_be_bytes())
2,016✔
224
                    .expect("STXBalance serialization: failed writing amount_locked.");
2,016✔
225
                buffer
2,016✔
226
                    .write_all(&unlock_height.to_be_bytes())
2,016✔
227
                    .expect("STXBalance serialization: failed writing unlock_height.");
2,016✔
228
            }
2,016✔
229
            STXBalance::LockedPoxTwo {
230
                amount_unlocked,
1,812✔
231
                amount_locked,
1,812✔
232
                unlock_height,
1,812✔
233
            } => {
1,812✔
234
                buffer
1,812✔
235
                    .write_all(&[STXBalance::pox_2_version])
1,812✔
236
                    .expect("STXBalance serialization: failed to write PoX version byte");
1,812✔
237
                buffer
1,812✔
238
                    .write_all(&amount_unlocked.to_be_bytes())
1,812✔
239
                    .expect("STXBalance serialization: failed writing amount_unlocked.");
1,812✔
240
                buffer
1,812✔
241
                    .write_all(&amount_locked.to_be_bytes())
1,812✔
242
                    .expect("STXBalance serialization: failed writing amount_locked.");
1,812✔
243
                buffer
1,812✔
244
                    .write_all(&unlock_height.to_be_bytes())
1,812✔
245
                    .expect("STXBalance serialization: failed writing unlock_height.");
1,812✔
246
            }
1,812✔
247
            STXBalance::LockedPoxThree {
248
                amount_unlocked,
841,224✔
249
                amount_locked,
841,224✔
250
                unlock_height,
841,224✔
251
            } => {
841,224✔
252
                buffer
841,224✔
253
                    .write_all(&[STXBalance::pox_3_version])
841,224✔
254
                    .expect("STXBalance serialization: failed to write PoX version byte");
841,224✔
255
                buffer
841,224✔
256
                    .write_all(&amount_unlocked.to_be_bytes())
841,224✔
257
                    .expect("STXBalance serialization: failed writing amount_unlocked.");
841,224✔
258
                buffer
841,224✔
259
                    .write_all(&amount_locked.to_be_bytes())
841,224✔
260
                    .expect("STXBalance serialization: failed writing amount_locked.");
841,224✔
261
                buffer
841,224✔
262
                    .write_all(&unlock_height.to_be_bytes())
841,224✔
263
                    .expect("STXBalance serialization: failed writing unlock_height.");
841,224✔
264
            }
841,224✔
265
            STXBalance::LockedPoxFour {
266
                amount_unlocked,
110,436✔
267
                amount_locked,
110,436✔
268
                unlock_height,
110,436✔
269
            } => {
110,436✔
270
                buffer
110,436✔
271
                    .write_all(&[STXBalance::pox_4_version])
110,436✔
272
                    .expect("STXBalance serialization: failed to write PoX version byte");
110,436✔
273
                buffer
110,436✔
274
                    .write_all(&amount_unlocked.to_be_bytes())
110,436✔
275
                    .expect("STXBalance serialization: failed writing amount_unlocked.");
110,436✔
276
                buffer
110,436✔
277
                    .write_all(&amount_locked.to_be_bytes())
110,436✔
278
                    .expect("STXBalance serialization: failed writing amount_locked.");
110,436✔
279
                buffer
110,436✔
280
                    .write_all(&unlock_height.to_be_bytes())
110,436✔
281
                    .expect("STXBalance serialization: failed writing unlock_height.");
110,436✔
282
            }
110,436✔
283
        }
284
        to_hex(buffer.as_slice())
16,077,606✔
285
    }
16,077,606✔
286
}
287

288
impl ClarityDeserializable<STXBalance> for STXBalance {
289
    fn deserialize(input: &str) -> Result<Self, VmExecutionError> {
26,205,972✔
290
        let bytes = hex_bytes(input).map_err(|_| {
26,205,972✔
291
            VmInternalError::Expect("STXBalance deserialization: failed decoding bytes.".into())
×
292
        })?;
×
293
        let result = if bytes.len() == STXBalance::unlocked_and_v1_size {
26,205,972✔
294
            let amount_unlocked = u128::from_be_bytes(bytes[0..16].try_into().map_err(|_| {
26,164,824✔
295
                VmInternalError::Expect(
×
296
                    "STXBalance deserialization: failed reading amount_unlocked.".into(),
×
297
                )
×
298
            })?);
×
299
            let amount_locked = u128::from_be_bytes(bytes[16..32].try_into().map_err(|_| {
26,164,824✔
300
                VmInternalError::Expect(
×
301
                    "STXBalance deserialization: failed reading amount_locked.".into(),
×
302
                )
×
303
            })?);
×
304
            let unlock_height = u64::from_be_bytes(bytes[32..40].try_into().map_err(|_| {
26,164,824✔
305
                VmInternalError::Expect(
×
306
                    "STXBalance deserialization: failed reading unlock_height.".into(),
×
307
                )
×
308
            })?);
×
309

310
            if amount_locked == 0 {
26,164,824✔
311
                STXBalance::Unlocked {
26,154,900✔
312
                    amount: amount_unlocked,
26,154,900✔
313
                }
26,154,900✔
314
            } else {
315
                STXBalance::LockedPoxOne {
9,924✔
316
                    amount_unlocked,
9,924✔
317
                    amount_locked,
9,924✔
318
                    unlock_height,
9,924✔
319
                }
9,924✔
320
            }
321
        } else if bytes.len() == STXBalance::v2_to_v4_size {
41,148✔
322
            let version = &bytes[0];
41,148✔
323
            if version != &STXBalance::pox_2_version
41,148✔
324
                && version != &STXBalance::pox_3_version
19,152✔
325
                && version != &STXBalance::pox_4_version
10,536✔
326
            {
327
                return Err(VmInternalError::Expect(format!(
×
328
                    "Bad version byte in STX Balance serialization = {version}"
×
329
                ))
×
330
                .into());
×
331
            }
41,148✔
332
            let amount_unlocked = u128::from_be_bytes(bytes[1..17].try_into().map_err(|_| {
41,148✔
333
                VmInternalError::Expect(
×
334
                    "STXBalance deserialization: failed reading amount_unlocked.".into(),
×
335
                )
×
336
            })?);
×
337
            let amount_locked = u128::from_be_bytes(bytes[17..33].try_into().map_err(|_| {
41,148✔
338
                VmInternalError::Expect(
×
339
                    "STXBalance deserialization: failed reading amount_locked.".into(),
×
340
                )
×
341
            })?);
×
342
            let unlock_height = u64::from_be_bytes(bytes[33..41].try_into().map_err(|_| {
41,148✔
343
                VmInternalError::Expect(
×
344
                    "STXBalance deserialization: failed reading unlock_height.".into(),
×
345
                )
×
346
            })?);
×
347

348
            if amount_locked == 0 {
41,148✔
349
                STXBalance::Unlocked {
×
350
                    amount: amount_unlocked,
×
351
                }
×
352
            } else if version == &STXBalance::pox_2_version {
41,148✔
353
                STXBalance::LockedPoxTwo {
21,996✔
354
                    amount_unlocked,
21,996✔
355
                    amount_locked,
21,996✔
356
                    unlock_height,
21,996✔
357
                }
21,996✔
358
            } else if version == &STXBalance::pox_3_version {
19,152✔
359
                STXBalance::LockedPoxThree {
8,616✔
360
                    amount_unlocked,
8,616✔
361
                    amount_locked,
8,616✔
362
                    unlock_height,
8,616✔
363
                }
8,616✔
364
            } else if version == &STXBalance::pox_4_version {
10,536✔
365
                STXBalance::LockedPoxFour {
10,536✔
366
                    amount_unlocked,
10,536✔
367
                    amount_locked,
10,536✔
368
                    unlock_height,
10,536✔
369
                }
10,536✔
370
            } else {
371
                return Err(VmInternalError::Expect(
×
372
                    "Version is checked for pox_3 or pox_2 version compliance above".into(),
×
373
                )
×
374
                .into());
×
375
            }
376
        } else {
377
            return Err(VmInternalError::Expect(format!(
×
378
                "Bad STX Balance serialization size = {}",
×
379
                bytes.len()
×
380
            ))
×
381
            .into());
×
382
        };
383
        Ok(result)
26,205,972✔
384
    }
26,205,972✔
385
}
386

387
impl<'db, 'conn> STXBalanceSnapshot<'db, 'conn> {
388
    pub fn new(
11,823,541✔
389
        principal: &PrincipalData,
11,823,541✔
390
        balance: STXBalance,
11,823,541✔
391
        burn_height: u64,
11,823,541✔
392
        db_ref: &'conn mut ClarityDatabase<'db>,
11,823,541✔
393
    ) -> STXBalanceSnapshot<'db, 'conn> {
11,823,541✔
394
        STXBalanceSnapshot {
11,823,541✔
395
            principal: principal.clone(),
11,823,541✔
396
            balance,
11,823,541✔
397
            burn_block_height: burn_height,
11,823,541✔
398
            db_ref,
11,823,541✔
399
        }
11,823,541✔
400
    }
11,823,541✔
401

402
    pub fn balance(&self) -> &STXBalance {
1,470✔
403
        &self.balance
1,470✔
404
    }
1,470✔
405

406
    pub fn save(self) -> Result<(), VmExecutionError> {
11,375,373✔
407
        let key = ClarityDatabase::make_key_for_account_balance(&self.principal);
11,375,373✔
408
        self.db_ref.put_data(&key, &self.balance)
11,375,373✔
409
    }
11,375,373✔
410

411
    pub fn transfer_to(
3,862,005✔
412
        mut self,
3,862,005✔
413
        recipient: &PrincipalData,
3,862,005✔
414
        amount: u128,
3,862,005✔
415
    ) -> Result<(), VmExecutionError> {
3,862,005✔
416
        if !self.can_transfer(amount)? {
3,862,005✔
417
            return Err(VmInternalError::InsufficientBalance.into());
×
418
        }
3,862,005✔
419

420
        let recipient_key = ClarityDatabase::make_key_for_account_balance(recipient);
3,862,005✔
421
        let mut recipient_balance = self
3,862,005✔
422
            .db_ref
3,862,005✔
423
            .get_data(&recipient_key)?
3,862,005✔
424
            .unwrap_or(STXBalance::zero());
3,862,005✔
425

426
        recipient_balance
3,862,005✔
427
            .checked_add_unlocked_amount(amount)
3,862,005✔
428
            .ok_or(VmExecutionError::Runtime(
3,862,005✔
429
                RuntimeError::ArithmeticOverflow,
3,862,005✔
430
                None,
3,862,005✔
431
            ))?;
3,862,005✔
432

433
        self.debit(amount)?;
3,862,005✔
434
        self.db_ref.put_data(&recipient_key, &recipient_balance)?;
3,862,005✔
435
        self.save()?;
3,862,005✔
436
        Ok(())
3,862,005✔
437
    }
3,862,005✔
438

439
    pub fn get_available_balance(&mut self) -> Result<u128, VmExecutionError> {
13,558,208✔
440
        let v1_unlock_height = self.db_ref.get_v1_unlock_height();
13,558,208✔
441
        let v2_unlock_height = self.db_ref.get_v2_unlock_height()?;
13,558,208✔
442
        let v3_unlock_height = self.db_ref.get_v3_unlock_height()?;
13,558,208✔
443
        self.balance.get_available_balance_at_burn_block(
13,558,208✔
444
            self.burn_block_height,
13,558,208✔
445
            v1_unlock_height,
13,558,208✔
446
            v2_unlock_height,
13,558,208✔
447
            v3_unlock_height,
13,558,208✔
448
        )
449
    }
13,558,208✔
450

451
    pub fn canonical_balance_repr(&mut self) -> Result<STXBalance, VmExecutionError> {
152,187✔
452
        let v1_unlock_height = self.db_ref.get_v1_unlock_height();
152,187✔
453
        let v2_unlock_height = self.db_ref.get_v2_unlock_height()?;
152,187✔
454
        let v3_unlock_height = self.db_ref.get_v3_unlock_height()?;
152,187✔
455
        Ok(self
152,187✔
456
            .balance
152,187✔
457
            .canonical_repr_at_block(
152,187✔
458
                self.burn_block_height,
152,187✔
459
                v1_unlock_height,
152,187✔
460
                v2_unlock_height,
152,187✔
461
                v3_unlock_height,
152,187✔
462
            )?
×
463
            .0)
464
    }
152,187✔
465

466
    pub fn has_locked_tokens(&mut self) -> Result<bool, VmExecutionError> {
223,608✔
467
        let v1_unlock_height = self.db_ref.get_v1_unlock_height();
223,608✔
468
        let v2_unlock_height = self.db_ref.get_v2_unlock_height()?;
223,608✔
469
        let v3_unlock_height = self.db_ref.get_v3_unlock_height()?;
223,608✔
470
        Ok(self.balance.has_locked_tokens_at_burn_block(
223,608✔
471
            self.burn_block_height,
223,608✔
472
            v1_unlock_height,
223,608✔
473
            v2_unlock_height,
223,608✔
474
            v3_unlock_height,
223,608✔
475
        ))
223,608✔
476
    }
223,608✔
477

478
    pub fn has_unlockable_tokens(&mut self) -> Result<bool, VmExecutionError> {
×
479
        let v1_unlock_height = self.db_ref.get_v1_unlock_height();
×
480
        let v2_unlock_height = self.db_ref.get_v2_unlock_height()?;
×
481
        let v3_unlock_height = self.db_ref.get_v3_unlock_height()?;
×
482
        Ok(self.balance.has_unlockable_tokens_at_burn_block(
×
483
            self.burn_block_height,
×
484
            v1_unlock_height,
×
485
            v2_unlock_height,
×
486
            v3_unlock_height,
×
487
        ))
×
488
    }
×
489

490
    pub fn can_transfer(&mut self, amount: u128) -> Result<bool, VmExecutionError> {
13,186,027✔
491
        Ok(self.get_available_balance()? >= amount)
13,186,027✔
492
    }
13,186,027✔
493

494
    pub fn debit(&mut self, amount: u128) -> Result<(), VmExecutionError> {
9,213,298✔
495
        let unlocked = self.unlock_available_tokens_if_any()?;
9,213,298✔
496
        if unlocked > 0 {
9,213,298✔
497
            debug!("Consolidated after account-debit");
696✔
498
        }
9,212,602✔
499

500
        self.balance.debit_unlocked_amount(amount)
9,213,298✔
501
    }
9,213,298✔
502

503
    pub fn credit(&mut self, amount: u128) -> Result<(), VmExecutionError> {
2,035,297✔
504
        let unlocked = self.unlock_available_tokens_if_any()?;
2,035,297✔
505
        if unlocked > 0 {
2,035,297✔
506
            debug!("Consolidated after account-credit");
×
507
        }
2,035,297✔
508

509
        self.balance
2,035,297✔
510
            .checked_add_unlocked_amount(amount)
2,035,297✔
511
            .ok_or_else(|| VmInternalError::Expect("STX balance overflow".into()))?;
2,035,297✔
512
        Ok(())
2,035,297✔
513
    }
2,035,297✔
514

515
    pub fn set_balance(&mut self, balance: STXBalance) {
14,840✔
516
        self.balance = balance;
14,840✔
517
    }
14,840✔
518

519
    pub fn lock_tokens_v1(
960✔
520
        &mut self,
960✔
521
        amount_to_lock: u128,
960✔
522
        unlock_burn_height: u64,
960✔
523
    ) -> Result<(), VmExecutionError> {
960✔
524
        let unlocked = self.unlock_available_tokens_if_any()?;
960✔
525
        if unlocked > 0 {
960✔
526
            debug!("Consolidated after account-token-lock");
24✔
527
        }
936✔
528

529
        // caller needs to have checked this
530
        assert!(amount_to_lock > 0, "BUG: cannot lock 0 tokens");
960✔
531

532
        if unlock_burn_height <= self.burn_block_height {
960✔
533
            // caller needs to have checked this
534
            return Err(VmInternalError::Expect(
×
535
                "FATAL: cannot set a lock with expired unlock burn height".into(),
×
536
            )
×
537
            .into());
×
538
        }
960✔
539

540
        if self.has_locked_tokens()? {
960✔
541
            // caller needs to have checked this
542
            return Err(
×
543
                VmInternalError::Expect("FATAL: account already has locked tokens".into()).into(),
×
544
            );
×
545
        }
960✔
546

547
        // from `unlock_available_tokens_if_any` call above, `self.balance` should
548
        //  be canonicalized already
549

550
        let new_amount_unlocked = self
960✔
551
            .balance
960✔
552
            .get_total_balance()?
960✔
553
            .checked_sub(amount_to_lock)
960✔
554
            .ok_or_else(|| VmInternalError::Expect("STX underflow".into()))?;
960✔
555

556
        self.balance = STXBalance::LockedPoxOne {
960✔
557
            amount_unlocked: new_amount_unlocked,
960✔
558
            amount_locked: amount_to_lock,
960✔
559
            unlock_height: unlock_burn_height,
960✔
560
        };
960✔
561
        Ok(())
960✔
562
    }
960✔
563

564
    ////////////// Pox-2 /////////////////
565

566
    /// Return true iff `self` represents a snapshot that has a lock
567
    ///  created by PoX v2.
568
    pub fn is_v2_locked(&mut self) -> Result<bool, VmExecutionError> {
240✔
569
        match self.canonical_balance_repr()? {
240✔
570
            STXBalance::LockedPoxTwo { .. } => Ok(true),
240✔
571
            _ => Ok(false),
×
572
        }
573
    }
240✔
574

575
    /// Increase the account's current lock to `new_total_locked`.
576
    /// Panics if `self` was not locked by V2 PoX.
577
    pub fn increase_lock_v2(&mut self, new_total_locked: u128) -> Result<(), VmExecutionError> {
120✔
578
        let unlocked = self.unlock_available_tokens_if_any()?;
120✔
579
        if unlocked > 0 {
120✔
580
            debug!("Consolidated after extend-token-lock");
×
581
        }
120✔
582

583
        if !self.has_locked_tokens()? {
120✔
584
            // caller needs to have checked this
585
            return Err(VmInternalError::Expect(
×
586
                "FATAL: account does not have locked tokens".into(),
×
587
            )
×
588
            .into());
×
589
        }
120✔
590

591
        if !self.is_v2_locked()? {
120✔
592
            // caller needs to have checked this
593
            return Err(
×
594
                VmInternalError::Expect("FATAL: account must be locked by pox-2".into()).into(),
×
595
            );
×
596
        }
120✔
597

598
        if self.balance.amount_locked() > new_total_locked {
120✔
599
            return Err(VmInternalError::Expect(
×
600
                "FATAL: account must lock more after `increase_lock_v2`".into(),
×
601
            )
×
602
            .into());
×
603
        }
120✔
604

605
        let total_amount = self
120✔
606
            .balance
120✔
607
            .amount_unlocked()
120✔
608
            .checked_add(self.balance.amount_locked())
120✔
609
            .ok_or_else(|| VmInternalError::Expect("STX balance overflowed u128".into()))?;
120✔
610
        let amount_unlocked = total_amount.checked_sub(new_total_locked).ok_or_else(|| {
120✔
611
            VmInternalError::Expect("STX underflow: more is locked than total balance".into())
×
612
        })?;
×
613

614
        self.balance = STXBalance::LockedPoxTwo {
120✔
615
            amount_unlocked,
120✔
616
            amount_locked: new_total_locked,
120✔
617
            unlock_height: self.balance.unlock_height(),
120✔
618
        };
120✔
619

620
        Ok(())
120✔
621
    }
120✔
622

623
    /// Extend this account's current lock to `unlock_burn_height`.
624
    /// After calling, this method will set the balance to a "LockedPoxTwo" balance,
625
    ///  because this method is only invoked as a result of PoX2 interactions
626
    pub fn extend_lock_v2(&mut self, unlock_burn_height: u64) -> Result<(), VmExecutionError> {
276✔
627
        let unlocked = self.unlock_available_tokens_if_any()?;
276✔
628
        if unlocked > 0 {
276✔
629
            debug!("Consolidated after extend-token-lock");
×
630
        }
276✔
631

632
        if !self.has_locked_tokens()? {
276✔
633
            // caller needs to have checked this
634
            return Err(VmInternalError::Expect(
×
635
                "FATAL: account does not have locked tokens".into(),
×
636
            )
×
637
            .into());
×
638
        }
276✔
639

640
        if unlock_burn_height <= self.burn_block_height {
276✔
641
            // caller needs to have checked this
642
            return Err(VmInternalError::Expect(
×
643
                "FATAL: cannot set a lock with expired unlock burn height".into(),
×
644
            )
×
645
            .into());
×
646
        }
276✔
647

648
        self.balance = STXBalance::LockedPoxTwo {
276✔
649
            amount_unlocked: self.balance.amount_unlocked(),
276✔
650
            amount_locked: self.balance.amount_locked(),
276✔
651
            unlock_height: unlock_burn_height,
276✔
652
        };
276✔
653
        Ok(())
276✔
654
    }
276✔
655

656
    /// Lock `amount_to_lock` tokens on this account until `unlock_burn_height`.
657
    /// After calling, this method will set the balance to a "LockedPoxTwo" balance,
658
    ///  because this method is only invoked as a result of PoX2 interactions
659
    pub fn lock_tokens_v2(
912✔
660
        &mut self,
912✔
661
        amount_to_lock: u128,
912✔
662
        unlock_burn_height: u64,
912✔
663
    ) -> Result<(), VmExecutionError> {
912✔
664
        let unlocked = self.unlock_available_tokens_if_any()?;
912✔
665
        if unlocked > 0 {
912✔
666
            debug!("Consolidated after account-token-lock");
24✔
667
        }
888✔
668

669
        // caller needs to have checked this
670
        if amount_to_lock == 0 {
912✔
671
            return Err(VmInternalError::Expect("BUG: cannot lock 0 tokens".into()).into());
×
672
        }
912✔
673

674
        if unlock_burn_height <= self.burn_block_height {
912✔
675
            // caller needs to have checked this
676
            return Err(VmInternalError::Expect(
×
677
                "FATAL: cannot set a lock with expired unlock burn height".into(),
×
678
            )
×
679
            .into());
×
680
        }
912✔
681

682
        if self.has_locked_tokens()? {
912✔
683
            // caller needs to have checked this
684
            return Err(
×
685
                VmInternalError::Expect("FATAL: account already has locked tokens".into()).into(),
×
686
            );
×
687
        }
912✔
688

689
        // from `unlock_available_tokens_if_any` call above, `self.balance` should
690
        //  be canonicalized already
691

692
        let new_amount_unlocked = self
912✔
693
            .balance
912✔
694
            .get_total_balance()?
912✔
695
            .checked_sub(amount_to_lock)
912✔
696
            .ok_or_else(|| VmInternalError::Expect("STX underflow".into()))?;
912✔
697

698
        self.balance = STXBalance::LockedPoxTwo {
912✔
699
            amount_unlocked: new_amount_unlocked,
912✔
700
            amount_locked: amount_to_lock,
912✔
701
            unlock_height: unlock_burn_height,
912✔
702
        };
912✔
703
        Ok(())
912✔
704
    }
912✔
705

706
    //////////////// Pox-3 //////////////////
707

708
    /// Lock `amount_to_lock` tokens on this account until `unlock_burn_height`.
709
    /// After calling, this method will set the balance to a "LockedPoxThree" balance,
710
    ///  because this method is only invoked as a result of PoX3 interactions
711
    pub fn lock_tokens_v3(
684✔
712
        &mut self,
684✔
713
        amount_to_lock: u128,
684✔
714
        unlock_burn_height: u64,
684✔
715
    ) -> Result<(), VmExecutionError> {
684✔
716
        let unlocked = self.unlock_available_tokens_if_any()?;
684✔
717
        if unlocked > 0 {
684✔
718
            debug!("Consolidated after account-token-lock");
×
719
        }
684✔
720

721
        // caller needs to have checked this
722
        assert!(amount_to_lock > 0, "BUG: cannot lock 0 tokens");
684✔
723

724
        if unlock_burn_height <= self.burn_block_height {
684✔
725
            // caller needs to have checked this
726
            return Err(VmInternalError::Expect(
×
727
                "FATAL: cannot set a lock with expired unlock burn height".into(),
×
728
            )
×
729
            .into());
×
730
        }
684✔
731

732
        if self.has_locked_tokens()? {
684✔
733
            // caller needs to have checked this
734
            return Err(
×
735
                VmInternalError::Expect("FATAL: account already has locked tokens".into()).into(),
×
736
            );
×
737
        }
684✔
738

739
        // from `unlock_available_tokens_if_any` call above, `self.balance` should
740
        //  be canonicalized already
741

742
        let new_amount_unlocked = self
684✔
743
            .balance
684✔
744
            .get_total_balance()?
684✔
745
            .checked_sub(amount_to_lock)
684✔
746
            .ok_or_else(|| {
684✔
747
                VmInternalError::Expect(
×
748
                    "FATAL: account locks more STX than balance possessed".into(),
×
749
                )
×
750
            })?;
×
751

752
        self.balance = STXBalance::LockedPoxThree {
684✔
753
            amount_unlocked: new_amount_unlocked,
684✔
754
            amount_locked: amount_to_lock,
684✔
755
            unlock_height: unlock_burn_height,
684✔
756
        };
684✔
757

758
        Ok(())
684✔
759
    }
684✔
760

761
    /// Extend this account's current lock to `unlock_burn_height`.
762
    /// After calling, this method will set the balance to a "LockedPoxThree" balance,
763
    ///  because this method is only invoked as a result of PoX3 interactions
764
    pub fn extend_lock_v3(&mut self, unlock_burn_height: u64) -> Result<(), VmExecutionError> {
72✔
765
        let unlocked = self.unlock_available_tokens_if_any()?;
72✔
766
        if unlocked > 0 {
72✔
767
            debug!("Consolidated after extend-token-lock");
×
768
        }
72✔
769

770
        if !self.has_locked_tokens()? {
72✔
771
            // caller needs to have checked this
772
            return Err(VmInternalError::Expect(
×
773
                "FATAL: account does not have locked tokens".into(),
×
774
            )
×
775
            .into());
×
776
        }
72✔
777

778
        if unlock_burn_height <= self.burn_block_height {
72✔
779
            // caller needs to have checked this
780
            return Err(VmInternalError::Expect(
×
781
                "FATAL: cannot set a lock with expired unlock burn height".into(),
×
782
            )
×
783
            .into());
×
784
        }
72✔
785

786
        self.balance = STXBalance::LockedPoxThree {
72✔
787
            amount_unlocked: self.balance.amount_unlocked(),
72✔
788
            amount_locked: self.balance.amount_locked(),
72✔
789
            unlock_height: unlock_burn_height,
72✔
790
        };
72✔
791
        Ok(())
72✔
792
    }
72✔
793

794
    /// Increase the account's current lock to `new_total_locked`.
795
    /// Panics if `self` was not locked by V3 PoX.
796
    pub fn increase_lock_v3(&mut self, new_total_locked: u128) -> Result<(), VmExecutionError> {
72✔
797
        let unlocked = self.unlock_available_tokens_if_any()?;
72✔
798
        if unlocked > 0 {
72✔
799
            debug!("Consolidated after extend-token-lock");
×
800
        }
72✔
801

802
        if !self.has_locked_tokens()? {
72✔
803
            // caller needs to have checked this
804
            return Err(VmInternalError::Expect(
×
805
                "FATAL: account does not have locked tokens".into(),
×
806
            )
×
807
            .into());
×
808
        }
72✔
809

810
        if !self.is_v3_locked()? {
72✔
811
            // caller needs to have checked this
812
            return Err(
×
813
                VmInternalError::Expect("FATAL: account must be locked by pox-3".into()).into(),
×
814
            );
×
815
        }
72✔
816

817
        assert!(
72✔
818
            self.balance.amount_locked() <= new_total_locked,
72✔
819
            "FATAL: account must lock more after `increase_lock_v3`"
820
        );
821

822
        let total_amount = self
72✔
823
            .balance
72✔
824
            .amount_unlocked()
72✔
825
            .checked_add(self.balance.amount_locked())
72✔
826
            .ok_or_else(|| VmInternalError::Expect("STX balance overflowed u128".into()))?;
72✔
827
        let amount_unlocked = total_amount.checked_sub(new_total_locked).ok_or_else(|| {
72✔
828
            VmInternalError::Expect("STX underflow: more is locked than total balance".into())
×
829
        })?;
×
830

831
        self.balance = STXBalance::LockedPoxThree {
72✔
832
            amount_unlocked,
72✔
833
            amount_locked: new_total_locked,
72✔
834
            unlock_height: self.balance.unlock_height(),
72✔
835
        };
72✔
836
        Ok(())
72✔
837
    }
72✔
838

839
    /// Return true iff `self` represents a snapshot that has a lock
840
    ///  created by PoX v3.
841
    pub fn is_v3_locked(&mut self) -> Result<bool, VmExecutionError> {
72✔
842
        match self.canonical_balance_repr()? {
72✔
843
            STXBalance::LockedPoxThree { .. } => Ok(true),
72✔
844
            _ => Ok(false),
×
845
        }
846
    }
72✔
847

848
    //////////////// Pox-4 //////////////////
849

850
    /// Lock `amount_to_lock` tokens on this account until `unlock_burn_height`.
851
    /// After calling, this method will set the balance to a "LockedPoxFour" balance,
852
    ///  because this method is only invoked as a result of PoX4 interactions
853
    pub fn lock_tokens_v4(
108,072✔
854
        &mut self,
108,072✔
855
        amount_to_lock: u128,
108,072✔
856
        unlock_burn_height: u64,
108,072✔
857
    ) -> Result<(), VmExecutionError> {
108,072✔
858
        let unlocked = self.unlock_available_tokens_if_any()?;
108,072✔
859
        if unlocked > 0 {
108,072✔
860
            debug!("Consolidated after account-token-lock");
×
861
        }
108,072✔
862

863
        // caller needs to have checked this
864
        assert!(amount_to_lock > 0, "BUG: cannot lock 0 tokens");
108,072✔
865

866
        if unlock_burn_height <= self.burn_block_height {
108,072✔
867
            // caller needs to have checked this
868
            panic!("FATAL: cannot set a lock with expired unlock burn height");
×
869
        }
108,072✔
870

871
        if self.has_locked_tokens()? {
108,072✔
872
            // caller needs to have checked this
873
            panic!("FATAL: account already has locked tokens");
×
874
        }
108,072✔
875

876
        // from `unlock_available_tokens_if_any` call above, `self.balance` should
877
        //  be canonicalized already
878

879
        let new_amount_unlocked = self
108,072✔
880
            .balance
108,072✔
881
            .get_total_balance()?
108,072✔
882
            .checked_sub(amount_to_lock)
108,072✔
883
            .expect("FATAL: account locks more STX than balance possessed");
108,072✔
884

885
        self.balance = STXBalance::LockedPoxFour {
108,072✔
886
            amount_unlocked: new_amount_unlocked,
108,072✔
887
            amount_locked: amount_to_lock,
108,072✔
888
            unlock_height: unlock_burn_height,
108,072✔
889
        };
108,072✔
890
        Ok(())
108,072✔
891
    }
108,072✔
892

893
    /// Extend this account's current lock to `unlock_burn_height`.
894
    /// After calling, this method will set the balance to a "LockedPoxFour" balance,
895
    ///  because this method is only invoked as a result of PoX3 interactions
896
    pub fn extend_lock_v4(&mut self, unlock_burn_height: u64) -> Result<(), VmExecutionError> {
408✔
897
        let unlocked = self.unlock_available_tokens_if_any()?;
408✔
898
        if unlocked > 0 {
408✔
899
            debug!("Consolidated after extend-token-lock");
×
900
        }
408✔
901

902
        if !self.has_locked_tokens()? {
408✔
903
            // caller needs to have checked this
904
            panic!("FATAL: account does not have locked tokens");
×
905
        }
408✔
906

907
        if unlock_burn_height <= self.burn_block_height {
408✔
908
            // caller needs to have checked this
909
            panic!("FATAL: cannot set a lock with expired unlock burn height");
×
910
        }
408✔
911

912
        self.balance = STXBalance::LockedPoxFour {
408✔
913
            amount_unlocked: self.balance.amount_unlocked(),
408✔
914
            amount_locked: self.balance.amount_locked(),
408✔
915
            unlock_height: unlock_burn_height,
408✔
916
        };
408✔
917
        Ok(())
408✔
918
    }
408✔
919

920
    /// Increase the account's current lock to `new_total_locked`.
921
    /// Panics if `self` was not locked by V3 PoX.
922
    pub fn increase_lock_v4(&mut self, new_total_locked: u128) -> Result<(), VmExecutionError> {
216✔
923
        let unlocked = self.unlock_available_tokens_if_any()?;
216✔
924
        if unlocked > 0 {
216✔
925
            debug!("Consolidated after extend-token-lock");
×
926
        }
216✔
927

928
        if !self.has_locked_tokens()? {
216✔
929
            // caller needs to have checked this
930
            panic!("FATAL: account does not have locked tokens");
×
931
        }
216✔
932

933
        if !self.is_v4_locked()? {
216✔
934
            // caller needs to have checked this
935
            panic!("FATAL: account must be locked by pox-3");
×
936
        }
216✔
937

938
        assert!(
216✔
939
            self.balance.amount_locked() <= new_total_locked,
216✔
940
            "FATAL: account must lock more after `increase_lock_v3`"
941
        );
942

943
        let total_amount = self
216✔
944
            .balance
216✔
945
            .amount_unlocked()
216✔
946
            .checked_add(self.balance.amount_locked())
216✔
947
            .expect("STX balance overflowed u128");
216✔
948
        let amount_unlocked = total_amount
216✔
949
            .checked_sub(new_total_locked)
216✔
950
            .expect("STX underflow: more is locked than total balance");
216✔
951

952
        self.balance = STXBalance::LockedPoxFour {
216✔
953
            amount_unlocked,
216✔
954
            amount_locked: new_total_locked,
216✔
955
            unlock_height: self.balance.unlock_height(),
216✔
956
        };
216✔
957
        Ok(())
216✔
958
    }
216✔
959

960
    /// Return true iff `self` represents a snapshot that has a lock
961
    ///  created by PoX v3.
962
    pub fn is_v4_locked(&mut self) -> Result<bool, VmExecutionError> {
216✔
963
        match self.canonical_balance_repr()? {
216✔
964
            STXBalance::LockedPoxFour { .. } => Ok(true),
216✔
965
            _ => Ok(false),
×
966
        }
967
    }
216✔
968

969
    /////////////// GENERAL //////////////////////
970

971
    /// If this snapshot is locked, then alter the lock height to be
972
    /// the next burn block (i.e., `self.burn_block_height + 1`)
973
    pub fn accelerate_unlock(&mut self) -> Result<(), VmExecutionError> {
144✔
974
        let unlocked = self.unlock_available_tokens_if_any()?;
144✔
975
        if unlocked > 0 {
144✔
976
            debug!("Consolidated after account-token-lock");
×
977
        }
144✔
978

979
        let new_unlock_height = self.burn_block_height + 1;
144✔
980
        self.balance = match self.balance {
144✔
981
            STXBalance::Unlocked { amount } => STXBalance::Unlocked { amount },
×
982
            STXBalance::LockedPoxOne { .. } => {
983
                return Err(VmInternalError::Expect(
×
984
                    "Attempted to accelerate the unlock of a lockup created by PoX-1".into(),
×
985
                )
×
986
                .into());
×
987
            }
988
            STXBalance::LockedPoxTwo {
989
                amount_unlocked,
96✔
990
                amount_locked,
96✔
991
                ..
992
            } => STXBalance::LockedPoxTwo {
96✔
993
                amount_unlocked,
96✔
994
                amount_locked,
96✔
995
                unlock_height: new_unlock_height,
96✔
996
            },
96✔
997
            STXBalance::LockedPoxThree {
998
                amount_unlocked,
48✔
999
                amount_locked,
48✔
1000
                ..
1001
            } => STXBalance::LockedPoxThree {
48✔
1002
                amount_unlocked,
48✔
1003
                amount_locked,
48✔
1004
                unlock_height: new_unlock_height,
48✔
1005
            },
48✔
1006
            STXBalance::LockedPoxFour {
1007
                amount_unlocked,
×
1008
                amount_locked,
×
1009
                ..
1010
            } => STXBalance::LockedPoxFour {
×
1011
                amount_unlocked,
×
1012
                amount_locked,
×
1013
                unlock_height: new_unlock_height,
×
1014
            },
×
1015
        };
1016
        Ok(())
144✔
1017
    }
144✔
1018

1019
    /// Unlock any tokens that are unlockable at the current
1020
    ///  burn block height, and return the amount newly unlocked
1021
    fn unlock_available_tokens_if_any(&mut self) -> Result<u128, VmExecutionError> {
11,360,531✔
1022
        let (new_balance, unlocked) = self.balance.canonical_repr_at_block(
11,360,531✔
1023
            self.burn_block_height,
11,360,531✔
1024
            self.db_ref.get_v1_unlock_height(),
11,360,531✔
1025
            self.db_ref.get_v2_unlock_height()?,
11,360,531✔
1026
            self.db_ref.get_v3_unlock_height()?,
11,360,531✔
1027
        )?;
×
1028
        self.balance = new_balance;
11,360,531✔
1029
        Ok(unlocked)
11,360,531✔
1030
    }
11,360,531✔
1031
}
1032

1033
impl Default for STXBalance {
1034
    fn default() -> Self {
595,933✔
1035
        STXBalance::zero()
595,933✔
1036
    }
595,933✔
1037
}
1038

1039
// NOTE: do _not_ add mutation methods to this struct. Put them in STXBalanceSnapshot!
1040
impl STXBalance {
1041
    pub const unlocked_and_v1_size: usize = 40;
1042
    pub const v2_to_v4_size: usize = 41;
1043
    pub const pox_2_version: u8 = 0;
1044
    pub const pox_3_version: u8 = 1;
1045
    pub const pox_4_version: u8 = 2;
1046

1047
    pub fn zero() -> STXBalance {
4,586,482✔
1048
        STXBalance::Unlocked { amount: 0 }
4,586,482✔
1049
    }
4,586,482✔
1050

1051
    pub fn initial(amount: u128) -> STXBalance {
11,489✔
1052
        STXBalance::Unlocked { amount }
11,489✔
1053
    }
11,489✔
1054

1055
    /// This method returns the datastructure's lazy view of the unlock_height:
1056
    ///  this *may* be updated by a canonicalized view of the account
1057
    pub fn unlock_height(&self) -> u64 {
24,816✔
1058
        match self {
24,816✔
1059
            STXBalance::Unlocked { .. } => 0,
756✔
1060
            STXBalance::LockedPoxOne { unlock_height, .. }
2,088✔
1061
            | STXBalance::LockedPoxTwo { unlock_height, .. }
14,544✔
1062
            | STXBalance::LockedPoxThree { unlock_height, .. }
6,960✔
1063
            | STXBalance::LockedPoxFour { unlock_height, .. } => *unlock_height,
24,060✔
1064
        }
1065
    }
24,816✔
1066

1067
    /// This method returns the datastructure's lazy view of the unlock_height
1068
    ///  *while* factoring in the PoX 2 early unlock for PoX 1 and PoX 3 early unlock for PoX 2.
1069
    /// This value is still lazy: this unlock height may be less than the current
1070
    ///  burn block height, if so it will be updated in a canonicalized view.
1071
    pub fn effective_unlock_height(
128,367✔
1072
        &self,
128,367✔
1073
        v1_unlock_height: u32,
128,367✔
1074
        v2_unlock_height: u32,
128,367✔
1075
        v3_unlock_height: u32,
128,367✔
1076
    ) -> u64 {
128,367✔
1077
        match self {
128,367✔
1078
            STXBalance::Unlocked { .. } => 0,
123,147✔
1079
            STXBalance::LockedPoxOne { unlock_height, .. } => {
408✔
1080
                if *unlock_height >= u64::from(v1_unlock_height) {
408✔
1081
                    u64::from(v1_unlock_height)
408✔
1082
                } else {
1083
                    *unlock_height
×
1084
                }
1085
            }
1086
            STXBalance::LockedPoxTwo { unlock_height, .. } => {
1,332✔
1087
                if *unlock_height >= u64::from(v2_unlock_height) {
1,332✔
1088
                    u64::from(v2_unlock_height)
×
1089
                } else {
1090
                    *unlock_height
1,332✔
1091
                }
1092
            }
1093
            STXBalance::LockedPoxThree { unlock_height, .. } => {
624✔
1094
                if *unlock_height >= u64::from(v3_unlock_height) {
624✔
1095
                    u64::from(v3_unlock_height)
×
1096
                } else {
1097
                    *unlock_height
624✔
1098
                }
1099
            }
1100
            STXBalance::LockedPoxFour { unlock_height, .. } => *unlock_height,
2,856✔
1101
        }
1102
    }
128,367✔
1103

1104
    /// This method returns the datastructure's lazy view of the amount locked:
1105
    ///  this *may* be updated by a canonicalized view of the account
1106
    pub fn amount_locked(&self) -> u128 {
137,595✔
1107
        match self {
137,595✔
1108
            STXBalance::Unlocked { .. } => 0,
124,299✔
1109
            STXBalance::LockedPoxOne { amount_locked, .. }
3,240✔
1110
            | STXBalance::LockedPoxTwo { amount_locked, .. }
3,228✔
1111
            | STXBalance::LockedPoxThree { amount_locked, .. }
1,572✔
1112
            | STXBalance::LockedPoxFour { amount_locked, .. } => *amount_locked,
13,296✔
1113
        }
1114
    }
137,595✔
1115

1116
    /// This method returns the datastructure's lazy view of the amount unlocked:
1117
    ///  this *may* be updated by a canonicalized view of the account
1118
    pub fn amount_unlocked(&self) -> u128 {
150,279✔
1119
        match self {
150,279✔
1120
            STXBalance::Unlocked {
1121
                amount: amount_unlocked,
141,039✔
1122
            }
1123
            | STXBalance::LockedPoxOne {
1124
                amount_unlocked, ..
2,928✔
1125
            }
1126
            | STXBalance::LockedPoxTwo {
1127
                amount_unlocked, ..
1,752✔
1128
            }
1129
            | STXBalance::LockedPoxThree {
1130
                amount_unlocked, ..
864✔
1131
            }
1132
            | STXBalance::LockedPoxFour {
1133
                amount_unlocked, ..
3,696✔
1134
            } => *amount_unlocked,
150,279✔
1135
        }
1136
    }
150,279✔
1137

1138
    fn debit_unlocked_amount(&mut self, delta: u128) -> Result<(), VmExecutionError> {
9,213,298✔
1139
        match self {
9,213,298✔
1140
            STXBalance::Unlocked {
1141
                amount: amount_unlocked,
9,209,974✔
1142
            }
1143
            | STXBalance::LockedPoxOne {
1144
                amount_unlocked, ..
1,056✔
1145
            }
1146
            | STXBalance::LockedPoxTwo {
1147
                amount_unlocked, ..
408✔
1148
            }
1149
            | STXBalance::LockedPoxThree {
1150
                amount_unlocked, ..
120✔
1151
            }
1152
            | STXBalance::LockedPoxFour {
1153
                amount_unlocked, ..
1,740✔
1154
            } => {
1155
                *amount_unlocked = amount_unlocked
9,213,298✔
1156
                    .checked_sub(delta)
9,213,298✔
1157
                    .ok_or_else(|| VmInternalError::Expect("STX underflow".into()))?;
9,213,298✔
1158
                Ok(())
9,213,298✔
1159
            }
1160
        }
1161
    }
9,213,298✔
1162

1163
    fn checked_add_unlocked_amount(&mut self, delta: u128) -> Option<u128> {
5,897,302✔
1164
        match self {
5,897,302✔
1165
            STXBalance::Unlocked {
1166
                amount: amount_unlocked,
5,897,302✔
1167
            }
1168
            | STXBalance::LockedPoxOne {
1169
                amount_unlocked, ..
×
1170
            }
1171
            | STXBalance::LockedPoxTwo {
1172
                amount_unlocked, ..
×
1173
            }
1174
            | STXBalance::LockedPoxThree {
1175
                amount_unlocked, ..
×
1176
            }
1177
            | STXBalance::LockedPoxFour {
1178
                amount_unlocked, ..
×
1179
            } => {
1180
                if let Some(new_amount) = amount_unlocked.checked_add(delta) {
5,897,302✔
1181
                    *amount_unlocked = new_amount;
5,897,302✔
1182
                    Some(new_amount)
5,897,302✔
1183
                } else {
1184
                    None
×
1185
                }
1186
            }
1187
        }
1188
    }
5,897,302✔
1189

1190
    /// Returns a canonicalized STXBalance at a given burn_block_height
1191
    /// (i.e., if burn_block_height >= unlock_height, then return struct where
1192
    ///   amount_unlocked = 0, unlock_height = 0), and the amount of tokens which
1193
    ///   are "unlocked" by the canonicalization
1194
    pub fn canonical_repr_at_block(
11,512,790✔
1195
        &self,
11,512,790✔
1196
        burn_block_height: u64,
11,512,790✔
1197
        v1_unlock_height: u32,
11,512,790✔
1198
        v2_unlock_height: u32,
11,512,790✔
1199
        v3_unlock_height: u32,
11,512,790✔
1200
    ) -> Result<(STXBalance, u128), VmExecutionError> {
11,512,790✔
1201
        if self.has_unlockable_tokens_at_burn_block(
11,512,790✔
1202
            burn_block_height,
11,512,790✔
1203
            v1_unlock_height,
11,512,790✔
1204
            v2_unlock_height,
11,512,790✔
1205
            v3_unlock_height,
11,512,790✔
1206
        ) {
1207
            Ok((
1208
                STXBalance::Unlocked {
1209
                    amount: self.get_total_balance()?,
1,152✔
1210
                },
1211
                self.amount_locked(),
1,152✔
1212
            ))
1213
        } else {
1214
            Ok((self.clone(), 0))
11,511,638✔
1215
        }
1216
    }
11,512,790✔
1217

1218
    pub fn get_available_balance_at_burn_block(
19,132,628✔
1219
        &self,
19,132,628✔
1220
        burn_block_height: u64,
19,132,628✔
1221
        v1_unlock_height: u32,
19,132,628✔
1222
        v2_unlock_height: u32,
19,132,628✔
1223
        v3_unlock_height: u32,
19,132,628✔
1224
    ) -> Result<u128, VmExecutionError> {
19,132,628✔
1225
        if self.has_unlockable_tokens_at_burn_block(
19,132,628✔
1226
            burn_block_height,
19,132,628✔
1227
            v1_unlock_height,
19,132,628✔
1228
            v2_unlock_height,
19,132,628✔
1229
            v3_unlock_height,
19,132,628✔
1230
        ) {
1231
            self.get_total_balance()
3,060✔
1232
        } else {
1233
            let out = match self {
19,129,568✔
1234
                STXBalance::Unlocked { amount } => *amount,
19,115,384✔
1235
                STXBalance::LockedPoxOne {
1236
                    amount_unlocked, ..
5,412✔
1237
                } => *amount_unlocked,
5,412✔
1238
                STXBalance::LockedPoxTwo {
1239
                    amount_unlocked, ..
3,564✔
1240
                } => *amount_unlocked,
3,564✔
1241
                STXBalance::LockedPoxThree {
1242
                    amount_unlocked, ..
504✔
1243
                } => *amount_unlocked,
504✔
1244
                STXBalance::LockedPoxFour {
1245
                    amount_unlocked, ..
4,704✔
1246
                } => *amount_unlocked,
4,704✔
1247
            };
1248
            Ok(out)
19,129,568✔
1249
        }
1250
    }
19,132,628✔
1251

1252
    pub fn get_locked_balance_at_burn_block(
74,640✔
1253
        &self,
74,640✔
1254
        burn_block_height: u64,
74,640✔
1255
        v1_unlock_height: u32,
74,640✔
1256
        v2_unlock_height: u32,
74,640✔
1257
        v3_unlock_height: u32,
74,640✔
1258
    ) -> (u128, u64) {
74,640✔
1259
        if self.has_unlockable_tokens_at_burn_block(
74,640✔
1260
            burn_block_height,
74,640✔
1261
            v1_unlock_height,
74,640✔
1262
            v2_unlock_height,
74,640✔
1263
            v3_unlock_height,
74,640✔
1264
        ) {
1265
            (0, 0)
36✔
1266
        } else {
1267
            match self {
74,604✔
1268
                STXBalance::Unlocked { .. } => (0, 0),
74,052✔
1269
                STXBalance::LockedPoxOne {
1270
                    amount_locked,
×
1271
                    unlock_height,
×
1272
                    ..
1273
                } => (*amount_locked, *unlock_height),
×
1274
                STXBalance::LockedPoxTwo {
1275
                    amount_locked,
12✔
1276
                    unlock_height,
12✔
1277
                    ..
1278
                } => (*amount_locked, *unlock_height),
12✔
1279
                STXBalance::LockedPoxThree {
1280
                    amount_locked,
×
1281
                    unlock_height,
×
1282
                    ..
1283
                } => (*amount_locked, *unlock_height),
×
1284
                STXBalance::LockedPoxFour {
1285
                    amount_locked,
540✔
1286
                    unlock_height,
540✔
1287
                    ..
1288
                } => (*amount_locked, *unlock_height),
540✔
1289
            }
1290
        }
1291
    }
74,640✔
1292

1293
    pub fn get_total_balance(&self) -> Result<u128, VmExecutionError> {
115,080✔
1294
        let (unlocked, locked) = match self {
115,080✔
1295
            STXBalance::Unlocked { amount } => (*amount, 0),
110,796✔
1296
            STXBalance::LockedPoxOne {
1297
                amount_unlocked,
1,428✔
1298
                amount_locked,
1,428✔
1299
                ..
1300
            } => (*amount_unlocked, *amount_locked),
1,428✔
1301
            STXBalance::LockedPoxTwo {
1302
                amount_unlocked,
2,052✔
1303
                amount_locked,
2,052✔
1304
                ..
1305
            } => (*amount_unlocked, *amount_locked),
2,052✔
1306
            STXBalance::LockedPoxThree {
1307
                amount_unlocked,
192✔
1308
                amount_locked,
192✔
1309
                ..
1310
            } => (*amount_unlocked, *amount_locked),
192✔
1311
            STXBalance::LockedPoxFour {
1312
                amount_unlocked,
612✔
1313
                amount_locked,
612✔
1314
                ..
1315
            } => (*amount_unlocked, *amount_locked),
612✔
1316
        };
1317
        unlocked
115,080✔
1318
            .checked_add(locked)
115,080✔
1319
            .ok_or_else(|| VmInternalError::Expect("STX overflow".into()).into())
115,080✔
1320
    }
115,080✔
1321

1322
    pub fn was_locked_by_v1(&self) -> bool {
×
1323
        matches!(self, STXBalance::LockedPoxOne { .. })
×
1324
    }
×
1325

1326
    pub fn was_locked_by_v2(&self) -> bool {
756✔
1327
        matches!(self, STXBalance::LockedPoxTwo { .. })
756✔
1328
    }
756✔
1329

1330
    pub fn was_locked_by_v3(&self) -> bool {
×
1331
        matches!(self, STXBalance::LockedPoxThree { .. })
×
1332
    }
×
1333

1334
    pub fn has_locked_tokens_at_burn_block(
223,608✔
1335
        &self,
223,608✔
1336
        burn_block_height: u64,
223,608✔
1337
        v1_unlock_height: u32,
223,608✔
1338
        v2_unlock_height: u32,
223,608✔
1339
        v3_unlock_height: u32,
223,608✔
1340
    ) -> bool {
223,608✔
1341
        match self {
223,608✔
1342
            STXBalance::Unlocked { .. } => false,
221,196✔
1343
            STXBalance::LockedPoxOne {
1344
                amount_locked,
312✔
1345
                unlock_height,
312✔
1346
                ..
1347
            } => {
1348
                if *amount_locked == 0 {
312✔
1349
                    return false;
×
1350
                }
312✔
1351
                // if normally unlockable, return false
1352
                if *unlock_height <= burn_block_height {
312✔
1353
                    return false;
24✔
1354
                }
288✔
1355
                // if unlockable due to Stacks 2.1 early unlock
1356
                if u64::from(v1_unlock_height) <= burn_block_height {
288✔
1357
                    return false;
24✔
1358
                }
264✔
1359
                true
264✔
1360
            }
1361
            STXBalance::LockedPoxTwo {
1362
                amount_locked,
552✔
1363
                unlock_height,
552✔
1364
                ..
1365
            } => {
1366
                if *amount_locked == 0 {
552✔
1367
                    return false;
×
1368
                }
552✔
1369
                if *unlock_height <= burn_block_height {
552✔
1370
                    return false;
×
1371
                }
552✔
1372
                // if unlockable due to Stacks 2.2 early unlock
1373
                if u64::from(v2_unlock_height) <= burn_block_height {
552✔
1374
                    return false;
×
1375
                }
552✔
1376
                true
552✔
1377
            }
1378
            STXBalance::LockedPoxThree {
1379
                amount_locked,
300✔
1380
                unlock_height,
300✔
1381
                ..
1382
            } => {
1383
                if *amount_locked == 0 {
300✔
1384
                    return false;
×
1385
                }
300✔
1386
                if *unlock_height <= burn_block_height {
300✔
1387
                    return false;
×
1388
                }
300✔
1389
                // if unlockable due to Stacks 2.5 early unlock
1390
                if u64::from(v3_unlock_height) <= burn_block_height {
300✔
1391
                    return false;
×
1392
                }
300✔
1393
                true
300✔
1394
            }
1395
            STXBalance::LockedPoxFour {
1396
                amount_locked,
1,248✔
1397
                unlock_height,
1,248✔
1398
                ..
1399
            } => {
1400
                if *amount_locked == 0 {
1,248✔
1401
                    return false;
×
1402
                }
1,248✔
1403
                if *unlock_height <= burn_block_height {
1,248✔
1404
                    return false;
×
1405
                }
1,248✔
1406
                true
1,248✔
1407
            }
1408
        }
1409
    }
223,608✔
1410

1411
    pub fn has_unlockable_tokens_at_burn_block(
30,720,058✔
1412
        &self,
30,720,058✔
1413
        burn_block_height: u64,
30,720,058✔
1414
        v1_unlock_height: u32,
30,720,058✔
1415
        v2_unlock_height: u32,
30,720,058✔
1416
        v3_unlock_height: u32,
30,720,058✔
1417
    ) -> bool {
30,720,058✔
1418
        match self {
30,720,058✔
1419
            STXBalance::Unlocked { .. } => false,
30,667,966✔
1420
            STXBalance::LockedPoxOne {
1421
                amount_locked,
8,424✔
1422
                unlock_height,
8,424✔
1423
                ..
1424
            } => {
1425
                if *amount_locked == 0 {
8,424✔
1426
                    return false;
×
1427
                }
8,424✔
1428
                // if normally unlockable, return true
1429
                if *unlock_height <= burn_block_height {
8,424✔
1430
                    return true;
1,032✔
1431
                }
7,392✔
1432
                // if unlockable due to Stacks 2.1 early unlock
1433
                if u64::from(v1_unlock_height) <= burn_block_height {
7,392✔
1434
                    return true;
396✔
1435
                }
6,996✔
1436
                false
6,996✔
1437
            }
1438
            STXBalance::LockedPoxTwo {
1439
                amount_locked,
22,896✔
1440
                unlock_height,
22,896✔
1441
                ..
1442
            } => {
1443
                if *amount_locked == 0 {
22,896✔
1444
                    return false;
×
1445
                }
22,896✔
1446
                // if normally unlockable, return true
1447
                if *unlock_height <= burn_block_height {
22,896✔
1448
                    return true;
1,308✔
1449
                }
21,588✔
1450
                // if unlockable due to Stacks 2.2 early unlock
1451
                if u64::from(v2_unlock_height) <= burn_block_height {
21,588✔
1452
                    return true;
696✔
1453
                }
20,892✔
1454
                false
20,892✔
1455
            }
1456
            STXBalance::LockedPoxThree {
1457
                amount_locked,
8,844✔
1458
                unlock_height,
8,844✔
1459
                ..
1460
            } => {
1461
                if *amount_locked == 0 {
8,844✔
1462
                    return false;
×
1463
                }
8,844✔
1464
                // if normally unlockable, return true
1465
                if *unlock_height <= burn_block_height {
8,844✔
1466
                    return true;
120✔
1467
                }
8,724✔
1468
                // if unlockable due to Stacks 2.5 early unlock
1469
                if u64::from(v3_unlock_height) <= burn_block_height {
8,724✔
1470
                    return true;
48✔
1471
                }
8,676✔
1472
                false
8,676✔
1473
            }
1474
            STXBalance::LockedPoxFour {
1475
                amount_locked,
11,928✔
1476
                unlock_height,
11,928✔
1477
                ..
1478
            } => {
1479
                if *amount_locked == 0 {
11,928✔
1480
                    return false;
×
1481
                }
11,928✔
1482
                // if normally unlockable, return true
1483
                if *unlock_height <= burn_block_height {
11,928✔
1484
                    return true;
648✔
1485
                }
11,280✔
1486
                false
11,280✔
1487
            }
1488
        }
1489
    }
30,720,058✔
1490

1491
    pub fn can_transfer_at_burn_block(
147,384✔
1492
        &self,
147,384✔
1493
        amount: u128,
147,384✔
1494
        burn_block_height: u64,
147,384✔
1495
        v1_unlock_height: u32,
147,384✔
1496
        v2_unlock_height: u32,
147,384✔
1497
        v3_unlock_height: u32,
147,384✔
1498
    ) -> Result<bool, VmExecutionError> {
147,384✔
1499
        Ok(self.get_available_balance_at_burn_block(
147,384✔
1500
            burn_block_height,
147,384✔
1501
            v1_unlock_height,
147,384✔
1502
            v2_unlock_height,
147,384✔
1503
            v3_unlock_height,
147,384✔
1504
        )? >= amount)
147,384✔
1505
    }
147,384✔
1506
}
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