Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions .github/workflows/build_and_test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
name: Build And Test

on:
push:
branches:
- main
- cli-rust-rewrite
- "releases/*"
pull_request:
types: [opened, synchronize, reopened, ready_for_review]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ (github.ref != 'refs/heads/main') }}

env:
CARGO_TERM_COLOR: always

CARGOFLAGS: --workspace --all-targets
CARGOFLAGS_ALL_FEATURES: --workspace --all-targets --all-features

jobs:
debug_tests_all_features:
name: Debug | All features | Test
runs-on: ubicloud-standard-16

env:
INFO_LOG_FILE: ${{ github.workspace }}/test-logs/debug/debug-all-features-test.log

steps:
- name: Collect Workflow Telemetry
uses: catchpoint/workflow-telemetry-action@v2
with:
comment_on_pr: false

- uses: actions/checkout@v4

- name: Create test log directories
run: mkdir -p test-logs/debug

- name: Run tests
run: |
set -o pipefail
cargo test $CARGOFLAGS_ALL_FEATURES

- name: Upload deposit state artifact
if: failure()
uses: actions/upload-artifact@v4
with:
name: deposit-state-debug
path: core/src/test/data/deposit_state_debug.bincode
if-no-files-found: ignore
retention-days: 1

release_tests_all_features:
name: Release | All features | Test
runs-on: ubicloud-standard-16

env:
INFO_LOG_FILE: ${{ github.workspace }}/test-logs/debug/debug-all-features-test.log

steps:
- name: Collect Workflow Telemetry
uses: catchpoint/workflow-telemetry-action@v2
with:
comment_on_pr: false

- uses: actions/checkout@v4

- name: Create test log directories
run: mkdir -p test-logs/debug

- name: Run tests
run: |
set -o pipefail
cargo test --release $CARGOFLAGS_ALL_FEATURES

- name: Upload deposit state artifact
if: failure()
uses: actions/upload-artifact@v4
with:
name: deposit-state-debug
path: core/src/test/data/deposit_state_debug.bincode
if-no-files-found: ignore
retention-days: 1
94 changes: 94 additions & 0 deletions .github/workflows/code_checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
name: Code Checks

on:
push:
branches:
- main
- "releases/*"
- cli-rust-rewrite
pull_request:
types: [opened, synchronize, reopened, ready_for_review]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ (github.ref != 'refs/heads/main') }}

env:
CARGO_TERM_COLOR: always

jobs:
formatting:
name: Check formatting
runs-on: ubicloud-standard-2

steps:
- uses: actions/checkout@v4
- name: Run Cargo fmt
run: cargo fmt --check

linting:
name: Check linting
runs-on: ubicloud-standard-2

steps:
- uses: actions/checkout@v4
- name: Install Clippy
run: rustup component add --toolchain 1.89-x86_64-unknown-linux-gnu clippy
- name: Run Cargo clippy
run: cargo clippy --no-deps --all-targets --all-features -- -Dwarnings

udeps:
name: Check unused dependencies
runs-on: ubicloud-standard-2

steps:
- uses: actions/checkout@v4

- name: Toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: nightly-2025-03-09
override: true

- name: Run cargo-udeps
env:
RUSTFLAGS: -A warnings
uses: aig787/cargo-udeps-action@v1
with:
version: "latest"
args: "--workspace --all-features --all-targets"

codespell:
name: Check spelling
runs-on: ubicloud-standard-2
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.x"

- name: Install codespell
run: pip install codespell

- name: Run codespell
run: |
codespell --skip="*.lock,./target" -I="codespell_ignore.txt"

check_for_todos:
name: Check for TODOs
runs-on: ubicloud-standard-2
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v4

- name: Check for TODOs
run: |
if git grep -i "TODO" -- ':!docs' ':!*.md' ':!*.txt' ':!*.rst' ':!*.lock' ':!target' ':!scripts' ':!tests' ':!examples' ':!**/code_checks.yml'; then
echo "Found TODOs in code directories. Please address them before merging.";
exit 1;
else
echo "No TODOs found in code directories.";
fi
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
A wallet-agnostic command-line tool for interacting with Citrea, supporting secure Bitcoin deposits and withdrawals without requiring wallet connection.

