Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f72a18b
Phase 01 + 02: E2E coverage + observability for transacting users (#174)
alastairong1 May 30, 2026
35b2a56
revert: roll back stale-orderbook cache changes (#183–#185)
Siddharth2207 May 30, 2026
bd00077
Merge pull request #191 from SARKEX/revert/stale-orderbook-prs-183-185
Siddharth2207 May 30, 2026
dfcb08c
fix(tokens): use AMEX:SPYM for wtSPYM TradingView chart
Siddharth2207 Jun 1, 2026
39e06c1
Merge pull request #193 from SARKEX/fix/spym-tradingview-symbol
Siddharth2207 Jun 1, 2026
cb597c3
fix(api): curb bursty upstream calls via window bucketing + cache sta…
alastairong1 Jun 1, 2026
68a8b4a
feat(trade): wrap-ratio UX (chip, explainer, ratio history, denom tog…
alastairong1 May 28, 2026
d22d0a4
Fix market buy fallback when aggregated take simulation fails on orac…
Siddharth2207 May 20, 2026
aa08563
sgov
Siddharth2207 May 22, 2026
35bc1b7
update SGOV
Siddharth2207 May 28, 2026
cbebb2f
fix(tokens): drop duplicate wtSGOV entry from wrap-ratio commit
alastairong1 May 28, 2026
9c56048
refactor(wrap): hardcode SGOV rate + dividend distribution; drop API dep
alastairong1 May 28, 2026
6b53a70
fix(wrap): SGOV TradingView symbol + explainer modal polish
alastairong1 May 30, 2026
31b185e
fix(wrap): OrdersTable cells re-scale when denom toggle flips
alastairong1 May 30, 2026
15ad227
test(e2e): wrapRatio spec — switch to All Orders so toggle assertion …
alastairong1 May 31, 2026
e93aea3
ux(wrap): simplify Ratio History tab + frame chart axis around data
alastairong1 May 31, 2026
692ce07
ux(wrap): split Bid/Offer prices into share + token rows + wrap-ratio…
alastairong1 Jun 1, 2026
fa75b75
fix(trade): guard concat() and for-of against undefined trades response
alastairong1 Jun 1, 2026
358a79f
ux(wrap): panel denom toggle + dashboard holdings unwrapped view
alastairong1 Jun 1, 2026
5e72bca
copy(wrap-explainer): replace 'backed by' + rewrite 'why wrap at all'
alastairong1 Jun 1, 2026
24aa0b7
copy(wrap-explainer): tighten 'why wrap' → 'Built for DeFi' single-pa…
alastairong1 Jun 1, 2026
0ffef60
ux(wrap): pin Order Summary buying + price to wt with t-equivalent below
alastairong1 Jun 1, 2026
2746c40
fix(wrap): order-summary precision + late-toggle reactivity
alastairong1 Jun 1, 2026
3ff6325
Merge remote-tracking branch 'origin/phase-01-ui-driven-e2e-tests' in…
alastairong1 Jun 2, 2026
7daccd0
fix(ci): resolve lint job — eslint no-unused-expressions + prettier f…
alastairong1 Jun 2, 2026
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
6 changes: 3 additions & 3 deletions .planning/codebase/INTEGRATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@
- Base URL: env `ST0X_API_URL`
- Auth: HTTP Basic via `ST0X_API_KEY` + `ST0X_API_SECRET` (`btoa('key:secret')`, server-only)
- Allowlist of proxied routes (with per-route Vercel edge cache):
- `GET v1/orders/token/:address` — `private, no-store` (browser + Vercel CDN; upstream API caches ~15s)
- `GET v1/trades/token/:address` — same as orders/token
- `GET v1/orders/token/:address` — `s-maxage=5, stale-while-revalidate=120`
- `GET v1/trades/token/:address` — `s-maxage=5, stale-while-revalidate=120`
- `GET v1/orders/owner/:address`, `GET v1/trades/:address`, `GET v1/trades/taker/:address` — no shared cache
- `POST v1/trades/batch` — no cache
- `POST v1/trades/query` — no cache
- `GET health`
- Client wrapper: `src/lib/api/st0xApi.ts` (typed `apiGet*` functions; all gated to browser context)

Expand Down
5 changes: 2 additions & 3 deletions src/lib/api/st0xApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ export async function apiGetOrdersByToken(
pageSize: options?.pageSize,
side: options?.side
});
return fetchJson<ApiOrdersListResponse>(url, { cache: 'no-store' });
return fetchJson<ApiOrdersListResponse>(url);
}

/**
Expand Down Expand Up @@ -226,7 +226,6 @@ export async function apiGetTradesByToken(
if (startTime !== undefined) params.set('startTime', String(startTime));
if (endTime !== undefined) params.set('endTime', String(endTime));
return fetchJson<ApiTradesByAddressResponse>(
`/api/st0x/v1/trades/token/${tokenAddress}?${params}`,
{ cache: 'no-store' }
`/api/st0x/v1/trades/token/${tokenAddress}?${params}`
);
}
68 changes: 61 additions & 7 deletions src/lib/components/TradeAmountInput.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@
export let compact: boolean = false;
export let noBorder: boolean = false;

// Optional label override — when set, this string is shown as the input's
// trailing unit instead of `amountToken.symbol`. Used by the trade panel
// to flip wtX ↔ tX when the user toggles share-denominated display on.
export let unitOverride: string | undefined = undefined;

// Display-only scale factor. When > 0 and !== 1, the user types values
// in the SCALED denomination (e.g. tSGOV shares) while `amount` remains
// in the token's native denomination (wtSGOV wei). Math is:
// amount = parseUnits(inputString / displayScale, decimals)
// display = formatUnits(amount * displayScale, decimals)
// `displayScale` is a float — the wrap ratio is bounded (1.0–~2.0) and
// only has ~4 significant digits in practice, so float precision is fine.
export let displayScale: number = 1;

// Expose balance to parent component
export let balance: bigint = 0n;
export let balanceDecimals: number | null = null;
Expand Down Expand Up @@ -69,21 +83,60 @@
amount = undefined;
};

function activeScale(): number {
return Number.isFinite(displayScale) && displayScale > 0 ? displayScale : 1;
}

// Convert a user-typed string (in the displayed denomination) into a wt-
// denominated BigInt at `amountDecimals` precision. When `displayScale`
// is 1 (the default) this is just `parseUnits`. When >1, we divide by
// the ratio first so the stored BigInt is the on-chain (wt) amount.
function parseDisplayInput(input: string, decimals: number): bigint {
const scale = activeScale();
if (scale === 1) return parseUnits(input, decimals);
// `parseFloat` is OK here: the wrap ratio's significant digits (~4) and
// typical user inputs (≤ 10^10) stay well within float64 precision.
const tValue = parseFloat(input);
if (!Number.isFinite(tValue)) throw new Error('invalid input');
const wtValue = tValue / scale;
// Clamp to the token's decimals so we never exceed parseUnits' digit
// budget. toFixed(decimals) gives a fixed-point string with up to
// `decimals` fractional digits — safe input for parseUnits.
return parseUnits(wtValue.toFixed(decimals), decimals);
}

function formatWtAsDisplay(wt: bigint, decimals: number): string {
const scale = activeScale();
const native = formatUnits(wt, decimals);
if (scale === 1) return native;
const tValue = parseFloat(native) * scale;
return tValue.toFixed(decimals);
}

// Reference `displayScale` here so Svelte's reactive tracker re-runs this
// block when the parent flips the share-denominated toggle after the user
// has already typed. Without this the bound `amount` BigInt stays in
// whichever denom was active at type-time and the user thinks the
// conversion isn't working.
$: if (inputAmount && canParseDecimals(amountDecimals)) {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions -- justification: bare reference registers displayScale as a Svelte reactive dependency; it is consumed transitively via activeScale() inside parseDisplayInput, which the compiler cannot see.
displayScale; // explicit dep — see comment above
try {
amount = parseUnits(inputAmount, amountDecimals);
amount = parseDisplayInput(inputAmount, amountDecimals);
} catch {
amount = undefined;
}
} else if (!inputAmount) {
amount = undefined;
}

// Expose function to set amount from parent (for percentage buttons)
// Expose function to set amount from parent (for percentage buttons).
// Parent always speaks wt-denominated BigInts; we render the display
// string in whichever denomination is active.
export function setAmountValue(newAmount: bigint) {
if (!canParseDecimals(amountDecimals)) return;
amount = newAmount;
inputAmount = formatUnits(newAmount, amountDecimals);
inputAmount = formatWtAsDisplay(newAmount, amountDecimals);
}

$: balancePromise = (async () => {
Expand Down Expand Up @@ -139,7 +192,7 @@
if (!canParseDecimals(amountDecimals)) {
return;
}
inputAmount = formatUnits(balance, amountDecimals);
inputAmount = formatWtAsDisplay(balance, amountDecimals);
amount = balance;
};
</script>
Expand All @@ -149,7 +202,7 @@
{...$$restProps}
bind:amount={inputAmount}
type="number"
unit={showUnit ? amountToken.symbol : ''}
unit={showUnit ? unitOverride ?? amountToken.symbol : ''}
maxButton={showMaxButton}
on:setValueToMax={setValueToMax}
{dataTestId}
Expand All @@ -165,10 +218,11 @@
</span>
{:then data}
{#if data}
{@const balanceFormatted = parseFloat(formatUnits(data.balance, data.decimals))}
{@const balanceFormatted =
parseFloat(formatUnits(data.balance, data.decimals)) * activeScale()}
{@const balanceRounded = Math.round(balanceFormatted * 1000) / 1000}
Balance: {balanceRounded.toFixed(3)}
{(balanceToken ?? amountToken)?.symbol}
{unitOverride ?? (balanceToken ?? amountToken)?.symbol}
{:else}
Balance: —
{/if}
Expand Down
13 changes: 12 additions & 1 deletion src/lib/components/orders/DcaOrder.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@
: 'bg-red-500 hover:bg-red-600 text-white';

export let assetToken: PythToken | undefined; // The token we're accumulating
/** Share-denominated display toggle — see MarketOrder for the contract. */
export let displayDenom: 'wrapped' | 'unwrapped' = 'wrapped';
export let wrapRatio: number = 1;
$: displayedAssetSymbol =
displayDenom === 'unwrapped' && assetToken
? assetToken.symbol.replace(/^wt/, 't')
: assetToken?.symbol ?? '';
$: displayScale =
displayDenom === 'unwrapped' && Number.isFinite(wrapRatio) && wrapRatio > 0 ? wrapRatio : 1;

// Filter tokens based on current network
$: ALL_TOKENS = $currentNetwork ? getAllTokensByNetwork($currentNetwork.chainId) : [];
Expand Down Expand Up @@ -315,7 +324,7 @@
<div class="mb-2 block text-sm font-medium text-gray-300">
{orderSide === 'Buy' ? 'Purchase Budget' : 'Amount to Sell'}
<span class="ml-1 text-xs text-gray-500"
>({orderSide === 'Buy' ? settlementLabel : selectedInputToken.symbol})</span
>({orderSide === 'Buy' ? settlementLabel : displayedAssetSymbol})</span
>
</div>
<TradeAmountInput
Expand All @@ -329,6 +338,8 @@
validate={validateSelectedAmount}
bind:isError={selectedAmountError}
showMaxButton={false}
displayScale={orderSide === 'Buy' ? 1 : displayScale}
unitOverride={orderSide === 'Buy' ? settlementLabel : displayedAssetSymbol}
/>
<!-- Percentage buttons -->
<div class="mt-2 flex gap-2">
Expand Down
42 changes: 34 additions & 8 deletions src/lib/components/orders/LimitOrder.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,17 @@
export let orderSide: 'Buy' | 'Sell' = 'Buy';
export let buyPrice: number | null = null; // Best bid price (what you get when selling)
export let sellPrice: number | null = null; // Best ask price (what you pay when buying)
/** Share-denominated display toggle — see MarketOrder for the contract. */
export let displayDenom: 'wrapped' | 'unwrapped' = 'wrapped';
export let wrapRatio: number = 1;
$: displayedAssetSymbol =
displayDenom === 'unwrapped' && assetToken
? assetToken.symbol.replace(/^wt/, 't')
: assetToken?.symbol ?? '';
$: displayScale =
displayDenom === 'unwrapped' && Number.isFinite(wrapRatio) && wrapRatio > 0 ? wrapRatio : 1;
$: showShareEquivalent = displayDenom === 'unwrapped' && displayScale !== 1;
$: wtDecimalsForSummary = showShareEquivalent ? 5 : 3;

// Filter tokens based on current network
$: ALL_TOKENS = $currentNetwork ? getAllTokensByNetwork($currentNetwork.chainId) : [];
Expand Down Expand Up @@ -437,6 +448,8 @@
bind:isError={selectedAmountError}
showUnit={false}
showMaxButton={false}
{displayScale}
unitOverride={displayedAssetSymbol}
/>
<!-- Percentage buttons -->
<div class="mt-2 flex gap-2">
Expand All @@ -455,7 +468,7 @@
<div class="mb-2 block text-sm font-medium text-gray-300">
Limit Price
<span class="ml-1 text-xs text-gray-500"
>({settlementLabel} per {assetToken.symbol})</span
>({settlementLabel} per {displayedAssetSymbol})</span
>
</div>
<Input
Expand All @@ -475,14 +488,27 @@
<div class={containerStyles.cardBordered}>
<h4 class="mb-3 text-sm font-medium text-gray-300">Order Summary</h4>
<div class="space-y-2 text-sm">
<div class="flex justify-between">
<div class="flex items-start justify-between gap-3">
<span class="text-gray-400">{orderSide === 'Buy' ? 'Buying' : 'Selling'}</span>
<span class="font-medium">
{selectedAmount
? parseFloat(formatUnits(selectedAmount, assetToken.decimals)).toFixed(3)
: '0'}
{assetToken.symbol}
</span>
<div class="text-right">
<span class="font-medium">
{(selectedAmount
? parseFloat(formatUnits(selectedAmount, assetToken.decimals))
: 0
).toFixed(wtDecimalsForSummary)}
{assetToken.symbol}
</span>
{#if showShareEquivalent}
<div class="text-[11px] text-gray-500">
equivalent to {(
(selectedAmount
? parseFloat(formatUnits(selectedAmount, assetToken.decimals))
: 0) * displayScale
).toFixed(5)}
{displayedAssetSymbol}
</div>
{/if}
</div>
</div>
<div class="flex justify-between">
<span class="text-gray-400">At price</span>
Expand Down
Loading
Loading