-
Notifications
You must be signed in to change notification settings - Fork 43
Feat/onchain bounty status #167
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
clintjeff2
wants to merge
23
commits into
boundlessfi:main
Choose a base branch
from
clintjeff2:feat/onchain-bounty-status
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 9 commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
ae0d4c9
chore: allow .env.example to be tracked by git
clintjeff2 f6aa9bf
chore: add env example for Stellar RPC and contract config
clintjeff2 0c94f2f
chore: add @stellar/stellar-sdk dependency
clintjeff2 5018b09
feat: add contract event poller for Stellar RPC bounty events
clintjeff2 621ad86
feat: add useOnChainBounty hook for on-chain status verification
clintjeff2 16a8203
feat: add useTransactionStatus hook for tx confirmation tracking
clintjeff2 b3c000d
feat: add OnChainStatusBadge component with conflict detection
clintjeff2 34ad076
docs: add PR description for #147 on-chain bounty status sync
clintjeff2 eb48b5a
removed unwanted file
clintjeff2 cbb2082
removed unused file
clintjeff2 326dea5
chore: extract shared Stellar RPC and contract constants into config
clintjeff2 d6646f5
refactor: import Stellar constants from shared config in event-listener
clintjeff2 3f2588b
refactor: import Stellar constants from shared config in useOnChainBo…
clintjeff2 af65ca0
fix: replace faked finalized timeout with real ledger-based finality …
clintjeff2 f9c4e96
fix: make tooltip trigger keyboard-accessible and remove non-null ass…
clintjeff2 ad3f908
fix: start contractEventPoller on mount so on-chain sync is active at…
clintjeff2 f3444b6
fix: ignore all .env* variants while keeping .env.example tracked
clintjeff2 fe69822
fix: switch CI from npm to pnpm to match project package manager
clintjeff2 738c56c
fix: add isPolling guard and paginate all events before advancing led…
clintjeff2 8e1f917
fix: treat DRAFT and DISPUTED as unverified instead of false conflict
clintjeff2 967b98a
fix: re-validate hash after async boundary, guard interval setup, and…
clintjeff2 7ea6af8
fixed ci
clintjeff2 3e3480f
Merge branch 'main' into feat/onchain-bounty-status
clintjeff2 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| NEXT_PUBLIC_STELLAR_RPC_URL=https://soroban-testnet.stellar.org | ||
| NEXT_PUBLIC_BOUNTY_CONTRACT_ID=contract_id | ||
| NEXT_PUBLIC_STELLAR_EXPLORER_URL=https://stellar.expert/explorer/testnet |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| # feat: implement on-chain bounty status sync and event listener (#147) | ||
|
|
||
| ## Summary | ||
|
|
||
| Closes #147. Implements real-time on-chain bounty state syncing by polling Stellar RPC for Soroban contract events, reading contract state directly, and tracking transaction confirmation lifecycle. | ||
|
|
||
| --- | ||
|
|
||
| ## Changes | ||
|
|
||
| ### New files | ||
|
|
||
| | File | Purpose | | ||
| | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | ||
| | `lib/contracts/event-listener.ts` | Singleton poller that queries Stellar RPC for contract events and dispatches React Query cache invalidations | | ||
| | `hooks/use-onchain-bounty.ts` | Per-bounty hook that reads state directly from the contract and compares it with GraphQL data to detect conflicts | | ||
| | `hooks/use-transaction-status.ts` | Hook that tracks a submitted transaction from `pending` → `confirmed` → `finalized` with Stellar Explorer links | | ||
| | `components/bounty/onchain-status-badge.tsx` | Badge component that shows verification state and surfaces status conflicts with a tooltip | | ||
|
|
||
| ### Dependencies added | ||
|
|
||
| - `@stellar/stellar-sdk` — Soroban RPC client, XDR parsing, `scValToNative` | ||
|
|
||
| --- | ||
|
|
||
| ## How it works | ||
|
|
||
| ### Contract Event Poller (`lib/contracts/event-listener.ts`) | ||
|
|
||
| - Polls `NEXT_PUBLIC_STELLAR_RPC_URL` every **6 s** using `rpc.Server.getEvents()` | ||
| - Filters by `NEXT_PUBLIC_BOUNTY_CONTRACT_ID` | ||
| - Parses XDR topics to typed `ParsedContractEvent` objects for all 9 event types | ||
| - Calls `QueryClient` cache invalidations scoped to affected bounty keys | ||
| - Exposes `contractEventPoller.start(queryClient)` / `.stop()` / `.subscribe(cb)` | ||
|
|
||
| ### On-Chain Bounty Hook (`hooks/use-onchain-bounty.ts`) | ||
|
|
||
| - Fetches `BountyStatus` storage entry from contract using `rpc.Server.getContractData()` | ||
| - Subscribes to `contractEventPoller` to receive push updates for the specific bounty | ||
| - Maps GraphQL status → on-chain enum for cross-layer comparison | ||
| - Returns `consistencyState`: `consistent | conflict | unverified | loading` | ||
|
|
||
| ### Transaction Status Hook (`hooks/use-transaction-status.ts`) | ||
|
|
||
| - Call `track(hash)` after sending any Stellar transaction | ||
| - Polls `rpc.Server.getTransaction()` every **3 s** up to 40 attempts (≈ 2 min) | ||
| - Status progression: `idle → pending → confirmed → finalized` (or `failed / not_found`) | ||
| - Returns `explorerUrl` pointing to Stellar Expert | ||
|
|
||
| ### On-Chain Status Badge (`components/bounty/onchain-status-badge.tsx`) | ||
|
|
||
| - Mounts on any bounty detail page: `<OnChainStatusBadge bountyId={id} showConflictDetails />` | ||
| - Renders a shield icon: green ✓ consistent, amber ⚠ conflict, grey – unverified | ||
| - Tooltip shows ledger number, both statuses when conflicting, and links to explorer | ||
|
|
||
| --- | ||
|
|
||
| ## Environment variables required | ||
|
|
||
| ```env | ||
| NEXT_PUBLIC_STELLAR_RPC_URL=https://soroban-testnet.stellar.org | ||
| NEXT_PUBLIC_BOUNTY_CONTRACT_ID=<your_contract_id> | ||
| NEXT_PUBLIC_STELLAR_EXPLORER_URL=https://stellar.expert/explorer/testnet | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Proof of build | ||
|
|
||
|
|
||
| ## Acceptance criteria checklist | ||
|
|
||
| - [x] Contract events trigger UI updates (via `contractEventPoller` → query invalidation) | ||
| - [x] On-chain status readable for any bounty (`useOnChainBounty`) | ||
| - [x] Transaction confirmation tracked after submission (`useTransactionStatus`) | ||
| - [x] Status conflicts between GraphQL and on-chain detected and surfaced (`OnChainStatusBadge`) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| "use client"; | ||
|
|
||
| import { cn } from "@/lib/utils"; | ||
| import { | ||
| useOnChainBounty, | ||
| type ConsistencyState, | ||
| type OnChainStatus, | ||
| } from "@/hooks/use-onchain-bounty"; | ||
| import { | ||
| ExternalLink, | ||
| ShieldCheck, | ||
| ShieldAlert, | ||
| ShieldOff, | ||
| Loader2, | ||
| } from "lucide-react"; | ||
| import { Badge } from "@/components/ui/badge"; | ||
| import { | ||
| Tooltip, | ||
| TooltipContent, | ||
| TooltipProvider, | ||
| TooltipTrigger, | ||
| } from "@/components/ui/tooltip"; | ||
|
|
||
| interface OnChainStatusBadgeProps { | ||
| bountyId: string; | ||
| className?: string; | ||
| showConflictDetails?: boolean; | ||
| } | ||
|
|
||
| const statusLabels: Record<OnChainStatus, string> = { | ||
| open: "Open", | ||
| in_progress: "In Progress", | ||
| submitted: "Submitted", | ||
| approved: "Approved", | ||
| claimed: "Claimed", | ||
| cancelled: "Cancelled", | ||
| unknown: "Unknown", | ||
| }; | ||
|
|
||
| const consistencyConfig: Record< | ||
| ConsistencyState, | ||
| { | ||
| icon: React.ReactNode; | ||
| label: string; | ||
| badgeClass: string; | ||
| tooltipText: string; | ||
| } | ||
| > = { | ||
| loading: { | ||
| icon: <Loader2 className="size-3 animate-spin" />, | ||
| label: "Verifying", | ||
| badgeClass: "border-muted-foreground/30 text-muted-foreground", | ||
| tooltipText: "Checking on-chain state…", | ||
| }, | ||
| consistent: { | ||
| icon: <ShieldCheck className="size-3" />, | ||
| label: "Verified on-chain", | ||
| badgeClass: "border-emerald-500/60 text-emerald-500 bg-emerald-500/10", | ||
| tooltipText: "On-chain status matches off-chain data.", | ||
| }, | ||
| conflict: { | ||
| icon: <ShieldAlert className="size-3" />, | ||
| label: "Status conflict", | ||
| badgeClass: "border-amber-500/60 text-amber-500 bg-amber-500/10", | ||
| tooltipText: | ||
| "On-chain status differs from the database. On-chain is the source of truth.", | ||
| }, | ||
| unverified: { | ||
| icon: <ShieldOff className="size-3" />, | ||
| label: "Not verified", | ||
| badgeClass: "border-muted-foreground/30 text-muted-foreground", | ||
| tooltipText: "On-chain status could not be verified.", | ||
| }, | ||
| }; | ||
|
|
||
| export function OnChainStatusBadge({ | ||
| bountyId, | ||
| className, | ||
| showConflictDetails = false, | ||
| }: OnChainStatusBadgeProps) { | ||
| const { consistencyState, onChainData, graphqlBounty, explorerUrl } = | ||
| useOnChainBounty(bountyId); | ||
|
|
||
| const config = consistencyConfig[consistencyState]; | ||
|
|
||
| return ( | ||
| <TooltipProvider delayDuration={300}> | ||
| <Tooltip> | ||
| <TooltipTrigger asChild> | ||
| <div className={cn("flex items-center gap-1.5", className)}> | ||
| <Badge | ||
| variant="outline" | ||
| className={cn( | ||
| "flex items-center gap-1 text-xs font-medium px-2 py-0.5 cursor-default select-none", | ||
| config.badgeClass, | ||
| )} | ||
| > | ||
| {config.icon} | ||
| <span>{config.label}</span> | ||
| </Badge> | ||
|
|
||
| {explorerUrl && ( | ||
| <a | ||
| href={explorerUrl} | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| className="text-muted-foreground hover:text-foreground transition-colors" | ||
| aria-label="View on Stellar Explorer" | ||
| > | ||
| <ExternalLink className="size-3" /> | ||
| </a> | ||
| )} | ||
| </div> | ||
| </TooltipTrigger> | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| <TooltipContent className="max-w-xs text-xs" side="bottom"> | ||
| <p>{config.tooltipText}</p> | ||
|
|
||
| {consistencyState === "conflict" && showConflictDetails && ( | ||
| <div className="mt-1.5 space-y-0.5 border-t border-border/50 pt-1.5"> | ||
| {onChainData?.status !== "unknown" && ( | ||
| <p> | ||
| <span className="text-muted-foreground">On-chain: </span> | ||
| <span className="font-medium text-foreground"> | ||
| {statusLabels[onChainData!.status]} | ||
| </span> | ||
| </p> | ||
| )} | ||
| {graphqlBounty && ( | ||
| <p> | ||
| <span className="text-muted-foreground">Database: </span> | ||
| <span className="font-medium text-foreground"> | ||
| {graphqlBounty.status} | ||
| </span> | ||
| </p> | ||
| )} | ||
| </div> | ||
| )} | ||
|
|
||
| {onChainData?.ledger && ( | ||
| <p className="mt-1 text-muted-foreground"> | ||
| Ledger #{onChainData.ledger} | ||
| </p> | ||
| )} | ||
| </TooltipContent> | ||
| </Tooltip> | ||
| </TooltipProvider> | ||
| ); | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.