Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Implement create-wallet tool with static build support#1

Merged
juntao merged 8 commits intomainfrom
feat/create-wallet
Jan 31, 2026
Merged

Implement create-wallet tool with static build support#1
juntao merged 8 commits intomainfrom
feat/create-wallet

Conversation

@juntao
Copy link
Member

@juntao juntao commented Jan 31, 2026

Summary

  • Implement the create-wallet CLI tool for generating Ethereum-compatible wallets
  • Add shared x402-common library with wallet, config, and error modules
  • Configure static builds using musl for Linux

Features

create-wallet tool

  • Generate secure random private key using system CSPRNG
  • Encrypt with Web3 Secret Storage (keystore v3) format
  • Auto-generate 32-character password if not provided
  • Store address in keystore for easy retrieval
  • Output address to stdout, status to stderr

x402-common library

  • wallet module: create, get_address, password utilities
  • config module: network profiles (base-sepolia, base-mainnet, etc.)
  • error module: consistent exit codes across tools

Static builds

  • Use musl targets (x86_64-unknown-linux-musl, aarch64-unknown-linux-musl)
  • Use rustls instead of native-tls (no OpenSSL dependency)
  • Binaries have no dynamic dependencies on Linux

Usage

```bash

Create wallet with auto-generated password

./create-wallet

Create with specific password

./create-wallet --password "my-password"

Create at custom location

./create-wallet --output /path/to/wallet.json
```

Test plan

  • Build succeeds locally
  • Tool creates wallet at default location
  • Tool outputs address to stdout
  • Wallet file contains encrypted keystore with address field
  • Password file created with proper permissions
  • CI builds pass on all platforms
  • Verify static linking on Linux

🤖 Generated with Claude Code

Juntao Yuan and others added 8 commits January 31, 2026 02:38
Features:
- Generate Ethereum-compatible wallet with secure random private key
- Encrypt using Web3 Secret Storage (keystore v3) format
- Auto-generate 32-character password if not provided
- Save password to ~/.x402/password.txt with 600 permissions
- Store address in keystore for easy retrieval without decryption
- Output address to stdout, status messages to stderr

x402-common library:
- Add wallet module with create, get_address, and password utilities
- Add config module with network profiles (base-sepolia, base-mainnet, etc.)
- Add error types with consistent exit codes
- Add default path functions for ~/.x402/ directory

Static build configuration:
- Use musl targets for fully static Linux binaries
- Use rustls instead of native-tls (no OpenSSL dependency)
- Configure cross-compilation with cross-rs
- Add .cargo/config.toml for static linking flags

CI updates:
- Switch to musl targets for Linux (x86_64 and aarch64)
- Add static linking verification step
- Use cross for Linux builds

Co-Authored-By: Claude Opus 4.5 <[email protected]>
The action is rust-toolchain, not rust-action.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Edition 2024 doesn't exist yet. The latest stable edition is 2021.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
The config and wallet tests use tempfile::tempdir for temporary directories.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Use strip_prefix instead of manual string slicing in expand_tilde
- Remove unnecessary borrow in encrypt_key call

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Run cargo fmt to fix formatting issues.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Add proper [[bin]] sections with correct names
- Fix edition from 2024 to 2021 (matching workspace)
- All 4 stub projects (get-address, x402-config, pay, x402curl) now compile

Co-Authored-By: Claude Opus 4.5 <[email protected]>
macos-13 runner was not available/cancelled. Using macos-14 (Apple
Silicon) with cross-compilation for x86_64 target instead.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request implements a create-wallet CLI tool for generating Ethereum-compatible wallets, along with a shared x402-common library and static build configuration using musl targets.

