1use candid::{CandidType, Deserialize, Principal};
4use serde::Serialize;
5use serde_bytes::ByteBuf;
6use std::fmt;
7use std::str::FromStr;
8
9pub type Address = String;
10pub type Satoshi = u64;
11pub type MillisatoshiPerByte = u64;
12pub type BlockHash = Vec<u8>;
13pub type Height = u32;
14pub type Page = ByteBuf;
15pub type BlockHeader = Vec<u8>;
16
17#[derive(CandidType, Clone, Copy, Deserialize, Debug, Eq, PartialEq, Serialize, Hash)]
18pub enum Network {
19 #[serde(rename = "mainnet")]
20 Mainnet,
21 #[serde(rename = "testnet")]
22 Testnet,
23 #[serde(rename = "regtest")]
24 Regtest,
25}
26
27impl fmt::Display for Network {
28 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
29 match self {
30 Self::Mainnet => write!(f, "mainnet"),
31 Self::Testnet => write!(f, "testnet"),
32 Self::Regtest => write!(f, "regtest"),
33 }
34 }
35}
36
37impl FromStr for Network {
38 type Err = String;
39
40 fn from_str(s: &str) -> Result<Self, Self::Err> {
41 match s {
42 "mainnet" => Ok(Network::Mainnet),
43 "testnet" => Ok(Network::Testnet),
44 "regtest" => Ok(Network::Regtest),
45 _ => Err("Bad network".to_string()),
46 }
47 }
48}
49
50impl From<Network> for NetworkInRequest {
51 fn from(network: Network) -> Self {
52 match network {
53 Network::Mainnet => Self::Mainnet,
54 Network::Testnet => Self::Testnet,
55 Network::Regtest => Self::Regtest,
56 }
57 }
58}
59
60impl From<NetworkInRequest> for Network {
61 fn from(network: NetworkInRequest) -> Self {
62 match network {
63 NetworkInRequest::Mainnet => Self::Mainnet,
64 NetworkInRequest::mainnet => Self::Mainnet,
65 NetworkInRequest::Testnet => Self::Testnet,
66 NetworkInRequest::testnet => Self::Testnet,
67 NetworkInRequest::Regtest => Self::Regtest,
68 NetworkInRequest::regtest => Self::Regtest,
69 }
70 }
71}
72
73#[derive(CandidType, Clone, Copy, Deserialize, Debug, Eq, PartialEq, Serialize, Hash)]
77pub enum NetworkInRequest {
78 Mainnet,
79 #[allow(non_camel_case_types)]
80 mainnet,
81 Testnet,
82 #[allow(non_camel_case_types)]
83 testnet,
84 Regtest,
85 #[allow(non_camel_case_types)]
86 regtest,
87}
88
89impl fmt::Display for NetworkInRequest {
90 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
91 match self {
92 Self::Mainnet => write!(f, "mainnet"),
93 Self::Testnet => write!(f, "testnet"),
94 Self::Regtest => write!(f, "regtest"),
95 Self::mainnet => write!(f, "mainnet"),
96 Self::testnet => write!(f, "testnet"),
97 Self::regtest => write!(f, "regtest"),
98 }
99 }
100}
101
102#[derive(CandidType, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
103pub struct Txid([u8; 32]);
104
105impl AsRef<[u8]> for Txid {
106 fn as_ref(&self) -> &[u8] {
107 &self.0
108 }
109}
110
111impl From<Txid> for [u8; 32] {
112 fn from(txid: Txid) -> Self {
113 txid.0
114 }
115}
116
117impl serde::Serialize for Txid {
118 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
119 where
120 S: serde::ser::Serializer,
121 {
122 serializer.serialize_bytes(&self.0)
123 }
124}
125
126impl<'de> serde::de::Deserialize<'de> for Txid {
127 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
128 where
129 D: serde::de::Deserializer<'de>,
130 {
131 struct TxidVisitor;
132
133 impl<'de> serde::de::Visitor<'de> for TxidVisitor {
134 type Value = Txid;
135
136 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
137 formatter.write_str("a 32-byte array")
138 }
139
140 fn visit_bytes<E>(self, value: &[u8]) -> Result<Self::Value, E>
141 where
142 E: serde::de::Error,
143 {
144 match TryInto::<[u8; 32]>::try_into(value) {
145 Ok(txid) => Ok(Txid(txid)),
146 Err(_) => Err(E::invalid_length(value.len(), &self)),
147 }
148 }
149
150 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
151 where
152 A: serde::de::SeqAccess<'de>,
153 {
154 use serde::de::Error;
155 if let Some(size_hint) = seq.size_hint() {
156 if size_hint != 32 {
157 return Err(A::Error::invalid_length(size_hint, &self));
158 }
159 }
160 let mut bytes = [0u8; 32];
161 let mut i = 0;
162 while let Some(byte) = seq.next_element()? {
163 if i == 32 {
164 return Err(A::Error::invalid_length(i + 1, &self));
165 }
166
167 bytes[i] = byte;
168 i += 1;
169 }
170 if i != 32 {
171 return Err(A::Error::invalid_length(i, &self));
172 }
173 Ok(Txid(bytes))
174 }
175 }
176
177 deserializer.deserialize_bytes(TxidVisitor)
178 }
179}
180
181impl fmt::Display for Txid {
182 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
183 for b in self.0.iter().rev() {
193 write!(fmt, "{:02x}", *b)?
194 }
195 Ok(())
196 }
197}
198
199impl From<[u8; 32]> for Txid {
200 fn from(bytes: [u8; 32]) -> Self {
201 Self(bytes)
202 }
203}
204
205impl TryFrom<&'_ [u8]> for Txid {
206 type Error = core::array::TryFromSliceError;
207 fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
208 let txid: [u8; 32] = bytes.try_into()?;
209 Ok(Txid(txid))
210 }
211}
212
213#[derive(Debug, Clone, PartialEq, Eq)]
214pub enum TxidFromStrError {
215 InvalidChar(u8),
216 InvalidLength { expected: usize, actual: usize },
217}
218
219impl fmt::Display for TxidFromStrError {
220 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
221 match self {
222 Self::InvalidChar(c) => write!(f, "char {c} is not a valid hex"),
223 Self::InvalidLength { expected, actual } => write!(
224 f,
225 "Bitcoin transaction id must be precisely {expected} characters, got {actual}"
226 ),
227 }
228 }
229}
230
231impl FromStr for Txid {
232 type Err = TxidFromStrError;
233
234 fn from_str(s: &str) -> Result<Self, Self::Err> {
235 fn decode_hex_char(c: u8) -> Result<u8, TxidFromStrError> {
236 match c {
237 b'A'..=b'F' => Ok(c - b'A' + 10),
238 b'a'..=b'f' => Ok(c - b'a' + 10),
239 b'0'..=b'9' => Ok(c - b'0'),
240 _ => Err(TxidFromStrError::InvalidChar(c)),
241 }
242 }
243 if s.len() != 64 {
244 return Err(TxidFromStrError::InvalidLength {
245 expected: 64,
246 actual: s.len(),
247 });
248 }
249 let mut bytes = [0u8; 32];
250 let chars = s.as_bytes();
251 for i in 0..32 {
252 bytes[31 - i] =
253 (decode_hex_char(chars[2 * i])? << 4) | decode_hex_char(chars[2 * i + 1])?;
254 }
255 Ok(Self(bytes))
256 }
257}
258
259#[derive(
261 CandidType, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash, PartialOrd, Ord,
262)]
263pub struct OutPoint {
264 pub txid: Txid,
267 pub vout: u32,
269}
270
271#[derive(CandidType, Debug, Deserialize, PartialEq, Serialize, Clone, Hash, Eq)]
273pub struct Utxo {
274 pub outpoint: OutPoint,
275 pub value: Satoshi,
276 pub height: Height,
277}
278
279impl std::cmp::PartialOrd for Utxo {
280 fn partial_cmp(&self, other: &Utxo) -> Option<std::cmp::Ordering> {
281 Some(self.cmp(other))
282 }
283}
284
285impl std::cmp::Ord for Utxo {
286 fn cmp(&self, other: &Utxo) -> std::cmp::Ordering {
287 self.outpoint.cmp(&other.outpoint)
290 }
291}
292
293#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
295pub enum UtxosFilter {
296 MinConfirmations(u32),
297 Page(Page),
298}
299
300impl From<UtxosFilterInRequest> for UtxosFilter {
301 fn from(filter: UtxosFilterInRequest) -> Self {
302 match filter {
303 UtxosFilterInRequest::MinConfirmations(x) => Self::MinConfirmations(x),
304 UtxosFilterInRequest::min_confirmations(x) => Self::MinConfirmations(x),
305 UtxosFilterInRequest::Page(p) => Self::Page(p),
306 UtxosFilterInRequest::page(p) => Self::Page(p),
307 }
308 }
309}
310
311#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
315pub enum UtxosFilterInRequest {
316 MinConfirmations(u32),
317 #[allow(non_camel_case_types)]
318 min_confirmations(u32),
319 Page(Page),
320 #[allow(non_camel_case_types)]
321 page(Page),
322}
323
324#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
326pub struct GetUtxosRequest {
327 pub address: Address,
328 pub network: NetworkInRequest,
329 pub filter: Option<UtxosFilterInRequest>,
330}
331
332#[derive(CandidType, Debug, Deserialize, PartialEq, Eq, Clone)]
334pub struct GetUtxosResponse {
335 pub utxos: Vec<Utxo>,
336 pub tip_block_hash: BlockHash,
337 pub tip_height: Height,
338 pub next_page: Option<Page>,
339}
340
341#[derive(CandidType, Debug, Deserialize, PartialEq, Eq, Clone)]
343pub enum GetUtxosError {
344 MalformedAddress,
345 MinConfirmationsTooLarge { given: u32, max: u32 },
346 UnknownTipBlockHash { tip_block_hash: BlockHash },
347 MalformedPage { err: String },
348}
349
350#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
352pub struct GetBlockHeadersRequest {
353 pub start_height: Height,
354 pub end_height: Option<Height>,
355 pub network: NetworkInRequest,
356}
357
358#[derive(CandidType, Debug, Deserialize, PartialEq, Eq, Clone)]
360pub struct GetBlockHeadersResponse {
361 pub tip_height: Height,
362 pub block_headers: Vec<BlockHeader>,
363}
364
365#[derive(CandidType, Debug, Deserialize, PartialEq, Eq, Clone)]
367pub enum GetBlockHeadersError {
368 StartHeightDoesNotExist {
369 requested: Height,
370 chain_height: Height,
371 },
372 EndHeightDoesNotExist {
373 requested: Height,
374 chain_height: Height,
375 },
376 StartHeightLargerThanEndHeight {
377 start_height: Height,
378 end_height: Height,
379 },
380}
381
382impl fmt::Display for GetBlockHeadersError {
383 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
384 match self {
385 Self::StartHeightDoesNotExist {
386 requested,
387 chain_height,
388 } => {
389 write!(
390 f,
391 "The requested start_height is larger than the height of the chain. Requested: {}, height of chain: {}",
392 requested, chain_height
393 )
394 }
395 Self::EndHeightDoesNotExist {
396 requested,
397 chain_height,
398 } => {
399 write!(
400 f,
401 "The requested start_height is larger than the height of the chain. Requested: {}, height of chain: {}",
402 requested, chain_height
403 )
404 }
405 Self::StartHeightLargerThanEndHeight {
406 start_height,
407 end_height,
408 } => {
409 write!(
410 f,
411 "The requested start_height is larger than the requested end_height. start_height: {}, end_height: {}", start_height, end_height)
412 }
413 }
414 }
415}
416
417#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
419pub struct GetCurrentFeePercentilesRequest {
420 pub network: NetworkInRequest,
421}
422
423impl fmt::Display for GetUtxosError {
424 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
425 match self {
426 Self::MalformedAddress => {
427 write!(f, "Malformed address.")
428 }
429 Self::MinConfirmationsTooLarge { given, max } => {
430 write!(
431 f,
432 "The requested min_confirmations is too large. Given: {}, max supported: {}",
433 given, max
434 )
435 }
436 Self::UnknownTipBlockHash { tip_block_hash } => {
437 write!(
438 f,
439 "The provided tip block hash {:?} is unknown.",
440 tip_block_hash
441 )
442 }
443 Self::MalformedPage { err } => {
444 write!(f, "The provided page is malformed {}", err)
445 }
446 }
447 }
448}
449
450#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
451pub struct GetBalanceRequest {
452 pub address: Address,
453 pub network: NetworkInRequest,
454 pub min_confirmations: Option<u32>,
455}
456
457#[derive(CandidType, Debug, Deserialize, PartialEq, Eq, Clone)]
458pub enum GetBalanceError {
459 MalformedAddress,
460 MinConfirmationsTooLarge { given: u32, max: u32 },
461}
462
463impl fmt::Display for GetBalanceError {
464 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
465 match self {
466 Self::MalformedAddress => {
467 write!(f, "Malformed address.")
468 }
469 Self::MinConfirmationsTooLarge { given, max } => {
470 write!(
471 f,
472 "The requested min_confirmations is too large. Given: {}, max supported: {}",
473 given, max
474 )
475 }
476 }
477 }
478}
479
480#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
481pub struct SendTransactionRequest {
482 #[serde(with = "serde_bytes")]
483 pub transaction: Vec<u8>,
484 pub network: NetworkInRequest,
485}
486
487#[derive(CandidType, Clone, Debug, Deserialize, PartialEq, Eq)]
488pub enum SendTransactionError {
489 MalformedTransaction,
491 QueueFull,
493}
494
495impl fmt::Display for SendTransactionError {
496 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
497 match self {
498 Self::MalformedTransaction => {
499 write!(f, "Can't deserialize transaction because it's malformed.")
500 }
501 Self::QueueFull => {
502 write!(
503 f,
504 "Request can not be enqueued because the queue has reached its capacity. Please retry later."
505 )
506 }
507 }
508 }
509}
510
511#[derive(CandidType, Deserialize, Default, Serialize)]
513pub struct SetConfigRequest {
514 pub stability_threshold: Option<u128>,
515
516 pub syncing: Option<Flag>,
518
519 pub fees: Option<Fees>,
521
522 pub api_access: Option<Flag>,
524
525 pub disable_api_if_not_fully_synced: Option<Flag>,
527
528 pub watchdog_canister: Option<Option<Principal>>,
532
533 pub lazily_evaluate_fee_percentiles: Option<Flag>,
536}
537
538#[derive(CandidType, Serialize, Deserialize, PartialEq, Eq, Copy, Clone, Debug, Default)]
539pub enum Flag {
540 #[serde(rename = "enabled")]
541 #[default]
542 Enabled,
543 #[serde(rename = "disabled")]
544 Disabled,
545}
546
547#[derive(CandidType, Deserialize, Debug, Default)]
553pub struct InitConfig {
554 pub stability_threshold: Option<u128>,
555 pub network: Option<Network>,
556 pub blocks_source: Option<Principal>,
557 pub syncing: Option<Flag>,
558 pub fees: Option<Fees>,
559 pub api_access: Option<Flag>,
560 pub disable_api_if_not_fully_synced: Option<Flag>,
561 pub watchdog_canister: Option<Option<Principal>>,
562 pub burn_cycles: Option<Flag>,
563 pub lazily_evaluate_fee_percentiles: Option<Flag>,
564}
565
566#[derive(CandidType, Deserialize, Debug)]
568pub struct Config {
569 pub stability_threshold: u128,
570 pub network: Network,
571
572 pub blocks_source: Principal,
577
578 pub syncing: Flag,
579
580 pub fees: Fees,
581
582 pub api_access: Flag,
584
585 pub disable_api_if_not_fully_synced: Flag,
588
589 pub watchdog_canister: Option<Principal>,
593
594 pub burn_cycles: Flag,
597
598 pub lazily_evaluate_fee_percentiles: Flag,
601}
602
603impl From<InitConfig> for Config {
604 fn from(init_config: InitConfig) -> Self {
605 let mut config = Config::default();
606
607 if let Some(stability_threshold) = init_config.stability_threshold {
608 config.stability_threshold = stability_threshold;
609 }
610
611 if let Some(network) = init_config.network {
612 config.network = network;
613 }
614
615 if let Some(blocks_source) = init_config.blocks_source {
616 config.blocks_source = blocks_source;
617 }
618
619 if let Some(syncing) = init_config.syncing {
620 config.syncing = syncing;
621 }
622
623 if let Some(fees) = init_config.fees {
624 config.fees = fees;
625 }
626
627 if let Some(api_access) = init_config.api_access {
628 config.api_access = api_access;
629 }
630
631 if let Some(disable_api_if_not_fully_synced) = init_config.disable_api_if_not_fully_synced {
632 config.disable_api_if_not_fully_synced = disable_api_if_not_fully_synced;
633 }
634
635 if let Some(watchdog_canister) = init_config.watchdog_canister {
636 config.watchdog_canister = watchdog_canister;
637 }
638
639 if let Some(burn_cycles) = init_config.burn_cycles {
640 config.burn_cycles = burn_cycles;
641 }
642
643 if let Some(lazily_evaluate_fee_percentiles) = init_config.lazily_evaluate_fee_percentiles {
644 config.lazily_evaluate_fee_percentiles = lazily_evaluate_fee_percentiles;
645 }
646
647 config
648 }
649}
650
651impl Default for Config {
652 fn default() -> Self {
653 Self {
654 stability_threshold: 0,
655 network: Network::Regtest,
656 blocks_source: Principal::management_canister(),
657 syncing: Flag::Enabled,
658 fees: Fees::default(),
659 api_access: Flag::Enabled,
660 disable_api_if_not_fully_synced: Flag::Enabled,
661 watchdog_canister: None,
662 burn_cycles: Flag::Disabled,
663 lazily_evaluate_fee_percentiles: Flag::Disabled,
664 }
665 }
666}
667
668#[derive(CandidType, Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Default)]
669pub struct Fees {
670 pub get_utxos_base: u128,
672
673 pub get_utxos_cycles_per_ten_instructions: u128,
675
676 pub get_utxos_maximum: u128,
679
680 pub get_balance: u128,
682
683 pub get_balance_maximum: u128,
686
687 pub get_current_fee_percentiles: u128,
689
690 pub get_current_fee_percentiles_maximum: u128,
693
694 pub send_transaction_base: u128,
696
697 pub send_transaction_per_byte: u128,
699
700 #[serde(default)]
701 pub get_block_headers_base: u128,
703
704 #[serde(default)]
705 pub get_block_headers_cycles_per_ten_instructions: u128,
707
708 #[serde(default)]
709 pub get_block_headers_maximum: u128,
712}
713
714#[cfg(test)]
715mod test {
716 use super::*;
717
718 #[test]
719 fn test_config_debug_formatter_is_enabled() {
720 assert!(
723 !format!("{:?}", Config::default()).is_empty(),
724 "Config should be printable using debug formatter {{:?}}."
725 );
726 }
727
728 #[test]
729 fn can_extract_bytes_from_txid() {
730 let tx_id = Txid([1; 32]);
731 let tx: [u8; 32] = tx_id.into();
732 assert_eq!(tx, [1; 32]);
733 }
734}