saito_core/core/consensus/
wallet.rs

1use ahash::{AHashMap, AHashSet};
2use log::{debug, info, trace, warn};
3use std::fmt::Display;
4use std::io::{Error, ErrorKind};
5
6use crate::core::consensus::block::Block;
7use crate::core::consensus::golden_ticket::GoldenTicket;
8use crate::core::consensus::slip::{Slip, SlipType};
9use crate::core::consensus::transaction::{Transaction, TransactionType};
10use crate::core::defs::{
11    BlockId, Currency, PrintForLog, SaitoHash, SaitoPrivateKey, SaitoPublicKey, SaitoSignature,
12    SaitoUTXOSetKey, UTXO_KEY_LENGTH,
13};
14use crate::core::io::interface_io::{InterfaceEvent, InterfaceIO};
15use crate::core::io::network::Network;
16use crate::core::io::storage::Storage;
17use crate::core::process::version::{read_pkg_version, Version};
18use crate::core::util::balance_snapshot::BalanceSnapshot;
19use crate::core::util::crypto::{generate_keys, hash, sign};
20
21pub const WALLET_SIZE: usize = 65;
22
23pub type WalletUpdateStatus = bool;
24pub const WALLET_UPDATED: WalletUpdateStatus = true;
25pub const WALLET_NOT_UPDATED: WalletUpdateStatus = false;
26
27/// The `WalletSlip` stores the essential information needed to track which
28/// slips are spendable and managing them as they move onto and off of the
29/// longest-chain.
30///
31/// Please note that the wallet in this Saito Rust client is intended primarily
32/// to hold the public/private_key and that slip-spending and tracking code is
33/// not coded in a way intended to be robust against chain-reorganizations but
34/// rather for testing of basic functions like transaction creation. Slips that
35/// are spent on one fork are not recaptured on chains, for instance, and once
36/// a slip is spent it is marked as spent.
37///
38#[derive(Clone, Debug, PartialEq)]
39pub struct WalletSlip {
40    pub utxokey: SaitoUTXOSetKey,
41    pub amount: Currency,
42    pub block_id: u64,
43    pub tx_ordinal: u64,
44    pub lc: bool,
45    pub slip_index: u8,
46    pub spent: bool,
47    pub slip_type: SlipType,
48}
49
50#[derive(Clone, Debug, PartialEq)]
51pub struct NFT {
52    pub slip1: SaitoUTXOSetKey,
53    pub slip2: SaitoUTXOSetKey,
54    pub slip3: SaitoUTXOSetKey,
55    pub id: Vec<u8>,
56    pub tx_sig: SaitoSignature,
57}
58
59impl Default for NFT {
60    fn default() -> Self {
61        Self {
62            slip1: [0; UTXO_KEY_LENGTH], // bound
63            slip2: [0; UTXO_KEY_LENGTH], // normal
64            slip3: [0; UTXO_KEY_LENGTH], // bound
65            id: vec![],
66            tx_sig: [0; 64],
67        }
68    }
69}
70
71/// The `Wallet` manages the public and private keypair of the node and holds the
72/// slips that are used to form transactions on the network.
73#[derive(Clone, Debug, PartialEq)]
74pub struct Wallet {
75    pub public_key: SaitoPublicKey,
76    pub private_key: SaitoPrivateKey,
77    pub slips: AHashMap<SaitoUTXOSetKey, WalletSlip>,
78    pub unspent_slips: AHashSet<SaitoUTXOSetKey>,
79    pub staking_slips: AHashSet<SaitoUTXOSetKey>,
80    pub filename: String,
81    pub filepass: String,
82    available_balance: Currency,
83    pub pending_txs: AHashMap<SaitoHash, Transaction>,
84    // TODO : this version should be removed. only added as a temporary hack to allow SLR app version to be easily upgraded in browsers
85    pub wallet_version: Version,
86    pub core_version: Version,
87    pub key_list: Vec<SaitoPublicKey>,
88    pub nfts: Vec<NFT>,
89}
90
91impl Wallet {
92    pub fn new(private_key: SaitoPrivateKey, public_key: SaitoPublicKey) -> Wallet {
93        info!("generating new wallet...");
94        // let (public_key, private_key) = generate_keys();
95
96        Wallet {
97            public_key,
98            private_key,
99            slips: AHashMap::new(),
100            unspent_slips: AHashSet::new(),
101            staking_slips: Default::default(),
102            filename: "default".to_string(),
103            filepass: "password".to_string(),
104            available_balance: 0,
105            pending_txs: Default::default(),
106            wallet_version: Default::default(),
107            core_version: read_pkg_version(),
108            key_list: vec![],
109            nfts: Vec::new(),
110        }
111    }
112
113    pub async fn load(wallet: &mut Wallet, io: &(dyn InterfaceIO + Send + Sync)) {
114        info!("loading wallet...");
115        let result = io.load_wallet(wallet).await;
116        if result.is_err() {
117            warn!("loading wallet failed. saving new wallet");
118            // TODO : check error code
119            io.save_wallet(wallet).await.unwrap();
120        } else {
121            info!("wallet loaded");
122            io.send_interface_event(InterfaceEvent::WalletUpdate());
123        }
124    }
125    pub async fn save(wallet: &mut Wallet, io: &(dyn InterfaceIO + Send + Sync)) {
126        trace!("saving wallet");
127        io.save_wallet(wallet).await.unwrap();
128        trace!("wallet saved");
129    }
130
131    pub async fn reset(
132        &mut self,
133        _storage: &mut Storage,
134        network: Option<&Network>,
135        keep_keys: bool,
136    ) {
137        info!("resetting wallet");
138        if !keep_keys {
139            let keys = generate_keys();
140            self.public_key = keys.0;
141            self.private_key = keys.1;
142        }
143
144        self.pending_txs.clear();
145        self.available_balance = 0;
146        self.slips.clear();
147        self.unspent_slips.clear();
148        self.staking_slips.clear();
149        if let Some(network) = network {
150            network
151                .io_interface
152                .send_interface_event(InterfaceEvent::WalletUpdate());
153        }
154    }
155
156    /// [private_key - 32 bytes]
157    /// [public_key - 33 bytes]
158    pub fn serialize_for_disk(&self) -> Vec<u8> {
159        let mut vbytes: Vec<u8> = vec![];
160
161        vbytes.extend(&self.private_key);
162        vbytes.extend(&self.public_key);
163
164        // TODO : should we write key list here for rust nodes ?
165
166        vbytes
167    }
168
169    /// [private_key - 32 bytes]
170    /// [public_key - 33 bytes]
171    pub fn deserialize_from_disk(&mut self, bytes: &[u8]) {
172        self.private_key = bytes[0..32].try_into().unwrap();
173        self.public_key = bytes[32..65].try_into().unwrap();
174    }
175
176    pub fn on_chain_reorganization(
177        &mut self,
178        block: &Block,
179        lc: bool,
180        genesis_period: BlockId,
181    ) -> WalletUpdateStatus {
182        let mut wallet_changed = WALLET_NOT_UPDATED;
183        debug!("tx count : {}", block.transactions.len());
184        let mut tx_index = 0;
185        if lc {
186            for tx in block.transactions.iter() {
187                //
188                // outputs
189                //
190                let mut i = 0;
191                while i < tx.to.len() {
192                    let output = &tx.to[i];
193                    //
194                    // if the output is a bound slip, then we are expecting
195                    // the 2nd slip to be NORMAL and the 3rd slip to be another
196                    // bound slip.
197                    //
198
199                    //
200                    // is this an NFT ?
201                    //
202                    // note that we do not need to validate that the NFT meets the
203                    // criteria here as we only process blocks that pass validation
204                    // requirements. so we are just doing a superficial check to
205                    // make sure that we will not be "skipping" any normal slips
206                    // before inserting into the wallet
207                    //
208                    let is_this_an_nft = tx.is_nft(&tx.to, i);
209
210                    //
211                    // NFT slips are added to a separate data-storage space, so that
212                    // they will not be affected by the normal workings of the wallet
213                    //
214                    if is_this_an_nft {
215                        let slip1 = &tx.to[i];
216                        let slip2 = &tx.to[i + 1];
217                        let slip3 = &tx.to[i + 2];
218
219                        //
220                        // `nft_id` = UTXO key of the second bound slip (`slip2`)
221                        //
222                        // Use it to:
223                        // 1. Reconstruct the original UTXO via `parse_slip_from_utxokey(nft_id)`,
224                        //    since its public key packs block ID, tx ID, and slip index.
225                        // 2. Transfer the NFT by passing only `nft_id`, which lets us pull
226                        //    the three slips (slip1, linked slip, slip2) for `create_send_bound()`.
227                        //
228                        let nft = NFT {
229                            slip1: slip1.utxoset_key,       // bound
230                            slip2: slip2.utxoset_key,       // normal
231                            slip3: slip3.utxoset_key,       // bound
232                            id: slip2.utxoset_key.to_vec(), // derive NFT id from second Bound slip’s key
233                            tx_sig: tx.signature,
234                        };
235                        self.nfts.push(nft);
236
237                        i += 3;
238                    } else {
239                        //
240                        // normal transaction
241                        //
242                        if output.amount > 0 && output.public_key == self.public_key {
243                            wallet_changed |= WALLET_UPDATED;
244                            self.add_slip(block.id, tx_index, output, true, None);
245                        }
246
247                        i += 1;
248                    }
249                }
250
251                //
252                // inputs
253                //
254                let mut i = 0;
255                while i < tx.from.len() {
256                    let input = &tx.from[i];
257
258                    //
259                    // if the output is a bound slip, then we are expecting
260                    // the 2nd slip to be NORMAL and the 3rd slip to be another
261                    // bound slip.
262                    //
263                    let is_this_an_nft = tx.is_nft(&tx.from, i);
264
265                    //
266                    // NFT slips are removed from the existing NFT storage
267                    // area, as we have received new versions and need to
268                    // update our NFT storage.
269                    //
270                    if is_this_an_nft == true {
271                        if i == 0 && input.slip_type == SlipType::Bound && tx.from.len() >= 3 {
272                            let nft_id = &tx.from[2].utxoset_key;
273
274                            if let Some(pos) =
275                                self.nfts.iter().position(|nft| nft.id == nft_id.to_vec())
276                            {
277                                self.nfts.remove(pos);
278                                debug!(
279                                    "Send-bound NFT input group detected. Removed NFT with id: {:?}",
280                                    nft_id.to_hex()
281                                );
282                            }
283                        }
284
285                        i += 3;
286                    } else {
287                        //
288                        // otherwise we have a normal transaction
289                        //
290
291                        //
292                        // normal slip must be addressed to us
293                        //
294                        if input.public_key == self.public_key {
295                            //
296                            // with non-zero amount
297                            //
298                            if input.amount > 0 {
299                                wallet_changed |= WALLET_UPDATED;
300                                self.delete_slip(input, None);
301                            }
302
303                            //
304                            // also delete from pending
305                            //
306                            if self.delete_pending_transaction(tx) {
307                                wallet_changed |= WALLET_UPDATED;
308                            }
309                        }
310
311                        i += 1;
312                    }
313                }
314
315                if let TransactionType::SPV = tx.transaction_type {
316                    tx_index += tx.txs_replacements as u64;
317                } else {
318                    tx_index += 1;
319                }
320
321                if block.id > genesis_period {
322                    self.remove_old_slips(block.id - genesis_period);
323                }
324            }
325        } else {
326            //
327            // we're unwinding (block not in longest chain),
328            // so outputs → delete, inputs → add, NFT logic reversed
329            //
330            for tx in block.transactions.iter() {
331                //
332                // outputs (reverse of lc’s outputs)
333                //
334                let mut i = 0;
335                while i < tx.to.len() {
336                    let output = &tx.to[i];
337
338                    let is_this_an_nft = tx.is_nft(&tx.to, i);
339
340                    if is_this_an_nft {
341                        //
342                        // remove from NFT storage
343                        //
344                        let slip2 = &tx.to[i + 1];
345                        let nft_id = slip2.utxoset_key.to_vec();
346                        if let Some(pos) = self.nfts.iter().position(|n| n.id == nft_id) {
347                            self.nfts.remove(pos);
348                            debug!(
349                                "Unwound NFT output group, removed id: {:?}",
350                                slip2.utxoset_key.to_hex()
351                            );
352                        }
353                        i += 3;
354                    } else {
355                        //
356                        // normal output → delete
357                        //
358                        if output.amount > 0 && output.public_key == self.public_key {
359                            wallet_changed |= WALLET_UPDATED;
360                            self.delete_slip(output, None);
361                        }
362                        i += 1;
363                    }
364                }
365
366                //
367                // inputs (reverse of lc’s inputs)
368                //
369                let mut i = 0;
370                while i < tx.from.len() {
371                    let input = &tx.from[i];
372
373                    //
374                    // NFT group check
375                    //
376                    let is_this_an_nft = tx.is_nft(&tx.from, i);
377
378                    if is_this_an_nft {
379                        //
380                        // re-add old NFT
381                        //
382                        let slip1 = &tx.from[i];
383                        let slip2 = &tx.from[i + 1];
384                        let slip3 = &tx.from[i + 2];
385
386                        let nft = NFT {
387                            slip1: slip1.utxoset_key,
388                            slip2: slip2.utxoset_key,
389                            slip3: slip3.utxoset_key,
390                            id: slip2.utxoset_key.to_vec(),
391                            tx_sig: tx.signature,
392                        };
393
394                        self.nfts.push(nft);
395
396                        debug!(
397                            "Unwound NFT input group, re‑added id: {:?}",
398                            slip2.utxoset_key.to_hex()
399                        );
400
401                        i += 3;
402                    } else {
403                        //
404                        // normal input → add back
405                        //
406                        if input.amount > 0 && input.public_key == self.public_key {
407                            wallet_changed |= WALLET_UPDATED;
408                            self.add_slip(block.id, tx_index, input, true, None);
409                        }
410                        i += 1;
411                    }
412                }
413
414                //
415                // advance index exactly as in lc
416                //
417                if let TransactionType::SPV = tx.transaction_type {
418                    tx_index += tx.txs_replacements as u64;
419                } else {
420                    tx_index += 1;
421                }
422            }
423        }
424
425        debug!("wallet changed ? {:?}", wallet_changed);
426
427        wallet_changed
428    }
429
430    // removes all slips in block when pruned / deleted
431    pub fn delete_block(&mut self, block: &Block) -> WalletUpdateStatus {
432        let mut wallet_changed = WALLET_NOT_UPDATED;
433        for tx in block.transactions.iter() {
434            for input in tx.from.iter() {
435                if input.public_key == self.public_key {
436                    wallet_changed = WALLET_UPDATED;
437                }
438                self.delete_slip(input, None);
439            }
440            for output in tx.to.iter() {
441                if output.amount > 0 {
442                    self.delete_slip(output, None);
443                }
444            }
445        }
446
447        wallet_changed
448    }
449
450    pub fn remove_old_slips(&mut self, block_id: BlockId) {
451        let mut keys_to_remove = vec![];
452        for (key, slip) in self.slips.iter() {
453            if slip.block_id < block_id {
454                keys_to_remove.push(*key);
455            }
456        }
457
458        for key in keys_to_remove {
459            let slip = Slip::parse_slip_from_utxokey(&key).unwrap();
460            debug!("removing old slip : {}", slip);
461            self.delete_slip(&slip, None);
462        }
463    }
464
465    pub fn add_slip(
466        &mut self,
467        block_id: u64,
468        tx_index: u64,
469        slip: &Slip,
470        lc: bool,
471        network: Option<&Network>,
472    ) {
473        if self.slips.contains_key(&slip.get_utxoset_key()) {
474            debug!("wallet already has slip : {}", slip);
475            return;
476        }
477        let mut wallet_slip = WalletSlip::new();
478        assert_ne!(block_id, 0);
479        wallet_slip.utxokey = slip.get_utxoset_key();
480        wallet_slip.amount = slip.amount;
481        wallet_slip.slip_index = slip.slip_index;
482        wallet_slip.block_id = block_id;
483        wallet_slip.tx_ordinal = tx_index;
484        wallet_slip.lc = lc;
485        wallet_slip.slip_type = slip.slip_type;
486
487        if let SlipType::BlockStake = slip.slip_type {
488            self.staking_slips.insert(wallet_slip.utxokey);
489        } else if let SlipType::Bound = slip.slip_type {
490        } else {
491            self.available_balance += slip.amount;
492            self.unspent_slips.insert(wallet_slip.utxokey);
493        }
494
495        trace!(
496            "adding slip of type : {:?} with value : {:?} to wallet : {:?} \nslip : {}",
497            wallet_slip.slip_type,
498            wallet_slip.amount,
499            wallet_slip.utxokey.to_hex(),
500            Slip::parse_slip_from_utxokey(&wallet_slip.utxokey).unwrap()
501        );
502        self.slips.insert(wallet_slip.utxokey, wallet_slip);
503        if let Some(network) = network {
504            network
505                .io_interface
506                .send_interface_event(InterfaceEvent::WalletUpdate());
507        }
508    }
509
510    pub fn delete_slip(&mut self, slip: &Slip, network: Option<&Network>) {
511        trace!(
512            "deleting slip : {:?} with value : {:?} from wallet",
513            slip.utxoset_key.to_hex(),
514            slip.amount
515        );
516        if let Some(removed_slip) = self.slips.remove(&slip.utxoset_key) {
517            let in_unspent_list = self.unspent_slips.remove(&slip.utxoset_key);
518            if in_unspent_list {
519                self.available_balance -= removed_slip.amount;
520            } else {
521                self.staking_slips.remove(&slip.utxoset_key);
522            }
523            if let Some(network) = network {
524                network
525                    .io_interface
526                    .send_interface_event(InterfaceEvent::WalletUpdate());
527            }
528        }
529    }
530
531    pub fn get_available_balance(&self) -> Currency {
532        self.available_balance
533    }
534
535    pub fn get_unspent_slip_count(&self) -> u64 {
536        self.unspent_slips.len() as u64
537    }
538
539    // the nolan_requested is omitted from the slips created - only the change
540    // address is provided as an output. so make sure that any function calling
541    // this manually creates the output for its desired payment
542    pub fn generate_slips(
543        &mut self,
544        nolan_requested: Currency,
545        network: Option<&Network>,
546        latest_block_id: u64,
547        genesis_period: u64,
548    ) -> (Vec<Slip>, Vec<Slip>) {
549        let mut inputs: Vec<Slip> = Vec::new();
550        let mut nolan_in: Currency = 0;
551        let mut nolan_out: Currency = 0;
552        let my_public_key = self.public_key;
553
554        // grab inputs
555        let mut keys_to_remove = Vec::new();
556        let mut unspent_slips;
557        #[cfg(test)]
558        {
559            // this part is compiled for tests to make sure selected slips are predictable. otherwise we will get random slips from a hashset
560            unspent_slips = self.unspent_slips.iter().collect::<Vec<&SaitoUTXOSetKey>>();
561            unspent_slips.sort_by(|slip, slip2| {
562                let slip = Slip::parse_slip_from_utxokey(slip).unwrap();
563                let slip2 = Slip::parse_slip_from_utxokey(slip2).unwrap();
564                slip.amount.cmp(&slip2.amount)
565            });
566        }
567        #[cfg(not(test))]
568        {
569            unspent_slips = &self.unspent_slips;
570        }
571
572        for key in unspent_slips {
573            let slip = self.slips.get_mut(key).expect("slip should be here");
574
575            // Prevent using slips from blocks earlier than (latest_block_id - (genesis_period-1)
576            if slip.block_id <= latest_block_id.saturating_sub(genesis_period - 1) {
577                debug!("Balance in process of rebroadcasting. Please wait 2 blocks and retry...");
578                continue;
579            }
580
581            if nolan_in >= nolan_requested {
582                break;
583            }
584
585            nolan_in += slip.amount;
586
587            let mut input = Slip::default();
588            input.public_key = my_public_key;
589            input.amount = slip.amount;
590            input.block_id = slip.block_id;
591            input.tx_ordinal = slip.tx_ordinal;
592            input.slip_index = slip.slip_index;
593            input.slip_type = slip.slip_type;
594            inputs.push(input);
595
596            slip.spent = true;
597            self.available_balance -= slip.amount;
598
599            trace!(
600                "marking slip : {:?} with value : {:?} as spent",
601                slip.utxokey.to_hex(),
602                slip.amount
603            );
604            keys_to_remove.push(slip.utxokey);
605        }
606
607        for key in keys_to_remove {
608            self.unspent_slips.remove(&key);
609        }
610
611        // create outputs
612        if nolan_in > nolan_requested {
613            nolan_out = nolan_in - nolan_requested;
614        }
615
616        if nolan_in < nolan_requested {
617            warn!(
618                "Trying to spend more than available. requested : {:?}, available : {:?}",
619                nolan_requested, nolan_in
620            );
621        }
622
623        let mut outputs: Vec<Slip> = Vec::new();
624        // add change address
625        let output = Slip {
626            public_key: my_public_key,
627            amount: nolan_out,
628            ..Default::default()
629        };
630        outputs.push(output);
631
632        // ensure not empty
633        if inputs.is_empty() {
634            let input = Slip {
635                public_key: my_public_key,
636                amount: 0,
637                block_id: 0,
638                tx_ordinal: 0,
639                ..Default::default()
640            };
641            inputs.push(input);
642        }
643        if outputs.is_empty() {
644            let output = Slip {
645                public_key: my_public_key,
646                amount: 0,
647                block_id: 0,
648                tx_ordinal: 0,
649                ..Default::default()
650            };
651            outputs.push(output);
652        }
653        if let Some(network) = network {
654            network
655                .io_interface
656                .send_interface_event(InterfaceEvent::WalletUpdate());
657        }
658
659        (inputs, outputs)
660    }
661
662    pub fn sign(&self, message_bytes: &[u8]) -> SaitoSignature {
663        sign(message_bytes, &self.private_key)
664    }
665
666    pub async fn create_bound_transaction(
667        &mut self,
668        nft_input_amount: Currency,   // amount in input slip creating NFT
669        nft_uuid_block_id: u64,       // block_id in input slip creating NFT
670        nft_uuid_transaction_id: u64, // transaction_id in input slip creating NFT
671        nft_uuid_slip_id: u64,        // slip_id in input slip creating NFT
672        nft_create_deposit_amt: Currency, // AMOUNT to deposit in slip2 (output)
673        nft_data: Vec<u32>,           // DATA field to attach to TX
674        recipient: &SaitoPublicKey,   // receiver
675        network: Option<&Network>,
676        latest_block_id: u64,
677        genesis_period: u64,
678        nft_type: String,
679    ) -> Result<Transaction, Error> {
680        let mut transaction = Transaction::default();
681        transaction.transaction_type = TransactionType::Bound;
682
683        //
684        // all NFTs have a UUID that is created from the UTXO slip that the
685        // creator selects as an input value. this is because each slip is
686        // guaranteed to be unique, which means that the NFT is guaranteed
687        // to be unique -- no-one else will be able to create an NFT with
688        // the same values.
689        //
690        // here we recreate the input slip given the values provided to us
691        // by the application calling this function. this slip is expected
692        // to be valid. if it is not we will error-out.
693        //
694        let input_slip = Slip {
695            public_key: self.public_key,         // Wallet's own public key (creator)
696            amount: nft_input_amount,            // The amount from the provided input UTXO
697            block_id: nft_uuid_block_id,         // Block id from the NFT UUID parameters
698            slip_index: nft_uuid_slip_id as u8,  // Slip index from the NFT UUID parameters
699            tx_ordinal: nft_uuid_transaction_id, // Transaction ordinal from the NFT UUID parameters
700            ..Default::default()
701        };
702
703        //
704        // now we compute the unique UTXO key for the input slip. since every
705        // slip will have a unique UTXO key, this is the UUID for the NFT. by
706        // assigning each NFT the UUID from the slip that is used to create it,
707        // we ensure that each NFT will have an unforgeable ID.
708        //
709        let utxo_key = input_slip.get_utxoset_key(); // Compute the unique UTXO key for the input slip
710
711        //
712        // check that our wallet has this slip available. this check avoids
713        // issues where the slip we are using to create our NFT has already
714        // been spent for some reason. note that this is a safety check for
715        // US rather than a security check for the network, since double-spends
716        // are not possible, so users cannot "re-spend" UTXO to create
717        // duplicate NFTs after their initial NFTs have been created.
718        //
719        if !self.unspent_slips.contains(&utxo_key) {
720            info!("UTXO Key not found: {:?}", utxo_key);
721            return Err(Error::new(
722                ErrorKind::NotFound,
723                format!("UTXO not found: {:?}", utxo_key),
724            ));
725        }
726
727        //
728        // CREATE-NFTs Transactions have the following structure
729        //
730        // input slip #1 -- provides UUID
731        //
732        // output slip #1 -- special NFT slip #1
733        // output slip #2 -- normal slip
734        // output slip #3 -- special NFT slip #2
735        // output slip #4, #5 -- change addresses etc.
736        //
737        // the special NFT slips #1 and #3 are formatted in a way that their "publickey"
738        // does not contain the publickey of the holder, but the information necessary
739        // to recreate the original NFT UUID (i.e. the slip that was originally spent
740        // to create the NFT.
741        //
742        // specifically, note that UTXOs have the following format:
743        //
744        // public_key (33 bytes)
745        // block_id (8 bytes)
746        // transaction_id (8 bytes)
747        // slip_id (1 byte)
748        // amount (8 bytes)
749        // type (1 byte)
750        //
751        // as UTXO are transferred between addresses (and loop around the chain) the
752        // block_id, slip_id and transaction_id all need to be updated. this is why
753        // we require NFTs to have TWO bound slips -- the first provides the
754        // publickey of the original NFT UUID slip. the second re-purposes the publickey
755        // space to provide the original block_id, transaction_id, and slip_id.
756        //
757        // if the NFT is fractional, the "amount" of the NFT is stored in the first
758        // slip. the second slip should always have an amount of 0. the amounts in
759        // these two slips will never be interpreted as L1 SAITO tokens, and cannot
760        // be used to pay network fees, etc.
761        //
762        // these two slips can then move around the network (updating their block,
763        // transaction and slip IDS as they are transferred) without our losing the
764        // ability to recreate the original slip regardless of how many times they
765        // have been transferred, split or merged.
766        //
767
768        //
769        // Output [0] - slip 1
770        //
771        // since we are creating the NFT, we set ourselves as the publickey of the
772        // first output slip, since this will also be the publickey that is in the
773        // input slip that we are spending to create the NFT.
774        //
775        let output_slip1 = Slip {
776            public_key: self.public_key,
777            amount: 1, // temporary
778            slip_type: SlipType::Bound,
779            ..Default::default()
780        };
781
782        //
783        // Output [1] - slip2
784        //
785        // the second slip is a normal output which will need to be spent in order
786        // to move the NFT, and will loop around the chain with it, paying any
787        // rebroadcasting fees and receiving any ATR payout.
788        //
789        // the publickey is set to the address of whoever will own the NFT that we
790        // create here.
791        //
792        let output_slip2 = Slip {
793            public_key: *recipient,
794            amount: nft_create_deposit_amt,
795            ..Default::default()
796        };
797
798        //
799        // Output [2] - slip3
800        //
801        // our third slip is another "bound" NFT slip that will be unspendable unless
802        // moved together with the slip2. since slip1 contains the publickey of the
803        // original UTXO that was spent to create the transaction, this slip re-uses
804        // the publickey data-field to store the other necessary information.
805        //
806        //   - "nft_uuid_data" consists:
807        //
808        //       • 8 bytes of nft_uuid_block_id,
809        //       • 8 bytes of nft_uuid_transaction_id,
810        //       • 1 byte of nft_uuid_slip_id (totaling 17 bytes)
811        //       • 16 bytes padded by remainder of recipient's public key
812        //
813        // the transaction includes a copy of the NFT UUID at the head of the
814        // transaction MSG field. Whenever the NFT is send between addresses
815        // this field is recreated using the two non-normal bound slips that
816        // encode the UTXO that was used to create the NFT originally.
817        //
818        // accordingly, we merge these fields into a new "publickey"
819        //
820
821        let uuid_pubkey = Wallet::create_nft_uuid(&input_slip, &nft_type);
822
823        //
824        // and create the slip with this "artificially-created" publickey
825        //
826        let output_slip3 = Slip {
827            public_key: uuid_pubkey,
828            amount: 0,
829            slip_type: SlipType::Bound,
830            ..Default::default()
831        };
832
833        //
834        // change slip
835        //
836        // we now examine the inputs (and the amount that slip2 contains
837        // to determine if we need to add additional inputs/outputs to provide
838        // more SAITO to the NFT or to capture any surplus amount as a change
839        // address.
840        //
841        let mut additional_input_slips: Vec<Slip> = Vec::new();
842        let mut change_slip_opt: Option<Slip> = None;
843
844        //
845        // too much money? we need a change address
846        //
847        if nft_input_amount > nft_create_deposit_amt {
848            let change_slip_amt = nft_input_amount - nft_create_deposit_amt;
849            change_slip_opt = Some(Slip {
850                public_key: self.public_key, // Return the change to the creator's address
851                amount: change_slip_amt,
852                slip_type: SlipType::Normal,
853                ..Default::default()
854            });
855
856        //
857        // too little money? we need extra inputs + change address
858        //
859        } else if nft_input_amount < nft_create_deposit_amt {
860            let additional_needed = nft_create_deposit_amt - nft_input_amount;
861            let (generated_inputs, generated_outputs) =
862                self.generate_slips(additional_needed, network, latest_block_id, genesis_period);
863            additional_input_slips = generated_inputs;
864            if let Some(first_generated_output) = generated_outputs.into_iter().next() {
865                change_slip_opt = Some(first_generated_output);
866            } else {
867                return Err(Error::new(
868                    ErrorKind::Other,
869                    "Failed to generate change slip via generate_slips",
870                ));
871            }
872        }
873
874        //
875        // now we create the transaction...
876        //
877        // ... add inputs
878        //
879        transaction.add_from_slip(input_slip);
880        for slip in additional_input_slips {
881            transaction.add_from_slip(slip);
882        }
883
884        //
885        // ... add outputs
886        //
887        transaction.add_to_slip(output_slip1);
888        transaction.add_to_slip(output_slip2);
889        transaction.add_to_slip(output_slip3);
890
891        //
892        // ... add change slip
893        //
894        if let Some(change) = change_slip_opt {
895            transaction.add_to_slip(change);
896        }
897
898        //
899        // ... hash and sign
900        //
901        let hash_for_signature: SaitoHash = hash(&transaction.serialize_for_signature());
902        transaction.hash_for_signature = Some(hash_for_signature);
903        transaction.sign(&self.private_key);
904
905        //
906        // ...and return
907        //
908        Ok(transaction)
909    }
910
911    pub async fn create_send_bound_transaction(
912        &mut self,
913        nft_amount: Currency,
914        nft_id: Vec<u8>,
915        nft_data: Vec<u32>,
916        recipient_public_key: &SaitoPublicKey,
917    ) -> Result<Transaction, Error> {
918        //
919        // create our Bound-type transaction for this transfer
920        //
921        let mut transaction = Transaction::default();
922        transaction.transaction_type = TransactionType::Bound;
923
924        //
925        // locate NFT from our repository of NFT slips
926        //
927        let pos = self
928            .nfts
929            .iter()
930            .position(|nft| nft.id == nft_id)
931            .ok_or(Error::new(ErrorKind::NotFound, "NFT not found"))?;
932        let old_nft = self.nfts.remove(pos);
933
934        //
935        //
936        // extract the 3 input slips
937        //
938        // slip #1 - bound
939        // slip #2 - normal
940        // slip #3 - bound
941        //
942        // validation rules require all three slips to move together in order
943        // for an NFT transfer to be considered valid by network rules. lucky
944        // for us, our NFT repository stores this information.
945        //
946        let input_slip1 = Slip::parse_slip_from_utxokey(&old_nft.slip1)?;
947        let input_slip2 = Slip::parse_slip_from_utxokey(&old_nft.slip2)?;
948        let input_slip3 = Slip::parse_slip_from_utxokey(&old_nft.slip3)?;
949
950        //
951        // generate the 3 output slips
952        //
953        let output_slip1 = input_slip1.clone();
954        let mut output_slip2 = input_slip2.clone();
955        let output_slip3 = input_slip3.clone();
956
957        //
958        // update recipient of output_slip2
959        //
960        output_slip2.public_key = recipient_public_key.clone();
961
962        //
963        // add slips
964        //
965        transaction.add_from_slip(input_slip1.clone());
966        transaction.add_from_slip(input_slip2.clone());
967        transaction.add_from_slip(input_slip3.clone());
968        transaction.add_to_slip(output_slip1);
969        transaction.add_to_slip(output_slip2);
970        transaction.add_to_slip(output_slip3);
971
972        //
973        // finalize transaction
974        //
975        let hash_for_signature: SaitoHash = hash(&transaction.serialize_for_signature());
976        transaction.hash_for_signature = Some(hash_for_signature);
977        transaction.sign(&self.private_key);
978        let tx_sig = transaction.signature.clone();
979
980        Ok(transaction)
981    }
982
983    //
984    // Constructs a 33-byte “UUID” for an NFT based on:
985    //   1. The unique coordinates of the UTXO slip used to mint the NFT
986    //      (block ID, transaction ordinal, slip index)
987    //   2. A short, up-to-16‑byte string identifier (`nft_type`) that lets
988    //      us tag or categorize the NFT.
989    //
990    // By embedding both pieces of data in a fixed 33-byte array, we ensure:
991    //  The NFT is globally unique (no two UTXOs share the same coords)
992    //  We carry along an easy-to-read type tag in the final bytes
993    //
994    // Layout of the 33 bytes:
995    //   [0..8)   = block ID (8 bytes, big-endian)
996    //   [8..16)  = tx ordinal (8 bytes, big-endian)
997    //   [16]     = slip index (1 byte)
998    //   [17..33) = `nft_type` string (up to 16 bytes, UTF‑8; padded or truncated)
999    //
1000    pub fn create_nft_uuid(input_slip: &Slip, nft_type: &str) -> [u8; 33] {
1001        //
1002        // 1: Encode the UTXO coordinates (17 bytes total)
1003        // Every slip in the blockchain is uniquely identified by:
1004        //   which block it came from (block_id)
1005        //   the index of the transaction within that block (tx_ordinal)
1006        //   which slip number inside that transaction (slip_index)
1007        //
1008        // By concatenating these, we get a 17-byte “fingerprint” that no
1009        // one else can duplicate unless they somehow create an identical UTXO.
1010        //
1011        let mut uuid = Vec::with_capacity(33);
1012        uuid.extend(&input_slip.block_id.to_be_bytes()); //  8 bytes
1013        uuid.extend(&input_slip.tx_ordinal.to_be_bytes()); //  8 bytes
1014        uuid.push(input_slip.slip_index); //  1 byte
1015
1016        //
1017        // 2: Embed the `nft_type` label into the remaining 16 bytes
1018        // We take the UTF‑8 bytes of the provided `nft_type` string, then:
1019        //  If it’s shorter than 16 bytes, pad with zeros (null bytes).
1020        //  If it’s longer, truncate to the first 16 bytes.
1021        //
1022        // This way, anyone reading the UUID can extract those final bytes
1023        // categorizing the NFT.
1024        //
1025        let mut tbytes = nft_type.as_bytes().to_vec();
1026        if tbytes.len() < 16 {
1027            // Pad with zero bytes to reach exactly 16
1028            tbytes.resize(16, 0);
1029        } else if tbytes.len() > 16 {
1030            // Truncate to the first 16 bytes
1031            tbytes.truncate(16);
1032        }
1033        uuid.extend(&tbytes); // Now uuid.len() == 17 + 16 = 33
1034
1035        //
1036        // 3: At this point, `uuid` is exactly 33 bytes long. We can safely
1037        // turn it into a `[u8; 33]` for use as a Slip.public_key.
1038        //
1039        uuid.try_into().expect("NFT UUID must be exactly 33 bytes")
1040    }
1041
1042    pub async fn create_golden_ticket_transaction(
1043        golden_ticket: GoldenTicket,
1044        public_key: &SaitoPublicKey,
1045        private_key: &SaitoPrivateKey,
1046    ) -> Transaction {
1047        let mut transaction = Transaction::default();
1048
1049        // for now we'll use bincode to de/serialize
1050        transaction.transaction_type = TransactionType::GoldenTicket;
1051        transaction.data = golden_ticket.serialize_for_net();
1052
1053        let mut input1 = Slip::default();
1054        input1.public_key = *public_key;
1055        input1.amount = 0;
1056        input1.block_id = 0;
1057        input1.tx_ordinal = 0;
1058
1059        let mut output1 = Slip::default();
1060        output1.public_key = *public_key;
1061        output1.amount = 0;
1062        output1.block_id = 0;
1063        output1.tx_ordinal = 0;
1064
1065        transaction.add_from_slip(input1);
1066        transaction.add_to_slip(output1);
1067
1068        let hash_for_signature: SaitoHash = hash(&transaction.serialize_for_signature());
1069        transaction.hash_for_signature = Some(hash_for_signature);
1070
1071        transaction.sign(private_key);
1072
1073        transaction
1074    }
1075    pub fn add_to_pending(&mut self, tx: Transaction) {
1076        assert_eq!(tx.from.first().unwrap().public_key, self.public_key);
1077        assert_ne!(tx.transaction_type, TransactionType::GoldenTicket);
1078        assert!(tx.hash_for_signature.is_some());
1079        self.pending_txs.insert(tx.hash_for_signature.unwrap(), tx);
1080    }
1081
1082    pub fn delete_pending_transaction(&mut self, tx: &Transaction) -> bool {
1083        let hash = tx.hash_for_signature.unwrap();
1084        if self.pending_txs.remove(&hash).is_some() {
1085            true
1086        } else {
1087            // debug!("Transaction not found in pending_txs");
1088            false
1089        }
1090    }
1091
1092    pub fn update_from_balance_snapshot(
1093        &mut self,
1094        snapshot: BalanceSnapshot,
1095        network: Option<&Network>,
1096    ) {
1097        // need to reset balance and slips to avoid failing integrity from forks
1098        self.unspent_slips.clear();
1099        self.slips.clear();
1100        self.available_balance = 0;
1101
1102        snapshot.slips.iter().for_each(|slip| {
1103            assert_ne!(slip.utxoset_key, [0; UTXO_KEY_LENGTH]);
1104            let wallet_slip = WalletSlip {
1105                utxokey: slip.utxoset_key,
1106                amount: slip.amount,
1107                block_id: slip.block_id,
1108                tx_ordinal: slip.tx_ordinal,
1109                lc: true,
1110                slip_index: slip.slip_index,
1111                spent: false,
1112                slip_type: slip.slip_type,
1113            };
1114            let result = self.slips.insert(slip.utxoset_key, wallet_slip);
1115            if result.is_none() {
1116                self.unspent_slips.insert(slip.utxoset_key);
1117                self.available_balance += slip.amount;
1118                info!("slip key : {:?} with value : {:?} added to wallet from snapshot for address : {:?}",
1119                    slip.utxoset_key.to_hex(),
1120                    slip.amount,
1121                    slip.public_key.to_base58());
1122            } else {
1123                info!(
1124                    "slip with utxo key : {:?} was already available",
1125                    slip.utxoset_key.to_hex()
1126                );
1127            }
1128        });
1129
1130        if let Some(network) = network {
1131            network
1132                .io_interface
1133                .send_interface_event(InterfaceEvent::WalletUpdate());
1134        }
1135    }
1136    pub fn set_key_list(&mut self, key_list: Vec<SaitoPublicKey>) {
1137        self.key_list = key_list;
1138    }
1139
1140    pub fn create_staking_transaction(
1141        &mut self,
1142        staking_amount: Currency,
1143        latest_unlocked_block_id: BlockId,
1144        last_valid_slips_in_block_id: BlockId,
1145    ) -> Result<Transaction, Error> {
1146        debug!(
1147            "creating staking transaction with amount : {:?}",
1148            staking_amount
1149        );
1150
1151        let mut transaction: Transaction = Transaction {
1152            transaction_type: TransactionType::BlockStake,
1153            ..Default::default()
1154        };
1155
1156        let (inputs, outputs) = self.find_slips_for_staking(
1157            staking_amount,
1158            latest_unlocked_block_id,
1159            last_valid_slips_in_block_id,
1160        )?;
1161
1162        for input in inputs {
1163            transaction.add_from_slip(input);
1164        }
1165        for output in outputs {
1166            transaction.add_to_slip(output);
1167        }
1168
1169        let hash_for_signature: SaitoHash = hash(&transaction.serialize_for_signature());
1170        transaction.hash_for_signature = Some(hash_for_signature);
1171
1172        transaction.sign(&self.private_key);
1173
1174        Ok(transaction)
1175    }
1176
1177    fn find_slips_for_staking(
1178        &mut self,
1179        staking_amount: Currency,
1180        latest_unlocked_block_id: BlockId,
1181        last_valid_slips_in_block_id: BlockId,
1182    ) -> Result<(Vec<Slip>, Vec<Slip>), std::io::Error> {
1183        debug!(
1184            "finding slips for staking : {:?} latest_unblocked_block_id: {:?} staking_slip_count: {:?}",
1185            staking_amount, latest_unlocked_block_id, self.staking_slips.len()
1186        );
1187
1188        let mut selected_staking_inputs: Vec<Slip> = vec![];
1189        let mut collected_amount: Currency = 0;
1190        let mut unlocked_slips_to_remove = vec![];
1191
1192        for key in self.staking_slips.iter() {
1193            let slip = self.slips.get(key).unwrap();
1194            if !slip.is_staking_slip_unlocked(latest_unlocked_block_id) {
1195                // slip cannot be used for staking yet
1196                continue;
1197            }
1198            if slip.block_id < last_valid_slips_in_block_id {
1199                // slip is too old
1200                continue;
1201            }
1202
1203            collected_amount += slip.amount;
1204
1205            unlocked_slips_to_remove.push(*key);
1206            selected_staking_inputs.push(slip.to_slip());
1207
1208            if collected_amount >= staking_amount {
1209                // we have enough staking slips
1210                break;
1211            }
1212        }
1213
1214        let mut should_break_slips = false;
1215        if collected_amount < staking_amount {
1216            debug!("not enough funds in staking slips. searching in normal slips. current_balance : {:?}", self.available_balance);
1217            let required_from_unspent_slips = staking_amount - collected_amount;
1218            let mut collected_from_unspent_slips: Currency = 0;
1219            let mut unspent_slips_to_remove = vec![];
1220
1221            let mut unspent_slips = self.unspent_slips.iter().collect::<Vec<&SaitoUTXOSetKey>>();
1222            unspent_slips.sort_by(|slip, slip2| {
1223                let slip = Slip::parse_slip_from_utxokey(slip).unwrap();
1224                let slip2 = Slip::parse_slip_from_utxokey(slip2).unwrap();
1225                slip2.amount.cmp(&slip.amount)
1226            });
1227            for key in unspent_slips {
1228                let slip = self.slips.get(key).unwrap();
1229
1230                collected_from_unspent_slips += slip.amount;
1231
1232                selected_staking_inputs.push(slip.to_slip());
1233                unspent_slips_to_remove.push(*key);
1234
1235                if collected_from_unspent_slips >= required_from_unspent_slips {
1236                    // if we only have a single slip, and we access it for staking, we need to break it into multiple slips
1237                    should_break_slips = self.unspent_slips.len() == 1;
1238                    break;
1239                }
1240            }
1241
1242            if collected_from_unspent_slips < required_from_unspent_slips {
1243                warn!("couldn't collect enough funds upto requested staking amount. requested: {:?}, collected: {:?} required_from_unspent: {:?}",
1244                    staking_amount,collected_amount,required_from_unspent_slips);
1245                warn!("wallet balance : {:?}", self.available_balance);
1246                return Err(Error::from(ErrorKind::NotFound));
1247            }
1248
1249            for key in unspent_slips_to_remove {
1250                self.unspent_slips.remove(&key);
1251            }
1252            collected_amount += collected_from_unspent_slips;
1253            self.available_balance -= collected_from_unspent_slips;
1254        }
1255
1256        for key in unlocked_slips_to_remove {
1257            self.staking_slips.remove(&key);
1258        }
1259
1260        let mut outputs = vec![];
1261
1262        let mut output: Slip = Default::default();
1263        output.amount = staking_amount;
1264        output.slip_type = SlipType::BlockStake;
1265        output.public_key = self.public_key;
1266        outputs.push(output);
1267
1268        if collected_amount > staking_amount {
1269            let amount = collected_amount - staking_amount;
1270            let mut remainder = amount;
1271            let mut slip_count = 1;
1272            if should_break_slips {
1273                slip_count = 2;
1274            }
1275            {
1276                let mut output: Slip = Default::default();
1277                output.amount = amount / slip_count;
1278                remainder -= output.amount;
1279                output.slip_type = SlipType::Normal;
1280                output.public_key = self.public_key;
1281                outputs.push(output);
1282            }
1283            if remainder > 0 {
1284                let mut output: Slip = Default::default();
1285                output.amount = remainder;
1286                output.slip_type = SlipType::Normal;
1287                output.public_key = self.public_key;
1288                outputs.push(output);
1289            }
1290        }
1291
1292        Ok((selected_staking_inputs, outputs))
1293    }
1294
1295    pub fn get_nft_list(&self) -> &[NFT] {
1296        &self.nfts
1297    }
1298}
1299
1300impl WalletSlip {
1301    #[allow(clippy::new_without_default)]
1302    pub fn new() -> Self {
1303        WalletSlip {
1304            utxokey: [0; UTXO_KEY_LENGTH],
1305            amount: 0,
1306            block_id: 0,
1307            tx_ordinal: 0,
1308            lc: true,
1309            slip_index: 0,
1310            spent: false,
1311            slip_type: SlipType::Normal,
1312        }
1313    }
1314
1315    /// Checks if this staking slip is unlocked and can be used again
1316    ///
1317    /// # Arguments
1318    ///
1319    /// * `latest_unlocked_block_id`: latest block id for which the staking slips are unlocked
1320    ///
1321    /// returns: bool True if this is a staking slip AND can be staked again
1322    ///
1323    /// # Examples
1324    ///
1325    /// ```
1326    ///
1327    /// ```
1328    pub fn is_staking_slip_unlocked(&self, latest_unlocked_block_id: BlockId) -> bool {
1329        matches!(self.slip_type, SlipType::BlockStake) && self.block_id <= latest_unlocked_block_id
1330    }
1331
1332    fn to_slip(&self) -> Slip {
1333        Slip::parse_slip_from_utxokey(&self.utxokey)
1334            .expect("since we already have a wallet slip, utxo key should be valid")
1335    }
1336}
1337
1338impl Display for WalletSlip {
1339    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1340        write!(f, "WalletSlip : utxokey : {:?}, amount : {:?}, block_id : {:?}, tx_ordinal : {:?}, lc : {:?}, slip_index : {:?}, spent : {:?}, slip_type : {:?}", self.utxokey.to_hex(), self.amount, self.block_id, self.tx_ordinal, self.lc, self.slip_index, self.spent, self.slip_type)
1341    }
1342}
1343
1344#[cfg(test)]
1345mod tests {
1346    use crate::core::consensus::wallet::Wallet;
1347    use crate::core::defs::SaitoPublicKey;
1348    use crate::core::io::storage::Storage;
1349    use crate::core::util::crypto::generate_keys;
1350    use crate::core::util::test::test_manager::test::TestManager;
1351
1352    use super::*;
1353
1354    #[test]
1355    fn wallet_new_test() {
1356        let keys = generate_keys();
1357        let wallet = Wallet::new(keys.1, keys.0);
1358        assert_ne!(wallet.public_key, [0; 33]);
1359        assert_ne!(wallet.private_key, [0; 32]);
1360        assert_eq!(wallet.serialize_for_disk().len(), WALLET_SIZE);
1361    }
1362
1363    // tests value transfer to other addresses and verifies the resulting utxo hashmap
1364    #[tokio::test]
1365    #[serial_test::serial]
1366    async fn wallet_transfer_to_address_test() {
1367        let mut t = TestManager::default();
1368        t.initialize(100, 100000).await;
1369
1370        let mut last_param = 120000;
1371
1372        let public_keys = [
1373            "s8oFPjBX97NC2vbm9E5Kd2oHWUShuSTUuZwSB1U4wsPR",
1374            "s9adoFPjBX972vbm9E5Kd2oHWUShuSTUuZwSB1U4wsPR",
1375            "s223oFPjBX97NC2bmE5Kd2oHWUShuSTUuZwSB1U4wsPR",
1376        ];
1377
1378        for &public_key_string in &public_keys {
1379            let public_key = Storage::decode_str(public_key_string).unwrap();
1380            let mut to_public_key: SaitoPublicKey = [0u8; 33];
1381            to_public_key.copy_from_slice(&public_key);
1382            t.transfer_value_to_public_key(to_public_key, 500, last_param)
1383                .await
1384                .unwrap();
1385            let balance_map = t.balance_map().await;
1386            let their_balance = *balance_map.get(&to_public_key).unwrap();
1387            assert_eq!(500, their_balance);
1388
1389            last_param += 120000;
1390        }
1391
1392        let my_balance = t.get_balance().await;
1393
1394        let expected_balance = 10000000 - 500 * public_keys.len() as u64; // 500 is the amount transferred each time
1395        assert_eq!(expected_balance, my_balance);
1396    }
1397
1398    // Test if transfer is possible even with issufficient funds
1399    #[tokio::test]
1400    #[serial_test::serial]
1401    async fn transfer_with_insufficient_funds_failure_test() {
1402        // pretty_env_logger::init();
1403        let mut t = TestManager::default();
1404        t.initialize(100, 200_000_000_000_000).await;
1405        let public_key_string = "s8oFPjBX97NC2vbm9E5Kd2oHWUShuSTUuZwSB1U4wsPR";
1406        let public_key = Storage::decode_str(public_key_string).unwrap();
1407        let mut to_public_key: SaitoPublicKey = [0u8; 33];
1408        to_public_key.copy_from_slice(&public_key);
1409
1410        // Try transferring more than what the wallet contains
1411        let result = t
1412            .transfer_value_to_public_key(to_public_key, 200_000_000_000_000_000, 120000)
1413            .await;
1414        assert!(result.is_err());
1415    }
1416
1417    // tests transfer of exact amount
1418    #[tokio::test]
1419    #[serial_test::serial]
1420    async fn test_transfer_with_exact_funds() {
1421        // pretty_env_logger::init();
1422        let mut t = TestManager::default();
1423        {
1424            let mut blockchain = t.blockchain_lock.write().await;
1425            blockchain.social_stake_requirement = 0;
1426        }
1427        t.initialize(1, 500).await;
1428
1429        let public_key_string = "s8oFPjBX97NC2vbm9E5Kd2oHWUShuSTUuZwSB1U4wsPR";
1430        let public_key = Storage::decode_str(public_key_string).unwrap();
1431        let mut to_public_key: SaitoPublicKey = [0u8; 33];
1432        to_public_key.copy_from_slice(&public_key);
1433
1434        t.transfer_value_to_public_key(to_public_key, 500, 120000)
1435            .await
1436            .unwrap();
1437
1438        let balance_map = t.balance_map().await;
1439
1440        let their_balance = *balance_map.get(&to_public_key).unwrap();
1441        assert_eq!(500, their_balance);
1442        let my_balance = t.get_balance().await;
1443        assert_eq!(0, my_balance);
1444    }
1445
1446    #[test]
1447    fn wallet_serialize_and_deserialize_test() {
1448        let keys = generate_keys();
1449        let wallet1 = Wallet::new(keys.1, keys.0);
1450        let keys = generate_keys();
1451        let mut wallet2 = Wallet::new(keys.1, keys.0);
1452        let serialized = wallet1.serialize_for_disk();
1453        wallet2.deserialize_from_disk(&serialized);
1454        assert_eq!(wallet1, wallet2);
1455    }
1456
1457    #[tokio::test]
1458    #[serial_test::serial]
1459    async fn find_staking_slips_with_normal_slips() {
1460        let t = TestManager::default();
1461
1462        let mut wallet = t.wallet_lock.write().await;
1463
1464        let mut slip = Slip {
1465            public_key: wallet.public_key,
1466            amount: 1_000_000,
1467            slip_type: SlipType::Normal,
1468            ..Slip::default()
1469        };
1470        slip.generate_utxoset_key();
1471        wallet.add_slip(1, 1, &slip, true, Some(&t.network));
1472        assert_eq!(wallet.available_balance, 1_000_000);
1473
1474        let result = wallet.find_slips_for_staking(1_000_000, 1, 0);
1475        assert!(result.is_ok());
1476        let (inputs, outputs) = result.unwrap();
1477        assert_eq!(inputs.len(), 1);
1478        assert_eq!(outputs.len(), 1);
1479        assert_eq!(outputs[0].amount, 1_000_000);
1480        assert_eq!(outputs[0].slip_type, SlipType::BlockStake);
1481
1482        assert_eq!(wallet.staking_slips.len(), 0);
1483        assert_eq!(wallet.unspent_slips.len(), 0);
1484        assert_eq!(wallet.available_balance, 0);
1485
1486        let result = wallet.find_slips_for_staking(1_000, 2, 0);
1487        assert!(result.is_err());
1488
1489        assert_eq!(wallet.staking_slips.len(), 0);
1490        assert_eq!(wallet.unspent_slips.len(), 0);
1491
1492        let mut slip = Slip {
1493            public_key: wallet.public_key,
1494            amount: 1_000,
1495            ..Slip::default()
1496        };
1497        slip.generate_utxoset_key();
1498        wallet.add_slip(1, 2, &slip, true, Some(&t.network));
1499
1500        let result = wallet.find_slips_for_staking(1_000_000, 2, 0);
1501        assert!(result.is_err());
1502        assert_eq!(wallet.staking_slips.len(), 0);
1503        assert_eq!(wallet.unspent_slips.len(), 1);
1504    }
1505    #[tokio::test]
1506    #[serial_test::serial]
1507    async fn find_staking_slips_with_normal_slips_with_extra_funds() {
1508        let t = TestManager::default();
1509
1510        let mut wallet = t.wallet_lock.write().await;
1511
1512        let mut slip = Slip {
1513            public_key: wallet.public_key,
1514            amount: 2_500_000,
1515            slip_type: SlipType::Normal,
1516            ..Slip::default()
1517        };
1518        slip.generate_utxoset_key();
1519        wallet.add_slip(1, 1, &slip, true, Some(&t.network));
1520        assert_eq!(wallet.available_balance, 2_500_000);
1521
1522        let result = wallet.find_slips_for_staking(1_000_000, 1, 0);
1523        assert!(result.is_ok());
1524        let (inputs, outputs) = result.unwrap();
1525        assert_eq!(inputs.len(), 1);
1526        assert_eq!(outputs.len(), 3);
1527        assert_eq!(outputs[0].amount, 1_000_000);
1528        assert_eq!(outputs[0].slip_type, SlipType::BlockStake);
1529
1530        assert_eq!(outputs[1].amount, 750_000);
1531        assert_eq!(outputs[1].slip_type, SlipType::Normal);
1532
1533        assert_eq!(outputs[2].amount, 750_000);
1534        assert_eq!(outputs[2].slip_type, SlipType::Normal);
1535
1536        assert_eq!(wallet.staking_slips.len(), 0);
1537        assert_eq!(wallet.unspent_slips.len(), 0);
1538        assert_eq!(wallet.available_balance, 0);
1539
1540        let result = wallet.find_slips_for_staking(1_000, 2, 0);
1541        assert!(result.is_err());
1542
1543        assert_eq!(wallet.staking_slips.len(), 0);
1544        assert_eq!(wallet.unspent_slips.len(), 0);
1545
1546        let mut slip = Slip {
1547            public_key: wallet.public_key,
1548            amount: 1_000,
1549            ..Slip::default()
1550        };
1551        slip.generate_utxoset_key();
1552        wallet.add_slip(1, 2, &slip, true, Some(&t.network));
1553
1554        let result = wallet.find_slips_for_staking(1_000_000, 2, 0);
1555        assert!(result.is_err());
1556        assert_eq!(wallet.staking_slips.len(), 0);
1557        assert_eq!(wallet.unspent_slips.len(), 1);
1558    }
1559
1560    #[tokio::test]
1561    #[serial_test::serial]
1562    async fn find_staking_slips_with_staking_slips() {
1563        let t = TestManager::default();
1564
1565        let mut wallet = t.wallet_lock.write().await;
1566
1567        let mut slip = Slip {
1568            public_key: wallet.public_key,
1569            amount: 1_000_000,
1570            slip_type: SlipType::BlockStake,
1571            ..Slip::default()
1572        };
1573        slip.generate_utxoset_key();
1574        wallet.add_slip(1, 1, &slip, true, Some(&t.network));
1575        assert_eq!(wallet.available_balance, 0);
1576
1577        let result = wallet.find_slips_for_staking(1_000_000, 1, 0);
1578        assert!(result.is_ok());
1579        let (inputs, outputs) = result.unwrap();
1580        assert_eq!(inputs.len(), 1);
1581        assert_eq!(outputs[0].amount, 1_000_000);
1582        assert_eq!(outputs[0].slip_type, SlipType::BlockStake);
1583
1584        assert_eq!(wallet.staking_slips.len(), 0);
1585        assert_eq!(wallet.unspent_slips.len(), 0);
1586        assert_eq!(wallet.available_balance, 0);
1587
1588        let result = wallet.find_slips_for_staking(1_000, 2, 0);
1589        assert!(result.is_err());
1590
1591        assert_eq!(wallet.staking_slips.len(), 0);
1592        assert_eq!(wallet.unspent_slips.len(), 0);
1593    }
1594
1595    // #[tokio::test]
1596    // #[serial_test::serial]
1597    // async fn save_and_restore_wallet_test() {
1598    //     info!("current dir = {:?}", std::env::current_dir().unwrap());
1599    //
1600    //     let _t = TestManager::new();
1601    //
1602    //     let keys = generate_keys();
1603    //     let mut wallet = Wallet::new(keys.1, keys.0);
1604    //     let public_key1 = wallet.public_key.clone();
1605    //     let private_key1 = wallet.private_key.clone();
1606    //
1607    //     let mut storage = Storage {
1608    //         io_interface: Box::new(TestIOHandler::new()),
1609    //     };
1610    //     wallet.save(&mut storage).await;
1611    //
1612    //     let keys = generate_keys();
1613    //     wallet = Wallet::new(keys.1, keys.0);
1614    //
1615    //     assert_ne!(wallet.public_key, public_key1);
1616    //     assert_ne!(wallet.private_key, private_key1);
1617    //
1618    //     wallet.load(&mut storage).await;
1619    //
1620    //     assert_eq!(wallet.public_key, public_key1);
1621    //     assert_eq!(wallet.private_key, private_key1);
1622    // }
1623}