Completed
Pull Request — master (#591)
by thomas
22:54 queued 09:12
created

RpcServer::fundOutput()   B

Complexity

Conditions 4
Paths 6

Size

Total Lines 31
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 20
nc 6
nop 2
dl 0
loc 31
rs 8.5806
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace BitWasp\Bitcoin\RpcTest;
6
7
use BitWasp\Bitcoin\Network\NetworkInterface;
8
use BitWasp\Bitcoin\Script\ScriptInterface;
9
use BitWasp\Bitcoin\Transaction\Factory\TxBuilder;
10
use BitWasp\Bitcoin\Transaction\OutPoint;
11
use BitWasp\Bitcoin\Transaction\TransactionFactory;
12
use BitWasp\Bitcoin\Transaction\TransactionOutput;
13
use BitWasp\Bitcoin\Utxo\Utxo;
14
use BitWasp\Buffertools\Buffer;
15
use Nbobtc\Command\Command;
16
use Nbobtc\Http\Client;
17
18
class RpcServer
19
{
20
    const ERROR_STARTUP = -28;
21
    const ERROR_TX_MEMPOOL_CONFLICT = -26;
22
23
    /**
24
     * @var string
25
     */
26
    private $dataDir;
27
28
    /**
29
     * @var string
30
     */
31
    private $bitcoind;
32
33
    /**
34
     * @var NetworkInterface
35
     */
36
    private $network;
37
38
    /**
39
     * @var RpcCredential
40
     */
41
    private $credential;
42
43
    /**
44
     * @var Client
45
     */
46
    private $client;
47
48
    /**
49
     * @var bool
50
     */
51
    private $softforks = false;
52
53
    private $defaultOptions = [
54
        "daemon" => 1,
55
        "server" => 1,
56
        "regtest" => 1,
57
    ];
58
59
    private $options = [];
60
61
    /**
62
     * RpcServer constructor.
63
     * @param $bitcoind
64
     * @param $dataDir
65
     * @param NetworkInterface $network
66
     * @param RpcCredential $credential
67
     * @param array $options
68
     */
69
    public function __construct(string $bitcoind, string $dataDir, NetworkInterface $network, RpcCredential $credential, array $options = [])
70
    {
71
        $this->bitcoind = $bitcoind;
72
        $this->dataDir = $dataDir;
73
        $this->network = $network;
74
        $this->credential = $credential;
75
        $this->options = array_merge($options, $this->defaultOptions);
76
    }
77
78
    /**
79
     * @return string
80
     */
81
    private function getPidFile(): string
82
    {
83
        return "{$this->dataDir}/regtest/bitcoind.pid";
84
    }
85
86
    /**
87
     * @return string
88
     */
89
    private function getConfigFile(): string
90
    {
91
        return "{$this->dataDir}/bitcoin.conf";
92
    }
93
94
    private function secondsToMicro(float $seconds): int
95
    {
96
        return (int) $seconds * 1000000;
97
    }
98
99
    /**
100
     * @param RpcCredential $rpcCredential
101
     */
102
    private function writeConfigToFile(RpcCredential $rpcCredential)
103
    {
104
        $fd = fopen($this->getConfigFile(), "w");
105
        if (!$fd) {
106
            throw new \RuntimeException("Failed to open bitcoin.conf for writing");
107
        }
108
109
        $config = array_merge(
110
            $this->options,
111
            $rpcCredential->getConfigArray()
112
        );
113
114
        $iniConfig = implode("\n", array_map(function ($value, $key) {
115
            return "{$key}={$value}";
116
        }, $config, array_keys($config)));
117
118
        if (!fwrite($fd, $iniConfig)) {
119
            throw new \RuntimeException("Failed to write to bitcoin.conf");
120
        }
121
122
        fclose($fd);
123
    }
124
125
    /**
126
     * @return void
127
     */
128
    public function start()
129
    {
130
        if ($this->isRunning()) {
131
            return;
132
        }
133
134
        $this->writeConfigToFile($this->credential);
135
        $res = 0;
136
        $out = '';
137
        $result = exec(sprintf("%s -datadir=%s", $this->bitcoind, $this->dataDir), $out, $res);
138
139
        if ($res !== 0) {
140
            throw new \RuntimeException("Failed to start bitcoind: {$this->dataDir}\n");
141
        }
142
143
        $start = microtime(true);
144
        $limit = 10;
145
        $connected = false;
146
147
        $conn = $this->getClient();
148
        do {
149
            try {
150
                $result = json_decode($conn->sendCommand(new Command("getblockchaininfo"))->getBody()->getContents(), true);
151
                if ($result['error'] === null) {
152
                    $connected = true;
153
                } else {
154
                    if ($result['error']['code'] !== self::ERROR_STARTUP) {
155
                        throw new \RuntimeException("Unexpected error code during startup");
156
                    }
157
158
                    // 0.2 seconds sleep
159
                    usleep($this->secondsToMicro(0.02));
160
                }
161
            } catch (\Exception $e) {
162
                // 0.2 seconds sleep
163
                usleep($this->secondsToMicro(0.02));
164
            }
165
166
            if (microtime(true) > $start + $limit) {
167
                throw new \RuntimeException("Timeout elapsed, never made connection to bitcoind");
168
            }
169
        } while (!$connected);
170
    }
171
172
    /**
173
     * @return Client
174
     */
175
    private function getClient(): Client
176
    {
177
        $client = new Client($this->credential->getDsn());
178
        $client->withDriver(new CurlDriver());
179
        return $client;
180
    }
181
182
    private function activateSoftforks()
183
    {
184
        if ($this->softforks) {
185
            return;
186
        }
187
188
        $chainInfo = $this->makeRpcRequest('getblockchaininfo');
189
        $bestHeight = $chainInfo['result']['blocks'];
190
191
        while ($bestHeight < 150 || $chainInfo['result']['bip9_softforks']['segwit']['status'] !== 'active') {
192
            // ought to finish in 1!
193
            $this->makeRpcRequest("generate", [435]);
194
            $chainInfo = $this->makeRpcRequest('getblockchaininfo');
195
            $bestHeight = $chainInfo['result']['blocks'];
196
        }
197
198
        $this->softforks = true;
199
    }
200
201
    /**
202
     * @param int $value
203
     * @param ScriptInterface $script
204
     * @return Utxo
205
     */
206
    public function fundOutput(int $value, ScriptInterface $script)
207
    {
208
        $this->activateSoftforks();
209
210
        $builder = new TxBuilder();
211
        $builder->output($value, $script);
212
        $hex = $builder->get()->getHex();
213
214
        $result = $this->makeRpcRequest('fundrawtransaction', [$hex, ['feeRate'=>0.0001]]);
215
        $unsigned = $result['result']['hex'];
216
        $result = $this->makeRpcRequest('signrawtransaction', [$unsigned]);
217
        $signedHex = $result['result']['hex'];
218
        $signed = TransactionFactory::fromHex($signedHex);
219
220
        $outIdx = -1;
221
        foreach ($signed->getOutputs() as $i => $output) {
222
            if ($output->getScript()->equals($script)) {
223
                $outIdx = $i;
224
            }
225
        }
226
227
        if ($outIdx === -1) {
228
            throw new \RuntimeException("Sanity check failed, should have found the output we funded");
229
        }
230
231
        $result = $this->makeRpcRequest('sendrawtransaction', [$signedHex]);
232
        $txid = $result['result'];
233
        $this->makeRpcRequest("generate", [1]);
234
235
        return new Utxo(new OutPoint(Buffer::hex($txid), $outIdx), new TransactionOutput($value, $script));
236
    }
237
238
    /**
239
     * @param string $src
240
     */
241
    private function recursiveDelete(string $src)
242
    {
243
        $dir = opendir($src);
244
        while (false !== ( $file = readdir($dir))) {
245
            if (( $file != '.' ) && ( $file != '..' )) {
246
                $full = $src . '/' . $file;
247
                if (is_dir($full)) {
248
                    $this->recursiveDelete($full);
249
                } else {
250
                    unlink($full);
251
                }
252
            }
253
        }
254
        closedir($dir);
255
        rmdir($src);
256
    }
257
258
    /**
259
     * @return void
260
     */
261
    public function destroy()
262
    {
263
        if ($this->isRunning()) {
264
            $this->request("stop");
265
266
            do {
267
                usleep($this->secondsToMicro(0.02));
268
            } while ($this->isRunning());
269
270
            $this->recursiveDelete($this->dataDir);
271
        }
272
    }
273
274
    /**
275
     * @return bool
276
     */
277
    public function isRunning(): bool
278
    {
279
        return file_exists($this->getPidFile());
280
    }
281
282
    /**
283
     * @return Client
284
     */
285
    public function makeClient(): Client
286
    {
287
        if (!$this->isRunning()) {
288
            throw new \RuntimeException("No client, server not running");
289
        }
290
291
        if (null === $this->client) {
292
            $this->client = $this->getClient();
293
        }
294
295
        return $this->client;
296
    }
297
298
    /**
299
     * @param string $method
300
     * @param array $params
301
     * @return array
302
     */
303
    public function request(string $method, array $params = []): array
304
    {
305
        $unsorted = $this->makeClient()->sendCommand(new Command($method, $params));
306
        $jsonResult = $unsorted->getBody()->getContents();
307
        $json = json_decode($jsonResult, true);
308
        if (false === $json) {
309
            throw new \RuntimeException("Invalid JSON from server");
310
        }
311
        return $json;
312
    }
313
314
    /**
315
     * @param string $method
316
     * @param array $params
317
     * @return array
318
     */
319
    public function makeRpcRequest(string $method, array $params = []): array
320
    {
321
        return $this->request($method, $params);
322
    }
323
}
324