Changes:

  • Implemented create-wallet tool with secure key generation, Web3 Secret Storage encryption, and auto-generated passwords
  • Added x402-common library with wallet, config, and error handling modules
  • Configured static builds for Linux (musl), macOS, and Windows with rustls for TLS

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
Cargo.toml Fixed edition from "2024" to "2021"
.cargo/config.toml Added static build configuration for all platforms
.github/workflows/ci.yml Updated to use musl targets and cross for Linux builds
x402-common/Cargo.toml Added dependencies with rustls for static builds
x402-common/src/lib.rs Added path utilities and data directory management
x402-common/src/error.rs Added error types with exit codes
x402-common/src/config.rs Added configuration with network profiles
x402-common/src/wallet.rs Implemented wallet creation and address retrieval
create-wallet/Cargo.toml Added package metadata and dependencies
create-wallet/src/main.rs Implemented CLI with password and force options
create-wallet/README.md Added comprehensive documentation
x402curl/Cargo.toml Updated package metadata
x402-config/Cargo.toml Updated package metadata
pay/Cargo.toml Updated package metadata
get-address/Cargo.toml Updated package metadata

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

## Output

The tool outputs:
- **stdout**: The wallet's public Ethereum address (checksummed, 0x-prefixed)
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation states that the address is "checksummed" (line 78), but the implementation in wallet.rs only adds the "0x" prefix without applying EIP-55 checksum encoding. This is inconsistent with the code behavior. Either update the documentation to remove the "checksummed" claim, or implement proper checksum encoding in the code.

Suggested change
- **stdout**: The wallet's public Ethereum address (checksummed, 0x-prefixed)
- **stdout**: The wallet's public Ethereum address (0x-prefixed)

Copilot uses AI. Check for mistakes.
Comment on lines +146 to +151
// Ensure 0x prefix and checksum
let address = if address.starts_with("0x") {
address.to_string()
} else {
format!("0x{}", address)
};
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment on line 146 states "Ensure 0x prefix and checksum", but the code only adds the 0x prefix without applying EIP-55 checksum encoding. The address returned will not have the proper mixed-case checksum format. Either update the comment to accurately describe what the code does, or implement proper checksum encoding using a library like ethers or by converting the address through the alloy types.

Copilot uses AI. Check for mistakes.
Comment on lines +109 to +119
if should_save_password {
let pw_path = password_file
.map(PathBuf::from)
.unwrap_or_else(default_password_path);
fs::write(&pw_path, &password_str)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&pw_path, fs::Permissions::from_mode(0o600))?;
}
}
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Windows, the password file is created with default file permissions which may allow other users to read it. Unix systems set restrictive 0o600 permissions (lines 114-118), but Windows has no equivalent protection here. Consider adding Windows-specific permission restrictions using the windows crate or documenting this security limitation in the README.

Copilot uses AI. Check for mistakes.
Comment on lines +36 to 43
if !dir.exists() {
std::fs::create_dir_all(&dir)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700))?;
}
}
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Windows, the data directory ~/.x402 is created with default permissions which may allow other users to access it. Unix systems set restrictive 0o700 permissions (lines 38-42), but Windows has no equivalent protection here. Since this directory will contain sensitive wallet and password files, consider adding Windows-specific permission restrictions or documenting this security limitation.

