diff --git a/apps/web-app/src/app/(app)/dashboard/backstop/page.tsx b/apps/web-app/src/app/(app)/dashboard/backstop/page.tsx
new file mode 100644
index 0000000..00f0b1c
--- /dev/null
+++ b/apps/web-app/src/app/(app)/dashboard/backstop/page.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from "next";
+import Backstop from "@/features/lending/components/pages/Backstop";
+
+export const metadata: Metadata = {
+ title: "Backstop | Neko Protocol",
+ description:
+ "Create and participate in interest auctions. Accumulated protocol interest is distributed to backstop depositors.",
+};
+
+export default function BackstopPage() {
+ return ;
+}
diff --git a/apps/web-app/src/features/dashboard/components/ui/QuickActions.tsx b/apps/web-app/src/features/dashboard/components/ui/QuickActions.tsx
index 7060b91..87586c0 100644
--- a/apps/web-app/src/features/dashboard/components/ui/QuickActions.tsx
+++ b/apps/web-app/src/features/dashboard/components/ui/QuickActions.tsx
@@ -6,8 +6,8 @@ import {
ArrowLeftRight,
Droplets,
Compass,
- ArrowRight,
Gavel,
+ ArrowRight,
} from "lucide-react";
import { useWallet } from "@/hooks/useWallet";
import { ConnectWalletModal } from "@/features/wallet/components/ConnectWalletModal";
@@ -32,6 +32,10 @@ const actions = [
icon: Compass,
},
{
+ label: "Backstop",
+ description: "Interest auctions",
+ href: "/dashboard/backstop",
+ },
label: "Liquidations",
description: "Bad debt auctions",
href: "/borrowing?tab=liquidations",
diff --git a/apps/web-app/src/features/lending/components/InterestAuctionSection.tsx b/apps/web-app/src/features/lending/components/InterestAuctionSection.tsx
new file mode 100644
index 0000000..06ed97e
--- /dev/null
+++ b/apps/web-app/src/features/lending/components/InterestAuctionSection.tsx
@@ -0,0 +1,409 @@
+"use client";
+
+import React, { useState } from "react";
+import Link from "next/link";
+import {
+ Gavel,
+ HandCoins,
+ Loader2,
+ Info,
+ ChevronDown,
+ ChevronRight,
+ ArrowRight,
+} from "lucide-react";
+import { useInterestAuction } from "../hooks/useInterestAuction";
+import { ConnectWalletModal } from "@/features/wallet/components/ConnectWalletModal";
+
+const MIN_AUCTION_AMOUNT = 100;
+
+function HowItWorks() {
+ const [expanded, setExpanded] = useState(false);
+ return (
+
+
+ {expanded && (
+
+
+ -
+
+ 1
+
+
+ Interest accumulates{" "}
+ when users borrow from lending pools. A portion goes to the
+ backstop.
+
+
+ -
+
+ 2
+
+
+ Create an auction when
+ there is at least {MIN_AUCTION_AMOUNT} units accumulated. Anyone
+ can trigger it.
+
+
+ -
+
+ 3
+
+
+ Participate by paying
+ backstop tokens to receive the interest. You need backstop
+ tokens (from depositing in the backstop).
+
+
+
+
+ )}
+
+ );
+}
+
+export function InterestAuctionSection() {
+ const [showWalletModal, setShowWalletModal] = useState(false);
+ const [participateAuctionId, setParticipateAuctionId] = useState("");
+ const [participateAsset, setParticipateAsset] = useState("");
+ const [participateContractId, setParticipateContractId] = useState("");
+ const [participateFillPercent, setParticipateFillPercent] = useState("100");
+ const [isCreating, setIsCreating] = useState(null);
+ const [isFilling, setIsFilling] = useState(false);
+
+ const {
+ assets,
+ isLoadingAssets,
+ hasWallet,
+ createAuction,
+ fillAuction,
+ } = useInterestAuction();
+
+ const handleCreateAuction = async (assetCode: string, contractId: string) => {
+ if (!hasWallet) {
+ setShowWalletModal(true);
+ return;
+ }
+ setIsCreating(assetCode);
+ try {
+ await createAuction(assetCode, contractId);
+ } finally {
+ setIsCreating(null);
+ }
+ };
+
+ const handleFillAuction = async () => {
+ if (!hasWallet) {
+ setShowWalletModal(true);
+ return;
+ }
+ const auctionId = parseInt(participateAuctionId, 10);
+ const fillPercent = parseFloat(participateFillPercent);
+ if (
+ !Number.isFinite(auctionId) ||
+ auctionId < 0 ||
+ !participateAsset ||
+ !participateContractId
+ ) {
+ return;
+ }
+ setIsFilling(true);
+ try {
+ await fillAuction({
+ auctionId,
+ assetCode: participateAsset,
+ contractId: participateContractId,
+ fillPercent,
+ });
+ setParticipateAuctionId("");
+ setParticipateAsset("");
+ setParticipateContractId("");
+ setParticipateFillPercent("100");
+ } finally {
+ setIsFilling(false);
+ }
+ };
+
+ const canFill =
+ hasWallet &&
+ participateAuctionId.trim() !== "" &&
+ participateAsset !== "" &&
+ participateContractId !== "" &&
+ participateFillPercent !== "" &&
+ parseFloat(participateFillPercent) >= 1 &&
+ parseFloat(participateFillPercent) <= 100 &&
+ Number.isFinite(parseInt(participateAuctionId, 10));
+
+ const hasAnyReady = assets.some((a) => a.canCreate);
+
+ return (
+
+ {/* How it works */}
+
+
+ {/* Create Auction */}
+
+
+
+
+
+
+
+ Create Interest Auction
+
+
+ Trigger an auction when enough interest has accumulated
+
+
+
+
+ {!hasWallet ? (
+
+
+ Connect your wallet to create or participate in auctions
+
+
+
+ ) : isLoadingAssets ? (
+
+
+ Loading assets...
+
+ ) : assets.length === 0 ? (
+
+ No lending pools available
+
+ ) : (
+ <>
+ {!hasAnyReady && hasWallet && assets.length > 0 && (
+
+
+
+
+
+ No auctions available yet
+
+
+ You need at least {MIN_AUCTION_AMOUNT} units of accumulated
+ interest per asset. Interest accrues when users borrow from
+ the pool.
+
+
+
+ Deposit to pool
+
+
+
·
+
+ Borrow (generates interest)
+
+
+
+
+
+
+ )}
+
+ {assets.map((asset) => (
+
+
+
+
+ {asset.assetCode}
+
+ {asset.canCreate ? (
+
+ Ready to create
+
+ ) : (
+
+ Need {MIN_AUCTION_AMOUNT}+ {asset.assetCode}
+
+ )}
+
+
+ Accumulated: {asset.accumulatedInterestFormatted}{" "}
+ {asset.assetCode}
+
+
+
+
+ ))}
+
+ >
+ )}
+
+
+ {/* Participate in Auction */}
+
+
+
+
+
+
+
+ Participate in Auction
+
+
+ Pay backstop tokens to receive accumulated interest
+
+
+
+
+ {!hasWallet ? (
+
+
+ Connect your wallet to participate
+
+
+ ) : (
+
+
+ Enter the auction details below. You need backstop tokens to
+ participate.
+
+
+
+
setParticipateAuctionId(e.target.value)}
+ placeholder="e.g. 1"
+ className="w-full rounded-lg bg-[#222222] border border-white/10 px-3 py-2 text-white text-sm placeholder:text-white/30 focus:outline-none focus:border-[#229EDF]"
+ />
+
+ Shown when you create an auction, or from transaction events
+
+
+
+
+
+
+ The asset being auctioned (interest you will receive)
+
+
+
+
+
setParticipateFillPercent(e.target.value)}
+ className="w-full rounded-lg bg-[#222222] border border-white/10 px-3 py-2 text-white text-sm placeholder:text-white/30 focus:outline-none focus:border-[#229EDF]"
+ />
+
+ Percentage of the auction to fill (100 = full auction)
+
+
+ {!canFill && (
+
+ Fill in Auction ID and select an asset to enable participation
+
+ )}
+
+
+ )}
+
+
+
setShowWalletModal(false)}
+ />
+
+ );
+}
diff --git a/apps/web-app/src/features/lending/components/pages/Backstop.tsx b/apps/web-app/src/features/lending/components/pages/Backstop.tsx
new file mode 100644
index 0000000..2cf4768
--- /dev/null
+++ b/apps/web-app/src/features/lending/components/pages/Backstop.tsx
@@ -0,0 +1,25 @@
+"use client";
+
+import React from "react";
+import { BannerPage } from "@/components/ui/BannerPage";
+import { PageContainer } from "@/components/ui/PageContainer";
+import { InterestAuctionSection } from "../InterestAuctionSection";
+
+const Backstop: React.FC = () => {
+ return (
+
+ );
+};
+
+export default Backstop;
diff --git a/apps/web-app/src/features/lending/hooks/useInterestAuction.ts b/apps/web-app/src/features/lending/hooks/useInterestAuction.ts
new file mode 100644
index 0000000..3c5008d
--- /dev/null
+++ b/apps/web-app/src/features/lending/hooks/useInterestAuction.ts
@@ -0,0 +1,230 @@
+"use client";
+
+import { useCallback, useMemo } from "react";
+import { useQueries, useQueryClient } from "@tanstack/react-query";
+import { Networks } from "@stellar/stellar-sdk";
+import { useWallet } from "@/hooks/useWallet";
+import { useToast } from "@/hooks/useToast";
+import {
+ canCreateInterestAuction,
+ getAccumulatedInterest,
+ createInterestAuctionXdr,
+ fillInterestAuctionXdr,
+ type FillInterestAuctionParams,
+} from "@/lib/helpers/stellar/lending";
+import {
+ signAndSendTransaction,
+ type SignTransactionFn,
+} from "@/lib/helpers/stellar/transaction";
+import { rpcUrl } from "@/lib/constants/network";
+import { extractContractErrorOrNull } from "@/lib/helpers/stellar/contractErrors";
+import { TOAST_CONFIG } from "@/lib/constants/toast.config";
+import { fromSmallestUnit } from "@/lib/helpers/tokenUtils";
+import { useLendingPools } from "./useLendingPools";
+import { networks } from "@neko/lending";
+
+export interface InterestAuctionAsset {
+ assetCode: string;
+ contractId: string;
+ canCreate: boolean;
+ accumulatedInterest: bigint;
+ accumulatedInterestFormatted: string;
+ decimals: number;
+}
+
+export const INTEREST_AUCTION_QUERY_KEY = "interestAuction";
+
+export function useInterestAuction() {
+ const queryClient = useQueryClient();
+ const { addNotification } = useToast();
+ const { address, signTransaction, networkPassphrase } = useWallet();
+ const { data: lendingPools = [], refetch: refetchPools } = useLendingPools();
+
+ const showError = useCallback(
+ (msg: string) =>
+ addNotification("Something went wrong", "error", {
+ ...TOAST_CONFIG.defaultOpts,
+ description: msg,
+ }),
+ [addNotification]
+ );
+
+ const showSuccess = useCallback(
+ (msg: string) =>
+ addNotification("Success", "success", {
+ ...TOAST_CONFIG.defaultOpts,
+ description: msg,
+ }),
+ [addNotification]
+ );
+
+ const assetQueries = useQueries({
+ queries: lendingPools.map((pool) => ({
+ queryKey: [INTEREST_AUCTION_QUERY_KEY, pool.assetCode, pool.contractId],
+ queryFn: async (): Promise => {
+ const [canCreate, accumulatedInterest] = await Promise.all([
+ canCreateInterestAuction(pool.assetCode, pool.contractId),
+ getAccumulatedInterest(pool.assetCode, pool.contractId),
+ ]);
+
+ const decimals = 7;
+ const accumulatedInterestFormatted =
+ accumulatedInterest > 0n
+ ? fromSmallestUnit(accumulatedInterest.toString(), decimals)
+ : "0";
+
+ return {
+ assetCode: pool.assetCode,
+ contractId: pool.contractId,
+ canCreate,
+ accumulatedInterest,
+ accumulatedInterestFormatted,
+ decimals,
+ };
+ },
+ staleTime: 60_000,
+ enabled: !!pool.assetCode && !!pool.contractId,
+ })),
+ });
+
+ const assets: InterestAuctionAsset[] = useMemo(
+ () =>
+ assetQueries
+ .filter((q) => q.data)
+ .map((q) => q.data!) as InterestAuctionAsset[],
+ [assetQueries]
+ );
+
+ const isLoadingAssets = assetQueries.some((q) => q.isLoading);
+ const invalidateQueries = useCallback(() => {
+ queryClient.invalidateQueries({ queryKey: [INTEREST_AUCTION_QUERY_KEY] });
+ refetchPools();
+ }, [queryClient, refetchPools]);
+
+ const createAuction = useCallback(
+ async (assetCode: string, contractId: string) => {
+ if (!address) {
+ showError("Please connect your wallet first");
+ return { success: false as const, auctionId: undefined };
+ }
+
+ try {
+ const xdr = await createInterestAuctionXdr(
+ assetCode,
+ address,
+ contractId
+ );
+
+ await signAndSendTransaction(
+ xdr,
+ signTransaction as SignTransactionFn,
+ {
+ networkPassphrase: networkPassphrase || Networks.TESTNET,
+ rpcUrl,
+ address,
+ waitForPending: true,
+ }
+ );
+
+ showSuccess(`Interest auction created for ${assetCode}`);
+ invalidateQueries();
+ return { success: true as const, auctionId: undefined };
+ } catch (err) {
+ const friendlyError = extractContractErrorOrNull(err);
+ showError(
+ typeof friendlyError === "string"
+ ? friendlyError
+ : "Failed to create interest auction. Please try again."
+ );
+ return { success: false as const, auctionId: undefined };
+ }
+ },
+ [
+ address,
+ networkPassphrase,
+ signTransaction,
+ showError,
+ showSuccess,
+ invalidateQueries,
+ ]
+ );
+
+ const fillAuction = useCallback(
+ async (params: {
+ auctionId: number;
+ assetCode: string;
+ contractId: string;
+ fillPercent: number;
+ }) => {
+ if (!address) {
+ showError("Please connect your wallet first");
+ return { success: false as const };
+ }
+
+ if (
+ params.fillPercent <= 0 ||
+ params.fillPercent > 100 ||
+ !Number.isFinite(params.fillPercent)
+ ) {
+ showError("Fill percentage must be between 1 and 100");
+ return { success: false as const };
+ }
+
+ try {
+ const fillParams: FillInterestAuctionParams = {
+ auctionId: params.auctionId,
+ bidder: address,
+ asset: params.assetCode,
+ fillPercent: params.fillPercent,
+ };
+
+ const xdr = await fillInterestAuctionXdr(fillParams, params.contractId);
+
+ await signAndSendTransaction(
+ xdr,
+ signTransaction as SignTransactionFn,
+ {
+ networkPassphrase: networkPassphrase || Networks.TESTNET,
+ rpcUrl,
+ address,
+ waitForPending: true,
+ }
+ );
+
+ showSuccess(
+ `Successfully participated in auction #${params.auctionId} for ${params.assetCode}`
+ );
+ invalidateQueries();
+ return { success: true as const };
+ } catch (err) {
+ const friendlyError = extractContractErrorOrNull(err);
+ showError(
+ typeof friendlyError === "string"
+ ? friendlyError
+ : "Failed to participate in auction. Please try again."
+ );
+ return { success: false as const };
+ }
+ },
+ [
+ address,
+ networkPassphrase,
+ signTransaction,
+ showError,
+ showSuccess,
+ invalidateQueries,
+ ]
+ );
+
+ return {
+ assets,
+ isLoadingAssets,
+ hasWallet: !!address,
+ createAuction,
+ fillAuction,
+ poolContractIds: {
+ pool1: networks.testnet.pool1ContractId,
+ pool2: networks.testnet.pool2ContractId,
+ },
+ };
+}
diff --git a/apps/web-app/src/lib/constants/generated/contract-errors.ts b/apps/web-app/src/lib/constants/generated/contract-errors.ts
index de4d9a3..b7d573c 100644
--- a/apps/web-app/src/lib/constants/generated/contract-errors.ts
+++ b/apps/web-app/src/lib/constants/generated/contract-errors.ts
@@ -600,10 +600,7 @@ export function isValidErrorCode(code: number): code is ContractErrorCode {
* Errors grouped by contract name, then by error code.
* Use this for contract-specific lookups when multiple contracts share the same code.
*/
-export const CONTRACT_ERRORS_BY_CONTRACT: Record<
- string,
- Record
-> = (() => {
+export const CONTRACT_ERRORS_BY_CONTRACT: Record> = (() => {
const byContract: Record> = {};
for (const [code, info] of Object.entries(CONTRACT_ERRORS)) {
const n = Number(code);
diff --git a/apps/web-app/src/lib/helpers/stellar/lending.ts b/apps/web-app/src/lib/helpers/stellar/lending.ts
index 817e762..59c86b6 100644
--- a/apps/web-app/src/lib/helpers/stellar/lending.ts
+++ b/apps/web-app/src/lib/helpers/stellar/lending.ts
@@ -997,6 +997,11 @@ export const getBTokenBalance = async (
}
};
+// ========== Interest Auction Helpers ==========
+
+/** SCALAR_7 = 10^7 for 7-decimal percentage (100% = 100_000000) */
+const SCALAR_7 = 100_000_000n;
+
/**
* Check if a borrower has bad debt (debt > 0 and collateral = 0)
*/
@@ -1021,6 +1026,127 @@ export const hasBadDebt = async (
}
};
+/**
+ * Check if an interest auction can be created for an asset
+ */
+export const canCreateInterestAuction = async (
+ asset: string,
+ contractId: string = networks.testnet.contractId
+): Promise => {
+ try {
+ const client = new RwaLendingClient({
+ contractId,
+ rpcUrl: rpcUrl,
+ networkPassphrase: networkPassphrase,
+ ...(allowHttpForSoroban && { allowHttp: true }),
+ });
+
+ const tx = await client.can_create_interest_auction(
+ { asset },
+ { simulate: true }
+ );
+
+ return tx.result ?? false;
+ } catch (error) {
+ console.error("Error checking can_create_interest_auction:", error);
+ return false;
+ }
+};
+
+/**
+ * Get accumulated backstop_credit (interest) for an asset
+ */
+export const getAccumulatedInterest = async (
+ asset: string,
+ contractId: string = networks.testnet.contractId
+): Promise => {
+ try {
+ const client = new RwaLendingClient({
+ contractId,
+ rpcUrl: rpcUrl,
+ networkPassphrase: networkPassphrase,
+ ...(allowHttpForSoroban && { allowHttp: true }),
+ });
+
+ const tx = await client.get_accumulated_interest(
+ { asset },
+ { simulate: true }
+ );
+
+ const value = tx.result;
+ if (!value) return 0n;
+
+ return typeof value === "bigint" ? value : BigInt(String(value));
+ } catch (error) {
+ console.error("Error getting accumulated interest:", error);
+ return 0n;
+ }
+};
+
+/**
+ * Build create_interest_auction transaction XDR for signing
+ */
+export const createInterestAuctionXdr = async (
+ asset: string,
+ walletAddress: string,
+ contractId: string = networks.testnet.contractId
+): Promise => {
+ try {
+ const sorobanServer = new rpc.Server(rpcUrl, {
+ allowHttp: stellarNetwork === "LOCAL",
+ });
+ const horizonServer = new Horizon.Server(horizonUrl);
+ const lendingContract = new Contract(contractId);
+
+ const assetSymbol = xdr.ScVal.scvSymbol(asset);
+
+ const operation = lendingContract.call(
+ "create_interest_auction",
+ assetSymbol
+ );
+
+ const account = await horizonServer.loadAccount(walletAddress);
+
+ const transaction = new TransactionBuilder(account, {
+ fee: "100",
+ networkPassphrase: networkPassphrase,
+ })
+ .addOperation(operation)
+ .setTimeout(300)
+ .build();
+
+ try {
+ await sorobanServer.simulateTransaction(transaction);
+ } catch (simError) {
+ const errorMessage =
+ simError instanceof Error ? simError.message : String(simError);
+ if (
+ !errorMessage.includes("Auth") &&
+ !errorMessage.includes("require_auth") &&
+ !errorMessage.includes("InvalidAction")
+ ) {
+ const friendlyError = extractContractError(simError, "rwa-lending");
+ throw new Error(friendlyError);
+ }
+ }
+
+ const preparedTx = await sorobanServer.prepareTransaction(transaction);
+
+ return preparedTx.toXDR();
+ } catch (error) {
+ console.error("Error building create_interest_auction transaction:", error);
+ if (
+ error instanceof Error &&
+ error.message &&
+ !error.message.includes("Failed to build")
+ ) {
+ throw error;
+ }
+ const friendlyError = extractContractError(error, "rwa-lending");
+ throw new Error(friendlyError);
+ }
+};
+
/**
* Build create_bad_debt_auction transaction XDR
*/
@@ -1087,6 +1213,83 @@ export const createBadDebtAuction = async (
}
};
+export interface FillInterestAuctionParams {
+ auctionId: number;
+ bidder: string;
+ asset: string;
+ /** Human-readable percentage 1-100 (e.g. 50 = 50%) */
+ fillPercent: number;
+}
+
+/**
+ * Build fill_interest_auction transaction XDR for signing
+ * fillPercent is converted to 7 decimals (e.g. 50 -> 50_000000)
+ */
+export const fillInterestAuctionXdr = async (
+ params: FillInterestAuctionParams,
+ contractId: string = networks.testnet.contractId
+): Promise => {
+ try {
+ const sorobanServer = new rpc.Server(rpcUrl, {
+ allowHttp: stellarNetwork === "LOCAL",
+ });
+ const horizonServer = new Horizon.Server(horizonUrl);
+ const lendingContract = new Contract(contractId);
+
+ const fillPercentScaled = BigInt(
+ Math.floor((params.fillPercent * Number(SCALAR_7)) / 100)
+ );
+
+ const operation = lendingContract.call(
+ "fill_interest_auction",
+ nativeToScVal(params.auctionId, { type: "u32" }),
+ new Address(params.bidder).toScVal(),
+ xdr.ScVal.scvSymbol(params.asset),
+ nativeToScVal(fillPercentScaled, { type: "i128" })
+ );
+
+ const account = await horizonServer.loadAccount(params.bidder);
+
+ const transaction = new TransactionBuilder(account, {
+ fee: "100",
+ networkPassphrase: networkPassphrase,
+ })
+ .addOperation(operation)
+ .setTimeout(300)
+ .build();
+
+ try {
+ await sorobanServer.simulateTransaction(transaction);
+ } catch (simError) {
+ const errorMessage =
+ simError instanceof Error ? simError.message : String(simError);
+ if (
+ !errorMessage.includes("Auth") &&
+ !errorMessage.includes("require_auth") &&
+ !errorMessage.includes("InvalidAction")
+ ) {
+ const friendlyError = extractContractError(simError, "rwa-lending");
+ throw new Error(friendlyError);
+ }
+ }
+
+ const preparedTx = await sorobanServer.prepareTransaction(transaction);
+
+ return preparedTx.toXDR();
+ } catch (error) {
+ console.error("Error building fill_interest_auction transaction:", error);
+ if (
+ error instanceof Error &&
+ error.message &&
+ !error.message.includes("Failed to build")
+ ) {
+ throw error;
+ }
+ const friendlyError = extractContractError(error, "rwa-lending");
+ throw new Error(friendlyError);
+ }
+};
+
export type FillBadDebtAuctionResult = {
approveXdr: string;
fillXdr: string;
diff --git a/apps/web-app/src/lib/services/lending.service.ts b/apps/web-app/src/lib/services/lending.service.ts
index 840f953..68c2b2b 100644
--- a/apps/web-app/src/lib/services/lending.service.ts
+++ b/apps/web-app/src/lib/services/lending.service.ts
@@ -38,7 +38,13 @@ import {
depositToBackstop,
withdrawFromBackstop,
borrowFromPool as borrowFromPoolHelper,
+ removeCollateral,
hasBadDebt as hasBadDebtHelper,
+ canCreateInterestAuction as canCreateInterestAuctionHelper,
+ getAccumulatedInterest as getAccumulatedInterestHelper,
+ createInterestAuctionXdr as buildCreateInterestAuctionXdr,
+ fillInterestAuctionXdr as buildFillInterestAuctionXdr,
+ type FillInterestAuctionParams,
createBadDebtAuction as createBadDebtAuctionHelper,
buildFillBadDebtAuctionXdr,
} from "../helpers/stellar/lending";
@@ -875,6 +881,74 @@ export class LendingService {
}
}
+ /**
+ * Check if an interest auction can be created for an asset
+ */
+ async canCreateInterestAuction(
+ asset: string,
+ contractId?: string
+ ): Promise {
+ return canCreateInterestAuctionHelper(
+ asset,
+ contractId ?? this.lendingClient.options.contractId
+ );
+ }
+
+ /**
+ * Get accumulated backstop_credit (interest) for an asset
+ */
+ async getAccumulatedInterest(
+ asset: string,
+ contractId?: string
+ ): Promise {
+ return getAccumulatedInterestHelper(
+ asset,
+ contractId ?? this.lendingClient.options.contractId
+ );
+ }
+
+ /**
+ * Build create_interest_auction transaction XDR for signing
+ */
+ async createInterestAuctionXdr(
+ asset: string,
+ walletAddress: string,
+ contractId?: string
+ ): Promise {
+ try {
+ const xdr = await buildCreateInterestAuctionXdr(
+ asset,
+ walletAddress,
+ contractId ?? this.lendingClient.options.contractId
+ );
+ return { xdr };
+ } catch (error) {
+ console.error("Error building create_interest_auction:", error);
+ const friendlyError = extractContractError(error, "rwa-lending");
+ return { xdr: "", error: friendlyError };
+ }
+ }
+
+ /**
+ * Build fill_interest_auction transaction XDR for signing
+ */
+ async fillInterestAuctionXdr(
+ params: FillInterestAuctionParams,
+ contractId?: string
+ ): Promise {
+ try {
+ const xdr = await buildFillInterestAuctionXdr(
+ params,
+ contractId ?? this.lendingClient.options.contractId
+ );
+ return { xdr };
+ } catch (error) {
+ console.error("Error building fill_interest_auction:", error);
+ const friendlyError = extractContractError(error, "rwa-lending");
+ return { xdr: "", error: friendlyError };
+ }
+ }
+
/**
* Get collateral balance for a user and RWA token
*/
@@ -925,7 +999,7 @@ export class LendingService {
}
}
- // ========== Liquidations (feat/liquidatios-ui) ==========
+ // ========== Liquidations / Bad Debt ==========
/**
* Check if a borrower has bad debt (debt > 0 and collateral = 0)