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
83 changes: 36 additions & 47 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,44 @@ name: Soroban CI

on:
push:
branches: [ "main" ]
branches: [ main ]
pull_request:
branches: [ "main" ]

env:
CARGO_TERM_COLOR: always
branches: [ main ]

jobs:
test:
test-and-lint:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
targets: wasm32-unknown-unknown

- name: Cache Cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-

- name: Install Stellar CLI
run: |
wget https://github.com/stellar/stellar-cli/releases/download/v25.1.0/stellar-cli-25.1.0-x86_64-unknown-linux-gnu.tar.gz
tar -xzf stellar-cli-25.1.0-x86_64-unknown-linux-gnu.tar.gz
sudo mv stellar-cli-25.1.0-x86_64-unknown-linux-gnu/stellar /usr/local/bin/

- name: Build Contract
run: cargo build --target wasm32-unknown-unknown --release

- name: Install dependencies for optimization
run: sudo apt-get update && sudo apt-get install -y binaryen

- name: Optimize Wasm
run: |
stellar contract optimize --wasm target/wasm32-unknown-unknown/release/price_oracle.wasm
stellar contract optimize --wasm target/wasm32-unknown-unknown/release/utility_contracts.wasm

- name: Run Tests
run: cargo test --workspace
- uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
components: rustfmt, clippy
- name: Cache dependencies
uses: Swatinem/rust-cache@v2
- name: Check formatting
run: cargo fmt --all -- --check
- name: Run clippy
run: cargo clippy --all-targets --all-features -- -D warnings
- name: Run tests
run: cargo test

