diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..370314a --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @erubboli @ImplOfAnImpl @OBorce diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..93b1760 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,106 @@ +name: Build and run tests + +on: + push: + branches: + - master + pull_request: + branches: + - "**" # target all branches + schedule: + - cron: '15 0 * * *' # every day at 00:15 UTC + +env: + CARGO_TERM_COLOR: always + RUST_LOG: debug + RUST_BACKTRACE: full + +jobs: + build_ubuntu: + runs-on: ubuntu-latest + steps: + - name: Checkout the repository + uses: actions/checkout@v5 + with: + submodules: recursive + + - name: Update the list of available system packages + run: sudo apt-get update + + - name: Install build dependencies + run: sudo apt-get install -yqq --no-install-recommends build-essential + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version-file: './build-tools/.python-version' + + - name: Install Rust + run: | + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \ + --default-toolchain $(python ./build-tools/cargo-info-extractor/extract.py --rust-version) + + - name: Build + run: cargo build --locked + + - name: Run tests + run: cargo test --all + + - name: Run doc tests + run: cargo test --doc + + build_macos: + runs-on: macos-latest + steps: + - name: Checkout the repository + uses: actions/checkout@v5 + with: + submodules: recursive + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version-file: './build-tools/.python-version' + + - name: Install Rust + run: | + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \ + --default-toolchain $(python ./build-tools/cargo-info-extractor/extract.py --rust-version) + + - name: Build + run: cargo build --locked + + - name: Run tests + run: cargo test --all + + - name: Run doc tests + run: cargo test --doc + + build_windows: + runs-on: windows-latest + steps: + - name: Checkout the repository + uses: actions/checkout@v5 + with: + submodules: recursive + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version-file: './build-tools/.python-version' + + - name: Install Rust + # Use bash to be able to escape the newline via '\'. + shell: bash + run: | + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \ + --default-toolchain $(python ./build-tools/cargo-info-extractor/extract.py --rust-version) + + - name: Build + run: cargo build --locked + + - name: Run tests + run: cargo test --all + + - name: Run doc tests + run: cargo test --doc diff --git a/.github/workflows/code_checks.yml b/.github/workflows/code_checks.yml new file mode 100644 index 0000000..e16703d --- /dev/null +++ b/.github/workflows/code_checks.yml @@ -0,0 +1,112 @@ +name: Static code checks + +on: + push: + branches: + - master + pull_request: + branches: + - "**" # target all branches + +env: + CARGO_TERM_COLOR: always + RUST_LOG: debug + RUST_BACKTRACE: full + +jobs: + static_checks_ubuntu: + runs-on: ubuntu-latest + steps: + - name: Checkout the repository + uses: actions/checkout@v5 + with: + submodules: recursive + + - name: Update the list of available system packages + run: sudo apt-get update + + - name: Install dependencies + run: sudo apt-get install -yqq --no-install-recommends build-essential + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version-file: './build-tools/.python-version' + + - name: Install Rust + run: | + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \ + --default-toolchain $(python ./build-tools/cargo-info-extractor/extract.py --rust-version) + + - name: Install Clippy + run: rustup component add clippy + + - name: Install cargo-deny + run: cargo install cargo-deny --locked + + - name: Run checks + run: ./do_checks.sh + + static_checks_macos: + runs-on: macos-latest + steps: + - name: Checkout the repository + uses: actions/checkout@v5 + with: + submodules: recursive + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version-file: './build-tools/.python-version' + + - name: Install Rust + run: | + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \ + --default-toolchain $(python ./build-tools/cargo-info-extractor/extract.py --rust-version) + + - name: Install Clippy + run: rustup component add clippy + + - name: Install cargo-deny + run: cargo install cargo-deny --locked + + - name: Run checks + shell: bash + run: ./do_checks.sh + + static_checks_windows: + runs-on: windows-latest + steps: + # This prevents git from changing line-endings to crlf, which messes cargo fmt checks + - name: Set git to use LF + run: | + git config --global core.autocrlf false + git config --global core.eol lf + + - name: Checkout the repository + uses: actions/checkout@v5 + with: + submodules: recursive + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version-file: './build-tools/.python-version' + + - name: Install Rust + # Use bash to be able to escape the newline via '\'. + shell: bash + run: | + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \ + --default-toolchain $(python ./build-tools/cargo-info-extractor/extract.py --rust-version) + + - name: Install Clippy + run: rustup component add clippy + + - name: Install cargo-deny + run: cargo install cargo-deny --locked + + - name: Run checks + shell: bash + run: ./do_checks.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b5548cc --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +/target + +.vscode/ + +.DS_Store + +# Python cache +**/__pycache__ + +# Python compiled files +*.pyc diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..be10bba --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,295 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "byte-slice-cast" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" + +[[package]] +name = "const_format" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "indexmap" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mintlayer-core-primitives" +version = "1.0.0" +dependencies = [ + "derive_more", + "fixed-hash", + "hex", + "parity-scale-codec", + "strum", +] + +[[package]] +name = "parity-scale-codec" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" +dependencies = [ + "arrayvec", + "byte-slice-cast", + "const_format", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "rustversion", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a26dbd934e5451d21ef060c018dae56fc073894c5a7896f882928a76e6d081b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow", +] + +[[package]] +name = "unicode-ident" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..05e3da5 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "mintlayer-core-primitives" +description = "Mintlayer Core primitive types" +homepage = "https://mintlayer.org" +repository = "https://github.com/mintlayer/mintlayer-core-primitives" +readme = "README.md" +license = "MIT" +version = "1.0.0" +edition = "2024" +rust-version = "1.88" + +[dependencies] +derive_more = { version = "2.0", default-features = false, features = ["debug"] } +fixed-hash = { version = "0.8", default-features = false } +parity-scale-codec = { version = "3.7", default-features = false, features = ["derive"] } +strum = { version = "0.27", default-features = false, features = ["derive"] } + +[dev-dependencies] +hex = "0.4" + +[features] +dev = [] # used by fixed-hash diff --git a/LICENSE b/LICENSE index 65bdee8..994e3a0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 Mintlayer +Copyright (c) 2021-2025 RBB S.r.l Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md new file mode 100644 index 0000000..2d548f5 --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# Mintlayer Core primitive types + +Here we have certain primitive types copied from Mintlayer Core and intended for use +in hardware wallets' firmware/apps in `no_std` mode. + +Notes: +- The types in this repository are not always identical to those from Mintlayer Core, however +they are encode-compatible with them. +- Ideally, we should get rid of the code duplication and, instead, extract the Mintlayer Core's +"foundational" crates [^1] into a separate repository and make them usable (in a limited way) +in `no_std` mode. + +[^1]: iI.e. `common` and `crypto` as well as the utility crates that they depend on - `utils`, +`serialization`, `logging` etc). diff --git a/build-tools/.python-version b/build-tools/.python-version new file mode 100644 index 0000000..50a8965 --- /dev/null +++ b/build-tools/.python-version @@ -0,0 +1,2 @@ +# Need at least v3.11 to be able to use tomllib +>=3.11 diff --git a/build-tools/cargo-info-extractor/extract.py b/build-tools/cargo-info-extractor/extract.py new file mode 100755 index 0000000..fd631a2 --- /dev/null +++ b/build-tools/cargo-info-extractor/extract.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +''' +A simple program that extracts certain info from Cargo.toml and prints it to stdout. +To be used in CI. +''' + +import argparse +import pathlib +import tomllib + + +ROOT_DIR = pathlib.Path(__file__).resolve().parent.parent.parent +ROOT_CARGO_TOML = ROOT_DIR.joinpath("Cargo.toml") + + +def get_rust_version(cargo_toml_root): + version = cargo_toml_root["package"]["rust-version"] + + if len(version.split('.')) == 2: + version = version + '.0' + + return version + + +def main(): + parser = argparse.ArgumentParser() + mutex_group = parser.add_mutually_exclusive_group(required=True) + mutex_group.add_argument('--rust-version', action='store_true', help='extract Rust version') + args = parser.parse_args() + + with open(ROOT_CARGO_TOML, "rb") as file: + cargo_toml_root = tomllib.load(file) + + if args.rust_version: + result = get_rust_version(cargo_toml_root) + print(result) + + +if __name__ == "__main__": + main() diff --git a/build-tools/codecheck/codecheck.py b/build-tools/codecheck/codecheck.py new file mode 100644 index 0000000..0d5a36a --- /dev/null +++ b/build-tools/codecheck/codecheck.py @@ -0,0 +1,336 @@ +#!/usr/bin/env python3 +# Some simple custom code lints, mostly implemented by means of grepping code + +# Note: this was copied from the Mintlayer Core repository with minor modifications +# (e.g. some redundant checks were removed). + +import fnmatch +import itertools +import os +import re +import sys +import tomllib + + +LICENSE_TEMPLATE = [ + r'// Copyright \(c\) 202[0-9](-202[0-9])? .+', + r'// opensource@mintlayer\.org', + r'// SPDX-License-Identifier: MIT', + r'// Licensed under the MIT License;', + r'// you may not use this file except in compliance with the License\.', + r'// You may obtain a copy of the License at', + r'//', + r'// https://github.com/mintlayer/mintlayer-core-primitives/blob/master/LICENSE', + r'//', + r'// Unless required by applicable law or agreed to in writing, software', + r'// distributed under the License is distributed on an "AS IS" BASIS,', + r'// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied\.', + r'// See the License for the specific language governing permissions and', + r'// limitations under the License\.', + r'|//' +] + +COMMON_EXCLUDE_DIRS = [ + '.git', + '.vscode', + 'target' +] + + +# List Rust source files +def rs_sources(exclude = []): + return sources_with_extensions(['.rs'], exclude) + + +# List Cargo config files +def cargo_config_files(exclude = []): + return sources_with_extensions(['.toml'], exclude) + + +# List Python source files +def py_sources(exclude = []): + return sources_with_extensions(['.py'], exclude) + + +# Cargo.toml files +def cargo_toml_files(exclude = []): + exclude = [ os.path.normpath(dir) for dir in COMMON_EXCLUDE_DIRS + ['.github'] + exclude ] + is_excluded = lambda top, d: os.path.normpath(os.path.join(top, d).lower()) in exclude + + for top, dirs, files in os.walk('.', topdown=True): + dirs[:] = [ d for d in dirs if not is_excluded(top, d) ] + for file in files: + if file == 'Cargo.toml': + yield os.path.join(top, file) + +def _sources_with_extension(ext: str, exclude = []): + exclude = [ os.path.normpath(dir) for dir in COMMON_EXCLUDE_DIRS + ['.github'] + exclude ] + is_excluded = lambda top, d: os.path.normpath(os.path.join(top, d).lower()) in exclude + + for top, dirs, files in os.walk('.', topdown=True): + dirs[:] = [ d for d in dirs if not is_excluded(top, d) ] + for file in files: + if os.path.splitext(file)[1].lower() == ext: + yield os.path.join(top, file) + +# List source files with given extensions +def sources_with_extensions(exts: list[str], exclude = []): + return list(itertools.chain(*[_sources_with_extension(ext, exclude) for ext in exts])) + + +# All files +def all_files(exclude = []): + exclude_full_paths = [ os.path.normpath(dir) for dir in COMMON_EXCLUDE_DIRS + exclude ] + exclude_dir_names = ['__pycache__'] + + def is_excluded(top, d): + return (d in exclude_dir_names or + os.path.normpath(os.path.join(top, d).lower()) in exclude_full_paths) + + for top, dirs, files in os.walk('.', topdown=True): + dirs[:] = [ d for d in dirs if not is_excluded(top, d) ] + for file in files: + yield os.path.join(top, file) + + +# Disallow certain pattern in source files, with exceptions +def disallow(pat, exclude = []): + print("==== Searching for '{}':".format(pat)) + pat = re.compile(pat) + + found_re = False + for path in rs_sources(exclude): + with open(path, 'r', encoding='utf-8') as file: + for (line_num, line) in enumerate(file, start = 1): + line = line.rstrip() + if pat.search(line): + found_re = True + print("{}:{}:{}".format(path, line_num, line)) + + print() + return not found_re + + +# Retrieve an item from arbitrarily nested dicts given a list of keys. +# E.g. get_from_nested_dicts({'a': {'b': 1, 'c': 2}}, ['a', 'b']) will +# return 1. +def get_from_nested_dicts(nested_dicts, keys_list) -> bool: + cur_dict = nested_dicts + while keys_list: + key = keys_list.pop(0) + if key in cur_dict: + cur_dict = cur_dict[key] + else: + return None + + return cur_dict + + +# Since 'dependencies', 'dev-dependencies' and 'workspace.dependencies' +# have the same structure, we check the versions the same way for all +# of them. +# Here 'root_node' is the root node of the Cargo.toml file, +# 'dependencies_name' is the name of the 'dependencies' node (may contain +# dots) and 'file_path' is the path of the Cargo.toml file, for logging. +def internal_check_dependency_versions(root_node, dependencies_name: str, file_path) -> bool: + res = True + + # list of crates, whose version may not have a minor version or may have a patch version + exempted_crates = [ + # left here as an example, remove if you ever add one crate that is exempt + # 'ctor' + ] + + # Names with dots actually represent paths inside the tree of nodes. + dependencies_path = dependencies_name.split('.') + + deps = get_from_nested_dicts(root_node, dependencies_path) + if deps is not None: + for dep in deps: + # skip exempted crates + if dep in exempted_crates: + continue + + # versions that looks like `tokio = { version = "1.2.3" }` + if 'version' in deps[dep]: + version = deps[dep]['version'] + # versions that looks like late `tokio = "1.2.3"` + elif type(deps[dep]) == str: + version = deps[dep] + else: + version = None + + if version is not None: + if len(version.split('.')) < 2: + print((f"In {dependencies_name} of '{file_path}' " + f"{dep} doesn't have a minor version: {version}")) + res = False + elif len(version.split('.')) > 2: + print((f"In {dependencies_name} of '{file_path}' " + f"{dep} has a patch version: {version}")) + res = False + + return res + + +# Ensure that the versions in all Cargo.toml have a minor version but not a patch version. +def check_dependency_versions_patch_version(): + print("==== Ensuring that all versions in Cargo.toml have a minor version but not a patch version") + + # list of files exempt from patch version check + exempted_files = [ + ] + + result = True + + for path in cargo_toml_files(): + if any(fnmatch.fnmatch(os.path.abspath(path), os.path.abspath(exempted)) + for exempted in exempted_files): + continue + + # load the file + with open(path, "rb") as file: + root = tomllib.load(file) + + # check dependencies + intermediary_result = internal_check_dependency_versions(root, 'dependencies', path) + result = result and intermediary_result + + # check dev-dependencies + intermediary_result = internal_check_dependency_versions(root, 'dev-dependencies', path) + result = result and intermediary_result + + # check workspace.dependencies + intermediary_result = internal_check_dependency_versions(root, 'workspace.dependencies', path) + result = result and intermediary_result + + print() + + return result + + +# Check license header in current project crates +def check_local_licenses(): + print("==== Checking local license headers:") + + # list of files exempted from license check + exempted_files = [ + ] + + template = re.compile('(?:' + r')\n(?:'.join(LICENSE_TEMPLATE) + ')') + + ok = True + for path in rs_sources(): + if any(fnmatch.fnmatch(os.path.abspath(path), os.path.abspath(exempted)) + for exempted in exempted_files): + continue + + with open(path, 'r', encoding='utf-8') as file: + if not template.search(file.read()): + ok = False + print("{}: License missing or incorrect".format(path)) + + print() + return ok + + +# check TODO(PR) and FIXME instances +def check_todos(): + print("==== Checking TODO(PR) and FIXME instances:") + + # list of files exempted from checks + exempted_files = [ + ] + + ok = True + for path in itertools.chain(rs_sources(), cargo_config_files()): + if any(fnmatch.fnmatch(os.path.abspath(path), os.path.abspath(exempted)) + for exempted in exempted_files): + continue + + with open(path, 'r', encoding='utf-8') as file: + file_data = file.read() + if 'TODO(PR)' in file_data or 'FIXME' in file_data: + ok = False + print("{}: Found TODO(PR) or FIXME or todo!() instances".format(path)) + + print() + return ok + + +def file_ends_with_newline(file_path): + with open(file_path, 'r', encoding='utf-8') as file: + file_data = file.read() + if len(file_data) == 0: + # Exclude empty files + return True + + last_char = file_data[-1] + if last_char == '\n': + return True + return False + + +def check_files_end_with_newline(): + print("==== Checking file endings with EOL:") + + # list of files exempted from checks + exempted_files = [ + ] + + ok = True + for path in sources_with_extensions([".toml",".rs",".py",".js",".yml",".yaml",".json",".htm",".html"]): + if any(fnmatch.fnmatch(os.path.abspath(path), os.path.abspath(exempted)) + for exempted in exempted_files): + continue + + if file_ends_with_newline(path) is False: + ok = False + print("{}: File does not end with EOL".format(path)) + + print() + return ok + + +# Check for trailing whitespaces +def check_trailing_whitespaces(): + print("==== Checking for trailing whitespaces:") + + # list of files exempted from checks + exempted_files = [ + ] + + ok = True + for path in all_files(): + if any(fnmatch.fnmatch(os.path.abspath(path), os.path.abspath(exempted)) + for exempted in exempted_files): + continue + + with open(path, 'r', encoding='utf-8') as file: + try: + for line_idx, line in enumerate(file, start=1): + line = line.rstrip('\n\r') + if line != line.rstrip(): + ok = False + print(f"{path}: trailing whitespaces at line {line_idx}") + except: + print(f"{path}: can't check for trailing whitespaces, " + "perhaps it should be in 'exempted_files'?") + + print() + return ok + + +def run_checks(): + return all([ + check_local_licenses(), + check_dependency_versions_patch_version(), + check_todos(), + check_trailing_whitespaces(), + check_files_end_with_newline() + ]) + + +if __name__ == '__main__': + if not run_checks(): + sys.exit(1) diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..dd2da22 --- /dev/null +++ b/deny.toml @@ -0,0 +1,50 @@ +[sources.allow-org] +github = [ + "mintlayer", # allow any code from mintlayer's github +] + +[licenses] +# We reject code without a license. +# TODO: we also use the "MITNFA" license (AKA "MIT +no-false-attribs"); this comes from the `hex_lit` +# crate, which is indirectly used by `trezor-client`. The license itself is fine, but for some reason +# `cargo deny` doesn't complain about it even though it's not in the list (but note that it does complain +# about it in the `bridge_v2` repo, which inherits `hex_lit` from `mintlayer-core`). +# Need to investigate why it happens. +confidence-threshold = 0.92 +allow = [ + "0BSD", + "Apache-2.0", + "BSD-2-Clause", + "BSD-3-Clause", + "BSL-1.0", + "CC0-1.0", + "ISC", + "MIT", + "MPL-2.0", + "Unicode-3.0", + "Unlicense", # this is a specific license rather than no license at all + "Zlib", +] # deny a license not in this set of licenses + +[[licenses.clarify]] +name = "ring" +expression = "LicenseRef-ring" +license-files = [ + { path = "LICENSE", hash = 0xbd0eed23 }, +] + +[[licenses.clarify]] +name = "webpki" +expression = "LicenseRef-webpki" +license-files = [ + { path = "LICENSE", hash = 0x001c7e6c }, +] + +[advisories] +version = 2 +db-path = "~/.cargo/advisory-dbs" +db-urls = [ "https://github.com/RustSec/advisory-db" ] +yanked = "warn" +ignore = [ + "RUSTSEC-2024-0436", # "paste" is no longer maintained +] diff --git a/do_checks.sh b/do_checks.sh new file mode 100755 index 0000000..367909f --- /dev/null +++ b/do_checks.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +set -e +set -o nounset + +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +PYTHON=$(which python || which python3) + +cargo fmt --check + +"$PYTHON" "$SCRIPT_DIR/build-tools/codecheck/codecheck.py" + +cargo deny --log-level error check --hide-inclusion-graph + +# Checks enabled everywhere, including tests, benchmarks. +cargo clippy --all-features --workspace --all-targets -- \ + -D warnings \ + -A clippy::unnecessary_literal_unwrap \ + -A clippy::new_without_default \ + -D clippy::implicit_saturating_sub \ + -D clippy::implicit_clone \ + -D clippy::map_unwrap_or \ + -D clippy::unnested_or_patterns \ + -D clippy::manual_assert \ + -D clippy::unused_async \ + -D clippy::mut_mut \ + -D clippy::todo + +# Checks that only apply to production code +cargo clippy --all-features --workspace --lib --bins --examples -- \ + -A clippy::all \ + -D clippy::float_arithmetic \ + -D clippy::unwrap_used \ + -D clippy::dbg_macro \ + -D clippy::items_after_statements \ + -D clippy::fallible_impl_from \ + -D clippy::string_slice diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..f5fd38c --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,12 @@ +max_width = 100 +hard_tabs = false +tab_spaces = 4 +array_width = 80 +chain_width = 80 +single_line_if_else_max_width = 50 +newline_style = "Unix" +# Note: this has to be consistent with Cargo.toml's `edition`, otherwise `rustfmt` and `cargo fmt` +# will behave differently. +edition = "2024" + +match_arm_leading_pipes = "Preserve" diff --git a/src/accounts.rs b/src/accounts.rs new file mode 100644 index 0000000..a8d6ba5 --- /dev/null +++ b/src/accounts.rs @@ -0,0 +1,101 @@ +// Copyright (c) 2024-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core-primitives/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use parity_scale_codec::{Decode, Encode}; + +use crate::{Amount, DelegationId, Destination, IsTokenUnfreezable, OrderId, PscVec, TokenId}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Encode, Decode)] +pub struct AccountNonce(#[codec(compact)] pub u64); + +/// The type that represents withdrawal from an account. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, strum::EnumDiscriminants)] +#[strum_discriminants(name(AccountSpendingTag), derive(strum::EnumIter))] +pub enum AccountSpending { + #[codec(index = 0)] + DelegationBalance(DelegationId, Amount), +} + +/// A type of OutPoint that represents spending from an account. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct AccountOutPoint { + pub nonce: AccountNonce, + pub spending: AccountSpending, +} + +/// This represents a command that can be performed on an account. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, strum::EnumDiscriminants)] +#[strum_discriminants(name(AccountCommandTag), derive(strum::EnumIter))] +pub enum AccountCommand { + /// Create certain amount of tokens and add them to circulating supply. + #[codec(index = 0)] + MintTokens(TokenId, Amount), + + /// Take tokens out of circulation. Not the same as Burn because unminting means that certain + /// amount of tokens is no longer supported by underlying fiat currency, which can only be + /// done by the authority. + #[codec(index = 1)] + UnmintTokens(TokenId), + + /// After supply is locked tokens cannot be minted or unminted ever again. + /// Works only for Lockable tokens supply. + #[codec(index = 2)] + LockTokenSupply(TokenId), + + /// Freezing token forbids any operation with all the tokens (except for optional unfreeze). + #[codec(index = 3)] + FreezeToken(TokenId, IsTokenUnfreezable), + + /// By unfreezing token all operations are available for the tokens again. + #[codec(index = 4)] + UnfreezeToken(TokenId), + + /// Change the authority who can authorize operations for a token. + #[codec(index = 5)] + ChangeTokenAuthority(TokenId, Destination), + + /// Legacy ConcludeOrder command (orders V0). + #[codec(index = 6)] + ConcludeOrder(OrderId), + + /// Legacy FillOrder command (orders V0). + #[codec(index = 7)] + FillOrder(OrderId, Amount, Destination), + + /// Change token metadata uri. + #[codec(index = 8)] + ChangeTokenMetadataUri(TokenId, PscVec), +} + +/// This represents a command that can be performed on an order account (in orders V1). +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, strum::EnumDiscriminants)] +#[strum_discriminants(name(OrderAccountCommandTag), derive(strum::EnumIter))] +pub enum OrderAccountCommand { + /// Satisfy an order completely or partially. + /// The second element is the fill amount in the order's "ask" currency. + #[codec(index = 0)] + FillOrder(OrderId, Amount), + + /// Freeze an order which effectively forbids any fill operations. + /// Frozen order can only be concluded. + /// Only the address specified as `conclude_key` can authorize this command. + #[codec(index = 1)] + FreezeOrder(OrderId), + + /// Close an order and withdraw all remaining funds from both give and ask balances. + /// Only the address specified as `conclude_key` can authorize this command. + #[codec(index = 2)] + ConcludeOrder(OrderId), +} diff --git a/src/crypto.rs b/src/crypto.rs new file mode 100644 index 0000000..cce25d7 --- /dev/null +++ b/src/crypto.rs @@ -0,0 +1,51 @@ +// Copyright (c) 2024-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core-primitives/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use parity_scale_codec::{Decode, Encode}; + +pub const PUBLIC_KEY_HASH_SIZE: usize = 20; +pub const SECP256K1_PUBLIC_KEY_SIZE: usize = 33; +pub const SCHNORRKEL_PUBLIC_KEY_SIZE: usize = 32; + +// Note: Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord are already implemented by the macro, +// so no need to derive them. +fixed_hash::construct_fixed_hash! { + #[derive(Encode, Decode)] + pub struct PublicKeyHash(PUBLIC_KEY_HASH_SIZE); +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Encode, Decode)] +pub struct Secp256k1PublicKey(pub [u8; SECP256K1_PUBLIC_KEY_SIZE]); + +#[derive( + Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, strum::EnumDiscriminants, +)] +#[strum_discriminants(name(PublicKeyTag), derive(strum::EnumIter))] +pub enum PublicKey { + #[codec(index = 0)] + Secp256k1Schnorr(Secp256k1PublicKey), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Encode, Decode)] +pub struct SchnorrkelPublicKey(pub [u8; SCHNORRKEL_PUBLIC_KEY_SIZE]); + +#[derive( + Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, strum::EnumDiscriminants, +)] +#[strum_discriminants(name(VrfPublicKeyTag), derive(strum::EnumIter))] +pub enum VrfPublicKey { + #[codec(index = 0)] + Schnorrkel(SchnorrkelPublicKey), +} diff --git a/src/destination.rs b/src/destination.rs new file mode 100644 index 0000000..23cccaa --- /dev/null +++ b/src/destination.rs @@ -0,0 +1,39 @@ +// Copyright (c) 2024-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core-primitives/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use parity_scale_codec::{Decode, Encode}; + +use crate::{PublicKey, PublicKeyHash, ScriptId}; + +#[derive( + Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, strum::EnumDiscriminants, +)] +#[strum_discriminants(name(DestinationTag), derive(strum::EnumIter))] +pub enum Destination { + #[codec(index = 0)] + AnyoneCanSpend, + + #[codec(index = 1)] + PublicKeyHash(PublicKeyHash), + + #[codec(index = 2)] + PublicKey(PublicKey), + + #[codec(index = 3)] + ScriptHash(ScriptId), + + #[codec(index = 4)] + ClassicMultisig(PublicKeyHash), +} diff --git a/src/id.rs b/src/id.rs new file mode 100644 index 0000000..75bb002 --- /dev/null +++ b/src/id.rs @@ -0,0 +1,76 @@ +// Copyright (c) 2024-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core-primitives/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use core::marker::PhantomData; + +use parity_scale_codec::{Decode, Encode}; + +#[derive(derive_more::Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Encode, Decode)] +pub struct Id { + hash: H256, + #[debug(skip)] + _pd: PhantomData, +} + +impl Id { + pub fn new(hash: H256) -> Self { + Self { + hash, + _pd: PhantomData, + } + } + + pub fn hash(&self) -> &H256 { + &self.hash + } +} + +// Note: Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord are already implemented by the macro, +// so no need to derive them. +fixed_hash::construct_fixed_hash! { + #[derive(Encode, Decode)] + pub struct H256(32); +} + +// Note: the derives on the tag types below are technically useless, but they allow us to derive +// the same traits for Id itself. + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct OrderIdTag; +pub type OrderId = Id; + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct TokenIdTag; +pub type TokenId = Id; + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct DelegationIdTag; +pub type DelegationId = Id; + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct PoolIdTag; +pub type PoolId = Id; + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct TransactionIdTag; +pub type TransactionId = Id; + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct GenBlockIdTag; +pub type GenBlockId = Id; + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct ScriptIdTag; +pub type ScriptId = Id; diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..04318df --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,41 @@ +// Copyright (c) 2024-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core-primitives/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![no_std] + +mod accounts; +mod crypto; +mod destination; +mod id; +mod misc; +mod sighash_input_commitment; +mod tokens; +mod tx_input; +mod tx_output; +mod utxo_outpoint; + +#[cfg(test)] +mod tests; + +pub use accounts::*; +pub use crypto::*; +pub use destination::*; +pub use id::*; +pub use misc::*; +pub use sighash_input_commitment::*; +pub use tokens::*; +pub use tx_input::*; +pub use tx_output::*; +pub use utxo_outpoint::*; diff --git a/src/misc.rs b/src/misc.rs new file mode 100644 index 0000000..4cacc6d --- /dev/null +++ b/src/misc.rs @@ -0,0 +1,91 @@ +// Copyright (c) 2024-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core-primitives/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use parity_scale_codec::{Decode, Encode}; + +use crate::TokenId; + +pub type PscVec = parity_scale_codec::alloc::vec::Vec; + +/// The number of parts per thousand. The valid values are in [0, 1000]. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Encode, Decode)] +pub struct PerThousand(pub u16); + +pub type AmountUIntType = u128; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Encode, Decode)] +pub struct Amount { + #[codec(compact)] + atoms: AmountUIntType, +} + +impl Amount { + pub const fn from_atoms(v: AmountUIntType) -> Self { + Amount { atoms: v } + } + + pub const fn into_atoms(&self) -> AmountUIntType { + self.atoms + } +} + +/// This represents an amount of an asset, which can be either coins or tokens. +#[derive( + Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, strum::EnumDiscriminants, +)] +#[strum_discriminants(name(OutputValueTag), derive(strum::EnumIter))] +pub enum OutputValue { + #[codec(index = 0)] + Coin(Amount), + + // Note: index = 1 corresponds to TokenV0, which only existed in the early days of testnet + // and never existed on mainnet. + #[codec(index = 2)] + TokenV1(TokenId, Amount), +} + +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, strum::EnumDiscriminants)] +#[strum_discriminants(name(OutputTimeLockTag), derive(strum::EnumIter))] +pub enum OutputTimeLock { + #[codec(index = 0)] + UntilHeight(BlockHeight), + + #[codec(index = 1)] + UntilTime(BlockTimestamp), + + #[codec(index = 2)] + ForBlockCount(BlocksCount), + + #[codec(index = 3)] + ForSeconds(SecondsCount), +} + +pub type BlockHeightUIntType = u64; + +#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Encode, Decode)] +pub struct BlockHeight(#[codec(compact)] pub BlockHeightUIntType); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Encode, Decode)] +pub struct BlockTimestamp(pub SecondsCount); + +pub type BlocksCountUIntType = u64; + +#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Encode, Decode)] +pub struct BlocksCount(#[codec(compact)] pub BlocksCountUIntType); + +pub type SecondsCountUIntType = u64; + +#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Encode, Decode)] +pub struct SecondsCount(#[codec(compact)] pub SecondsCountUIntType); diff --git a/src/sighash_input_commitment.rs b/src/sighash_input_commitment.rs new file mode 100644 index 0000000..112bd22 --- /dev/null +++ b/src/sighash_input_commitment.rs @@ -0,0 +1,49 @@ +// Copyright (c) 2024-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core-primitives/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use parity_scale_codec::{Decode, Encode}; + +use crate::{Amount, OutputValue, TxOutput}; + +/// Extra data related to an input to which we commit when signing a transaction. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, strum::EnumDiscriminants)] +#[strum_discriminants(name(SighashInputCommitmentTag), derive(strum::EnumIter))] +pub enum SighashInputCommitment { + #[codec(index = 0)] + None, + + #[codec(index = 1)] + Utxo(TxOutput), + + #[codec(index = 2)] + ProduceBlockFromStakeUtxo { + utxo: TxOutput, + staker_balance: Amount, + }, + + #[codec(index = 3)] + FillOrderAccountCommand { + initially_asked: OutputValue, + initially_given: OutputValue, + }, + + #[codec(index = 4)] + ConcludeOrderAccountCommand { + initially_asked: OutputValue, + initially_given: OutputValue, + ask_balance: Amount, + give_balance: Amount, + }, +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs new file mode 100644 index 0000000..8fed9d5 --- /dev/null +++ b/src/tests/mod.rs @@ -0,0 +1,1132 @@ +// Copyright (c) 2024-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core-primitives/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#[cfg(test)] +extern crate std; + +mod utils; + +use std::prelude::v1::*; + +use hex::FromHex; +use parity_scale_codec::Encode; +use strum::IntoEnumIterator as _; + +use crate::tests::utils::{ + SCALE_CODEC_COMPACT_ENC_2_BYTE_VAL_START, SCALE_CODEC_COMPACT_ENC_4_BYTE_VAL_START, + SCALE_CODEC_COMPACT_ENC_5_BYTE_VAL_START, SCALE_CODEC_COMPACT_ENC_6_BYTE_VAL_START, + SCALE_CODEC_COMPACT_ENC_7_BYTE_VAL_START, SCALE_CODEC_COMPACT_ENC_8_BYTE_VAL_START, + SCALE_CODEC_COMPACT_ENC_9_BYTE_VAL_START, SCALE_CODEC_COMPACT_ENC_10_BYTE_VAL_START, + SCALE_CODEC_COMPACT_ENC_11_BYTE_VAL_START, SCALE_CODEC_COMPACT_ENC_12_BYTE_VAL_START, + SCALE_CODEC_COMPACT_ENC_13_BYTE_VAL_START, SCALE_CODEC_COMPACT_ENC_14_BYTE_VAL_START, + SCALE_CODEC_COMPACT_ENC_15_BYTE_VAL_START, SCALE_CODEC_COMPACT_ENC_16_BYTE_VAL_START, + SCALE_CODEC_COMPACT_ENC_17_BYTE_VAL_START, +}; + +use super::*; + +fn hex_encode(t: &T) -> String { + hex::encode(t.encode()) +} + +fn from_hex(s: &str) -> T +where + ::Error: std::fmt::Debug, +{ + ::from_hex(s).unwrap() +} + +#[test] +fn test_amount_encoding() { + let val = Amount::from_atoms(0); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "00"); + + let val = Amount::from_atoms(SCALE_CODEC_COMPACT_ENC_2_BYTE_VAL_START.into()); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "0101"); + + let val = Amount::from_atoms(SCALE_CODEC_COMPACT_ENC_4_BYTE_VAL_START.into()); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "02000100"); + + let val = Amount::from_atoms(SCALE_CODEC_COMPACT_ENC_5_BYTE_VAL_START.into()); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "0300000040"); + + let val = Amount::from_atoms(SCALE_CODEC_COMPACT_ENC_6_BYTE_VAL_START.into()); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "070000000001"); + + let val = Amount::from_atoms(SCALE_CODEC_COMPACT_ENC_7_BYTE_VAL_START.into()); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "0b000000000001"); + + let val = Amount::from_atoms(SCALE_CODEC_COMPACT_ENC_8_BYTE_VAL_START.into()); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "0f00000000000001"); + + let val = Amount::from_atoms(SCALE_CODEC_COMPACT_ENC_9_BYTE_VAL_START.into()); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "130000000000000001"); + + let val = Amount::from_atoms(SCALE_CODEC_COMPACT_ENC_10_BYTE_VAL_START); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "17000000000000000001"); + + let val = Amount::from_atoms(SCALE_CODEC_COMPACT_ENC_11_BYTE_VAL_START); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "1b00000000000000000001"); + + let val = Amount::from_atoms(SCALE_CODEC_COMPACT_ENC_12_BYTE_VAL_START); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "1f0000000000000000000001"); + + let val = Amount::from_atoms(SCALE_CODEC_COMPACT_ENC_13_BYTE_VAL_START); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "23000000000000000000000001"); + + let val = Amount::from_atoms(SCALE_CODEC_COMPACT_ENC_14_BYTE_VAL_START); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "2700000000000000000000000001"); + + let val = Amount::from_atoms(SCALE_CODEC_COMPACT_ENC_15_BYTE_VAL_START); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "2b0000000000000000000000000001"); + + let val = Amount::from_atoms(SCALE_CODEC_COMPACT_ENC_16_BYTE_VAL_START); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "2f000000000000000000000000000001"); + + let val = Amount::from_atoms(SCALE_CODEC_COMPACT_ENC_17_BYTE_VAL_START); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "3300000000000000000000000000000001"); +} + +#[test] +fn test_block_height_encoding() { + let val = BlockHeight(0); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "00"); + + let val = BlockHeight(SCALE_CODEC_COMPACT_ENC_2_BYTE_VAL_START.into()); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "0101"); + + let val = BlockHeight(SCALE_CODEC_COMPACT_ENC_4_BYTE_VAL_START.into()); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "02000100"); + + let val = BlockHeight(SCALE_CODEC_COMPACT_ENC_5_BYTE_VAL_START.into()); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "0300000040"); + + let val = BlockHeight(SCALE_CODEC_COMPACT_ENC_6_BYTE_VAL_START); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "070000000001"); + + let val = BlockHeight(SCALE_CODEC_COMPACT_ENC_7_BYTE_VAL_START); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "0b000000000001"); + + let val = BlockHeight(SCALE_CODEC_COMPACT_ENC_8_BYTE_VAL_START); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "0f00000000000001"); + + let val = BlockHeight(SCALE_CODEC_COMPACT_ENC_9_BYTE_VAL_START); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "130000000000000001"); +} + +#[test] +fn test_blocks_count_encoding() { + let val = BlocksCount(0); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "00"); + + let val = BlocksCount(SCALE_CODEC_COMPACT_ENC_2_BYTE_VAL_START.into()); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "0101"); + + let val = BlocksCount(SCALE_CODEC_COMPACT_ENC_4_BYTE_VAL_START.into()); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "02000100"); + + let val = BlocksCount(SCALE_CODEC_COMPACT_ENC_5_BYTE_VAL_START.into()); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "0300000040"); + + let val = BlocksCount(SCALE_CODEC_COMPACT_ENC_6_BYTE_VAL_START); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "070000000001"); + + let val = BlocksCount(SCALE_CODEC_COMPACT_ENC_7_BYTE_VAL_START); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "0b000000000001"); + + let val = BlocksCount(SCALE_CODEC_COMPACT_ENC_8_BYTE_VAL_START); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "0f00000000000001"); + + let val = BlocksCount(SCALE_CODEC_COMPACT_ENC_9_BYTE_VAL_START); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "130000000000000001"); +} + +#[test] +fn test_seconds_count_encoding() { + let val = SecondsCount(0); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "00"); + + let val = SecondsCount(SCALE_CODEC_COMPACT_ENC_2_BYTE_VAL_START.into()); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "0101"); + + let val = SecondsCount(SCALE_CODEC_COMPACT_ENC_4_BYTE_VAL_START.into()); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "02000100"); + + let val = SecondsCount(SCALE_CODEC_COMPACT_ENC_5_BYTE_VAL_START.into()); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "0300000040"); + + let val = SecondsCount(SCALE_CODEC_COMPACT_ENC_6_BYTE_VAL_START); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "070000000001"); + + let val = SecondsCount(SCALE_CODEC_COMPACT_ENC_7_BYTE_VAL_START); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "0b000000000001"); + + let val = SecondsCount(SCALE_CODEC_COMPACT_ENC_8_BYTE_VAL_START); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "0f00000000000001"); + + let val = SecondsCount(SCALE_CODEC_COMPACT_ENC_9_BYTE_VAL_START); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "130000000000000001"); +} + +#[test] +fn test_block_timestamp_encoding() { + let val = BlockTimestamp(SecondsCount(0)); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "00"); + + let val = BlockTimestamp(SecondsCount( + SCALE_CODEC_COMPACT_ENC_2_BYTE_VAL_START.into(), + )); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "0101"); + + let val = BlockTimestamp(SecondsCount( + SCALE_CODEC_COMPACT_ENC_4_BYTE_VAL_START.into(), + )); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "02000100"); + + let val = BlockTimestamp(SecondsCount( + SCALE_CODEC_COMPACT_ENC_5_BYTE_VAL_START.into(), + )); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "0300000040"); + + let val = BlockTimestamp(SecondsCount(SCALE_CODEC_COMPACT_ENC_6_BYTE_VAL_START)); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "070000000001"); + + let val = BlockTimestamp(SecondsCount(SCALE_CODEC_COMPACT_ENC_7_BYTE_VAL_START)); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "0b000000000001"); + + let val = BlockTimestamp(SecondsCount(SCALE_CODEC_COMPACT_ENC_8_BYTE_VAL_START)); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "0f00000000000001"); + + let val = BlockTimestamp(SecondsCount(SCALE_CODEC_COMPACT_ENC_9_BYTE_VAL_START)); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "130000000000000001"); +} + +#[test] +fn test_public_key_hash_encoding() { + let val = PublicKeyHash(from_hex("1122334455667788990011223344556677889900")); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "1122334455667788990011223344556677889900"); +} + +#[test] +fn test_public_key_encoding() { + for tag in PublicKeyTag::iter() { + match tag { + PublicKeyTag::Secp256k1Schnorr => { + let val = PublicKey::Secp256k1Schnorr(Secp256k1PublicKey(from_hex( + "112233445566778899001122334455667788990011223344556677889900112233", + ))); + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + "00112233445566778899001122334455667788990011223344556677889900112233" + ); + } + } + } +} + +#[test] +fn test_vrf_public_key_encoding() { + for tag in VrfPublicKeyTag::iter() { + match tag { + VrfPublicKeyTag::Schnorrkel => { + let val = VrfPublicKey::Schnorrkel(SchnorrkelPublicKey(from_hex( + "1122334455667788990011223344556677889900112233445566778899001122", + ))); + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + "001122334455667788990011223344556677889900112233445566778899001122" + ); + } + } + } +} + +#[test] +fn test_id_encoding() { + #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] + struct CustomTag; + type CustomId = Id; + + let val = CustomId::new(H256(from_hex( + "1122334455667788990011223344556677889900112233445566778899001122", + ))); + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + "1122334455667788990011223344556677889900112233445566778899001122" + ); +} + +#[test] +fn test_destination_encoding() { + for tag in DestinationTag::iter() { + match tag { + DestinationTag::AnyoneCanSpend => { + let val = Destination::AnyoneCanSpend; + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "00"); + } + DestinationTag::PublicKeyHash => { + let val = Destination::PublicKeyHash(PublicKeyHash(from_hex( + "1122334455667788990011223344556677889900", + ))); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "011122334455667788990011223344556677889900"); + } + DestinationTag::PublicKey => { + let val = Destination::PublicKey(PublicKey::Secp256k1Schnorr(Secp256k1PublicKey( + from_hex("112233445566778899001122334455667788990011223344556677889900112233"), + ))); + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + "0200112233445566778899001122334455667788990011223344556677889900112233" + ); + } + DestinationTag::ScriptHash => { + let val = Destination::ScriptHash(Id::new(H256(from_hex( + "1122334455667788990011223344556677889900112233445566778899001122", + )))); + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + "031122334455667788990011223344556677889900112233445566778899001122" + ); + } + DestinationTag::ClassicMultisig => { + let val = Destination::ClassicMultisig(PublicKeyHash(from_hex( + "1122334455667788990011223344556677889900", + ))); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "041122334455667788990011223344556677889900"); + } + } + } +} + +#[test] +fn test_per_thousand_encoding() { + let val = PerThousand(123); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "7b00"); +} + +#[test] +fn test_output_value_encoding() { + for tag in OutputValueTag::iter() { + match tag { + OutputValueTag::Coin => { + let val = OutputValue::Coin(Amount::from_atoms(123)); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "00ed01"); + } + OutputValueTag::TokenV1 => { + let val = OutputValue::TokenV1( + Id::new(H256(from_hex( + "1122334455667788990011223344556677889900112233445566778899001122", + ))), + Amount::from_atoms(123), + ); + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + "021122334455667788990011223344556677889900112233445566778899001122ed01" + ); + } + } + } +} + +#[test] +fn test_account_nonce_encoding() { + let val = AccountNonce(0); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "00"); + + let val = AccountNonce(SCALE_CODEC_COMPACT_ENC_2_BYTE_VAL_START.into()); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "0101"); + + let val = AccountNonce(SCALE_CODEC_COMPACT_ENC_4_BYTE_VAL_START.into()); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "02000100"); + + let val = AccountNonce(SCALE_CODEC_COMPACT_ENC_5_BYTE_VAL_START.into()); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "0300000040"); + + let val = AccountNonce(SCALE_CODEC_COMPACT_ENC_6_BYTE_VAL_START); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "070000000001"); + + let val = AccountNonce(SCALE_CODEC_COMPACT_ENC_7_BYTE_VAL_START); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "0b000000000001"); + + let val = AccountNonce(SCALE_CODEC_COMPACT_ENC_8_BYTE_VAL_START); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "0f00000000000001"); + + let val = AccountNonce(SCALE_CODEC_COMPACT_ENC_9_BYTE_VAL_START); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "130000000000000001"); +} + +#[test] +fn test_account_spending_encoding() { + for tag in AccountSpendingTag::iter() { + match tag { + AccountSpendingTag::DelegationBalance => { + let val = AccountSpending::DelegationBalance( + Id::new(H256(from_hex( + "1122334455667788990011223344556677889900112233445566778899001122", + ))), + Amount::from_atoms(123), + ); + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + "001122334455667788990011223344556677889900112233445566778899001122ed01" + ); + } + } + } +} + +#[test] +fn test_account_outpoint_encoding() { + let val = AccountOutPoint { + nonce: AccountNonce(123), + spending: AccountSpending::DelegationBalance( + Id::new(H256(from_hex( + "1122334455667788990011223344556677889900112233445566778899001122", + ))), + Amount::from_atoms(123), + ), + }; + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + "ed01001122334455667788990011223344556677889900112233445566778899001122ed01" + ); +} + +#[test] +fn test_is_token_freezable_encoding() { + for val in IsTokenFreezable::iter() { + match val { + IsTokenFreezable::No => { + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "00"); + } + IsTokenFreezable::Yes => { + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "01"); + } + } + } +} + +#[test] +fn test_is_token_unfreezable_encoding() { + for val in IsTokenUnfreezable::iter() { + match val { + IsTokenUnfreezable::No => { + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "00"); + } + IsTokenUnfreezable::Yes => { + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "01"); + } + } + } +} + +#[test] +fn test_account_command_encoding() { + for tag in AccountCommandTag::iter() { + match tag { + AccountCommandTag::MintTokens => { + let val = AccountCommand::MintTokens( + Id::new(H256(from_hex( + "1122334455667788990011223344556677889900112233445566778899001122", + ))), + Amount::from_atoms(123), + ); + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + "001122334455667788990011223344556677889900112233445566778899001122ed01" + ); + } + AccountCommandTag::UnmintTokens => { + let val = AccountCommand::UnmintTokens(Id::new(H256(from_hex( + "1122334455667788990011223344556677889900112233445566778899001122", + )))); + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + "011122334455667788990011223344556677889900112233445566778899001122" + ); + } + AccountCommandTag::LockTokenSupply => { + let val = AccountCommand::LockTokenSupply(Id::new(H256(from_hex( + "1122334455667788990011223344556677889900112233445566778899001122", + )))); + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + "021122334455667788990011223344556677889900112233445566778899001122" + ); + } + AccountCommandTag::FreezeToken => { + let val = AccountCommand::FreezeToken( + Id::new(H256(from_hex( + "1122334455667788990011223344556677889900112233445566778899001122", + ))), + IsTokenUnfreezable::Yes, + ); + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + "03112233445566778899001122334455667788990011223344556677889900112201" + ); + } + AccountCommandTag::UnfreezeToken => { + let val = AccountCommand::UnfreezeToken(Id::new(H256(from_hex( + "1122334455667788990011223344556677889900112233445566778899001122", + )))); + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + "041122334455667788990011223344556677889900112233445566778899001122" + ); + } + AccountCommandTag::ChangeTokenAuthority => { + let val = AccountCommand::ChangeTokenAuthority( + Id::new(H256(from_hex( + "1122334455667788990011223344556677889900112233445566778899001122", + ))), + Destination::AnyoneCanSpend, + ); + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + "05112233445566778899001122334455667788990011223344556677889900112200" + ); + } + AccountCommandTag::ConcludeOrder => { + let val = AccountCommand::ConcludeOrder(Id::new(H256(from_hex( + "1122334455667788990011223344556677889900112233445566778899001122", + )))); + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + "061122334455667788990011223344556677889900112233445566778899001122" + ); + } + AccountCommandTag::FillOrder => { + let val = AccountCommand::FillOrder( + Id::new(H256(from_hex( + "1122334455667788990011223344556677889900112233445566778899001122", + ))), + Amount::from_atoms(123), + Destination::AnyoneCanSpend, + ); + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + "071122334455667788990011223344556677889900112233445566778899001122ed0100" + ); + } + AccountCommandTag::ChangeTokenMetadataUri => { + let val = AccountCommand::ChangeTokenMetadataUri( + Id::new(H256(from_hex( + "1122334455667788990011223344556677889900112233445566778899001122", + ))), + from_hex("111122223333"), + ); + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + "08112233445566778899001122334455667788990011223344556677889900112218111122223333" + ); + } + } + } +} + +#[test] +fn test_output_time_lock_encoding() { + for tag in OutputTimeLockTag::iter() { + match tag { + OutputTimeLockTag::UntilHeight => { + let val = OutputTimeLock::UntilHeight(BlockHeight(123)); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "00ed01"); + } + OutputTimeLockTag::UntilTime => { + let val = OutputTimeLock::UntilTime(BlockTimestamp(SecondsCount(123))); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "01ed01"); + } + OutputTimeLockTag::ForBlockCount => { + let val = OutputTimeLock::ForBlockCount(BlocksCount(123)); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "02ed01"); + } + OutputTimeLockTag::ForSeconds => { + let val = OutputTimeLock::ForSeconds(SecondsCount(123)); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "03ed01"); + } + } + } +} + +#[test] +fn test_order_account_command_encoding() { + for tag in OrderAccountCommandTag::iter() { + match tag { + OrderAccountCommandTag::FillOrder => { + let val = OrderAccountCommand::FillOrder( + Id::new(H256(from_hex( + "1122334455667788990011223344556677889900112233445566778899001122", + ))), + Amount::from_atoms(123), + ); + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + "001122334455667788990011223344556677889900112233445566778899001122ed01" + ); + } + OrderAccountCommandTag::FreezeOrder => { + let val = OrderAccountCommand::FreezeOrder(Id::new(H256(from_hex( + "1122334455667788990011223344556677889900112233445566778899001122", + )))); + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + "011122334455667788990011223344556677889900112233445566778899001122" + ); + } + OrderAccountCommandTag::ConcludeOrder => { + let val = OrderAccountCommand::ConcludeOrder(Id::new(H256(from_hex( + "1122334455667788990011223344556677889900112233445566778899001122", + )))); + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + "021122334455667788990011223344556677889900112233445566778899001122" + ); + } + } + } +} + +#[test] +fn test_stake_pool_data_encoding() { + let val = StakePoolData { + pledge: Amount::from_atoms(123), + staker: Destination::AnyoneCanSpend, + vrf_public_key: VrfPublicKey::Schnorrkel(SchnorrkelPublicKey(from_hex( + "1122334455667788990011223344556677889900112233445566778899001122", + ))), + decommission_key: Destination::AnyoneCanSpend, + margin_ratio_per_thousand: PerThousand(123), + cost_per_block: Amount::from_atoms(123), + }; + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + "ed0100001122334455667788990011223344556677889900112233445566778899001122007b00ed01" + ); +} + +#[test] +fn test_token_total_supply_encoding() { + for tag in TokenTotalSupplyTag::iter() { + match tag { + TokenTotalSupplyTag::Fixed => { + let val = TokenTotalSupply::Fixed(Amount::from_atoms(123)); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "00ed01"); + } + TokenTotalSupplyTag::Lockable => { + let val = TokenTotalSupply::Lockable; + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "01"); + } + TokenTotalSupplyTag::Unlimited => { + let val = TokenTotalSupply::Unlimited; + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "02"); + } + } + } +} + +#[test] +fn test_token_issuance_encoding() { + for tag in TokenIssuanceTag::iter() { + match tag { + TokenIssuanceTag::V1 => { + let val = TokenIssuance::V1(TokenIssuanceV1 { + token_ticker: from_hex("111122223333"), + number_of_decimals: 123, + metadata_uri: from_hex("444455556666"), + total_supply: TokenTotalSupply::Unlimited, + authority: Destination::AnyoneCanSpend, + is_freezable: IsTokenFreezable::Yes, + }); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "01181111222233337b18444455556666020001"); + } + } + } +} + +#[test] +fn test_nft_issuance_encoding() { + for tag in NftIssuanceTag::iter() { + match tag { + NftIssuanceTag::V0 => { + let val = NftIssuance::V0(NftIssuanceV0 { + creator: Some(PublicKey::Secp256k1Schnorr(Secp256k1PublicKey(from_hex( + "112233445566778899001122334455667788990011223344556677889900112233", + )))), + name: from_hex("1234"), + description: from_hex("2345"), + ticker: from_hex("3456"), + icon_uri: from_hex("4567"), + additional_metadata_uri: from_hex("5678"), + media_uri: from_hex("6789"), + media_hash: from_hex("7890"), + }); + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + concat!( + "000100112233445566778899001122334455667788990011223344556677889900112233", + "081234082345083456084567085678086789087890" + ) + ); + } + } + } +} + +#[test] +fn test_htlc_encoding() { + let val = HashedTimelockContract { + secret_hash: HtlcSecretHash(from_hex("1122334455667788990011223344556677889900")), + spend_key: Destination::AnyoneCanSpend, + refund_timelock: OutputTimeLock::ForBlockCount(BlocksCount(123)), + refund_key: Destination::AnyoneCanSpend, + }; + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + "11223344556677889900112233445566778899000002ed0100" + ); +} + +#[test] +fn test_order_data_encoding() { + let val = OrderData { + conclude_key: Destination::AnyoneCanSpend, + ask: OutputValue::Coin(Amount::from_atoms(123)), + give: OutputValue::Coin(Amount::from_atoms(234)), + }; + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "0000ed0100a903"); +} + +#[test] +fn test_tx_output_encoding() { + for tag in TxOutputTag::iter() { + match tag { + TxOutputTag::Transfer => { + let val = TxOutput::Transfer( + OutputValue::Coin(Amount::from_atoms(123)), + Destination::AnyoneCanSpend, + ); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "0000ed0100"); + } + TxOutputTag::LockThenTransfer => { + let val = TxOutput::LockThenTransfer( + OutputValue::Coin(Amount::from_atoms(123)), + Destination::AnyoneCanSpend, + OutputTimeLock::UntilHeight(BlockHeight(123)), + ); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "0100ed010000ed01"); + } + TxOutputTag::Burn => { + let val = TxOutput::Burn(OutputValue::Coin(Amount::from_atoms(123))); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "0200ed01"); + } + TxOutputTag::CreateStakePool => { + let val = TxOutput::CreateStakePool( + Id::new(H256(from_hex( + "1122334455667788990011223344556677889900112233445566778899001122", + ))), + StakePoolData { + pledge: Amount::from_atoms(123), + staker: Destination::AnyoneCanSpend, + vrf_public_key: VrfPublicKey::Schnorrkel(SchnorrkelPublicKey(from_hex( + "1122334455667788990011223344556677889900112233445566778899001122", + ))), + decommission_key: Destination::AnyoneCanSpend, + margin_ratio_per_thousand: PerThousand(123), + cost_per_block: Amount::from_atoms(123), + }, + ); + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + concat!( + "031122334455667788990011223344556677889900112233445566778899001122", + "ed010000", + "1122334455667788990011223344556677889900112233445566778899001122", + "007b00ed01" + ) + ); + } + TxOutputTag::ProduceBlockFromStake => { + let val = TxOutput::ProduceBlockFromStake( + Destination::AnyoneCanSpend, + Id::new(H256(from_hex( + "1122334455667788990011223344556677889900112233445566778899001122", + ))), + ); + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + "04001122334455667788990011223344556677889900112233445566778899001122" + ); + } + TxOutputTag::CreateDelegationId => { + let val = TxOutput::CreateDelegationId( + Destination::AnyoneCanSpend, + Id::new(H256(from_hex( + "1122334455667788990011223344556677889900112233445566778899001122", + ))), + ); + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + "05001122334455667788990011223344556677889900112233445566778899001122" + ); + } + TxOutputTag::DelegateStaking => { + let val = TxOutput::DelegateStaking( + Amount::from_atoms(123), + Id::new(H256(from_hex( + "1122334455667788990011223344556677889900112233445566778899001122", + ))), + ); + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + "06ed011122334455667788990011223344556677889900112233445566778899001122" + ); + } + TxOutputTag::IssueFungibleToken => { + let val = TxOutput::IssueFungibleToken(TokenIssuance::V1(TokenIssuanceV1 { + token_ticker: from_hex("111122223333"), + number_of_decimals: 123, + metadata_uri: from_hex("444455556666"), + total_supply: TokenTotalSupply::Unlimited, + authority: Destination::AnyoneCanSpend, + is_freezable: IsTokenFreezable::Yes, + })); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "0701181111222233337b18444455556666020001"); + } + TxOutputTag::IssueNft => { + let val = TxOutput::IssueNft( + Id::new(H256(from_hex( + "1122334455667788990011223344556677889900112233445566778899001122", + ))), + NftIssuance::V0(NftIssuanceV0 { + creator: Some(PublicKey::Secp256k1Schnorr(Secp256k1PublicKey(from_hex( + "112233445566778899001122334455667788990011223344556677889900112233", + )))), + name: from_hex("1234"), + description: from_hex("2345"), + ticker: from_hex("3456"), + icon_uri: from_hex("4567"), + additional_metadata_uri: from_hex("5678"), + media_uri: from_hex("6789"), + media_hash: from_hex("7890"), + }), + Destination::AnyoneCanSpend, + ); + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + concat!( + "081122334455667788990011223344556677889900112233445566778899001122", + "000100112233445566778899001122334455667788990011223344556677889900112233", + "08123408234508345608456708567808678908789000" + ) + ); + } + TxOutputTag::DataDeposit => { + let val = TxOutput::DataDeposit(from_hex("1234567890")); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "09141234567890"); + } + TxOutputTag::Htlc => { + let val = TxOutput::Htlc( + OutputValue::Coin(Amount::from_atoms(123)), + HashedTimelockContract { + secret_hash: HtlcSecretHash(from_hex( + "1122334455667788990011223344556677889900", + )), + spend_key: Destination::AnyoneCanSpend, + refund_timelock: OutputTimeLock::ForBlockCount(BlocksCount(123)), + refund_key: Destination::AnyoneCanSpend, + }, + ); + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + "0a00ed0111223344556677889900112233445566778899000002ed0100" + ); + } + TxOutputTag::CreateOrder => { + let val = TxOutput::CreateOrder(OrderData { + conclude_key: Destination::AnyoneCanSpend, + ask: OutputValue::Coin(Amount::from_atoms(123)), + give: OutputValue::Coin(Amount::from_atoms(234)), + }); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "0b0000ed0100a903"); + } + } + } +} + +#[test] +fn test_sighash_input_commitment_encoding() { + for tag in SighashInputCommitmentTag::iter() { + match tag { + SighashInputCommitmentTag::None => { + let val = SighashInputCommitment::None; + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "00"); + } + SighashInputCommitmentTag::Utxo => { + let val = SighashInputCommitment::Utxo(TxOutput::Transfer( + OutputValue::Coin(Amount::from_atoms(123)), + Destination::AnyoneCanSpend, + )); + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "010000ed0100"); + } + SighashInputCommitmentTag::ProduceBlockFromStakeUtxo => { + let val = SighashInputCommitment::ProduceBlockFromStakeUtxo { + utxo: TxOutput::Transfer( + OutputValue::Coin(Amount::from_atoms(123)), + Destination::AnyoneCanSpend, + ), + staker_balance: Amount::from_atoms(123), + }; + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "020000ed0100ed01"); + } + SighashInputCommitmentTag::FillOrderAccountCommand => { + let val = SighashInputCommitment::FillOrderAccountCommand { + initially_asked: OutputValue::Coin(Amount::from_atoms(123)), + initially_given: OutputValue::Coin(Amount::from_atoms(234)), + }; + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "0300ed0100a903"); + } + SighashInputCommitmentTag::ConcludeOrderAccountCommand => { + let val = SighashInputCommitment::ConcludeOrderAccountCommand { + initially_asked: OutputValue::Coin(Amount::from_atoms(123)), + initially_given: OutputValue::Coin(Amount::from_atoms(234)), + ask_balance: Amount::from_atoms(11), + give_balance: Amount::from_atoms(22), + }; + let encoded_val = hex_encode(&val); + assert_eq!(encoded_val, "0400ed0100a9032c58"); + } + } + } +} + +#[test] +fn test_outpoint_source_id_encoding() { + for tag in OutPointSourceIdTag::iter() { + match tag { + OutPointSourceIdTag::Transaction => { + let val = OutPointSourceId::Transaction(Id::new(H256(from_hex( + "1122334455667788990011223344556677889900112233445566778899001122", + )))); + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + "001122334455667788990011223344556677889900112233445566778899001122" + ); + } + OutPointSourceIdTag::BlockReward => { + let val = OutPointSourceId::BlockReward(Id::new(H256(from_hex( + "1122334455667788990011223344556677889900112233445566778899001122", + )))); + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + "011122334455667788990011223344556677889900112233445566778899001122" + ); + } + } + } +} + +#[test] +fn test_utxo_outpoint_encoding() { + let val = UtxoOutPoint::new( + OutPointSourceId::Transaction(Id::new(H256(from_hex( + "1122334455667788990011223344556677889900112233445566778899001122", + )))), + 123, + ); + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + "0011223344556677889900112233445566778899001122334455667788990011227b000000" + ); +} + +#[test] +fn test_tx_input_encoding() { + for tag in TxInputTag::iter() { + match tag { + TxInputTag::Utxo => { + let val = TxInput::Utxo(UtxoOutPoint::new( + OutPointSourceId::Transaction(Id::new(H256(from_hex( + "1122334455667788990011223344556677889900112233445566778899001122", + )))), + 123, + )); + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + "000011223344556677889900112233445566778899001122334455667788990011227b000000" + ); + } + TxInputTag::Account => { + let val = TxInput::Account(AccountOutPoint { + nonce: AccountNonce(123), + spending: AccountSpending::DelegationBalance( + Id::new(H256(from_hex( + "1122334455667788990011223344556677889900112233445566778899001122", + ))), + Amount::from_atoms(123), + ), + }); + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + "01ed01001122334455667788990011223344556677889900112233445566778899001122ed01" + ); + } + TxInputTag::AccountCommand => { + let val = TxInput::AccountCommand( + AccountNonce(123), + AccountCommand::UnmintTokens(Id::new(H256(from_hex( + "1122334455667788990011223344556677889900112233445566778899001122", + )))), + ); + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + "02ed01011122334455667788990011223344556677889900112233445566778899001122" + ); + } + TxInputTag::OrderAccountCommand => { + let val = TxInput::OrderAccountCommand(OrderAccountCommand::FreezeOrder(Id::new( + H256(from_hex( + "1122334455667788990011223344556677889900112233445566778899001122", + )), + ))); + let encoded_val = hex_encode(&val); + assert_eq!( + encoded_val, + "03011122334455667788990011223344556677889900112233445566778899001122" + ); + } + } + } +} diff --git a/src/tests/utils.rs b/src/tests/utils.rs new file mode 100644 index 0000000..ea1568b --- /dev/null +++ b/src/tests/utils.rs @@ -0,0 +1,32 @@ +// Copyright (c) 2024-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core-primitives/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// The value that a compact-encoded integer must have for its encoded form to be 2-bytes long, +// 4-bytes long etc. +pub const SCALE_CODEC_COMPACT_ENC_2_BYTE_VAL_START: u16 = 1 << 6; +pub const SCALE_CODEC_COMPACT_ENC_4_BYTE_VAL_START: u32 = 1 << 14; +pub const SCALE_CODEC_COMPACT_ENC_5_BYTE_VAL_START: u32 = 1 << 30; +pub const SCALE_CODEC_COMPACT_ENC_6_BYTE_VAL_START: u64 = 1 << 32; +pub const SCALE_CODEC_COMPACT_ENC_7_BYTE_VAL_START: u64 = 1 << 40; +pub const SCALE_CODEC_COMPACT_ENC_8_BYTE_VAL_START: u64 = 1 << 48; +pub const SCALE_CODEC_COMPACT_ENC_9_BYTE_VAL_START: u64 = 1 << 56; +pub const SCALE_CODEC_COMPACT_ENC_10_BYTE_VAL_START: u128 = 1 << 64; +pub const SCALE_CODEC_COMPACT_ENC_11_BYTE_VAL_START: u128 = 1 << 72; +pub const SCALE_CODEC_COMPACT_ENC_12_BYTE_VAL_START: u128 = 1 << 80; +pub const SCALE_CODEC_COMPACT_ENC_13_BYTE_VAL_START: u128 = 1 << 88; +pub const SCALE_CODEC_COMPACT_ENC_14_BYTE_VAL_START: u128 = 1 << 96; +pub const SCALE_CODEC_COMPACT_ENC_15_BYTE_VAL_START: u128 = 1 << 104; +pub const SCALE_CODEC_COMPACT_ENC_16_BYTE_VAL_START: u128 = 1 << 112; +pub const SCALE_CODEC_COMPACT_ENC_17_BYTE_VAL_START: u128 = 1 << 120; diff --git a/src/tokens.rs b/src/tokens.rs new file mode 100644 index 0000000..e8ef2c9 --- /dev/null +++ b/src/tokens.rs @@ -0,0 +1,88 @@ +// Copyright (c) 2024-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core-primitives/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use parity_scale_codec::{Decode, Encode}; + +use crate::{Amount, Destination, PscVec, PublicKey}; + +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, strum::EnumIter)] +pub enum IsTokenFreezable { + #[codec(index = 0)] + No, + + #[codec(index = 1)] + Yes, +} + +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, strum::EnumIter)] +pub enum IsTokenUnfreezable { + #[codec(index = 0)] + No, + + #[codec(index = 1)] + Yes, +} + +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, strum::EnumDiscriminants)] +#[strum_discriminants(name(TokenIssuanceTag), derive(strum::EnumIter))] +pub enum TokenIssuance { + #[codec(index = 1)] + V1(TokenIssuanceV1), +} + +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, strum::EnumDiscriminants)] +#[strum_discriminants(name(TokenTotalSupplyTag), derive(strum::EnumIter))] +pub enum TokenTotalSupply { + /// Fixed to a certain amount. + #[codec(index = 0)] + Fixed(Amount), + + /// Not known in advance but can be locked once at some point in time. + #[codec(index = 1)] + Lockable, + + /// Limited only by the Amount data type. + #[codec(index = 2)] + Unlimited, +} + +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct TokenIssuanceV1 { + pub token_ticker: PscVec, + pub number_of_decimals: u8, + pub metadata_uri: PscVec, + pub total_supply: TokenTotalSupply, + pub authority: Destination, + pub is_freezable: IsTokenFreezable, +} + +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, strum::EnumDiscriminants)] +#[strum_discriminants(name(NftIssuanceTag), derive(strum::EnumIter))] +pub enum NftIssuance { + #[codec(index = 0)] + V0(NftIssuanceV0), +} + +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct NftIssuanceV0 { + pub creator: Option, + pub name: PscVec, + pub description: PscVec, + pub ticker: PscVec, + pub icon_uri: PscVec, + pub additional_metadata_uri: PscVec, + pub media_uri: PscVec, + pub media_hash: PscVec, +} diff --git a/src/tx_input.rs b/src/tx_input.rs new file mode 100644 index 0000000..7920aac --- /dev/null +++ b/src/tx_input.rs @@ -0,0 +1,34 @@ +// Copyright (c) 2024-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core-primitives/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use parity_scale_codec::{Decode, Encode}; + +use crate::{AccountCommand, AccountNonce, AccountOutPoint, OrderAccountCommand, UtxoOutPoint}; + +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, strum::EnumDiscriminants)] +#[strum_discriminants(name(TxInputTag), derive(strum::EnumIter))] +pub enum TxInput { + #[codec(index = 0)] + Utxo(UtxoOutPoint), + + #[codec(index = 1)] + Account(AccountOutPoint), + + #[codec(index = 2)] + AccountCommand(AccountNonce, AccountCommand), + + #[codec(index = 3)] + OrderAccountCommand(OrderAccountCommand), +} diff --git a/src/tx_output.rs b/src/tx_output.rs new file mode 100644 index 0000000..585e3fc --- /dev/null +++ b/src/tx_output.rs @@ -0,0 +1,120 @@ +// Copyright (c) 2024-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core-primitives/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use parity_scale_codec::{Decode, Encode}; + +use crate::{ + Amount, DelegationId, Destination, NftIssuance, OutputTimeLock, OutputValue, PerThousand, + PoolId, PscVec, TokenId, TokenIssuance, VrfPublicKey, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, strum::EnumDiscriminants)] +#[strum_discriminants(name(TxOutputTag), derive(strum::EnumIter))] +pub enum TxOutput { + /// Transfer an output value, giving the provided Destination the authority to + /// spend it (no conditions). + #[codec(index = 0)] + Transfer(OutputValue, Destination), + + /// Same as Transfer, but with the condition that the output can only be + /// spent after some point in time. + #[codec(index = 1)] + LockThenTransfer(OutputValue, Destination, OutputTimeLock), + + /// Burn an amount (whether coin or token). + #[codec(index = 2)] + Burn(OutputValue), + + /// Output type that is used to create a stake pool. + #[codec(index = 3)] + CreateStakePool(PoolId, StakePoolData), + + /// Output type that represents spending of a stake pool output in a block + /// reward in order to produce a block. + #[codec(index = 4)] + ProduceBlockFromStake(Destination, PoolId), + + /// Create a delegation account to a specific pool, defined by its id. + /// Takes the owner destination, which is the address authorized to withdraw from the delegation. + #[codec(index = 5)] + CreateDelegationId(Destination, PoolId), + + /// Transfer an amount to a delegation. + #[codec(index = 6)] + DelegateStaking(Amount, DelegationId), + + /// Issues a new fungible token. + #[codec(index = 7)] + IssueFungibleToken(TokenIssuance), + + /// Issue an NFT. + #[codec(index = 8)] + IssueNft(TokenId, NftIssuance, Destination), + + /// Deposit data into the blockchain. + #[codec(index = 9)] + DataDeposit(PscVec), + + /// Transfer an output value under Hashed TimeLock Contract. + #[codec(index = 10)] + Htlc(OutputValue, HashedTimelockContract), + + /// Create an order. + #[codec(index = 11)] + CreateOrder(OrderData), +} + +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct StakePoolData { + pub pledge: Amount, + pub staker: Destination, + pub vrf_public_key: VrfPublicKey, + pub decommission_key: Destination, + pub margin_ratio_per_thousand: PerThousand, + pub cost_per_block: Amount, +} + +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct OrderData { + /// The key that can authorize the conclusion or freezing of the order. + pub conclude_key: Destination, + /// The amount of an asset that the order maker wants to receive. + pub ask: OutputValue, + /// The amount of an asset that the order maker wants to give. + pub give: OutputValue, +} + +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct HashedTimelockContract { + /// The hash of the HTLC secret. + pub secret_hash: HtlcSecretHash, + /// The key that can authorize the normal spending of the HTLC output (i.e. when the secret + /// is provided). + pub spend_key: Destination, + + /// The timelock after which the HTLC output can be spent via `refund_key`. + pub refund_timelock: OutputTimeLock, + /// The key that can authorize the refund of the HTLC. + pub refund_key: Destination, +} + +pub const HTLC_SECRET_HASH_SIZE: usize = 20; + +// Note: Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord are already implemented by the macro, +// so no need to derive them. +fixed_hash::construct_fixed_hash! { + #[derive(Encode, Decode)] + pub struct HtlcSecretHash(HTLC_SECRET_HASH_SIZE); +} diff --git a/src/utxo_outpoint.rs b/src/utxo_outpoint.rs new file mode 100644 index 0000000..755f446 --- /dev/null +++ b/src/utxo_outpoint.rs @@ -0,0 +1,53 @@ +// Copyright (c) 2024-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core-primitives/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use parity_scale_codec::{Decode, Encode}; + +use crate::{GenBlockId, TransactionId}; + +#[derive( + Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, strum::EnumDiscriminants, +)] +#[strum_discriminants(name(OutPointSourceIdTag), derive(strum::EnumIter))] +pub enum OutPointSourceId { + #[codec(index = 0)] + Transaction(TransactionId), + + #[codec(index = 1)] + BlockReward(GenBlockId), +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Encode, Decode)] +pub struct UtxoOutPoint { + id: OutPointSourceId, + index: u32, +} + +impl UtxoOutPoint { + pub fn new(outpoint_source_id: OutPointSourceId, output_index: u32) -> Self { + UtxoOutPoint { + id: outpoint_source_id, + index: output_index, + } + } + + pub fn source_id(&self) -> OutPointSourceId { + self.id.clone() + } + + pub fn output_index(&self) -> u32 { + self.index + } +}