Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
15941a2
Implement connectWallet logic and sophisticated UI for Stellar integr…
kryputh Apr 24, 2026
f37cb9a
Fix linting errors and undefined variables to resolve CI failures
kryputh Apr 24, 2026
8ec420c
Merge branch 'main' into wallet-integration-98
kryputh Apr 24, 2026
4b75bd6
fix: resolve build and type errors in wallet integration
kryputh Apr 24, 2026
c7942a0
Merge branch 'main' into wallet-integration-98
kryputh Apr 24, 2026
e6d1ed7
Merge branch 'main' into wallet-integration-98
kryputh Apr 25, 2026
491b19c
fix: resolve wallet integration ci failures
kryputh Apr 25, 2026
8edb8c0
Merge branch 'main' into wallet-integration-98
soomtochukwu Apr 25, 2026
66ad1a0
Resolve PR 238 merge conflicts and CI failures
kryputh May 1, 2026
c7b7216
Fix wallet hydration lint error
kryputh May 1, 2026
b4338d8
Fix transaction details ledger type narrowing
kryputh May 1, 2026
da1e9a1
Fix transaction signing type import
kryputh May 1, 2026
bdda760
Pass Soroban RPC to transaction simulator
kryputh May 1, 2026
19e70e2
Normalize Soroban builder base fee
kryputh May 1, 2026
56a35ff
Fix transaction XDR serialization call
kryputh May 1, 2026
27d911d
Fix Horizon fallback response typing
kryputh May 1, 2026
94d3417
Fix Horizon fallback error typing
kryputh May 1, 2026
3d65513
Fix Horizon not found handling type
kryputh May 1, 2026
9c0f14a
Normalize Soroban polling timestamps
kryputh May 1, 2026
020d127
Fix simulator response narrowing
kryputh May 1, 2026
34794fd
Map not found polling status in submitter
kryputh May 1, 2026
7523362
Fix transaction rebuild operation typing
kryputh May 1, 2026
91ba518
Fix transaction time bounds property
kryputh May 1, 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
62 changes: 31 additions & 31 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,34 +75,34 @@ jobs:
run: npm run build --prefix apps/web

playwright-e2e:
name: E2E Tests (Playwright)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Install dependencies
run: npm install
env:
npm_config_fetch_timeout: 120000
npm_config_fetch_retry_mintimeout: 20000
npm_config_fetch_retry_maxtimeout: 120000
npm_config_fetch_retries: 5
- name: Install web dependencies
run: npm install --prefix apps/web
env:
npm_config_fetch_timeout: 120000
npm_config_fetch_retry_mintimeout: 20000
npm_config_fetch_retry_maxtimeout: 120000
npm_config_fetch_retries: 5
- name: Install Playwright Browsers
run: npx playwright install --with-deps chromium
- name: Run E2E tests
run: npm run test:e2e
env:
NEXT_PUBLIC_E2E: "true"
NEXT_PUBLIC_API_URL: "http://localhost:3001"
name: E2E Tests (Playwright)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Install dependencies
run: npm install
env:
npm_config_fetch_timeout: 120000
npm_config_fetch_retry_mintimeout: 20000
npm_config_fetch_retry_maxtimeout: 120000
npm_config_fetch_retries: 5
- name: Install web dependencies
run: npm install --prefix apps/web
env:
npm_config_fetch_timeout: 120000
npm_config_fetch_retry_mintimeout: 20000
npm_config_fetch_retry_maxtimeout: 120000
npm_config_fetch_retries: 5
- name: Install Playwright Browsers
run: npx playwright install --with-deps chromium
- name: Run E2E tests
run: npm run test:e2e
env:
NEXT_PUBLIC_E2E: "true"
NEXT_PUBLIC_API_URL: "http://localhost:3001"
137 changes: 85 additions & 52 deletions apps/web/app/globals.css
Original file line number Diff line number Diff line change
@@ -1,53 +1,84 @@
@import "tailwindcss";

