/ frontend / src / components / delta / StakeVerification.tsx
StakeVerification.tsx
  1  import { useState } from 'react'
  2  import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
  3  import { useAuthStore } from '../../store/auth'
  4  import * as chainService from '../../services/chain'
  5  
  6  interface StakeInfo {
  7    address: string
  8    stakedAmount: number
  9    lockedUntil: number | null
 10    isEligible: boolean
 11  }
 12  
 13  async function fetchStakeInfo(address: string): Promise<StakeInfo | null> {
 14    try {
 15      const res = await fetch(`/api/cache/stakes/${address}`)
 16      if (!res.ok) return null
 17      return res.json()
 18    } catch {
 19      return null
 20    }
 21  }
 22  
 23  export default function StakeVerification() {
 24    const { address, isConnected } = useAuthStore()
 25    const queryClient = useQueryClient()
 26    const [isRegistering, setIsRegistering] = useState(false)
 27    const [error, setError] = useState<string | null>(null)
 28  
 29    const { data: stakeInfo, isLoading } = useQuery({
 30      queryKey: ['stakeInfo', address],
 31      queryFn: () => fetchStakeInfo(address!),
 32      enabled: !!address && isConnected,
 33    })
 34  
 35    const registerMutation = useMutation({
 36      mutationFn: async () => {
 37        if (!stakeInfo || !stakeInfo.isEligible) {
 38          throw new Error('Insufficient stake')
 39        }
 40        return chainService.registerCodeGovernor(stakeInfo.stakedAmount)
 41      },
 42      onSuccess: () => {
 43        queryClient.invalidateQueries({ queryKey: ['governors'] })
 44        setIsRegistering(false)
 45      },
 46      onError: (err) => {
 47        setError(err instanceof Error ? err.message : 'Registration failed')
 48      },
 49    })
 50  
 51    if (!isConnected) {
 52      return (
 53        <div className="bg-gray-50 border border-gray-200 rounded-lg p-6">
 54          <p className="text-gray-600 text-center">
 55            Connect your wallet to verify your DX stake.
 56          </p>
 57        </div>
 58      )
 59    }
 60  
 61    if (isLoading) {
 62      return (
 63        <div className="bg-delta-50 border border-delta-200 rounded-lg p-6">
 64          <div className="flex items-center justify-center gap-3">
 65            <div className="animate-spin h-5 w-5 border-2 border-delta-500 border-t-transparent rounded-full" />
 66            <span className="text-delta-700">Verifying stake on Delta chain...</span>
 67          </div>
 68        </div>
 69      )
 70    }
 71  
 72    return (
 73      <div className="bg-delta-50 border border-delta-200 rounded-lg p-6">
 74        <h3 className="text-lg font-semibold text-delta-800 mb-4">
 75          DX Stake Verification
 76        </h3>
 77  
 78        {stakeInfo ? (
 79          <div className="space-y-4">
 80            {/* Stake Summary */}
 81            <div className="bg-white rounded-lg p-4 border border-delta-200">
 82              <dl className="grid grid-cols-2 gap-4">
 83                <div>
 84                  <dt className="text-sm text-gray-500">Your Staked Amount</dt>
 85                  <dd className="text-2xl font-bold text-delta-700">
 86                    {stakeInfo.stakedAmount.toLocaleString()} DX
 87                  </dd>
 88                </div>
 89                <div>
 90                  <dt className="text-sm text-gray-500">Minimum Required</dt>
 91                  <dd className="text-2xl font-bold text-gray-500">10,000 DX</dd>
 92                </div>
 93              </dl>
 94  
 95              {/* Progress bar */}
 96              <div className="mt-4">
 97                <div className="flex justify-between text-xs text-gray-500 mb-1">
 98                  <span>Progress to eligibility</span>
 99                  <span>
100                    {Math.min(100, (stakeInfo.stakedAmount / 10000) * 100).toFixed(1)}%
101                  </span>
102                </div>
103                <div className="h-2 bg-gray-200 rounded-full overflow-hidden">
104                  <div
105                    className={`h-full rounded-full transition-all ${
106                      stakeInfo.isEligible ? 'bg-green-500' : 'bg-delta-500'
107                    }`}
108                    style={{
109                      width: `${Math.min(100, (stakeInfo.stakedAmount / 10000) * 100)}%`,
110                    }}
111                  />
112                </div>
113              </div>
114            </div>
115  
116            {/* Status */}
117            {stakeInfo.isEligible ? (
118              <div className="bg-green-50 border border-green-200 rounded-lg p-4">
119                <div className="flex items-center gap-2 text-green-700">
120                  <CheckIcon className="h-5 w-5" />
121                  <span className="font-medium">Eligible to become a Code Governor</span>
122                </div>
123                <p className="text-sm text-green-600 mt-2">
124                  You have staked enough DX to register as a Delta Code Governor.
125                </p>
126  
127                {!isRegistering ? (
128                  <button
129                    onClick={() => setIsRegistering(true)}
130                    className="mt-4 w-full py-2 px-4 bg-delta-600 text-white rounded-lg font-medium hover:bg-delta-700 transition-colors"
131                  >
132                    Register as Code Governor
133                  </button>
134                ) : (
135                  <div className="mt-4 space-y-3">
136                    <div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
137                      <p className="text-sm text-yellow-700">
138                        <strong>Important:</strong> By registering, you commit to
139                        participating in governance votes. Missing 3 consecutive votes
140                        will result in automatic deregistration.
141                      </p>
142                    </div>
143                    <div className="flex gap-3">
144                      <button
145                        onClick={() => setIsRegistering(false)}
146                        className="flex-1 py-2 px-4 border border-gray-300 text-gray-700 rounded-lg font-medium hover:bg-gray-50"
147                      >
148                        Cancel
149                      </button>
150                      <button
151                        onClick={() => registerMutation.mutate()}
152                        disabled={registerMutation.isPending}
153                        className="flex-1 py-2 px-4 bg-delta-600 text-white rounded-lg font-medium hover:bg-delta-700 disabled:opacity-50"
154                      >
155                        {registerMutation.isPending ? 'Registering...' : 'Confirm Registration'}
156                      </button>
157                    </div>
158                  </div>
159                )}
160              </div>
161            ) : (
162              <div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
163                <div className="flex items-center gap-2 text-yellow-700">
164                  <WarningIcon className="h-5 w-5" />
165                  <span className="font-medium">Insufficient Stake</span>
166                </div>
167                <p className="text-sm text-yellow-600 mt-2">
168                  You need to stake at least{' '}
169                  <strong>{(10000 - stakeInfo.stakedAmount).toLocaleString()} more DX</strong>{' '}
170                  to become eligible as a Code Governor.
171                </p>
172                <a
173                  href="https://dex.ac-dc.network/stake"
174                  target="_blank"
175                  rel="noopener noreferrer"
176                  className="mt-3 inline-block text-sm text-delta-600 hover:text-delta-700 font-medium"
177                >
178                  Stake more DX &rarr;
179                </a>
180              </div>
181            )}
182  
183            {stakeInfo.lockedUntil && (
184              <div className="text-xs text-gray-500">
185                Stake locked until:{' '}
186                {new Date(stakeInfo.lockedUntil * 1000).toLocaleDateString()}
187              </div>
188            )}
189  
190            {error && (
191              <div className="bg-red-50 border border-red-200 rounded-lg p-3 text-sm text-red-700">
192                {error}
193              </div>
194            )}
195          </div>
196        ) : (
197          <div className="text-center py-4">
198            <p className="text-gray-600 mb-4">No stake found for your address.</p>
199            <a
200              href="https://dex.ac-dc.network/stake"
201              target="_blank"
202              rel="noopener noreferrer"
203              className="inline-block px-4 py-2 bg-delta-600 text-white rounded-lg font-medium hover:bg-delta-700"
204            >
205              Stake DX to Participate
206            </a>
207          </div>
208        )}
209      </div>
210    )
211  }
212  
213  function CheckIcon({ className }: { className?: string }) {
214    return (
215      <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
216        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
217      </svg>
218    )
219  }
220  
221  function WarningIcon({ className }: { className?: string }) {
222    return (
223      <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
224        <path
225          strokeLinecap="round"
226          strokeLinejoin="round"
227          strokeWidth={2}
228          d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
229        />
230      </svg>
231    )
232  }