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

marscoin / martianrepublic / 23829952143

01 Apr 2026 03:05AM UTC coverage: 11.256% (-0.09%) from 11.348%
23829952143

push

github

Martian Congress
feat: shared MarsWallet signing module — eliminate Pebas dependency across all pages

Extract transaction signing, UTXO selection, and broadcast into a shared
mars-tx.blade.php partial (MarsWallet global). All 8 signing pages now use
MarsWallet instead of inline Pebas calls, removing ~1,150 lines of duplicated
crypto code.

Key changes:
- MarsWallet.signCivicAction() for all civic OP_RETURN transactions
- MarsWallet.signSend() for wallet sends with multi-address UTXO selection
- Scheme-aware key derivation (5 BIP44 schemes + brute-force fallback)
- Multi-address UTXO selection with mempool-spent filtering
- CSRF-protected broadcast via /api/broadcast
- Marscoin network constant auto-defined for pages that lack it
- BlockchainRpc: multi-address getUtxosMulti(), mempool filtering, error reporting
- Fix send-to-self bug (receiver address was ignored in UTXO output construction)
- Fix Res Publica feed: render HTML links properly ({!! !!} not {{ }})
- Fix pending items: unmined feed entries now sort first with "Pending" label
- Friendly error messages for mempool conflicts, insufficient funds, key mismatches

Migrated pages: wallet/hd-open, wallet/anchor, wallet/migrate, citizen/join,
citizen/registry, congress/activeproposal, congress/ballot, congress/newproposal,
logbook/dashboard

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

0 of 69 new or added lines in 4 files covered. (0.0%)

1 existing line in 1 file now uncovered.

664 of 5899 relevant lines covered (11.26%)

1.6 hits per line

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

2.48
/app/Services/BlockchainRpc.php
1
<?php
2

3
namespace App\Services;
4

5
use Illuminate\Support\Facades\Log;
6
use Illuminate\Support\Facades\Process;
7

8
/**
9
 * Direct marscoind RPC service — eliminates Electrum/Pebas dependency.
10
 * Requires txindex=1 in marscoin.conf.
11
 */
