Skip to content

Commit 9e41e5e

Browse files
authored
Merge pull request #19 from LittleFingerrr/feat/minimal-test-frontend
feat: added minimal frontend
2 parents c88b162 + 88ad9a3 commit 9e41e5e

38 files changed

Lines changed: 5679 additions & 178 deletions

package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"type": "git",
88
"url": "https://github.com/OWK50GA/Griffin"
99
},
10-
"workspaces":[
10+
"workspaces": [
1111
"packages/*"
1212
],
1313
"scripts": {
@@ -20,7 +20,10 @@
2020
"license": "MIT",
2121
"packageManager": "pnpm@10.24.0",
2222
"devDependencies": {
23-
"typescript": "^5.9.0",
24-
"tsup": "^8.5.0"
23+
"tsup": "^8.5.0",
24+
"typescript": "^5.9.0"
25+
},
26+
"dependencies": {
27+
"dotenv": "^17.4.2"
2528
}
2629
}

packages/app/.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
VITE_GRIFFIN_API_URL=http://localhost:3000

packages/app/index.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Griffin — Cross-chain Payments</title>
7+
</head>
8+
<body>
9+
<div id="root"></div>
10+
<script type="module" src="/src/main.tsx"></script>
11+
</body>
12+
</html>

packages/app/package.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "@griffin/app",
3+
"version": "0.1.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"dev": "vite",
8+
"build": "tsc && vite build",
9+
"preview": "vite preview"
10+
},
11+
"dependencies": {
12+
"react": "^18.3.1",
13+
"react-dom": "^18.3.1",
14+
"wagmi": "^2.14.16",
15+
"viem": "^2.23.10",
16+
"@tanstack/react-query": "^5.64.2"
17+
},
18+
"devDependencies": {
19+
"@vitejs/plugin-react": "^4.3.4",
20+
"@types/react": "^18.3.18",
21+
"@types/react-dom": "^18.3.5",
22+
"typescript": "^5.7.3",
23+
"vite": "^6.1.0"
24+
}
25+
}

packages/app/src/App.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { WagmiProvider, createConfig, http } from "wagmi";
2+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
3+
import { HASHKEY_TESTNET } from "./config";
4+
import { WalletButton } from "./components/WalletButton";
5+
import { SwapForm } from "./components/SwapForm";
6+
import "./index.css";
7+
8+
const wagmiConfig = createConfig({
9+
chains: [HASHKEY_TESTNET],
10+
transports: { [HASHKEY_TESTNET.id]: http() },
11+
});
12+
13+
const queryClient = new QueryClient();
14+
15+
export default function App() {
16+
return (
17+
<WagmiProvider config={wagmiConfig}>
18+
<QueryClientProvider client={queryClient}>
19+
<div className="app">
20+
<header>
21+
<h1>Griffin</h1>
22+
<p className="tagline">Cross-token payments, simplified</p>
23+
<WalletButton />
24+
</header>
25+
<main>
26+
<SwapForm />
27+
</main>
28+
</div>
29+
</QueryClientProvider>
30+
</WagmiProvider>
31+
);
32+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { IntentState } from "../hooks/useIntent";
2+
3+
export function IntentStatus({ state, onReset }: { state: IntentState; onReset: () => void }) {
4+
const success = state.status === "completed";
5+
return (
6+
<div className={`intent-result ${success ? "success" : "failure"}`}>
7+
<div className="result-icon">{success ? "✅" : "❌"}</div>
8+
<h3>{success ? "Payment sent!" : "Payment failed"}</h3>
9+
{state.intentId && <p className="intent-id">Intent: {state.intentId}</p>}
10+
{state.error && <p className="error-msg">{state.error}</p>}
11+
<button onClick={onReset}>New payment</button>
12+
</div>
13+
);
14+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { useState } from "react";
2+
import { useAccount, useSignTypedData } from "wagmi";
3+
import { TOKENS } from "../config";
4+
import { useQuote } from "../hooks/useQuote";
5+
import { useIntent } from "../hooks/useIntent";
6+
import { IntentStatus } from "./IntentStatus";
7+
8+
export function SwapForm() {
9+
const { address, isConnected } = useAccount();
10+
const { signTypedDataAsync } = useSignTypedData();
11+
12+
const [amount, setAmount] = useState("");
13+
const [recipient, setRecipient] = useState("");
14+
15+
const fromToken = TOKENS.tHSK.address;
16+
const toToken = TOKENS.tUSDC.address;
17+
18+
const { quote, loading: quoteLoading } = useQuote(fromToken, toToken, amount);
19+
const { state, submit, reset } = useIntent();
20+
21+
const isActive = state.status !== "idle" && state.status !== "completed" && state.status !== "failed";
22+
23+
async function handleSubmit(e: React.FormEvent) {
24+
e.preventDefault();
25+
if (!address) return;
26+
await submit({
27+
fromToken,
28+
toToken,
29+
amount,
30+
recipient,
31+
userAddress: address,
32+
signTypedData: (args) => signTypedDataAsync(args as any),
33+
});
34+
}
35+
36+
if (state.status === "completed" || state.status === "failed") {
37+
return <IntentStatus state={state} onReset={reset} />;
38+
}
39+
40+
return (
41+
<form className="swap-form" onSubmit={handleSubmit}>
42+
<h2>Send Payment</h2>
43+
44+
<div className="token-row">
45+
<span className="token-label">You send</span>
46+
<span className="token-badge">{TOKENS.tHSK.symbol}</span>
47+
</div>
48+
49+
<div className="field">
50+
<label>Amount</label>
51+
<input
52+
type="number"
53+
placeholder="0.0"
54+
value={amount}
55+
onChange={e => setAmount(e.target.value)}
56+
min="0"
57+
step="any"
58+
required
59+
/>
60+
</div>
61+
62+
<div className="token-row">
63+
<span className="token-label">Recipient receives</span>
64+
<span className="token-badge">{TOKENS.tUSDC.symbol}</span>
65+
</div>
66+
67+
{amount && !quoteLoading && quote?.bestRoute && (
68+
<div className="quote-preview">
69+
{quote.bestRoute.steps[0]?.estimatedOutput} tUSDC
70+
</div>
71+
)}
72+
73+
<div className="field">
74+
<label>Recipient address</label>
75+
<input
76+
type="text"
77+
placeholder="0x..."
78+
value={recipient}
79+
onChange={e => setRecipient(e.target.value)}
80+
required
81+
/>
82+
</div>
83+
84+
{isActive && (
85+
<div className="status-inline">
86+
{state.status === "signing" && "⏳ Waiting for signature..."}
87+
{state.status === "submitting" && "⏳ Submitting intent..."}
88+
{state.status === "polling" && "⏳ Executing swap..."}
89+
</div>
90+
)}
91+
92+
{state.error && <div className="error">{state.error}</div>}
93+
94+
<button type="submit" disabled={!isConnected || isActive}>
95+
{!isConnected ? "Connect wallet first" : isActive ? "Processing..." : "Send"}
96+
</button>
97+
</form>
98+
);
99+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { useAccount, useConnect, useDisconnect } from "wagmi";
2+
import { injected } from "wagmi/connectors";
3+
4+
export function WalletButton() {
5+
const { address, isConnected } = useAccount();
6+
const { connect } = useConnect();
7+
const { disconnect } = useDisconnect();
8+
9+
if (isConnected) {
10+
return (
11+
<div className="wallet-connected">
12+
<span>{address?.slice(0, 6)}...{address?.slice(-4)}</span>
13+
<button onClick={() => disconnect()}>Disconnect</button>
14+
</div>
15+
);
16+
}
17+
18+
return (
19+
<button className="wallet-connect-btn" onClick={() => connect({ connector: injected() })}>
20+
Connect Wallet
21+
</button>
22+
);
23+
}

packages/app/src/config.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export const GRIFFIN_API_URL = import.meta.env.VITE_GRIFFIN_API_URL || "http://localhost:3000";
2+
3+
export const HASHKEY_TESTNET = {
4+
id: 133,
5+
name: "HashKey Testnet",
6+
nativeCurrency: { name: "HSK", symbol: "HSK", decimals: 18 },
7+
rpcUrls: { default: { http: ["https://testnet.hsk.xyz"] } },
8+
} as const;
9+
10+
export const TOKENS = {
11+
tHSK: { address: "0xb8F355f10569FD2A765296161d082Cc37c5843c2", symbol: "tHSK", decimals: 18 },
12+
tUSDC: { address: "0xc4C2841367016C9e2652Fecc49bBA9229787bA82", symbol: "tUSDC", decimals: 6 },
13+
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { useMemo } from "react";
2+
import { GriffinClient } from "@griffin/sdk";
3+
import { GRIFFIN_API_URL } from "../config";
4+
5+
export function useGriffinClient() {
6+
return useMemo(
7+
() => new GriffinClient({ baseUrl: GRIFFIN_API_URL, timeoutMs: 60_000 }),
8+
[],
9+
);
10+
}

0 commit comments

Comments
 (0)