diff --git a/src/rumi_protocol_backend/src/main.rs b/src/rumi_protocol_backend/src/main.rs index 44eb006..945e0bb 100644 --- a/src/rumi_protocol_backend/src/main.rs +++ b/src/rumi_protocol_backend/src/main.rs @@ -5,9 +5,9 @@ use ic_cdk_macros::{init, post_upgrade, query, update}; use rumi_protocol_backend::{ event::Event, logs::INFO, - numeric::{ICUSD, UsdIcp}, + numeric::{ICUSD, UsdIcp, UsdCkBtc}, state::{read_state, replace_state, Mode, State}, - vault::{CandidVault, OpenVaultSuccess, VaultArg}, + vault::{CandidVault, OpenVaultSuccess, VaultArg, CollateralType}, Fees, GetEventsArg, ProtocolArg, ProtocolError, ProtocolStatus, SuccessWithFee, }; use rumi_protocol_backend::logs::DEBUG; @@ -23,7 +23,6 @@ use rumi_protocol_backend::LiquidityStatus; use candid_parser::utils::CandidSource; use candid_parser::utils::service_equal; - #[cfg(feature = "self_check")] fn ok_or_die(result: Result<(), String>) { if let Err(msg) = result { @@ -81,9 +80,15 @@ fn validate_mode() -> Result<(), ProtocolError> { } fn setup_timers() { + // Existing ICP rate fetching timer ic_cdk_timers::set_timer_interval(rumi_protocol_backend::xrc::FETCHING_ICP_RATE_INTERVAL, || { ic_cdk::spawn(rumi_protocol_backend::xrc::fetch_icp_rate()) }); + + // New ckBTC rate fetching timer + ic_cdk_timers::set_timer_interval(rumi_protocol_backend::xrc::FETCHING_CKBTC_RATE_INTERVAL, || { + ic_cdk::spawn(rumi_protocol_backend::xrc::fetch_ckbtc_rate()) + }); } fn main() {} @@ -156,7 +161,13 @@ fn get_protocol_status() -> ProtocolStatus { .unwrap_or(UsdIcp::from(Decimal::ZERO)) .to_f64(), last_icp_timestamp: s.last_icp_timestamp.unwrap_or(0), + last_ckbtc_rate: s + .last_ckbtc_rate + .unwrap_or(UsdCkBtc::from(Decimal::ZERO)) + .to_f64(), + last_ckbtc_timestamp: s.last_ckbtc_timestamp.unwrap_or(0), total_icp_margin: s.total_icp_margin_amount().to_u64(), + total_ckbtc_margin: s.total_ckbtc_margin_amount().to_u64(), total_icusd_borrowed: s.total_borrowed_icusd_amount().to_u64(), total_collateral_ratio: s.total_collateral_ratio.to_f64(), mode: s.mode, @@ -235,7 +246,9 @@ fn get_vaults(target: Option) -> Vec { owner: vault.owner, borrowed_icusd_amount: vault.borrowed_icusd_amount.to_u64(), icp_margin_amount: vault.icp_margin_amount.to_u64(), + ckbtc_margin_amount: vault.ckbtc_margin_amount.to_u64(), vault_id: vault.vault_id, + collateral_type: vault.collateral_type, } }) .collect(), @@ -248,7 +261,9 @@ fn get_vaults(target: Option) -> Vec { owner: vault.owner, borrowed_icusd_amount: vault.borrowed_icusd_amount.to_u64(), icp_margin_amount: vault.icp_margin_amount.to_u64(), + ckbtc_margin_amount: vault.ckbtc_margin_amount.to_u64(), vault_id: vault.vault_id, + collateral_type: vault.collateral_type, }) .collect::>() }), @@ -275,9 +290,9 @@ fn get_redemption_rate() -> f64 { #[candid_method(update)] #[update] -async fn open_vault(icp_margin: u64) -> Result { +async fn open_vault(collateral_amount: u64, collateral_type: CollateralType) -> Result { validate_call()?; - check_postcondition(rumi_protocol_backend::vault::open_vault(icp_margin).await) + check_postcondition(rumi_protocol_backend::vault::open_vault(collateral_amount, collateral_type).await) } #[candid_method(update)] @@ -338,31 +353,45 @@ async fn liquidate_vault(vault_id: u64) -> Result fn get_liquidatable_vaults() -> Vec { read_state(|s| { let current_icp_rate = s.last_icp_rate.unwrap_or(UsdIcp::from(dec!(0.0))); + let current_ckbtc_rate = s.last_ckbtc_rate.unwrap_or(UsdCkBtc::from(dec!(0.0))); - if current_icp_rate.to_f64() == 0.0 { + if current_icp_rate.to_f64() == 0.0 && current_ckbtc_rate.to_f64() == 0.0 { return vec![]; } s.vault_id_to_vaults .values() .filter(|vault| { - let ratio = rumi_protocol_backend::compute_collateral_ratio(vault, current_icp_rate); + let ratio = match vault.collateral_type { + CollateralType::ICP => { + if current_icp_rate.to_f64() == 0.0 { return false; } + rumi_protocol_backend::compute_collateral_ratio(vault, current_icp_rate, CollateralType::ICP) + }, + CollateralType::CkBTC => { + if current_ckbtc_rate.to_f64() == 0.0 { return false; } + rumi_protocol_backend::compute_collateral_ratio(vault, current_ckbtc_rate, CollateralType::CkBTC) + } + }; ratio < s.mode.get_minimum_liquidation_collateral_ratio() }) .map(|vault| { - let collateral_ratio = rumi_protocol_backend::compute_collateral_ratio(vault, current_icp_rate); + let collateral_ratio = match vault.collateral_type { + CollateralType::ICP => rumi_protocol_backend::compute_collateral_ratio(vault, current_icp_rate, CollateralType::ICP), + CollateralType::CkBTC => rumi_protocol_backend::compute_collateral_ratio(vault, current_ckbtc_rate, CollateralType::CkBTC), + }; CandidVault { owner: vault.owner, borrowed_icusd_amount: vault.borrowed_icusd_amount.to_u64(), icp_margin_amount: vault.icp_margin_amount.to_u64(), + ckbtc_margin_amount: vault.ckbtc_margin_amount.to_u64(), vault_id: vault.vault_id, + collateral_type: vault.collateral_type, } }) .collect::>() }) } - // Liquidity related operations #[candid_method(update)] #[update] @@ -457,10 +486,20 @@ fn http_request(req: HttpRequest) -> HttpResponse { "ICP rate.", )?; + w.encode_gauge( + "rumi_ckbtc_rate", + s.last_ckbtc_rate.unwrap_or(UsdCkBtc::from(dec!(0))).to_f64(), + "ckBTC rate.", + )?; + let total_icp_dec = Decimal::from_u64(s.total_icp_margin_amount().0) .expect("failed to construct decimal from u64") / dec!(100_000_000); + let total_ckbtc_dec = Decimal::from_u64(s.total_ckbtc_margin_amount().0) + .expect("failed to construct decimal from u64") + / dec!(100_000_000); + w.encode_gauge( "icp_total_ICP_margin", total_icp_dec.to_f64().unwrap(), @@ -468,10 +507,17 @@ fn http_request(req: HttpRequest) -> HttpResponse { )?; w.encode_gauge( - "ICP_total_tvl", - (total_icp_dec * s.last_icp_rate.unwrap_or(UsdIcp::from(dec!(0))).0) - .to_f64() - .unwrap(), + "ckbtc_total_CKBTC_margin", + total_ckbtc_dec.to_f64().unwrap(), + "Total ckBTC Margin.", + )?; + + let total_tvl = (total_icp_dec * s.last_icp_rate.unwrap_or(UsdIcp::from(dec!(0))).0) + + (total_ckbtc_dec * s.last_ckbtc_rate.unwrap_or(UsdCkBtc::from(dec!(0))).0); + + w.encode_gauge( + "total_tvl", + total_tvl.to_f64().unwrap(), "Total TVL.", )?; @@ -486,7 +532,7 @@ fn http_request(req: HttpRequest) -> HttpResponse { )?; w.encode_gauge( - "ICP_total_collateral_ratio", + "total_collateral_ratio", s.total_collateral_ratio.to_f64(), "TCR.", )?; @@ -597,14 +643,27 @@ async fn recover_pending_transfer(vault_id: u64) -> Result }); if let Some(transfer) = transfer_opt { - let icp_transfer_fee = read_state(|s| s.icp_ledger_fee); + let transfer_fee = match transfer.collateral_type { + CollateralType::ICP => read_state(|s| s.icp_ledger_fee), + CollateralType::CkBTC => read_state(|s| s.ckbtc_ledger_fee), + }; - match crate::management::transfer_icp( - transfer.margin - icp_transfer_fee, - transfer.owner, - ) - .await - { + let result = match transfer.collateral_type { + CollateralType::ICP => { + crate::management::transfer_icp( + transfer.margin - transfer_fee, + transfer.owner, + ).await + }, + CollateralType::CkBTC => { + crate::management::transfer_ckbtc( + transfer.margin - transfer_fee, + transfer.owner, + ).await + } + }; + + match result { Ok(block_index) => { mutate_state(|s| crate::event::record_margin_transfer(s, vault_id, block_index)); Ok(true) @@ -637,7 +696,6 @@ fn check_candid_interface_compatibility() { } } - fn check_service_compatible( new_name: &str, new: CandidSource, diff --git a/src/vault_frontend/src/lib/components/dashboard/ProtocolStats.svelte b/src/vault_frontend/src/lib/components/dashboard/ProtocolStats.svelte index ea1ee55..6c41588 100644 --- a/src/vault_frontend/src/lib/components/dashboard/ProtocolStats.svelte +++ b/src/vault_frontend/src/lib/components/dashboard/ProtocolStats.svelte @@ -6,9 +6,12 @@ let protocolStatus = { mode: 'GeneralAvailability', totalIcpMargin: 0, + totalCkbtcMargin: 0, totalIcusdBorrowed: 0, lastIcpRate: 0, lastIcpTimestamp: 0, + lastCkbtcRate: 0, + lastCkbtcTimestamp: 0, totalCollateralRatio: 0 }; @@ -27,9 +30,12 @@ protocolStatus = { mode: status.mode || 'GeneralAvailability', totalIcpMargin: Number(status.totalIcpMargin || 0), + totalCkbtcMargin: Number(status.totalCkbtcMargin || 0), totalIcusdBorrowed: Number(status.totalIcusdBorrowed || 0), lastIcpRate: Number(status.lastIcpRate || 0), lastIcpTimestamp: Number(status.lastIcpTimestamp || 0), + lastCkbtcRate: Number(status.lastCkbtcRate || 0), + lastCkbtcTimestamp: Number(status.lastCkbtcTimestamp || 0), totalCollateralRatio: Number(status.totalCollateralRatio || 0) }; @@ -67,9 +73,11 @@ }); $: icpValueInUsd = protocolStatus.totalIcpMargin * protocolStatus.lastIcpRate; + $: ckbtcValueInUsd = protocolStatus.totalCkbtcMargin * protocolStatus.lastCkbtcRate; + $: totalCollateralUsd = icpValueInUsd + ckbtcValueInUsd; $: collateralPercent = protocolStatus.totalIcusdBorrowed > 0 ? protocolStatus.totalCollateralRatio * 100 - : protocolStatus.totalIcpMargin > 0 + : (protocolStatus.totalIcpMargin > 0 || protocolStatus.totalCkbtcMargin > 0) ? Infinity : 0; @@ -93,32 +101,48 @@ }[protocolStatus.mode] || 'text-gray-500'; -
+
+
-
Total Collateral (ICP)
-
{formatNumber(protocolStatus.totalIcpMargin)} ICP
-
≈ ${formatNumber(icpValueInUsd)}
-
- -
-
Total icUSD Borrowed
-
{formatNumber(protocolStatus.totalIcusdBorrowed)} icUSD
+
Total Collateral Value
+
${formatNumber(totalCollateralUsd)}
+
+ ICP: {formatNumber(protocolStatus.totalIcpMargin)} (${formatNumber(icpValueInUsd)}) +
+
+ ckBTC: {formatNumber(protocolStatus.totalCkbtcMargin, 8)} (${formatNumber(ckbtcValueInUsd)}) +
+
-
Current ICP Price
-
+
Current Asset Prices
+
{#if isLoading}
{:else} - ${formatNumber(protocolStatus.lastIcpRate)} +
+
ICP: ${formatNumber(protocolStatus.lastIcpRate, 2)}
+
ckBTC: ${formatNumber(protocolStatus.lastCkbtcRate, 0)}
+
{/if}
- + +
-
Total Collateral Ratio
-
{formattedCollateralPercent}%
+
Protocol Metrics
+
+
+ icUSD Borrowed: {formatNumber(protocolStatus.totalIcusdBorrowed)} +
+
+ Collateral Ratio: {formattedCollateralPercent}% +
+
+ Mode: {modeDisplay} +
+
diff --git a/src/vault_frontend/src/lib/components/vault/CreateVault.svelte b/src/vault_frontend/src/lib/components/vault/CreateVault.svelte index f1aa951..536dd89 100644 --- a/src/vault_frontend/src/lib/components/vault/CreateVault.svelte +++ b/src/vault_frontend/src/lib/components/vault/CreateVault.svelte @@ -7,12 +7,17 @@ import { vaultStore } from '../../stores/vaultStore'; import { safeLog } from '../../utils/bigint'; import { BackendDebugService } from '../../services/backendDebug'; + import { CollateralType } from '../../services/types'; + import { priceService } from '../../services/priceService'; + import { onMount } from 'svelte'; export let icpPrice: number; + export let ckbtcPrice: number = 94500; // Default fallback price let collateralAmount = ""; + let collateralType: CollateralType = CollateralType.ICP; let isCreating = false; let error = ""; let showPasskeyInput = true; // Changed to true to show by default @@ -42,11 +47,24 @@ let cancelCheckRunning = false; let vaultCheckMode = false; - $: potentialUsdValue = Number(collateralAmount) * icpPrice; + $: currentPrice = collateralType === CollateralType.ICP ? icpPrice : ckbtcPrice; + $: potentialUsdValue = Number(collateralAmount) * currentPrice; $: isConnected = $walletStore.isConnected; $: isDeveloper = $developerAccess; + $: collateralSymbol = collateralType === CollateralType.ICP ? 'ICP' : 'ckBTC'; + $: minAmount = collateralType === CollateralType.ICP ? 0.001 : 0.00001; + onMount(async () => { + // Fetch current ckBTC price + try { + ckbtcPrice = await priceService.getCurrentCkbtcPrice(); + } catch (err) { + console.warn('Failed to fetch ckBTC price:', err); + // Keep default price + } + }); + onDestroy(() => { if (processingTimeout) { clearTimeout(processingTimeout); @@ -146,14 +164,22 @@ return false; } - // Check wallet balance - updateStatus('Checking balance...'); - const hasBalance = await protocolService.checkIcpAllowance(amount.toString()); - if (!hasBalance) { - error = `Insufficient ICP balance. Required: ${amount} ICP`; + if (amount < minAmount) { + error = `Amount too low. Minimum: ${minAmount} ${collateralSymbol}`; return false; } + // Check wallet balance (only for ICP for now) + updateStatus('Checking balance...'); + if (collateralType === CollateralType.ICP) { + const hasBalance = await protocolService.checkIcpAllowance(amount.toString()); + if (!hasBalance) { + error = `Insufficient ICP balance. Required: ${amount} ICP`; + return false; + } + } + // TODO: Add ckBTC balance checking when wallet integration is complete + return true; } catch (err) { console.error('Wallet prerequisite check failed:', err); @@ -256,14 +282,17 @@ startProcessingTimer(); // Broken down steps for better troubleshooting - updateStatus('Requesting ICP approval...'); + updateStatus(`Requesting ${collateralSymbol} approval...`); // Convert amount const amount = Number(collateralAmount); - console.log('Creating vault with collateral:', amount); + console.log(`Creating vault with ${amount} ${collateralSymbol} collateral`); - // Create vault with longer timeouts - const result = await protocolService.openVault(amount); + // Create vault with collateral type + const result = await protocolService.openVault({ + collateralAmount: amount, + collateralType: collateralType + }); // Success handling console.log('Vault creation succeeded:', result); @@ -283,7 +312,7 @@ // Better error categorization if (err instanceof Error) { if (err.message.includes('approval timeout')) { - error = "ICP approval timed out. Please try again."; + error = `${collateralSymbol} approval timed out. Please try again.`; } else if (err.message.includes('busy') || err.message.includes('AlreadyProcessing')) { error = "System is busy processing another request. Please wait and try again."; handleRetryLogic(err); @@ -506,23 +535,53 @@

Create New Vault

+ +
+ +
+ + +
+
+ +
{#if collateralAmount}

- ≈ ${potentialUsdValue.toFixed(2)} USD + ≈ ${potentialUsdValue.toFixed(2)} USD at ${currentPrice.toFixed(collateralType === CollateralType.ICP ? 2 : 0)} USD/{collateralSymbol}

{/if}
diff --git a/src/vault_frontend/src/lib/components/vault/VaultCard.svelte b/src/vault_frontend/src/lib/components/vault/VaultCard.svelte index 629c69e..6fdcc34 100644 --- a/src/vault_frontend/src/lib/components/vault/VaultCard.svelte +++ b/src/vault_frontend/src/lib/components/vault/VaultCard.svelte @@ -3,6 +3,7 @@ import { onMount } from "svelte"; import { developerAccess } from '../../stores/developer'; import type { Vault } from '../../services/types'; + import { CollateralType } from '../../services/types'; import { protocolService } from '../../services/protocol'; import { createEventDispatcher } from 'svelte'; @@ -10,6 +11,7 @@ // Proper typing for the vault prop export let vault: Vault; export let icpPrice: number = 0; + export let ckbtcPrice: number = 94500; // Default fallback export let showActions: boolean = true; const dispatch = createEventDispatcher<{ @@ -17,8 +19,15 @@ manage: { vaultId: number }; }>(); + // Determine collateral type and related calculations + $: collateralType = vault.collateralType || CollateralType.ICP; + $: isIcp = collateralType === CollateralType.ICP; + $: collateralAmount = isIcp ? vault.icpMargin : vault.ckbtcMargin; + $: collateralPrice = isIcp ? icpPrice : ckbtcPrice; + $: collateralSymbol = isIcp ? 'ICP' : 'ckBTC'; + // Calculate display values with proper reactivity - $: collateralValueUsd = vault.icpMargin * icpPrice; + $: collateralValueUsd = collateralAmount * collateralPrice; $: collateralRatio = vault.borrowedIcusd > 0 ? collateralValueUsd / vault.borrowedIcusd : Infinity; @@ -30,7 +39,7 @@ // Format display values $: formattedCollateralValue = formatNumber(collateralValueUsd, 2); - $: formattedMargin = formatNumber(vault.icpMargin); + $: formattedMargin = formatNumber(collateralAmount, isIcp ? 4 : 8); // Different precision for ckBTC $: formattedBorrowedAmount = formatNumber(vault.borrowedIcusd); $: formattedMaxBorrowable = formatNumber(Math.max(0, maxBorrowable), 2); $: formattedCollateralRatio = collateralRatio === Infinity @@ -77,7 +86,7 @@ $: isDeveloper = $developerAccess; // Check if collateral can be withdrawn (has collateral, no matter the debt) - $: hasCollateral = Number(vault.icpMargin) > 0; + $: hasCollateral = collateralAmount > 0; // Helper methods for handling vault interactions async function handleMint() { @@ -180,9 +189,12 @@ // Add debugging to check received data console.log(`VaultCard mounted for vault #${vault.vaultId}:`, { vaultId: vault.vaultId, + collateralType: vault.collateralType, icpMargin: vault.icpMargin, + ckbtcMargin: vault.ckbtcMargin, borrowedIcusd: vault.borrowedIcusd, typeof_icpMargin: typeof vault.icpMargin, + typeof_ckbtcMargin: typeof vault.ckbtcMargin, typeof_borrowedIcusd: typeof vault.borrowedIcusd }); }); @@ -218,8 +230,8 @@
-
Collateral
-
{formattedMargin} ICP
+
Collateral ({collateralSymbol})
+
{formattedMargin} {collateralSymbol}
${formattedCollateralValue}
diff --git a/src/vault_frontend/src/lib/config.ts b/src/vault_frontend/src/lib/config.ts index c464840..8d65db5 100644 --- a/src/vault_frontend/src/lib/config.ts +++ b/src/vault_frontend/src/lib/config.ts @@ -1,12 +1,14 @@ import { idlFactory as rumi_backendIDL } from '../../../declarations/test_rumi_protocol_backend/test_rumi_protocol_backend.did.js'; import { idlFactory as icp_ledgerIDL } from '../../../declarations/icp_ledger/icp_ledger.did.js'; import { idlFactory as icusd_ledgerIDL } from '../../../declarations/icusd_ledger/icusd_ledger.did.js'; +import { idlFactory as ckbtc_ledgerIDL } from '../idls/ledger.idl.js'; // Canister IDs for production export const CANISTER_IDS = { PROTOCOL: "aakb7-rqaaa-aaaai-q3oua-cai", ICP_LEDGER: "ryjl3-tyaaa-aaaaa-aaaba-cai", ICUSD_LEDGER: "4kejc-maaaa-aaaai-q3tqq-cai", + CKBTC_LEDGER: "mxzaz-hqaaa-aaaar-qaada-cai", // ckBTC mainnet ledger ID } as const; // Canister IDs for local development @@ -14,6 +16,7 @@ export const LOCAL_CANISTER_IDS = { PROTOCOL: "aakb7-rqaaa-aaaai-q3oua-cai", ICP_LEDGER: "ryjl3-tyaaa-aaaaa-aaaba-cai", ICUSD_LEDGER: "4kejc-maaaa-aaaai-q3tqq-cai", + CKBTC_LEDGER: "mxzaz-hqaaa-aaaar-qaada-cai", // Use same for local development } as const; // Frontend canister ID @@ -38,6 +41,11 @@ export const CONFIG = { ? LOCAL_CANISTER_IDS.ICUSD_LEDGER : CANISTER_IDS.ICUSD_LEDGER, + // ckBTC ledger ID configuration + currentCkbtcLedgerId: process.env.DFX_NETWORK !== 'ic' + ? LOCAL_CANISTER_IDS.CKBTC_LEDGER + : CANISTER_IDS.CKBTC_LEDGER, + // Configure the host based on environment host: process.env.DFX_NETWORK === 'ic' ? 'https://icp0.io' @@ -69,5 +77,6 @@ export const CONFIG = { // Export IDLs through config for convenience rumi_backendIDL, icp_ledgerIDL, - icusd_ledgerIDL + icusd_ledgerIDL, + ckbtc_ledgerIDL }; diff --git a/src/vault_frontend/src/lib/services/priceService.ts b/src/vault_frontend/src/lib/services/priceService.ts index a1c9ea6..eebc10f 100644 --- a/src/vault_frontend/src/lib/services/priceService.ts +++ b/src/vault_frontend/src/lib/services/priceService.ts @@ -18,19 +18,36 @@ export const currentIcpPriceStore = writable<{ error: null }); +/** + * Store to track the latest ckBTC price + */ +export const currentCkbtcPriceStore = writable<{ + price: number | null; + source: 'logs' | 'metrics' | 'protocol' | 'cached' | null; + timestamp: number; + error: string | null; +}>({ + price: null, + source: null, + timestamp: 0, + error: null +}); + /** * Service for fetching the current ICP price from different sources */ export class PriceServiceClass { // Cache price values for a short period - private cachedPrice: number | null = null; - private lastPriceUpdate: number = 0; + private cachedIcpPrice: number | null = null; + private cachedCkbtcPrice: number | null = null; + private lastIcpPriceUpdate: number = 0; + private lastCkbtcPriceUpdate: number = 0; private readonly CACHE_DURATION = 15000; // 15 seconds /** * Get the current ICP price from logs */ - async fetchPriceFromLogs(): Promise { + async fetchIcpPriceFromLogs(): Promise { try { console.log("Fetching ICP price from logs..."); @@ -65,10 +82,47 @@ export class PriceServiceClass { } } + /** + * Get the current ckBTC price from logs + */ + async fetchCkbtcPriceFromLogs(): Promise { + try { + console.log("Fetching ckBTC price from logs..."); + + const response = await fetch( + `${CONFIG.host}/api/${CONFIG.currentCanisterId}/logs?priority=TraceXrc` + ); + + if (!response.ok) { + throw new Error(`HTTP error: ${response.status}`); + } + + const text = await response.text(); + const matches = text.matchAll(/\[FetchPrice\] fetched new ckBTC rate: ([0-9.]+)/g); + let latestPrice = null; + + for (const match of Array.from(matches)) { + if (match && match[1]) { + latestPrice = parseFloat(match[1]); + } + } + + if (latestPrice !== null && latestPrice > 0) { + console.log('✓ Found ckBTC price in logs:', latestPrice); + return latestPrice; + } + + return null; + } catch (err) { + console.error('Error fetching ckBTC price from logs:', err); + return null; + } + } + /** * Get the current ICP price from metrics */ - async fetchPriceFromMetrics(): Promise { + async fetchIcpPriceFromMetrics(): Promise { try { console.log("Fetching ICP price from metrics..."); @@ -98,6 +152,39 @@ export class PriceServiceClass { return null; } } + + /** + * Get the current ckBTC price from metrics + */ + async fetchCkbtcPriceFromMetrics(): Promise { + try { + console.log("Fetching ckBTC price from metrics..."); + + const response = await fetch( + `${CONFIG.host}/api/${CONFIG.currentCanisterId}/metrics` + ); + + if (!response.ok) { + throw new Error(`HTTP error: ${response.status}`); + } + + const text = await response.text(); + const match = text.match(/rumi_ckbtc_rate\s+([0-9.]+)/); + + if (match && match[1]) { + const price = parseFloat(match[1]); + if (price > 0) { + console.log('✓ Found ckBTC price in metrics:', price); + return price; + } + } + + return null; + } catch (err) { + console.error('Error fetching ckBTC price from metrics:', err); + return null; + } + } /** * Get the current ICP price using the best available source @@ -106,42 +193,42 @@ export class PriceServiceClass { const now = Date.now(); // Return cached price if it's recent - if (this.cachedPrice !== null && now - this.lastPriceUpdate < this.CACHE_DURATION) { - return this.cachedPrice; + if (this.cachedIcpPrice !== null && now - this.lastIcpPriceUpdate < this.CACHE_DURATION) { + return this.cachedIcpPrice; } try { // Try logs first (most recent data) const logsPrice = await promiseWithTimeout( - this.fetchPriceFromLogs(), + this.fetchIcpPriceFromLogs(), 5000, 'Logs price fetch timed out' ); if (logsPrice !== null) { - this.cachedPrice = logsPrice; - this.lastPriceUpdate = now; + this.cachedIcpPrice = logsPrice; + this.lastIcpPriceUpdate = now; return logsPrice; } // Try metrics next const metricsPrice = await promiseWithTimeout( - this.fetchPriceFromMetrics(), + this.fetchIcpPriceFromMetrics(), 5000, 'Metrics price fetch timed out' ); if (metricsPrice !== null) { - this.cachedPrice = metricsPrice; - this.lastPriceUpdate = now; + this.cachedIcpPrice = metricsPrice; + this.lastIcpPriceUpdate = now; return metricsPrice; } // If we reach here, we couldn't get a price from logs or metrics - if (this.cachedPrice !== null) { + if (this.cachedIcpPrice !== null) { // Return cached price even if old - console.log('Using stale cached price:', this.cachedPrice); - return this.cachedPrice; + console.log('Using stale cached ICP price:', this.cachedIcpPrice); + return this.cachedIcpPrice; } // Default fallback price @@ -150,16 +237,73 @@ export class PriceServiceClass { console.error('Error getting current ICP price:', err); // Return cached price if available, otherwise fallback - return this.cachedPrice !== null ? this.cachedPrice : 6.41; + return this.cachedIcpPrice !== null ? this.cachedIcpPrice : 6.41; + } + } + + /** + * Get the current ckBTC price using the best available source + */ + async getCurrentCkbtcPrice(): Promise { + const now = Date.now(); + + // Return cached price if it's recent + if (this.cachedCkbtcPrice !== null && now - this.lastCkbtcPriceUpdate < this.CACHE_DURATION) { + return this.cachedCkbtcPrice; + } + + try { + // Try logs first (most recent data) + const logsPrice = await promiseWithTimeout( + this.fetchCkbtcPriceFromLogs(), + 5000, + 'Logs price fetch timed out' + ); + + if (logsPrice !== null) { + this.cachedCkbtcPrice = logsPrice; + this.lastCkbtcPriceUpdate = now; + return logsPrice; + } + + // Try metrics next + const metricsPrice = await promiseWithTimeout( + this.fetchCkbtcPriceFromMetrics(), + 5000, + 'Metrics price fetch timed out' + ); + + if (metricsPrice !== null) { + this.cachedCkbtcPrice = metricsPrice; + this.lastCkbtcPriceUpdate = now; + return metricsPrice; + } + + // If we reach here, we couldn't get a price from logs or metrics + if (this.cachedCkbtcPrice !== null) { + // Return cached price even if old + console.log('Using stale cached ckBTC price:', this.cachedCkbtcPrice); + return this.cachedCkbtcPrice; + } + + // Default fallback price - current BTC price + return 94500; // Current BTC price as a fallback + } catch (err) { + console.error('Error getting current ckBTC price:', err); + + // Return cached price if available, otherwise fallback + return this.cachedCkbtcPrice !== null ? this.cachedCkbtcPrice : 94500; } } - // Start auto-refresh of price + // Start auto-refresh of prices startPriceRefresh(intervalMs: number = 30000) { this.getCurrentIcpPrice().catch(console.error); + this.getCurrentCkbtcPrice().catch(console.error); setInterval(() => { this.getCurrentIcpPrice().catch(console.error); + this.getCurrentCkbtcPrice().catch(console.error); }, intervalMs); } } diff --git a/src/vault_frontend/src/lib/services/protocol/apiClient.ts b/src/vault_frontend/src/lib/services/protocol/apiClient.ts index 75c12c0..d1386cf 100644 --- a/src/vault_frontend/src/lib/services/protocol/apiClient.ts +++ b/src/vault_frontend/src/lib/services/protocol/apiClient.ts @@ -25,8 +25,10 @@ import type { VaultOperationResult, FeesDTO, LiquidityStatusDTO, - CandidVault + CandidVault, + CreateVaultParams } from '../types'; +import { CollateralType } from '../types'; import { protocolService } from '../protocol'; @@ -34,6 +36,7 @@ import { protocolService } from '../protocol'; // Constants from backend export const E8S = 100_000_000; export const MIN_ICP_AMOUNT = 100_000; // 0.001 ICP +export const MIN_CKBTC_AMOUNT = 1000; // 0.00001 ckBTC (1000 satoshi) export const MIN_ICUSD_AMOUNT = 500_000_000; // 5 icUSD (changed from 10) export const MINIMUM_COLLATERAL_RATIO = 1.33; // 133% export const RECOVERY_COLLATERAL_RATIO = 1.5; // 150% @@ -374,19 +377,25 @@ private static async refreshVaultData(): Promise { } /** - * Open a new vault with ICP collateral + * Open a new vault with specified collateral type and amount */ - static async openVault(icpAmount: number): Promise { + static async openVault(params: CreateVaultParams): Promise { // Keep track of ongoing request let abortController: AbortController | null = null; try { - console.log(`Creating vault with ${icpAmount} ICP`); + const { collateralAmount, collateralType } = params; + console.log(`Creating vault with ${collateralAmount} ${collateralType}`); - if (icpAmount * E8S < MIN_ICP_AMOUNT) { + // Validate minimum amounts based on collateral type + const minAmount = collateralType === CollateralType.ICP ? MIN_ICP_AMOUNT : MIN_CKBTC_AMOUNT; + const amountE8s = BigInt(Math.floor(collateralAmount * E8S)); + + if (Number(amountE8s) < minAmount) { + const minDisplay = minAmount / E8S; return { success: false, - error: `Amount too low. Minimum required: ${MIN_ICP_AMOUNT / E8S} ICP` + error: `Amount too low. Minimum required: ${minDisplay} ${collateralType}` }; } @@ -406,21 +415,22 @@ private static async refreshVaultData(): Promise { // Enhanced error handling for wallet signer issues try { const actor = await ApiClient.getAuthenticatedActor(); - const amountE8s = BigInt(Math.floor(icpAmount * E8S)); // CRITICAL: Check and increase allowance before proceeding const spenderCanisterId = CONFIG.currentCanisterId; - // First check current allowance - const currentAllowance = await walletOperations.checkIcpAllowance(spenderCanisterId); - console.log(`Current allowance for protocol canister: ${Number(currentAllowance) / E8S} ICP`); - - // If allowance is insufficient, request approval - if (currentAllowance < amountE8s) { - console.log(`Requesting approval for ${icpAmount} ICP`); + // Handle allowance checking based on collateral type + if (collateralType === CollateralType.ICP) { + // First check current ICP allowance + const currentAllowance = await walletOperations.checkIcpAllowance(spenderCanisterId); + console.log(`Current ICP allowance for protocol canister: ${Number(currentAllowance) / E8S} ICP`); - // Use a higher allowance (5% more than needed) to avoid small rounding issues - const requestedAllowance = amountE8s * 105n / 100n; + // If allowance is insufficient, request approval + if (currentAllowance < amountE8s) { + console.log(`Requesting ICP approval for ${collateralAmount} ICP`); + + // Use a higher allowance (5% more than needed) to avoid small rounding issues + const requestedAllowance = amountE8s * 105n / 100n; const approvalResult = await walletOperations.approveIcpTransfer(requestedAllowance, spenderCanisterId); @@ -431,7 +441,31 @@ private static async refreshVaultData(): Promise { }; } - console.log(`Successfully set allowance to ${Number(requestedAllowance) / E8S} ICP`); + console.log(`Successfully set ICP allowance to ${Number(requestedAllowance) / E8S} ICP`); + } + } else if (collateralType === CollateralType.CkBTC) { + // First check current ckBTC allowance + const currentAllowance = await walletOperations.checkCkbtcAllowance(spenderCanisterId); + console.log(`Current ckBTC allowance for protocol canister: ${Number(currentAllowance) / E8S} ckBTC`); + + // If allowance is insufficient, request approval + if (currentAllowance < amountE8s) { + console.log(`Requesting ckBTC approval for ${collateralAmount} ckBTC`); + + // Use a higher allowance (5% more than needed) to avoid small rounding issues + const requestedAllowance = amountE8s * 105n / 100n; + + const approvalResult = await walletOperations.approveCkbtcTransfer(requestedAllowance, spenderCanisterId); + + if (!approvalResult.success) { + return { + success: false, + error: approvalResult.error || "Failed to approve ckBTC transfer" + }; + } + + console.log(`Successfully set ckBTC allowance to ${Number(requestedAllowance) / E8S} ckBTC`); + } } // Add a timeout to catch hanging signatures @@ -441,7 +475,7 @@ private static async refreshVaultData(): Promise { // Race between the actual operation and the timeout const result = await Promise.race([ - actor.open_vault(amountE8s), + actor.open_vault(amountE8s, { [collateralType]: null }), timeoutPromise ]); @@ -474,7 +508,7 @@ private static async refreshVaultData(): Promise { errMsg.includes('insufficient allowance')) { return { success: false, - error: "Insufficient ICP allowance. Please try again to approve the required amount." + error: `Insufficient ${collateralType} allowance. Please try again to approve the required amount.` }; } @@ -964,16 +998,20 @@ static async repayToVault(vaultId: number, icusdAmount: number): Promise { + try { + const walletState = get(walletStore); + if (!walletState.principal) { + throw new Error('Wallet not connected'); + } + + // Get ckBTC ledger actor + const ckbtcActor = await walletStore.getActor(CONFIG.currentCkbtcLedgerId, CONFIG.ckbtc_ledgerIDL); + + // Make allowance query + const result = await ckbtcActor.icrc2_allowance({ + account: { + owner: walletState.principal, + subaccount: [] + }, + spender: { + owner: Principal.fromText(spenderCanisterId), + subaccount: [] + } + }); + + return result.allowance; + } catch (err) { + console.error('Failed to check ckBTC allowance:', err); + return BigInt(0); + } + } + + /** + * Approve ckBTC transfer to a specified canister + */ + static async approveCkbtcTransfer(amount: bigint, spenderCanisterId: string): Promise<{success: boolean, error?: string}> { + try { + const walletState = get(walletStore); + if (!walletState.principal) { + throw new Error('Wallet not connected'); + } + + // Get ckBTC ledger actor + const ckbtcActor = await walletStore.getActor(CONFIG.currentCkbtcLedgerId, CONFIG.ckbtc_ledgerIDL); + + // Prepare approval args + const approvalArgs = { + spender: { + owner: Principal.fromText(spenderCanisterId), + subaccount: [] + }, + amount: amount, + expires_at: [], + fee: [], + memo: [], + from_subaccount: [], + created_at_time: [] + }; + + console.log(`Approving ${Number(amount) / E8S} ckBTC for spender: ${spenderCanisterId}`); + + const result = await ckbtcActor.icrc2_approve(approvalArgs); + + if ('Ok' in result) { + console.log(`✓ ckBTC approval successful. Block index: ${result.Ok}`); + return { success: true }; + } else { + console.error('ckBTC approval failed:', result.Err); + return { + success: false, + error: `ckBTC approval failed: ${JSON.stringify(result.Err)}` + }; + } + } catch (err) { + console.error('Error approving ckBTC transfer:', err); + return { + success: false, + error: err instanceof Error ? err.message : 'Unknown error during ckBTC approval' + }; + } + } + /** * Check current ICP allowance for the protocol canister */ @@ -278,13 +360,17 @@ export class walletOperations { const walletState = get(walletStore); if (!walletState.isConnected || !walletState.principal) { - return { icp: 0, icusd: 0 }; + return { icp: 0, ckbtc: 0, icusd: 0 }; } // Start with values from tokenBalances if available let icpBalance = walletState.tokenBalances?.ICP?.raw ? Number(walletState.tokenBalances.ICP.raw) / E8S : 0; + + let ckbtcBalance = walletState.tokenBalances?.CKBTC?.raw + ? Number(walletState.tokenBalances.CKBTC.raw) / E8S + : 0; let icusdBalance = walletState.tokenBalances?.ICUSD?.raw ? Number(walletState.tokenBalances.ICUSD.raw) / E8S @@ -304,6 +390,19 @@ export class walletOperations { } } + if (ckbtcBalance === 0) { + try { + const ckbtcActor = await walletStore.getActor(CONFIG.currentCkbtcLedgerId, CONFIG.ckbtc_ledgerIDL); + const balance = await ckbtcActor.icrc1_balance_of({ + owner: walletState.principal, + subaccount: [] + }); + ckbtcBalance = Number(balance) / E8S; + } catch (err) { + console.warn('Failed to fetch ckBTC balance:', err); + } + } + if (icusdBalance === 0) { try { const icusdActor = await walletStore.getActor(CONFIG.currentIcusdLedgerId, CONFIG.icusd_ledgerIDL); @@ -319,11 +418,12 @@ export class walletOperations { return { icp: icpBalance, + ckbtc: ckbtcBalance, icusd: icusdBalance }; } catch (err) { console.error('Error getting user balances:', err); - return { icp: 0, icusd: 0 }; + return { icp: 0, ckbtc: 0, icusd: 0 }; } } } \ No newline at end of file diff --git a/src/vault_frontend/src/lib/services/types.ts b/src/vault_frontend/src/lib/services/types.ts index 0a5e49a..0512cad 100644 --- a/src/vault_frontend/src/lib/services/types.ts +++ b/src/vault_frontend/src/lib/services/types.ts @@ -2,6 +2,12 @@ import type { Principal } from '@dfinity/principal'; const E8S = 100_000_000; +// Collateral type enum to match backend +export enum CollateralType { + ICP = 'ICP', + CkBTC = 'CkBTC' +} + // Liquidity provider status export interface LiquidityStatus { liquidityProvided: number; @@ -35,9 +41,16 @@ export interface VaultHistoryEvent { export interface UserBalances { icp: number; + ckbtc: number; icusd: number; } +// Interface for creating vaults with collateral type +export interface CreateVaultParams { + collateralAmount: number; + collateralType: CollateralType; +} + // Fees returned to the frontend export interface FeesDTO { borrowingFee: number; @@ -49,7 +62,9 @@ export interface VaultDTO { vaultId: number; owner: string; icpMargin: number; + ckbtcMargin: number; borrowedIcusd: number; + collateralType: CollateralType; timestamp?: number; } @@ -61,6 +76,8 @@ export interface CandidVault { owner: string; borrowed_icusd_amount: number; icp_margin_amount: number; + ckbtc_margin_amount: number; + collateral_type: CollateralType; } // Liquidity status as returned to the frontend @@ -78,9 +95,12 @@ export type Vault = VaultDTO; export interface ProtocolStatusDTO { mode: any; totalIcpMargin: number; + totalCkbtcMargin: number; totalIcusdBorrowed: number; lastIcpRate: number; lastIcpTimestamp: number; + lastCkbtcRate: number; + lastCkbtcTimestamp: number; totalCollateralRatio: number; } @@ -90,7 +110,9 @@ export interface EnhancedVault { vaultId: number; owner: string; icpMargin: number; + ckbtcMargin: number; borrowedIcusd: number; + collateralType: CollateralType; timestamp: number; lastUpdated: number; collateralRatio?: number;