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
40 changes: 40 additions & 0 deletions app/copy-trade/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ function CopyTradeContent() {
const [traceError, setTraceError] = useState<string | null>(null);
const { data: market } = useMarket(marketIdParam ?? trace?.marketId ?? undefined);
const onChainMarketId = market?.onChainMarketId ?? null;
const onChainMarket = useOnChainMarket(onChainMarketId);

const balance = useUsdcBalance(address);
const reserve = useAccountReserve(address);
Expand Down Expand Up @@ -184,6 +185,24 @@ function CopyTradeContent() {
setStatusMessage("This market isn't on-chain yet, so it can't be traded.");
return;
}
if (!onChainMarket.data) {
setStatusMessage("Checking the on-chain market and ledger time…");
return;
}
if (!marketCanTrade) {
setStatusMessage("This market is closed or too close to its deadline to safely place a trade.");
return;
}
if (!trustlineReady) {
setStatusMessage(
trustlineMissing
? "Enable your USDC trustline before copying this trade."
: trustline.data?.kind === "present"
? "The issuer hasn't authorized your USDC trustline."
: "Checking your USDC trustline…",
);
return;
}
if (!amountRaw || amountRaw <= 0n) {
setStatusMessage("Choose an allocation greater than zero. You may need USDC in your wallet first.");
return;
Expand All @@ -197,6 +216,17 @@ function CopyTradeContent() {
return;
}

const freshMarket = await fetchOnChainMarket(onChainMarketId);
const freshLedgerNow = getLedgerNow();
if (
freshLedgerNow === null ||
freshMarket.market.status.tag !== "Open" ||
BigInt(Math.floor(freshLedgerNow)) + BigInt(SAFETY_MARGIN_S + MIN_TX_TIMEOUT_S) >= freshMarket.market.close_time
) {
setStatusMessage("This market is closed or too close to its deadline to safely place a trade.");
return;
}

setStatusMessage("Recording your copy trade…");
const { copyTradeId } = await initiateCopyTrade({
traceId: trace.id,
Expand Down Expand Up @@ -230,6 +260,16 @@ function CopyTradeContent() {
}
};

const handleEnableUsdc = async () => {
setStatusMessage("Checking XLM reserve, then opening your wallet…");
try {
await enableUsdc.mutateAsync();
setStatusMessage("USDC is enabled. Click Confirm Copy Trade when you're ready.");
} catch (error) {
setStatusMessage(error instanceof Error ? error.message : "Couldn't enable USDC.");
}
};

return (
<div className="bg-background text-on-surface min-h-screen flex flex-col">
<style jsx global>{`
Expand Down
65 changes: 60 additions & 5 deletions app/markets/quick-view/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ function MarketQuickViewContent() {
const onChainMarketId = market?.onChainMarketId ?? onChainIdParam ?? null;
const chain = useOnChainMarket(onChainMarketId);

const { address, isConnected, openModal } = useWallet();
const { address, isConnected, isWrongNetwork, openModal } = useWallet();
const position = usePosition(onChainMarketId, address);
const balance = useUsdcBalance(address);
const reserve = useAccountReserve(address);
Expand All @@ -102,6 +102,7 @@ function MarketQuickViewContent() {
const [showModal] = useState(true);
const [side, setSide] = useState<"Yes" | "No">(initialSide);
const [amountInput, setAmountInput] = useState("10");
const [buyMode, setBuyMode] = useState<"usdc" | "shares">("usdc");
const [buyStatus, setBuyStatus] = useState<string | null>(null);
const [reviewModel, setReviewModel] = useState<{ model: ReviewModel; changedFrom?: ReviewModel } | null>(null);
const reviewResolver = useRef<((approved: boolean) => void) | null>(null);
Expand Down Expand Up @@ -161,8 +162,28 @@ function MarketQuickViewContent() {
return;
}
if (!onChainMarketId) return;
if (trustlineMissing) {
setBuyStatus("Enable your USDC trustline below, then click Buy again.");
return;
}
if (trustlineUnauthorized) {
setBuyStatus("The issuer hasn't authorized your USDC trustline.");
return;
}
if (isWrongNetwork) {
setBuyStatus("Switch your wallet to Stellar Testnet to place this trade.");
return;
}
if (!amountRaw || amountRaw <= 0n) {
setBuyStatus("Enter an amount like 10 or 2.5.");
setBuyStatus(buyMode === "usdc" ? "Enter an amount like 10 or 2.5." : "Enter a share amount greater than zero.");
return;
}
if (ledger.now === null) {
setBuyStatus("Checking market time…");
return;
}
if (!safeToTrade) {
setBuyStatus("This market closes too soon to safely place a trade.");
return;
}
setBuyStatus(null);
Expand All @@ -183,6 +204,16 @@ function MarketQuickViewContent() {
}
};

const handleEnableUsdc = async () => {
setBuyStatus("Checking XLM reserve, then opening your wallet…");
try {
await enableUsdc.mutateAsync();
setBuyStatus("USDC is enabled. Review your amount and click Buy to submit the trade.");
} catch (err) {
setBuyStatus(err instanceof Error ? err.message : "Couldn't enable USDC.");
}
};

if (isLoading) {
return (
<main className="relative min-h-[calc(100vh-64px)] overflow-hidden bg-surface flex items-center justify-center">
Expand Down Expand Up @@ -256,6 +287,7 @@ function MarketQuickViewContent() {
{statusLabel}
{chainStatus && <SourceTag source="chain" />}
</span>
{countdown && <span className="text-[10px] font-data-mono text-on-surface-variant">{countdown}</span>}
<span className="font-label-caps text-label-caps text-outline uppercase font-bold tracking-wider">{categoryLabel}</span>
</div>
<Link
Expand Down Expand Up @@ -336,8 +368,12 @@ function MarketQuickViewContent() {
{onChainMarketId !== null && (
<div className="space-y-3">
<div className="flex items-center gap-3">
<div className="inline-flex rounded border border-outline-variant p-0.5" aria-label="Buy amount unit">
<button type="button" aria-pressed={buyMode === "usdc"} onClick={() => setBuyMode("usdc")} className={`px-2 py-1 text-[10px] rounded ${buyMode === "usdc" ? "bg-primary text-primary-foreground" : "text-on-surface-variant"}`}>USDC</button>
<button type="button" aria-pressed={buyMode === "shares"} onClick={() => setBuyMode("shares")} className={`px-2 py-1 text-[10px] rounded ${buyMode === "shares" ? "bg-primary text-primary-foreground" : "text-on-surface-variant"}`}>Shares</button>
</div>
<label htmlFor="buy-amount" className="font-label-caps text-label-caps text-on-surface-variant uppercase text-[10px] shrink-0">
Amount (USDC)
Amount ({buyMode === "usdc" ? "USDC" : `${side.toUpperCase()} shares`})
</label>
<input
id="buy-amount"
Expand Down Expand Up @@ -371,8 +407,14 @@ function MarketQuickViewContent() {
<div className="flex justify-between text-[11px] font-data-mono text-on-surface-variant">
<span>
{amountRaw === null
? "Enter an amount like 10 or 2.5"
: quote.data !== undefined
? buyMode === "usdc" ? "Enter an amount like 10 or 2.5" : "Enter a share amount"
: buyMode === "shares" && exactQuote.data
? `Costs ≈ ${formatUsdc(exactQuote.data.collateralIn, { maxDecimals: 2 })} USDC (fee ${formatUsdc(exactQuote.data.fee, { maxDecimals: 2 })}), at most ${formatUsdc(maxIn(exactQuote.data.collateralIn, SLIPPAGE_BPS), { maxDecimals: 2 })} with 1% slippage`
: buyMode === "shares" && exactQuote.error
? exactQuote.error.message
: buyMode === "shares" && exactQuote.isFetching
? "Quoting exact share amount…"
: quote.data !== undefined
? `≈ ${formatUsdc(quote.data, { maxDecimals: 2 })} shares, at least ${formatUsdc(minOut(quote.data, SLIPPAGE_BPS), { maxDecimals: 2 })} (1% slippage)`
: quote.error ? quote.error.message : quote.isFetching ? "Quoting…" : ""}
</span>
Expand Down Expand Up @@ -420,6 +462,19 @@ function MarketQuickViewContent() {
{!isOpen && view && (
<p className="text-[11px] text-on-surface-variant">This market is closed to trading.</p>
)}
{ledger.now === null && <p className="text-[11px] text-on-surface-variant">Checking market time…</p>}
{isOpen && !safeToTrade && <p className="text-[11px] text-on-surface-variant">Trading is disabled this close to the market deadline.</p>}
{trustlineMissing && !isWrongNetwork && (
<div className="rounded-lg border border-outline-variant bg-surface-container-low p-3 space-y-2">
<p className="text-xs text-on-surface-variant">
Enable USDC once to buy on Stellar. This creates a trustline and locks one base reserve (typically 0.5 XLM); it does not submit your trade.
</p>
<button type="button" onClick={handleEnableUsdc} disabled={enableUsdc.isPending} className="bg-primary text-primary-foreground px-3 py-2 rounded-lg text-xs font-bold disabled:opacity-50">
{enableUsdc.isPending ? "Enabling…" : "Enable USDC"}
</button>
</div>
)}
{trustlineUnauthorized && <p className="text-[11px] text-error">The issuer hasn&apos;t authorized your USDC trustline.</p>}
{position.data && (position.data.yes > 0n || position.data.no > 0n) && (
<p className="text-[11px] font-data-mono text-on-surface">
Your position: {formatUsdc(position.data.yes, { maxDecimals: 2 })} YES · {formatUsdc(position.data.no, { maxDecimals: 2 })} NO
Expand Down
34 changes: 32 additions & 2 deletions components/layout/WalletDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import React, { useState, useRef, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { useWallet } from "@/lib/contexts/WalletContext";
import { useUsdcBalance } from "@/lib/hooks/useStellar";
import { useEnableUsdc, useUsdcBalance, useUsdcTrustline } from "@/lib/hooks/useStellar";
import { NETWORK_LABEL, explorerAccount } from "@/lib/stellar/config";
import { formatUsdc, shortAddress } from "@/lib/stellar/units";

Expand All @@ -13,6 +13,8 @@ const WalletDropdown = () => {
const [copied, setCopied] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const balance = useUsdcBalance(address);
const trustline = useUsdcTrustline(address);
const enableUsdc = useEnableUsdc();

// Close dropdown when clicking outside
useEffect(() => {
Expand All @@ -39,10 +41,22 @@ const WalletDropdown = () => {
}

const truncatedAddress = shortAddress(address, 6, 4);
const balanceText = balance.data !== undefined
const trustlineMissing = !isWrongNetwork && trustline.data?.kind === "missing";
const unauthorized = trustline.data?.kind === "present" && !trustline.data.authorized;
const balanceText = trustlineMissing
? "USDC not enabled"
: balance.data !== undefined
? formatUsdc(balance.data, { maxDecimals: 2, minDecimals: 2, grouping: true })
: balance.isError ? "—" : "…";

const handleEnableUsdc = async () => {
try {
await enableUsdc.mutateAsync();
} catch {
// The mutation error is rendered in the wallet menu below.
}
};

const copyAddress = async () => {
try {
await navigator.clipboard.writeText(address);
Expand Down Expand Up @@ -101,6 +115,22 @@ const WalletDropdown = () => {
USDC
</span>
</div>
{trustlineMissing && !isWrongNetwork && (
<button
type="button"
onClick={handleEnableUsdc}
disabled={enableUsdc.isPending}
className="mt-2 rounded-md bg-primary px-3 py-1.5 text-xs font-bold text-primary-foreground disabled:opacity-50"
>
{enableUsdc.isPending ? "Enabling…" : "Enable USDC"}
</button>
)}
{unauthorized && (
<p className="mt-2 text-[11px] text-error">The issuer hasn&apos;t authorized your USDC trustline.</p>
)}
{enableUsdc.error && (
<p className="mt-2 text-[11px] text-error">{enableUsdc.error.message}</p>
)}
{isWrongNetwork && (
<p className="mt-2 text-[11px] text-error">Your wallet is on a different network. Switch it to Stellar Testnet.</p>
)}
Expand Down
8 changes: 8 additions & 0 deletions docs/contract-requests.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,11 @@ the commitment hash has to rebuild the XDR by hand. A no-op view function
that takes a `ResolutionSpec`, or a `spec_hash(spec) -> BytesN<32>` view,
would put the type in the spec and let clients ask the contract for the
hash instead of re-implementing it.

## 5. Add a share-budget sell quote

The frontend converts "sell N shares" into `collateral_out` by binary-searching
the contract's integer-rounded `calc_sell` math, then confirms the result with
`quote_sell`. A view such as `quote_sell_shares(market_id, outcome, shares) ->
{ collateral_out, shares_in }` would provide the exact inversion on-chain and
avoid repeated local work and quote simulations.
8 changes: 5 additions & 3 deletions docs/manual-test.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ These steps cover what automated checks can't: a real browser wallet signing rea

## Before you start: two known blockers

1. **Test USDC can only be minted by the deployer key.** The collateral is a self-issued test asset (`USDC` issued by `GD2NB2VUME7ECMHFHQN5M3KUZLLJF3R6Z4OFCBLE4SJNHIQHITRPQGHQ`, wrapped as the SAC `CA2WQQJ4OHQCLHQW6XN4BCLILGRV6V4YDYDT3GVIWXB53BTOO7EMREQH`). There is no faucet, so step 4 needs a maintainer (see [contract-requests.md](contract-requests.md) #3).
1. **Test USDC can only be minted by the deployer key.** The collateral is a self-issued test asset wrapped as a Stellar Asset Contract. There is no faucet, so step 4 still needs a maintainer (see [contract-requests.md](contract-requests.md) #3).
2. **Both existing testnet markets are closed.** Markets 0 and 1 were created by the contracts repo's demo with a 90-second trading window on 2026-09-21; market 0 is resolved YES. Buying on them fails with "This market has closed". Step 7 therefore needs a fresh, open market (see below).

## You need
Expand All @@ -31,9 +31,11 @@ These steps cover what automated checks can't: a real browser wallet signing rea

## 4. Get test USDC

a. **Add the trustline** (the account must trust the asset before it can hold it):
a. **Enable the trustline in OracleDesk** (the account must trust the asset before it can hold it):

- [ ] Freighter → Manage assets → Add manually: code `USDC`, issuer `GD2NB2VUME7ECMHFHQN5M3KUZLLJF3R6Z4OFCBLE4SJNHIQHITRPQGHQ`. Approve the transaction.
- [ ] Connect your funded wallet, then open the wallet menu or an on-chain market's quick view.
- [ ] Click **Enable USDC** and approve the one-time `changeTrust` transaction. The flow checks spendable XLM for the trustline reserve and transaction fee first.
- [ ] Wait for the trustline to appear. Enabling it does not submit a pending buy; review the amount and click **Buy** again.

b. **Ask a maintainer to mint.** With the deployer key from `scripts/deploy-testnet.sh` (the CLI identity `oracledesk-deployer`), they run:

Expand Down
Loading