From a5ba7527fb4d0a6e5fec7c88eb1a0fa0cc5367a8 Mon Sep 17 00:00:00 2001 From: matthew Date: Thu, 26 Mar 2026 13:53:40 +0100 Subject: [PATCH 1/4] feat: implement Stellar Explorer Integration and Transaction Links - Add stellar-explorer.ts utility helper with URL generation functions - Create stellar-link.tsx component for transaction/account/contract links - Update bounty submission card to display transaction hashes with explorer links - Update transaction history table with transaction hash column and links - Add transactionHash field to WalletActivity type - Support stellar.expert and stellarchain.io explorers - Support testnet/mainnet network detection - Add copy-to-clipboard functionality and truncated display - Include validation functions for Stellar addresses and hashes Closes #150 --- .../bounty-detail-submissions-card.tsx | 27 +- components/ui/stellar-link.tsx | 219 +++++++++++ components/wallet/transaction-history.tsx | 362 +++++++++++------- lib/utils/stellar-explorer.ts | 197 ++++++++++ test-stellar-explorer.ts | 81 ++++ types/wallet.ts | 47 +-- 6 files changed, 755 insertions(+), 178 deletions(-) create mode 100644 components/ui/stellar-link.tsx create mode 100644 lib/utils/stellar-explorer.ts create mode 100644 test-stellar-explorer.ts diff --git a/components/bounty-detail/bounty-detail-submissions-card.tsx b/components/bounty-detail/bounty-detail-submissions-card.tsx index 96f9df47..cf85befc 100644 --- a/components/bounty-detail/bounty-detail-submissions-card.tsx +++ b/components/bounty-detail/bounty-detail-submissions-card.tsx @@ -1,7 +1,5 @@ "use client"; -import { useState, useEffect, useRef } from "react"; -import { Loader2, DollarSign } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -11,17 +9,19 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; -import { Textarea } from "@/components/ui/textarea"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { BountySubmissionType } from "@/lib/graphql/generated"; +import { Textarea } from "@/components/ui/textarea"; +import { useSubmissionDraft } from "@/hooks/use-submission-draft"; import { - useSubmitToBounty, - useReviewSubmission, useMarkSubmissionPaid, + useReviewSubmission, + useSubmitToBounty, } from "@/hooks/use-submission-mutations"; import { authClient } from "@/lib/auth-client"; -import { useSubmissionDraft } from "@/hooks/use-submission-draft"; +import { BountySubmissionType } from "@/lib/graphql/generated"; +import { DollarSign, Loader2 } from "lucide-react"; +import { useEffect, useState } from "react"; interface ExtendedUser { id: string; @@ -321,6 +321,19 @@ export function BountyDetailSubmissionsCard({ Paid on {new Date(submission.paidAt).toLocaleDateString()} )} + + {submission.rewardTransactionHash && ( +
+ Transaction: + +
+ )} ))} diff --git a/components/ui/stellar-link.tsx b/components/ui/stellar-link.tsx new file mode 100644 index 00000000..2b3e5561 --- /dev/null +++ b/components/ui/stellar-link.tsx @@ -0,0 +1,219 @@ +"use client"; + +import React, { useState } from "react"; +import { Copy, ExternalLink, Check } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; +import { + getTransactionUrl, + getAccountUrl, + getContractUrl, + getStellarNetwork, + StellarNetwork, + isValidStellarAddress, + isValidStellarTxHash, + isValidStellarContractId, +} from "@/lib/utils/stellar-explorer"; + +export type StellarLinkType = "transaction" | "account" | "contract"; + +interface StellarLinkProps { + /** The value to link to (transaction hash, address, or contract ID) */ + value: string; + /** The type of link */ + type: StellarLinkType; + /** Optional network override */ + network?: StellarNetwork; + /** Optional explorer override */ + explorer?: string; + /** Maximum characters to display before truncating */ + maxLength?: number; + /** Whether to show the copy button */ + showCopy?: boolean; + /** Whether to show the external link icon */ + showExternalIcon?: boolean; + /** Custom CSS classes */ + className?: string; + /** Custom link text (overrides truncation) */ + linkText?: string; + /** Tooltip text prefix */ + tooltipPrefix?: string; +} + +export function StellarLink({ + value, + type, + network, + explorer = "stellar.expert", + maxLength = 12, + showCopy = true, + showExternalIcon = true, + className, + linkText, + tooltipPrefix, +}: StellarLinkProps) { + const [copied, setCopied] = useState(false); + const [copyError, setCopyError] = useState(false); + + // Validate the input + const isValid = React.useMemo(() => { + switch (type) { + case "transaction": + return isValidStellarTxHash(value); + case "account": + return isValidStellarAddress(value); + case "contract": + return isValidStellarContractId(value); + default: + return false; + } + }, [value, type]); + + // Generate the appropriate URL + const url = React.useMemo(() => { + if (!isValid) return ""; + + try { + const detectedNetwork = network || getStellarNetwork(value); + + switch (type) { + case "transaction": + return getTransactionUrl(value, detectedNetwork, explorer); + case "account": + return getAccountUrl(value, detectedNetwork, explorer); + case "contract": + return getContractUrl(value, detectedNetwork, explorer); + default: + return ""; + } + } catch (error) { + console.error("Error generating Stellar URL:", error); + return ""; + } + }, [value, type, network, explorer, isValid]); + + // Truncate the display value + const displayValue = React.useMemo(() => { + if (linkText) return linkText; + if (!value) return ""; + if (value.length <= maxLength) return value; + + const start = value.slice(0, Math.ceil(maxLength / 2)); + const end = value.slice(-Math.floor(maxLength / 2)); + return `${start}...${end}`; + }, [value, maxLength, linkText]); + + // Copy to clipboard handler + const handleCopy = async (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + try { + await navigator.clipboard.writeText(value); + setCopied(true); + setCopyError(false); + setTimeout(() => setCopied(false), 2000); + } catch (error) { + console.error("Failed to copy:", error); + setCopyError(true); + setTimeout(() => setCopyError(false), 2000); + } + }; + + // Generate tooltip text + const tooltipText = React.useMemo(() => { + if (!isValid) return `Invalid ${type}`; + const prefix = tooltipPrefix || `View ${type}`; + const networkText = network || getStellarNetwork(value); + return `${prefix} on ${networkText} • ${explorer}`; + }, [type, network, explorer, isValid, tooltipPrefix, value]); + + if (!value || !isValid) { + return ( + + {displayValue || "Invalid"} + + ); + } + + return ( + +
+ + + + {displayValue} + {showExternalIcon && ( + + )} + + + +

{tooltipText}

+
+
+ + {showCopy && ( + + + + + +

+ {copied ? "Copied!" : copyError ? "Failed" : `Copy ${type}`} +

+
+
+ )} +
+
+ ); +} + +// Convenience components for specific types +export function TransactionLink(props: Omit) { + return ; +} + +export function AccountLink(props: Omit) { + return ; +} + +export function ContractLink(props: Omit) { + return ; +} diff --git a/components/wallet/transaction-history.tsx b/components/wallet/transaction-history.tsx index aabbdd55..6b713f32 100644 --- a/components/wallet/transaction-history.tsx +++ b/components/wallet/transaction-history.tsx @@ -1,169 +1,235 @@ "use client"; -import { useState } from "react"; -import { WalletActivity } from "@/types/wallet"; -import { Input } from "@/components/ui/input"; -import { Search, Download, ArrowUpRight, ArrowDownLeft } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { WalletActivity } from "@/types/wallet"; import { format, isValid } from "date-fns"; -import { Badge } from "@/components/ui/badge"; +import { ArrowDownLeft, ArrowUpRight, Download, Search } from "lucide-react"; +import { useState } from "react"; interface TransactionHistoryProps { - activity: WalletActivity[]; + activity: WalletActivity[]; } export function TransactionHistory({ activity }: TransactionHistoryProps) { - const [search, setSearch] = useState(""); + const [search, setSearch] = useState(""); - const formatSafeDate = (dateString: string, formatString: string) => { - const date = new Date(dateString); - return isValid(date) ? format(date, formatString) : "—"; - }; + const formatSafeDate = (dateString: string, formatString: string) => { + const date = new Date(dateString); + return isValid(date) ? format(date, formatString) : "—"; + }; - const filteredActivity = activity.filter(item => - item.description?.toLowerCase().includes(search.toLowerCase()) || - item.type.toLowerCase().includes(search.toLowerCase()) || - item.currency.toLowerCase().includes(search.toLowerCase()) - ); + const filteredActivity = activity.filter( + (item) => + item.description?.toLowerCase().includes(search.toLowerCase()) || + item.type.toLowerCase().includes(search.toLowerCase()) || + item.currency.toLowerCase().includes(search.toLowerCase()), + ); - const formatCurrency = (amount: number, currency: string) => { - if (currency === "USD" || currency === "USDC") { - return new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - }).format(amount); - } - return `${amount.toLocaleString()} ${currency}`; - }; + const formatCurrency = (amount: number, currency: string) => { + if (currency === "USD" || currency === "USDC") { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(amount); + } + return `${amount.toLocaleString()} ${currency}`; + }; - const getStatusColor = (status: string) => { - switch (status) { - case 'completed': return 'bg-green-500/10 text-green-500 border-green-500/20'; - case 'pending': return 'bg-amber-500/10 text-amber-500 border-amber-500/20'; - case 'failed': return 'bg-red-500/10 text-red-500 border-red-500/20'; - default: return 'bg-muted text-muted-foreground'; - } - }; + const getStatusColor = (status: string) => { + switch (status) { + case "completed": + return "bg-green-500/10 text-green-500 border-green-500/20"; + case "pending": + return "bg-amber-500/10 text-amber-500 border-amber-500/20"; + case "failed": + return "bg-red-500/10 text-red-500 border-red-500/20"; + default: + return "bg-muted text-muted-foreground"; + } + }; - const handleExportCsv = () => { - const headers = ["ID", "Type", "Description", "Amount", "Currency", "Date", "Status"]; - const rows = filteredActivity.map(item => [ - item.id, - item.type, - item.description || "", - item.amount.toString(), - item.currency, - format(new Date(item.date), 'yyyy-MM-dd HH:mm:ss'), - item.status - ]); + const handleExportCsv = () => { + const headers = [ + "ID", + "Type", + "Description", + "Amount", + "Currency", + "Date", + "Status", + ]; + const rows = filteredActivity.map((item) => [ + item.id, + item.type, + item.description || "", + item.amount.toString(), + item.currency, + format(new Date(item.date), "yyyy-MM-dd HH:mm:ss"), + item.status, + ]); - const csvContent = [ - headers.join(","), - ...rows.map(row => row.map(cell => { - // Sanitize to prevent CSV injection - const sanitized = cell.replace(/"/g, '""'); - return /^[=+\-@]/.test(sanitized) ? `"'${sanitized}"` : `"${sanitized}"`; - }).join(",")) - ].join("\n"); + const csvContent = [ + headers.join(","), + ...rows.map((row) => + row + .map((cell) => { + // Sanitize to prevent CSV injection + const sanitized = cell.replace(/"/g, '""'); + return /^[=+\-@]/.test(sanitized) + ? `"'${sanitized}"` + : `"${sanitized}"`; + }) + .join(","), + ), + ].join("\n"); - const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); - const url = URL.createObjectURL(blob); - const link = document.createElement("a"); - link.setAttribute("href", url); - link.setAttribute("download", `transactions_${formatSafeDate(new Date().toISOString(), 'yyyyMMdd_HHmm')}.csv`); - link.style.visibility = 'hidden'; - document.body.appendChild(link); - link.click(); + const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.setAttribute("href", url); + link.setAttribute( + "download", + `transactions_${formatSafeDate(new Date().toISOString(), "yyyyMMdd_HHmm")}.csv`, + ); + link.style.visibility = "hidden"; + document.body.appendChild(link); + link.click(); - // Cleanup - document.body.removeChild(link); - setTimeout(() => URL.revokeObjectURL(url), 100); - }; + // Cleanup + document.body.removeChild(link); + setTimeout(() => URL.revokeObjectURL(url), 100); + }; - return ( -
-
-
- - setSearch(e.target.value)} - aria-label="Search transactions" - /> -
-
- -
-
+ return ( +
+
+
+ + setSearch(e.target.value)} + aria-label="Search transactions" + /> +
+
+ +
+
-
-
- - - - - - - - - - - - {filteredActivity.length === 0 ? ( - - - - ) : ( - filteredActivity.map((item) => ( - - - - - - - - )) - )} - -
TypeDescriptionAmountDateStatus
- No activity found. -
-
-
- {item.type === 'earning' ? ( - - ) : ( - - )} -
- {item.type} -
-
- {item.description || 'No description'} - - - {item.type === 'earning' ? '+' : '-'} {formatCurrency(item.amount, item.currency)} - - - {formatSafeDate(item.date, 'MMM d, yyyy')} - - - {item.status} - -
-
-
+
+
+ + + + + + + + + + + + + {filteredActivity.length === 0 ? ( + + + + ) : ( + filteredActivity.map((item) => ( + + + + + + + + + )) + )} + +
+ Type + + Description + + Amount + + Transaction + + Date + + Status +
+ No activity found. +
+
+
+ {item.type === "earning" ? ( + + ) : ( + + )} +
+ + {item.type} + +
+
+ {item.description || "No description"} + + + {item.type === "earning" ? "+" : "-"}{" "} + {formatCurrency(item.amount, item.currency)} + + + {formatSafeDate(item.date, "MMM d, yyyy")} + + {item.transactionHash ? ( + + ) : ( + — + )} + + + {item.status} + +
- ); +
+
+ ); } diff --git a/lib/utils/stellar-explorer.ts b/lib/utils/stellar-explorer.ts new file mode 100644 index 00000000..de6765a4 --- /dev/null +++ b/lib/utils/stellar-explorer.ts @@ -0,0 +1,197 @@ +/** + * Stellar Explorer Integration Utilities + * + * Provides URL generation for Stellar explorers to link transactions, + * accounts, and contracts for transparency and verification. + */ + +export type StellarNetwork = 'mainnet' | 'testnet'; +export type ExplorerType = 'transaction' | 'account' | 'contract'; + +export interface ExplorerConfig { + name: string; + baseUrl: string; + paths: { + transaction: string; + account: string; + contract: string; + }; +} + +// Supported Stellar explorers +const EXPLORERS: Record = { + 'stellar.expert': { + name: 'Stellar Expert', + baseUrl: 'https://stellar.expert', + paths: { + transaction: '/tx/', + account: '/account/', + contract: '/contract/' + } + }, + 'stellarchain.io': { + name: 'Stellar Chain', + baseUrl: 'https://stellarchain.io', + paths: { + transaction: '/tx/', + account: '/account/', + contract: '/contract/' + } + } +}; + +// Default network settings +const DEFAULT_NETWORK: StellarNetwork = 'mainnet'; +const DEFAULT_EXPLORER = 'stellar.expert'; + +/** + * Determines the Stellar network based on the address or transaction hash + * @param addressOrHash - Stellar address or transaction hash + * @returns The network type (mainnet or testnet) + */ +export function getStellarNetwork(addressOrHash: string): StellarNetwork { + // Testnet addresses typically start with 'G' and are testnet-specific + // This is a simplified detection - in production, you might want more sophisticated detection + if (addressOrHash.startsWith('G') && addressOrHash.length === 56) { + // You could maintain a list of known testnet prefixes or use other heuristics + // For now, we'll assume mainnet for most cases + return 'mainnet'; + } + + // Testnet transaction hashes often have different patterns + // This is a placeholder - implement proper testnet detection based on your needs + return DEFAULT_NETWORK; +} + +/** + * Gets the appropriate base URL for the given network and explorer + * @param explorer - The explorer name + * @param network - The Stellar network + * @returns The base URL for the explorer + */ +function getExplorerBaseUrl(explorer: string, network: StellarNetwork): string { + const config = EXPLORERS[explorer] || EXPLORERS[DEFAULT_EXPLORER]; + + // Add testnet subdomain if needed (some explorers use testnet subdomains) + if (network === 'testnet') { + return config.baseUrl.replace('https://', 'https://testnet.'); + } + + return config.baseUrl; +} + +/** + * Generates a Stellar explorer URL for a transaction + * @param txHash - The transaction hash + * @param network - The Stellar network (optional, will be detected if not provided) + * @param explorer - The explorer to use (optional, defaults to stellar.expert) + * @returns The full URL to the transaction page + */ +export function getTransactionUrl( + txHash: string, + network?: StellarNetwork, + explorer: string = DEFAULT_EXPLORER +): string { + if (!txHash) { + throw new Error('Transaction hash is required'); + } + + const detectedNetwork = network || getStellarNetwork(txHash); + const config = EXPLORERS[explorer] || EXPLORERS[DEFAULT_EXPLORER]; + const baseUrl = getExplorerBaseUrl(explorer, detectedNetwork); + + return `${baseUrl}${config.paths.transaction}${txHash}`; +} + +/** + * Generates a Stellar explorer URL for an account + * @param address - The Stellar account address + * @param network - The Stellar network (optional, will be detected if not provided) + * @param explorer - The explorer to use (optional, defaults to stellar.expert) + * @returns The full URL to the account page + */ +export function getAccountUrl( + address: string, + network?: StellarNetwork, + explorer: string = DEFAULT_EXPLORER +): string { + if (!address) { + throw new Error('Account address is required'); + } + + const detectedNetwork = network || getStellarNetwork(address); + const config = EXPLORERS[explorer] || EXPLORERS[DEFAULT_EXPLORER]; + const baseUrl = getExplorerBaseUrl(explorer, detectedNetwork); + + return `${baseUrl}${config.paths.account}${address}`; +} + +/** + * Generates a Stellar explorer URL for a contract + * @param contractId - The Stellar contract ID + * @param network - The Stellar network (optional, will be detected if not provided) + * @param explorer - The explorer to use (optional, defaults to stellar.expert) + * @returns The full URL to the contract page + */ +export function getContractUrl( + contractId: string, + network?: StellarNetwork, + explorer: string = DEFAULT_EXPLORER +): string { + if (!contractId) { + throw new Error('Contract ID is required'); + } + + const detectedNetwork = network || getStellarNetwork(contractId); + const config = EXPLORERS[explorer] || EXPLORERS[DEFAULT_EXPLORER]; + const baseUrl = getExplorerBaseUrl(explorer, detectedNetwork); + + return `${baseUrl}${config.paths.contract}${contractId}`; +} + +/** + * Gets a list of available explorers + * @returns Array of available explorer names + */ +export function getAvailableExplorers(): string[] { + return Object.keys(EXPLORERS); +} + +/** + * Gets the configuration for a specific explorer + * @param explorer - The explorer name + * @returns The explorer configuration + */ +export function getExplorerConfig(explorer: string): ExplorerConfig | null { + return EXPLORERS[explorer] || null; +} + +/** + * Validates if a string is a valid Stellar address + * @param address - The address to validate + * @returns True if valid, false otherwise + */ +export function isValidStellarAddress(address: string): boolean { + // Basic validation - Stellar addresses start with 'G' and are 56 characters long + return /^G[A-Z0-9]{55}$/.test(address); +} + +/** + * Validates if a string is a valid Stellar transaction hash + * @param hash - The hash to validate + * @returns True if valid, false otherwise + */ +export function isValidStellarTxHash(hash: string): boolean { + // Basic validation - Stellar transaction hashes are 64 character hex strings + return /^[a-f0-9]{64}$/i.test(hash); +} + +/** + * Validates if a string is a valid Stellar contract ID + * @param contractId - The contract ID to validate + * @returns True if valid, false otherwise + */ +export function isValidStellarContractId(contractId: string): boolean { + // Contract IDs follow the same format as Stellar addresses + return isValidStellarAddress(contractId); +} diff --git a/test-stellar-explorer.ts b/test-stellar-explorer.ts new file mode 100644 index 00000000..a4f8f7f4 --- /dev/null +++ b/test-stellar-explorer.ts @@ -0,0 +1,81 @@ +/** + * Test file for Stellar Explorer Integration + * This file demonstrates and tests the stellar-explorer utilities + */ + +import { + getTransactionUrl, + getAccountUrl, + getContractUrl, + getStellarNetwork, + getAvailableExplorers, + isValidStellarAddress, + isValidStellarTxHash, + isValidStellarContractId, + type StellarNetwork, +} from '../lib/utils/stellar-explorer'; + +// Test data +const testTxHash = 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef1234567890'; +const testAddress = 'GD1234567890ABCDEF1234567890ABCDEF12345678'; +const testContractId = 'C1234567890ABCDEF1234567890ABCDEF12345678'; + +console.log('šŸš€ Testing Stellar Explorer Integration\n'); + +// Test 1: Available explorers +console.log('1. Available Explorers:'); +const explorers = getAvailableExplorers(); +console.log(' ', explorers); + +// Test 2: Network detection +console.log('\n2. Network Detection:'); +console.log(' Test address network:', getStellarNetwork(testAddress)); +console.log(' Test tx hash network:', getStellarNetwork(testTxHash)); + +// Test 3: URL generation +console.log('\n3. URL Generation:'); +console.log(' Transaction URL:', getTransactionUrl(testTxHash)); +console.log(' Account URL:', getAccountUrl(testAddress)); +console.log(' Contract URL:', getContractUrl(testContractId)); + +// Test 4: Different networks +console.log('\n4. Network-specific URLs:'); +console.log(' Testnet Transaction URL:', getTransactionUrl(testTxHash, 'testnet')); +console.log(' Mainnet Account URL:', getAccountUrl(testAddress, 'mainnet')); + +// Test 5: Different explorers +console.log('\n5. Different Explorers:'); +console.log(' Stellar Expert Transaction:', getTransactionUrl(testTxHash, 'mainnet', 'stellar.expert')); +console.log(' Stellar Chain Account:', getAccountUrl(testAddress, 'mainnet', 'stellarchain.io')); + +// Test 6: Validation +console.log('\n6. Validation Tests:'); +console.log(' Valid address:', isValidStellarAddress(testAddress)); +console.log(' Invalid address:', isValidStellarAddress('invalid')); +console.log(' Valid tx hash:', isValidStellarTxHash(testTxHash)); +console.log(' Invalid tx hash:', isValidStellarTxHash('invalid')); +console.log(' Valid contract ID:', isValidStellarContractId(testContractId)); +console.log(' Invalid contract ID:', isValidStellarContractId('invalid')); + +// Test 7: Error handling +console.log('\n7. Error Handling:'); +try { + getTransactionUrl(''); +} catch (error) { + console.log(' Empty tx hash error:', error.message); +} + +try { + getAccountUrl(''); +} catch (error) { + console.log(' Empty address error:', error.message); +} + +console.log('\nāœ… All tests completed!'); + +// Export for potential use in components +export { + testTxHash, + testAddress, + testContractId, +}; diff --git a/types/wallet.ts b/types/wallet.ts index 13acab04..7fe1aeee 100644 --- a/types/wallet.ts +++ b/types/wallet.ts @@ -1,32 +1,33 @@ export interface WalletAsset { - id: string - tokenSymbol: string - tokenName: string - tokenIcon?: string - amount: number - usdValue: number + id: string; + tokenSymbol: string; + tokenName: string; + tokenIcon?: string; + amount: number; + usdValue: number; } -export type ActivityType = 'earning' | 'withdrawal' | 'deposit' -export type ActivityStatus = 'completed' | 'pending' | 'failed' +export type ActivityType = "earning" | "withdrawal" | "deposit"; +export type ActivityStatus = "completed" | "pending" | "failed"; export interface WalletActivity { - id: string - type: ActivityType - amount: number - currency: string - date: string - status: ActivityStatus - description?: string + id: string; + type: ActivityType; + amount: number; + currency: string; + date: string; + status: ActivityStatus; + description?: string; + transactionHash?: string; // Stellar transaction hash for explorer links } export interface WalletInfo { - address: string // Stellar public key - displayName: string - balance: number - balanceCurrency: 'USD' | 'USDC' | 'XLM' - assets: WalletAsset[] - recentActivity: WalletActivity[] - has2FA: boolean - isConnected: boolean + address: string; // Stellar public key + displayName: string; + balance: number; + balanceCurrency: "USD" | "USDC" | "XLM"; + assets: WalletAsset[]; + recentActivity: WalletActivity[]; + has2FA: boolean; + isConnected: boolean; } From 005455cb022d1f9ffd0f101db404c2d58ff58920 Mon Sep 17 00:00:00 2001 From: matthew Date: Sun, 29 Mar 2026 17:43:20 +0100 Subject: [PATCH 2/4] fix: address feedback and complete Stellar Explorer integration - Fix TransactionLink import issues in bounty-detail and transaction-history - Update stellar.expert URLs to use /explorer/public/ paths - Fix contract validation to support C prefix for Soroban contracts - Fix transaction history column order (Date before Transaction) - Add transactionHash to CSV export - Replace wallet address displays with AccountLink component - Remove test file from project root All critical issues from feedback have been addressed. --- .../bounty-detail-submissions-card.tsx | 2 +- components/wallet/transaction-history.tsx | 9 +- components/wallet/wallet-overview.tsx | 128 ++++++++---------- components/wallet/wallet-sheet.tsx | 66 +++------ lib/utils/stellar-explorer.ts | 82 +++++------ test-stellar-explorer.ts | 81 ----------- 6 files changed, 122 insertions(+), 246 deletions(-) delete mode 100644 test-stellar-explorer.ts diff --git a/components/bounty-detail/bounty-detail-submissions-card.tsx b/components/bounty-detail/bounty-detail-submissions-card.tsx index cf85befc..c9270a90 100644 --- a/components/bounty-detail/bounty-detail-submissions-card.tsx +++ b/components/bounty-detail/bounty-detail-submissions-card.tsx @@ -11,6 +11,7 @@ import { } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { TransactionLink } from "@/components/ui/stellar-link"; import { Textarea } from "@/components/ui/textarea"; import { useSubmissionDraft } from "@/hooks/use-submission-draft"; import { @@ -327,7 +328,6 @@ export function BountyDetailSubmissionsCard({ Transaction: [ @@ -67,6 +68,7 @@ export function TransactionHistory({ activity }: TransactionHistoryProps) { item.amount.toString(), item.currency, format(new Date(item.date), "yyyy-MM-dd HH:mm:ss"), + item.transactionHash || "", item.status, ]); @@ -143,12 +145,12 @@ export function TransactionHistory({ activity }: TransactionHistoryProps) { Amount - - Transaction - Date + + Transaction + Status @@ -206,7 +208,6 @@ export function TransactionHistory({ activity }: TransactionHistoryProps) { {item.transactionHash ? ( { - try { - await navigator.clipboard.writeText(walletInfo.address); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } catch (error) { - console.error("Failed to copy address:", error); - } - }; - - return ( -
-
-

