diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa865a9..2548177 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index ae69fd7..68214cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } \ No newline at end of file diff --git a/contracts/utility_contracts/src/lib.rs b/contracts/utility_contracts/src/lib.rs index 94719e0..0590713 100644 --- a/contracts/utility_contracts/src/lib.rs +++ b/contracts/utility_contracts/src/lib.rs @@ -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(); @@ -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(); @@ -1789,10 +1811,20 @@ 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() @@ -1800,6 +1832,12 @@ impl UtilityContract { .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() @@ -1809,7 +1847,14 @@ 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() @@ -1817,7 +1862,15 @@ impl UtilityContract { .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); @@ -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() diff --git a/main.rs b/main.rs new file mode 100644 index 0000000..7b3880b --- /dev/null +++ b/main.rs @@ -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::(); + let pub_hex = verifying_key.to_bytes().iter().map(|b| format!("{:02x}", b)).collect::(); + + 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); +} \ No newline at end of file