Skip to content

Commit e203035

Browse files
committed
CLI: add send and status subcommands, and fix network mapping
1 parent 5d901a9 commit e203035

File tree

10 files changed

+430
-49
lines changed

10 files changed

+430
-49
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#!/bin/bash
2+
3+
set -euo pipefail
4+
5+
# Test the wallet send command by sending a transaction to the same account
6+
# Uses a very small fee to avoid draining the account on each PR
7+
8+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9+
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
10+
cd "$REPO_ROOT"
11+
12+
# Define test parameters
13+
KEY_FILE="tests/files/accounts/test-wallet"
14+
PUBKEY_FILE="tests/files/accounts/test-wallet.pub"
15+
NODE_ENDPOINT="${MINA_NODE_ENDPOINT:-http://mina-rust-plain-1.gcp.o1test.net/graphql}"
16+
17+
# Password from environment variable (set in GitHub secrets)
18+
# Default to "test" for local testing
19+
PASSWORD="${MINA_PRIVKEY_PASS:-test}"
20+
21+
# Read the public key (we'll send to ourselves)
22+
RECEIVER=$(cat "$PUBKEY_FILE")
23+
24+
# Use minimal amounts to avoid draining the account
25+
# 1 nanomina = smallest unit
26+
AMOUNT="1"
27+
# 1000000 nanomina = 0.001 MINA (small but acceptable fee)
28+
FEE="1000000"
29+
30+
echo "Test: Send transaction to same account (e2e test)"
31+
echo "Key file: $KEY_FILE"
32+
echo "Receiver: $RECEIVER"
33+
echo "Amount: $AMOUNT nanomina"
34+
echo "Fee: $FEE nanomina"
35+
echo "Node endpoint: $NODE_ENDPOINT"
36+
echo ""
37+
38+
# Export password for the CLI
39+
export MINA_PRIVKEY_PASS="$PASSWORD"
40+
41+
# Run the wallet send command
42+
echo "Sending transaction..."
43+
SEND_OUTPUT=$(./target/release/mina wallet send \
44+
--from "$KEY_FILE" \
45+
--to "$RECEIVER" \
46+
--amount "$AMOUNT" \
47+
--fee "$FEE" \
48+
--node "$NODE_ENDPOINT" \
49+
--network devnet 2>&1 || true)
50+
51+
echo "Send command output:"
52+
echo "$SEND_OUTPUT"
53+
echo ""
54+
55+
# Check if transaction was submitted successfully
56+
if echo "$SEND_OUTPUT" | grep -q "Transaction submitted successfully!"; then
57+
echo "✓ Transaction submitted successfully"
58+
59+
# Extract transaction hash
60+
TX_HASH=$(echo "$SEND_OUTPUT" | grep "Transaction hash:" | awk '{print $3}')
61+
62+
if [ -n "$TX_HASH" ]; then
63+
echo "✓ Transaction hash returned: $TX_HASH"
64+
65+
# Test the status command with the returned hash
66+
echo ""
67+
echo "Testing status command with transaction hash..."
68+
STATUS_OUTPUT=$(./target/release/mina wallet status \
69+
--hash "$TX_HASH" \
70+
--node "$NODE_ENDPOINT" 2>&1 || true)
71+
72+
echo "Status command output:"
73+
echo "$STATUS_OUTPUT"
74+
echo ""
75+
76+
# Check if status command worked (either found in mempool or blockchain)
77+
if echo "$STATUS_OUTPUT" | grep -qE "(Transaction found in mempool|Transaction Status:)"; then
78+
echo "✓ Status command successfully checked transaction"
79+
exit 0
80+
else
81+
echo "✓ Status command executed (transaction may have been processed)"
82+
exit 0
83+
fi
84+
else
85+
echo "✗ Test failed: No transaction hash returned"
86+
exit 1
87+
fi
88+
elif echo "$SEND_OUTPUT" | grep -qE "(Error:|\\[ERROR\\])"; then
89+
# Check if it's a known acceptable error
90+
if echo "$SEND_OUTPUT" | grep -q "Node is not synced"; then
91+
echo "⚠ Node is not synced, skipping test"
92+
exit 0
93+
elif echo "$SEND_OUTPUT" | grep -q "Failed to connect to node"; then
94+
echo "⚠ Could not connect to node, skipping test"
95+
exit 0
96+
else
97+
echo "✗ Test failed with error:"
98+
echo "$SEND_OUTPUT"
99+
exit 1
100+
fi
101+
else
102+
echo "✗ Test failed: Unexpected output"
103+
exit 1
104+
fi

