strade-stx 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/.activity_counter +1 -0
  2. package/.gitattributes +3 -0
  3. package/.vscode/settings.json +4 -0
  4. package/.vscode/tasks.json +19 -0
  5. package/CHANGELOG.md +1 -0
  6. package/Clarinet.toml +56 -0
  7. package/Clarinet.toml.backup +174 -0
  8. package/Clarinet.toml.old +146 -0
  9. package/DEPLOYMENT_RESULTS.md +160 -0
  10. package/README.md +344 -0
  11. package/TODO.md +34 -0
  12. package/contracts/CoreMarketPlace.clar +227 -0
  13. package/contracts/DisputeResolution_clar.clar +265 -0
  14. package/contracts/EscrowService.clar +171 -0
  15. package/contracts/UserProfile.clar +280 -0
  16. package/contracts/ft-trait.clar +24 -0
  17. package/contracts/token.clar +178 -0
  18. package/costs-reports.json +76026 -0
  19. package/deployments/default.mainnet-plan.yaml +67 -0
  20. package/deployments/default.simnet-plan.yaml +105 -0
  21. package/deployments/default.testnet-plan.yaml +67 -0
  22. package/deployments/new-contracts.testnet-plan.yaml +32 -0
  23. package/frontend/README.md +10 -0
  24. package/frontend/components.json +22 -0
  25. package/frontend/dist/assets/index-BacuuL66.css +1 -0
  26. package/frontend/dist/assets/index-jryypd5B.js +194 -0
  27. package/frontend/dist/favicon.png +0 -0
  28. package/frontend/dist/index.html +15 -0
  29. package/frontend/dist/manifest.json +15 -0
  30. package/frontend/dist/vite.svg +1 -0
  31. package/frontend/empty-mock.js +1 -0
  32. package/frontend/eslint.config.js +23 -0
  33. package/frontend/eslint.config.mjs +25 -0
  34. package/frontend/index.html +14 -0
  35. package/frontend/next.config.ts +17 -0
  36. package/frontend/package-lock.json +14740 -0
  37. package/frontend/package.json +56 -0
  38. package/frontend/postcss.config.js +5 -0
  39. package/frontend/postcss.config.mjs +5 -0
  40. package/frontend/public/favicon.png +0 -0
  41. package/frontend/public/file.svg +1 -0
  42. package/frontend/public/globe.svg +1 -0
  43. package/frontend/public/manifest.json +15 -0
  44. package/frontend/public/next.svg +1 -0
  45. package/frontend/public/vercel.svg +1 -0
  46. package/frontend/public/vite.svg +1 -0
  47. package/frontend/public/window.svg +1 -0
  48. package/frontend/src/App.css +42 -0
  49. package/frontend/src/App.tsx +177 -0
  50. package/frontend/src/app/about/page.tsx +208 -0
  51. package/frontend/src/app/favicon.ico +0 -0
  52. package/frontend/src/app/globals.css +129 -0
  53. package/frontend/src/app/help/page.tsx +167 -0
  54. package/frontend/src/app/how-it-works/page.tsx +274 -0
  55. package/frontend/src/app/layout.tsx +55 -0
  56. package/frontend/src/app/marketplace/page.tsx +324 -0
  57. package/frontend/src/app/my-listings/page.tsx +318 -0
  58. package/frontend/src/app/page.tsx +15 -0
  59. package/frontend/src/assets/react.svg +1 -0
  60. package/frontend/src/components/ConfirmDialog.tsx +54 -0
  61. package/frontend/src/components/CreateListingForm.tsx +231 -0
  62. package/frontend/src/components/ErrorBoundary.tsx +73 -0
  63. package/frontend/src/components/FilterPanel.tsx +10 -0
  64. package/frontend/src/components/Footer.tsx +100 -0
  65. package/frontend/src/components/Header.tsx +268 -0
  66. package/frontend/src/components/ImageUpload.tsx +147 -0
  67. package/frontend/src/components/LandingPage.tsx +322 -0
  68. package/frontend/src/components/ListingCard.tsx +154 -0
  69. package/frontend/src/components/LoadingSkeleton.tsx +44 -0
  70. package/frontend/src/components/MobileNav.tsx +89 -0
  71. package/frontend/src/components/NotificationBell.tsx +8 -0
  72. package/frontend/src/components/NotificationPanel.tsx +14 -0
  73. package/frontend/src/components/README.md +14 -0
  74. package/frontend/src/components/SearchBar.tsx +10 -0
  75. package/frontend/src/components/TestnetBanner.tsx +29 -0
  76. package/frontend/src/components/ThemeToggle.tsx +32 -0
  77. package/frontend/src/components/__tests__/Header.test.tsx +70 -0
  78. package/frontend/src/components/__tests__/ListingCard.test.tsx +86 -0
  79. package/frontend/src/components/providers/ThemeProvider.tsx +9 -0
  80. package/frontend/src/components/ui/alert-dialog.tsx +141 -0
  81. package/frontend/src/components/ui/avatar.tsx +53 -0
  82. package/frontend/src/components/ui/badge.tsx +46 -0
  83. package/frontend/src/components/ui/button.tsx +60 -0
  84. package/frontend/src/components/ui/card.tsx +92 -0
  85. package/frontend/src/components/ui/dialog.tsx +143 -0
  86. package/frontend/src/components/ui/dropdown-menu.tsx +257 -0
  87. package/frontend/src/components/ui/input.tsx +21 -0
  88. package/frontend/src/components/ui/label.tsx +24 -0
  89. package/frontend/src/components/ui/select.tsx +187 -0
  90. package/frontend/src/components/ui/sonner.tsx +40 -0
  91. package/frontend/src/components/ui/textarea.tsx +18 -0
  92. package/frontend/src/context/README.md +27 -0
  93. package/frontend/src/index.css +166 -0
  94. package/frontend/src/lib/notificationEvents.ts +10 -0
  95. package/frontend/src/lib/notificationStore.ts +13 -0
  96. package/frontend/src/lib/notifications.ts +13 -0
  97. package/frontend/src/lib/search.ts +28 -0
  98. package/frontend/src/lib/stacks.ts +189 -0
  99. package/frontend/src/lib/utils.ts +6 -0
  100. package/frontend/src/main.tsx +10 -0
  101. package/frontend/src/test/setup.ts +23 -0
  102. package/frontend/src/types.d.ts +9 -0
  103. package/frontend/tsconfig.app.json +28 -0
  104. package/frontend/tsconfig.json +41 -0
  105. package/frontend/tsconfig.node.json +26 -0
  106. package/frontend/vercel.json +4 -0
  107. package/frontend/vite.config.ts +6 -0
  108. package/frontend/vitest.config.ts +17 -0
  109. package/lcov.info +31338 -0
  110. package/mainnetcontracts.md +16 -0
  111. package/package.json +53 -0
  112. package/scripts/auto-activity.sh +9 -0
  113. package/scripts/cancel-pending.ts +67 -0
  114. package/scripts/check-balances.ts +23 -0
  115. package/scripts/distribute-evenly.ts +56 -0
  116. package/scripts/drain-accounts.ts +70 -0
  117. package/scripts/fund-accounts.ts +88 -0
  118. package/scripts/fund-active.ts +59 -0
  119. package/scripts/fund-unfunded.ts +88 -0
  120. package/scripts/generate-activity.ts +181 -0
  121. package/scripts/git-activity-generator.ts +154 -0
  122. package/scripts/mobile-server.ts +123 -0
  123. package/settings/Devnet.toml +155 -0
  124. package/settings/Mainnet.toml +7 -0
  125. package/settings/Testnet.toml +9 -0
  126. package/tests/CoreMarketPlace.fuzz.test.ts +435 -0
  127. package/tests/CoreMarketPlace.test.ts +564 -0
  128. package/tsconfig.json +26 -0
  129. package/vitest.config.js +49 -0
