makestx-frontend 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.
@@ -0,0 +1,182 @@
1
+ import Head from 'next/head';
2
+ import { useState } from 'react';
3
+ import Navbar from '@/components/Navbar';
4
+ import { useWallet } from '@/hooks/useWallet';
5
+ import { useContractActions } from '@/hooks/useContract';
6
+ import { Zap, Info, CheckCircle2, Loader2, ExternalLink, Terminal } from 'lucide-react';
7
+ import { CONTRACT_ADDRESS, CONTRACT_NAME } from '@/hooks/useWallet';
8
+
9
+ const EXPLORER = 'https://explorer.hiro.so';
10
+
11
+ export default function Mint() {
12
+ const { isConnected, address, connect } = useWallet();
13
+ const { mintNFT } = useContractActions();
14
+ const [loading, setLoading] = useState(false);
15
+ const [txId, setTxId] = useState<string | null>(null);
16
+ const [previewSeed] = useState(() => Math.floor(Math.random() * 10000));
17
+
18
+ const handleMint = async () => {
19
+ if (!address) return;
20
+ setLoading(true);
21
+ try {
22
+ await mintNFT(address, () => setLoading(false));
23
+ } catch { setLoading(false); }
24
+ };
25
+
26
+ return (
27
+ <>
28
+ <Head>
29
+ <title>Mint — MakeSTX Cyberpunk</title>
30
+ <meta name="description" content="Mint your MakeSTX Cyberpunk NFT on Stacks. Free — gas only." />
31
+ </Head>
32
+
33
+ <div className="min-h-screen bg-ms-bg grid-bg">
34
+ <Navbar />
35
+
36
+ <main className="pt-28 pb-16 px-4 max-w-5xl mx-auto">
37
+
38
+ {/* Header */}
39
+ <div className="mb-12">
40
+ <p className="font-mono text-xs text-ms-muted tracking-widest mb-2">// mint_interface_v1.clar</p>
41
+ <h1 className="font-display font-black text-4xl md:text-5xl text-ms-text tracking-tight">
42
+ MINT YOUR <span className="gradient-text">CYBERPUNK</span>
43
+ </h1>
44
+ </div>
45
+
46
+ <div className="grid md:grid-cols-2 gap-12 items-start">
47
+
48
+ {/* NFT Preview */}
49
+ <div className="space-y-4">
50
+ <div className="relative aspect-square overflow-hidden bg-gradient-to-br from-ms-cyan/10 to-ms-magenta/10 border border-ms-border"
51
+ style={{ clipPath: 'polygon(0 0, calc(100% - 20px) 0, 100% 20px, 100% 100%, 20px 100%, 0 calc(100% - 20px))' }}>
52
+ <img
53
+ src={`https://api.dicebear.com/8.x/pixel-art/svg?seed=makestx${previewSeed}&backgroundColor=030308&colorful=true`}
54
+ alt="NFT Preview"
55
+ className="w-full h-full object-cover"
56
+ />
57
+ <div className="absolute inset-0 bg-gradient-to-t from-ms-bg/50 to-transparent" />
58
+
59
+ {/* Corner accents */}
60
+ <div className="absolute top-0 left-0 w-8 h-px bg-ms-cyan" />
61
+ <div className="absolute top-0 left-0 w-px h-8 bg-ms-cyan" />
62
+ <div className="absolute bottom-0 right-0 w-8 h-px bg-ms-magenta" />
63
+ <div className="absolute bottom-0 right-0 w-px h-8 bg-ms-magenta" />
64
+
65
+ <div className="absolute bottom-4 left-4">
66
+ <span className="tag tag-cyan">PREVIEW · RANDOMIZED</span>
67
+ </div>
68
+ </div>
69
+
70
+ {/* Collection info */}
71
+ <div className="card-cyber p-5">
72
+ <h3 className="font-display font-bold text-xs text-ms-text mb-4 tracking-widest">COLLECTION DATA</h3>
73
+ <div className="grid grid-cols-2 gap-3 text-xs font-mono">
74
+ {[
75
+ { k: 'CONTRACT', v: 'makestx-nft' },
76
+ { k: 'STANDARD', v: 'SIP-009' },
77
+ { k: 'MAX SUPPLY', v: '10,000' },
78
+ { k: 'ROYALTIES', v: '5%' },
79
+ { k: 'NETWORK', v: 'STACKS' },
80
+ { k: 'SECURED BY', v: 'BITCOIN' },
81
+ ].map(({ k, v }) => (
82
+ <div key={k} className="bg-ms-bg p-3 border border-ms-border">
83
+ <p className="text-ms-muted text-xs">{k}</p>
84
+ <p className="text-ms-cyan mt-0.5">{v}</p>
85
+ </div>
86
+ ))}
87
+ </div>
88
+ </div>
89
+ </div>
90
+
91
+ {/* Mint Panel */}
92
+ <div className="space-y-5">
93
+ <div>
94
+ <p className="font-mono text-xs text-ms-muted mb-2">MAKESTX CYBERPUNK #????</p>
95
+ <p className="text-ms-muted leading-relaxed text-sm">
96
+ Each MakeSTX Cyberpunk is a unique, algorithmically generated NFT.
97
+ 10,000 total supply. Free mint — pay only the gas fee.
98
+ Stored on Stacks, secured by Bitcoin.
99
+ </p>
100
+ </div>
101
+
102
+ {/* Price card */}
103
+ <div className="card-cyber p-6">
104
+ <div className="flex items-center justify-between mb-5">
105
+ <span className="font-mono text-xs text-ms-muted tracking-widest">MINT PRICE</span>
106
+ <span className="tag tag-green">FREE MINT</span>
107
+ </div>
108
+ {[
109
+ { k: 'PRICE', v: '0 STX', color: 'text-ms-cyan' },
110
+ { k: 'GAS FEE', v: '~0.002 STX', color: 'text-ms-muted' },
111
+ { k: 'NETWORK', v: 'Stacks Mainnet', color: 'text-ms-muted' },
112
+ { k: 'SETTLEMENT', v: 'Bitcoin', color: 'text-ms-muted' },
113
+ ].map(({ k, v, color }) => (
114
+ <div key={k} className="flex items-center justify-between text-xs font-mono mb-2 last:mb-0">
115
+ <span className="text-ms-muted">{k}</span>
116
+ <span className={color}>{v}</span>
117
+ </div>
118
+ ))}
119
+ </div>
120
+
121
+ {/* Info */}
122
+ <div className="flex gap-3 p-4 bg-ms-cyan/5 border border-ms-cyan/20">
123
+ <Info size={15} className="text-ms-cyan shrink-0 mt-0.5" />
124
+ <p className="text-xs text-ms-muted leading-relaxed font-mono">
125
+ Requires Hiro Wallet or Xverse. NFT appears in your wallet within a few blocks (~10 min).
126
+ </p>
127
+ </div>
128
+
129
+ {/* Success */}
130
+ {txId && (
131
+ <div className="flex gap-3 p-4 bg-green-500/10 border border-green-500/20">
132
+ <CheckCircle2 size={15} className="text-green-400 shrink-0 mt-0.5" />
133
+ <div>
134
+ <p className="text-sm text-green-400 font-mono font-bold">TX SUBMITTED</p>
135
+ <a href={`${EXPLORER}/txid/${txId}?chain=mainnet`} target="_blank" rel="noopener noreferrer"
136
+ className="text-xs text-ms-muted hover:text-ms-cyan flex items-center gap-1 mt-1 font-mono">
137
+ VIEW ON EXPLORER <ExternalLink size={10} />
138
+ </a>
139
+ </div>
140
+ </div>
141
+ )}
142
+
143
+ {/* Mint button */}
144
+ {!isConnected ? (
145
+ <button onClick={connect} className="btn-primary w-full py-5 text-sm neon-pulse flex items-center justify-center gap-2">
146
+ <Wallet size={16} />
147
+ <span>CONNECT WALLET</span>
148
+ </button>
149
+ ) : (
150
+ <button
151
+ onClick={handleMint}
152
+ disabled={loading}
153
+ className="btn-primary w-full py-5 text-sm flex items-center justify-center gap-2 disabled:opacity-40 disabled:cursor-not-allowed"
154
+ >
155
+ {loading ? (
156
+ <><Loader2 size={16} className="animate-spin" /><span>MINTING...</span></>
157
+ ) : (
158
+ <><Zap size={16} /><span>MINT FREE NFT</span></>
159
+ )}
160
+ </button>
161
+ )}
162
+
163
+ <p className="text-xs text-center text-ms-muted font-mono">
164
+ BY MINTING YOU ACCEPT THE TERMS. GAS ONLY (~0.002 STX).
165
+ </p>
166
+ </div>
167
+ </div>
168
+ </main>
169
+ </div>
170
+ </>
171
+ );
172
+ }
173
+
174
+ function Wallet({ size }: { size: number }) {
175
+ return (
176
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
177
+ <path d="M20 7H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/>
178
+ <path d="M16 3H8a2 2 0 0 0-2 2v2h12V5a2 2 0 0 0-2-2z"/>
179
+ <circle cx="18" cy="13" r="1"/>
180
+ </svg>
181
+ );
182
+ }
@@ -0,0 +1,358 @@
1
+ import Head from 'next/head';
2
+ import { useState, useRef } from 'react';
3
+ import {
4
+ makeContractCall,
5
+ makeSTXTokenTransfer,
6
+ broadcastTransaction,
7
+ AnchorMode,
8
+ PostConditionMode,
9
+ principalCV,
10
+ getAddressFromPrivateKey,
11
+ TransactionVersion,
12
+ } from '@stacks/transactions';
13
+ import { generateWallet, getStxAddress } from '@stacks/wallet-sdk';
14
+ import { network, CONTRACT_ADDRESS, CONTRACT_NAME } from '@/hooks/useWallet';
15
+ import Navbar from '@/components/Navbar';
16
+ import { Zap, Wallet, Play, Square, AlertTriangle, CheckCircle2, XCircle, Info, Activity } from 'lucide-react';
17
+
18
+ interface LogEntry { time: string; msg: string; type: 'ok' | 'err' | 'info'; }
19
+ interface Stats { totalMinted: number; totalFailed: number; totalWallets: number; rounds: number; }
20
+
21
+ const MINT_FEE = BigInt(2000);
22
+ const TRANSFER_FEE = BigInt(1500);
23
+ const FUNDING_WAIT = 12_000;
24
+
25
+ const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
26
+
27
+ function generatePrivateKey() {
28
+ const arr = new Uint8Array(32);
29
+ crypto.getRandomValues(arr);
30
+ return Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join('') + '01';
31
+ }
32
+
33
+ function getSubAddress(pk: string) {
34
+ return getAddressFromPrivateKey(pk, TransactionVersion.Mainnet);
35
+ }
36
+
37
+ export default function MultiMint() {
38
+ const [mnemonic, setMnemonic] = useState('');
39
+ const [stxPerWallet, setStxPerWallet] = useState(0.15);
40
+ const [mintsPerWallet, setMintsPerWallet] = useState(50);
41
+ const [parallelWorkers, setParallelWorkers] = useState(5);
42
+ const [running, setRunning] = useState(false);
43
+ const [logs, setLogs] = useState<LogEntry[]>([]);
44
+ const [stats, setStats] = useState<Stats>({ totalMinted: 0, totalFailed: 0, totalWallets: 0, rounds: 0 });
45
+ const [balance, setBalance] = useState<number | null>(null);
46
+ const [phase, setPhase] = useState<'idle' | 'funding' | 'minting' | 'done'>('idle');
47
+ const [round, setRound] = useState(0);
48
+ const stopFlag = useRef(false);
49
+ const statsRef = useRef<Stats>({ totalMinted: 0, totalFailed: 0, totalWallets: 0, rounds: 0 });
50
+ const logRef = useRef<HTMLDivElement>(null);
51
+
52
+ const addLog = (msg: string, type: LogEntry['type'] = 'info') => {
53
+ const time = new Date().toLocaleTimeString('tr-TR');
54
+ setLogs(prev => [...prev, { time, msg, type }].slice(-400));
55
+ setTimeout(() => { if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight; }, 10);
56
+ };
57
+
58
+ const getNonce = async (addr: string) => {
59
+ try {
60
+ const r = await fetch(`https://api.mainnet.hiro.so/v2/accounts/${addr}?unanchored=true`);
61
+ return parseInt((await r.json()).nonce);
62
+ } catch { return 0; }
63
+ };
64
+
65
+ const getBalanceMicro = async (addr: string): Promise<bigint> => {
66
+ try {
67
+ const r = await fetch(`https://api.mainnet.hiro.so/v2/accounts/${addr}?unanchored=true`);
68
+ return BigInt((await r.json()).balance ?? '0');
69
+ } catch { return BigInt(0); }
70
+ };
71
+
72
+ const sendSTX = async (fromKey: string, to: string, amount: bigint, nonce: number) => {
73
+ for (let t = 0; t <= 3; t++) {
74
+ try {
75
+ const tx = await makeSTXTokenTransfer({ recipient: to, amount, senderKey: fromKey, network, nonce: BigInt(nonce), anchorMode: AnchorMode.Any, fee: TRANSFER_FEE });
76
+ const res = await broadcastTransaction(tx, network);
77
+ if (res.error) throw new Error(res.error);
78
+ return { ok: true, txid: res.txid };
79
+ } catch (e: any) {
80
+ if (t < 3) { await sleep(3000); continue; }
81
+ return { ok: false, error: e.message };
82
+ }
83
+ }
84
+ return { ok: false, error: 'max retries' };
85
+ };
86
+
87
+ const mintOne = async (fromKey: string, toAddr: string, nonce: number) => {
88
+ for (let t = 0; t <= 3; t++) {
89
+ try {
90
+ const tx = await makeContractCall({ contractAddress: CONTRACT_ADDRESS, contractName: CONTRACT_NAME, functionName: 'mint', functionArgs: [principalCV(toAddr)], senderKey: fromKey, network, nonce: BigInt(nonce), anchorMode: AnchorMode.Any, postConditionMode: PostConditionMode.Allow, fee: MINT_FEE });
91
+ const res = await broadcastTransaction(tx, network);
92
+ if (res.error) throw new Error(res.error);
93
+ return { ok: true, txid: res.txid };
94
+ } catch (e: any) {
95
+ if (t < 3) { await sleep(3000); continue; }
96
+ return { ok: false, error: e.message };
97
+ }
98
+ }
99
+ return { ok: false, error: 'max retries' };
100
+ };
101
+
102
+ const runWorker = async (id: number, pk: string, addr: string) => {
103
+ let nonce = await getNonce(addr);
104
+ let ok = 0, fail = 0;
105
+ for (let i = 0; i < mintsPerWallet; i++) {
106
+ if (stopFlag.current) break;
107
+ const r = await mintOne(pk, addr, nonce + i);
108
+ if (r.ok) { ok++; statsRef.current.totalMinted++; addLog(`[W${id}] TX_${i+1} ✓ ${r.txid?.slice(0,16)}...`, 'ok'); }
109
+ else { fail++; statsRef.current.totalFailed++; addLog(`[W${id}] TX_${i+1} ✗ ${r.error}`, 'err'); }
110
+ setStats({ ...statsRef.current });
111
+ if (i < mintsPerWallet - 1 && !stopFlag.current) await sleep(300);
112
+ }
113
+ addLog(`[W${id}] DONE → OK:${ok} FAIL:${fail}`, ok > 0 ? 'ok' : 'err');
114
+ };
115
+
116
+ const startMultiMint = async () => {
117
+ if (running) return;
118
+ if (!mnemonic.trim()) { addLog('ERROR: Seed phrase empty', 'err'); return; }
119
+
120
+ stopFlag.current = false;
121
+ setRunning(true);
122
+ setLogs([]);
123
+ statsRef.current = { totalMinted: 0, totalFailed: 0, totalWallets: 0, rounds: 0 };
124
+ setStats({ ...statsRef.current });
125
+ setPhase('idle'); setRound(0);
126
+
127
+ let masterKey: string, masterAddress: string;
128
+ try {
129
+ const wallet = await generateWallet({ secretKey: mnemonic.trim(), password: '' });
130
+ const acc = wallet.accounts[0];
131
+ masterKey = acc.stxPrivateKey;
132
+ masterAddress = getStxAddress({ account: acc, transactionVersion: TransactionVersion.Mainnet });
133
+ addLog(`MASTER: ${masterAddress}`, 'ok');
134
+ } catch (e: any) {
135
+ addLog('ERR: ' + e.message, 'err');
136
+ setRunning(false); return;
137
+ }
138
+
139
+ const stxMicro = BigInt(Math.round(stxPerWallet * 1_000_000));
140
+ const costPerWallet = stxMicro + TRANSFER_FEE + BigInt(mintsPerWallet) * MINT_FEE;
141
+
142
+ const bal = await getBalanceMicro(masterAddress);
143
+ setBalance(Number(bal) / 1_000_000);
144
+ addLog(`BALANCE: ${Number(bal) / 1_000_000} STX`, 'info');
145
+ addLog(`COST_PER_WALLET: ${Number(costPerWallet) / 1_000_000} STX`, 'info');
146
+
147
+ if (bal < costPerWallet) {
148
+ addLog(`INSUFFICIENT BALANCE — need ${Number(costPerWallet) / 1_000_000} STX`, 'err');
149
+ setRunning(false); return;
150
+ }
151
+
152
+ let masterNonce = await getNonce(masterAddress);
153
+ let roundNum = 0;
154
+
155
+ while (!stopFlag.current) {
156
+ roundNum++;
157
+ setRound(roundNum);
158
+ const curBal = await getBalanceMicro(masterAddress);
159
+ setBalance(Number(curBal) / 1_000_000);
160
+ const affordable = Number(curBal / costPerWallet);
161
+ const activeWorkers = Math.min(parallelWorkers, affordable);
162
+
163
+ addLog(`\n=== ROUND_${roundNum} | BAL: ${Number(curBal)/1e6} STX | WORKERS: ${activeWorkers} ===`, 'info');
164
+
165
+ if (activeWorkers === 0) { addLog('BALANCE_DEPLETED — process complete', 'info'); break; }
166
+
167
+ const batch = Array.from({ length: activeWorkers }, () => {
168
+ const pk = generatePrivateKey();
169
+ const addr = getSubAddress(pk);
170
+ return { pk, addr };
171
+ });
172
+
173
+ setPhase('funding');
174
+ addLog(`FUNDING ${activeWorkers} wallets...`, 'info');
175
+ const funded: typeof batch = [];
176
+
177
+ for (let i = 0; i < batch.length; i++) {
178
+ if (stopFlag.current) break;
179
+ const { pk, addr } = batch[i];
180
+ const r = await sendSTX(masterKey, addr, stxMicro, masterNonce++);
181
+ if (r.ok) {
182
+ funded.push({ pk, addr });
183
+ statsRef.current.totalWallets++;
184
+ addLog(`FUNDED_${i+1}: ${addr.slice(0,16)}... ✓`, 'ok');
185
+ } else {
186
+ addLog(`FUND_FAIL_${i+1}: ${r.error}`, 'err');
187
+ }
188
+ }
189
+
190
+ if (funded.length === 0) { await sleep(10_000); continue; }
191
+
192
+ addLog(`WAITING ${FUNDING_WAIT/1000}s for confirmation...`, 'info');
193
+ await sleep(FUNDING_WAIT);
194
+
195
+ setPhase('minting');
196
+ addLog(`LAUNCHING ${funded.length} parallel workers...`, 'info');
197
+ await Promise.all(funded.map(({ pk, addr }, i) => runWorker(i + 1, pk, addr)));
198
+
199
+ statsRef.current.rounds++;
200
+ setStats({ ...statsRef.current });
201
+ if (stopFlag.current) break;
202
+ }
203
+
204
+ setPhase('done');
205
+ setRunning(false);
206
+ addLog(`\n=== COMPLETE | MINTED: ${statsRef.current.totalMinted} | WALLETS: ${statsRef.current.totalWallets} ===`, 'ok');
207
+ };
208
+
209
+ const costPerWallet = (stxPerWallet + (Number(TRANSFER_FEE) + mintsPerWallet * Number(MINT_FEE)) / 1_000_000).toFixed(4);
210
+ const estimatedMints = balance ? Math.floor(balance / parseFloat(costPerWallet)) * mintsPerWallet : 0;
211
+
212
+ return (
213
+ <>
214
+ <Head><title>Multi Mint — MakeSTX</title></Head>
215
+ <div className="min-h-screen bg-ms-bg grid-bg">
216
+ <Navbar />
217
+
218
+ <main className="pt-28 pb-16 px-4 max-w-2xl mx-auto">
219
+
220
+ {/* Header */}
221
+ <div className="mb-8 flex items-start gap-4">
222
+ <div className="w-12 h-12 border border-ms-magenta flex items-center justify-center shrink-0"
223
+ style={{ clipPath: 'polygon(20% 0%, 80% 0%, 100% 20%, 100% 80%, 80% 100%, 20% 100%, 0% 80%, 0% 20%)' }}>
224
+ <Zap size={20} className="text-ms-magenta" />
225
+ </div>
226
+ <div>
227
+ <p className="font-mono text-xs text-ms-muted tracking-widest mb-1">// multi_wallet_mint.js</p>
228
+ <h1 className="font-display font-black text-3xl text-ms-text tracking-tight">
229
+ MULTI <span className="text-ms-magenta">MINT</span>
230
+ </h1>
231
+ <p className="font-mono text-xs text-ms-muted mt-1">
232
+ MASTER WALLET → GENERATE WALLETS → FUND → PARALLEL MINT
233
+ </p>
234
+ </div>
235
+ </div>
236
+
237
+ {/* Warning */}
238
+ <div className="flex gap-2 p-3 mb-5 bg-yellow-500/5 border border-yellow-500/30">
239
+ <AlertTriangle size={14} className="text-yellow-400 shrink-0 mt-0.5" />
240
+ <p className="text-xs font-mono text-yellow-400/80">
241
+ SEED PHRASE PROCESSED LOCALLY ONLY. USE A DEDICATED WALLET WITH ENOUGH STX.
242
+ </p>
243
+ </div>
244
+
245
+ {/* Seed phrase */}
246
+ <div className="card-dark p-4 mb-3">
247
+ <label className="block text-xs font-mono text-ms-muted tracking-widest mb-2 flex items-center gap-1.5">
248
+ <Wallet size={11} /> // MASTER_WALLET SEED (12 OR 24 WORDS)
249
+ </label>
250
+ <input
251
+ type="password"
252
+ value={mnemonic}
253
+ onChange={e => setMnemonic(e.target.value)}
254
+ placeholder="word1 word2 word3 ..."
255
+ disabled={running}
256
+ className="w-full bg-ms-bg border border-ms-border px-3 py-2 text-xs font-mono text-ms-text placeholder-ms-muted focus:outline-none focus:border-ms-cyan disabled:opacity-40 tracking-wider"
257
+ />
258
+ </div>
259
+
260
+ {/* Config */}
261
+ <div className="card-dark p-4 mb-4">
262
+ <div className="grid grid-cols-3 gap-3">
263
+ {[
264
+ { label: 'STX_PER_WALLET', val: stxPerWallet, set: setStxPerWallet, step: 0.05, min: 0.05 },
265
+ { label: 'MINTS_PER_WALLET', val: mintsPerWallet, set: setMintsPerWallet, step: 1, min: 1 },
266
+ { label: 'PARALLEL_WORKERS', val: parallelWorkers, set: setParallelWorkers, step: 1, min: 1 },
267
+ ].map(({ label, val, set, step, min }) => (
268
+ <div key={label}>
269
+ <label className="block text-xs font-mono text-ms-muted tracking-wider mb-2">{label}</label>
270
+ <input type="number" value={val}
271
+ onChange={e => (set as any)(parseFloat(e.target.value))}
272
+ step={step} min={min} disabled={running}
273
+ className="w-full bg-ms-bg border border-ms-border px-3 py-2 text-xs font-mono text-ms-text focus:outline-none focus:border-ms-cyan disabled:opacity-40"
274
+ />
275
+ </div>
276
+ ))}
277
+ </div>
278
+ <div className="mt-3 pt-3 border-t border-ms-border flex items-center justify-between font-mono text-xs text-ms-muted">
279
+ <span><Info size={10} className="inline mr-1" />COST/WALLET: <span className="text-ms-cyan">{costPerWallet} STX</span></span>
280
+ {balance !== null && (
281
+ <span>EST TOTAL: <span className="text-ms-magenta font-bold">{estimatedMints.toLocaleString()} MINTS</span></span>
282
+ )}
283
+ </div>
284
+ </div>
285
+
286
+ {/* Stats */}
287
+ <div className="grid grid-cols-4 gap-2 mb-3">
288
+ {[
289
+ { label: 'MINTED', val: stats.totalMinted, color: 'text-green-400', icon: <CheckCircle2 size={11}/> },
290
+ { label: 'FAILED', val: stats.totalFailed, color: 'text-red-400', icon: <XCircle size={11}/> },
291
+ { label: 'WALLETS', val: stats.totalWallets, color: 'text-ms-cyan', icon: <Wallet size={11}/> },
292
+ { label: 'ROUNDS', val: round, color: 'text-ms-magenta', icon: <Zap size={11}/> },
293
+ ].map(({ label, val, color, icon }) => (
294
+ <div key={label} className="card-dark p-3">
295
+ <div className={`flex items-center gap-1 ${color} mb-1`}>{icon}<span className="font-mono text-xs text-ms-muted">{label}</span></div>
296
+ <div className={`text-xl font-display font-bold ${color}`}>{val}</div>
297
+ </div>
298
+ ))}
299
+ </div>
300
+
301
+ {/* Phase indicator */}
302
+ {running && (
303
+ <div className="mb-3 p-3 border border-ms-border bg-ms-surface flex items-center gap-3">
304
+ <div className="w-2 h-2 bg-ms-cyan animate-pulse" />
305
+ <span className="font-mono text-xs text-ms-muted tracking-wider">
306
+ {phase === 'funding' && `FUNDING WALLETS... (ROUND ${round})`}
307
+ {phase === 'minting' && `ROUND_${round} — PARALLEL MINT ACTIVE`}
308
+ </span>
309
+ {balance !== null && (
310
+ <span className="ml-auto font-mono text-xs text-ms-muted">BAL: {balance.toFixed(4)} STX</span>
311
+ )}
312
+ </div>
313
+ )}
314
+
315
+ {/* Terminal log */}
316
+ <div
317
+ ref={logRef}
318
+ className="h-56 overflow-y-auto bg-ms-bg border border-ms-border p-3 font-mono text-xs mb-4"
319
+ style={{ fontFamily: "'Share Tech Mono', monospace" }}
320
+ >
321
+ <div className="text-ms-muted mb-1">MakeSTX Multi-Wallet Mint Terminal v1.0</div>
322
+ <div className="text-ms-muted mb-2 border-b border-ms-border pb-2">
323
+ {'>'} CONFIG: {parallelWorkers} workers × {mintsPerWallet} mints = {parallelWorkers * mintsPerWallet} tx/round
324
+ </div>
325
+ {logs.length === 0 && <div className="text-ms-muted">{'>'} Awaiting start...</div>}
326
+ {logs.map((l, i) => (
327
+ <div key={i} className={l.type === 'ok' ? 'text-green-400' : l.type === 'err' ? 'text-red-400' : 'text-ms-muted'}>
328
+ <span className="text-ms-border">[{l.time}]</span> {l.msg}
329
+ </div>
330
+ ))}
331
+ {running && <div className="text-ms-cyan animate-pulse mt-1">▋</div>}
332
+ </div>
333
+
334
+ {/* Button */}
335
+ {!running ? (
336
+ <button onClick={startMultiMint}
337
+ className="btn-magenta w-full py-5 text-xs flex items-center justify-center gap-2 font-display tracking-widest">
338
+ <Zap size={16} />
339
+ START MULTI-WALLET MINT
340
+ </button>
341
+ ) : (
342
+ <button onClick={() => { stopFlag.current = true; }}
343
+ className="w-full py-5 border border-red-500 text-red-400 font-mono text-xs tracking-widest hover:bg-red-500/10 transition-all flex items-center justify-center gap-2">
344
+ <Square size={12} fill="currentColor" />
345
+ TERMINATE ALL WORKERS
346
+ </button>
347
+ )}
348
+
349
+ {phase === 'done' && (
350
+ <div className="mt-3 p-3 border border-green-500/30 bg-green-500/5 text-green-400 font-mono text-xs text-center tracking-widest">
351
+ ✓ PROCESS COMPLETE · {stats.totalMinted} MINTED · {stats.totalWallets} WALLETS USED
352
+ </div>
353
+ )}
354
+ </main>
355
+ </div>
356
+ </>
357
+ );
358
+ }
@@ -0,0 +1,55 @@
1
+ import Head from 'next/head';
2
+ import Navbar from '@/components/Navbar';
3
+ import NFTCard from '@/components/NFTCard';
4
+ import { useWallet } from '@/hooks/useWallet';
5
+ import { Wallet } from 'lucide-react';
6
+
7
+ const MOCK_MY = Array.from({ length: 6 }, (_, i) => ({
8
+ tokenId: i + 1,
9
+ name: `MakeSTX #${String(i + 1).padStart(4, '0')}`,
10
+ image: `https://api.dicebear.com/8.x/pixel-art/svg?seed=my${i + 1}&backgroundColor=030308&colorful=true`,
11
+ isListed: i % 3 === 0,
12
+ price: i % 3 === 0 ? Math.floor(Math.random() * 50 * 1_000_000) : undefined,
13
+ }));
14
+
15
+ export default function MyNFTs() {
16
+ const { isConnected, address, connect } = useWallet();
17
+
18
+ return (
19
+ <>
20
+ <Head>
21
+ <title>My NFTs — MakeSTX</title>
22
+ </Head>
23
+ <div className="min-h-screen bg-ms-bg">
24
+ <Navbar />
25
+ <main className="pt-24 pb-16 px-4 max-w-7xl mx-auto">
26
+ <div className="mb-8">
27
+ <p className="font-mono text-xs text-ms-muted tracking-widest mb-1">// my_wallet.tsx</p>
28
+ <h1 className="font-display font-black text-4xl text-ms-text tracking-tight">
29
+ MY <span className="text-ms-cyan">NFTS</span>
30
+ </h1>
31
+ {address && (
32
+ <p className="font-mono text-xs text-ms-muted mt-2">{address}</p>
33
+ )}
34
+ </div>
35
+
36
+ {!isConnected ? (
37
+ <div className="text-center py-32">
38
+ <Wallet size={40} className="text-ms-muted mx-auto mb-4" />
39
+ <p className="font-mono text-ms-muted text-sm tracking-widest mb-6">// WALLET NOT CONNECTED</p>
40
+ <button onClick={connect} className="btn-primary px-10 py-4 text-sm">
41
+ <span>CONNECT WALLET</span>
42
+ </button>
43
+ </div>
44
+ ) : (
45
+ <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
46
+ {MOCK_MY.map(nft => (
47
+ <NFTCard key={nft.tokenId} {...nft} owner={address ?? undefined} />
48
+ ))}
49
+ </div>
50
+ )}
51
+ </main>
52
+ </div>
53
+ </>
54
+ );
55
+ }