Skip to content

Commit 49e7e1a

Browse files
committed
CLI: fix wallet and add status
1 parent 5d901a9 commit 49e7e1a

File tree

10 files changed

+432
-50
lines changed

10 files changed

+432
-50
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: 44 additions & 34 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,11 +105,14 @@ 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 {
121113
println!("Fetching nonce from node...");
122-
self.fetch_nonce(&fee_payer_pk)?
114+
let current_nonce = self.fetch_nonce(&fee_payer_pk)?;
115+
current_nonce
123116
};
124117

125118
println!("Using nonce: {}", nonce);
@@ -144,7 +137,7 @@ impl Send {
144137

145138
// Sign the transaction
146139
println!("Signing transaction...");
147-
let network_id = self.network.clone().into();
140+
let network_id = network_to_network_id(&network);
148141
let signed_command = self.sign_transaction(payload, &sender_key, network_id)?;
149142

150143
// Submit to node
@@ -160,8 +153,11 @@ impl Send {
160153
Ok(())
161154
}
162155

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

167163
// GraphQL query to check sync status and network ID
@@ -213,16 +209,16 @@ impl Send {
213209
.context("Network ID not found in GraphQL response")?;
214210

215211
// 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
212+
let expected_network = match network {
213+
Network::Mainnet => "mina:mainnet",
214+
Network::Devnet => "mina:devnet",
219215
};
220216

221217
if !network_id.contains(expected_network) {
222218
anyhow::bail!(
223219
"Network mismatch: node is on '{}' but you selected {:?}. Use --network to specify the correct network.",
224220
network_id,
225-
self.network
221+
network
226222
);
227223
}
228224

@@ -232,7 +228,10 @@ impl Send {
232228
}
233229

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

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

290290
Ok(SignedCommand {
291291
payload,
@@ -295,7 +295,10 @@ impl Send {
295295
}
296296

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

301304
// Convert to v2 types for easier field extraction
@@ -317,6 +320,13 @@ impl Send {
317320

318321
let fee_payer_pk = signed_cmd_v2.payload.common.fee_payer_pk.to_string();
319322

323+
// Build memo field - omit if empty
324+
let memo_field = if self.memo.is_empty() {
325+
String::new()
326+
} else {
327+
format!(r#"memo: "{}""#, self.memo)
328+
};
329+
320330
// Build GraphQL mutation
321331
let mutation = format!(
322332
r#"mutation {{
@@ -326,7 +336,7 @@ impl Send {
326336
to: "{}"
327337
amount: "{}"
328338
fee: "{}"
329-
memo: "{}"
339+
{}
330340
nonce: "{}"
331341
validUntil: "{}"
332342
}}
@@ -345,7 +355,7 @@ impl Send {
345355
receiver_pk,
346356
amount,
347357
***signed_cmd_v2.payload.common.fee,
348-
signed_cmd_v2.payload.common.memo.to_base58check(),
358+
memo_field,
349359
**signed_cmd_v2.payload.common.nonce,
350360
signed_cmd_v2.payload.common.valid_until.as_u32(),
351361
sig_field,

0 commit comments

Comments
 (0)