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(CandidType, Debug, Deserialize, PartialEq, Serialize, Clone, Hash, Eq)]
292pub struct Utxo {
293 pub outpoint: OutPoint,
294 pub value: Satoshi,
295 pub height: Height,
296}
297
298impl std::cmp::PartialOrd for Utxo {
299 fn partial_cmp(&self, other: &Utxo) -> Option<std::cmp::Ordering> {
300 Some(self.cmp(other))
301 }
302}
303
304impl std::cmp::Ord for Utxo {
305 fn cmp(&self, other: &Utxo) -> std::cmp::Ordering {
306 self.outpoint.cmp(&other.outpoint)
309 }
310}
311
312#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
314pub enum UtxosFilter {
315 MinConfirmations(u32),
316 Page(Page),
317}
318
319impl From<UtxosFilterInRequest> for UtxosFilter {
320 fn from(filter: UtxosFilterInRequest) -> Self {
321 match filter {
322 UtxosFilterInRequest::MinConfirmations(x) => Self::MinConfirmations(x),
323 UtxosFilterInRequest::min_confirmations(x) => Self::MinConfirmations(x),
324 UtxosFilterInRequest::Page(p) => Self::Page(p),
325 UtxosFilterInRequest::page(p) => Self::Page(p),
326 }
327 }
328}
329
330#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
334pub enum UtxosFilterInRequest {
335 MinConfirmations(u32),
336 #[allow(non_camel_case_types)]
337 min_confirmations(u32),
338 Page(Page),
339 #[allow(non_camel_case_types)]
340 page(Page),
341}
342
343#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
345pub struct GetUtxosRequest {
346 pub address: Address,
347 pub network: NetworkInRequest,
348 pub filter: Option<UtxosFilterInRequest>,
349}
350
351#[derive(CandidType, Debug, Deserialize, PartialEq, Eq, Clone)]
353pub struct GetUtxosResponse {
354 pub utxos: Vec<Utxo>,
355 pub tip_block_hash: BlockHash,
356 pub tip_height: Height,
357 pub next_page: Option<Page>,
358}
359
360#[derive(CandidType, Debug, Deserialize, PartialEq, Eq, Clone)]
362pub enum GetUtxosError {
363 MalformedAddress,
364 MinConfirmationsTooLarge { given: u32, max: u32 },
365 UnknownTipBlockHash { tip_block_hash: BlockHash },
366 MalformedPage { err: String },
367}
368
369#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
371pub struct GetBlockHeadersRequest {
372 pub start_height: Height,
373 pub end_height: Option<Height>,
374 pub network: NetworkInRequest,
375}
376
377#[derive(CandidType, Debug, Deserialize, PartialEq, Eq, Clone)]
379pub struct GetBlockHeadersResponse {
380 pub tip_height: Height,
381 pub block_headers: Vec<BlockHeader>,
382}
383
384#[derive(CandidType, Debug, Deserialize, PartialEq, Eq, Clone)]
386pub enum GetBlockHeadersError {
387 StartHeightDoesNotExist {
388 requested: Height,
389 chain_height: Height,
390 },
391 EndHeightDoesNotExist {
392 requested: Height,
393 chain_height: Height,
394 },
395 StartHeightLargerThanEndHeight {
396 start_height: Height,
397 end_height: Height,
398 },
399}
400
401impl fmt::Display for GetBlockHeadersError {
402 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
403 match self {
404 Self::StartHeightDoesNotExist {
405 requested,
406 chain_height,
407 } => {
408 write!(
409 f,
410 "The requested start_height is larger than the height of the chain. Requested: {}, height of chain: {}",
411 requested, chain_height
412 )
413 }
414 Self::EndHeightDoesNotExist {
415 requested,
416 chain_height,
417 } => {
418 write!(
419 f,
420 "The requested start_height is larger than the height of the chain. Requested: {}, height of chain: {}",
421 requested, chain_height
422 )
423 }
424 Self::StartHeightLargerThanEndHeight {
425 start_height,
426 end_height,
427 } => {
428 write!(
429 f,
430 "The requested start_height is larger than the requested end_height. start_height: {}, end_height: {}", start_height, end_height)
431 }
432 }
433 }
434}
435
436#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
438pub struct GetCurrentFeePercentilesRequest {
439 pub network: NetworkInRequest,
440}
441
442impl fmt::Display for GetUtxosError {
443 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
444 match self {
445 Self::MalformedAddress => {
446 write!(f, "Malformed address.")
447 }
448 Self::MinConfirmationsTooLarge { given, max } => {
449 write!(
450 f,
451 "The requested min_confirmations is too large. Given: {}, max supported: {}",
452 given, max
453 )
454 }
455 Self::UnknownTipBlockHash { tip_block_hash } => {
456 write!(
457 f,
458 "The provided tip block hash {:?} is unknown.",
459 tip_block_hash
460 )
461 }
462 Self::MalformedPage { err } => {
463 write!(f, "The provided page is malformed {}", err)
464 }
465 }
466 }
467}
468
469#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
470pub struct GetBalanceRequest {
471 pub address: Address,
472 pub network: NetworkInRequest,
473 pub min_confirmations: Option<u32>,
474}
475
476#[derive(CandidType, Debug, Deserialize, PartialEq, Eq, Clone)]
477pub enum GetBalanceError {
478 MalformedAddress,
479 MinConfirmationsTooLarge { given: u32, max: u32 },
480}
481
482impl fmt::Display for GetBalanceError {
483 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
484 match self {
485 Self::MalformedAddress => {
486 write!(f, "Malformed address.")
487 }
488 Self::MinConfirmationsTooLarge { given, max } => {
489 write!(
490 f,
491 "The requested min_confirmations is too large. Given: {}, max supported: {}",
492 given, max
493 )
494 }
495 }
496 }
497}
498
499#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
500pub struct SendTransactionRequest {
501 #[serde(with = "serde_bytes")]
502 pub transaction: Vec<u8>,
503 pub network: NetworkInRequest,
504}
505
506#[derive(CandidType, Clone, Debug, Deserialize, PartialEq, Eq)]
507pub enum SendTransactionError {
508 MalformedTransaction,
510 QueueFull,
512}
513
514impl fmt::Display for SendTransactionError {
515 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
516 match self {
517 Self::MalformedTransaction => {
518 write!(f, "Can't deserialize transaction because it's malformed.")
519 }
520 Self::QueueFull => {
521 write!(
522 f,
523 "Request can not be enqueued because the queue has reached its capacity. Please retry later."
524 )
525 }
526 }
527 }
528}
529
530#[derive(CandidType, Deserialize, Default, Serialize)]
532pub struct SetConfigRequest {
533 pub stability_threshold: Option<u128>,
534
535 pub syncing: Option<Flag>,
537
538 pub fees: Option<Fees>,
540
541 pub api_access: Option<Flag>,
543
544 pub disable_api_if_not_fully_synced: Option<Flag>,
546
547 pub watchdog_canister: Option<Option<Principal>>,
551
552 pub lazily_evaluate_fee_percentiles: Option<Flag>,
555}
556
557#[derive(CandidType, Serialize, Deserialize, PartialEq, Eq, Copy, Clone, Debug, Default)]
558pub enum Flag {
559 #[serde(rename = "enabled")]
560 #[default]
561 Enabled,
562 #[serde(rename = "disabled")]
563 Disabled,
564}
565
566#[derive(CandidType, Deserialize, Debug, Default)]
572pub struct InitConfig {
573 pub stability_threshold: Option<u128>,
574 pub network: Option<Network>,
575 pub blocks_source: Option<Principal>,
576 pub syncing: Option<Flag>,
577 pub fees: Option<Fees>,
578 pub api_access: Option<Flag>,
579 pub disable_api_if_not_fully_synced: Option<Flag>,
580 pub watchdog_canister: Option<Option<Principal>>,
581 pub burn_cycles: Option<Flag>,
582 pub lazily_evaluate_fee_percentiles: Option<Flag>,
583}
584
585#[derive(CandidType, Deserialize, Debug)]
587pub struct Config {
588 pub stability_threshold: u128,
589 pub network: Network,
590
591 pub blocks_source: Principal,
596
597 pub syncing: Flag,
598
599 pub fees: Fees,
600
601 pub api_access: Flag,
603
604 pub disable_api_if_not_fully_synced: Flag,
607
608 pub watchdog_canister: Option<Principal>,
612
613 pub burn_cycles: Flag,
616
617 pub lazily_evaluate_fee_percentiles: Flag,
620}
621
622impl From<InitConfig> for Config {
623 fn from(init_config: InitConfig) -> Self {
624 let mut config = Config::default();
625
626 if let Some(stability_threshold) = init_config.stability_threshold {
627 config.stability_threshold = stability_threshold;
628 }
629
630 if let Some(network) = init_config.network {
631 config.network = network;
632 }
633
634 if let Some(blocks_source) = init_config.blocks_source {
635 config.blocks_source = blocks_source;
636 }
637
638 if let Some(syncing) = init_config.syncing {
639 config.syncing = syncing;
640 }
641
642 let fees_explicitly_set = init_config.fees.is_some();
643 if let Some(fees) = init_config.fees {
644 config.fees = fees;
645 }
646
647 if let Some(api_access) = init_config.api_access {
648 config.api_access = api_access;
649 }
650
651 if let Some(disable_api_if_not_fully_synced) = init_config.disable_api_if_not_fully_synced {
652 config.disable_api_if_not_fully_synced = disable_api_if_not_fully_synced;
653 }
654
655 if let Some(watchdog_canister) = init_config.watchdog_canister {
656 config.watchdog_canister = watchdog_canister;
657 }
658
659 if let Some(burn_cycles) = init_config.burn_cycles {
660 config.burn_cycles = burn_cycles;
661 }
662
663 if let Some(lazily_evaluate_fee_percentiles) = init_config.lazily_evaluate_fee_percentiles {
664 config.lazily_evaluate_fee_percentiles = lazily_evaluate_fee_percentiles;
665 }
666
667 if !fees_explicitly_set {
669 config.fees = match config.network {
670 Network::Mainnet => Fees::mainnet(),
671 Network::Testnet => Fees::testnet(),
672 Network::Regtest => config.fees, };
674 }
675
676 config
677 }
678}
679
680impl Default for Config {
681 fn default() -> Self {
682 Self {
683 stability_threshold: DEFAULT_STABILITY_THRESHOLD,
684 network: Network::Regtest,
685 blocks_source: Principal::management_canister(),
686 syncing: Flag::Enabled,
687 fees: Fees::default(),
688 api_access: Flag::Enabled,
689 disable_api_if_not_fully_synced: Flag::Enabled,
690 watchdog_canister: None,
691 burn_cycles: Flag::Disabled,
692 lazily_evaluate_fee_percentiles: Flag::Disabled,
693 }
694 }
695}
696
697#[derive(CandidType, Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Default)]
698pub struct Fees {
699 pub get_utxos_base: u128,
701
702 pub get_utxos_cycles_per_ten_instructions: u128,
704
705 pub get_utxos_maximum: u128,
708
709 pub get_balance: u128,
711
712 pub get_balance_maximum: u128,
715
716 pub get_current_fee_percentiles: u128,
718
719 pub get_current_fee_percentiles_maximum: u128,
722
723 pub send_transaction_base: u128,
725
726 pub send_transaction_per_byte: u128,
728
729 #[serde(default)]
730 pub get_block_headers_base: u128,
732
733 #[serde(default)]
734 pub get_block_headers_cycles_per_ten_instructions: u128,
736
737 #[serde(default)]
738 pub get_block_headers_maximum: u128,
741}
742
743impl Fees {
744 pub fn testnet() -> Self {
745 Self {
747 get_utxos_base: 20_000_000,
748 get_utxos_cycles_per_ten_instructions: 4,
749 get_utxos_maximum: 4_000_000_000,
750
751 get_current_fee_percentiles: 4_000_000,
752 get_current_fee_percentiles_maximum: 40_000_000,
753
754 get_balance: 4_000_000,
755 get_balance_maximum: 40_000_000,
756
757 send_transaction_base: 2_000_000_000,
758 send_transaction_per_byte: 8_000_000,
759
760 get_block_headers_base: 20_000_000,
761 get_block_headers_cycles_per_ten_instructions: 4,
762 get_block_headers_maximum: 4_000_000_000,
763 }
764 }
765
766 pub fn mainnet() -> Self {
767 Self {
769 get_utxos_base: 50_000_000,
770 get_utxos_cycles_per_ten_instructions: 10,
771 get_utxos_maximum: 10_000_000_000,
772
773 get_current_fee_percentiles: 10_000_000,
774 get_current_fee_percentiles_maximum: 100_000_000,
775
776 get_balance: 10_000_000,
777 get_balance_maximum: 100_000_000,
778
779 send_transaction_base: 5_000_000_000,
780 send_transaction_per_byte: 20_000_000,
781
782 get_block_headers_base: 50_000_000,
783 get_block_headers_cycles_per_ten_instructions: 10,
784 get_block_headers_maximum: 10_000_000_000,
785 }
786 }
787}
788
789#[cfg(test)]
790mod test {
791 use super::*;
792
793 #[test]
794 fn test_config_debug_formatter_is_enabled() {
795 assert!(
798 !format!("{:?}", Config::default()).is_empty(),
799 "Config should be printable using debug formatter {{:?}}."
800 );
801 }
802
803 #[test]
804 fn can_extract_bytes_from_txid() {
805 let tx_id = Txid([1; 32]);
806 let tx: [u8; 32] = tx_id.into();
807 assert_eq!(tx, [1; 32]);
808 }
809}