12
class BlockchainRpc
13
{
14
    private string $cli;
15
    private string $dataDir;
16

17
    public function __construct()
1✔
18
    {
19
        $this->cli = config('blockchain.rpc.cli_path', '/usr/local/bin/marscoin-cli');
1✔
20
        $this->dataDir = config('blockchain.rpc.data_dir', '/root/.marscoin');
1✔
21
    }
22

23
    public function call(string $method, array $params = [], int $timeout = 30): mixed
×
24
    {
25
        $args = [$this->cli, '-datadir='.$this->dataDir, $method, ...$params];
×
26
        $result = Process::timeout($timeout)->run($args);
×
27
        if (! $result->successful()) {
×
NEW
28
            Log::warning("RPC {$method} failed", ['stderr' => trim($result->errorOutput()), 'exit' => $result->exitCode()]);
×
UNCOV
29
            return null;
×
30
        }
31
        $output = trim($result->output());
×
32
        $decoded = json_decode($output, true);
×
33

34
        return $decoded !== null ? $decoded : $output;
×
35
    }
36

37
    public function getBalance(string $address): array
×
38
    {
39
        $result = $this->call('scantxoutset', ['start', json_encode(["addr({$address})"])], 60);
×
40
        if (! $result || ! isset($result['total_amount'])) {
×
41
            return ['balance' => 0, 'utxos' => 0];
×
42
        }
43

44
        return ['balance' => $result['total_amount'], 'utxos' => count($result['unspents'] ?? [])];
×
45
    }
46

47
    public function getUtxos(string $address): array
×
48
    {
49
        $result = $this->call('scantxoutset', ['start', json_encode(["addr({$address})"])], 60);
×
50
        if (! $result || ! isset($result['unspents'])) {
×
51
            return [];
×
52
        }
53

54
        return array_map(fn ($u) => [
×
55
            'txid' => $u['txid'], 'vout' => $u['vout'],
×
56
            'amount' => $u['amount'], 'satoshis' => (int) round($u['amount'] * 1e8),
×
57
            'height' => $u['height'] ?? null, 'confirmations' => $u['confirmations'] ?? 0,
×
58
        ], $result['unspents']);
×
59
    }
60

61
    /**
62
     * Get UTXOs across multiple addresses, filtering out mempool-spent ones.
63
     */
NEW
64
    public function getUtxosMulti(array $addresses): array
×
65
    {
66
        // Build descriptors for all addresses in one scantxoutset call
NEW
67
        $descriptors = array_map(fn ($a) => "addr({$a})", $addresses);
×
NEW
68
        $result = $this->call('scantxoutset', ['start', json_encode($descriptors)], 60);
×
NEW
69
        if (! $result || ! isset($result['unspents'])) {
×
NEW
70
            return [];
×
71
        }
72

73
        // Get mempool txids to filter out already-spent UTXOs
NEW
74
        $mempoolSpent = $this->getMempoolSpentOutputs();
×
75

NEW
76
        $utxos = [];
×
NEW
77
        foreach ($result['unspents'] as $u) {
×
NEW
78
            $outpoint = $u['txid'].':'.$u['vout'];
×
NEW
79
            if (isset($mempoolSpent[$outpoint])) {
×
NEW
80
                Log::debug("Skipping mempool-spent UTXO: {$outpoint}");
×
NEW
81
                continue;
×
82
            }
83
            // Find which address owns this UTXO
NEW
84
            $addr = $u['desc'] ?? null;
×
NEW
85
            if ($addr && preg_match('/addr\(([A-Za-z0-9]+)\)/', $addr, $m)) {
×
NEW
86
                $addr = $m[1];
×
87
            } else {
88
                // Fallback: decode the raw tx to find the address
NEW
89
                $addr = $addresses[0];
×
NEW
90
                foreach ($addresses as $a) {
×
NEW
91
                    if (str_contains($u['desc'] ?? '', $a)) {
×
NEW
92
                        $addr = $a;
×
NEW
93
                        break;
×
94
                    }
95
                }
96
            }
NEW
97
            $utxos[] = [
×
NEW
98
                'txid' => $u['txid'], 'vout' => $u['vout'],
×
NEW
99
                'amount' => $u['amount'], 'satoshis' => (int) round($u['amount'] * 1e8),
×
NEW
100
                'height' => $u['height'] ?? null, 'confirmations' => $u['confirmations'] ?? 0,
×
NEW
101
                'address' => $addr,
×
NEW
102
            ];
×
103
        }
104

NEW
105
        return $utxos;
×
106
    }
107

108
    /**
109
     * Get set of outpoints (txid:vout) that are spent by mempool transactions.
110
     */
NEW
111
    private function getMempoolSpentOutputs(): array
×
112
    {
NEW
113
        $rawMempool = $this->call('getrawmempool', ['true'], 10);
×
NEW
114
        $spent = [];
×
NEW
115
        if (is_array($rawMempool)) {
×
NEW
116
            foreach ($rawMempool as $txid => $info) {
×
117
                // Get the full tx to find its inputs
NEW
118
                $tx = $this->call('getrawtransaction', [$txid, '1'], 10);
×
NEW
119
                if ($tx && isset($tx['vin'])) {
×
NEW
120
                    foreach ($tx['vin'] as $vin) {
×
NEW
121
                        if (isset($vin['txid'])) {
×
NEW
122
                            $spent[$vin['txid'].':'.$vin['vout']] = true;
×
123
                        }
124
                    }
125
                }
126
            }
127
        }
128

NEW
129
        return $spent;
×
130
    }
131

NEW
132
    public function getUtxosForTx(array|string $addresses, float $amount, string $receiver = null, string $changeAddress = null): array
×
133
    {
134
        // Accept single address or array
NEW
135
        if (is_string($addresses)) {
×
NEW
136
            $addresses = [$addresses];
×
137
        }
138

NEW
139
        $utxos = $this->getUtxosMulti($addresses);
×
140
        if (empty($utxos)) {
×
141
            return ['error' => 'No UTXOs found'];
×
142
        }
143
        usort($utxos, fn ($a, $b) => $b['amount'] <=> $a['amount']);
×
144

145
        $selected = [];
×
146
        $total = 0;
×
147
        $targetSat = (int) round($amount * 1e8);
×
148

149
        foreach ($utxos as $utxo) {
×
150
            $rawTx = $this->call('getrawtransaction', [$utxo['txid']]);
×
151
            $selected[] = [
×
152
                'txId' => $utxo['txid'], 'vout' => $utxo['vout'],
×
NEW
153
                'value' => $utxo['satoshis'], 'rawTx' => $rawTx, 'address' => $utxo['address'],
×
154
            ];
×
155
            $total += $utxo['satoshis'];
×
156
            $estFee = (count($selected) * 180 + 68 + 10) * 10;
×
157
            if ($total >= $targetSat + $estFee) {
×
158
                break;
×
159
            }
160
        }
161

NEW
162
        if ($total < $targetSat) {
×
NEW
163
            return ['error' => 'Insufficient funds'];
×
164
        }
165

166
        $fee = (count($selected) * 180 + 68 + 10) * 10;
×
167
        $change = $total - $targetSat - $fee;
×
NEW
168
        $toAddr = $receiver ?: $addresses[0];
×
NEW
169
        $chgAddr = $changeAddress ?: $addresses[0];
×
NEW
170
        $outputs = [['address' => $toAddr, 'value' => $targetSat]];
×
171
        if ($change > 546) {
×
NEW
172
            $outputs[] = ['address' => $chgAddr, 'value' => $change];
×
173
        }
174

175
        return ['inputs' => $selected, 'outputs' => $outputs, 'fee' => $fee];
×
176
    }
177

178
    public function getTransactionHistory(string $address, int $limit = 50): array
×
179
    {
180
        $result = $this->call('scantxoutset', ['start', json_encode(["addr({$address})"])], 60);
×
181
        if (! $result || ! isset($result['unspents'])) {
×
182
            return [];
×
183
        }
184
        $txs = [];
×
185
        foreach (array_slice($result['unspents'], 0, $limit) as $u) {
×
186
            $tx = $this->call('getrawtransaction', [$u['txid'], '1']);
×
187
            if ($tx) {
×
188
                $txs[] = [
×
189
                    'txid' => $u['txid'], 'amount' => $u['amount'],
×
190
                    'confirmations' => $u['confirmations'] ?? 0,
×
191
                    'height' => $u['height'] ?? null, 'time' => $tx['time'] ?? null,
×
192
                ];
×
193
            }
194
        }
195

196
        return $txs;
×
197
    }
198

NEW
199
    public function broadcast(string $rawTxHex): array
×
200
    {
201
        // sendrawtransaction returns txid on success, or CLI exits non-zero with error on stderr
NEW
202
        $args = [$this->cli, '-datadir='.$this->dataDir, 'sendrawtransaction', $rawTxHex];
×
NEW
203
        $result = Process::timeout(30)->run($args);
×
204

NEW
205
        $output = trim($result->output());
×
NEW
206
        $error = trim($result->errorOutput());
×
207

NEW
208
        if ($result->successful() && strlen($output) === 64) {
×
NEW
209
            Log::info("TX broadcast: {$output}");
×
NEW
210
            return ['txid' => $output];
×
211
        }
212

NEW
213
        Log::error('Broadcast failed', ['output' => $output, 'error' => $error, 'exit' => $result->exitCode()]);
×
NEW
214
        return ['error' => $error ?: 'Broadcast failed', 'txid' => null];
×
215
    }
216

217
    public function getBlockCount(): int
×
218
    {
219
        $r = $this->call('getblockcount');
×
220

221
        return is_numeric($r) ? (int) $r : 0;
×
222
    }
223
}
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