• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

marscoin / martianrepublic / 23751722506

30 Mar 2026 03:03PM UTC coverage: 8.391% (-0.01%) from 8.401%
23751722506

push

github

Martian Congress
refactor: Sprint 2 — architecture cleanup (A3, A4, A7, A9)

A3: Added $fillable to 7 models (Proposals, Vote, Threads, HDWallet,
    IPFSRoot, Publication, Voucher) — closes mass assignment risk

A4: Created config/blockchain.php centralizing all hardcoded URLs
    (RPC, Pebas, IPFS, Explorer, Ballot, Price). Updated 7 files
    to use config() instead of hardcoded strings.

A7: Removed dead code:
    - Deleted ApiController.php.bak, AppHelper.php.bak
    - Deleted unused models: Pkimport.php, Recovery.php
    - Deleted resources/views/wallet/testing.html
    - Removed 8 commented-out routes from web.php and api.php

A9: Added performance indexes migration:
    - proposals.status (phase sync, tab filtering)
    - ballots.userid, ballots.proposalid, ballots(userid,status)
    - votes.proposal_id (vote tallying)
    - forum_posts.author_id, forum_threads.author_id (joins)

A1 (controller split) deferred to dedicated session — too much
in-page JS coupling to batch safely.

79 tests passing, 0 PHPStan errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

2 of 48 new or added lines in 7 files covered. (4.17%)

2 existing lines in 2 files now uncovered.

445 of 5303 relevant lines covered (8.39%)

1.06 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

1.89
/app/Http/Controllers/Wallet/ApiController.php
1
<?php
2
namespace App\Http\Controllers\Wallet;
3

4
use App\Http\Controllers\Controller;
5
use Illuminate\Http\Request;
6
use Illuminate\Support\Facades\Auth;
7
use App\Includes\AppHelper;
8
use App\Models\Posts;
9
use App\Models\Profile;
10
use App\Models\User;
11
use App\Models\Proposals;
12
use App\Models\Publication;
13
use App\Models\Threads;
14
use App\Models\Citizen;
15
use App\Models\HDWallet;
16
use App\Models\Feed;
17
use App\Models\CivicWallet;
18
use Illuminate\Support\Facades\Log;
19

