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

marscoin / martianrepublic / 23804883405

31 Mar 2026 03:14PM UTC coverage: 11.195% (+1.8%) from 9.347%
23804883405

push

github

Martian Congress
refactor: Tier 1 — security fixes, route standardization, code style, docs

S9:  File upload path traversal — assert realpath() within allowed dirs,
     sanitize citizen address in file paths
S11: Citizen registry queries capped with LIMIT 100
S12: External HTTP calls — replaced file_get_contents with Http::timeout()
     in CongressController, Wallet/ApiController, AppHelper
A8:  All routes converted to Laravel 9+ array notation
     [Controller::class, 'method']
D2:  Created DEVELOPMENT.md — complete setup guide for contributors
D4:  Laravel Pint code style enforced across 211 files

Tests updated: public route assertions corrected after route syntax
standardization exposed that old string-notation routes never resolved
properly in tests.

127 tests, 251 assertions, 0 PHPStan errors.

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

302 of 3262 new or added lines in 58 files covered. (9.26%)

139 existing lines in 24 files now uncovered.

620 of 5538 relevant lines covered (11.2%)

1.54 hits per line

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

0.0
/app/Http/Controllers/Wallet/DiscoveryController.php
1
<?php
2

3
namespace App\Http\Controllers\Wallet;
4

5
use App\Http\Controllers\Controller;
6
use Illuminate\Http\Request;
7
use Illuminate\Support\Facades\Auth;
8
use Illuminate\Support\Facades\Cache;
9
use Illuminate\Support\Facades\Log;
10
use Illuminate\Support\Facades\Process;
11

