use crate::primitives::PublicKey;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub mod hex_bytes {
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S>(bytes: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&hex::encode(bytes))
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
hex::decode(&s).map_err(serde::de::Error::custom)
}
}
pub mod hex_bytes_option {
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S>(bytes: &Option<Vec<u8>>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match bytes {
Some(b) => serializer.serialize_str(&hex::encode(b)),
None => serializer.serialize_none(),
}
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error>
where
D: Deserializer<'de>,
{
let opt: Option<String> = Option::deserialize(deserializer)?;
match opt {
Some(s) => hex::decode(&s).map(Some).map_err(serde::de::Error::custom),
None => Ok(None),
}
}
}
pub mod hex_txid {
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S>(bytes: &[u8; 32], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&hex::encode(bytes))
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<[u8; 32], D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let bytes = hex::decode(&s).map_err(serde::de::Error::custom)?;
if bytes.len() != 32 {
return Err(serde::de::Error::custom(format!(
"expected 32 bytes for txid, got {}",
bytes.len()
)));
}
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);
Ok(arr)
}
}
pub mod hex_txid_vec_option {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub fn serialize<S>(txids: &Option<Vec<[u8; 32]>>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match txids {
Some(vec) => {
let hex_strings: Vec<String> = vec.iter().map(hex::encode).collect();
hex_strings.serialize(serializer)
}
None => serializer.serialize_none(),
}
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Vec<[u8; 32]>>, D::Error>
where
D: Deserializer<'de>,
{
let opt: Option<Vec<String>> = Option::deserialize(deserializer)?;
match opt {
Some(vec) => {
let mut result = Vec::with_capacity(vec.len());
for s in vec {
let bytes = hex::decode(&s).map_err(serde::de::Error::custom)?;
if bytes.len() != 32 {
return Err(serde::de::Error::custom(format!(
"expected 32 bytes for txid, got {}",
bytes.len()
)));
}
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);
result.push(arr);
}
Ok(Some(result))
}
None => Ok(None),
}
}
}
pub type TxId = [u8; 32];
pub type SatoshiValue = u64;
pub const MAX_SATOSHIS: u64 = 2_100_000_000_000_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Network {
#[default]
Mainnet,
Testnet,
}
impl Network {
pub fn as_str(&self) -> &'static str {
match self {
Network::Mainnet => "mainnet",
Network::Testnet => "testnet",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[repr(u8)]
pub enum SecurityLevel {
#[default]
#[serde(rename = "0")]
Silent = 0,
#[serde(rename = "1")]
App = 1,
#[serde(rename = "2")]
Counterparty = 2,
}
impl SecurityLevel {
pub fn from_u8(value: u8) -> Option<Self> {
match value {
0 => Some(SecurityLevel::Silent),
1 => Some(SecurityLevel::App),
2 => Some(SecurityLevel::Counterparty),
_ => None,
}
}
pub fn as_u8(&self) -> u8 {
*self as u8
}
}
impl From<SecurityLevel> for u8 {
fn from(level: SecurityLevel) -> Self {
level as u8
}
}
impl TryFrom<u8> for SecurityLevel {
type Error = ();
fn try_from(value: u8) -> Result<Self, Self::Error> {
SecurityLevel::from_u8(value).ok_or(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Protocol {
pub security_level: SecurityLevel,
pub protocol_name: String,
}
impl Protocol {
pub fn new(security_level: SecurityLevel, protocol_name: impl Into<String>) -> Self {
Self {
security_level,
protocol_name: protocol_name.into(),
}
}
pub fn from_tuple(tuple: (u8, &str)) -> Option<Self> {
let security_level = SecurityLevel::from_u8(tuple.0)?;
Some(Self {
security_level,
protocol_name: tuple.1.to_string(),
})
}
pub fn to_tuple(&self) -> (u8, &str) {
(self.security_level.as_u8(), &self.protocol_name)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Counterparty {
Self_,
Anyone,
Other(PublicKey),
}
impl Counterparty {
pub fn from_hex(hex: &str) -> crate::Result<Self> {
let pubkey = PublicKey::from_hex(hex)?;
Ok(Counterparty::Other(pubkey))
}
pub fn is_self(&self) -> bool {
matches!(self, Counterparty::Self_)
}
pub fn is_anyone(&self) -> bool {
matches!(self, Counterparty::Anyone)
}
pub fn public_key(&self) -> Option<&PublicKey> {
match self {
Counterparty::Other(pk) => Some(pk),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Outpoint {
pub txid: TxId,
pub vout: u32,
}
impl serde::Serialize for Outpoint {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> serde::Deserialize<'de> for Outpoint {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::{self, MapAccess, Visitor};
struct OutpointVisitor;
impl<'de> Visitor<'de> for OutpointVisitor {
type Value = Outpoint;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter
.write_str("a string like 'txid.vout' or an object with txid and vout fields")
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Outpoint::from_string(s).map_err(de::Error::custom)
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut txid: Option<TxId> = None;
let mut vout: Option<u32> = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"txid" => {
let hex: String = map.next_value()?;
let bytes =
crate::primitives::from_hex(&hex).map_err(de::Error::custom)?;
if bytes.len() != 32 {
return Err(de::Error::custom("txid must be 32 bytes"));
}
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);
txid = Some(arr);
}
"vout" => {
vout = Some(map.next_value()?);
}
_ => {
let _: serde::de::IgnoredAny = map.next_value()?;
}
}
}
let txid = txid.ok_or_else(|| de::Error::missing_field("txid"))?;
let vout = vout.ok_or_else(|| de::Error::missing_field("vout"))?;
Ok(Outpoint { txid, vout })
}
}
deserializer.deserialize_any(OutpointVisitor)
}
}
impl Outpoint {
pub fn new(txid: TxId, vout: u32) -> Self {
Self { txid, vout }
}
pub fn from_string(s: &str) -> crate::Result<Self> {
let parts: Vec<&str> = s.split('.').collect();
if parts.len() != 2 {
return Err(crate::Error::WalletError(format!(
"Invalid outpoint format: expected 'txid.vout', got '{}'",
s
)));
}
let txid_hex = parts[0];
if txid_hex.len() != 64 {
return Err(crate::Error::WalletError(format!(
"Invalid txid length: expected 64 hex chars, got {}",
txid_hex.len()
)));
}
let txid_bytes = crate::primitives::from_hex(txid_hex)?;
let mut txid = [0u8; 32];
txid.copy_from_slice(&txid_bytes);
let vout: u32 = parts[1]
.parse()
.map_err(|_| crate::Error::WalletError(format!("Invalid vout: '{}'", parts[1])))?;
Ok(Self { txid, vout })
}
}
impl std::fmt::Display for Outpoint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}", crate::primitives::to_hex(&self.txid), self.vout)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ActionStatus {
Completed,
Unprocessed,
Sending,
Unproven,
Unsigned,
#[serde(rename = "nosend")]
NoSend,
#[serde(rename = "nonfinal")]
NonFinal,
Failed,
}
impl ActionStatus {
pub fn as_str(&self) -> &'static str {
match self {
ActionStatus::Completed => "completed",
ActionStatus::Unprocessed => "unprocessed",
ActionStatus::Sending => "sending",
ActionStatus::Unproven => "unproven",
ActionStatus::Unsigned => "unsigned",
ActionStatus::NoSend => "nosend",
ActionStatus::NonFinal => "nonfinal",
ActionStatus::Failed => "failed",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateActionInput {
pub outpoint: Outpoint,
pub input_description: String,
#[serde(
with = "hex_bytes_option",
skip_serializing_if = "Option::is_none",
default
)]
pub unlocking_script: Option<Vec<u8>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub unlocking_script_length: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sequence_number: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateActionOutput {
#[serde(with = "hex_bytes")]
pub locking_script: Vec<u8>,
pub satoshis: SatoshiValue,
pub output_description: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub basket: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_instructions: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<String>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateActionOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub sign_and_process: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub accept_delayed_broadcast: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trust_self: Option<TrustSelf>,
#[serde(skip_serializing_if = "Option::is_none")]
pub known_txids: Option<Vec<TxId>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub return_txid_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub no_send: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub no_send_change: Option<Vec<Outpoint>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub send_with: Option<Vec<TxId>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub randomize_outputs: Option<bool>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TrustSelf {
Known,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SendWithResultStatus {
Unproven,
Sending,
Failed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SendWithResult {
#[serde(with = "hex_txid")]
pub txid: TxId,
pub status: SendWithResultStatus,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum ReviewActionResultStatus {
Success,
DoubleSpend,
ServiceError,
InvalidTx,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReviewActionResult {
#[serde(with = "hex_txid")]
pub txid: TxId,
pub status: ReviewActionResultStatus,
#[serde(
with = "hex_txid_vec_option",
skip_serializing_if = "Option::is_none",
default
)]
pub competing_txs: Option<Vec<TxId>>,
#[serde(
with = "hex_bytes_option",
skip_serializing_if = "Option::is_none",
default
)]
pub competing_beef: Option<Vec<u8>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SignableTransaction {
#[serde(with = "hex_bytes")]
pub tx: Vec<u8>,
#[serde(with = "hex_bytes")]
pub reference: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase", default)]
pub struct CreateActionResult {
#[serde(skip_serializing_if = "Option::is_none")]
pub txid: Option<TxId>,
#[serde(
with = "hex_bytes_option",
skip_serializing_if = "Option::is_none",
default
)]
pub tx: Option<Vec<u8>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub no_send_change: Option<Vec<Outpoint>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub send_with_results: Option<Vec<SendWithResult>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub signable_transaction: Option<SignableTransaction>,
#[serde(skip_serializing_if = "Option::is_none")]
pub input_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub inputs: Option<Vec<CreateActionInput>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reference_number: Option<String>,
#[serde(
with = "hex_bytes_option",
skip_serializing_if = "Option::is_none",
default
)]
pub beef: Option<Vec<u8>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AcquisitionProtocol {
Direct,
Issuance,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Certificate {
pub certificate_type: String,
pub subject: PublicKey,
pub serial_number: String,
pub certifier: PublicKey,
#[serde(skip_serializing_if = "Option::is_none")]
pub revocation_outpoint: Option<Outpoint>,
pub fields: HashMap<String, String>,
#[serde(
with = "hex_bytes_option",
skip_serializing_if = "Option::is_none",
default
)]
pub signature: Option<Vec<u8>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum KeyringRevealer {
Certifier,
PublicKey(PublicKey),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WalletActionInput {
pub source_outpoint: Outpoint,
pub source_satoshis: SatoshiValue,
#[serde(
with = "hex_bytes_option",
skip_serializing_if = "Option::is_none",
default
)]
pub source_locking_script: Option<Vec<u8>>,
#[serde(
with = "hex_bytes_option",
skip_serializing_if = "Option::is_none",
default
)]
pub unlocking_script: Option<Vec<u8>>,
pub input_description: String,
pub sequence_number: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WalletActionOutput {
pub satoshis: SatoshiValue,
#[serde(
with = "hex_bytes_option",
skip_serializing_if = "Option::is_none",
default
)]
pub locking_script: Option<Vec<u8>>,
pub spendable: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_instructions: Option<String>,
pub tags: Vec<String>,
pub output_index: u32,
pub output_description: String,
pub basket: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WalletAction {
#[serde(with = "hex_txid")]
pub txid: TxId,
pub satoshis: i64,
pub status: ActionStatus,
pub is_outgoing: bool,
pub description: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub labels: Option<Vec<String>>,
pub version: u32,
pub lock_time: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub inputs: Option<Vec<WalletActionInput>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub outputs: Option<Vec<WalletActionOutput>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WalletOutput {
pub satoshis: SatoshiValue,
#[serde(
with = "hex_bytes_option",
skip_serializing_if = "Option::is_none",
default
)]
pub locking_script: Option<Vec<u8>>,
pub spendable: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_instructions: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<String>>,
pub outpoint: Outpoint,
#[serde(skip_serializing_if = "Option::is_none")]
pub labels: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KeyLinkageResult {
#[serde(with = "hex_bytes")]
pub encrypted_linkage: Vec<u8>,
#[serde(with = "hex_bytes")]
pub encrypted_linkage_proof: Vec<u8>,
pub prover: PublicKey,
pub verifier: PublicKey,
pub counterparty: PublicKey,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RevealCounterpartyKeyLinkageResult {
pub linkage: KeyLinkageResult,
pub revelation_time: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RevealSpecificKeyLinkageResult {
pub linkage: KeyLinkageResult,
pub protocol: Protocol,
pub key_id: String,
pub proof_type: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum QueryMode {
#[default]
Any,
All,
}
impl QueryMode {
pub fn as_str(&self) -> &'static str {
match self {
QueryMode::Any => "any",
QueryMode::All => "all",
}
}
}
impl std::str::FromStr for QueryMode {
type Err = crate::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"any" => Ok(QueryMode::Any),
"all" => Ok(QueryMode::All),
_ => Err(crate::Error::WalletError(format!(
"Invalid query mode: expected 'any' or 'all', got '{}'",
s
))),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum OutputInclude {
#[serde(rename = "locking scripts")]
LockingScripts,
#[serde(rename = "entire transactions")]
EntireTransactions,
}
impl OutputInclude {
pub fn as_str(&self) -> &'static str {
match self {
OutputInclude::LockingScripts => "locking scripts",
OutputInclude::EntireTransactions => "entire transactions",
}
}
}
impl std::str::FromStr for OutputInclude {
type Err = crate::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"locking scripts" => Ok(OutputInclude::LockingScripts),
"entire transactions" => Ok(OutputInclude::EntireTransactions),
_ => Err(crate::Error::WalletError(format!(
"Invalid output include mode: expected 'locking scripts' or 'entire transactions', got '{}'",
s
))),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateActionArgs {
pub description: String,
#[serde(
with = "hex_bytes_option",
skip_serializing_if = "Option::is_none",
default
)]
pub input_beef: Option<Vec<u8>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub inputs: Option<Vec<CreateActionInput>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub outputs: Option<Vec<CreateActionOutput>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lock_time: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub labels: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub options: Option<CreateActionOptions>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SignActionSpend {
#[serde(with = "hex_bytes")]
pub unlocking_script: Vec<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sequence_number: Option<u32>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SignActionOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub accept_delayed_broadcast: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub return_txid_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub no_send: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub send_with: Option<Vec<TxId>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SignActionArgs {
pub spends: HashMap<u32, SignActionSpend>,
pub reference: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub options: Option<SignActionOptions>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SignActionResult {
#[serde(skip_serializing_if = "Option::is_none")]
pub txid: Option<TxId>,
#[serde(
with = "hex_bytes_option",
skip_serializing_if = "Option::is_none",
default
)]
pub tx: Option<Vec<u8>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub send_with_results: Option<Vec<SendWithResult>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AbortActionArgs {
pub reference: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AbortActionResult {
pub aborted: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListActionsArgs {
pub labels: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label_query_mode: Option<QueryMode>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_labels: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_inputs: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_input_source_locking_scripts: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_input_unlocking_scripts: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_outputs: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_output_locking_scripts: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub seek_permission: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListActionsResult {
pub total_actions: u32,
pub actions: Vec<WalletAction>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WalletPayment {
pub derivation_prefix: String,
pub derivation_suffix: String,
pub sender_identity_key: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BasketInsertion {
pub basket: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_instructions: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InternalizeOutput {
pub output_index: u32,
pub protocol: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub payment_remittance: Option<WalletPayment>,
#[serde(skip_serializing_if = "Option::is_none")]
pub insertion_remittance: Option<BasketInsertion>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InternalizeActionArgs {
#[serde(with = "hex_bytes")]
pub tx: Vec<u8>,
pub outputs: Vec<InternalizeOutput>,
pub description: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub labels: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub seek_permission: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InternalizeActionResult {
pub accepted: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListOutputsArgs {
pub basket: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tag_query_mode: Option<QueryMode>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include: Option<OutputInclude>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_custom_instructions: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_tags: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_labels: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub seek_permission: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListOutputsResult {
pub total_outputs: u32,
#[serde(
with = "hex_bytes_option",
skip_serializing_if = "Option::is_none",
default
)]
pub beef: Option<Vec<u8>>,
pub outputs: Vec<WalletOutput>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RelinquishOutputArgs {
pub basket: String,
pub output: Outpoint,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RelinquishOutputResult {
pub relinquished: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WalletCertificate {
pub certificate_type: String,
pub subject: String,
pub serial_number: String,
pub certifier: String,
pub revocation_outpoint: String,
pub signature: String,
pub fields: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AcquireCertificateArgs {
pub certificate_type: String,
pub certifier: String,
pub acquisition_protocol: AcquisitionProtocol,
pub fields: HashMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub serial_number: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub revocation_outpoint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub signature: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub certifier_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub keyring_revealer: Option<KeyringRevealer>,
#[serde(skip_serializing_if = "Option::is_none")]
pub keyring_for_subject: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub privileged: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub privileged_reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListCertificatesArgs {
pub certifiers: Vec<String>,
pub types: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub privileged: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub privileged_reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CertificateResult {
pub certificate: WalletCertificate,
#[serde(skip_serializing_if = "Option::is_none")]
pub keyring: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub verifier: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListCertificatesResult {
pub total_certificates: u32,
pub certificates: Vec<CertificateResult>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProveCertificateArgs {
pub certificate: WalletCertificate,
pub fields_to_reveal: Vec<String>,
pub verifier: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub privileged: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub privileged_reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProveCertificateResult {
pub keyring_for_verifier: HashMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub certificate: Option<WalletCertificate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub verifier: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RelinquishCertificateArgs {
pub certificate_type: String,
pub serial_number: String,
pub certifier: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RelinquishCertificateResult {
pub relinquished: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IdentityCertifier {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub icon_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub trust: u8,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IdentityCertificate {
pub certificate: WalletCertificate,
#[serde(skip_serializing_if = "Option::is_none")]
pub certifier_info: Option<IdentityCertifier>,
#[serde(skip_serializing_if = "Option::is_none")]
pub publicly_revealed_keyring: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub decrypted_fields: Option<HashMap<String, String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DiscoverByIdentityKeyArgs {
pub identity_key: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub seek_permission: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DiscoverByAttributesArgs {
pub attributes: HashMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub seek_permission: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DiscoverCertificatesResult {
pub total_certificates: u32,
pub certificates: Vec<IdentityCertificate>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthenticatedResult {
pub authenticated: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetHeightResult {
pub height: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetHeaderArgs {
pub height: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetHeaderResult {
pub header: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetNetworkResult {
pub network: Network,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetVersionResult {
pub version: String,
}
pub fn validate_satoshis(value: u64, name: &str) -> crate::Result<u64> {
if value > MAX_SATOSHIS {
return Err(crate::Error::WalletError(format!(
"Invalid {}: {} exceeds maximum of {} satoshis",
name, value, MAX_SATOSHIS
)));
}
Ok(value)
}
pub fn validate_description(desc: &str, name: &str) -> crate::Result<()> {
if desc.len() < 5 {
return Err(crate::Error::WalletError(format!(
"Invalid {}: must be at least 5 characters, got {}",
name,
desc.len()
)));
}
if desc.len() > 50 {
return Err(crate::Error::WalletError(format!(
"Invalid {}: must be at most 50 characters, got {}",
name,
desc.len()
)));
}
Ok(())
}
pub fn validate_key_id(key_id: &str) -> crate::Result<()> {
if key_id.is_empty() {
return Err(crate::Error::ProtocolValidationError(
"key ID must be at least 1 character".to_string(),
));
}
if key_id.len() > 800 {
return Err(crate::Error::ProtocolValidationError(
"key ID must be 800 characters or less".to_string(),
));
}
Ok(())
}
pub fn validate_protocol_name(name: &str) -> crate::Result<String> {
let protocol_name = name.trim().to_lowercase();
let max_len = if protocol_name.starts_with("specific linkage revelation ") {
430
} else {
400
};
if protocol_name.len() > max_len {
return Err(crate::Error::ProtocolValidationError(format!(
"protocol name must be {} characters or less",
max_len
)));
}
if protocol_name.len() < 5 {
return Err(crate::Error::ProtocolValidationError(
"protocol name must be at least 5 characters".to_string(),
));
}
if protocol_name.contains(" ") {
return Err(crate::Error::ProtocolValidationError(
"protocol name cannot contain consecutive spaces".to_string(),
));
}
if !protocol_name
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == ' ')
{
return Err(crate::Error::ProtocolValidationError(
"protocol name can only contain lowercase letters, numbers, and spaces".to_string(),
));
}
if protocol_name.ends_with(" protocol") {
return Err(crate::Error::ProtocolValidationError(
"protocol name should not end with ' protocol'".to_string(),
));
}
Ok(protocol_name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_security_level_conversion() {
assert_eq!(SecurityLevel::from_u8(0), Some(SecurityLevel::Silent));
assert_eq!(SecurityLevel::from_u8(1), Some(SecurityLevel::App));
assert_eq!(SecurityLevel::from_u8(2), Some(SecurityLevel::Counterparty));
assert_eq!(SecurityLevel::from_u8(3), None);
assert_eq!(SecurityLevel::Silent.as_u8(), 0);
assert_eq!(SecurityLevel::App.as_u8(), 1);
assert_eq!(SecurityLevel::Counterparty.as_u8(), 2);
}
#[test]
fn test_protocol_from_tuple() {
let proto = Protocol::from_tuple((1, "test protocol name")).unwrap();
assert_eq!(proto.security_level, SecurityLevel::App);
assert_eq!(proto.protocol_name, "test protocol name");
let tuple = proto.to_tuple();
assert_eq!(tuple, (1, "test protocol name"));
}
#[test]
fn test_outpoint_parsing() {
let txid_hex = "0000000000000000000000000000000000000000000000000000000000000001";
let outpoint_str = format!("{}.5", txid_hex);
let outpoint = Outpoint::from_string(&outpoint_str).unwrap();
assert_eq!(outpoint.vout, 5);
assert_eq!(outpoint.txid[31], 1);
assert_eq!(outpoint.to_string(), outpoint_str);
}
#[test]
fn test_outpoint_invalid_format() {
assert!(Outpoint::from_string("invalid").is_err());
assert!(Outpoint::from_string("txid").is_err());
assert!(Outpoint::from_string("abc.1").is_err()); }
#[test]
fn test_validate_satoshis() {
assert!(validate_satoshis(0, "test").is_ok());
assert!(validate_satoshis(MAX_SATOSHIS, "test").is_ok());
assert!(validate_satoshis(MAX_SATOSHIS + 1, "test").is_err());
}
#[test]
fn test_validate_description() {
assert!(validate_description("hello", "test").is_ok());
assert!(validate_description("a".repeat(50).as_str(), "test").is_ok());
assert!(validate_description("tiny", "test").is_err()); assert!(validate_description("a".repeat(51).as_str(), "test").is_err());
}
#[test]
fn test_validate_key_id() {
assert!(validate_key_id("a").is_ok());
assert!(validate_key_id("a".repeat(800).as_str()).is_ok());
assert!(validate_key_id("").is_err());
assert!(validate_key_id("a".repeat(801).as_str()).is_err());
}
#[test]
fn test_validate_protocol_name() {
assert!(validate_protocol_name("hello").is_ok());
assert!(validate_protocol_name("test protocol 123").is_ok());
assert!(validate_protocol_name("TEST SYSTEM").is_ok());
assert!(validate_protocol_name("tiny").is_err()); assert!(validate_protocol_name("hello world").is_err()); assert!(validate_protocol_name("hello-world").is_err()); assert!(validate_protocol_name("test protocol").is_err()); }
#[test]
fn test_counterparty_variants() {
let cp_self = Counterparty::Self_;
assert!(cp_self.is_self());
assert!(!cp_self.is_anyone());
assert!(cp_self.public_key().is_none());
let cp_anyone = Counterparty::Anyone;
assert!(!cp_anyone.is_self());
assert!(cp_anyone.is_anyone());
assert!(cp_anyone.public_key().is_none());
}
#[test]
fn test_hex_bytes_serialization() {
let output = CreateActionOutput {
locking_script: vec![0x76, 0xa9, 0x14], satoshis: 1000,
output_description: "Test output description".to_string(),
basket: None,
custom_instructions: None,
tags: None,
};
let json = serde_json::to_string(&output).unwrap();
assert!(
json.contains("\"76a914\""),
"Expected hex string in JSON: {}",
json
);
assert!(
!json.contains("[118"),
"Should not contain int array: {}",
json
);
let json_input = r#"{"lockingScript":"76a914","satoshis":1000,"outputDescription":"Test output description"}"#;
let parsed: CreateActionOutput = serde_json::from_str(json_input).unwrap();
assert_eq!(parsed.locking_script, vec![0x76, 0xa9, 0x14]);
}
#[test]
fn test_hex_bytes_option_serialization() {
let txid_hex = "0000000000000000000000000000000000000000000000000000000000000001";
let outpoint = Outpoint::from_string(&format!("{}.0", txid_hex)).unwrap();
let input_with_script = CreateActionInput {
outpoint: outpoint.clone(),
input_description: "Test input description".to_string(),
unlocking_script: Some(vec![0x48, 0x30, 0x45]), unlocking_script_length: None,
sequence_number: None,
};
let json = serde_json::to_string(&input_with_script).unwrap();
assert!(
json.contains("\"483045\""),
"Expected hex string in JSON: {}",
json
);
assert!(
!json.contains("[72"),
"Should not contain int array: {}",
json
);
let input_without_script = CreateActionInput {
outpoint,
input_description: "Test input description".to_string(),
unlocking_script: None,
unlocking_script_length: None,
sequence_number: None,
};
let json = serde_json::to_string(&input_without_script).unwrap();
assert!(
!json.contains("unlockingScript"),
"Field should be absent when None: {}",
json
);
let json_input = format!(
r#"{{"outpoint":"{}.0","inputDescription":"Test input description","unlockingScript":"483045"}}"#,
txid_hex
);
let parsed: CreateActionInput = serde_json::from_str(&json_input).unwrap();
assert_eq!(parsed.unlocking_script, Some(vec![0x48, 0x30, 0x45]));
}
#[test]
fn test_create_action_args_hex_serialization() {
let args = CreateActionArgs {
description: "Test action description".to_string(),
input_beef: Some(vec![0xBE, 0xEF, 0x00, 0x01]),
inputs: None,
outputs: None,
lock_time: None,
version: None,
labels: None,
options: None,
};
let json = serde_json::to_string(&args).unwrap();
assert!(
json.contains("\"beef0001\""),
"Expected hex string for inputBeef: {}",
json
);
assert!(
!json.contains("[190"),
"Should not contain int array: {}",
json
);
let json_input = r#"{"description":"Test action description","inputBeef":"beef0001"}"#;
let parsed: CreateActionArgs = serde_json::from_str(json_input).unwrap();
assert_eq!(parsed.input_beef, Some(vec![0xBE, 0xEF, 0x00, 0x01]));
}
}