Copilot uses AI. Check for mistakes.
Comment on lines +118 to +123
fn expand_tilde(path: &str) -> PathBuf {
if let Some(stripped) = path.strip_prefix("~/") {
if let Some(home) = dirs::home_dir() {
return home.join(stripped);
}
}
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expand_tilde function only handles paths starting with ~/ but not paths that are exactly ~. If a user sets a path to just ~ in the config file, it will be treated as a literal directory name rather than expanding to the home directory. Consider also handling the case where path == "~" by returning the home directory directly.

Suggested change
fn expand_tilde(path: &str) -> PathBuf {
if let Some(stripped) = path.strip_prefix("~/") {
if let Some(home) = dirs::home_dir() {
return home.join(stripped);
}
}
fn expand_tilde(path: &str) -> PathBuf {
// Handle path that is exactly "~"
if path == "~" {
if let Some(home) = dirs::home_dir() {
return home;
} else {
// Fall back to treating "~" as a literal path if home directory is unavailable
return PathBuf::from(path);
}
}
// Handle paths starting with "~/"
if let Some(stripped) = path.strip_prefix("~/") {
if let Some(home) = dirs::home_dir() {
return home.join(stripped);
}
}
// No tilde expansion possible; treat as a literal path

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +39
let idx = rng.gen_range(0..62);
match idx {
0..=9 => (b'0' + idx) as char,
10..=35 => (b'a' + idx - 10) as char,
_ => (b'A' + idx - 36) as char,
}
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the password generation, idx is inferred from gen_range(0..62) which defaults to i32, while b'0', b'a', and b'A' are u8 literals. The addition operations like b'0' + idx will require implicit type conversions. While this works, it would be clearer and safer to explicitly type idx or cast it. Consider either: 1) explicitly typing: let idx: u8 = rng.gen_range(0..62);, or 2) casting in the additions: (b'0' + idx as u8) as char. This makes the type conversions explicit and prevents potential issues.

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +24
/// Encrypted wallet storage format
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncryptedWallet {
pub address: String,
pub crypto: serde_json::Value,
pub id: String,
pub version: u32,
}

Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The EncryptedWallet struct is defined but never used in the codebase and is not exported from the library. Consider removing it to reduce code clutter, or export it from lib.rs if it's intended for external use. If it's planned for future use, consider adding a comment explaining its purpose.

Suggested change
/// Encrypted wallet storage format
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncryptedWallet {
pub address: String,
pub crypto: serde_json::Value,
pub id: String,
pub version: u32,
}

Copilot uses AI. Check for mistakes.
Comment on lines +101 to +106
// Set restrictive permissions on the wallet file
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&wallet_path, fs::Permissions::from_mode(0o600))?;
}
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Windows, the wallet keystore file is created with default file permissions which may allow other users to read it. Unix systems set restrictive 0o600 permissions (lines 102-106), but Windows has no equivalent protection here. Consider adding Windows-specific permission restrictions using the windows crate or documenting this security limitation in the README.

Copilot uses AI. Check for mistakes.
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&config_path, fs::Permissions::from_mode(0o600))?;
}

Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Windows, the config file is created with default permissions. While the config file may be less sensitive than wallet/password files, it contains wallet paths and network configuration that could be useful to an attacker. Consider adding Windows-specific permission restrictions or documenting this limitation.

Suggested change
// NOTE (Windows):
// The Rust standard library does not currently provide a simple, portable way
// to tighten Windows ACLs to the current user only. As a result, on Windows
// this config file is created with the default permissions inherited from the
// containing directory. While this file does not store private keys or
// passwords, it does contain wallet paths and network configuration, which
// may be useful to an attacker with local access.
//
// If stronger guarantees are required, consider:
// * Restricting the directory ACL manually, or
// * Using a Windows-specific crate to set an explicit DACL for this file.
#[cfg(windows)]
{
eprintln!(
"Warning: config file '{}' is using default Windows permissions; \
paths and network configuration may be readable by other local users.",
config_path.display()
);
}

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +15
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".x402")
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If dirs::home_dir() returns None, the function falls back to using the current working directory (".") which could lead to unexpected behavior. Wallet files would be created in ./.x402 relative to wherever the command is run, potentially creating multiple wallet directories. Consider returning an error instead of falling back to the current directory, or at least logging a warning about this unusual condition.

Suggested change
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".x402")
match dirs::home_dir() {
Some(home) => home.join(".x402"),
None => {
eprintln!(
"Warning: could not determine home directory; using current directory for x402 data (~/.x402 -> ./.x402)."
);
PathBuf::from(".").join(".x402")
}
}

Copilot uses AI. Check for mistakes.
@juntao juntao merged commit f72d817 into main Jan 31, 2026
17 checks passed
@juntao juntao deleted the feat/create-wallet branch January 31, 2026 03:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants