Skip to content
Open
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
9 changes: 9 additions & 0 deletions apps/onchain/Cargo.lock

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

3 changes: 2 additions & 1 deletion apps/onchain/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ members = [
"contracts/treasury",
"contracts/idempotency-guard",
"contracts/yield_vault",
"contracts/feature_flags"
"contracts/feature_flags",
"contracts/cross_contract_view_helpers"
]
exclude = ["contracts/tests"]

Expand Down
12 changes: 12 additions & 0 deletions apps/onchain/contracts/cross_contract_view_helpers/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "cross_contract_view_helpers"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["lib"]

documentation = "../README.md"

[dependencies]
soroban-sdk = { workspace = true }
22 changes: 22 additions & 0 deletions apps/onchain/contracts/cross_contract_view_helpers/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Cross Contract View Helpers

This shared crate provides safe, standardized helpers for read-only cross-contract calls in Soroban.

## Usage

- `invoke_view0(env, contract, fn_name)` for zero-argument view functions.
- `invoke_view1(env, contract, fn_name, arg)` for single-argument view functions.

### Error handling

All helpers return `Result<T, ViewCallError>`:

- `ContractNotSet` when the target address is empty.
- `CallFailed` when the invocation itself fails.
- `InvalidResponse` when the result cannot be decoded.

### Example

```rust
let score: u64 = invoke_view1(env, &registry, &Symbol::new(env, "get_reputation"), voter.clone())?;
```
56 changes: 56 additions & 0 deletions apps/onchain/contracts/cross_contract_view_helpers/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#![no_std]

use soroban_sdk::{Address, Env, IntoVal, InvokeError, Symbol, TryFromVal, Val, Vec};

/// Standardized cross-contract view call failure reason.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ViewCallError {
ContractNotSet,
CallFailed,
InvalidResponse,
}

/// Invoke a view function on a target contract and return the decoded result.
pub fn invoke_view<T>(
env: &Env,
contract: &Address,
fn_name: &Symbol,
args: Vec<Val>,
) -> Result<T, ViewCallError>
where
T: TryFromVal<Env, Val>,
{
if contract.to_string().is_empty() {
return Err(ViewCallError::ContractNotSet);
}

match env.try_invoke_contract::<T, InvokeError>(contract, fn_name, args) {
Ok(Ok(val)) => Ok(val),
Ok(Err(_)) => Err(ViewCallError::InvalidResponse),
Err(_) => Err(ViewCallError::CallFailed),
}
}

/// Convenience helper for zero-argument view calls.
pub fn invoke_view0<T>(env: &Env, contract: &Address, fn_name: &Symbol) -> Result<T, ViewCallError>
where
T: TryFromVal<Env, Val>,
{
invoke_view(env, contract, fn_name, Vec::new(env))
}

/// Convenience helper for single-argument view calls.
pub fn invoke_view1<A, T>(
env: &Env,
contract: &Address,
fn_name: &Symbol,
arg: A,
) -> Result<T, ViewCallError>
where
A: IntoVal<Env, Val>,
T: TryFromVal<Env, Val>,
{
let mut args = Vec::new(env);
args.push_back(arg.into_val(env));
invoke_view(env, contract, fn_name, args)
}
1 change: 1 addition & 0 deletions apps/onchain/contracts/lumenpulse-curation/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ edition = "2021"
crate-type = ["cdylib"]

[dependencies]
cross_contract_view_helpers = { path = "../cross_contract_view_helpers" }
soroban-sdk = { workspace = true }

[dev-dependencies]
Expand Down
21 changes: 14 additions & 7 deletions apps/onchain/contracts/lumenpulse-curation/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod types;
pub use errors::CurationError;
pub use types::{ProjectMetadata, ProjectStatus, ProposalState, VoteRecord};

use cross_contract_view_helpers::{invoke_view0, invoke_view1};
use soroban_sdk::{contract, contractimpl, token, Address, Env};

use events::*;
Expand Down Expand Up @@ -269,23 +270,29 @@ impl CommunityCurationContract {

/// Cross-contract call into contributor-registry to read a voter's reputation.
fn get_reputation(env: &Env, voter: &Address) -> u64 {
// contributor-registry exposes: get_reputation(address) -> u64
let registry = get_contributor_registry(env);
env.invoke_contract(
match invoke_view1(
env,
&registry,
&soroban_sdk::Symbol::new(env, "get_reputation"),
soroban_sdk::vec![env, voter.to_val()],
)
voter.clone(),
) {
Ok(score) => score,
Err(_) => 0,
}
}

/// Cross-contract call to read the sum of all reputations (total supply proxy).
fn get_total_reputation(env: &Env) -> u64 {
let registry = get_contributor_registry(env);
env.invoke_contract(
match invoke_view0(
env,
&registry,
&soroban_sdk::Symbol::new(env, "total_reputation"),
soroban_sdk::vec![env],
)
) {
Ok(total) => total,
Err(_) => 0,
}
}

/// Check whether YES votes cross the threshold; update status in place.
Expand Down
1 change: 1 addition & 0 deletions apps/onchain/contracts/project_registry/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ crate-type = ["lib", "cdylib"]
doctest = false

[dependencies]
cross_contract_view_helpers = { path = "../cross_contract_view_helpers" }
soroban-sdk = { workspace = true }

[dev-dependencies]
Expand Down
36 changes: 21 additions & 15 deletions apps/onchain/contracts/project_registry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ mod errors;
mod events;
mod storage;

use cross_contract_view_helpers::invoke_view1;
use errors::RegistryError;
use soroban_sdk::token::TokenClient;
use soroban_sdk::{contract, contractimpl, Address, BytesN, Env, IntoVal, Symbol};
use soroban_sdk::{contract, contractimpl, Address, BytesN, Env, Symbol};
use storage::{DataKey, ProjectEntry, RegistryConfig, VerificationStatus, WeightMode};

#[contract]
Expand Down Expand Up @@ -46,16 +47,16 @@ impl ProjectRegistryContract {
fn resolve_weight(env: &Env, config: &RegistryConfig, voter: &Address) -> i128 {
let weight = match config.weight_mode {
WeightMode::Reputation => {
// Read reputation_score from contributor_registry via cross-contract call.
// The contributor_registry exposes get_reputation(contributor) -> u64.
// We call it generically via invoke_contract.
if let Some(ref registry) = config.contributor_registry {
let score: u64 = env.invoke_contract(
match invoke_view1::<Address, i128>(
env,
registry,
&Symbol::new(env, "get_reputation"),
soroban_sdk::vec![env, voter.into_val(env)],
);
score as i128
voter.clone(),
) {
Ok(score) => score,
Err(_) => 0,
}
} else {
0
}
Expand All @@ -72,15 +73,20 @@ impl ProjectRegistryContract {
// We check registration via contributor_registry if configured,
// otherwise grant weight 1 to any caller.
if let Some(ref registry) = config.contributor_registry {
let exists: bool = env.invoke_contract(
match invoke_view1::<Address, bool>(
env,
registry,
&Symbol::new(env, "is_registered"),
soroban_sdk::vec![env, voter.into_val(env)],
);
if exists {
1
} else {
0
voter.clone(),
) {
Ok(exists) => {
if exists {
1
} else {
0
}
}
Err(_) => 0,
}
} else {
1
Expand Down
Loading