@theme inline {
--color-background: #09090b; /* zinc-950 */
--color-foreground: #fafafa; /* zinc-50 */

--color-primary: #6366f1; /* indigo-500 */
--color-primary-foreground: #ffffff;

--color-muted: #18181b; /* zinc-900 */
--color-muted-foreground: #a1a1aa; /* zinc-400 */

--color-border: #27272a; /* zinc-800 */
--color-input: #27272a;

--radius-xl: 12px;
--radius-lg: 10px;
--radius-md: 8px;
--radius-sm: 4px;

--font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
--font-mono: 'Geist Mono', ui-monospace, monospace;
:root {
--background: #f4f4f5;
--foreground: #18181b;
--card: #ffffff;
--card-foreground: #18181b;
--muted: #e4e4e7;
--muted-foreground: #3f3f46;
--border: #d4d4d8;
--input: #d4d4d8;
--primary: #27272a;
--primary-foreground: #fafafa;
--accent: #e4e4e7;
--accent-foreground: #18181b;
--ring: #27272a;
--success: #22c55e;
--warning: #f59e0b;
--font-geist-sans:
Inter, "Segoe UI", "Helvetica Neue", Arial, ui-sans-serif, system-ui,
sans-serif;
--font-geist-mono:
"SFMono-Regular", "Cascadia Code", "Fira Code", Consolas, ui-monospace,
monospace;
}

:root {
--background: #09090b;
.dark {
--background: #18181b;
--foreground: #fafafa;
--primary: #6366f1;
--border: #27272a;
--muted: #18181b;
--card: #27272a;
--card-foreground: #fafafa;
--muted: #3f3f46;
--muted-foreground: #a1a1aa;
--border: #3f3f46;
--input: #3f3f46;
--primary: #6366f1;
--primary-foreground: #fafafa;
--accent: #27272a;
--accent-foreground: #fafafa;
--ring: #6366f1;
--success: #22c55e;
--warning: #f59e0b;
--zinc-900: #18181b;
--zinc-800: #27272a;
--zinc-700: #3f3f46;
--zinc-600: #52525b;
--indigo-500: #6366f1;
--indigo-400: #818cf8;
--indigo-300: #a5b4fc;
}

body {
background-color: var(--background);
color: var(--foreground);
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-ring: var(--ring);
--color-success: var(--success);
--color-warning: var(--warning);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);

/* Glass effect for shells and cards */
.glass-surface {
background: rgba(24, 24, 27, 0.6);
backdrop-filter: blur(12px);
border: 1px solid rgba(39, 39, 42, 0.5);
--radius-xl: 12px;
--radius-lg: 10px;
--radius-md: 8px;
--radius-sm: 4px;
}

.transition-smooth {
transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
body {
background: var(--background);
color: var(--foreground);
font-family: var(--font-geist-sans), sans-serif;
@apply antialiased selection:bg-indigo-500/30;
}

button,
Expand All @@ -56,7 +87,8 @@ input,
textarea,
select {
transition-duration: 200ms;
transition-property: color, background-color, border-color, box-shadow, opacity, transform;
transition-property: color, background-color, border-color, box-shadow,
opacity, transform;
transition-timing-function: ease;
}

Expand All @@ -65,37 +97,43 @@ select {
outline-offset: 2px;
}

/* Responsive design utilities */
.glass-surface {
background: color-mix(in srgb, var(--card) 82%, transparent);
backdrop-filter: blur(14px);
}

.transition-smooth {
transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
}

@media (max-width: 640px) {
.responsive-text-xs {
font-size: 0.625rem;
line-height: 0.75rem;
}

.responsive-text-sm {
font-size: 0.75rem;
line-height: 1rem;
}

.responsive-gap {
gap: 0.5rem;
}
}

/* High contrast mode support */
@media (prefers-contrast: high) {
:root {
--border: #000000;
--ring: #ffffff;
}

.dark {
--border: #ffffff;
--ring: #000000;
}
}

/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
button,
a,
Expand All @@ -104,18 +142,13 @@ select {
select {
transition: none;
}

.animate-spin,
.animate-pulse {
animation: none;
}
}

.glass-surface {
background: color-mix(in srgb, var(--card) 82%, transparent);
backdrop-filter: blur(14px);
}

@keyframes shimmer {
0% {
background-position: 200% 0;
Expand Down
125 changes: 125 additions & 0 deletions apps/web/components/auth/wallet-connect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"use client";

import { useSyncExternalStore } from "react";
import { Button } from "@/components/ui/button";
import { useWalletStore } from "@/lib/store/use-wallet-store";
import { connectWallet, getWalletsKit, signAuthMessage } from "@/lib/stellar";
import { Loader2, Wallet, LogOut, AlertCircle, ChevronRight, User } from "lucide-react";
import { toast } from "sonner";
import { cn } from "@/lib/utils";

export function WalletConnect() {
const {
publicKey,
isConnected,
isConnecting,
setConnecting,
setConnection,
disconnect,
setError,
error
} = useWalletStore();

const isHydrated = useSyncExternalStore(
() => () => {},
() => true,
() => false,
);

const handleConnect = async () => {
setConnecting(true);
setError(null);

try {
const address = await connectWallet();

// SIWS Flow (Sign-In With Stellar)
// 1. Generate nonce/message (mocked here, usually from backend)
const message = `Sign in to Lance\n\nDomain: lance.so\nAddress: ${address}\nNonce: ${Math.random().toString(36).substring(2)}`;

// 2. Sign message
await signAuthMessage(message);

// 3. Check Network
const kit = getWalletsKit();
const connectedNetwork = await kit.getNetwork();
const appNetwork = (process.env.NEXT_PUBLIC_STELLAR_NETWORK as string) ?? "TESTNET";

if (connectedNetwork.network.toUpperCase() !== appNetwork.toUpperCase()) {
toast.warning(`Network mismatch: Wallet is on ${connectedNetwork.network}, but app is on ${appNetwork}`);
}

const walletId = kit.selectedWalletId || "freighter";
setConnection(address, walletId);
toast.success("Wallet connected successfully");
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : "Failed to connect wallet";
setError(errorMessage);
toast.error(errorMessage);
} finally {
setConnecting(false);
}
};

if (!isHydrated) return null;

if (isConnected && publicKey) {
return (
<div className="flex items-center gap-2 animate-in fade-in duration-300">
<div className="flex items-center gap-3 rounded-full border border-zinc-800 bg-zinc-900/50 px-3 py-1.5 ring-1 ring-white/5 backdrop-blur-md">
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-indigo-500/10 text-indigo-400">
<User className="h-3.5 w-3.5" />
</div>
<span className="text-sm font-medium text-zinc-200 font-mono">
{publicKey.slice(0, 4)}...{publicKey.slice(-4)}
</span>
<div className="h-3 w-[1px] bg-zinc-800" />
<Button
variant="ghost"
size="sm"
onClick={() => disconnect()}
aria-label="Disconnect wallet"
className="h-7 rounded-full px-2 text-xs text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100 transition-all duration-200"
>
<LogOut className="mr-1.5 h-3 w-3" />
Disconnect
</Button>
</div>
</div>
);
}

return (
<div className="flex flex-col gap-2">
<Button
onClick={handleConnect}
disabled={isConnecting}
aria-label="Connect Stellar wallet"
className={cn(
"relative h-11 min-w-[160px] overflow-hidden rounded-xl border-none bg-indigo-600 px-6 font-medium text-white transition-all duration-200 hover:bg-indigo-500 hover:shadow-[0_0_20px_rgba(99,102,241,0.3)] disabled:opacity-70",
"after:absolute after:inset-0 after:bg-gradient-to-r after:from-white/0 after:via-white/10 after:to-white/0 after:translate-x-[-100%] hover:after:translate-x-[100%] after:transition-transform after:duration-1000"
)}
>
{isConnecting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Connecting...
</>
) : (
<>
<Wallet className="mr-2 h-4 w-4" />
Connect Wallet
<ChevronRight className="ml-2 h-4 w-4 opacity-50" />
</>
)}
</Button>

{error && (
<div className="flex items-center gap-2 px-2 text-[11px] font-medium text-red-400 animate-in slide-in-from-top-1 duration-200">
<AlertCircle className="h-3 w-3" />
{error}
</div>
)}
</div>
);
}
Loading
Loading