├── content_hash ├── py.typed ├── decodes │ ├── utf8.py │ ├── hex_multi_hash.py │ ├── b58_multi_hash.py │ └── __init__.py ├── encodes │ ├── utf8.py │ ├── ipfs.py │ ├── swarm.py │ └── __init__.py ├── utils.py ├── __init__.py ├── profiles │ └── __init__.py └── __main__.py ├── MANIFEST.in ├── example.py ├── LICENSE ├── pyproject.toml └── README.md /content_hash/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # Package Details 2 | include README.md 3 | include LICENSE 4 | 5 | # Package Example 6 | include example.py 7 | 8 | # Package Code 9 | graft content_hash 10 | 11 | # Ignore Cache 12 | global-exclude *.py[co] 13 | global-exclude __pycache__ 14 | -------------------------------------------------------------------------------- /content_hash/decodes/utf8.py: -------------------------------------------------------------------------------- 1 | """Decode module for UTF8.""" 2 | 3 | 4 | def decode(value: bytes) -> str: 5 | """ 6 | Decode UTF8. 7 | 8 | :param value: An encoded content 9 | :return: The decoded content 10 | """ 11 | 12 | return value.decode("utf-8") 13 | -------------------------------------------------------------------------------- /content_hash/encodes/utf8.py: -------------------------------------------------------------------------------- 1 | """Encode module for UTF8.""" 2 | 3 | 4 | def encode(value: str) -> bytes: 5 | """ 6 | Encode UTF8. 7 | 8 | :param value: A decoded content 9 | :return: The encoded content 10 | """ 11 | 12 | return value.encode("utf-8") 13 | -------------------------------------------------------------------------------- /content_hash/utils.py: -------------------------------------------------------------------------------- 1 | """Util functions used in the project.""" 2 | 3 | from multiformats import CID, multicodec 4 | 5 | 6 | def raw_cid_value(cid: CID) -> bytes: 7 | """Return raw representation of the CID as seen in multiformats_cid in bytes.""" 8 | 9 | return b"".join( 10 | [ 11 | cid.version.to_bytes(1, byteorder="big"), 12 | multicodec.wrap(cid.codec, cid.digest), 13 | ], 14 | ) 15 | -------------------------------------------------------------------------------- /content_hash/decodes/hex_multi_hash.py: -------------------------------------------------------------------------------- 1 | """Decode module for HEX multi hash.""" 2 | 3 | import typing 4 | 5 | from multiformats import CID, multihash 6 | from multiformats.varint import BytesLike 7 | 8 | 9 | def decode(value: typing.Union[str, BytesLike]) -> str: 10 | """ 11 | Decode HEX multi hash. 12 | 13 | :param bytes value: An encoded content 14 | :return: The decoded content 15 | """ 16 | 17 | return multihash.unwrap(CID.decode(value).digest).hex() 18 | -------------------------------------------------------------------------------- /content_hash/encodes/ipfs.py: -------------------------------------------------------------------------------- 1 | """Encode module for IPFS.""" 2 | 3 | import base58check 4 | from multiformats import CID 5 | 6 | from ..utils import raw_cid_value 7 | 8 | 9 | def encode(value: str) -> bytes: 10 | """ 11 | Encode IPFS. 12 | 13 | :param value: A decoded content 14 | :return: The encoded content 15 | """ 16 | 17 | mhash = base58check.b58decode(value) 18 | cid = CID(base="base58btc", codec="dag-pb", version=1, digest=mhash) 19 | return raw_cid_value(cid) 20 | -------------------------------------------------------------------------------- /content_hash/decodes/b58_multi_hash.py: -------------------------------------------------------------------------------- 1 | """Decode module for B58 multi hash.""" 2 | 3 | import typing 4 | 5 | from base58check import b58encode 6 | from multiformats import CID 7 | from multiformats.varint import BytesLike 8 | 9 | 10 | def decode(value: typing.Union[str, BytesLike]) -> str: 11 | """ 12 | Decode B58 multi hash. 13 | 14 | :param value: An encoded content 15 | :return: The decoded content 16 | """ 17 | 18 | return b58encode(CID.decode(value).digest).decode("utf-8") 19 | -------------------------------------------------------------------------------- /content_hash/encodes/swarm.py: -------------------------------------------------------------------------------- 1 | """Encode module for Swarm.""" 2 | 3 | from multiformats import CID, multihash 4 | 5 | from ..utils import raw_cid_value 6 | 7 | 8 | def encode(value: str) -> bytes: 9 | """ 10 | Encode Swarm. 11 | 12 | :param value: A decoded content 13 | :return: The encoded content 14 | """ 15 | 16 | mhash = multihash.wrap(bytes.fromhex(value), "keccak-256") 17 | cid = CID(base="base58btc", codec="swarm-manifest", version=1, digest=mhash) 18 | return raw_cid_value(cid) 19 | -------------------------------------------------------------------------------- /content_hash/decodes/__init__.py: -------------------------------------------------------------------------------- 1 | """Known decoding functions.""" 2 | 3 | import importlib 4 | import typing 5 | 6 | CACHE: typing.Dict[str, typing.Callable[[bytes], str]] = {} 7 | """A cache of known decoding functions.""" 8 | 9 | 10 | def get_decode(name: str) -> typing.Callable[[bytes], str]: 11 | """ 12 | Get decoding function by name. 13 | 14 | Decoding should be a function that takes 15 | a `bytes` input and returns a `str` result. 16 | 17 | :param name: A decode name 18 | :return: The resulting decode 19 | """ 20 | 21 | decode = CACHE.get(name) 22 | 23 | if not decode: 24 | decode = CACHE[name] = importlib.import_module(f".{name}", __name__).decode 25 | 26 | return decode 27 | -------------------------------------------------------------------------------- /content_hash/encodes/__init__.py: -------------------------------------------------------------------------------- 1 | """Known encoding functions.""" 2 | 3 | import importlib 4 | import typing 5 | 6 | CACHE: typing.Dict[str, typing.Callable[[str], bytes]] = {} 7 | """A cache of known encoding functions.""" 8 | 9 | 10 | def get_encode(name: str) -> typing.Callable[[str], bytes]: 11 | """ 12 | Get encoding function by name. 13 | 14 | Encoding should be a function that takes 15 | a `str` input and returns a `bytes` result. 16 | 17 | :param name: An encode name 18 | :return: The resulting encode 19 | """ 20 | 21 | encode = CACHE.get(name) 22 | 23 | if not encode: 24 | encode = CACHE[name] = importlib.import_module(f".{name}", __name__).encode 25 | 26 | return encode 27 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | """Example for EIP 1577 content hash.""" 2 | 3 | import content_hash 4 | 5 | 6 | def main(): 7 | """Handle example.""" 8 | 9 | # Get codec of content hash 10 | chash = "e6013148654c4c6f34757a6a614c65744678364e4833504d774650337162526254663344" 11 | codec = content_hash.get_codec(chash) 12 | print("Codec of content hash:", codec) 13 | 14 | # Decode content hash 15 | chash = "e6013148654c4c6f34757a6a614c65744678364e4833504d774650337162526254663344" 16 | address = content_hash.decode(chash) 17 | print("Decoded content hash:", address) 18 | 19 | # Encode content hash 20 | address = "1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D" 21 | chash = content_hash.encode("zeronet", address) 22 | print("Encoded content hash:", chash) 23 | 24 | 25 | if __name__ == "__main__": 26 | main() 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Filip Štamcar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /content_hash/__init__.py: -------------------------------------------------------------------------------- 1 | """Python implementation of EIP 1577 content hash.""" 2 | 3 | from multiformats import multicodec 4 | 5 | from .profiles import get_profile 6 | 7 | 8 | def decode(chash: str) -> str: 9 | """ 10 | Decode a content hash. 11 | 12 | :param chash: A hex string containing a content hash 13 | :return: The decoded content 14 | """ 15 | 16 | codec, raw_data = multicodec.unwrap(bytes.fromhex(chash.lstrip("0x"))) 17 | profile = get_profile(codec.name) 18 | return profile.decode(raw_data) 19 | 20 | 21 | def encode(codec: str, value: str) -> str: 22 | """ 23 | Encode a content hash. 24 | 25 | :param codec: A codec of a content hash 26 | :param value: A value of a content hash 27 | :return: The resulting content hash 28 | """ 29 | 30 | value = get_profile(codec).encode(value) 31 | return multicodec.wrap(codec, value).hex() 32 | 33 | 34 | def get_codec(chash: str) -> str: 35 | """ 36 | Extract the codec of a content hash 37 | 38 | :param chash: A hex string containing a content hash 39 | :return: The extracted codec 40 | """ 41 | 42 | codec, _ = multicodec.unwrap(bytes.fromhex(chash.lstrip("0x"))) 43 | return codec.name 44 | -------------------------------------------------------------------------------- /content_hash/profiles/__init__.py: -------------------------------------------------------------------------------- 1 | """Known decoding and encoding profiles.""" 2 | 3 | import typing 4 | 5 | from ..decodes import get_decode 6 | from ..encodes import get_encode 7 | 8 | 9 | class Profile: 10 | """Decoding and encoding profile""" 11 | 12 | __slots__ = ["__encode", "__decode"] 13 | 14 | def __init__( 15 | self, 16 | decode: typing.Union[str, typing.Callable[[bytes], str]], 17 | encode: typing.Union[str, typing.Callable[[str], bytes]], 18 | ): 19 | """ 20 | Initialize the class. 21 | 22 | :param decode: A profile decode 23 | :param encode: A profile encode 24 | """ 25 | 26 | if isinstance(decode, str): 27 | self.__decode = get_decode(decode) 28 | elif callable(decode): 29 | self.__decode = decode 30 | else: 31 | raise TypeError("Argument `decode` must be a string or a callable") 32 | 33 | if isinstance(encode, str): 34 | self.__encode = get_encode(encode) 35 | elif callable(decode): 36 | self.__encode = encode 37 | else: 38 | raise TypeError("Argument `encode` must be a string or a callable") 39 | 40 | @property 41 | def decode(self) -> typing.Callable[[bytes], str]: 42 | """ 43 | Get profile decode. 44 | 45 | :return: The profile decode 46 | """ 47 | 48 | return self.__decode 49 | 50 | @property 51 | def encode(self) -> typing.Callable[[str], bytes]: 52 | """ 53 | Get profile encode. 54 | 55 | :return: The profile encode 56 | """ 57 | 58 | return self.__encode 59 | 60 | 61 | def get_profile(name: str) -> Profile: 62 | """ 63 | Get profile by name. 64 | 65 | :param name: A profile name 66 | :return: The profile 67 | """ 68 | 69 | return PROFILES.get(name, PROFILES["default"]) 70 | 71 | 72 | PROFILES = { 73 | "ipfs": Profile(decode="b58_multi_hash", encode="ipfs"), 74 | "ipns": Profile(decode="b58_multi_hash", encode="ipfs"), 75 | "swarm": Profile(decode="hex_multi_hash", encode="swarm"), 76 | "default": Profile(decode="utf8", encode="utf8"), 77 | } 78 | """ 79 | A dict of known profiles. 80 | 81 | `encode` should be chosen among the `encode` modules 82 | `decode` should be chosen among the `decode` modules 83 | """ 84 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=64", "setuptools_scm>=8"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "content-hash" 7 | description = "Python implementation of EIP 1577 content hash" 8 | authors = [{ email = "projects@filips.si", name = "Filip Š" }] 9 | readme = "README.md" 10 | license = { file = "LICENSE" } 11 | dynamic = ["version"] 12 | 13 | requires-python = ">=3.8" 14 | 15 | classifiers = [ 16 | "Development Status :: 5 - Production/Stable", 17 | "Intended Audience :: Developers", 18 | "License :: OSI Approved :: MIT License", 19 | "Programming Language :: Python :: 3", 20 | "Topic :: Internet :: Name Service (DNS)", 21 | "Topic :: Scientific/Engineering :: Interface Engine/Protocol Translator", 22 | "Topic :: Security :: Cryptography", 23 | "Topic :: Software Development :: Libraries", 24 | "Topic :: Utilities", 25 | "Typing :: Typed", 26 | ] 27 | 28 | keywords = [ 29 | "ethereum", 30 | "ethereum-name-service", 31 | "ens", 32 | "eip1577", 33 | "erc1577", 34 | "web3", 35 | "decentralized", 36 | ] 37 | 38 | dependencies = [ 39 | "multiformats>=0.3.1.post4", 40 | "base58check==1.0.2", # for b58 encode/decode 41 | "pycryptodomex", # required by multiformats for keccak 42 | ] 43 | 44 | [project.optional-dependencies] 45 | lint = ["pylint", "black", "mypy"] 46 | test = ["pytest", "pytest-cov"] 47 | 48 | [project.urls] 49 | Homepage = "https://github.com/filips123/ContentHashPy/" 50 | Repository = "https://github.com/filips123/ContentHashPy.git" 51 | Issues = "https://github.com/filips123/ContentHashPy/issues" 52 | Changelog = "https://github.com/filips123/ContentHashPy/releases" 53 | 54 | [tool.setuptools_scm] 55 | fallback_version = "0.0.0" 56 | 57 | [tool.pylint] 58 | max-line-length = 110 59 | 60 | [tool.black] 61 | line-length = 110 62 | disable = ["R0801"] 63 | 64 | [tool.mypy] 65 | python_version = "3.8" 66 | show_column_numbers = true 67 | show_error_codes = true 68 | allow_redefinition = true 69 | disallow_untyped_decorators = true 70 | disallow_untyped_defs = true 71 | disallow_incomplete_defs = true 72 | no_implicit_optional = true 73 | warn_redundant_casts = true 74 | warn_unused_configs = true 75 | warn_unused_ignores = true 76 | warn_no_return = true 77 | warn_return_any = true 78 | warn_unreachable = true 79 | exclude = "__main__.py" 80 | -------------------------------------------------------------------------------- /content_hash/__main__.py: -------------------------------------------------------------------------------- 1 | """Command line program for EIP 1577 content hash.""" 2 | 3 | # pylint: disable=W0703 4 | 5 | import argparse 6 | import sys 7 | 8 | from . import decode, encode, get_codec 9 | 10 | 11 | def handle_get_codec(args): 12 | """Handle get codec.""" 13 | 14 | try: 15 | chash = args.chash 16 | codec = get_codec(chash) 17 | 18 | except BaseException as err: # pragma: no cover 19 | print(str(err), file=sys.stderr) 20 | sys.exit(1) 21 | 22 | print(codec) 23 | 24 | 25 | def handle_decode(args): 26 | """Handle decode.""" 27 | 28 | try: 29 | chash = args.chash 30 | value = decode(chash) 31 | 32 | except BaseException as err: # pragma: no cover 33 | print(str(err), file=sys.stderr) 34 | sys.exit(1) 35 | 36 | print(value) 37 | 38 | 39 | def handle_encode(args): 40 | """Handle encode.""" 41 | 42 | try: 43 | codec = args.codec 44 | value = args.value 45 | chash = encode(codec, value) 46 | 47 | except BaseException as err: # pragma: no cover 48 | print(str(err), file=sys.stderr) 49 | sys.exit(1) 50 | 51 | print(chash) 52 | 53 | 54 | def main(): 55 | """Handle command line program.""" 56 | 57 | parser = argparse.ArgumentParser( 58 | prog=__package__, 59 | description="Python implementation of EIP 1577 content hash", 60 | ) 61 | subparsers = parser.add_subparsers() 62 | 63 | get_codec_parser = subparsers.add_parser(name="get-codec", help="extract the codec of a content hash") 64 | get_codec_parser.set_defaults(which="get-codec") 65 | get_codec_parser.add_argument("chash", help="an hex string containing a content hash") 66 | 67 | get_codec_parser = subparsers.add_parser(name="decode", help="decode a content hash") 68 | get_codec_parser.set_defaults(which="decode") 69 | get_codec_parser.add_argument("chash", help="an hex string containing a content hash") 70 | 71 | get_codec_parser = subparsers.add_parser(name="encode", help="encode a content hash") 72 | get_codec_parser.set_defaults(which="encode") 73 | get_codec_parser.add_argument("codec", help="a codec of a content hash") 74 | get_codec_parser.add_argument("value", help="a value of a content hash") 75 | 76 | if len(sys.argv) == 1: # pragma: no cover 77 | parser.print_help(sys.stderr) 78 | sys.exit(1) 79 | 80 | args = parser.parse_args() 81 | 82 | if args.which == "get-codec": 83 | handle_get_codec(args) 84 | elif args.which == "decode": 85 | handle_decode(args) 86 | elif args.which == "encode": 87 | handle_encode(args) 88 | 89 | 90 | if __name__ == "__main__": # pragma: no cover 91 | main() 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ContentHash for Python 2 | ====================== 3 | 4 | [![version][icon-version]][link-pypi] 5 | [![downloads][icon-downloads]][link-pypi] 6 | [![license][icon-license]][link-license] 7 | [![python][icon-python]][link-python] 8 | 9 | [![build][icon-build]][link-build] 10 | [![coverage][icon-coverage]][link-coverage] 11 | [![quality][icon-quality]][link-quality] 12 | 13 | Python implementation of EIP 1577 content hash. 14 | 15 | ## Description 16 | 17 | This is a simple package made for encoding and decoding content hashes has specified in the [EIP 1577][link-eip-1577]. 18 | This package will be useful for every [Ethereum][link-ethereum] developer wanting to interact with [EIP 1577][link-eip-1577] compliant [ENS resolvers][link-resolvers]. 19 | 20 | For JavaScript implementation, see [`pldespaigne/content-hash`][link-javascript-implementation]. 21 | 22 | ## Installation 23 | 24 | ### Requirements 25 | 26 | ContentHash requires Python 3.8 or higher. 27 | 28 | ### From PyPI 29 | 30 | The recommended way to install ContentHash is from PyPI with PIP. 31 | 32 | ```bash 33 | pip install content-hash 34 | ``` 35 | 36 | ### From Source 37 | 38 | Alternatively, you can also install it from the source. 39 | 40 | ```bash 41 | git clone https://github.com/filips123/ContentHashPy.git 42 | cd ContentHashPy 43 | pip install . 44 | ``` 45 | 46 | ## Usage 47 | 48 | ### Supported Codecs 49 | 50 | The following codecs are currently supported: 51 | 52 | - `swarm` 53 | - `ipfs` 54 | - `ipns` 55 | 56 | Every other codec supported by [`multicodec`][link-multicodec] will be encoded by default in `utf-8`. You can see the full list of the supported codecs [here][link-supported-codecs]. 57 | 58 | ### Getting Codec 59 | 60 | You can use a `get_codec` function to get codec from the content hash. 61 | 62 | It takes a content hash as a HEX string and returns the codec name. A content hash can be prefixed with a `0x`, but it's not mandatory. 63 | 64 | ```py 65 | import content_hash 66 | 67 | chash = 'bc037a716b746c776934666563766f367269' 68 | codec = content_hash.get_codec(chash) 69 | 70 | print(codec) # onion 71 | ``` 72 | 73 | ### Decoding 74 | 75 | You can use a `decode` function to decode a content hash. 76 | 77 | It takes a content hash as a HEX string and returns the decoded content as a string. A content hash can be prefixed with a `0x`, but it's not mandatory. 78 | 79 | ```py 80 | import content_hash 81 | 82 | chash = 'e3010170122029f2d17be6139079dc48696d1f582a8530eb9805b561eda517e22a892c7e3f1f' 83 | value = content_hash.decode(chash) 84 | 85 | print(value) # QmRAQB6YaCyidP37UdDnjFY5vQuiBrcqdyoW1CuDgwxkD4 86 | ``` 87 | 88 | ### Encoding 89 | 90 | You can use an `encode` function to encode a content hash. 91 | 92 | It takes a supported codec as a string and a value as a string and returns the corresponding content hash as a HEX string. The output will not be prefixed with a `0x`. 93 | 94 | ```py 95 | import content_hash 96 | 97 | codec = 'swarm-ns' 98 | value = 'd1de9994b4d039f6548d191eb26786769f580809256b4685ef316805265ea162' 99 | chash = content_hash.encode(codec, value) 100 | 101 | print(chash) # e40101701b20d1de9994b4d039f6548d191eb26786769f580809256b4685ef316805265ea162 102 | ``` 103 | 104 | ## Creating Codecs 105 | 106 | All supported codec profiles are available in [`content_hash/profiles/__init__.py`][link-profiles-file], in `PROFILES` dictionary. You need to add a new profile there. You only need to add a new profile if your codec encoding and decoding are different from `utf-8`. 107 | 108 | Each profile must have the same name as the corresponding codec in the `multicodec` library. 109 | 110 | A profile must also have decode and encode function. They should be passed as a string containing the name of the module for required decode or encode. All such modules are available in [`content_hash/decodes`][link-decodes-directory] and [`content_hash/encodes`][link-encodes-directory]. 111 | 112 | Each module name should describe it as much as possible. Its name can only contain valid characters for Python modules. 113 | 114 | Each decode module must have a `decode` function. It must be a function that takes a `bytes` input and returns a `str` result. 115 | 116 | Each encode module must have an `encode` function. It must be a function that takes a `str` input and returns a `bytes` result. 117 | 118 | All inputs and outputs must be the same as in the [JavaScript implementation][link-javascript-implementation]. Multiple profiles can share the same decodes and encodes. 119 | 120 | ## Versioning 121 | 122 | This library uses [SemVer][link-semver] for versioning. For the versions available, see [the tags][link-tags] on this repository. 123 | 124 | ## License 125 | 126 | This library is licensed under the MIT license. See the [LICENSE][link-license-file] file for details. 127 | 128 | [icon-version]: https://img.shields.io/pypi/v/content-hash.svg?style=flat-square&label=version 129 | [icon-downloads]: https://img.shields.io/pypi/dm/content-hash.svg?style=flat-square&label=downloads 130 | [icon-license]: https://img.shields.io/pypi/l/content-hash.svg?style=flat-square&label=license 131 | [icon-python]: https://img.shields.io/pypi/pyversions/content-hash?style=flat-square&label=python 132 | 133 | [icon-build]: https://img.shields.io/github/actions/workflow/status/filips123/ContentHashPy/main.yml?style=flat-square&label=build 134 | [icon-coverage]: https://img.shields.io/scrutinizer/coverage/g/filips123/ContentHashPy.svg?style=flat-square&label=coverage 135 | [icon-quality]: https://img.shields.io/scrutinizer/g/filips123/ContentHashPy.svg?style=flat-square&label=quality 136 | 137 | [link-pypi]: https://pypi.org/project/content-hash/ 138 | [link-license]: https://choosealicense.com/licenses/mit/ 139 | [link-python]: https://python.org/ 140 | [link-build]: https://github.com/filips123/ContentHashPy/actions 141 | [link-coverage]: https://scrutinizer-ci.com/g/filips123/ContentHashPy/code-structure/ 142 | [link-quality]: https://scrutinizer-ci.com/g/filips123/ContentHashPy/ 143 | [link-semver]: https://semver.org/ 144 | 145 | [link-eip-1577]: https://github.com/ethereum/ercs/blob/master/ERCS/erc-1577.md 146 | [link-ethereum]: https://www.ethereum.org/ 147 | [link-resolvers]: http://docs.ens.domains/en/latest/introduction.html 148 | [link-multicodec]: https://github.com/multiformats/multicodec/ 149 | [link-supported-codecs]: https://github.com/multiformats/multicodec/blob/master/table.csv 150 | 151 | [link-tags]: https://github.com/filips123/ContentHashPy/tags/ 152 | [link-license-file]: https://github.com/filips123/ContentHashPy/blob/master/LICENSE 153 | [link-profiles-file]: https://github.com/filips123/ContentHashPy/blob/master/content_hash/profiles/__init__.py 154 | [link-decodes-directory]: https://github.com/filips123/ContentHashPy/tree/master/content_hash/decodes/ 155 | [link-encodes-directory]: https://github.com/filips123/ContentHashPy/tree/master/content_hash/encodes/ 156 | 157 | [link-javascript-implementation]: https://github.com/pldespaigne/content-hash/ 158 | --------------------------------------------------------------------------------