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
88 changes: 87 additions & 1 deletion app/markets/quick-view/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ import { useQuery } from "@tanstack/react-query";
import { useMarket } from "@/lib/hooks/useMarkets";
import { getMarketByOnChainId } from "@/lib/api/markets";
import { ApiError } from "@/lib/api/client";
import { useBuy, useOnChainMarket, usePosition, useQuoteBuy, useUsdcBalance, useAccountReserve, usePrepareBuy, useMarketConfig, useLedgerNow, useEnableUsdc, useUsdcTrustline, useExactBuyQuote, usePositionState, useUsdcBalanceState } from "@/lib/hooks/useStellar";
import { useBuy, useOnChainMarket, usePosition, useQuoteBuy, useUsdcBalance, useAccountReserve, usePrepareBuy, useMarketConfig, useLedgerNow, useEnableUsdc, useUsdcTrustline, useExactBuyQuote, usePositionState, useUsdcBalanceState, useAccountAuth, usePrepareMultisig } from "@/lib/hooks/useStellar";
import { useWallet } from "@/lib/contexts/WalletContext";
import { explorerTx } from "@/lib/stellar/config";
import { formatUsdc, formatXlm, maxIn, minOut, tryParseUsdc } from "@/lib/stellar/units";
import { buyBreakdown } from "@/lib/stellar/buy-breakdown";
import { isMultisigAccount, MULTISIG_DEFAULT_SLIPPAGE_BPS, type MultisigSession } from "@/lib/stellar/multisig";
import { MultisigReview } from "@/components/multisig-review";

