Completed
Pull Request — master (#567)
by thomas
71:15
created

RpcServer::isRunning()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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