* cleanup
* error handling
This commit is contained in:
mfahampshire
2023-07-18 15:28:59 +02:00
parent 83f58aa1a3
commit 3d299f80fd
5 changed files with 239 additions and 178 deletions
+46 -23
View File
@@ -1,12 +1,17 @@
use clap::{Parser, Subcommand, Args};
use clap::{Args, Parser, Subcommand};
use nym_sdk::mixnet::Recipient;
use nym_validator_client::nyxd::AccountId;
use rust_cosmos_broadcaster::{client::{offline_sign, send_tx}, create_client};
use nym_bin_common::logging::setup_logging;
use rust_cosmos_broadcaster::{
client::{offline_sign, send_tx},
create_client,
};
#[derive(Debug, Parser)]
#[clap(name = "nym cosmos tx signer ")]
#[clap(about = "demo binary with which users can perform offline signing and transmission of signed tx to broadcaster via the mixnet ")]
#[clap(
about = "demo binary with which users can perform offline signing and transmission of signed tx to broadcaster via the mixnet "
)]
struct Cli {
#[clap(subcommand)]
command: Option<Commands>,
@@ -14,10 +19,10 @@ struct Cli {
#[derive(Debug, Subcommand)]
enum Commands {
/// sign a transaction offline
/// sign a transaction offline
OfflineSignTx(OfflineSignTx),
/// send signed tx to SP for broadcast
SendTx(SendTx)
SendTx(SendTx),
}
#[derive(Debug, Clone, Args)]
@@ -25,7 +30,7 @@ struct OfflineSignTx {
/// mnemonic of signing + sending account (you!) - TODO this will be removed and replaced with file
mnemonic: bip39::Mnemonic,
/// recipient nyx chain address for token transfer
nyx_token_receipient: AccountId
nyx_token_receipient: AccountId,
}
#[derive(Debug, Args)]
@@ -35,43 +40,61 @@ struct SendTx {
}
#[tokio::main]
async fn main() {
async fn main() -> anyhow::Result<()> {
// setup_logging();
let cli = Cli::parse();
let mut client = create_client("/tmp/cosmos-broadcaster-mixnet-client-2".into()).await;
let mut client = create_client("/tmp/cosmos-broadcaster-mixnet-client-5".into()).await;
let our_address = client.nym_address();
println!("\nclient's nym address: {our_address}\n");
let sp_address = Recipient::try_from_base58_string("HfbesQm2pRYCN4BAdYXhkqXBbV1Pp929mtKsESVeWXh8.8AgoUPUQbXNBCPaqAaWd3vnxhc9484qwfgrrQwBngQk2@Ck8zpXTSXMtS9YZ7k7a5BiaoLZfffWuqGWLndujh4Lw4").unwrap();
let sp_address = Recipient::try_from_base58_string("DP84PUbje5nMuz4HYSqpdPYHrb5WPjitTyufGubc6MNy.8YhkurLGEeSuRLxhKG5uY7Kz6M4YjytKpUNdZhmo8z56@HcH4JQ4oZ8M4mMXDj5UDAb4WpuhpTKGHBEsZ112mkPkm").unwrap();
match &cli.command {
Some(Commands::OfflineSignTx(OfflineSignTx { mnemonic, nyx_token_receipient} )) => {
println!("sending offline sign info to broadcaster via the mixnet: getting signing account sequence and chain ID");
let base58_tx_bytes = offline_sign(mnemonic.clone(), nyx_token_receipient.clone(), &mut client, sp_address).await;
Some(Commands::OfflineSignTx(OfflineSignTx {
mnemonic,
nyx_token_receipient,
})) => {
println!("sending offline sign info to broadcaster via the mixnet: getting signing account sequence and chain ID");
let base58_tx_bytes = offline_sign(
mnemonic.clone(),
nyx_token_receipient.clone(),
&mut client,
sp_address,
)
.await;
println!("Encoded response (signed tx data) as base58 for tx broadcast: \n\n{}\n", &base58_tx_bytes);
println!(
"Encoded response (signed tx data) as base58 for tx broadcast: \n\n{:?}\n",
&base58_tx_bytes
);
println!("do you wish to send the tx? y/n");
let mut input = String::new();
let stdin = std::io::stdin();
let _n = stdin.read_line(&mut input).unwrap();
if input.starts_with('y') {
if input.starts_with('y') {
println!("\nsending tx thru the mixnet to broadcaster service");
let (tx_hash, success) = send_tx(base58_tx_bytes, sp_address, &mut client).await;
println!("tx hash returned from the broadcaster: {}\ntx was successful: {}", tx_hash, success);
let Ok((tx_hash, success)) = send_tx(base58_tx_bytes.unwrap(), sp_address, &mut client).await else { todo!() };
println!(
"tx hash returned from the broadcaster: {}\ntx was successful: {}",
tx_hash, success
);
} else if input.starts_with('n') {
println!("\nok, you can send the signed tx at a later date by passing the base58 string above as the argument for send-tx");
} else {
println!("\nok, you can send the signed tx at a later date by passing the base58 string above as the argument for send-tx");
} else {
println!("\nunrecognised user input");
}
}
Some(Commands::SendTx(SendTx { base58_payload } )) => {
Some(Commands::SendTx(SendTx { base58_payload })) => {
let tx_hash = send_tx(base58_payload.clone(), sp_address, &mut client).await;
println!("response from the broadcaster (tx hash) {:#?}", tx_hash);
}
None => {println!("\nno command specified - nothing to do")}
None => {
println!("\nno command specified - nothing to do")
}
}
println!("\ndisconnecting client");
println!("end")
println!("\ndisconnecting client");
println!("end");
Ok(())
}
+68 -52
View File
@@ -1,68 +1,84 @@
use nym_sdk::mixnet::ReconstructedMessage;
use nym_bin_common::logging::setup_logging;
use nym_sphinx_anonymous_replies::{self, requests::AnonymousSenderTag};
use rust_cosmos_broadcaster::{RequestTypes, SequenceRequestResponse, BroadcastResponse, service::{get_sequence, broadcast, create_broadcaster}, create_client};
use rust_cosmos_broadcaster::{
create_client,
service::{broadcast, create_broadcaster, get_sequence},
BroadcastResponse, RequestTypes, SequenceRequestResponse,
};
#[tokio::main]
async fn main() {
async fn main() -> anyhow::Result<()> {
// setup_logging();
let mut client = create_client("/tmp/cosmos-broadcaster-mixnet-server-2".into()).await;
let mut client = create_client("/tmp/cosmos-broadcaster-mixnet-server-3".into()).await;
let our_address = client.nym_address();
println!("\nservice's nym address: {our_address}");
// the httpclient we will use to broadcast our signed tx to the Nyx blockchain
let broadcaster = create_broadcaster().await;
// the httpclient we will use to broadcast our signed tx to the Nyx blockchain
let broadcaster = create_broadcaster().await;
loop {
// check incoming is empty - SURB requests also send data ( empty vec ) along
let mut received: Vec<ReconstructedMessage> = Vec::new();
// get the actual message - discard the empty vec sent along with the SURB request
loop {
// check incoming is empty - SURB requests also send data ( empty vec ) along
let mut received: Vec<ReconstructedMessage> = Vec::new();
// get the actual message - discard the empty vec sent along with the SURB request
while let Some(new_message) = client.wait_for_messages().await {
if new_message.is_empty() {
continue;
}
}
received = new_message;
break
}
break;
}
for r in received.iter() {
// convert incoming vec<u8> -> String
let s = String::from_utf8(r.message.clone());
if s.is_ok() {
let p = s.unwrap();
// parse JSON string -> request type & match
let request: RequestTypes = serde_json::from_str(&p).unwrap();
match request {
RequestTypes::Sequence(request) => {
println!("\nincoming sequence request details:\nsigner address: {}", request.signer_address);
let sequence: SequenceRequestResponse = get_sequence(broadcaster.clone(), request.signer_address).await;
if Some(r.sender_tag).is_some() {
let return_recipient: AnonymousSenderTag = r.sender_tag.unwrap();
println!("replying to sender with sequence response from Nyx chain");
println!("return recipient surb bucket: {}", &return_recipient);
client.send_str_reply(return_recipient, &serde_json::to_string(&sequence).unwrap()).await;
} else {
// TODO replace with actual error type to return
println!("no surbs cannot reply an0n")
}
},
RequestTypes::Broadcast(request) => {
println!("\nincoming broadcast request: {}\n", request.base58_tx_bytes);
let tx_hash: BroadcastResponse = broadcast(request.base58_tx_bytes, broadcaster.clone()).await;
if Some(r.sender_tag).is_some() {
let return_recipient: AnonymousSenderTag = r.sender_tag.unwrap();
println!("return recipient surb bucket: {}", &return_recipient);
client.send_str_reply(return_recipient, &serde_json::to_string(&tx_hash).unwrap()).await;
} else {
// TODO replace with actual error type to return
println!("no surbs cannot reply an0n")
}
}
}
}
}
// convert incoming vec<u8> -> String
let s = String::from_utf8(r.message.clone());
if s.is_ok() {
let p = s.unwrap();
// parse JSON string -> request type & match
let request: RequestTypes = serde_json::from_str(&p).unwrap();
match request {
RequestTypes::Sequence(request) => {
println!(
"\nincoming sequence request details:\nsigner address: {}",
request.signer_address
);
let sequence: SequenceRequestResponse =
get_sequence(broadcaster.clone(), request.signer_address).await.unwrap();
if Some(r.sender_tag).is_some() {
let return_recipient: AnonymousSenderTag = r.sender_tag.unwrap();
println!("replying to sender with sequence response from Nyx chain");
println!("return recipient surb bucket: {}", &return_recipient);
client
.send_str_reply(
return_recipient,
&serde_json::to_string(&sequence).unwrap(),
)
.await;
} else {
println!("no surbs cannot reply an0n")
}
}
RequestTypes::Broadcast(request) => {
println!(
"\nincoming broadcast request: {}\n",
request.base58_tx_bytes
);
let tx_hash: BroadcastResponse =
broadcast(request.base58_tx_bytes, broadcaster.clone()).await.unwrap();
if Some(r.sender_tag).is_some() {
let return_recipient: AnonymousSenderTag = r.sender_tag.unwrap();
println!("return recipient surb bucket: {}", &return_recipient);
client
.send_str_reply(
return_recipient,
&serde_json::to_string(&tx_hash).unwrap(),
)
.await;
} else {
println!("no surbs cannot reply an0n")
}
}
}
}
}
}
}
+65 -58
View File
@@ -1,26 +1,23 @@
use nym_sphinx_addressing::clients::Recipient;
use nym_validator_client::signing::direct_wallet::DirectSecp256k1HdWallet;
use nym_validator_client::signing::tx_signer::TxSigner;
use nym_validator_client::signing::SignerData;
use nym_validator_client::nyxd::cosmwasm_client::types;
use crate::{DEFAULT_DENOM, DEFAULT_PREFIX, DEFAULT_VALIDATOR_RPC};
use bip39;
use bs58;
use cosmrs::bank::MsgSend;
use cosmrs::tx::Msg;
use cosmrs::{tx, AccountId, Coin, Denom};
use bip39;
use bs58;
use nym_sdk::mixnet::{MixnetClient, ReconstructedMessage};
use crate::{DEFAULT_VALIDATOR_RPC, DEFAULT_DENOM, DEFAULT_PREFIX, ResponseTypes};
use nym_sphinx_addressing::clients::Recipient;
use nym_validator_client::nyxd::cosmwasm_client::types;
use nym_validator_client::signing::direct_wallet::DirectSecp256k1HdWallet;
use nym_validator_client::signing::tx_signer::TxSigner;
use nym_validator_client::signing::SignerData;
// parse incoming: ignore empty SURB data packets + parse incoming message to struct or error
// we know we are expecting JSON here but an irl helper would parse conditionally on bytes / string incoming
pub fn parse_incoming(incoming: Option<Vec<ReconstructedMessage>>) -> ResponseTypes {
todo!()
}
// TODO take coin amount from function args
pub async fn offline_sign(mnemonic: bip39::Mnemonic, to: AccountId, client: &mut MixnetClient , sp_address: Recipient) -> String {
let denom: Denom = DEFAULT_DENOM.parse().unwrap();
pub async fn offline_sign(
mnemonic: bip39::Mnemonic,
to: AccountId,
client: &mut MixnetClient,
sp_address: Recipient,
) -> Result<String, std::io::Error> {
let denom: Denom = DEFAULT_DENOM.parse().unwrap();
let signer = DirectSecp256k1HdWallet::from_mnemonic(DEFAULT_PREFIX, mnemonic.clone());
let signer_address = signer.try_derive_accounts().unwrap()[0].address().clone();
@@ -28,45 +25,49 @@ pub async fn offline_sign(mnemonic: bip39::Mnemonic, to: AccountId, client: &mut
let tx_signer = TxSigner::new(signer);
// sequence request type
let message = crate::SequenceRequest{
let message = crate::SequenceRequest {
validator: DEFAULT_VALIDATOR_RPC.to_owned(), // rpc endpoint for broadcaster to use
signer_address, // our (sender) address, derived from mnemonic
signer_address, // our (sender) address, derived from mnemonic
};
// send req to client
client.send_str(sp_address, &serde_json::to_string(&message).unwrap()).await;
client
.send_str(sp_address, &serde_json::to_string(&message).unwrap())
.await;
// handle incoming message - we presume its a reply from the SP
let mut message: Vec<ReconstructedMessage> = Vec::new();
// get the actual message - discard the empty vec sent along with the SURB topup request
while let Some(new_message) = client.wait_for_messages().await {
if new_message.is_empty() {
continue;
}
if new_message.is_empty() {
continue;
}
message = new_message;
break
break;
}
// parse vec<u8> -> JSON String
let mut parsed = String::new();
for r in message.iter() {
parsed = String::from_utf8(r.message.clone()).unwrap();
break
};
if let Some(r) = message.iter().next() {
parsed = String::from_utf8(r.message.clone()).unwrap();
}
let sp_response: crate::ResponseTypes = serde_json::from_str(&parsed).unwrap();
// match JSON -> ResponseType
let res = match sp_response {
crate::ResponseTypes::Sequence(request) => {
println!("got a response to the chain sequence request. using this to sign our tx offline");
println!(
"got a response to the chain sequence request. using this to sign our tx offline"
);
// use the response to create SignerData instance
let sequence_response = types::SequenceResponse {
account_number: request.account_number,
sequence: request.sequence
sequence: request.sequence,
};
let signer_data = SignerData::new_from_sequence_response( sequence_response, request.chain_id);
let signer_data =
SignerData::new_from_sequence_response(sequence_response, request.chain_id);
// create (and sign) the send message
let amount = vec![Coin {
@@ -74,7 +75,6 @@ pub async fn offline_sign(mnemonic: bip39::Mnemonic, to: AccountId, client: &mut
amount: 12345u32.into(),
}];
// TODO there must be a better way of doing this instead of re-generating the signer address from the mnemonic twice
let signer = DirectSecp256k1HdWallet::from_mnemonic(DEFAULT_PREFIX, mnemonic.clone());
let signer_address = signer.try_derive_accounts().unwrap()[0].address().clone();
@@ -102,22 +102,30 @@ pub async fn offline_sign(mnemonic: bip39::Mnemonic, to: AccountId, client: &mut
let tx_bytes = tx_raw.to_bytes().unwrap();
let base58_tx_bytes = bs58::encode(tx_bytes).into_string();
base58_tx_bytes
},
// TODO make this a proper error
_ => { println!("weird response"); String::from("placeholder error") }
}
_ => {
String::from("unexpected response")
}
};
res
Ok(res)
}
pub async fn send_tx(base58_tx: String, sp_address: Recipient, client: &mut MixnetClient) -> (String, bool) {
pub async fn send_tx(
base58_tx: String,
sp_address: Recipient,
client: &mut MixnetClient,
) -> Result<(String, bool), std::io::Error> {
let broadcast_request = crate::BroadcastRequest {
base58_tx_bytes: base58_tx
base58_tx_bytes: base58_tx,
};
client.send_str(sp_address, &serde_json::to_string(&broadcast_request).unwrap()).await;
client
.send_str(
sp_address,
&serde_json::to_string(&broadcast_request).unwrap(),
)
.await;
println!("Waiting for reply");
@@ -126,34 +134,33 @@ pub async fn send_tx(base58_tx: String, sp_address: Recipient, client: &mut Mixn
// get the actual message - discard the empty vec sent along with the SURB topup request
while let Some(new_message) = client.wait_for_messages().await {
if new_message.is_empty() {
continue;
}
if new_message.is_empty() {
continue;
}
message = new_message;
break
break;
}
// parse vec<u8> -> JSON String
let mut parsed = String::new();
for r in message.iter() {
parsed = String::from_utf8(r.message.clone()).unwrap();
break
};
if let Some(r) = message.iter().next() {
parsed = String::from_utf8(r.message.clone()).unwrap();
}
let sp_response: crate::ResponseTypes = serde_json::from_str(&parsed).unwrap();
let res = match sp_response {
crate::ResponseTypes::Broadcast(response) => {
let broadcast_response = crate::BroadcastResponse {
tx_hash: response.tx_hash,
success: response.success
success: response.success,
};
(broadcast_response.tx_hash, broadcast_response.success)
},
// TODO make this a proper error
_ => { println!("weird response"); (String::from("placeholder error"), false) }
}
_ => {
(String::from("Got strange incoming response, couldn't match"), false)
}
};
res
}
Ok(res)
}
+25 -19
View File
@@ -1,49 +1,49 @@
use serde::{Deserialize, Serialize};
use cosmrs::{AccountId, tendermint};
use cosmrs::{tendermint, AccountId};
use nym_sdk::mixnet::{MixnetClient, MixnetClientBuilder, StoragePaths, ReconstructedMessage};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use nym_sdk::mixnet::{StoragePaths, MixnetClientBuilder, MixnetClient};
pub mod client;
pub mod client;
pub mod service;
pub const DEFAULT_VALIDATOR_RPC: &str = "https://qwerty-validator.qa.nymte.ch";
pub const DEFAULT_DENOM: &str = "unym";
pub const DEFAULT_PREFIX: &str = "n";
pub const DEFAULT_VALIDATOR_RPC: &str = "https://qwerty-validator.qa.nymte.ch";
pub const DEFAULT_DENOM: &str = "unym";
pub const DEFAULT_PREFIX: &str = "n";
#[derive(Deserialize, Serialize, Debug)]
pub struct SequenceRequest {
pub validator: String,
pub signer_address: AccountId,
pub validator: String,
pub signer_address: AccountId,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct SequenceRequestResponse {
pub account_number: u64,
pub sequence: u64,
pub chain_id: tendermint::chain::Id
pub sequence: u64,
pub chain_id: tendermint::chain::Id,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct BroadcastRequest {
pub base58_tx_bytes: String
pub base58_tx_bytes: String,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct BroadcastResponse{
pub tx_hash: String,
pub success: bool
pub struct BroadcastResponse {
pub tx_hash: String,
pub success: bool,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(untagged)]
pub enum RequestTypes {
Sequence(SequenceRequest),
Broadcast(BroadcastRequest)
Broadcast(BroadcastRequest),
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(untagged)]
pub enum ResponseTypes {
Sequence(SequenceRequestResponse),
Broadcast(BroadcastResponse)
Sequence(SequenceRequestResponse),
Broadcast(BroadcastResponse),
}
pub async fn create_client(config_path: PathBuf) -> MixnetClient {
@@ -55,6 +55,12 @@ pub async fn create_client(config_path: PathBuf) -> MixnetClient {
.build()
.await
.unwrap();
client.connect_to_mixnet().await.unwrap()
}
// parse incoming: ignore empty SURB data packets + parse incoming message to struct or error
// we know we are expecting JSON here but an irl helper would parse conditionally on bytes / string incoming
pub fn _parse_incoming(_incoming: Option<Vec<ReconstructedMessage>>) -> ResponseTypes {
todo!()
}
+35 -26
View File
@@ -1,26 +1,36 @@
use nym_validator_client::nyxd::CosmWasmClient;
use cosmrs::rpc::{HttpClient, Client};
use cosmrs::{AccountId, tendermint};
use crate::DEFAULT_VALIDATOR_RPC;
use bs58;
use crate::DEFAULT_VALIDATOR_RPC;
use cosmrs::rpc::{Client, HttpClient};
use cosmrs::{tendermint, AccountId};
use nym_validator_client::nyxd::CosmWasmClient;
pub async fn create_broadcaster() -> HttpClient {
let broadcaster: HttpClient = HttpClient::new(DEFAULT_VALIDATOR_RPC).unwrap();
broadcaster
pub async fn create_broadcaster() -> HttpClient {
let broadcaster: HttpClient = HttpClient::new(DEFAULT_VALIDATOR_RPC).unwrap();
broadcaster
}
pub async fn get_sequence(broadcaster: HttpClient, signer_address: AccountId) -> crate::SequenceRequestResponse {
pub async fn get_sequence(
broadcaster: HttpClient,
signer_address: AccountId,
) -> Result<crate::SequenceRequestResponse, std::io::Error> {
// get signer information
let sequence = broadcaster.get_sequence(&signer_address).await.unwrap();
let chain_id: tendermint::chain::Id = broadcaster.get_chain_id().await.unwrap();
crate::SequenceRequestResponse { account_number: sequence.account_number, sequence: sequence.sequence, chain_id }
Ok(crate::SequenceRequestResponse {
account_number: sequence.account_number,
sequence: sequence.sequence,
chain_id,
})
}
pub async fn broadcast(base58_tx_bytes: String, broadcaster: HttpClient) -> crate::BroadcastResponse {
pub async fn broadcast(
base58_tx_bytes: String,
broadcaster: HttpClient,
) -> Result<crate::BroadcastResponse, std::io::Error> {
// decode the base58 tx to vec<u8>
let tx_bytes = bs58::decode(base58_tx_bytes).into_vec().unwrap();
let tx_bytes = bs58::decode(base58_tx_bytes).into_vec().unwrap();
// this is our sender address hardcoded for ease of the demo logging
// this is our sender address hardcoded for ease of the demo logging
let from_address: AccountId = "n1p8ayfmdash352gh6yy8zlxk24dm6yzc9mdq0p6".parse().unwrap();
// compare balances from before and after the tx
@@ -41,20 +51,19 @@ pub async fn broadcast(base58_tx_bytes: String, broadcaster: HttpClient) -> crat
.await
.unwrap()
.unwrap();
println!("returned transaction hash: {:#?}", broadcast_res.hash.to_string());
println!(
"returned transaction hash: {:#?}",
broadcast_res.hash.to_string()
);
println!("balance before transaction: {before}");
println!("balance after transaction: {after}");
println!("returning tx hash to sender");
let success: bool = if after.amount < before.amount {
true
} else {
false
};
crate::BroadcastResponse {
tx_hash: serde_json::to_string(&broadcast_res.hash).unwrap(),
success,
}
}
let success: bool = after.amount < before.amount;
Ok(crate::BroadcastResponse {
tx_hash: serde_json::to_string(&broadcast_res.hash).unwrap(),
success,
})
}