import type { TxPhase } from "@/lib/stellar/tx-lifecycle";
import { preflightBuy } from "@/lib/stellar/preflight";
Expand Down Expand Up @@ -114,6 +116,12 @@ function MarketQuickViewContent() {
const trustlineUnauthorized = trustline.data?.kind === "present" && !trustline.data.authorized;
const buy = useBuy();
const submitQueue = useSubmitQueueState(address);
const accountAuth = useAccountAuth(address);
const isMultisig = accountAuth.data ? isMultisigAccount(accountAuth.data) : false;
const [multisigMode, setMultisigMode] = useState(false);
const [multisigSession, setMultisigSession] = useState<MultisigSession | null>(null);
const [multisigStatus, setMultisigStatus] = useState<string | null>(null);
const prepareMultisig = usePrepareMultisig();
const [now, setNow] = useState<number | null>(null);

useEffect(() => {
Expand Down Expand Up @@ -255,6 +263,41 @@ function MarketQuickViewContent() {
}
};

const handlePrepareMultisig = async () => {
if (!isConnected) {
void openModal();
return;
}
if (!onChainMarketId) return;
if (isWrongNetwork) {
setMultisigStatus("Switch your wallet to Stellar Testnet to prepare this trade.");
return;
}
if (!spendRaw || spendRaw <= 0n) {
setMultisigStatus(buyMode === "usdc" ? "Enter an amount like 10 or 2.5." : "Enter a share amount greater than zero.");
return;
}
if (buyMode === "shares") {
setMultisigStatus("Multisig prepare currently supports USDC amounts. Switch to USDC and try again.");
return;
}
setMultisigStatus(null);
try {
const session = await prepareMultisig.mutateAsync({
kind: "buy",
marketId: onChainMarketId,
outcome: side,
collateralIn: spendRaw,
slippageBps: MULTISIG_DEFAULT_SLIPPAGE_BPS,
sourceAccount: address ?? undefined,
timeoutInSeconds: 3600,
});
setMultisigSession(session);
} catch (err) {
setMultisigStatus(err instanceof Error ? err.message : "Couldn't prepare the multisig transaction.");
}
};

if (isLoading) {
return (
<main className="relative min-h-[calc(100vh-64px)] overflow-hidden bg-surface flex items-center justify-center">
Expand Down Expand Up @@ -544,6 +587,42 @@ function MarketQuickViewContent() {
{!buy.isPending && buyStatus && (
<p className="text-xs text-on-surface-variant break-all" role="status">{buyStatus}</p>
)}

{/* Multisig path */}
{isConnected && (
<div className="rounded-lg border border-outline-variant bg-surface-container-low p-3 space-y-2">
<label className="flex items-center gap-2 text-xs">
<input
type="checkbox"
checked={multisigMode}
onChange={(e) => setMultisigMode(e.target.checked)}
/>
Use multisig (export XDR for co-signers)
</label>
{accountAuth.isLoading && <p className="text-[11px] text-on-surface-variant">Checking multisig thresholds…</p>}
{isMultisig && !multisigMode && (
<p className="text-[11px] text-on-surface-variant">
This account needs multiple signatures (medium threshold {accountAuth.data?.med}, {accountAuth.data?.signers.length} signers). For treasuries, use “Prepare for multisig”.
</p>
)}
{(multisigMode || isMultisig) && (
<div className="space-y-2">
<button
type="button"
onClick={handlePrepareMultisig}
disabled={prepareMultisig.isPending || !spendRaw || spendRaw <= 0n}
className="bg-surface-container px-3 py-2 rounded-lg text-xs font-bold border border-outline-variant disabled:opacity-50"
>
{prepareMultisig.isPending ? "Preparing…" : "Prepare for multisig"}
</button>
<p className="text-[11px] text-on-surface-variant">
Builds with a 1-hour window and 3% slippage. Quote is from the current ledger; the trade fails with SlippageExceeded (error 12) if the price moves further.
</p>
{multisigStatus && <p className="text-xs text-on-surface-variant" role="status">{multisigStatus}</p>}
</div>
)}
</div>
)}
</div>
)}

Expand Down Expand Up @@ -594,6 +673,13 @@ function MarketQuickViewContent() {
onCancel={() => closeReview(false)}
/>
)}
{multisigSession && (
<MultisigReview
session={multisigSession}
onClose={() => setMultisigSession(null)}
onUpdated={(next) => setMultisigSession(next)}
/>
)}
</main>
);
}
53 changes: 52 additions & 1 deletion app/premium/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import React, { useCallback, useEffect, useRef, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";

import { useWallet } from "@/lib/contexts/WalletContext";
import { useUsdcTransfer, useAccountReserve, usePrepareUsdcTransfer } from "@/lib/hooks/useStellar";
import { useUsdcTransfer, useAccountReserve, usePrepareUsdcTransfer, useAccountAuth, usePrepareMultisig } from "@/lib/hooks/useStellar";
import { useUnlockTrace } from "@/lib/hooks/useTraces";
import { PAYMENTS_RECIPIENT, explorerTx } from "@/lib/stellar/config";
import { formatUsdc, formatXlm, parseUsdc } from "@/lib/stellar/units";
import { TxPhase } from "@/lib/stellar/tx-lifecycle";
import { isMultisigAccount, type MultisigSession } from "@/lib/stellar/multisig";
import { MultisigReview } from "@/components/multisig-review";

function renderTxPhase(phase: TxPhase): string | null {
switch (phase.name) {
Expand Down Expand Up @@ -124,6 +126,11 @@ export default function PremiumPage() {
const [statusMessage, setStatusMessage] = useState<string | null>(null);
const [reviewModel, setReviewModel] = useState<{ model: ReviewModel; changedFrom?: ReviewModel } | null>(null);
const reviewResolver = useRef<((approved: boolean) => void) | null>(null);
const accountAuth = useAccountAuth(address);
const isMultisig = accountAuth.data ? isMultisigAccount(accountAuth.data) : false;
const [multisigMode, setMultisigMode] = useState(false);
const [multisigSession, setMultisigSession] = useState<MultisigSession | null>(null);
const prepareMultisig = usePrepareMultisig();
const waitingForTransfer =
submitQueue.waiting.some((job) => job.label === "USDC transfer") ||
(submitQueue.lockWaiting && submitQueue.running?.label === "USDC transfer");
Expand Down Expand Up @@ -176,6 +183,22 @@ export default function PremiumPage() {
setSelectedTier(tier);
};

const handlePrepareMultisigTransfer = async () => {
if (!selectedTier?.priceRaw || !address) return;
try {
const session = await prepareMultisig.mutateAsync({
kind: "transfer",
to: PAYMENTS_RECIPIENT,
amount: selectedTier.priceRaw,
sourceAccount: address,
timeoutInSeconds: 3600,
});
setMultisigSession(session);
} catch (error) {
setStatusMessage(error instanceof Error ? error.message : "Couldn't prepare the multisig payment.");
}
};

const handlePayment = async () => {
if (!selectedTier?.priceRaw) return;

Expand Down Expand Up @@ -526,6 +549,27 @@ export default function PremiumPage() {
? "Insufficient XLM"
: paymentMethod === 'usdc' ? "Pay with Wallet" : "Proceed to Payment"}
</button>
{isConnected && (
<div className="rounded-lg border border-outline-variant bg-surface-container-low p-3 space-y-2 mt-2">
<label className="flex items-center gap-2 text-xs">
<input type="checkbox" checked={multisigMode} onChange={(e) => setMultisigMode(e.target.checked)} />
Use multisig (treasury account)
</label>
{isMultisig && !multisigMode && (
<p className="text-[11px] text-on-surface-variant">Detected {accountAuth.data?.signers.length} signers, medium threshold {accountAuth.data?.med}. Use multisig prepare for treasury payments.</p>
)}
{(multisigMode || isMultisig) && (
<button
type="button"
onClick={handlePrepareMultisigTransfer}
disabled={prepareMultisig.isPending}
className="w-full py-2 border border-outline-variant rounded-lg text-xs font-bold disabled:opacity-50"
>
{prepareMultisig.isPending ? "Preparing…" : "Prepare multisig payment (1h window)"}
</button>
)}
</div>
)}
<p className="text-center text-[10px] text-on-surface-variant uppercase tracking-widest mt-4">
Verified on-chain by OracleDesk before access is granted
</p>
Expand All @@ -546,6 +590,13 @@ export default function PremiumPage() {
onCancel={() => closeReview(false)}
/>
)}
{multisigSession && (
<MultisigReview
session={multisigSession}
onClose={() => setMultisigSession(null)}
onUpdated={(next) => setMultisigSession(next)}
/>
)}
</main>
);
}
Loading