All files / src base58.ts

100% Statements 102/102
96.55% Branches 28/29
100% Functions 12/12
100% Lines 95/95

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177        1x 1x 1x 1x     1x 1x     13x 13x   13x       1x 1x 1x 58x       1x 181x 122x   59x 59x 3736x 3736x 1x           3735x 200375x 200375x 200375x   3735x 1x     57x 57x       1x 18x 18x 18x 18x 18x 3x             15x       1x 157x 157x 157x 157x 157x 7x             150x       1x 71x 71x 71x 3931x 3931x 218269x 218269x 218269x   3931x 5346x 5346x     71x 72x 70x   2x     71x 71x     1x 3x 3x 3x     1x 66x 66x 66x         122x 122x 6538x 6538x 1x           6537x 175714x 175714x 175714x   6537x 4816x     121x 124x 4x   120x     121x 121x         223x 223x 121x   223x         21x 21x 21x      
import {ripemd160, sha256} from 'hash.js'
import {arrayEquals} from './utils'
import {Bytes, BytesType} from './chain'
 
export namespace Base58 {
    export enum ErrorCode {
        E_CHECKSUM = 'E_CHECKSUM',
        E_INVALID = 'E_INVALID',
    }
 
    export class DecodingError extends Error {
        static __className = 'DecodingError'
        constructor(
            message: string,
            public readonly code: ErrorCode,
            public readonly info: Record<string, any> = {}
        ) {
            super(message)
        }
    }
 
    const chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
    const charMap = new Int16Array(0xff).fill(-1)
    for (let i = 0; i < 58; ++i) {
        charMap[chars.charCodeAt(i)] = i
    }
 
    /** Decode a Base58 encoded string. */
    export function decode(s: string, size?: number): Bytes {
        if (size == null) {
            return decodeVar(s)
        }
        const result = new Uint8Array(size)
        for (let i = 0; i < s.length; ++i) {
            let carry = charMap[s.charCodeAt(i)]
            if (carry < 0) {
                throw new DecodingError(
                    'Invalid Base58 character encountered',
                    ErrorCode.E_INVALID,
                    {char: s[i]}
                )
            }
            for (let j = 0; j < size; ++j) {
                const x = result[j] * 58 + carry
                result[j] = x
                carry = x >> 8
            }
            if (carry) {
                throw new DecodingError('Base58 value is out of range', ErrorCode.E_INVALID)
            }
        }
        result.reverse()
        return new Bytes(result)
    }
 
    /** Decode a Base58Check encoded string. */
    export function decodeCheck(encoded: string, size?: number) {
        const decoded = decode(encoded, size != null ? size + 4 : size)
        const data = decoded.array.subarray(0, -4)
        const expected = decoded.array.subarray(-4)
        const actual = dsha256Checksum(data)
        if (!arrayEquals(expected, actual)) {
            throw new DecodingError('Checksum mismatch', ErrorCode.E_CHECKSUM, {
                actual,
                expected,
                data,
                hash: 'double_sha256',
            })
        }
        return new Bytes(data)
    }
 
    /** Decode a Base58Check encoded string that uses ripemd160 instead of double sha256 for the digest. */
    export function decodeRipemd160Check(encoded: string, size?: number, suffix?: string) {
        const decoded = decode(encoded, size != null ? size + 4 : size)
        const data = decoded.array.subarray(0, -4)
        const expected = decoded.array.subarray(-4)
        const actual = ripemd160Checksum(data, suffix)
        if (!arrayEquals(expected, actual)) {
            throw new DecodingError('Checksum mismatch', ErrorCode.E_CHECKSUM, {
                actual,
                expected,
                data,
                hash: 'ripemd160',
            })
        }
        return new Bytes(data)
    }
 
    /** Encode bytes to a Base58 string.  */
    export function encode(data: BytesType) {
        data = Bytes.from(data)
        const result = [] as number[]
        for (const byte of data.array) {
            let carry = byte
            for (let j = 0; j < result.length; ++j) {
                const x = (charMap[result[j]] << 8) + carry
                result[j] = chars.charCodeAt(x % 58)
                carry = (x / 58) | 0
            }
            while (carry) {
                result.push(chars.charCodeAt(carry % 58))
                carry = (carry / 58) | 0
            }
        }
        for (const byte of data.array) {
            if (byte) {
                break
            } else {
                result.push('1'.charCodeAt(0))
            }
        }
        result.reverse()
        return String.fromCharCode(...result)
    }
 
    export function encodeCheck(data: BytesType) {
        data = Bytes.from(data)
        data = data.appending(dsha256Checksum(data.array))
        return encode(data)
    }
 
    export function encodeRipemd160Check(data: BytesType, suffix?: string) {
        data = Bytes.from(data)
        data = data.appending(ripemd160Checksum(data.array, suffix))
        return encode(data)
    }
 
    /** @internal */
    function decodeVar(s: string) {
        const result: number[] = []
        for (let i = 0; i < s.length; ++i) {
            let carry = charMap[s.charCodeAt(i)]
            if (carry < 0) {
                throw new DecodingError(
                    'Invalid Base58 character encountered',
                    ErrorCode.E_INVALID,
                    {char: s[i]}
                )
            }
            for (let j = 0; j < result.length; ++j) {
                const x = result[j] * 58 + carry
                result[j] = x & 0xff
                carry = x >> 8
            }
            if (carry) {
                result.push(carry)
            }
        }
        for (const ch of s) {
            if (ch === '1') {
                result.push(0)
            } else {
                break
            }
        }
        result.reverse()
        return Bytes.from(result)
    }
 
    /** @internal */
    function ripemd160Checksum(data: Uint8Array, suffix?: string) {
        const hash = ripemd160().update(data)
        if (suffix) {
            hash.update(suffix)
        }
        return new Uint8Array(hash.digest().slice(0, 4))
    }
 
    /** @internal */
    function dsha256Checksum(data: Uint8Array) {
        const round1 = sha256().update(data).digest()
        const round2 = sha256().update(round1).digest()
        return new Uint8Array(round2.slice(0, 4))
    }
}