canic-host 0.110.0

Host-side App build and desired-state Fleet reconciliation library for Canic workspaces
Documentation
use candid::{CandidType, Deserialize, Nat, Principal};
use ic_cdk::call::Call;
use std::cell::RefCell;

#[used]
static CANIC_RELEASE_BUILD_ID: &str = match option_env!("CANIC_RELEASE_BUILD_ID") {
    Some(value) => value,
    None => "",
};

#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
struct PoolLedgerRecoveryInit {
    amount: Nat,
    canister: Principal,
    created_at_time_ns: u64,
    cycles_ledger: Principal,
    operation_id: [u8; 32],
    root: Principal,
}

#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
struct PoolLedgerRecoveryReceipt {
    amount: Nat,
    block_index: Nat,
    canister: Principal,
    operation_id: [u8; 32],
}

#[derive(CandidType, Clone)]
struct WithdrawArgs {
    amount: Nat,
    created_at_time: Option<u64>,
    from_subaccount: Option<[u8; 32]>,
    to: Principal,
}

#[derive(CandidType, Debug, Deserialize)]
enum WithdrawError {
    BadFee {
        expected_fee: Nat,
    },
    CreatedInFuture {
        ledger_time: u64,
    },
    Duplicate {
        duplicate_of: Nat,
    },
    FailedToWithdraw {
        fee_block: Option<Nat>,
        rejection_code: RejectionCode,
        rejection_reason: String,
    },
    GenericError {
        error_code: Nat,
        message: String,
    },
    InsufficientFunds {
        balance: Nat,
    },
    InvalidReceiver {
        receiver: Principal,
    },
    TemporarilyUnavailable,
    TooOld,
}

#[derive(CandidType, Debug, Deserialize)]
enum RejectionCode {
    CanisterError,
    CanisterReject,
    DestinationInvalid,
    NoError,
    SysFatal,
    SysTransient,
    Unknown,
}

thread_local! {
    static AUTHORITY: RefCell<Option<PoolLedgerRecoveryInit>> = const { RefCell::new(None) };
}

#[ic_cdk::init]
fn init(authority: PoolLedgerRecoveryInit) {
    assert_eq!(authority.canister, ic_cdk::api::canister_self());
    assert_ne!(authority.root, Principal::anonymous());
    assert_ne!(authority.cycles_ledger, Principal::anonymous());
    assert!(authority.amount > Nat::from(0_u8));
    AUTHORITY.with_borrow_mut(|slot| *slot = Some(authority));
}

#[ic_cdk::query]
fn authority() -> PoolLedgerRecoveryInit {
    AUTHORITY.with_borrow(|slot| slot.clone().expect("recovery helper is initialized"))
}

#[ic_cdk::update]
async fn recover() -> Result<PoolLedgerRecoveryReceipt, String> {
    let authority = authority();
    if ic_cdk::api::msg_caller() != authority.root {
        return Err("only the exact owning Root may recover this pool account".to_string());
    }
    let request = WithdrawArgs {
        amount: authority.amount.clone(),
        created_at_time: Some(authority.created_at_time_ns),
        from_subaccount: None,
        to: authority.canister,
    };
    let response = Call::bounded_wait(authority.cycles_ledger, "withdraw")
        .with_arg(request)
        .await
        .map_err(|error| format!("Cycles Ledger withdrawal failed: {error}"))?;
    let result = response
        .candid::<Result<Nat, WithdrawError>>()
        .map_err(|error| format!("Cycles Ledger withdrawal response is invalid: {error}"))?;
    let block_index = match result {
        Ok(block_index)
        | Err(WithdrawError::Duplicate {
            duplicate_of: block_index,
        }) => block_index,
        Err(error) => return Err(withdraw_error(error)),
    };
    Ok(PoolLedgerRecoveryReceipt {
        amount: authority.amount,
        block_index,
        canister: authority.canister,
        operation_id: authority.operation_id,
    })
}

fn withdraw_error(error: WithdrawError) -> String {
    match error {
        WithdrawError::BadFee { expected_fee } => format!("bad fee; expected {expected_fee}"),
        WithdrawError::CreatedInFuture { ledger_time } => {
            format!("created in future; ledger time {ledger_time}")
        }
        WithdrawError::FailedToWithdraw {
            fee_block,
            rejection_code,
            rejection_reason,
        } => {
            format!(
                "withdrawal failed: {rejection_code:?}; {rejection_reason}; fee block {fee_block:?}"
            )
        }
        WithdrawError::GenericError {
            error_code,
            message,
        } => format!("Ledger error {error_code}: {message}"),
        WithdrawError::InsufficientFunds { balance } => {
            format!("insufficient funds; balance {balance}")
        }
        WithdrawError::InvalidReceiver { receiver } => format!("invalid receiver {receiver}"),
        WithdrawError::TemporarilyUnavailable => "Ledger temporarily unavailable".to_string(),
        WithdrawError::TooOld => "Ledger request is too old".to_string(),
        WithdrawError::Duplicate { .. } => unreachable!("duplicate is accepted above"),
    }
}

ic_cdk::export_candid!();