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
12 changes: 6 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion balius-runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ tracing = "0.1.40"
hex = "0.4.3"
itertools = "0.14.0"
async-trait = "0.1.83"
utxorpc = { version = "0.12.0" }
utxorpc = { version = "0.13.0" }
# utxorpc = { path = "../../../utxorpc/rust-sdk" }
tokio-util = "0.7.12"
prost = "0.13"
Expand Down
1 change: 1 addition & 0 deletions balius-runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ impl Block {
slot: block.header.as_ref().unwrap().slot,
hash: block.header.as_ref().unwrap().hash.clone(),
height: block.header.as_ref().unwrap().height,
timestamp: block.timestamp,
}),
}
}
Expand Down
25 changes: 11 additions & 14 deletions balius-runtime/src/submit/u5c.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,18 @@ impl Submit {
}

pub async fn submit_tx(&mut self, tx: wit::Cbor) -> Result<(), wit::SubmitError> {
self.client
.submit_tx(vec![tx])
.await
.map_err(|err| match err {
utxorpc::Error::GrpcError(status) => {
let code: i32 = status.code().into();
if code == 3 {
wit::SubmitError::Invalid(status.to_string())
} else {
wit::SubmitError::Internal(status.to_string())
}
self.client.submit_tx(tx).await.map_err(|err| match err {
utxorpc::Error::GrpcError(status) => {
let code: i32 = status.code().into();
if code == 3 {
wit::SubmitError::Invalid(status.to_string())
} else {
wit::SubmitError::Internal(status.to_string())
}
utxorpc::Error::TransportError(err) => wit::SubmitError::Internal(err.to_string()),
utxorpc::Error::ParseError(err) => wit::SubmitError::Internal(err.to_string()),
})?;
}
utxorpc::Error::TransportError(err) => wit::SubmitError::Internal(err.to_string()),
utxorpc::Error::ParseError(err) => wit::SubmitError::Internal(err.to_string()),
})?;
Ok(())
}
}
2 changes: 1 addition & 1 deletion balius-runtime/tests/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ async fn faucet_claim() {

let store = Store::open("tests/balius.db", None).unwrap();

let mut runtime = Runtime::builder(store)
let runtime = Runtime::builder(store)
.with_ledger(ledgers::mock::Ledger.into())
.build()
.unwrap();
Expand Down
16 changes: 10 additions & 6 deletions balius-runtime/tests/u5c-chainsync.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#![cfg(test)]
#![cfg(feature = "utxorpc")]

use std::collections::HashMap;

use balius_runtime::{drivers, Runtime, Store};
use serde_json::json;
Expand All @@ -8,21 +9,24 @@ use tokio_util::sync::CancellationToken;
#[tokio::test]
async fn wallet_balance() {
let store = Store::open("tests/balius.db", None).unwrap();

let mut runtime = Runtime::builder(store).build().unwrap();

let runtime = Runtime::builder(store).build().unwrap();
let config = json!({
"address": "addr1qx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgse35a3x"
});

let wasm = std::fs::read("tests/wallet.wasm").unwrap();
runtime
.register_worker("wallet", "tests/wallet.wasm", config)
.register_worker("wallet", &wasm, config)
.await
.unwrap();

let chainsync_config = drivers::chainsync::Config {
endpoint_url: "https://mainnet.utxorpc-v0.demeter.run".to_string(),
api_key: "dmtr_utxorpc1wgnnj0qcfj32zxsz2uc8d4g7uclm2s2w".to_string(),
headers: Some(HashMap::from([(
"api-key".to_string(),
"dmtr_utxorpc1wgnnj0qcfj32zxsz2uc8d4g7uclm2s2w".to_string(),
)])),
// api_key: "dmtr_utxorpc1wgnnj0qcfj32zxsz2uc8d4g7uclm2s2w".to_string(),
};
Comment on lines +25 to 30
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Remove committed API key from test code.

Lines 25–30 expose a live credential (including in a comment). This is a security blocker; move it to environment/config secrets and rotate the leaked key.

🔐 Suggested fix
-    let chainsync_config = drivers::chainsync::Config {
+    let api_key = std::env::var("UTXORPC_API_KEY")
+        .expect("UTXORPC_API_KEY must be set for integration test");
+
+    let chainsync_config = drivers::chainsync::Config {
         endpoint_url: "https://mainnet.utxorpc-v0.demeter.run".to_string(),
         headers: Some(HashMap::from([(
             "api-key".to_string(),
-            "dmtr_utxorpc1wgnnj0qcfj32zxsz2uc8d4g7uclm2s2w".to_string(),
+            api_key,
         )])),
-        // api_key: "dmtr_utxorpc1wgnnj0qcfj32zxsz2uc8d4g7uclm2s2w".to_string(),
     };
🧰 Tools
🪛 Gitleaks (8.30.0)

[high] 29-29: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@balius-runtime/tests/u5c-chainsync.rs` around lines 25 - 30, The test
currently commits a real API key in the headers assignment (the "api-key" entry
and the commented api_key line); remove the literal string and instead read the
secret from an environment variable (e.g., std::env::var("UTXORPC_API_KEY"))
when constructing headers, and fail the test early or skip it with a clear
message if the env var is not set; also delete the commented hardcoded key and
rotate the leaked key in your secret store.


drivers::chainsync::run(chainsync_config, runtime, CancellationToken::new())
Expand Down
14 changes: 8 additions & 6 deletions balius-runtime/tests/u5c-ledger.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
#![cfg(test)]
#![cfg(feature = "utxorpc")]

use balius_runtime::{ledgers, Runtime, Store};
use serde_json::json;
use std::collections::HashMap;

#[tokio::test]
async fn faucet_claim() {
let store = Store::open("tests/balius.db", None).unwrap();

let ledger = ledgers::u5c::Ledger::new(ledgers::u5c::Config {
let ledger = ledgers::u5c::Ledger::new(&ledgers::u5c::Config {
endpoint_url: "https://mainnet.utxorpc-v0.demeter.run".to_string(),
api_key: "dmtr_utxorpc1wgnnj0qcfj32zxsz2uc8d4g7uclm2s2w".to_string(),
headers: Some(HashMap::from([
("api-key".to_string(), "dmtr_utxorpc1wgnnj0qcfj32zxsz2uc8d4g7uclm2s2w".to_string()),
])),
Comment on lines +10 to +14
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Remove hardcoded API key from test source.

Line 13 embeds a real credential in plaintext. This is a secret-leak risk and should be replaced with environment-based injection (or test skip when missing).

🔐 Suggested fix
-    let ledger = ledgers::u5c::Ledger::new(&ledgers::u5c::Config {
+    let api_key = match std::env::var("UTXORPC_API_KEY") {
+        Ok(v) => v,
+        Err(_) => {
+            eprintln!("Skipping test: UTXORPC_API_KEY is not set");
+            return;
+        }
+    };
+
+    let ledger = ledgers::u5c::Ledger::new(&ledgers::u5c::Config {
         endpoint_url: "https://mainnet.utxorpc-v0.demeter.run".to_string(),
         headers: Some(HashMap::from([
-            ("api-key".to_string(), "dmtr_utxorpc1wgnnj0qcfj32zxsz2uc8d4g7uclm2s2w".to_string()),
+            ("api-key".to_string(), api_key),
         ])),
     })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let ledger = ledgers::u5c::Ledger::new(&ledgers::u5c::Config {
endpoint_url: "https://mainnet.utxorpc-v0.demeter.run".to_string(),
api_key: "dmtr_utxorpc1wgnnj0qcfj32zxsz2uc8d4g7uclm2s2w".to_string(),
headers: Some(HashMap::from([
("api-key".to_string(), "dmtr_utxorpc1wgnnj0qcfj32zxsz2uc8d4g7uclm2s2w".to_string()),
])),
let api_key = match std::env::var("UTXORPC_API_KEY") {
Ok(v) => v,
Err(_) => {
eprintln!("Skipping test: UTXORPC_API_KEY is not set");
return;
}
};
let ledger = ledgers::u5c::Ledger::new(&ledgers::u5c::Config {
endpoint_url: "https://mainnet.utxorpc-v0.demeter.run".to_string(),
headers: Some(HashMap::from([
("api-key".to_string(), api_key),
])),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@balius-runtime/tests/u5c-ledger.rs` around lines 10 - 14, Replace the
hardcoded API key in the test that constructs ledgers::u5c::Ledger::new with
environment-based injection: read the API key from an env var (e.g. U5C_API_KEY)
and set headers only if the variable is present, and if not present either omit
the header and run the test against a public endpoint or early-skip the test
(use a test skip/return) instead of embedding secrets; update the Config.headers
construction where headers: Some(HashMap::from([...])) currently includes the
literal key to conditionally build headers from the env var or skip the test.

})
.await
.unwrap();

let mut runtime = Runtime::builder(store)
let runtime = Runtime::builder(store)
.with_ledger(ledger.into())
.build()
.unwrap();
Expand All @@ -31,8 +32,9 @@ async fn faucet_claim() {
}
});

