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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ WEB_PORT=80
VITE_API_URL=http://localhost/api
VITE_ESPLORA_BASE_URL=https://blockstream.info/liquidtestnet/api
VITE_ESPLORA_EXPLORER_URL=https://blockstream.info/liquidtestnet
VITE_FAUCET_URL=https://liquidtestnet.com/faucet
2 changes: 1 addition & 1 deletion crates/indexer/configuration/base.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ database:
password: "password"
database_name: "lending-indexer"
esplora:
base_url: "https://blockstream.info/liquidtestnet/api"
base_url: "https://liquid.network/liquidtestnet/api"
timeout: 10
indexer:
interval: 10000
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ services:
VITE_API_URL: ${VITE_API_URL}
VITE_ESPLORA_BASE_URL: ${VITE_ESPLORA_BASE_URL}
VITE_ESPLORA_EXPLORER_URL: ${VITE_ESPLORA_EXPLORER_URL}
VITE_FAUCET_URL: ${VITE_FAUCET_URL}
restart: unless-stopped
ports:
- '${WEB_PORT}:80'
Expand Down
3 changes: 3 additions & 0 deletions web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ VITE_ESPLORA_BASE_URL=https://blockstream.info/liquidtestnet/api
# Block explorer base URL for transaction links (optional; default: same as VITE_ESPLORA_BASE_URL)
# Example: https://blockstream.info/liquidtestnet (no /api) for Blockstream Explorer
VITE_ESPLORA_EXPLORER_URL=https://blockstream.info/liquidtestnet

# Liquid testnet faucet (shown on seed entry screen; optional)
VITE_FAUCET_URL=https://liquidtestnet.com/faucet
2 changes: 2 additions & 0 deletions web/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@ WORKDIR /app/web
ARG VITE_API_URL
ARG VITE_ESPLORA_BASE_URL
ARG VITE_ESPLORA_EXPLORER_URL
ARG VITE_FAUCET_URL
ENV VITE_API_URL=$VITE_API_URL
ENV VITE_ESPLORA_BASE_URL=$VITE_ESPLORA_BASE_URL
ENV VITE_ESPLORA_EXPLORER_URL=$VITE_ESPLORA_EXPLORER_URL
ENV VITE_FAUCET_URL=$VITE_FAUCET_URL
RUN npm run build

