1use candid::{CandidType, Deserialize, Principal};
4use datasize::DataSize;
5use serde::Serialize;
6use serde_bytes::ByteBuf;
7use std::fmt;
8use std::str::FromStr;
9
10pub type Address = String;
11pub type Satoshi = u64;
12pub type MillisatoshiPerByte = u64;
13pub type BlockHash = Vec<u8>;
14pub type Height = u32;
15pub type Page = ByteBuf;
16pub type BlockHeader = Vec<u8>;
17
18const DEFAULT_STABILITY_THRESHOLD: u128 = 144; #[derive(CandidType, Clone, Copy, Deserialize, Debug, Eq, PartialEq, Serialize, Hash, DataSize)]
24pub enum Network {
25 #[serde(rename = "mainnet")]
27 Mainnet,
28
29 #[serde(rename = "testnet")]
31 Testnet,
32
33 #[serde(rename = "regtest")]
35 Regtest,
36}
37
38impl fmt::Display for Network {
39 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
40 match self {
41 Self::Mainnet => write!(f, "mainnet"),
42 Self::Testnet => write!(f, "testnet"),
43 Self::Regtest => write!(f, "regtest"),
44 }
45 }
46}
47
48impl FromStr for Network {
49 type Err = String;
50
51 fn from_str(s: &str) -> Result<Self, Self::Err> {
52 match s {
53 "mainnet" => Ok(Network::Mainnet),
54 "testnet" => Ok(Network::Testnet),
55 "regtest" => Ok(Network::Regtest),
56 _ => Err("Bad network".to_string()),
57 }
58 }
59}
60
61impl From<Network> for NetworkInRequest {
62 fn from(network: Network) -> Self {
63 match network {
64 Network::Mainnet => Self::Mainnet,
65 Network::Testnet => Self::Testnet,
66 Network::Regtest => Self::Regtest,
67 }
68 }
69}
70
71impl From<NetworkInRequest> for Network {
72 fn from(network: NetworkInRequest) -> Self {
73 match network {
74 NetworkInRequest::Mainnet => Self::Mainnet,
75 NetworkInRequest::mainnet => Self::Mainnet,
76 NetworkInRequest::Testnet => Self::Testnet,
77 NetworkInRequest::testnet => Self::Testnet,
78 NetworkInRequest::Regtest => Self::Regtest,
79 NetworkInRequest::regtest => Self::Regtest,
80 }
81 }
82}
83
84#[derive(CandidType, Clone, Copy, Deserialize, Debug, Eq, PartialEq, Serialize, Hash)]
88pub enum NetworkInRequest {
89 Mainnet,
91 #[allow(non_camel_case_types)]
93 mainnet,
94
95 Testnet,
97 #[allow(non_camel_case_types)]
99 testnet,
100
101 Regtest,
103 #[allow(non_camel_case_types)]
105 regtest,
106}
107
108impl fmt::Display for NetworkInRequest {
109 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
110 match self {
111 Self::Mainnet => write!(f, "mainnet"),
112 Self::Testnet => write!(f, "testnet"),
113 Self::Regtest => write!(f, "regtest"),
114 Self::mainnet => write!(f, "mainnet"),
115 Self::testnet => write!(f, "testnet"),
116 Self::regtest => write!(f, "regtest"),
117 }
118 }
119}
120
121#[derive(CandidType, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
122pub struct Txid([u8; 32]);
123
124impl AsRef<[u8]> for Txid {
125 fn as_ref(&self) -> &[u8] {
126 &self.0
127 }
128}
129
130impl From<Txid> for [u8; 32] {
131 fn from(txid: Txid) -> Self {
132 txid.0
133 }
134}
135
136impl serde::Serialize for Txid {
137 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
138 where
139 S: serde::ser::Serializer,
140 {
141 serializer.serialize_bytes(&self.0)
142 }
143}
144
145impl<'de> serde::de::Deserialize<'de> for Txid {
146 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
147 where
148 D: serde::de::Deserializer<'de>,
149 {
150 struct TxidVisitor;
151
152 impl<'de> serde::de::Visitor<'de> for TxidVisitor {
153 type Value = Txid;
154
155 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
156 formatter.write_str("a 32-byte array")
157 }
158
159 fn visit_bytes<E>(self, value: &[u8]) -> Result<Self::Value, E>
160 where
161 E: serde::de::Error,
162 {
163 match TryInto::<[u8; 32]>::try_into(value) {
164 Ok(txid) => Ok(Txid(txid)),
165 Err(_) => Err(E::invalid_length(value.len(), &self)),
166 }
167 }
168
169 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
170 where
171 A: serde::de::SeqAccess<'de>,
172 {
173 use serde::de::Error;
174 if let Some(size_hint) = seq.size_hint() {
175 if size_hint != 32 {
176 return Err(A::Error::invalid_length(size_hint, &self));
177 }
178 }
179 let mut bytes = [0u8; 32];
180 let mut i = 0;
181 while let Some(byte) = seq.next_element()? {
182 if i == 32 {
183 return Err(A::Error::invalid_length(i + 1, &self));
184 }
185
186 bytes[i] = byte;
187 i += 1;
188 }
189 if i != 32 {
190 return Err(A::Error::invalid_length(i, &self));
191 }
192 Ok(Txid(bytes))
193 }
194 }
195
196 deserializer.deserialize_bytes(TxidVisitor)
197 }
198}
199
200impl fmt::Display for Txid {
201 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
202 for b in self.0.iter().rev() {
212 write!(fmt, "{:02x}", *b)?
213 }
214 Ok(())
215 }
216}
217
218impl From<[u8; 32]> for Txid {
219 fn from(bytes: [u8; 32]) -> Self {
220 Self(bytes)
221 }
222}
223
224impl TryFrom<&'_ [u8]> for Txid {
225 type Error = core::array::TryFromSliceError;
226 fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
227 let txid: [u8; 32] = bytes.try_into()?;
228 Ok(Txid(txid))
229 }
230}
231
232#[derive(Debug, Clone, PartialEq, Eq)]
233pub enum TxidFromStrError {
234 InvalidChar(u8),
235 InvalidLength { expected: usize, actual: usize },
236}
237
238impl fmt::Display for TxidFromStrError {
239 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
240 match self {
241 Self::InvalidChar(c) => write!(f, "char {c} is not a valid hex"),
242 Self::InvalidLength { expected, actual } => write!(
243 f,
244 "Bitcoin transaction id must be precisely {expected} characters, got {actual}"
245 ),
246 }
247 }
248}
249
250impl FromStr for Txid {
251 type Err = TxidFromStrError;
252
253 fn from_str(s: &str) -> Result<Self, Self::Err> {
254 fn decode_hex_char(c: u8) -> Result<u8, TxidFromStrError> {
255 match c {
256 b'A'..=b'F' => Ok(c - b'A' + 10),
257 b'a'..=b'f' => Ok(c - b'a' + 10),
258 b'0'..=b'9' => Ok(c - b'0'),
259 _ => Err(TxidFromStrError::InvalidChar(c)),
260 }
261 }
262 if s.len() != 64 {
263 return Err(TxidFromStrError::InvalidLength {
264 expected: 64,
265 actual: s.len(),
266 });
267 }
268 let mut bytes = [0u8; 32];
269 let chars = s.as_bytes();
270 for i in 0..32 {
271 bytes[31 - i] =
272 (decode_hex_char(chars[2 * i])? << 4) | decode_hex_char(chars[2 * i + 1])?;
273 }
274 Ok(Self(bytes))
275 }
276}
277
278#[derive(
280 CandidType, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash, PartialOrd, Ord,
281)]
282pub struct OutPoint {
283 pub txid: Txid,
286 pub vout: u32,
288}
289
290#[derive(
292 CandidType, Debug, Deserialize, Ord, PartialOrd, PartialEq, Serialize, Clone, Hash, Eq,
293)]
294pub struct Utxo {
295 pub outpoint: OutPoint,
296 pub value: Satoshi,
297 pub height: Height,
298}
299
300#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
302pub enum UtxosFilter {
303 MinConfirmations(u32),
304 Page(Page),
305}
306
307impl From<UtxosFilterInRequest> for UtxosFilter {
308 fn from(filter: UtxosFilterInRequest) -> Self {
309 match filter {
310 UtxosFilterInRequest::MinConfirmations(x) => Self::MinConfirmations(x),
311 UtxosFilterInRequest::min_confirmations(x) => Self::MinConfirmations(x),
312 UtxosFilterInRequest::Page(p) => Self::Page(p),
313 UtxosFilterInRequest::page(p) => Self::Page(p),
314 }
315 }
316}
317
318#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
322pub enum UtxosFilterInRequest {
323 MinConfirmations(u32),
324 #[allow(non_camel_case_types)]
325 min_confirmations(u32),
326 Page(Page),
327 #[allow(non_camel_case_types)]
328 page(Page),
329}
330
331#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
333pub struct GetUtxosRequest {
334 pub address: Address,
335 pub network: NetworkInRequest,
336 pub filter: Option<UtxosFilterInRequest>,
337}
338
339#[derive(CandidType, Debug, Deserialize, PartialEq, Eq, Clone)]
341pub struct GetUtxosResponse {
342 pub utxos: Vec<Utxo>,
343 pub tip_block_hash: BlockHash,
344 pub tip_height: Height,
345 pub next_page: Option<Page>,
346}
347
348#[derive(CandidType, Debug, Deserialize, PartialEq, Eq, Clone)]
350pub enum GetUtxosError {
351 MalformedAddress,
352 AddressForWrongNetwork { expected: Network },
353 MinConfirmationsTooLarge { given: u32, max: u32 },
354 UnknownTipBlockHash { tip_block_hash: BlockHash },
355 MalformedPage { err: String },
356}
357
358#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
360pub struct GetBlockHeadersRequest {
361 pub start_height: Height,
362 pub end_height: Option<Height>,
363 pub network: NetworkInRequest,
364}
365
366#[derive(CandidType, Debug, Deserialize, PartialEq, Eq, Clone)]
368pub struct GetBlockHeadersResponse {
369 pub tip_height: Height,
370 pub block_headers: Vec<BlockHeader>,
371}
372
373#[derive(CandidType, Debug, Deserialize, PartialEq, Eq, Clone)]
375pub enum GetBlockHeadersError {
376 StartHeightDoesNotExist {
377 requested: Height,
378 chain_height: Height,
379 },
380 EndHeightDoesNotExist {
381 requested: Height,
382 chain_height: Height,
383 },
384 StartHeightLargerThanEndHeight {
385 start_height: Height,
386 end_height: Height,
387 },
388}
389
390impl fmt::Display for GetBlockHeadersError {
391 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
392 match self {
393 Self::StartHeightDoesNotExist {
394 requested,
395 chain_height,
396 } => {
397 write!(
398 f,
399 "The requested start_height is larger than the height of the chain. Requested: {}, height of chain: {}",
400 requested, chain_height
401 )
402 }
403 Self::EndHeightDoesNotExist {
404 requested,
405 chain_height,
406 } => {
407 write!(
408 f,
409 "The requested start_height is larger than the height of the chain. Requested: {}, height of chain: {}",
410 requested, chain_height
411 )
412 }
413 Self::StartHeightLargerThanEndHeight {
414 start_height,
415 end_height,
416 } => {
417 write!(
418 f,
419 "The requested start_height is larger than the requested end_height. start_height: {}, end_height: {}", start_height, end_height)
420 }
421 }
422 }
423}
424
425#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
427pub struct GetCurrentFeePercentilesRequest {
428 pub network: NetworkInRequest,
429}
430
431impl fmt::Display for GetUtxosError {
432 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
433 match self {
434 Self::MalformedAddress => {
435 write!(f, "Malformed address.")
436 }
437 Self::MinConfirmationsTooLarge { given, max } => {
438 write!(
439 f,
440 "The requested min_confirmations is too large. Given: {}, max supported: {}",
441 given, max
442 )
443 }
444 Self::UnknownTipBlockHash { tip_block_hash } => {
445 write!(
446 f,
447 "The provided tip block hash {:?} is unknown.",
448 tip_block_hash
449 )
450 }
451 Self::MalformedPage { err } => {
452 write!(f, "The provided page is malformed {}", err)
453 }
454 Self::AddressForWrongNetwork { expected } => {
455 write!(
456 f,
457 "Address does not belong to the expected network: {}",
458 expected
459 )
460 }
461 }
462 }
463}
464
465#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
466pub struct GetBalanceRequest {
467 pub address: Address,
468 pub network: NetworkInRequest,
469 pub min_confirmations: Option<u32>,
470}
471
472#[derive(CandidType, Debug, Deserialize, PartialEq, Eq, Clone)]
473pub enum GetBalanceError {
474 MalformedAddress,
475 AddressForWrongNetwork { expected: Network },
476 MinConfirmationsTooLarge { given: u32, max: u32 },
477}
478
479impl fmt::Display for GetBalanceError {
480 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
481 match self {
482 Self::MalformedAddress => {
483 write!(f, "Malformed address.")
484 }
485 Self::MinConfirmationsTooLarge { given, max } => {
486 write!(
487 f,
488 "The requested min_confirmations is too large. Given: {}, max supported: {}",
489 given, max
490 )
491 }
492 Self::AddressForWrongNetwork { expected } => {
493 write!(
494 f,
495 "Address does not belong to the expected network: {}",
496 expected
497 )
498 }
499 }
500 }
501}
502
503#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
504pub struct SendTransactionRequest {
505 #[serde(with = "serde_bytes")]
506 pub transaction: Vec<u8>,
507 pub network: NetworkInRequest,
508}
509
510#[derive(CandidType, Clone, Debug, Deserialize, PartialEq, Eq)]
511pub enum SendTransactionError {
512 MalformedTransaction,
514 QueueFull,
516}
517
518impl fmt::Display for SendTransactionError {
519 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
520 match self {
521 Self::MalformedTransaction => {
522 write!(f, "Can't deserialize transaction because it's malformed.")
523 }
524 Self::QueueFull => {
525 write!(
526 f,
527 "Request can not be enqueued because the queue has reached its capacity. Please retry later."
528 )
529 }
530 }
531 }
532}
533
534#[derive(CandidType, Deserialize, Default, Serialize)]
536pub struct SetConfigRequest {
537 pub stability_threshold: Option<u128>,
538
539 pub syncing: Option<Flag>,
541
542 pub fees: Option<Fees>,
544
545 pub api_access: Option<Flag>,
547
548 pub disable_api_if_not_fully_synced: Option<Flag>,
550
551 pub watchdog_canister: Option<Option<Principal>>,
555
556 pub lazily_evaluate_fee_percentiles: Option<Flag>,
559
560 pub burn_cycles: Option<Flag>,
563}
564
565#[derive(CandidType, Serialize, Deserialize, PartialEq, Eq, Copy, Clone, Debug, Default)]
566pub enum Flag {
567 #[serde(rename = "enabled")]
568 #[default]
569 Enabled,
570 #[serde(rename = "disabled")]
571 Disabled,
572}
573
574#[derive(CandidType, Deserialize)]
575pub enum CanisterArg {
576 #[serde(rename = "init")]
577 Init(InitConfig),
578 #[serde(rename = "upgrade")]
579 Upgrade(Option<SetConfigRequest>),
580}
581
582#[derive(CandidType, Deserialize, Debug, Default)]
588pub struct InitConfig {
589 pub stability_threshold: Option<u128>,
590 pub network: Option<Network>,
591 pub blocks_source: Option<Principal>,
592 pub syncing: Option<Flag>,
593 pub fees: Option<Fees>,
594 pub api_access: Option<Flag>,
595 pub disable_api_if_not_fully_synced: Option<Flag>,
596 pub watchdog_canister: Option<Option<Principal>>,
597 pub burn_cycles: Option<Flag>,
598 pub lazily_evaluate_fee_percentiles: Option<Flag>,
599}
600
601#[derive(CandidType, Deserialize, Debug)]
603pub struct Config {
604 pub stability_threshold: u128,
605 pub network: Network,
606
607 pub blocks_source: Principal,
612
613 pub syncing: Flag,
614
615 pub fees: Fees,
616
617 pub api_access: Flag,
619
620 pub disable_api_if_not_fully_synced: Flag,
623
624 pub watchdog_canister: Option<Principal>,
628
629 pub burn_cycles: Flag,
632
633 pub lazily_evaluate_fee_percentiles: Flag,
636}
637
638impl From<InitConfig> for Config {
639 fn from(init_config: InitConfig) -> Self {
640 let mut config = Config::default();
641
642 if let Some(stability_threshold) = init_config.stability_threshold {
643 config.stability_threshold = stability_threshold;
644 }
645
646 if let Some(network) = init_config.network {
647 config.network = network;
648 }
649
650 if let Some(blocks_source) = init_config.blocks_source {
651 config.blocks_source = blocks_source;
652 }
653
654 if let Some(syncing) = init_config.syncing {
655 config.syncing = syncing;
656 }
657
658 let fees_explicitly_set = init_config.fees.is_some();
659 if let Some(fees) = init_config.fees {
660 config.fees = fees;
661 }
662
663 if let Some(api_access) = init_config.api_access {
664 config.api_access = api_access;
665 }
666
667 if let Some(disable_api_if_not_fully_synced) = init_config.disable_api_if_not_fully_synced {
668 config.disable_api_if_not_fully_synced = disable_api_if_not_fully_synced;
669 }
670
671 if let Some(watchdog_canister) = init_config.watchdog_canister {
672 config.watchdog_canister = watchdog_canister;
673 }
674
675 if let Some(burn_cycles) = init_config.burn_cycles {
676 config.burn_cycles = burn_cycles;
677 }
678
679 if let Some(lazily_evaluate_fee_percentiles) = init_config.lazily_evaluate_fee_percentiles {
680 config.lazily_evaluate_fee_percentiles = lazily_evaluate_fee_percentiles;
681 }
682
683 if !fees_explicitly_set {
685 config.fees = match config.network {
686 Network::Mainnet => Fees::mainnet(),
687 Network::Testnet => Fees::testnet(),
688 Network::Regtest => config.fees, };
690 }
691
692 config
693 }
694}
695
696impl Default for Config {
697 fn default() -> Self {
698 Self {
699 stability_threshold: DEFAULT_STABILITY_THRESHOLD,
700 network: Network::Regtest,
701 blocks_source: Principal::management_canister(),
702 syncing: Flag::Enabled,
703 fees: Fees::default(),
704 api_access: Flag::Enabled,
705 disable_api_if_not_fully_synced: Flag::Enabled,
706 watchdog_canister: None,
707 burn_cycles: Flag::Disabled,
708 lazily_evaluate_fee_percentiles: Flag::Disabled,
709 }
710 }
711}
712
713#[derive(CandidType, Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Default)]
714pub struct Fees {
715 pub get_utxos_base: u128,
717
718 pub get_utxos_cycles_per_ten_instructions: u128,
720
721 pub get_utxos_maximum: u128,
724
725 pub get_balance: u128,
727
728 pub get_balance_maximum: u128,
731
732 pub get_current_fee_percentiles: u128,
734
735 pub get_current_fee_percentiles_maximum: u128,
738
739 pub send_transaction_base: u128,
741
742 pub send_transaction_per_byte: u128,
744
745 #[serde(default)]
746 pub get_block_headers_base: u128,
748
749 #[serde(default)]
750 pub get_block_headers_cycles_per_ten_instructions: u128,
752
753 #[serde(default)]
754 pub get_block_headers_maximum: u128,
757}
758
759impl Fees {
760 pub fn testnet() -> Self {
761 Self {
763 get_utxos_base: 20_000_000,
764 get_utxos_cycles_per_ten_instructions: 4,
765 get_utxos_maximum: 4_000_000_000,
766
767 get_current_fee_percentiles: 4_000_000,
768 get_current_fee_percentiles_maximum: 40_000_000,
769
770 get_balance: 4_000_000,
771 get_balance_maximum: 40_000_000,
772
773 send_transaction_base: 2_000_000_000,
774 send_transaction_per_byte: 8_000_000,
775
776 get_block_headers_base: 20_000_000,
777 get_block_headers_cycles_per_ten_instructions: 4,
778 get_block_headers_maximum: 4_000_000_000,
779 }
780 }
781
782 pub fn mainnet() -> Self {
783 Self {
785 get_utxos_base: 50_000_000,
786 get_utxos_cycles_per_ten_instructions: 10,
787 get_utxos_maximum: 10_000_000_000,
788
789 get_current_fee_percentiles: 10_000_000,
790 get_current_fee_percentiles_maximum: 100_000_000,
791
792 get_balance: 10_000_000,
793 get_balance_maximum: 100_000_000,
794
795 send_transaction_base: 5_000_000_000,
796 send_transaction_per_byte: 20_000_000,
797
798 get_block_headers_base: 50_000_000,
799 get_block_headers_cycles_per_ten_instructions: 10,
800 get_block_headers_maximum: 10_000_000_000,
801 }
802 }
803}
804
805#[derive(CandidType, Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
814pub struct BlockchainInfo {
815 pub height: Height,
817 pub block_hash: BlockHash,
819 pub timestamp: u32,
821 pub difficulty: u128,
823 pub utxos_length: u64,
825}
826
827#[cfg(test)]
828mod test {
829 use super::*;
830
831 #[test]
832 fn test_config_debug_formatter_is_enabled() {
833 assert!(
836 !format!("{:?}", Config::default()).is_empty(),
837 "Config should be printable using debug formatter {{:?}}."
838 );
839 }
840
841 #[test]
842 fn can_extract_bytes_from_txid() {
843 let tx_id = Txid([1; 32]);
844 let tx: [u8; 32] = tx_id.into();
845 assert_eq!(tx, [1; 32]);
846 }
847}