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