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
2 changes: 2 additions & 0 deletions packages/ui/.env.exemple
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ NEXT_PUBLIC_APP_NAME="Sui AMM"
NEXT_PUBLIC_APP_DESCRIPTION="Full-Stack Sui AMM"
NEXT_PUBLIC_LOCALNET_CONTRACT_PACKAGE_ID=""
NEXT_PUBLIC_TESTNET_CONTRACT_PACKAGE_ID=""
NEXT_PUBLIC_LOCALNET_AMM_CONFIG_ID=""
NEXT_PUBLIC_TESTNET_AMM_CONFIG_ID=""
6 changes: 4 additions & 2 deletions packages/ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This UI is a Next.js 16 app that talks directly to Sui via Mysten dapp-kit.

## 1. Prereqs
1. Localnet running (or a target network RPC).
2. A published `` package and a AMM ID.
2. A published package and an AMM config object ID.
3. A wallet with the right network selected.

## 2. Run it
Expand All @@ -13,10 +13,12 @@ pnpm ui dev
```

## 3. Configure networks (.env.local)
Create `packages/ui/.env.local` and set package + package IDs:
Create `packages/ui/.env.local` and set package + AMM config IDs:
```bash
NEXT_PUBLIC_LOCALNET_CONTRACT_PACKAGE_ID=0x...
NEXT_PUBLIC_TESTNET_CONTRACT_PACKAGE_ID=0x...
NEXT_PUBLIC_LOCALNET_AMM_CONFIG_ID=0x...
NEXT_PUBLIC_TESTNET_AMM_CONFIG_ID=0x...
```

Optional UI labels:
Expand Down
12 changes: 12 additions & 0 deletions packages/ui/src/app/components/AmmConfigCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"use client"

import useAmmConfigCardViewModel from "../hooks/useAmmConfigCardViewModel"
import AmmConfigCardView from "./AmmConfigCardView"

const AmmConfigCard = () => {
const { viewModel } = useAmmConfigCardViewModel()

return <AmmConfigCardView {...viewModel} />
}

export default AmmConfigCard
139 changes: 139 additions & 0 deletions packages/ui/src/app/components/AmmConfigCardView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"use client"

import type { ReactNode } from "react"
import type {
TAmmConfigCardContent,
TAmmConfigCardViewModel
} from "../types/TAmmConfigCard"
import CopyableId from "./CopyableId"
import Loading from "./Loading"

const ConfigTile = ({
label,
children
}: {
label: string
children: ReactNode
}) => {
return (
<div className="rounded-xl border border-slate-200/80 bg-white/80 p-4 shadow-[0_12px_28px_-24px_rgba(15,23,42,0.4)] dark:border-slate-50/15 dark:bg-slate-950/70">
<div className="text-[0.6rem] uppercase tracking-[0.18em] text-slate-500 dark:text-slate-200/70">
{label}
</div>
<div className="mt-2 text-sm text-sds-dark dark:text-sds-light">
{children}
</div>
</div>
)
}

const renderContent = (content: TAmmConfigCardContent) => {
switch (content.state) {
case "loading":
return <Loading />
case "missing-id":
case "error":
return (
<div className="rounded-xl border border-rose-200/70 bg-rose-50/60 p-4 text-sm text-rose-600 dark:border-rose-500/30 dark:bg-rose-500/10">
{content.message}
</div>
)
case "ready": {
const { details } = content
return (
<div className="grid gap-4 md:grid-cols-2">
<ConfigTile label="Base spread (bps)">
<span className="text-lg font-semibold text-sds-dark dark:text-sds-light">
{details.baseSpreadBps}
</span>
</ConfigTile>
<ConfigTile label="Volatility multiplier (bps)">
<span className="text-lg font-semibold text-sds-dark dark:text-sds-light">
{details.volatilityMultiplierBps}
</span>
</ConfigTile>
<ConfigTile label="Laser">
<span className={details.laserBadge.className}>
{details.laserBadge.label}
</span>
</ConfigTile>
<ConfigTile label="Trading status">
<span className={details.tradingBadge.className}>
{details.tradingBadge.label}
</span>
</ConfigTile>
<div className="md:col-span-2">
<ConfigTile label="Pyth price feed id">
{details.pythPriceFeedIdHex ? (
<CopyableId
value={details.pythPriceFeedIdHex}
label="Feed"
showExplorer={false}
className="w-full"
/>
) : (
"Unknown"
)}
</ConfigTile>
</div>
</div>
)
}
default:
return (
<div className="rounded-xl border border-dashed border-slate-300/60 p-4 text-sm text-slate-500 dark:border-slate-100/20 dark:text-slate-200/70">
No AMM config loaded yet.
</div>
)
}
}

const AmmConfigCardView = ({
title,
description,
networkLabel,
explorerUrl,
ammConfigId,
content
}: TAmmConfigCardViewModel) => {
const resolvedNetworkLabel =
typeof networkLabel === "string" && networkLabel.trim().length > 0
? networkLabel
: "unknown network"

return (
<section className="w-full max-w-4xl px-4">
<div className="rounded-2xl border border-slate-300/80 bg-white/90 shadow-[0_22px_65px_-45px_rgba(15,23,42,0.45)] backdrop-blur-md transition dark:border-slate-50/30 dark:bg-slate-950/70">
<div className="flex flex-wrap items-center gap-3 border-b border-slate-300/70 px-6 py-4 dark:border-slate-50/25">
<div className="flex flex-col gap-1">
<h2 className="text-base font-semibold text-sds-dark dark:text-sds-light">
{title}
</h2>
<p className="text-xs uppercase tracking-[0.16em] text-slate-500 dark:text-slate-200/60">
{description}
</p>
<p className="text-xs uppercase tracking-[0.16em] text-slate-500 dark:text-slate-200/60">
{`Network: ${resolvedNetworkLabel}`}
</p>
</div>
</div>
<div className="space-y-4 px-6 py-5">
<div className="flex flex-wrap items-center gap-3 text-xs uppercase tracking-[0.16em] text-slate-500 dark:text-slate-200/60">
{ammConfigId ? (
<CopyableId
value={ammConfigId}
label="AMM config"
explorerUrl={explorerUrl}
/>
) : (
<span className="text-rose-500">Missing AMM config ID</span>
)}
</div>
{renderContent(content)}
</div>
</div>
</section>
)
}

export default AmmConfigCardView
12 changes: 11 additions & 1 deletion packages/ui/src/app/config/network.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// We automatically create/update .env.local with the deployed package ID after deployment.
export const CONTRACT_PACKAGE_ID_NOT_DEFINED = "0xNOTDEFINED"
export const CONTRACT_PACKAGE_ID_NOT_DEFINED = "0xUNDEFINED"
export const AMM_CONFIG_ID_NOT_DEFINED = "0xUNDEFINED"
export const LOCALNET_CONTRACT_PACKAGE_ID =
process.env.NEXT_PUBLIC_LOCALNET_CONTRACT_PACKAGE_ID ||
CONTRACT_PACKAGE_ID_NOT_DEFINED
Expand All @@ -13,13 +14,22 @@ export const TESTNET_CONTRACT_PACKAGE_ID =
export const MAINNET_CONTRACT_PACKAGE_ID =
process.env.NEXT_PUBLIC_MAINNET_CONTRACT_PACKAGE_ID ||
CONTRACT_PACKAGE_ID_NOT_DEFINED
export const LOCALNET_AMM_CONFIG_ID =
process.env.NEXT_PUBLIC_LOCALNET_AMM_CONFIG_ID || AMM_CONFIG_ID_NOT_DEFINED
export const DEVNET_AMM_CONFIG_ID =
process.env.NEXT_PUBLIC_DEVNET_AMM_CONFIG_ID || AMM_CONFIG_ID_NOT_DEFINED
export const TESTNET_AMM_CONFIG_ID =
process.env.NEXT_PUBLIC_TESTNET_AMM_CONFIG_ID || AMM_CONFIG_ID_NOT_DEFINED
export const MAINNET_AMM_CONFIG_ID =
process.env.NEXT_PUBLIC_MAINNET_AMM_CONFIG_ID || AMM_CONFIG_ID_NOT_DEFINED

export const LOCALNET_EXPLORER_URL = "http://localhost:9001"
export const DEVNET_EXPLORER_URL = "https://devnet.suivision.xyz"
export const TESTNET_EXPLORER_URL = "https://testnet.suivision.xyz"
export const MAINNET_EXPLORER_URL = "https://suivision.xyz"

export const CONTRACT_PACKAGE_VARIABLE_NAME = "contractPackageId"
export const AMM_CONFIG_VARIABLE_NAME = "ammConfigId"

export const CONTRACT_MODULE_NAME = "amm"

Expand Down
9 changes: 6 additions & 3 deletions packages/ui/src/app/helpers/customNetworks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ export const createEmptyCustomNetworkDraft = (): TCustomNetworkDraft => ({
label: "",
rpcUrl: "",
explorerUrl: "",
contractPackageId: ""
contractPackageId: "",
ammConfigId: ""
})

export const normalizeCustomNetworkDraft = (
Expand All @@ -39,7 +40,8 @@ export const normalizeCustomNetworkDraft = (
label: trimValue(draft.label),
rpcUrl: trimValue(draft.rpcUrl),
explorerUrl: trimValue(draft.explorerUrl),
contractPackageId: trimValue(draft.contractPackageId)
contractPackageId: trimValue(draft.contractPackageId),
ammConfigId: trimValue(draft.ammConfigId ?? "") || undefined
})

export const validateCustomNetworkDraft = ({
Expand Down Expand Up @@ -123,7 +125,8 @@ export const parseStoredCustomNetworks = (
label: trimValue(String(entry.label ?? "")),
rpcUrl: trimValue(String(entry.rpcUrl ?? "")),
explorerUrl: trimValue(String(entry.explorerUrl ?? "")),
contractPackageId: trimValue(String(entry.contractPackageId ?? ""))
contractPackageId: trimValue(String(entry.contractPackageId ?? "")),
ammConfigId: trimValue(String(entry.ammConfigId ?? "")) || undefined
}))
.filter(
(entry) =>
Expand Down
146 changes: 146 additions & 0 deletions packages/ui/src/app/hooks/useAmmConfigCardViewModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"use client"

import { useSuiClientContext } from "@mysten/dapp-kit"
import type { AmmConfigOverview } from "@sui-amm/domain-core/models/amm"
import { useMemo } from "react"
import { formatNetworkType } from "../helpers/network"
import type {
TAmmConfigBadge,
TAmmConfigCardContent,
TAmmConfigCardState,
TAmmConfigCardViewModel,
TAmmConfigDetails
} from "../types/TAmmConfigCard"
import useAmmConfigOverview, {
type AmmConfigStatus
} from "./useAmmConfigOverview"
import useExplorerUrl from "./useExplorerUrl"
import useResolvedAmmConfigId from "./useResolvedAmmConfigId"

const headerTitle = "AMM configuration"
const headerDescription =
"Snapshot of the on-chain AMM config for this environment."
const missingConfigMessage = "AMM config ID is not configured for this network."
const defaultLoadErrorMessage = "Unable to load AMM config."
const positiveToneClassName =
"bg-emerald-100/70 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-200"
const negativeToneClassName =
"bg-rose-100/70 text-rose-700 dark:bg-rose-500/20 dark:text-rose-200"
const badgeBaseClassName =
"inline-flex rounded-full px-3 py-1 text-xs font-semibold uppercase tracking-[0.12em]"

const resolveNetworkLabel = (network?: string) =>
network ? formatNetworkType(network) : "unknown"

const buildBadgeClassName = (toneClassName: string) =>
`${badgeBaseClassName} ${toneClassName}`

const buildStatusBadge = ({
isActive,
activeLabel,
inactiveLabel
}: {
isActive: boolean
activeLabel: string
inactiveLabel: string
}): TAmmConfigBadge => ({
label: isActive ? activeLabel : inactiveLabel,
className: buildBadgeClassName(
isActive ? positiveToneClassName : negativeToneClassName
)
})

const buildLaserBadge = (useLaser: boolean): TAmmConfigBadge =>
buildStatusBadge({
isActive: useLaser,
activeLabel: "Enabled",
inactiveLabel: "Disabled"
})

const buildTradingBadge = (tradingPaused: boolean): TAmmConfigBadge =>
buildStatusBadge({
isActive: !tradingPaused,
activeLabel: "Live",
inactiveLabel: "Paused"
})

const buildAmmConfigDetails = (
ammConfig: AmmConfigOverview
): TAmmConfigDetails => ({
baseSpreadBps: ammConfig.baseSpreadBps,
volatilityMultiplierBps: ammConfig.volatilityMultiplierBps,
laserBadge: buildLaserBadge(ammConfig.useLaser),
tradingBadge: buildTradingBadge(ammConfig.tradingPaused),
pythPriceFeedIdHex: ammConfig.pythPriceFeedIdHex
})

const resolveContentState = ({
ammConfigId,
status,
ammConfig,
error
}: {
ammConfigId?: string
status: AmmConfigStatus
ammConfig?: AmmConfigOverview
error?: string
}): TAmmConfigCardContent => {
if (!ammConfigId) {
return { state: "missing-id", message: missingConfigMessage }
}

if (status === "idle" || status === "loading") {
return { state: "loading" }
}

if (status === "error") {
return { state: "error", message: error ?? defaultLoadErrorMessage }
}

if (!ammConfig) {
return { state: "error", message: defaultLoadErrorMessage }
}

return { state: "ready", details: buildAmmConfigDetails(ammConfig) }
}

const useAmmConfigCardViewModel = (): TAmmConfigCardState => {
const { network: currentNetwork } = useSuiClientContext()
const explorerUrl = useExplorerUrl()
const ammConfigId = useResolvedAmmConfigId()
const { status, ammConfig, error, refreshAmmConfig } =
useAmmConfigOverview(ammConfigId)

const networkLabel = useMemo(
() => resolveNetworkLabel(currentNetwork),
[currentNetwork]
)

const content = useMemo(
() =>
resolveContentState({
ammConfigId,
status,
ammConfig,
error
}),
[ammConfigId, status, ammConfig, error]
)

const viewModel: TAmmConfigCardViewModel = {
title: headerTitle,
description: headerDescription,
networkLabel,
explorerUrl,
ammConfigId,
content
}

return {
viewModel,
ammConfig,
refreshAmmConfig
}
}

export default useAmmConfigCardViewModel
Loading
Loading