All files / src base58.ts

100% Statements 91/91
95% Branches 19/20
100% Functions 12/12
100% Lines 85/85

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          1x 1x 1x 1x       1x     13x 13x   13x       1x 1x 1x 58x         313x 49x   264x 264x 14082x 14082x 1x           14081x 591976x 591976x 591976x   14081x 1x     262x 262x         30x 30x 30x 30x 30x 3x             27x         277x 277x 277x 277x 277x 7x             270x         131x 131x 131x 6787x 6787x 325476x 325476x 325476x   6787x 9044x 9044x     131x 262x 130x   132x     131x 131x       3x 3x 3x       126x 126x 126x         49x 49x 3324x 3324x 1x           3323x 144393x 144393x 144393x   3323x 2425x     48x 51x 4x   47x     48x 48x         403x 403x         33x   33x      
import {ripemd160} from '@noble/hashes/legacy.js'
import {sha256} from '@noble/hashes/sha2.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 input = suffix ? new Uint8Array([...data, ...new TextEncoder().encode(suffix)]) : data
        return ripemd160(input).slice(0, 4)
    }
 
    /** @internal */
    function dsha256Checksum(data: Uint8Array) {
        const round1 = sha256(data)
        const round2 = sha256(round1)
        return round2.slice(0, 4)
    }
}