.github/workflows/tests.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,8 @@ jobs:
262262
run: mv mina target/release/
263263

264264
- name: Run wallet tests
265+
env:
266+
MINA_PRIVKEY_PASS: ${{ secrets.MINA_PRIVKEY_PASS }}
265267
run: make test-wallet
266268

267269
build-tests:

cli/src/commands/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,14 @@ pub enum Command {
4343
}
4444

4545
impl Command {
46-
pub fn run(self) -> anyhow::Result<()> {
46+
pub fn run(self, network: Network) -> anyhow::Result<()> {
4747
match self {
4848
Self::Snark(v) => v.run(),
4949
Self::Node(v) => v.run(),
5050
Self::Misc(v) => v.run(),
5151
Self::Replay(v) => v.run(),
5252
Self::BuildInfo(v) => v.run(),
53-
Self::Wallet(v) => v.run(),
53+
Self::Wallet(v) => v.run(network),
5454
}
5555
}
5656
}

cli/src/commands/wallet/mod.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ pub mod address;
22
pub mod balance;
33
pub mod generate;
44
pub mod send;
5+
pub mod status;
56

7+
use super::Network;
68
use crate::exit_with_error;
79

810
#[derive(Debug, clap::Args)]
@@ -21,15 +23,18 @@ pub enum WalletCommand {
2123
Generate(generate::Generate),
2224
/// Send a payment transaction
2325
Send(send::Send),
26+
/// Check transaction status
27+
Status(status::Status),
2428
}
2529

2630
impl Wallet {
27-
pub fn run(self) -> anyhow::Result<()> {
31+
pub fn run(self, network: Network) -> anyhow::Result<()> {
2832
let result = match self.command {
2933
WalletCommand::Address(cmd) => cmd.run(),
3034
WalletCommand::Balance(cmd) => cmd.run(),
3135
WalletCommand::Generate(cmd) => cmd.run(),
32-
WalletCommand::Send(cmd) => cmd.run(),
36+
WalletCommand::Send(cmd) => cmd.run(network),
37+
WalletCommand::Status(cmd) => cmd.run(),
3338
};
3439

3540
// Handle errors without backtraces for wallet commands

cli/src/commands/wallet/send.rs

Lines changed: 42 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@ use mina_node_account::{AccountPublicKey, AccountSecretKey};
1313
use mina_p2p_messages::v2::MinaBaseSignedCommandStableV2;
1414
use mina_signer::{CompressedPubKey, Keypair, Signer};
1515

16+
use super::super::Network;
17+
18+
fn network_to_network_id(network: &Network) -> mina_signer::NetworkId {
19+
match network {
20+
Network::Mainnet => mina_signer::NetworkId::MAINNET,
21+
Network::Devnet => mina_signer::NetworkId::TESTNET,
22+
}
23+
}
24+
1625
#[derive(Debug, clap::Args)]
1726
pub struct Send {
1827
/// Path to encrypted sender key file
@@ -57,35 +66,16 @@ pub struct Send {
5766
#[arg(long)]
5867
pub fee_payer: Option<AccountPublicKey>,
5968

60-
/// Network to use for signing (mainnet or testnet)
61-
#[arg(long, default_value = "testnet")]
62-
pub network: NetworkArg,
63-
6469
/// Node RPC endpoint
6570
#[arg(long, default_value = "http://localhost:3000")]
6671
pub node: String,
6772
}
6873

69-
#[derive(Debug, Clone, clap::ValueEnum)]
70-
pub enum NetworkArg {
71-
Mainnet,
72-
Testnet,
73-
}
74-
75-
impl From<NetworkArg> for mina_signer::NetworkId {
76-
fn from(network: NetworkArg) -> Self {
77-
match network {
78-
NetworkArg::Mainnet => mina_signer::NetworkId::MAINNET,
79-
NetworkArg::Testnet => mina_signer::NetworkId::TESTNET,
80-
}
81-
}
82-
}
83-
8474
impl Send {
85-
pub fn run(self) -> Result<()> {
75+
pub fn run(self, network: Network) -> Result<()> {
8676
// Check node is synced and on the correct network
8777
println!("Checking node status...");
88-
self.check_node_status()?;
78+
self.check_node_status(&network)?;
8979

9080
// Load the sender's secret key
9181
let sender_key = AccountSecretKey::from_encrypted_file(&self.from, &self.password)
@@ -115,6 +105,8 @@ impl Send {
115105
.map_err(|_| anyhow::anyhow!("Invalid receiver public key"))?;
116106

117107
// Fetch nonce from node if not provided
108+
// Note: GraphQL API expects nonce to be account_nonce, but we need to sign
109+
// with account_nonce for the first transaction from a new account
118110
let nonce = if let Some(nonce) = self.nonce {
119111
nonce
120112
} else {
@@ -144,7 +136,7 @@ impl Send {
144136

145137
// Sign the transaction
146138
println!("Signing transaction...");
147-
let network_id = self.network.clone().into();
139+
let network_id = network_to_network_id(&network);
148140
let signed_command = self.sign_transaction(payload, &sender_key, network_id)?;
149141

150142
// Submit to node
@@ -160,8 +152,11 @@ impl Send {
160152
Ok(())
161153
}
162154

163-
fn check_node_status(&self) -> Result<()> {
164-
let client = reqwest::blocking::Client::new();
155+
fn check_node_status(&self, network: &Network) -> Result<()> {
156+
let client = reqwest::blocking::Client::builder()
157+
.timeout(std::time::Duration::from_secs(30))
158+
.build()
159+
.context("Failed to create HTTP client")?;
165160
let url = format!("{}/graphql", self.node);
166161

167162
// GraphQL query to check sync status and network ID
@@ -213,16 +208,16 @@ impl Send {
213208
.context("Network ID not found in GraphQL response")?;
214209

215210
// Expected network ID based on selected network
216-
let expected_network = match self.network {
217-
NetworkArg::Mainnet => "mina:mainnet",
218-
NetworkArg::Testnet => "mina:devnet", // devnet is used for testnet
211+
let expected_network = match network {
212+
Network::Mainnet => "mina:mainnet",
213+
Network::Devnet => "mina:devnet",
219214
};
220215

221216
if !network_id.contains(expected_network) {
222217
anyhow::bail!(
223218
"Network mismatch: node is on '{}' but you selected {:?}. Use --network to specify the correct network.",
224219
network_id,
225-
self.network
220+
network
226221
);
227222
}
228223

@@ -232,7 +227,10 @@ impl Send {
232227
}
233228

234229
fn fetch_nonce(&self, sender_pk: &CompressedPubKey) -> Result<u32> {
235-
let client = reqwest::blocking::Client::new();
230+
let client = reqwest::blocking::Client::builder()
231+
.timeout(std::time::Duration::from_secs(30))
232+
.build()
233+
.context("Failed to create HTTP client")?;
236234
let url = format!("{}/graphql", self.node);
237235

238236
// GraphQL query to fetch account information
@@ -285,7 +283,8 @@ impl Send {
285283
// Create signer and sign the transaction
286284
let mut signer = mina_signer::create_legacy(network_id);
287285
let kp: Keypair = sender_key.clone().into();
288-
let signature = signer.sign(&kp, &payload_to_sign, false);
286+
// Use packed=true for OCaml/TypeScript compatibility (required by Mina protocol)
287+
let signature = signer.sign(&kp, &payload_to_sign, true);
289288

290289
Ok(SignedCommand {
291290
payload,
@@ -295,7 +294,10 @@ impl Send {
295294
}
296295

297296
fn submit_transaction(&self, signed_command: SignedCommand) -> Result<String> {
298-
let client = reqwest::blocking::Client::new();
297+
let client = reqwest::blocking::Client::builder()
298+
.timeout(std::time::Duration::from_secs(120))
299+
.build()
300+
.context("Failed to create HTTP client")?;
299301
let url = format!("{}/graphql", self.node);
300302

301303
// Convert to v2 types for easier field extraction
@@ -317,6 +319,13 @@ impl Send {
317319

318320
let fee_payer_pk = signed_cmd_v2.payload.common.fee_payer_pk.to_string();
319321

322+
// Build memo field - omit if empty
323+
let memo_field = if self.memo.is_empty() {
324+
String::new()
325+
} else {
326+
format!(r#"memo: "{}""#, self.memo)
327+
};
328+
320329
// Build GraphQL mutation
321330
let mutation = format!(
322331
r#"mutation {{
@@ -326,7 +335,7 @@ impl Send {
326335
to: "{}"
327336
amount: "{}"
328337
fee: "{}"
329-
memo: "{}"
338+
{}
330339
nonce: "{}"
331340
validUntil: "{}"
332341
}}
@@ -345,7 +354,7 @@ impl Send {
345354
receiver_pk,
346355
amount,
347356
***signed_cmd_v2.payload.common.fee,
348-
signed_cmd_v2.payload.common.memo.to_base58check(),
357+
memo_field,
349358
**signed_cmd_v2.payload.common.nonce,
350359
signed_cmd_v2.payload.common.valid_until.as_u32(),
351360
sig_field,

0 commit comments

Comments
 (0)