Commit 3dbd8ac9 authored by Éloïs's avatar Éloïs Committed by Éloïs

[tests] ws2p: add connection negociation test

parent 4382d616
......@@ -45,7 +45,7 @@ pub mod v2;
use dup_crypto::hashs::Hash;
use dup_crypto::keys::bin_signable::BinSignable;
use dup_crypto::keys::SigError;
use dup_crypto::keys::*;
use v2::WS2Pv2Message;
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
......@@ -85,11 +85,28 @@ impl WS2PMessage {
WS2PMessage::V2(ref msg_v2) => msg_v2.message_hash,
}
}
/// Parse and check bin message
pub fn parse_and_check_bin_message(bin_msg: &[u8]) -> Result<WS2PMessage, WS2PMessageError> {
let msg: WS2PMessage = bincode::deserialize(&bin_msg)?;
let hash = msg.hash();
if hash.is_some() && Hash::compute(&bin_msg) == hash.expect("safe unwrap") {
//println!("DEBUG: parse_and_check_bin_message: hash={:?}", hash);
// Compute hash len
let hash_len = 33;
// Compute signature len
let sig_len = if let Some(sig) = msg.signature() {
match sig {
Sig::Ed25519(_) => 69,
Sig::Schnorr() => panic!("Schnorr algo not yet implemented !"),
}
} else {
1
};
if hash.is_none()
|| Hash::compute(&bin_msg[0..(bin_msg.len() - hash_len - sig_len)])
== hash.expect("safe unwrap")
{
match msg.verify() {
Ok(()) => Ok(msg),
Err(e) => Err(WS2PMessageError::SigError(e)),
......@@ -100,6 +117,39 @@ impl WS2PMessage {
}
}
impl<'de> BinSignable<'de> for WS2PMessage {
fn issuer_pubkey(&self) -> PubKey {
match *self {
WS2PMessage::V2(ref msg_v2) => msg_v2.issuer_pubkey(),
}
}
fn store_hash(&self) -> bool {
match *self {
WS2PMessage::V2(ref msg_v2) => msg_v2.store_hash(),
}
}
fn hash(&self) -> Option<Hash> {
match *self {
WS2PMessage::V2(ref msg_v2) => msg_v2.hash(),
}
}
fn set_hash(&mut self, hash: Hash) {
match *self {
WS2PMessage::V2(ref mut msg_v2) => msg_v2.set_hash(hash),
}
}
fn signature(&self) -> Option<Sig> {
match *self {
WS2PMessage::V2(ref msg_v2) => msg_v2.signature(),
}
}
fn set_signature(&mut self, signature: Sig) {
match *self {
WS2PMessage::V2(ref mut msg_v2) => msg_v2.set_signature(signature),
}
}
}
#[cfg(test)]
mod tests {
use bincode;
......
......@@ -82,7 +82,7 @@ impl WS2Pv2ConnectType {
flags: &WS2PConnectFlags,
blockstamp: Option<Blockstamp>,
) -> WS2Pv2ConnectType {
if flags.sync() {
if !flags.is_empty() && flags.sync() {
if flags.ask_sync_chunk() && blockstamp.is_some() {
WS2Pv2ConnectType::AskChunk(blockstamp.expect("safe unwrap"))
} else if flags.res_sync_chunk() {
......
......@@ -34,6 +34,7 @@ use dup_crypto::keys::bin_signable::BinSignable;
use dup_crypto::keys::*;
use durs_network_documents::NodeId;
use v2::payload_container::*;
use WS2PMessage;
/// WS2P v2 message metadata size
pub static WS2P_V2_MESSAGE_METADATA_SIZE: &'static usize = &144;
......@@ -65,15 +66,15 @@ impl WS2Pv2Message {
issuer_node_id: NodeId,
issuer_keypair: KeyPairEnum,
payload: WS2Pv2MessagePayload,
) -> Result<(WS2Pv2Message, Vec<u8>), SignError> {
let mut msg = WS2Pv2Message {
) -> Result<(WS2PMessage, Vec<u8>), SignError> {
let mut msg = WS2PMessage::V2(WS2Pv2Message {
currency_name,
issuer_node_id,
issuer_pubkey: issuer_keypair.public_key(),
payload,
message_hash: None,
signature: None,
};
});
match msg.sign(issuer_keypair.private_key()) {
Ok(bin_msg) => Ok((msg, bin_msg)),
Err(e) => Err(e),
......
......@@ -312,7 +312,11 @@ impl ToString for EndpointV2 {
impl EndpointV2 {
/// Generate endpoint url
pub fn get_url(&self, get_protocol: bool, supported_ip_v6: bool) -> Option<String> {
let protocol = self.api.0.clone();
let protocol = match &self.api.0[..] {
"WS2P" | "WS2PTOR" => "ws",
_ => "http",
};
let tls = match self.port {
443 => "s",
_ => "",
......
......@@ -6,7 +6,7 @@ description = "WebSocketToPeer API for DURS Project."
license = "AGPL-3.0"
[lib]
path = "lib.rs"
path = "src/lib.rs"
[dependencies]
bincode = "1.0.*"
......
......@@ -13,8 +13,20 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//! WS2p Services
pub static WS2P_DEFAULT_OUTCOMING_QUOTA: &'static usize = &10;
/// Ws2pServiceSender
#[derive(Debug, Copy, Clone)]
pub enum Ws2pServiceSender {}
/*pub static WS2P_OUTCOMING_INTERVAL_AT_STARTUP: &'static u64 = &75;
pub static WS2P_OUTCOMING_INTERVAL: &'static u64 = &300;*/
pub static WS2P_NEGOTIATION_TIMEOUT: &'static u64 = &15_000;
pub static WS2P_EXPIRE_TIMEOUT_IN_SECS: &'static u64 = &120;
pub static WS2P_RECV_SERVICE_FREQ_IN_MS: &'static u64 = &1_000;
pub static WS2P_SPAM_INTERVAL_IN_MILLI_SECS: &'static u64 = &80;
pub static WS2P_SPAM_LIMIT: &'static usize = &6;
pub static WS2P_SPAM_SLEEP_TIME_IN_SEC: &'static u64 = &100;
pub static WS2P_INVALID_MSGS_LIMIT: &'static usize = &5;
/*
pub static WS2P_REQUEST_TIMEOUT: &'static u64 = &30_000;
pub static DURATION_BEFORE_RECORDING_ENDPOINT: &'static u64 = &180;
pub static BLOCKS_REQUEST_INTERVAL: &'static u64 = &60;
pub static PENDING_IDENTITIES_REQUEST_INTERVAL: &'static u64 = &40;
*/
......@@ -29,6 +29,8 @@ pub fn process_ws2p_v2_connect_msg(
remote_full_id: NodeFullId,
connect_msg: &WS2Pv2ConnectMsg,
) {
println!("DEBUG: Receive CONNECT message !");
// Get remote node datas
let remote_challenge = connect_msg.challenge;
let remote_node_datas = Ws2pRemoteNodeDatas {
......@@ -89,6 +91,11 @@ pub fn process_ws2p_v2_connect_msg(
.close_with_reason(CloseCode::Unsupported, "Unsupported features !");
}
}
// Update Status to ConnectMessOk
handler.conn_datas.state = WS2PConnectionState::ConnectMessOk;
handler.send_new_conn_state_to_service();
// Encapsulate and binarize ACK message
let (_, bin_ack_msg) = WS2Pv2Message::encapsulate_payload(
handler.currency.clone(),
......
......@@ -22,6 +22,7 @@ extern crate serde_json;
use constants::*;
use controllers::ws::{util::Token, CloseCode, /*Frame,*/ Handler, Handshake, Message};
use controllers::*;
use services::*;
//use dup_crypto::keys::KeyPairEnum;
use duniter_documents::CurrencyName;
use durs_network_documents::NodeFullId;
......@@ -46,6 +47,8 @@ const RECV_SERVICE: Token = Token(3);
/// whereas a closure captures the Sender for us automatically.
#[derive(Debug)]
pub struct Ws2pConnectionHandler {
/// Controller receiver
pub receiver: mpsc::Receiver<Ws2pControllerOrder>,
/// Websocket sender
pub ws: WsSender,
/// Service Sender
......@@ -68,15 +71,39 @@ impl Ws2pConnectionHandler {
currency: CurrencyName,
local_node: MySelfWs2pNode,
conn_datas: Ws2pConnectionDatas,
) -> Self {
Ws2pConnectionHandler {
) -> Result<Ws2pConnectionHandler, mpsc::SendError<Ws2pServiceSender>> {
// Create controller channel
let (sender, receiver): (
mpsc::Sender<Ws2pControllerOrder>,
mpsc::Receiver<Ws2pControllerOrder>,
) = mpsc::channel();
// Send controller sender to service
println!("DEBUG: Send controller sender to service");
service_sender.send(Ws2pServiceSender::ControllerSender(sender))?;
Ok(Ws2pConnectionHandler {
receiver,
ws,
service_sender,
currency,
local_node,
conn_datas,
count_invalid_msgs: 0,
}
})
}
fn send_new_conn_state_to_service(&self) {
let remote_full_id = if let Some(remote_full_id) = self.conn_datas.remote_full_id {
remote_full_id
} else {
NodeFullId::default()
};
self.service_sender
.send(Ws2pServiceSender::ChangeConnectionState(
remote_full_id,
self.conn_datas.state,
))
.expect("WS2p Service unreacheable !");
}
}
......@@ -104,6 +131,7 @@ impl Handler for Ws2pConnectionHandler {
// Update connection state
self.conn_datas.state = WS2PConnectionState::TryToSendConnectMess;
self.send_new_conn_state_to_service();
// Generate connect message
let connect_msg = generate_connect_message(
......@@ -122,7 +150,7 @@ impl Handler for Ws2pConnectionHandler {
)
.expect("WS2P : Fail to sign own connect message !");
// TESTS ONLY : Send CONNECT Message in JSON (for debug)
/*// TESTS ONLY : Send CONNECT Message in JSON (for debug)
#[cfg(test)]
match self.ws.0.send(Message::text(
serde_json::to_string_pretty(&_ws2p_full_msg)
......@@ -150,7 +178,7 @@ impl Handler for Ws2pConnectionHandler {
.0
.close_with_reason(CloseCode::Error, "Fail to send CONNECT JSON message !");
}
}
}*/
// Start negociation timeouts
self.ws.0.timeout(*WS2P_NEGOTIATION_TIMEOUT, CONNECT)?;
......@@ -165,6 +193,7 @@ impl Handler for Ws2pConnectionHandler {
// Update state
if let WS2PConnectionState::TryToSendConnectMess = self.conn_datas.state {
self.conn_datas.state = WS2PConnectionState::WaitingConnectMess;
self.send_new_conn_state_to_service();
}
// Log
info!(
......@@ -246,8 +275,9 @@ impl Handler for Ws2pConnectionHandler {
self.conn_datas.last_mess_time = SystemTime::now();
if msg.is_binary() {
if let Ok(valid_msg) = WS2PMessage::parse_and_check_bin_message(&msg.into_data()) {
match valid_msg {
println!("DEBUG: Receive new message there is not a spam !");
match WS2PMessage::parse_and_check_bin_message(&msg.into_data()) {
Ok(valid_msg) => match valid_msg {
WS2PMessage::V2(msg_v2) => {
match msg_v2.payload {
WS2Pv2MessagePayload::Connect(ref box_connect_msg) => {
......@@ -278,14 +308,16 @@ impl Handler for Ws2pConnectionHandler {
}
}
}
}
} else {
self.count_invalid_msgs += 1;
if self.count_invalid_msgs >= *WS2P_INVALID_MSGS_LIMIT {
let _ = self.ws.0.close_with_reason(
CloseCode::Invalid,
"Receive several invalid messages !",
);
},
Err(ws2p_msg_err) => {
println!("DEBUG: Message is invalid : {:?}", ws2p_msg_err);
self.count_invalid_msgs += 1;
if self.count_invalid_msgs >= *WS2P_INVALID_MSGS_LIMIT {
let _ = self.ws.0.close_with_reason(
CloseCode::Invalid,
"Receive several invalid messages !",
);
}
}
}
} else if msg.is_text() {
......
......@@ -21,7 +21,7 @@ use controllers::handler::Ws2pConnectionHandler;
use controllers::ws::deflate::DeflateBuilder;
use controllers::ws::listen;
use controllers::*;
use services::Ws2pServiceSender;
use services::*;
//use duniter_network::*;
use durs_ws2p_messages::v2::connect::WS2Pv2ConnectType;
use std::sync::mpsc;
......@@ -39,22 +39,28 @@ pub fn listen_on_ws2p_v2_endpoint(
// Log
info!("Listen on {} ...", ws_url);
println!("DEBUG: call function listen({}) ...", ws_url);
// Connect to websocket
listen(ws_url, move |ws| {
DeflateBuilder::new().build(Ws2pConnectionHandler::new(
WsSender(ws),
service_sender.clone(),
currency.clone(),
self_node.clone(),
Ws2pConnectionDatas::new(WS2Pv2ConnectType::Incoming),
))
println!("DEBUG: Listen on host:port...");
DeflateBuilder::new().build(
Ws2pConnectionHandler::new(
WsSender(ws),
service_sender.clone(),
currency.clone(),
self_node.clone(),
Ws2pConnectionDatas::new(WS2Pv2ConnectType::Incoming),
)
.expect("WS2P Service unrechable"),
)
})
}
#[cfg(test)]
mod tests {
use super::*;
use dup_crypto::keys::*;
use std::thread;
use std::time::Duration;
......@@ -75,7 +81,6 @@ mod tests {
) = mpsc::channel();
thread::spawn(move || {
println!("TESTS: Listen on ws://localhost:10899...");
let result = listen_on_ws2p_v2_endpoint(
&CurrencyName(String::from("default")),
service_sender,
......
......@@ -21,11 +21,12 @@ extern crate ws;
use self::ws::Sender;
use duniter_documents::Blockstamp;
use dup_crypto::hashs::Hash;
use dup_crypto::keys::*;
//use dup_crypto::keys::*;
use durs_network_documents::network_peer::PeerCardV11;
use durs_network_documents::*;
use durs_ws2p_messages::v2::api_features::WS2PFeatures;
use durs_ws2p_messages::v2::connect::WS2Pv2ConnectType;
use durs_ws2p_messages::*;
//use std::sync::mpsc;
use std::time::SystemTime;
......@@ -33,6 +34,15 @@ pub mod handler;
pub mod incoming_connections;
pub mod outgoing_connections;
/// Order transmitted to the controller
#[derive(Debug, Clone)]
pub enum Ws2pControllerOrder {
/// Give a message to be transmitted
SendMsg(Box<WS2PMessage>),
/// Close the connection
Close,
}
/// Store a websocket sender
pub struct WsSender(pub Sender);
......@@ -42,14 +52,6 @@ impl ::std::fmt::Debug for WsSender {
}
}
/// Store self WS2P properties
#[derive(Debug, Clone, PartialEq)]
pub struct MySelfWs2pNode {
my_node_id: NodeId,
my_key_pair: KeyPairEnum,
my_features: WS2PFeatures,
}
#[derive(Debug, Copy, Clone, PartialEq)]
/// WS2P connection state
pub enum WS2PConnectionState {
......
......@@ -23,7 +23,7 @@ use controllers::ws::deflate::DeflateBuilder;
use controllers::*;
use durs_network_documents::network_endpoint::EndpointEnum;
use durs_network_documents::NodeFullId;
use services::Ws2pServiceSender;
use services::*;
//use duniter_network::*;
use durs_ws2p_messages::v2::connect::WS2Pv2ConnectType;
use std::sync::mpsc;
......@@ -31,8 +31,9 @@ use std::sync::mpsc;
/// Connect to WSPv2 Endpoint
pub fn connect_to_ws2p_v2_endpoint(
currency: &CurrencyName,
service_sender: mpsc::Sender<Ws2pServiceSender>,
self_node: MySelfWs2pNode,
service_sender: &mpsc::Sender<Ws2pServiceSender>,
self_node: &MySelfWs2pNode,
expected_remote_full_id: Option<NodeFullId>,
endpoint: &EndpointEnum,
) -> ws::Result<()> {
// Get endpoint url
......@@ -42,24 +43,23 @@ pub fn connect_to_ws2p_v2_endpoint(
let mut conn_meta_datas = Ws2pConnectionDatas::new(WS2Pv2ConnectType::Classic);
// Indicate expected remote_full_id
conn_meta_datas.remote_full_id = Some(NodeFullId(
endpoint
.node_uuid()
.expect("WS2P: Fail to get ep.node_id() !"),
endpoint.pubkey(),
));
conn_meta_datas.remote_full_id = expected_remote_full_id;
// Log
info!("Try connection to {} ...", ws_url);
println!("DEBUG: Try connection to {} ...", ws_url);
// Connect to websocket
connect(ws_url, move |ws| {
DeflateBuilder::new().build(Ws2pConnectionHandler::new(
WsSender(ws),
service_sender.clone(),
currency.clone(),
self_node.clone(),
conn_meta_datas.clone(),
))
DeflateBuilder::new().build(
Ws2pConnectionHandler::new(
WsSender(ws),
service_sender.clone(),
currency.clone(),
self_node.clone(),
conn_meta_datas.clone(),
)
.expect("WS2P Service unrechable"),
)
})
}
// Copyright (C) 2018 The Durs Project Developers.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//! WebSocketToPeer API for the Duniter project.
#![cfg_attr(feature = "strict", deny(warnings))]
#![deny(
missing_docs,
missing_debug_implementations,
missing_copy_implementations,
trivial_casts,
unsafe_code,
unstable_features,
unused_import_braces,
unused_qualifications
)]
#[macro_use]
extern crate log;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate structopt;
extern crate bincode;
extern crate dubp_documents;
extern crate duniter_conf;
extern crate duniter_message;
extern crate duniter_module;
extern crate duniter_network;
extern crate dup_crypto;
extern crate durs_network_documents;
extern crate durs_ws2p_messages;
mod constants;
mod generate_peer;
pub mod controllers;
pub mod services;
use constants::*;
use duniter_conf::DuRsConf;
use duniter_message::DursMsg;
use duniter_module::*;
use duniter_network::*;
use durs_network_documents::network_endpoint::*;
use std::sync::mpsc;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
/// WS2P Configuration
pub struct WS2PConf {
/// Limit of outcoming connections
pub outcoming_quota: usize,
/// Default WS2P endpoints provides by configuration file
pub sync_endpoints: Vec<EndpointEnum>,
}
impl Default for WS2PConf {
fn default() -> Self {
WS2PConf {
outcoming_quota: *WS2P_DEFAULT_OUTCOMING_QUOTA,
sync_endpoints: vec![
EndpointV2::parse_from_raw("WS2P g1-monit.librelois.fr 443 ws2p").unwrap(),
EndpointV2::parse_from_raw("WS2P g1.monnaielibreoccitanie.org 443 ws2p").unwrap(),
],
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
/// WS2Pv2 Module
pub struct WS2Pv2Module {}
impl Default for WS2Pv2Module {
fn default() -> WS2Pv2Module {
WS2Pv2Module {}
}
}
#[derive(Debug)]
/// WS2PFeaturesParseError
pub enum WS2PFeaturesParseError {
/// UnknowApiFeature
UnknowApiFeature(String),
}
impl ApiModule<DuRsConf, DursMsg> for WS2Pv2Module {
type ParseErr = WS2PFeaturesParseError;
/// Parse raw api features
fn parse_raw_api_features(str_features: &str) -> Result<ApiFeatures, Self::ParseErr> {
let str_features: Vec<&str> = str_features.split(' ').collect();
let mut api_features = Vec::with_capacity(0);
for str_feature in str_features {
match str_feature {
"DEF" => api_features[0] += 1u8,
"LOW" => api_features[0] += 2u8,
"ABF" => api_features[0] += 4u8,
_ => {
debug!(
"parse_raw_api_features() = UnknowApiFeature({})",
str_feature
);
return Err(WS2PFeaturesParseError::UnknowApiFeature(String::from(
str_feature,
)));
}
}
}
Ok(ApiFeatures(api_features))
}
}
impl NetworkModule<DuRsConf, DursMsg> for WS2Pv2Module {
fn sync(
_soft_meta_datas: &SoftwareMetaDatas<DuRsConf>,
_keys: RequiredKeysContent,
_conf: WS2PConf,
_main_sender: mpsc::Sender<RooterThreadMessage<DursMsg>>,
_sync_params: SyncParams,
) -> Result<(), ModuleInitError> {
unimplemented!()
}
}
#[derive(StructOpt, Debug, Copy, Clone)]
#[structopt(
name = "ws2p",
raw(setting = "structopt::clap::AppSettings::ColoredHelp")
)]
/// WS2P subcommand options
pub struct WS2POpt {}
impl DuniterModule<DuRsConf, DursMsg> for WS2Pv2Module {
type ModuleConf = WS2PConf;
type ModuleOpt = WS2POpt;
fn name() -> ModuleStaticName {
ModuleStaticName("ws2p")
}
fn priority() -> ModulePriority {
ModulePriority::Essential()
}
fn ask_required_keys() -> RequiredKeys {
RequiredKeys::NetworkKeyPair()
}
fn have_subcommand() -> bool {
true
}
fn exec_subcommand(
_soft_meta_datas: &SoftwareMetaDatas<DuRsConf>,
_keys: RequiredKeysContent,
_module_conf: Self::ModuleConf,
_subcommand_args: WS2POpt,
) {
println!("Succesfully exec ws2p subcommand !")
}
fn start(
_soft_meta_datas: &SoftwareMetaDatas<DuRsConf>,
_keys: RequiredKeysContent,
_conf: WS2PConf,
_rooter_sender: mpsc::Sender<RooterThreadMessage<DursMsg>>,
_load_conf_only: bool,
) -> Result<(), ModuleInitError> {
unimplemented!()
}
}
// Copyright (C) 2018 The Durs Project Developers.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//! WS2P Services
use controllers::*;
use duniter_network::documents::BlockchainDocument;
use dup_crypto::keys::KeyPairEnum;
use durs_network_documents::network_head::NetworkHead;
use durs_network_documents::network_peer::PeerCard;
use durs_network_documents::*;
use durs_ws2p_messages::v2::api_features::WS2PFeatures;
use std::sync::mpsc;
pub mod outgoing;
/// Websocket Error
#[derive(Debug, Copy, Clone)]
pub enum WsError {
/// Unknown error
UnknownError,
}
/// Store self WS2P properties
#[derive(Debug, Clone, PartialEq)]
pub struct MySelfWs2pNode {
/// Local node id
pub my_node_id: NodeId,
/// Local network keypair
pub my_key_pair: KeyPairEnum,
/// Local node WWS2PFeatures
pub my_features: WS2PFeatures,
}
/// Message for the ws2p service
#[derive(Debug, Clone)]
pub enum Ws2pServiceSender {
/// Controller sender
ControllerSender(mpsc::Sender<Ws2pControllerOrder>),
/// A new incoming connection has been established
NewIncomingConnection(NodeFullId),
/// A connection has changed status
ChangeConnectionState(NodeFullId, WS2PConnectionState),
/// A valid head has been received
ReceiveValidHead(NetworkHead),
/// A valid peer has been received
ReceiveValidPeer(PeerCard),
/// A valid blockchain document has been received
ReceiveValidDocument(BlockchainDocument),
}
// Copyright (C) 2018 The Durs Project Developers.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//! WS2P outgoing Services
use duniter_documents::CurrencyName;