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

marscoin / martianrepublic / 23768536652

30 Mar 2026 09:25PM UTC coverage: 9.452% (-0.2%) from 9.636%
23768536652

push

github

Martian Congress
feat: multi-path HD wallet discovery via local marscoind

Replaces Pebas/Electrum-based discovery with direct marscoind
scantxoutset RPC. No more Electrum socket dependency for wallets.

Client-side now derives addresses from all three known BIP44 paths:
- m/44'/2'/0'    (standard Litecoin BIP44)
- m/44'/2'/0'/0' (genSeed extra hardened level)
- m/44'/107'/0'  (legacy Marscoin coin type)

New DiscoveryController batches all addresses into a single
scantxoutset call — checks 120 addresses in one RPC round-trip.
Caches discovered derivation schemes per user for faster subsequent loads.

Also: removed .ralph/ and .vscode/ from repo, added to .gitignore.

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

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

511 of 5406 relevant lines covered (9.45%)

1.55 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
     */
NEW
21
    public function discover(Request $request)
×
22
    {
NEW
23
        $request->validate([
×
NEW
24
            'addresses' => 'required|array|min:1|max:200',
×
NEW
25
            'addresses.*.address' => 'required|string|max:50',
×
NEW
26
            'addresses.*.path' => 'required|string|max:50',
×
NEW
27
            'addresses.*.chain' => 'required|string|in:receiving,change',
×
NEW
28
            'addresses.*.index' => 'required|integer|min:0',
×
NEW
29
            'addresses.*.scheme' => 'required|string|max:30',
×
NEW
30
        ]);
×
31

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

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

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

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

NEW
52
            if (!$result->successful()) {
×
NEW
53
                Log::warning('Discovery: scantxoutset failed', ['error' => $result->errorOutput()]);
×
NEW
54
                return response()->json(['error' => 'UTXO scan failed'], 500);
×
55
            }
56

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

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

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

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

97
            // Also check addresses with zero balance but historical transactions
98
            // via getrawtransaction if txindex=1 (future enhancement)
99

NEW
100
        } catch (\Exception $e) {
×
NEW
101
            Log::error('Discovery: exception', ['error' => $e->getMessage()]);
×
NEW
102
            return response()->json(['error' => 'Discovery failed: ' . $e->getMessage()], 500);
×
103
        }
104

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

NEW
111
        return response()->json([
×
NEW
112
            'totalBalance' => round($totalBalance, 8),
×
NEW
113
            'totalUnconfirmed' => $totalUnconfirmed,
×
NEW
114
            'addressCount' => count($discovered),
×
NEW
115
            'addresses' => $discovered,
×
NEW
116
        ]);
×
117
    }
118

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

NEW
129
        $cli = config('blockchain.rpc.cli_path');
×
NEW
130
        $dataDir = config('blockchain.rpc.data_dir');
×
131

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

NEW
139
            if (!$result->successful()) {
×
NEW
140
                return response()->json(['error' => 'Scan failed'], 500);
×
141
            }
142

NEW
143
            $scan = json_decode($result->output(), true);
×
NEW
144
            $utxos = $scan['unspents'] ?? [];
×
145

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

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

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