## Features

- Deposit and withdrawal flows for Citrea
- Airgapped key generation and signing
- Bitcoin address and transaction utilities
Expand All @@ -28,14 +29,18 @@ cargo install --path .
Run `clementine --help` to see available commands.

## Security Warning

Some commands (notably key generation and signing) must be run in an airgapped environment. The CLI will prompt for confirmation before proceeding with sensitive operations.

## Documentation

See the [CLI documentation](./docs/cli.md) for detailed command usage.

## Contributing

- If you have suggestions for naming or structure, please open an issue or PR.
- For musig2 and advanced cryptography, see TODOs and stubs in the codebase.

## License
MIT

MIT
Empty file added codespell_ignore.txt
Empty file.
4 changes: 4 additions & 0 deletions rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[toolchain]
channel = "1.89"
components = ["rustfmt", "rust-src"]
profile = "minimal"
12 changes: 6 additions & 6 deletions src/bin/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ async fn main() {
}
}
DepositCommands::DepositStatus { deposit_address } => {
println!("TODO: deposit.deposit_status: {}", deposit_address);
unimplemented!("deposit.deposit_status: {}", deposit_address);
}
DepositCommands::GetDepositParams {
move_to_vault_txid,
Expand Down Expand Up @@ -309,7 +309,7 @@ async fn main() {
}
}
WithdrawalCommands::Status { withdrawal_index } => {
println!("TODO: withdrawal.status: {}", withdrawal_index);
unimplemented!("withdrawal.status: {}", withdrawal_index);
}
WithdrawalCommands::GenerateOperatorWithdrawalSignatures {
withdrawal_address,
Expand All @@ -318,8 +318,8 @@ async fn main() {
withdrawal_utxo_vout,
withdrawal_amount,
} => {
println!(
"TODO: withdrawal.generate_operator_withdrawal_signatures: {} {} {} {} {}",
unimplemented!(
"withdrawal.generate_operator_withdrawal_signatures: {} {} {} {} {}",
withdrawal_address,
signer_address,
withdrawal_utxo_txid,
Expand All @@ -335,8 +335,8 @@ async fn main() {
withdrawal_index,
signature,
} => {
println!(
"TODO: withdrawal.send_withdrawal_signatures_to_operators: {} {} {} {} {} {}",
unimplemented!(
"withdrawal.send_withdrawal_signatures_to_operators: {} {} {} {} {} {}",
withdrawal_address,
signer_address,
withdrawal_utxo_txid,
Expand Down
25 changes: 4 additions & 21 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ pub const USER_TAKES_AFTER: u64 = 200;
pub fn get_backend_endpoint(network: Network) -> &'static str {
match network {
Network::Bitcoin => "https://api.citrea.xyz/",
Network::Testnet4 => "https://api.testnet.citrea.xyz/",
Network::Testnet4 | Network::Testnet => "https://api.testnet.citrea.xyz/",
Network::Signet => "https://api.devnet.citrea.xyz/",
_ => {
panic!("No backend endpoint configured for network {:?}", network);
Expand Down Expand Up @@ -98,33 +98,16 @@ mod tests {
get_backend_endpoint(Network::Signet),
"https://api.devnet.citrea.xyz/"
);
// For other networks, falls back to testnet
assert_eq!(
get_backend_endpoint(Network::Testnet),
"https://api.testnet.citrea.xyz/"
);
assert_eq!(
get_backend_endpoint(Network::Regtest),
"https://api.testnet.citrea.xyz/"
);
}

#[test]
#[should_panic]
fn test_get_verifier_pks_bitcoin() {
get_verifier_pks(Network::Bitcoin);
}

#[test]
#[should_panic]
fn test_get_verifier_pks_testnet4() {
get_verifier_pks(Network::Testnet4);
}

#[test]
#[should_panic]
fn test_get_verifier_pks_signet() {
get_verifier_pks(Network::Signet);
fn test_get_backend_endpoint_regtest() {
get_backend_endpoint(Network::Regtest);
}

#[test]
Expand All @@ -139,7 +122,7 @@ mod tests {
assert_eq!(USER_TAKES_AFTER, 200);
assert_eq!(
UNSPENDABLE_XONLY_PUBKEY.to_string(),
"50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"
"93c7378d96518a75448821c4f7c8f4bae7ce60f804d03d1f0628dd5dd0f5de51"
);
}
}
2 changes: 0 additions & 2 deletions src/deposit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,6 @@ pub fn verify_recovery_tx(
Ok((txid, address, amount))
}

// TODO: Implement deposit.deposit_status

#[cfg(test)]
mod tests {
use super::*;
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ pub mod musig2;
pub mod parameters;
pub mod script;
pub mod storage;
pub mod withdrawal;
pub mod types;
pub mod withdrawal;

/// EVM Address type - 20 bytes
#[derive(Copy, Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
Expand Down
2 changes: 1 addition & 1 deletion src/script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ pub fn deposit_script(evm_address: EVMAddress, nofn_xonly_pk: XOnlyPublicKey) ->
.push_opcode(OP_IF)
.push_slice(citrea)
.push_slice(evm_address.0)
.push_slice(PushBytesBuf::try_from(hex::decode("000000003b9aca00").unwrap()).unwrap()) // TODO: Remove this.
.push_slice(PushBytesBuf::try_from(hex::decode("000000003b9aca00").unwrap()).unwrap()) // #22
.push_opcode(OP_ENDIF)
.into_script()
}
4 changes: 2 additions & 2 deletions src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ pub fn store_key(
let storage_dir = get_storage_dir()?;
fs::create_dir_all(&storage_dir)?;

// Store the keypair in plaintext (TODO: implement encryption)
// Store the keypair in plaintext (see #12)
let key_file = storage_dir.join(format!("key_{}.json", address));
let key_data = serde_json::json!({
"network": network.to_string(),
Expand All @@ -35,7 +35,7 @@ pub fn store_key(
"stored_at": chrono::Utc::now().to_rfc3339()
});
fs::write(&key_file, serde_json::to_string_pretty(&key_data)?)?;

// Set file permissions to 700 (rwx------)
#[cfg(unix)]
{
Expand Down
13 changes: 4 additions & 9 deletions src/withdrawal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,11 +185,9 @@ pub async fn get_tx_details(
bitcoin_rpc_password: Option<&str>,
network: Network,
) -> Result<(Transaction, Block, u32), Box<dyn std::error::Error>> {
if bitcoin_rpc_url.is_some() && bitcoin_rpc_user.is_some() && bitcoin_rpc_password.is_some() {
let bitcoin_rpc_url = bitcoin_rpc_url.unwrap();
let bitcoin_rpc_user = bitcoin_rpc_user.unwrap();
let bitcoin_rpc_password = bitcoin_rpc_password.unwrap();

if let (Some(bitcoin_rpc_url), Some(bitcoin_rpc_user), Some(bitcoin_rpc_password)) =
(bitcoin_rpc_url, bitcoin_rpc_user, bitcoin_rpc_password)
{
let tx_details = get_tx_details_from_rpc(
bitcoin_rpc_url,
bitcoin_rpc_user,
Expand All @@ -199,6 +197,7 @@ pub async fn get_tx_details(
.await?;
return Ok(tx_details);
}

get_tx_details_from_mempool(prepare_txid, network).await
}

Expand Down Expand Up @@ -376,7 +375,3 @@ pub async fn send_safe_withdrawal(

Ok(())
}

// TODO: Implement withdrawal.status
// TODO: Implement withdrawal.generate_operator_withdrawal_signatures
// TODO: Implement withdrawal.send_withdrawal_signatures_to_operators
Loading