Completed
Pull Request — master (#744)
by thomas
23:27
created

HierarchicalKeySequence::decodePath()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
nc 4
nop 1
dl 0
loc 15
ccs 8
cts 8
cp 1
crap 5
rs 9.4555
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace BitWasp\Bitcoin\Key\Deterministic;
6
7
class HierarchicalKeySequence
8
{
9
    private static $filterBip32Index = [
10
        'min_range' => 0,
11
        'max_range' => (1 << 31) - 1,
12
    ];
13
14
    /**
15
     * decodeAbsolute accepts an absolute BIP32 path, one beginning
16
     * with m or M. It returns an array, containing two elements.
17
     * The first is whether the prefix was public or private.
18
     * The second is the array of indices contained in the path.
19
     *
20
     * @param string $path
21
     * @return array
22
     */
23
    public function decodeAbsolute(string $path)
24
    {
25
        $parts = explode("/", $path);
26
        if (count($parts) < 1) {
27
            throw new \InvalidArgumentException("Invalid BIP32 path - must have at least one component");
28
        }
29
30
        if (!($parts[0] === "m" || $parts[0] === "M")) {
31
            throw new \InvalidArgumentException("Invalid start of absolute BIP32 path - should be m or M");
32
        }
33
34
        return [
35
            $parts[0] === "m",
36
            $this->decodeDerivation(...array_slice($parts, 1)),
37
        ];
38
    }
39
40
    /**
41
     * decodeRelative accepts a relative BIP32 path, that is,
42
     * one without the prefix of m or M. These are usually provided
43
     * when requesting some child derivation of a key.
44
     *
45
     * @param string $path
46
     * @return array
47
     */
48 38
    public function decodeRelative(string $path): array
49
    {
50 38
        $parts = explode("/", $path);
51 38
        if (count($parts) < 1) {
52
            throw new \InvalidArgumentException("Invalid BIP32 path - must have at least one component");
53
        }
54
55 38
        if ($parts[0] === "m" || $parts[0] === "M") {
56
            throw new \InvalidArgumentException("Only relative paths accepted");
57
        }
58
59 38
        return $this->decodeDerivation(...$parts);
60
    }
61
62
    /**
63
     * Inner routine for decoding the numeric section of a path
64
     * @param string ...$parts
65
     * @return int[]
66
     */
67 38
    private function decodeDerivation(string... $parts): array
68
    {
69 38
        $indices = [];
70 38
        foreach ($parts as $i => $part) {
71 38
            if ($part === "") {
72 1
                throw new \InvalidArgumentException("Invalid BIP32 path - Empty path section");
73
            }
74
75 37
            $last = substr($part, -1);
76 37
            $hardened = $last == "h" || $last == "'";
77 37
            if ($hardened) {
78 35
                if (strlen($part) === 1) {
79
                    throw new \InvalidArgumentException("Invalid BIP32 path - section contains only hardened flag");
80
                }
81 35
                $part = substr($part, 0, -1);
82
            }
83
84 37
            if (false !== strpos($part, "h") || false !== strpos($part, "'")) {
85
                throw new \InvalidArgumentException("Invalid BIP32 path - section contains extra hardened characters");
86
            }
87
88 37
            if (false === filter_var($part, FILTER_VALIDATE_INT, self::$filterBip32Index)) {
89
                throw new \InvalidArgumentException("Index is invalid or outside valid range: $part");
90
            }
91
92 37
            $index = (int) $part;
93 37
            if ($hardened) {
94 35
                $index |= 1 << 31;
95
            }
96 37
            $indices[] = $index;
97
        }
98 37
        return $indices;
99
    }
100
}
101