build-wasm:
needs: test-and-lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
- name: Cache dependencies
uses: Swatinem/rust-cache@v2
- name: Build WASM artifact
run: cargo build --target wasm32-unknown-unknown --release
- name: Upload compiled artifacts
uses: actions/upload-artifact@v4
with:
name: wasm-artifacts
path: target/wasm32-unknown-unknown/release/*.wasm
34 changes: 12 additions & 22 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,23 +1,13 @@
[workspace]
resolver = "2"
members = [
"contracts/*",
]
[package]
name = "iot-payload-generator"
version = "0.1.0"
edition = "2021"
description = "Generates mock hardware signatures and JSON payloads for local testing"

[workspace.dependencies]
soroban-sdk = "25"

[profile.release]
opt-level = "z"
overflow-checks = true
debug = 0
strip = "symbols"
debug-assertions = false
panic = "abort"
codegen-units = 1
lto = true

# For more information about this profile see https://soroban.stellar.org/docs/basic-tutorials/logging#cargotoml-profile
[profile.release-with-logs]
inherits = "release"
debug-assertions = true
[dependencies]
ed25519-dalek = { version = "2.1.0", features = ["rand_core"] }
rand = "0.8.5"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = "0.4"
clap = { version = "4.4", features = ["derive"] }
67 changes: 65 additions & 2 deletions contracts/utility_contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1766,6 +1766,18 @@ fn withdraw_from_flow(

#[contractimpl]
impl UtilityContract {
/// Assigns a reseller to a specific meter with a defined fee percentage.
///
/// # Arguments
/// * `env` - The execution environment.
/// * `meter_id` - The unique identifier of the meter.
/// * `reseller` - The address of the reseller to assign.
/// * `fee_bps` - The reseller fee in basis points (1 bp = 0.01%).
///
/// # Panics
/// * Panics if the caller is not the provider of the meter.
/// * Panics if the meter does not exist (`ContractError::MeterNotFound`).
/// * Panics if `fee_bps` exceeds `MAX_RESELLER_FEE_BPS` (`ContractError::InvalidResellerFee`).
pub fn assign_reseller(env: Env, meter_id: u64, reseller: Address, fee_bps: i128) {
let meter = get_meter_or_panic(&env, meter_id);
meter.provider.require_auth();
Expand All @@ -1776,6 +1788,16 @@ impl UtilityContract {
env.events().publish((symbol_short!("RslrSet"), meter_id), (reseller, fee_bps));
}

/// Claims an Impact Soulbound Token (SBT) for a user based on renewable energy usage.
///
/// # Arguments
/// * `env` - The execution environment.
/// * `meter_id` - The unique identifier of the meter.
///
/// # Panics
/// * Panics if the caller is not the user of the meter.
/// * Panics if the SBT has already been minted for this meter (`ContractError::SBTAlreadyMinted`).
/// * Panics if the renewable energy usage is below the threshold (`ContractError::ImpactNotSignificantEnough`).
pub fn claim_impact_sbt(env: Env, meter_id: u64) {
let meter = get_meter_or_panic(&env, meter_id);
meter.user.require_auth();
Expand All @@ -1789,17 +1811,33 @@ impl UtilityContract {
panic_with_error!(&env, ContractError::ImpactNotSignificantEnough);
}
}

/// Retrieves the minimum balance required for a continuous flow to remain active.
///
/// # Returns
/// * `i128` - The minimum balance required to flow.
pub fn get_minimum_balance_to_flow() -> i128 {
MINIMUM_BALANCE_TO_FLOW
}

/// Sets the address of the trusted price oracle.
///
/// # Arguments
/// * `env` - The execution environment.
/// * `oracle_address` - The address of the oracle contract.
pub fn set_oracle(env: Env, oracle_address: Address) {
// This should be called by admin to set the oracle address
env.storage()
.instance()
.set(&DataKey::Oracle, &oracle_address);
}

/// Sets the maintenance wallet address and the protocol fee basis points.
///
/// # Arguments
/// * `env` - The execution environment.
/// * `wallet` - The address of the maintenance wallet to receive protocol fees.
/// * `fee_bps` - The protocol fee in basis points.
pub fn set_maintenance_config(env: Env, wallet: Address, fee_bps: i128) {
env.storage()
.instance()
Expand All @@ -1809,15 +1847,30 @@ impl UtilityContract {
.set(&DataKey::ProtocolFeeBps, &fee_bps);
}

/// Set admin address for dust sweeper authorization
/// Sets the admin address for the contract, used for dust sweeper authorization.
///
/// # Arguments
/// * `env` - The execution environment.
/// * `admin_address` - The address to be set as the new admin.
///
/// # Panics
/// * Panics if the caller is not the current contract address (self-invocation).
pub fn set_admin(env: Env, admin_address: Address) {
env.current_contract_address().require_auth();
env.storage()
.instance()
.set(&DataKey::AdminAddress, &admin_address);
}

/// Add funds to gas bounty pool for dust sweepers
/// Adds funds to the gas bounty pool used to reward dust sweepers.
///
/// # Arguments
/// * `env` - The execution environment.
/// * `amount` - The amount of tokens to add to the gas bounty pool.
///
/// # Panics
/// * Panics if the caller is not the authorized admin (`ContractError::UnauthorizedAdmin`).
/// * Panics if `amount` is zero or negative (`ContractError::InvalidTokenAmount`).
pub fn fund_gas_bounty(env: Env, amount: i128) {
require_admin_auth(&env);

Expand All @@ -1840,12 +1893,22 @@ impl UtilityContract {
.publish((symbol_short!("BntyFund"),), amount);
}

/// Marks a given token address as supported by the system.
///
/// # Arguments
/// * `env` - The execution environment.
/// * `token` - The token address to whitelist.
pub fn add_supported_token(env: Env, token: Address) {
env.storage()
.instance()
.set(&DataKey::SupportedToken(token), &true);
}

/// Removes a previously supported token from the system's whitelist.
///
/// # Arguments
/// * `env` - The execution environment.
/// * `token` - The token address to revoke.
pub fn remove_supported_token(env: Env, token: Address) {
env.storage()
.instance()
Expand Down
69 changes: 69 additions & 0 deletions main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use chrono::Utc;
use clap::Parser;
use ed25519_dalek::{Signer, SigningKey, VerifyingKey};
use rand::rngs::OsRng;
use serde::{Deserialize, Serialize};

#[derive(Parser, Debug)]
#[command(author, version, about = "IoT Payload Generator for Utility Drip Contracts", long_about = None)]
struct Args {
/// Meter ID to simulate
#[arg(short, long, default_value_t = 1)]
meter_id: u64,

/// Watt hours consumed since last reading
#[arg(short, long, default_value_t = 1500)]
watt_hours: u64,

/// Abstract units consumed
#[arg(short, long, default_value_t = 1)]
units: u64,
}

#[derive(Serialize, Deserialize, Debug)]
struct MessageData {
meter_id: u64,
timestamp: u64,
watt_hours_consumed: u64,
units_consumed: u64,
}

#[derive(Serialize, Deserialize, Debug)]
struct IotPayload {
#[serde(flatten)]
data: MessageData,
signature: String,
public_key: String,
}

fn main() {
let args = Args::parse();
let mut csprng = OsRng;

// Generate an ed25519 keypair securely
let signing_key: SigningKey = SigningKey::generate(&mut csprng);
let verifying_key: VerifyingKey = (&signing_key).into();

let payload_data = MessageData {
meter_id: args.meter_id,
timestamp: Utc::now().timestamp() as u64,
watt_hours_consumed: args.watt_hours,
units_consumed: args.units,
};

// Generate ed25519 signature of the serialized JSON
let message_bytes = serde_json::to_vec(&payload_data).unwrap();
let signature = signing_key.sign(&message_bytes);

let sig_hex = signature.to_bytes().iter().map(|b| format!("{:02x}", b)).collect::<String>();
let pub_hex = verifying_key.to_bytes().iter().map(|b| format!("{:02x}", b)).collect::<String>();

let final_payload = IotPayload {
data: payload_data,
signature: sig_hex,
public_key: pub_hex,
};

let json_output = serde_json::to_string_pretty(&final_payload).unwrap();
println!("{}", json_output);
}
Loading