FROM nginx:1.27-alpine AS runtime
Expand Down
12 changes: 12 additions & 0 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ function Header({
onDisconnect: () => void
showTabs: boolean
}) {
const faucetUrl = import.meta.env.VITE_FAUCET_URL?.trim()

return (
<header className="border-b border-gray-200 bg-white">
<div className="max-w-7xl mx-auto px-8 py-4 flex justify-between items-center">
Expand Down Expand Up @@ -95,6 +97,16 @@ function Header({
>
Utility
</button>
{faucetUrl ? (
<a
href={faucetUrl}
target="_blank"
rel="noopener noreferrer"
className="font-medium text-gray-600 hover:text-gray-900 hover:underline"
>
Faucet
</a>
) : null}
</nav>
<AccountMenu
accountIndex={accountIndex}
Expand Down
109 changes: 95 additions & 14 deletions web/src/SeedGate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@ import { SeedContext } from './SeedContext'
import { parseSeedHex, deriveSecretKeyFromIndex } from './utility/seed'
import { Input } from './components/Input'

function normalizeSeedInput(value: string): string {
return value.trim().toLowerCase().replace(/^0x/, '')
}

function randomSeedHex(): string {
const bytes = new Uint8Array(32)
crypto.getRandomValues(bytes)
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('')
}

type Props = {
seedHex: string | null
setSeedHex: (hex: string) => void
Expand All @@ -13,12 +23,43 @@ type Props = {
export function SeedGate({ seedHex, setSeedHex, accountIndex, children }: Props) {
const [inputValue, setInputValue] = useState('')
const [error, setError] = useState<string | null>(null)
const [displayedGeneratedSeed, setDisplayedGeneratedSeed] = useState<string | null>(null)
const [copyFeedback, setCopyFeedback] = useState(false)
const faucetUrl = import.meta.env.VITE_FAUCET_URL?.trim()

const handleRandomSeed = () => {
const hex = randomSeedHex()
setError(null)
setInputValue(hex)
setDisplayedGeneratedSeed(hex)
setCopyFeedback(false)
}

const handleSeedInputChange = (value: string) => {
setInputValue(value)
const normalized = normalizeSeedInput(value)
setDisplayedGeneratedSeed((prev) =>
prev !== null && normalized === prev ? prev : null
)
setCopyFeedback(false)
}

const handleCopyGeneratedSeed = async () => {
if (!displayedGeneratedSeed) return
try {
await navigator.clipboard.writeText(displayedGeneratedSeed)
setCopyFeedback(true)
window.setTimeout(() => setCopyFeedback(false), 2000)
} catch {
setCopyFeedback(false)
}
}

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
setError(null)
try {
const trimmed = inputValue.trim().toLowerCase().replace(/^0x/, '')
const trimmed = normalizeSeedInput(inputValue)
parseSeedHex(trimmed)
setSeedHex(trimmed)
} catch (err) {
Expand All @@ -31,21 +72,61 @@ export function SeedGate({ seedHex, setSeedHex, accountIndex, children }: Props)
<div className="w-full flex-1 flex items-center justify-center">
<div className="w-full max-w-7xl px-8 flex flex-col items-center text-center">
<p className="text-gray-600 mb-6">Demo signer: enter SEED_HEX (32 bytes, 64 hex chars)</p>
<form onSubmit={handleSubmit} className="flex gap-3 mb-4 w-full max-w-md justify-center">
<Input
type="password"
inputMode="text"
autoComplete="off"
placeholder="SEED_HEX"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
className="flex-1 min-w-0 font-mono"
/>
<button type="submit">Continue</button>
</form>
<div className="mb-4 flex w-full max-w-md flex-col gap-3">
<form onSubmit={handleSubmit} className="flex flex-wrap gap-3 justify-center">
<Input
type="password"
inputMode="text"
autoComplete="off"
placeholder="SEED_HEX"
value={inputValue}
onChange={(e) => handleSeedInputChange(e.target.value)}
className="min-w-0 flex-1 font-mono"
/>
<button type="submit">Continue</button>
</form>
<button
type="button"
onClick={handleRandomSeed}
className="rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-800 hover:bg-gray-50"
>
Generate random seed
</button>
</div>
{displayedGeneratedSeed ? (
<div className="mb-4 w-full max-w-md rounded-xl border border-gray-200 bg-gray-50 p-4 text-left shadow-sm">
<p className="mb-2 text-xs font-medium uppercase tracking-wide text-gray-500">
Generated seed (copy and keep private)
</p>
<code className="mb-3 block break-all font-mono text-sm leading-relaxed text-gray-900">
{displayedGeneratedSeed}
</code>
<button
type="button"
onClick={() => void handleCopyGeneratedSeed()}
className="rounded-lg bg-[#5F3DC4] px-3 py-1.5 text-sm font-semibold text-white hover:bg-[#4f36a8]"
>
{copyFeedback ? 'Copied' : 'Copy'}
</button>
</div>
) : null}
{faucetUrl ? (
<p className="mb-4 text-sm text-gray-600">
Need test coins?{' '}
<a
href={faucetUrl}
target="_blank"
rel="noopener noreferrer"
className="font-medium text-indigo-600 underline hover:text-indigo-800"
>
Open Liquid testnet faucet
</a>
</p>
) : null}
{error && <p className="text-red-700 bg-red-50 p-4 rounded-lg">{error}</p>}
<p className="text-gray-500 text-sm mt-4 max-w-md">
Only for testing. No real wallet yet. Seed is not persisted.
Only for testing. No real wallet yet. Seed is stored in your browser (localStorage) for this
demo.
</p>
</div>
</div>
Expand Down
68 changes: 37 additions & 31 deletions web/src/pages/Dashboard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,8 @@ export function Dashboard({

return (
<div className="space-y-8">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div className="rounded-2xl border border-gray-200 bg-white p-8 shadow-sm">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 md:items-stretch">
<div className="flex h-full min-h-0 flex-col rounded-2xl border border-gray-200 bg-white p-8 shadow-sm">
<div className="mb-5 flex items-center gap-3">
<span className="flex h-9 w-9 items-center justify-center rounded-full bg-indigo-50 text-sm font-semibold text-indigo-700">
B
Expand All @@ -216,39 +216,42 @@ export function Dashboard({
Your Borrows
</h2>
</div>
<div className="mb-7 space-y-4">
<div className="rounded-xl border border-gray-100 bg-gray-50/70 px-4 py-3">
<p className="text-xs uppercase tracking-wide text-gray-500">LBTC Locked</p>
<StatValue
loading={statsLoading}
value={`${formatBigint(borrowStats.lockedLbtc)} sats`}
emphasize
/>
</div>
<div className="rounded-xl border border-gray-100 bg-gray-50/70 px-4 py-3">
<p className="text-xs uppercase tracking-wide text-gray-500">Active Deals</p>
<StatValue loading={statsLoading} value={borrowStats.activeDeals} />
</div>
<div className="rounded-xl border border-gray-100 bg-gray-50/70 px-4 py-3">
<p className="text-xs uppercase tracking-wide text-gray-500">Pending Deals</p>
<StatValue loading={statsLoading} value={borrowStats.pendingDeals} />
<div className="flex min-h-0 flex-1 flex-col">
<div className="space-y-4">
<div className="rounded-xl border border-gray-100 bg-gray-50/70 px-4 py-3">
<p className="text-xs uppercase tracking-wide text-gray-500">LBTC Locked</p>
<StatValue
loading={statsLoading}
value={`${formatBigint(borrowStats.lockedLbtc)} sats`}
emphasize
/>
</div>
<div className="rounded-xl border border-gray-100 bg-gray-50/70 px-4 py-3">
<p className="text-xs uppercase tracking-wide text-gray-500">Active Deals</p>
<StatValue loading={statsLoading} value={borrowStats.activeDeals} />
</div>
<div className="rounded-xl border border-gray-100 bg-gray-50/70 px-4 py-3">
<p className="text-xs uppercase tracking-wide text-gray-500">Pending Deals</p>
<StatValue loading={statsLoading} value={borrowStats.pendingDeals} />
</div>
</div>
<div className="min-h-0 flex-1" aria-hidden="true" />
</div>
{statsError && (
<p className="mb-4 rounded-lg border border-red-100 bg-red-50 px-3 py-2 text-xs text-red-700">
<p className="mt-4 mb-4 rounded-lg border border-red-100 bg-red-50 px-3 py-2 text-xs text-red-700">
{statsError}
</p>
)}
<button
type="button"
onClick={() => onTab('borrower')}
className="w-full rounded-xl bg-[#5F3DC4] px-4 py-3 text-sm font-semibold text-white transition-all duration-150 hover:bg-[#4f36a8] hover:shadow-sm"
className="mt-4 w-full rounded-xl bg-[#5F3DC4] px-4 py-3 text-sm font-semibold text-white transition-all duration-150 hover:bg-[#4f36a8] hover:shadow-sm"
>
Borrow
</button>
</div>

<div className="rounded-2xl border border-gray-200 bg-white p-8 shadow-sm">
<div className="flex h-full min-h-0 flex-col rounded-2xl border border-gray-200 bg-white p-8 shadow-sm">
<div className="mb-5 flex items-center gap-3">
<span className="flex h-9 w-9 items-center justify-center rounded-full bg-indigo-50 text-sm font-semibold text-indigo-700">
S
Expand All @@ -257,25 +260,28 @@ export function Dashboard({
Your Supply
</h2>
</div>
<div className="mb-7 space-y-4">
<div className="rounded-xl border border-gray-100 bg-gray-50/70 px-4 py-3">
<p className="text-xs uppercase tracking-wide text-gray-500">Active Offers</p>
<StatValue loading={statsLoading} value={supplyStats.activeOffers} emphasize />
</div>
<div className="rounded-xl border border-gray-100 bg-gray-50/70 px-4 py-3">
<p className="text-xs uppercase tracking-wide text-gray-500">Expecting Liquidation</p>
<StatValue loading={statsLoading} value={supplyStats.waitingLiquidation} />
<div className="flex min-h-0 flex-1 flex-col">
<div className="space-y-4">
<div className="rounded-xl border border-gray-100 bg-gray-50/70 px-4 py-3">
<p className="text-xs uppercase tracking-wide text-gray-500">Active Offers</p>
<StatValue loading={statsLoading} value={supplyStats.activeOffers} emphasize />
</div>
<div className="rounded-xl border border-gray-100 bg-gray-50/70 px-4 py-3">
<p className="text-xs uppercase tracking-wide text-gray-500">Expecting Liquidation</p>
<StatValue loading={statsLoading} value={supplyStats.waitingLiquidation} />
</div>
</div>
<div className="min-h-0 flex-1" aria-hidden="true" />
</div>
{statsError && (
<p className="mb-4 rounded-lg border border-red-100 bg-red-50 px-3 py-2 text-xs text-red-700">
<p className="mt-4 mb-4 rounded-lg border border-red-100 bg-red-50 px-3 py-2 text-xs text-red-700">
{statsError}
</p>
)}
<button
type="button"
onClick={() => onTab('lender')}
className="w-full rounded-xl bg-[#5F3DC4] px-4 py-3 text-sm font-semibold text-white transition-all duration-150 hover:bg-[#4f36a8] hover:shadow-sm"
className="mt-4 w-full rounded-xl bg-[#5F3DC4] px-4 py-3 text-sm font-semibold text-white transition-all duration-150 hover:bg-[#4f36a8] hover:shadow-sm"
>
Supply
</button>
Expand Down
4 changes: 4 additions & 0 deletions web/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
/// <reference types="vite/client" />

interface ImportMetaEnv {
readonly VITE_FAUCET_URL?: string
}

declare module 'virtual:simplicity-sources' {
export const sources: Record<string, string>
}
Loading