├── .github ├── FUNDING.yml └── workflows │ ├── build.yml │ ├── codecov.yml │ ├── lint.yml │ └── test.yml ├── .gitignore ├── .rustfmt.toml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── DCO ├── LICENSE ├── MAINTAINERS.md ├── README.md ├── SECURITY.md ├── _typos.toml ├── cli ├── Cargo.toml └── src │ ├── cmd.rs │ ├── dump.rs │ ├── exec.rs │ └── main.rs ├── codecov.yml ├── invoice ├── Cargo.toml └── src │ ├── bp.rs │ └── lib.rs ├── persistence └── fs │ ├── Cargo.toml │ └── src │ ├── lib.rs │ ├── pile.rs │ └── stockpile.rs ├── rust-toolchain.toml ├── scripts └── typelib.sh ├── src ├── bin │ └── rgb-stl.rs ├── consignment.rs ├── contract.rs ├── contracts.rs ├── lib.rs ├── pile.rs ├── popls │ ├── bp.rs │ ├── mod.rs │ └── prime.rs ├── stl.rs ├── stockpile.rs └── util.rs ├── stl ├── RGB@0.12.0.sta ├── RGB@0.12.0.stl ├── RGB@0.12.0.sty ├── SingleUseSeals@0.12.0.sta ├── SingleUseSeals@0.12.0.stl └── SingleUseSeals@0.12.0.sty └── tests ├── .gitignore ├── consignment.rs ├── data └── Test.issuer ├── reorgs.rs └── utils └── mod.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [lnp-bp, RGB-WG, dr-orlovsky] 4 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - 'v[0-9]+.*' 9 | pull_request: 10 | branches: 11 | - master 12 | - develop 13 | - 'v[0-9]+.?*' 14 | 15 | env: 16 | CARGO_TERM_COLOR: always 17 | 18 | jobs: 19 | default: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: dtolnay/rust-toolchain@stable 24 | - run: cargo check --workspace 25 | no-default: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v4 29 | - uses: dtolnay/rust-toolchain@stable 30 | - run: cargo check --workspace --no-default-features 31 | features: 32 | runs-on: ubuntu-latest 33 | strategy: 34 | fail-fast: false 35 | matrix: 36 | feature: [ bitcoin, liquid, prime, uri, binfile, serde, stl ] 37 | steps: 38 | - uses: actions/checkout@v4 39 | - uses: dtolnay/rust-toolchain@stable 40 | - name: Feature ${{matrix.feature}} 41 | run: cargo check --workspace --no-default-features --features=${{matrix.feature}} 42 | - name: Feature ${{matrix.feature}} 43 | run: cargo check --workspace --features=${{matrix.feature}} 44 | platforms: 45 | runs-on: ${{ matrix.os }} 46 | strategy: 47 | fail-fast: false 48 | matrix: 49 | os: [ ubuntu-22.04, ubuntu-latest, macos-13, macos-latest, windows-2019, windows-latest ] 50 | steps: 51 | - uses: actions/checkout@v4 52 | - uses: dtolnay/rust-toolchain@stable 53 | - name: Platform ${{matrix.os}} 54 | run: cargo check --workspace --all-features # we skip test targets here to be sure that the main library can be built 55 | toolchains: 56 | runs-on: ubuntu-latest 57 | strategy: 58 | fail-fast: false 59 | matrix: 60 | toolchain: [ nightly, beta, stable, 1.85.0 ] 61 | steps: 62 | - uses: actions/checkout@v4 63 | - uses: dtolnay/rust-toolchain@master 64 | with: 65 | toolchain: ${{matrix.toolchain}} 66 | - name: Toolchain ${{matrix.toolchain}} 67 | run: cargo +${{matrix.toolchain}} check --workspace --all-targets --all-features 68 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: Codecov 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - 'v[0-9]+.*' 9 | pull_request: 10 | branches: 11 | - master 12 | - develop 13 | - 'v[0-9]+.?*' 14 | 15 | env: 16 | CARGO_TERM_COLOR: always 17 | 18 | jobs: 19 | codecov: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: dtolnay/rust-toolchain@nightly 24 | with: 25 | components: llvm-tools-preview 26 | - uses: taiki-e/install-action@cargo-llvm-cov 27 | - uses: taiki-e/install-action@nextest 28 | - name: Collect coverage data (including doctests) 29 | run: | 30 | cargo +nightly llvm-cov --no-report nextest --workspace --all-features 31 | cargo +nightly llvm-cov --no-report --doc --workspace --all-features 32 | cargo +nightly llvm-cov report --doctests --lcov --output-path lcov.info 33 | - name: Upload coverage data to codecov 34 | uses: codecov/codecov-action@v4 35 | with: 36 | flags: rust 37 | files: lcov.info 38 | fail_ci_if_error: true 39 | token: ${{ secrets.CODECOV_TOKEN }} 40 | verbose: true 41 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lints 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | - develop 8 | - 'v[0-9]+.?*' 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | 13 | jobs: 14 | fmt: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: dtolnay/rust-toolchain@nightly 19 | with: 20 | components: rustfmt 21 | - name: Formatting 22 | run: cargo +nightly fmt --all -- --check 23 | clippy: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v4 27 | - uses: dtolnay/rust-toolchain@stable 28 | with: 29 | components: clippy 30 | - name: Formatting 31 | run: cargo clippy --workspace --all-features --all-targets -- -D warnings 32 | doc: 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v4 36 | - uses: dtolnay/rust-toolchain@nightly 37 | with: 38 | components: rust-docs 39 | - name: Formatting 40 | run: cargo +nightly doc --workspace --all-features 41 | typos: 42 | runs-on: ubuntu-latest 43 | steps: 44 | - uses: actions/checkout@v4 45 | - uses: crate-ci/typos@master 46 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - 'v[0-9]+.*' 9 | pull_request: 10 | branches: 11 | - master 12 | - develop 13 | - 'v[0-9]+.?*' 14 | 15 | env: 16 | CARGO_TERM_COLOR: always 17 | 18 | jobs: 19 | testing: 20 | runs-on: ${{ matrix.os }} 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | os: [ ubuntu-latest, macos-13, macos-latest, windows-latest ] 25 | steps: 26 | - uses: actions/checkout@v4 27 | - uses: dtolnay/rust-toolchain@stable 28 | - name: Test ${{matrix.os}} 29 | run: cargo test --workspace --all-features --no-fail-fast 30 | wasm-testing: 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v4 34 | - uses: dtolnay/rust-toolchain@nightly 35 | - uses: jetli/wasm-pack-action@v0.4.0 36 | - name: Add wasm32 target 37 | run: rustup target add wasm32-unknown-unknown 38 | - name: Test in headless Chrome 39 | run: RUSTFLAGS='--cfg getrandom_backend="wasm_js"' wasm-pack test --headless --chrome --all-features 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target 4 | 5 | # These are backup files generated by rustfmt 6 | **/*.rs.bk 7 | 8 | .idea 9 | 10 | *.swp 11 | 12 | /dep_test 13 | result 14 | default_*.profraw 15 | coverage.lcov 16 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | style_edition = "2021" 3 | 4 | max_width = 100 5 | array_width = 100 6 | attr_fn_like_width = 100 7 | comment_width = 100 8 | chain_width = 60 9 | fn_call_width = 100 10 | single_line_if_else_max_width = 100 11 | struct_lit_width = 60 12 | 13 | fn_single_line = true 14 | format_code_in_doc_comments = true 15 | format_macro_matchers = true 16 | format_macro_bodies = true 17 | format_strings = true 18 | merge_derives = false 19 | overflow_delimited_expr = true 20 | reorder_modules = false 21 | use_field_init_shorthand = true 22 | use_try_shorthand = true 23 | wrap_comments = true 24 | where_single_line = true 25 | unstable_features = true 26 | 27 | imports_granularity = "Module" 28 | group_imports = "StdExternalCrate" 29 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | We do not apply any code of conduct expectations for contributors and 4 | maintainers in their live and behaviour outside the scope of this project. 5 | We believe that a restriction is the word of sin: free people write code, take 6 | their decisions and act in a way they will, taking responsibility for the 7 | consequences. 8 | 9 | Moreover, we will try to protect the freedom of speech of contributors, and 10 | explicit distance from personal or public life of contributors, as long as 11 | they behave in a civil and productive way when contributing and interacting 12 | within the project, and will go to great lengths to not deny anyone 13 | participation. 14 | 15 | Actions within the technical scope of the project (code quality, spamming etc), 16 | as well as interaction with other maintainers and contributors of course is 17 | a factor of the access to the project development and lifecycle. The decision in 18 | these cases will be made by the project maintainers, with the right of veto or 19 | overriding vote reserved for the original project author, Maxim Orlovsky. 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing guidelines 2 | ======================= 3 | 4 | Contributions are very welcome. When contributing code, please follow these 5 | simple guidelines. 6 | 7 | #### Table Of Contents 8 | - [Contribution workflow](#contribution-workflow) 9 | * [Proposing changes](#proposing-changes) 10 | * [Preparing PRs](#preparing-prs) 11 | * [Peer review](#peer-review) 12 | - [Coding conventions](#coding-conventions) 13 | - [Security](#security) 14 | - [Testing](#testing) 15 | - [Going further](#going-further) 16 | 17 | Overview 18 | -------- 19 | 20 | * Before adding any code dependencies, check with the maintainers if this is okay. 21 | * Write properly formatted comments: they should be English sentences, eg: 22 | 23 | // Return the current UNIX time. 24 | 25 | * Read the DCO and make sure all commits are signed off, using `git commit -s`. 26 | * Follow the guidelines when proposing code changes (see below). 27 | * Write properly formatted git commits (see below). 28 | * Run the tests with `cargo test --workspace --all-features`. 29 | * Make sure you run `rustfmt` on your code (see below details). 30 | * Please don't file an issue to ask a question. Each repository - or 31 | GitHub organization has a "Discussions" with Q&A section; please post your 32 | questions there. You'll get faster results by using this channel. 33 | 34 | Contribution Workflow 35 | --------------------- 36 | The codebase is maintained using the "contributor workflow" where everyone 37 | without exception contributes patch proposals using "pull requests". This 38 | facilitates social contribution, easy testing and peer review. 39 | 40 | To contribute a patch, the workflow is a as follows: 41 | 42 | 1. Fork Repository 43 | 2. Create topic branch 44 | 3. Commit patches 45 | 46 | In general commits should be atomic and diffs should be easy to read. For this 47 | reason do not mix any formatting fixes or code moves with actual code changes. 48 | Further, each commit, individually, should compile and pass tests, in order to 49 | ensure git bisect and other automated tools function properly. 50 | 51 | When adding a new feature thought must be given to the long term technical debt. 52 | Every new features should be covered by unit tests. 53 | 54 | When refactoring, structure your PR to make it easy to review and don't hesitate 55 | to split it into multiple small, focused PRs. 56 | 57 | Commits should cover both the issue fixed and the solution's rationale. 58 | These [guidelines](https://chris.beams.io/posts/git-commit/) should be kept in 59 | mind. 60 | 61 | To facilitate communication with other contributors, the project is making use 62 | of GitHub's "assignee" field. First check that no one is assigned and then 63 | comment suggesting that you're working on it. If someone is already assigned, 64 | don't hesitate to ask if the assigned party or previous commenters are still 65 | working on it if it has been awhile. 66 | 67 | ### Proposing changes 68 | 69 | When proposing changes via a pull-request or patch: 70 | 71 | * Isolate changes in separate commits to make the review process easier. 72 | * Don't make unrelated changes, unless it happens to be an obvious improvement to 73 | code you are touching anyway ("boyscout rule"). 74 | * Rebase on `master` when needed. 75 | * Keep your changesets small, specific and uncontroversial, so that they can be 76 | merged more quickly. 77 | * If the change is substantial or requires re-architecting certain parts of the 78 | codebase, write a proposal in English first, and get consensus on that before 79 | proposing the code changes. 80 | 81 | ### Preparing PRs 82 | 83 | The main library development happens in the `master` branch. This branch must 84 | always compile without errors (using Travis CI). All external contributions are 85 | made within PRs into this branch. 86 | 87 | Prerequisites that a PR must satisfy for merging into the `master` branch: 88 | * the tip of any PR branch must compile and pass unit tests with no errors, with 89 | every feature combination (including compiling the fuzztests) on MSRV, stable 90 | and nightly compilers (this is partially automated with CI, so the rule 91 | is that we will not accept commits which do not pass GitHub CI); 92 | * contain all necessary tests for the introduced functional (either as a part of 93 | commits, or, more preferably, as separate commits, so that it's easy to 94 | reorder them during review and check that the new tests fail without the new 95 | code); 96 | * contain all inline docs for newly introduced API and pass doc tests; 97 | * be based on the recent `master` tip from the original repository at. 98 | 99 | NB: reviewers may run more complex test/CI scripts, thus, satisfying all the 100 | requirements above is just a preliminary, but not necessary sufficient step for 101 | getting the PR accepted as a valid candidate PR for the `master` branch. 102 | 103 | Additionally, to the `master` branch some repositories may have `develop` branch 104 | for any experimental developments. This branch may not compile and should not be 105 | used by any projects depending on the library. 106 | 107 | ### Writing Git commit messages 108 | 109 | A properly formed git commit subject line should always be able to complete the 110 | following sentence: 111 | 112 | If applied, this commit will _____ 113 | 114 | In addition, it should be capitalized and *must not* include a period. 115 | 116 | For example, the following message is well formed: 117 | 118 | Add support for .gif files 119 | 120 | While these ones are **not**: `Adding support for .gif files`, 121 | `Added support for .gif files`. 122 | 123 | When it comes to formatting, here's a model git commit message[1]: 124 | 125 | Capitalized, short (50 chars or less) summary 126 | 127 | More detailed explanatory text, if necessary. Wrap it to about 72 128 | characters or so. In some contexts, the first line is treated as the 129 | subject of an email and the rest of the text as the body. The blank 130 | line separating the summary from the body is critical (unless you omit 131 | the body entirely); tools like rebase can get confused if you run the 132 | two together. 133 | 134 | Write your commit message in the imperative: "Fix bug" and not "Fixed bug" 135 | or "Fixes bug." This convention matches up with commit messages generated 136 | by commands like git merge and git revert. 137 | 138 | Further paragraphs come after blank lines. 139 | 140 | - Bullet points are okay, too. 141 | 142 | - Typically a hyphen or asterisk is used for the bullet, followed by a 143 | single space, with blank lines in between, but conventions vary here. 144 | 145 | - Use a hanging indent. 146 | 147 | ### Peer review 148 | 149 | Anyone may participate in peer review which is expressed by comments in the pull 150 | request. Typically reviewers will review the code for obvious errors, as well as 151 | test out the patch set and opine on the technical merits of the patch. PR should 152 | be reviewed first on the conceptual level before focusing on code style or 153 | grammar fixes. 154 | 155 | Coding Conventions 156 | ------------------ 157 | Our CI enforces [clippy's](https://github.com/rust-lang/rust-clippy) 158 | [default linting](https://rust-lang.github.io/rust-clippy/rust-1.52.0/index.html) 159 | and [rustfmt](https://github.com/rust-lang/rustfmt) formatting defined by rules 160 | in [.rustfmt.toml](./.rustfmt.toml). The linter should be run with current 161 | stable rust compiler, while formatter requires nightly version due to the use of 162 | unstable formatting parameters. 163 | 164 | If you use rustup, to lint locally you may run the following instructions: 165 | 166 | ```console 167 | rustup component add clippy 168 | rustup component add fmt 169 | cargo +stable clippy --workspace --all-features 170 | cargo +nightly fmt --all 171 | ``` 172 | 173 | Security 174 | -------- 175 | Responsible disclosure of security vulnerabilities helps prevent user loss of 176 | privacy. If you believe a vulnerability may affect other implementations, please 177 | inform them. Guidelines for a responsible disclosure can be found in 178 | [SECURITY.md](./SECURITY.md) file in the project root. 179 | 180 | Note that some of our projects are currently considered "pre-production". 181 | Such projects can be distinguished by the absence of `SECURITY.md`. In such 182 | cases there are no special handling of security issues; please simply open 183 | an issue on GitHub. 184 | 185 | Going further 186 | ------------- 187 | You may be interested in Jon Atack guide on 188 | [How to review Bitcoin Core PRs][Review] and [How to make Bitcoin Core PRs][PR]. 189 | While there are differences between the projects in terms of context and 190 | maturity, many of the suggestions offered apply to this project. 191 | 192 | Overall, have fun :) 193 | 194 | [1]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html 195 | [Review]: https://github.com/jonatack/bitcoin-development/blob/master/how-to-review-bitcoin-core-prs.md 196 | [PR]: https://github.com/jonatack/bitcoin-development/blob/master/how-to-make-bitcoin-core-prs.md 197 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [".", "cli", "invoice", "persistence/fs"] 3 | default-members = [".", "invoice", "persistence/fs"] 4 | 5 | [workspace.package] 6 | version = "0.12.0-rc.2" 7 | keywords = ["bitcoin", "lightning", "rgb", "smart-contracts", "lnp-bp"] 8 | categories = ["cryptography::cryptocurrencies"] 9 | authors = ["Dr Maxim Orlovsky "] 10 | homepage = "https://rgb.tech" 11 | repository = "https://github.com/RGB-WG/rgb-std" 12 | rust-version = "1.85.0" # Due to associated types generic bound requirements 13 | edition = "2021" 14 | license = "Apache-2.0" 15 | 16 | [workspace.dependencies] 17 | amplify = "4.9.0" 18 | strict_encoding = "2.9.1" 19 | strict_types = "2.9.0" 20 | commit_verify = "0.12.0-rc.1" 21 | single_use_seals = "0.12.0-rc.1" 22 | hypersonic = "0.12.0-rc.2" 23 | sonic-callreq = "0.12.0-rc.2" 24 | sonic-persist-fs = "0.12.0-rc.2" 25 | sonix = "0.12.0-rc.2" 26 | bp-core = "0.12.0-rc.2" 27 | bp-invoice = "0.12.0-rc.2" 28 | rgb-core = "0.12.0-rc.2" 29 | rgb-std = { version = "0.12.0-rc.2", path = "." } 30 | rgb-invoice = { version = "0.12.0-rc.2", path = "./invoice" } 31 | rgb-persist-fs = { version = "0.12.0-rc.2", path = "./persistence/fs" } 32 | aora = ">=0.6.4" 33 | baid64 = "0.4.2" 34 | binfile = "0.2.0" 35 | indexmap = "2.9.0" 36 | chrono = "0.4.41" 37 | serde = "1.0" 38 | serde_with = "1.14" 39 | 40 | [package] 41 | name = "rgb-std" 42 | description = "Standard Library for RGB smart contracts" 43 | version.workspace = true 44 | keywords.workspace = true 45 | categories.workspace = true 46 | authors.workspace = true 47 | homepage.workspace = true 48 | repository.workspace = true 49 | rust-version.workspace = true 50 | edition.workspace = true 51 | license.workspace = true 52 | readme = "README.md" 53 | 54 | [[bin]] 55 | name = "rgb-stl" 56 | required-features = ["stl"] 57 | 58 | [lib] 59 | name = "rgb" 60 | crate-type = ["cdylib", "rlib"] # We need this for WASM 61 | 62 | [dependencies] 63 | amplify.workspace = true 64 | strict_encoding.workspace = true 65 | strict_types.workspace = true 66 | commit_verify.workspace = true 67 | single_use_seals.workspace = true 68 | aora.workspace = true 69 | binfile = { workspace = true, optional = true } 70 | hypersonic.workspace = true 71 | bp-core = { workspace = true, optional = true } 72 | bp-invoice = { workspace = true, optional = true } 73 | rgb-core.workspace = true 74 | rgb-invoice.workspace = true 75 | indexmap.workspace = true 76 | chrono.workspace = true 77 | serde = { workspace = true, optional = true } 78 | serde_with = { workspace = true, optional = true } 79 | 80 | [dev-dependencies] 81 | rand = "0.9.1" 82 | rgb-persist-fs.workspace = true 83 | 84 | [features] 85 | default = ["std", "bitcoin"] 86 | all = ["std", "bitcoin", "liquid", "prime", "binfile", "uri", "stl", "serde"] 87 | std = ["rgb-invoice/std", "indexmap/std"] 88 | 89 | bitcoin = ["bp-core", "bp-invoice", "rgb-core/bitcoin", "rgb-invoice/bitcoin"] 90 | liquid = ["rgb-core/liquid", "rgb-invoice/liquid"] 91 | prime = ["rgb-core/prime", "rgb-invoice/prime"] 92 | 93 | uri = ["rgb-invoice/uri"] 94 | binfile = ["std", "dep:binfile", "hypersonic/binfile", "aora/file-strict"] 95 | 96 | stl = ["commit_verify/stl", "hypersonic/stl", "strict_types/armor", "bitcoin", "bp-core/stl"] 97 | 98 | serde = [ 99 | "std", 100 | "dep:serde", 101 | "dep:serde_with", 102 | "amplify/serde", 103 | "strict_encoding/serde", 104 | "strict_types/serde", 105 | "commit_verify/serde", 106 | "hypersonic/serde", 107 | "bp-core/serde", 108 | "rgb-core/serde", 109 | "rgb-invoice/serde", 110 | "chrono/serde" 111 | ] 112 | 113 | [target.'cfg(target_arch = "wasm32")'.dependencies] 114 | wasm-bindgen = "0.2" 115 | rand = { version = "0.9.1", optional = true } 116 | getrandom = { version = "0.3", features = ["wasm_js"] } 117 | getrandom2 = { package = "getrandom", version = "0.2", features = ["js"] } 118 | 119 | [target.'cfg(target_arch = "wasm32")'.dev-dependencies] 120 | wasm-bindgen-test = "0.3" 121 | 122 | [package.metadata.docs.rs] 123 | features = ["all"] 124 | 125 | [lints.rust] 126 | unexpected_cfgs = { level = "allow", check-cfg = ['cfg(coverage_nightly)'] } 127 | -------------------------------------------------------------------------------- /DCO: -------------------------------------------------------------------------------- 1 | Developer's Certificate of Origin 1.1 2 | Copyright © 2004, 2006 The Linux Foundation and its contributors. 3 | 4 | --- 5 | 6 | By making a contribution to this project, I certify that: 7 | 8 | (a) The contribution was created in whole or in part by me and I 9 | have the right to submit it under the open source license 10 | indicated in the file; or 11 | 12 | (b) The contribution is based upon previous work that, to the best 13 | of my knowledge, is covered under an appropriate open source 14 | license and I have the right under that license to submit that 15 | work with modifications, whether created in whole or in part 16 | by me, under the same open source license (unless I am 17 | permitted to submit under a different license), as indicated 18 | in the file; or 19 | 20 | (c) The contribution was provided directly to me by some other 21 | person who certified (a), (b) or (c) and I have not modified 22 | it. 23 | 24 | (d) I understand and agree that this project and the contribution 25 | are public and that a record of the contribution (including all 26 | personal information I submit with it, including my sign-off) is 27 | maintained indefinitely and may be redistributed consistent with 28 | this project or the open source license(s) involved. 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | Maxim Orlovsky 2 | --------------- 3 | - GitHub: [@dr-orlovsky](https://github.com/dr-orlovsky) 4 | - GPG: `EAE730CEC0C663763F028A5860094BAF18A26EC9` 5 | - SSH: `BoSGFzbyOKC7Jm28MJElFboGepihCpHop60nS8OoG/A` 6 | - EMail: [dr@orlovsky.ch](mailto:dr@orlovsky.ch) 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RGB wallet & standard libraries for smart contracts on Bitcoin & Lightning 2 | 3 | ![Build](https://github.com/RGB-WG/rgb-std/workflows/Build/badge.svg) 4 | ![Tests](https://github.com/RGB-WG/rgb-std/workflows/Tests/badge.svg) 5 | ![Lints](https://github.com/RGB-WG/rgb-std/workflows/Lints/badge.svg) 6 | [![codecov](https://codecov.io/gh/RGB-WG/rgb-std/branch/master/graph/badge.svg)](https://codecov.io/gh/RGB-WG/rgb-std) 7 | 8 | [![crates.io](https://img.shields.io/crates/v/rgb-std)](https://crates.io/crates/rgb-std) 9 | [![Docs](https://docs.rs/rgb-std/badge.svg)](https://docs.rs/rgb-std) 10 | [![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/) 11 | [![Apache-2 licensed](https://img.shields.io/crates/l/rgb-std)](./LICENSE) 12 | 13 | RGB is confidential & scalable client-validated smart contracts for Bitcoin & 14 | Lightning. To learn more about RGB please check [RGB blueprint][Blueprint] and 15 | [RGB FAQ][FAQ] websites. 16 | 17 | RGB wallet standard library provides non-consensus-critical high-level API for 18 | RGB applications. It is based on [RGB Core Lib][Core], implementing validation 19 | and consensus rules for RGB smart contracts. 20 | 21 | The development of the project is supported and managed by [LNP/BP Standards 22 | Association][Association]. The design of RGB smart contract system and 23 | implementation of this and underlying consensus libraries was done in 2019-2022 24 | by [Dr Maxim Orlovsky][Max] basing or earlier ideas of client-side-validation 25 | and RGB as "assets for bitcoin and LN" by [Peter Todd][Todd] and 26 | [Giacomo Zucco][Zucco]. Upon the release of RGBv1 the protocol will be immutable 27 | and this library will accept only bugfixes; i.e. it will be ossified by 28 | requiring consensus ACK for the new changes across the large set of maintainers. 29 | 30 | The current list of the projects based on the library include: 31 | 32 | * [RGB Node][RGB Node]: standalone & embeddable node for running RGB. 33 | * [MyCitadel Node][MyCitadel Node]: wallet node providing RGB smart contract 34 | functionality integrated with Lightning network, bitcoin blockchain indexers, 35 | decentralized data storage and propagation (Storm) and wallet services. It can 36 | run as embedded, desktop, server or cloud-based node. 37 | 38 | ## Library 39 | 40 | The library can be integrated into other rust projects via `Cargo.toml` 41 | `[dependencies]` section: 42 | 43 | ```toml 44 | rgb-std = "0.11.0" 45 | ``` 46 | 47 | For serialization purposes library provides `serde` feature, which is turned off 48 | by default. 49 | 50 | ### MSRV 51 | 52 | Minimum supported rust compiler version (MSRV) is shown in `rust-version` of `Cargo.toml`. 53 | 54 | ## Contributing 55 | 56 | Altcoins and "blockchains" other than Bitcoin blockchain/Bitcoin protocols are 57 | not supported and not planned to be supported; pull requests targeting them will 58 | be declined. 59 | 60 | ## License 61 | 62 | See [LICENCE](LICENSE) file. 63 | 64 | 65 | [LNPBPs]: https://github.com/LNP-BP/LNPBPs 66 | 67 | [Association]: https://lnp-bp.org 68 | 69 | [Blueprint]: https://rgb.network 70 | 71 | [FAQ]: https://rgbfaq.com 72 | 73 | [Foundation]: https://github.com/LNP-BP/client_side_validation 74 | 75 | [BP]: https://github.com/BP-WG/bp-core 76 | 77 | [RGB Std]: https://github.com/RGB-WG/rgb-std 78 | 79 | [RGB Node]: https://github.com/RGB-WG/rgb-node 80 | 81 | [Max]: https://github.com/dr-orlovsky 82 | 83 | [Todd]: https://petertodd.org/ 84 | 85 | [Zucco]: https://giacomozucco.com/ 86 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | We take the security of our software products and services seriously, which 4 | includes all source code repositories managed through our GitHub organizations. 5 | 6 | If you believe you have found a security vulnerability in any of our repository 7 | that meets [definition of a security vulnerability][definition], please report 8 | it to us as described below. 9 | 10 | ## Reporting Security Issues 11 | 12 | **Please do not report security vulnerabilities through public GitHub issues.** 13 | 14 | Instead, please report them to the repository maintainers by sending a **GPG 15 | encrypted e-mail** to _all maintainers of a specific repo_ using their GPG keys. 16 | 17 | A list of repository maintainers and their keys and e-mail addresses are 18 | provided inside MAINTAINERS.md file and MANIFEST.yml, with the latter also 19 | included in the README.md as a manifest block, which looks in the following way: 20 | 21 | ```yaml 22 | Name: 23 | ... 24 | Maintained: 25 | Maintainers: 26 | : 27 | GPG: 28 | EMail: 29 | : 30 | ... 31 | ``` 32 | 33 | You should receive a response within 72 hours. If for some reason you do not, 34 | please follow up via email to ensure we received your original message. 35 | 36 | Please include the requested information listed below (as much as you can 37 | provide) to help us better understand the nature and scope of the possible 38 | issue: 39 | 40 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 41 | * Full paths of source file(s) related to the manifestation of the issue 42 | * The location of the affected source code (tag/branch/commit or direct URL) 43 | * Any special configuration required to reproduce the issue 44 | * Step-by-step instructions to reproduce the issue 45 | * Proof-of-concept or exploit code (if possible) 46 | * Impact of the issue, including how an attacker might exploit the issue 47 | 48 | This information will help us triage your report more quickly. 49 | 50 | ## Preferred Languages 51 | 52 | We prefer all communications to be in English. 53 | 54 | ## Policy 55 | 56 | We follow the principle of [Coordinated Vulnerability Disclosure][disclosure]. 57 | 58 | [definition]: https://aka.ms/opensource/security/definition 59 | [disclosure]: https://aka.ms/opensource/security/cvd 60 | -------------------------------------------------------------------------------- /_typos.toml: -------------------------------------------------------------------------------- 1 | [files] 2 | # exclude the directory "stl/" 3 | extend-exclude = ["stl/"] 4 | 5 | [default.extend-words] 6 | # Don't correct the name "Jon Atack". 7 | Atack = "Atack" 8 | 9 | [default] 10 | extend-ignore-re = [ 11 | # Don't correct URIs 12 | "([a-z]+:)*[$!0-9A-Za-z-]+", 13 | ] 14 | -------------------------------------------------------------------------------- /cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rgbx" 3 | description = "RGB command-line toolbox utility" 4 | version.workspace = true 5 | keywords.workspace = true 6 | categories.workspace = true 7 | authors.workspace = true 8 | homepage.workspace = true 9 | repository.workspace = true 10 | rust-version.workspace = true 11 | edition.workspace = true 12 | license.workspace = true 13 | readme = "../README.md" 14 | 15 | [dependencies] 16 | amplify.workspace = true 17 | strict_encoding.workspace = true 18 | hypersonic.workspace = true 19 | sonix.workspace = true 20 | bp-core.workspace = true 21 | rgb-std = { workspace = true, features = ["binfile", "serde"] } 22 | rgb-persist-fs.workspace = true 23 | binfile.workspace = true 24 | serde.workspace = true 25 | serde_yaml = "0.9.34" 26 | anyhow = "1.0.93" 27 | clap = { version = "4.5.21", features = ["derive", "env"] } 28 | 29 | [features] 30 | defaul = ["bitcoin"] 31 | all = ["bitcoin", "liquid", "prime"] 32 | 33 | bitcoin = ["rgb-std/bitcoin"] 34 | liquid = ["rgb-std/liquid"] 35 | prime = ["rgb-std/prime"] 36 | 37 | [lints.rust] 38 | unexpected_cfgs = { level = "allow", check-cfg = ['cfg(coverage_nightly)'] } 39 | -------------------------------------------------------------------------------- /cli/src/cmd.rs: -------------------------------------------------------------------------------- 1 | // RGB command-line toolbox utility 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Designed in 2019-2025 by Dr Maxim Orlovsky 6 | // Written in 2024-2025 by Dr Maxim Orlovsky 7 | // 8 | // Copyright (C) 2019-2024 LNP/BP Standards Association, Switzerland. 9 | // Copyright (C) 2024-2025 LNP/BP Laboratories, 10 | // Institute for Distributed and Cognitive Systems (InDCS), Switzerland. 11 | // Copyright (C) 2025 RGB Consortium, Switzerland. 12 | // Copyright (C) 2019-2025 Dr Maxim Orlovsky. 13 | // All rights under the above copyrights are reserved. 14 | // 15 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 16 | // in compliance with the License. You may obtain a copy of the License at 17 | // 18 | // http://www.apache.org/licenses/LICENSE-2.0 19 | // 20 | // Unless required by applicable law or agreed to in writing, software distributed under the License 21 | // is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 22 | // or implied. See the License for the specific language governing permissions and limitations under 23 | // the License. 24 | 25 | use std::path::PathBuf; 26 | 27 | use clap::ValueHint; 28 | 29 | #[derive(Parser)] 30 | pub struct Args { 31 | /// Command to execute 32 | #[clap(subcommand)] 33 | pub command: Cmd, 34 | } 35 | 36 | #[derive(Parser)] 37 | pub enum Cmd { 38 | /// Provide information about a given file (type, used ids etc) 39 | Info { 40 | /// File to inspect 41 | #[clap(value_hint = ValueHint::FilePath)] 42 | file: PathBuf, 43 | }, 44 | 45 | /// Inspect the provided binary file by converting it into YAML representation 46 | Inspect { 47 | /// File to inspect 48 | #[clap(value_hint = ValueHint::FilePath)] 49 | file: PathBuf, 50 | }, 51 | 52 | /// Dump complex data into multiple debug files 53 | /// 54 | /// Works for contract consignments and stockpiles 55 | Dump { 56 | /// Remove the destination directory if it already exists 57 | #[clap(short, long, global = true)] 58 | force: bool, 59 | 60 | /// Source data to process 61 | #[clap(value_hint = ValueHint::FilePath)] 62 | src: PathBuf, 63 | 64 | /// Destination directory to put dump files 65 | /// 66 | /// If skipped, adds the `dump` subdirectory to the `src` path. 67 | #[clap(value_hint = ValueHint::FilePath)] 68 | dst: Option, 69 | }, 70 | } 71 | -------------------------------------------------------------------------------- /cli/src/dump.rs: -------------------------------------------------------------------------------- 1 | // RGB command-line toolbox utility 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Designed in 2019-2025 by Dr Maxim Orlovsky 6 | // Written in 2024-2025 by Dr Maxim Orlovsky 7 | // 8 | // Copyright (C) 2019-2024 LNP/BP Standards Association, Switzerland. 9 | // Copyright (C) 2024-2025 LNP/BP Laboratories, 10 | // Institute for Distributed and Cognitive Systems (InDCS), Switzerland. 11 | // Copyright (C) 2025 RGB Consortium, Switzerland. 12 | // Copyright (C) 2019-2025 Dr Maxim Orlovsky. 13 | // All rights under the above copyrights are reserved. 14 | // 15 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 16 | // in compliance with the License. You may obtain a copy of the License at 17 | // 18 | // http://www.apache.org/licenses/LICENSE-2.0 19 | // 20 | // Unless required by applicable law or agreed to in writing, software distributed under the License 21 | // is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 22 | // or implied. See the License for the specific language governing permissions and limitations under 23 | // the License. 24 | 25 | use std::convert::Infallible; 26 | use std::fs; 27 | use std::fs::File; 28 | use std::path::Path; 29 | 30 | use amplify::confinement::{SmallBlob, SmallOrdMap, TinyVec}; 31 | use binfile::BinFile; 32 | use hypersonic::Operation; 33 | use rgb::{ 34 | parse_consignment, Articles, Contract, Issue, PublishedWitness, RgbSeal, RgbSealDef, 35 | SealWitness, Semantics, SigBlob, SingleUseSeal, CONSIGN_MAGIC_NUMBER, CONSIGN_VERSION, 36 | }; 37 | use rgb_persist_fs::{PileFs, StockFs}; 38 | use serde::{Deserialize, Serialize}; 39 | use sonix::{dump_articles, dump_ledger}; 40 | use strict_encoding::{StreamReader, StrictDecode, StrictEncode, StrictReader}; 41 | 42 | pub fn dump_stockpile( 43 | src: impl AsRef, 44 | dst: impl AsRef, 45 | force: bool, 46 | ) -> anyhow::Result<()> 47 | where 48 | Seal: RgbSeal + Serialize + for<'de> Deserialize<'de>, 49 | Seal::Definition: Serialize + for<'de> Deserialize<'de>, 50 | Seal::Client: Serialize + StrictEncode + StrictDecode, 51 | Seal::Published: Eq + Serialize + StrictEncode + StrictDecode, 52 | Seal::WitnessId: Ord + From<[u8; 32]> + Into<[u8; 32]> + Serialize, 53 | { 54 | let src = src.as_ref(); 55 | let dst = dst.as_ref(); 56 | dump_ledger(src, dst, force)?; 57 | 58 | print!("Reading contract pile from '{}' ... ", src.display()); 59 | let path = src.to_path_buf(); 60 | let contract = Contract::>::load(path.clone(), path)?; 61 | println!("success reading {}", contract.contract_id()); 62 | 63 | print!("Processing genesis seals ... "); 64 | let articles = contract.articles(); 65 | let genesis_opid = articles.genesis_opid(); 66 | let out = File::create_new(dst.join(format!("0000-seals-{genesis_opid}.yaml")))?; 67 | serde_yaml::to_writer( 68 | &out, 69 | &contract.op_seals(genesis_opid, articles.genesis().destructible_out.len_u16()), 70 | )?; 71 | println!("success"); 72 | 73 | print!("Processing operations ... none found"); 74 | for (no, (opid, _, rels)) in contract.operations().enumerate() { 75 | let out = File::create_new(dst.join(format!("{:04}-seals-{opid}.yaml", no + 1)))?; 76 | serde_yaml::to_writer(&out, &rels)?; 77 | print!("\rProcessing operations ... {} processed", no + 1); 78 | } 79 | println!(); 80 | 81 | print!("Processing state ... "); 82 | let out = File::create_new(dst.join("state.yaml"))?; 83 | serde_yaml::to_writer(&out, &contract.state())?; 84 | 85 | print!("Processing witnesses ... none found"); 86 | for (no, witness) in contract.witnesses().enumerate() { 87 | let out = File::create_new(dst.join(format!("witness-{}.yaml", witness.id)))?; 88 | serde_yaml::to_writer(&out, &witness)?; 89 | print!("\rProcessing witnesses ... {} processed", no + 1); 90 | } 91 | println!(); 92 | 93 | Ok(()) 94 | } 95 | 96 | pub fn dump_consignment( 97 | src: impl AsRef, 98 | dst: impl AsRef, 99 | force: bool, 100 | ) -> anyhow::Result<()> 101 | where 102 | SealDef: RgbSealDef + Serialize, 103 | SealDef::Src: Serialize, 104 | ::CliWitness: 105 | Serialize + for<'de> Deserialize<'de> + StrictEncode + StrictDecode, 106 | ::PubWitness: 107 | Eq + Serialize + for<'de> Deserialize<'de> + StrictEncode + StrictDecode, 108 | <::PubWitness as PublishedWitness>::PubId: 109 | Ord + From<[u8; 32]> + Into<[u8; 32]> + Serialize, 110 | { 111 | let src = src.as_ref(); 112 | let dst = dst.as_ref(); 113 | if force { 114 | let _ = fs::remove_dir_all(dst); 115 | } 116 | fs::create_dir_all(dst)?; 117 | 118 | let file = BinFile::::open(src)?; 119 | let mut stream = StrictReader::with(StreamReader::new::<{ usize::MAX }>(file)); 120 | 121 | let contract_id = parse_consignment(&mut stream).map_err(|e| anyhow!(e.to_string()))?; 122 | println!("Dumping consignment for {} into '{}'", contract_id, dst.display()); 123 | 124 | let mut seal_count = 0; 125 | let mut witness_count = 0; 126 | 127 | println!("Skipping extension blocks ... "); 128 | let _ = TinyVec::::strict_decode(&mut stream)?; 129 | 130 | print!("Processing contract articles ... "); 131 | 132 | let semantics = Semantics::strict_decode(&mut stream)?; 133 | let sig = Option::::strict_decode(&mut stream)?; 134 | let issue = Issue::strict_decode(&mut stream)?; 135 | let articles = 136 | Articles::with(semantics, issue, sig, |_, _, _| Result::<_, Infallible>::Ok(()))?; 137 | 138 | let genesis_opid = dump_articles(&articles, dst)?; 139 | let out = File::create_new(dst.join(format!("0000-seals-{genesis_opid}.yml")))?; 140 | let defined_seals = SmallOrdMap::::strict_decode(&mut stream) 141 | .expect("Failed to read the consignment stream"); 142 | serde_yaml::to_writer(&out, &defined_seals)?; 143 | seal_count += defined_seals.len(); 144 | 145 | let count = bool::strict_decode(&mut stream)?; 146 | if count { 147 | println!("error"); 148 | bail!( 149 | "Consignment stream has {count} witnesses for genesis, but zero witnesses are expected", 150 | ); 151 | } 152 | println!("success"); 153 | 154 | let count = u32::strict_decode(&mut stream)?; 155 | println!(); 156 | for i in 0..count { 157 | let op_count = i + 1; 158 | let operation = Operation::strict_decode(&mut stream)?; 159 | let opid = operation.opid(); 160 | 161 | let out = File::create_new(dst.join(format!("{op_count:04}-op-{opid}.yaml")))?; 162 | serde_yaml::to_writer(&out, &operation)?; 163 | 164 | let out = File::create_new(dst.join(format!("{op_count:04}-seals-{opid}.yml")))?; 165 | let defined_seals = SmallOrdMap::::strict_decode(&mut stream) 166 | .expect("Failed to read the consignment stream"); 167 | serde_yaml::to_writer(&out, &defined_seals)?; 168 | seal_count += defined_seals.len(); 169 | 170 | let witness = bool::strict_decode(&mut stream)?; 171 | if witness { 172 | let witness = SealWitness::::strict_decode(&mut stream)?; 173 | let out = File::create_new( 174 | dst.join(format!("{op_count:04}-witness-{}.yaml", witness.published.pub_id())), 175 | )?; 176 | serde_yaml::to_writer(&out, &witness)?; 177 | witness_count += 1; 178 | } 179 | 180 | print!( 181 | "\rParsing stream ... {op_count} operations, {seal_count} seals, {witness_count} \ 182 | witnesses processed", 183 | ); 184 | } 185 | println!(); 186 | Ok(()) 187 | } 188 | -------------------------------------------------------------------------------- /cli/src/exec.rs: -------------------------------------------------------------------------------- 1 | // RGB command-line toolbox utility 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Designed in 2019-2025 by Dr Maxim Orlovsky 6 | // Written in 2024-2025 by Dr Maxim Orlovsky 7 | // 8 | // Copyright (C) 2019-2024 LNP/BP Standards Association, Switzerland. 9 | // Copyright (C) 2024-2025 LNP/BP Laboratories, 10 | // Institute for Distributed and Cognitive Systems (InDCS), Switzerland. 11 | // Copyright (C) 2025 RGB Consortium, Switzerland. 12 | // Copyright (C) 2019-2025 Dr Maxim Orlovsky. 13 | // All rights under the above copyrights are reserved. 14 | // 15 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 16 | // in compliance with the License. You may obtain a copy of the License at 17 | // 18 | // http://www.apache.org/licenses/LICENSE-2.0 19 | // 20 | // Unless required by applicable law or agreed to in writing, software distributed under the License 21 | // is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 22 | // or implied. See the License for the specific language governing permissions and limitations under 23 | // the License. 24 | 25 | use std::convert::Infallible; 26 | use std::io::stdout; 27 | 28 | use bp::seals::{TxoSeal, WTxoSeal}; 29 | use rgb::popls::bp::PrefabBundle; 30 | use rgb::Issuer; 31 | 32 | use crate::cmd::{Args, Cmd}; 33 | use crate::dump::{dump_consignment, dump_stockpile}; 34 | 35 | impl Args { 36 | pub fn exec(&self) -> anyhow::Result<()> { 37 | match &self.command { 38 | Cmd::Info { file } => match file.extension() { 39 | Some(ext) if ext == "issuer" => { 40 | let issuer = Issuer::load(file, |_, _, _| Result::<_, Infallible>::Ok(()))?; 41 | eprintln!("File type: Issuer (contract schema)"); 42 | eprintln!("Issuer Id: {}", issuer.issuer_id()); 43 | eprintln!( 44 | "Signature: {}", 45 | if issuer.is_signed() { "present" } else { "absent" } 46 | ); 47 | } 48 | Some(_) => { 49 | return Err(anyhow!( 50 | "Unknown file type for '{}': the extension is not recognized", 51 | file.display() 52 | )) 53 | } 54 | None => { 55 | return Err(anyhow!( 56 | "The file '{}' has no extension; unable to detect the file type", 57 | file.display() 58 | )) 59 | } 60 | }, 61 | 62 | Cmd::Inspect { file } => match file.extension() { 63 | Some(ext) if ext == "pfab" => { 64 | let pfab = PrefabBundle::load(file)?; 65 | serde_yaml::to_writer(stdout(), &pfab)?; 66 | } 67 | Some(ext) if ext == "issuer" => { 68 | let issuer = Issuer::load(file, |_, _, _| Result::<_, Infallible>::Ok(()))?; 69 | serde_yaml::to_writer(stdout(), &issuer.codex())?; 70 | serde_yaml::to_writer(stdout(), &issuer.semantics())?; 71 | eprintln!("sig: {}", if issuer.is_signed() { "present" } else { "absent" }); 72 | } 73 | Some(_) => { 74 | return Err(anyhow!( 75 | "Unknown file type for '{}': the extension is not recognized", 76 | file.display() 77 | )) 78 | } 79 | None => { 80 | return Err(anyhow!( 81 | "The file '{}' has no extension; unable to detect the file type", 82 | file.display() 83 | )) 84 | } 85 | }, 86 | Cmd::Dump { force, src, dst } => match src.extension() { 87 | Some(ext) if ext == "rgb" => { 88 | let dst = dst 89 | .as_ref() 90 | .map(|p| p.to_owned()) 91 | .or_else(|| src.parent().map(|path| path.join("dump"))) 92 | .ok_or(anyhow!( 93 | "Can't detect a destination path for '{}'", 94 | src.display() 95 | ))?; 96 | dump_consignment::(src, dst, *force).inspect_err(|_| println!())?; 97 | } 98 | Some(ext) if ext == "contract" => { 99 | let dst = dst 100 | .as_ref() 101 | .map(|p| p.to_owned()) 102 | .unwrap_or_else(|| src.join("dump")); 103 | dump_stockpile::(src, dst, *force).inspect_err(|_| println!())?; 104 | } 105 | Some(_) => { 106 | return Err(anyhow!( 107 | "Can't detect the type for '{}': the extension is not recognized", 108 | src.display() 109 | )) 110 | } 111 | None => { 112 | return Err(anyhow!( 113 | "The path '{}' can't be recognized as known data", 114 | src.display() 115 | )) 116 | } 117 | }, 118 | } 119 | Ok(()) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /cli/src/main.rs: -------------------------------------------------------------------------------- 1 | // RGB command-line toolbox utility 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Designed in 2019-2025 by Dr Maxim Orlovsky 6 | // Written in 2024-2025 by Dr Maxim Orlovsky 7 | // 8 | // Copyright (C) 2019-2024 LNP/BP Standards Association, Switzerland. 9 | // Copyright (C) 2024-2025 LNP/BP Laboratories, 10 | // Institute for Distributed and Cognitive Systems (InDCS), Switzerland. 11 | // Copyright (C) 2025 RGB Consortium, Switzerland. 12 | // Copyright (C) 2019-2025 Dr Maxim Orlovsky. 13 | // All rights under the above copyrights are reserved. 14 | // 15 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 16 | // in compliance with the License. You may obtain a copy of the License at 17 | // 18 | // http://www.apache.org/licenses/LICENSE-2.0 19 | // 20 | // Unless required by applicable law or agreed to in writing, software distributed under the License 21 | // is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 22 | // or implied. See the License for the specific language governing permissions and limitations under 23 | // the License. 24 | 25 | #![cfg_attr(coverage_nightly, feature(coverage_attribute), coverage(off))] 26 | 27 | #[macro_use] 28 | extern crate clap; 29 | #[macro_use] 30 | extern crate anyhow; 31 | 32 | pub mod cmd; 33 | mod exec; 34 | mod dump; 35 | 36 | use clap::Parser; 37 | 38 | use crate::cmd::Args; 39 | 40 | fn main() -> anyhow::Result<()> { Args::parse().exec() } 41 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: no 3 | 4 | coverage: 5 | precision: 1 6 | round: nearest 7 | range: "0...95" 8 | status: 9 | project: 10 | default: 11 | target: 0% 12 | threshold: 1% 13 | branches: 14 | - master 15 | patch: 16 | default: 17 | target: 0% 18 | threshold: 1% 19 | only_pulls: true 20 | -------------------------------------------------------------------------------- /invoice/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rgb-invoice" 3 | description = "Invoice Library for RGB smart contracts" 4 | version.workspace = true 5 | keywords.workspace = true 6 | categories.workspace = true 7 | authors.workspace = true 8 | homepage.workspace = true 9 | repository.workspace = true 10 | rust-version.workspace = true 11 | edition.workspace = true 12 | license.workspace = true 13 | 14 | [dependencies] 15 | amplify.workspace = true 16 | commit_verify.workspace = true 17 | strict_encoding.workspace = true 18 | baid64.workspace = true 19 | hypersonic.workspace = true 20 | sonic-callreq.workspace = true 21 | bp-core = { workspace = true, optional = true } 22 | bp-invoice = { workspace = true, optional = true, features = ["strict_encoding"] } 23 | rgb-core.workspace = true 24 | serde = { workspace = true, optional = true } 25 | 26 | [features] 27 | default = ["std", "bitcoin"] 28 | all = ["std", "uri", "bitcoin", "liquid", "prime", "serde"] 29 | 30 | bitcoin = ["bp-core", "rgb-core/bitcoin", "bp-invoice"] 31 | liquid = ["bp-core", "rgb-core/liquid", "bp-invoice"] 32 | prime = ["rgb-core/prime"] 33 | std = ["amplify/std", "sonic-callreq/std"] 34 | uri = ["sonic-callreq/uri"] 35 | serde = ["dep:serde", "sonic-callreq/serde", "bp-core/serde", "rgb-core/serde", "bp-invoice/serde"] 36 | 37 | [package.metadata.docs.rs] 38 | features = ["all"] 39 | 40 | [lints.rust] 41 | unexpected_cfgs = { level = "allow", check-cfg = ['cfg(coverage_nightly)'] } 42 | -------------------------------------------------------------------------------- /invoice/src/bp.rs: -------------------------------------------------------------------------------- 1 | // Invoice Library for RGB smart contracts 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Designed in 2019-2025 by Dr Maxim Orlovsky 6 | // Written in 2024-2025 by Dr Maxim Orlovsky 7 | // 8 | // Copyright (C) 2019-2024 LNP/BP Standards Association, Switzerland. 9 | // Copyright (C) 2024-2025 LNP/BP Laboratories, 10 | // Institute for Distributed and Cognitive Systems (InDCS), Switzerland. 11 | // Copyright (C) 2025 RGB Consortium, Switzerland. 12 | // Copyright (C) 2019-2025 Dr Maxim Orlovsky. 13 | // All rights under the above copyrights are reserved. 14 | // 15 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 16 | // in compliance with the License. You may obtain a copy of the License at 17 | // 18 | // http://www.apache.org/licenses/LICENSE-2.0 19 | // 20 | // Unless required by applicable law or agreed to in writing, software distributed under the License 21 | // is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 22 | // or implied. See the License for the specific language governing permissions and limitations under 23 | // the License. 24 | 25 | use core::fmt::{self, Display, Formatter}; 26 | use core::str::FromStr; 27 | 28 | use amplify::confinement::{self, TinyBlob}; 29 | use amplify::Bytes; 30 | use baid64::base64::alphabet::Alphabet; 31 | use baid64::base64::engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig}; 32 | use baid64::base64::{DecodeError, Engine}; 33 | use baid64::BAID64_ALPHABET; 34 | use bp::seals::Noise; 35 | use bp::ScriptPubkey; 36 | use commit_verify::{Digest, DigestExt, ReservedBytes, Sha256}; 37 | pub use invoice::*; 38 | use strict_encoding::{DeserializeError, StrictDeserialize, StrictSerialize}; 39 | 40 | pub const WITNESS_OUT_HRI: &str = "wout"; 41 | 42 | #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] 43 | #[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)] 44 | // Strict type here is used only for Display serialization, so we do not include the type into any 45 | // library 46 | #[strict_type(lib = "_")] 47 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))] 48 | pub struct WitnessOut { 49 | #[strict_type(skip)] 50 | #[cfg_attr(feature = "serde", serde(skip))] 51 | reserved: ReservedBytes<1>, 52 | salt: u64, 53 | address: AddressPayload, 54 | } 55 | impl StrictSerialize for WitnessOut {} 56 | impl StrictDeserialize for WitnessOut {} 57 | 58 | impl From for ScriptPubkey { 59 | fn from(val: WitnessOut) -> Self { val.script_pubkey() } 60 | } 61 | 62 | impl WitnessOut { 63 | pub fn new(address: impl Into, salt: u64) -> Self { 64 | WitnessOut { reserved: default!(), salt, address: address.into() } 65 | } 66 | 67 | pub fn noise(&self) -> Noise { 68 | let mut noise_engine = Sha256::new(); 69 | noise_engine.input_raw(&self.salt.to_le_bytes()); 70 | noise_engine.input_raw(self.script_pubkey().as_ref()); 71 | let mut noise = [0xFFu8; 40]; 72 | noise[..32].copy_from_slice(&noise_engine.finish()); 73 | Bytes::from(noise).into() 74 | } 75 | 76 | pub fn script_pubkey(&self) -> ScriptPubkey { self.address.script_pubkey() } 77 | 78 | pub fn checksum(&self) -> [u8; 4] { 79 | let key = Sha256::digest(WITNESS_OUT_HRI.as_bytes()); 80 | let mut sha = Sha256::new_with_prefix(key); 81 | sha.update([0]); 82 | sha.update(self.salt.to_le_bytes()); 83 | sha.update(self.script_pubkey().as_slice()); 84 | let sha = sha.finalize(); 85 | [sha[0], sha[1], sha[1], sha[2]] 86 | } 87 | } 88 | 89 | impl Display for WitnessOut { 90 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 91 | f.write_str(WITNESS_OUT_HRI)?; 92 | f.write_str(":")?; 93 | 94 | let mut data = self 95 | .to_strict_serialized::<{ u8::MAX as usize }>() 96 | .expect("script pubkey length in WitnessOut should be controlled during creation") 97 | .release(); 98 | data.extend(self.checksum()); 99 | 100 | let alphabet = Alphabet::new(BAID64_ALPHABET).expect("invalid Baid64 alphabet"); 101 | let engine = 102 | GeneralPurpose::new(&alphabet, GeneralPurposeConfig::new().with_encode_padding(false)); 103 | let encoded = engine.encode(data).chars().collect::>(); 104 | 105 | let mut iter = encoded.chunks(8).peekable(); 106 | while let Some(chunk) = iter.next() { 107 | f.write_str(&chunk.iter().collect::())?; 108 | if iter.by_ref().peek().is_some() { 109 | f.write_str("-")?; 110 | } 111 | } 112 | 113 | Ok(()) 114 | } 115 | } 116 | 117 | impl FromStr for WitnessOut { 118 | type Err = ParseWitnessOutError; 119 | 120 | fn from_str(s: &str) -> Result { 121 | let s = s 122 | .strip_prefix(WITNESS_OUT_HRI) 123 | .and_then(|s| s.strip_prefix(':')) 124 | .ok_or(ParseWitnessOutError::NoPrefix)? 125 | .replace('-', ""); 126 | 127 | let alphabet = Alphabet::new(BAID64_ALPHABET).expect("invalid Baid64 alphabet"); 128 | let engine = GeneralPurpose::new( 129 | &alphabet, 130 | GeneralPurposeConfig::new().with_decode_padding_mode(DecodePaddingMode::RequireNone), 131 | ); 132 | let decoded = engine.decode(s.as_bytes())?; 133 | 134 | let (data, checksum) = decoded 135 | .split_last_chunk::<4>() 136 | .ok_or(ParseWitnessOutError::NoChecksum)?; 137 | 138 | let data = TinyBlob::try_from_slice(data)?; 139 | let wout = WitnessOut::from_strict_serialized::<{ u8::MAX as usize }>(data)?; 140 | 141 | if *checksum != wout.checksum() { 142 | return Err(ParseWitnessOutError::InvalidChecksum); 143 | } 144 | 145 | Ok(wout) 146 | } 147 | } 148 | 149 | #[derive(Clone, PartialEq, Eq, Debug, Display, Error, From)] 150 | #[display(doc_comments)] 151 | pub enum ParseWitnessOutError { 152 | /// witness output seal definition doesn't start with a necessary prefix `wout:`. 153 | NoPrefix, 154 | 155 | /// the provided witness output seal definition doesn't contain checksum. 156 | NoChecksum, 157 | 158 | /// checksum of the provided witness output seal definition is invalid. 159 | InvalidChecksum, 160 | 161 | /// invalid Base64 encoding in witness output seal definition - {0}. 162 | #[from] 163 | Base64(DecodeError), 164 | 165 | /// the length of encoded witness output seal definition string exceeds 255 chars. 166 | #[from(confinement::Error)] 167 | TooLong, 168 | 169 | /// invalid witness output seal definition binary data - {0}. 170 | #[from] 171 | Encoding(DeserializeError), 172 | } 173 | 174 | #[cfg(test)] 175 | mod tests { 176 | #![cfg_attr(coverage_nightly, coverage(off))] 177 | 178 | use bp::OutputPk; 179 | use strict_encoding::StrictDumb; 180 | 181 | use super::*; 182 | 183 | #[test] 184 | fn display_from_str() { 185 | let wout = WitnessOut::new(AddressPayload::Tr(OutputPk::strict_dumb()), 0xdeadbeaf1badcafe); 186 | let s = wout.to_string(); 187 | assert_eq!(s, "wout:~sqtG6__-rd4gAQEB-AQEBAQEB-AQEBAQEB-AQEBAQEB-AQEBAQEB-AQEBAQFM-DAx2"); 188 | let wout2 = WitnessOut::from_str(&s).unwrap(); 189 | assert_eq!(wout, wout2); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /invoice/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Invoice Library for RGB smart contracts 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Designed in 2019-2025 by Dr Maxim Orlovsky 6 | // Written in 2024-2025 by Dr Maxim Orlovsky 7 | // 8 | // Copyright (C) 2019-2024 LNP/BP Standards Association, Switzerland. 9 | // Copyright (C) 2024-2025 LNP/BP Laboratories, 10 | // Institute for Distributed and Cognitive Systems (InDCS), Switzerland. 11 | // Copyright (C) 2025 RGB Consortium, Switzerland. 12 | // Copyright (C) 2019-2025 Dr Maxim Orlovsky. 13 | // All rights under the above copyrights are reserved. 14 | // 15 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 16 | // in compliance with the License. You may obtain a copy of the License at 17 | // 18 | // http://www.apache.org/licenses/LICENSE-2.0 19 | // 20 | // Unless required by applicable law or agreed to in writing, software distributed under the License 21 | // is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 22 | // or implied. See the License for the specific language governing permissions and limitations under 23 | // the License. 24 | 25 | // TODO: Activate once StrictEncoding will be no_std 26 | // #![cfg_attr(not(feature = "std"), no_std)] 27 | #![deny( 28 | unsafe_code, 29 | dead_code, 30 | // TODO: Complete documentation 31 | // missing_docs, 32 | unused_variables, 33 | unused_mut, 34 | unused_imports, 35 | non_upper_case_globals, 36 | non_camel_case_types, 37 | non_snake_case 38 | )] 39 | #![cfg_attr(coverage_nightly, feature(coverage_attribute))] 40 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 41 | 42 | #[macro_use] 43 | extern crate amplify; 44 | #[cfg(any(feature = "bitcoin", feature = "liquid"))] 45 | #[macro_use] 46 | extern crate strict_encoding; 47 | #[cfg(feature = "serde")] 48 | #[macro_use] 49 | extern crate serde; 50 | 51 | #[cfg(any(feature = "bitcoin", feature = "liquid"))] 52 | pub mod bp; 53 | 54 | use core::fmt::{self, Display, Formatter}; 55 | use core::str::FromStr; 56 | 57 | use baid64::Baid64ParseError; 58 | use hypersonic::{AuthToken, Consensus}; 59 | use sonic_callreq::{CallRequest, CallScope}; 60 | 61 | pub type RgbInvoice> = CallRequest; 62 | 63 | #[derive(Clone, Eq, PartialEq, Debug, Display)] 64 | #[display(inner)] 65 | #[cfg_attr( 66 | feature = "serde", 67 | derive(Serialize, Deserialize), 68 | serde(rename_all = "camelCase", untagged) 69 | )] 70 | pub enum RgbBeneficiary { 71 | Token(AuthToken), 72 | 73 | #[cfg(any(feature = "bitcoin", feature = "liquid"))] 74 | WitnessOut(bp::WitnessOut), 75 | } 76 | 77 | impl FromStr for RgbBeneficiary { 78 | type Err = ParseInvoiceError; 79 | 80 | fn from_str(s: &str) -> Result { 81 | match AuthToken::from_str(s) { 82 | Ok(auth) => Ok(Self::Token(auth)), 83 | 84 | #[cfg(any(feature = "bitcoin", feature = "liquid"))] 85 | Err(_) => { 86 | let wout = bp::WitnessOut::from_str(s)?; 87 | Ok(Self::WitnessOut(wout)) 88 | } 89 | #[cfg(not(any(feature = "bitcoin", feature = "liquid")))] 90 | Err(err) => Err(err), 91 | } 92 | } 93 | } 94 | 95 | #[cfg(any(feature = "bitcoin", feature = "liquid"))] 96 | impl RgbBeneficiary { 97 | pub fn witness_out(&self) -> Option<&bp::WitnessOut> { 98 | match self { 99 | Self::WitnessOut(wout) => Some(wout), 100 | _ => None, 101 | } 102 | } 103 | } 104 | 105 | #[derive(Clone, Eq, PartialEq, Debug)] 106 | pub struct ContractQuery { 107 | pub consensus: Consensus, 108 | pub testnet: bool, 109 | } 110 | 111 | impl Display for ContractQuery { 112 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 113 | if self.testnet { 114 | f.write_str("testnet@")?; 115 | } 116 | Display::fmt(&self.consensus, f) 117 | } 118 | } 119 | 120 | impl FromStr for ContractQuery { 121 | type Err = String; 122 | 123 | fn from_str(s: &str) -> Result { 124 | let testnet = s.starts_with("testnet@"); 125 | let s = s.trim_start_matches("testnet@"); 126 | Consensus::from_str(s).map(|consensus| Self { consensus, testnet }) 127 | } 128 | } 129 | 130 | #[derive(Debug, Display, Error, From)] 131 | #[display(doc_comments)] 132 | pub enum ParseInvoiceError { 133 | /// RGB invoice misses URI scheme prefix `contract:`. 134 | NoScheme, 135 | 136 | /// RGB invoice contains unrecognizable URI authority, which is neither contract id nor a 137 | /// contract query. 138 | Unrecognizable(Baid64ParseError), 139 | 140 | #[cfg(any(feature = "bitcoin", feature = "liquid"))] 141 | #[from] 142 | Bp(bp::ParseWitnessOutError), 143 | } 144 | -------------------------------------------------------------------------------- /persistence/fs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rgb-persist-fs" 3 | version.workspace = true 4 | authors.workspace = true 5 | description = "Filesystem persistence for RGB smart contracts" 6 | repository.workspace = true 7 | homepage.workspace = true 8 | keywords.workspace = true 9 | categories = ["algorithms", "cryptography", "science", "no-std"] 10 | readme.workspace = true 11 | license.workspace = true 12 | edition.workspace = true 13 | rust-version.workspace = true 14 | exclude = [".github"] 15 | 16 | [dependencies] 17 | amplify.workspace = true 18 | strict_encoding.workspace = true 19 | sonic-persist-fs.workspace = true 20 | rgb-std = { workspace = true, features = ["binfile"] } 21 | aora.workspace = true 22 | 23 | [features] 24 | 25 | [lints.rust] 26 | unexpected_cfgs = { level = "allow", check-cfg = ['cfg(coverage_nightly)'] } 27 | -------------------------------------------------------------------------------- /persistence/fs/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Standard Library for RGB smart contracts 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Designed in 2019-2025 by Dr Maxim Orlovsky 6 | // Written in 2024-2025 by Dr Maxim Orlovsky 7 | // 8 | // Copyright (C) 2019-2024 LNP/BP Standards Association, Switzerland. 9 | // Copyright (C) 2024-2025 LNP/BP Laboratories, 10 | // Institute for Distributed and Cognitive Systems (InDCS), Switzerland. 11 | // Copyright (C) 2025 RGB Consortium, Switzerland. 12 | // Copyright (C) 2019-2025 Dr Maxim Orlovsky. 13 | // All rights under the above copyrights are reserved. 14 | // 15 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 16 | // in compliance with the License. You may obtain a copy of the License at 17 | // 18 | // http://www.apache.org/licenses/LICENSE-2.0 19 | // 20 | // Unless required by applicable law or agreed to in writing, software distributed under the License 21 | // is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 22 | // or implied. See the License for the specific language governing permissions and limitations under 23 | // the License. 24 | 25 | mod pile; 26 | mod stockpile; 27 | 28 | pub use pile::PileFs; 29 | pub use sonic_persist_fs::*; 30 | pub use stockpile::StockpileDir; 31 | -------------------------------------------------------------------------------- /persistence/fs/src/pile.rs: -------------------------------------------------------------------------------- 1 | // Standard Library for RGB smart contracts 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Designed in 2019-2025 by Dr Maxim Orlovsky 6 | // Written in 2024-2025 by Dr Maxim Orlovsky 7 | // 8 | // Copyright (C) 2019-2024 LNP/BP Standards Association, Switzerland. 9 | // Copyright (C) 2024-2025 LNP/BP Laboratories, 10 | // Institute for Distributed and Cognitive Systems (InDCS), Switzerland. 11 | // Copyright (C) 2025 RGB Consortium, Switzerland. 12 | // Copyright (C) 2019-2025 Dr Maxim Orlovsky. 13 | // All rights under the above copyrights are reserved. 14 | // 15 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 16 | // in compliance with the License. You may obtain a copy of the License at 17 | // 18 | // http://www.apache.org/licenses/LICENSE-2.0 19 | // 20 | // Unless required by applicable law or agreed to in writing, software distributed under the License 21 | // is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 22 | // or implied. See the License for the specific language governing permissions and limitations under 23 | // the License. 24 | 25 | use std::io; 26 | use std::marker::PhantomData; 27 | use std::path::PathBuf; 28 | 29 | use amplify::confinement::SmallOrdMap; 30 | use aora::file::{FileAoraIndex, FileAoraMap, FileAuraMap}; 31 | use aora::{AoraIndex, AoraMap, AuraMap, TransactionalMap}; 32 | use rgb::{CellAddr, OpRels, Opid, Pile, RgbSeal, Witness, WitnessStatus}; 33 | use strict_encoding::{StrictDecode, StrictEncode}; 34 | 35 | const HOARD_MAGIC: u64 = u64::from_be_bytes(*b"RGBHOARD"); 36 | const CACHE_MAGIC: u64 = u64::from_be_bytes(*b"RGBCACHE"); 37 | const KEEP_MAGIC: u64 = u64::from_be_bytes(*b"RGBKEEPS"); 38 | const INDEX_MAGIC: u64 = u64::from_be_bytes(*b"RGBINDEX"); 39 | const STAND_MAGIC: u64 = u64::from_be_bytes(*b"RGBSTAND"); 40 | const MINE_MAGIC: u64 = u64::from_be_bytes(*b"RGBMINES"); 41 | 42 | #[derive(Debug)] 43 | pub struct PileFs 44 | where Seal::WitnessId: From<[u8; 32]> + Into<[u8; 32]> 45 | { 46 | hoard: FileAoraMap, 47 | cache: FileAoraMap, 48 | keep: FileAoraMap, 49 | index: FileAoraIndex, 50 | stand: FileAoraIndex, 51 | mine: FileAuraMap, 52 | _phantom: PhantomData, 53 | } 54 | 55 | impl Pile for PileFs 56 | where 57 | Seal::Client: StrictEncode + StrictDecode, 58 | Seal::Published: Eq + StrictEncode + StrictDecode, 59 | Seal::WitnessId: From<[u8; 32]> + Into<[u8; 32]>, 60 | { 61 | type Seal = Seal; 62 | type Conf = PathBuf; 63 | type Error = io::Error; 64 | 65 | fn new(path: Self::Conf) -> Result 66 | where Self: Sized { 67 | let hoard = FileAoraMap::create_new(&path, "hoard")?; 68 | let cache = FileAoraMap::create_new(&path, "cache")?; 69 | let keep = FileAoraMap::create_new(&path, "keep")?; 70 | 71 | let index = FileAoraIndex::create_new(&path, "index.dat")?; 72 | let stand = FileAoraIndex::create_new(&path, "stand.dat")?; 73 | let mine = FileAuraMap::create_new(&path, "mine.dat")?; 74 | 75 | Ok(Self { 76 | hoard, 77 | cache, 78 | keep, 79 | index, 80 | stand, 81 | mine, 82 | _phantom: PhantomData, 83 | }) 84 | } 85 | 86 | fn load(path: Self::Conf) -> Result 87 | where Self: Sized { 88 | let hoard = FileAoraMap::open(&path, "hoard")?; 89 | let cache = FileAoraMap::open(&path, "cache")?; 90 | let keep = FileAoraMap::open(&path, "keep")?; 91 | 92 | let index = FileAoraIndex::open(&path, "index.dat")?; 93 | let stand = FileAoraIndex::open(&path, "stand.dat")?; 94 | let mine = FileAuraMap::open(&path, "mine.dat")?; 95 | 96 | Ok(Self { 97 | hoard, 98 | cache, 99 | keep, 100 | index, 101 | stand, 102 | mine, 103 | _phantom: PhantomData, 104 | }) 105 | } 106 | 107 | fn has_witness(&self, wid: Seal::WitnessId) -> bool { self.hoard.contains_key(wid) } 108 | 109 | fn pub_witness(&self, wid: Seal::WitnessId) -> Seal::Published { self.cache.get_expect(wid) } 110 | 111 | fn cli_witness(&self, wid: Seal::WitnessId) -> Seal::Client { self.hoard.get_expect(wid) } 112 | 113 | fn witness_status(&self, wid: Seal::WitnessId) -> WitnessStatus { self.mine.get_expect(wid) } 114 | 115 | fn witness_ids(&self) -> impl Iterator::WitnessId> { 116 | self.stand.keys() 117 | } 118 | 119 | fn op_witness_ids(&self, opid: Opid) -> impl ExactSizeIterator { 120 | self.index.get(opid) 121 | } 122 | 123 | fn ops_by_witness_id(&self, wid: Seal::WitnessId) -> impl ExactSizeIterator { 124 | self.stand.get(wid) 125 | } 126 | 127 | fn seal(&self, addr: CellAddr) -> Option { self.keep.get(addr) } 128 | 129 | fn seals( 130 | &self, 131 | opid: Opid, 132 | up_to: u16, 133 | ) -> SmallOrdMap::Definition> { 134 | let mut seals = SmallOrdMap::new(); 135 | for no in 0..up_to { 136 | let addr = CellAddr::new(opid, no); 137 | if let Some(seal) = self.keep.get(addr) { 138 | let _ = seals.insert(no, seal); 139 | } 140 | } 141 | seals 142 | } 143 | 144 | fn add_witness( 145 | &mut self, 146 | opid: Opid, 147 | wid: ::WitnessId, 148 | published: &::Published, 149 | anchor: &::Client, 150 | status: WitnessStatus, 151 | ) { 152 | self.index.push(opid, wid); 153 | self.stand.push(wid, opid); 154 | self.hoard.insert(wid, anchor); 155 | self.cache.insert(wid, published); 156 | if !self.mine.contains_key(wid) { 157 | self.mine.insert_only(wid, status); 158 | } 159 | } 160 | 161 | fn add_seals( 162 | &mut self, 163 | opid: Opid, 164 | seals: SmallOrdMap::Definition>, 165 | ) { 166 | for (no, seal) in seals { 167 | self.keep.insert(CellAddr::new(opid, no), &seal) 168 | } 169 | } 170 | 171 | fn update_witness_status( 172 | &mut self, 173 | wid: ::WitnessId, 174 | status: WitnessStatus, 175 | ) { 176 | self.mine.update_only(wid, status); 177 | } 178 | 179 | fn commit_transaction(&mut self) { self.mine.commit_transaction(); } 180 | 181 | fn witnesses(&self) -> impl Iterator> { 182 | self.hoard.iter().map(|(wid, client)| { 183 | let published = self.cache.get_expect(wid); 184 | let status = self.mine.get_expect(wid); 185 | let opids = self.stand.get(wid).collect(); 186 | Witness { id: wid, published, client, status, opids } 187 | }) 188 | } 189 | 190 | fn op_relations(&self, opid: Opid, up_to: u16) -> OpRels { 191 | let seals = self.seals(opid, up_to); 192 | let witness_ids = self.index.get(opid).collect(); 193 | OpRels { opid, witness_ids, defines: seals, _phantom: PhantomData } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /persistence/fs/src/stockpile.rs: -------------------------------------------------------------------------------- 1 | // Standard Library for RGB smart contracts 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Designed in 2019-2025 by Dr Maxim Orlovsky 6 | // Written in 2024-2025 by Dr Maxim Orlovsky 7 | // 8 | // Copyright (C) 2019-2024 LNP/BP Standards Association, Switzerland. 9 | // Copyright (C) 2024-2025 LNP/BP Laboratories, 10 | // Institute for Distributed and Cognitive Systems (InDCS), Switzerland. 11 | // Copyright (C) 2025 RGB Consortium, Switzerland. 12 | // Copyright (C) 2019-2025 Dr Maxim Orlovsky. 13 | // All rights under the above copyrights are reserved. 14 | // 15 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 16 | // in compliance with the License. You may obtain a copy of the License at 17 | // 18 | // http://www.apache.org/licenses/LICENSE-2.0 19 | // 20 | // Unless required by applicable law or agreed to in writing, software distributed under the License 21 | // is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 22 | // or implied. See the License for the specific language governing permissions and limitations under 23 | // the License. 24 | 25 | use std::collections::HashMap; 26 | use std::convert::Infallible; 27 | use std::ffi::OsStr; 28 | use std::marker::PhantomData; 29 | use std::path::{Path, PathBuf}; 30 | use std::str::FromStr; 31 | use std::{fs, io}; 32 | 33 | use amplify::MultiError; 34 | use rgb::{ 35 | Articles, CodexId, Consensus, Consignment, ConsumeError, Contract, ContractId, CreateParams, 36 | Issuer, IssuerError, Pile, RgbSeal, Stock, Stockpile, 37 | }; 38 | use sonic_persist_fs::{FsError, StockFs}; 39 | use strict_encoding::{StrictDecode, StrictEncode}; 40 | 41 | use crate::PileFs; 42 | 43 | #[derive(Clone, PartialEq, Eq, Debug)] 44 | pub struct StockpileDir { 45 | consensus: Consensus, 46 | testnet: bool, 47 | dir: PathBuf, 48 | issuers: HashMap, 49 | contracts: HashMap, 50 | _phantom: PhantomData, 51 | } 52 | 53 | impl StockpileDir { 54 | pub fn load(dir: PathBuf, consensus: Consensus, testnet: bool) -> Result { 55 | let mut issuers = HashMap::new(); 56 | let mut contracts = HashMap::new(); 57 | 58 | let readdir = fs::read_dir(&dir)?; 59 | for entry in readdir { 60 | let entry = entry?; 61 | let path = entry.path(); 62 | let ty = entry.file_type()?; 63 | let Some(extension) = path.extension().and_then(OsStr::to_str) else { 64 | continue; 65 | }; 66 | let Some(name) = path.file_stem().and_then(OsStr::to_str) else { 67 | continue; 68 | }; 69 | let Some((name, id_str)) = name.split_once('.') else { 70 | continue; 71 | }; 72 | if ty.is_file() && extension == "issuer" { 73 | let Ok(id) = CodexId::from_str(id_str) else { 74 | continue; 75 | }; 76 | issuers.insert(id, name.to_string()); 77 | } else if ty.is_dir() && extension == "contract" { 78 | let Ok(id) = ContractId::from_str(id_str) else { 79 | continue; 80 | }; 81 | contracts.insert(id, name.to_string()); 82 | } 83 | } 84 | 85 | Ok(Self { 86 | consensus, 87 | testnet, 88 | dir, 89 | issuers, 90 | contracts, 91 | _phantom: PhantomData, 92 | }) 93 | } 94 | 95 | pub fn dir(&self) -> &Path { self.dir.as_path() } 96 | 97 | fn get_contract_dir(&self, contract_id: ContractId) -> Option { 98 | let subdir = self.contracts.get(&contract_id)?; 99 | let path = self.dir.join(format!("{subdir}.{contract_id:-}.contract")); 100 | Some(path) 101 | } 102 | 103 | fn create_contract_dir(&self, articles: &Articles) -> io::Result { 104 | let dir = self.dir.join(format!( 105 | "{}.{:-}.contract", 106 | articles.issue().meta.name, 107 | articles.contract_id() 108 | )); 109 | 110 | if fs::exists(&dir)? { 111 | return Err(io::Error::new(io::ErrorKind::AlreadyExists, "Contract already exists")); 112 | } 113 | fs::create_dir_all(&dir)?; 114 | 115 | Ok(dir) 116 | } 117 | } 118 | 119 | impl Stockpile for StockpileDir 120 | where 121 | Seal::Client: StrictEncode + StrictDecode, 122 | Seal::Published: Eq + StrictEncode + StrictDecode, 123 | Seal::WitnessId: From<[u8; 32]> + Into<[u8; 32]>, 124 | { 125 | type Stock = StockFs; 126 | type Pile = PileFs; 127 | type Error = io::Error; 128 | 129 | fn consensus(&self) -> Consensus { self.consensus } 130 | 131 | fn is_testnet(&self) -> bool { self.testnet } 132 | 133 | fn issuers_count(&self) -> usize { self.issuers.len() } 134 | 135 | fn contracts_count(&self) -> usize { self.contracts.len() } 136 | 137 | fn has_issuer(&self, codex_id: CodexId) -> bool { self.issuers.contains_key(&codex_id) } 138 | 139 | fn has_contract(&self, contract_id: ContractId) -> bool { 140 | self.contracts.contains_key(&contract_id) 141 | } 142 | 143 | fn codex_ids(&self) -> impl Iterator { self.issuers.keys().copied() } 144 | 145 | fn contract_ids(&self) -> impl Iterator { self.contracts.keys().copied() } 146 | 147 | fn issuer(&self, codex_id: CodexId) -> Option { 148 | let name = self.issuers.get(&codex_id)?; 149 | let path = self.dir.join(format!("{name}.{codex_id:#}.issuer")); 150 | // We trust the storage 151 | Issuer::load(path, |_, _, _| -> Result<_, Infallible> { Ok(()) }).ok() 152 | } 153 | 154 | fn contract(&self, contract_id: ContractId) -> Option> { 155 | let path = self.get_contract_dir(contract_id)?; 156 | let contract = Contract::load(path.clone(), path).ok()?; 157 | let meta = &contract.articles().issue().meta; 158 | if meta.consensus != self.consensus || meta.testnet != self.testnet { 159 | return None; 160 | } 161 | Some(contract) 162 | } 163 | 164 | fn import_issuer(&mut self, issuer: Issuer) -> Result { 165 | let codex_id = issuer.codex_id(); 166 | let name = issuer.codex().name.to_string(); 167 | let path = self.dir.join(format!("{name}.{codex_id:#}.issuer")); 168 | issuer.save(path)?; 169 | self.issuers.insert(codex_id, name); 170 | Ok(issuer) 171 | } 172 | 173 | fn import_contract( 174 | &mut self, 175 | articles: Articles, 176 | consignment: Consignment, 177 | ) -> Result< 178 | Contract, 179 | MultiError< 180 | ConsumeError, 181 | ::Error, 182 | ::Error, 183 | >, 184 | > 185 | where 186 | Seal::Client: StrictDecode, 187 | Seal::Published: StrictDecode, 188 | Seal::WitnessId: StrictDecode, 189 | { 190 | let dir = self.create_contract_dir(&articles).map_err(MultiError::C)?; 191 | let contract = Contract::with(articles, consignment, dir)?; 192 | self.contracts 193 | .insert(contract.contract_id(), contract.articles().issue().meta.name.to_string()); 194 | Ok(contract) 195 | } 196 | 197 | fn issue( 198 | &mut self, 199 | params: CreateParams<<::Seal as RgbSeal>::Definition>, 200 | ) -> Result, MultiError> 201 | { 202 | let schema = self 203 | .issuer(params.issuer.codex_id()) 204 | .ok_or(MultiError::A(IssuerError::UnknownCodex(params.issuer.codex_id())))?; 205 | let contract = 206 | Contract::issue(schema, params, |articles| Ok(self.create_contract_dir(articles)?)) 207 | .map_err(MultiError::from_other_a)?; 208 | self.contracts 209 | .insert(contract.contract_id(), contract.articles().issue().meta.name.to_string()); 210 | Ok(contract) 211 | } 212 | 213 | fn purge(&mut self, contract_id: ContractId) -> Result<(), Self::Error> { 214 | let path = self 215 | .get_contract_dir(contract_id) 216 | .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Contract not found"))?; 217 | fs::remove_dir_all(&path) 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | -------------------------------------------------------------------------------- /scripts/typelib.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cargo run --features stl --package rgb-std --bin rgb-stl -- --stl 4 | cargo run --features stl --package rgb-std --bin rgb-stl -- --sty 5 | cargo run --features stl --package rgb-std --bin rgb-stl -- --sta 6 | -------------------------------------------------------------------------------- /src/bin/rgb-stl.rs: -------------------------------------------------------------------------------- 1 | // Standard Library for RGB smart contracts 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Designed in 2019-2025 by Dr Maxim Orlovsky 6 | // Written in 2024-2025 by Dr Maxim Orlovsky 7 | // 8 | // Copyright (C) 2019-2024 LNP/BP Standards Association, Switzerland. 9 | // Copyright (C) 2024-2025 LNP/BP Laboratories, 10 | // Institute for Distributed and Cognitive Systems (InDCS), Switzerland. 11 | // Copyright (C) 2025 RGB Consortium, Switzerland. 12 | // Copyright (C) 2019-2025 Dr Maxim Orlovsky. 13 | // All rights under the above copyrights are reserved. 14 | // 15 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 16 | // in compliance with the License. You may obtain a copy of the License at 17 | // 18 | // http://www.apache.org/licenses/LICENSE-2.0 19 | // 20 | // Unless required by applicable law or agreed to in writing, software distributed under the License 21 | // is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 22 | // or implied. See the License for the specific language governing permissions and limitations under 23 | // the License. 24 | 25 | #![cfg_attr(coverage_nightly, feature(coverage_attribute), coverage(off))] 26 | 27 | use rgb::stl::{rgb_seals, rgb_stl}; 28 | use strict_types::parse_args; 29 | 30 | fn main() { 31 | let (format, dir) = parse_args(); 32 | 33 | rgb_stl() 34 | .serialize( 35 | format, 36 | dir.as_ref(), 37 | "0.12.0", 38 | Some( 39 | " 40 | Description: RGB smart contracts library 41 | Author: Dr Maxim Orlovsky 42 | Copyright (C) 2024-2025 LNP/BP Labs, Institute for Distributed and Cognitive Systems, \ 43 | Switzerland. 44 | All rights reserved. 45 | License: Apache-2.0", 46 | ), 47 | ) 48 | .expect("unable to write to the file"); 49 | 50 | rgb_seals() 51 | .serialize( 52 | format, 53 | dir.as_ref(), 54 | "0.12.0", 55 | Some( 56 | " 57 | Description: RGB smart contracts library 58 | Author: Dr Maxim Orlovsky 59 | Copyright (C) 2024-2025 LNP/BP Labs, Institute for Distributed and Cognitive Systems, \ 60 | Switzerland. 61 | All rights reserved. 62 | License: Apache-2.0", 63 | ), 64 | ) 65 | .expect("unable to write to the file"); 66 | } 67 | -------------------------------------------------------------------------------- /src/consignment.rs: -------------------------------------------------------------------------------- 1 | // Standard Library for RGB smart contracts 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Designed in 2019-2025 by Dr Maxim Orlovsky 6 | // Written in 2024-2025 by Dr Maxim Orlovsky 7 | // 8 | // Copyright (C) 2019-2024 LNP/BP Standards Association, Switzerland. 9 | // Copyright (C) 2024-2025 LNP/BP Laboratories, 10 | // Institute for Distributed and Cognitive Systems (InDCS), Switzerland. 11 | // Copyright (C) 2025 RGB Consortium, Switzerland. 12 | // Copyright (C) 2019-2025 Dr Maxim Orlovsky. 13 | // All rights under the above copyrights are reserved. 14 | // 15 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 16 | // in compliance with the License. You may obtain a copy of the License at 17 | // 18 | // http://www.apache.org/licenses/LICENSE-2.0 19 | // 20 | // Unless required by applicable law or agreed to in writing, software distributed under the License 21 | // is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 22 | // or implied. See the License for the specific language governing permissions and limitations under 23 | // the License. 24 | 25 | use std::convert::Infallible; 26 | use std::error::Error; 27 | use std::vec; 28 | 29 | use amplify::confinement::{LargeVec, SmallBlob, SmallOrdMap, TinyVec}; 30 | use amplify::ByteArray; 31 | use commit_verify::{ReservedBytes, StrictHash}; 32 | use hypersonic::Articles; 33 | use rgb::{OperationSeals, ReadOperation, RgbSeal, LIB_NAME_RGB}; 34 | use strict_encoding::{DecodeError, ReadRaw, StrictDecode, StrictReader, TypedRead}; 35 | 36 | use crate::{ContractId, Identity, Issue, SemanticError, Semantics, SigBlob, CONSIGN_VERSION}; 37 | 38 | pub const MAX_CONSIGNMENT_OPS: u32 = u16::MAX as u32; 39 | 40 | #[derive(StrictType, StrictDumb, StrictEncode)] 41 | #[strict_type(lib = LIB_NAME_RGB)] 42 | pub struct Consignment { 43 | header: ConsignmentHeader, 44 | operation_seals: LargeVec>, 45 | } 46 | 47 | #[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)] 48 | #[strict_type(lib = LIB_NAME_RGB)] 49 | struct ConsignmentHeader { 50 | extensions: TinyVec, 51 | semantics: Semantics, 52 | sig: Option, 53 | issue: Issue, 54 | genesis_seals: SmallOrdMap, 55 | witness: ReservedBytes<1>, 56 | op_count: u32, 57 | } 58 | 59 | impl StrictDecode for Consignment { 60 | fn strict_decode(reader: &mut impl TypedRead) -> Result { 61 | let header = ConsignmentHeader::::strict_decode(reader)?; 62 | if header.op_count > MAX_CONSIGNMENT_OPS { 63 | return Err(DecodeError::DataIntegrityError(format!( 64 | "number of operations in contract consignment ({}) exceeds maximum allowed \ 65 | ({MAX_CONSIGNMENT_OPS})", 66 | header.op_count 67 | ))); 68 | } 69 | let mut operation_seals = LargeVec::with_capacity(header.op_count as usize); 70 | for _ in 0..header.op_count { 71 | operation_seals 72 | .push(OperationSeals::::strict_decode(reader)?) 73 | .ok(); 74 | } 75 | 76 | Ok(Self { header, operation_seals }) 77 | } 78 | } 79 | 80 | impl Consignment { 81 | pub fn articles( 82 | &self, 83 | sig_validator: impl FnOnce(StrictHash, &Identity, &SigBlob) -> Result<(), E>, 84 | ) -> Result { 85 | Articles::with( 86 | self.header.semantics.clone(), 87 | self.header.issue.clone(), 88 | self.header.sig.clone(), 89 | sig_validator, 90 | ) 91 | } 92 | 93 | pub(crate) fn into_operations(self) -> InMemOps { 94 | let genesis = OperationSeals { 95 | operation: self 96 | .header 97 | .issue 98 | .genesis 99 | .to_operation(ContractId::from_byte_array( 100 | self.header.issue.codex_id().to_byte_array(), 101 | )), 102 | defined_seals: self.header.genesis_seals, 103 | witness: None, 104 | }; 105 | InMemOps(Some(genesis), self.operation_seals.into_iter()) 106 | } 107 | } 108 | 109 | pub(crate) struct InMemOps( 110 | Option>, 111 | vec::IntoIter>, 112 | ); 113 | 114 | impl ReadOperation for InMemOps { 115 | type Seal = Seal; 116 | 117 | fn read_operation( 118 | &mut self, 119 | ) -> Result>, impl Error + 'static> { 120 | Result::<_, Infallible>::Ok(self.0.take().or_else(|| self.1.next())) 121 | } 122 | } 123 | 124 | pub fn parse_consignment( 125 | reader: &mut StrictReader, 126 | ) -> Result { 127 | ReservedBytes::<1, { CONSIGN_VERSION as u8 }>::strict_decode(reader).map_err(|e| { 128 | if matches!(e, DecodeError::DataIntegrityError(_)) { 129 | DecodeError::DataIntegrityError(s!("unsupported future consignment version")) 130 | } else { 131 | e 132 | } 133 | })?; 134 | ContractId::strict_decode(reader) 135 | } 136 | -------------------------------------------------------------------------------- /src/contract.rs: -------------------------------------------------------------------------------- 1 | // Standard Library for RGB smart contracts 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Designed in 2019-2025 by Dr Maxim Orlovsky 6 | // Written in 2024-2025 by Dr Maxim Orlovsky 7 | // 8 | // Copyright (C) 2019-2024 LNP/BP Standards Association, Switzerland. 9 | // Copyright (C) 2024-2025 LNP/BP Laboratories, 10 | // Institute for Distributed and Cognitive Systems (InDCS), Switzerland. 11 | // Copyright (C) 2025 RGB Consortium, Switzerland. 12 | // Copyright (C) 2019-2025 Dr Maxim Orlovsky. 13 | // All rights under the above copyrights are reserved. 14 | // 15 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 16 | // in compliance with the License. You may obtain a copy of the License at 17 | // 18 | // http://www.apache.org/licenses/LICENSE-2.0 19 | // 20 | // Unless required by applicable law or agreed to in writing, software distributed under the License 21 | // is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 22 | // or implied. See the License for the specific language governing permissions and limitations under 23 | // the License. 24 | 25 | use alloc::collections::BTreeMap; 26 | use core::borrow::Borrow; 27 | use core::error::Error; 28 | use core::marker::PhantomData; 29 | use std::io; 30 | 31 | use amplify::confinement::SmallOrdMap; 32 | use amplify::{IoError, MultiError}; 33 | use chrono::{DateTime, Utc}; 34 | use commit_verify::{ReservedBytes, StrictHash}; 35 | use hypersonic::{ 36 | AcceptError, Articles, AuthToken, CallParams, CellAddr, Codex, Consensus, ContractId, 37 | CoreParams, DataCell, EffectiveState, IssueError, IssueParams, Ledger, LibRepo, Memory, 38 | MethodName, NamedState, Operation, Opid, SemanticError, Semantics, SigBlob, StateAtom, 39 | StateName, Stock, Transition, 40 | }; 41 | use indexmap::{IndexMap, IndexSet}; 42 | use rgb::{ 43 | ContractApi, ContractVerify, OperationSeals, ReadOperation, RgbSeal, RgbSealDef, 44 | VerificationError, 45 | }; 46 | use single_use_seals::{ClientSideWitness, PublishedWitness, SealWitness}; 47 | use strict_encoding::{ 48 | DecodeError, ReadRaw, StrictDecode, StrictDumb, StrictEncode, StrictReader, StrictWriter, 49 | TypeName, TypedRead, WriteRaw, 50 | }; 51 | use strict_types::StrictVal; 52 | 53 | use crate::{ 54 | parse_consignment, Consignment, ContractMeta, Identity, Issue, Issuer, IssuerError, IssuerSpec, 55 | OpRels, Pile, VerifiedOperation, Witness, WitnessStatus, 56 | }; 57 | 58 | #[derive(Copy, Clone, PartialEq, Eq, Debug, From)] 59 | #[cfg_attr( 60 | feature = "serde", 61 | derive(Serialize, Deserialize), 62 | serde(untagged, bound = "Seal: serde::Serialize + for<'d> serde::Deserialize<'d>") 63 | )] 64 | pub enum EitherSeal { 65 | Alt(Seal), 66 | 67 | #[from] 68 | Token(AuthToken), 69 | } 70 | 71 | impl EitherSeal { 72 | pub fn auth_token(&self) -> AuthToken 73 | where Seal: RgbSealDef { 74 | match self { 75 | EitherSeal::Alt(seal) => seal.auth_token(), 76 | EitherSeal::Token(auth) => *auth, 77 | } 78 | } 79 | 80 | pub fn to_explicit(&self) -> Option 81 | where Seal: Clone { 82 | match self { 83 | EitherSeal::Alt(seal) => Some(seal.clone()), 84 | EitherSeal::Token(_) => None, 85 | } 86 | } 87 | } 88 | 89 | #[derive(Clone, PartialEq, Eq, Debug)] 90 | #[cfg_attr( 91 | feature = "serde", 92 | derive(Serialize, Deserialize), 93 | serde(bound = "Seal: serde::Serialize + for<'d> serde::Deserialize<'d>") 94 | )] 95 | pub struct Assignment { 96 | pub seal: Seal, 97 | pub data: StrictVal, 98 | } 99 | 100 | impl Assignment { 101 | pub fn new(seal: Seal, data: impl Into) -> Self { Self { seal, data: data.into() } } 102 | } 103 | 104 | impl Assignment> { 105 | pub fn new_external(auth: AuthToken, data: impl Into) -> Self { 106 | Self { seal: EitherSeal::Token(auth), data: data.into() } 107 | } 108 | pub fn new_internal(seal: Seal, data: impl Into) -> Self { 109 | Self { seal: EitherSeal::Alt(seal), data: data.into() } 110 | } 111 | } 112 | 113 | /// Element of the contract owned state, carrying information about its confirmation status. 114 | #[derive(Clone, PartialEq, Eq, Debug)] 115 | #[cfg_attr( 116 | feature = "serde", 117 | derive(Serialize, Deserialize), 118 | serde(bound = "Seal: serde::Serialize + for<'d> serde::Deserialize<'d>") 119 | )] 120 | pub struct OwnedState { 121 | /// Operation output defining this element of owned state. 122 | pub addr: CellAddr, 123 | 124 | /// State assignment. 125 | #[cfg_attr(feature = "serde", serde(flatten))] 126 | pub assignment: Assignment, 127 | 128 | /// Status of the state: the maximal confirmation depth for the whole history of operations 129 | /// leading to this status, since genesis. 130 | /// 131 | /// If any operation has known multiple witnesses (related to RBF'ed transactions or lightning 132 | /// channels), the best confirmation is used for that specific operation (i.e., the deepest 133 | /// mined). 134 | /// 135 | /// Formally, if $H$ is a set of all operations in the history between genesis and this state, 136 | /// and $W_o$ is a set of all witnesses for operation $o \in H$, the status here is defined as 137 | /// $max_{o \in H} min_{w \in W_o} status(w)$. 138 | pub status: WitnessStatus, 139 | } 140 | 141 | #[derive(Clone, PartialEq, Eq, Debug)] 142 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 143 | pub struct ImmutableState { 144 | pub addr: CellAddr, 145 | #[cfg_attr(feature = "serde", serde(flatten))] 146 | pub data: StateAtom, 147 | pub status: WitnessStatus, 148 | } 149 | 150 | #[derive(Clone, Eq, PartialEq, Debug, Default)] 151 | #[cfg_attr( 152 | feature = "serde", 153 | derive(Serialize, Deserialize), 154 | serde( 155 | rename_all = "camelCase", 156 | bound = "Seal: serde::Serialize + for<'d> serde::Deserialize<'d>" 157 | ) 158 | )] 159 | pub struct ContractState { 160 | pub immutable: BTreeMap>, 161 | pub owned: BTreeMap>>, 162 | pub aggregated: BTreeMap, 163 | } 164 | 165 | impl ContractState { 166 | pub fn map(self, f: impl Fn(Seal) -> To) -> ContractState { 167 | ContractState { 168 | immutable: self.immutable, 169 | owned: self 170 | .owned 171 | .into_iter() 172 | .map(|(name, map)| { 173 | let map = map 174 | .into_iter() 175 | .map(|owned| OwnedState { 176 | addr: owned.addr, 177 | assignment: Assignment { 178 | seal: f(owned.assignment.seal), 179 | data: owned.assignment.data, 180 | }, 181 | status: owned.status, 182 | }) 183 | .collect(); 184 | (name, map) 185 | }) 186 | .collect(), 187 | aggregated: self.aggregated, 188 | } 189 | } 190 | 191 | pub fn filter_map(self, f: impl Fn(Seal) -> Option) -> ContractState { 192 | ContractState { 193 | immutable: self.immutable, 194 | owned: self 195 | .owned 196 | .into_iter() 197 | .map(|(name, map)| { 198 | let map = map 199 | .into_iter() 200 | .filter_map(|owned| { 201 | Some(OwnedState { 202 | addr: owned.addr, 203 | assignment: Assignment { 204 | seal: f(owned.assignment.seal)?, 205 | data: owned.assignment.data, 206 | }, 207 | status: owned.status, 208 | }) 209 | }) 210 | .collect(); 211 | (name, map) 212 | }) 213 | .collect(), 214 | aggregated: self.aggregated, 215 | } 216 | } 217 | } 218 | 219 | /// Parameters used by RGB for contract creation operations. 220 | /// 221 | /// Differs from [`IssueParams`] in the fact that it uses full seal data instead of 222 | /// [`hypersonic::AuthTokens`] for output definitions. 223 | #[derive(Clone, Debug)] 224 | #[cfg_attr( 225 | feature = "serde", 226 | derive(Serialize, Deserialize), 227 | serde( 228 | rename_all = "camelCase", 229 | bound = "Seal: serde::Serialize + for<'d> serde::Deserialize<'d>" 230 | ) 231 | )] 232 | pub struct CreateParams { 233 | pub issuer: IssuerSpec, 234 | pub consensus: Consensus, 235 | pub testnet: bool, 236 | pub method: MethodName, 237 | pub name: TypeName, 238 | pub timestamp: Option>, 239 | pub global: Vec>, 240 | pub owned: Vec>>>, 241 | } 242 | 243 | impl CreateParams { 244 | pub fn new_testnet( 245 | issuer: impl Into, 246 | consensus: Consensus, 247 | name: impl Into, 248 | ) -> Self { 249 | Self { 250 | issuer: issuer.into(), 251 | consensus, 252 | testnet: true, 253 | method: vname!("issue"), 254 | name: name.into(), 255 | timestamp: None, 256 | global: none![], 257 | owned: none![], 258 | } 259 | } 260 | 261 | pub fn with_global_verified( 262 | mut self, 263 | name: impl Into, 264 | data: impl Into, 265 | ) -> Self { 266 | self.global 267 | .push(NamedState { name: name.into(), state: StateAtom::new_verified(data) }); 268 | self 269 | } 270 | 271 | pub fn push_owned_unlocked( 272 | &mut self, 273 | name: impl Into, 274 | assignment: Assignment>, 275 | ) { 276 | self.owned 277 | .push(NamedState { name: name.into(), state: assignment }); 278 | } 279 | } 280 | 281 | #[derive(Clone, Debug)] 282 | pub struct Contract { 283 | /// Cached contract id 284 | contract_id: ContractId, 285 | ledger: Ledger, 286 | pile: P, 287 | } 288 | 289 | impl Contract { 290 | /// Initializes contract from contract articles, consignment and a persistence configuration. 291 | pub fn with( 292 | articles: Articles, 293 | consignment: Consignment, 294 | conf: S::Conf, 295 | ) -> Result::Definition>, S::Error, P::Error>> 296 | where 297 | P::Conf: From, 298 | ::Client: StrictDecode, 299 | ::Published: StrictDecode, 300 | ::WitnessId: StrictDecode, 301 | { 302 | let contract_id = articles.contract_id(); 303 | let genesis_opid = articles.genesis_opid(); 304 | let ledger = Ledger::new(articles, conf) 305 | .map_err(MultiError::with_third) 306 | .map_err(MultiError::from_other_a)?; 307 | let conf: S::Conf = ledger.config(); 308 | let mut pile = P::new(conf.into()).map_err(MultiError::C)?; 309 | pile.add_seals(genesis_opid, none!()); 310 | let mut contract = Self { ledger, pile, contract_id }; 311 | contract 312 | .evaluate_commit(consignment.into_operations()) 313 | .map_err(MultiError::from_a)?; 314 | Ok(contract) 315 | } 316 | 317 | pub fn issue( 318 | issuer: Issuer, 319 | params: CreateParams<::Definition>, 320 | conf: impl FnOnce(&Articles) -> Result, 321 | ) -> Result> 322 | where 323 | P::Conf: From, 324 | { 325 | if !params.issuer.check(issuer.issuer_id()) { 326 | return Err(MultiError::A(IssuerError::IssuerMismatch)); 327 | } 328 | 329 | let seals = SmallOrdMap::try_from_iter(params.owned.iter().enumerate().filter_map( 330 | |(pos, assignment)| { 331 | assignment 332 | .state 333 | .seal 334 | .to_explicit() 335 | .map(|seal| (pos as u16, seal)) 336 | }, 337 | )) 338 | .expect("too many outputs"); 339 | let params = IssueParams { 340 | issuer: params.issuer, 341 | name: params.name, 342 | consensus: params.consensus, 343 | testnet: params.testnet, 344 | timestamp: params.timestamp, 345 | core: CoreParams { 346 | method: params.method, 347 | global: params.global, 348 | owned: params 349 | .owned 350 | .into_iter() 351 | .map(|assignment| NamedState { 352 | name: assignment.name, 353 | state: DataCell { 354 | auth: assignment.state.seal.auth_token(), 355 | data: assignment.state.data, 356 | lock: None, 357 | }, 358 | }) 359 | .collect(), 360 | }, 361 | }; 362 | 363 | let articles = issuer.issue(params); 364 | let conf = conf(&articles).map_err(MultiError::B)?; 365 | let ledger = Ledger::new(articles, conf) 366 | .map_err(MultiError::with_third) 367 | .map_err(MultiError::from_other_a)?; 368 | let conf: S::Conf = ledger.config(); 369 | let contract_id = ledger.contract_id(); 370 | 371 | // Init seals 372 | let mut pile = P::new(conf.into()).map_err(MultiError::C)?; 373 | pile.add_seals(ledger.articles().genesis_opid(), seals); 374 | 375 | Ok(Self { ledger, pile, contract_id }) 376 | } 377 | 378 | pub fn load( 379 | stock_conf: S::Conf, 380 | pile_conf: P::Conf, 381 | ) -> Result> { 382 | let ledger = Ledger::load(stock_conf).map_err(MultiError::A)?; 383 | let contract_id = ledger.contract_id(); 384 | let pile = P::load(pile_conf).map_err(MultiError::B)?; 385 | Ok(Self { ledger, pile, contract_id }) 386 | } 387 | 388 | /// Get the best mining status for a given operation ("best" means "the most deeply mined"). 389 | fn witness_status(&self, opid: Opid) -> WitnessStatus { 390 | self.pile 391 | .op_witness_ids(opid) 392 | .map(|wid| self.pile.witness_status(wid)) 393 | // "best" means "the most deeply mined" 394 | .reduce(|best, other| best.best(other)) 395 | .unwrap_or(WitnessStatus::Genesis) 396 | } 397 | 398 | fn retrieve(&self, opid: Opid) -> Option> { 399 | let (status, wid) = self 400 | .pile 401 | .op_witness_ids(opid) 402 | .map(|wid| (self.pile.witness_status(wid), wid)) 403 | .reduce(|best, other| if best.0.is_better(other.0) { best } else { other })?; 404 | if !status.is_valid() { 405 | return None; 406 | } 407 | let client = self.pile.cli_witness(wid); 408 | let published = self.pile.pub_witness(wid); 409 | Some(SealWitness::new(published, client)) 410 | } 411 | 412 | pub fn contract_id(&self) -> ContractId { self.contract_id } 413 | 414 | pub fn articles(&self) -> &Articles { self.ledger.articles() } 415 | 416 | /// # Nota bene 417 | /// 418 | /// Does not include genesis 419 | pub fn operations( 420 | &self, 421 | ) -> impl Iterator)> + use<'_, S, P> { 422 | self.ledger.operations().map(|(opid, op)| { 423 | let rels = self.pile.op_relations(opid, op.destructible_out.len_u16()); 424 | (opid, op, rels) 425 | }) 426 | } 427 | 428 | pub fn trace(&self) -> impl Iterator + use<'_, S, P> { 429 | self.ledger.trace() 430 | } 431 | 432 | pub fn witness_ids( 433 | &self, 434 | ) -> impl Iterator::WitnessId> + use<'_, S, P> { 435 | self.pile.witness_ids() 436 | } 437 | 438 | pub fn witnesses(&self) -> impl Iterator> + use<'_, S, P> { 439 | self.pile.witnesses() 440 | } 441 | 442 | pub fn ops_by_witness_id( 443 | &self, 444 | wid: ::WitnessId, 445 | ) -> impl Iterator + use<'_, S, P> { 446 | self.pile.ops_by_witness_id(wid) 447 | } 448 | 449 | pub fn op_seals(&self, opid: Opid, up_to: u16) -> OpRels { 450 | self.pile.op_relations(opid, up_to) 451 | } 452 | 453 | pub fn seal(&self, seal: &::Definition) -> Option { 454 | let auth = seal.auth_token(); 455 | self.ledger.state().raw.auth.get(&auth).copied() 456 | } 457 | 458 | /// Get the contract state. 459 | /// 460 | /// The call does not recompute the contract state, but does a seal resolution, 461 | /// taking into account the status of the witnesses in the whole history. 462 | pub fn state(&self) -> ContractState { 463 | let mut cache = bmap! {}; 464 | let mut ancestor_cache = bmap! {}; 465 | let mut cached_status = |opid: Opid| { 466 | *cache 467 | .entry(opid) 468 | .or_insert_with(|| self.witness_status(opid)) 469 | }; 470 | let mut get_status = |opid: Opid, or: WitnessStatus| { 471 | (*ancestor_cache.entry(opid).or_insert_with(|| { 472 | self.ledger 473 | .ancestors([opid]) 474 | .map(|ancestor| self.witness_status(ancestor)) 475 | .fold(WitnessStatus::Genesis, |worst, other| worst.worst(other)) 476 | })) 477 | .worst(or) 478 | }; 479 | let state = self.ledger.state().main.clone(); 480 | let mut owned = bmap! {}; 481 | for (name, map) in state.owned { 482 | let mut state = vec![]; 483 | for (addr, data) in map { 484 | let Some(seal) = self.pile.seal(addr) else { 485 | continue; 486 | }; 487 | if let Some(seal) = seal.to_src() { 488 | state.push(OwnedState { 489 | addr, 490 | assignment: Assignment { seal, data }, 491 | status: get_status(addr.opid, cached_status(addr.opid)), 492 | }); 493 | } else { 494 | // We insert a copy of state for each of the witnesses created for the operation 495 | for wid in self.pile.op_witness_ids(addr.opid) { 496 | state.push(OwnedState { 497 | addr, 498 | assignment: Assignment { seal: seal.resolve(wid), data: data.clone() }, 499 | status: get_status(addr.opid, self.pile.witness_status(wid)), 500 | }); 501 | } 502 | } 503 | } 504 | owned.insert(name, state); 505 | } 506 | let mut immutable = bmap! {}; 507 | for (name, map) in state.global { 508 | let mut state = vec![]; 509 | for (addr, data) in map { 510 | let status = get_status(addr.opid, cached_status(addr.opid)); 511 | state.push(ImmutableState { addr, data, status }); 512 | } 513 | immutable.insert(name, state); 514 | } 515 | ContractState { immutable, owned, aggregated: state.aggregated } 516 | } 517 | 518 | pub fn full_state(&self) -> &EffectiveState { self.ledger.state() } 519 | 520 | /// Synchronize the status of all witnesses and single-use seal definitions. 521 | /// 522 | /// # Panics 523 | /// 524 | /// If the contract id is not known. 525 | pub fn sync( 526 | &mut self, 527 | changed: impl IntoIterator::WitnessId, WitnessStatus)>, 528 | ) -> Result<(), MultiError> { 529 | // Step 1: Sanitize the list of changed wids 530 | let mut affected_wids = IndexMap::new(); 531 | for (wid, status) in changed { 532 | if !self.pile.has_witness(wid) { 533 | continue; 534 | } 535 | let prev_status = self.pile.witness_status(wid); 536 | if status == prev_status { 537 | continue; 538 | } 539 | 540 | let old_status = affected_wids.insert(wid, status); 541 | debug_assert!( 542 | old_status.is_none() || old_status == Some(status), 543 | "the same transaction with different status passed to a sync operation" 544 | ); 545 | } 546 | 547 | // Step 2: Select opdis which may be affected by the changed witnesses and their pre-sync 548 | // operation status 549 | let mut affected_ops = IndexMap::new(); 550 | for wid in affected_wids.keys() { 551 | for opid in self.pile.ops_by_witness_id(*wid) { 552 | let op_status = self.witness_status(opid); 553 | let old = affected_ops.insert(opid, op_status); 554 | debug_assert!(old.is_none() || old == Some(op_status)); 555 | } 556 | } 557 | 558 | // Step 3: Update witness status. 559 | // NB: This cannot be done at the same time as step 2 due to many-to-many relation between 560 | // witnesses and operations, such that one witness change may affect other operation witness 561 | // status. 562 | for (wid, status) in affected_wids { 563 | self.pile.update_witness_status(wid, status); 564 | } 565 | 566 | // Step 4: Filter opids and leave only those whose status has changed after the witness 567 | // update 568 | let mut roll_back = IndexSet::new(); 569 | let mut forward = IndexSet::new(); 570 | for (opid, old_status) in affected_ops { 571 | let new_status = self.witness_status(opid); 572 | if old_status.is_valid() == new_status.is_valid() { 573 | continue; 574 | } 575 | if new_status.is_valid() { 576 | forward.insert(opid); 577 | } else { 578 | roll_back.insert(opid); 579 | } 580 | } 581 | debug_assert_eq!(forward.intersection(&roll_back).count(), 0); 582 | 583 | // Step 5: Perform rollback and forward operations 584 | self.ledger.rollback(roll_back).map_err(MultiError::B)?; 585 | // Ledger has already committed as a part of `rollback` 586 | self.pile.commit_transaction(); 587 | 588 | self.ledger.forward(forward)?; 589 | // Ledger has already committed as a part of `forward` 590 | self.pile.commit_transaction(); 591 | 592 | Ok(()) 593 | } 594 | 595 | /// Do a call to the contract method, creating and operation. 596 | /// 597 | /// The operation is automatically included in the contract history. 598 | /// 599 | /// The state of the contract is not automatically updated, but on the next update it will 600 | /// reflect the call results. 601 | pub fn call( 602 | &mut self, 603 | call: CallParams, 604 | seals: SmallOrdMap::Definition>, 605 | ) -> Result> { 606 | let opid = self.ledger.call(call)?; 607 | let operation = self.ledger.operation(opid); 608 | debug_assert_eq!(operation.opid(), opid); 609 | self.pile.add_seals(opid, seals); 610 | debug_assert_eq!(operation.contract_id, self.contract_id()); 611 | Ok(operation) 612 | } 613 | 614 | /// Include an operation and its witness to the history of known operations and the contract 615 | /// state. 616 | pub fn include( 617 | &mut self, 618 | opid: Opid, 619 | anchor: ::Client, 620 | published: &::Published, 621 | ) { 622 | let wid = published.pub_id(); 623 | let anchor = if self.pile.has_witness(wid) { 624 | let mut prev_anchor = self.pile.cli_witness(wid); 625 | if prev_anchor != anchor { 626 | prev_anchor.merge(anchor).expect( 627 | "the existing anchor is not compatible with the new one; this indicates \ 628 | either a bug in the RGB standard library or a compromised storage", 629 | ); 630 | } 631 | prev_anchor 632 | } else { 633 | anchor 634 | }; 635 | self.pile 636 | .add_witness(opid, wid, published, &anchor, WitnessStatus::Tentative); 637 | self.pile.commit_transaction(); 638 | } 639 | 640 | fn aux( 641 | &self, 642 | opid: Opid, 643 | op: &Operation, 644 | mut writer: StrictWriter, 645 | ) -> io::Result> { 646 | // Write seal definitions 647 | let seals = self.pile.seals(opid, op.destructible_out.len_u16()); 648 | writer = seals.strict_encode(writer)?; 649 | 650 | // Write witnesses 651 | let witness = self.retrieve(opid); 652 | writer = witness.is_some().strict_encode(writer)?; 653 | if let Some(witness) = witness { 654 | writer = witness.strict_encode(writer)?; 655 | } 656 | 657 | Ok(writer) 658 | } 659 | 660 | /// Export a contract to a strictly encoded stream. 661 | /// 662 | /// # Errors 663 | /// 664 | /// If the output stream failures, like when the stream cannot accept more data or got 665 | /// disconnected. 666 | pub fn export(&self, writer: StrictWriter) -> io::Result<()> 667 | where 668 | ::Client: StrictDumb + StrictEncode, 669 | ::Published: StrictDumb + StrictEncode, 670 | ::WitnessId: StrictEncode, 671 | { 672 | self.ledger 673 | .export_all_aux(writer, |opid, op, writer| self.aux(opid, op, writer)) 674 | } 675 | 676 | /// Create a consignment with a history from the genesis to each of the `terminals`, and 677 | /// serialize it to a strictly encoded stream `writer`. 678 | /// 679 | /// # Errors 680 | /// 681 | /// If the output stream failures, like when the stream cannot accept more data or got 682 | /// disconnected. 683 | pub fn consign( 684 | &self, 685 | terminals: impl IntoIterator>, 686 | writer: StrictWriter, 687 | ) -> io::Result<()> 688 | where 689 | ::Client: StrictDumb + StrictEncode, 690 | ::Published: StrictDumb + StrictEncode, 691 | ::WitnessId: StrictEncode, 692 | { 693 | self.ledger 694 | .export_aux(terminals, writer, |opid, op, writer| self.aux(opid, op, writer)) 695 | } 696 | 697 | /// Consume a consignment stream. 698 | /// 699 | /// The method: 700 | /// - validates the consignment; 701 | /// - resolves auth tokens into seal definitions known to the current wallet (i.e., coming from 702 | /// the invoices produced by the wallet); 703 | /// - checks the signature of the issuer over the contract articles; 704 | /// 705 | /// # Arguments 706 | /// 707 | /// - `allow_unknown`: allows importing a contract which was not known to the system; 708 | /// - `reader`: the input stream; 709 | /// - `seal_resolver`: lambda which knows about the seal definitions from the wallet-generated 710 | /// invoices; 711 | /// - `sig_validator`: a validator for the signature of the issuer over the contract articles. 712 | pub fn consume( 713 | &mut self, 714 | reader: &mut StrictReader, 715 | seal_resolver: impl FnMut(&Operation) -> BTreeMap::Definition>, 716 | sig_validator: impl FnOnce(StrictHash, &Identity, &SigBlob) -> Result<(), E>, 717 | ) -> Result<(), MultiError::Definition>, S::Error>> 718 | where 719 | ::Client: StrictDecode, 720 | ::Published: StrictDecode, 721 | ::WitnessId: StrictDecode, 722 | { 723 | let contract_id = parse_consignment(reader).map_err(MultiError::from_a)?; 724 | if contract_id != self.contract_id() { 725 | return Err(MultiError::A(ConsumeError::UnknownContract(contract_id))); 726 | } 727 | self.consume_internal(reader, seal_resolver, sig_validator) 728 | } 729 | 730 | pub(crate) fn consume_internal( 731 | &mut self, 732 | reader: &mut StrictReader, 733 | seal_resolver: impl FnMut(&Operation) -> BTreeMap::Definition>, 734 | sig_validator: impl FnOnce(StrictHash, &Identity, &SigBlob) -> Result<(), E>, 735 | ) -> Result<(), MultiError::Definition>, S::Error>> 736 | where 737 | ::Client: StrictDecode, 738 | ::Published: StrictDecode, 739 | ::WitnessId: StrictDecode, 740 | { 741 | let articles = (|| -> Result> { 742 | // Read and ignore the extension block 743 | let ext_blocks = u8::strict_decode(reader)?; 744 | for _ in 0..ext_blocks { 745 | let len = u16::strict_decode(reader)?; 746 | let r = unsafe { reader.raw_reader() }; 747 | let _ = r.read_raw::<{ u16::MAX as usize }>(len as usize)?; 748 | } 749 | 750 | // We need to read articles field by field since we have to evaluate genesis separately 751 | let semantics = Semantics::strict_decode(reader)?; 752 | let sig = Option::::strict_decode(reader)?; 753 | 754 | let issue_version = ReservedBytes::<1>::strict_decode(reader)?; 755 | let meta = ContractMeta::strict_decode(reader)?; 756 | let codex = Codex::strict_decode(reader)?; 757 | 758 | let op_reader = OpReader { 759 | stream: reader, 760 | seal_resolver, 761 | // We start with this hardcoded value to signal that we need to read the actual 762 | // count right after the genesis (first operation). 763 | count: u32::MAX, 764 | _phantom: PhantomData, 765 | }; 766 | self.evaluate_commit(op_reader)?; 767 | 768 | // We need to clone due to a borrow checker. 769 | let genesis = self.ledger.articles().genesis().clone(); 770 | let issue = Issue { version: issue_version, meta, codex, genesis }; 771 | let articles = Articles::with(semantics, issue, sig, sig_validator)?; 772 | 773 | Ok(articles) 774 | })() 775 | .map_err(MultiError::A)?; 776 | 777 | // Here we do not check for the end of the stream, 778 | // so in the future we can have arbitrary extensions 779 | // put here with no backward compatibility issues. 780 | 781 | self.ledger 782 | .upgrade_apis(articles) 783 | .map_err(MultiError::from_other_a)?; 784 | Ok(()) 785 | } 786 | 787 | pub(crate) fn evaluate_commit>( 788 | &mut self, 789 | reader: R, 790 | ) -> Result<(), VerificationError> 791 | where 792 | ::Client: StrictDecode, 793 | ::Published: StrictDecode, 794 | ::WitnessId: StrictDecode, 795 | { 796 | self.evaluate(reader)?; 797 | self.ledger.commit_transaction(); 798 | self.pile.commit_transaction(); 799 | Ok(()) 800 | } 801 | } 802 | 803 | pub struct OpReader< 804 | 'r, 805 | Seal: RgbSeal, 806 | R: ReadRaw, 807 | F: FnMut(&Operation) -> BTreeMap, 808 | > { 809 | stream: &'r mut StrictReader, 810 | count: u32, 811 | seal_resolver: F, 812 | _phantom: PhantomData, 813 | } 814 | 815 | impl<'r, Seal: RgbSeal, R: ReadRaw, F: FnMut(&Operation) -> BTreeMap> 816 | ReadOperation for OpReader<'r, Seal, R, F> 817 | { 818 | type Seal = Seal; 819 | 820 | fn read_operation( 821 | &mut self, 822 | ) -> Result>, impl Error + 'static> { 823 | if self.count == 0 { 824 | return Result::<_, DecodeError>::Ok(None); 825 | } 826 | let operation = Operation::strict_decode(self.stream)?; 827 | 828 | let mut defined_seals = SmallOrdMap::strict_decode(self.stream)?; 829 | defined_seals 830 | .extend((self.seal_resolver)(&operation)) 831 | .map_err(|_| { 832 | DecodeError::DataIntegrityError(format!( 833 | "too many seals defined for the operation {}", 834 | operation.opid() 835 | )) 836 | })?; 837 | 838 | let witness = Option::>::strict_decode(self.stream)?; 839 | 840 | // We start with this hardcoded value to signal that we need to read the actual 841 | // count right after the genesis (first operation). 842 | if self.count == u32::MAX { 843 | self.count = u32::strict_decode(self.stream)?; 844 | } else { 845 | self.count -= 1; 846 | } 847 | 848 | Ok(Some(OperationSeals { operation, defined_seals, witness })) 849 | } 850 | } 851 | 852 | impl ContractApi for Contract { 853 | fn contract_id(&self) -> ContractId { self.ledger.contract_id() } 854 | 855 | fn codex(&self) -> &Codex { self.ledger.articles().codex() } 856 | 857 | fn repo(&self) -> &impl LibRepo { self.ledger.articles() } 858 | 859 | fn memory(&self) -> &impl Memory { &self.ledger.state().raw } 860 | 861 | fn is_known(&self, opid: Opid) -> bool { self.ledger.is_valid(opid) } 862 | 863 | fn apply_operation(&mut self, op: VerifiedOperation) { 864 | self.ledger.apply(op).expect("unable to apply operation"); 865 | } 866 | 867 | fn apply_seals( 868 | &mut self, 869 | opid: Opid, 870 | seals: SmallOrdMap::Definition>, 871 | ) { 872 | self.pile.add_seals(opid, seals); 873 | } 874 | 875 | fn apply_witness(&mut self, opid: Opid, witness: SealWitness) { 876 | self.include(opid, witness.client, &witness.published) 877 | } 878 | } 879 | 880 | #[derive(Debug, Display, Error, From)] 881 | #[display(inner)] 882 | pub enum ConsumeError { 883 | #[from] 884 | #[from(io::Error)] 885 | Io(IoError), 886 | 887 | /// unknown {0} can't be consumed; please import contract articles first. 888 | #[display(doc_comments)] 889 | UnknownContract(ContractId), 890 | 891 | #[from] 892 | Semantics(SemanticError), 893 | 894 | #[from] 895 | Decode(DecodeError), 896 | 897 | #[from] 898 | Verify(VerificationError), 899 | 900 | #[from] 901 | #[from(IssueError)] 902 | // FIXME 903 | Issue(IssuerError), 904 | } 905 | 906 | #[cfg(feature = "binfile")] 907 | mod fs { 908 | use std::path::Path; 909 | 910 | use binfile::BinFile; 911 | use strict_encoding::{StreamWriter, StrictDumb, StrictEncode}; 912 | 913 | use super::*; 914 | use crate::{CONSIGN_MAGIC_NUMBER, CONSIGN_VERSION}; 915 | 916 | impl Contract { 917 | /// Export a contract to a file at `path`. 918 | /// 919 | /// # Errors 920 | /// 921 | /// If writing to the file failures, like when the file already exists, there is no write 922 | /// access to it, or no sufficient disk space. 923 | pub fn export_to_file(&self, path: impl AsRef) -> io::Result<()> 924 | where 925 | ::Client: StrictDumb + StrictEncode, 926 | ::Published: StrictDumb + StrictEncode, 927 | ::WitnessId: StrictEncode, 928 | { 929 | let file = BinFile::::create_new(path)?; 930 | let writer = StrictWriter::with(StreamWriter::new::<{ usize::MAX }>(file)); 931 | self.export(writer) 932 | } 933 | 934 | /// Create a consignment with a history from the genesis to each of the `terminals`, and 935 | /// serialize it to a `file`. 936 | /// 937 | /// # Errors 938 | /// 939 | /// If writing to the file failures, like when the file already exists, there is no write 940 | /// access to it, or no sufficient disk space. 941 | pub fn consign_to_file( 942 | &self, 943 | path: impl AsRef, 944 | terminals: impl IntoIterator>, 945 | ) -> io::Result<()> 946 | where 947 | ::Client: StrictDumb + StrictEncode, 948 | ::Published: StrictDumb + StrictEncode, 949 | ::WitnessId: StrictEncode, 950 | { 951 | let file = BinFile::::create_new(path)?; 952 | let writer = StrictWriter::with(StreamWriter::new::<{ usize::MAX }>(file)); 953 | self.consign(terminals, writer) 954 | } 955 | } 956 | } 957 | -------------------------------------------------------------------------------- /src/contracts.rs: -------------------------------------------------------------------------------- 1 | // Standard Library for RGB smart contracts 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Designed in 2019-2025 by Dr Maxim Orlovsky 6 | // Written in 2024-2025 by Dr Maxim Orlovsky 7 | // 8 | // Copyright (C) 2019-2024 LNP/BP Standards Association, Switzerland. 9 | // Copyright (C) 2024-2025 LNP/BP Laboratories, 10 | // Institute for Distributed and Cognitive Systems (InDCS), Switzerland. 11 | // Copyright (C) 2025 RGB Consortium, Switzerland. 12 | // Copyright (C) 2019-2025 Dr Maxim Orlovsky. 13 | // All rights under the above copyrights are reserved. 14 | // 15 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 16 | // in compliance with the License. You may obtain a copy of the License at 17 | // 18 | // http://www.apache.org/licenses/LICENSE-2.0 19 | // 20 | // Unless required by applicable law or agreed to in writing, software distributed under the License 21 | // is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 22 | // or implied. See the License for the specific language governing permissions and limitations under 23 | // the License. 24 | 25 | use alloc::collections::BTreeMap; 26 | use core::borrow::Borrow; 27 | use core::cell::RefCell; 28 | use std::collections::HashMap; 29 | use std::io; 30 | 31 | use amplify::confinement::{KeyedCollection, SmallOrdMap}; 32 | use amplify::MultiError; 33 | use commit_verify::StrictHash; 34 | use hypersonic::{ 35 | AcceptError, AuthToken, CallParams, CodexId, ContractId, ContractName, Opid, Stock, 36 | }; 37 | use rgb::RgbSeal; 38 | use strict_encoding::{ 39 | ReadRaw, StrictDecode, StrictDumb, StrictEncode, StrictReader, StrictWriter, WriteRaw, 40 | }; 41 | 42 | use crate::{ 43 | parse_consignment, Articles, Consensus, Consignment, ConsumeError, Contract, ContractRef, 44 | ContractState, CreateParams, Identity, Issuer, Operation, Pile, SigBlob, Stockpile, 45 | WitnessStatus, 46 | }; 47 | 48 | pub const CONSIGN_VERSION: u16 = 0; 49 | #[cfg(feature = "binfile")] 50 | pub use _fs::CONSIGN_MAGIC_NUMBER; 51 | 52 | /// Collection of RGB smart contracts and contract issuers, which can be cached in memory. 53 | /// 54 | /// # Generics 55 | /// 56 | /// - `S` provides a specific cache implementation for an in-mem copy of issuers, 57 | /// - `C` provides a specific cache implementation for an in-mem copy of contracts. 58 | #[derive(Clone, Debug)] 59 | pub struct Contracts< 60 | Sp, 61 | S = HashMap, 62 | C = HashMap::Stock, ::Pile>>, 63 | > where 64 | Sp: Stockpile, 65 | S: KeyedCollection, 66 | C: KeyedCollection>, 67 | { 68 | issuers: RefCell, 69 | contracts: RefCell, 70 | persistence: Sp, 71 | } 72 | 73 | impl Contracts 74 | where 75 | Sp: Stockpile, 76 | S: KeyedCollection, 77 | C: KeyedCollection>, 78 | { 79 | pub fn load(persistence: Sp) -> Self 80 | where 81 | S: Default, 82 | C: Default, 83 | { 84 | Self { issuers: none!(), contracts: none!(), persistence } 85 | } 86 | 87 | fn with_contract( 88 | &self, 89 | id: ContractId, 90 | f: impl FnOnce(&Contract) -> R, 91 | or: Option, 92 | ) -> R { 93 | // We need this bullshit due to a failed rust `RefCell` implementation which panics if we do 94 | // this block any other way. 95 | if self.contracts.borrow().contains_key(&id) { 96 | return f(self.contracts.borrow().get(&id).unwrap()); 97 | } 98 | if let Some(contract) = self.persistence.contract(id) { 99 | let res = f(&contract); 100 | self.contracts.borrow_mut().insert(id, contract); 101 | res 102 | } else if let Some(or) = or { 103 | or 104 | } else { 105 | panic!("Contract {} not found", id) 106 | } 107 | } 108 | 109 | fn with_contract_mut( 110 | &mut self, 111 | id: ContractId, 112 | f: impl FnOnce(&mut Contract) -> R, 113 | ) -> R { 114 | // We need this bullshit due to a failed rust `RefCell` implementation which panics if we do 115 | // this block any other way. 116 | if self.contracts.borrow().contains_key(&id) { 117 | return f(self.contracts.borrow_mut().get_mut(&id).unwrap()); 118 | } 119 | if let Some(mut contract) = self.persistence.contract(id) { 120 | let res = f(&mut contract); 121 | self.contracts.borrow_mut().insert(id, contract); 122 | res 123 | } else { 124 | panic!("Contract {} not found", id) 125 | } 126 | } 127 | } 128 | 129 | impl Contracts 130 | where 131 | Sp: Stockpile, 132 | S: KeyedCollection, 133 | C: KeyedCollection>, 134 | { 135 | pub fn codex_ids(&self) -> impl Iterator + use<'_, Sp, S, C> { 136 | self.persistence.codex_ids() 137 | } 138 | 139 | pub fn issuers_count(&self) -> usize { self.persistence.issuers_count() } 140 | 141 | pub fn has_issuer(&self, codex_id: CodexId) -> bool { self.persistence.has_issuer(codex_id) } 142 | 143 | pub fn issuers(&self) -> impl Iterator + use<'_, Sp, S, C> { 144 | self.persistence 145 | .codex_ids() 146 | .filter_map(|codex_id| self.issuer(codex_id).map(|schema| (codex_id, schema))) 147 | } 148 | 149 | pub fn issuer(&self, codex_id: CodexId) -> Option { 150 | if let Some(issuer) = self.issuers.borrow().get(&codex_id) { 151 | return Some(issuer.clone()); 152 | }; 153 | let issuer = self.persistence.issuer(codex_id)?; 154 | self.issuers.borrow_mut().insert(codex_id, issuer); 155 | self.issuers.borrow().get(&codex_id).cloned() 156 | } 157 | 158 | pub fn contracts_count(&self) -> usize { self.persistence.contracts_count() } 159 | 160 | pub fn has_contract(&self, contract_id: ContractId) -> bool { 161 | self.persistence.has_contract(contract_id) 162 | } 163 | 164 | pub fn contract_ids(&self) -> impl Iterator + use<'_, Sp, S, C> { 165 | self.persistence.contract_ids() 166 | } 167 | 168 | /// Get the contract state. 169 | /// 170 | /// The call does not recompute the contract state, but does a seal resolution, 171 | /// taking into account the status of the witnesses in the whole history. 172 | /// 173 | /// # Panics 174 | /// 175 | /// If the contract id is not known. 176 | pub fn contract_state( 177 | &self, 178 | contract_id: ContractId, 179 | ) -> ContractState<::Seal> { 180 | self.with_contract(contract_id, |contract| contract.state(), None) 181 | } 182 | 183 | pub fn contract_articles(&self, contract_id: ContractId) -> Articles { 184 | self.with_contract(contract_id, |contract| contract.articles().clone(), None) 185 | } 186 | 187 | pub fn find_contract_id(&self, r: impl Into) -> Option { 188 | match r.into() { 189 | ContractRef::Id(id) if self.has_contract(id) => Some(id), 190 | ContractRef::Id(_) => None, 191 | ContractRef::Name(name) => { 192 | let name = ContractName::Named(name); 193 | if let Some(id) = self 194 | .contracts 195 | .borrow() 196 | .iter() 197 | .find(|(_, contract)| contract.articles().issue().meta.name == name) 198 | .map(|(id, _)| *id) 199 | { 200 | return Some(id); 201 | } 202 | self.persistence 203 | .contract_ids() 204 | .filter_map(|id| self.persistence.contract(id)) 205 | .find(|contract| contract.articles().issue().meta.name == name) 206 | .map(|contract| contract.contract_id()) 207 | } 208 | } 209 | } 210 | 211 | /// Iterates over all witness ids known to the set of contracts. 212 | /// 213 | /// # Nota bene 214 | /// 215 | /// Iterator may repeat the same id multiple times. 216 | pub fn witness_ids( 217 | &self, 218 | ) -> impl Iterator::Seal as RgbSeal>::WitnessId> + use<'_, Sp, S, C> 219 | { 220 | self.persistence.contract_ids().flat_map(move |id| { 221 | self.with_contract( 222 | id, 223 | |contract| contract.witness_ids().collect::>(), 224 | Some(none!()), 225 | ) 226 | }) 227 | } 228 | 229 | pub fn import_issuer(&mut self, issuer: Issuer) -> Result { 230 | let codex_id = issuer.codex_id(); 231 | let schema = self.persistence.import_issuer(issuer)?; 232 | self.issuers.borrow_mut().insert(codex_id, schema); 233 | Ok(codex_id) 234 | } 235 | 236 | fn check_layer1( 237 | &self, 238 | consensus: Consensus, 239 | testnet: bool, 240 | ) -> Result<(), MultiError::Error, ::Error>> 241 | { 242 | if consensus != self.persistence.consensus() { 243 | return Err(MultiError::A(IssuerError::ConsensusMismatch)); 244 | } 245 | if testnet != self.persistence.is_testnet() { 246 | Err(if testnet { 247 | MultiError::A(IssuerError::TestnetMismatch) 248 | } else { 249 | MultiError::A(IssuerError::MainnetMismatch) 250 | }) 251 | } else { 252 | Ok(()) 253 | } 254 | } 255 | 256 | pub fn issue( 257 | &mut self, 258 | params: CreateParams<<::Seal as RgbSeal>::Definition>, 259 | ) -> Result< 260 | ContractId, 261 | MultiError::Error, ::Error>, 262 | > { 263 | self.check_layer1(params.consensus, params.testnet)?; 264 | let contract = self.persistence.issue(params)?; 265 | let id = contract.contract_id(); 266 | self.contracts.borrow_mut().insert(id, contract); 267 | Ok(id) 268 | } 269 | 270 | /// Do a call to the contract method, creating and operation. 271 | /// 272 | /// The operation is automatically included in the contract history. 273 | /// 274 | /// The state of the contract is not automatically updated, but on the next update it will 275 | /// reflect the call results. 276 | /// 277 | /// # Panics 278 | /// 279 | /// If the contract id is not known. 280 | pub fn contract_call( 281 | &mut self, 282 | contract_id: ContractId, 283 | call: CallParams, 284 | seals: SmallOrdMap::Seal as RgbSeal>::Definition>, 285 | ) -> Result::Error>> { 286 | self.with_contract_mut(contract_id, |contract| contract.call(call, seals)) 287 | } 288 | 289 | /// Synchronize the status of all witnesses and single-use seal definitions. 290 | /// 291 | /// # Panics 292 | /// 293 | /// If the contract id is not known. 294 | pub fn sync<'a, I>( 295 | &mut self, 296 | changed: I, 297 | ) -> Result<(), MultiError::Error>> 298 | where 299 | I: IntoIterator< 300 | Item = (&'a <::Seal as RgbSeal>::WitnessId, &'a WitnessStatus), 301 | > + Copy, 302 | <::Seal as RgbSeal>::WitnessId: 'a, 303 | { 304 | let contract_ids = self.persistence.contract_ids().collect::>(); 305 | for contract_id in &contract_ids { 306 | self.with_contract_mut(*contract_id, |contract| { 307 | contract.sync(changed.into_iter().map(|(id, status)| (*id, *status))) 308 | })?; 309 | } 310 | Ok(()) 311 | } 312 | 313 | /// Include an operation and its witness to the history of known operations and the contract 314 | /// state. 315 | /// 316 | /// # Panics 317 | /// 318 | /// If the contract id is not known. 319 | pub fn include( 320 | &mut self, 321 | contract_id: ContractId, 322 | opid: Opid, 323 | pub_witness: &<::Seal as RgbSeal>::Published, 324 | anchor: <::Seal as RgbSeal>::Client, 325 | ) { 326 | self.with_contract_mut(contract_id, |contract| contract.include(opid, anchor, pub_witness)) 327 | } 328 | 329 | /// Export a contract to a strictly encoded stream. 330 | /// 331 | /// # Panics 332 | /// 333 | /// If the contract id is not known. 334 | /// 335 | /// # Errors 336 | /// 337 | /// If the output stream failures, like when the stream cannot accept more data or got 338 | /// disconnected. 339 | pub fn export( 340 | &self, 341 | contract_id: ContractId, 342 | writer: StrictWriter, 343 | ) -> io::Result<()> 344 | where 345 | <::Seal as RgbSeal>::Client: StrictDumb + StrictEncode, 346 | <::Seal as RgbSeal>::Published: StrictDumb + StrictEncode, 347 | <::Seal as RgbSeal>::WitnessId: StrictEncode, 348 | { 349 | self.with_contract(contract_id, |contract| contract.export(writer), None) 350 | } 351 | 352 | /// Purge a contract from the system. 353 | pub fn purge(&mut self, contract_id: ContractId) -> Result<(), Sp::Error> { 354 | self.contracts.borrow_mut().remove(&contract_id); 355 | self.persistence.purge(contract_id)?; 356 | Ok(()) 357 | } 358 | 359 | /// Create a consignment with a history from the genesis to each of the `terminals`, and 360 | /// serialize it to a strictly encoded stream `writer`. 361 | /// 362 | /// # Panics 363 | /// 364 | /// If the contract id is not known. 365 | /// 366 | /// # Errors 367 | /// 368 | /// If the output stream failures, like when the stream cannot accept more data or got 369 | /// disconnected. 370 | pub fn consign( 371 | &mut self, 372 | contract_id: ContractId, 373 | terminals: impl IntoIterator>, 374 | writer: StrictWriter, 375 | ) -> io::Result<()> 376 | where 377 | <::Seal as RgbSeal>::Client: StrictDumb + StrictEncode, 378 | <::Seal as RgbSeal>::Published: StrictDumb + StrictEncode, 379 | <::Seal as RgbSeal>::WitnessId: StrictEncode, 380 | { 381 | self.with_contract_mut(contract_id, |contract| contract.consign(terminals, writer)) 382 | } 383 | 384 | /// Consume a consignment stream. 385 | /// 386 | /// The method: 387 | /// - validates the consignment; 388 | /// - resolves auth tokens into seal definitions known to the current wallet (i.e., coming from 389 | /// the invoices produced by the wallet); 390 | /// - checks the signature of the issuer over the contract articles; 391 | /// 392 | /// # Arguments 393 | /// 394 | /// - `allow_unknown`: allows importing a contract which was not known to the system; 395 | /// - `reader`: the input stream; 396 | /// - `seal_resolver`: lambda which knows about the seal definitions from the wallet-generated 397 | /// invoices; 398 | /// - `sig_validator`: a validator for the signature of the issuer over the contract articles. 399 | pub fn consume( 400 | &mut self, 401 | allow_unknown: bool, 402 | reader: &mut StrictReader, 403 | seal_resolver: impl FnMut( 404 | &Operation, 405 | ) 406 | -> BTreeMap::Seal as RgbSeal>::Definition>, 407 | sig_validator: impl FnOnce(StrictHash, &Identity, &SigBlob) -> Result<(), E>, 408 | ) -> Result< 409 | (), 410 | MultiError< 411 | ConsumeError<<::Seal as RgbSeal>::Definition>, 412 | ::Error, 413 | ::Error, 414 | >, 415 | > 416 | where 417 | ::Conf: From<::Conf>, 418 | <::Seal as RgbSeal>::Client: StrictDecode, 419 | <::Seal as RgbSeal>::Published: StrictDecode, 420 | <::Seal as RgbSeal>::WitnessId: StrictDecode, 421 | { 422 | // Checking version and getting contract id 423 | let contract_id = parse_consignment(reader).map_err(MultiError::from_a)?; 424 | if !self.has_contract(contract_id) { 425 | if allow_unknown { 426 | let consignment = Consignment::strict_decode(reader).map_err(MultiError::from_a)?; 427 | // Here we do not check for the end of the stream, 428 | // so in the future we can have arbitrary extensions 429 | // put here with no backward compatibility issues. 430 | 431 | let articles = consignment 432 | .articles(sig_validator) 433 | .map_err(MultiError::from_a)?; 434 | self.check_layer1( 435 | articles.contract_meta().consensus, 436 | articles.contract_meta().testnet, 437 | ) 438 | .map_err(MultiError::from_other_a)?; 439 | 440 | let contract = self.persistence.import_contract(articles, consignment)?; 441 | self.contracts.borrow_mut().insert(contract_id, contract); 442 | Ok(()) 443 | } else { 444 | Err(MultiError::A(ConsumeError::UnknownContract(contract_id))) 445 | } 446 | } else { 447 | self.with_contract_mut(contract_id, |contract| { 448 | contract.consume_internal(reader, seal_resolver, sig_validator) 449 | }) 450 | .map_err(|err| match err { 451 | MultiError::A(a) => MultiError::A(a), 452 | MultiError::B(b) => MultiError::B(b), 453 | MultiError::C(_) => unreachable!(), 454 | }) 455 | } 456 | } 457 | } 458 | 459 | #[derive(Clone, PartialEq, Eq, Debug, Display, Error, From)] 460 | #[display(doc_comments)] 461 | pub enum IssuerError { 462 | /// the issuer version does not match the required one. 463 | IssuerMismatch, 464 | /// proof of publication layer mismatch. 465 | ConsensusMismatch, 466 | /// unable to consume a testnet contract for mainnet. 467 | TestnetMismatch, 468 | /// unable to consume a mainnet contract for testnet. 469 | MainnetMismatch, 470 | /// unknown codex for contract issue {0}. 471 | UnknownCodex(CodexId), 472 | 473 | #[from] 474 | #[display(inner)] 475 | Inner(hypersonic::IssueError), 476 | } 477 | 478 | #[cfg(feature = "binfile")] 479 | mod _fs { 480 | use std::path::Path; 481 | 482 | use binfile::BinFile; 483 | use strict_encoding::StreamReader; 484 | 485 | use super::*; 486 | 487 | pub const CONSIGN_MAGIC_NUMBER: u64 = u64::from_be_bytes(*b"RGBCNSGN"); 488 | 489 | impl Contracts 490 | where 491 | Sp: Stockpile, 492 | S: KeyedCollection, 493 | C: KeyedCollection>, 494 | { 495 | /// Export a contract to a file at `path`. 496 | /// 497 | /// # Panics 498 | /// 499 | /// If the contract id is not known. 500 | /// 501 | /// # Errors 502 | /// 503 | /// If writing to the file failures, like when the file already exists, there is no write 504 | /// access to it, or no sufficient disk space. 505 | pub fn export_to_file( 506 | &self, 507 | path: impl AsRef, 508 | contract_id: ContractId, 509 | ) -> io::Result<()> 510 | where 511 | <::Seal as RgbSeal>::Client: StrictDumb + StrictEncode, 512 | <::Seal as RgbSeal>::Published: StrictDumb + StrictEncode, 513 | <::Seal as RgbSeal>::WitnessId: StrictEncode, 514 | { 515 | self.with_contract(contract_id, |contract| contract.export_to_file(path), None) 516 | } 517 | 518 | /// Create a consignment with a history from the genesis to each of the `terminals`, and 519 | /// serialize it to a `file`. 520 | /// 521 | /// # Panics 522 | /// 523 | /// If the contract id is not known. 524 | /// 525 | /// # Errors 526 | /// 527 | /// If writing to the file failures, like when the file already exists, there is no write 528 | /// access to it, or no sufficient disk space. 529 | pub fn consign_to_file( 530 | &self, 531 | path: impl AsRef, 532 | contract_id: ContractId, 533 | terminals: impl IntoIterator>, 534 | ) -> io::Result<()> 535 | where 536 | <::Seal as RgbSeal>::Client: StrictDumb + StrictEncode, 537 | <::Seal as RgbSeal>::Published: StrictDumb + StrictEncode, 538 | <::Seal as RgbSeal>::WitnessId: StrictEncode, 539 | { 540 | self.with_contract( 541 | contract_id, 542 | |contract| contract.consign_to_file(path, terminals), 543 | None, 544 | ) 545 | } 546 | 547 | /// Consume a consignment from a `file`. 548 | /// 549 | /// The method: 550 | /// - validates the consignment; 551 | /// - resolves auth tokens into seal definitions known to the current wallet (i.e., coming 552 | /// from the invoices produced by the wallet); 553 | /// - checks the signature of the issuer over the contract articles; 554 | /// 555 | /// # Arguments 556 | /// 557 | /// - `allow_unknown`: allows importing a contract which was not known to the system; 558 | /// - `reader`: the input stream; 559 | /// - `seal_resolver`: lambda which knows about the seal definitions from the 560 | /// wallet-generated invoices; 561 | /// - `sig_validator`: a validator for the signature of the issuer over the contract 562 | /// articles. 563 | pub fn consume_from_file( 564 | &mut self, 565 | allow_unknown: bool, 566 | path: impl AsRef, 567 | seal_resolver: impl FnMut( 568 | &Operation, 569 | ) -> BTreeMap< 570 | u16, 571 | <::Seal as RgbSeal>::Definition, 572 | >, 573 | sig_validator: impl FnOnce(StrictHash, &Identity, &SigBlob) -> Result<(), E>, 574 | ) -> Result< 575 | (), 576 | MultiError< 577 | ConsumeError<<::Seal as RgbSeal>::Definition>, 578 | ::Error, 579 | ::Error, 580 | >, 581 | > 582 | where 583 | ::Conf: From<::Conf>, 584 | <::Seal as RgbSeal>::Client: StrictDecode, 585 | <::Seal as RgbSeal>::Published: StrictDecode, 586 | <::Seal as RgbSeal>::WitnessId: StrictDecode, 587 | { 588 | let file = BinFile::::open(path) 589 | .map_err(MultiError::from_a)?; 590 | let mut reader = StrictReader::with(StreamReader::new::<{ usize::MAX }>(file)); 591 | self.consume(allow_unknown, &mut reader, seal_resolver, sig_validator) 592 | } 593 | } 594 | } 595 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Standard Library for RGB smart contracts 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Designed in 2019-2025 by Dr Maxim Orlovsky 6 | // Written in 2024-2025 by Dr Maxim Orlovsky 7 | // 8 | // Copyright (C) 2019-2024 LNP/BP Standards Association, Switzerland. 9 | // Copyright (C) 2024-2025 LNP/BP Laboratories, 10 | // Institute for Distributed and Cognitive Systems (InDCS), Switzerland. 11 | // Copyright (C) 2025 RGB Consortium, Switzerland. 12 | // Copyright (C) 2019-2025 Dr Maxim Orlovsky. 13 | // All rights under the above copyrights are reserved. 14 | // 15 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 16 | // in compliance with the License. You may obtain a copy of the License at 17 | // 18 | // http://www.apache.org/licenses/LICENSE-2.0 19 | // 20 | // Unless required by applicable law or agreed to in writing, software distributed under the License 21 | // is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 22 | // or implied. See the License for the specific language governing permissions and limitations under 23 | // the License. 24 | 25 | // TODO: Activate once StrictEncoding will be no_std 26 | // #![cfg_attr(not(feature = "std"), no_std)] 27 | #![deny( 28 | // TODO: Activate once StrictEncoding removes invalid unsafe fn modifiers from the raw reader 29 | // unsafe_code, 30 | dead_code, 31 | // TODO: Complete documentation 32 | // missing_docs, 33 | unused_variables, 34 | unused_mut, 35 | unused_imports, 36 | non_upper_case_globals, 37 | non_camel_case_types, 38 | non_snake_case 39 | )] 40 | #![cfg_attr(coverage_nightly, feature(coverage_attribute))] 41 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 42 | #![allow(clippy::type_complexity)] 43 | 44 | extern crate alloc; 45 | 46 | #[macro_use] 47 | extern crate amplify; 48 | extern crate rgbcore as rgb; 49 | 50 | #[cfg(feature = "bitcoin")] 51 | #[macro_use] 52 | extern crate strict_encoding; 53 | #[cfg(all(feature = "serde", feature = "bitcoin"))] 54 | #[macro_use] 55 | extern crate serde; 56 | 57 | extern crate core; 58 | pub extern crate rgb_invoice as invoice; 59 | 60 | mod pile; 61 | mod stockpile; 62 | mod contract; 63 | mod consignment; 64 | mod contracts; 65 | pub mod popls; 66 | mod util; 67 | #[cfg(feature = "stl")] 68 | pub mod stl; 69 | 70 | #[cfg(feature = "bitcoin")] 71 | pub use bp::{Outpoint, Txid}; 72 | pub use consignment::{parse_consignment, Consignment, MAX_CONSIGNMENT_OPS}; 73 | pub use contract::{ 74 | Assignment, ConsumeError, Contract, ContractState, CreateParams, EitherSeal, ImmutableState, 75 | OwnedState, 76 | }; 77 | #[cfg(feature = "binfile")] 78 | pub use contracts::CONSIGN_MAGIC_NUMBER; 79 | pub use contracts::{Contracts, IssuerError, CONSIGN_VERSION}; 80 | pub use hypersonic::*; 81 | pub use pile::{OpRels, Pile, Witness, WitnessStatus}; 82 | pub use rgb::*; 83 | pub use stockpile::Stockpile; 84 | pub use util::{ContractRef, InvalidContractRef}; 85 | -------------------------------------------------------------------------------- /src/pile.rs: -------------------------------------------------------------------------------- 1 | // Standard Library for RGB smart contracts 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Designed in 2019-2025 by Dr Maxim Orlovsky 6 | // Written in 2024-2025 by Dr Maxim Orlovsky 7 | // 8 | // Copyright (C) 2019-2024 LNP/BP Standards Association, Switzerland. 9 | // Copyright (C) 2024-2025 LNP/BP Laboratories, 10 | // Institute for Distributed and Cognitive Systems (InDCS), Switzerland. 11 | // Copyright (C) 2025 RGB Consortium, Switzerland. 12 | // Copyright (C) 2019-2025 Dr Maxim Orlovsky. 13 | // All rights under the above copyrights are reserved. 14 | // 15 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 16 | // in compliance with the License. You may obtain a copy of the License at 17 | // 18 | // http://www.apache.org/licenses/LICENSE-2.0 19 | // 20 | // Unless required by applicable law or agreed to in writing, software distributed under the License 21 | // is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 22 | // or implied. See the License for the specific language governing permissions and limitations under 23 | // the License. 24 | 25 | use alloc::collections::BTreeSet; 26 | use core::error::Error as StdError; 27 | use core::fmt::Debug; 28 | use core::marker::PhantomData; 29 | use core::num::NonZeroU64; 30 | use std::collections::HashSet; 31 | 32 | use amplify::confinement::SmallOrdMap; 33 | use hypersonic::Opid; 34 | use rgb::RgbSeal; 35 | 36 | use crate::CellAddr; 37 | 38 | /// The status of the witness transaction. 39 | /// 40 | /// Note on comparison: 41 | /// the ordering is done in a way that more trustfull status is always greater than less trustfull. 42 | #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Display, Default)] 43 | #[display(lowercase)] 44 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))] 45 | pub enum WitnessStatus { 46 | /// No witness, which is the case for genesis operations. 47 | Genesis, 48 | 49 | /// Valid public witness included in the current consensus at a specific height, which, 50 | /// however, may be eventually re-orged. 51 | /// 52 | /// A zero height (used by Bitcoin genesis which can't contain any RGB operation) is used for 53 | /// the operations which do not have witness (genesis operation) - see [`Self::Genesis`]. 54 | #[display(inner)] 55 | Mined(NonZeroU64), 56 | 57 | /// Indicates offchain status where the receiver is in at least partial control over 58 | /// transaction execution and the transaction can't be replaced (RBFed) without the 59 | /// receiver participation - for instance, like in lightning channel transactions (but only 60 | /// for the current channel state). 61 | Offchain, 62 | 63 | /// Indicates known public witness which can be replaced or RBF'ed without the control of the 64 | /// receiving side. 65 | Tentative, 66 | 67 | /// Indicates past public witness which is no more valid - it is not included in the 68 | /// blockchain, not present in the mempool, or belongs to the past Lightning channel state. 69 | #[default] 70 | Archived, 71 | } 72 | 73 | impl WitnessStatus { 74 | const GENESIS: u64 = 0; 75 | const TENTATIVE: u64 = u64::MAX ^ 0x01; 76 | const OFFCHAIN: u64 = u64::MAX ^ 0x02; 77 | const ARCHIVED: u64 = u64::MAX; 78 | 79 | pub fn is_mined(&self) -> bool { matches!(self, Self::Mined(_)) } 80 | pub fn is_valid(&self) -> bool { !matches!(self, Self::Archived) } 81 | pub fn is_tentative(&self) -> bool { matches!(self, Self::Tentative) } 82 | pub fn is_archived(&self) -> bool { matches!(self, Self::Archived) } 83 | pub fn is_offchain(&self) -> bool { matches!(self, Self::Offchain) } 84 | 85 | fn quasi_height(&self) -> u64 { 86 | match self { 87 | Self::Genesis => Self::GENESIS, 88 | Self::Archived => Self::ARCHIVED, 89 | Self::Tentative => Self::TENTATIVE, 90 | Self::Offchain => Self::OFFCHAIN, 91 | Self::Mined(height) => height.get(), 92 | } 93 | } 94 | 95 | pub fn is_better(self, other: Self) -> bool { 96 | // Lesser height means "mined deeper", which implies better security 97 | self.quasi_height() < other.quasi_height() 98 | } 99 | 100 | pub fn is_worse(self, other: Self) -> bool { !self.is_better(other) } 101 | 102 | pub fn best(self, other: Self) -> Self { 103 | if self.is_better(other) { 104 | self 105 | } else { 106 | other 107 | } 108 | } 109 | 110 | pub fn worst(self, other: Self) -> Self { 111 | if self.is_worse(other) { 112 | self 113 | } else { 114 | other 115 | } 116 | } 117 | } 118 | 119 | impl From<[u8; 8]> for WitnessStatus { 120 | fn from(value: [u8; 8]) -> Self { 121 | let depth = u64::from_be_bytes(value); 122 | let height = u64::MAX - depth; 123 | match height { 124 | Self::GENESIS => Self::Genesis, 125 | Self::ARCHIVED => Self::Archived, 126 | Self::TENTATIVE => Self::Tentative, 127 | Self::OFFCHAIN => Self::Offchain, 128 | height => Self::Mined(NonZeroU64::new(height).expect("GENESIS=0 is already checked")), 129 | } 130 | } 131 | } 132 | 133 | impl From for [u8; 8] { 134 | fn from(value: WitnessStatus) -> Self { 135 | let height = value.quasi_height(); 136 | let depth = u64::MAX - height; 137 | depth.to_be_bytes() 138 | } 139 | } 140 | 141 | #[derive(Clone, Debug)] 142 | #[cfg_attr(feature = "serde", derive(Serialize))] 143 | pub struct Witness { 144 | pub id: Seal::WitnessId, 145 | pub published: Seal::Published, 146 | pub client: Seal::Client, 147 | pub status: WitnessStatus, 148 | pub opids: HashSet, 149 | } 150 | 151 | #[derive(Clone, Debug)] 152 | #[cfg_attr( 153 | feature = "serde", 154 | derive(Serialize), 155 | serde(bound = "Seal::WitnessId: serde::Serialize, Seal::Definition: serde::Serialize") 156 | )] 157 | pub struct OpRels { 158 | pub opid: Opid, 159 | pub witness_ids: BTreeSet, 160 | pub defines: SmallOrdMap, 161 | #[cfg_attr(feature = "serde", serde(skip))] 162 | pub _phantom: PhantomData, 163 | } 164 | 165 | /// Persistent storage for contract witness and single-use seal definition data. 166 | pub trait Pile { 167 | /// Type of RGB seal used in the contract. 168 | type Seal: RgbSeal; 169 | 170 | /// Persistence configuration type. 171 | type Conf; 172 | 173 | type Error: StdError; 174 | 175 | /// Instantiates a new pile (persistence for contract witness data) using a given 176 | /// implementation-specific configuration. 177 | /// 178 | /// # Panics 179 | /// 180 | /// This call must not panic, and instead must return an error. 181 | /// 182 | /// # Blocking I/O 183 | /// 184 | /// This call MAY perform I/O operations. 185 | fn new(conf: Self::Conf) -> Result 186 | where Self: Sized; 187 | 188 | /// Loads a contract from persistence using the provided configuration. 189 | /// 190 | /// # Panics 191 | /// 192 | /// This call must not panic, and instead must return an error. 193 | /// 194 | /// # Blocking I/O 195 | /// 196 | /// This call MAY perform I/O operations. 197 | fn load(conf: Self::Conf) -> Result 198 | where Self: Sized; 199 | 200 | fn pub_witness( 201 | &self, 202 | wid: ::WitnessId, 203 | ) -> ::Published; 204 | 205 | fn has_witness(&self, wid: ::WitnessId) -> bool; 206 | 207 | fn cli_witness( 208 | &self, 209 | wid: ::WitnessId, 210 | ) -> ::Client; 211 | 212 | fn witness_status(&self, wid: ::WitnessId) -> WitnessStatus; 213 | 214 | fn witness_ids(&self) -> impl Iterator::WitnessId>; 215 | 216 | fn witnesses(&self) -> impl Iterator>; 217 | 218 | fn op_witness_ids( 219 | &self, 220 | opid: Opid, 221 | ) -> impl ExactSizeIterator::WitnessId>; 222 | 223 | fn ops_by_witness_id( 224 | &self, 225 | wid: ::WitnessId, 226 | ) -> impl ExactSizeIterator; 227 | 228 | fn seal(&self, addr: CellAddr) -> Option<::Definition>; 229 | 230 | fn seals( 231 | &self, 232 | opid: Opid, 233 | up_to: u16, 234 | ) -> SmallOrdMap::Definition>; 235 | 236 | fn op_relations(&self, opid: Opid, up_to: u16) -> OpRels; 237 | 238 | /// Adds operation id and witness components, registers witness as `Archived`. 239 | /// 240 | /// If the anchor (client-side witness) is already present, MUST update the anchor. 241 | fn add_witness( 242 | &mut self, 243 | opid: Opid, 244 | wid: ::WitnessId, 245 | published: &::Published, 246 | anchor: &::Client, 247 | status: WitnessStatus, 248 | ); 249 | 250 | fn add_seals( 251 | &mut self, 252 | opid: Opid, 253 | seals: SmallOrdMap::Definition>, 254 | ); 255 | 256 | /// # Panics 257 | /// 258 | /// If the witness is not known 259 | fn update_witness_status( 260 | &mut self, 261 | wid: ::WitnessId, 262 | status: WitnessStatus, 263 | ); 264 | 265 | /// Commits information about all updated witness statuses ("mine" structure) to the 266 | /// persistence as a new database transaction. 267 | /// 268 | /// # Nota bene 269 | /// 270 | /// It is required to call this method after each witness update or consignment consumption. 271 | /// If the method was not called, the data won't persist, and on termination the program will 272 | /// panic. 273 | fn commit_transaction(&mut self); 274 | } 275 | 276 | #[cfg(test)] 277 | mod tests { 278 | #![cfg_attr(coverage_nightly, coverage(off))] 279 | 280 | use super::*; 281 | 282 | #[test] 283 | fn witness_status_bytes() { 284 | assert_eq!( 285 | WitnessStatus::Genesis, 286 | [0xFFu8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF].into() 287 | ); 288 | assert_eq!(<[u8; 8]>::from(WitnessStatus::Genesis), [ 289 | 0xFFu8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF 290 | ]); 291 | } 292 | 293 | #[test] 294 | fn witness_status_ordering() { 295 | assert!(WitnessStatus::Genesis.is_better(WitnessStatus::Mined(NonZeroU64::new(1).unwrap()))); 296 | assert!(WitnessStatus::Mined(NonZeroU64::new(10).unwrap()) 297 | .is_better(WitnessStatus::Mined(NonZeroU64::new(100).unwrap()))); 298 | assert!( 299 | WitnessStatus::Mined(NonZeroU64::new(1).unwrap()).is_better(WitnessStatus::Tentative) 300 | ); 301 | assert!(WitnessStatus::Tentative.is_worse(WitnessStatus::Offchain)); 302 | assert!(WitnessStatus::Offchain.is_better(WitnessStatus::Archived)); 303 | assert!(WitnessStatus::Archived.is_worse(WitnessStatus::Genesis)); 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /src/popls/mod.rs: -------------------------------------------------------------------------------- 1 | // Standard Library for RGB smart contracts 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Designed in 2019-2025 by Dr Maxim Orlovsky 6 | // Written in 2024-2025 by Dr Maxim Orlovsky 7 | // 8 | // Copyright (C) 2019-2024 LNP/BP Standards Association, Switzerland. 9 | // Copyright (C) 2024-2025 LNP/BP Laboratories, 10 | // Institute for Distributed and Cognitive Systems (InDCS), Switzerland. 11 | // Copyright (C) 2025 RGB Consortium, Switzerland. 12 | // Copyright (C) 2019-2025 Dr Maxim Orlovsky. 13 | // All rights under the above copyrights are reserved. 14 | // 15 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 16 | // in compliance with the License. You may obtain a copy of the License at 17 | // 18 | // http://www.apache.org/licenses/LICENSE-2.0 19 | // 20 | // Unless required by applicable law or agreed to in writing, software distributed under the License 21 | // is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 22 | // or implied. See the License for the specific language governing permissions and limitations under 23 | // the License. 24 | 25 | //! Proof of publication layers supported by RGB. 26 | 27 | #[cfg(any(feature = "bitcoin", feature = "liquid"))] 28 | pub mod bp; 29 | #[cfg(feature = "prime")] 30 | pub mod prime; 31 | -------------------------------------------------------------------------------- /src/popls/prime.rs: -------------------------------------------------------------------------------- 1 | // Standard Library for RGB smart contracts 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Designed in 2019-2025 by Dr Maxim Orlovsky 6 | // Written in 2024-2025 by Dr Maxim Orlovsky 7 | // 8 | // Copyright (C) 2019-2024 LNP/BP Standards Association, Switzerland. 9 | // Copyright (C) 2024-2025 LNP/BP Laboratories, 10 | // Institute for Distributed and Cognitive Systems (InDCS), Switzerland. 11 | // Copyright (C) 2025 RGB Consortium, Switzerland. 12 | // Copyright (C) 2019-2025 Dr Maxim Orlovsky. 13 | // All rights under the above copyrights are reserved. 14 | // 15 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 16 | // in compliance with the License. You may obtain a copy of the License at 17 | // 18 | // http://www.apache.org/licenses/LICENSE-2.0 19 | // 20 | // Unless required by applicable law or agreed to in writing, software distributed under the License 21 | // is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 22 | // or implied. See the License for the specific language governing permissions and limitations under 23 | // the License. 24 | -------------------------------------------------------------------------------- /src/stl.rs: -------------------------------------------------------------------------------- 1 | // Standard Library for RGB smart contracts 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Designed in 2019-2025 by Dr Maxim Orlovsky 6 | // Written in 2024-2025 by Dr Maxim Orlovsky 7 | // 8 | // Copyright (C) 2019-2024 LNP/BP Standards Association, Switzerland. 9 | // Copyright (C) 2024-2025 LNP/BP Laboratories, 10 | // Institute for Distributed and Cognitive Systems (InDCS), Switzerland. 11 | // Copyright (C) 2025 RGB Consortium, Switzerland. 12 | // Copyright (C) 2019-2025 Dr Maxim Orlovsky. 13 | // All rights under the above copyrights are reserved. 14 | // 15 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 16 | // in compliance with the License. You may obtain a copy of the License at 17 | // 18 | // http://www.apache.org/licenses/LICENSE-2.0 19 | // 20 | // Unless required by applicable law or agreed to in writing, software distributed under the License 21 | // is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 22 | // or implied. See the License for the specific language governing permissions and limitations under 23 | // the License. 24 | 25 | use bp::bc::stl::bp_consensus_stl; 26 | use bp::seals::TxoSeal; 27 | use bp::stl::bp_core_stl; 28 | use commit_verify::stl::commit_verify_stl; 29 | use hypersonic::aluvm::alu::stl::aluvm_stl; 30 | use hypersonic::aluvm::zkstl::finite_field_stl; 31 | use hypersonic::stl::{sonic_stl, usonic_stl}; 32 | use rgb::LIB_NAME_RGB; 33 | use single_use_seals::SealWitness; 34 | use strict_types::stl::{std_stl, strict_types_stl}; 35 | use strict_types::typelib::LibBuilder; 36 | use strict_types::{CompileError, TypeLib}; 37 | 38 | use crate::popls::bp::PrefabBundle; 39 | use crate::Consignment; 40 | 41 | /// Strict types id for the library providing data types for RGB types. 42 | pub const LIB_ID_RGB: &str = 43 | "stl:oh9dhz11-~Sz2azs-gn3EUNv-LJBaJwr-W2lDlCh-7XgOcTg#joker-austin-null"; 44 | 45 | #[allow(clippy::result_large_err)] 46 | fn _rgb_seals() -> Result { 47 | LibBuilder::with(libname!("SingleUseSeals"), [ 48 | std_stl().to_dependency_types(), 49 | commit_verify_stl().to_dependency_types(), 50 | bp_consensus_stl().to_dependency_types(), 51 | bp_core_stl().to_dependency_types(), 52 | ]) 53 | .transpile::>() 54 | .compile() 55 | } 56 | 57 | #[allow(clippy::result_large_err)] 58 | fn _rgb_stl() -> Result { 59 | LibBuilder::with(libname!(LIB_NAME_RGB), [ 60 | std_stl().to_dependency_types(), 61 | strict_types_stl().to_dependency_types(), 62 | commit_verify_stl().to_dependency_types(), 63 | aluvm_stl().to_dependency_types(), 64 | finite_field_stl().to_dependency_types(), 65 | usonic_stl().to_dependency_types(), 66 | sonic_stl().to_dependency_types(), 67 | bp_consensus_stl().to_dependency_types(), 68 | bp_core_stl().to_dependency_types(), 69 | rgb_seals().to_dependency_types(), 70 | ]) 71 | .transpile::>() 72 | .transpile::() 73 | .compile() 74 | } 75 | 76 | /// Generates a version of SingleUseSeal strict type library specific for RGB. 77 | pub fn rgb_seals() -> TypeLib { _rgb_seals().expect("invalid strict type SingleUseSeals library") } 78 | 79 | /// Generates a strict type library providing data types for RGB types. 80 | pub fn rgb_stl() -> TypeLib { _rgb_stl().expect("invalid strict type RGB library") } 81 | 82 | #[cfg(test)] 83 | mod test { 84 | #![cfg_attr(coverage_nightly, coverage(off))] 85 | 86 | use super::*; 87 | 88 | #[test] 89 | fn lib_id() { 90 | let lib = rgb_stl(); 91 | assert_eq!(lib.id().to_string(), LIB_ID_RGB); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/stockpile.rs: -------------------------------------------------------------------------------- 1 | // Standard Library for RGB smart contracts 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Designed in 2019-2025 by Dr Maxim Orlovsky 6 | // Written in 2024-2025 by Dr Maxim Orlovsky 7 | // 8 | // Copyright (C) 2019-2024 LNP/BP Standards Association, Switzerland. 9 | // Copyright (C) 2024-2025 LNP/BP Laboratories, 10 | // Institute for Distributed and Cognitive Systems (InDCS), Switzerland. 11 | // Copyright (C) 2025 RGB Consortium, Switzerland. 12 | // Copyright (C) 2019-2025 Dr Maxim Orlovsky. 13 | // All rights under the above copyrights are reserved. 14 | // 15 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 16 | // in compliance with the License. You may obtain a copy of the License at 17 | // 18 | // http://www.apache.org/licenses/LICENSE-2.0 19 | // 20 | // Unless required by applicable law or agreed to in writing, software distributed under the License 21 | // is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 22 | // or implied. See the License for the specific language governing permissions and limitations under 23 | // the License. 24 | 25 | use core::error::Error as StdError; 26 | 27 | use amplify::MultiError; 28 | use hypersonic::{CodexId, ContractId, Stock}; 29 | use rgb::RgbSeal; 30 | use strict_encoding::StrictDecode; 31 | 32 | use crate::{ 33 | Articles, Consensus, Consignment, ConsumeError, Contract, CreateParams, Issuer, IssuerError, 34 | Pile, 35 | }; 36 | 37 | /// Stockpile provides a specific persistence implementation for the use in [`crate::Contracts`]. 38 | /// It allows for it to abstract from a specific storage media, whether it is a file system, 39 | /// database, or a network service. Its main task is to load already known contract issuers and 40 | /// contract runtimes ([`Contract`]); or to add new ones via the [`Self::import_issuer`] and 41 | /// [`Self::issue`] procedures. 42 | /// 43 | /// # Reading contracts 44 | /// 45 | /// Since there might be many thousands of contracts, loading all of them at once may not make a 46 | /// sense performance-one. So the contracts are instantiated one-by-one, using the 47 | /// [`Self::contract`] method. 48 | /// 49 | /// To iterate over all known contracts, a specific [`crate::Contracts`] implementation should 50 | /// iterate over contract ids, present in the system, and instantiate them one by one. This allows 51 | /// the full use of Rust iterators, including instantiating contracts in "pages" of a certain size 52 | /// (by using [`Iterator::skip`] and [`Iterator::take`]) etc. 53 | pub trait Stockpile { 54 | /// Specific stock runtime used by [`Contract`]s instantiated by this stockpile. 55 | type Stock: Stock; 56 | /// Specific pile runtime used by [`Contract`]s instantiated by this stockpile. 57 | type Pile: Pile; 58 | /// Errors happening during storage procedures. 59 | type Error: StdError; 60 | 61 | fn consensus(&self) -> Consensus; 62 | fn is_testnet(&self) -> bool; 63 | 64 | fn issuers_count(&self) -> usize; 65 | fn contracts_count(&self) -> usize; 66 | 67 | fn has_issuer(&self, codex_id: CodexId) -> bool; 68 | fn has_contract(&self, contract_id: ContractId) -> bool; 69 | 70 | fn codex_ids(&self) -> impl Iterator; 71 | fn contract_ids(&self) -> impl Iterator; 72 | 73 | fn issuer(&self, codex_id: CodexId) -> Option; 74 | fn contract(&self, contract_id: ContractId) -> Option>; 75 | 76 | fn import_issuer(&mut self, issuer: Issuer) -> Result; 77 | 78 | fn import_contract( 79 | &mut self, 80 | articles: Articles, 81 | consignment: Consignment<::Seal>, 82 | ) -> Result< 83 | Contract, 84 | MultiError< 85 | ConsumeError<<::Seal as RgbSeal>::Definition>, 86 | ::Error, 87 | ::Error, 88 | >, 89 | > 90 | where 91 | <::Seal as RgbSeal>::Client: StrictDecode, 92 | <::Seal as RgbSeal>::Published: StrictDecode, 93 | <::Seal as RgbSeal>::WitnessId: StrictDecode; 94 | 95 | fn issue( 96 | &mut self, 97 | params: CreateParams<<::Seal as RgbSeal>::Definition>, 98 | ) -> Result< 99 | Contract, 100 | MultiError::Error, ::Error>, 101 | >; 102 | 103 | fn purge(&mut self, contract_id: ContractId) -> Result<(), Self::Error>; 104 | } 105 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | // Standard Library for RGB smart contracts 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Designed in 2019-2025 by Dr Maxim Orlovsky 6 | // Written in 2024-2025 by Dr Maxim Orlovsky 7 | // 8 | // Copyright (C) 2019-2024 LNP/BP Standards Association, Switzerland. 9 | // Copyright (C) 2024-2025 LNP/BP Laboratories, 10 | // Institute for Distributed and Cognitive Systems (InDCS), Switzerland. 11 | // Copyright (C) 2025 RGB Consortium, Switzerland. 12 | // Copyright (C) 2019-2025 Dr Maxim Orlovsky. 13 | // All rights under the above copyrights are reserved. 14 | // 15 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 16 | // in compliance with the License. You may obtain a copy of the License at 17 | // 18 | // http://www.apache.org/licenses/LICENSE-2.0 19 | // 20 | // Unless required by applicable law or agreed to in writing, software distributed under the License 21 | // is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 22 | // or implied. See the License for the specific language governing permissions and limitations under 23 | // the License. 24 | 25 | use core::str::FromStr; 26 | 27 | use hypersonic::ContractId; 28 | use strict_encoding::TypeName; 29 | 30 | #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Display, From)] 31 | #[display(inner)] 32 | pub enum ContractRef { 33 | #[from] 34 | Id(ContractId), 35 | 36 | #[from] 37 | #[from(&'static str)] 38 | Name(TypeName), 39 | } 40 | 41 | #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Display, Error)] 42 | #[display("invalid contract reference '{0}'")] 43 | pub struct InvalidContractRef(String); 44 | 45 | impl FromStr for ContractRef { 46 | type Err = InvalidContractRef; 47 | 48 | fn from_str(s: &str) -> Result { 49 | if let Ok(id) = ContractId::from_str(s) { 50 | Ok(ContractRef::Id(id)) 51 | } else if let Ok(name) = TypeName::from_str(s) { 52 | Ok(ContractRef::Name(name)) 53 | } else { 54 | Err(InvalidContractRef(s.to_owned())) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /stl/RGB@0.12.0.sta: -------------------------------------------------------------------------------- 1 | -----BEGIN STRICT TYPE LIB----- 2 | Id: stl:oh9dhz11-~Sz2azs-gn3EUNv-LJBaJwr-W2lDlCh-7XgOcTg#joker-austin-null 3 | Name: RGB 4 | Dependencies: 5 | UltraSONIC#korea-helena-moral, 6 | SONIC#river-atomic-dallas, 7 | BPCore#point-state-colony, 8 | Bitcoin#postal-sunday-basket, 9 | FiniteField#report-canal-convert, 10 | Std#delete-roman-hair, 11 | SingleUseSeals#marvin-social-senior, 12 | AluVM#reward-accent-swim, 13 | CommitVerify#biology-news-adam, 14 | StrictTypes#henry-heart-survive 15 | Check-SHA256: da8ff7663a1d0211423726e67c26f4afb5dcd149bbbbd6f6667cd334504b768a 16 | 17 | 15!sq3IycLmIL91_#B1EIwa`nbitx=l+v|DM?bg#xxRGf0}54abaG)+Pfkfg6zRzSOtEdt-T7+2#x=L@ 18 | o2tzX&8T6A@C!|J?zm~s1yfH>NkbR=SrgsV=Hx{{t|<`s&oomhOVCHFn=U4_9oEMdibn=QP(yEWWg3}O 19 | T5|07nO2GM-L7QDgT$BQqMzOqn>5lhi@)vc@&`g`bYpL6ZZ>T)gx(ddJ<(3FEnI}P;o?=);Zw&m&4m_J 20 | =Y$s0*b7E!ZfSI7MrmbiWP*w7O%DrjRIhYP1?a)oog)LLTw}}6rDvG=`c^zKY6DYrWQEvx)T9vaM@yWL 21 | &u3sFniRbuxM)(+AnYOL>?CMjs}56XZf9&|RdZ!iWnpY{w^=E)kF7_+BGG1^(r$N1t7sZ7Knlpb7_LPz 22 | v?=G$1wm|eR!zWtaF}ei((p}N63i?Ao=!1xa&ZNo1!mRRS&fT*&Z= 25 | qeY@Wmfle*z!SF)@h8|JkU^FEQwjx4X<|uayuqr7GQ?L&0*1(_efZK}CC9@|^eNQv3l{;DCYdNN3PW#l 26 | WkYXnW@%^E^W8yOb6|fgT149yr~sO2Tv2Na_ZHTZ79ax%L5`ybOle|MX>?@D 29 | 6U0+eW+%HuC5$^~^vuG3{`}-8x6fV={eh1!etXz_4^&}ra%FT-VRUFva&K>DH8-hI70Bv^+*0?ef%0)> 30 | Q3WPbltNdpi4*91)SI!>2Tf&jb75y?I6q=8aZ}RBA(1@GcO9QSWZ!o3C{GZewU~a&n&FfTQMZ~2NyZ3tIXcyCi>VQiTz7!+_r5}8}Xz#=C$!zUV8ZCE(`G?<`j;GLD={4WYrcyCi> 33 | VQfWsblE0E#HD_EesHd`SUkg!KeYrbuZfyavGoJ9ePLf94GThbZe(m_P;zf?X5L1cslj2DB8{G=?&*rT 34 | Co3$n0*OoGCfiQuT=HJbxCKscX>(-%$n#j0HLDJN5-IKgM_J7b(p 36 | +0MPGk2Gl)y2(Rz1Xgc#bS10xxe^o?x}!PNUwajGr*TW+dUY6G&@nZ7)X6RBh6__;a%pgMLV0v$b1}QF 37 | =!A)P#jpo4axu-4_As_7EzOC4+`8Vyy2R;!*$Y%*a87SzWk_LjXf@g$6*(YoyWQNR!##&F>hhbX+H~JN 38 | $bujoP8PMf90*KrV{24tZDlxh1hGqe4n}Q9o)<@bBy=Qy_yc6@Jxi+hIw1E!bZZGyX=hVoa%pgMU#!_} 39 | aHwX>fFqJ7jQqgpV60Q!3=n#-@oxpi@}5@PW%F 40 | v%B~$o;&jeRCrHybeHwl212eXGm<4cs7@Wu#FOK{KGSirhjWHCPRxjcYXnnabaS0mldQV=&ET6jM)-pX 41 | anm@-FK%_beB&TRo~t++rXB}ZX>@L7b91ADLi5Yl(a@n1+Ku60FILp}Zw|!7cE!MGSxid=WmW`Kcxhy) 42 | e8zcXXXRJdMCHk1I^Yb;mDw5%F9Y9nz;zN&zQ>*gRCrcpa;b%ccT=8d`>?<6$C@F;S3|*6`1-v+nBdcq 43 | J?FPKcnV2wbY*gGVQf%qwlfK-7{9iX4Q|L-q$GzUMp|h#f#zWg5iW~CYZ 44 | WKwE66NffESNf 45 | Jhg}MqX$BHbY)X?a>aWgn!oosZgNI|twmNZeC(lYZa*g7-2eQ3Yy;-pL;_TJ=xRXCTqXIv;)MTcr4cfx 46 | K`S9uy$)6q!N22#m0-mN2v2o%aBpdDbo`>HD!!5a&4Q@0n2=*4!cKOosx|T?(Q^f3pcpQQSqE58Zfto_ 47 | YYangZEb0ER%LQ&W_btz17eyujlDm-l!bcJ4l3enMo5^RQ~n#iGkyWd8~8$g22EvjXm4aNm44Lkd(R(OIzR#{i@7s0dGYZe?UaaA_gO7j4igKpjn9r^|;p@vV@r5S*`M7yxdMqH9J{cMAtoX=g%g 55 | Z(=7}jqCj3uBBi`PSvC}Y1my#{`B|0#NGCpHQ}XJ>L{XJK@2axS^!!$La8vccxVbqEugXKc2D 56 | C3=r}Pe0HE0o1nE@d;C9ZDDS7X=8IXfh_`cmLHdq1#e;Jyg%mhBiyuV`WV#u@Rcc+oi?cnM{I9mVQfKg 57 | X+dX_ah@P3qvF}zw2RZm<~&9E@=d94{TY2?K@}`*atu>+VRU6eb!lv5Wpa`4Sh2=pqY5<1(br?Xy7E4w 58 | 1fUIUAA#7JB!_)M-s25ZbYXO5Q)O&rV{~tFr{i_kQw#cT;QeDG!ck3nXYV}e%RXSJwOCg5aE-4A3{qit 59 | LvL<&WpZ?HaDVRU6da%psEZP#zCcYl!udKzn6RYBAzQ=MVTbvT3l6Ne)wVVRU6fZ*F#Fa&&KU+~Bu-$2D^R=g&OsBR86Kfn-W4H>;%M$&Z5j 61 | Y5(0N2}5CQY*Tb$bY=L@o=5~3LSCrmQ43`s_RA=%Mpo4G*Vtem$&wnH2W|sFaB2HtRyk`k?kWEQdWrTz 62 | a1nu+B|GJQ%{OSD#2Xf2lLZP=VRu4xX>4R=a{JDmbHJ&K=73;xT9e8zPrRwVe+qg8W^KOf5Mb-Ek_}UJ 63 | VnJtTa%E>>bZ>GFQ)zBzY-Lq*Wm9EgY;yqs57DuXjCou?xUqOe6>SbrJsQhU^}Jd!P)(;Q*wp<05>sVi 64 | Y*%S?Ze??GRCsSwWnpXsQ*>ko07$+g7b@t4MVjY>G@u4Q3HlB(d+LiLJm-R=h;`?dxDG*cV`*tna%paK 65 | VPb4$VTK~nd#>-Vi}-aA;vuZDDL|OmAdib7%`wbaH89bX0k8WpfVz35LOoBKkGaY9#cS 69 | 7Qj{WgyAGcS>>g~&^g7Kv@4tY;$BCg=lGO40qbyjMBe4%@A^HhWa%pX8bZK^FG5w(M*PErPQ*K8) 71 | );4q9;G_%)IzXn}g(wG03t}BM5{Lay5>;+)VQpn(MrmbiWOGwxZAoNn1fvw5rj-B|XP@r^w5ufb=C_Ju$l1`nW&GEp 74 | SWb-vQ)O*QWPQm(C)8p9*(R2SB=5|9lKCV3N0b-?Ol>0MdKRd5P6t+Da%o|1bb-?>B-g{}GTFmo{mAr> 75 | kexq=D7-RGP2^0W;fb3W1_o1UdTDNFlG6hDK5~2WhJ*PG7zYWLxz$!}&%4AY&2YWlsz$Eb5KdujWn@Na 76 | Wo%?~Q)O*QWS1d>s?i)zLD2{^84?*=6Qgx+2Ybs0 77 | Lm?xkSqB0NLAl2~i-ZdPG(X<=@3b5mt)No4(ju7iFH2b-u)>&PZdlOljoA7|k;k>s6qoa5|8f~g8rd2nS@ 79 | d2@7SZ3Wyf+x%_dM<2`2?)BDo=Mx%~5d>KuFr|;-a1xapjb#y8q<#qu_#(K#)`wcVHr!+M;CFW=`nIc;WmaKqb!CaM+H2qZPeA#eSuS;MlVD&E80+f~tBjF2L5xp4$G8|HaaRE?_1xa&rb!C|Z{T?dQJ?d;o1M7&`K~vb; 86 | q0WUv*UkDBz!||TdTRtvaA{h5j^*S;NLvL<$a$#e1No2E$ 87 | IM0+Zf@L2l@d@(MQJsr54hK$(!e6HQqr!PDy=MtQb#!P{Z);_4wXEwu(4(eXD|DxJ;;J@Cth|EksiOt- 88 | HPJ-=lHfc*2SaaUWq3(sw&;L{94K`ndk%K5+?9Jv$dw7jc}U5p5@2#$kUJ%u2uWmRZggpMdE)$3b;8=t 89 | WpRVu##lfU`rB@;{0!s$QI%|;TCcBl2MJSjVRU6fWo&HcSCMQ+L5+U?RjosJMsRzTDrVTi&VRUJ4Zu_Oja$v6rX_fBJi~iN-tVcdiDm&03^NQ+Jc&xL55C}tMY-~YfWODsk!@}uY;P9TP 91 | (d!7@of`=KIP8iA99GVIJ589_dfo?8VRuAfbYTSm7(;Jvb7^O8ZDnqBNM&JUWpY$_Z&PJqYz7AkWq5RD 92 | ZgXjGZgT(#00;m8KmY&$000000RR600000000000000000RI3000000010ztZDDS7X=8H&6zRzSOtEdt 93 | -T7+2#x=L@o2tzX&8T6A@C!|J?zm~sF1h2wLORE?!REtt2osrSY_@_WdXITeKhOjL)V9{~19NF-00aU6 94 | 1a5C`WdHyG0R(ezZDjxj0Rj~1$p1{SZOh&HYQM%cx9*#&%?-_{VTbSwO?2+KY0n|Y7j4igKpjn9r^|;p 95 | @vV@r5S*`M7yxdMqH9J{cMAn+b8~fN0tDpDmIL91_#B1EIwa`nbitx=l+v|DM?bg#xxRGf1CTP<{Q=-M 96 | !C>_+*aDQ63M1hQS`ob&12P;^gK+^+j0|UGZe??6b5mtuY;yn#000647yDTg-PGpfML@185ctnDQz=W( 97 | N2{AICbS*a#}S1Y?PjfbGv}h@kbSP(#awZ_lQ0`HDzw0000000000|Nj60000002X|?7 98 | Ze??G0>FK6m~6Jv@J(A1%q#z%L9jBiOaFR-W)8f_JK`4j{gd}#cyL4!ji%3ykIV>*WnpY{00;r7E$J0TK-?kY^pVmc=H-hx8Lg|N&;`vAB2#zu_JsNX 101 | 0000000030|Ns900000LPjF>&VRUJ4Zc}ApY;#n2Z&PJqYz6}fZ*XODVRUJ4ZUO}4%$5V;h4>tW$T}qG 102 | >2$%Oag@@vMMpol0J*+&<^$(fk!(gmjeh=BtwVQ4aC?<1X4t~xlZ)#2*W}^V#()fDWoBt^Wn@!jVQg~% 103 | 3IG5C0vG#P6W!G2P2G*c-{&_}DAE+(`c*2fl#M{=IvfTQM 104 | Z~2NyZ2$lO000000RR600000000(zzbZ%vHa{vSa00eGtZe;)f009JZZ*64&1pxwu*m%^W5bsAzoRH6F 105 | U?Q3ny&<@0QqmyoA?EBPXkM!i(Xoz5lhi@)vc 108 | @*(3sL&d6G@+l`%qd385?K@+fP1(-9sgE>i7rMzqbpQYW000000RR6000000010n!WpZJ3X>V=<1mw(? 109 | 1L1}E9EHd_Bi5^=;nl`~3{Y}q 110 | W?^DNb#7#AWd#8M2?62M@pewAjJW8t+(xcVxOk%Mdo#h0gi}QNpx(N&bbSB-000000096000000000 111 | 112 | -----END STRICT TYPE LIB----- 113 | 114 | -------------------------------------------------------------------------------- /stl/RGB@0.12.0.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RGB-WG/rgb-std/a79d48ed6d919dc81aa9562fc77b6f6bf2a7dd65/stl/RGB@0.12.0.stl -------------------------------------------------------------------------------- /stl/RGB@0.12.0.sty: -------------------------------------------------------------------------------- 1 | {- 2 | Id: stl:oh9dhz11-~Sz2azs-gn3EUNv-LJBaJwr-W2lDlCh-7XgOcTg#joker-austin-null 3 | Name: RGB 4 | Version: 0.12.0 5 | Description: RGB smart contracts library 6 | Author: Dr Maxim Orlovsky 7 | Copyright (C) 2024-2025 LNP/BP Labs, Institute for Distributed and Cognitive Systems, Switzerland. 8 | All rights reserved. 9 | License: Apache-2.0 10 | -} 11 | 12 | @context 13 | typelib RGB 14 | 15 | import UltraSONIC#korea-helena-moral 16 | use StateData#mission-first-owner 17 | use Input#broken-equal-delphi 18 | use Consensus#nissan-congo-clark 19 | use ContractName#value-reform-lesson 20 | use Genesis#minute-prism-small 21 | use ContractMeta#element-novel-salute 22 | use CellLock#jasmine-demo-resume 23 | use StateValue#kiwi-million-ford 24 | use Codex#adam-mega-analyze 25 | use Issue#finance-peru-havana 26 | use Opid#storm-dragon-brain 27 | use ContractId#uniform-welcome-papa 28 | use AuthToken#western-america-patrol 29 | use CodexId#cargo-season-impact 30 | use Identity#smart-pioneer-nominal 31 | use StateCell#tarzan-jordan-camera 32 | use Operation#canada-habitat-heart 33 | use CellAddr#lecture-vincent-carbon 34 | use RawData#lima-anvil-karate 35 | 36 | import SONIC#river-atomic-dallas 37 | use OwnedApi#volume-chess-model 38 | use SigBlob#insect-cello-avalon 39 | use Aggregator#europe-paradox-empire 40 | use Semantics#salami-sigma-micro 41 | use GlobalApi#break-pizza-polite 42 | use StateBuilder#reverse-delphi-camel 43 | use StateSelector#benny-marina-fashion 44 | use RawConvertor#result-right-amazon 45 | use StateArithm#pablo-cotton-mirror 46 | use StateConvertor#beach-congo-instant 47 | use CallState#sparta-ginger-analyze 48 | use Api#status-russian-bandit 49 | use RawBuilder#tropic-brenda-reply 50 | use SubAggregator#pastel-orion-hilton 51 | 52 | import BPCore#point-state-colony 53 | use TapretNodePartner#roger-member-educate 54 | use WOutpoint#clock-absorb-side 55 | use TapretProof#marco-border-sample 56 | use TapretPathProof#kiwi-mirror-paris 57 | use Message#druid-blitz-rover 58 | use TapretRightBranch#miracle-patriot-touch 59 | use Anchor#domino-bank-grand 60 | use WTxoSeal#nadia-rent-sofia 61 | use TxoSealExt#beach-subject-abraham 62 | use BundleProof#user-nadia-stone 63 | use Noise#bruce-gravity-titanic 64 | 65 | import Bitcoin#postal-sunday-basket 66 | use SeqNo#copper-verbal-ingrid 67 | use TxIn#slang-cherry-gizmo 68 | use Vout#brush-gloria-heroic 69 | use ScriptBytes#equator-cockpit-gong 70 | use TapNodeHash#paprika-amanda-hunter 71 | use LockTime#lobster-liberal-jump 72 | use SigScript#neptune-spiral-sample 73 | use LeafScript#bison-doctor-oscar 74 | use TxOut#aspect-eddie-message 75 | use Sats#metro-picasso-roger 76 | use Witness#engine-daniel-magnum 77 | use Txid#shallow-light-reverse 78 | use TxVer#nepal-symbol-uniform 79 | use InternalPk#habitat-paprika-oliver 80 | use LeafVer#benefit-carbon-africa 81 | use ScriptPubkey#second-lobster-philips 82 | use ByteStr#royal-anatomy-june 83 | use Tx#radar-salon-page 84 | use Outpoint#logo-alamo-madam 85 | use XOnlyPk#clever-swim-carpet 86 | 87 | import FiniteField#report-canal-convert 88 | use Fe256#palace-mixer-visual 89 | 90 | import Std#delete-roman-hair 91 | use AsciiPrintable#ultra-sunset-format 92 | use Bool#oxygen-complex-duet 93 | use AlphaCapsNum#aladdin-zebra-marble 94 | use AlphaNumLodash#percent-bingo-caesar 95 | use AlphaCapsLodash#duet-hammer-labor 96 | use AlphaSmallLodash#pioneer-eagle-spell 97 | 98 | import SingleUseSeals#marvin-social-senior 99 | use SealWitnessTxoSeal#logic-angel-bravo 100 | 101 | import AluVM#reward-accent-swim 102 | use Lib#report-gordon-recycle 103 | use IsaId#mobile-letter-absorb 104 | use LibId#germany-culture-olivia 105 | use CoreConfig#ventura-ibiza-special 106 | use LibSite#polo-macro-elite 107 | 108 | import CommitVerify#biology-news-adam 109 | use Method#subject-justin-cowboy 110 | use ProtocolId#shadow-eclipse-program 111 | use MerkleHash#horse-popcorn-bundle 112 | use MerkleProof#austria-jaguar-donald 113 | use ReservedBytes1#origin-roger-relax 114 | use ReservedBytes2#florida-libra-circus 115 | use ReservedBytes6#joker-peru-brave 116 | use ReservedBytes4#young-goblin-academy 117 | 118 | import StrictTypes#henry-heart-survive 119 | use VariantName#theory-austin-before 120 | use FieldName#present-flute-herman 121 | use Primitive#deliver-arrow-boxer 122 | use TySemId#popcorn-super-young 123 | use FieldSemId#spiral-road-marco 124 | use TypeName#edgar-carol-mystery 125 | use UnnamedFieldsSemId#freedom-degree-gregory 126 | use SemId#logic-absorb-hilton 127 | use Variant#humor-regard-promise 128 | use Sizing#courage-alien-salon 129 | use NamedFieldsSemId#solar-salad-smoke 130 | use EnumVariants#dispute-natasha-vega 131 | use VariantInfoSemId#museum-edward-mirror 132 | use UnionVariantsSemId#santana-address-pepper 133 | use TypeSystem#adrian-boris-sponsor 134 | 135 | 136 | @mnemonic(apple-sponsor-jessica) 137 | data ConsignmentHeaderTxoSeal : extensions [[Byte] ^ ..0xff] 138 | , semantics SONIC.Semantics 139 | , sig SONIC.SigBlob? 140 | , issue UltraSONIC.Issue 141 | , genesisSeals {U16 -> BPCore.WTxoSeal} 142 | , witness CommitVerify.ReservedBytes1 143 | , opCount U32 144 | 145 | @mnemonic(system-violin-side) 146 | data ConsignmentTxoSeal : header ConsignmentHeaderTxoSeal, operationSeals [OperationSealsTxoSeal ^ ..0xffffffff] 147 | 148 | @mnemonic(sharp-think-license) 149 | data OperationSealsTxoSeal : operation UltraSONIC.Operation 150 | , definedSeals {U16 -> BPCore.WTxoSeal} 151 | , witness SingleUseSeals.SealWitnessTxoSeal? 152 | 153 | @mnemonic(serpent-cheese-parole) 154 | data Prefab : closes {Bitcoin.Outpoint} 155 | , defines {Bitcoin.Vout} 156 | , operation UltraSONIC.Operation 157 | 158 | @mnemonic(civil-triton-outside) 159 | data PrefabBundle : {Prefab} 160 | 161 | 162 | -------------------------------------------------------------------------------- /stl/SingleUseSeals@0.12.0.sta: -------------------------------------------------------------------------------- 1 | -----BEGIN STRICT TYPE LIB----- 2 | Id: stl:hdh41KQQ-70dLnJD-PZ2Aimh-S9IbhoU-tIg7CHm-7CRoXqs#marvin-social-senior 3 | Name: SingleUseSeals 4 | Dependencies: 5 | BPCore#point-state-colony, 6 | Bitcoin#postal-sunday-basket, 7 | CommitVerify#biology-news-adam 8 | Check-SHA256: f39e6cada67f6d6f55f044acd34a6911cdb847316563b202ebf350e338065f2a 9 | 10 | 4pV7vXKZCvb7fOyVQg~)7yDTg-PGpfML@185ctnDQz=W(N2{AICbS*a#}WN(ld*{?d?X)a%pCH0|r7+LvM0r2LJ(l2VDR_OBR)w8yCZ2EylR&t_^>1Sz?kFbz0>alMxYAVQ_L~bWU$% 13 | Wl&*qbZ%vG54IneKN{_;j(f`H9IfkFzO$PGD6U0+eW+%HuC5$^~ 14 | ^vuG3{`}-8x6fV={eh1!etXz_4^&}ra%FT-VRUFva&K>DH8-hI70Bv^+*0?ef%0)>Q3WPbltNdpi4*91 15 | )SI!>2Tf&jb75y?I6q=8aZ}RBA(1@GcO9QSWZ!o3C{GZewU~a@i(C#HD_EesHd`SUkg!KeYrbuZfyavGoJ9 17 | ePLf94GThbZe(m_P;zf?W(PuPbYpL6ZWI6k8Eu6r$oASqO%+a!oQ%Dm4~>ZeT05|jA;vvYupWm6Q)O{Z 18 | ZweWZ*HiKem1Z9kJM|+S+XYD&bY*ifyRPVjiFd`Y 20 | 2QhLn&64&owka*miGSR>-o?7a>3`V^RAF#VZ)9aiVRL9T+8q@+Aa1+e+@!-jhcW8%o2S}z-#y5JARJB> 21 | wYeM!OmAarRB3HxICTWEOMDJSZAYFLM|~u8B!Bn=Wb8dls`ok|_d#@P2~%ljQ)6;zaCBd+*=^-NPQ?`2 22 | v5jYd+6t@dEhY>7H!Y*UdZb-BpG^u(WnpGhV{&P5bdWn_aCwA}8zxgKpy7|rEn>a@Jg9&ldILR+= 24 | b-aAzAVr?5I2ooM2UlryZe??Gqk=;7%h%D+p%U7S;b1RT)c9`>#Kd;Rz-U=aO9W+B1XOrwWTLLzo&{8RR%LRjg@kugo@o29zwXDHA;ech!BqJAy+4@X(~&*rw>NkS 26 | Np5sya&BR4P;0g`38@&rwvr8Q$XKK#ha*N>X+Ls92fzOv*E(~7PRR#MWnpGkWpcj!9{gsd8U18ZYC02# 27 | KAOx^Y=h@bX*KmV{&P5bWn9-Yh`)Fa%+!|DA9Vsm&hHC4WXN2M4aZ(WL^Hp>3BS~hw-Ba 28 | LV0v$Q*?60dm);?_c?BIMu4qFRxf<)p=@qHCf(fs{C;c$=G;UARCwrWK+Rkw`Mu(V|7oQWGN(Z+AyvH& 29 | RuaL#N4?X)a%pCH1potLnmCQUKfIKMdeaUn;%i1on4VMq8@@As0m&QqLVgBKWprq7WH6O}<{e=)S-S-YClv)aMjKgwAH@`bu1x<7g|G$};xvA~n-$_S3Qc8l 32 | YiwmmVRL9ok({ALGPx>x`rRI~Y5)OGH%NCipfaP+#@?w1p3;*GO=WUxY-Lb#Z*OLk_h5K%L=laq&yA1J 33 | oJ^{7>oKLkF4~iax8KK|47hp@Qe|^xa&~28LV0v$b1?w`5>sViY*%S?Ze??GRCsSwWnpXv0ts++Vr*%1 34 | Xk}yq8ktmDa_sk+R*CW5u4Kl8#FylvpWYLjG}1GRzwPYu#d{%|zxO$Aaz=oyMOH6-?4fLKKPKJW|NMSz 35 | 1LoXB24ie#Wo~o=7yDTg-PGpfML@185ctnDQz=W(N2{AICbS*a#} 7 | Copyright (C) 2024-2025 LNP/BP Labs, Institute for Distributed and Cognitive Systems, Switzerland. 8 | All rights reserved. 9 | License: Apache-2.0 10 | -} 11 | 12 | @context 13 | typelib SingleUseSeals 14 | 15 | import BPCore#point-state-colony 16 | use TapretNodePartner#roger-member-educate 17 | use TapretProof#marco-border-sample 18 | use TapretPathProof#kiwi-mirror-paris 19 | use Message#druid-blitz-rover 20 | use TapretRightBranch#miracle-patriot-touch 21 | use Anchor#domino-bank-grand 22 | use BundleProof#user-nadia-stone 23 | 24 | import Bitcoin#postal-sunday-basket 25 | use SeqNo#copper-verbal-ingrid 26 | use TxIn#slang-cherry-gizmo 27 | use Vout#brush-gloria-heroic 28 | use ScriptBytes#equator-cockpit-gong 29 | use TapNodeHash#paprika-amanda-hunter 30 | use LockTime#lobster-liberal-jump 31 | use SigScript#neptune-spiral-sample 32 | use LeafScript#bison-doctor-oscar 33 | use TxOut#aspect-eddie-message 34 | use Sats#metro-picasso-roger 35 | use Witness#engine-daniel-magnum 36 | use Txid#shallow-light-reverse 37 | use TxVer#nepal-symbol-uniform 38 | use InternalPk#habitat-paprika-oliver 39 | use LeafVer#benefit-carbon-africa 40 | use ScriptPubkey#second-lobster-philips 41 | use ByteStr#royal-anatomy-june 42 | use Tx#radar-salon-page 43 | use Outpoint#logo-alamo-madam 44 | use XOnlyPk#clever-swim-carpet 45 | 46 | import CommitVerify#biology-news-adam 47 | use Method#subject-justin-cowboy 48 | use ProtocolId#shadow-eclipse-program 49 | use MerkleHash#horse-popcorn-bundle 50 | use MerkleProof#austria-jaguar-donald 51 | use ReservedBytes1#origin-roger-relax 52 | 53 | 54 | @mnemonic(logic-angel-bravo) 55 | data SealWitnessTxoSeal : published Bitcoin.Tx, client BPCore.Anchor 56 | 57 | 58 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | /data/*.contract 2 | /data/*.rgb 3 | /data/storage* 4 | -------------------------------------------------------------------------------- /tests/consignment.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | 3 | #[macro_use] 4 | extern crate amplify; 5 | #[macro_use] 6 | extern crate strict_types; 7 | 8 | mod utils; 9 | 10 | use std::collections::{BTreeMap, HashMap}; 11 | use std::convert::Infallible; 12 | use std::fs; 13 | use std::path::PathBuf; 14 | 15 | use bp::seals::TxoSeal; 16 | use rgb::{Consensus, Contracts, Operation}; 17 | use rgb_persist_fs::StockpileDir; 18 | 19 | use crate::utils::setup; 20 | 21 | #[test] 22 | #[should_panic(expected = "single-use seals are not closed properly with witness")] 23 | fn export_import_contract() { 24 | let contract = setup("Consign"); 25 | 26 | let filename = "tests/data/imex.rgb"; 27 | 28 | let terminals = contract.full_state().raw.auth.keys().collect::>(); 29 | 30 | fs::remove_file(filename).ok(); 31 | contract.consign_to_file(filename, terminals).unwrap(); 32 | 33 | let dir = PathBuf::from("tests/data/storage"); 34 | fs::remove_dir_all(&dir).ok(); 35 | fs::create_dir_all(&dir).ok(); 36 | let stockpile = StockpileDir::::load(dir, Consensus::Bitcoin, true).unwrap(); 37 | let mut contracts = Contracts::<_, HashMap<_, _>, HashMap<_, _>>::load(stockpile); 38 | 39 | let resolver = |_: &Operation| -> BTreeMap<_, _> { bmap![] }; 40 | 41 | contracts 42 | .consume_from_file(false, filename, resolver, |_, _, _| -> Result<_, Infallible> { 43 | unreachable!() 44 | }) 45 | .unwrap_err(); 46 | 47 | contracts 48 | .consume_from_file(true, filename, resolver, |_, _, _| -> Result<_, Infallible> { 49 | unreachable!() 50 | }) 51 | .unwrap(); 52 | 53 | contracts 54 | .consume_from_file(false, filename, resolver, |_, _, _| -> Result<_, Infallible> { 55 | unreachable!() 56 | }) 57 | .unwrap(); 58 | } 59 | -------------------------------------------------------------------------------- /tests/data/Test.issuer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RGB-WG/rgb-std/a79d48ed6d919dc81aa9562fc77b6f6bf2a7dd65/tests/data/Test.issuer -------------------------------------------------------------------------------- /tests/reorgs.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | 3 | #[macro_use] 4 | extern crate amplify; 5 | #[macro_use] 6 | extern crate strict_types; 7 | 8 | mod utils; 9 | 10 | use std::num::NonZeroU64; 11 | 12 | use bp::Tx; 13 | use rgb::WitnessStatus; 14 | use rgbcore::ContractApi; 15 | use single_use_seals::SealWitness; 16 | use strict_encoding::StrictDumb; 17 | 18 | use crate::utils::setup; 19 | 20 | #[test] 21 | fn no_reorgs() { setup("NoReorgs"); } 22 | 23 | #[test] 24 | fn single_rollback() { 25 | let mut contract = setup("SingleRollback"); 26 | let wid = contract.witness_ids().nth(50).unwrap(); 27 | contract.sync([(wid, WitnessStatus::Archived)]).unwrap(); 28 | // Idempotence 29 | contract.sync([(wid, WitnessStatus::Archived)]).unwrap(); 30 | } 31 | 32 | #[test] 33 | fn double_rollback() { 34 | let mut contract = setup("DoubleRollback"); 35 | let wid1 = contract.witness_ids().nth(50).unwrap(); 36 | let wid2 = contract.witness_ids().nth(60).unwrap(); 37 | contract 38 | .sync([(wid1, WitnessStatus::Archived), (wid2, WitnessStatus::Archived)]) 39 | .unwrap(); 40 | } 41 | 42 | #[test] 43 | fn rollback_forward() { 44 | let mut contract = setup("RollbackForward"); 45 | let wid = contract.witness_ids().nth(50).unwrap(); 46 | contract.sync([(wid, WitnessStatus::Archived)]).unwrap(); 47 | contract.sync([(wid, WitnessStatus::Offchain)]).unwrap(); 48 | // Idempotence 49 | contract 50 | .sync([(wid, WitnessStatus::Archived), (wid, WitnessStatus::Offchain)]) 51 | .unwrap(); 52 | } 53 | 54 | #[test] 55 | fn rbf() { 56 | let mut contract = setup("Rbf"); 57 | 58 | let old_txid = contract.witness_ids().nth(50).unwrap(); 59 | let opid = contract.ops_by_witness_id(old_txid).next().unwrap(); 60 | 61 | let tx = Tx::strict_dumb(); 62 | let rbf_txid = tx.txid(); 63 | contract.apply_witness(opid, SealWitness::new(tx, strict_dumb!())); 64 | 65 | contract 66 | .sync([ 67 | (old_txid, WitnessStatus::Archived), 68 | (rbf_txid, WitnessStatus::Mined(NonZeroU64::new(100).unwrap())), 69 | ]) 70 | .unwrap(); 71 | } 72 | -------------------------------------------------------------------------------- /tests/utils/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeSet; 2 | use std::convert::Infallible; 3 | use std::fs; 4 | use std::path::PathBuf; 5 | 6 | use amplify::confinement::Confined; 7 | use amplify::none; 8 | use bp::seals::{Anchor, TxoSeal, WTxoSeal}; 9 | use bp::{LockTime, Tx}; 10 | use commit_verify::{Digest, DigestExt, Sha256}; 11 | use hypersonic::CallParams; 12 | use rand::prelude::SliceRandom; 13 | use rand::rng; 14 | use rgb::{Assignment, CellAddr, Contract, CoreParams, CreateParams, Issuer, NamedState, Outpoint}; 15 | use rgb_persist_fs::{PileFs, StockFs}; 16 | use rgbcore::{ContractApi, RgbSealDef}; 17 | use single_use_seals::SealWitness; 18 | use strict_encoding::{vname, StrictDumb}; 19 | 20 | pub fn setup(name: &str) -> Contract> { 21 | let mut noise_engine = Sha256::new(); 22 | noise_engine.input_raw(b"test"); 23 | 24 | let issuer = Issuer::load("tests/data/Test.issuer", |_, _, _| -> Result<_, Infallible> { 25 | unreachable!() 26 | }) 27 | .unwrap(); 28 | 29 | let mut params = CreateParams::new_bitcoin_testnet(issuer.codex_id(), "Test"); 30 | for _ in 0..20 { 31 | params.push_owned_unlocked( 32 | "amount", 33 | Assignment::new_internal(Outpoint::strict_dumb(), 100u64), 34 | ); 35 | } 36 | 37 | let contract_path = PathBuf::from(format!("tests/data/{name}.contract")); 38 | if contract_path.exists() { 39 | fs::remove_dir_all(&contract_path).expect("Unable to remove a contract file"); 40 | } 41 | fs::create_dir_all(&contract_path).expect("Unable to create a contract folder"); 42 | let mut contract = 43 | Contract::issue(issuer, params.transform(noise_engine.clone()), |_| Ok(contract_path)) 44 | .unwrap(); 45 | let opid = contract.articles().genesis_opid(); 46 | 47 | let owned = &contract.full_state().main.owned; 48 | assert_eq!(owned.len(), 1); 49 | let owned = owned.get("amount").unwrap(); 50 | assert_eq!(owned.len(), 20); 51 | let mut prev = vec![]; 52 | for (addr, val) in owned { 53 | assert_eq!(val, &svnum!(100u64)); 54 | assert_eq!(addr.opid, opid); 55 | prev.push(*addr); 56 | } 57 | assert_eq!(prev.len(), 20); 58 | 59 | let mut no = 0; 60 | let mut next_seal = || -> WTxoSeal { 61 | no += 1; 62 | WTxoSeal::vout_no_fallback(no.into(), noise_engine.clone(), no as u64) 63 | }; 64 | 65 | let params = CallParams { 66 | core: CoreParams { method: vname!("transfer"), global: none!(), owned: none!() }, 67 | using: none!(), 68 | reading: none!(), 69 | }; 70 | let mut tx = Tx { 71 | version: default!(), 72 | inputs: Confined::from_checked(vec![]), 73 | outputs: Confined::from_checked(vec![]), 74 | lock_time: default!(), 75 | }; 76 | let anchor = Anchor::strict_dumb(); 77 | for round in 0u16..10 { 78 | // shuffle outputs to create twisted DAG 79 | prev.shuffle(&mut rng()); 80 | let mut iter = prev.into_iter(); 81 | let mut new_prev = vec![]; 82 | while let Some((first, second)) = iter.next().zip(iter.next()) { 83 | let mut params = params.clone(); 84 | params.using.insert(first, None); 85 | params.using.insert(second, None); 86 | let seals = small_bmap![0 => next_seal(), 1 => next_seal()]; 87 | let amount = 100u64 - round as u64; 88 | params.core.owned.push(NamedState::new_unlocked( 89 | "amount", 90 | seals[&0].auth_token(), 91 | amount, 92 | )); 93 | params.core.owned.push(NamedState::new_unlocked( 94 | "amount", 95 | seals[&1].auth_token(), 96 | amount, 97 | )); 98 | let op = contract.call(params, seals).unwrap(); 99 | let opid = op.opid(); 100 | new_prev.push(CellAddr::new(opid, 0)); 101 | new_prev.push(CellAddr::new(opid, 1)); 102 | 103 | tx.lock_time = LockTime::from_consensus_u32(tx.lock_time.into_consensus_u32() + 1); 104 | contract.apply_witness(opid, SealWitness::new(tx.clone(), anchor.clone())); 105 | } 106 | prev = new_prev; 107 | } 108 | 109 | let owned = &contract.full_state().main.owned; 110 | assert_eq!(owned.len(), 1); 111 | assert_eq!(prev.len(), 20); 112 | let owned = owned.get("amount").unwrap(); 113 | assert_eq!(owned.len(), 20); 114 | for (_, val) in owned.iter() { 115 | assert_eq!(val, &svnum!(91u64)); 116 | } 117 | assert_eq!(owned.keys().collect::>(), prev.iter().collect::>()); 118 | 119 | contract 120 | } 121 | --------------------------------------------------------------------------------