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

marscoin / martianrepublic / 23749891416

30 Mar 2026 02:25PM UTC coverage: 8.467% (+3.2%) from 5.241%
23749891416

push

github

Martian Congress
fix: resolve all 10 PHPStan errors + add 37 new tests

Fixes:
- DashboardController: undefined $user/$citizen in showReports/showCamera
- AiHelperController: env() → config() for cached config safety
- BADS models: add return type hints so Larastan resolves relations
- Ballots model: add return type hints for proposal/user relations
- ErrorTriageMail: fix regex delimiter and add unicode flag
- services.php: add openrouter config block

New test suites (37 tests, 97 assertions):
- AcademyTest: 10 article page loads + 404 handling
- ForumTest: categories, threads, posts, ordering, proposal linkage
- CongressTest: ballot relations, pendingForUser, controller methods
- InstrumentTest: chain of trust, calibration, attestations, soft delete
- AiHelperTest: validation, error triage mail, severity extraction

Total: 79 tests, 161 assertions, 0 PHPStan errors

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

14 of 21 new or added lines in 8 files covered. (66.67%)

1257 existing lines in 18 files now uncovered.

445 of 5256 relevant lines covered (8.47%)

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');
×
UNCOV
44
                $type = $request->input('type');
×
UNCOV
45
                $public_address = $request->input('address');
×
46

47
                        // --- SECURITY: Sanitize the public_address to prevent directory traversal ---
48
                        $safeAddress = AppHelper::sanitizePathSegment($public_address);
×
UNCOV
49
                        if ($safeAddress === null) {
×
UNCOV
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']) {
×
UNCOV
56
                                Log::warning('permapinpic upload rejected: ' . $validation['error'] . ' (user: ' . $uid . ')');
×
UNCOV
57
                                return response()->json(['error' => $validation['error']], 422);
×
58
                        }
59

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

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

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

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

UNCOV
74
                        file_put_contents($file_path, $decodedData);
×
75
                        $hash = AppHelper::upload($file_path, "http://127.0.0.1:5001/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;
×
UNCOV
80
                        $citcache->avatar_link = "https://ipfs.marscoin.org/ipfs/".$hash;
×
81
                        $citcache->save();
×
82

UNCOV
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');
×
UNCOV
97
                $type = $request->input('type');
×
UNCOV
98
                $public_address = $request->input('address');
×
99

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

UNCOV
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']) {
×
UNCOV
114
                                        Log::warning('permapinvideo upload rejected: ' . $validation['error'] . ' (user: ' . $uid . ')');
×
UNCOV
115
                                        return response()->json(['error' => $validation['error']], 422);
×
116
                                }
117

118
                                $file_path = "./assets/citizen/" . $safeAddress . "/";
×
UNCOV
119
                                if (!file_exists($file_path)) {
×
UNCOV
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" );
×
UNCOV
128
                                $file_path = $file_path . "profile_video.webm";
×
129
                                $hash = AppHelper::upload($file_path, "http://127.0.0.1:5001/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;
×
UNCOV
134
                                $citcache->liveness_link = "https://ipfs.marscoin.org/ipfs/".$hash;
×
135
                                $citcache->save();
×
136

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

UNCOV
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');
×
UNCOV
153
                $entry = $request->input('entry');
×
UNCOV
154
                $uid = Auth::user()->id;
×
155

156
                // --- SECURITY: Sanitize the public_address to prevent directory traversal ---
157
                $safeAddress = AppHelper::sanitizePathSegment($public_address);
×
UNCOV
158
                if ($safeAddress === null) {
×
UNCOV
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) ---
UNCOV
163
                if (empty($title)) {
×
UNCOV
164
                        return response()->json(['error' => 'Title is required.'], 400);
×
165
                }
166

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

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

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

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

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

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

191
                $files = $request->file('filenames');
×
UNCOV
192
                if ($files && is_array($files)) {
×
193
                        foreach ($files as $f) {
×
194
                                // --- SECURITY: Validate each uploaded file ---
195
                                $validation = AppHelper::validateUploadedFile($f);
×
UNCOV
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
UNCOV
199
                                        continue;
×
200
                                }
201

UNCOV
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)) {
×
UNCOV
207
                                        Log::warning('permapinlog blocked dangerous file extension: ' . $ext . ' (user: ' . $uid . ')');
×
UNCOV
208
                                        continue;
×
209
                                }
210

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

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

UNCOV
223
                return response()->json(["Hash" => $hash, "Path" => $file_path], 200)
×
UNCOV
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

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

238
                // Validate CID format to prevent injection into the IPFS API URL
UNCOV
239
                if (!AppHelper::isValidCID($cid)) {
×
UNCOV
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();
×
UNCOV
246
                if ($publication && $publication->userid != $uid) {
×
UNCOV
247
                        return response()->json(["error" => "Unauthorized: you can only remove your own publications."], 403);
×
248
                }
249

UNCOV
250
                $ipfsApiUrl = 'http://127.0.0.1:5001/api/v0/pin/rm?arg=' . urlencode($cid) . "&recursive=true";
×
UNCOV
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);
×
UNCOV
257
                        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
×
258
                        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST"); // Ensure this is POST
×
259

UNCOV
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)) {
×
UNCOV
269
                                        if (!empty($responseJson->Message)) {
×
UNCOV
270
                                                $errorMsg = $responseJson->Message;
×
271
                                        }
272
                                }
