Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
b358a73
feat(codegen): add Soroban contract spec format and codegen script
Jul 21, 2026
ab69678
feat(codegen): generate type-safe contract bindings from specs
Jul 21, 2026
ec462a8
feat(codegen): integrate codegen into build and dev scripts
Jul 21, 2026
4aa65f2
chore: exclude auto-generated contract bindings from version control
Jul 21, 2026
7558434
feat(hooks): add type-safe contract interaction hooks
Jul 21, 2026
b2effd7
feat(form-pledge): integrate typed escrow contract for deposits
Jul 21, 2026
3f55480
feat(deposits): integrate typed escrow contract for balance fetching
Jul 21, 2026
6672fec
feat(types): add typed event data interfaces for contract events
Jul 21, 2026
806ae1d
feat(dispute-event-feed): display typed event data in feed
Jul 21, 2026
d996e17
docs: document contract bindings codegen in README
Jul 21, 2026
431d8bc
fix(codegen): fix TypeScript errors in generated bindings
Jul 21, 2026
fef478d
feat(codegen): add spec validation and improve event type naming
Jul 22, 2026
1053535
ci: add codegen validation step to CI pipeline
Jul 22, 2026
637d223
feat(dashboard): add contracts status page with type-safe contract in…
Jul 22, 2026
808f096
feat(dashboard): add Contracts link to sidebar navigation
Jul 22, 2026
81fe6b2
test(codegen): add test suite for contract bindings codegen
Jul 22, 2026
bf5eef4
docs: update README with contracts page and codegen validation
Jul 22, 2026
91715f6
Merge branch 'main' into rhem-grantfox
rhemy-arc Jul 23, 2026
2bd6a13
fix: add missing codegen:validate script to package.json
Jul 23, 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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ jobs:
if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: npm ci

- name: Verify contract bindings are up-to-date
run: npm run codegen:validate

- name: Run linter
run: npm run lint

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ TrustFlow Frontend is the Next.js web application that gives users a seamless in
- **`/`** - Landing page with feature showcase
- **`/dashboard`** - User dashboard (My Gigs)
- **`/dashboard/disputes`** - Active disputes
- **`/dashboard/contracts`** - Contract bindings status and interaction
- **`/dashboard/profile`** - User profile and reputation
- **`/dashboard/settings`** - Account settings
- **`/explore`** - Browse available gigs
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"bindings:abundance": "./target/bin/soroban contract bindings typescript --wasm ./target/wasm32-unknown-unknown/release/abundance_token.wasm --id $(cat ./.soroban-example-dapp/abundance_token_id) --output-dir ./.soroban-example-dapp/abundance-token --network $(cat ./.soroban-example-dapp/network) --overwrite",
"bindings": "npm run bindings:crowdfund && npm run bindings:abundance",
"codegen": "node scripts/generate-contract-bindings.js",
"codegen:validate": "npm run codegen && git diff --exit-code shared/contracts-gen/",
"convert:assets": "node scripts/convert-to-webp.js"
},
"dependencies": {
Expand Down
1 change: 1 addition & 0 deletions pages/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ interface NavItem {
const NAV_ITEMS: NavItem[] = [
{ label: 'My Gigs', href: '/dashboard', icon: '💼', description: 'View and manage your active gigs' },
{ label: 'Disputes', href: '/dashboard/disputes', icon: '⚖️', description: 'Active dispute resolutions' },
{ label: 'Contracts', href: '/dashboard/contracts', icon: '🔗', description: 'Contract bindings and interaction' },
{ label: 'Profile', href: '/dashboard/profile', icon: '👤', description: 'Your reputation and work history' },
{ label: 'Settings', href: '/dashboard/settings', icon: '⚙️', description: 'Account and preferences' },
]
Expand Down
267 changes: 267 additions & 0 deletions pages/dashboard/contracts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
import { useState } from 'react'
import Head from 'next/head'
import { useEscrowContract, type Milestone } from '../../hooks/useEscrowContract'
import { useDisputeContract, type Dispute } from '../../hooks/useDisputeContract'
import { ESCROW_CONTRACT_ID, DISPUTE_CONTRACT_ID } from '../../shared/contracts'

function StatusBadge({ ready, label }: { ready: boolean; label: string }) {
return (
<span
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${
ready
? 'bg-emerald-50 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400'
: 'bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-400'
}`}
>
<span
className={`w-1.5 h-1.5 rounded-full ${ready ? 'bg-emerald-500' : 'bg-gray-400'}`}
/>
{label}
</span>
)
}

function ContractCard({
name,
contractId,
ready,
children,
}: {
name: string
contractId: string
ready: boolean
children: React.ReactNode
}) {
return (
<div className="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-800 p-6 shadow-sm">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">{name}</h3>
<StatusBadge ready={ready} label={ready ? 'Connected' : 'Not configured'} />
</div>
<div className="mb-4">
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Contract ID</p>
<code className="block text-sm font-mono text-gray-700 dark:text-gray-300 bg-gray-50 dark:bg-gray-800 px-3 py-2 rounded-lg break-all">
{contractId || 'Not set'}
</code>
</div>
{children}
</div>
)
}

function MilestoneTable({ milestones }: { milestones: Milestone[] }) {
if (milestones.length === 0) {
return <p className="text-sm text-gray-500 dark:text-gray-400">No milestones found.</p>
}

return (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-200 dark:border-gray-700">
<th className="text-left py-2 text-gray-500 dark:text-gray-400 font-medium">#</th>
<th className="text-left py-2 text-gray-500 dark:text-gray-400 font-medium">Status</th>
<th className="text-left py-2 text-gray-500 dark:text-gray-400 font-medium">Amount</th>
<th className="text-left py-2 text-gray-500 dark:text-gray-400 font-medium">Freelancer</th>
</tr>
</thead>
<tbody>
{milestones.map((m) => (
<tr key={m.index} className="border-b border-gray-100 dark:border-gray-800">
<td className="py-2 text-gray-900 dark:text-white">{m.index}</td>
<td className="py-2">
<span className="inline-block px-2 py-0.5 rounded text-xs font-medium bg-blue-50 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400">
{m.status}
</span>
</td>
<td className="py-2 text-gray-700 dark:text-gray-300 font-mono">{m.amount.toString()}</td>
<td className="py-2 text-gray-500 dark:text-gray-400 font-mono text-xs break-all">
{m.freelancer.slice(0, 8)}...{m.freelancer.slice(-4)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)
}

function DisputeDetail({ dispute }: { dispute: Dispute }) {
return (
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-500 dark:text-gray-400">Status</span>
<span className="font-medium text-gray-900 dark:text-white">{dispute.status}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500 dark:text-gray-400">Votes For</span>
<span className="font-medium text-emerald-600 dark:text-emerald-400">{dispute.votes_for}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500 dark:text-gray-400">Votes Against</span>
<span className="font-medium text-red-600 dark:text-red-400">{dispute.votes_against}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500 dark:text-gray-400">Reason</span>
<span className="font-medium text-gray-900 dark:text-white text-right max-w-[60%]">{dispute.reason}</span>
</div>
</div>
)
}

export default function ContractsPage() {
const escrow = useEscrowContract()
const dispute = useDisputeContract()
const [gigIdInput, setGigIdInput] = useState('')
const [milestoneIndex, setMilestoneIndex] = useState('0')
const [disputeIdInput, setDisputeIdInput] = useState('')
const [milestones, setMilestones] = useState<Milestone[] | null>(null)
const [disputeDetail, setDisputeDetail] = useState<Dispute | null>(null)
const [loading, setLoading] = useState(false)

const handleFetchMilestones = async () => {
if (!gigIdInput) return
setLoading(true)
try {
const gigId = Buffer.from(gigIdInput, 'hex')
const result = await escrow.getMilestones(gigId)
setMilestones(result)
} catch {
setMilestones(null)
} finally {
setLoading(false)
}
}

const handleFetchDispute = async () => {
if (!disputeIdInput) return
setLoading(true)
try {
const disputeId = Buffer.from(disputeIdInput, 'hex')
const result = await dispute.getDispute(disputeId)
setDisputeDetail(result)
} catch {
setDisputeDetail(null)
} finally {
setLoading(false)
}
}

return (
<>
<Head>
<title>Contracts | TrustFlow Dashboard</title>
</Head>

<div className="max-w-5xl mx-auto px-4 py-8">
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">Contract Bindings</h1>
<p className="text-gray-600 dark:text-gray-400">
Type-safe interfaces for interacting with TrustFlow Soroban contracts.
Bindings are auto-generated from contract specs and compile-time checked.
</p>
</div>

<div className="grid gap-6 md:grid-cols-2">
<ContractCard
name="Escrow Contract"
contractId={ESCROW_CONTRACT_ID}
ready={escrow.isReady}
>
<div className="space-y-3">
<div>
<label className="block text-xs text-gray-500 dark:text-gray-400 mb-1">
Gig ID (hex)
</label>
<input
type="text"
value={gigIdInput}
onChange={(e) => setGigIdInput(e.target.value)}
placeholder="e.g. 0x1a2b3c..."
className="w-full text-sm font-mono px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-xs text-gray-500 dark:text-gray-400 mb-1">
Milestone Index
</label>
<input
type="number"
value={milestoneIndex}
onChange={(e) => setMilestoneIndex(e.target.value)}
min="0"
className="w-full text-sm px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<button
onClick={handleFetchMilestones}
disabled={!escrow.isReady || !gigIdInput || loading}
className="w-full px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{loading ? 'Loading...' : 'Fetch Milestones'}
</button>
{escrow.error && (
<p className="text-xs text-red-600 dark:text-red-400">{escrow.error}</p>
)}
{milestones !== null && <MilestoneTable milestones={milestones} />}
</div>
</ContractCard>

<ContractCard
name="Dispute Contract"
contractId={DISPUTE_CONTRACT_ID}
ready={dispute.isReady}
>
<div className="space-y-3">
<div>
<label className="block text-xs text-gray-500 dark:text-gray-400 mb-1">
Dispute ID (hex)
</label>
<input
type="text"
value={disputeIdInput}
onChange={(e) => setDisputeIdInput(e.target.value)}
placeholder="e.g. 0x4d5e6f..."
className="w-full text-sm font-mono px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<button
onClick={handleFetchDispute}
disabled={!dispute.isReady || !disputeIdInput || loading}
className="w-full px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{loading ? 'Loading...' : 'Fetch Dispute'}
</button>
{dispute.error && (
<p className="text-xs text-red-600 dark:text-red-400">{dispute.error}</p>
)}
{disputeDetail !== null && <DisputeDetail dispute={disputeDetail} />}
</div>
</ContractCard>
</div>

<div className="mt-8 bg-gray-50 dark:bg-gray-900/50 rounded-xl border border-gray-200 dark:border-gray-800 p-6">
<h2 className="text-sm font-semibold text-gray-900 dark:text-white mb-3">About Contract Bindings</h2>
<ul className="space-y-2 text-sm text-gray-600 dark:text-gray-400">
<li className="flex items-start gap-2">
<span className="text-blue-500 mt-0.5">&#8226;</span>
Bindings are auto-generated from Soroban contract spec JSON files in <code className="text-xs bg-gray-200 dark:bg-gray-800 px-1.5 py-0.5 rounded">shared/contracts-raw/</code>
</li>
<li className="flex items-start gap-2">
<span className="text-blue-500 mt-0.5">&#8226;</span>
All contract method calls, arguments, and return types are compile-time checked
</li>
<li className="flex items-start gap-2">
<span className="text-blue-500 mt-0.5">&#8226;</span>
Run <code className="text-xs bg-gray-200 dark:bg-gray-800 px-1.5 py-0.5 rounded">npm run codegen</code> to regenerate after spec changes
</li>
<li className="flex items-start gap-2">
<span className="text-blue-500 mt-0.5">&#8226;</span>
Run <code className="text-xs bg-gray-200 dark:bg-gray-800 px-1.5 py-0.5 rounded">npm run codegen:validate</code> to verify bindings are up-to-date
</li>
</ul>
</div>
</div>
</>
)
}
Loading
Loading