mirror of
https://github.com/jwinterm/mwixnet.git
synced 2026-10-17 14:21:25 +02:00
converted to async/.await to support integration testing framework
(cherry picked from commit 26c129fa787ffec44f229ad8047d5c375fb4d257)
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
// Copyright 2021 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Mining service, gets a block to mine, and based on mining configuration
|
||||
//! chooses a version of the cuckoo miner to mine the block and produce a valid
|
||||
//! header with its proof-of-work. Any valid mined blocks are submitted to the
|
||||
//! network.
|
||||
|
||||
use crate::common::types::BlockFees;
|
||||
use crate::common::wallet::IntegrationGrinWallet;
|
||||
use chrono::prelude::Utc;
|
||||
use chrono::{DateTime, NaiveDateTime};
|
||||
use grin_chain::Chain;
|
||||
use grin_core::core::hash::{Hash, Hashed};
|
||||
use grin_core::core::{Block, BlockHeader, Transaction};
|
||||
use grin_core::{consensus, global};
|
||||
use grin_keychain::Identifier;
|
||||
use grin_util::Mutex;
|
||||
use rand::{thread_rng, Rng};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
pub struct Miner {
|
||||
chain: Arc<Chain>,
|
||||
}
|
||||
|
||||
impl Miner {
|
||||
// Creates a new Miner. Needs references to the chain state and its
|
||||
/// storage.
|
||||
pub fn new(chain: Arc<Chain>) -> Miner {
|
||||
Miner { chain }
|
||||
}
|
||||
|
||||
pub async fn async_mine_empty_blocks(
|
||||
&self,
|
||||
wallet: &Arc<Mutex<IntegrationGrinWallet>>,
|
||||
num_blocks: usize,
|
||||
) {
|
||||
for _ in 0..num_blocks {
|
||||
self.async_mine_next_block(wallet, &vec![]).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a new block on top of the existing chain.
|
||||
pub async fn async_mine_next_block(
|
||||
&self,
|
||||
wallet: &Arc<Mutex<IntegrationGrinWallet>>,
|
||||
txs: &Vec<Transaction>,
|
||||
) {
|
||||
info!("Starting test miner loop.");
|
||||
|
||||
// iteration, we keep the returned derivation to provide it back when
|
||||
// nothing has changed. We only want to create a new key_id for each new block.
|
||||
let mut key_id = None;
|
||||
|
||||
loop {
|
||||
// get the latest chain state and build a block on top of it
|
||||
let head = self.chain.head_header().unwrap();
|
||||
let mut latest_hash = self.chain.head().unwrap().last_block_h;
|
||||
|
||||
let (mut b, block_fees) = self.async_get_block(wallet, txs, key_id.clone()).await;
|
||||
let sol = self.inner_mining_loop(&mut b, &head, &mut latest_hash);
|
||||
|
||||
// we found a solution, push our block through the chain processing pipeline
|
||||
if sol {
|
||||
info!(
|
||||
"Found valid proof of work, adding block {} (prev_root {}).",
|
||||
b.hash(),
|
||||
b.header.prev_root,
|
||||
);
|
||||
let res = self.chain.process_block(b, grin_chain::Options::MINE);
|
||||
if let Err(e) = res {
|
||||
error!("Error validating mined block: {:?}", e);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
key_id = None;
|
||||
} else {
|
||||
key_id = block_fees.key_id();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The inner part of mining loop for the internal miner
|
||||
/// kept around mostly for automated testing purposes
|
||||
fn inner_mining_loop(&self, b: &mut Block, head: &BlockHeader, latest_hash: &mut Hash) -> bool {
|
||||
while head.hash() == *latest_hash {
|
||||
let mut ctx = global::create_pow_context::<u32>(
|
||||
head.height,
|
||||
global::min_edge_bits(),
|
||||
global::proofsize(),
|
||||
10,
|
||||
)
|
||||
.unwrap();
|
||||
ctx.set_header_nonce(b.header.pre_pow(), None, true)
|
||||
.unwrap();
|
||||
if let Ok(proofs) = ctx.find_cycles() {
|
||||
b.header.pow.proof = proofs[0].clone();
|
||||
let proof_diff = b.header.pow.to_difficulty(b.header.height);
|
||||
if proof_diff >= (b.header.total_difficulty() - head.total_difficulty()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
b.header.pow.nonce += 1;
|
||||
*latest_hash = self.chain.head().unwrap().last_block_h;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
// Ensure a block suitable for mining is built and returned
|
||||
// If a wallet listener URL is not provided the reward will be "burnt"
|
||||
// Warning: This call does not return until/unless a new block can be built
|
||||
async fn async_get_block(
|
||||
&self,
|
||||
wallet: &Arc<Mutex<IntegrationGrinWallet>>,
|
||||
txs: &Vec<Transaction>,
|
||||
key_id: Option<Identifier>,
|
||||
) -> (Block, BlockFees) {
|
||||
let wallet_retry_interval = 5;
|
||||
// get the latest chain state and build a block on top of it
|
||||
let mut result = self.async_build_block(wallet, txs, key_id.clone()).await;
|
||||
while let Err(e) = result {
|
||||
println!("Error: {:?}", &e);
|
||||
let mut new_key_id = key_id.to_owned();
|
||||
match e {
|
||||
grin_servers::common::types::Error::Chain(c) => match c {
|
||||
grin_chain::Error::DuplicateCommitment(_) => {
|
||||
debug!(
|
||||
"Duplicate commit for potential coinbase detected. Trying next derivation."
|
||||
);
|
||||
// use the next available key to generate a different coinbase commitment
|
||||
new_key_id = None;
|
||||
}
|
||||
_ => {
|
||||
error!("Chain Error: {}", c);
|
||||
}
|
||||
},
|
||||
grin_servers::common::types::Error::WalletComm(_) => {
|
||||
error!(
|
||||
"Error building new block: Can't connect to wallet listener; will retry"
|
||||
);
|
||||
async_std::task::sleep(Duration::from_secs(wallet_retry_interval)).await;
|
||||
}
|
||||
ae => {
|
||||
warn!("Error building new block: {:?}. Retrying.", ae);
|
||||
}
|
||||
}
|
||||
|
||||
// only wait if we are still using the same key: a different coinbase commitment is unlikely
|
||||
// to have duplication
|
||||
if new_key_id.is_some() {
|
||||
async_std::task::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
|
||||
result = self.async_build_block(wallet, txs, new_key_id).await;
|
||||
}
|
||||
return result.unwrap();
|
||||
}
|
||||
|
||||
/// Builds a new block with the chain head as previous and eligible
|
||||
/// transactions from the pool.
|
||||
async fn async_build_block(
|
||||
&self,
|
||||
wallet: &Arc<Mutex<IntegrationGrinWallet>>,
|
||||
txs: &Vec<Transaction>,
|
||||
key_id: Option<Identifier>,
|
||||
) -> Result<(Block, BlockFees), grin_servers::common::types::Error> {
|
||||
let head = self.chain.head_header()?;
|
||||
|
||||
// prepare the block header timestamp
|
||||
let mut now_sec = Utc::now().timestamp();
|
||||
let head_sec = head.timestamp.timestamp();
|
||||
if now_sec <= head_sec {
|
||||
now_sec = head_sec + 1;
|
||||
}
|
||||
|
||||
// Determine the difficulty our block should be at.
|
||||
// Note: do not keep the difficulty_iter in scope (it has an active batch).
|
||||
let difficulty = consensus::next_difficulty(head.height + 1, self.chain.difficulty_iter()?);
|
||||
|
||||
// build the coinbase and the block itself
|
||||
let fees = txs.iter().map(|tx| tx.fee()).sum();
|
||||
let height = head.height + 1;
|
||||
let block_fees = BlockFees {
|
||||
fees,
|
||||
key_id,
|
||||
height,
|
||||
};
|
||||
|
||||
let res = wallet.lock().async_create_coinbase(&block_fees).await?;
|
||||
let output = res.output;
|
||||
let kernel = res.kernel;
|
||||
let block_fees = BlockFees {
|
||||
key_id: res.key_id,
|
||||
..block_fees
|
||||
};
|
||||
let mut b = Block::from_reward(&head, &txs, output, kernel, difficulty.difficulty)?;
|
||||
|
||||
// making sure we're not spending time mining a useless block
|
||||
b.validate(&head.total_kernel_offset)?;
|
||||
|
||||
b.header.pow.nonce = thread_rng().gen();
|
||||
b.header.pow.secondary_scaling = difficulty.secondary_scaling;
|
||||
b.header.timestamp = DateTime::<Utc>::from_naive_utc_and_offset(
|
||||
NaiveDateTime::from_timestamp_opt(now_sec, 0).unwrap(),
|
||||
Utc,
|
||||
);
|
||||
|
||||
debug!(
|
||||
"Built new block with {} inputs and {} outputs, block difficulty: {}, cumulative difficulty {}",
|
||||
b.inputs().len(),
|
||||
b.outputs().len(),
|
||||
difficulty.difficulty,
|
||||
b.header.total_difficulty().to_num(),
|
||||
);
|
||||
|
||||
// Now set txhashset roots and sizes on the header of the block being built.
|
||||
match self.chain.set_txhashset_roots(&mut b) {
|
||||
Ok(_) => Ok((b, block_fees)),
|
||||
Err(e) => {
|
||||
match e {
|
||||
// If this is a duplicate commitment then likely trying to use
|
||||
// a key that hass already been derived but not in the wallet
|
||||
// for some reason, allow caller to retry.
|
||||
grin_chain::Error::DuplicateCommitment(e) => {
|
||||
Err(grin_servers::common::types::Error::Chain(
|
||||
grin_chain::Error::DuplicateCommitment(e),
|
||||
))
|
||||
}
|
||||
|
||||
// Some other issue, possibly duplicate kernel
|
||||
_ => {
|
||||
error!("Error setting txhashset root to build a block: {:?}", e);
|
||||
Err(grin_servers::common::types::Error::Chain(
|
||||
grin_chain::Error::Other(format!("{:?}", e)),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod miner;
|
||||
pub mod node;
|
||||
pub mod server;
|
||||
pub mod types;
|
||||
pub mod wallet;
|
||||
@@ -0,0 +1,122 @@
|
||||
extern crate grin_wallet_api as apiwallet;
|
||||
extern crate grin_wallet_config as wallet_config;
|
||||
extern crate grin_wallet_controller as wallet_controller;
|
||||
extern crate grin_wallet_impls as wallet;
|
||||
extern crate grin_wallet_libwallet as libwallet;
|
||||
|
||||
use futures::channel::oneshot;
|
||||
|
||||
use grin_core as core;
|
||||
|
||||
use grin_p2p as p2p;
|
||||
use grin_servers as servers;
|
||||
|
||||
use grin_util::logger::LogEntry;
|
||||
use grin_util::{Mutex, StopState};
|
||||
use std::default::Default;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use mwixnet::{GrinNode, HttpGrinNode};
|
||||
use std::sync::{mpsc, Arc};
|
||||
use std::thread;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct IntegrationGrinNode {
|
||||
server_config: servers::ServerConfig,
|
||||
stop_state: Arc<StopState>,
|
||||
server: Option<Arc<servers::Server>>,
|
||||
}
|
||||
|
||||
impl IntegrationGrinNode {
|
||||
pub fn start(&mut self) -> Arc<servers::Server> {
|
||||
let stop_state_thread = self.stop_state.clone();
|
||||
let server_config_thread = self.server_config.clone();
|
||||
|
||||
// Create a channel to communicate between threads
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
// Start the node in a new thread
|
||||
thread::spawn(move || {
|
||||
let api_chan: &'static mut (oneshot::Sender<()>, oneshot::Receiver<()>) =
|
||||
Box::leak(Box::new(oneshot::channel::<()>()));
|
||||
|
||||
servers::Server::start(
|
||||
server_config_thread.clone(),
|
||||
None,
|
||||
move |serv: servers::Server, _: Option<mpsc::Receiver<LogEntry>>| {
|
||||
// Signal that the callback has been called
|
||||
tx.send(serv).unwrap();
|
||||
// Do other necessary stuff here
|
||||
},
|
||||
Some(stop_state_thread.clone()),
|
||||
api_chan,
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
// Wait for the signal from the node-running thread
|
||||
let server = Arc::new(rx.recv().unwrap());
|
||||
self.server = Some(server.clone());
|
||||
|
||||
server
|
||||
}
|
||||
|
||||
pub fn stop(&self) {
|
||||
self.stop_state.stop();
|
||||
}
|
||||
|
||||
pub fn api_address(&self) -> SocketAddr {
|
||||
self.server_config.api_http_addr.parse().unwrap()
|
||||
}
|
||||
|
||||
pub fn to_client(&self) -> Arc<dyn GrinNode> {
|
||||
Arc::new(HttpGrinNode::new(&self.api_address(), &None))
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct GrinNodeManager {
|
||||
// base directory for the server instance
|
||||
working_dir: String,
|
||||
|
||||
nodes: Vec<Arc<Mutex<IntegrationGrinNode>>>,
|
||||
}
|
||||
|
||||
impl GrinNodeManager {
|
||||
pub fn new(test_dir: &str) -> GrinNodeManager {
|
||||
GrinNodeManager {
|
||||
working_dir: String::from(test_dir),
|
||||
nodes: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_node(&mut self) -> Arc<Mutex<IntegrationGrinNode>> {
|
||||
let server_config = servers::ServerConfig {
|
||||
api_http_addr: format!("127.0.0.1:{}", 20000 + self.nodes.len()),
|
||||
api_secret_path: None,
|
||||
db_root: format!("{}/nodes/{}", self.working_dir, self.nodes.len()),
|
||||
p2p_config: p2p::P2PConfig {
|
||||
port: 13414,
|
||||
seeding_type: p2p::Seeding::None,
|
||||
..p2p::P2PConfig::default()
|
||||
},
|
||||
chain_type: core::global::ChainTypes::AutomatedTesting,
|
||||
skip_sync_wait: Some(true),
|
||||
stratum_mining_config: None,
|
||||
..Default::default()
|
||||
};
|
||||
let node = Arc::new(Mutex::new(IntegrationGrinNode {
|
||||
server_config,
|
||||
stop_state: Arc::new(StopState::new()),
|
||||
server: None,
|
||||
}));
|
||||
self.nodes.push(node.clone());
|
||||
node
|
||||
}
|
||||
|
||||
pub fn stop_all(&self) {
|
||||
for node in &self.nodes {
|
||||
node.lock().stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
use crate::common::node::IntegrationGrinNode;
|
||||
use crate::common::wallet::{GrinWalletManager, IntegrationGrinWallet};
|
||||
use grin_core::core::Transaction;
|
||||
use grin_onion::crypto::comsig::ComSignature;
|
||||
use grin_onion::crypto::dalek::DalekPublicKey;
|
||||
use grin_onion::onion::Onion;
|
||||
use grin_wallet_impls::tor::process::TorProcess;
|
||||
use mwixnet::client::MixClientImpl;
|
||||
use mwixnet::{tor, SwapError, SwapServer, SwapStore};
|
||||
use secp256k1zkp::SecretKey;
|
||||
use std::iter;
|
||||
use std::net::TcpListener;
|
||||
use std::sync::Arc;
|
||||
use x25519_dalek::{PublicKey as xPublicKey, StaticSecret};
|
||||
|
||||
pub struct IntegrationSwapServer {
|
||||
server_key: SecretKey,
|
||||
tor_process: TorProcess,
|
||||
swap_server: Arc<tokio::sync::Mutex<dyn SwapServer>>,
|
||||
rpc_server: jsonrpc_http_server::Server,
|
||||
_wallet: Arc<grin_util::Mutex<IntegrationGrinWallet>>,
|
||||
}
|
||||
|
||||
impl IntegrationSwapServer {
|
||||
pub async fn async_swap(&self, onion: &Onion, comsig: &ComSignature) -> Result<(), SwapError> {
|
||||
self.swap_server.lock().await.swap(&onion, &comsig).await
|
||||
}
|
||||
|
||||
pub async fn async_execute_round(&self) -> Result<Option<Arc<Transaction>>, SwapError> {
|
||||
self.swap_server.lock().await.execute_round().await
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IntegrationMixServer {
|
||||
server_key: SecretKey,
|
||||
tor_process: TorProcess,
|
||||
rpc_server: jsonrpc_http_server::Server,
|
||||
_wallet: Arc<grin_util::Mutex<IntegrationGrinWallet>>,
|
||||
}
|
||||
|
||||
async fn async_new_swap_server(
|
||||
data_dir: &str,
|
||||
rt_handle: &tokio::runtime::Handle,
|
||||
wallets: &mut GrinWalletManager,
|
||||
server_key: &SecretKey,
|
||||
node: &Arc<grin_util::Mutex<IntegrationGrinNode>>,
|
||||
next_server: Option<&IntegrationMixServer>,
|
||||
) -> IntegrationSwapServer {
|
||||
let wallet = wallets.async_new_wallet(&node.lock().api_address()).await;
|
||||
|
||||
let server_config = mwixnet::ServerConfig {
|
||||
key: server_key.clone(),
|
||||
interval_s: 15,
|
||||
addr: TcpListener::bind("127.0.0.1:0")
|
||||
.unwrap()
|
||||
.local_addr()
|
||||
.unwrap(),
|
||||
socks_proxy_addr: TcpListener::bind("127.0.0.1:0")
|
||||
.unwrap()
|
||||
.local_addr()
|
||||
.unwrap(),
|
||||
grin_node_url: node.lock().api_address(),
|
||||
grin_node_secret_path: None,
|
||||
wallet_owner_url: wallet.lock().owner_address(),
|
||||
wallet_owner_secret_path: None,
|
||||
prev_server: None,
|
||||
next_server: match next_server {
|
||||
Some(s) => Some(DalekPublicKey::from_secret(&s.server_key)),
|
||||
None => None,
|
||||
},
|
||||
};
|
||||
|
||||
// Open SwapStore
|
||||
let store = SwapStore::new(format!("{}/db", data_dir).as_str()).unwrap();
|
||||
let tor_process = tor::init_tor_listener(&data_dir, &server_config).unwrap();
|
||||
|
||||
let (swap_server, rpc_server) = mwixnet::swap_listen(
|
||||
rt_handle,
|
||||
&server_config,
|
||||
match next_server {
|
||||
Some(s) => Some(Arc::new(MixClientImpl::new(
|
||||
server_config.clone(),
|
||||
DalekPublicKey::from_secret(&s.server_key),
|
||||
))),
|
||||
None => None,
|
||||
},
|
||||
wallet.lock().get_client(),
|
||||
node.lock().to_client(),
|
||||
store,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
IntegrationSwapServer {
|
||||
server_key: server_key.clone(),
|
||||
tor_process,
|
||||
swap_server,
|
||||
rpc_server,
|
||||
_wallet: wallet,
|
||||
}
|
||||
}
|
||||
|
||||
async fn async_new_mix_server(
|
||||
data_dir: &str,
|
||||
rt_handle: &tokio::runtime::Handle,
|
||||
wallets: &mut GrinWalletManager,
|
||||
server_key: &SecretKey,
|
||||
node: &Arc<grin_util::Mutex<IntegrationGrinNode>>,
|
||||
prev_server: DalekPublicKey,
|
||||
next_server: Option<&IntegrationMixServer>,
|
||||
) -> IntegrationMixServer {
|
||||
let wallet = wallets.async_new_wallet(&node.lock().api_address()).await;
|
||||
let server_config = mwixnet::ServerConfig {
|
||||
key: server_key.clone(),
|
||||
interval_s: 15,
|
||||
addr: TcpListener::bind("127.0.0.1:0")
|
||||
.unwrap()
|
||||
.local_addr()
|
||||
.unwrap(),
|
||||
socks_proxy_addr: TcpListener::bind("127.0.0.1:0")
|
||||
.unwrap()
|
||||
.local_addr()
|
||||
.unwrap(),
|
||||
grin_node_url: node.lock().api_address(),
|
||||
grin_node_secret_path: None,
|
||||
wallet_owner_url: wallet.lock().owner_address(),
|
||||
wallet_owner_secret_path: None,
|
||||
prev_server: Some(prev_server),
|
||||
next_server: match next_server {
|
||||
Some(s) => Some(DalekPublicKey::from_secret(&s.server_key)),
|
||||
None => None,
|
||||
},
|
||||
};
|
||||
|
||||
let tor_process = tor::init_tor_listener(&data_dir, &server_config).unwrap();
|
||||
|
||||
let (_, rpc_server) = mwixnet::mix_listen(
|
||||
rt_handle,
|
||||
server_config.clone(),
|
||||
match next_server {
|
||||
Some(s) => Some(Arc::new(MixClientImpl::new(
|
||||
server_config.clone(),
|
||||
DalekPublicKey::from_secret(&s.server_key),
|
||||
))),
|
||||
None => None,
|
||||
},
|
||||
wallet.lock().get_client(),
|
||||
node.lock().to_client(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
IntegrationMixServer {
|
||||
server_key: server_key.clone(),
|
||||
tor_process,
|
||||
rpc_server,
|
||||
_wallet: wallet,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Servers {
|
||||
pub swapper: IntegrationSwapServer,
|
||||
|
||||
pub mixers: Vec<IntegrationMixServer>,
|
||||
}
|
||||
|
||||
impl Servers {
|
||||
pub async fn async_setup(
|
||||
test_dir: &str,
|
||||
rt_handle: &tokio::runtime::Handle,
|
||||
wallets: &mut GrinWalletManager,
|
||||
node: &Arc<grin_util::Mutex<IntegrationGrinNode>>,
|
||||
num_mixers: usize,
|
||||
) -> Servers {
|
||||
// Pre-generate all server keys
|
||||
let server_keys: Vec<SecretKey> =
|
||||
iter::repeat_with(|| grin_onion::crypto::secp::random_secret())
|
||||
.take(num_mixers + 1)
|
||||
.collect();
|
||||
|
||||
// Build mixers in reverse order
|
||||
let mut mixers = Vec::new();
|
||||
for i in (0..num_mixers).rev() {
|
||||
let mix_server = async_new_mix_server(
|
||||
format!("{}/mixers/{}", test_dir, i).as_str(),
|
||||
rt_handle,
|
||||
wallets,
|
||||
&server_keys[i + 1],
|
||||
&node,
|
||||
DalekPublicKey::from_secret(&server_keys[i]),
|
||||
mixers.last(),
|
||||
)
|
||||
.await;
|
||||
println!(
|
||||
"Mixer {}: server_key={}, prev_server={}, next_server={}",
|
||||
i,
|
||||
DalekPublicKey::from_secret(&server_keys[i + 1]).to_hex(),
|
||||
DalekPublicKey::from_secret(&server_keys[i]).to_hex(),
|
||||
match mixers.last() {
|
||||
Some(s) => DalekPublicKey::from_secret(&s.server_key).to_hex(),
|
||||
None => "NONE".to_string(),
|
||||
},
|
||||
);
|
||||
mixers.push(mix_server);
|
||||
}
|
||||
mixers.reverse();
|
||||
|
||||
let swapper = async_new_swap_server(
|
||||
format!("{}/swapper", test_dir).as_str(),
|
||||
rt_handle,
|
||||
wallets,
|
||||
&server_keys[0],
|
||||
&node,
|
||||
mixers.first(),
|
||||
)
|
||||
.await;
|
||||
println!(
|
||||
"Swapper: server_key={}",
|
||||
DalekPublicKey::from_secret(&server_keys[0]).to_hex()
|
||||
);
|
||||
|
||||
Servers { swapper, mixers }
|
||||
}
|
||||
|
||||
pub fn get_pub_keys(&self) -> Vec<xPublicKey> {
|
||||
let mut pub_keys = vec![xPublicKey::from(&StaticSecret::from(
|
||||
self.swapper.server_key.0.clone(),
|
||||
))];
|
||||
for mixer in &self.mixers {
|
||||
pub_keys.push(xPublicKey::from(&StaticSecret::from(
|
||||
mixer.server_key.0.clone(),
|
||||
)))
|
||||
}
|
||||
pub_keys
|
||||
}
|
||||
|
||||
pub fn stop_all(&mut self) {
|
||||
self.swapper.rpc_server.close_handle().close();
|
||||
self.swapper.tor_process.kill().unwrap();
|
||||
|
||||
self.mixers.iter_mut().for_each(|mixer| {
|
||||
mixer.rpc_server.close_handle().close();
|
||||
mixer.tor_process.kill().unwrap();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
use grin_core::libtx::secp_ser;
|
||||
use grin_keychain::Identifier;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
/// Fees in block to use for coinbase amount calculation
|
||||
/// (Duplicated from Grin wallet project)
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct BlockFees {
|
||||
/// fees
|
||||
#[serde(with = "secp_ser::string_or_u64")]
|
||||
pub fees: u64,
|
||||
/// height
|
||||
#[serde(with = "secp_ser::string_or_u64")]
|
||||
pub height: u64,
|
||||
/// key id
|
||||
pub key_id: Option<Identifier>,
|
||||
}
|
||||
|
||||
impl BlockFees {
|
||||
/// return key id
|
||||
pub fn key_id(&self) -> Option<Identifier> {
|
||||
self.key_id.clone()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,431 @@
|
||||
use crate::common::types::BlockFees;
|
||||
use grin_api::client;
|
||||
use grin_api::json_rpc::Response;
|
||||
use grin_core::core::{FeeFields, Output, OutputFeatures, Transaction, TxKernel};
|
||||
use grin_core::global::ChainTypes;
|
||||
use grin_core::libtx::tx_fee;
|
||||
use grin_keychain::{BlindingFactor, ExtKeychain, Identifier, Keychain, SwitchCommitmentType};
|
||||
use grin_onion::crypto::comsig::ComSignature;
|
||||
use grin_onion::onion::Onion;
|
||||
use grin_onion::Hop;
|
||||
use grin_util::{Mutex, ToHex, ZeroingString};
|
||||
use grin_wallet_api::Owner;
|
||||
use grin_wallet_config::WalletConfig;
|
||||
use grin_wallet_controller::controller;
|
||||
use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl, HTTPNodeClient};
|
||||
use grin_wallet_libwallet::{InitTxArgs, Slate, VersionedSlate, WalletInfo, WalletInst};
|
||||
use log::error;
|
||||
use mwixnet::wallet::HttpWallet;
|
||||
use secp256k1zkp::pedersen::Commitment;
|
||||
use secp256k1zkp::{Secp256k1, SecretKey};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use x25519_dalek::PublicKey as xPublicKey;
|
||||
|
||||
/// Response to build a coinbase output.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct CbData {
|
||||
/// Output
|
||||
pub output: Output,
|
||||
/// Kernel
|
||||
pub kernel: TxKernel,
|
||||
/// Key Id
|
||||
pub key_id: Option<Identifier>,
|
||||
}
|
||||
|
||||
pub struct IntegrationGrinWallet {
|
||||
wallet: Arc<
|
||||
Mutex<
|
||||
Box<
|
||||
dyn WalletInst<
|
||||
'static,
|
||||
DefaultLCProvider<'static, HTTPNodeClient, ExtKeychain>,
|
||||
HTTPNodeClient,
|
||||
ExtKeychain,
|
||||
>,
|
||||
>,
|
||||
>,
|
||||
>,
|
||||
api_listen_port: u16,
|
||||
owner_api: Arc<
|
||||
Owner<DefaultLCProvider<'static, HTTPNodeClient, ExtKeychain>, HTTPNodeClient, ExtKeychain>,
|
||||
>,
|
||||
http_client: Arc<HttpWallet>,
|
||||
}
|
||||
|
||||
impl IntegrationGrinWallet {
|
||||
pub async fn async_new_wallet(
|
||||
wallet_dir: String,
|
||||
api_listen_port: u16,
|
||||
node_api: String,
|
||||
) -> IntegrationGrinWallet {
|
||||
let node_client = HTTPNodeClient::new(&node_api, None).unwrap();
|
||||
let mut wallet = Box::new(
|
||||
DefaultWalletImpl::<'static, HTTPNodeClient>::new(node_client.clone()).unwrap(),
|
||||
)
|
||||
as Box<
|
||||
dyn WalletInst<
|
||||
'static,
|
||||
DefaultLCProvider<HTTPNodeClient, ExtKeychain>,
|
||||
HTTPNodeClient,
|
||||
ExtKeychain,
|
||||
>,
|
||||
>;
|
||||
|
||||
// Wallet LifeCycle Provider provides all functions init wallet and work with seeds, etc...
|
||||
let lc = wallet.lc_provider().unwrap();
|
||||
|
||||
let mut wallet_config = WalletConfig::default();
|
||||
wallet_config.check_node_api_http_addr = node_api.clone();
|
||||
wallet_config.owner_api_listen_port = Some(api_listen_port);
|
||||
wallet_config.api_secret_path = None;
|
||||
wallet_config.data_file_dir = wallet_dir.clone();
|
||||
|
||||
// The top level wallet directory should be set manually (in the reference implementation,
|
||||
// this is provided in the WalletConfig)
|
||||
let _ = lc.set_top_level_directory(&wallet_config.data_file_dir);
|
||||
|
||||
lc.create_config(
|
||||
&ChainTypes::AutomatedTesting,
|
||||
"grin-wallet.toml",
|
||||
Some(wallet_config.clone()),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
lc.create_wallet(None, None, 12, ZeroingString::from("pass"), false)
|
||||
.unwrap();
|
||||
|
||||
// Start owner API
|
||||
let km = Arc::new(Mutex::new(None));
|
||||
let wallet = Arc::new(Mutex::new(wallet));
|
||||
let owner_api = Arc::new(Owner::new(wallet.clone(), None));
|
||||
|
||||
let address_str = format!("127.0.0.1:{}", api_listen_port);
|
||||
let owner_addr: SocketAddr = address_str.parse().unwrap();
|
||||
let thr_wallet = wallet.clone();
|
||||
let _thread_handle = thread::spawn(move || {
|
||||
controller::owner_listener(
|
||||
thr_wallet,
|
||||
km,
|
||||
address_str.as_str(),
|
||||
None,
|
||||
None,
|
||||
Some(true),
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
let http_client = Arc::new(
|
||||
HttpWallet::async_open_wallet(&owner_addr, &None, &ZeroingString::from("pass"))
|
||||
.await
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
IntegrationGrinWallet {
|
||||
wallet,
|
||||
api_listen_port,
|
||||
owner_api,
|
||||
http_client,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn async_retrieve_summary_info(&self) -> Result<WalletInfo, mwixnet::WalletError> {
|
||||
let params = json!({
|
||||
"token": self.http_client.clone().get_token(),
|
||||
"refresh_from_node": true,
|
||||
"minimum_confirmations": 1
|
||||
});
|
||||
let (_, wallet_info): (bool, WalletInfo) = self
|
||||
.http_client
|
||||
.clone()
|
||||
.async_perform_request("retrieve_summary_info", ¶ms)
|
||||
.await?;
|
||||
Ok(wallet_info)
|
||||
}
|
||||
|
||||
pub async fn async_send(
|
||||
&self,
|
||||
receiving_wallet: &IntegrationGrinWallet,
|
||||
amount: u64,
|
||||
) -> Result<Transaction, mwixnet::WalletError> {
|
||||
let slate = self.async_init_send_tx(amount).await.unwrap();
|
||||
let slate = receiving_wallet.async_receive_tx(&slate).await.unwrap();
|
||||
let slate = self.async_finalize_tx(&slate).await.unwrap();
|
||||
let tx = Slate::from(slate).tx_or_err().unwrap().clone();
|
||||
Ok(tx)
|
||||
}
|
||||
|
||||
async fn async_init_send_tx(
|
||||
&self,
|
||||
amount: u64,
|
||||
) -> Result<VersionedSlate, mwixnet::WalletError> {
|
||||
let args = InitTxArgs {
|
||||
src_acct_name: None,
|
||||
amount,
|
||||
minimum_confirmations: 0,
|
||||
max_outputs: 10,
|
||||
num_change_outputs: 1,
|
||||
selection_strategy_is_use_all: false,
|
||||
..Default::default()
|
||||
};
|
||||
let params = json!({
|
||||
"token": self.http_client.clone().get_token(),
|
||||
"args": args
|
||||
});
|
||||
|
||||
let slate: VersionedSlate = self
|
||||
.http_client
|
||||
.clone()
|
||||
.async_perform_request("init_send_tx", ¶ms)
|
||||
.await?;
|
||||
|
||||
let params = json!({
|
||||
"token": self.http_client.clone().get_token(),
|
||||
"slate": &slate
|
||||
});
|
||||
self.http_client
|
||||
.clone()
|
||||
.async_perform_request("tx_lock_outputs", ¶ms)
|
||||
.await?;
|
||||
|
||||
Ok(slate)
|
||||
}
|
||||
|
||||
pub async fn async_receive_tx(
|
||||
&self,
|
||||
slate: &VersionedSlate,
|
||||
) -> Result<VersionedSlate, grin_servers::common::types::Error> {
|
||||
let req_body = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "receive_tx",
|
||||
"id": 1,
|
||||
"params": [slate, null, null]
|
||||
});
|
||||
|
||||
let res: Response = client::post_async(self.foreign_api().as_str(), &req_body, None)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let report = format!("Failed to receive tx. Is the wallet listening? {}", e);
|
||||
error!("{}", report);
|
||||
grin_servers::common::types::Error::WalletComm(report)
|
||||
})?;
|
||||
|
||||
let parsed: VersionedSlate = res.clone().into_result().map_err(|e| {
|
||||
let report = format!("Error parsing result: {}", e);
|
||||
error!("{}", report);
|
||||
grin_servers::common::types::Error::WalletComm(report)
|
||||
})?;
|
||||
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
async fn async_finalize_tx(
|
||||
&self,
|
||||
slate: &VersionedSlate,
|
||||
) -> Result<VersionedSlate, mwixnet::WalletError> {
|
||||
let params = json!({
|
||||
"token": self.http_client.clone().get_token(),
|
||||
"slate": slate
|
||||
});
|
||||
|
||||
self.http_client
|
||||
.clone()
|
||||
.async_perform_request("finalize_tx", ¶ms)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn async_post_tx(
|
||||
&self,
|
||||
finalized_slate: &VersionedSlate,
|
||||
fluff: bool,
|
||||
) -> Result<VersionedSlate, mwixnet::WalletError> {
|
||||
let params = json!({
|
||||
"token": self.http_client.clone().get_token(),
|
||||
"slate": finalized_slate,
|
||||
"fluff": fluff
|
||||
});
|
||||
|
||||
self.http_client
|
||||
.clone()
|
||||
.async_perform_request("post_tx", ¶ms)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Call the wallet API to create a coinbase output for the given block_fees.
|
||||
/// Will retry based on default "retry forever with backoff" behavior.
|
||||
pub async fn async_create_coinbase(
|
||||
&self,
|
||||
block_fees: &BlockFees,
|
||||
) -> Result<CbData, grin_servers::common::types::Error> {
|
||||
let req_body = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "build_coinbase",
|
||||
"id": 1,
|
||||
"params": {
|
||||
"block_fees": block_fees
|
||||
}
|
||||
});
|
||||
|
||||
let res: Response = client::post_async(self.foreign_api().as_str(), &req_body, None)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let report = format!("Failed to get coinbase. Is the wallet listening? {}", e);
|
||||
error!("{}", report);
|
||||
grin_servers::common::types::Error::WalletComm(report)
|
||||
})?;
|
||||
let parsed: CbData = res.clone().into_result().map_err(|e| {
|
||||
let report = format!("Error parsing result: {}", e);
|
||||
error!("{}", report);
|
||||
grin_servers::common::types::Error::WalletComm(report)
|
||||
})?;
|
||||
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
pub fn build_onion(
|
||||
&self,
|
||||
commitment: &Commitment,
|
||||
server_pubkeys: &Vec<xPublicKey>,
|
||||
) -> Result<(Onion, ComSignature), grin_wallet_libwallet::Error> {
|
||||
let keychain = self
|
||||
.wallet
|
||||
.lock()
|
||||
.lc_provider()?
|
||||
.wallet_inst()?
|
||||
.keychain(self.keychain_mask().as_ref())?;
|
||||
let (_, outputs) =
|
||||
self.owner_api
|
||||
.retrieve_outputs(self.keychain_mask().as_ref(), false, false, None)?;
|
||||
|
||||
let mut output = None;
|
||||
for o in &outputs {
|
||||
if o.commit == *commitment {
|
||||
output = Some(o.output.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if output.is_none() {
|
||||
return Err(grin_wallet_libwallet::Error::GenericError(String::from(
|
||||
"output not found",
|
||||
)));
|
||||
}
|
||||
|
||||
let amount = output.clone().unwrap().value;
|
||||
let input_blind = keychain.derive_key(
|
||||
amount,
|
||||
&output.clone().unwrap().key_id,
|
||||
SwitchCommitmentType::Regular,
|
||||
)?;
|
||||
|
||||
let fee = tx_fee(1, 1, 1);
|
||||
let new_amount = amount - (fee * server_pubkeys.len() as u64);
|
||||
let new_output = self.owner_api.build_output(
|
||||
self.keychain_mask().as_ref(),
|
||||
OutputFeatures::Plain,
|
||||
new_amount,
|
||||
)?;
|
||||
|
||||
let secp = Secp256k1::new();
|
||||
let mut blind_sum = new_output
|
||||
.blind
|
||||
.split(&BlindingFactor::from_secret_key(input_blind.clone()), &secp)?;
|
||||
|
||||
let hops = server_pubkeys
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &p)| {
|
||||
if (i + 1) == server_pubkeys.len() {
|
||||
Hop {
|
||||
server_pubkey: p.clone(),
|
||||
excess: blind_sum.secret_key(&secp).unwrap(),
|
||||
fee: FeeFields::from(fee as u32),
|
||||
rangeproof: Some(new_output.output.proof.clone()),
|
||||
}
|
||||
} else {
|
||||
let hop_excess = BlindingFactor::rand(&secp);
|
||||
blind_sum = blind_sum.split(&hop_excess, &secp).unwrap();
|
||||
Hop {
|
||||
server_pubkey: p.clone(),
|
||||
excess: hop_excess.secret_key(&secp).unwrap(),
|
||||
fee: FeeFields::from(fee as u32),
|
||||
rangeproof: None,
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let onion = grin_onion::create_onion(&commitment, &hops).unwrap();
|
||||
let comsig = ComSignature::sign(amount, &input_blind, &onion.serialize().unwrap()).unwrap();
|
||||
|
||||
Ok((onion, comsig))
|
||||
}
|
||||
|
||||
pub fn owner_api(
|
||||
&self,
|
||||
) -> Arc<
|
||||
Owner<DefaultLCProvider<'static, HTTPNodeClient, ExtKeychain>, HTTPNodeClient, ExtKeychain>,
|
||||
> {
|
||||
self.owner_api.clone()
|
||||
}
|
||||
|
||||
pub fn foreign_api(&self) -> String {
|
||||
format!("http://127.0.0.1:{}/v2/foreign", self.api_listen_port)
|
||||
}
|
||||
|
||||
pub fn owner_address(&self) -> SocketAddr {
|
||||
SocketAddr::new(
|
||||
IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
|
||||
self.api_listen_port,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn keychain_mask(&self) -> Option<SecretKey> {
|
||||
self.http_client.as_ref().get_token().keychain_mask.clone()
|
||||
}
|
||||
|
||||
pub fn get_client(&self) -> Arc<HttpWallet> {
|
||||
self.http_client.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct GrinWalletManager {
|
||||
// base directory for the server instance
|
||||
working_dir: String,
|
||||
|
||||
wallets: Vec<Arc<Mutex<IntegrationGrinWallet>>>,
|
||||
}
|
||||
|
||||
impl GrinWalletManager {
|
||||
pub fn new(test_dir: &str) -> GrinWalletManager {
|
||||
GrinWalletManager {
|
||||
working_dir: String::from(test_dir),
|
||||
wallets: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn async_new_wallet(
|
||||
&mut self,
|
||||
node_api_addr: &SocketAddr,
|
||||
) -> Arc<Mutex<IntegrationGrinWallet>> {
|
||||
let wallet_dir = format!("{}/wallets/{}", self.working_dir, self.wallets.len());
|
||||
let wallet = Arc::new(Mutex::new(
|
||||
IntegrationGrinWallet::async_new_wallet(
|
||||
wallet_dir,
|
||||
21000 + self.wallets.len() as u16,
|
||||
format!("http://{}", node_api_addr),
|
||||
)
|
||||
.await,
|
||||
));
|
||||
self.wallets.push(wallet.clone());
|
||||
wallet
|
||||
}
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
use crate::common::miner::Miner;
|
||||
use crate::common::node::GrinNodeManager;
|
||||
use crate::common::server::Servers;
|
||||
use crate::common::wallet::GrinWalletManager;
|
||||
use function_name::named;
|
||||
use grin_core::global;
|
||||
use grin_util::logger::LoggingConfig;
|
||||
use log::Level;
|
||||
use std::ops::Deref;
|
||||
|
||||
mod common;
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
/// Just removes all results from previous runs
|
||||
fn clean_all_output(test_dir: &str) {
|
||||
if let Err(e) = remove_dir_all::remove_dir_all(test_dir) {
|
||||
println!("can't remove output from previous test :{}, may be ok", e);
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_test(test_name: &str) -> (GrinNodeManager, GrinWalletManager, String) {
|
||||
let test_dir = format!("./target/tmp/.{}", test_name);
|
||||
clean_all_output(test_dir.as_str());
|
||||
|
||||
let mut logger = LoggingConfig::default();
|
||||
logger.log_to_file = false;
|
||||
logger.stdout_log_level = Level::Error;
|
||||
grin_util::init_logger(Some(logger), None);
|
||||
global::set_local_chain_type(global::ChainTypes::AutomatedTesting);
|
||||
global::set_local_accept_fee_base(50_000_000);
|
||||
global::init_global_chain_type(global::ChainTypes::AutomatedTesting);
|
||||
|
||||
let nodes = GrinNodeManager::new(test_dir.as_str());
|
||||
let wallets = GrinWalletManager::new(test_dir.as_str());
|
||||
|
||||
(nodes, wallets, test_dir)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[named]
|
||||
fn integration_test() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (mut nodes, mut wallets, test_dir) = setup_test(function_name!());
|
||||
let mut rt = tokio::runtime::Builder::new()
|
||||
.threaded_scheduler()
|
||||
.enable_all()
|
||||
.build()?;
|
||||
|
||||
// Create node
|
||||
let node1 = nodes.new_node();
|
||||
let node1_url = node1.lock().api_address();
|
||||
let node1_server = node1.lock().start();
|
||||
|
||||
// Setup swap & mix servers and their wallets
|
||||
let rt_handle = rt.handle().clone();
|
||||
let mut servers = rt.block_on(Servers::async_setup(
|
||||
test_dir.as_str(),
|
||||
&rt_handle,
|
||||
&mut wallets,
|
||||
&node1,
|
||||
2usize,
|
||||
));
|
||||
|
||||
rt.block_on(async {
|
||||
// Setup wallet to use with miner
|
||||
let mining_wallet = wallets.async_new_wallet(&node1_url).await;
|
||||
|
||||
// Mine enough blocks to have spendable coins
|
||||
let miner = Miner::new(node1_server.chain.clone());
|
||||
miner
|
||||
.async_mine_empty_blocks(&mining_wallet, 5 + global::coinbase_maturity() as usize)
|
||||
.await;
|
||||
|
||||
// Setup wallets for swap users
|
||||
let user1_wallet = wallets.async_new_wallet(&node1_url).await;
|
||||
let user2_wallet = wallets.async_new_wallet(&node1_url).await;
|
||||
|
||||
// Send from mining_wallet to user1_wallet
|
||||
let tx1 = mining_wallet
|
||||
.lock()
|
||||
.async_send(user1_wallet.lock().deref(), 10_000_000_000)
|
||||
.await
|
||||
.unwrap();
|
||||
let tx2 = mining_wallet
|
||||
.lock()
|
||||
.async_send(user2_wallet.lock().deref(), 20_000_000_000)
|
||||
.await
|
||||
.unwrap();
|
||||
miner
|
||||
.async_mine_next_block(&mining_wallet, &vec![tx1, tx2])
|
||||
.await;
|
||||
|
||||
let user1_km = user1_wallet.lock().keychain_mask();
|
||||
let (_, outputs) = user1_wallet
|
||||
.lock()
|
||||
.owner_api()
|
||||
.retrieve_outputs(user1_km.as_ref(), false, false, None)
|
||||
.unwrap();
|
||||
assert_eq!(outputs.len(), 1);
|
||||
for output in &outputs {
|
||||
let (onion, comsig) = user1_wallet
|
||||
.lock()
|
||||
.build_onion(&output.commit, &servers.get_pub_keys())
|
||||
.unwrap();
|
||||
servers.swapper.async_swap(&onion, &comsig).await.unwrap();
|
||||
}
|
||||
|
||||
let mining_wallet_info = mining_wallet
|
||||
.lock()
|
||||
.async_retrieve_summary_info()
|
||||
.await
|
||||
.unwrap();
|
||||
println!("Mining wallet: {:?}", mining_wallet_info);
|
||||
let user1_wallet_info = user1_wallet
|
||||
.lock()
|
||||
.async_retrieve_summary_info()
|
||||
.await
|
||||
.unwrap();
|
||||
println!("User1 wallet: {:?}", user1_wallet_info);
|
||||
let user2_wallet_info = user2_wallet
|
||||
.lock()
|
||||
.async_retrieve_summary_info()
|
||||
.await
|
||||
.unwrap();
|
||||
println!("User2 wallet: {:?}", user2_wallet_info);
|
||||
|
||||
let _tx = servers.swapper.async_execute_round().await.unwrap();
|
||||
});
|
||||
|
||||
servers.stop_all();
|
||||
nodes.stop_all();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user