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