use candid::{CandidType, Deserialize, Principal};
use serde::Serialize;
use serde_bytes::ByteBuf;
use std::fmt;
use std::str::FromStr;
pub type Address = String;
pub type Satoshi = u64;
pub type MillisatoshiPerByte = u64;
pub type BlockHash = Vec<u8>;
pub type Height = u32;
pub type Page = ByteBuf;
#[derive(CandidType, Clone, Copy, Deserialize, Debug, Eq, PartialEq, Serialize, Hash)]
pub enum Network {
#[serde(rename = "mainnet")]
Mainnet,
#[serde(rename = "testnet")]
Testnet,
#[serde(rename = "regtest")]
Regtest,
}
impl fmt::Display for Network {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Mainnet => write!(f, "mainnet"),
Self::Testnet => write!(f, "testnet"),
Self::Regtest => write!(f, "regtest"),
}
}
}
impl FromStr for Network {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"mainnet" => Ok(Network::Mainnet),
"testnet" => Ok(Network::Testnet),
"regtest" => Ok(Network::Regtest),
_ => Err("Bad network".to_string()),
}
}
}
impl From<Network> for NetworkInRequest {
fn from(network: Network) -> Self {
match network {
Network::Mainnet => Self::Mainnet,
Network::Testnet => Self::Testnet,
Network::Regtest => Self::Regtest,
}
}
}
impl From<NetworkInRequest> for Network {
fn from(network: NetworkInRequest) -> Self {
match network {
NetworkInRequest::Mainnet => Self::Mainnet,
NetworkInRequest::mainnet => Self::Mainnet,
NetworkInRequest::Testnet => Self::Testnet,
NetworkInRequest::testnet => Self::Testnet,
NetworkInRequest::Regtest => Self::Regtest,
NetworkInRequest::regtest => Self::Regtest,
}
}
}
#[derive(CandidType, Clone, Copy, Deserialize, Debug, Eq, PartialEq, Serialize, Hash)]
pub enum NetworkInRequest {
Mainnet,
#[allow(non_camel_case_types)]
mainnet,
Testnet,
#[allow(non_camel_case_types)]
testnet,
Regtest,
#[allow(non_camel_case_types)]
regtest,
}
impl fmt::Display for NetworkInRequest {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Mainnet => write!(f, "mainnet"),
Self::Testnet => write!(f, "testnet"),
Self::Regtest => write!(f, "regtest"),
Self::mainnet => write!(f, "mainnet"),
Self::testnet => write!(f, "testnet"),
Self::regtest => write!(f, "regtest"),
}
}
}
#[derive(CandidType, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Txid([u8; 32]);
impl AsRef<[u8]> for Txid {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl From<Txid> for [u8; 32] {
fn from(txid: Txid) -> Self {
txid.0
}
}
impl serde::Serialize for Txid {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
serializer.serialize_bytes(&self.0)
}
}
impl<'de> serde::de::Deserialize<'de> for Txid {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>,
{
struct TxidVisitor;
impl<'de> serde::de::Visitor<'de> for TxidVisitor {
type Value = Txid;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a 32-byte array")
}
fn visit_bytes<E>(self, value: &[u8]) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
match TryInto::<[u8; 32]>::try_into(value) {
Ok(txid) => Ok(Txid(txid)),
Err(_) => Err(E::invalid_length(value.len(), &self)),
}
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
use serde::de::Error;
if let Some(size_hint) = seq.size_hint() {
if size_hint != 32 {
return Err(A::Error::invalid_length(size_hint, &self));
}
}
let mut bytes = [0u8; 32];
let mut i = 0;
while let Some(byte) = seq.next_element()? {
if i == 32 {
return Err(A::Error::invalid_length(i + 1, &self));
}
bytes[i] = byte;
i += 1;
}
if i != 32 {
return Err(A::Error::invalid_length(i, &self));
}
Ok(Txid(bytes))
}
}
deserializer.deserialize_bytes(TxidVisitor)
}
}
impl fmt::Display for Txid {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
for b in self.0.iter().rev() {
write!(fmt, "{:02x}", *b)?
}
Ok(())
}
}
impl From<[u8; 32]> for Txid {
fn from(bytes: [u8; 32]) -> Self {
Self(bytes)
}
}
impl TryFrom<&'_ [u8]> for Txid {
type Error = core::array::TryFromSliceError;
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
let txid: [u8; 32] = bytes.try_into()?;
Ok(Txid(txid))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TxidFromStrError {
InvalidChar(u8),
InvalidLength { expected: usize, actual: usize },
}
impl fmt::Display for TxidFromStrError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::InvalidChar(c) => write!(f, "char {c} is not a valid hex"),
Self::InvalidLength { expected, actual } => write!(
f,
"Bitcoin transaction id must be precisely {expected} characters, got {actual}"
),
}
}
}
impl FromStr for Txid {
type Err = TxidFromStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
fn decode_hex_char(c: u8) -> Result<u8, TxidFromStrError> {
match c {
b'A'..=b'F' => Ok(c - b'A' + 10),
b'a'..=b'f' => Ok(c - b'a' + 10),
b'0'..=b'9' => Ok(c - b'0'),
_ => Err(TxidFromStrError::InvalidChar(c)),
}
}
if s.len() != 64 {
return Err(TxidFromStrError::InvalidLength {
expected: 64,
actual: s.len(),
});
}
let mut bytes = [0u8; 32];
let chars = s.as_bytes();
for i in 0..32 {
bytes[31 - i] =
(decode_hex_char(chars[2 * i])? << 4) | decode_hex_char(chars[2 * i + 1])?;
}
Ok(Self(bytes))
}
}
#[derive(
CandidType, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash, PartialOrd, Ord,
)]
pub struct OutPoint {
pub txid: Txid,
pub vout: u32,
}
#[derive(CandidType, Debug, Deserialize, PartialEq, Serialize, Clone, Hash, Eq)]
pub struct Utxo {
pub outpoint: OutPoint,
pub value: Satoshi,
pub height: u32,
}
impl std::cmp::PartialOrd for Utxo {
fn partial_cmp(&self, other: &Utxo) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl std::cmp::Ord for Utxo {
fn cmp(&self, other: &Utxo) -> std::cmp::Ordering {
self.outpoint.cmp(&other.outpoint)
}
}
#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
pub enum UtxosFilter {
MinConfirmations(u32),
Page(Page),
}
impl From<UtxosFilterInRequest> for UtxosFilter {
fn from(filter: UtxosFilterInRequest) -> Self {
match filter {
UtxosFilterInRequest::MinConfirmations(x) => Self::MinConfirmations(x),
UtxosFilterInRequest::min_confirmations(x) => Self::MinConfirmations(x),
UtxosFilterInRequest::Page(p) => Self::Page(p),
UtxosFilterInRequest::page(p) => Self::Page(p),
}
}
}
#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
pub enum UtxosFilterInRequest {
MinConfirmations(u32),
#[allow(non_camel_case_types)]
min_confirmations(u32),
Page(Page),
#[allow(non_camel_case_types)]
page(Page),
}
#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
pub struct GetUtxosRequest {
pub address: Address,
pub network: NetworkInRequest,
pub filter: Option<UtxosFilterInRequest>,
}
#[derive(CandidType, Debug, Deserialize, PartialEq, Eq, Clone)]
pub struct GetUtxosResponse {
pub utxos: Vec<Utxo>,
pub tip_block_hash: BlockHash,
pub tip_height: u32,
pub next_page: Option<Page>,
}
#[derive(CandidType, Debug, Deserialize, PartialEq, Eq, Clone)]
pub enum GetUtxosError {
MalformedAddress,
MinConfirmationsTooLarge { given: u32, max: u32 },
UnknownTipBlockHash { tip_block_hash: BlockHash },
MalformedPage { err: String },
}
#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
pub struct GetCurrentFeePercentilesRequest {
pub network: NetworkInRequest,
}
impl fmt::Display for GetUtxosError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MalformedAddress => {
write!(f, "Malformed address.")
}
Self::MinConfirmationsTooLarge { given, max } => {
write!(
f,
"The requested min_confirmations is too large. Given: {}, max supported: {}",
given, max
)
}
Self::UnknownTipBlockHash { tip_block_hash } => {
write!(
f,
"The provided tip block hash {:?} is unknown.",
tip_block_hash
)
}
Self::MalformedPage { err } => {
write!(f, "The provided page is malformed {}", err)
}
}
}
}
#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
pub struct GetBalanceRequest {
pub address: Address,
pub network: NetworkInRequest,
pub min_confirmations: Option<u32>,
}
#[derive(CandidType, Debug, Deserialize, PartialEq, Eq, Clone)]
pub enum GetBalanceError {
MalformedAddress,
MinConfirmationsTooLarge { given: u32, max: u32 },
}
impl fmt::Display for GetBalanceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MalformedAddress => {
write!(f, "Malformed address.")
}
Self::MinConfirmationsTooLarge { given, max } => {
write!(
f,
"The requested min_confirmations is too large. Given: {}, max supported: {}",
given, max
)
}
}
}
}
#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
pub struct SendTransactionRequest {
#[serde(with = "serde_bytes")]
pub transaction: Vec<u8>,
pub network: NetworkInRequest,
}
#[derive(CandidType, Clone, Debug, Deserialize, PartialEq, Eq)]
pub enum SendTransactionError {
MalformedTransaction,
QueueFull,
}
impl fmt::Display for SendTransactionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MalformedTransaction => {
write!(f, "Can't deserialize transaction because it's malformed.")
}
Self::QueueFull => {
write!(
f,
"Request can not be enqueued because the queue has reached its capacity. Please retry later."
)
}
}
}
}
#[derive(CandidType, Deserialize, Default, Serialize)]
pub struct SetConfigRequest {
pub stability_threshold: Option<u128>,
pub syncing: Option<Flag>,
pub fees: Option<Fees>,
pub api_access: Option<Flag>,
pub disable_api_if_not_fully_synced: Option<Flag>,
pub watchdog_canister: Option<Option<Principal>>,
}
#[derive(CandidType, Serialize, Deserialize, PartialEq, Eq, Copy, Clone, Debug, Default)]
pub enum Flag {
#[serde(rename = "enabled")]
#[default]
Enabled,
#[serde(rename = "disabled")]
Disabled,
}
#[derive(CandidType, Deserialize, Debug)]
pub struct Config {
pub stability_threshold: u128,
pub network: Network,
pub blocks_source: Principal,
pub syncing: Flag,
pub fees: Fees,
pub api_access: Flag,
pub disable_api_if_not_fully_synced: Flag,
pub watchdog_canister: Option<Principal>,
}
impl Default for Config {
fn default() -> Self {
Self {
stability_threshold: 0,
network: Network::Regtest,
blocks_source: Principal::management_canister(),
syncing: Flag::Enabled,
fees: Fees::default(),
api_access: Flag::Enabled,
disable_api_if_not_fully_synced: Flag::Enabled,
watchdog_canister: None,
}
}
}
#[derive(CandidType, Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Default)]
pub struct Fees {
pub get_utxos_base: u128,
pub get_utxos_cycles_per_ten_instructions: u128,
pub get_utxos_maximum: u128,
pub get_balance: u128,
pub get_balance_maximum: u128,
pub get_current_fee_percentiles: u128,
pub get_current_fee_percentiles_maximum: u128,
pub send_transaction_base: u128,
pub send_transaction_per_byte: u128,
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_config_debug_formatter_is_enabled() {
assert!(
!format!("{:?}", Config::default()).is_empty(),
"Config should be printable using debug formatter {{:?}}."
);
}
#[test]
fn can_extract_bytes_from_txid() {
let tx_id = Txid([1; 32]);
let tx: [u8; 32] = tx_id.into();
assert_eq!(tx, [1; 32]);
}
}