20
class ApiController extends Controller {
21

22
        /**
23
         * Setup the layout used by the controller.
24
         *
25
         * @return void
26
         */
27

28
        public function __construct() {
1✔
29
                $this->middleware("auth");
1✔
30
        }
31

32

33

34
        /**
35
         * Internal
36
         *
37
         * @ignore
38
         * @hideFromAPIDocumentation
39
         */
40
        public function permapinpic(Request $request){
×
41
                $uid = Auth::user()->id;
×
42
                $hash = "";
×
43
                $dataPic = $request->input('picture');
×
44
                $type = $request->input('type');
×
45
                $public_address = $request->input('address');
×
46

47
                        // --- SECURITY: Sanitize the public_address to prevent directory traversal ---
48
                        $safeAddress = AppHelper::sanitizePathSegment($public_address);
×
49
                        if ($safeAddress === null) {
×
50
                                return response()->json(['error' => 'Invalid address format.'], 400);
×
51
                        }
52

53
                        // --- SECURITY: Validate the base64 image data (extension, MIME, size, PHP code) ---
54
                        $validation = AppHelper::validateBase64Image($dataPic);
×
55
                        if (!$validation['valid']) {
×
56
                                Log::warning('permapinpic upload rejected: ' . $validation['error'] . ' (user: ' . $uid . ')');
×
57
                                return response()->json(['error' => $validation['error']], 422);
×
58
                        }
59

60
                        $safeExtension = $validation['extension'];
×
61
                        $decodedData = $validation['data'];
×
62

63
                        $file_path = "./assets/citizen/" . $safeAddress . "/";
×
64
                        if (!file_exists($file_path)) {
×
65
                                mkdir($file_path, 0755, true);
×
66
                        }
67

68
                        // --- SECURITY: Write .htaccess to prevent PHP execution in upload dir ---
69
                        AppHelper::writeUploadHtaccess($file_path);
×
70

71
                        // Use validated extension, not user-supplied one
72
                        $file_path = "./assets/citizen/" . $safeAddress . "/profile_pic." . $safeExtension;
×
73

74
                        file_put_contents($file_path, $decodedData);
×
NEW
75
                        $hash = AppHelper::upload($file_path, config('blockchain.ipfs.api_url') . "/api/v0/add?pin=true");
×
76

77
                        $citcache = Citizen::where('userid', '=', $uid)->first();
×
78
                        if(is_null($citcache)) $citcache = new Citizen;
×
79
                        $citcache->userid = $uid;
×
NEW
80
                        $citcache->avatar_link = config('blockchain.ipfs.gateway_url').$hash;
×
81
                        $citcache->save();
×
82

83
                        return response()->json(["Hash" => $hash], 200);
×
84
        }
85

86

87
        /**
88
         * Internal
89
         *
90
         * @ignore
91
         * @hideFromAPIDocumentation
92
         */
93
        public function permapinvideo(Request $request){
×
94
                $uid = Auth::user()->id;
×
95
                $hash = "";
×
96
                $dataPic = $request->input('file');
×
97
                $type = $request->input('type');
×
98
                $public_address = $request->input('address');
×
99

100
                        // --- SECURITY: Sanitize the public_address to prevent directory traversal ---
101
                        $safeAddress = AppHelper::sanitizePathSegment($public_address);
×
102
                        if ($safeAddress === null) {
×
103
                                return response()->json(['error' => 'Invalid address format.'], 400);
×
104
                        }
105

106
                        if ($request->hasFile('file'))
×
107
                        {
108
                                // --- SECURITY: Validate the uploaded file (extension, MIME, size, PHP code) ---
109
                                $uploadedFile = $request->file('file');
×
110
                                $validation = AppHelper::validateUploadedFile($uploadedFile, [
×
111
                                        'webm' => ['video/webm', 'audio/webm'],
×
112
                                ]);
×
113
                                if (!$validation['valid']) {
×
114
                                        Log::warning('permapinvideo upload rejected: ' . $validation['error'] . ' (user: ' . $uid . ')');
×
115
                                        return response()->json(['error' => $validation['error']], 422);
×
116
                                }
117

118
                                $file_path = "./assets/citizen/" . $safeAddress . "/";
×
119
                                if (!file_exists($file_path)) {
×
120
                                        mkdir($file_path, 0755, true);
×
121
                                }
122

123
                                // --- SECURITY: Write .htaccess to prevent PHP execution in upload dir ---
124
                                AppHelper::writeUploadHtaccess($file_path);
×
125

126
                                $file_path = "./assets/citizen/" . $safeAddress  . "/";
×
127
                                $request->file('file')->move($file_path, "profile_video.webm" );
×
128
                                $file_path = $file_path . "profile_video.webm";
×
NEW
129
                                $hash = AppHelper::upload($file_path, config('blockchain.ipfs.api_url') . "/api/v0/add?pin=true");
×
130

131
                                $citcache = Citizen::where('userid', '=', $uid)->first();
×
132
                                if(is_null($citcache)) $citcache = new Citizen;
×
133
                                $citcache->userid = $uid;
×
NEW
134
                                $citcache->liveness_link = config('blockchain.ipfs.gateway_url').$hash;
×
135
                                $citcache->save();
×
136

137
                                return response()->json(["Hash" => $hash], 200);
×
138
                        }
139

140
                return response()->json(['error' => 'No file uploaded.'], 400);
×
141
        }
142

143

144
        /**
145
         * Internal
146
         *
147
         * @hideFromAPIDocumentation
148
         */
149
        public function permapinlog(Request $request)
×
150
        {
151
                $public_address = $request->input('address');
×
152
                $title = $request->input('title');
×
153
                $entry = $request->input('entry');
×
154
                $uid = Auth::user()->id;
×
155

156
                // --- SECURITY: Sanitize the public_address to prevent directory traversal ---
157
                $safeAddress = AppHelper::sanitizePathSegment($public_address);
×
158
                if ($safeAddress === null) {
×
159
                        return response()->json(['error' => 'Invalid address format.'], 400);
×
160
                }
161

162
                // --- SECURITY: Sanitize title for use in path (md5 hash is safe, but validate title exists) ---
163
                if (empty($title)) {
×
164
                        return response()->json(['error' => 'Title is required.'], 400);
×
165
                }
166

167
                $file_path = "./assets/citizen/" . $safeAddress . "/logbook/" . md5($title);
×
168

169
                if (!file_exists($file_path)) {
×
170
                        mkdir($file_path, 0755, true); // More secure permissions
×
171
                }
172

173
                // --- SECURITY: Write .htaccess to prevent PHP execution in upload dir ---
174
                AppHelper::writeUploadHtaccess($file_path);
×
175

176
                // --- SECURITY: Check content for PHP code ---
177
                $logContent = $title . "\n\n" . $entry;
×
178
                if (AppHelper::containsPhpCode($logContent)) {
×
179
                        Log::warning('permapinlog rejected: content contains PHP code (user: ' . $uid . ')');
×
180
                        return response()->json(['error' => 'Content contains potentially dangerous code.'], 422);
×
181
                }
182

183
                // --- SECURITY: Check content size (max 5MB) ---
184
                if (strlen($logContent) > 5242880) {
×
185
                        return response()->json(['error' => 'Content exceeds maximum size of 5MB.'], 422);
×
186
                }
187

188
                $file = $file_path . "/log.markdown";
×
189
                file_put_contents($file, $logContent);
×
190

191
                $files = $request->file('filenames');
×
192
                if ($files && is_array($files)) {
×
193
                        foreach ($files as $f) {
×
194
                                // --- SECURITY: Validate each uploaded file ---
195
                                $validation = AppHelper::validateUploadedFile($f);
×
196
                                if (!$validation['valid']) {
×
197
                                        Log::warning('permapinlog file rejected: ' . $validation['error'] . ' (user: ' . $uid . ', file: ' . $f->getClientOriginalName() . ')');
×
198
                                        // Skip invalid files but continue processing valid ones
199
                                        continue;
×
200
                                }
201

202
                                $name = $f->hashName(); // Generates a unique, random name...
×
203

204
                                // --- SECURITY: Ensure the generated filename does not have a dangerous extension ---
205
                                $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
×
206
                                if (AppHelper::isExtensionBlocked($ext)) {
×
207
                                        Log::warning('permapinlog blocked dangerous file extension: ' . $ext . ' (user: ' . $uid . ')');
×
208
                                        continue;
×
209
                                }
210

211
                                $f->move($file_path, $name);
×
212
                        }
213
                }
214

215
                try {
NEW
216
                        $hash = AppHelper::uploadFolder($file_path, config('blockchain.ipfs.api_url') . '/api/v0/add?pin=true&recursive=true&wrap-with-directory=true&quieter'); // Example: use a config value or env variable
×
217
                        AppHelper::insertPublicationCache($uid, $file_path, $hash, $title);
×
218
                } catch (\Exception $e) {
×
219
                        // Handle error; possibly log it and return a user-friendly message
220
                        return response()->json(["error" => $e->getMessage()], 500);
×
221
                }
222

223
                return response()->json(["Hash" => $hash, "Path" => $file_path], 200)
×
224
                        ->header('Content-Type', "application/json;");
×
225
        }
226

227

228

229

230
        public function removepinlog(Request $request)
×
231
        {
232
                $cid = $request->input('cid'); // The CID to unpin
×
233

234
                if (!$cid) {
×
235
                        return response()->json(["error" => "CID is required"], 400);
×
236
                }
237

238
                // Validate CID format to prevent injection into the IPFS API URL
239
                if (!AppHelper::isValidCID($cid)) {
×
240
                        return response()->json(["error" => "Invalid CID format"], 400);
×
241
                }
242

243
                // Verify the publication belongs to the current user
244
                $uid = Auth::user()->id;
×
245
                $publication = Publication::where('ipfs_hash', $cid)->first();
×
246
                if ($publication && $publication->userid != $uid) {
×
247
                        return response()->json(["error" => "Unauthorized: you can only remove your own publications."], 403);
×
248
                }
249

NEW
250
                $ipfsApiUrl = config('blockchain.ipfs.api_url') . '/api/v0/pin/rm?arg=' . urlencode($cid) . "&recursive=true";
×
251
                Log::debug($ipfsApiUrl);
×
252
                try {
253
                        // Initialize cURL session
254
                        $ch = curl_init();
×
255
                        curl_setopt($ch, CURLOPT_URL, $ipfsApiUrl);
×
256
                        curl_setopt($ch, CURLOPT_VERBOSE, true);
×
257
                        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
×
258
                        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST"); // Ensure this is POST
×
259

260
                        $response = curl_exec($ch);
×
261
                        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
×
262

263
                        Log::debug($response);
×
264

265
                        if ($httpCode != 200) {
×
266
                                // Error handling, IPFS might return error messages as JSON
267
                                $errorMsg = "Failed to unpin CID. HTTP status code: $httpCode";
×
268
                                if ($responseJson = json_decode($response)) {
×
269
                                        if (!empty($responseJson->Message)) {
×
270
                                                $errorMsg = $responseJson->Message;
×
271
                                        }
272
                                }
273
                                throw new \Exception($errorMsg);
×
274
                        }
275

276
                        $publicationDeleted = Publication::where('ipfs_hash', $cid)->delete();
×
277
                        if (!$publicationDeleted) {
×
278
                                throw new \Exception("Failed to delete publication from the database.");
×
279
                        }
280

281
                } catch (\Exception $e) {
×
282
                        // Handle error; possibly log it and return a user-friendly message
283
                        return response()->json(["error" => $e->getMessage()], 500);
×
284
                }
285

286
                // Close cURL session
287
                curl_close($ch);
×
288

289
                // Respond to the client
290
                return response()->json(["message" => "Successfully unpinned CID: $cid"], 200)
×
291
                        ->header('Content-Type', "application/json;");
×
292
        }
293

294

295

296
        /**
297
         * Handles JSON storage and pinning to a distributed file system.
298
         *
299
         * @hideFromAPIDocumentation
300
         */
301
        public function permapinjson(Request $request)
×
302
        {
303
                $public_address = $request->input('address');
×
304
                $type = $request->input('type');
×
305
                $json = $request->input('payload');
×
306

307
                // --- SECURITY: Sanitize the public_address to prevent directory traversal ---
308
                $safeAddress = AppHelper::sanitizePathSegment($public_address);
×
309
                if ($safeAddress === null) {
×
310
                        return response()->json(['error' => 'Invalid address format.'], 400);
×
311
                }
312

313
                // --- SECURITY: Sanitize the type parameter to prevent path traversal ---
314
                $safeType = AppHelper::sanitizePathSegment($type);
×
315
                if ($safeType === null) {
×
316
                        return response()->json(['error' => 'Invalid type format.'], 400);
×
317
                }
318

319
                // --- SECURITY: Check file size (max 5MB) ---
320
                if (strlen($json) > 5242880) {
×
321
                        return response()->json(['error' => 'Payload exceeds maximum size of 5MB.'], 422);
×
322
                }
323

324
                // --- SECURITY: Reject payloads containing PHP code ---
325
                if (AppHelper::containsPhpCode($json)) {
×
326
                        Log::warning('permapinjson rejected: payload contains PHP code');
×
327
                        return response()->json(['error' => 'Payload contains potentially dangerous content.'], 422);
×
328
                }
329

330
                // --- SECURITY: Validate that payload is valid JSON ---
331
                $decodedJson = json_decode($json);
×
332
                if ($json !== '' && json_last_error() !== JSON_ERROR_NONE) {
×
333
                        return response()->json(['error' => 'Invalid JSON payload.'], 422);
×
334
                }
335

336
                $projectRoot = config('app.project_root', base_path());
×
337
                $base_path =  $projectRoot . "/assets/citizen/" . $safeAddress;
×
338

339
                // Check and create the directory if it doesn't exist
340
                Log::info($base_path);
×
341
                clearstatcache();
×
342
                if (!is_dir($base_path)) {
×
343
                        Log::info("Trying to create directory: " . $base_path);
×
344
                        if (!mkdir($base_path, 0755, true)) {
×
345
                                Log::error("Failed to create directory: " . $base_path);
×
346
                                return response()->json(["error" => "Failed to create directory. Check permissions."], 500);
×
347
                        }
348
                        Log::info("Directory created: " . $base_path);
×
349
                }
350

351
                // Check if the directory is writable, regardless of whether it was just created or already existed
352
                if (!is_writable($base_path)) {
×
353
                        Log::error("Directory not writable: " . $base_path);
×
354
                        return response()->json(["error" => "Directory is not writable. Check permissions."], 500);
×
355
                }
356

357
                // --- SECURITY: Write .htaccess to prevent PHP execution in upload dir ---
358
                AppHelper::writeUploadHtaccess($base_path);
×
359

360
                $file_path = $base_path . "/" . $safeType . ".json";
×
361

362
                // Attempt to write the JSON data to the file
363
                if (file_put_contents($file_path, $json) === false) {
×
364
                        return response()->json(["error" => "Failed to write to file."], 500);
×
365
                }
366

367
                try {
368
                        Log::info("PermaJson: " . $file_path);
×
369

370
                        // Check if the type contains the word 'log'
371
                        if (strpos($safeType, 'log') !== false) {
×
372
                                // The type contains 'log', use uploadFolder
NEW
373
                                $apiResponse = AppHelper::uploadFolder($file_path, config('blockchain.ipfs.api_url') . "/api/v0/add?pin=true&recursive=true&wrap-with-directory=true&quieter");
×
374
                        } else {
375
                                // The type does not contain 'log', use upload
NEW
376
                                $apiResponse = AppHelper::upload($file_path, config('blockchain.ipfs.api_url') . "/api/v0/add?pin=true");
×
377
                        }
378

379
                        if (is_string($apiResponse)) {
×
380
                                $formattedResponse = ['Hash' => $apiResponse];
×
381
                        } else {
382
                                Log::error("Upload error: Formatting");
×
383
                                return response()->json(["error"=>"formatting error"], 500);
×
384
                        }
385

386
                        return response()->json($formattedResponse, 200)->header('Content-Type', "application/json;");
×
387
                } catch (\Exception $e) {
×
388
                        // Handle exceptions during the upload and pinning process
389
                        Log::error("Upload error: " . $e->getMessage());
×
390
                        return response()->json(["error" => $e->getMessage()], 500);
×
391
                }
392
        }
393

394

395

396

397
        /**
398
         * Internal
399
         *
400
         * @hideFromAPIDocumentation
401
         */
402
        public function setfeed(Request $request)
×
403
        {
404
                $uid = Auth::user()->id;
×
405
                $txid = $request->input('txid');
×
406
                $action_tag = $request->input('type');
×
407
                $public_address = $request->input('address');
×
408
                $embedded_link = $request->input('embedded_link');
×
409
                $message = $request->input('message');
×
410

411
                AppHelper::insertBlockchainCache($public_address, $uid, $action_tag, $message, $embedded_link, $txid);
×
412

413
                $profile = Profile::where('userid', '=', $uid)->first();
×
414
                if ($profile) {
×
415
                        $profile->general_public = 1;
×
416
                        // Set has_application when GP application is submitted
417
                        if ($action_tag === 'GP') {
×
418
                                $profile->has_application = 1;
×
419
                        }
420
                        $profile->save();
×
421
                }
422

423
                return response()->json(["Hash" => $txid], 200);
×
424
        }
425

426

427
        /**
428
         * Internal
429
         *
430
         * @hideFromAPIDocumentation
431
         */
432
        public function getBalance($address)
×
433
        {
434
                $balance = AppHelper::getMarscoinBalance($address);
×
435
                return response()->json(["balance" => $balance], 200);
×
436
        }
437

438

439

440
        /**
441
         * Internal
442
         *
443
         * @hideFromAPIDocumentation
444
         */
445
        public function dismissAlert(Request $request)
×
446
        {
447
                $alertType = $request->input('alertType');
×
448

449
                // Only allow known alert types to prevent session key injection
450
                $allowedAlerts = ['wallet_alert', 'citizen_alert', 'onboarding_alert', 'backup_alert', 'endorsement_alert'];
×
451
                if (!in_array($alertType, $allowedAlerts, true)) {
×
452
                        return response()->json(['error' => 'Invalid alert type.'], 400);
×
453
                }
454

455
                session()->put($alertType, true);
×
456
                return response()->json(['success' => true]);
×
457
        }
458

459

460

461
        /**
462
         * Internal
463
         *
464
         * @hideFromAPIDocumentation
465
         */
466
        public function getPrice(Request $request)
×
467
        {
468
                $price = AppHelper::getMarscoinPrice();
×
469
                return response()->json(["mars_price" => $price], 200);
×
470
        }
471

472

473
        /**
474
         * Internal
475
         *
476
         * @hideFromAPIDocumentation
477
         */
478
        public function getTransactions(Request $request)
×
479
        {
480
                $address = $request->input('address');
×
481

482
                // Validate Marscoin address to prevent URL injection
483
                if (!$address || !AppHelper::isValidMarscoinAddress($address)) {
×
484
                        return response()->json(['error' => 'Invalid Marscoin address.'], 400);
×
485
                }
486

NEW
487
                $json = AppHelper::file_get_contents_curl(config('blockchain.explorer.fallback_url') . "/api/txs/?address=" . urlencode($address));
×
488

489
                return response($json)->header('Content-Type', 'application/json');
×
490
        }
491

492
        /**
493
         * Internal
494
         *
495
         * @hideFromAPIDocumentation
496
         */
497
        public function setfullname(Request $request)
×
498
        {
499
                $uid = Auth::user()->id;
×
500
                $firstname = $request->input('firstname');
×
501
                $lastname = $request->input('lastname');
×
502
                if (!$firstname || !$lastname) {
×
503
                        return response()->json(['error' => 'First and last name are required.'], 400);
×
504
                }
505

506
                $fullname = $firstname . " " . $lastname;
×
507

508
                $citcache = Citizen::where('userid', '=', $uid)->first();
×
509
                if (is_null($citcache)) $citcache = new Citizen;
×
510

511
                $citcache->userid = $uid;
×
512
                $citcache->firstname = $firstname;
×
513
                $citcache->lastname = $lastname;
×
514
                $citcache->save();
×
515

516
                $user = User::where('id', '=', $uid)->first();
×
517
                if ($user) {
×
518
                        $user->fullname = $fullname;
×
519
                        $user->save();
×
520
                }
521
                return response()->json(['success' => true]);
×
522
        }
523

524
        /**
525
         *
526
         * @hideFromAPIDocumentation
527
         */
528
        public function cacheonboarding(Request $request)
×
529
        {
530
                $uid = Auth::user()->id;
×
531
                $shortbio = $request->input('shortbio');
×
532
                $displayname = $request->input('displayname');
×
533
                $publicaddress = $request->input('publicaddress');
×
534

535
                $citcache = Citizen::where('userid', '=', $uid)->first();
×
536
                if (is_null($citcache)) $citcache = new Citizen;
×
537

538
                $citcache->userid = $uid;
×
539
                $citcache->shortbio = $shortbio;
×
540
                $citcache->displayname = $displayname;
×
541
                $citcache->public_address = $publicaddress;
×
542
                $citcache->save();
×
543

544
                return response()->json(['success' => true]);
×
545
        }
546

547

548
        public function rejectApplication(Request $request)
×
549
        {
550
                $user = Auth::user();
×
551
                $profile = Profile::where('userid', $user->id)->first();
×
552
                $reporter = Citizen::where('userid', '=', $user->id)->first();
×
553

554
                $rejectionReasons = [
×
555
                        'avatar_link' => 'Missing Personal Image',
×
556
                        'liveness_link' => 'Incomplete Video',
×
557
                        'duplicate' => 'Duplicate Entry'
×
558
                ];
×
559

560
                if (!$profile || !$profile->citizen) {
×
561
                        return response()->json(['error' => 'Unauthorized access.'], 403);
×
562
                }
563

564
                $applicantUserId = $request->input('applicantUserId');
×
565
                $fieldToUpdate = $request->input('field');
×
566

567
                // Validate the field to update
568
                if (!in_array($fieldToUpdate, ['avatar_link', 'liveness_link'])) {
×
569
                        return response()->json(['error' => 'Invalid field specified.'], 400);
×
570
                }
571

572
                // Update the citizen table, setting the specified field to NULL for the applicant
573
                Citizen::where('userid', $applicantUserId)->update([$fieldToUpdate => NULL]);
×
574

575
                $applicant = Citizen::where('userid', '=', $applicantUserId)->first();
×
576
                $applicantAddress = $applicant ? $applicant->public_address : 'Unknown';
×
577

578
                $content = "The application of {$applicantAddress} has been rejected due to " . $rejectionReasons[$fieldToUpdate] . ".";
×
579

580
                $fullname = $reporter ? ($reporter->firstname . ' ' . $reporter->lastname) : $user->fullname;
×
581
                Posts::create([
×
582
                        'thread_id' => 27,
×
583
                        'author_id' => $user->id,
×
584
                        'content' => $content,
×
585
                        'authorName' => $fullname,
×
586
                        'created_at' => now(),
×
587
                        'updated_at' => now(),
×
588
                ]);
×
589

590
                return response()->json(['success' => 'Application has been rejected and recorded.']);
×
591
        }
592

593
        /**
594
         *
595
         * @hideFromAPIDocumentation
596
         */
597
        /**
598
         * Proxy for price.marscoin.org - avoids Cloudflare CSP
599
         */
600
        public function marsPrice()
×
601
        {
602
                try {
NEW
603
                        $response = @file_get_contents(config('blockchain.price.marscoin_url'));
×
604
                        if ($response) {
×
605
                                return response($response)->header('Content-Type', 'application/json');
×
606
                        }
607
                } catch (\Exception $e) {}
×
608
                return response()->json(['error' => 'Price unavailable'], 500);
×
609
        }
610

611
        public function marsUtxoMulti(Request $request)
×
612
        {
613
                $xpub = $request->input('xpub');
×
614
                $receiver = $request->input('receiver_address');
×
615
                $amount = $request->input('amount');
×
616
                if (!$xpub || !$receiver || !$amount) {
×
617
                        return response()->json(['error' => 'Missing parameters'], 400);
×
618
                }
619
                try {
NEW
620
                        $url = config('blockchain.pebas.url') . '/api/mars/utxo-multi?xpub=' . urlencode($xpub) . '&receiver_address=' . urlencode($receiver) . '&amount=' . urlencode($amount);
×
621
                        $json = @file_get_contents($url);
×
622
                        if ($json) {
×
623
                                return response($json)->header('Content-Type', 'application/json');
×
624
                        }
625
                } catch (\Exception $e) {}
×
626
                return response()->json(['error' => 'UTXO selection failed'], 500);
×
627
        }
628

629
        public function marsTxHistory(Request $request)
×
630
        {
631
                $address = $request->input('address');
×
632
                if (!$address || !AppHelper::isValidMarscoinAddress($address)) {
×
633
                        return response()->json(['error' => 'Invalid address'], 400);
×
634
                }
635
                try {
NEW
636
                        $json = @file_get_contents(config('blockchain.pebas.url') . '/api/mars/txhistory/?address=' . urlencode($address));
×
637
                        if ($json) {
×
638
                                return response($json)->header('Content-Type', 'application/json');
×
639
                        }
640
                } catch (\Exception $e) {}
×
641
                return response()->json(['error' => 'Transaction history unavailable'], 500);
×
642
        }
643

644
        public function closewallet(Request $request)
1✔
645
        {
646
                $uid = Auth::user()->id;
1✔
647
                $profile = Profile::where('userid', '=', $uid)->first();
1✔
648
                if ($profile) {
1✔
649
                        $profile->wallet_open = 0;
1✔
650
                        $profile->save();
1✔
651
                }
652
                return response()->json(['success' => true]);
1✔
653
        }
654

655
        /**
656
         *
657
         * @hideFromAPIDocumentation
658
         */
659
        /**
660
         * Called after HD discovery finds an address matching the user's civic wallet.
661
         * Links the HD session to the civic wallet so civic-only features are accessible.
662
         */
663
        /**
664
         * Proxy for pebas HD discovery - avoids CSP issues with Cloudflare
665
         */
666
        public function discoverAddresses(Request $request)
×
667
        {
668
                $xpub = $request->input('xpub');
×
669
                $gapLimit = $request->input('gap_limit', 20);
×
670

671
                if (!$xpub) {
×
672
                        return response()->json(['error' => 'xpub required'], 400);
×
673
                }
674

675
                try {
676
                        $response = \Illuminate\Support\Facades\Http::timeout(30)
×
NEW
677
                                ->get(config('blockchain.pebas.url') . "/api/mars/discover", [
×
678
                                        'xpub' => $xpub,
×
679
                                        'gap_limit' => $gapLimit,
×
680
                                ]);
×
681

682
                        return response()->json($response->json());
×
683
                } catch (\Exception $e) {
×
684
                        return response()->json(['error' => 'Discovery failed: ' . $e->getMessage()], 500);
×
685
                }
686
        }
687

688
        public function linkCivicWallet(Request $request)
×
689
        {
690
                $uid = Auth::user()->id;
×
691
                $address = $request->input('address');
×
692

693
                if (!$address || !AppHelper::isValidMarscoinAddress($address)) {
×
694
                        return response()->json(['error' => 'Invalid address'], 400);
×
695
                }
696

697
                $civicWallet = CivicWallet::where('user_id', $uid)
×
698
                        ->where('public_addr', $address)
×
699
                        ->first();
×
700

701
                if (!$civicWallet) {
×
702
                        return response()->json(['error' => 'No matching civic wallet found'], 404);
×
703
                }
704

705
                $profile = Profile::where('userid', $uid)->first();
×
706
                if ($profile) {
×
707
                        $profile->civic_wallet_open = $civicWallet->id;
×
708
                        $profile->save();
×
709
                        Log::info("Civic wallet linked for user {$uid}: {$address}");
×
710
                }
711

712
                return response()->json([
×
713
                        'success' => true,
×
714
                        'civic_wallet_id' => $civicWallet->id,
×
715
                        'address' => $address,
×
716
                ]);
×
717
        }
718

719
        public function renameWallet(Request $request)
×
720
        {
721
                $request->validate([
×
722
                        'hdwallet_id' => 'required',
×
723
                        'new_name' => 'required|string|max:500',
×
724
                ]);
×
725

726
                $wallet = HDWallet::where('id', $request->hdwallet_id)
×
727
                                                        ->where('user_id', Auth::id())
×
728
                                                        ->firstOrFail();
×
729
                $wallet->wallet_type = $request->new_name;
×
730
                $wallet->save();
×
731
                return response()->json(['success' => 'Wallet renamed successfully']);
×
732
        }
733

734

735
        /**
736
         * @hideFromAPIDocumentation
737
         */
738
        public function setendorsed(Request $request)
×
739
        {
740
                $endorserId = Auth::user()->id;
×
741
                $targetUserId = $request->input("id");
×
742

743
                // Prevent self-endorsement
744
                if ((int) $endorserId === (int) $targetUserId) {
×
745
                        return response()->json(['error' => 'Cannot endorse yourself.'], 400);
×
746
                }
747

748
                // Only citizens can endorse
749
                $endorserProfile = Profile::where('userid', '=', $endorserId)->first();
×
750
                if (!$endorserProfile || !$endorserProfile->citizen) {
×
751
                        return response()->json(['error' => 'Only citizens can endorse.'], 403);
×
752
                }
753

754
                $targetProfile = Profile::where('userid', '=', $targetUserId)->first();
×
755
                if (!$targetProfile) {
×
756
                        return response()->json(['error' => 'User not found.'], 404);
×
757
                }
758

759
                // Cannot endorse someone who is already a citizen
760
                if ($targetProfile->citizen) {
×
761
                        return response()->json(['error' => 'This user is already a citizen.'], 400);
×
762
                }
763

764
                // Check for duplicate endorsement (endorser already endorsed this target)
765
                $targetCitizen = Citizen::where('userid', '=', $targetUserId)->first();
×
766
                $targetAddress = $targetCitizen ? $targetCitizen->public_address : null;
×
767
                if (!$targetAddress) {
×
768
                        $targetWallet = CivicWallet::where('user_id', '=', $targetUserId)->first();
×
769
                        $targetAddress = $targetWallet ? $targetWallet->public_addr : null;
×
770
                }
771

772
                if ($targetAddress) {
×
773
                        $alreadyEndorsed = Feed::where('userid', '=', $endorserId)
×
774
                                ->where('tag', '=', 'ED')
×
775
                                ->where('message', '=', $targetAddress)
×
776
                                ->exists();
×
777
                        if ($alreadyEndorsed) {
×
778
                                return response()->json(['error' => 'You have already endorsed this person.'], 400);
×
779
                        }
780
                }
781

782
                // Enforce endorsement limit: 1 endorsement per 10 citizens, max 5
783
                $citizenCount = Profile::where('citizen', '=', 1)->count();
×
784
                $endorsementAllowance = min(5, max(1, (int) floor($citizenCount / 10)));
×
785
                $endorsementsGiven = Feed::where('userid', '=', $endorserId)
×
786
                        ->where('tag', '=', 'ED')
×
787
                        ->count();
×
788
                if ($endorsementsGiven >= $endorsementAllowance) {
×
789
                        return response()->json([
×
790
                                'error' => "You have reached your endorsement limit ({$endorsementAllowance}). Each citizen may give 1 endorsement per 10 citizens in the republic (max 5)."
×
791
                        ], 400);
×
792
                }
793

794
                // Increment endorsement count
795
                $targetProfile->endorse_cnt = ($targetProfile->endorse_cnt ?? 0) + 1;
×
796
                $targetProfile->save();
×
797

798
                // Check if target now meets the threshold for auto-upgrade to citizen
799
                // Bootstrap: with 0 citizens, threshold is 0 (first pioneer auto-qualifies)
800
                // Then: 1 per 10 citizens (rounded up), capped at 5
801
                $endorsementThreshold = $citizenCount === 0 ? 0 : min(5, max(1, (int) ceil($citizenCount * 0.1)));
×
802
                if ($targetProfile->endorse_cnt >= $endorsementThreshold) {
×
803
                        $targetProfile->citizen = 1;
×
804
                        $targetProfile->save();
×
805

806
                        Log::info("Auto-upgrade to citizen: user {$targetUserId} reached {$targetProfile->endorse_cnt} endorsements (threshold: {$endorsementThreshold})");
×
807
                }
808

809
                return response()->json([
×
810
                        'success' => true,
×
811
                        'endorse_cnt' => $targetProfile->endorse_cnt,
×
812
                        'threshold' => $endorsementThreshold,
×
813
                        'promoted' => (bool) $targetProfile->citizen,
×
814
                ]);
×
815
        }
816

817

818
        /**
819
         * @hideFromAPIDocumentation
820
         */
821
        public function cacheproposal(Request $request)
×
822
        {
823
                $uid = Auth::user()->id;
×
824
                $txid = $request->input('txid');
×
825
                $public_address = $request->input('address');
×
826
                $embedded_link = $request->input('embedded_link');
×
827
                $json = $request->input('message');
×
828
                $data = json_decode($json);
×
829

830
                if (!$data || !isset($data->data)) {
×
831
                        return response()->json(['error' => 'Invalid message payload.'], 400);
×
832
                }
833

834
                $citcache = Citizen::where('userid', '=', $uid)->first();
×
835

836
                if (!AppHelper::isValidCID($embedded_link ?? '')) {
×
837
                        return response()->json(['error' => 'Invalid IPFS hash'], 400);
×
838
                }
839

840
                $category = $data->data->category ?? '';
×
841
                $tier = \App\Includes\GovernanceTiers::categoryToTier($category);
×
842
                $tierConfig = \App\Includes\GovernanceTiers::get($tier);
×
843

844
                $proposal = new Proposals;
×
845
                $proposal->user_id = $uid;
×
846
                $proposal->title = $data->data->title ?? '';
×
847
                $proposal->description = $data->data->description ?? '';
×
848
                $proposal->category = $category;
×
849
                $proposal->tier = $tier;
×
850
                $proposal->author = Auth::user()->fullname;
×
851
                $proposal->ipfs_hash = $embedded_link;
×
852
                $proposal->participation = $data->data->participation ?? $tierConfig['quorum_percent'];
×
853
                $proposal->threshold = $data->data->threshold ?? $tierConfig['threshold'];
×
854
                $proposal->duration = $data->data->duration ?? $tierConfig['duration_sols'];
×
855
                $proposal->expiration = $data->data->expiration ?? $tierConfig['sunset_sols'];
×
856
                $proposal->txid = $txid;
×
857
                $proposal->public_address = $public_address;
×
858
                $proposal->status = 'screening';
×
859

860
                // Calculate lifecycle timestamps
861
                $timestamps = \App\Includes\GovernanceTiers::calculateTimestamps($tier);
×
862
                $proposal->screening_ends_at = $timestamps['screening_ends_at'];
×
863
                $proposal->voting_ends_at = $timestamps['voting_ends_at'];
×
864
                $proposal->timelock_ends_at = $timestamps['timelock_ends_at'];
×
865
                $proposal->sunset_at = $timestamps['sunset_at'];
×
866

867
                $proposal->save();
×
868

869
                // Commit to LegislationRepo
870
                try {
871
                        $repo = new \App\Includes\LegislationRepo();
×
872
                        $gitHash = $repo->submitProposal(
×
873
                                $proposal->id,
×
874
                                $proposal->title,
×
875
                                $proposal->description,
×
876
                                $proposal->author,
×
877
                                $tier,
×
878
                                [
×
879
                                        'participation' => $proposal->participation . '%',
×
880
                                        'threshold' => $proposal->threshold . '%',
×
881
                                        'duration' => $proposal->duration . ' sols',
×
882
                                        'expiration' => $proposal->expiration > 0 ? $proposal->expiration . ' sols' : 'never',
×
883
                                        'txid' => $txid,
×
884
                                ]
×
885
                        );
×
886
                        $proposal->git_hash = $gitHash;
×
887
                        $proposal->save();
×
888
                } catch (\Exception $e) {
×
889
                        Log::warning('LegislationRepo commit failed: ' . $e->getMessage());
×
890
                }
891
                $prop_id = $proposal->id;
×
892

893
                $authorName = $citcache ? ($citcache->firstname . ' ' . $citcache->lastname) : Auth::user()->fullname;
×
894

895
                $post = new Posts;
×
896
                $post->thread_id = 2;
×
897
                $post->author_id = $uid;
×
898
                $post->content = $proposal->description;
×
899
                $post->authorName = $authorName;
×
900
                $post->save();
×
901

902
                $post_id = $post->id;
×
903

904
                $threads = new Threads;
×
905
                $threads->category_id = 2;
×
906
                $threads->author_id = $uid;
×
907
                $threads->title = $data->data->title ?? '';
×
908
                $threads->first_post_id = $post_id;
×
909
                $threads->proposal_id = $prop_id;
×
910
                $threads->save();
×
911

912
                $thd_id = $threads->id;
×
913

914
                Proposals::where('id', $prop_id)->update(['discussion' => $thd_id]);
×
915

916
                return response()->json(["Proposal" => $prop_id, "Discussion" => $thd_id], 200);
×
917
        }
918

919

920

921

922

923
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc