forked from jl777/SuperNET
-
Notifications
You must be signed in to change notification settings - Fork 94
/
utxo.rs
2049 lines (1817 loc) · 75.6 KB
/
utxo.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/******************************************************************************
* Copyright © 2023 Pampex LTD and TillyHK LTD *
* *
* See the CONTRIBUTOR-LICENSE-AGREEMENT, COPYING, LICENSE-COPYRIGHT-NOTICE *
* and DEVELOPER-CERTIFICATE-OF-ORIGIN files in the LEGAL directory in *
* the top-level directory of this distribution for the individual copyright *
* holder information and the developer policies on copyright and licensing. *
* *
* Unless otherwise agreed in a custom licensing agreement, no part of the *
* Komodo DeFi Framework software, including this file may be copied, modified, propagated*
* or distributed except according to the terms contained in the *
* LICENSE-COPYRIGHT-NOTICE file. *
* *
* Removal or modification of this copyright notice is prohibited. *
* *
******************************************************************************/
//
// utxo.rs
// marketmaker
//
// Copyright © 2023 Pampex LTD and TillyHK LTD. All rights reserved.
//
pub mod bch;
pub(crate) mod bchd_grpc;
#[allow(clippy::all)]
#[rustfmt::skip]
#[path = "utxo/pb.rs"]
mod bchd_pb;
pub mod qtum;
pub mod rpc_clients;
pub mod slp;
pub mod spv;
pub mod swap_proto_v2_scripts;
pub mod utxo_balance_events;
pub mod utxo_block_header_storage;
pub mod utxo_builder;
pub mod utxo_common;
pub mod utxo_standard;
pub mod utxo_tx_history_v2;
pub mod utxo_withdraw;
use async_trait::async_trait;
#[cfg(not(target_arch = "wasm32"))]
use bitcoin::network::constants::Network as BitcoinNetwork;
pub use bitcrypto::{dhash160, sha256, ChecksumType};
pub use chain::Transaction as UtxoTx;
use chain::{OutPoint, TransactionOutput, TxHashAlgo};
use common::executor::abortable_queue::AbortableQueue;
#[cfg(not(target_arch = "wasm32"))]
use common::first_char_to_upper;
use common::jsonrpc_client::JsonRpcError;
use common::log::LogOnError;
use common::{now_sec, now_sec_u32};
use crypto::{Bip32DerPathOps, Bip32Error, Bip44Chain, ChildNumber, DerivationPath, Secp256k1ExtendedPublicKey,
StandardHDCoinAddress, StandardHDPathError, StandardHDPathToAccount, StandardHDPathToCoin};
use derive_more::Display;
#[cfg(not(target_arch = "wasm32"))] use dirs::home_dir;
use futures::channel::mpsc::{Receiver as AsyncReceiver, Sender as AsyncSender, UnboundedReceiver, UnboundedSender};
use futures::compat::Future01CompatExt;
use futures::lock::{Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard};
use futures01::Future;
use keys::bytes::Bytes;
use keys::NetworkAddressPrefixes;
use keys::Signature;
pub use keys::{Address, AddressBuilder, AddressFormat as UtxoAddressFormat, AddressHashEnum, AddressPrefix,
AddressScriptType, KeyPair, LegacyAddress, Private, Public, Secret};
#[cfg(not(target_arch = "wasm32"))]
use lightning_invoice::Currency as LightningCurrency;
use mm2_core::mm_ctx::{MmArc, MmWeak};
use mm2_err_handle::prelude::*;
use mm2_metrics::MetricsArc;
use mm2_number::BigDecimal;
use mm2_rpc::data::legacy::UtxoMergeParams;
#[cfg(test)] use mocktopus::macros::*;
use num_traits::ToPrimitive;
use primitives::hash::{H160, H256, H264};
use rpc::v1::types::{Bytes as BytesJson, Transaction as RpcTransaction, H256 as H256Json};
use script::{Builder, Script, SignatureVersion, TransactionInputSigner};
use secp256k1::Signature as SecpSignature;
use serde_json::{self as json, Value as Json};
use serialization::{deserialize, serialize, serialize_with_flags, Error as SerError, SERIALIZE_TRANSACTION_WITNESS};
use spv_validation::conf::SPVConf;
use spv_validation::helpers_validation::SPVError;
use spv_validation::storage::BlockHeaderStorageError;
use std::array::TryFromSliceError;
use std::collections::{HashMap, HashSet};
use std::convert::TryInto;
use std::hash::Hash;
use std::num::{NonZeroU64, TryFromIntError};
use std::ops::Deref;
#[cfg(not(target_arch = "wasm32"))]
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::atomic::{AtomicBool, AtomicU64};
use std::sync::{Arc, Mutex, Weak};
use utxo_builder::UtxoConfBuilder;
use utxo_common::{big_decimal_from_sat, UtxoTxBuilder};
use utxo_signer::with_key_pair::sign_tx;
use utxo_signer::{TxProvider, TxProviderError, UtxoSignTxError, UtxoSignTxResult};
use self::rpc_clients::{electrum_script_hash, ElectrumClient, ElectrumRpcRequest, EstimateFeeMethod, EstimateFeeMode,
NativeClient, UnspentInfo, UnspentMap, UtxoRpcClientEnum, UtxoRpcError, UtxoRpcFut,
UtxoRpcResult};
use super::{big_decimal_from_sat_unsigned, BalanceError, BalanceFut, BalanceResult, CoinBalance, CoinFutSpawner,
CoinsContext, DerivationMethod, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, KmdRewardsDetails,
MarketCoinOps, MmCoin, NumConversError, NumConversResult, PrivKeyActivationPolicy, PrivKeyPolicy,
PrivKeyPolicyNotAllowed, RawTransactionFut, RpcTransportEventHandler, RpcTransportEventHandlerShared,
TradeFee, TradePreimageError, TradePreimageFut, TradePreimageResult, Transaction, TransactionDetails,
TransactionEnum, TransactionErr, UnexpectedDerivationMethod, VerificationError, WithdrawError,
WithdrawRequest};
use crate::coin_balance::{EnableCoinScanPolicy, EnabledCoinBalanceParams, HDAddressBalanceScanner};
use crate::hd_wallet::{HDAccountOps, HDAccountsMutex, HDAddress, HDAddressId, HDWalletCoinOps, HDWalletOps,
InvalidBip44ChainError};
use crate::hd_wallet_storage::{HDAccountStorageItem, HDWalletCoinStorage, HDWalletStorageError, HDWalletStorageResult};
use crate::utxo::tx_cache::UtxoVerboseCacheShared;
use crate::{CoinAssocTypes, ToBytes};
pub mod tx_cache;
#[cfg(any(test, target_arch = "wasm32"))]
pub mod utxo_common_tests;
#[cfg(test)] pub mod utxo_tests;
#[cfg(target_arch = "wasm32")] pub mod utxo_wasm_tests;
const KILO_BYTE: u64 = 1000;
/// https://bitcoin.stackexchange.com/a/77192
const MAX_DER_SIGNATURE_LEN: usize = 72;
const COMPRESSED_PUBKEY_LEN: usize = 33;
const P2PKH_OUTPUT_LEN: u64 = 34;
const MATURE_CONFIRMATIONS_DEFAULT: u32 = 100;
const UTXO_DUST_AMOUNT: u64 = 1000;
/// Block count for KMD median time past calculation
///
/// # Safety
/// 11 > 0
const KMD_MTP_BLOCK_COUNT: NonZeroU64 = unsafe { NonZeroU64::new_unchecked(11u64) };
const DEFAULT_DYNAMIC_FEE_VOLATILITY_PERCENT: f64 = 0.5;
const DEFAULT_GAP_LIMIT: u32 = 20;
pub type GenerateTxResult = Result<(TransactionInputSigner, AdditionalTxData), MmError<GenerateTxError>>;
pub type HistoryUtxoTxMap = HashMap<H256Json, HistoryUtxoTx>;
pub type MatureUnspentMap = HashMap<Address, MatureUnspentList>;
pub type RecentlySpentOutPointsGuard<'a> = AsyncMutexGuard<'a, RecentlySpentOutPoints>;
pub type UtxoHDAddress = HDAddress<Address, Public>;
pub enum ScripthashNotification {
Triggered(String),
SubscribeToAddresses(HashSet<Address>),
RefreshSubscriptions,
}
pub type ScripthashNotificationSender = Option<UnboundedSender<ScripthashNotification>>;
type ScripthashNotificationHandler = Option<Arc<AsyncMutex<UnboundedReceiver<ScripthashNotification>>>>;
#[cfg(windows)]
#[cfg(not(target_arch = "wasm32"))]
fn get_special_folder_path() -> PathBuf {
use libc::c_char;
use std::ffi::CStr;
use std::mem::zeroed;
use std::ptr::null_mut;
use winapi::shared::minwindef::MAX_PATH;
use winapi::um::shlobj::SHGetSpecialFolderPathA;
use winapi::um::shlobj::CSIDL_APPDATA;
let mut buf: [c_char; MAX_PATH + 1] = unsafe { zeroed() };
// https://docs.microsoft.com/en-us/windows/desktop/api/shlobj_core/nf-shlobj_core-shgetspecialfolderpatha
let rc = unsafe { SHGetSpecialFolderPathA(null_mut(), buf.as_mut_ptr(), CSIDL_APPDATA, 1) };
if rc != 1 {
panic!("!SHGetSpecialFolderPathA")
}
Path::new(unsafe { CStr::from_ptr(buf.as_ptr()) }.to_str().unwrap()).to_path_buf()
}
#[cfg(not(windows))]
#[cfg(not(target_arch = "wasm32"))]
fn get_special_folder_path() -> PathBuf { panic!("!windows") }
impl Transaction for UtxoTx {
fn tx_hex(&self) -> Vec<u8> {
if self.has_witness() {
serialize_with_flags(self, SERIALIZE_TRANSACTION_WITNESS).into()
} else {
serialize(self).into()
}
}
fn tx_hash(&self) -> BytesJson { self.hash().reversed().to_vec().into() }
}
impl From<JsonRpcError> for BalanceError {
fn from(e: JsonRpcError) -> Self { BalanceError::Transport(e.to_string()) }
}
impl From<UtxoRpcError> for BalanceError {
fn from(e: UtxoRpcError) -> Self {
match e {
UtxoRpcError::Internal(desc) => BalanceError::Internal(desc),
_ => BalanceError::Transport(e.to_string()),
}
}
}
impl From<keys::Error> for BalanceError {
fn from(e: keys::Error) -> Self { BalanceError::Internal(e.to_string()) }
}
impl From<UtxoRpcError> for WithdrawError {
fn from(e: UtxoRpcError) -> Self {
match e {
UtxoRpcError::Transport(transport) | UtxoRpcError::ResponseParseError(transport) => {
WithdrawError::Transport(transport.to_string())
},
UtxoRpcError::InvalidResponse(resp) => WithdrawError::Transport(resp),
UtxoRpcError::Internal(internal) => WithdrawError::InternalError(internal),
}
}
}
impl From<JsonRpcError> for TradePreimageError {
fn from(e: JsonRpcError) -> Self { TradePreimageError::Transport(e.to_string()) }
}
impl From<UtxoRpcError> for TradePreimageError {
fn from(e: UtxoRpcError) -> Self {
match e {
UtxoRpcError::Transport(transport) | UtxoRpcError::ResponseParseError(transport) => {
TradePreimageError::Transport(transport.to_string())
},
UtxoRpcError::InvalidResponse(resp) => TradePreimageError::Transport(resp),
UtxoRpcError::Internal(internal) => TradePreimageError::InternalError(internal),
}
}
}
impl From<UtxoRpcError> for TxProviderError {
fn from(rpc: UtxoRpcError) -> Self {
match rpc {
resp @ UtxoRpcError::ResponseParseError(_) | resp @ UtxoRpcError::InvalidResponse(_) => {
TxProviderError::InvalidResponse(resp.to_string())
},
UtxoRpcError::Transport(transport) => TxProviderError::Transport(transport.to_string()),
UtxoRpcError::Internal(internal) => TxProviderError::Internal(internal),
}
}
}
impl From<StandardHDPathError> for HDWalletStorageError {
fn from(e: StandardHDPathError) -> Self { HDWalletStorageError::ErrorDeserializing(e.to_string()) }
}
impl From<Bip32Error> for HDWalletStorageError {
fn from(e: Bip32Error) -> Self { HDWalletStorageError::ErrorDeserializing(e.to_string()) }
}
#[async_trait]
impl TxProvider for UtxoRpcClientEnum {
async fn get_rpc_transaction(&self, tx_hash: &H256Json) -> Result<RpcTransaction, MmError<TxProviderError>> {
Ok(self.get_verbose_transaction(tx_hash).compat().await?)
}
}
/// The `UtxoTx` with the block height transaction mined in.
pub struct HistoryUtxoTx {
pub height: Option<u64>,
pub tx: UtxoTx,
}
/// Additional transaction data that can't be easily got from raw transaction without calling
/// additional RPC methods, e.g. to get input amount we need to request all previous transactions
/// and check output values
#[derive(Debug)]
pub struct AdditionalTxData {
pub received_by_me: u64,
pub spent_by_me: u64,
pub fee_amount: u64,
pub unused_change: u64,
pub kmd_rewards: Option<KmdRewardsDetails>,
}
/// The fee set from coins config
#[derive(Debug)]
pub enum TxFee {
/// Tell the coin that it should request the fee from daemon RPC and calculate it relying on tx size
Dynamic(EstimateFeeMethod),
/// Tell the coin that it has fixed tx fee per kb.
FixedPerKb(u64),
}
/// The actual "runtime" fee that is received from RPC in case of dynamic calculation
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum ActualTxFee {
/// fee amount per Kbyte received from coin RPC
Dynamic(u64),
/// Use specified amount per each 1 kb of transaction and also per each output less than amount.
/// Used by DOGE, but more coins might support it too.
FixedPerKb(u64),
}
/// Fee policy applied on transaction creation
pub enum FeePolicy {
/// Send the exact amount specified in output(s), fee is added to spent input amount
SendExact,
/// Contains the index of output from which fee should be deducted
DeductFromOutput(usize),
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct CachedUnspentInfo {
pub outpoint: OutPoint,
pub value: u64,
}
impl From<UnspentInfo> for CachedUnspentInfo {
fn from(unspent: UnspentInfo) -> CachedUnspentInfo {
CachedUnspentInfo {
outpoint: unspent.outpoint,
value: unspent.value,
}
}
}
impl From<CachedUnspentInfo> for UnspentInfo {
fn from(cached: CachedUnspentInfo) -> UnspentInfo {
UnspentInfo {
outpoint: cached.outpoint,
value: cached.value,
height: None,
}
}
}
/// The cache of recently send transactions used to track the spent UTXOs and replace them with new outputs
/// The daemon needs some time to update the listunspent list for address which makes it return already spent UTXOs
/// This cache helps to prevent UTXO reuse in such cases
pub struct RecentlySpentOutPoints {
/// Maps CachedUnspentInfo A to a set of CachedUnspentInfo which `spent` A
input_to_output_map: HashMap<CachedUnspentInfo, HashSet<CachedUnspentInfo>>,
/// Maps CachedUnspentInfo A to a set of CachedUnspentInfo that `were spent by` A
output_to_input_map: HashMap<CachedUnspentInfo, HashSet<CachedUnspentInfo>>,
/// Cache includes only outputs having script_pubkey == for_script_pubkey
for_script_pubkey: Bytes,
}
impl RecentlySpentOutPoints {
fn new(for_script_pubkey: Bytes) -> Self {
RecentlySpentOutPoints {
input_to_output_map: HashMap::new(),
output_to_input_map: HashMap::new(),
for_script_pubkey,
}
}
pub fn add_spent(&mut self, inputs: Vec<UnspentInfo>, spend_tx_hash: H256, outputs: Vec<TransactionOutput>) {
let inputs: HashSet<_> = inputs.into_iter().map(From::from).collect();
let to_replace: HashSet<_> = outputs
.iter()
.enumerate()
.filter_map(|(index, output)| {
if output.script_pubkey == self.for_script_pubkey {
Some(CachedUnspentInfo {
outpoint: OutPoint {
hash: spend_tx_hash,
index: index as u32,
},
value: output.value,
})
} else {
None
}
})
.collect();
let mut prev_inputs_spent = HashSet::new();
// check if inputs are already in spending cached chain
for input in &inputs {
if let Some(prev_inputs) = self.output_to_input_map.get(input) {
for prev_input in prev_inputs {
if let Some(outputs) = self.input_to_output_map.get_mut(prev_input) {
prev_inputs_spent.insert(prev_input.clone());
outputs.remove(input);
for replace in &to_replace {
outputs.insert(replace.clone());
}
}
}
}
}
prev_inputs_spent.extend(inputs.clone());
for output in &to_replace {
self.output_to_input_map
.insert(output.clone(), prev_inputs_spent.clone());
}
for input in inputs {
self.input_to_output_map.insert(input, to_replace.clone());
}
}
pub fn replace_spent_outputs_with_cache(&self, mut outputs: HashSet<UnspentInfo>) -> HashSet<UnspentInfo> {
let mut replacement_unspents = HashSet::new();
outputs.retain(|unspent| {
let outs = self.input_to_output_map.get(&unspent.clone().into());
match outs {
Some(outs) => {
for out in outs.iter() {
if !replacement_unspents.contains(out) {
replacement_unspents.insert(out.clone());
}
}
false
},
None => true,
}
});
if replacement_unspents.is_empty() {
return outputs;
}
outputs.extend(replacement_unspents.into_iter().map(From::from));
self.replace_spent_outputs_with_cache(outputs)
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum BlockchainNetwork {
#[serde(rename = "mainnet")]
Mainnet,
#[serde(rename = "testnet")]
Testnet,
#[serde(rename = "regtest")]
Regtest,
}
#[cfg(not(target_arch = "wasm32"))]
impl From<BlockchainNetwork> for BitcoinNetwork {
fn from(network: BlockchainNetwork) -> Self {
match network {
BlockchainNetwork::Mainnet => BitcoinNetwork::Bitcoin,
BlockchainNetwork::Testnet => BitcoinNetwork::Testnet,
BlockchainNetwork::Regtest => BitcoinNetwork::Regtest,
}
}
}
#[cfg(not(target_arch = "wasm32"))]
impl From<BlockchainNetwork> for LightningCurrency {
fn from(network: BlockchainNetwork) -> Self {
match network {
BlockchainNetwork::Mainnet => LightningCurrency::Bitcoin,
BlockchainNetwork::Testnet => LightningCurrency::BitcoinTestnet,
BlockchainNetwork::Regtest => LightningCurrency::Regtest,
}
}
}
pub enum UtxoSyncStatus {
SyncingBlockHeaders {
current_scanned_block: u64,
last_block: u64,
},
TemporaryError(String),
PermanentError(String),
Finished {
block_number: u64,
},
}
#[derive(Clone)]
pub struct UtxoSyncStatusLoopHandle(AsyncSender<UtxoSyncStatus>);
impl UtxoSyncStatusLoopHandle {
pub fn new(sync_status_notifier: AsyncSender<UtxoSyncStatus>) -> Self {
UtxoSyncStatusLoopHandle(sync_status_notifier)
}
pub fn notify_blocks_headers_sync_status(&mut self, current_scanned_block: u64, last_block: u64) {
self.0
.try_send(UtxoSyncStatus::SyncingBlockHeaders {
current_scanned_block,
last_block,
})
.debug_log_with_msg("No one seems interested in UtxoSyncStatus");
}
pub fn notify_on_temp_error(&mut self, error: impl ToString) {
self.0
.try_send(UtxoSyncStatus::TemporaryError(error.to_string()))
.debug_log_with_msg("No one seems interested in UtxoSyncStatus");
}
pub fn notify_on_permanent_error(&mut self, error: impl ToString) {
self.0
.try_send(UtxoSyncStatus::PermanentError(error.to_string()))
.debug_log_with_msg("No one seems interested in UtxoSyncStatus");
}
pub fn notify_sync_finished(&mut self, block_number: u64) {
self.0
.try_send(UtxoSyncStatus::Finished { block_number })
.debug_log_with_msg("No one seems interested in UtxoSyncStatus");
}
}
#[derive(Debug)]
pub struct UtxoCoinConf {
pub ticker: String,
/// https://en.bitcoin.it/wiki/List_of_address_prefixes
/// https://github.com/jl777/coins/blob/master/coins
pub wif_prefix: u8,
pub address_prefixes: NetworkAddressPrefixes,
pub sign_message_prefix: Option<String>,
// https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#Segwit_address_format
pub bech32_hrp: Option<String>,
/// True if coins uses Proof of Stake consensus algo
/// Proof of Work is expected by default
/// https://en.bitcoin.it/wiki/Proof_of_Stake
/// https://en.bitcoin.it/wiki/Proof_of_work
/// The actual meaning of this is nTime field is used in transaction
pub is_pos: bool,
/// Defines if coin uses PoSV transaction format (Reddcoin, Potcoin, et al).
/// n_time field is appended to end of transaction
pub is_posv: bool,
/// Special field for Zcash and it's forks
/// Defines if Overwinter network upgrade was activated
/// https://z.cash/upgrade/overwinter/
pub overwintered: bool,
/// The tx version used to detect the transaction ser/de/signing algo
/// For now it's mostly used for Zcash and forks because they changed the algo in
/// Overwinter and then Sapling upgrades
/// https://github.com/zcash/zips/blob/master/zip-0243.rst
pub tx_version: i32,
/// Defines if Segwit is enabled for this coin.
/// https://en.bitcoin.it/wiki/Segregated_Witness
pub segwit: bool,
/// Does coin require transactions to be notarized to be considered as confirmed?
/// https://komodoplatform.com/security-delayed-proof-of-work-dpow/
pub requires_notarization: AtomicBool,
/// The address format indicates the default address format from coin config file
pub default_address_format: UtxoAddressFormat,
/// Is current coin KMD asset chain?
/// https://komodoplatform.atlassian.net/wiki/spaces/KPSD/pages/71729160/What+is+a+Parallel+Chain+Asset+Chain
pub asset_chain: bool,
/// Dynamic transaction fee volatility in percent. The value is used to predict a possible increase in dynamic fee.
pub tx_fee_volatility_percent: f64,
/// Transaction version group id for Zcash transactions since Overwinter: https://github.com/zcash/zips/blob/master/zip-0202.rst
pub version_group_id: u32,
/// Consensus branch id for Zcash transactions since Overwinter: https://github.com/zcash/zcash/blob/master/src/consensus/upgrades.cpp#L11
/// used in transaction sig hash calculation
pub consensus_branch_id: u32,
/// Defines if coin uses Zcash transaction format
pub zcash: bool,
/// Address and privkey checksum type
pub checksum_type: ChecksumType,
/// Fork id used in sighash
pub fork_id: u32,
/// Signature version
pub signature_version: SignatureVersion,
pub required_confirmations: AtomicU64,
/// if set to true MM2 will check whether calculated fee is lower than relay fee and use
/// relay fee amount instead of calculated
/// https://github.com/KomodoPlatform/atomicDEX-API/issues/617
pub force_min_relay_fee: bool,
/// Block count for median time past calculation
pub mtp_block_count: NonZeroU64,
pub estimate_fee_mode: Option<EstimateFeeMode>,
/// The minimum number of confirmations at which a transaction is considered mature
pub mature_confirmations: u32,
/// The number of blocks used for estimate_fee/estimate_smart_fee RPC calls
pub estimate_fee_blocks: u32,
/// The name of the coin with which Trezor wallet associates this asset.
pub trezor_coin: Option<String>,
/// Whether to verify swaps and lightning transactions using spv or not. When enabled, block headers will be retrieved, verified according
/// to [`SPVConf::validation_params`] and stored in the DB. Can be false if the coin's RPC server is trusted.
pub spv_conf: Option<SPVConf>,
/// Derivation path of the coin.
/// This derivation path consists of `purpose` and `coin_type` only
/// where the full `BIP44` address has the following structure:
/// `m/purpose'/coin_type'/account'/change/address_index`.
pub derivation_path: Option<StandardHDPathToCoin>,
/// The average time in seconds needed to mine a new block for this coin.
pub avg_blocktime: Option<u64>,
}
pub struct UtxoCoinFields {
/// UTXO coin config
pub conf: UtxoCoinConf,
/// Default decimals amount is 8 (BTC and almost all other UTXO coins)
/// But there are forks which have different decimals:
/// Peercoin has 6
/// Emercoin has 6
/// Bitcoin Diamond has 7
pub decimals: u8,
pub tx_fee: TxFee,
/// Minimum transaction value at which the value is not less than fee
pub dust_amount: u64,
/// RPC client
pub rpc_client: UtxoRpcClientEnum,
/// Either ECDSA key pair or a Hardware Wallet info.
pub priv_key_policy: PrivKeyPolicy<KeyPair>,
/// Either an Iguana address or an info about last derived account/address.
pub derivation_method: DerivationMethod<Address, UtxoHDWallet>,
pub history_sync_state: Mutex<HistorySyncState>,
/// The cache of verbose transactions.
pub tx_cache: UtxoVerboseCacheShared,
/// The cache of recently send transactions used to track the spent UTXOs and replace them with new outputs
/// The daemon needs some time to update the listunspent list for address which makes it return already spent UTXOs
/// This cache helps to prevent UTXO reuse in such cases
pub recently_spent_outpoints: AsyncMutex<RecentlySpentOutPoints>,
pub tx_hash_algo: TxHashAlgo,
/// The flag determines whether to use mature unspent outputs *only* to generate transactions.
/// https://github.com/KomodoPlatform/atomicDEX-API/issues/1181
pub check_utxo_maturity: bool,
/// The notifier/sender of the block headers synchronization status,
/// initialized only for non-native mode if spv is enabled for the coin.
pub block_headers_status_notifier: Option<UtxoSyncStatusLoopHandle>,
/// The watcher/receiver of the block headers synchronization status,
/// initialized only for non-native mode if spv is enabled for the coin.
pub block_headers_status_watcher: Option<AsyncMutex<AsyncReceiver<UtxoSyncStatus>>>,
/// This abortable system is used to spawn coin's related futures that should be aborted on coin deactivation
/// and on [`MmArc::stop`].
pub abortable_system: AbortableQueue,
pub(crate) ctx: MmWeak,
/// This is used for balance event streaming implementation for UTXOs.
/// If balance event streaming isn't enabled, this value will always be `None`; otherwise,
/// it will be used for receiving scripthash notifications to re-fetch balances.
scripthash_notification_handler: ScripthashNotificationHandler,
}
#[derive(Debug, Display)]
pub enum UnsupportedAddr {
#[display(
fmt = "{} address format activated for {}, but {} format used instead",
activated_format,
ticker,
used_format
)]
FormatMismatch {
ticker: String,
activated_format: String,
used_format: String,
},
#[display(fmt = "Expected a valid P2PKH or P2SH prefix for {}", _0)]
PrefixError(String),
#[display(fmt = "Address hrp {} is not a valid hrp for {}", hrp, ticker)]
HrpError { ticker: String, hrp: String },
#[display(fmt = "Segwit not activated in the config for {}", _0)]
SegwitNotActivated(String),
#[display(fmt = "Internal error {}", _0)]
InternalError(String),
}
impl From<UnsupportedAddr> for WithdrawError {
fn from(e: UnsupportedAddr) -> Self { WithdrawError::InvalidAddress(e.to_string()) }
}
impl From<keys::Error> for UnsupportedAddr {
fn from(e: keys::Error) -> Self { UnsupportedAddr::InternalError(e.to_string()) }
}
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum GetTxError {
Rpc(UtxoRpcError),
TxDeserialization(SerError),
}
impl From<UtxoRpcError> for GetTxError {
fn from(err: UtxoRpcError) -> GetTxError { GetTxError::Rpc(err) }
}
impl From<SerError> for GetTxError {
fn from(err: SerError) -> GetTxError { GetTxError::TxDeserialization(err) }
}
#[derive(Debug, Display)]
pub enum GetTxHeightError {
HeightNotFound(String),
StorageError(BlockHeaderStorageError),
ConversionError(TryFromIntError),
}
impl From<GetTxHeightError> for SPVError {
fn from(e: GetTxHeightError) -> Self {
match e {
GetTxHeightError::HeightNotFound(e) => SPVError::InvalidHeight(e),
GetTxHeightError::StorageError(e) => SPVError::HeaderStorageError(e),
GetTxHeightError::ConversionError(e) => SPVError::Internal(e.to_string()),
}
}
}
impl From<UtxoRpcError> for GetTxHeightError {
fn from(e: UtxoRpcError) -> Self { GetTxHeightError::HeightNotFound(e.to_string()) }
}
impl From<BlockHeaderStorageError> for GetTxHeightError {
fn from(e: BlockHeaderStorageError) -> Self { GetTxHeightError::StorageError(e) }
}
impl From<TryFromIntError> for GetTxHeightError {
fn from(err: TryFromIntError) -> GetTxHeightError { GetTxHeightError::ConversionError(err) }
}
#[derive(Debug, Display)]
pub enum GetBlockHeaderError {
#[display(fmt = "Block header storage error: {}", _0)]
StorageError(BlockHeaderStorageError),
#[display(fmt = "RPC error: {}", _0)]
RpcError(JsonRpcError),
#[display(fmt = "Serialization error: {}", _0)]
SerializationError(serialization::Error),
#[display(fmt = "Invalid response: {}", _0)]
InvalidResponse(String),
#[display(fmt = "Error validating headers: {}", _0)]
SPVError(SPVError),
#[display(fmt = "Internal error: {}", _0)]
Internal(String),
}
impl From<JsonRpcError> for GetBlockHeaderError {
fn from(err: JsonRpcError) -> Self { GetBlockHeaderError::RpcError(err) }
}
impl From<UtxoRpcError> for GetBlockHeaderError {
fn from(e: UtxoRpcError) -> Self {
match e {
UtxoRpcError::Transport(e) | UtxoRpcError::ResponseParseError(e) => GetBlockHeaderError::RpcError(e),
UtxoRpcError::InvalidResponse(e) => GetBlockHeaderError::InvalidResponse(e),
UtxoRpcError::Internal(e) => GetBlockHeaderError::Internal(e),
}
}
}
impl From<serialization::Error> for GetBlockHeaderError {
fn from(err: serialization::Error) -> Self { GetBlockHeaderError::SerializationError(err) }
}
impl From<BlockHeaderStorageError> for GetBlockHeaderError {
fn from(err: BlockHeaderStorageError) -> Self { GetBlockHeaderError::StorageError(err) }
}
impl From<GetBlockHeaderError> for SPVError {
fn from(e: GetBlockHeaderError) -> Self { SPVError::UnableToGetHeader(e.to_string()) }
}
#[derive(Debug, Display)]
pub enum GetConfirmedTxError {
HeightNotFound(GetTxHeightError),
UnableToGetHeader(GetBlockHeaderError),
RpcError(JsonRpcError),
SerializationError(serialization::Error),
SPVError(SPVError),
}
impl From<GetTxHeightError> for GetConfirmedTxError {
fn from(err: GetTxHeightError) -> Self { GetConfirmedTxError::HeightNotFound(err) }
}
impl From<GetBlockHeaderError> for GetConfirmedTxError {
fn from(err: GetBlockHeaderError) -> Self { GetConfirmedTxError::UnableToGetHeader(err) }
}
impl From<JsonRpcError> for GetConfirmedTxError {
fn from(err: JsonRpcError) -> Self { GetConfirmedTxError::RpcError(err) }
}
impl From<serialization::Error> for GetConfirmedTxError {
fn from(err: serialization::Error) -> Self { GetConfirmedTxError::SerializationError(err) }
}
#[derive(Debug, Display)]
pub enum AddrFromStrError {
#[display(fmt = "{}", _0)]
Unsupported(UnsupportedAddr),
#[display(fmt = "Cannot determine format: {:?}", _0)]
CannotDetermineFormat(Vec<String>),
}
impl From<UnsupportedAddr> for AddrFromStrError {
fn from(e: UnsupportedAddr) -> Self { AddrFromStrError::Unsupported(e) }
}
impl From<AddrFromStrError> for VerificationError {
fn from(e: AddrFromStrError) -> Self { VerificationError::AddressDecodingError(e.to_string()) }
}
impl From<AddrFromStrError> for WithdrawError {
fn from(e: AddrFromStrError) -> Self { WithdrawError::InvalidAddress(e.to_string()) }
}
impl UtxoCoinFields {
pub fn transaction_preimage(&self) -> TransactionInputSigner {
let lock_time = if self.conf.ticker == "KMD" {
now_sec_u32() - 3600 + 777 * 2
} else {
now_sec_u32()
};
let str_d_zeel = if self.conf.ticker == "NAV" {
Some("".into())
} else {
None
};
let n_time = if self.conf.is_pos || self.conf.is_posv {
Some(now_sec_u32())
} else {
None
};
TransactionInputSigner {
version: self.conf.tx_version,
n_time,
overwintered: self.conf.overwintered,
version_group_id: self.conf.version_group_id,
consensus_branch_id: self.conf.consensus_branch_id,
expiry_height: 0,
value_balance: 0,
inputs: vec![],
outputs: vec![],
lock_time,
join_splits: vec![],
shielded_spends: vec![],
shielded_outputs: vec![],
zcash: self.conf.zcash,
posv: self.conf.is_posv,
str_d_zeel,
hash_algo: self.tx_hash_algo.into(),
}
}
}
#[derive(Debug, Display)]
#[allow(clippy::large_enum_variant)]
pub enum BroadcastTxErr {
/// RPC client error
Rpc(UtxoRpcError),
/// Other specific error
Other(String),
}
impl From<UtxoRpcError> for BroadcastTxErr {
fn from(err: UtxoRpcError) -> Self { BroadcastTxErr::Rpc(err) }
}
#[async_trait]
#[cfg_attr(test, mockable)]
pub trait UtxoTxBroadcastOps {
async fn broadcast_tx(&self, tx: &UtxoTx) -> Result<H256Json, MmError<BroadcastTxErr>>;
}
#[async_trait]
#[cfg_attr(test, mockable)]
pub trait UtxoTxGenerationOps {
async fn get_tx_fee(&self) -> UtxoRpcResult<ActualTxFee>;
/// Calculates interest if the coin is KMD
/// Adds the value to existing output to my_script_pub or creates additional interest output
/// returns transaction and data as is if the coin is not KMD
async fn calc_interest_if_required(
&self,
mut unsigned: TransactionInputSigner,
mut data: AdditionalTxData,
my_script_pub: Bytes,
dust: u64,
) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)>;
}
/// The UTXO address balance scanner.
/// If the coin is initialized with a native RPC client, it's better to request the list of used addresses
/// right on `UtxoAddressBalanceScanner` initialization.
/// See [`NativeClientImpl::list_transactions`].
pub enum UtxoAddressScanner {
Native { non_empty_addresses: HashSet<String> },
Electrum(ElectrumClient),
}
#[async_trait]
#[cfg_attr(test, mockable)]
impl HDAddressBalanceScanner for UtxoAddressScanner {
type Address = Address;
async fn is_address_used(&self, address: &Self::Address) -> BalanceResult<bool> {
let is_used = match self {
UtxoAddressScanner::Native { non_empty_addresses } => non_empty_addresses.contains(&address.to_string()),
UtxoAddressScanner::Electrum(electrum_client) => {
let script = output_script(address)?;
let script_hash = electrum_script_hash(&script);
let electrum_history = electrum_client
.scripthash_get_history(&hex::encode(script_hash))
.compat()
.await?;
!electrum_history.is_empty()
},
};
Ok(is_used)
}
}
impl UtxoAddressScanner {
pub async fn init(rpc_client: UtxoRpcClientEnum) -> UtxoRpcResult<UtxoAddressScanner> {
match rpc_client {
UtxoRpcClientEnum::Native(native) => UtxoAddressScanner::init_with_native_client(&native).await,
UtxoRpcClientEnum::Electrum(electrum) => Ok(UtxoAddressScanner::Electrum(electrum)),
}
}
pub async fn init_with_native_client(native: &NativeClient) -> UtxoRpcResult<UtxoAddressScanner> {
const STEP: u64 = 100;
let non_empty_addresses = native
.list_all_transactions(STEP)
.compat()
.await?
.into_iter()
.map(|tx_item| tx_item.address)
.collect();
Ok(UtxoAddressScanner::Native { non_empty_addresses })
}
}
/// Contains lists of mature and immature UTXOs.
#[derive(Debug, Default)]
pub struct MatureUnspentList {
mature: Vec<UnspentInfo>,
immature: Vec<UnspentInfo>,
}
impl MatureUnspentList {
#[inline]
pub fn with_capacity(capacity: usize) -> MatureUnspentList {
MatureUnspentList {
mature: Vec::with_capacity(capacity),
immature: Vec::with_capacity(capacity),
}
}
#[inline]
pub fn new_mature(mature: Vec<UnspentInfo>) -> MatureUnspentList {
MatureUnspentList {
mature,
immature: Vec::new(),
}
}
#[inline]
pub fn only_mature(self) -> Vec<UnspentInfo> { self.mature }
#[inline]
pub fn to_coin_balance(&self, decimals: u8) -> CoinBalance {
let fold = |acc: BigDecimal, x: &UnspentInfo| acc + big_decimal_from_sat_unsigned(x.value, decimals);
CoinBalance {
spendable: self.mature.iter().fold(BigDecimal::default(), fold),
unspendable: self.immature.iter().fold(BigDecimal::default(), fold),
}
}
}
#[async_trait]
#[cfg_attr(test, mockable)]
pub trait UtxoCommonOps:
AsRef<UtxoCoinFields> + UtxoTxGenerationOps + UtxoTxBroadcastOps + Clone + Send + Sync + 'static
{
async fn get_htlc_spend_fee(&self, tx_size: u64, stage: &FeeApproxStage) -> UtxoRpcResult<u64>;
fn addresses_from_script(&self, script: &Script) -> Result<Vec<Address>, String>;
fn denominate_satoshis(&self, satoshi: i64) -> f64;
/// Get a public key that matches [`PrivKeyPolicy::Iguana`].
///
/// # Fail
///
/// The method is expected to fail if [`UtxoCoinFields::priv_key_policy`] is [`PrivKeyPolicy::HardwareWallet`].
/// It's worth adding a method like `my_public_key_der_path`
/// that takes a derivation path from which we derive the corresponding public key.
fn my_public_key(&self) -> Result<&Public, MmError<UnexpectedDerivationMethod>>;
/// Try to parse address from string using specified on asset enable format,
/// and if it failed inform user that he used a wrong format.
fn address_from_str(&self, address: &str) -> MmResult<Address, AddrFromStrError>;
/// For an address create corresponding utxo output script
fn script_for_address(&self, address: &Address) -> MmResult<Script, UnsupportedAddr>;
async fn get_current_mtp(&self) -> UtxoRpcResult<u32>;
/// Check if the output is spendable (is not coinbase or it has enough confirmations).
fn is_unspent_mature(&self, output: &RpcTransaction) -> bool;
/// Calculates interest of the specified transaction.
/// Please note, this method has to be used for KMD transactions only.
async fn calc_interest_of_tx(&self, tx: &UtxoTx, input_transactions: &mut HistoryUtxoTxMap) -> UtxoRpcResult<u64>;
/// Try to get a `HistoryUtxoTx` transaction from `utxo_tx_map` or try to request it from Rpc client.