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 MinConfirmationsTooLarge { given: u32, max: u32 },
353 UnknownTipBlockHash { tip_block_hash: BlockHash },
354 MalformedPage { err: String },
355}
356
357#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
359pub struct GetBlockHeadersRequest {
360 pub start_height: Height,
361 pub end_height: Option<Height>,
362 pub network: NetworkInRequest,
363}
364
365#[derive(CandidType, Debug, Deserialize, PartialEq, Eq, Clone)]
367pub struct GetBlockHeadersResponse {
368 pub tip_height: Height,
369 pub block_headers: Vec<BlockHeader>,
370}
371
372#[derive(CandidType, Debug, Deserialize, PartialEq, Eq, Clone)]
374pub enum GetBlockHeadersError {
375 StartHeightDoesNotExist {
376 requested: Height,
377 chain_height: Height,
378 },
379 EndHeightDoesNotExist {
380 requested: Height,
381 chain_height: Height,
382 },
383 StartHeightLargerThanEndHeight {
384 start_height: Height,
385 end_height: Height,
386 },
387}
388
389impl fmt::Display for GetBlockHeadersError {
390 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
391 match self {
392 Self::StartHeightDoesNotExist {
393 requested,
394 chain_height,
395 } => {
396 write!(
397 f,
398 "The requested start_height is larger than the height of the chain. Requested: {}, height of chain: {}",
399 requested, chain_height
400 )
401 }
402 Self::EndHeightDoesNotExist {
403 requested,
404 chain_height,
405 } => {
406 write!(
407 f,
408 "The requested start_height is larger than the height of the chain. Requested: {}, height of chain: {}",
409 requested, chain_height
410 )
411 }
412 Self::StartHeightLargerThanEndHeight {
413 start_height,
414 end_height,
415 } => {
416 write!(
417 f,
418 "The requested start_height is larger than the requested end_height. start_height: {}, end_height: {}", start_height, end_height)
419 }
420 }
421 }
422}
423
424#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
426pub struct GetCurrentFeePercentilesRequest {
427 pub network: NetworkInRequest,
428}
429
430impl fmt::Display for GetUtxosError {
431 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
432 match self {
433 Self::MalformedAddress => {
434 write!(f, "Malformed address.")
435 }
436 Self::MinConfirmationsTooLarge { given, max } => {
437 write!(
438 f,
439 "The requested min_confirmations is too large. Given: {}, max supported: {}",
440 given, max
441 )
442 }
443 Self::UnknownTipBlockHash { tip_block_hash } => {
444 write!(
445 f,
446 "The provided tip block hash {:?} is unknown.",
447 tip_block_hash
448 )
449 }
450 Self::MalformedPage { err } => {
451 write!(f, "The provided page is malformed {}", err)
452 }
453 }
454 }
455}
456
457#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
458pub struct GetBalanceRequest {
459 pub address: Address,
460 pub network: NetworkInRequest,
461 pub min_confirmations: Option<u32>,
462}
463
464#[derive(CandidType, Debug, Deserialize, PartialEq, Eq, Clone)]
465pub enum GetBalanceError {
466 MalformedAddress,
467 MinConfirmationsTooLarge { given: u32, max: u32 },
468}
469
470impl fmt::Display for GetBalanceError {
471 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
472 match self {
473 Self::MalformedAddress => {
474 write!(f, "Malformed address.")
475 }
476 Self::MinConfirmationsTooLarge { given, max } => {
477 write!(
478 f,
479 "The requested min_confirmations is too large. Given: {}, max supported: {}",
480 given, max
481 )
482 }
483 }
484 }
485}
486
487#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
488pub struct SendTransactionRequest {
489 #[serde(with = "serde_bytes")]
490 pub transaction: Vec<u8>,
491 pub network: NetworkInRequest,
492}
493
494#[derive(CandidType, Clone, Debug, Deserialize, PartialEq, Eq)]
495pub enum SendTransactionError {
496 MalformedTransaction,
498 QueueFull,
500}
501
502impl fmt::Display for SendTransactionError {
503 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
504 match self {
505 Self::MalformedTransaction => {
506 write!(f, "Can't deserialize transaction because it's malformed.")
507 }
508 Self::QueueFull => {
509 write!(
510 f,
511 "Request can not be enqueued because the queue has reached its capacity. Please retry later."
512 )
513 }
514 }
515 }
516}
517
518#[derive(CandidType, Deserialize, Default, Serialize)]
520pub struct SetConfigRequest {
521 pub stability_threshold: Option<u128>,
522
523 pub syncing: Option<Flag>,
525
526 pub fees: Option<Fees>,
528
529 pub api_access: Option<Flag>,
531
532 pub disable_api_if_not_fully_synced: Option<Flag>,
534
535 pub watchdog_canister: Option<Option<Principal>>,
539
540 pub lazily_evaluate_fee_percentiles: Option<Flag>,
543
544 pub burn_cycles: Option<Flag>,
547}
548
549#[derive(CandidType, Serialize, Deserialize, PartialEq, Eq, Copy, Clone, Debug, Default)]
550pub enum Flag {
551 #[serde(rename = "enabled")]
552 #[default]
553 Enabled,
554 #[serde(rename = "disabled")]
555 Disabled,
556}
557
558#[derive(CandidType, Deserialize, Debug, Default)]
564pub struct InitConfig {
565 pub stability_threshold: Option<u128>,
566 pub network: Option<Network>,
567 pub blocks_source: Option<Principal>,
568 pub syncing: Option<Flag>,
569 pub fees: Option<Fees>,
570 pub api_access: Option<Flag>,
571 pub disable_api_if_not_fully_synced: Option<Flag>,
572 pub watchdog_canister: Option<Option<Principal>>,
573 pub burn_cycles: Option<Flag>,
574 pub lazily_evaluate_fee_percentiles: Option<Flag>,
575}
576
577#[derive(CandidType, Deserialize, Debug)]
579pub struct Config {
580 pub stability_threshold: u128,
581 pub network: Network,
582
583 pub blocks_source: Principal,
588
589 pub syncing: Flag,
590
591 pub fees: Fees,
592
593 pub api_access: Flag,
595
596 pub disable_api_if_not_fully_synced: Flag,
599
600 pub watchdog_canister: Option<Principal>,
604
605 pub burn_cycles: Flag,
608
609 pub lazily_evaluate_fee_percentiles: Flag,
612}
613
614impl From<InitConfig> for Config {
615 fn from(init_config: InitConfig) -> Self {
616 let mut config = Config::default();
617
618 if let Some(stability_threshold) = init_config.stability_threshold {
619 config.stability_threshold = stability_threshold;
620 }
621
622 if let Some(network) = init_config.network {
623 config.network = network;
624 }
625
626 if let Some(blocks_source) = init_config.blocks_source {
627 config.blocks_source = blocks_source;
628 }
629
630 if let Some(syncing) = init_config.syncing {
631 config.syncing = syncing;
632 }
633
634 let fees_explicitly_set = init_config.fees.is_some();
635 if let Some(fees) = init_config.fees {
636 config.fees = fees;
637 }
638
639 if let Some(api_access) = init_config.api_access {
640 config.api_access = api_access;
641 }
642
643 if let Some(disable_api_if_not_fully_synced) = init_config.disable_api_if_not_fully_synced {
644 config.disable_api_if_not_fully_synced = disable_api_if_not_fully_synced;
645 }
646
647 if let Some(watchdog_canister) = init_config.watchdog_canister {
648 config.watchdog_canister = watchdog_canister;
649 }
650
651 if let Some(burn_cycles) = init_config.burn_cycles {
652 config.burn_cycles = burn_cycles;
653 }
654
655 if let Some(lazily_evaluate_fee_percentiles) = init_config.lazily_evaluate_fee_percentiles {
656 config.lazily_evaluate_fee_percentiles = lazily_evaluate_fee_percentiles;
657 }
658
659 if !fees_explicitly_set {
661 config.fees = match config.network {
662 Network::Mainnet => Fees::mainnet(),
663 Network::Testnet => Fees::testnet(),
664 Network::Regtest => config.fees, };
666 }
667
668 config
669 }
670}
671
672impl Default for Config {
673 fn default() -> Self {
674 Self {
675 stability_threshold: DEFAULT_STABILITY_THRESHOLD,
676 network: Network::Regtest,
677 blocks_source: Principal::management_canister(),
678 syncing: Flag::Enabled,
679 fees: Fees::default(),
680 api_access: Flag::Enabled,
681 disable_api_if_not_fully_synced: Flag::Enabled,
682 watchdog_canister: None,
683 burn_cycles: Flag::Disabled,
684 lazily_evaluate_fee_percentiles: Flag::Disabled,
685 }
686 }
687}
688
689#[derive(CandidType, Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Default)]
690pub struct Fees {
691 pub get_utxos_base: u128,
693
694 pub get_utxos_cycles_per_ten_instructions: u128,
696
697 pub get_utxos_maximum: u128,
700
701 pub get_balance: u128,
703
704 pub get_balance_maximum: u128,
707
708 pub get_current_fee_percentiles: u128,
710
711 pub get_current_fee_percentiles_maximum: u128,
714
715 pub send_transaction_base: u128,
717
718 pub send_transaction_per_byte: u128,
720
721 #[serde(default)]
722 pub get_block_headers_base: u128,
724
725 #[serde(default)]
726 pub get_block_headers_cycles_per_ten_instructions: u128,
728
729 #[serde(default)]
730 pub get_block_headers_maximum: u128,
733}
734
735impl Fees {
736 pub fn testnet() -> Self {
737 Self {
739 get_utxos_base: 20_000_000,
740 get_utxos_cycles_per_ten_instructions: 4,
741 get_utxos_maximum: 4_000_000_000,
742
743 get_current_fee_percentiles: 4_000_000,
744 get_current_fee_percentiles_maximum: 40_000_000,
745
746 get_balance: 4_000_000,
747 get_balance_maximum: 40_000_000,
748
749 send_transaction_base: 2_000_000_000,
750 send_transaction_per_byte: 8_000_000,
751
752 get_block_headers_base: 20_000_000,
753 get_block_headers_cycles_per_ten_instructions: 4,
754 get_block_headers_maximum: 4_000_000_000,
755 }
756 }
757
758 pub fn mainnet() -> Self {
759 Self {
761 get_utxos_base: 50_000_000,
762 get_utxos_cycles_per_ten_instructions: 10,
763 get_utxos_maximum: 10_000_000_000,
764
765 get_current_fee_percentiles: 10_000_000,
766 get_current_fee_percentiles_maximum: 100_000_000,
767
768 get_balance: 10_000_000,
769 get_balance_maximum: 100_000_000,
770
771 send_transaction_base: 5_000_000_000,
772 send_transaction_per_byte: 20_000_000,
773
774 get_block_headers_base: 50_000_000,
775 get_block_headers_cycles_per_ten_instructions: 10,
776 get_block_headers_maximum: 10_000_000_000,
777 }
778 }
779}
780
781#[cfg(test)]
782mod test {
783 use super::*;
784
785 #[test]
786 fn test_config_debug_formatter_is_enabled() {
787 assert!(
790 !format!("{:?}", Config::default()).is_empty(),
791 "Config should be printable using debug formatter {{:?}}."
792 );
793 }
794
795 #[test]
796 fn can_extract_bytes_from_txid() {
797 let tx_id = Txid([1; 32]);
798 let tx: [u8; 32] = tx_id.into();
799 assert_eq!(tx, [1; 32]);
800 }
801}