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
7 changes: 7 additions & 0 deletions contracts/Cargo.lock

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

1 change: 1 addition & 0 deletions contracts/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ members = [
"contrib",
"multisig-wallet",
"multisig_transfer",
"opsce",
]

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

[lib]
crate-type = ["lib", "cdylib"]
doctest = false

[dependencies]
soroban-sdk = { workspace = true }

[dev-dependencies]
soroban-sdk = { workspace = true, features = ["testutils"] }
20 changes: 20 additions & 0 deletions contracts/opsce/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use soroban_sdk::contracterror;

#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum ContractError {
WalletNotFound = 1,
TransactionNotFound = 2,
NotAnOwner = 3,
AlreadyApproved = 4,
ApprovalNotFound = 5,
AlreadyExecuted = 6,
InsufficientApprovals = 7,
InvalidThreshold = 8,
InsufficientOwners = 9,
InvalidAssetId = 10,
InvalidCost = 11,
DuplicateRecord = 12,
RecordNotFound = 13,
}
232 changes: 232 additions & 0 deletions contracts/opsce/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
#![no_std]

use soroban_sdk::{contract, contractimpl, Address, BytesN, Env, String, Vec};

pub mod error;
pub mod maintenance_record;
pub mod multisig_revoke;
pub mod types;

#[cfg(test)]
mod tests;

pub use crate::error::ContractError;
pub use crate::types::{
DataKey, MaintenanceRecord, MaintenanceRecordType, MaintenanceStatus, Transaction, Wallet,
};

#[contract]
pub struct OpsceMultisig;

#[contractimpl]
impl OpsceMultisig {
/// Create a new multisig wallet and return its `wallet_id`.
pub fn create_wallet(
env: Env,
admin: Address,
owners: Vec<Address>,
threshold: u32,
) -> Result<u64, ContractError> {
admin.require_auth();

if owners.len() < 2 {
return Err(ContractError::InsufficientOwners);
}
if threshold == 0 || threshold > owners.len() {
return Err(ContractError::InvalidThreshold);
}

let wallet_id: u64 = env
.storage()
.instance()
.get(&DataKey::NextWalletId)
.unwrap_or(1);
env.storage()
.instance()
.set(&DataKey::NextWalletId, &(wallet_id + 1));

let wallet = Wallet {
id: wallet_id,
owners,
threshold,
};
env.storage()
.persistent()
.set(&DataKey::Wallet(wallet_id), &wallet);
env.storage()
.persistent()
.set(&DataKey::NextTxId(wallet_id), &1u64);

Ok(wallet_id)
}

/// Submit a new transaction proposal for a wallet. Returns its `tx_id`.
pub fn submit_transaction(
env: Env,
initiator: Address,
wallet_id: u64,
) -> Result<u64, ContractError> {
initiator.require_auth();

let wallet: Wallet = env
.storage()
.persistent()
.get(&DataKey::Wallet(wallet_id))
.ok_or(ContractError::WalletNotFound)?;

if !wallet.owners.contains(&initiator) {
return Err(ContractError::NotAnOwner);
}

let tx_id: u64 = env
.storage()
.persistent()
.get(&DataKey::NextTxId(wallet_id))
.unwrap_or(1);
env.storage()
.persistent()
.set(&DataKey::NextTxId(wallet_id), &(tx_id + 1));

let tx = Transaction {
id: tx_id,
wallet_id,
initiator,
approvers: Vec::new(&env),
approvals: 0,
executed: false,
};
env.storage()
.persistent()
.set(&DataKey::Transaction(wallet_id, tx_id), &tx);

Ok(tx_id)
}

/// Approve a pending transaction.
pub fn approve_transaction(
env: Env,
caller: Address,
wallet_id: u64,
tx_id: u64,
) -> Result<(), ContractError> {
caller.require_auth();

let wallet: Wallet = env
.storage()
.persistent()
.get(&DataKey::Wallet(wallet_id))
.ok_or(ContractError::WalletNotFound)?;

if !wallet.owners.contains(&caller) {
return Err(ContractError::NotAnOwner);
}

let mut tx: Transaction = env
.storage()
.persistent()
.get(&DataKey::Transaction(wallet_id, tx_id))
.ok_or(ContractError::TransactionNotFound)?;

if tx.executed {
return Err(ContractError::AlreadyExecuted);
}
if tx.approvers.contains(&caller) {
return Err(ContractError::AlreadyApproved);
}

tx.approvers.push_back(caller);
tx.approvals += 1;

env.storage()
.persistent()
.set(&DataKey::Transaction(wallet_id, tx_id), &tx);

Ok(())
}

/// Execute the transaction once the approval threshold has been reached.
pub fn execute_transaction(
env: Env,
wallet_id: u64,
tx_id: u64,
) -> Result<(), ContractError> {
let wallet: Wallet = env
.storage()
.persistent()
.get(&DataKey::Wallet(wallet_id))
.ok_or(ContractError::WalletNotFound)?;

let mut tx: Transaction = env
.storage()
.persistent()
.get(&DataKey::Transaction(wallet_id, tx_id))
.ok_or(ContractError::TransactionNotFound)?;

if tx.executed {
return Err(ContractError::AlreadyExecuted);
}
if tx.approvals < wallet.threshold {
return Err(ContractError::InsufficientApprovals);
}

tx.executed = true;
env.storage()
.persistent()
.set(&DataKey::Transaction(wallet_id, tx_id), &tx);

Ok(())
}

/// Revoke a previously submitted approval (see [`multisig_revoke::revoke_approval`]).
pub fn revoke_approval(
env: Env,
caller: Address,
wallet_id: u64,
tx_id: u64,
) -> Result<(), ContractError> {
multisig_revoke::revoke_approval(&env, caller, wallet_id, tx_id)
}

/// Create a maintenance record (see [`maintenance_record::create_maintenance_record`]).
pub fn create_maintenance_record(
env: Env,
asset_id: String,
record_type: MaintenanceRecordType,
provider: Address,
scheduled_date: u64,
cost: i128,
notes: String,
) -> Result<BytesN<32>, ContractError> {
maintenance_record::create_maintenance_record(
&env,
asset_id,
record_type,
provider,
scheduled_date,
cost,
notes,
)
}

/// Get all maintenance records associated with the given `asset_id`.
pub fn get_maintenance_records(env: Env, asset_id: String) -> Vec<MaintenanceRecord> {
maintenance_record::get_maintenance_records(&env, asset_id)
}

/// Get a single maintenance record by its `record_id`.
pub fn get_maintenance_record(
env: Env,
record_id: BytesN<32>,
) -> Option<MaintenanceRecord> {
env.storage()
.persistent()
.get(&DataKey::MaintenanceRecord(record_id))
}

/// Read-only getter for tests / clients.
pub fn get_transaction(env: Env, wallet_id: u64, tx_id: u64) -> Option<Transaction> {
env.storage()
.persistent()
.get(&DataKey::Transaction(wallet_id, tx_id))
}
}
Loading
Loading