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

marscoin / martianrepublic / 23768961579

30 Mar 2026 09:37PM UTC coverage: 9.39% (-0.06%) from 9.452%
23768961579

push

github

Martian Congress
fix: txhistory falls back to local marscoind when Electrum is down

Pebas Electrum connection is flaky (remote server at 147.182.177.23).
Now tries Pebas first with 5s timeout, falls back to local marscoind
scantxoutset + getrawtransaction (txindex=1).

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

0 of 40 new or added lines in 1 file covered. (0.0%)

511 of 5442 relevant lines covered (9.39%)

1.54 hits per line

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

8.38
/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() {
7✔
29
                $this->middleware("auth");
7✔
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);
×
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;
×
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";
×
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;
×
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 {
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

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
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
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)
2✔
403
        {
404
                $uid = Auth::user()->id;
2✔
405
                $txid = $request->input('txid');
2✔
406
                $action_tag = $request->input('type');
2✔
407
                $public_address = $request->input('address');
2✔
408
                $embedded_link = $request->input('embedded_link');
2✔
409
                $message = $request->input('message');
2✔
410

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

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

423
                return response()->json(["Hash" => $txid], 200);
2✔
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

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 {
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 {
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

636
                // Try Pebas first (has full Electrum history)
637
                try {
NEW
638
                        $response = \Illuminate\Support\Facades\Http::timeout(5)
×
NEW
639
                                ->get(config('blockchain.pebas.url') . '/api/mars/txhistory/', ['address' => $address]);
×
NEW
640
                        if ($response->successful()) {
×
NEW
641
                                $data = $response->json();
×
NEW
642
                                if (!isset($data['error'])) {
×
NEW
643
                                        return response()->json($data);
×
644
                                }
645
                        }
646
                } catch (\Exception $e) {}
×
647

648
                // Fallback: marscoind scantxoutset + getrawtransaction (txindex=1)
649
                try {
NEW
650
                        $cli = config('blockchain.rpc.cli_path');
×
NEW
651
                        $dataDir = config('blockchain.rpc.data_dir');
×
NEW
652
                        $descriptors = json_encode(['addr(' . $address . ')']);
×
653

NEW
654
                        $result = \Illuminate\Support\Facades\Process::timeout(30)->run([
×
NEW
655
                                $cli, '-datadir=' . $dataDir, 'scantxoutset', 'start', $descriptors,
×
NEW
656
                        ]);
×
657

NEW
658
                        if (!$result->successful()) {
×
NEW
659
                                return response()->json(['error' => 'Transaction history unavailable'], 500);
×
660
                        }
661

NEW
662
                        $scan = json_decode($result->output(), true);
×
NEW
663
                        $utxos = $scan['unspents'] ?? [];
×
NEW
664
                        $transactions = [];
×
665

NEW
666
                        foreach (array_slice($utxos, 0, 50) as $utxo) {
×
667
                                try {
NEW
668
                                        $txResult = \Illuminate\Support\Facades\Process::timeout(10)->run([
×
NEW
669
                                                $cli, '-datadir=' . $dataDir, 'getrawtransaction', $utxo['txid'], '1',
×
NEW
670
                                        ]);
×
NEW
671
                                        if ($txResult->successful()) {
×
NEW
672
                                                $tx = json_decode($txResult->output(), true);
×
NEW
673
                                                $transactions[] = [
×
NEW
674
                                                        'txid' => $utxo['txid'],
×
NEW
675
                                                        'amount' => $utxo['amount'],
×
NEW
676
                                                        'confirmations' => $utxo['confirmations'] ?? 0,
×
NEW
677
                                                        'height' => $utxo['height'] ?? null,
×
NEW
678
                                                        'time' => $tx['time'] ?? null,
×
NEW
679
                                                        'blocktime' => $tx['blocktime'] ?? null,
×
NEW
680
                                                ];
×
681
                                        }
NEW
682
                                } catch (\Exception $e) { continue; }
×
683
                        }
684

NEW
685
                        return response()->json([
×
NEW
686
                                'address' => $address,
×
NEW
687
                                'balance' => $scan['total_amount'] ?? 0,
×
NEW
688
                                'transactions' => $transactions,
×
NEW
689
                                'source' => 'marscoind',
×
NEW
690
                        ]);
×
NEW
691
                } catch (\Exception $e) {
×
NEW
692
                        return response()->json(['error' => 'Transaction history unavailable: ' . $e->getMessage()], 500);
×
693
                }
694
        }
695

696
        public function closewallet(Request $request)
3✔
697
        {
698
                $uid = Auth::user()->id;
3✔
699
                $profile = Profile::where('userid', '=', $uid)->first();
3✔
700
                if ($profile) {
3✔
701
                        $profile->wallet_open = 0;
3✔
702
                        $profile->save();
3✔
703
                }
704
                return response()->json(['success' => true]);
3✔
705
        }
706

707
        /**
708
         *
709
         * @hideFromAPIDocumentation
710
         */
711
        /**
712
         * Called after HD discovery finds an address matching the user's civic wallet.
713
         * Links the HD session to the civic wallet so civic-only features are accessible.
714
         */
715
        /**
716
         * Proxy for pebas HD discovery - avoids CSP issues with Cloudflare
717
         */
718
        public function discoverAddresses(Request $request)
×
719
        {
720
                $xpub = $request->input('xpub');
×
721
                $gapLimit = $request->input('gap_limit', 20);
×
722

723
                if (!$xpub) {
×
724
                        return response()->json(['error' => 'xpub required'], 400);
×
725
                }
726

727
                try {
728
                        $response = \Illuminate\Support\Facades\Http::timeout(30)
×
729
                                ->get(config('blockchain.pebas.url') . "/api/mars/discover", [
×
730
                                        'xpub' => $xpub,
×
731
                                        'gap_limit' => $gapLimit,
×
732
                                ]);
×
733

734
                        return response()->json($response->json());
×
735
                } catch (\Exception $e) {
×
736
                        return response()->json(['error' => 'Discovery failed: ' . $e->getMessage()], 500);
×
737
                }
738
        }
739

740
        public function linkCivicWallet(Request $request)
2✔
741
        {
742
                $uid = Auth::user()->id;
2✔
743
                $address = $request->input('address');
2✔
744

745
                if (!$address || !AppHelper::isValidMarscoinAddress($address)) {
2✔
746
                        return response()->json(['error' => 'Invalid address'], 400);
×
747
                }
748

749
                $civicWallet = CivicWallet::where('user_id', $uid)
2✔
750
                        ->where('public_addr', $address)
2✔
751
                        ->first();
2✔
752

753
                if (!$civicWallet) {
2✔
754
                        return response()->json(['error' => 'No matching civic wallet found'], 404);
1✔
755
                }
756

757
                $profile = Profile::where('userid', $uid)->first();
1✔
758
                if ($profile) {
1✔
759
                        $profile->civic_wallet_open = $civicWallet->id;
1✔
760
                        $profile->save();
1✔
761
                        Log::info("Civic wallet linked for user {$uid}: {$address}");
1✔
762
                }
763

764
                return response()->json([
1✔
765
                        'success' => true,
1✔
766
                        'civic_wallet_id' => $civicWallet->id,
1✔
767
                        'address' => $address,
1✔
768
                ]);
1✔
769
        }
770

771
        public function renameWallet(Request $request)
×
772
        {
773
                $request->validate([
×
774
                        'hdwallet_id' => 'required',
×
775
                        'new_name' => 'required|string|max:500',
×
776
                ]);
×
777

778
                $wallet = HDWallet::where('id', $request->hdwallet_id)
×
779
                                                        ->where('user_id', Auth::id())
×
780
                                                        ->firstOrFail();
×
781
                $wallet->wallet_type = $request->new_name;
×
782
                $wallet->save();
×
783
                return response()->json(['success' => 'Wallet renamed successfully']);
×
784
        }
785

786

787
        /**
788
         * @hideFromAPIDocumentation
789
         */
790
        public function setendorsed(Request $request)
×
791
        {
792
                $endorserId = Auth::user()->id;
×
793
                $targetUserId = $request->input("id");
×
794

795
                // Prevent self-endorsement
796
                if ((int) $endorserId === (int) $targetUserId) {
×
797
                        return response()->json(['error' => 'Cannot endorse yourself.'], 400);
×
798
                }
799

800
                // Only citizens can endorse
801
                $endorserProfile = Profile::where('userid', '=', $endorserId)->first();
×
802
                if (!$endorserProfile || !$endorserProfile->citizen) {
×
803
                        return response()->json(['error' => 'Only citizens can endorse.'], 403);
×
804
                }
805

806
                $targetProfile = Profile::where('userid', '=', $targetUserId)->first();
×
807
                if (!$targetProfile) {
×
808
                        return response()->json(['error' => 'User not found.'], 404);
×
809
                }
810

811
                // Cannot endorse someone who is already a citizen
812
                if ($targetProfile->citizen) {
×
813
                        return response()->json(['error' => 'This user is already a citizen.'], 400);
×
814
                }
815

816
                // Check for duplicate endorsement (endorser already endorsed this target)
817
                $targetCitizen = Citizen::where('userid', '=', $targetUserId)->first();
×
818
                $targetAddress = $targetCitizen ? $targetCitizen->public_address : null;
×
819
                if (!$targetAddress) {
×
820
                        $targetWallet = CivicWallet::where('user_id', '=', $targetUserId)->first();
×
821
                        $targetAddress = $targetWallet ? $targetWallet->public_addr : null;
×
822
                }
823

824
                if ($targetAddress) {
×
825
                        $alreadyEndorsed = Feed::where('userid', '=', $endorserId)
×
826
                                ->where('tag', '=', 'ED')
×
827
                                ->where('message', '=', $targetAddress)
×
828
                                ->exists();
×
829
                        if ($alreadyEndorsed) {
×
830
                                return response()->json(['error' => 'You have already endorsed this person.'], 400);
×
831
                        }
832
                }
833

834
                // Enforce endorsement limit: 1 endorsement per 10 citizens, max 5
835
                $citizenCount = Profile::where('citizen', '=', 1)->count();
×
836
                $endorsementAllowance = min(5, max(1, (int) floor($citizenCount / 10)));
×
837
                $endorsementsGiven = Feed::where('userid', '=', $endorserId)
×
838
                        ->where('tag', '=', 'ED')
×
839
                        ->count();
×
840
                if ($endorsementsGiven >= $endorsementAllowance) {
×
841
                        return response()->json([
×
842
                                'error' => "You have reached your endorsement limit ({$endorsementAllowance}). Each citizen may give 1 endorsement per 10 citizens in the republic (max 5)."
×
843
                        ], 400);
×
844
                }
845

846
                // Increment endorsement count
847
                $targetProfile->endorse_cnt = ($targetProfile->endorse_cnt ?? 0) + 1;
×
848
                $targetProfile->save();
×
849

850
                // Check if target now meets the threshold for auto-upgrade to citizen
851
                // Bootstrap: with 0 citizens, threshold is 0 (first pioneer auto-qualifies)
852
                // Then: 1 per 10 citizens (rounded up), capped at 5
853
                $endorsementThreshold = $citizenCount === 0 ? 0 : min(5, max(1, (int) ceil($citizenCount * 0.1)));
×
854
                if ($targetProfile->endorse_cnt >= $endorsementThreshold) {
×
855
                        $targetProfile->citizen = 1;
×
856
                        $targetProfile->save();
×
857

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

861
                return response()->json([
×
862
                        'success' => true,
×
863
                        'endorse_cnt' => $targetProfile->endorse_cnt,
×
864
                        'threshold' => $endorsementThreshold,
×
865
                        'promoted' => (bool) $targetProfile->citizen,
×
866
                ]);
×
867
        }
868

869

870
        /**
871
         * @hideFromAPIDocumentation
872
         */
873
        public function cacheproposal(Request $request)
×
874
        {
875
                $uid = Auth::user()->id;
×
876
                $txid = $request->input('txid');
×
877
                $public_address = $request->input('address');
×
878
                $embedded_link = $request->input('embedded_link');
×
879
                $json = $request->input('message');
×
880
                $data = json_decode($json);
×
881

882
                if (!$data || !isset($data->data)) {
×
883
                        return response()->json(['error' => 'Invalid message payload.'], 400);
×
884
                }
885

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

888
                if (!AppHelper::isValidCID($embedded_link ?? '')) {
×
889
                        return response()->json(['error' => 'Invalid IPFS hash'], 400);
×
890
                }
891

892
                $category = $data->data->category ?? '';
×
893
                $tier = \App\Includes\GovernanceTiers::categoryToTier($category);
×
894
                $tierConfig = \App\Includes\GovernanceTiers::get($tier);
×
895

896
                $proposal = new Proposals;
×
897
                $proposal->user_id = $uid;
×
898
                $proposal->title = $data->data->title ?? '';
×
899
                $proposal->description = $data->data->description ?? '';
×
900
                $proposal->category = $category;
×
901
                $proposal->tier = $tier;
×
902
                $proposal->author = Auth::user()->fullname;
×
903
                $proposal->ipfs_hash = $embedded_link;
×
904
                $proposal->participation = $data->data->participation ?? $tierConfig['quorum_percent'];
×
905
                $proposal->threshold = $data->data->threshold ?? $tierConfig['threshold'];
×
906
                $proposal->duration = $data->data->duration ?? $tierConfig['duration_sols'];
×
907
                $proposal->expiration = $data->data->expiration ?? $tierConfig['sunset_sols'];
×
908
                $proposal->txid = $txid;
×
909
                $proposal->public_address = $public_address;
×
910
                $proposal->status = 'screening';
×
911

912
                // Calculate lifecycle timestamps
913
                $timestamps = \App\Includes\GovernanceTiers::calculateTimestamps($tier);
×
914
                $proposal->screening_ends_at = $timestamps['screening_ends_at'];
×
915
                $proposal->voting_ends_at = $timestamps['voting_ends_at'];
×
916
                $proposal->timelock_ends_at = $timestamps['timelock_ends_at'];
×
917
                $proposal->sunset_at = $timestamps['sunset_at'];
×
918

919
                $proposal->save();
×
920

921
                // Commit to LegislationRepo
922
                try {
923
                        $repo = new \App\Includes\LegislationRepo();
×
924
                        $gitHash = $repo->submitProposal(
×
925
                                $proposal->id,
×
926
                                $proposal->title,
×
927
                                $proposal->description,
×
928
                                $proposal->author,
×
929
                                $tier,
×
930
                                [
×
931
                                        'participation' => $proposal->participation . '%',
×
932
                                        'threshold' => $proposal->threshold . '%',
×
933
                                        'duration' => $proposal->duration . ' sols',
×
934
                                        'expiration' => $proposal->expiration > 0 ? $proposal->expiration . ' sols' : 'never',
×
935
                                        'txid' => $txid,
×
936
                                ]
×
937
                        );
×
938
                        $proposal->git_hash = $gitHash;
×
939
                        $proposal->save();
×
940
                } catch (\Exception $e) {
×
941
                        Log::warning('LegislationRepo commit failed: ' . $e->getMessage());
×
942
                }
943
                $prop_id = $proposal->id;
×
944

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

947
                $post = new Posts;
×
948
                $post->thread_id = 2;
×
949
                $post->author_id = $uid;
×
950
                $post->content = $proposal->description;
×
951
                $post->authorName = $authorName;
×
952
                $post->save();
×
953

954
                $post_id = $post->id;
×
955

956
                $threads = new Threads;
×
957
                $threads->category_id = 2;
×
958
                $threads->author_id = $uid;
×
959
                $threads->title = $data->data->title ?? '';
×
960
                $threads->first_post_id = $post_id;
×
961
                $threads->proposal_id = $prop_id;
×
962
                $threads->save();
×
963

964
                $thd_id = $threads->id;
×
965

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

968
                return response()->json(["Proposal" => $prop_id, "Discussion" => $thd_id], 200);
×
969
        }
970

971

972

973

974

975
}
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