let wasm = std::fs::read("tests/faucet.wasm").unwrap();
runtime
.register_worker("faucet", "tests/faucet.wasm", config)
.register_worker("faucet", &wasm, config)
.await
.unwrap();

Expand Down
2 changes: 1 addition & 1 deletion balius-sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ repository = "https://github.com/txpipe/balius"
readme = "README.md"

[dependencies]
utxorpc-spec = { version = "0.16.0", features = ["ledgers"], default-features = false }
utxorpc-spec = { version = "0.18.1", features = ["ledgers"], default-features = false }
balius-macros = { version = "0.5.2", path = "../balius-macros" }
hex = "0.4.3"
pallas-addresses = { version = "0.32.0" }
Expand Down
17 changes: 17 additions & 0 deletions balius-sdk/src/qol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,23 @@ pub struct Utxo<D> {
pub datum: Option<D>,
}

impl<D> Utxo<D> {
pub fn coin(&self) -> u64 {
self.utxo
.coin
.as_ref()
.and_then(|x| {
x.big_int.as_ref().and_then(|y| match y {
utxorpc_spec::utxorpc::v1alpha::cardano::big_int::BigInt::Int(z) => {
Some(*z as u64)
}
Comment on lines +291 to +293
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find and examine the qol.rs file
find . -name "qol.rs" -type f

Repository: txpipe/balius

Length of output: 80


🏁 Script executed:

# Check the structure of the repository
git ls-files | head -20

Repository: txpipe/balius

Length of output: 552


🏁 Script executed:

# Search for the coin() method in the codebase
rg "fn coin" -A 15

Repository: txpipe/balius

Length of output: 843


🏁 Script executed:

# Look for the utxorpc-spec dependency version
fd "Cargo.toml" | xargs grep -l "utxorpc" 2>/dev/null

Repository: txpipe/balius

Length of output: 140


🏁 Script executed:

# Check the balius-sdk Cargo.toml for utxorpc-spec version
cat balius-sdk/Cargo.toml

Repository: txpipe/balius

Length of output: 892


🏁 Script executed:

# Search for BigInt type definition in the codebase
rg "enum BigInt" -A 10

Repository: txpipe/balius

Length of output: 39


🏁 Script executed:

# Also search for where BigInt is imported from
rg "BigInt::" | head -20

Repository: txpipe/balius

Length of output: 673


🏁 Script executed:

# Check how BigInt::Int is handled in dsl.rs
rg "BigInt::Int" -B 3 -A 3 balius-sdk/src/txbuilder/dsl.rs

Repository: txpipe/balius

Length of output: 347


🏁 Script executed:

# Let's also check the full context around the coin() method in qol.rs
cat -n balius-sdk/src/qol.rs | sed -n '280,300p'

Repository: txpipe/balius

Length of output: 750


🌐 Web query:

utxorpc-spec 0.18.1 Rust BigInt::Int type definition cardano

💡 Result:

In utxorpc-spec Rust crate v0.18.1, Cardano’s BigInt::Int is the Int(i64) variant of the generated enum:

pub enum BigInt {
    Int(i64),
    BigUInt(Bytes),
    BigNInt(Bytes),
}

So the BigInt::Int payload/type is i64. [1]


🏁 Script executed:

# Check the similar pattern in dsl.rs to see if it has the same issue
cat -n balius-sdk/src/txbuilder/dsl.rs | grep -B 5 -A 5 "BigInt::Int"

Repository: txpipe/balius

Length of output: 512


Fix lossy cast in coin() extraction.

Line 292 casts an i64 to u64 without bounds checking. Since BigInt::Int holds i64 values from utxorpc-spec 0.18.1, negative values will wrap to very large u64s, producing incorrect balances. Use u64::try_from(*z).ok() instead to properly handle out-of-range values.

💡 Suggested fix
 impl<D> Utxo<D> {
     pub fn coin(&self) -> u64 {
         self.utxo
             .coin
             .as_ref()
             .and_then(|x| {
                 x.big_int.as_ref().and_then(|y| match y {
                     utxorpc_spec::utxorpc::v1alpha::cardano::big_int::BigInt::Int(z) => {
-                        Some(*z as u64)
+                        u64::try_from(*z).ok()
                     }
                     _ => None,
                 })
             })
             .unwrap_or_default()
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
utxorpc_spec::utxorpc::v1alpha::cardano::big_int::BigInt::Int(z) => {
Some(*z as u64)
}
utxorpc_spec::utxorpc::v1alpha::cardano::big_int::BigInt::Int(z) => {
u64::try_from(*z).ok()
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@balius-sdk/src/qol.rs` around lines 291 - 293, The match arm in coin() that
handles utxorpc_spec::utxorpc::v1alpha::cardano::big_int::BigInt::Int currently
does a lossy cast (*z as u64) which wraps negatives; change it to attempt a
fallible conversion using u64::try_from(*z).ok() so negative or out-of-range i64
values map to None instead of producing huge u64 balances, i.e. replace the
direct cast in the BigInt::Int arm with a try_from-based conversion and return
that Option result from coin().

_ => None,
})
})
.unwrap_or_default()
}
}

impl<D> TryFrom<wit::Event> for Utxo<D> {
type Error = Error;

Expand Down
17 changes: 16 additions & 1 deletion balius-sdk/src/txbuilder/dsl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,22 @@ impl ValueExpr for MinUtxoLovelace {
};

let serialized = pallas_codec::minicbor::to_vec(parent).unwrap();
let min_lovelace = (160u64 + serialized.len() as u64) * ctx.pparams.coins_per_utxo_byte;
let coins_per_utxo_byte = ctx
.pparams
.coins_per_utxo_byte
.as_ref()
.and_then(|x| {
x.big_int.as_ref().and_then(|y| match y {
utxorpc_spec::utxorpc::v1alpha::cardano::big_int::BigInt::Int(z) => {
Some(*z as u64)
}
Comment on lines +349 to +351
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find the file and examine the specific lines
find . -name "dsl.rs" -path "*/txbuilder/*" | head -5

Repository: txpipe/balius

Length of output: 90


🏁 Script executed:

# Get a broader context around the lines in question
sed -n '340,365p' balius-sdk/src/txbuilder/dsl.rs

Repository: txpipe/balius

Length of output: 1016


🏁 Script executed:

# Search for all occurrences of coins_per_utxo_byte in the file
rg "coins_per_utxo_byte" balius-sdk/src/txbuilder/dsl.rs -A 5 -B 5

Repository: txpipe/balius

Length of output: 1026


🏁 Script executed:

# Let's check the type definition of BigInt::Int to confirm the type
rg "BigInt::Int" balius-sdk/src/txbuilder/dsl.rs -B 10 -A 10 | head -40

Repository: txpipe/balius

Length of output: 881


🏁 Script executed:

# Check if utxorpc-spec is in the dependencies
find . -name "Cargo.toml" -exec grep -l "utxorpc" {} \;

Repository: txpipe/balius

Length of output: 146


🏁 Script executed:

# Check the Cargo.toml for utxorpc-spec version
grep -A 10 "utxorpc" balius-sdk/Cargo.toml | head -20

Repository: txpipe/balius

Length of output: 521


🏁 Script executed:

# Check around lines 355-357 to see if there's a similar pattern
sed -n '355,365p' balius-sdk/src/txbuilder/dsl.rs

Repository: txpipe/balius

Length of output: 513


🏁 Script executed:

# Search for type definition of BigInt in the dependencies/source
rg "BigInt" balius-sdk/Cargo.lock -A 3 | head -30

Repository: txpipe/balius

Length of output: 118


🏁 Script executed:

# Check if there's a utxorpc-spec definition file in the repo
find . -path "*/utxorpc*" -name "*.rs" | head -10

Repository: txpipe/balius

Length of output: 39


🏁 Script executed:

# Look at the imports in dsl.rs to understand the full path
head -50 balius-sdk/src/txbuilder/dsl.rs | grep -E "use|import"

Repository: txpipe/balius

Length of output: 261


🏁 Script executed:

# Let's check for the second occurrence mentioned "Also applies to: 355-357"
# First get the full context around these lines
sed -n '349,365p' balius-sdk/src/txbuilder/dsl.rs

Repository: txpipe/balius

Length of output: 730


🏁 Script executed:

# Search for all similar patterns in the file that might have the same issue
rg "as u64" balius-sdk/src/txbuilder/dsl.rs -B 5 -A 2

Repository: txpipe/balius

Length of output: 986


🏁 Script executed:

# Check the struct/module definitions to understand the full picture
ast-grep --pattern 'BigInt::Int($_) => { $$$  }'

Repository: txpipe/balius

Length of output: 39


🌐 Web query:

utxorpc-spec 0.18.1 BigInt::Int type definition Cardano coins_per_utxo_byte

💡 Result:

In utxorpc-spec 0.18.1 (Rust bindings):

  • BigInt is an enum whose Int variant is defined as Int(i64) (with the other variants BigUInt(Bytes) and BigNInt(Bytes)). [1]
  • In Cardano protocol parameters (PParams), coins_per_utxo_byte is typed as Option<BigInt>. [2]

Implication: coins_per_utxo_byte may be represented as Some(BigInt::Int(<i64>)) when it fits in 64-bit signed range, otherwise via the big-(u)int byte variants. [1][2]

Sources: [1] [2]


🏁 Script executed:

# Let's check if there are other similar patterns with BigInt::Int casting
rg "BigInt::Int" balius-sdk/src/txbuilder/dsl.rs -B 3 -A 3

Repository: txpipe/balius

Length of output: 347


🏁 Script executed:

# Count all occurrences of "as u64" specifically after BigInt::Int
grep -n "as u64" balius-sdk/src/txbuilder/dsl.rs

Repository: txpipe/balius

Length of output: 272


🏁 Script executed:

# Let's look at the complete function to understand both locations
sed -n '330,375p' balius-sdk/src/txbuilder/dsl.rs

Repository: txpipe/balius

Length of output: 1667


🏁 Script executed:

# Search for other occurrences of BigInt in the codebase that might match the "Also applies to" claim
rg "BigInt::" balius-sdk/src/txbuilder/dsl.rs -n

Repository: txpipe/balius

Length of output: 150


🏁 Script executed:

# Check if there might be another function with similar pattern
rg "as u64" balius-sdk/src/txbuilder/ -B 5 -A 2

Repository: txpipe/balius

Length of output: 2380


🏁 Script executed:

# Verify the line numbers more carefully - check what's at lines 355-357
sed -n '354,358p' balius-sdk/src/txbuilder/dsl.rs | cat -n

Repository: txpipe/balius

Length of output: 330


Replace unsafe cast with proper validation for coins_per_utxo_byte.

Line 350 uses as u64 to cast an i64, which wraps negative values and corrupts min_lovelace calculation. Use u64::try_from(*z).ok() to reject negative/invalid values, and update the error message to reflect that the parameter is invalid rather than missing.

💡 Suggested fix
             .and_then(|x| {
                 x.big_int.as_ref().and_then(|y| match y {
                     utxorpc_spec::utxorpc::v1alpha::cardano::big_int::BigInt::Int(z) => {
-                        Some(*z as u64)
+                        u64::try_from(*z).ok()
                     }
                     _ => None,
                 })
             })
             .ok_or(BuildError::LedgerError(
-                "Missing coins_per_utxo_byte protocol parameter".to_string(),
+                "Invalid coins_per_utxo_byte protocol parameter".to_string(),
             ))?;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@balius-sdk/src/txbuilder/dsl.rs` around lines 349 - 351, The match arm for
utxorpc_spec::utxorpc::v1alpha::cardano::big_int::BigInt::Int(z) currently uses
a wrapping cast (*z as u64) which corrupts negative values for
coins_per_utxo_byte; replace that cast with u64::try_from(*z).ok() so negative
or out-of-range i64 values return None, and update the subsequent error handling
message that references coins_per_utxo_byte/min_lovelace to say the parameter is
invalid (not missing) when conversion fails; search for the match using
BigInt::Int and the code that computes min_lovelace/coins_per_utxo_byte to apply
this change.

_ => None,
})
})
.ok_or(BuildError::LedgerError(
"Missing coins_per_utxo_byte protocol parameter".to_string(),
))?;
let min_lovelace = (160u64 + serialized.len() as u64) * coins_per_utxo_byte;
let current_value = match parent {
conway::PseudoTransactionOutput::PostAlonzo(x) => &x.value,
_ => unimplemented!(),
Expand Down
2 changes: 1 addition & 1 deletion examples/wallet/offchain/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ fn handle_utxo(_: Config<WalletConfig>, utxo: Utxo<Datum>) -> WorkerResult<Ack>
let balances = BalanceTable::new("balances".to_string());

balances
.set(&hex::encode(&utxo.utxo.address), utxo.utxo.coin)
.set(&hex::encode(&utxo.utxo.address), utxo.coin())
.unwrap();

Ok(Ack)
Expand Down
Loading