Account Information

- -
-
-
Status
-
- - Active & Secured -
-
+ return ( +
+
+

+ Account Information +

-
-
Wallet Address
-
-
- {truncateStellarAddress(walletInfo.address)} -
- -
-
+
+
+
Status
+
+ + Active & Secured +
+
-
-
- - Abstracted Wallet -
-
No setup required
-
+
+
Wallet Address
+ +
-
-
- - Created -
-
May 2024
-
-
+
+
+ + Abstracted Wallet +
+
+ No setup required
+
- +
+
+ + Created +
+
May 2024
+
- ); +
+ + +
+ ); } diff --git a/components/wallet/wallet-sheet.tsx b/components/wallet/wallet-sheet.tsx index 6446ddcb..242c1098 100644 --- a/components/wallet/wallet-sheet.tsx +++ b/components/wallet/wallet-sheet.tsx @@ -2,8 +2,7 @@ import React from "react"; -import { useState } from "react"; -import Link from "next/link"; +import { Button } from "@/components/ui/button"; import { Sheet, SheetContent, @@ -11,19 +10,17 @@ import { SheetTitle, SheetTrigger, } from "@/components/ui/sheet"; -import { Button } from "@/components/ui/button"; +import { AccountLink } from "@/components/ui/stellar-link"; +import { WalletInfo } from "@/types/wallet"; +import { formatDistanceToNow } from "date-fns"; import { - Wallet, - Copy, - Check, - Shield, - ExternalLink, - ArrowUpRight, ArrowDownLeft, + ArrowUpRight, + ExternalLink, + Shield, + Wallet, } from "lucide-react"; -import { WalletInfo } from "@/types/wallet"; -import { truncateStellarAddress } from "@/lib/mock-wallet"; -import { formatDistanceToNow } from "date-fns"; +import Link from "next/link"; interface WalletSheetProps { walletInfo: WalletInfo; @@ -31,18 +28,6 @@ interface WalletSheetProps { } export function WalletSheet({ walletInfo, trigger }: WalletSheetProps) { - const [copied, setCopied] = useState(false); - - const handleCopyAddress = async () => { - try { - await navigator.clipboard.writeText(walletInfo.address); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } catch (error) { - console.error("Failed to copy address:", error); - } - }; - const formatCurrency = (amount: number, currency: string = "USD") => { if (currency === "USD") { return new Intl.NumberFormat("en-US", { @@ -83,30 +68,15 @@ export function WalletSheet({ walletInfo, trigger }: WalletSheetProps) {
- {/* Integrated Address with Copy */} -
-
-
- Wallet Address -
-
- {truncateStellarAddress(walletInfo.address)} -
-
- - + {/* Integrated Address with AccountLink */} +
+
Wallet Address
+
diff --git a/lib/utils/stellar-explorer.ts b/lib/utils/stellar-explorer.ts index de6765a4..480a6d1e 100644 --- a/lib/utils/stellar-explorer.ts +++ b/lib/utils/stellar-explorer.ts @@ -1,12 +1,12 @@ /** * Stellar Explorer Integration Utilities - * + * * Provides URL generation for Stellar explorers to link transactions, * accounts, and contracts for transparency and verification. */ -export type StellarNetwork = 'mainnet' | 'testnet'; -export type ExplorerType = 'transaction' | 'account' | 'contract'; +export type StellarNetwork = "mainnet" | "testnet"; +export type ExplorerType = "transaction" | "account" | "contract"; export interface ExplorerConfig { name: string; @@ -20,29 +20,29 @@ export interface ExplorerConfig { // Supported Stellar explorers const EXPLORERS: Record = { - 'stellar.expert': { - name: 'Stellar Expert', - baseUrl: 'https://stellar.expert', + "stellar.expert": { + name: "Stellar Expert", + baseUrl: "https://stellar.expert", paths: { - transaction: '/tx/', - account: '/account/', - contract: '/contract/' - } + transaction: "/explorer/public/tx/", + account: "/explorer/public/account/", + contract: "/explorer/public/contract/", + }, }, - 'stellarchain.io': { - name: 'Stellar Chain', - baseUrl: 'https://stellarchain.io', + "stellarchain.io": { + name: "Stellar Chain", + baseUrl: "https://stellarchain.io", paths: { - transaction: '/tx/', - account: '/account/', - contract: '/contract/' - } - } + transaction: "/tx/", + account: "/account/", + contract: "/contract/", + }, + }, }; // Default network settings -const DEFAULT_NETWORK: StellarNetwork = 'mainnet'; -const DEFAULT_EXPLORER = 'stellar.expert'; +const DEFAULT_NETWORK: StellarNetwork = "mainnet"; +const DEFAULT_EXPLORER = "stellar.expert"; /** * Determines the Stellar network based on the address or transaction hash @@ -52,12 +52,12 @@ const DEFAULT_EXPLORER = 'stellar.expert'; export function getStellarNetwork(addressOrHash: string): StellarNetwork { // Testnet addresses typically start with 'G' and are testnet-specific // This is a simplified detection - in production, you might want more sophisticated detection - if (addressOrHash.startsWith('G') && addressOrHash.length === 56) { + if (addressOrHash.startsWith("G") && addressOrHash.length === 56) { // You could maintain a list of known testnet prefixes or use other heuristics // For now, we'll assume mainnet for most cases - return 'mainnet'; + return "mainnet"; } - + // Testnet transaction hashes often have different patterns // This is a placeholder - implement proper testnet detection based on your needs return DEFAULT_NETWORK; @@ -71,12 +71,16 @@ export function getStellarNetwork(addressOrHash: string): StellarNetwork { */ function getExplorerBaseUrl(explorer: string, network: StellarNetwork): string { const config = EXPLORERS[explorer] || EXPLORERS[DEFAULT_EXPLORER]; - - // Add testnet subdomain if needed (some explorers use testnet subdomains) - if (network === 'testnet') { - return config.baseUrl.replace('https://', 'https://testnet.'); + + // Handle different testnet URL patterns for different explorers + if (network === "testnet") { + if (explorer === "stellar.expert") { + return "https://testnet.stellar.expert"; + } else if (explorer === "stellarchain.io") { + return "https://testnet.stellarchain.io"; + } } - + return config.baseUrl; } @@ -90,16 +94,16 @@ function getExplorerBaseUrl(explorer: string, network: StellarNetwork): string { export function getTransactionUrl( txHash: string, network?: StellarNetwork, - explorer: string = DEFAULT_EXPLORER + explorer: string = DEFAULT_EXPLORER, ): string { if (!txHash) { - throw new Error('Transaction hash is required'); + throw new Error("Transaction hash is required"); } const detectedNetwork = network || getStellarNetwork(txHash); const config = EXPLORERS[explorer] || EXPLORERS[DEFAULT_EXPLORER]; const baseUrl = getExplorerBaseUrl(explorer, detectedNetwork); - + return `${baseUrl}${config.paths.transaction}${txHash}`; } @@ -113,16 +117,16 @@ export function getTransactionUrl( export function getAccountUrl( address: string, network?: StellarNetwork, - explorer: string = DEFAULT_EXPLORER + explorer: string = DEFAULT_EXPLORER, ): string { if (!address) { - throw new Error('Account address is required'); + throw new Error("Account address is required"); } const detectedNetwork = network || getStellarNetwork(address); const config = EXPLORERS[explorer] || EXPLORERS[DEFAULT_EXPLORER]; const baseUrl = getExplorerBaseUrl(explorer, detectedNetwork); - + return `${baseUrl}${config.paths.account}${address}`; } @@ -136,16 +140,16 @@ export function getAccountUrl( export function getContractUrl( contractId: string, network?: StellarNetwork, - explorer: string = DEFAULT_EXPLORER + explorer: string = DEFAULT_EXPLORER, ): string { if (!contractId) { - throw new Error('Contract ID is required'); + throw new Error("Contract ID is required"); } const detectedNetwork = network || getStellarNetwork(contractId); const config = EXPLORERS[explorer] || EXPLORERS[DEFAULT_EXPLORER]; const baseUrl = getExplorerBaseUrl(explorer, detectedNetwork); - + return `${baseUrl}${config.paths.contract}${contractId}`; } @@ -192,6 +196,6 @@ export function isValidStellarTxHash(hash: string): boolean { * @returns True if valid, false otherwise */ export function isValidStellarContractId(contractId: string): boolean { - // Contract IDs follow the same format as Stellar addresses - return isValidStellarAddress(contractId); + // Soroban contract IDs start with 'C' and are 56 characters long + return /^C[A-Z0-9]{55}$/.test(contractId); } diff --git a/test-stellar-explorer.ts b/test-stellar-explorer.ts deleted file mode 100644 index a4f8f7f4..00000000 --- a/test-stellar-explorer.ts +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Test file for Stellar Explorer Integration - * This file demonstrates and tests the stellar-explorer utilities - */ - -import { - getTransactionUrl, - getAccountUrl, - getContractUrl, - getStellarNetwork, - getAvailableExplorers, - isValidStellarAddress, - isValidStellarTxHash, - isValidStellarContractId, - type StellarNetwork, -} from '../lib/utils/stellar-explorer'; - -// Test data -const testTxHash = 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef1234567890'; -const testAddress = 'GD1234567890ABCDEF1234567890ABCDEF12345678'; -const testContractId = 'C1234567890ABCDEF1234567890ABCDEF12345678'; - -console.log('šŸš€ Testing Stellar Explorer Integration\n'); - -// Test 1: Available explorers -console.log('1. Available Explorers:'); -const explorers = getAvailableExplorers(); -console.log(' ', explorers); - -// Test 2: Network detection -console.log('\n2. Network Detection:'); -console.log(' Test address network:', getStellarNetwork(testAddress)); -console.log(' Test tx hash network:', getStellarNetwork(testTxHash)); - -// Test 3: URL generation -console.log('\n3. URL Generation:'); -console.log(' Transaction URL:', getTransactionUrl(testTxHash)); -console.log(' Account URL:', getAccountUrl(testAddress)); -console.log(' Contract URL:', getContractUrl(testContractId)); - -// Test 4: Different networks -console.log('\n4. Network-specific URLs:'); -console.log(' Testnet Transaction URL:', getTransactionUrl(testTxHash, 'testnet')); -console.log(' Mainnet Account URL:', getAccountUrl(testAddress, 'mainnet')); - -// Test 5: Different explorers -console.log('\n5. Different Explorers:'); -console.log(' Stellar Expert Transaction:', getTransactionUrl(testTxHash, 'mainnet', 'stellar.expert')); -console.log(' Stellar Chain Account:', getAccountUrl(testAddress, 'mainnet', 'stellarchain.io')); - -// Test 6: Validation -console.log('\n6. Validation Tests:'); -console.log(' Valid address:', isValidStellarAddress(testAddress)); -console.log(' Invalid address:', isValidStellarAddress('invalid')); -console.log(' Valid tx hash:', isValidStellarTxHash(testTxHash)); -console.log(' Invalid tx hash:', isValidStellarTxHash('invalid')); -console.log(' Valid contract ID:', isValidStellarContractId(testContractId)); -console.log(' Invalid contract ID:', isValidStellarContractId('invalid')); - -// Test 7: Error handling -console.log('\n7. Error Handling:'); -try { - getTransactionUrl(''); -} catch (error) { - console.log(' Empty tx hash error:', error.message); -} - -try { - getAccountUrl(''); -} catch (error) { - console.log(' Empty address error:', error.message); -} - -console.log('\nāœ… All tests completed!'); - -// Export for potential use in components -export { - testTxHash, - testAddress, - testContractId, -}; From a924fd2b9bb6ba0c2da717e900abb1dd8d60aa45 Mon Sep 17 00:00:00 2001 From: Collins Ikechukwu Date: Mon, 30 Mar 2026 20:02:15 +0100 Subject: [PATCH 3/4] feat: add comprehensive platform documentation and local configuration files --- lib/utils/stellar-explorer.ts | 160 ++++++++++++++++------------------ 1 file changed, 73 insertions(+), 87 deletions(-) diff --git a/lib/utils/stellar-explorer.ts b/lib/utils/stellar-explorer.ts index 81758780..6445dd79 100644 --- a/lib/utils/stellar-explorer.ts +++ b/lib/utils/stellar-explorer.ts @@ -3,103 +3,103 @@ * * Provides URL generation for Stellar explorers to link transactions, * accounts, and contracts for transparency and verification. + * + * Uses @stellar/stellar-sdk StrKey for proper checksum validation. */ -export type StellarNetwork = "mainnet" | "testnet"; +import { StrKey } from "@stellar/stellar-sdk"; + +export type StellarNetwork = "public" | "testnet"; export type ExplorerType = "transaction" | "account" | "contract"; export interface ExplorerConfig { name: string; baseUrl: string; - paths: { - transaction: string; - account: string; - contract: string; - }; + /** Path template per network: keys are "public" and "testnet" */ + networkPaths: Record< + StellarNetwork, + { + transaction: string; + account: string; + contract: string; + } + >; } -// Supported Stellar explorers +// Supported Stellar explorers with per-network path configuration const EXPLORERS: Record = { "stellar.expert": { name: "Stellar Expert", baseUrl: "https://stellar.expert", - paths: { - transaction: "/explorer/public/tx/", - account: "/explorer/public/account/", - contract: "/explorer/public/contract/", + networkPaths: { + public: { + transaction: "/explorer/public/tx/", + account: "/explorer/public/account/", + contract: "/explorer/public/contract/", + }, + testnet: { + transaction: "/explorer/testnet/tx/", + account: "/explorer/testnet/account/", + contract: "/explorer/testnet/contract/", + }, }, }, "stellarchain.io": { name: "Stellar Chain", baseUrl: "https://stellarchain.io", - paths: { - transaction: "/tx/", - account: "/account/", - contract: "/contract/", + networkPaths: { + public: { + transaction: "/tx/", + account: "/account/", + contract: "/contract/", + }, + testnet: { + transaction: "/tx/", + account: "/account/", + contract: "/contract/", + }, }, }, }; -// Default network settings -const DEFAULT_NETWORK: StellarNetwork = "mainnet"; const DEFAULT_EXPLORER = "stellar.expert"; /** * Returns the configured Stellar network from environment variable. - * @returns The network type (mainnet or testnet) + * Maps "mainnet" / "public" → "public", defaults to "testnet" otherwise. */ export function getStellarNetwork(): StellarNetwork { - // Use environment variable for network configuration - const envNetwork = process.env.NEXT_PUBLIC_STELLAR_NETWORK as StellarNetwork; - return envNetwork === "testnet" ? "testnet" : DEFAULT_NETWORK; + const raw = process.env.NEXT_PUBLIC_STELLAR_NETWORK || "testnet"; + return raw === "public" || raw === "mainnet" ? "public" : "testnet"; } /** - * Gets the appropriate base URL for the given network and explorer - * @param explorer - The explorer name - * @param network - The Stellar network - * @returns The base URL for the explorer + * Gets the base URL for the given network and explorer. + * stellarchain.io uses a subdomain for testnet; stellar.expert uses paths. */ function getExplorerBaseUrl(explorer: string, network: StellarNetwork): string { const config = EXPLORERS[explorer] || EXPLORERS[DEFAULT_EXPLORER]; - // Handle different testnet URL patterns for different explorers - if (network === "testnet") { - if (explorer === "stellarchain.io") { - return "https://testnet.stellarchain.io"; - } + if (network === "testnet" && explorer === "stellarchain.io") { + return "https://testnet.stellarchain.io"; } return config.baseUrl; } /** - * Gets the explorer paths adjusted for the network. - * stellar.expert uses /explorer/testnet/ for testnet instead of /explorer/public/ + * Gets the explorer paths for the given network. */ function getExplorerPaths( explorer: string, network: StellarNetwork, -): ExplorerConfig["paths"] { +): ExplorerConfig["networkPaths"]["public"] { const config = EXPLORERS[explorer] || EXPLORERS[DEFAULT_EXPLORER]; - - if (network === "testnet" && explorer === "stellar.expert") { - return { - transaction: "/explorer/testnet/tx/", - account: "/explorer/testnet/account/", - contract: "/explorer/testnet/contract/", - }; - } - - return config.paths; + return config.networkPaths[network]; } /** - * Generates a Stellar explorer URL for a transaction - * @param txHash - The transaction hash - * @param network - The Stellar network (optional, will be detected if not provided) - * @param explorer - The explorer to use (optional, defaults to stellar.expert) - * @returns The full URL to the transaction page + * Generates a Stellar explorer URL for a transaction. */ export function getTransactionUrl( txHash: string, @@ -110,19 +110,15 @@ export function getTransactionUrl( throw new Error("Transaction hash is required"); } - const detectedNetwork = network || getStellarNetwork(); - const paths = getExplorerPaths(explorer, detectedNetwork); - const baseUrl = getExplorerBaseUrl(explorer, detectedNetwork); + const resolvedNetwork = network || getStellarNetwork(); + const baseUrl = getExplorerBaseUrl(explorer, resolvedNetwork); + const paths = getExplorerPaths(explorer, resolvedNetwork); return `${baseUrl}${paths.transaction}${txHash}`; } /** - * Generates a Stellar explorer URL for an account - * @param address - The Stellar account address - * @param network - The Stellar network (optional, will be detected if not provided) - * @param explorer - The explorer to use (optional, defaults to stellar.expert) - * @returns The full URL to the account page + * Generates a Stellar explorer URL for an account. */ export function getAccountUrl( address: string, @@ -133,19 +129,15 @@ export function getAccountUrl( throw new Error("Account address is required"); } - const detectedNetwork = network || getStellarNetwork(); - const paths = getExplorerPaths(explorer, detectedNetwork); - const baseUrl = getExplorerBaseUrl(explorer, detectedNetwork); + const resolvedNetwork = network || getStellarNetwork(); + const baseUrl = getExplorerBaseUrl(explorer, resolvedNetwork); + const paths = getExplorerPaths(explorer, resolvedNetwork); return `${baseUrl}${paths.account}${address}`; } /** - * Generates a Stellar explorer URL for a contract - * @param contractId - The Stellar contract ID - * @param network - The Stellar network (optional, will be detected if not provided) - * @param explorer - The explorer to use (optional, defaults to stellar.expert) - * @returns The full URL to the contract page + * Generates a Stellar explorer URL for a contract. */ export function getContractUrl( contractId: string, @@ -156,56 +148,50 @@ export function getContractUrl( throw new Error("Contract ID is required"); } - const detectedNetwork = network || getStellarNetwork(); - const paths = getExplorerPaths(explorer, detectedNetwork); - const baseUrl = getExplorerBaseUrl(explorer, detectedNetwork); + const resolvedNetwork = network || getStellarNetwork(); + const baseUrl = getExplorerBaseUrl(explorer, resolvedNetwork); + const paths = getExplorerPaths(explorer, resolvedNetwork); return `${baseUrl}${paths.contract}${contractId}`; } /** - * Gets a list of available explorers - * @returns Array of available explorer names + * Gets a list of available explorers. */ export function getAvailableExplorers(): string[] { return Object.keys(EXPLORERS); } /** - * Gets the configuration for a specific explorer - * @param explorer - The explorer name - * @returns The explorer configuration + * Gets the configuration for a specific explorer. */ export function getExplorerConfig(explorer: string): ExplorerConfig | null { return EXPLORERS[explorer] || null; } /** - * Validates if a string is a valid Stellar address - * @param address - The address to validate - * @returns True if valid, false otherwise + * Validates if a string is a valid Stellar account address (G-address). + * Uses StrKey checksum validation from @stellar/stellar-sdk. */ export function isValidStellarAddress(address: string): boolean { - // Basic validation - Stellar addresses start with 'G' and are 56 characters long - return /^G[A-Z2-7]{55}$/.test(address); + if (!address || typeof address !== "string") return false; + return StrKey.isValidEd25519PublicKey(address.trim()); } /** - * Validates if a string is a valid Stellar transaction hash - * @param hash - The hash to validate - * @returns True if valid, false otherwise + * Validates if a string is a valid Stellar transaction hash. + * Transaction hashes are 64-character lowercase hex strings. */ export function isValidStellarTxHash(hash: string): boolean { - // Basic validation - Stellar transaction hashes are 64 character hex strings - return /^[a-f0-9]{64}$/i.test(hash); + if (!hash || typeof hash !== "string") return false; + return /^[a-f0-9]{64}$/i.test(hash.trim()); } /** - * Validates if a string is a valid Stellar contract ID - * @param contractId - The contract ID to validate - * @returns True if valid, false otherwise + * Validates if a string is a valid Stellar/Soroban contract ID (C-address). + * Uses StrKey checksum validation from @stellar/stellar-sdk. */ export function isValidStellarContractId(contractId: string): boolean { - // Soroban contract IDs start with 'C' and are 56 characters long - return /^C[A-Z2-7]{55}$/.test(contractId); + if (!contractId || typeof contractId !== "string") return false; + return StrKey.isValidContract(contractId.trim()); } From 245b6352747b4342a09b14f725bfa1b140f43b9e Mon Sep 17 00:00:00 2001 From: Collins Ikechukwu Date: Mon, 30 Mar 2026 20:06:47 +0100 Subject: [PATCH 4/4] feat: add platform documentation and local configuration while refining smart wallet environment variable handling --- lib/smart-wallet/config.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/smart-wallet/config.ts b/lib/smart-wallet/config.ts index 7f252e7a..60d0c0ea 100644 --- a/lib/smart-wallet/config.ts +++ b/lib/smart-wallet/config.ts @@ -1,11 +1,11 @@ import { Networks } from "@stellar/stellar-sdk"; -const requireEnv = (name: string, fallback: string) => { +const requireEnv = (name: string, fallback?: string) => { const value = process.env[name]; - if (process.env.NODE_ENV === "production" && !value) { + if (!value && !fallback) { throw new Error(`Missing required environment variable: ${name}`); } - return value || fallback; + return value || fallback || ""; }; export const SMART_WALLET_CONFIG = {