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.
- package/.activity_counter +1 -0
- package/.gitattributes +3 -0
- package/.vscode/settings.json +4 -0
- package/.vscode/tasks.json +19 -0
- package/CHANGELOG.md +1 -0
- package/Clarinet.toml +56 -0
- package/Clarinet.toml.backup +174 -0
- package/Clarinet.toml.old +146 -0
- package/DEPLOYMENT_RESULTS.md +160 -0
- package/README.md +344 -0
- package/TODO.md +34 -0
- package/contracts/CoreMarketPlace.clar +227 -0
- package/contracts/DisputeResolution_clar.clar +265 -0
- package/contracts/EscrowService.clar +171 -0
- package/contracts/UserProfile.clar +280 -0
- package/contracts/ft-trait.clar +24 -0
- package/contracts/token.clar +178 -0
- package/costs-reports.json +76026 -0
- package/deployments/default.mainnet-plan.yaml +67 -0
- package/deployments/default.simnet-plan.yaml +105 -0
- package/deployments/default.testnet-plan.yaml +67 -0
- package/deployments/new-contracts.testnet-plan.yaml +32 -0
- package/frontend/README.md +10 -0
- package/frontend/components.json +22 -0
- package/frontend/dist/assets/index-BacuuL66.css +1 -0
- package/frontend/dist/assets/index-jryypd5B.js +194 -0
- package/frontend/dist/favicon.png +0 -0
- package/frontend/dist/index.html +15 -0
- package/frontend/dist/manifest.json +15 -0
- package/frontend/dist/vite.svg +1 -0
- package/frontend/empty-mock.js +1 -0
- package/frontend/eslint.config.js +23 -0
- package/frontend/eslint.config.mjs +25 -0
- package/frontend/index.html +14 -0
- package/frontend/next.config.ts +17 -0
- package/frontend/package-lock.json +14740 -0
- package/frontend/package.json +56 -0
- package/frontend/postcss.config.js +5 -0
- package/frontend/postcss.config.mjs +5 -0
- package/frontend/public/favicon.png +0 -0
- package/frontend/public/file.svg +1 -0
- package/frontend/public/globe.svg +1 -0
- package/frontend/public/manifest.json +15 -0
- package/frontend/public/next.svg +1 -0
- package/frontend/public/vercel.svg +1 -0
- package/frontend/public/vite.svg +1 -0
- package/frontend/public/window.svg +1 -0
- package/frontend/src/App.css +42 -0
- package/frontend/src/App.tsx +177 -0
- package/frontend/src/app/about/page.tsx +208 -0
- package/frontend/src/app/favicon.ico +0 -0
- package/frontend/src/app/globals.css +129 -0
- package/frontend/src/app/help/page.tsx +167 -0
- package/frontend/src/app/how-it-works/page.tsx +274 -0
- package/frontend/src/app/layout.tsx +55 -0
- package/frontend/src/app/marketplace/page.tsx +324 -0
- package/frontend/src/app/my-listings/page.tsx +318 -0
- package/frontend/src/app/page.tsx +15 -0
- package/frontend/src/assets/react.svg +1 -0
- package/frontend/src/components/ConfirmDialog.tsx +54 -0
- package/frontend/src/components/CreateListingForm.tsx +231 -0
- package/frontend/src/components/ErrorBoundary.tsx +73 -0
- package/frontend/src/components/FilterPanel.tsx +10 -0
- package/frontend/src/components/Footer.tsx +100 -0
- package/frontend/src/components/Header.tsx +268 -0
- package/frontend/src/components/ImageUpload.tsx +147 -0
- package/frontend/src/components/LandingPage.tsx +322 -0
- package/frontend/src/components/ListingCard.tsx +154 -0
- package/frontend/src/components/LoadingSkeleton.tsx +44 -0
- package/frontend/src/components/MobileNav.tsx +89 -0
- package/frontend/src/components/NotificationBell.tsx +8 -0
- package/frontend/src/components/NotificationPanel.tsx +14 -0
- package/frontend/src/components/README.md +14 -0
- package/frontend/src/components/SearchBar.tsx +10 -0
- package/frontend/src/components/TestnetBanner.tsx +29 -0
- package/frontend/src/components/ThemeToggle.tsx +32 -0
- package/frontend/src/components/__tests__/Header.test.tsx +70 -0
- package/frontend/src/components/__tests__/ListingCard.test.tsx +86 -0
- package/frontend/src/components/providers/ThemeProvider.tsx +9 -0
- package/frontend/src/components/ui/alert-dialog.tsx +141 -0
- package/frontend/src/components/ui/avatar.tsx +53 -0
- package/frontend/src/components/ui/badge.tsx +46 -0
- package/frontend/src/components/ui/button.tsx +60 -0
- package/frontend/src/components/ui/card.tsx +92 -0
- package/frontend/src/components/ui/dialog.tsx +143 -0
- package/frontend/src/components/ui/dropdown-menu.tsx +257 -0
- package/frontend/src/components/ui/input.tsx +21 -0
- package/frontend/src/components/ui/label.tsx +24 -0
- package/frontend/src/components/ui/select.tsx +187 -0
- package/frontend/src/components/ui/sonner.tsx +40 -0
- package/frontend/src/components/ui/textarea.tsx +18 -0
- package/frontend/src/context/README.md +27 -0
- package/frontend/src/index.css +166 -0
- package/frontend/src/lib/notificationEvents.ts +10 -0
- package/frontend/src/lib/notificationStore.ts +13 -0
- package/frontend/src/lib/notifications.ts +13 -0
- package/frontend/src/lib/search.ts +28 -0
- package/frontend/src/lib/stacks.ts +189 -0
- package/frontend/src/lib/utils.ts +6 -0
- package/frontend/src/main.tsx +10 -0
- package/frontend/src/test/setup.ts +23 -0
- package/frontend/src/types.d.ts +9 -0
- package/frontend/tsconfig.app.json +28 -0
- package/frontend/tsconfig.json +41 -0
- package/frontend/tsconfig.node.json +26 -0
- package/frontend/vercel.json +4 -0
- package/frontend/vite.config.ts +6 -0
- package/frontend/vitest.config.ts +17 -0
- package/lcov.info +31338 -0
- package/mainnetcontracts.md +16 -0
- package/package.json +53 -0
- package/scripts/auto-activity.sh +9 -0
- package/scripts/cancel-pending.ts +67 -0
- package/scripts/check-balances.ts +23 -0
- package/scripts/distribute-evenly.ts +56 -0
- package/scripts/drain-accounts.ts +70 -0
- package/scripts/fund-accounts.ts +88 -0
- package/scripts/fund-active.ts +59 -0
- package/scripts/fund-unfunded.ts +88 -0
- package/scripts/generate-activity.ts +181 -0
- package/scripts/git-activity-generator.ts +154 -0
- package/scripts/mobile-server.ts +123 -0
- package/settings/Devnet.toml +155 -0
- package/settings/Mainnet.toml +7 -0
- package/settings/Testnet.toml +9 -0
- package/tests/CoreMarketPlace.fuzz.test.ts +435 -0
- package/tests/CoreMarketPlace.test.ts +564 -0
- package/tsconfig.json +26 -0
- 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,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,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,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,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
|
+
});
|