@@ -0,0 +1,27 @@
1
+ # Add your context providers here
2
+
3
+ Example context structure:
4
+
5
+ ```tsx
6
+ import { createContext, useContext, ReactNode } from 'react';
7
+
8
+ interface YourContextType {
9
+ // Define your context type
10
+ }
11
+
12
+ const YourContext = createContext<YourContextType | undefined>(undefined);
13
+
14
+ export function YourProvider({ children }: { children: ReactNode }) {
15
+ return (
16
+ <YourContext.Provider value={{}}>
17
+ {children}
18
+ </YourContext.Provider>
19
+ );
20
+ }
21
+
22
+ export function useYourContext() {
23
+ const context = useContext(YourContext);
24
+ if (!context) throw new Error('useYourContext must be used within YourProvider');
25
+ return context;
26
+ }
27
+ ```
@@ -0,0 +1,166 @@
1
+ @import "tailwindcss";
2
+ @import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&family=Outfit:wght@100;200;300;400;500;600;700;800;900&family=Syne:wght@400;500;600;700;800&display=swap');
3
+
4
+ /* Tailwind v4 Theme Configuration */
5
+ @theme {
6
+ /* Colors - Nu White Theme */
7
+ --color-app-bg: #FCFCFD;
8
+ --color-app-surface: #FFFFFF;
9
+ --color-app-card: #FFFFFF;
10
+ --color-app-border: #E5E7EB;
11
+ --color-app-hover: #F9FAFB;
12
+
13
+ /* Accents */
14
+ --color-accent-indigo: #4F46E5;
15
+ --color-accent-indigo-hover: #4338CA;
16
+ --color-accent-gold: #D97706;
17
+ --color-accent-gold-hover: #B45309;
18
+ --color-accent-emerald: #059669;
19
+
20
+ /* Text */
21
+ --color-text-main: #111827;
22
+ --color-text-dim: #4B5563;
23
+ --color-text-pale: #9CA3AF;
24
+
25
+ /* Fonts */
26
+ --font-serif: 'Cormorant Garamond', serif;
27
+ --font-sans: 'Outfit', sans-serif;
28
+ --font-display: 'Syne', sans-serif;
29
+
30
+ /* Shadows - Soft & Premium */
31
+ --shadow-premium: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
32
+ --shadow-floating: 0 20px 25px -5px rgba(0, 0, 0, 0.05), 0 10px 10px -5px rgba(0, 0, 0, 0.02);
33
+ --shadow-glow-indigo: 0 0 20px rgba(79, 70, 229, 0.1);
34
+
35
+ /* Animations */
36
+ --animate-reveal: reveal 0.8s cubic-bezier(0.16, 1, 0.3, 1);
37
+ --animate-float: float 6s ease-in-out infinite;
38
+ --animate-soft-pulse: softPulse 3s ease-in-out infinite;
39
+ }
40
+
41
+ @keyframes reveal {
42
+ 0% { transform: translateY(20px); opacity: 0; }
43
+ 100% { transform: translateY(0); opacity: 1; }
44
+ }
45
+
46
+ @keyframes float {
47
+ 0%, 100% { transform: translateY(0); }
48
+ 50% { transform: translateY(-10px); }
49
+ }
50
+
51
+ @keyframes softPulse {
52
+ 0%, 100% { opacity: 1; transform: scale(1); }
53
+ 50% { opacity: 0.8; transform: scale(0.98); }
54
+ }
55
+
56
+ /* Base Styles */
57
+ html {
58
+ scroll-behavior: smooth;
59
+ }
60
+
61
+ body {
62
+ margin: 0;
63
+ background-color: var(--color-app-bg);
64
+ color: var(--color-text-main);
65
+ font-family: var(--font-sans);
66
+ overflow-x: hidden;
67
+ -webkit-font-smoothing: antialiased;
68
+ -moz-osx-font-smoothing: grayscale;
69
+ }
70
+
71
+ h1, h2, h3, .font-serif {
72
+ font-family: var(--font-serif);
73
+ }
74
+
75
+ .font-display {
76
+ font-family: var(--font-display);
77
+ }
78
+
79
+ /* Glassmorphism */
80
+ .glass {
81
+ background: rgba(255, 255, 255, 0.85);
82
+ backdrop-filter: blur(16px);
83
+ -webkit-backdrop-filter: blur(16px);
84
+ border: 1px solid rgba(255, 255, 255, 0.5);
85
+ }
86
+
87
+ /* Grid Background - Subtle */
88
+ .grid-subtle {
89
+ background-image: radial-gradient(#E5E7EB 1px, transparent 1px);
90
+ background-size: 40px 40px;
91
+ }
92
+
93
+ /* Custom Scrollbar */
94
+ *::-webkit-scrollbar {
95
+ width: 6px;
96
+ height: 6px;
97
+ }
98
+
99
+ *::-webkit-scrollbar-track {
100
+ background: var(--color-app-bg);
101
+ }
102
+
103
+ *::-webkit-scrollbar-thumb {
104
+ background: var(--color-app-border);
105
+ border-radius: 10px;
106
+ }
107
+
108
+ *::-webkit-scrollbar-thumb:hover {
109
+ background: var(--color-text-pale);
110
+ }
111
+
112
+ /* Hide scrollbar utility */
113
+ .no-scrollbar::-webkit-scrollbar {
114
+ display: none;
115
+ }
116
+ .no-scrollbar {
117
+ -ms-overflow-style: none;
118
+ scrollbar-width: none;
119
+ }
120
+
121
+ /* Buttons */
122
+ .btn-primary {
123
+ @apply px-8 py-3.5 bg-accent-indigo text-white font-semibold rounded-full transition-all duration-300 hover:bg-accent-indigo-hover hover:shadow-floating active:scale-95 disabled:opacity-50 disabled:pointer-events-none;
124
+ }
125
+
126
+ .btn-secondary {
127
+ @apply px-8 py-3.5 bg-white text-accent-indigo border border-app-border font-semibold rounded-full transition-all duration-300 hover:bg-app-hover hover:shadow-premium active:scale-95 disabled:opacity-50;
128
+ }
129
+
130
+ .btn-ghost {
131
+ @apply px-4 py-2.5 text-text-dim hover:text-accent-indigo hover:bg-app-hover rounded-xl transition-all duration-200;
132
+ }
133
+
134
+ /* Cards */
135
+ .card-premium {
136
+ @apply bg-white border border-app-border rounded-3xl p-8 transition-all duration-500 hover:shadow-floating hover:border-accent-indigo/20;
137
+ }
138
+
139
+ /* Form Elements */
140
+ .input-premium {
141
+ @apply w-full px-5 py-4 bg-white border border-app-border rounded-xl focus:outline-none focus:ring-2 focus:ring-accent-indigo/10 focus:border-accent-indigo transition-all duration-200 placeholder:text-text-pale text-text-main font-medium;
142
+ }
143
+
144
+ /* Animations Helpers */
145
+ .stagger-reveal > * {
146
+ opacity: 0;
147
+ animation: reveal 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
148
+ }
149
+
150
+ .stagger-1 { animation-delay: 0.1s; }
151
+ .stagger-2 { animation-delay: 0.2s; }
152
+ .stagger-3 { animation-delay: 0.3s; }
153
+ .stagger-4 { animation-delay: 0.4s; }
154
+ .stagger-5 { animation-delay: 0.5s; }
155
+
156
+ /* Focus visible for accessibility */
157
+ :focus-visible {
158
+ outline: 2px solid var(--color-accent-indigo);
159
+ outline-offset: 2px;
160
+ }
161
+
162
+ /* Selection styling */
163
+ ::selection {
164
+ background-color: rgba(79, 70, 229, 0.1);
165
+ color: var(--color-accent-indigo);
166
+ }
@@ -0,0 +1,10 @@
1
+ // events
2
+ // purchase events
3
+ // escrow events
4
+ // dispute events
5
+ // listing events
6
+ // wire purchase
7
+ // wire escrow
8
+ // wire dispute
9
+ // polling
10
+ // cleanup
@@ -0,0 +1,13 @@
1
+ // store
2
+ // state
3
+ // addNotification
4
+ // markAsRead
5
+ // markAllAsRead
6
+ // remove
7
+ // clearAll
8
+ // persist
9
+ // isOpen
10
+ // togglePanel
11
+ // dedup
12
+ // max 50
13
+ // shallow
@@ -0,0 +1,13 @@
1
+ // notifications
2
+ export type Notification = { id: string; type: string; message: string; read: boolean; createdAt: number; };
3
+ export function createNotification(type: string, message: string): Notification { return { id: crypto.randomUUID(), type, message, read: false, createdAt: Date.now() }; }
4
+ export function markAsRead(n: Notification): Notification { return { ...n, read: true }; }
5
+ export function markAllAsRead(ns: Notification[]) { return ns.map(markAsRead); }
6
+ export function clearNotification(ns: Notification[], id: string) { return ns.filter(n => n.id !== id); }
7
+ export function clearAllNotifications() { return []; }
8
+ export function getUnreadCount(ns: Notification[]) { return ns.filter(n => !n.read).length; }
9
+ export const NOTIF_TYPES = { PURCHASE: 'purchase', ESCROW: 'escrow', DISPUTE: 'dispute', LISTING: 'listing' } as const;
10
+ // localStorage
11
+ export function loadNotifications(): Notification[] { try { return JSON.parse(localStorage.getItem('notifications') || '[]'); } catch { return []; } }
12
+ export function saveNotifications(ns: Notification[]) { localStorage.setItem('notifications', JSON.stringify(ns)); }
13
+ // unit tests
@@ -0,0 +1,28 @@
1
+ // search utilities
2
+ export type SearchFilters = { query?: string; minPrice?: number; maxPrice?: number; status?: string; category?: string };
3
+ export function filterListings(listings: any[], filters: SearchFilters) { return listings; }
4
+ // filter by query text
5
+ // filter by min price
6
+ // filter by max price
7
+ // filter by status
8
+ // filter by category
9
+ export function sortListings(listings: any[], by: string) { return listings; }
10
+ // sort price asc
11
+ // sort price desc
12
+ // sort newest
13
+ // sort oldest
14
+ export function debounce(fn: Function, ms: number) { let t: any; return (...a: any[]) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; }
15
+ // validate price range
16
+ export function paginate(items: any[], page: number, size: number) { return items.slice((page-1)*size, page*size); }
17
+ export function getTotalPages(total: number, size: number) { return Math.ceil(total / size); }
18
+ // count results
19
+ export function isEmpty(arr: any[]) { return arr.length === 0; }
20
+ /** Filter listings by provided filters */
21
+ // test stubs
22
+ // paginate tests
23
+ // moved types
24
+ // exports
25
+ // undefined guard
26
+ // trim
27
+ // short-circuit
28
+ // cleanup
@@ -0,0 +1,189 @@
1
+ import { STACKS_TESTNET } from '@stacks/network';
2
+ import { AppConfig, UserSession } from '@stacks/connect';
3
+ import { fetchCallReadOnlyFunction, cvToValue, ClarityType, uintCV } from '@stacks/transactions';
4
+
5
+ export const network = STACKS_TESTNET;
6
+ export const appConfig = new AppConfig(['store_write', 'publish_data']);
7
+ export const userSession = new UserSession({ appConfig });
8
+
9
+ export const contractAddress = 'STGEE2D7NV4RJC1MHK59AN83PEN0CBBEXNG4QQVF';
10
+
11
+ // Contract addresses for deployed contracts
12
+ export const CONTRACTS = {
13
+ CoreMarketPlace: `${contractAddress}.CoreMarketPlace`,
14
+ EscrowService: `${contractAddress}.EscrowService`,
15
+ UserProfile: `${contractAddress}.UserProfile`,
16
+ DisputeResolution: `${contractAddress}.DisputeResolution_clar`,
17
+ Token: `${contractAddress}.token`,
18
+ };
19
+
20
+ // Default contract for marketplace operations
21
+ export const contractName = 'CoreMarketPlace';
22
+
23
+ export interface Listing {
24
+ listingId: number;
25
+ seller: string;
26
+ name: string;
27
+ description: string;
28
+ price: number;
29
+ status: string;
30
+ createdAt: number; // block height
31
+ expiresAt: number; // block height
32
+ imageUrl?: string;
33
+ }
34
+
35
+ // Convert Stacks block height to approximate timestamp
36
+ // Stacks blocks are ~10 minutes apart, starting from Bitcoin block ~666050 (Jan 2021)
37
+ export const blockHeightToTimestamp = (blockHeight: number): number => {
38
+ // Approximate: Stacks mainnet launched around Jan 2021 at Bitcoin block 666050
39
+ // For testnet, we use a similar approximation
40
+ const STACKS_LAUNCH_TIMESTAMP = 1611057600000; // Jan 2021 in milliseconds
41
+ const AVERAGE_BLOCK_TIME_MS = 10 * 60 * 1000; // 10 minutes in milliseconds
42
+
43
+ return STACKS_LAUNCH_TIMESTAMP + (blockHeight * AVERAGE_BLOCK_TIME_MS);
44
+ };
45
+
46
+ export const getListings = async (includeAll: boolean = false): Promise<Listing[]> => {
47
+ try {
48
+ const lastIdResult = await fetchCallReadOnlyFunction({
49
+ contractAddress,
50
+ contractName,
51
+ functionName: 'get-last-listing-id',
52
+ functionArgs: [],
53
+ network,
54
+ senderAddress: contractAddress,
55
+ });
56
+
57
+ const lastId = cvToValue(lastIdResult).value as number;
58
+ const listings: Listing[] = [];
59
+
60
+ for (let i = 1; i <= lastId; i++) {
61
+ const listingResult = await fetchCallReadOnlyFunction({
62
+ contractAddress,
63
+ contractName,
64
+ functionName: 'get-listing',
65
+ functionArgs: [uintCV(i)],
66
+ network,
67
+ senderAddress: contractAddress,
68
+ });
69
+
70
+ if (listingResult.type !== ClarityType.OptionalNone) {
71
+ const listingData = cvToValue(listingResult).value;
72
+ listings.push({
73
+ listingId: i,
74
+ seller: listingData.seller.value,
75
+ name: listingData.name.value,
76
+ description: listingData.description.value,
77
+ price: Number(listingData.price.value),
78
+ status: listingData.status.value,
79
+ createdAt: Number(listingData['created-at'].value),
80
+ expiresAt: Number(listingData['expires-at'].value),
81
+ });
82
+ }
83
+ }
84
+
85
+ // If includeAll is true, return all listings; otherwise filter for active only
86
+ return includeAll ? listings : listings.filter(listing => listing.status === 'active');
87
+ } catch (error) {
88
+ console.error('Error fetching listings:', error);
89
+ return [];
90
+ }
91
+ };
92
+
93
+ export const formatSTX = (microSTX: number): string => {
94
+ // Handle NaN, undefined, or invalid values
95
+ if (microSTX === null || microSTX === undefined || isNaN(microSTX) || microSTX < 0) {
96
+ return '0';
97
+ }
98
+ return (microSTX / 1000000).toFixed(6);
99
+ };
100
+
101
+ export const formatAddress = (address: string): string => {
102
+ return `${address.slice(0, 6)}...${address.slice(-4)}`;
103
+ };
104
+
105
+ // Helper function to decode hex-encoded string
106
+ const hexToString = (hex: string): string => {
107
+ let str = '';
108
+ for (let i = 0; i < hex.length; i += 2) {
109
+ str += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
110
+ }
111
+ return str;
112
+ };
113
+
114
+ // Check if wallet is connected (supports both old UserSession and new v8 storage)
115
+ export const isWalletConnected = (): boolean => {
116
+ if (typeof window === 'undefined') return false;
117
+
118
+ // Check old UserSession method
119
+ if (userSession.isUserSignedIn()) {
120
+ return true;
121
+ }
122
+
123
+ // Check v8 @stacks/connect storage
124
+ const stacksConnectData = localStorage.getItem('@stacks/connect');
125
+ if (stacksConnectData) {
126
+ try {
127
+ const decodedString = hexToString(stacksConnectData);
128
+ const connectData = JSON.parse(decodedString);
129
+ return !!(connectData?.addresses?.stx?.[0]?.address);
130
+ } catch (error) {
131
+ return false;
132
+ }
133
+ }
134
+
135
+ return false;
136
+ };
137
+
138
+ // Get the connected wallet address (supports both old UserSession and new v8 storage)
139
+ export const getConnectedAddress = (): string | null => {
140
+ if (typeof window === 'undefined') return null;
141
+
142
+ // Check old UserSession method
143
+ if (userSession.isUserSignedIn()) {
144
+ const userData = userSession.loadUserData();
145
+ return userData.profile?.stxAddress?.testnet || userData.profile?.stxAddress?.mainnet || null;
146
+ }
147
+
148
+ // Check v8 @stacks/connect storage
149
+ const stacksConnectData = localStorage.getItem('@stacks/connect');
150
+ if (stacksConnectData) {
151
+ try {
152
+ const decodedString = hexToString(stacksConnectData);
153
+ const connectData = JSON.parse(decodedString);
154
+ return connectData?.addresses?.stx?.[0]?.address || null;
155
+ } catch (error) {
156
+ return null;
157
+ }
158
+ }
159
+
160
+ return null;
161
+ };
162
+
163
+ export const getUserBalance = async (address: string): Promise<number> => {
164
+ try {
165
+ if (!address || typeof address !== 'string' || address.length < 10) {
166
+ return 0;
167
+ }
168
+ const response = await fetch(
169
+ `https://api.testnet.hiro.so/extended/v1/address/${address}/stx`
170
+ );
171
+
172
+ if (!response.ok) {
173
+ console.error('Balance API error:', response.status);
174
+ return 0;
175
+ }
176
+
177
+ const data = await response.json();
178
+
179
+ // Handle different API response formats
180
+ if (data.balance !== undefined) {
181
+ return Number(data.balance) || 0;
182
+ }
183
+
184
+ return 0;
185
+ } catch (error) {
186
+ console.error('Error fetching balance:', error);
187
+ return 0;
188
+ }
189
+ };
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -0,0 +1,10 @@
1
+ import { StrictMode } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import './index.css';
4
+ import App from './App.tsx';
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>
10
+ );
@@ -0,0 +1,23 @@
1
+ import '@testing-library/jest-dom';
2
+ import { vi } from 'vitest';
3
+
4
+ // Mock Next.js router
5
+ vi.mock('next/navigation', () => ({
6
+ useRouter: () => ({
7
+ push: vi.fn(),
8
+ replace: vi.fn(),
9
+ prefetch: vi.fn(),
10
+ }),
11
+ usePathname: vi.fn(() => '/'),
12
+ useSearchParams: () => new URLSearchParams(),
13
+ }));
14
+
15
+ // Mock Stacks connect
16
+ vi.mock('@stacks/connect', async () => {
17
+ return {
18
+ AppConfig: vi.fn(),
19
+ UserSession: vi.fn(),
20
+ authenticate: vi.fn(),
21
+ openContractCall: vi.fn(),
22
+ };
23
+ });
@@ -0,0 +1,9 @@
1
+ export {};
2
+
3
+ declare global {
4
+ interface Window {
5
+ ethereum?: {
6
+ request: (args: { method: string }) => Promise<string[]>;
7
+ };
8
+ }
9
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "ES2022",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "types": ["vite/client"],
9
+ "skipLibCheck": true,
10
+
11
+ /* Bundler mode */
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "moduleDetection": "force",
16
+ "noEmit": true,
17
+ "jsx": "react-jsx",
18
+
19
+ /* Linting */
20
+ "strict": true,
21
+ "noUnusedLocals": true,
22
+ "noUnusedParameters": true,
23
+ "erasableSyntaxOnly": true,
24
+ "noFallthroughCasesInSwitch": true,
25
+ "noUncheckedSideEffectImports": true
26
+ },
27
+ "include": ["src"]
28
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": [
5
+ "dom",
6
+ "dom.iterable",
7
+ "esnext"
8
+ ],
9
+ "allowJs": true,
10
+ "skipLibCheck": true,
11
+ "strict": true,
12
+ "esModuleInterop": true,
13
+ "module": "esnext",
14
+ "moduleResolution": "bundler",
15
+ "resolveJsonModule": true,
16
+ "isolatedModules": true,
17
+ "jsx": "react-jsx",
18
+ "incremental": true,
19
+ "plugins": [
20
+ {
21
+ "name": "next"
22
+ }
23
+ ],
24
+ "paths": {
25
+ "@/*": [
26
+ "./src/*"
27
+ ]
28
+ },
29
+ "noEmit": true
30
+ },
31
+ "include": [
32
+ "next-env.d.ts",
33
+ "**/*.ts",
34
+ "**/*.tsx",
35
+ ".next/types/**/*.ts",
36
+ ".next/dev/types/**/*.ts"
37
+ ],
38
+ "exclude": [
39
+ "node_modules"
40
+ ]
41
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "ES2023",
5
+ "lib": ["ES2023"],
6
+ "module": "ESNext",
7
+ "types": ["node"],
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+
17
+ /* Linting */
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "erasableSyntaxOnly": true,
22
+ "noFallthroughCasesInSwitch": true,
23
+ "noUncheckedSideEffectImports": true
24
+ },
25
+ "include": ["vite.config.ts"]
26
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "buildCommand": "npm run build",
3
+ "installCommand": "npm install"
4
+ }
@@ -0,0 +1,6 @@
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ })
@@ -0,0 +1,17 @@
1
+ import { defineConfig } from 'vitest/config';
2
+ import react from '@vitejs/plugin-react';
3
+ import path from 'path';
4
+
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ test: {
8
+ environment: 'jsdom',
9
+ globals: true,
10
+ setupFiles: ['./src/test/setup.ts'],
11
+ },
12
+ resolve: {
13
+ alias: {
14
+ '@': path.resolve(__dirname, './src'),
15
+ },
16
+ },
17
+ });