UNCOV
273
                                throw new \Exception($errorMsg);
×
274
                        }
275

276
                        $publicationDeleted = Publication::where('ipfs_hash', $cid)->delete();
×
UNCOV
277
                        if (!$publicationDeleted) {
×
UNCOV
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
UNCOV
283
                        return response()->json(["error" => $e->getMessage()], 500);
×
284
                }
285

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

289
                // Respond to the client
UNCOV
290
                return response()->json(["message" => "Successfully unpinned CID: $cid"], 200)
×
UNCOV
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');
×
UNCOV
304
                $type = $request->input('type');
×
UNCOV
305
                $json = $request->input('payload');
×
306

307
                // --- SECURITY: Sanitize the public_address to prevent directory traversal ---
308
                $safeAddress = AppHelper::sanitizePathSegment($public_address);
×
UNCOV
309
                if ($safeAddress === null) {
×
UNCOV
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);
×
UNCOV
315
                if ($safeType === null) {
×
UNCOV
316
                        return response()->json(['error' => 'Invalid type format.'], 400);
×
317
                }
318

319
                // --- SECURITY: Check file size (max 5MB) ---
UNCOV
320
                if (strlen($json) > 5242880) {
×
UNCOV
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)) {
×
UNCOV
326
                        Log::warning('permapinjson rejected: payload contains PHP code');
×
UNCOV
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);
×
UNCOV
332
                if ($json !== '' && json_last_error() !== JSON_ERROR_NONE) {
×
UNCOV
333
                        return response()->json(['error' => 'Invalid JSON payload.'], 422);
×
334
                }
335

UNCOV
336
                $projectRoot = config('app.project_root', base_path());
×
UNCOV
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)) {
×
UNCOV
345
                                Log::error("Failed to create directory: " . $base_path);
×
346
                                return response()->json(["error" => "Failed to create directory. Check permissions."], 500);
×
347
                        }
UNCOV
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)) {
×
UNCOV
353
                        Log::error("Directory not writable: " . $base_path);
×
UNCOV
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

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

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

367
                try {
UNCOV
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
UNCOV
373
                                $apiResponse = AppHelper::uploadFolder($file_path, "http://127.0.0.1:5001/api/v0/add?pin=true&recursive=true&wrap-with-directory=true&quieter");
×
374
                        } else {
375
                                // The type does not contain 'log', use upload
UNCOV
376
                                $apiResponse = AppHelper::upload($file_path, "http://127.0.0.1:5001/api/v0/add?pin=true");
×
377
                        }
378

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

UNCOV
386
                        return response()->json($formattedResponse, 200)->header('Content-Type', "application/json;");
×
387
                } catch (\Exception $e) {
×
388
                        // Handle exceptions during the upload and pinning process
UNCOV
389
                        Log::error("Upload error: " . $e->getMessage());
×
UNCOV
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');
×
UNCOV
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) {
×
UNCOV
415
                        $profile->general_public = 1;
×
416
                        // Set has_application when GP application is submitted
417
                        if ($action_tag === 'GP') {
×
UNCOV
418
                                $profile->has_application = 1;
×
419
                        }
UNCOV
420
                        $profile->save();
×
421
                }
422

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

426

427
        /**
428
         * Internal
429
         *
430
         * @hideFromAPIDocumentation
431
         */
UNCOV
432
        public function getBalance($address)
×
433
        {
UNCOV
434
                $balance = AppHelper::getMarscoinBalance($address);
×
UNCOV
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
        {
UNCOV
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'];
×
UNCOV
451
                if (!in_array($alertType, $allowedAlerts, true)) {
×
UNCOV
452
                        return response()->json(['error' => 'Invalid alert type.'], 400);
×
453
                }
454

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

459

460

461
        /**
462
         * Internal
463
         *
464
         * @hideFromAPIDocumentation
465
         */
UNCOV
466
        public function getPrice(Request $request)
×
467
        {
UNCOV
468
                $price = AppHelper::getMarscoinPrice();
×
UNCOV
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
        {
UNCOV
480
                $address = $request->input('address');
×
481

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

UNCOV
487
                $json = AppHelper::file_get_contents_curl("http://explore1.marscoin.org/api/txs/?address=" . urlencode($address));
×
488

UNCOV
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
        {
UNCOV
499
                $uid = Auth::user()->id;
×
500
                $firstname = $request->input('firstname');
×
UNCOV
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();
×
UNCOV
509
                if (is_null($citcache)) $citcache = new Citizen;
×
510

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

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

524
        /**
525
         *
526
         * @hideFromAPIDocumentation
527
         */
UNCOV
528
        public function cacheonboarding(Request $request)
×
529
        {
530
                $uid = Auth::user()->id;
×
UNCOV
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;
×
UNCOV
539
                $citcache->shortbio = $shortbio;
×
UNCOV
540
                $citcache->displayname = $displayname;
×
UNCOV
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',
×
UNCOV
556
                        'liveness_link' => 'Incomplete Video',
×
UNCOV
557
                        'duplicate' => 'Duplicate Entry'
×
558
                ];
×
559

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

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

567
                // Validate the field to update
UNCOV
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
UNCOV
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,
×
UNCOV
583
                        'author_id' => $user->id,
×
584
                        'content' => $content,
×
UNCOV
585
                        'authorName' => $fullname,
×
UNCOV
586
                        'created_at' => now(),
×
UNCOV
587
                        'updated_at' => now(),
×
UNCOV
588
                ]);
×
589

UNCOV
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
         */
UNCOV
600
        public function marsPrice()
×
601
        {
602
                try {
UNCOV
603
                        $response = @file_get_contents('https://price.marscoin.org/json/');
×
UNCOV
604
                        if ($response) {
×
UNCOV
605
                                return response($response)->header('Content-Type', 'application/json');
×
606
                        }
UNCOV
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 {
UNCOV
620
                        $url = 'http://localhost:3001/api/mars/utxo-multi?xpub=' . urlencode($xpub) . '&receiver_address=' . urlencode($receiver) . '&amount=' . urlencode($amount);
×
UNCOV
621
                        $json = @file_get_contents($url);
×
UNCOV
622
                        if ($json) {
×
UNCOV
623
                                return response($json)->header('Content-Type', 'application/json');
×
624
                        }
625
                } catch (\Exception $e) {}
×
UNCOV
626
                return response()->json(['error' => 'UTXO selection failed'], 500);
×
627
        }
628

UNCOV
629
        public function marsTxHistory(Request $request)
×
630
        {
631
                $address = $request->input('address');
×
632
                if (!$address || !AppHelper::isValidMarscoinAddress($address)) {
×
UNCOV
633
                        return response()->json(['error' => 'Invalid address'], 400);
×
634
                }
635
                try {
636
                        $json = @file_get_contents('http://localhost:3001/api/mars/txhistory/?address=' . urlencode($address));
×
637
                        if ($json) {
×
638
                                return response($json)->header('Content-Type', 'application/json');
×
639
                        }
UNCOV
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
         */
UNCOV
666
        public function discoverAddresses(Request $request)
×
667
        {
UNCOV
668
                $xpub = $request->input('xpub');
×
669
                $gapLimit = $request->input('gap_limit', 20);
×
670

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

675
                try {
676
                        $response = \Illuminate\Support\Facades\Http::timeout(30)
×
677
                                ->get("http://localhost:3001/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;
×
UNCOV
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)
×
UNCOV
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;
×
UNCOV
708
                        $profile->save();
×
709
                        Log::info("Civic wallet linked for user {$uid}: {$address}");
×
710
                }
711

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

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

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

734

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

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

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

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

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

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

UNCOV
772
                if ($targetAddress) {
×
UNCOV
773
                        $alreadyEndorsed = Feed::where('userid', '=', $endorserId)
×
UNCOV
774
                                ->where('tag', '=', 'ED')
×
UNCOV
775
                                ->where('message', '=', $targetAddress)
×
UNCOV
776
                                ->exists();
×
UNCOV
777
                        if ($alreadyEndorsed) {
×
UNCOV
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
UNCOV
783
                $citizenCount = Profile::where('citizen', '=', 1)->count();
×
UNCOV
784
                $endorsementAllowance = min(5, max(1, (int) floor($citizenCount / 10)));
×
UNCOV
785
                $endorsementsGiven = Feed::where('userid', '=', $endorserId)
×
UNCOV
786
                        ->where('tag', '=', 'ED')
×
UNCOV
787
                        ->count();
×
UNCOV
788
                if ($endorsementsGiven >= $endorsementAllowance) {
×
UNCOV
789
                        return response()->json([
×
UNCOV
790
                                'error' => "You have reached your endorsement limit ({$endorsementAllowance}). Each citizen may give 1 endorsement per 10 citizens in the republic (max 5)."
×
UNCOV
791
                        ], 400);
×
792
                }
793

794
                // Increment endorsement count
UNCOV
795
                $targetProfile->endorse_cnt = ($targetProfile->endorse_cnt ?? 0) + 1;
×
UNCOV
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
UNCOV
801
                $endorsementThreshold = $citizenCount === 0 ? 0 : min(5, max(1, (int) ceil($citizenCount * 0.1)));
×
UNCOV
802
                if ($targetProfile->endorse_cnt >= $endorsementThreshold) {
×
UNCOV
803
                        $targetProfile->citizen = 1;
×
UNCOV
804
                        $targetProfile->save();
×
805

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

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

817

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

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

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

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

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

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

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

UNCOV
867
                $proposal->save();
×
868

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

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

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

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

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

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

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

UNCOV
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