12
class DiscoveryController extends Controller
13
{
14
    /**
15
     * Discover HD wallet addresses with funds across multiple derivation paths.
16
     * Uses marscoind scantxoutset — no Electrum dependency.
17
     *
18
     * Client sends an array of addresses to check (derived client-side from all
19
     * possible BIP44 paths). Server checks each against the UTXO set.
20
     */
21
    public function discover(Request $request)
×
22
    {
23
        $request->validate([
×
24
            'addresses' => 'required|array|min:1|max:200',
×
25
            'addresses.*.address' => 'required|string|max:50',
×
26
            'addresses.*.path' => 'required|string|max:50',
×
27
            'addresses.*.chain' => 'required|string|in:receiving,change',
×
28
            'addresses.*.index' => 'required|integer|min:0',
×
29
            'addresses.*.scheme' => 'required|string|max:30',
×
30
        ]);
×
31

32
        $addresses = $request->input('addresses');
×
33
        $discovered = [];
×
34
        $totalBalance = 0;
×
35
        $totalUnconfirmed = 0;
×
36

37
        // Batch check via scantxoutset — single RPC call for all addresses
38
        $descriptors = array_map(function ($a) {
×
NEW
39
            return 'addr('.$a['address'].')';
×
40
        }, $addresses);
×
41

42
        $cli = config('blockchain.rpc.cli_path');
×
43
        $dataDir = config('blockchain.rpc.data_dir');
×
44
        $descriptorJson = json_encode($descriptors);
×
45

46
        try {
47
            $result = Process::timeout(60)->run([
×
NEW
48
                $cli, '-datadir='.$dataDir, 'scantxoutset', 'start',
×
49
                $descriptorJson,
×
50
            ]);
×
51

NEW
52
            if (! $result->successful()) {
×
53
                Log::warning('Discovery: scantxoutset failed', ['error' => $result->errorOutput()]);
×
54

UNCOV
55
                return response()->json(['error' => 'UTXO scan failed'], 500);
×
56
            }
57

58
            $scan = json_decode($result->output(), true);
×
NEW
59
            if (! $scan || ! isset($scan['unspents'])) {
×
60
                return response()->json(['error' => 'Invalid scan result'], 500);
×
61
            }
62

63
            // Build a map: address → UTXO totals
64
            $utxoMap = [];
×
65
            foreach ($scan['unspents'] as $utxo) {
×
66
                // Extract address from the descriptor
67
                $addr = null;
×
68
                if (preg_match('/addr\(([A-Za-z0-9]+)\)/', $utxo['desc'] ?? '', $m)) {
×
69
                    $addr = $m[1];
×
70
                }
NEW
71
                if (! $addr) {
×
NEW
72
                    continue;
×
73
                }
74

NEW
75
                if (! isset($utxoMap[$addr])) {
×
76
                    $utxoMap[$addr] = ['balance' => 0, 'utxoCount' => 0];
×
77
                }
78
                $utxoMap[$addr]['balance'] += $utxo['amount'];
×
79
                $utxoMap[$addr]['utxoCount']++;
×
80
            }
81

82
            // Match back to the input addresses
83
            foreach ($addresses as $a) {
×
84
                $addr = $a['address'];
×
85
                if (isset($utxoMap[$addr])) {
×
86
                    $discovered[] = [
×
87
                        'address' => $addr,
×
88
                        'balance' => round($utxoMap[$addr]['balance'], 8),
×
89
                        'unconfirmed' => 0,
×
90
                        'chain' => $a['chain'],
×
91
                        'index' => $a['index'],
×
92
                        'path' => $a['path'],
×
93
                        'scheme' => $a['scheme'],
×
94
                        'utxoCount' => $utxoMap[$addr]['utxoCount'],
×
95
                    ];
×
96
                    $totalBalance += $utxoMap[$addr]['balance'];
×
97
                }
98
            }
99

100
            // Also check addresses with zero balance but historical transactions
101
            // via getrawtransaction if txindex=1 (future enhancement)
102

103
        } catch (\Exception $e) {
×
104
            Log::error('Discovery: exception', ['error' => $e->getMessage()]);
×
105

NEW
106
            return response()->json(['error' => 'Discovery failed: '.$e->getMessage()], 500);
×
107
        }
108

109
        // Cache discovered derivation schemes for this user
110
        if (Auth::check() && count($discovered) > 0) {
×
111
            $schemes = array_unique(array_column($discovered, 'scheme'));
×
NEW
112
            Cache::put('wallet_schemes:'.Auth::id(), $schemes, now()->addDays(30));
×
113
        }
114

115
        return response()->json([
×
116
            'totalBalance' => round($totalBalance, 8),
×
117
            'totalUnconfirmed' => $totalUnconfirmed,
×
118
            'addressCount' => count($discovered),
×
119
            'addresses' => $discovered,
×
120
        ]);
×
121
    }
122

123
    /**
124
     * Get transaction history for a specific address via marscoind.
125
     * Uses getrawtransaction with txindex=1.
126
     */
127
    public function addressTransactions(Request $request, string $address)
×
128
    {
NEW
129
        if (! preg_match('/^M[A-Za-z0-9]{25,34}$/', $address)) {
×
130
            return response()->json(['error' => 'Invalid address'], 400);
×
131
        }
132

133
        $cli = config('blockchain.rpc.cli_path');
×
134
        $dataDir = config('blockchain.rpc.data_dir');
×
135

136
        // Get UTXOs for this address
137
        try {
138
            $result = Process::timeout(30)->run([
×
NEW
139
                $cli, '-datadir='.$dataDir, 'scantxoutset', 'start',
×
NEW
140
                json_encode(['addr('.$address.')']),
×
UNCOV
141
            ]);
×
142

NEW
143
            if (! $result->successful()) {
×
144
                return response()->json(['error' => 'Scan failed'], 500);
×
145
            }
146

147
            $scan = json_decode($result->output(), true);
×
148
            $utxos = $scan['unspents'] ?? [];
×
149

150
            // Get full transaction details for each UTXO
151
            $transactions = [];
×
152
            foreach (array_slice($utxos, 0, 50) as $utxo) {
×
153
                try {
154
                    $txResult = Process::timeout(10)->run([
×
NEW
155
                        $cli, '-datadir='.$dataDir, 'getrawtransaction',
×
156
                        $utxo['txid'], '1',
×
157
                    ]);
×
158

159
                    if ($txResult->successful()) {
×
160
                        $tx = json_decode($txResult->output(), true);
×
161
                        $transactions[] = [
×
162
                            'txid' => $utxo['txid'],
×
163
                            'amount' => $utxo['amount'],
×
164
                            'confirmations' => $utxo['confirmations'] ?? 0,
×
165
                            'height' => $utxo['height'] ?? null,
×
166
                            'time' => $tx['time'] ?? null,
×
167
                        ];
×
168
                    }
169
                } catch (\Exception $e) {
×
170
                    continue;
×
171
                }
172
            }
173

174
            return response()->json([
×
175
                'address' => $address,
×
176
                'balance' => $scan['total_amount'] ?? 0,
×
177
                'txCount' => count($transactions),
×
178
                'transactions' => $transactions,
×
179
            ]);
×
180
        } catch (\Exception $e) {
×
181
            return response()->json(['error' => $e->getMessage()], 500);
×
182
        }
183
    }
184
}
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